From cbe44238a181bca70465fca780dfe4d183b3f459 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 18 Feb 2024 01:30:14 -0800 Subject: [PATCH] feat: Conway notation page working --- etc/options.html | 5 + src/adapptypes.civet | 4 +- src/conway.civet | 266 +++++++++++++++++++++++++++++++++++++++---- src/giveAwrl.civet | 10 +- 4 files changed, 262 insertions(+), 23 deletions(-) diff --git a/etc/options.html b/etc/options.html index b512c0e..af3606f 100644 --- a/etc/options.html +++ b/etc/options.html @@ -7,6 +7,11 @@

Debugging

+

Embedded VRML/X3D display

+ Write to the JavaScript console:
+ + +

Java Geometry Applets

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