@es3d/es3d 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -7
- package/browser/es3d-stage.min.js +10349 -0
- package/docs/SPEC.md +9 -0
- package/package.json +4 -2
- package/scripts/es3d-cli.mjs +56 -20
- package/src/format/parse.js +43 -3
- package/src/index.js +12 -5
- package/src/io/convert.js +53 -5
- package/src/io/export-model.js +679 -0
- package/src/io/file-loader.js +3 -2
- package/src/io/gltf-pack.js +1461 -0
- package/src/io/openscad-import.js +1295 -0
- package/src/io/openscad.js +252 -0
- package/src/io/text-encode.js +87 -0
- package/src/io/threejs.js +397 -0
- package/src/io/usd.js +669 -0
- package/src/io/x3d.js +1183 -0
- package/src/optimize/optimizer.js +34 -9
- package/src/render/stage.js +87 -11
- package/src/scene/flatten.js +208 -4
- package/src/scene/scene-graph.js +13 -2
- package/src/stage.js +8 -0
package/docs/SPEC.md
CHANGED
|
@@ -403,11 +403,20 @@ The title names the phase (`Parse`, `Reference`, `Geometry`, `Render`, `File`) a
|
|
|
403
403
|
|
|
404
404
|
Unknown names get a suggestion when a known name is within a small edit distance or shares a prefix (`raduis` → `radius`, `spotlight` → `spot`, `sphear` → `sphere`).
|
|
405
405
|
|
|
406
|
+
The reference reader's human-readable report (`formatDiagnostics`, and the text from `validate`) shows a repeated warning once. Warnings group when they share a code, the element name written in the message (its first `<name>`), and the same message. The report keeps the first location and then says how many times that warning occurred:
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
Reported 906 times. Showing the first.
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Every warning remains its own entry in the diagnostic list. `validate --json` prints that list. Errors are listed one by one.
|
|
413
|
+
|
|
406
414
|
## 16. Extensibility and versioning
|
|
407
415
|
|
|
408
416
|
- Elements whose names start with `x-` (and their subtrees) are vendor extensions: readers that do not understand them skip them with `W_EXTENSION`. Unknown elements without the prefix are errors, so typos are never mistaken for extensions.
|
|
409
417
|
- Unknown attributes are always errors. (A future version may reserve an `x-` attribute prefix as well.)
|
|
410
418
|
- `version="1.x"` documents must remain readable by version-1 readers; incompatible changes require version 2, which version-1 readers reject with `E_VERSION`.
|
|
419
|
+
- A processor that rewrites a document keeps `x-` elements, and keeps any element whose id is named by a token in an `x-` attribute. The reference optimizer treats a whitespace-separated token (an optional leading `#` is ignored) as a reference when that id exists, and updates the token when it merges a duplicate material.
|
|
411
420
|
|
|
412
421
|
## 17. File names and media types
|
|
413
422
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@es3d/es3d",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "ES3D (Extensible Scalable 3D): a declarative, SVG-inspired 3D scene format with a WebGPU/WebGL2 reference renderer, editor and converter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
],
|
|
29
29
|
"main": "src/index.js",
|
|
30
30
|
"exports": {
|
|
31
|
-
".": "./src/index.js"
|
|
31
|
+
".": "./src/index.js",
|
|
32
|
+
"./stage": "./browser/es3d-stage.min.js"
|
|
32
33
|
},
|
|
33
34
|
"bin": {
|
|
34
35
|
"es3d": "scripts/es3d-cli.mjs"
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"!src/bench/",
|
|
41
42
|
"!src/main.js",
|
|
42
43
|
"scripts/es3d-cli.mjs",
|
|
44
|
+
"browser/es3d-stage.min.js",
|
|
43
45
|
"docs/SPEC.md",
|
|
44
46
|
"docs/REFERENCE.md"
|
|
45
47
|
],
|
package/scripts/es3d-cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// npm run es3d -- validate scene.es3d
|
|
5
5
|
// npm run es3d -- fmt scene.es3d -o pretty.es3d
|
|
6
6
|
// npm run es3d -- stats scene.es3d
|
|
7
|
-
// npm run es3d -- glb scene.es3d
|
|
7
|
+
// npm run es3d -- glb scene.es3d -o scene.glb
|
|
8
8
|
// npm run es3d -- import drawing.svg -o drawing.es3d
|
|
9
9
|
//
|
|
10
10
|
// Run without arguments for the full command list. Input "-" reads stdin;
|
|
@@ -13,21 +13,21 @@
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import zlib from 'node:zlib';
|
|
16
|
-
import { parse, detectSyntax } from '../src/format/parse.js';
|
|
16
|
+
import { parse, detectSyntax, formatDiagnostics } from '../src/format/parse.js';
|
|
17
17
|
import { serialize } from '../src/format/serializer.js';
|
|
18
18
|
import { compile } from '../src/pipeline.js';
|
|
19
19
|
import { exportOBJ } from '../src/io/obj.js';
|
|
20
|
-
import { exportGLB } from '../src/io/gltf.js';
|
|
21
20
|
import { exportSTL } from '../src/io/stl.js';
|
|
22
21
|
import { exportSTEP } from '../src/io/step.js';
|
|
23
|
-
import { detectKind, importFiles } from '../src/io/convert.js';
|
|
22
|
+
import { detectKind, importFiles, exportFormat } from '../src/io/convert.js';
|
|
24
23
|
import { exportableInstances } from '../src/io/external-scene.js';
|
|
25
24
|
import { optimize, expandProcedural } from '../src/optimize/optimizer.js';
|
|
26
25
|
|
|
27
26
|
const HELP = `es3d <command> [options] <input>
|
|
28
27
|
|
|
29
28
|
Commands
|
|
30
|
-
validate parse, validate and build the scene; print
|
|
29
|
+
validate parse, validate and build the scene; print diagnostics (exit 1 on errors).
|
|
30
|
+
Repeated warnings are grouped; --json lists every one
|
|
31
31
|
fmt pretty-print, keeping the syntax (markup or compact)
|
|
32
32
|
minify minified markup (short attribute aliases, no comments)
|
|
33
33
|
compact convert to the compact syntax (--min for one line)
|
|
@@ -36,21 +36,27 @@ Commands
|
|
|
36
36
|
optimize run the optimizer and print its report to stderr
|
|
37
37
|
expand replace <repeat>/<scatter> with explicit elements
|
|
38
38
|
obj export Wavefront OBJ (+ .mtl next to -o) (--lod <0-3>)
|
|
39
|
-
glb
|
|
39
|
+
glb packed glTF binary (instances, LODs, lights, cameras, animation)
|
|
40
40
|
stl export STL for 3D printing, Z-up (--lod <0-3>, --ascii)
|
|
41
41
|
step export tessellated STEP (AP242), Z-up (--lod <0-3>)
|
|
42
|
-
|
|
42
|
+
x3d export X3D Classic (.x3dv) (--lod <0-3>, --flatten)
|
|
43
|
+
usd export USD ASCII (.usda) (--lod <0-3>, --flatten)
|
|
44
|
+
scad export an OpenSCAD program (--lod <0-3>, --flatten)
|
|
45
|
+
js export a three.js module (--lod <0-3>, --flatten)
|
|
46
|
+
import convert SVG, OBJ (+MTL), STL, STEP, glTF, X3D, USDA or OpenSCAD to ES3D:
|
|
43
47
|
SVG shapes are extruded (--size, --depth, --bevel, --flat, --no-strokes, --plate),
|
|
44
48
|
STEP solids become ES3D shapes (boxes, extrusions, revolves, paths; free-form faces a mesh),
|
|
45
|
-
meshes become <mesh> elements (--precision, --smooth, --up y for Y-up STL or STEP)
|
|
49
|
+
meshes become <mesh> elements (--precision, --smooth, --up y for Y-up STL or STEP),
|
|
50
|
+
OpenSCAD programs become shapes (difference and intersection are kept and noted, not evaluated)
|
|
46
51
|
stats sizes, element / instance / triangle counts and pipeline timings
|
|
47
52
|
|
|
48
53
|
Options
|
|
49
54
|
-o, --out <file> output file (default: stdout for text)
|
|
50
55
|
--precision <n> round numbers to n decimals when writing text
|
|
51
|
-
--lod <n> level of detail for mesh
|
|
52
|
-
--
|
|
53
|
-
--
|
|
56
|
+
--lod <n> level of detail for mesh export (default 0 = finest)
|
|
57
|
+
--flatten x3d, usd, scad, js: write triangles instead of primitives
|
|
58
|
+
--instancing glb always writes EXT_mesh_gpu_instancing
|
|
59
|
+
--quantize glb always writes KHR_mesh_quantization
|
|
54
60
|
--min / --pretty force minified / pretty output
|
|
55
61
|
--json validate/stats: machine readable output
|
|
56
62
|
--ascii STL: write ASCII instead of binary
|
|
@@ -65,7 +71,7 @@ Options
|
|
|
65
71
|
-h, --help this text`;
|
|
66
72
|
|
|
67
73
|
function parseArgs(argv) {
|
|
68
|
-
const o = { cmd: null, input: null, out: null, precision: null, lod: 0, instancing: false, quantize: 0, min: false, pretty: false, json: false, ascii: false, svg: {}, mesh: {}, stl: {} };
|
|
74
|
+
const o = { cmd: null, input: null, out: null, precision: null, lod: 0, instancing: false, quantize: 0, min: false, pretty: false, json: false, ascii: false, flatten: false, svg: {}, mesh: {}, stl: {} };
|
|
69
75
|
for (let i = 0; i < argv.length; i++) {
|
|
70
76
|
const a = argv[i];
|
|
71
77
|
const val = () => {
|
|
@@ -82,6 +88,7 @@ function parseArgs(argv) {
|
|
|
82
88
|
else if (a === '--pretty') o.pretty = true;
|
|
83
89
|
else if (a === '--json') o.json = true;
|
|
84
90
|
else if (a === '--ascii') o.ascii = true;
|
|
91
|
+
else if (a === '--flatten') o.flatten = true;
|
|
85
92
|
else if (a === '--size') o.svg.size = Number(val());
|
|
86
93
|
else if (a === '--depth') o.svg.depth = Number(val()) / 100;
|
|
87
94
|
else if (a === '--bevel') o.svg.bevel = Number(val()) / 100;
|
|
@@ -132,9 +139,7 @@ function write(opts, data) {
|
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
function report(doc) {
|
|
135
|
-
|
|
136
|
-
for (const d of doc.diagnostics) out.push(d.format(doc.lineIndex));
|
|
137
|
-
return out.join('\n\n');
|
|
142
|
+
return formatDiagnostics(doc);
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/** Parse and validate, or exit with the diagnostics. */
|
|
@@ -144,7 +149,8 @@ function parsed(input) {
|
|
|
144
149
|
process.stderr.write(report(doc) + '\n');
|
|
145
150
|
process.exit(1);
|
|
146
151
|
}
|
|
147
|
-
|
|
152
|
+
const warnings = formatDiagnostics(doc.warnings || [], doc.lineIndex);
|
|
153
|
+
if (warnings) process.stderr.write(warnings + '\n\n');
|
|
148
154
|
return doc;
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -152,7 +158,7 @@ function compiled(input) {
|
|
|
152
158
|
const r = compile(input);
|
|
153
159
|
const errors = r.diagnostics.filter((d) => d.severity === 'error');
|
|
154
160
|
if (errors.length) {
|
|
155
|
-
process.stderr.write(
|
|
161
|
+
process.stderr.write(formatDiagnostics(r) + '\n');
|
|
156
162
|
process.exit(1);
|
|
157
163
|
}
|
|
158
164
|
return r;
|
|
@@ -264,7 +270,8 @@ async function main() {
|
|
|
264
270
|
const warnings = r.diagnostics.length - errors;
|
|
265
271
|
if (opts.json) console.log(JSON.stringify({ ok: errors === 0, diagnostics: r.diagnostics.map((d) => d.toJSON()) }, null, 2));
|
|
266
272
|
else {
|
|
267
|
-
|
|
273
|
+
const text = formatDiagnostics(r);
|
|
274
|
+
if (text) process.stderr.write(text + '\n\n');
|
|
268
275
|
console.log(`${opts.input}: ${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}${errors ? '' : `, ${r.scene.count.toLocaleString('en-US')} instances`}`);
|
|
269
276
|
}
|
|
270
277
|
process.exit(errors ? 1 : 0);
|
|
@@ -312,8 +319,21 @@ async function main() {
|
|
|
312
319
|
break;
|
|
313
320
|
}
|
|
314
321
|
case 'glb': {
|
|
315
|
-
|
|
316
|
-
|
|
322
|
+
let exportPackedGLB;
|
|
323
|
+
try {
|
|
324
|
+
({ exportPackedGLB } = await import('../src/io/gltf-pack.js'));
|
|
325
|
+
} catch (e) {
|
|
326
|
+
fail(`glb could not load the packer (${e.message})`, 1);
|
|
327
|
+
}
|
|
328
|
+
const baseDir = opts.input === '-' ? '' : path.dirname(path.resolve(opts.input));
|
|
329
|
+
const sourceName = opts.input === '-' ? 'stdin' : `${path.basename(path.dirname(opts.input))}/${path.basename(opts.input)}`;
|
|
330
|
+
let file;
|
|
331
|
+
try {
|
|
332
|
+
file = await exportPackedGLB(es3dInput(bytes), { baseDir, sourceName });
|
|
333
|
+
} catch (e) {
|
|
334
|
+
fail(e.message, 1);
|
|
335
|
+
}
|
|
336
|
+
write(opts, file);
|
|
317
337
|
break;
|
|
318
338
|
}
|
|
319
339
|
case 'stl': {
|
|
@@ -343,6 +363,22 @@ async function main() {
|
|
|
343
363
|
process.stderr.write(`${out.triangles.toLocaleString('en-US')} triangles\n`);
|
|
344
364
|
break;
|
|
345
365
|
}
|
|
366
|
+
case 'x3d':
|
|
367
|
+
case 'usd':
|
|
368
|
+
case 'scad':
|
|
369
|
+
case 'js': {
|
|
370
|
+
const format = { x3d: 'x3dv', usd: 'usda', scad: 'scad', js: 'js' }[opts.cmd];
|
|
371
|
+
const base = opts.out ? path.basename(opts.out).replace(/\.[^.]+$/, '') : 'scene';
|
|
372
|
+
let out;
|
|
373
|
+
try {
|
|
374
|
+
out = exportFormat(es3dInput(bytes), format, { baseName: base, lod: opts.lod, structure: opts.flatten ? 'flatten' : 'keep' });
|
|
375
|
+
} catch (e) {
|
|
376
|
+
fail(e.message, 1);
|
|
377
|
+
}
|
|
378
|
+
write(opts, out.files[0].data);
|
|
379
|
+
for (const note of out.notes || []) process.stderr.write(`${note}\n`);
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
346
382
|
case 'import': {
|
|
347
383
|
const name = opts.input === '-' ? 'stdin' : path.basename(opts.input);
|
|
348
384
|
const files = [{ name, bytes }];
|
package/src/format/parse.js
CHANGED
|
@@ -86,7 +86,47 @@ export function parseStrict(input, opts = {}) {
|
|
|
86
86
|
return doc;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
/** Element name written in a message, if it has one (`<x-anchors>`). */
|
|
90
|
+
function mentionedElement(message) {
|
|
91
|
+
const m = /<([^<>\s]+)>/.exec(message || '');
|
|
92
|
+
return m ? m[1] : '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Human-readable report of diagnostics.
|
|
97
|
+
* Repeated warnings that share a code, the element name in the message, and
|
|
98
|
+
* the same message are shown once, at the first location, with a count.
|
|
99
|
+
* Errors are listed one by one. The diagnostic objects are not changed.
|
|
100
|
+
* @param {object|Array} source a document, a compile result, or a diagnostic list
|
|
101
|
+
* @param {import('./errors.js').LineIndex} [lineIndex]
|
|
102
|
+
*/
|
|
103
|
+
export function formatDiagnostics(source, lineIndex) {
|
|
104
|
+
if (!source) return '';
|
|
105
|
+
const list = Array.isArray(source) ? source : source.diagnostics || [];
|
|
106
|
+
const index = lineIndex || source.lineIndex || (source.doc && source.doc.lineIndex) || null;
|
|
107
|
+
const seen = new Map();
|
|
108
|
+
const items = [];
|
|
109
|
+
for (const d of list) {
|
|
110
|
+
if (!d || d.severity !== 'warning') {
|
|
111
|
+
items.push(d);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const key = `${d.code}\0${mentionedElement(d.message)}\0${d.message}`;
|
|
115
|
+
const prev = seen.get(key);
|
|
116
|
+
if (prev) {
|
|
117
|
+
prev.count++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const slot = { d, count: 1 };
|
|
121
|
+
seen.set(key, slot);
|
|
122
|
+
items.push(slot);
|
|
123
|
+
}
|
|
124
|
+
return items
|
|
125
|
+
.map((item) => {
|
|
126
|
+
if (!item || !item.d) return item.format(index);
|
|
127
|
+
const text = item.d.format(index);
|
|
128
|
+
if (item.count < 2) return text;
|
|
129
|
+
return `${text}\n\nReported ${item.count} times. Showing the first.`;
|
|
130
|
+
})
|
|
131
|
+
.join('\n\n');
|
|
92
132
|
}
|
package/src/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// Public API of the ES3D reference implementation.
|
|
2
2
|
//
|
|
3
|
-
// import { compile, createRenderer, Camera } from '@es3d/es3d';
|
|
3
|
+
// import { compile, createRenderer, Camera, formatDiagnostics } from '@es3d/es3d';
|
|
4
4
|
//
|
|
5
5
|
// const result = compile(text); // parse + validate + build + flatten
|
|
6
|
-
// if (!result.ok) console.error(result
|
|
6
|
+
// if (!result.ok) console.error(formatDiagnostics(result));
|
|
7
7
|
// const renderer = await createRenderer(canvas); // WebGPU, else WebGL2
|
|
8
8
|
// renderer.setScene(result.scene, (entry, lod) => result.cache.mesh(entry, lod));
|
|
9
9
|
// const camera = new Camera().frame(result.scene.bounds);
|
|
10
10
|
// renderer.render(camera, { width: canvas.clientWidth, height: canvas.clientHeight });
|
|
11
11
|
//
|
|
12
|
-
// const svg = await importFiles([{ name: 'logo.svg', bytes }]); // SVG, OBJ, STL, STEP, glTF → ES3D
|
|
13
|
-
// const { files } = exportFormat(svg.text, 'glb'); // ES3D → glTF, OBJ, STL, STEP
|
|
12
|
+
// const svg = await importFiles([{ name: 'logo.svg', bytes }]); // SVG, OBJ, STL, STEP, glTF, X3D, USDA, OpenSCAD → ES3D
|
|
13
|
+
// const { files } = exportFormat(svg.text, 'glb'); // ES3D → glTF, OBJ, STL, STEP, X3D, USD, OpenSCAD, three.js
|
|
14
14
|
//
|
|
15
15
|
// The format, scene, geometry and I/O modules run in Node as well; only the
|
|
16
16
|
// renderer and controls need a browser.
|
|
@@ -25,7 +25,9 @@ export { ELEMENTS, elementSpec, attrSpec } from './format/schema.js';
|
|
|
25
25
|
// scene pipeline
|
|
26
26
|
export { compile, generateGeometry, GeometryCache } from './pipeline.js';
|
|
27
27
|
export { openGeometryStore, indexedDBStore, memoryStore, restoreGeometry, saveGeometry, persistGeometry, geometryKey, GEOMETRY_REVISION } from './cache/geometry-store.js';
|
|
28
|
-
export { RenderScene, updateDynamic, FLAG_MIRRORED, FLAG_NO_COLLIDE } from './scene/flatten.js';
|
|
28
|
+
export { RenderScene, updateDynamic, applyLocalMatrix, applyLocalMatrices, FLAG_MIRRORED, FLAG_NO_COLLIDE } from './scene/flatten.js';
|
|
29
|
+
export { mulberry32 } from './math/random.js';
|
|
30
|
+
export { es3dSin, es3dCos } from './math/trig.js';
|
|
29
31
|
export { pick, raycast } from './scene/picking.js';
|
|
30
32
|
export { applyLimits, sweepCamera, constrainCamera } from './scene/camera-constraints.js';
|
|
31
33
|
|
|
@@ -43,5 +45,10 @@ export { exportOBJ, parseOBJ, objToScene } from './io/obj.js';
|
|
|
43
45
|
export { exportGLB, parseGLB, glbToScene, isGLB, parseGLTF, gltfToScene } from './io/gltf.js';
|
|
44
46
|
export { exportSTL, parseSTL, stlToScene } from './io/stl.js';
|
|
45
47
|
export { exportSTEP, parseSTEP, stepToScene, isSTEP } from './io/step.js';
|
|
48
|
+
export { exportX3D, importX3D } from './io/x3d.js';
|
|
49
|
+
export { exportUSD, importUSD } from './io/usd.js';
|
|
50
|
+
export { exportOpenSCAD } from './io/openscad.js';
|
|
51
|
+
export { importOpenSCAD } from './io/openscad-import.js';
|
|
52
|
+
export { exportThree } from './io/threejs.js';
|
|
46
53
|
export { sceneToMeshDocument } from './io/mesh-import.js';
|
|
47
54
|
export { optimize, expandProcedural } from './optimize/optimizer.js';
|
package/src/io/convert.js
CHANGED
|
@@ -16,6 +16,12 @@ import { svgToES3D } from './svg-import.js';
|
|
|
16
16
|
import { sceneToMeshDocument } from './mesh-import.js';
|
|
17
17
|
import { compile } from '../pipeline.js';
|
|
18
18
|
import { exportableInstances } from './external-scene.js';
|
|
19
|
+
import { buildExportModel } from './export-model.js';
|
|
20
|
+
import { exportX3D, importX3D } from './x3d.js';
|
|
21
|
+
import { exportUSD, importUSD } from './usd.js';
|
|
22
|
+
import { exportOpenSCAD } from './openscad.js';
|
|
23
|
+
import { importOpenSCAD } from './openscad-import.js';
|
|
24
|
+
import { exportThree } from './threejs.js';
|
|
19
25
|
|
|
20
26
|
/** OBJ and STL repeat every copy's triangles; beyond this the file gets too large for a browser tab. */
|
|
21
27
|
const OBJ_TRIANGLE_LIMIT = 4e6;
|
|
@@ -28,6 +34,12 @@ export const INPUT_KINDS = {
|
|
|
28
34
|
gltf: 'glTF (JSON)',
|
|
29
35
|
glb: 'glTF binary (GLB)',
|
|
30
36
|
step: 'STEP',
|
|
37
|
+
x3dv: 'X3D Classic',
|
|
38
|
+
x3d: 'X3D XML',
|
|
39
|
+
usda: 'USD ASCII',
|
|
40
|
+
usdc: 'USD crate',
|
|
41
|
+
usdz: 'USDZ',
|
|
42
|
+
scad: 'OpenSCAD',
|
|
31
43
|
};
|
|
32
44
|
|
|
33
45
|
export const OUTPUT_FORMATS = {
|
|
@@ -38,6 +50,10 @@ export const OUTPUT_FORMATS = {
|
|
|
38
50
|
obj: { label: 'Wavefront OBJ + MTL (.obj)', ext: '.obj', mime: 'model/obj', text: true },
|
|
39
51
|
stl: { label: 'STL for 3D printing (.stl)', ext: '.stl', mime: 'model/stl' },
|
|
40
52
|
step: { label: 'STEP, tessellated (.step)', ext: '.step', mime: 'model/step', text: true },
|
|
53
|
+
x3dv: { label: 'X3D Classic (.x3dv)', ext: '.x3dv', mime: 'model/x3d+vrml', text: true },
|
|
54
|
+
usda: { label: 'USD ASCII (.usda)', ext: '.usda', mime: 'model/vnd.usda', text: true },
|
|
55
|
+
scad: { label: 'OpenSCAD (.scad)', ext: '.scad', mime: 'text/x-openscad', text: true },
|
|
56
|
+
js: { label: 'three.js module (.js)', ext: '.js', mime: 'text/javascript', text: true },
|
|
41
57
|
};
|
|
42
58
|
|
|
43
59
|
/** Files that only accompany a main file (OBJ materials, glTF buffers and textures). */
|
|
@@ -61,6 +77,11 @@ export function detectKind(name, bytes) {
|
|
|
61
77
|
if (ext === 'stl' || isAsciiSTL(bytes)) return 'stl';
|
|
62
78
|
if (ext === 'gltf' || /^\s*\{[\s\S]*"asset"\s*:/.test(head)) return 'gltf';
|
|
63
79
|
if (ext === 'obj' || /^\s*(#[^\n]*\n\s*)*(mtllib|o|g|v|vt|vn|f|usemtl|s)\s/.test(head)) return 'obj';
|
|
80
|
+
if (ext === 'x3dv' || /^#X3D\b/.test(head)) return 'x3dv';
|
|
81
|
+
if (ext === 'x3d' || /<X3D[\s>]/i.test(head)) return 'x3d';
|
|
82
|
+
if (ext === 'usda' || /^#usda\b/.test(head)) return 'usda';
|
|
83
|
+
if (ext === 'usdc' || ext === 'usdz') return ext;
|
|
84
|
+
if (ext === 'scad') return 'scad';
|
|
64
85
|
if (ext === 'step' || ext === 'stp' || ext === 'p21') return 'step';
|
|
65
86
|
return null;
|
|
66
87
|
}
|
|
@@ -93,7 +114,7 @@ export async function importFiles(files, opts = {}) {
|
|
|
93
114
|
const main = list.find((f) => !COMPANION.test(f.name)) || list[0];
|
|
94
115
|
const companions = list.filter((f) => f !== main);
|
|
95
116
|
const kind = detectKind(main.name, main.bytes);
|
|
96
|
-
if (!kind) throw new Error(`${main.name}: unknown file type. The converter reads SVG, OBJ, STL, STEP (.step, .stp)
|
|
117
|
+
if (!kind) throw new Error(`${main.name}: unknown file type. The converter reads SVG, OBJ, STL, STEP (.step, .stp), glTF (.gltf, .glb), X3D (.x3d, .x3dv), USDA, OpenSCAD (.scad) and ES3D (.es3d) files.`);
|
|
97
118
|
const title = main.name.replace(/\.[^.]+$/, '') || 'untitled';
|
|
98
119
|
const svgOpts = { ...IMPORT_DEFAULTS.svg, ...(opts.svg || {}) };
|
|
99
120
|
const meshOpts = { ...IMPORT_DEFAULTS.mesh, ...(opts.mesh || {}) };
|
|
@@ -165,6 +186,23 @@ export async function importFiles(files, opts = {}) {
|
|
|
165
186
|
text = meshDocument(s, hasNormals);
|
|
166
187
|
break;
|
|
167
188
|
}
|
|
189
|
+
case 'x3dv':
|
|
190
|
+
case 'x3d':
|
|
191
|
+
case 'usda': {
|
|
192
|
+
const imported = kind === 'usda' ? importUSD(decode(main.bytes)) : importX3D(decode(main.bytes));
|
|
193
|
+
text = imported.text;
|
|
194
|
+
notes.push(...imported.notes);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
case 'usdc':
|
|
198
|
+
case 'usdz':
|
|
199
|
+
throw new Error('USDC and USDZ are not read here. This tool reads and writes USDA text; convert a crate with usdcat if you have the USD tools.');
|
|
200
|
+
case 'scad': {
|
|
201
|
+
const imported = importOpenSCAD(decode(main.bytes), { files: companions });
|
|
202
|
+
text = imported.text;
|
|
203
|
+
notes.push(...imported.notes);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
168
206
|
}
|
|
169
207
|
return { kind, name: main.name, title, text, warnings, notes };
|
|
170
208
|
}
|
|
@@ -186,11 +224,13 @@ function describeErrors(diagnostics, lineIndex) {
|
|
|
186
224
|
* Write an ES3D document in another format.
|
|
187
225
|
* @param {string|Uint8Array} source ES3D text
|
|
188
226
|
* @param {keyof OUTPUT_FORMATS} format
|
|
189
|
-
* @param {{baseName?: string, lod?: number, compiled?: object, instancing?: boolean}} [opts]
|
|
190
|
-
* compiled: an existing compile() result for the same source (saves compiling twice)
|
|
191
|
-
*
|
|
227
|
+
* @param {{baseName?: string, lod?: number, compiled?: object, instancing?: boolean, structure?: 'keep'|'flatten'}} [opts]
|
|
228
|
+
* compiled: an existing compile() result for the same source (saves compiling twice).
|
|
229
|
+
* structure applies to X3D, USD, OpenSCAD and three.js: keep (the default) writes
|
|
230
|
+
* primitives and the target's own reuse; flatten writes the same triangles as glTF and OBJ.
|
|
231
|
+
* @returns {{files: {name: string, data: Uint8Array|string, mime: string}[], triangles?: number, notes?: string[]}}
|
|
192
232
|
*/
|
|
193
|
-
export function exportFormat(source, format, { baseName = 'scene', lod = 0, compiled = null, instancing = true } = {}) {
|
|
233
|
+
export function exportFormat(source, format, { baseName = 'scene', lod = 0, compiled = null, instancing = true, structure = 'keep' } = {}) {
|
|
194
234
|
const spec = OUTPUT_FORMATS[format];
|
|
195
235
|
if (!spec) throw new Error(`Unknown output format ${format}.`);
|
|
196
236
|
const name = baseName + spec.ext;
|
|
@@ -206,6 +246,14 @@ export function exportFormat(source, format, { baseName = 'scene', lod = 0, comp
|
|
|
206
246
|
const r = compiled || compile(source);
|
|
207
247
|
if (!r.ok) throw new Error(describeErrors(r.diagnostics, r.doc && r.doc.lineIndex));
|
|
208
248
|
const meshOf = (e, l) => r.cache.mesh(e, Math.min(l, e.lods - 1));
|
|
249
|
+
if (format === 'x3dv' || format === 'usda' || format === 'scad' || format === 'js') {
|
|
250
|
+
const model = buildExportModel(r, meshOf, { lod, structure: structure === 'flatten' ? 'flatten' : 'keep' });
|
|
251
|
+
if (model.meshTriangles > OBJ_TRIANGLE_LIMIT) {
|
|
252
|
+
throw new Error(`At this level of detail the scene has ${model.meshTriangles.toLocaleString('en-US')} triangles, too many for this file (limit ${OBJ_TRIANGLE_LIMIT.toLocaleString('en-US')}). Choose a coarser detail.`);
|
|
253
|
+
}
|
|
254
|
+
const written = format === 'x3dv' ? exportX3D(model) : format === 'usda' ? exportUSD(model) : format === 'scad' ? exportOpenSCAD(model) : exportThree(model);
|
|
255
|
+
return { ...one(written.text), notes: written.notes };
|
|
256
|
+
}
|
|
209
257
|
if (format === 'glb') return one(exportGLB(r.scene, meshOf, { lod, instancing }));
|
|
210
258
|
if (format === 'obj') {
|
|
211
259
|
let triangles = 0;
|