feat: Handle vrml generated on the fly in Conway notation page (#62)

Reviewed-on: #62
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
Glen Whitney 2024-02-18 09:32:17 +00:00 committed by Glen Whitney
parent 37bea13d30
commit dba8870c83
8 changed files with 784 additions and 54 deletions

View File

@ -36,6 +36,7 @@
"deps/x_ite/assets/components/Text.js", "deps/x_ite/assets/components/Text.js",
"adapptlet.js", "adapptlet.js",
"adapptypes.js", "adapptypes.js",
"conway.js",
"options.js", "options.js",
"deps/GeoGebra/deployggb.js", "deps/GeoGebra/deployggb.js",
"deps/GeoGebra/HTML5/5.0/webSimple/4B19686283BEF852F4C88C93522FB9A3.cache.js", "deps/GeoGebra/HTML5/5.0/webSimple/4B19686283BEF852F4C88C93522FB9A3.cache.js",

View File

@ -7,6 +7,11 @@
<body> <body>
<h3>Debugging</h3> <h3>Debugging</h3>
<h4>Embedded VRML/X3D display</h4>
Write to the JavaScript console: <br/>
<label for="vrml97">Generated VRML97 specifications</label>
<input type="checkbox" id="vrml97">
<br />
<h4>Java Geometry Applets</h4> <h4>Java Geometry Applets</h4>
Trace the following to the JavaScript console: <br/> Trace the following to the JavaScript console: <br/>
<label for="commands">Commands executed</label> <label for="commands">Commands executed</label>

View File

@ -34,12 +34,12 @@
url: 'https://code.studioinfinity.org/glen/archematics.git', url: 'https://code.studioinfinity.org/glen/archematics.git',
}, },
devDependencies: { devDependencies: {
'@danielx/civet': '^0.6.72', '@danielx/civet': '^0.6.73',
'@types/firefox-webext-browser': '^120.0.0', '@types/firefox-webext-browser': '^120.0.0',
'@types/jquery': '^3.5.29', '@types/jquery': '^3.5.29',
'@webcomponents/custom-elements': '^1.6.0', '@webcomponents/custom-elements': '^1.6.0',
'http-server': '^14.1.1', 'http-server': '^14.1.1',
rollup: '^4.10.0', rollup: '^4.11.0',
typescript: '^5.3.3', typescript: '^5.3.3',
'webextension-polyfill': '^0.10.0', 'webextension-polyfill': '^0.10.0',
}, },

View File

