feat: Handle vrml generated on the fly in Conway notation page #62

Merged
glen merged 3 commits from vrml_generator into main 2024-02-18 09:32:18 +00:00
4 changed files with 262 additions and 23 deletions
Showing only changes of commit cbe44238a1 - Show all commits

View File

@ -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>

View File

@ -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>>

View File

@ -92,6 +92,9 @@ function orb(r: number, n: number,
for i of [0...n] for i of [0...n]
[r*Math.cos(rho + i*theta), r*Math.sin(rho + i*theta), height] as XYZ [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 := seeds :=
P: (n: number) => // Prism P: (n: number) => // Prism
unless n then n = 3 unless n then n = 3
@ -130,7 +133,7 @@ seeds :=
depth := Math.sqrt (1-c)/(1+c) depth := Math.sqrt (1-c)/(1+c)
height := 2*Math.sqrt 1/(1 - c*c) height := 2*Math.sqrt 1/(1 - c*c)
xyz := orb baseRadius, n, depth xyz := orb baseRadius, n, depth
edgeMid2 := add xyz[0], xyz[1] edgeMid2 := xyz[0] add xyz[1]
xyz.push [0, 0, depth-height] xyz.push [0, 0, depth-height]
face := ([i, (i+1)%n, n] for i of [0...n]) face := ([i, (i+1)%n, n] for i of [0...n])
face.unshift [n-1..0] 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 // first collect a directory from face indices to new vertex numbers
nextVertex .= P.xyz.length nextVertex .= P.xyz.length
newVixes := newVixes :=
for f of P.face for each f of P.face
!digits or f.length is in allowed ? nextVertex++ : 0 !digits or f.length is in allowed ? nextVertex++ : 0
if nextVertex is P.xyz.length then return P // nothing to do if nextVertex is P.xyz.length then return P // nothing to do
xyz := P.xyz ++ faceCenters(P).filter (f,ix) => newVixes[ix] 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 if pw < w // avoid adding same face twice
face.push [v, pw, newVixes[neighbor], w] face.push [v, pw, newVixes[neighbor], w]
else face.push [v, pw, 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, function gyropel(P: Polyhedron, notation: string,
digits: string, ...ways: Gyway[]): Polyhedron digits: string, ...ways: Gyway[]): Polyhedron
// gyro and propellor are closely related operations. Both of them add new // 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 // they are just connected in sequence. For completeness, we also allow
// both at the same time, which is equivalent to propellor followed by kis // both at the same time, which is equivalent to propellor followed by kis
// just on the new rotated faces. // 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[] function parseSides(digits: string): number[]
unless digits return [] unless digits return []
@ -218,17 +275,41 @@ transforms :=
kisjoin P, notation, digits, false kisjoin P, notation, digits, false
j: (P: Polyhedron, notation: string, digits: string) => // join j: (P: Polyhedron, notation: string, digits: string) => // join
kisjoin P, notation, digits, true 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 type TransformOp = keyof typeof transforms
function dispatch(op: string, digits: string, function dispatch(op: string, digits: string,
P: Polyhedron, notation: string): Polyhedron P: Polyhedron, mynotation: string): Polyhedron
// Note mynotation starts with op!
return .= P return .= P
if op in seeds if op in seeds
return = seeds[op as SeedOp] Number(digits) or 0 return = seeds[op as SeedOp] Number(digits) or 0
else if op in transforms else if op in transforms
return = transforms[op as TransformOp] P, notation, digits return = transforms[op as TransformOp] P, mynotation, digits
polyCache[notation] = return.value else
console.error `Unknown operation ${op}${digits} in ${mynotation}.`
return = polyCache.T
polyCache[mynotation] = return.value
function topoDual(P: Polyhedron): Polyhedron function topoDual(P: Polyhedron): Polyhedron
// Note this maintains correspondence between V and F indices, but // 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. // 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 // This function returns the point closest to being on all of those planes
// (in the least-squares sense). // (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 // and very poorly otherwise. So it probably would not provide any better
// canonicalization than other methods of approximating the dual. // canonicalization than other methods of approximating the dual.
normals := (tangentPoint(v[f æ (i-1)], v[f[i]]) for i of [0...f.length]) 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[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] - 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 function tangentPoint(v: XYZ, w: XYZ) // closest point on vw to origin
d := sub w,v d := w sub v
sub v, scale d, d dot v / mag2 d v sub scale d, d dot v / mag2 d
function faceCenters(P: Polyhedron): XYZ[] function faceCenters(P: Polyhedron): XYZ[]
for each face of P.face for each face of P.face
@ -307,7 +391,7 @@ function adjustXYZ(P: Polyhedron, notation: string, iterations = 1): Polyhedron
dualNotation := 'd' + notation dualNotation := 'd' + notation
D .= topoDual P D .= topoDual P
if dualNotation in polyCache if dualNotation in polyCache
console.error 'Error: Creating', notation, 'after its dual' console.error 'Creating', notation, '_after_ its dual'
D = polyCache[dualNotation] D = polyCache[dualNotation]
for iter of [1..iterations] for iter of [1..iterations]
D.xyz = reciprocalC P D.xyz = reciprocalC P
@ -320,6 +404,143 @@ function reciprocalC(P: Polyhedron): XYZ[]
for each v of return.value for each v of return.value
scale v, 1/mag2 v 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 // arithmetic on 3-vectors
function accumulate(basket: XYZ, egg: XYZ) function accumulate(basket: XYZ, egg: XYZ)
basket[0] += egg[0] basket[0] += egg[0]
@ -336,24 +557,27 @@ function diminish(basket: XYZ, egg: XYZ)
function copy(a: XYZ) function copy(a: XYZ)
ð a[0], a[1], a[2] ð 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) function mag2(a: XYZ)
a[0]*a[0] + a[1]*a[1] + a[2]*a[2] a[0]*a[0] + a[1]*a[1] + a[2]*a[2]
function mag(a: XYZ) function mag(a: XYZ)
Math.sqrt mag2 a Math.sqrt mag2 a
function normalize(v: XYZ)
scale v, 1/mag v
function unit
function scale(subject: XYZ, by: number) function scale(subject: XYZ, by: number)
subject[0] *= by subject[0] *= by
subject[1] *= by subject[1] *= by
subject[2] *= by subject[2] *= by
subject subject
function lerp(start: XYZ, end: XYZ, howFar: number)
basket := scale copy(start), 1-howFar
accumulate basket, scale copy(end), howFar
// Feedback // Feedback
function inform(x: string) function inform(x: string)

View File

@ -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,6 +43,7 @@ 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
@ -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'
@ -102,6 +105,10 @@ links.after ->
$(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 let conwayBrowser: any
madeConway .= false madeConway .= false
@ -117,6 +124,7 @@ if inputs.length is 1
notation := $('input[name="notation"]').val() notation := $('input[name="notation"]').val()
unless notation then return unless notation then return
vrml := conway.generateVRML notation.toString() vrml := conway.generateVRML notation.toString()
maybeDebug vrml
unless madeConway unless madeConway
{canvas, browser3D} := await makeBrowser '', '250px', '250px' {canvas, browser3D} := await makeBrowser '', '250px', '250px'
conwayBrowser = browser3D conwayBrowser = browser3D