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:
parent
8186038efb
commit
66b24e657b
4 changed files with 359 additions and 29 deletions
|
@ -547,4 +547,5 @@ Separator {
|
|||
}
|
||||
}
|
||||
`
|
||||
console.log convert hartPoly
|
||||
|
||||
console.log convert hartPoly, 'https://www.georgehart.com/virtual-polyhedra/vrml/zonish-10-icosahedron.wrl'
|
||||
|
|
213
src/index.civet
213
src/index.civet
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue