feat: Set faces one or two-sided, conservatively (#17)

Set VRML97 translation of IndexedFaceSets to specify `solid false`
  unless the faces were explicitly indicated to have CLOCKWISE or
  COUNTERCLOCKWISE vertex ordering. This is conservative, but otherwise
  shapes may be "see-through" in undesirable ways, if the original
  VRML 1 vertices are not ordered consistently with the VRML97 default.

  Also fixes a typo/bug in picking up numerical values of parameters,
  and ensures that fields in the output VRML97 are always separated by
  at least a space, comma, or parenthesis.

  Also updates dependencies to their latest versions

Reviewed-on: #17
Co-authored-by: Glen Whitney <glen@studioinfinity.org>
Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
Glen Whitney 2024-02-12 21:16:21 +00:00 committed by Glen Whitney
parent bb0ef18903
commit 0624e67d1a
4 changed files with 58 additions and 37 deletions

View File

@ -62,3 +62,15 @@ Currently this package exports just two functions:
than broken into node names and property values. If there is a need to 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 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. modified to break the tree down further to be ready for conversion to XML.
## Conversion notes
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
do represent a solid, with the normals pointing outward. Unusual visual effects
ensue if the normals are not properly directed (basically, you 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
vertexOrdering of CLOCKWISE or COUNTERCLOCKWISE, the default solid treatment
will be turned off in the VRML97 output, meaning that all faces will be
rendered opaque in both directions.

View File

@ -1,6 +1,6 @@
{ {
name: 'vrml1to97', name: 'vrml1to97',
version: '0.2.2', version: '0.2.3',
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,11 +40,11 @@
}, },
type: 'module', type: 'module',
devDependencies: { devDependencies: {
'@danielx/civet': '^0.6.38', '@danielx/civet': '^0.6.71',
'@types/moo': '^0.5.6', '@types/moo': '^0.5.9',
'http-server': '^14.1.1', 'http-server': '^14.1.1',
json5: '^2.2.3', json5: '^2.2.3',
typescript: '^5.2.2', typescript: '^5.3.3',
}, },
dependencies: { dependencies: {
moo: '^0.5.2', moo: '^0.5.2',

View File

@ -11,11 +11,11 @@ dependencies:
devDependencies: devDependencies:
'@danielx/civet': '@danielx/civet':
specifier: ^0.6.38 specifier: ^0.6.71
version: 0.6.38(typescript@5.2.2) version: 0.6.71(typescript@5.3.3)
'@types/moo': '@types/moo':
specifier: ^0.5.6 specifier: ^0.5.9
version: 0.5.6 version: 0.5.9
http-server: http-server:
specifier: ^14.1.1 specifier: ^14.1.1
version: 14.1.1 version: 14.1.1
@ -23,8 +23,8 @@ devDependencies:
specifier: ^2.2.3 specifier: ^2.2.3
version: 2.2.3 version: 2.2.3
typescript: typescript:
specifier: ^5.2.2 specifier: ^5.3.3
version: 5.2.2 version: 5.3.3
packages: packages:
@ -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.38(typescript@5.2.2): /@danielx/civet@0.6.71(typescript@5.3.3):
resolution: {integrity: sha512-R63YGIfvV4DQianNPUfMfBX60ozlv5htnRXI1wK3Pg6+d4NZ2V3RifFRH0NkmXXoFkRcULzJ+9BzVeI2/yX+yA==} 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:
@ -44,8 +44,8 @@ packages:
dependencies: dependencies:
'@cspotcode/source-map-support': 0.8.1 '@cspotcode/source-map-support': 0.8.1
'@typescript/vfs': 1.5.0 '@typescript/vfs': 1.5.0
typescript: 5.2.2 typescript: 5.3.3
unplugin: 1.4.0 unplugin: 1.7.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
@ -66,8 +66,8 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
dev: true dev: true
/@types/moo@0.5.6: /@types/moo@0.5.9:
resolution: {integrity: sha512-Q60hZhulhl2Ox4LjbJvhH+HzsKrwzLPjEB8dZw0fK1MH2HyOLe6LDou68yTfsWasxGv7DPZe5VNM5vgpzOa2nw==} resolution: {integrity: sha512-ZsFVecFi66jGQ6L41TonEaBhsIVeVftTz6iQKWTctzacHhzYHWvv9S0IyAJi4BhN7vb9qCQ3+kpStP2vbZqmDg==}
dev: true dev: true
/@typescript/vfs@1.5.0: /@typescript/vfs@1.5.0:
@ -78,8 +78,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/acorn@8.10.0: /acorn@8.11.3:
resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
dev: true dev: true
@ -139,8 +139,8 @@ packages:
supports-color: 7.2.0 supports-color: 7.2.0
dev: true dev: true
/chokidar@3.5.3: /chokidar@3.6.0:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
dependencies: dependencies:
anymatch: 3.1.3 anymatch: 3.1.3
@ -462,8 +462,8 @@ packages:
is-number: 7.0.0 is-number: 7.0.0
dev: true dev: true
/typescript@5.2.2: /typescript@5.3.3:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
dev: true dev: true
@ -475,13 +475,13 @@ packages:
qs: 6.11.2 qs: 6.11.2
dev: true dev: true
/unplugin@1.4.0: /unplugin@1.7.1:
resolution: {integrity: sha512-5x4eIEL6WgbzqGtF9UV8VEC/ehKptPXDS6L2b0mv4FRMkJxRtjaJfOWDd6a8+kYbqsjklix7yWP0N3SUepjXcg==} resolution: {integrity: sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==}
dependencies: dependencies:
acorn: 8.10.0 acorn: 8.11.3
chokidar: 3.5.3 chokidar: 3.6.0
webpack-sources: 3.2.3 webpack-sources: 3.2.3
webpack-virtual-modules: 0.5.0 webpack-virtual-modules: 0.6.1
dev: true dev: true
/url-join@4.0.1: /url-join@4.0.1:
@ -493,8 +493,8 @@ packages:
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
dev: true dev: true
/webpack-virtual-modules@0.5.0: /webpack-virtual-modules@0.6.1:
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} resolution: {integrity: sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==}
dev: true dev: true
/whatwg-encoding@2.0.0: /whatwg-encoding@2.0.0:

View File

@ -67,7 +67,7 @@ function findNumbersAtTopLevel(
{type: 'cbrace'} depth -= 1 {type: 'cbrace'} depth -= 1
{type: 'word'} if depth === 1 and tok.value in fields {type: 'word'} if depth === 1 and tok.value in fields
selector = tok.value selector = tok.value
{tye: 'number'} if selecting then fields[selecting] = tok.value {type: 'number'} if selecting then fields[selecting] = tok.value
{type: 'obrace'} depth += 1 {type: 'obrace'} depth += 1
selecting = selector selecting = selector
@ -105,7 +105,9 @@ function parse(stream: Lexer, tree: Tree = {}): Tree
${renderList newKids} ] }\n`, tree ${renderList newKids} ] }\n`, tree
'ShapeHints' 'ShapeHints'
subTree := parse stream subTree := parse stream
if 'vertexOrdering' in subTree then tree.ccw = ['true'] if 'vertexOrdering' in subTree
tree.ccw = [
subTree.vertexOrdering[0] is 'ccw' ? 'true' : 'false']
if 'creaseAngle' in subTree if 'creaseAngle' in subTree
tree.creaseAngle = subTree.creaseAngle tree.creaseAngle = subTree.creaseAngle
'Coordinate3' 'Coordinate3'
@ -153,8 +155,9 @@ function parse(stream: Lexer, tree: Tree = {}): Tree
...tree.TextureCoordinate, " }\n" ...tree.TextureCoordinate, " }\n"
if isFaces and 'creaseAngle' in tree if isFaces and 'creaseAngle' in tree
params.push `creaseAngle ${tree.creaseAngle[0]}` params.push `creaseAngle ${tree.creaseAngle[0]}`
if isFaces and 'ccw' in tree if isFaces
params.push `ccw true` if 'ccw' in tree then params.push `ccw ${tree.ccw[0]}`
else params.push 'solid false'
params.push ...contents params.push ...contents
addShape held.value, params, tree addShape held.value, params, tree
/Light$/ /Light$/
@ -166,6 +169,9 @@ function parse(stream: Lexer, tree: Tree = {}): Tree
{ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'COUNTERCLOCKWISE'} {ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'COUNTERCLOCKWISE'}
tree.vertexOrdering = ['ccw'] tree.vertexOrdering = ['ccw']
held = filtered stream held = filtered stream
{ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'CLOCKWISE'}
tree.vertexOrdering = ['cw']
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
@ -236,7 +242,7 @@ function render(t: string | Tree): string
if 'type' in t and 'value' in t if 'type' in t and 'value' in t
val := renderList t.value val := renderList t.value
return switch t.type[0] return switch t.type[0]
/string|number|word/ ` ${val}` /string|number|word/ val
/comma|oparen|cparen/ val /comma|oparen|cparen/ val
/obrace|cbrace|obracket|cbracket/ `${val}\n` /obrace|cbrace|obracket|cbracket/ `${val}\n`
else `\nUNKNOWN TYPE ${t.type}\n\n` else `\nUNKNOWN TYPE ${t.type}\n\n`
@ -251,7 +257,10 @@ function renderList(l: (string | Tree)[]): string
commaNewlineOnce .= false commaNewlineOnce .= false
commaTriggersNewline .= false commaTriggersNewline .= false
for each item of l for each item of l
return.value += render item next := render item
if return.value and not ',()'.includes next[0]
return.value += ' '
return.value += next
switch item switch item
{type: ['word'], value: ['point']} {type: ['word'], value: ['point']}
commaTriggersNewline = true commaTriggersNewline = true