feat: support DEF and USE syntax to share subtrees (#18)
Reviewed-on: #18 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
1a2077a6b6
commit
1409d39665
35
README.md
35
README.md
@ -1,13 +1,26 @@
|
|||||||
# vrml1to97
|
# vrml1to97
|
||||||
|
|
||||||
JavaScript converter from VRML 1.0 to VRML97 file format, based on Wings 3D
|
JavaScript converter from VRML 1.0 to VRML97 file format.
|
||||||
conversion logic.
|
|
||||||
|
|
||||||
Essentially, this is a JavaScript reimplementation of the algorithm of the
|
This converter was originally a JavaScript reimplementation of the algorithm
|
||||||
"token rearranger" found in the
|
of 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
|
||||||
|
* Grouping nodes including Transform, Separator, Group, Switch, and WWWAnchor
|
||||||
|
* ShapeHints
|
||||||
|
* Rotations
|
||||||
|
* 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)
|
||||||
@ -44,8 +57,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, and some of the translations may be somewhat
|
VRML 1 is recognized (see above for a list of constructs that should
|
||||||
approximate.
|
be handled), and some of the translations may be somewhat 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
|
||||||
@ -66,10 +79,12 @@ 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
|
||||||
often used to render solids. Indeed, the default in VRML97 is to assume they
|
entities are often used to render solids. Indeed, the default in VRML97 is to
|
||||||
do represent a solid, with the normals pointing outward. Unusual visual effects
|
assume they do represent a solid, with the normals pointing outward. Unusual
|
||||||
ensue if the normals are not properly directed (basically, you see through the
|
visual effects ensue if the normals are not properly directed (basically, you
|
||||||
"front" of the solid and see the "backs" of the faces on the opposite side).
|
see through the "front" of the solid and see the "backs" of the faces on the
|
||||||
|
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
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
name: 'vrml1to97',
|
name: 'vrml1to97',
|
||||||
version: '0.2.4',
|
version: '0.3.0',
|
||||||
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.71',
|
'@danielx/civet': '^0.6.72',
|
||||||
'@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',
|
||||||
|
@ -11,8 +11,8 @@ dependencies:
|
|||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@danielx/civet':
|
'@danielx/civet':
|
||||||
specifier: ^0.6.71
|
specifier: ^0.6.72
|
||||||
version: 0.6.71(typescript@5.3.3)
|
version: 0.6.72(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.71(typescript@5.3.3):
|
/@danielx/civet@0.6.72(typescript@5.3.3):
|
||||||
resolution: {integrity: sha512-piOoHtJARe6YqRiXN02Ryb+nLU9JwU8TQLknvPwmlaNm6krdKN+X9dM+C9D4LRoAnAySC2wVshR0wf7YDIUV1Q==}
|
resolution: {integrity: sha512-jumnIbXbdFs0ZiKN62fmD+p8QGi+E0jmtc02dKz9wIIoPkODsa4XXlBrS5BRR5fr3w5d3ah8Vq7gWt+DL9Wa0Q==}
|
||||||
engines: {node: '>=19 || ^18.6.0 || ^16.17.0'}
|
engines: {node: '>=19 || ^18.6.0 || ^16.17.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
201
src/index.civet
201
src/index.civet
@ -2,6 +2,7 @@ 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: /#.*?$/
|
||||||
@ -86,30 +87,146 @@ 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}`)
|
||||||
|
|
||||||
function parse(stream: Lexer, tree: Tree = {}): Tree
|
// Current best way to re-use a pattern
|
||||||
held .= filtered stream // for lookahead
|
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
|
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
|
||||||
|
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'}
|
{ht: 'word', nt: 'obrace'}
|
||||||
switch held.value
|
switch held.value
|
||||||
/(?:Transform)?Separator|Group|Switch|WWWAnchor/
|
matches GroupNode
|
||||||
parent :=
|
parent :=
|
||||||
held.value.endsWith('Separator') ? 'Transform' : 'Group'
|
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
|
||||||
addChild `${parent} { children [
|
newChild .= `${parent} {\n `
|
||||||
${renderList newKids} ] }\n`, tree
|
if 'Rotation' in subTree
|
||||||
|
newChild += renderList subTree.Rotation
|
||||||
|
newChild += "\n "
|
||||||
|
newChild += `children [\n ${renderList newKids} ] }\n`
|
||||||
|
addChild newChild, tree
|
||||||
'ShapeHints'
|
'ShapeHints'
|
||||||
subTree := parse stream
|
subTree := parse stream
|
||||||
|
hints: Tree := {}
|
||||||
if 'vertexOrdering' in subTree
|
if 'vertexOrdering' in subTree
|
||||||
tree.ccw = [
|
hints.ccw = [
|
||||||
subTree.vertexOrdering[0] is 'ccw' ? 'true' : 'false']
|
subTree.vertexOrdering[0] is 'ccw' ? 'TRUE' : 'FALSE']
|
||||||
if 'creaseAngle' in subTree
|
if 'creaseAngle' in subTree
|
||||||
tree.creaseAngle = subTree.creaseAngle
|
hints.creaseAngle = subTree.creaseAngle
|
||||||
|
mergeTree hints, tree
|
||||||
|
if lastDefinition and tree._definitions
|
||||||
|
tree._definitions[lastDefinition] = [hints]
|
||||||
|
'Rotation'
|
||||||
|
tree.Rotation = toksUntilClose stream
|
||||||
'Coordinate3'
|
'Coordinate3'
|
||||||
tree.Coordinate = toksUntilClose stream
|
tree.Coordinate = toksUntilClose stream
|
||||||
'Normal'
|
'Normal'
|
||||||
@ -140,7 +257,7 @@ function parse(stream: Lexer, tree: Tree = {}): Tree
|
|||||||
dims := radius: ''
|
dims := radius: ''
|
||||||
findNumbersAtTopLevel stream, dims
|
findNumbersAtTopLevel stream, dims
|
||||||
addShape 'Sphere', [`radius ${dims.radius}`], tree
|
addShape 'Sphere', [`radius ${dims.radius}`], tree
|
||||||
/IndexedFaceSet|IndexedLineSet|PointSet/
|
matches SetNode
|
||||||
isFaces := held.value is 'IndexedFaceSet'
|
isFaces := held.value is 'IndexedFaceSet'
|
||||||
contents := translatedToksUntilClose stream
|
contents := translatedToksUntilClose stream
|
||||||
params := []
|
params := []
|
||||||
@ -157,69 +274,31 @@ function parse(stream: Lexer, tree: Tree = {}): Tree
|
|||||||
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
|
||||||
/Light$/
|
matches LightNode
|
||||||
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', nv: 'COUNTERCLOCKWISE'}
|
{ht: 'word', hv: 'vertexOrdering', nt: 'word'}
|
||||||
tree.vertexOrdering = ['ccw']
|
switch next.value
|
||||||
held = filtered stream
|
'COUNTERCLOCKWISE'
|
||||||
{ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'CLOCKWISE'}
|
tree.vertexOrdering = ['ccw']
|
||||||
tree.vertexOrdering = ['cw']
|
'CLOCKWISE'
|
||||||
|
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', nt: 'word', nv: 'Info'}
|
|
||||||
opener := 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.log 'Oddly ended up with held', held
|
console.error 'Incomplete parse, symbol at end:', held
|
||||||
tree
|
tree
|
||||||
|
|
||||||
function addShape(nodeType: string, params: (string | Tree)[], tree: Tree): void
|
function addShape(nodeType: string, params: (string | Tree)[], tree: Tree): void
|
||||||
@ -233,9 +312,15 @@ 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"
|
||||||
shape.Shape.push `geometry ${nodeType} {\n`, ...params, " }\n"
|
geometry: (string|Tree)[] := [`geometry ${nodeType}`]
|
||||||
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user