feat: Get viewer for notation generator working

This commit is contained in:
Glen Whitney 2024-02-17 09:10:42 -08:00
parent 44158f5595
commit af2d9e02c7
2 changed files with 228 additions and 60 deletions

View File

@ -19,28 +19,28 @@ icosahedron: Polyhedron :=
[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]]
[1,0,ihp], [-1,0,ihp], [1,0,-ihp], [-1,0,-ihp]]
polyCache: Record<Notation, Polyhedron> :=
'': face: [], xyz: []
T:
face: [[0,1,2], [0,2,3], [0,3,1], [1,3,2]]
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,1,2], [0,2,3], [0,3,4], [0,4,1],
[1,4,5], [1,5,2], [2,5,3], [3,5,4]]
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: [[3,0,1,2], [3,4,5,0], [0,5,6,1], [1,6,7,2], [2,7,4,3], [5,4,7,6]]
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)
D: geomDual icosahedron
export function generateVRML(notation: Notation): string
outputVRML notation, generatePoly notation
function generatePoly(notation: Notation): Polyhedron
getStandardPoly standardize notation
getStandardPoly inform standardize notation
function getStandardPoly(notation: Notation): Polyhedron
if notation in polyCache then return polyCache[notation]
@ -48,25 +48,39 @@ function getStandardPoly(notation: Notation): Polyhedron
parent := getStandardPoly rest
// may have created what we want by side effect
if notation in polyCache then return polyCache[notation]
dispatch op, Number(arg or 0), parent, notation // will do the caching
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*)`]: 'd$1d', j: 'dad', s: 'dgd', // dual operations
dd: '', ad: 'a', gd: 'g', // absorption of duals
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', aT: 'O', dC: 'O', dO: 'C',
dI: 'D', dD: 'I', aO: 'aC', aI: 'aD', gO: 'gC', gI: 'gD'
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
for [pat, rep] of standardizations
notation = notation.replace(pat, rep)
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,
@ -80,6 +94,7 @@ function orb(r: number, n: number,
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)
@ -89,6 +104,7 @@ seeds :=
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))
@ -98,20 +114,24 @@ seeds :=
halfHeight /= f
faceRadius /= f
xyz := orb(faceRadius, n, halfHeight)
++ orb(faceRadius, n, halfHeight, 0.5)
++ 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))
xyz := orb baseRadius, n, Math.tan theta/4
xyz.push [0, 0, -1/Math.tan theta/4]
depth := Math.sqrt (1-c)/(1+c)
height := 2*Math.sqrt 1/(1 - c*c)
xyz := orb baseRadius, n, depth
edgeMid2 := add xyz[0], 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}
@ -120,29 +140,94 @@ 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 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)
enum Gyway
FromCenter
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.
// TO BE IMPLEMENTED
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, n: number, notation: string): Polyhedron => // kis[n]
// aka "elevate" -- add a pyramid on each (n-sided) face
centers := faceCenters P
xyz := P.xyz.slice()
face: Face[] := []
for each f, ix of P.face
if n is 0 or f.length is n
v := xyz.length
xyz.push centers[ix]
for each j of [0...f.length]
face.push [v, f æ (j-1), f[j]]
else face.push f.slice()
adjustXYZ({face, xyz}, notation, 3)
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
type TransformOp = keyof typeof transforms
function dispatch(op: string, n: number,
function dispatch(op: string, digits: string,
P: Polyhedron, notation: string): Polyhedron
return .= P
if op in seeds
return = seeds[op as SeedOp] n
return = seeds[op as SeedOp] Number(digits) or 0
else if op in transforms
return = transforms[op as TransformOp] P, n, notation
return = transforms[op as TransformOp] P, notation, digits
polyCache[notation] = return.value
function topoDual(P: Polyhedron): Polyhedron
@ -151,34 +236,67 @@ function topoDual(P: Polyhedron): Polyhedron
// in some way.
face:
for v of [0...P.xyz.length]
infaces :=
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
// Takes the vertices of the dual to be the face centers of P
// all scaled so that the midpoint of the first edge is unit distance
// from the origin
return := topoDual(P)
newVertices := faceCenters(P)
aface := return.value.face[0]
mid2 := add newVertices[aface[0]], newVertices[aface[1]]
factor := 2/mag mid2
for each v of newVertices
scale(v, factor)
return.value.xyz = newVertices
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 well 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]
function tangentPoint(v: XYZ, w: XYZ) // closest point on vw to origin
d := sub w,v
sub v, scale d, d dot v / mag2 d
function faceCenters(P: Polyhedron): XYZ[]
for each face of P.face
@ -209,12 +327,21 @@ function accumulate(basket: XYZ, egg: XYZ)
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 add(a: XYZ, b: XYZ)
accumulate copy(a), b
function sub(a: XYZ, b: XYZ)
diminish copy(a), b
function mag2(a: XYZ)
a[0]*a[0] + a[1]*a[1] + a[2]*a[2]
@ -227,6 +354,13 @@ function scale(subject: XYZ, by: number)
subject[2] *= by
subject
// Feedback
function inform(x: string)
$('input[name="inform"]').val(x)
x
// VRML97 generation
function outputVRML(notation: Notation, P: Polyhedron): string
@ -235,17 +369,26 @@ function outputVRML(notation: Notation, P: Polyhedron): string
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."
info "By using this script, you agree that this image is released"
info "into the public domain, although you are requested to cite"
info "George Hart's Encyclopedia of Polyhedra as the source." }
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] }
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()} ] }
${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
@ -274,15 +417,19 @@ function polyVRML(P: Polyhedron, color: string): string
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()
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
@ -297,4 +444,15 @@ faceColors: Record<number, string> :=
function colorBySides(n: number)
if n in faceColors
return faceColors[n]
return '0.5 0.5 0.5' // gray
'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

@ -45,7 +45,7 @@ function makeBrowser(url: string, width: string, height: string)
browser3D.baseURL = url
scene := await browser3D.createX3DFromString text
browser3D.replaceWorld scene
canvas
{canvas, browser3D}
// Put eye icons after all of the eligible links
links := $('a').filter -> knownExtensions.test @.getAttribute('href') ?? ''
@ -79,7 +79,7 @@ links.after ->
overImg := floatLike and floatLike.tagName is 'IMG'
width := overImg ? ($(floatLike).width() + 'px') : '150px'
height := overImg ? ($(floatLike).height() + 'px') : '150px'
canvas := await makeBrowser url, width, height
{canvas} := await makeBrowser url, width, height
if float
canvas.style.float = float
if overImg
@ -102,6 +102,9 @@ links.after ->
$(eye).css 'text-decoration', 'none'
$(eye.lastElementChild as Element).hide()
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
@ -114,12 +117,19 @@ if inputs.length is 1
notation := $('input[name="notation"]').val()
unless notation then return
vrml := conway.generateVRML notation.toString()
viewerSpan := $(`<span>${vrml}</span>`)
viewerSpan.css 'float', 'left'
$('form[name="input"]').first().before viewerSpan
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'