From 66b24e657bfd26ccd503ec0379f7756fd94a3ad6 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 4 Sep 2023 17:31:44 +0000 Subject: [PATCH] feat: Translate VRML 1 to VRML97 (#6) Resolves #5 Reviewed-on: https://code.studioinfinity.org/glen/vrml1to97/pulls/6 Co-authored-by: Glen Whitney Co-committed-by: Glen Whitney --- package.json5 | 4 +- pnpm-lock.yaml | 168 +++++++++++++++++++++++++++++++++++- src/example.civet | 3 +- src/index.civet | 213 +++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 359 insertions(+), 29 deletions(-) diff --git a/package.json5 b/package.json5 index 6549026..cfa60a2 100644 --- a/package.json5 +++ b/package.json5 @@ -18,7 +18,7 @@ build_etc: 'cp etc/*.js* dist', build: 'pnpm --sequential /build_/', try: 'pnpm build && node dist/vrml1to97/example.js', - clean: 'rm -r build dist deps' + clean: 'rm -r build dist deps', }, keywords: [ 'javascript', @@ -36,7 +36,7 @@ vrml1to97: 'dist/vrml1to97.js', }, devDependencies: { - '@danielx/civet': '^0.6.26', + '@danielx/civet': '^0.6.30', '@types/moo': '^0.5.6', 'http-server': '^14.1.1', typescript: '^5.2.2', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c49530e..d91992f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,8 +11,8 @@ dependencies: devDependencies: '@danielx/civet': - specifier: ^0.6.26 - version: 0.6.26 + specifier: ^0.6.30 + version: 0.6.30(typescript@5.2.2) '@types/moo': specifier: ^0.5.6 version: 0.5.6 @@ -32,12 +32,19 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@danielx/civet@0.6.26: - resolution: {integrity: sha512-YQKANR9Ow3NvzOZAVjTpiniSRWBjIWW8v6KAgVIlrzd8XBbAP1cyeAaWFXzAqOXylzL0map0gyw2tQO5pQq7bw==} + /@danielx/civet@0.6.30(typescript@5.2.2): + resolution: {integrity: sha512-/aZjryo8T9TWVBczbeY8eO8Xmi91KkaKZG6YhCS2lCErmPXxP+zOEUATJiaZBp6AcT2bQEbW+poGyrU6BHL9Dg==} engines: {node: '>=19 || ^18.6.0 || ^16.17.0'} hasBin: true + peerDependencies: + typescript: ^4.5 || ^5.0 dependencies: '@cspotcode/source-map-support': 0.8.1 + '@typescript/vfs': 1.5.0 + typescript: 5.2.2 + unplugin: 1.4.0 + transitivePeerDependencies: + - supports-color dev: true /@jridgewell/resolve-uri@3.1.1: @@ -60,6 +67,20 @@ packages: resolution: {integrity: sha512-Q60hZhulhl2Ox4LjbJvhH+HzsKrwzLPjEB8dZw0fK1MH2HyOLe6LDou68yTfsWasxGv7DPZe5VNM5vgpzOa2nw==} dev: true + /@typescript/vfs@1.5.0: + resolution: {integrity: sha512-AJS307bPgbsZZ9ggCT3wwpg3VbTKMFNHfaY/uF0ahSkYYrPF2dSSKDNIDIQAHm9qJqbLvCsSJH7yN4Vs/CsMMg==} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -67,6 +88,14 @@ packages: color-convert: 2.0.1 dev: true + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + /async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} dependencies: @@ -80,6 +109,18 @@ packages: safe-buffer: 5.1.2 dev: true + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -95,6 +136,21 @@ packages: supports-color: 7.2.0 dev: true + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -122,10 +178,29 @@ packages: ms: 2.1.3 dev: true + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: true + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + /follow-redirects@1.15.2: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} @@ -136,6 +211,14 @@ packages: optional: true dev: true + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} dev: true @@ -149,6 +232,13 @@ packages: has-symbols: 1.0.3 dev: true + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -224,6 +314,30 @@ packages: safer-buffer: 2.1.2 dev: true + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true @@ -249,10 +363,19 @@ packages: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} dev: false + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true @@ -262,6 +385,11 @@ packages: hasBin: true dev: true + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + /portfinder@1.0.32: resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} engines: {node: '>= 0.12.0'} @@ -280,6 +408,13 @@ packages: side-channel: 1.0.4 dev: true + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: true @@ -311,6 +446,13 @@ packages: has-flag: 4.0.0 dev: true + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + /typescript@5.2.2: resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} @@ -324,10 +466,28 @@ packages: qs: 6.11.2 dev: true + /unplugin@1.4.0: + resolution: {integrity: sha512-5x4eIEL6WgbzqGtF9UV8VEC/ehKptPXDS6L2b0mv4FRMkJxRtjaJfOWDd6a8+kYbqsjklix7yWP0N3SUepjXcg==} + dependencies: + acorn: 8.10.0 + chokidar: 3.5.3 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.5.0 + dev: true + /url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} dev: true + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack-virtual-modules@0.5.0: + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} + dev: true + /whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} diff --git a/src/example.civet b/src/example.civet index 18190a1..7d0a196 100644 --- a/src/example.civet +++ b/src/example.civet @@ -547,4 +547,5 @@ Separator { } } ` -console.log convert hartPoly + +console.log convert hartPoly, 'https://www.georgehart.com/virtual-polyhedra/vrml/zonish-10-icosahedron.wrl' diff --git a/src/index.civet b/src/index.civet index 1787c12..167fc0d 100644 --- a/src/index.civet +++ b/src/index.civet @@ -1,6 +1,7 @@ moo from ../deps/moo.js +import type {Lexer, Token} from ../deps/moo.d.ts -type Tree = {[key:string]: string | Tree} +type Tree = {[key:string]: (string | Tree)[]} lexer := moo.compile comment: /#.*?$/ @@ -20,27 +21,195 @@ lexer := moo.compile oparen: '(' cparen: ')' -export function tree97(vrml1: string): Tree - tree: Tree := {} - for tok, index of lexer.reset vrml1 - if tok.type and tok.type !== 'whitespace' and tok.type !== 'comment' - tree[`${index}`] = {tok.type, tok.value} - tree +export function tree97(vrml1: string) + parse lexer.reset vrml1 -function render(t: string | Tree): string - if typeof t is 'string' - return t - result .= '' - for item of Object.values t - if typeof item is 'string' then result += item + ' ' - else - typ := item.type - result += render(typ) - if typeof typ === 'string' and - ['cbrace', 'cbracket', 'cparen'].includes(typ) - result += "\n" - else result += ' ' +function filtered(stream: Lexer): Token | undefined + result .= stream.next() + while result and (result.type === 'whitespace' or result.type === 'comment') + result = stream.next() result -export function convert(vrml1: string): string - render tree97 vrml1 +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): 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 + {tye: '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 + +function parse(stream: Lexer, tree: Tree = {}): Tree + held .= filtered stream // for lookahead + + while next := filtered stream + unless held then break + switch data := + {nt: next.type, nv: next.value, ht: held.type, hv: held.value} + {ht: 'word', nt: 'obrace'} + switch held.value + /(?:Transform)?Separator|Group|Switch|WWWAnchor/ + parent := + held.value.endsWith('Separator') ? 'Transform' : 'Group' + {children, ...context} := tree + subTree := parse stream, context + if newKids := subTree.children + addChild `${parent} { children [ + ${renderList newKids} ] }\n`, tree + 'ShapeHints' + subTree := parse stream + if 'vertexOrdering' in subTree then tree.ccw = ['1.0'] + '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 + /IndexedLineSet|PointSet/ // ignored + findNumbersAtTopLevel stream, {} + '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 'TextureCoordinate' in tree + params.push "texCoord TextureCoordinate {\n", + ...tree.TextureCoordinate, " }\n" + params.push ...contents + addShape 'IndexedFaceSet', params, tree + else + parse stream // discard the subgroup + held = filtered stream + if not held or held.type === 'cbrace' then break + {ht: 'word', hv: 'vertexOrdering', nt: 'word', nv: 'COUNTERCLOCKWISE'} + tree.vertexOrdering = ['ccw'] + held = filtered stream + if not held or held.type === 'cbrace' then break + else + held = next // ignore unknown words + if not held or held.type === 'cbrace' then break + if held and held.type !== 'cbrace' + console.log 'Oddly ended up with held', 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" + shape.Shape.push `geometry ${nodeType} {\n`, ...params, " }\n" + addChild shape, tree + +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 + return.value += render item + switch item + {type: ['word'], value: ['point']} + commaTriggersNewline = true + {type: ['word'], value: ['coordIndex']} + 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