@es3d/es3d 1.0.2 → 1.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@es3d/es3d",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
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
  ],
@@ -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
- import convert SVG, OBJ (+MTL), STL, STEP or glTF (.gltf + .bin, .glb) to ES3D:
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 obj, stl and step (default 0 = finest)
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) and glTF (.gltf, .glb) and ES3D (.es3d) files.`);
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
- * @returns {{files: {name: string, data: Uint8Array|string, mime: string}[], triangles?: number}}
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;