diff --git a/etc/manifest.json b/etc/manifest.json index 84ad5fe..4b286d4 100644 --- a/etc/manifest.json +++ b/etc/manifest.json @@ -38,6 +38,7 @@ "adapptypes.js", "conway.js", "options.js", + "prism.js", "deps/GeoGebra/deployggb.js", "deps/GeoGebra/HTML5/5.0/webSimple/4B19686283BEF852F4C88C93522FB9A3.cache.js", "deps/GeoGebra/HTML5/5.0/webSimple/webSimple.nocache.js", diff --git a/package.json5 b/package.json5 index 4d1cc03..db040e2 100644 --- a/package.json5 +++ b/package.json5 @@ -34,17 +34,17 @@ url: 'https://code.studioinfinity.org/glen/archematics.git', }, devDependencies: { - '@danielx/civet': '^0.6.73', + '@danielx/civet': '^0.6.78', '@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.11.0', + rollup: '^4.12.0', typescript: '^5.3.3', 'webextension-polyfill': '^0.10.0', }, dependencies: { colorsea: '^1.2.1', - vrml1to97: '^0.3.2', + vrml1to97: '^0.4.0', }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbe7f3b..7041c5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,13 +9,13 @@ dependencies: specifier: ^1.2.1 version: 1.2.1 vrml1to97: - specifier: ^0.3.2 - version: 0.3.2 + specifier: ^0.4.0 + version: 0.4.0 devDependencies: '@danielx/civet': - specifier: ^0.6.73 - version: 0.6.73(typescript@5.3.3) + specifier: ^0.6.78 + version: 0.6.78(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.11.0 - version: 4.11.0 + specifier: ^4.12.0 + version: 4.12.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.73(typescript@5.3.3): - resolution: {integrity: sha512-VOq02JgXNArsLzq/I2OnZtn16exiF8/mIhpR5O6Tsc+97RU6Qe8EP8XJskh/Hm9koUODzWUW8UuFpFdVzm7wTA==} + /@danielx/civet@0.6.78(typescript@5.3.3): + resolution: {integrity: sha512-GT8+Y+MIF7+SkiMSbh2diXWZckjbmWb8nPv17RAwYjFwhI5Z6lXtil+KPX2rVf0C/0ZFksYM65Rp2dJYxMYT3w==} 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.11.0: - resolution: {integrity: sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==} + /@rollup/rollup-android-arm-eabi@4.12.0: + resolution: {integrity: sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.11.0: - resolution: {integrity: sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==} + /@rollup/rollup-android-arm64@4.12.0: + resolution: {integrity: sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.11.0: - resolution: {integrity: sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==} + /@rollup/rollup-darwin-arm64@4.12.0: + resolution: {integrity: sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.11.0: - resolution: {integrity: sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==} + /@rollup/rollup-darwin-x64@4.12.0: + resolution: {integrity: sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.11.0: - resolution: {integrity: sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==} + /@rollup/rollup-linux-arm-gnueabihf@4.12.0: + resolution: {integrity: sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.11.0: - resolution: {integrity: sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==} + /@rollup/rollup-linux-arm64-gnu@4.12.0: + resolution: {integrity: sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.11.0: - resolution: {integrity: sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==} + /@rollup/rollup-linux-arm64-musl@4.12.0: + resolution: {integrity: sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-riscv64-gnu@4.11.0: - resolution: {integrity: sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==} + /@rollup/rollup-linux-riscv64-gnu@4.12.0: + resolution: {integrity: sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==} cpu: [riscv64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.11.0: - resolution: {integrity: sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==} + /@rollup/rollup-linux-x64-gnu@4.12.0: + resolution: {integrity: sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.11.0: - resolution: {integrity: sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==} + /@rollup/rollup-linux-x64-musl@4.12.0: + resolution: {integrity: sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.11.0: - resolution: {integrity: sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==} + /@rollup/rollup-win32-arm64-msvc@4.12.0: + resolution: {integrity: sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.11.0: - resolution: {integrity: sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==} + /@rollup/rollup-win32-ia32-msvc@4.12.0: + resolution: {integrity: sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.11.0: - resolution: {integrity: sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==} + /@rollup/rollup-win32-x64-msvc@4.12.0: + resolution: {integrity: sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==} cpu: [x64] os: [win32] requiresBuild: true @@ -556,26 +556,26 @@ packages: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true - /rollup@4.11.0: - resolution: {integrity: sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==} + /rollup@4.12.0: + resolution: {integrity: sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@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 + '@rollup/rollup-android-arm-eabi': 4.12.0 + '@rollup/rollup-android-arm64': 4.12.0 + '@rollup/rollup-darwin-arm64': 4.12.0 + '@rollup/rollup-darwin-x64': 4.12.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.12.0 + '@rollup/rollup-linux-arm64-gnu': 4.12.0 + '@rollup/rollup-linux-arm64-musl': 4.12.0 + '@rollup/rollup-linux-riscv64-gnu': 4.12.0 + '@rollup/rollup-linux-x64-gnu': 4.12.0 + '@rollup/rollup-linux-x64-musl': 4.12.0 + '@rollup/rollup-win32-arm64-msvc': 4.12.0 + '@rollup/rollup-win32-ia32-msvc': 4.12.0 + '@rollup/rollup-win32-x64-msvc': 4.12.0 fsevents: 2.3.3 dev: true @@ -639,8 +639,8 @@ packages: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} dev: true - /vrml1to97@0.3.2: - resolution: {integrity: sha512-1CYLeo9XawIr6ejjYgQj4H/l74Tb2EyyExbezRNHMpKvutVOi+1MplgFd0gHkQqQs13jNvJwRAB0LNzkf+41gQ==} + /vrml1to97@0.4.0: + resolution: {integrity: sha512-5gA9jr31f4z779ddST0EN+TNHrU1Auzf9GBeHoAjAZRBtwWqIehvDePGv73p8MM3ANueA6Z5puhtezK90Qy+kg==} hasBin: true dev: false diff --git a/src/conway.civet b/src/conway.civet index 14172a3..2a12ed6 100644 --- a/src/conway.civet +++ b/src/conway.civet @@ -1,6 +1,6 @@ type Face = number[] type XYZ = [number, number, number] -type Polyhedron = {face: Face[], xyz: XYZ[]} +type Polyhedron = {face: Face[], xyz: XYZ[], name?: string} type Notation = string @@ -11,7 +11,28 @@ phi := (1 + Math.sqrt 5)/2 ihp := 1/phi tau := 2*Math.PI -// edge midpoints on unit sphere for all seeds +// Sadly needs to be early because we initialize the Dodecahedron as the dual of +// the icosahedron: +// Only add one direction of each, will auto reverse as well +specialDuals: Record := + Tetrahedron: 'Tetrahedron', + Cube: 'Octahdron', + Dodecahedron: 'Icosahedron', + Cuboctahedron: 'Rhombic dodecahedron', + 'truncated Tetrahedron': 'triakis Tetrahedron', + 'truncated Cube': 'triakis Octahedron', + 'truncated Octahedron': 'tetrakis Cube', + Rhombicuboctahedron: 'Deltoidal icositetrahedron', + 'truncated Cuboctahedron': 'Disdyakis dodecahedron', + 'snub Cube': 'pentagonal icositetrahedron', + Icosidodecahedron: 'Rhombic triacontahedron', + 'truncated Dodecahedron': 'triakis Icosahedron', + 'truncated Icosahedron': 'pentakis Dodecahedron', + Rhombicosidodecahedron: 'Deltoidal hexecontahedron', + 'truncated Icosidodecahedron': 'Disdyakis triacontahedron', + 'snub Dodecahedron': 'pentagonal hexecontahedron' + +// All seeds are canonical, i.e., edges tangent to unit sphere 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], @@ -20,19 +41,24 @@ icosahedron: Polyhedron := 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]] + name: 'Icosahedron' + 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]] + name: 'Tetrahedron' 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]] + name: 'Octahedron' 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]] + name: 'Cube' I: icosahedron D: geomDual icosahedron @@ -69,7 +95,7 @@ rawStandardizations := 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', + jY: 'dA', 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 := @@ -95,6 +121,13 @@ function orb(r: number, n: number, operator add(a: XYZ, b: XYZ) accumulate copy(a), b +ngonalNames := 3: 'triangular', 4: 'square', 5: 'pentagonal', 6: 'hexagonal', + 7: 'heptagonal', 8: 'octagonal', 9: 'enneagonal', 10: 'decagonal', + 12: 'dodecagonal' +type SpecialNgon = keyof typeof ngonalNames +function ngonal(n: number) + ngonalNames[n as SpecialNgon] ?? n+`-gonal` + seeds := P: (n: number) => // Prism unless n then n = 3 @@ -105,7 +138,7 @@ seeds := for i of [0...n] ip1 := (i+1)%n // next vertex around face.push [i, ip1, ip1+n, i+n] - {face, xyz} + {face, xyz, name: ngonal(n)+` prism`} A: (n: number) => // Antiprism unless n then n = 4 theta := tau/n @@ -122,7 +155,7 @@ seeds := 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} + {face, xyz, name: ngonal(n)+` antiprism`} Y: (n: number) => // pYramid unless n then n = 4 // Canonical solution by Intelligenti Pauca and Ed Pegg, see @@ -137,11 +170,27 @@ seeds := xyz.push [0, 0, depth-height] face := ([i, (i+1)%n, n] for i of [0...n]) face.unshift [n-1..0] - {face, xyz} + {face, xyz, name: ngonal(n)+` pyramid`} type SeedOp = keyof typeof seeds // Syntactic sugar to deal with weird TypeScript typing: operator æ(A: T[], i: number) A.at(i) as T +function wordsof(s: string) 1 + (s.match(/\s+/g)?.length ?? 0) +specialJoins := + Tetrahedron: 'Cube', + Cube: 'Rhombic dodecahedron', Octahedron: 'Rhombic dodecahedron', + Dodecahedron: 'Rhombic triacontahedron', Icosahedron: 'Rhombic triacontahedron', + 'Rhombic dodecahedron': 'Deltoidal icositetrahedron', + 'Rhombic triacontahedron': 'Deltoidal hexecontahedron', + 'pentakis Dodecahedron': 'Rhombic enneacontahedron' +type SpecialJoin = keyof typeof specialJoins +specialKis := + 'Rhombic dodecahedron': 'Disdyakis dodecahedron' + 'Rhombic triacontahedron': 'Disdyakis triacontahedron' + +type SpecialKis = keyof typeof specialKis +kisWords:= 3: 'triakis', 4: 'tetrakis', 5: 'pentakis', 6: 'hexakis', 7: 'heptakis' +type KisNumber = keyof typeof kisWords function kisjoin(P: Polyhedron, notation: string, digits: string, join: boolean): Polyhedron @@ -173,7 +222,7 @@ function kisjoin(P: Polyhedron, notation: string, continue // Add the pyramid, possibly eliding edges: for each w, jx of f - pw := f æ (jx-1) + pw := f æ jx-1 neighbor .= 0 if join neighbor = P.face.findIndex (g, gx) => @@ -182,7 +231,35 @@ function kisjoin(P: Polyhedron, notation: string, 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 + // Nomenclature + let name: string|undefined + if P.name and wordsof(P.name) < 3 // don't go hog-wild with naming + if join + unless digits + name = specialJoins[P.name as SpecialJoin] ?? 'joined ' + P.name + else + size .= P.face[0].length + unless P.face.every (f) => f.length is size + size = 0 + if !size and digits then size = Number(digits) or 0 + // very special case + if size is 5 and P.name is 'pentagonal antiprism' + name = 'Icosahedron' + else if (!digits or Number(digits) is size) and size in kisWords + name = (specialKis[P.name as SpecialKis] + ?? kisWords[size as KisNumber] + ' ' + P.name) + // Cheaty super special case + if notation is 'jk5djP5' + name = 'dual elongated pentagonal orthobirotunda' + // Done, fix up the vertices a bit + adjustXYZ {face, xyz, name}, notation, 3 + + +specialGyro := + Cube: 'pentagonal icositetrahedron', Octahedron: 'pentagonal icositetrahedron', + Icosahedron: 'pentagonal hexecontahedron', + Dodecahedron: 'pentagonal hexecontahedron' +type SpecialGyro = keyof typeof specialGyro // how enums ought to work? FromCenter := Symbol() @@ -210,7 +287,7 @@ function gyropel(P: Polyhedron, notation: string, 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) + 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 @@ -223,7 +300,7 @@ function gyropel(P: Polyhedron, notation: string, // Just collect all of the vertices around newFace: Face := [] for each v, ix of f - pv := f æ (ix-1) + pv := f æ ix-1 reverseV := edgeV[v]?[pv] ?? -1 if reverseV >= 0 then newFace.push reverseV newFace.push v @@ -233,8 +310,8 @@ function gyropel(P: Polyhedron, notation: string, if fromCenter then xyz.push centers[fx] aroundOutside: Face .= [] for each v, ix of f - pv := f æ (ix-1) - ppv := f æ (ix-2) + pv := f æ ix-1 + ppv := f æ ix-2 firstNew := edgeV[ppv][pv] newSection := [firstNew] reverseV := edgeV[pv][ppv] ?? -1 @@ -251,7 +328,19 @@ function gyropel(P: Polyhedron, notation: string, face.push newSection else aroundOutside ++= newSection if aroundOutside.length then face.push aroundOutside - adjustXYZ {face, xyz}, notation, 3 + let name: string|undefined + nw .= 0 + if ((fromCenter xor alongEdge) + and P.name and !digits and (nw = wordsof(P.name)) < 3) + if alongEdge + if nw is 1 then name = 'propello' + P.name + else name = 'propellorized '+P.name + else + if P.name in specialGyro then name = specialGyro[P.name as SpecialGyro] + else if nw is 1 then name = 'gyro' + P.name + else name = 'gyrated '+P.name + // Done, fix up the vertices a bit + adjustXYZ {face, xyz, name}, notation, 3 function parseSides(digits: string): number[] unless digits return [] @@ -284,17 +373,19 @@ transforms := 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) + name: P.name // should we record that it's mirrored? 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 + adjustXYZ {face: P.face.slice(), P.xyz, P.name}, 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 + name: P.name x: approxCanonicalize // iterative direct adjustment algorithm type TransformOp = keyof typeof transforms @@ -326,7 +417,7 @@ function topoDual(P: Polyhedron): Polyhedron newface := [] do verts := P.face[current] - preV := verts æ (verts.indexOf(v)-1) + preV := verts æ verts.indexOf(v)-1 nextIx := infaces.findIndex ([face, label]) => label !== current and face.includes preV current = infaces[nextIx][1] @@ -338,6 +429,8 @@ function topoDual(P: Polyhedron): Polyhedron newface xyz: Array(P.face.length).fill([0,0,0]) // warning, every vertex is === + name: dualName P.name + function geomDual(P: Polyhedron): Polyhedron return := topoDual P @@ -346,7 +439,7 @@ function geomDual(P: Polyhedron): Polyhedron function approxDualVertices(P: Polyhedron): XYZ[] P.face.map (f) => approxDualVertex f, P.xyz -operator dot(v: number[], w: number[]) +operator dot same (/) (v: number[], w: number[]) v.reduce (l,r,i) => l + r*w[i], 0 function approxDualVertex(f: Face, v: XYZ[]): XYZ @@ -357,7 +450,9 @@ function approxDualVertex(f: Face, v: XYZ[]): XYZ // 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]) + // It might be better to replace this with the approximate intersection of the + // reciprocal planes to each of the vertices of the face... + 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 @@ -387,6 +482,30 @@ function faceCenters(P: Polyhedron): XYZ[] scale face.reduce((ctr,v) => accumulate(ctr, P.xyz[v]), [0,0,0]), 1/face.length +function dualName(p: string|undefined) + unless 'Octahedron' in specialDuals + // one-time reversal of all special duals + specialDuals[dual] = poly for poly, dual in specialDuals + + unless p return undefined + if p in specialDuals + return specialDuals[p] + words := p.split(' ') + if words[0] is 'dual' return words[1..].join ' ' + if words.length is 2 + switch words[1] + 'prism' + return words[0] + ' bipyramid' + 'bipyramid' + return words[0] + ' prism' + 'antiprism' + return words[0] + ' trapezohedron' + 'trapezohedron' + return words[0] + ' antiprism' + 'pyramid' + return p // self-dual + return 'dual ' + p + function adjustXYZ(P: Polyhedron, notation: string, iterations = 1): Polyhedron dualNotation := 'd' + notation D .= topoDual P @@ -418,7 +537,7 @@ function canonicalXYZ(P: Polyhedron, notation: string, iterations: number): XYZ[ polyCache[dualNotation] = D tempP.xyz -operator cross(v: XYZ, w: XYZ): XYZ +operator cross tighter (*) (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[] @@ -429,8 +548,8 @@ function reciprocalN(P: Polyhedron): XYZ[] 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)] + 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) @@ -469,14 +588,14 @@ function approxCanonicalize(P: Polyhedron, notation: string, normalizeEdges xyz, edge if Math.max(...(xyz[i] dist start[i] for i of [0...V])) < THRESHOLD break - {face: P.face.slice(), xyz} + {face: P.face.slice(), xyz, P.name} 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) + pv := f æ ix-1 if pv < v then return.value.push ð pv, v function normalizeEdges(xyz: XYZ[], edge: Edge[]): void @@ -588,6 +707,7 @@ function inform(x: string) // VRML97 generation function outputVRML(notation: Notation, P: Polyhedron): string + shortDescrip := P.name or `A ${P.face.length}-hedron` ``` #VRML V2.0 utf8 Group { children [ @@ -603,6 +723,7 @@ George Hart's Encyclopedia of Polyhedra as the source." } NavigationInfo { type [ "EXAMINE" ] } DirectionalLight {direction -.5 -1 1 intensity 0.75} DirectionalLight {direction .5 1 -1 intensity 0.75} + Viewpoint { position 0 0 4.5 description "${shortDescrip}" } ${polyVRML P, colorScheme()} Shape { appearance Appearance { @@ -677,6 +798,6 @@ function edgeIndices(P: Polyhedron) sep := ",\n " filtmap(P.face, (thisf) => filtmap(thisf, (v, ix, f) => - preV := f æ (ix-1) + preV := f æ ix-1 preV < v ? `${preV}, ${v}, -1` : '').join sep) .join sep diff --git a/src/giveAwrl.civet b/src/giveAwrl.civet index 82864ce..f867667 100644 --- a/src/giveAwrl.civet +++ b/src/giveAwrl.civet @@ -136,8 +136,48 @@ if inputs.length is 1 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 +panelFrame := $('frame[name="panel"][src="prism-maker-subpanel.html"]') +if panelFrame.length is 1 // Seems so, fix the generator - console.log 'Need to fix the prism generator' + panelFrame.on "load", => + panelDoc := frames[1].document + vrmlDoc := frames[0].document + vrmlBody := $('body', vrmlDoc) + // Grab the initial text while it is still easy to get + textNode := vrmlBody.contents()[0] + initialVrml1: string := textNode.textContent or '' + // Now build up the vrml frame as we want it + viewerDiv := $('
') + $('head').after $('') + $('body').append viewerDiv + // We are presuming here that the body just contains a single + // text node. That should stay true unless GWH changes the page. + initialVrml97 := convert initialVrml1 + {canvas, browser3D: prismBrowser} := await makeBrowser '', '300px', '300px' + viewerDiv.append canvas + initialScene := await prismBrowser.createX3DFromString initialVrml97 + prismBrowser.replaceWorld initialScene + $(textNode).remove() + $('frame[name="vrml"]').remove() + // OK, finally have the layout cleaned up. Now we can set up our + // replacement generator: + prismBtn := $('input[type="button"][value="View"][onclick="ViewVRML()"]', + panelDoc) + unless prismBtn.length is 1 return + prismBtn.prop 'onclick', (i, val) => => + import(browser.runtime.getURL('prism.js')).then (prism) => + numerator := + parseInt $('input[name="numerator"]', panelDoc).val() as string + denominator := + parseInt $('input[name="denominator"]', panelDoc).val() as string + checks: boolean[] := [] + $('input[name="what"]', panelDoc).each -> + checks.push (@ as HTMLInputElement).checked + return + {vrml, err} := prism.generateVRML numerator, denominator, checks + maybeDebug vrml + if err then alert err + if vrml + scene := await prismBrowser.createX3DFromString vrml + prismBrowser.replaceWorld scene diff --git a/src/prism.civet b/src/prism.civet new file mode 100644 index 0000000..b32c218 --- /dev/null +++ b/src/prism.civet @@ -0,0 +1,282 @@ +type GenVRML = vrml?: string, err?: string + +function gcdpos(a: number, b: number) + [a, b] = [b, a%b] while b >= 1 + a + +shapeNamesRaw := + ``` + prism|dipyramid|compound: prism + dipyramid + antiprism|trapezohedron|compound: antiprism + trapezohedron + crossed antiprism|concave trapezohedron + compound: crossed antiprism + concave trapezohedron|polygon + ``` +shapeName := shapeNamesRaw.split /[|\n]/ + +export function generateVRML(n: number, d: number, chk: boolean[]) + unless chk[6..8].every !& or 2 < n/d < 3 + return err: 'Crossed antiprisms require 2 < numerator/denominator < 3, but ' + + `it is ${n}/${d} = ${n/d}` + unless n > 0 and d > 0 return err: 'Numerator and denominator should be positive' + g := gcdpos n,d + n /= g + d /= g + if n < 3 + return + err: `Numerator in lowest terms (currently ${n}) needs to be at least 3.` + unless 1 <= d < n + return err: 'Denominator should be strictly between 1 and numerator.' + if d > n/2 then d = n-d // equivalent + which := chk.findIndex (&) + chk[which] = false + if chk.some (&) return err: 'Currently only one shape option may be checked.' + name .= d > 1 ? `${n}/${d}-gonal` : + switch n + when 3: 'triangular' + when 4: 'square' + when 5: 'pentagonal' + when 6: 'hexagonal' + when 7: 'heptagonal' + when 8: 'octagonal' + else `${n}-gonal` + name += ' ' + shapeName[which] + {vrml: header(name) + vrmlBody which, n, d} + +function header(name: string) + ``` +#VRML V2.0 utf8 +WorldInfo { + title "${name}." + info [ + "Generated by GTW's reimplementation of GWH's prism generator" + "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 [ 0.2 0.5 0.9 ] + skyColor [ 0.2 0.5 0.9 ] } +NavigationInfo { type [ "EXAMINE" ] } +DirectionalLight { direction -.5 -1 1 intensity 0.75 } +DirectionalLight { direction .5 1 -1 intensity 0.75 } +DEF General Viewpoint { + position 6 0 6 orientation 0 1 0 0.78 description "General" } +DEF Face Viewpoint { + position 0 -6 0 orientation 1 0 0 1.57 description "Face" } +DEF Top Viewpoint { position 0 0 8 description "Top" } +DEF Edge Viewpoint { + position 7 0 0 orientation 0 1 0 1.57 description "Edge" } +DEF Inside Viewpoint { + position 0 0 0 orientation 1 0 0 -1.57 description "Inside" } +DEF Far Viewpoint { + position -25 0 25 orientation 0 1 0 -0.78 description "Far" } +``` + +// Useful constants +tau := 2*Math.PI + +// Array "postscript" operator +operator ps(main: T[], extra: T): T[] + main.push extra + main +// Array "preface" operator +operator pf(extra: T, main: T[]): T[] + main.unshift extra + main + +function shIx(name: string): number + shapeName.findIndex((sn) => name is in sn) + +type ShapeMethod = (n: number, theta: number, d?: number) => string +shapeMethod: ShapeMethod[] := [] +shapeMethods: Record := + prism: (n, theta) => + name := 'Prism' + h := Math.sin theta/2 + + coords := orb(1, n, h, theta) ++ orb(1, n, -h, theta) ps [0,0,h] ps [0,0,-h] + top := ([i, (i+1)%n, 2*n] for i of [0...n]) + bottom := ([(i+1)%n + n, i+n, 2*n+1] for i of [0...n]) + vrml := firstFaces name, '0.8 0.7 0.5', coords, top ++ bottom + + sides := ([i, (i+1)%n, (i+1)%n + n, i+n] for i of [0...n]) + laterFaces vrml, name, '0.3 0.5 0.9', sides + laterLines vrml, name, sides + emitObject vrml + + dipyr: (n, theta) => + name := 'Dipyramid' + c := 1/Math.cos theta/2 + d := 1/Math.sin theta/2 + + coords := [0,0,d] pf orb(c, n, 0, theta, 0.5) ps [0,0,-d] + cap := ([0, i, i%n + 1] for i of [1..n]) + cup := ([i%n + 1, i, n+1] for i of [1..n]) + vrml := firstFaces name, '0.9 0.3 0.3', coords, cap++cup + + edges := [1..n] pf ([0, i, n+1] for i of [1..n]) + laterLines vrml, name, edges + emitObject vrml + + compound: (n, theta) => + shapeMethod[shIx 'prism'](n, theta) + shapeMethod[shIx 'dipyr'](n, theta) + + antip: (n, theta) => + name := 'Antiprism' + {h, r} := antiprismDims theta + + coords := (orb(r, n, h, theta) ++ orb(r, n, -h, theta, 0.5) + ps [0,0,h] ps [0,0,-h]) + top := ([i, 2*n, (i+1)%n] for i of [0...n]) + bottom := ([(i+1)%n + n, 2*n + 1, i+n] for i of [0...n]) + vrml := firstFaces name, '0.8 0.7 0.5', coords, top ++ bottom + + topsides := ([(i+1)%n, i+n, i] for i of [0...n]) + botsides := ([i, i+n, (n+i-1)%n + n] for i of [0...n]) + laterFaces vrml, name, '0.9 0.3 0.4', topsides ++ botsides + laterLines vrml, name, [[0...n] ps 0, [n...2*n] ps n, ...topsides] + emitObject vrml + + trap: (n, theta) => + name := 'Trapezohedron' + {h, r} := antiprismDims theta + [t, z] := dualPoint r*Math.cos(theta/2), h, r, -h + + botzig := orb(t, n, -z, theta) ps [0, 0, -1/h] + topzig := orb(t, n, z, theta, 0.5) ps [0, 0, 1/h] + coords := alternate botzig, topzig + faces := + for i of [0...2*n] + if i%2 then [2*n + 1, (i+2)%(2*n), (i+1)%(2*n), i] + else [2*n, i, i+1, (i+2)%(2*n)] + vrml := firstFaces name, '0.3 0.5 0.9', coords, faces + + edges := + for i of [0...2*n] + i%2 ? [2*n + 1, i, (i + 1)%(2*n)] : [2*n, i, i+1] + laterLines vrml, name, edges + emitObject vrml + + 'antiprism + trap': (n, theta) => + shapeMethod[shIx 'antip'](n, theta) + shapeMethod[shIx 'trap'](n, theta) + + crossed: (n, theta) => // should really unify with antiprism, code is so close + name := 'CrossedAntiprism' + {h, r} := crossedDims theta + + coords := (orb(r, n, h, theta) ++ orb(-r, n, -h, theta, 0.5) + ps [0,0,h] ps [0,0,-h]) + top := ([i, (i+1)%n, 2*n] for i of [0...n]) + bottom := ([(i+1)%n + n, i+n, 2*n + 1] for i of [0...n]) + vrml := firstFaces name, '0.8 0.7 0.5', coords, top ++ bottom + + topsides := ([(i+1)%n, i+n, i] for i of [0...n]) + botsides := ([i, i+n, (n+i-1)%n + n] for i of [0...n]) + laterFaces vrml, name, '0.9 0.3 0.4', topsides ++ botsides + laterLines vrml, name, [[0...n] ps 0, [n...2*n] ps n, ...topsides] + emitObject vrml + + concave: (n, theta, d) => + name := 'ConcaveTrapezohedron' + {h, r} := crossedDims theta + [t, z] := dualPoint r*Math.cos(theta/2), h, -r, -h + d ??= Math.floor (n-1)/2 // won't be needed, for TypeScript's sake + eta := tau * (n-d)/n + + botzig := orb(-t, n, -z, eta) ps [0, 0, -1/h] + topzig := orb(-t, n, z, eta, 0.5) ps [0, 0, 1/h] + coords := alternate botzig, topzig + halfFaces := + for i of [0...2*n] + i%2 ? [(i+1)%(2*n), i, 2*n + 1] : [2*n, i, i+1] + otherFaces := + for i of [0...2*n] + i%2 ? [2*n + 1, (i+2)%(2*n), (i+1)%(2*n)] : [2*n, i+1, (i+2)%(2*n)] + vrml := firstFaces name, '0.3 0.5 0.9', coords, halfFaces ++ otherFaces + laterLines vrml, name, halfFaces + emitObject vrml + + 'prism + concave': (n, theta, d) => + shapeMethod[shIx 'cross'](n, theta) + shapeMethod[shIx 'conc'](n, theta, d) + + polygon: (n, theta) => + name := 'Polygon' + coords := orb(1, n, 0, theta) ps [0, 0, 0] + faces := ([i, (i+1)%n, n] for i of [0...n]) + vrml := firstFaces name, '0.5 0.8 0.5', coords, faces + laterLines vrml, name, [[0...n] ps 0] + emitObject vrml + +for key, method in shapeMethods + shapeMethod[shIx key] = method + +function antiprismDims(theta: number) + h .= Math.sqrt 1 - 4/(4 + 2*Math.cos(theta/2) - 2*Math.cos(theta)) + r .= Math.sqrt 1 - h*h + f := Math.sqrt h*h + (r*Math.cos theta/2)**2 + h /= f + r /= f + {h, r} + +function crossedDims(theta: number) + r .= 2/Math.sqrt 4 - 2*Math.cos(theta/2) - 2*Math.cos(theta) + h .= Math.sqrt 1 - r*r + f .= Math.sqrt h*h + (r*Math.cos theta/2)**2 + r /= f + h /= f + {h, r} + +function dualPoint(x1: number, y1: number, x2: number, y2: number) + len := Math.sqrt (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1) + vx := (y2 - y1)/len + vy := (x1 - x2)/len + d := vx*x1 + vy*y1 + [vx/d, vy/d] + +function vrmlBody(which: number, n: number, d: number) + theta := tau * d/n + shapeMethod[which](n, theta, d) + +function orb(r: number, n: number, height: number, theta: number, t=0) + rho := t*theta + for i of [0...n] + [r*Math.cos(rho+i*theta), r*Math.sin(rho+i*theta), height] + +function alternate(a: T[], b: T[]) + return: T[] := [] + return.value.push n, b[i] for each n, i of a + +function firstFaces(name: string, color: string, + coords: number[][], faces: number[][]) + return: string[] := [] + startShape return.value, color + return.value.push 'geometry IndexedFaceSet {', + `coord DEF ${name}Coords Coordinate { point [` + return.value.push coord.join(' ') + ',' for each coord of coords + return.value.push '] }', + 'creaseAngle 0 solid FALSE coordIndex [' + return.value.push face.join(', ') + ', -1,' for each face of faces + return.value.push '] } }' + +function startShape(container: string[], color: string, colType = 'diffuse'): void + container.push 'Shape { appearance Appearance {', + ' material Material {', + ` ${colType}Color ${color} } }` + +function laterFaces(container: string[], name: string, color: string, + faces: number[][]): void + startShape container, color + container.push 'geometry IndexedFaceSet {', + `coord USE ${name}Coords`, + 'creaseAngle 0 solid FALSE coordIndex [' + container.push face.join(', ') + ', -1,' for each face of faces + container.push '] } }' + +function laterLines(container: string[], name: string, edges: number[][]): void + startShape container, '0 0 0', 'emissive' // per VRML97 standard + container.push 'geometry IndexedLineSet {', + `coord USE ${name}Coords`, + 'coordIndex [' + container.push edge.join(', ') + ', -1,' for each edge of edges + container.push '] } }' + +function emitObject(container: string[]) container.join "\n" diff --git a/tools/makePlugin.bash b/tools/makePlugin.bash index 38e5b8f..f70be10 100644 --- a/tools/makePlugin.bash +++ b/tools/makePlugin.bash @@ -22,6 +22,7 @@ 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 public/js/conway.js $1 +cp public/js/prism.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