@ -14,8 +14,8 @@ dependencies:
devDependencies: devDependencies:
'@danielx/civet': '@danielx/civet':
specifier: ^0.6.72 specifier: ^0.6.73
version: 0.6.72(typescript@5.3.3) version: 0.6.73(typescript@5.3.3)
'@types/firefox-webext-browser': '@types/firefox-webext-browser':
specifier: ^120.0.0 specifier: ^120.0.0
version: 120.0.0 version: 120.0.0
@ -29,8 +29,8 @@ devDependencies:
specifier: ^14.1.1 specifier: ^14.1.1
version: 14.1.1 version: 14.1.1
rollup: rollup:
specifier: ^4.10.0 specifier: ^4.11.0
version: 4.10.0 version: 4.11.0
typescript: typescript:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.3.3 version: 5.3.3
@ -47,8 +47,8 @@ packages:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
dev: true dev: true
/@danielx/civet@0.6.72(typescript@5.3.3): /@danielx/civet@0.6.73(typescript@5.3.3):
resolution: {integrity: sha512-jumnIbXbdFs0ZiKN62fmD+p8QGi+E0jmtc02dKz9wIIoPkODsa4XXlBrS5BRR5fr3w5d3ah8Vq7gWt+DL9Wa0Q==} resolution: {integrity: sha512-VOq02JgXNArsLzq/I2OnZtn16exiF8/mIhpR5O6Tsc+97RU6Qe8EP8XJskh/Hm9koUODzWUW8UuFpFdVzm7wTA==}
engines: {node: '>=19 || ^18.6.0 || ^16.17.0'} engines: {node: '>=19 || ^18.6.0 || ^16.17.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -78,104 +78,104 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
dev: true dev: true
/@rollup/rollup-android-arm-eabi@4.10.0: /@rollup/rollup-android-arm-eabi@4.11.0:
resolution: {integrity: sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A==} resolution: {integrity: sha512-BV+u2QSfK3i1o6FucqJh5IK9cjAU6icjFFhvknzFgu472jzl0bBojfDAkJLBEsHFMo+YZg6rthBvBBt8z12IBQ==}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-android-arm64@4.10.0: /@rollup/rollup-android-arm64@4.11.0:
resolution: {integrity: sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ==} resolution: {integrity: sha512-0ij3iw7sT5jbcdXofWO2NqDNjSVVsf6itcAkV2I6Xsq4+6wjW1A8rViVB67TfBEan7PV2kbLzT8rhOVWLI2YXw==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-darwin-arm64@4.10.0: /@rollup/rollup-darwin-arm64@4.11.0:
resolution: {integrity: sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg==} resolution: {integrity: sha512-yPLs6RbbBMupArf6qv1UDk6dzZvlH66z6NLYEwqTU0VHtss1wkI4UYeeMS7TVj5QRVvaNAWYKP0TD/MOeZ76Zg==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-darwin-x64@4.10.0: /@rollup/rollup-darwin-x64@4.11.0:
resolution: {integrity: sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q==} resolution: {integrity: sha512-OvqIgwaGAwnASzXaZEeoJY3RltOFg+WUbdkdfoluh2iqatd090UeOG3A/h0wNZmE93dDew9tAtXgm3/+U/B6bw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.10.0: /@rollup/rollup-linux-arm-gnueabihf@4.11.0:
resolution: {integrity: sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw==} resolution: {integrity: sha512-X17s4hZK3QbRmdAuLd2EE+qwwxL8JxyVupEqAkxKPa/IgX49ZO+vf0ka69gIKsaYeo6c1CuwY3k8trfDtZ9dFg==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm64-gnu@4.10.0: /@rollup/rollup-linux-arm64-gnu@4.11.0:
resolution: {integrity: sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q==} resolution: {integrity: sha512-673Lu9EJwxVB9NfYeA4AdNu0FOHz7g9t6N1DmT7bZPn1u6bTF+oZjj+fuxUcrfxWXE0r2jxl5QYMa9cUOj9NFg==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-arm64-musl@4.10.0: /@rollup/rollup-linux-arm64-musl@4.11.0:
resolution: {integrity: sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ==} resolution: {integrity: sha512-yFW2msTAQNpPJaMmh2NpRalr1KXI7ZUjlN6dY/FhWlOclMrZezm5GIhy3cP4Ts2rIAC+IPLAjNibjp1BsxCVGg==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-riscv64-gnu@4.10.0: /@rollup/rollup-linux-riscv64-gnu@4.11.0:
resolution: {integrity: sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA==} resolution: {integrity: sha512-kKT9XIuhbvYgiA3cPAGntvrBgzhWkGpBMzuk1V12Xuoqg7CI41chye4HU0vLJnGf9MiZzfNh4I7StPeOzOWJfA==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-x64-gnu@4.10.0: /@rollup/rollup-linux-x64-gnu@4.11.0:
resolution: {integrity: sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw==} resolution: {integrity: sha512-6q4ESWlyTO+erp1PSCmASac+ixaDv11dBk1fqyIuvIUc/CmRAX2Zk+2qK1FGo5q7kyDcjHCFVwgGFCGIZGVwCA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-linux-x64-musl@4.10.0: /@rollup/rollup-linux-x64-musl@4.11.0:
resolution: {integrity: sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw==} resolution: {integrity: sha512-vIAQUmXeMLmaDN78HSE4Kh6xqof2e3TJUKr+LPqXWU4NYNON0MDN9h2+t4KHrPAQNmU3w1GxBQ/n01PaWFwa5w==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-arm64-msvc@4.10.0: /@rollup/rollup-win32-arm64-msvc@4.11.0:
resolution: {integrity: sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ==} resolution: {integrity: sha512-LVXo9dDTGPr0nezMdqa1hK4JeoMZ02nstUxGYY/sMIDtTYlli1ZxTXBYAz3vzuuvKO4X6NBETciIh7N9+abT1g==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-ia32-msvc@4.10.0: /@rollup/rollup-win32-ia32-msvc@4.11.0:
resolution: {integrity: sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg==} resolution: {integrity: sha512-xZVt6K70Gr3I7nUhug2dN6VRR1ibot3rXqXS3wo+8JP64t7djc3lBFyqO4GiVrhNaAIhUCJtwQ/20dr0h0thmQ==}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
dev: true dev: true
optional: true optional: true
/@rollup/rollup-win32-x64-msvc@4.10.0: /@rollup/rollup-win32-x64-msvc@4.11.0:
resolution: {integrity: sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ==} resolution: {integrity: sha512-f3I7h9oTg79UitEco9/2bzwdciYkWr8pITs3meSDSlr1TdvQ7IxkQaaYN2YqZXX5uZhiYL+VuYDmHwNzhx+HOg==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
requiresBuild: true requiresBuild: true
@ -556,26 +556,26 @@ packages:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: true dev: true
/rollup@4.10.0: /rollup@4.11.0:
resolution: {integrity: sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==} resolution: {integrity: sha512-2xIbaXDXjf3u2tajvA5xROpib7eegJ9Y/uPlSFhXLNpK9ampCczXAhLEb5yLzJyG3LAdI1NWtNjDXiLyniNdjQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
optionalDependencies: optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.10.0 '@rollup/rollup-android-arm-eabi': 4.11.0
'@rollup/rollup-android-arm64': 4.10.0 '@rollup/rollup-android-arm64': 4.11.0
'@rollup/rollup-darwin-arm64': 4.10.0 '@rollup/rollup-darwin-arm64': 4.11.0
'@rollup/rollup-darwin-x64': 4.10.0 '@rollup/rollup-darwin-x64': 4.11.0
'@rollup/rollup-linux-arm-gnueabihf': 4.10.0 '@rollup/rollup-linux-arm-gnueabihf': 4.11.0
'@rollup/rollup-linux-arm64-gnu': 4.10.0 '@rollup/rollup-linux-arm64-gnu': 4.11.0
'@rollup/rollup-linux-arm64-musl': 4.10.0 '@rollup/rollup-linux-arm64-musl': 4.11.0
'@rollup/rollup-linux-riscv64-gnu': 4.10.0 '@rollup/rollup-linux-riscv64-gnu': 4.11.0
'@rollup/rollup-linux-x64-gnu': 4.10.0 '@rollup/rollup-linux-x64-gnu': 4.11.0
'@rollup/rollup-linux-x64-musl': 4.10.0 '@rollup/rollup-linux-x64-musl': 4.11.0
'@rollup/rollup-win32-arm64-msvc': 4.10.0 '@rollup/rollup-win32-arm64-msvc': 4.11.0
'@rollup/rollup-win32-ia32-msvc': 4.10.0 '@rollup/rollup-win32-ia32-msvc': 4.11.0
'@rollup/rollup-win32-x64-msvc': 4.10.0 '@rollup/rollup-win32-x64-msvc': 4.11.0
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true

View File

@ -1,5 +1,7 @@
// This file is a bit misnamed, as it has options for giveAwrl, too.
export const flags = [ 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 FlagType = (typeof flags)[number]
export type ConfigType = Partial<Record<FlagType, boolean>> export type ConfigType = Partial<Record<FlagType, boolean>>

682
src/conway.civet Normal file
View File

@ -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<Notation, Polyhedron> :=
'': 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 ð<Tup extends unknown[]>(...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 æ<T>(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<number, string> :=
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<T,U>(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

View File

@ -1,5 +1,6 @@
import ./deps/jquery.js import ./deps/jquery.js
{convert} from ./deps/vrml1to97/index.js {convert} from ./deps/vrml1to97/index.js
{ConfigType} from ./adapptypes.ts
knownExtensions := /[.](?:wrl|x3d|gltf|glb|obj|stl|ply)$/ knownExtensions := /[.](?:wrl|x3d|gltf|glb|obj|stl|ply)$/
certainlyHandled := certainlyHandled :=
@ -42,10 +43,11 @@ function makeBrowser(url: string, width: string, height: string)
text .= await response.text() text .= await response.text()
if /#\s*VRML\s*V?1[.]/i.test text if /#\s*VRML\s*V?1[.]/i.test text
text = convert text text = convert text
maybeDebug text
browser3D.baseURL = url browser3D.baseURL = url
scene := await browser3D.createX3DFromString text scene := await browser3D.createX3DFromString text
browser3D.replaceWorld scene browser3D.replaceWorld scene
canvas {canvas, browser3D}
// Put eye icons after all of the eligible links // Put eye icons after all of the eligible links
links := $('a').filter -> knownExtensions.test @.getAttribute('href') ?? '' links := $('a').filter -> knownExtensions.test @.getAttribute('href') ?? ''
@ -79,7 +81,7 @@ links.after ->
overImg := floatLike and floatLike.tagName is 'IMG' overImg := floatLike and floatLike.tagName is 'IMG'
width := overImg ? ($(floatLike).width() + 'px') : '150px' width := overImg ? ($(floatLike).width() + 'px') : '150px'
height := overImg ? ($(floatLike).height() + 'px') : '150px' height := overImg ? ($(floatLike).height() + 'px') : '150px'
canvas := await makeBrowser url, width, height {canvas} := await makeBrowser url, width, height
if float if float
canvas.style.float = float canvas.style.float = float
if overImg if overImg
@ -90,8 +92,9 @@ links.after ->
canvas.style.marginRight = imgSty.getPropertyValue 'margin-right' canvas.style.marginRight = imgSty.getPropertyValue 'margin-right'
if float is 'right' if float is 'right'
canvas.style.left = $(eye).width() + 'px' canvas.style.left = $(eye).width() + 'px'
else else if float is 'left'
canvas.style.right = $(eye).width() + 'px' canvas.style.right = $(eye).width() + 'px'
else canvas.style.left = floatLike.offsetLeft
$(eye).append canvas $(eye).append canvas
if state is 'off' if state is 'off'
eye.setAttribute 'data', 'on' eye.setAttribute 'data', 'on'
@ -101,3 +104,40 @@ links.after ->
eye.setAttribute 'data', 'off' eye.setAttribute 'data', 'off'
$(eye).css 'text-decoration', 'none' $(eye).css 'text-decoration', 'none'
$(eye.lastElementChild as Element).hide() $(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'

View File

@ -21,7 +21,7 @@ done
npx rollup public/js/giveAwrl.js --dir $1 npx rollup public/js/giveAwrl.js --dir $1
npx rollup public/js/adapptlet.js --file $1/adapptlet.js npx rollup public/js/adapptlet.js --file $1/adapptlet.js
npx rollup public/js/adapptext.js --file $1/adapptext.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/webextension-polyfill/dist/browser-polyfill.js $1
cp node_modules/@webcomponents/custom-elements/custom-elements.min.js $1 cp node_modules/@webcomponents/custom-elements/custom-elements.min.js $1
zip -r $1 $1 zip -r $1 $1