diff --git a/etc/manifest.json b/etc/manifest.json index d352204..84ad5fe 100644 --- a/etc/manifest.json +++ b/etc/manifest.json @@ -36,6 +36,7 @@ "deps/x_ite/assets/components/Text.js", "adapptlet.js", "adapptypes.js", + "conway.js", "options.js", "deps/GeoGebra/deployggb.js", "deps/GeoGebra/HTML5/5.0/webSimple/4B19686283BEF852F4C88C93522FB9A3.cache.js", diff --git a/etc/options.html b/etc/options.html index b512c0e..af3606f 100644 --- a/etc/options.html +++ b/etc/options.html @@ -7,6 +7,11 @@

Debugging

+

Embedded VRML/X3D display

+ Write to the JavaScript console:
+ + +

Java Geometry Applets

Trace the following to the JavaScript console:
diff --git a/package.json5 b/package.json5 index 420a342..4d1cc03 100644 --- a/package.json5 +++ b/package.json5 @@ -34,12 +34,12 @@ url: 'https://code.studioinfinity.org/glen/archematics.git', }, devDependencies: { - '@danielx/civet': '^0.6.72', + '@danielx/civet': '^0.6.73', '@types/firefox-webext-browser': '^120.0.0', '@types/jquery': '^3.5.29', '@webcomponents/custom-elements': '^1.6.0', 'http-server': '^14.1.1', - rollup: '^4.10.0', + rollup: '^4.11.0', typescript: '^5.3.3', 'webextension-polyfill': '^0.10.0', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c61379..bbe7f3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,8 +14,8 @@ dependencies: devDependencies: '@danielx/civet': - specifier: ^0.6.72 - version: 0.6.72(typescript@5.3.3) + specifier: ^0.6.73 + version: 0.6.73(typescript@5.3.3) '@types/firefox-webext-browser': specifier: ^120.0.0 version: 120.0.0 @@ -29,8 +29,8 @@ devDependencies: specifier: ^14.1.1 version: 14.1.1 rollup: - specifier: ^4.10.0 - version: 4.10.0 + specifier: ^4.11.0 + version: 4.11.0 typescript: specifier: ^5.3.3 version: 5.3.3 @@ -47,8 +47,8 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@danielx/civet@0.6.72(typescript@5.3.3): - resolution: {integrity: sha512-jumnIbXbdFs0ZiKN62fmD+p8QGi+E0jmtc02dKz9wIIoPkODsa4XXlBrS5BRR5fr3w5d3ah8Vq7gWt+DL9Wa0Q==} + /@danielx/civet@0.6.73(typescript@5.3.3): + resolution: {integrity: sha512-VOq02JgXNArsLzq/I2OnZtn16exiF8/mIhpR5O6Tsc+97RU6Qe8EP8XJskh/Hm9koUODzWUW8UuFpFdVzm7wTA==} engines: {node: '>=19 || ^18.6.0 || ^16.17.0'} hasBin: true peerDependencies: @@ -78,104 +78,104 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@rollup/rollup-android-arm-eabi@4.10.0: - resolution: {integrity: sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A==} + /@rollup/rollup-android-arm-eabi@4.11.0: + resolution: {integrity: sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.10.0: - resolution: {integrity: sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ==} + /@rollup/rollup-android-arm64@4.11.0: + resolution: {integrity: sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.10.0: - resolution: {integrity: sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg==} + /@rollup/rollup-darwin-arm64@4.11.0: + resolution: {integrity: sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.10.0: - resolution: {integrity: sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q==} + /@rollup/rollup-darwin-x64@4.11.0: + resolution: {integrity: sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.10.0: - resolution: {integrity: sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw==} + /@rollup/rollup-linux-arm-gnueabihf@4.11.0: + resolution: {integrity: sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.10.0: - resolution: {integrity: sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q==} + /@rollup/rollup-linux-arm64-gnu@4.11.0: + resolution: {integrity: sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.10.0: - resolution: {integrity: sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ==} + /@rollup/rollup-linux-arm64-musl@4.11.0: + resolution: {integrity: sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.10.0: - resolution: {integrity: sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA==} + /@rollup/rollup-linux-riscv64-gnu@4.11.0: + resolution: {integrity: sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.10.0: - resolution: {integrity: sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw==} + /@rollup/rollup-linux-x64-gnu@4.11.0: + resolution: {integrity: sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.10.0: - resolution: {integrity: sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw==} + /@rollup/rollup-linux-x64-musl@4.11.0: + resolution: {integrity: sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.10.0: - resolution: {integrity: sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ==} + /@rollup/rollup-win32-arm64-msvc@4.11.0: + resolution: {integrity: sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.10.0: - resolution: {integrity: sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg==} + /@rollup/rollup-win32-ia32-msvc@4.11.0: + resolution: {integrity: sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.10.0: - resolution: {integrity: sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ==} + /@rollup/rollup-win32-x64-msvc@4.11.0: + resolution: {integrity: sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==} cpu: [x64] os: [win32] requiresBuild: true @@ -556,26 +556,26 @@ packages: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true - /rollup@4.10.0: - resolution: {integrity: sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==} + /rollup@4.11.0: + resolution: {integrity: sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.10.0 - '@rollup/rollup-android-arm64': 4.10.0 - '@rollup/rollup-darwin-arm64': 4.10.0 - '@rollup/rollup-darwin-x64': 4.10.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.10.0 - '@rollup/rollup-linux-arm64-gnu': 4.10.0 - '@rollup/rollup-linux-arm64-musl': 4.10.0 - '@rollup/rollup-linux-riscv64-gnu': 4.10.0 - '@rollup/rollup-linux-x64-gnu': 4.10.0 - '@rollup/rollup-linux-x64-musl': 4.10.0 - '@rollup/rollup-win32-arm64-msvc': 4.10.0 - '@rollup/rollup-win32-ia32-msvc': 4.10.0 - '@rollup/rollup-win32-x64-msvc': 4.10.0 + '@rollup/rollup-android-arm-eabi': 4.11.0 + '@rollup/rollup-android-arm64': 4.11.0 + '@rollup/rollup-darwin-arm64': 4.11.0 + '@rollup/rollup-darwin-x64': 4.11.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.11.0 + '@rollup/rollup-linux-arm64-gnu': 4.11.0 + '@rollup/rollup-linux-arm64-musl': 4.11.0 + '@rollup/rollup-linux-riscv64-gnu': 4.11.0 + '@rollup/rollup-linux-x64-gnu': 4.11.0 + '@rollup/rollup-linux-x64-musl': 4.11.0 + '@rollup/rollup-win32-arm64-msvc': 4.11.0 + '@rollup/rollup-win32-ia32-msvc': 4.11.0 + '@rollup/rollup-win32-x64-msvc': 4.11.0 fsevents: 2.3.3 dev: true diff --git a/src/adapptypes.civet b/src/adapptypes.civet index 72d28ea..ef20292 100644 --- a/src/adapptypes.civet +++ b/src/adapptypes.civet @@ -1,5 +1,7 @@ +// This file is a bit misnamed, as it has options for giveAwrl, too. + export const flags = [ - 'color', 'commands', 'showall', 'showaux', 'algebra'] as const + 'color', 'commands', 'showall', 'showaux', 'algebra', 'vrml97'] as const export type FlagType = (typeof flags)[number] export type ConfigType = Partial> diff --git a/src/conway.civet b/src/conway.civet new file mode 100644 index 0000000..14172a3 --- /dev/null +++ b/src/conway.civet @@ -0,0 +1,682 @@ +type Face = number[] +type XYZ = [number, number, number] +type Polyhedron = {face: Face[], xyz: XYZ[]} + +type Notation = string + +// Useful constants +rt2 := Math.sqrt 2 +rth := rt2/2 +phi := (1 + Math.sqrt 5)/2 +ihp := 1/phi +tau := 2*Math.PI + +// edge midpoints on unit sphere for all seeds +icosahedron: Polyhedron := + face: [[0,1,9], [0,8,1], [0,4,8], [0,5,4], [0,9,5], + [4,5,2], [4,2,10], [4,10,8], [8,10,6], [8,6,1], + [1,6,7], [1,7,9], [9,7,11], [9,11,5], [5,11,2], + [3,2,11], [3,10,2], [3,6,10], [3,7,6], [3,11,7]] + xyz: [[0,ihp,1], [0,-ihp,1], [0,ihp,-1], [0,-ihp,-1], + [ihp,1,0], [-ihp,1,0], [ihp,-1,0], [-ihp,-1,0], + [1,0,ihp], [-1,0,ihp], [1,0,-ihp], [-1,0,-ihp]] +polyCache: Record := + '': face: [], xyz: [] + T: + face: [[0,2,1], [0,3,2], [0,1,3], [1,2,3]] + xyz: [[1,1,1], [1,-1,-1], [-1,1,-1], [-1,-1,1]] + O: + face: [[0,2,1], [0,3,2], [0,4,3], [0,1,4], + [1,5,4], [1,2,5], [2,3,5], [3,4,5]] + xyz: [[0,0,rt2], [rt2,0,0], [0,rt2,0], [-rt2,0,0], [0,-rt2,0], [0,0,-rt2]] + C: + face: [[0,3,2,1], [0,5,4,3], [0,1,6,5], [1,2,7,6], [2,3,4,7], [4,5,6,7]] + xyz: [[rth,rth,rth], [-rth,rth,rth], [-rth,-rth,rth], [rth,-rth,rth], + [rth,-rth,-rth], [rth,rth,-rth], [-rth,rth,-rth], [-rth,-rth,-rth]] + I: icosahedron + D: geomDual icosahedron + +export function generateVRML(notation: Notation): string + outputVRML notation, generatePoly notation + +function generatePoly(notation: Notation): Polyhedron + getStandardPoly inform standardize notation + +function getStandardPoly(notation: Notation): Polyhedron + if notation in polyCache then return polyCache[notation] + [ , op, arg, rest] := notation.match(/^(.)(\d*)(.*)$/) or [] + parent := getStandardPoly rest + // may have created what we want by side effect + if notation in polyCache then return polyCache[notation] + dispatch op, arg, parent, notation // will do the caching + +// Convenience tuple maker +function ð(...arg: Tup): Tup arg + +// Note we now allow numeric arguments on all of the basic operations, +// kis/truncate, join/ambo, and gyro/snub. Likely some of the operations +// we are taking as composite could have reasonable numeric versions, but +// there didn't seem to be any sensible way to propagate such an argument +// to the operations in their rewrites. In other words, the numeric-limited +// operations may not be composite, or at least not in the same way. So +// we have just left them as applying throughout the polyhedron. +rawStandardizations := + P4$: 'C', A3$: 'O', Y3$: 'T', // Seed synonyms + e: 'aa', b: 'ta', o: 'jj', m: 'kj', // abbreviations + [String.raw`t(\d*)`]: 'dk$1d', + [String.raw`a(\d*)`]: 'dj$1d', // dual operations + [String.raw`s(\d*)`]: 'dg$1d', + dd: '', rr: '', jd: 'j', gd: 'rgr', // absorption rules + rd: 'dr', // these commute; others? If so, move 'r' in to cancel w/ seed + // Remainder are all simplifications/unique selections for seeds: + aY: 'A', dT: 'T', gT: 'D', jT: 'C', dC: 'O', dO: 'C', + dI: 'D', dD: 'I', rO: 'O', rC: 'C', rI: 'I', rD: 'D', + jO: 'jC', jI: 'jD', gO: 'gC', gI: 'gD' +standardizations := + (ð RegExp(pat, 'g'), rep for pat, rep in rawStandardizations) + +function standardize(notation: Notation): Notation + lastNotation .= '' + while lastNotation != notation // iterate in case of rdrd, e.g. + lastNotation = notation + for [pat, rep] of standardizations + notation = notation.replace(pat, rep) + notation + +function orb(r: number, n: number, + height: number, t = 0): XYZ[] + // A regular n-gon inscribed in a horizontal circle of radius r + // at given height, rotated t*tau/n from standard position. + theta := tau/n + rho := t*theta + for i of [0...n] + [r*Math.cos(rho + i*theta), r*Math.sin(rho + i*theta), height] as XYZ + +operator add(a: XYZ, b: XYZ) + accumulate copy(a), b + +seeds := + P: (n: number) => // Prism + unless n then n = 3 + theta := tau/n + halfEdge := Math.sin theta/2 + xyz := orb(1, n, halfEdge) ++ orb(1, n, -halfEdge) + face := [[n-1..0], [n...2*n]] // top and bottom + for i of [0...n] + ip1 := (i+1)%n // next vertex around + face.push [i, ip1, ip1+n, i+n] + {face, xyz} + A: (n: number) => // Antiprism + unless n then n = 4 + theta := tau/n + halfHeight .= Math.sqrt + 1 - 4/(4 + 2*Math.cos(theta/2) - 2*Math.cos(theta)) + faceRadius .= Math.sqrt 1-halfHeight*halfHeight + // Scale to put edge midpoints on unit sphere + f := mag [halfHeight, faceRadius*Math.cos(theta/2), 0] + halfHeight /= f + faceRadius /= f + xyz := orb(faceRadius, n, halfHeight) + ++ orb(faceRadius, n, -halfHeight, 0.5) + face := [[n-1..0], [n...2*n]] // top and bottom + for i of [0...n] + face.push [i, (i+1)%n, i+n] + face.push [i, i+n, n + (n+i-1)%n] + {face, xyz} + Y: (n: number) => // pYramid + unless n then n = 4 + // Canonical solution by Intelligenti Pauca and Ed Pegg, see + // https://math.stackexchange.com/questions/2286628/canonical-pyramid-polynomials + theta := tau/n + c := Math.cos theta/2 + baseRadius := Math.sqrt 2/(c*(1+c)) + depth := Math.sqrt (1-c)/(1+c) + height := 2*Math.sqrt 1/(1 - c*c) + xyz := orb baseRadius, n, depth + edgeMid2 := xyz[0] add xyz[1] + xyz.push [0, 0, depth-height] + face := ([i, (i+1)%n, n] for i of [0...n]) + face.unshift [n-1..0] + {face, xyz} +type SeedOp = keyof typeof seeds + +// Syntactic sugar to deal with weird TypeScript typing: +operator æ(A: T[], i: number) A.at(i) as T + +function kisjoin(P: Polyhedron, notation: string, + digits: string, join: boolean): Polyhedron + // kis and join are closely related operations. Both of them add a + // pyramid on a selection of faces; join then further deletes any + // _original_ edge bordered by two _new_ triangles, producing a quad. + // Faces are selected by their numbers of sides, using the given digits. + // If there are none, all faces are used. Otherwise, the digits are turned + // into a list of numbers by breaking after every digit except as needed + // to prevent leading 0s or isolated 1s or 2s (since no face has one or + // two sides); this way you can list any subset of the numbers 3 - 32, + // which is plenty. + // The operation is then applied just to faces with the numbers of edges on + // the list. e.g. k3412 will add pyramids to the triangles, quads, and + // dodecagon faces. + allowed := parseSides digits + // first collect a directory from face indices to new vertex numbers + nextVertex .= P.xyz.length + newVixes := + for each f of P.face + !digits or f.length is in allowed ? nextVertex++ : 0 + if nextVertex is P.xyz.length then return P // nothing to do + xyz := P.xyz ++ faceCenters(P).filter (f,ix) => newVixes[ix] + face: Face[] := [] + for each f, ix of P.face + v := newVixes[ix] + if v is 0 + face.push f.slice() + continue + // Add the pyramid, possibly eliding edges: + for each w, jx of f + pw := f æ (jx-1) + neighbor .= 0 + if join + neighbor = P.face.findIndex (g, gx) => + gx !== ix and w is in g and pw is in g + if join and newVixes[neighbor] // elide this edge + if pw < w // avoid adding same face twice + face.push [v, pw, newVixes[neighbor], w] + else face.push [v, pw, w] + adjustXYZ {face, xyz}, notation, 3 + +// how enums ought to work? +FromCenter := Symbol() +AlongEdge := Symbol() +type Gyway = typeof FromCenter | typeof AlongEdge + +function gyropel(P: Polyhedron, notation: string, + digits: string, ...ways: Gyway[]): Polyhedron + // gyro and propellor are closely related operations. Both of them add new + // vertices one third of the way along each edge of each face selected + // by the digits argument (see kisjoin). They then differ in what edges + // are drawn to the new vertices. In gyro, another new vertex is added + // at the center of each face and connected to each of them; in propellor, + // they are just connected in sequence. For completeness, we also allow + // both at the same time, which is equivalent to propellor followed by kis + // just on the new rotated faces. + fromCenter := FromCenter is in ways + alongEdge := AlongEdge is in ways + unless fromCenter or alongEdge then return P // nothing to do + allowed := parseSides digits + // first collect a directory from directed edges to new vertex numbers + xyz := P.xyz.slice() + startV := xyz.length + edgeV: number[][] := [] + for each f of P.face + if digits and f.length is not in allowed then continue + for each v, ix of f + pv := f æ (ix-1) + (edgeV[pv] ??= [])[v] = xyz.length + xyz.push lerp xyz[pv], xyz[v], 1/3 + if xyz.length is startV then return P // nothing to do + + // Now revisit each face, accumulating the new faces. + face: Face[] := [] + centers: XYZ[] := fromCenter ? faceCenters P : [] + for each f, fx of P.face + if digits and f.length is not in allowed + // Just collect all of the vertices around + newFace: Face := [] + for each v, ix of f + pv := f æ (ix-1) + reverseV := edgeV[v]?[pv] ?? -1 + if reverseV >= 0 then newFace.push reverseV + newFace.push v + face.push newFace + continue + centerV := xyz.length + if fromCenter then xyz.push centers[fx] + aroundOutside: Face .= [] + for each v, ix of f + pv := f æ (ix-1) + ppv := f æ (ix-2) + firstNew := edgeV[ppv][pv] + newSection := [firstNew] + reverseV := edgeV[pv][ppv] ?? -1 + if reverseV >= 0 then newSection.push reverseV + newSection.push pv + secondNew := edgeV[pv][v] + if alongEdge + newSection.push secondNew + face.push newSection + if fromCenter then face.push ð centerV, firstNew, secondNew + else aroundOutside.push firstNew + else if fromCenter + newSection.push secondNew, centerV + face.push newSection + else aroundOutside ++= newSection + if aroundOutside.length then face.push aroundOutside + adjustXYZ {face, xyz}, notation, 3 + +function parseSides(digits: string): number[] + unless digits return [] + tooSmall := ['1', '2'] + last := digits.length - 1 + return := [] + current .= '' + pos .= 0 + while pos <= last + current += digits[pos++] + nextDigit := digits[pos] + if (current is in tooSmall + or nextDigit is '0' + or pos == last and nextDigit is in tooSmall) + continue + return.value.push parseInt current + current = '' + +transforms := + k: (P: Polyhedron, notation: string, digits: string) => // kis[n] + kisjoin P, notation, digits, false + j: (P: Polyhedron, notation: string, digits: string) => // join + kisjoin P, notation, digits, true + g: (P: Polyhedron, notation: string, digits: string) => // gyro + gyropel P, notation, digits, FromCenter + p: (P: Polyhedron, notation: string, digits: string) => // propellor + gyropel P, notation, digits, AlongEdge + f: (P: Polyhedron, notation: string, digits: string) => // fan [new? name?] + gyropel P, notation, digits, AlongEdge, FromCenter + r: (P: Polyhedron) => // reverse (mirror) + face: (f.toReversed() for each f of P.face) + xyz: (scale copy(v), -1 for each v of P.xyz) + d: (P: Polyhedron, notation: string, digits: string) => // dual + if digits + console.error `Ignoring ${digits} arg of d in ${notation}` + // Create a "fake" of P and adjust it (so we don't disturb the + // cached P), which will create the dual + parentNotation := notation[1+digits.length..] + adjustXYZ {face: P.face.slice(), P.xyz}, parentNotation, 1 + polyCache.`d${parentNotation}` + c: (P: Polyhedron, notation: string, digits: string) => // canonicalize + face: P.face.slice() + xyz: canonicalXYZ P, notation, Number(digits) or 10 + x: approxCanonicalize // iterative direct adjustment algorithm +type TransformOp = keyof typeof transforms + +function dispatch(op: string, digits: string, + P: Polyhedron, mynotation: string): Polyhedron + // Note mynotation starts with op! + return .= P + if op in seeds + return = seeds[op as SeedOp] Number(digits) or 0 + else if op in transforms + return = transforms[op as TransformOp] P, mynotation, digits + else + console.error `Unknown operation ${op}${digits} in ${mynotation}.` + return = polyCache.T + polyCache[mynotation] = return.value + +function topoDual(P: Polyhedron): Polyhedron + // Note this maintains correspondence between V and F indices, but + // places every vertex at the origin! Don't use without geometrizing + // in some way. + face: + for v of [0...P.xyz.length] + infaces := // gather labeled list of faces contining v + for f, index of P.face + unless f.includes v continue + ð f, index + start := infaces[0][1]; + current .= start + newface := [] + do + verts := P.face[current] + preV := verts æ (verts.indexOf(v)-1) + nextIx := infaces.findIndex ([face, label]) => + label !== current and face.includes preV + current = infaces[nextIx][1] + newface.push current + if newface.length > infaces.length + console.error 'In topoDual: Malformed polyhedron', P + break + until current is start + newface + xyz: + Array(P.face.length).fill([0,0,0]) // warning, every vertex is === + +function geomDual(P: Polyhedron): Polyhedron + return := topoDual P + return.value.xyz = approxDualVertices P + +function approxDualVertices(P: Polyhedron): XYZ[] + P.face.map (f) => approxDualVertex f, P.xyz + +operator dot(v: number[], w: number[]) + v.reduce (l,r,i) => l + r*w[i], 0 + +function approxDualVertex(f: Face, v: XYZ[]): XYZ + // For each edge of f, there is a plane containing it perpendicular + // to the line joining the origin to its nearest approach to the origin. + // This function returns the point closest to being on all of those planes + // (in the least-squares sense). + // This method seems to work OK when the neighborhood of f is convex, + // and very poorly otherwise. So it probably would not provide any better + // canonicalization than other methods of approximating the dual. + normals := (tangentPoint(v[f æ (i-1)], v[f[i]]) for i of [0...f.length]) + sqlens := normals.map mag2 + columns := (normals.map(&[i]) for i of [0..2]) + target := (columns[i] dot sqlens for i of [0..2]) as XYZ + CMsource := (for c of [0..2] + (columns[r] dot columns[c] for r of [0..2])) as [XYZ, XYZ, XYZ] + cramerD := det ...CMsource + if Math.abs(cramerD) < 1e-6 + console.error `Face ${f} of ${v.map (p) => '['+p+']'} ill conditioned` + return [0, 0, 0] + [ det(target,CMsource[1],CMsource[2])/cramerD, + det(CMsource[0],target,CMsource[2])/cramerD, + det(CMsource[0],CMsource[1],target)/cramerD ] + +function det(a: XYZ, b: XYZ, c:XYZ) + a[0]*b[1]*c[2] + a[1]*b[2]*c[0] + a[2]*b[0]*c[1] + - a[2]*b[1]*c[0] - a[1]*b[0]*c[2] - a[0]*b[2]*c[1] + +operator sub(a: XYZ, b: XYZ) + diminish copy(a), b + +function tangentPoint(v: XYZ, w: XYZ) // closest point on vw to origin + d := w sub v + v sub scale d, d dot v / mag2 d + +function faceCenters(P: Polyhedron): XYZ[] + for each face of P.face + scale face.reduce((ctr,v) => accumulate(ctr, P.xyz[v]), [0,0,0]), + 1/face.length + +function adjustXYZ(P: Polyhedron, notation: string, iterations = 1): Polyhedron + dualNotation := 'd' + notation + D .= topoDual P + if dualNotation in polyCache + console.error 'Creating', notation, '_after_ its dual' + D = polyCache[dualNotation] + for iter of [1..iterations] + D.xyz = reciprocalC P + P.xyz = reciprocalC D + polyCache[dualNotation] = D + P + +function reciprocalC(P: Polyhedron): XYZ[] + return := faceCenters P + for each v of return.value + scale v, 1/mag2 v + +function canonicalXYZ(P: Polyhedron, notation: string, iterations: number): XYZ[] + dualNotation := 'd' + notation + D .= topoDual P + if dualNotation in polyCache + console.error 'Creating', notation, '_after_ its dual' + D = polyCache[dualNotation] + tempP := Object.assign({}, P) // algorithm is read-only on original data + if iterations < 1 then iterations = 1 + for iter of [1..iterations] + D.xyz = reciprocalN tempP + tempP.xyz = reciprocalN D + polyCache[dualNotation] = D + tempP.xyz + +operator cross(v: XYZ, w: XYZ): XYZ + [ v[1]*w[2] - v[2]*w[1], v[2]*w[0] - v[0]*w[2], v[0]*w[1] - v[1]*w[0] ] + +function reciprocalN(P: Polyhedron): XYZ[] + for each f of P.face + centroid := ð 0, 0, 0 + normal := ð 0, 0, 0 + meanEdgeRadius .= 0 + for each vlabel, ix of f + v := P.xyz[vlabel] + accumulate centroid, v + pv := P.xyz[f æ (ix-1)] + ppv := P.xyz[f æ (ix-2)] + // original doc says unit normal but below isn't. Didn't help to try, tho + nextNormal := (pv sub ppv) cross (v sub pv) + // or instead, just chop down big ones: (didn't work either) + // magNext := mag nextNormal + // if magNext > 1 then scale nextNormal, 1/magNext + accumulate normal, nextNormal + meanEdgeRadius += mag tangentPoint pv, v + scale centroid, 1/f.length + scale normal, 1/mag normal + meanEdgeRadius /= f.length + scale normal, centroid dot normal + scale normal, 1/mag2 normal // invert in unit sphere + scale normal, (1 + meanEdgeRadius)/2 + +operator dist(a: XYZ, b: XYZ) mag a sub b + +// adapted from Polyhedronisme https://levskaya.github.io/polyhedronisme/ +function approxCanonicalize(P: Polyhedron, notation: string, + digits: String): Polyhedron + THRESHOLD := 1e-6 + // A difficulty is that the planarizing sometimes has the effect of + // "folding over" neighboring faces, in which case all is lost. + // Keeping the weight of the edge smoothing high compared to planarizing + // seems to help with that. + EDGE_SMOOTH_FACTOR := 0.5 + PLANARIZE_FACTOR := 0.1 + edge := edges P + xyz := P.xyz.map copy + V := xyz.length + normalizeEdges xyz, edge + for iter of [1..Number(digits) or 10] + start := xyz.map copy + smoothEdgeDists xyz, edge, EDGE_SMOOTH_FACTOR + normalizeEdges xyz, edge + planarize xyz, P.face, PLANARIZE_FACTOR + normalizeEdges xyz, edge + if Math.max(...(xyz[i] dist start[i] for i of [0...V])) < THRESHOLD + break + {face: P.face.slice(), xyz} + +type Edge = [number, number] +function edges(P:Polyhedron): Edge[] + return: Edge[] := [] + for each f of P.face + for each v, ix of f + pv := f æ (ix-1) + if pv < v then return.value.push ð pv, v + +function normalizeEdges(xyz: XYZ[], edge: Edge[]): void + // Adjusts xyz so that edge tangentpoints have centroid at origin and + // mean radius 1 + edgeP .= edge.map ([a,b]) => tangentPoint xyz[a], xyz[b] + edgeCentroid := centroid edgeP + xyz.forEach (pt) => diminish pt, edgeCentroid + edgeScale := 1/(mean edge.map ([a,b]) => mag tangentPoint xyz[a], xyz[b]) + xyz.forEach (pt) => scale pt, edgeScale + +function centroid(xyz: XYZ[]): XYZ + scale xyz.reduce(accumulate, ð 0,0,0), 1/xyz.length + +function smoothEdgeDists(xyz: XYZ[], edge: Edge[], fudge: number): void + // Attempts in the most straightforward way possible to reduce the + // variance of the radii of the edgepoints + V := xyz.length + adj := (ð 0,0,0 for i of [1..V]) + edgeDistsStart := edge.map ([a,b]) => mag tangentPoint xyz[a], xyz[b] + for each [a,b] of edge + t := tangentPoint xyz[a], xyz[b] + scale t, (1 - mag t)/2 + accumulate adj[a], t + accumulate adj[b], t + for i of [0...V] + accumulate xyz[i], scale adj[i], fudge + edgeDistsEnd := edge.map ([a,b]) => mag tangentPoint xyz[a], xyz[b] + +function summary(data: number[]) + [Math.min(...data), Math.max(...data), mean(data), + mean(data.map((x) => Math.abs(1-x)))] + +function planarize(xyz: XYZ[], face: Face[], fudge: number): void + V := xyz.length + adj := (ð 0,0,0 for i of [1..V]) + for each f of face + if f.length is 3 then continue // triangles always planar + fxyz := (xyz[v] for each v of f) + c := centroid fxyz + n := meanNormal fxyz + if c dot n < 0 then scale n, -1 + for each v of f + accumulate adj[v], scale copy(n), n dot (c sub xyz[v]) + for i of [0...V] + accumulate xyz[i], scale adj[i], fudge + +function meanNormal(xyz: XYZ[]): XYZ + mNormal := ð 0,0,0 + [v1, v2] .= xyz.slice(-2); + for each v3 of xyz + nextNormal := (v2 sub v1) cross (v3 sub v2) + magNext := mag nextNormal + // reduce influence of long edges? (didn't seem to help in brief testing) + // if magNext > 1 then scale nextNormal, 1/Math.sqrt magNext + accumulate mNormal, nextNormal + [v1, v2] = [v2, v3] // shift over one + scale mNormal, 1/mag mNormal + +function mean(a: number[]) + m .= 0 + m += e for each e of a + m / a.length + +// arithmetic on 3-vectors +function accumulate(basket: XYZ, egg: XYZ) + basket[0] += egg[0] + basket[1] += egg[1] + basket[2] += egg[2] + basket + +function diminish(basket: XYZ, egg: XYZ) + basket[0] -= egg[0] + basket[1] -= egg[1] + basket[2] -= egg[2] + basket + +function copy(a: XYZ) + ð a[0], a[1], a[2] + +function mag2(a: XYZ) + a[0]*a[0] + a[1]*a[1] + a[2]*a[2] + +function mag(a: XYZ) + Math.sqrt mag2 a + +function normalize(v: XYZ) + scale v, 1/mag v + +function unit + +function scale(subject: XYZ, by: number) + subject[0] *= by + subject[1] *= by + subject[2] *= by + subject + +function lerp(start: XYZ, end: XYZ, howFar: number) + basket := scale copy(start), 1-howFar + accumulate basket, scale copy(end), howFar + +// Feedback + +function inform(x: string) + $('input[name="inform"]').val(x) + x + + +// VRML97 generation + +function outputVRML(notation: Notation, P: Polyhedron): string + ``` +#VRML V2.0 utf8 +Group { children [ + WorldInfo { # Generated by GTW's reimplementation of GWH's Conway script. + title "${notation} ${stats P}" + info "Generated by GTW's Conway-notation script inspired by GWH's. +By using this script, you agree that this image is released +into the public domain, although you are requested to cite +George Hart's Encyclopedia of Polyhedra as the source." } + Background { + groundColor [ .2 .5 1 ] # light blue + skyColor [ .2 .5 1 ] } + NavigationInfo { type [ "EXAMINE" ] } + DirectionalLight {direction -.5 -1 1 intensity 0.75} + DirectionalLight {direction .5 1 -1 intensity 0.75} + ${polyVRML P, colorScheme()} + Shape { + appearance Appearance { + material Material { + diffuseColor 0 0 0 } } + geometry IndexedLineSet { + coord ${useVerts} + coordIndex [ + ${edgeIndices P} ] } } + ${showDual() ? polyVRML geomDual(P), '0.5 0.5 0.5' : ''} ] } + ``` + +function stats(P: Polyhedron): string + edges := P.face.reduce((e,f) => e + f.length, 0) / 2 + `[${P.face.length} faces, ${edges} edges, ${P.xyz.length} vertices]` + +useVerts := 'USE verts' +function defVerts(v: XYZ[]) + `DEF verts Coordinate { + point [ + ${v.map(.join ' ').join(",\n ")} ] }` + +function polyVRML(P: Polyhedron, color: string): string + facePartition := color ? [P.face] + : P.face.reduce ((parts, f) => + (parts[f.length] ??= []).push f + parts), + [] as Face[][] + emittedCoords .= '' + shapes := + for part of facePartition + unless part continue + ` + Shape { + appearance Appearance { + material Material { + diffuseColor ${color or colorBySides part[0].length} } } + geometry IndexedFaceSet { + ccw FALSE + coord ${emittedCoords ? useVerts : (emittedCoords = defVerts P.xyz)} + coordIndex [ + ${part.map(.join ', ').join(", -1,\n ")}, -1 ] }}` + shapes.join "\n" + +function colorScheme + button := document.getElementsByName('color')[0] as HTMLInputElement + button.checked ? '1 1 1' : '' + +function showDual + false + +faceColors: Record := + 3: '0.9 0.3 0.3' // red + 4: '0.4 0.4 1.0' // blue + 5: '0.2 0.9 0.3' // green + 6: '0.9 0.9 0.2' // yellow + 7: '0.5 0.25 0.25' // brown + 8: '0.8 0.2 0.8' // magenta + 9: '0.5 0.2 0.8' // purple + 10: '0.1 0.9 0.9' // cyan + 12: '1.0 0.6 0.1' // orange + +function colorBySides(n: number) + if n in faceColors + return faceColors[n] + '0.5 0.5 0.5' // gray + +function filtmap(A: T[], m: (e:T, i: number, arr: T[]) => U) + A.map(m).filter (e) => !!e + +function edgeIndices(P: Polyhedron) + sep := ",\n " + filtmap(P.face, (thisf) => + filtmap(thisf, (v, ix, f) => + preV := f æ (ix-1) + preV < v ? `${preV}, ${v}, -1` : '').join sep) + .join sep diff --git a/src/giveAwrl.civet b/src/giveAwrl.civet index 692ced2..82864ce 100644 --- a/src/giveAwrl.civet +++ b/src/giveAwrl.civet @@ -1,5 +1,6 @@ import ./deps/jquery.js {convert} from ./deps/vrml1to97/index.js +{ConfigType} from ./adapptypes.ts knownExtensions := /[.](?:wrl|x3d|gltf|glb|obj|stl|ply)$/ certainlyHandled := @@ -42,10 +43,11 @@ function makeBrowser(url: string, width: string, height: string) text .= await response.text() if /#\s*VRML\s*V?1[.]/i.test text text = convert text + maybeDebug text browser3D.baseURL = url scene := await browser3D.createX3DFromString text browser3D.replaceWorld scene - canvas + {canvas, browser3D} // Put eye icons after all of the eligible links links := $('a').filter -> knownExtensions.test @.getAttribute('href') ?? '' @@ -79,7 +81,7 @@ links.after -> overImg := floatLike and floatLike.tagName is 'IMG' width := overImg ? ($(floatLike).width() + 'px') : '150px' height := overImg ? ($(floatLike).height() + 'px') : '150px' - canvas := await makeBrowser url, width, height + {canvas} := await makeBrowser url, width, height if float canvas.style.float = float if overImg @@ -90,8 +92,9 @@ links.after -> canvas.style.marginRight = imgSty.getPropertyValue 'margin-right' if float is 'right' canvas.style.left = $(eye).width() + 'px' - else + else if float is 'left' canvas.style.right = $(eye).width() + 'px' + else canvas.style.left = floatLike.offsetLeft $(eye).append canvas if state is 'off' eye.setAttribute 'data', 'on' @@ -101,3 +104,40 @@ links.after -> eye.setAttribute 'data', 'off' $(eye).css 'text-decoration', 'none' $(eye.lastElementChild as Element).hide() + +function maybeDebug(vrml: string) + config := await browser.storage.local.get(['vrml97']) as ConfigType + if config.vrml97 then console.log 'Generated VRML97', vrml + +let conwayBrowser: any +madeConway .= false + +// See if we are on George Hart's Conway-notation generator page +inputs := $('input[type="button"][value="Generate"][onclick="viewVRML()"]') +if inputs.length is 1 + // Seems so, fix the generator + // Note that modifying the onclick prop is not the recommended way to + // change button click functionality, but we need to clear out the old + // behavior so I wasn't sure how else to do it + inputs.prop 'onclick', (i, val) => () => + import(browser.runtime.getURL('conway.js')).then (conway) => + notation := $('input[name="notation"]').val() + unless notation then return + vrml := conway.generateVRML notation.toString() + maybeDebug vrml + unless madeConway + {canvas, browser3D} := await makeBrowser '', '250px', '250px' + conwayBrowser = browser3D + canvas.style.float = 'left' + canvas.style.marginRight = '1em' + $('form[name="input"]').first().before canvas + madeConway = true + scene := await conwayBrowser.createX3DFromString vrml + conwayBrowser.replaceWorld scene + +// See if we are on George Hart's prism generator page +prisms := $('input[type="button"][value="View"][onclick="ViewVRML()"]') +if prisms.length is 1 + // Seems so, fix the generator + console.log 'Need to fix the prism generator' + diff --git a/tools/makePlugin.bash b/tools/makePlugin.bash index 709d2cd..38e5b8f 100644 --- a/tools/makePlugin.bash +++ b/tools/makePlugin.bash @@ -21,7 +21,7 @@ done npx rollup public/js/giveAwrl.js --dir $1 npx rollup public/js/adapptlet.js --file $1/adapptlet.js npx rollup public/js/adapptext.js --file $1/adapptext.js -cp public/js/options.js public/js/adapptypes.js $1 +cp public/js/options.js public/js/adapptypes.js public/js/conway.js $1 cp node_modules/webextension-polyfill/dist/browser-polyfill.js $1 cp node_modules/@webcomponents/custom-elements/custom-elements.min.js $1 zip -r $1 $1