diff --git a/src/conway.civet b/src/conway.civet index 0aeca42..db168ee 100644 --- a/src/conway.civet +++ b/src/conway.civet @@ -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 := '': 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 ð(...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 æ(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 := 3: '0.9 0.3 0.3' // red 4: '0.4 0.4 1.0' // blue @@ -297,4 +444,15 @@ faceColors: Record := 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(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 diff --git a/src/giveAwrl.civet b/src/giveAwrl.civet index 1c1dc5d..e8119f2 100644 --- a/src/giveAwrl.civet +++ b/src/giveAwrl.civet @@ -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 := $(`${vrml}`) - 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' +