283 lines
9.8 KiB
Plaintext
283 lines
9.8 KiB
Plaintext
type GenVRML = vrml?: string, err?: string
|
|
|
|
function gcdpos(a: number, b: number)
|
|
[a, b] = [b, a%b] while b >= 1
|
|
a
|
|
|
|
shapeNamesRaw :=
|
|
```
|
|
prism|dipyramid|compound: prism + dipyramid
|
|
antiprism|trapezohedron|compound: antiprism + trapezohedron
|
|
crossed antiprism|concave trapezohedron
|
|
compound: crossed antiprism + concave trapezohedron|polygon
|
|
```
|
|
shapeName := shapeNamesRaw.split /[|\n]/
|
|
|
|
export function generateVRML(n: number, d: number, chk: boolean[])
|
|
unless chk[6..8].every !& or 2 < n/d < 3
|
|
return err: 'Crossed antiprisms require 2 < numerator/denominator < 3, but '
|
|
+ `it is ${n}/${d} = ${n/d}`
|
|
unless n > 0 and d > 0 return err: 'Numerator and denominator should be positive'
|
|
g := gcdpos n,d
|
|
n /= g
|
|
d /= g
|
|
if n < 3
|
|
return
|
|
err: `Numerator in lowest terms (currently ${n}) needs to be at least 3.`
|
|
unless 1 <= d < n
|
|
return err: 'Denominator should be strictly between 1 and numerator.'
|
|
if d > n/2 then d = n-d // equivalent
|
|
which := chk.findIndex (&)
|
|
chk[which] = false
|
|
if chk.some (&) return err: 'Currently only one shape option may be checked.'
|
|
name .= d > 1 ? `${n}/${d}-gonal` :
|
|
switch n
|
|
when 3: 'triangular'
|
|
when 4: 'square'
|
|
when 5: 'pentagonal'
|
|
when 6: 'hexagonal'
|
|
when 7: 'heptagonal'
|
|
when 8: 'octagonal'
|
|
else `${n}-gonal`
|
|
name += ' ' + shapeName[which]
|
|
{vrml: header(name) + vrmlBody which, n, d}
|
|
|
|
function header(name: string)
|
|
```
|
|
#VRML V2.0 utf8
|
|
WorldInfo {
|
|
title "${name}."
|
|
info [
|
|
"Generated by GTW's reimplementation of GWH's prism generator"
|
|
"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 [ 0.2 0.5 0.9 ]
|
|
skyColor [ 0.2 0.5 0.9 ] }
|
|
NavigationInfo { type [ "EXAMINE" ] }
|
|
DirectionalLight { direction -.5 -1 1 intensity 0.75 }
|
|
DirectionalLight { direction .5 1 -1 intensity 0.75 }
|
|
DEF General Viewpoint {
|
|
position 6 0 6 orientation 0 1 0 0.78 description "General" }
|
|
DEF Face Viewpoint {
|
|
position 0 -6 0 orientation 1 0 0 1.57 description "Face" }
|
|
DEF Top Viewpoint { position 0 0 8 description "Top" }
|
|
DEF Edge Viewpoint {
|
|
position 7 0 0 orientation 0 1 0 1.57 description "Edge" }
|
|
DEF Inside Viewpoint {
|
|
position 0 0 0 orientation 1 0 0 -1.57 description "Inside" }
|
|
DEF Far Viewpoint {
|
|
position -25 0 25 orientation 0 1 0 -0.78 description "Far" }
|
|
```
|
|
|
|
// Useful constants
|
|
tau := 2*Math.PI
|
|
|
|
// Array "postscript" operator
|
|
operator ps<T>(main: T[], extra: T): T[]
|
|
main.push extra
|
|
main
|
|
// Array "preface" operator
|
|
operator pf<T>(extra: T, main: T[]): T[]
|
|
main.unshift extra
|
|
main
|
|
|
|
function shIx(name: string): number
|
|
shapeName.findIndex((sn) => name is in sn)
|
|
|
|
type ShapeMethod = (n: number, theta: number, d?: number) => string
|
|
shapeMethod: ShapeMethod[] := []
|
|
shapeMethods: Record<string, ShapeMethod> :=
|
|
prism: (n, theta) =>
|
|
name := 'Prism'
|
|
h := Math.sin theta/2
|
|
|
|
coords := orb(1, n, h, theta) ++ orb(1, n, -h, theta) ps [0,0,h] ps [0,0,-h]
|
|
top := ([i, (i+1)%n, 2*n] for i of [0...n])
|
|
bottom := ([(i+1)%n + n, i+n, 2*n+1] for i of [0...n])
|
|
vrml := firstFaces name, '0.8 0.7 0.5', coords, top ++ bottom
|
|
|
|
sides := ([i, (i+1)%n, (i+1)%n + n, i+n] for i of [0...n])
|
|
laterFaces vrml, name, '0.3 0.5 0.9', sides
|
|
laterLines vrml, name, sides
|
|
emitObject vrml
|
|
|
|
dipyr: (n, theta) =>
|
|
name := 'Dipyramid'
|
|
c := 1/Math.cos theta/2
|
|
d := 1/Math.sin theta/2
|
|
|
|
coords := [0,0,d] pf orb(c, n, 0, theta, 0.5) ps [0,0,-d]
|
|
cap := ([0, i, i%n + 1] for i of [1..n])
|
|
cup := ([i%n + 1, i, n+1] for i of [1..n])
|
|
vrml := firstFaces name, '0.9 0.3 0.3', coords, cap++cup
|
|
|
|
edges := [1..n] pf ([0, i, n+1] for i of [1..n])
|
|
laterLines vrml, name, edges
|
|
emitObject vrml
|
|
|
|
compound: (n, theta) =>
|
|
shapeMethod[shIx 'prism'](n, theta) + shapeMethod[shIx 'dipyr'](n, theta)
|
|
|
|
antip: (n, theta) =>
|
|
name := 'Antiprism'
|
|
{h, r} := antiprismDims theta
|
|
|
|
coords := (orb(r, n, h, theta) ++ orb(r, n, -h, theta, 0.5)
|
|
ps [0,0,h] ps [0,0,-h])
|
|
top := ([i, 2*n, (i+1)%n] for i of [0...n])
|
|
bottom := ([(i+1)%n + n, 2*n + 1, i+n] for i of [0...n])
|
|
vrml := firstFaces name, '0.8 0.7 0.5', coords, top ++ bottom
|
|
|
|
topsides := ([(i+1)%n, i+n, i] for i of [0...n])
|
|
botsides := ([i, i+n, (n+i-1)%n + n] for i of [0...n])
|
|
laterFaces vrml, name, '0.9 0.3 0.4', topsides ++ botsides
|
|
laterLines vrml, name, [[0...n] ps 0, [n...2*n] ps n, ...topsides]
|
|
emitObject vrml
|
|
|
|
trap: (n, theta) =>
|
|
name := 'Trapezohedron'
|
|
{h, r} := antiprismDims theta
|
|
[t, z] := dualPoint r*Math.cos(theta/2), h, r, -h
|
|
|
|
botzig := orb(t, n, -z, theta) ps [0, 0, -1/h]
|
|
topzig := orb(t, n, z, theta, 0.5) ps [0, 0, 1/h]
|
|
coords := alternate botzig, topzig
|
|
faces :=
|
|
for i of [0...2*n]
|
|
if i%2 then [2*n + 1, (i+2)%(2*n), (i+1)%(2*n), i]
|
|
else [2*n, i, i+1, (i+2)%(2*n)]
|
|
vrml := firstFaces name, '0.3 0.5 0.9', coords, faces
|
|
|
|
edges :=
|
|
for i of [0...2*n]
|
|
i%2 ? [2*n + 1, i, (i + 1)%(2*n)] : [2*n, i, i+1]
|
|
laterLines vrml, name, edges
|
|
emitObject vrml
|
|
|
|
'antiprism + trap': (n, theta) =>
|
|
shapeMethod[shIx 'antip'](n, theta) + shapeMethod[shIx 'trap'](n, theta)
|
|
|
|
crossed: (n, theta) => // should really unify with antiprism, code is so close
|
|
name := 'CrossedAntiprism'
|
|
{h, r} := crossedDims theta
|
|
|
|
coords := (orb(r, n, h, theta) ++ orb(-r, n, -h, theta, 0.5)
|
|
ps [0,0,h] ps [0,0,-h])
|
|
top := ([i, (i+1)%n, 2*n] for i of [0...n])
|
|
bottom := ([(i+1)%n + n, i+n, 2*n + 1] for i of [0...n])
|
|
vrml := firstFaces name, '0.8 0.7 0.5', coords, top ++ bottom
|
|
|
|
topsides := ([(i+1)%n, i+n, i] for i of [0...n])
|
|
botsides := ([i, i+n, (n+i-1)%n + n] for i of [0...n])
|
|
laterFaces vrml, name, '0.9 0.3 0.4', topsides ++ botsides
|
|
laterLines vrml, name, [[0...n] ps 0, [n...2*n] ps n, ...topsides]
|
|
emitObject vrml
|
|
|
|
concave: (n, theta, d) =>
|
|
name := 'ConcaveTrapezohedron'
|
|
{h, r} := crossedDims theta
|
|
[t, z] := dualPoint r*Math.cos(theta/2), h, -r, -h
|
|
d ??= Math.floor (n-1)/2 // won't be needed, for TypeScript's sake
|
|
eta := tau * (n-d)/n
|
|
|
|
botzig := orb(-t, n, -z, eta) ps [0, 0, -1/h]
|
|
topzig := orb(-t, n, z, eta, 0.5) ps [0, 0, 1/h]
|
|
coords := alternate botzig, topzig
|
|
halfFaces :=
|
|
for i of [0...2*n]
|
|
i%2 ? [(i+1)%(2*n), i, 2*n + 1] : [2*n, i, i+1]
|
|
otherFaces :=
|
|
for i of [0...2*n]
|
|
i%2 ? [2*n + 1, (i+2)%(2*n), (i+1)%(2*n)] : [2*n, i+1, (i+2)%(2*n)]
|
|
vrml := firstFaces name, '0.3 0.5 0.9', coords, halfFaces ++ otherFaces
|
|
laterLines vrml, name, halfFaces
|
|
emitObject vrml
|
|
|
|
'prism + concave': (n, theta, d) =>
|
|
shapeMethod[shIx 'cross'](n, theta) + shapeMethod[shIx 'conc'](n, theta, d)
|
|
|
|
polygon: (n, theta) =>
|
|
name := 'Polygon'
|
|
coords := orb(1, n, 0, theta) ps [0, 0, 0]
|
|
faces := ([i, (i+1)%n, n] for i of [0...n])
|
|
vrml := firstFaces name, '0.5 0.8 0.5', coords, faces
|
|
laterLines vrml, name, [[0...n] ps 0]
|
|
emitObject vrml
|
|
|
|
for key, method in shapeMethods
|
|
shapeMethod[shIx key] = method
|
|
|
|
function antiprismDims(theta: number)
|
|
h .= Math.sqrt 1 - 4/(4 + 2*Math.cos(theta/2) - 2*Math.cos(theta))
|
|
r .= Math.sqrt 1 - h*h
|
|
f := Math.sqrt h*h + (r*Math.cos theta/2)**2
|
|
h /= f
|
|
r /= f
|
|
{h, r}
|
|
|
|
function crossedDims(theta: number)
|
|
r .= 2/Math.sqrt 4 - 2*Math.cos(theta/2) - 2*Math.cos(theta)
|
|
h .= Math.sqrt 1 - r*r
|
|
f .= Math.sqrt h*h + (r*Math.cos theta/2)**2
|
|
r /= f
|
|
h /= f
|
|
{h, r}
|
|
|
|
function dualPoint(x1: number, y1: number, x2: number, y2: number)
|
|
len := Math.sqrt (x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)
|
|
vx := (y2 - y1)/len
|
|
vy := (x1 - x2)/len
|
|
d := vx*x1 + vy*y1
|
|
[vx/d, vy/d]
|
|
|
|
function vrmlBody(which: number, n: number, d: number)
|
|
theta := tau * d/n
|
|
shapeMethod[which](n, theta, d)
|
|
|
|
function orb(r: number, n: number, height: number, theta: number, t=0)
|
|
rho := t*theta
|
|
for i of [0...n]
|
|
[r*Math.cos(rho+i*theta), r*Math.sin(rho+i*theta), height]
|
|
|
|
function alternate<T>(a: T[], b: T[])
|
|
return: T[] := []
|
|
return.value.push n, b[i] for each n, i of a
|
|
|
|
function firstFaces(name: string, color: string,
|
|
coords: number[][], faces: number[][])
|
|
return: string[] := []
|
|
startShape return.value, color
|
|
return.value.push 'geometry IndexedFaceSet {',
|
|
`coord DEF ${name}Coords Coordinate { point [`
|
|
return.value.push coord.join(' ') + ',' for each coord of coords
|
|
return.value.push '] }',
|
|
'creaseAngle 0 solid FALSE coordIndex ['
|
|
return.value.push face.join(', ') + ', -1,' for each face of faces
|
|
return.value.push '] } }'
|
|
|
|
function startShape(container: string[], color: string, colType = 'diffuse'): void
|
|
container.push 'Shape { appearance Appearance {',
|
|
' material Material {',
|
|
` ${colType}Color ${color} } }`
|
|
|
|
function laterFaces(container: string[], name: string, color: string,
|
|
faces: number[][]): void
|
|
startShape container, color
|
|
container.push 'geometry IndexedFaceSet {',
|
|
`coord USE ${name}Coords`,
|
|
'creaseAngle 0 solid FALSE coordIndex ['
|
|
container.push face.join(', ') + ', -1,' for each face of faces
|
|
container.push '] } }'
|
|
|
|
function laterLines(container: string[], name: string, edges: number[][]): void
|
|
startShape container, '0 0 0', 'emissive' // per VRML97 standard
|
|
container.push 'geometry IndexedLineSet {',
|
|
`coord USE ${name}Coords`,
|
|
'coordIndex ['
|
|
container.push edge.join(', ') + ', -1,' for each edge of edges
|
|
container.push '] } }'
|
|
|
|
function emitObject(container: string[]) container.join "\n"
|