feat: Handle additional syntax

* Converts all Lighting nodes into the output
  * Translates Title Info and SceneInfo Info into a WorldInfo node
  * Translates Viewer Info into a NavigationInfo node
  * Captures the creaseAngle parameter
  * Also, this commit documents the API (such as it is)
  Resolves #9.
  Resolves #10.
This commit is contained in:
Glen Whitney 2023-09-11 00:12:44 -07:00
parent 48925d05d9
commit b95e2e21e3
3 changed files with 85 additions and 13 deletions

View File

@ -33,3 +33,26 @@ or from the command line via node
```shell
npx vrml1to97 < old.wrl > shinynew.wrl
```
## API
Currently this package exports just two functions:
* convert(vrml1: string): string
The main function, takes in VRML 1 syntax (note that it does not
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
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
approximate.
* tree97(vrml1: string): Tree
Takes the same input as convert, but rathre than returning a string, it
returns a very rough syntax tree for the converted VRML97 scene. The
returned Tree data structure is an object whose keys are property names
and whose values are lists of either strings or sub-Trees. Note however
that this syntax tree does not represent a complete parse; since VRML 1
and VRML97 are close, whole blocks of syntax are left as strings rather
than broken into node names and property values. If there is a need to
generate XML syntax output, for example, the code would need to be
modified to break the tree down further to be ready for conversion to XML.

View File

@ -1,6 +1,6 @@
{
name: 'vrml1to97',
version: '0.1.3',
version: '0.2.0',
description: 'JavaScript converter from VRML 1 to VRML97',
scripts: {
test: 'echo "Error: no test specified" && exit 1',

View File

@ -74,6 +74,18 @@ function findNumbersAtTopLevel(
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}`)
function parse(stream: Lexer, tree: Tree = {}): Tree
held .= filtered stream // for lookahead
@ -93,7 +105,9 @@ function parse(stream: Lexer, tree: Tree = {}): Tree
${renderList newKids} ] }\n`, tree
'ShapeHints'
subTree := parse stream
if 'vertexOrdering' in subTree then tree.ccw = ['1.0']
if 'vertexOrdering' in subTree then tree.ccw = ['true']
if 'creaseAngle' in subTree
tree.creaseAngle = subTree.creaseAngle
'Coordinate3'
tree.Coordinate = toksUntilClose stream
'Normal'
@ -124,7 +138,7 @@ function parse(stream: Lexer, tree: Tree = {}): Tree
dims := radius: ''
findNumbersAtTopLevel stream, dims
addShape 'Sphere', [`radius ${dims.radius}`], tree
/IndexedLineSet|PointSet/ // ignored
/IndexedLineSet|PointSet/ // ignored, but why? They are in VRML97
findNumbersAtTopLevel stream, {}
'IndexedFaceSet'
contents := translatedToksUntilClose stream
@ -138,27 +152,62 @@ function parse(stream: Lexer, tree: Tree = {}): Tree
if 'TextureCoordinate' in tree
params.push "texCoord TextureCoordinate {\n",
...tree.TextureCoordinate, " }\n"
if 'creaseAngle' in tree
params.push `creaseAngle ${tree.creaseAngle[0]}`
if 'ccw' in tree
params.push `ccw true`
params.push ...contents
addShape 'IndexedFaceSet', params, tree
/Light$/
contents := toksUntilClose stream
addChild {[held.value]: contents}, tree
else
parse stream // discard the subgroup
held = filtered stream
{ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'COUNTERCLOCKWISE'}
tree.vertexOrdering = ['ccw']
held = filtered stream
{ht: 'word', hv: 'BackgroundColor', nt: 'word', nv: 'Info'}
{ht: 'word', hv: 'creaseAngle', nt: 'number'}
tree.creaseAngle = [ next.value ]
held = filtered stream
{ht: 'word', nt: 'word', nv: 'Info'}
opener := filtered stream
if opener and opener.type === 'obrace'
contents := toksUntilClose stream
// 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
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