Compare commits

..

No commits in common. "main" and "0.2.4" have entirely different histories.
main ... 0.2.4

4 changed files with 76 additions and 231 deletions

View File

@ -1,28 +1,13 @@
# vrml1to97 # vrml1to97
JavaScript converter from VRML 1.0 to VRML97 file format. JavaScript converter from VRML 1.0 to VRML97 file format, based on Wings 3D
conversion logic.
This converter was originally a JavaScript reimplementation of the algorithm Essentially, this is a JavaScript reimplementation of the algorithm of the
of the "token rearranger" found in the "token rearranger" found in the
[Wings 3D x3d importer](https://github.com/dgud/wings/blob/master/plugins_src/import_export/x3d_import.erl) [Wings 3D x3d importer](https://github.com/dgud/wings/blob/master/plugins_src/import_export/x3d_import.erl)
(which was written in Erlang). (which was written in Erlang).
However, from version 0.3, it adds support for a significantly larger subset of
VRML 1.0. Overall, it recognizes and converts
* Title, SceneInfo, BackgroundColor, and View "Info" nodes [this node type
was removed in VRML97].
* All Light nodes
* PerspectiveCamera nodes (converted into Viewpoint nodes)
* Grouping nodes including Separator, Group, Switch, and WWWAnchor
* Interprets a Switch named "Cameras" (by a DEF) as a list of Viewpoints
* ShapeHints
* Transformation nodes including Translation, Rotation, Scale, and Transform
* All shape nodes including Cube, Cone, Cylinder, Sphere, IndexedFaceSet,
IndexedLineSet, PointSet, Coordinate3, and Normal
* All material nodes including Material, TextureCoordinate2, and Texture2
* DEF and USE constructs to share subtrees
## Usage ## Usage
From an es6 module under Node (for example) From an es6 module under Node (for example)
@ -59,8 +44,8 @@ Currently this package exports just two functions:
actually check that its input is VRML 1, so its behavior is undefined actually check that its input is VRML 1, so its behavior is undefined
if given anything but valid VRML 1 syntax). Returns VRML 97 syntax for if given anything but valid VRML 1 syntax). Returns VRML 97 syntax for
the same scene, as nearly as it can translate. Note that not all of the same scene, as nearly as it can translate. Note that not all of
VRML 1 is recognized (see above for a list of constructs that should VRML 1 is recognized, and some of the translations may be somewhat
be handled), and some of the translations may be somewhat approximate. approximate.
If the optional second argument `source` is supplied, `convert` adds If the optional second argument `source` is supplied, `convert` adds
a comment indicating that the original vrml1 came from the specified a comment indicating that the original vrml1 came from the specified
@ -81,12 +66,10 @@ Currently this package exports just two functions:
## Conversion notes ## Conversion notes
One sort of geometry common to VRML 1 and VRML97 is the IndexedFaceSet. These One sort of geometry common to VRML 1 and VRML97 is the IndexedFaceSet. These
entities are often used to render solids. Indeed, the default in VRML97 is to often used to render solids. Indeed, the default in VRML97 is to assume they
assume they do represent a solid, with the normals pointing outward. Unusual do represent a solid, with the normals pointing outward. Unusual visual effects
visual effects ensue if the normals are not properly directed (basically, you ensue if the normals are not properly directed (basically, you see through the
see through the "front" of the solid and see the "backs" of the faces on the "front" of the solid and see the "backs" of the faces on the opposite side).
opposite side).
As a result, unless the input VRML 1 explicitly includes an explicit As a result, unless the input VRML 1 explicitly includes an explicit
vertexOrdering of CLOCKWISE or COUNTERCLOCKWISE, the default solid treatment vertexOrdering of CLOCKWISE or COUNTERCLOCKWISE, the default solid treatment
will be turned off in the VRML97 output, meaning that all faces will be will be turned off in the VRML97 output, meaning that all faces will be

View File

