Trace the following to the JavaScript console:
diff --git a/src/adapptypes.civet b/src/adapptypes.civet
index 72d28ea..ef20292 100644
--- a/src/adapptypes.civet
+++ b/src/adapptypes.civet
@@ -1,5 +1,7 @@
+// This file is a bit misnamed, as it has options for giveAwrl, too.
+
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 ConfigType = Partial>
diff --git a/src/conway.civet b/src/conway.civet
index db168ee..14172a3 100644
--- a/src/conway.civet
+++ b/src/conway.civet
@@ -92,6 +92,9 @@ function orb(r: number, n: number,
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
@@ -130,7 +133,7 @@ seeds :=
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]
+ 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]
@@ -158,7 +161,7 @@ function kisjoin(P: Polyhedron, notation: string,
// first collect a directory from face indices to new vertex numbers
nextVertex .= P.xyz.length
newVixes :=
- for f of P.face
+ 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]
@@ -179,11 +182,13 @@ function kisjoin(P: Polyhedron, notation: string,
if pw < w // avoid adding same face twice
face.push [v, pw, newVixes[neighbor], w]
else face.push [v, pw, w]
- adjustXYZ({face, xyz}, notation, 3)
+ adjustXYZ {face, xyz}, notation, 3
+
+// how enums ought to work?
+FromCenter := Symbol()
+AlongEdge := Symbol()
+type Gyway = typeof FromCenter | typeof AlongEdge
-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
@@ -194,7 +199,59 @@ function gyropel(P: Polyhedron, notation: string,
// 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
+ 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 []
@@ -218,17 +275,41 @@ transforms :=
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, notation: string): Polyhedron
+ 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, notation, digits
- polyCache[notation] = return.value
+ 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
@@ -273,7 +354,7 @@ function approxDualVertex(f: Face, v: XYZ[]): XYZ
// 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,
+ // 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])
@@ -294,9 +375,12 @@ 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 := sub w,v
- sub v, scale d, d dot v / mag2 d
+ d := w sub v
+ v sub scale d, d dot v / mag2 d
function faceCenters(P: Polyhedron): XYZ[]
for each face of P.face
@@ -307,7 +391,7 @@ function adjustXYZ(P: Polyhedron, notation: string, iterations = 1): Polyhedron
dualNotation := 'd' + notation
D .= topoDual P
if dualNotation in polyCache
- console.error 'Error: Creating', notation, 'after its dual'
+ console.error 'Creating', notation, '_after_ its dual'
D = polyCache[dualNotation]
for iter of [1..iterations]
D.xyz = reciprocalC P
@@ -320,6 +404,143 @@ function reciprocalC(P: Polyhedron): XYZ[]
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]
@@ -336,24 +557,27 @@ function diminish(basket: XYZ, egg: XYZ)
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]
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)
diff --git a/src/giveAwrl.civet b/src/giveAwrl.civet
index e8119f2..82864ce 100644
--- a/src/giveAwrl.civet
+++ b/src/giveAwrl.civet
@@ -1,5 +1,6 @@
import ./deps/jquery.js
{convert} from ./deps/vrml1to97/index.js
+{ConfigType} from ./adapptypes.ts
knownExtensions := /[.](?:wrl|x3d|gltf|glb|obj|stl|ply)$/
certainlyHandled :=
@@ -42,6 +43,7 @@ function makeBrowser(url: string, width: string, height: string)
text .= await response.text()
if /#\s*VRML\s*V?1[.]/i.test text
text = convert text
+ maybeDebug text
browser3D.baseURL = url
scene := await browser3D.createX3DFromString text
browser3D.replaceWorld scene
@@ -90,8 +92,9 @@ links.after ->
canvas.style.marginRight = imgSty.getPropertyValue 'margin-right'
if float is 'right'
canvas.style.left = $(eye).width() + 'px'
- else
+ else if float is 'left'
canvas.style.right = $(eye).width() + 'px'
+ else canvas.style.left = floatLike.offsetLeft
$(eye).append canvas
if state is 'off'
eye.setAttribute 'data', 'on'
@@ -102,6 +105,10 @@ links.after ->
$(eye).css 'text-decoration', 'none'
$(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
@@ -117,6 +124,7 @@ if inputs.length is 1
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