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:
parent
37bea13d30
commit
dba8870c83
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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',
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
682
src/conway.civet
Normal 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
|
@ -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'
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user