feat: Conway notation page working
This commit is contained in:
parent
af2d9e02c7
commit
cbe44238a1
@ -7,6 +7,11 @@
|
||||
|
||||
<body>
|
||||
<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>
|
||||
Trace the following to the JavaScript console: <br/>
|
||||
<label for="commands">Commands executed</label>
|
||||
|
@ -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<Record<FlagType, boolean>>
|
||||
|
||||
|
266
src/conway.civet
266
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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user