@es3d/es3d 1.0.2 → 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 +16 -5
- package/browser/es3d-stage.min.js +10349 -0
- package/package.json +4 -2
- package/scripts/es3d-cli.mjs +28 -5
- package/src/index.js +10 -3
- 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/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/render/stage.js +85 -10
- package/src/scene/flatten.js +208 -4
- package/src/scene/scene-graph.js +13 -2
- package/src/stage.js +8 -0
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
|
@@ -19,7 +19,7 @@ import { compile } from '../src/pipeline.js';
|
|
|
19
19
|
import { exportOBJ } from '../src/io/obj.js';
|
|
20
20
|
import { exportSTL } from '../src/io/stl.js';
|
|
21
21
|
import { exportSTEP } from '../src/io/step.js';
|
|
22
|
-
import { detectKind, importFiles } from '../src/io/convert.js';
|
|
22
|
+
import { detectKind, importFiles, exportFormat } from '../src/io/convert.js';
|
|
23
23
|
import { exportableInstances } from '../src/io/external-scene.js';
|
|
24
24
|
import { optimize, expandProcedural } from '../src/optimize/optimizer.js';
|
|
25
25
|
|
|
@@ -39,16 +39,22 @@ Commands
|
|
|
39
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
|
|
56
|
+
--lod <n> level of detail for mesh export (default 0 = finest)
|
|
57
|
+
--flatten x3d, usd, scad, js: write triangles instead of primitives
|
|
52
58
|
--instancing glb always writes EXT_mesh_gpu_instancing
|
|
53
59
|
--quantize glb always writes KHR_mesh_quantization
|
|
54
60
|
--min / --pretty force minified / pretty output
|
|
@@ -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;
|
|
@@ -356,6 +363,22 @@ async function main() {
|
|
|
356
363
|
process.stderr.write(`${out.triangles.toLocaleString('en-US')} triangles\n`);
|
|
357
364
|
break;
|
|
358
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
|
+
}
|
|
359
382
|
case 'import': {
|
|
360
383
|
const name = opts.input === '-' ? 'stdin' : path.basename(opts.input);
|
|
361
384
|
const files = [{ name, bytes }];
|
package/src/index.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
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;
|