feat: Reimplementation of prism generator (#65)

Resolves #16.

Reviewed-on: #65
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-19 21:52:56 +00:00 committed by Glen Whitney
parent dba8870c83
commit b74921341c
7 changed files with 525 additions and 80 deletions

View File

@ -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",

View File

@ -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',
},
}

View File

@ -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

View File

@ -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<string, string> :=
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<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]]
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 æ<T>(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

View File

@ -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 := $('<div></div>')
$('head').after $('<body></body>')
$('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

282
src/prism.civet Normal file
View File

@ -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<T>(main: T[], extra: T): T[]
main.push extra
main
// Array "preface" operator
operator pf<T>(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<string, ShapeMethod> :=
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<T>(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"

View File

@ -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