vrml1to97/src/index.civet

371 lines
15 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/
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
if held?.type is 'word'
clause := `DEF ${currentDefinition}`
role .= held.value
switch role
matches GroupNode
role = 'Child'
'ShapeHints'
role = 'Dummy' // will fill in when we parse the ShapeHints
'Coordinate3'
role = 'Coordinate'
'TextureCoordinate2'
role = 'TextureCoordinate'
'Texture2'
role = 'Texture'
/Cube|Cone|Cylinder|Sphere/
role = 'Shape'
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
{}
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
parent :=
held.value.endsWith('Separator') ? 'Transform' : 'Group'
{children, ...context} := tree
subTree := parse stream, context
if newKids := subTree.children
newChild .= `${parent} {\n `
if 'Rotation' in subTree
newChild += renderList subTree.Rotation
newChild += "\n "
newChild += `children [\n ${renderList newKids} ] }\n`
addChild newChild, tree
'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]
'Rotation'
tree.Rotation = toksUntilClose stream
'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
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