@ -1,6 +1,6 @@
{ {
name: 'vrml1to97', name: 'vrml1to97',
version: '0.4.0', version: '0.2.4',
description: 'JavaScript converter from VRML 1 to VRML97', description: 'JavaScript converter from VRML 1 to VRML97',
scripts: { scripts: {
test: 'echo "Error: no test specified" && exit 1', test: 'echo "Error: no test specified" && exit 1',
@ -40,7 +40,7 @@
}, },
type: 'module', type: 'module',
devDependencies: { devDependencies: {
'@danielx/civet': '^0.6.72', '@danielx/civet': '^0.6.71',
'@types/moo': '^0.5.9', '@types/moo': '^0.5.9',
'http-server': '^14.1.1', 'http-server': '^14.1.1',
json5: '^2.2.3', json5: '^2.2.3',

View File

@ -11,8 +11,8 @@ dependencies:
devDependencies: devDependencies:
'@danielx/civet': '@danielx/civet':
specifier: ^0.6.72 specifier: ^0.6.71
version: 0.6.72(typescript@5.3.3) version: 0.6.71(typescript@5.3.3)
'@types/moo': '@types/moo':
specifier: ^0.5.9 specifier: ^0.5.9
version: 0.5.9 version: 0.5.9
@ -35,8 +35,8 @@ packages:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
dev: true dev: true
/@danielx/civet@0.6.72(typescript@5.3.3): /@danielx/civet@0.6.71(typescript@5.3.3):
resolution: {integrity: sha512-jumnIbXbdFs0ZiKN62fmD+p8QGi+E0jmtc02dKz9wIIoPkODsa4XXlBrS5BRR5fr3w5d3ah8Vq7gWt+DL9Wa0Q==} resolution: {integrity: sha512-piOoHtJARe6YqRiXN02Ryb+nLU9JwU8TQLknvPwmlaNm6krdKN+X9dM+C9D4LRoAnAySC2wVshR0wf7YDIUV1Q==}
engines: {node: '>=19 || ^18.6.0 || ^16.17.0'} engines: {node: '>=19 || ^18.6.0 || ^16.17.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:

View File

@ -2,7 +2,6 @@ moo from ../deps/moo.js
import type {Lexer, Token} from ../deps/moo.d.ts import type {Lexer, Token} from ../deps/moo.d.ts
type Tree = {[key:string]: (string | Tree)[]} type Tree = {[key:string]: (string | Tree)[]}
type DefTree = {_definitions?: Tree} & Tree
lexer := moo.compile lexer := moo.compile
comment: /#.*?$/ comment: /#.*?$/
@ -87,196 +86,30 @@ function addWorldParameter(name: string, value: string, tree: Tree): void
children.push world children.push world
world.WorldInfo.push(` ${name}`, ` ${value}`) world.WorldInfo.push(` ${name}`, ` ${value}`)
// Current best way to re-use a pattern function parse(stream: Lexer, tree: Tree = {}): Tree
matches := (str: string, pat: RegExp) => pat.test(str) held .= filtered stream // for lookahead
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 while next := filtered stream
unless held then break unless held then break
// May need to know the current definition, but it clears each round
lastDefinition := currentDefinition
currentDefinition = ''
switch data := switch data :=
{nt: next.type, nv: next.value, ht: held.type, hv: held.value} {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'} {ht: 'word', nt: 'obrace'}
switch held.value switch held.value
matches GroupNode /(?:Transform)?Separator|Group|Switch|WWWAnchor/
parent :=
held.value.endsWith('Separator') ? 'Transform' : 'Group'
{children, ...context} := tree {children, ...context} := tree
subTree := parse stream, context subTree := parse stream, context
if newKids := subTree.children if newKids := subTree.children
newChild .= '' addChild `${parent} { children [
if held.value is 'Switch' ${renderList newKids} ] }\n`, tree
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' 'ShapeHints'
subTree := parse stream subTree := parse stream
hints: Tree := {}
if 'vertexOrdering' in subTree if 'vertexOrdering' in subTree
hints.ccw = [ tree.ccw = [
subTree.vertexOrdering[0] is 'ccw' ? 'TRUE' : 'FALSE'] subTree.vertexOrdering[0] is 'ccw' ? 'true' : 'false']
if 'creaseAngle' in subTree if 'creaseAngle' in subTree
hints.creaseAngle = subTree.creaseAngle tree.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' 'Coordinate3'
tree.Coordinate = toksUntilClose stream tree.Coordinate = toksUntilClose stream
'Normal' 'Normal'
@ -307,7 +140,7 @@ function parse(stream: Lexer, tree: DefTree = {}): DefTree
dims := radius: '' dims := radius: ''
findNumbersAtTopLevel stream, dims findNumbersAtTopLevel stream, dims
addShape 'Sphere', [`radius ${dims.radius}`], tree addShape 'Sphere', [`radius ${dims.radius}`], tree
matches SetNode /IndexedFaceSet|IndexedLineSet|PointSet/
isFaces := held.value is 'IndexedFaceSet' isFaces := held.value is 'IndexedFaceSet'
contents := translatedToksUntilClose stream contents := translatedToksUntilClose stream
params := [] params := []
@ -324,34 +157,69 @@ function parse(stream: Lexer, tree: DefTree = {}): DefTree
params.push `creaseAngle ${tree.creaseAngle[0]}` params.push `creaseAngle ${tree.creaseAngle[0]}`
if isFaces if isFaces
if 'ccw' in tree then params.push `ccw ${tree.ccw[0]}` if 'ccw' in tree then params.push `ccw ${tree.ccw[0]}`
else params.push 'solid FALSE' else params.push 'solid false'
params.push ...contents params.push ...contents
addShape held.value, params, tree addShape held.value, params, tree
matches LightNode /Light$/
contents := toksUntilClose stream contents := toksUntilClose stream
addChild {[held.value]: contents}, tree addChild {[held.value]: contents}, tree
else else
parse stream // discard the subgroup parse stream // discard the subgroup
held = filtered stream held = filtered stream
{ht: 'word', hv: 'vertexOrdering', nt: 'word'} {ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'COUNTERCLOCKWISE'}
switch next.value tree.vertexOrdering = ['ccw']
'COUNTERCLOCKWISE' held = filtered stream
tree.vertexOrdering = ['ccw'] {ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'CLOCKWISE'}
'CLOCKWISE' tree.vertexOrdering = ['cw']
tree.vertexOrdering = ['cw']
held = filtered stream held = filtered stream
{ht: 'word', hv: 'creaseAngle', nt: 'number'} {ht: 'word', hv: 'creaseAngle', nt: 'number'}
tree.creaseAngle = [ next.value ] tree.creaseAngle = [ next.value ]
held = filtered stream held = filtered stream
{ht: 'word', hv: 'whichChild'} {ht: 'word', nt: 'word', nv: 'Info'}
tree.whichChoice = [ next.value ] opener := filtered stream
held = filtered stream if opener and opener.type === 'obrace'
contents := toksUntilClose stream
switch held.value
'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
held = filtered stream
else held = opener
else else
console.error 'Ignoring unparseable token', held
held = next // ignore unknown words held = next // ignore unknown words
if not held or held.type === 'cbrace' then break if not held or held.type === 'cbrace' then break
if held and held.type !== 'cbrace' if held and held.type !== 'cbrace'
console.error 'Incomplete parse, symbol at end:', held console.log 'Oddly ended up with held', held
tree tree
function addShape(nodeType: string, params: (string | Tree)[], tree: Tree): void function addShape(nodeType: string, params: (string | Tree)[], tree: Tree): void
@ -365,15 +233,9 @@ function addShape(nodeType: string, params: (string | Tree)[], tree: Tree): void
shape.Shape.push "texture ImageTexture {\n", shape.Shape.push "texture ImageTexture {\n",
...tree.Texture, " }\n" ...tree.Texture, " }\n"
shape.Shape.push " }\n" shape.Shape.push " }\n"
geometry: (string|Tree)[] := [`geometry ${nodeType}`] shape.Shape.push `geometry ${nodeType} {\n`, ...params, " }\n"
if params.length then geometry.push " {\n", ...params, " }\n"
shape.Shape.push ...geometry
addChild shape, tree addChild shape, tree
function mergeTree(subtree: Tree, tree: Tree): void
for key, value in subtree
tree[key] = value
function render(t: string | Tree): string function render(t: string | Tree): string
if typeof t is 'string' then return t if typeof t is 'string' then return t
if 'children' in t then return renderList t.children if 'children' in t then return renderList t.children