feat: Translate VRML 1 to VRML97 (#6)

Resolves #5

Reviewed-on: #6
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
Glen Whitney 2023-09-04 17:31:44 +00:00 committed by Glen Whitney
parent 8186038efb
commit 66b24e657b
4 changed files with 359 additions and 29 deletions

View File

@ -18,7 +18,7 @@
build_etc: 'cp etc/*.js* dist',
build: 'pnpm --sequential /build_/',
try: 'pnpm build && node dist/vrml1to97/example.js',
clean: 'rm -r build dist deps'
clean: 'rm -r build dist deps',
},
keywords: [
'javascript',
@ -36,7 +36,7 @@
vrml1to97: 'dist/vrml1to97.js',
},
devDependencies: {
'@danielx/civet': '^0.6.26',
'@danielx/civet': '^0.6.30',
'@types/moo': '^0.5.6',
'http-server': '^14.1.1',
typescript: '^5.2.2',

View File

@ -11,8 +11,8 @@ dependencies:
devDependencies:
'@danielx/civet':
specifier: ^0.6.26
version: 0.6.26
specifier: ^0.6.30
version: 0.6.30(typescript@5.2.2)
'@types/moo':
specifier: ^0.5.6
version: 0.5.6
@ -32,12 +32,19 @@ packages:
'@jridgewell/trace-mapping': 0.3.9
dev: true
/@danielx/civet@0.6.26:
resolution: {integrity: sha512-YQKANR9Ow3NvzOZAVjTpiniSRWBjIWW8v6KAgVIlrzd8XBbAP1cyeAaWFXzAqOXylzL0map0gyw2tQO5pQq7bw==}
/@danielx/civet@0.6.30(typescript@5.2.2):
resolution: {integrity: sha512-/aZjryo8T9TWVBczbeY8eO8Xmi91KkaKZG6YhCS2lCErmPXxP+zOEUATJiaZBp6AcT2bQEbW+poGyrU6BHL9Dg==}
engines: {node: '>=19 || ^18.6.0 || ^16.17.0'}
hasBin: true
peerDependencies:
typescript: ^4.5 || ^5.0
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@typescript/vfs': 1.5.0
typescript: 5.2.2
unplugin: 1.4.0
transitivePeerDependencies:
- supports-color
dev: true
/@jridgewell/resolve-uri@3.1.1:
@ -60,6 +67,20 @@ packages:
resolution: {integrity: sha512-Q60hZhulhl2Ox4LjbJvhH+HzsKrwzLPjEB8dZw0fK1MH2HyOLe6LDou68yTfsWasxGv7DPZe5VNM5vgpzOa2nw==}
dev: true
/@typescript/vfs@1.5.0:
resolution: {integrity: sha512-AJS307bPgbsZZ9ggCT3wwpg3VbTKMFNHfaY/uF0ahSkYYrPF2dSSKDNIDIQAHm9qJqbLvCsSJH7yN4Vs/CsMMg==}
dependencies:
debug: 4.3.4
transitivePeerDependencies:
- supports-color
dev: true
/acorn@8.10.0:
resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
/ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@ -67,6 +88,14 @@ packages:
color-convert: 2.0.1
dev: true
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
dev: true
/async@2.6.4:
resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==}
dependencies:
@ -80,6 +109,18 @@ packages:
safe-buffer: 5.1.2
dev: true
/binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
dev: true
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
dependencies:
fill-range: 7.0.1
dev: true
/call-bind@1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
@ -95,6 +136,21 @@ packages:
supports-color: 7.2.0
dev: true
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
dependencies:
anymatch: 3.1.3
braces: 3.0.2
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
dev: true
/color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -122,10 +178,29 @@ packages:
ms: 2.1.3
dev: true
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: true
/eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: true
/fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
dependencies:
to-regex-range: 5.0.1
dev: true
/follow-redirects@1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
@ -136,6 +211,14 @@ packages:
optional: true
dev: true
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/function-bind@1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
@ -149,6 +232,13 @@ packages:
has-symbols: 1.0.3
dev: true
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
dependencies:
is-glob: 4.0.3
dev: true
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@ -224,6 +314,30 @@ packages:
safer-buffer: 2.1.2
dev: true
/is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.2.0
dev: true
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
dev: true
/is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
dependencies:
is-extglob: 2.1.1
dev: true
/is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
dev: true
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
@ -249,10 +363,19 @@ packages:
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
dev: false
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
dev: true
/object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
dev: true
@ -262,6 +385,11 @@ packages:
hasBin: true
dev: true
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: true
/portfinder@1.0.32:
resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==}
engines: {node: '>= 0.12.0'}
@ -280,6 +408,13 @@ packages:
side-channel: 1.0.4
dev: true
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
dependencies:
picomatch: 2.3.1
dev: true
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: true
@ -311,6 +446,13 @@ packages:
has-flag: 4.0.0
dev: true
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
dependencies:
is-number: 7.0.0
dev: true
/typescript@5.2.2:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
engines: {node: '>=14.17'}
@ -324,10 +466,28 @@ packages:
qs: 6.11.2
dev: true
/unplugin@1.4.0:
resolution: {integrity: sha512-5x4eIEL6WgbzqGtF9UV8VEC/ehKptPXDS6L2b0mv4FRMkJxRtjaJfOWDd6a8+kYbqsjklix7yWP0N3SUepjXcg==}
dependencies:
acorn: 8.10.0
chokidar: 3.5.3
webpack-sources: 3.2.3
webpack-virtual-modules: 0.5.0
dev: true
/url-join@4.0.1:
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
dev: true
/webpack-sources@3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
engines: {node: '>=10.13.0'}
dev: true
/webpack-virtual-modules@0.5.0:
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
dev: true
/whatwg-encoding@2.0.0:
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
engines: {node: '>=12'}

