424 lines
18 KiB
Plaintext
424 lines
18 KiB
Plaintext
moo from ../deps/moo.js
|
|
import type {Lexer, Token} from ../deps/moo.d.ts
|
|
|
|
type Tree = {[key:string]: (string | Tree)[]}
|
|
type DefTree = {_definitions?: Tree} & Tree
|
|
|
|
lexer := moo.compile
|
|
comment: /#.*?$/
|
|
whitespace:
|
|
match: /\s+/
|
|
lineBreaks: true
|
|
string:
|
|
match: /"(?:\\"|[^"])*"/
|
|
lineBreaks: true
|
|
number: /[\d.-][\d.x]*/
|
|
word: /[^\d.-\W]\w*/
|
|
comma: ','
|
|
obrace: '{'
|
|
cbrace: '}'
|
|
obracket: '['
|
|
cbracket: ']'
|
|
oparen: '('
|
|
cparen: ')'
|
|
|
|
export function tree97(vrml1: string)
|
|
parse lexer.reset vrml1
|
|
|
|
function filtered(stream: Lexer): Token | undefined
|
|
result .= stream.next()
|
|
while result and (result.type === 'whitespace' or result.type === 'comment')
|
|
result = stream.next()
|
|
result
|
|
|
|
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
|
|
{type: '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
|
|
|
|
type WorldInfoNode = {WorldInfo: (string|Tree)[]} | undefined
|
|
function addWorldParameter(name: string, value: string, tree: Tree): void
|
|
children := tree.children ??= []
|
|
world .= (children.find (x) =>
|
|
if x <? 'object'
|
|
'WorldInfo' in x
|
|
else false) as WorldInfoNode
|
|
unless world
|
|
world = WorldInfo: []
|
|
children.push world
|
|
world.WorldInfo.push(` ${name}`, ` ${value}`)
|
|
|
|
// Current best way to re-use a pattern
|
|
matches := (str: string, pat: RegExp) => pat.test(str)
|
|
operator matches
|
|
GroupNode := /(?:Transform)?Separator|Group|Switch|WWWAnchor/
|
|
TransformNode := /^(?:Rotation|Scale|Transform|Translation)$/
|
|
SetNode := /IndexedFaceSet|IndexedLineSet|PointSet/
|
|
LightNode := /Light$/
|
|
|
|
function parse(stream: Lexer, tree: DefTree = {}): DefTree
|
|
held .= filtered stream // for lookahead
|
|
currentDefinition .= '' // if we have just started a DEF
|
|
|
|
while next := filtered stream
|
|
unless held then break
|
|
// May need to know the current definition, but it clears each round
|
|
lastDefinition := currentDefinition
|
|
currentDefinition = ''
|
|
|
|
switch data :=
|
|
{nt: next.type, nv: next.value, ht: held.type, hv: held.value}
|
|
{ht: 'word', hv: 'Info'}
|
|
// Here the meaning is entirely dependent on what DEF we are in
|
|
if next.type !== 'obrace'
|
|
// Not sure what this construct is, so ignore it
|
|
held = next
|
|
continue
|
|
contents := toksUntilClose stream
|
|
held = filtered stream
|
|
switch lastDefinition
|
|
'BackgroundColor'
|
|
// Find the first string token and assume it is the color
|
|
stok := contents.find .type[0] === 'string'
|
|
colorString := stok?.value[0]
|
|
if colorString and typeof colorString === 'string'
|
|
color := colorString.replace /^"|"$/g, ''
|
|
addChild `Background {
|
|
groundColor [ ${color} ]
|
|
skyColor [ ${color} ]
|
|
}\n`, tree
|
|
'Title'
|
|
// Find the first string token and assume it is the title
|
|
stok := contents.find .type[0] === 'string'
|
|
titleString := stok?.value[0]
|
|
if titleString and typeof titleString === 'string'
|
|
addWorldParameter 'title', titleString, tree
|
|
'SceneInfo'
|
|
// Filter all the strings and add them as info
|
|
info := contents
|
|
|> .filter .type[0] === 'string'
|
|
|> .map .value[0]
|
|
|> .join ' '
|
|
addWorldParameter 'info', `[ ${info} ]`, tree
|
|
'Viewer'
|
|
// Filter all of the strings, and translate them into
|
|
// current viewer names
|
|
tr: Record<string, string> := {'"examiner"': '"EXAMINE"'}
|
|
viewers := contents
|
|
|> .filter .type[0] === 'string'
|
|
|> .map .value[0] as string
|
|
|> .map((x) => x in tr ? tr[x] : x.toUpperCase())
|
|
|> .join ' '
|
|
addChild `NavigationInfo { type [ ${viewers} ] }\n`, tree
|
|
{ht: 'word', hv: 'DEF'}
|
|
unless next.type is 'word'
|
|
// Don't understand this construction, ignore the DEF
|
|
held = next
|
|
continue
|
|
currentDefinition = next.value
|
|
held = filtered stream
|
|
// Special case from VRML 1: DEF Cameras Switch { ... }
|
|
// needs to be hoisted to a list of viewpoints at this level
|
|
if currentDefinition is 'Cameras' and held?.value is 'Switch'
|
|
held = filtered stream
|
|
unless held?.type is 'obrace'
|
|
console.error
|
|
`DEF Cameras Switch followed by ${held?.value}, ignoring`
|
|
continue
|
|
{children, ...context} := tree
|
|
subTree := parse stream, context
|
|
addChild renderList(subTree.children), tree
|
|
held = filtered stream
|
|
continue
|
|
if held?.type is 'word'
|
|
clause := `DEF ${currentDefinition}`
|
|
role .= held.value
|
|
switch role
|
|
matches GroupNode
|
|
role = 'Child'
|
|
matches TransformNode
|
|
role = 'Transform'
|
|
'Coordinate3'
|
|
role = 'Coordinate'
|
|
/Cube|Cone|Cylinder|Sphere/
|
|
role = 'Shape'
|
|
'PerspectiveCamera'
|
|
role = 'Viewpoint'
|
|
'ShapeHints'
|
|
role = 'Dummy' // will fill in when we parse the ShapeHints
|
|
'Texture2'
|
|
role = 'Texture'
|
|
'TextureCoordinate2'
|
|
role = 'TextureCoordinate'
|
|
matches SetNode
|
|
role = 'Shape'
|
|
matches LightNode
|
|
role = 'Child'
|
|
(tree._definitions ??= {})[currentDefinition] = [role]
|
|
unless role is 'Info'
|
|
// Info nodes don't directly generate a node in translation
|
|
addChild clause, tree
|
|
{ht: 'word', hv: 'USE'}
|
|
unless next.type is 'word'
|
|
// Don't understand this construction, ignore the USE
|
|
held = next
|
|
continue
|
|
known := tree._definitions
|
|
if known and next.value in known
|
|
role := known[next.value][0]
|
|
clause := `USE ${next.value}`
|
|
switch role
|
|
'Child'
|
|
addChild clause, tree
|
|
'Shape'
|
|
addShape clause, [], tree
|
|
'Transform'
|
|
// VRML97 doesn't allow just the transform part of
|
|
// a Transform to be USEd (the whole node must be), so
|
|
// we have to recreate the transform. FIXME: dedupe code
|
|
content := known[next.value][1..]
|
|
{children, ...context} := tree
|
|
restOfTree := parse stream, context
|
|
if remainingKids := restOfTree.children
|
|
newChild := `Transform {\n ${renderList content}\n `
|
|
+ `children [\n ${renderList remainingKids}`
|
|
+ "] }\n"
|
|
addChild newChild, tree
|
|
return tree
|
|
{}
|
|
mergeTree role, tree
|
|
else
|
|
tree[role] = [clause]
|
|
else
|
|
console.error 'USE of unDEFined identifier', next.value
|
|
held = filtered stream
|
|
{ht: 'word', nt: 'obrace'}
|
|
switch held.value
|
|
matches GroupNode
|
|
{children, ...context} := tree
|
|
subTree := parse stream, context
|
|
if newKids := subTree.children
|
|
newChild .= ''
|
|
if held.value is 'Switch'
|
|
newChild = "Switch {\n "
|
|
if 'whichChoice' in subTree
|
|
newChild += `whichChoice ${subTree.whichChoice[0]}\n `
|
|
newChild += `choice [\n ${renderList newKids} ] }\n`
|
|
else newChild =
|
|
`Group { children [\n ${renderList newKids} ] }\n`
|
|
addChild newChild, tree
|
|
matches TransformNode
|
|
content := toksUntilClose stream
|
|
if (lastDefinition
|
|
and tree._definitions?[lastDefinition][0] is 'Transform')
|
|
tree._definitions[lastDefinition].push ...content
|
|
{children, ...context} := tree
|
|
restOfTree := parse stream, context
|
|
if remainingKids := restOfTree.children
|
|
newChild := `Transform {\n ${renderList content}\n `
|
|
+ `children [\n ${renderList remainingKids} ] }\n`
|
|
addChild newChild, tree
|
|
return tree // used the rest of the tree, so done
|
|
'ShapeHints'
|
|
subTree := parse stream
|
|
hints: Tree := {}
|
|
if 'vertexOrdering' in subTree
|
|
hints.ccw = [
|
|
subTree.vertexOrdering[0] is 'ccw' ? 'TRUE' : 'FALSE']
|
|
if 'creaseAngle' in subTree
|
|
hints.creaseAngle = subTree.creaseAngle
|
|
mergeTree hints, tree
|
|
if lastDefinition and tree._definitions
|
|
tree._definitions[lastDefinition] = [hints]
|
|
'PerspectiveCamera'
|
|
contents := toksUntilClose stream
|
|
hasDescription := contents.find (e) =>
|
|
e.type?[0] is 'word' and e.value?[0] is 'description'
|
|
if lastDefinition and hasDescription is undefined
|
|
contents.push type: ['word'], value: ['description']
|
|
contents.push type: ['word'], value: [`"${lastDefinition}"`]
|
|
addChild {Viewpoint: contents}, tree
|
|
'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
|
|
matches SetNode
|
|
isFaces := held.value is '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 isFaces and 'TextureCoordinate' in tree
|
|
params.push "texCoord TextureCoordinate {\n",
|
|
...tree.TextureCoordinate, " }\n"
|
|
if isFaces and 'creaseAngle' in tree
|
|
params.push `creaseAngle ${tree.creaseAngle[0]}`
|
|
if isFaces
|
|
if 'ccw' in tree then params.push `ccw ${tree.ccw[0]}`
|
|
else params.push 'solid FALSE'
|
|
params.push ...contents
|
|
addShape held.value, params, tree
|
|
matches LightNode
|
|
contents := toksUntilClose stream
|
|
addChild {[held.value]: contents}, tree
|
|
else
|
|
parse stream // discard the subgroup
|
|
held = filtered stream
|
|
{ht: 'word', hv: 'vertexOrdering', nt: 'word'}
|
|
switch next.value
|
|
'COUNTERCLOCKWISE'
|
|
tree.vertexOrdering = ['ccw']
|
|
'CLOCKWISE'
|
|
tree.vertexOrdering = ['cw']
|
|
held = filtered stream
|
|
{ht: 'word', hv: 'creaseAngle', nt: 'number'}
|
|
tree.creaseAngle = [ next.value ]
|
|
held = filtered stream
|
|
{ht: 'word', hv: 'whichChild'}
|
|
tree.whichChoice = [ next.value ]
|
|
held = filtered stream
|
|
else
|
|
console.error 'Ignoring unparseable token', held
|
|
held = next // ignore unknown words
|
|
if not held or held.type === 'cbrace' then break
|
|
if held and held.type !== 'cbrace'
|
|
console.error 'Incomplete parse, symbol at end:', 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"
|
|
geometry: (string|Tree)[] := [`geometry ${nodeType}`]
|
|
if params.length then geometry.push " {\n", ...params, " }\n"
|
|
shape.Shape.push ...geometry
|
|
addChild shape, tree
|
|
|
|
function mergeTree(subtree: Tree, tree: Tree): void
|
|
for key, value in subtree
|
|
tree[key] = value
|
|
|
|
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
|
|
next := render item
|
|
if return.value and not ',()'.includes next[0]
|
|
return.value += ' '
|
|
return.value += next
|
|
switch item
|
|
{type: ['word'], value: ['point']}
|
|
commaTriggersNewline = true
|
|
{type: ['word'], value: [/Index$/]}
|
|
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
|