type Face = number[] type XYZ = [number, number, number] type Polyhedron = {face: Face[], xyz: XYZ[], name?: string} 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 // 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 := 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], [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]] name: 'Icosahedron' polyCache: Record := '': 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 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 ð(...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: 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 := (ð 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 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 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, name: ngonal(n)+` prism`} 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, name: ngonal(n)+` antiprism`} 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, name: ngonal(n)+` pyramid`} type SeedOp = keyof typeof seeds // Syntactic sugar to deal with weird TypeScript typing: operator æ(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 // 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] // 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() 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 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 [] 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) 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, 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 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 === name: dualName P.name 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 same (/) (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. // 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 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 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 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 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[] 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, 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 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 shortDescrip := P.name or `A ${P.face.length}-hedron` ``` #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} Viewpoint { position 0 0 4.5 description "${shortDescrip}" } ${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 := 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(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