View File

@ -547,4 +547,5 @@ Separator {
}
}
`
console.log convert hartPoly
console.log convert hartPoly, 'https://www.georgehart.com/virtual-polyhedra/vrml/zonish-10-icosahedron.wrl'

View File

@ -1,6 +1,7 @@
moo from ../deps/moo.js
import type {Lexer, Token} from ../deps/moo.d.ts
type Tree = {[key:string]: string | Tree}
type Tree = {[key:string]: (string | Tree)[]}
lexer := moo.compile
comment: /#.*?$/
@ -20,27 +21,195 @@ lexer := moo.compile
oparen: '('
cparen: ')'
export function tree97(vrml1: string): Tree
tree: Tree := {}
for tok, index of lexer.reset vrml1
if tok.type and tok.type !== 'whitespace' and tok.type !== 'comment'
tree[`${index}`] = {tok.type, tok.value}
tree
export function tree97(vrml1: string)
parse lexer.reset vrml1
function render(t: string | Tree): string
if typeof t is 'string'
return t
result .= ''
for item of Object.values t
if typeof item is 'string' then result += item + ' '
else
typ := item.type
result += render(typ)
if typeof typ === 'string' and
['cbrace', 'cbracket', 'cparen'].includes(typ)
result += "\n"
else result += ' '
function filtered(stream: Lexer): Token | undefined
result .= stream.next()
while result and (result.type === 'whitespace' or result.type === 'comment')
result = stream.next()
result
export function convert(vrml1: string): string
render tree97 vrml1
function toksUntilClose(stream: Lexer): Tree[]
while tok := filtered stream
if tok.type == 'cbrace' then break
{type: [tok.type ?? ''], value: [tok.value]}
function sum(arr: number[]): number
return .= 0
return.value += n for each n of arr
function translatedToksUntilClose(stream: Lexer): (string | Tree)[]
while tok := filtered stream
if tok.type === 'cbrace' then break
if tok.type === 'word' and tok.value === 'filename'
{type: ['word'], value: ['url']}
else if tok.type === 'word' and tok.value === 'textureCoordIndex'
{type: ['word'], value: ['texCoordIndex']}
else if tok.type === 'word' and tok.value === 'ambientColor'
rgb := [filtered(stream), filtered(stream), filtered(stream)]
if rgb.every((&))
value := sum(rgb.map((t) => parseFloat(t!.value))) / 3
`ambientIntensity ${value}`
else continue
else {type: [tok.type ?? ''], value: [tok.value]}
function findNumbersAtTopLevel(
stream: Lexer, fields: Record<string,string>): void
// modifies fields by side effect
depth .= 1
selecting .= ''
while depth > 0
tok := filtered stream
unless tok then break
selector .= ''
switch tok
{type: 'cbrace'} depth -= 1
{type: 'word'} if depth === 1 and tok.value in fields
selector = tok.value
{tye: 'number'} if selecting then fields[selecting] = tok.value
{type: 'obrace'} depth += 1
selecting = selector
function addChild(child: string | Tree, tree: Tree): void
(tree.children ??= []).push child
function parse(stream: Lexer, tree: Tree = {}): Tree
held .= filtered stream // for lookahead
while next := filtered stream
unless held then break
switch data :=
{nt: next.type, nv: next.value, ht: held.type, hv: held.value}
{ht: 'word', nt: 'obrace'}
switch held.value
/(?:Transform)?Separator|Group|Switch|WWWAnchor/
parent :=
held.value.endsWith('Separator') ? 'Transform' : 'Group'
{children, ...context} := tree
subTree := parse stream, context
if newKids := subTree.children
addChild `${parent} { children [
${renderList newKids} ] }\n`, tree
'ShapeHints'
subTree := parse stream
if 'vertexOrdering' in subTree then tree.ccw = ['1.0']
'Coordinate3'
tree.Coordinate = toksUntilClose stream
'Normal'
tree.Normal = toksUntilClose stream
'TextureCoordinate2'
tree.TextureCoordinate = toksUntilClose stream
'Texture2'
tree.Texture = translatedToksUntilClose stream
'Material'
tree.Material = translatedToksUntilClose stream
'Cube'
dims := width: '', height: '', depth: ''
findNumbersAtTopLevel stream, dims
params := [`size ${dims.width} ${dims.height} ${dims.depth}`]
addShape 'Box', params, tree
'Cone'
dims := bottomRadius: '', height: ''
findNumbersAtTopLevel stream, dims
params := [`bottomRadius ${dims.bottomRadius}`,
`height ${dims.height}`]
addShape 'Cone', params, tree
'Cylinder'
dims := radius: '', height: ''
findNumbersAtTopLevel stream, dims
params := [`radius ${dims.radius} height ${dims.height}`]
addShape 'Cylinder', params, tree
'Sphere'
dims := radius: ''
findNumbersAtTopLevel stream, dims
addShape 'Sphere', [`radius ${dims.radius}`], tree
/IndexedLineSet|PointSet/ // ignored
findNumbersAtTopLevel stream, {}
'IndexedFaceSet'
contents := translatedToksUntilClose stream
params := []
if 'Coordinate' in tree
params.push "coord Coordinate {\n",
...tree.Coordinate, " }\n"
if 'Normal' in tree
params.push "normal Normal {\n",
...tree.Normal, " }\n"
if 'TextureCoordinate' in tree
params.push "texCoord TextureCoordinate {\n",
...tree.TextureCoordinate, " }\n"
params.push ...contents
addShape 'IndexedFaceSet', params, tree
else
parse stream // discard the subgroup
held = filtered stream
if not held or held.type === 'cbrace' then break
{ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'COUNTERCLOCKWISE'}
tree.vertexOrdering = ['ccw']
held = filtered stream
if not held or held.type === 'cbrace' then break
else
held = next // ignore unknown words
if not held or held.type === 'cbrace' then break
if held and held.type !== 'cbrace'
console.log 'Oddly ended up with held', held
tree
function addShape(nodeType: string, params: (string | Tree)[], tree: Tree): void
shape: Tree := {Shape: []}
if 'Texture' in tree or 'Material' in tree
shape.Shape.push "appearance Appearance {\n"
if 'Material' in tree
shape.Shape.push "material Material {\n",
...tree.Material, " }\n"
if 'Texture' in tree
shape.Shape.push "texture ImageTexture {\n",
...tree.Texture, " }\n"
shape.Shape.push " }\n"
shape.Shape.push `geometry ${nodeType} {\n`, ...params, " }\n"
addChild shape, tree
function render(t: string | Tree): string
if typeof t is 'string' then return t
if 'children' in t then return renderList t.children
if 'type' in t and 'value' in t
val := renderList t.value
return switch t.type[0]
/string|number|word/ ` ${val}`
/comma|oparen|cparen/ val
/obrace|cbrace|obracket|cbracket/ ` ${val}\n`
else `\nUNKNOWN TYPE ${t.type}\n\n`
result .= ''
for prop in t
result += `${prop} {\n ${renderList t[prop]} }\n`
return result
function renderList(l: (string | Tree)[]): string
return .= ''
neg1triggersComma .= false
commaNewlineOnce .= false
commaTriggersNewline .= false
for each item of l
return.value += render item
switch item
{type: ['word'], value: ['point']}
commaTriggersNewline = true
{type: ['word'], value: ['coordIndex']}
neg1triggersComma = true
{type: ['number'], value: ['-1']}
if neg1triggersComma then commaNewlineOnce = true
{type: ['comma']}
if commaTriggersNewline or commaNewlineOnce
return.value += "\n"
commaNewlineOnce = false
{type: ['cbracket']}
neg1triggersComma = false
commaNewlineOnce = false
commaTriggersNewline = false
export function convert(vrml1: string, source: string = ''): string
return .= '#VRML V2.0 utf8\n'
if source
return.value += `# Converted by npm vrml1to97 from ${source}\n`
return.value += "\n"
return.value += render tree97 vrml1