@es3d/es3d 1.1.0 → 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.1.0",
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",
package/src/io/threejs.js CHANGED
@@ -44,7 +44,11 @@ export function exportThree(model) {
44
44
  const tally = new Map();
45
45
  const add = (key, k = 1) => tally.set(key, (tally.get(key) || 0) + k);
46
46
  const lines = [];
47
+ // Geometries and materials are declared once, at the top of createScene(), so a
48
+ // repeat factory and the code after it can both use them.
49
+ const defs = [];
47
50
  const geos = new Map();
51
+ const bakedGeos = new Map();
48
52
  const mats = new Map();
49
53
  let seq = 0;
50
54
  const fresh = (p) => `${p}${seq++}`;
@@ -59,10 +63,10 @@ export function exportThree(model) {
59
63
  const c = m.baseColorSrgb;
60
64
  const side = (m.doubleSided || forceDouble) ? ', side: THREE.DoubleSide' : '';
61
65
  if (m.unlit) {
62
- lines.push(` const ${id} = new THREE.MeshBasicMaterial({ color: new THREE.Color(${n(c[0])}, ${n(c[1])}, ${n(c[2])}), transparent: ${m.opacity < 0.999}, opacity: ${n(m.opacity)}${side} });`);
66
+ defs.push(` const ${id} = new THREE.MeshBasicMaterial({ color: new THREE.Color(${n(c[0])}, ${n(c[1])}, ${n(c[2])}), transparent: ${m.opacity < 0.999}, opacity: ${n(m.opacity)}${side} });`);
63
67
  return id;
64
68
  }
65
- lines.push(` const ${id} = new THREE.MeshStandardMaterial({ color: new THREE.Color(${n(c[0])}, ${n(c[1])}, ${n(c[2])}), metalness: ${n(m.metallic)}, roughness: ${n(m.roughness)}, transparent: ${m.opacity < 0.999}, opacity: ${n(m.opacity)}${side} });`);
69
+ defs.push(` const ${id} = new THREE.MeshStandardMaterial({ color: new THREE.Color(${n(c[0])}, ${n(c[1])}, ${n(c[2])}), metalness: ${n(m.metallic)}, roughness: ${n(m.roughness)}, transparent: ${m.opacity < 0.999}, opacity: ${n(m.opacity)}${side} });`);
66
70
  return id;
67
71
  };
68
72
 
@@ -71,7 +75,7 @@ export function exportThree(model) {
71
75
  if (geos.has(key)) return geos.get(key);
72
76
  const id = fresh('g');
73
77
  geos.set(key, id);
74
- lines.push(` const ${id} = ${geometryExpr(solid)};`);
78
+ defs.push(` const ${id} = ${geometryExpr(solid)};`);
75
79
  return id;
76
80
  };
77
81
 
@@ -116,12 +120,17 @@ export function exportThree(model) {
116
120
  const baked = (node) => {
117
121
  const mesh = node.solid.kind === 'mesh' || node.solid.kind === 'lines' ? node.solid : model.bakedMesh(node.entry);
118
122
  add(node.solid.kind === 'mesh' || node.solid.kind === 'lines' ? 'mesh' : node.solid.kind);
119
- const g = fresh('g');
120
- const positions = `new Float32Array([${nums(mesh.positions)}])`;
121
- lines.push(` const ${g} = new THREE.BufferGeometry();`);
122
- lines.push(` ${g}.setAttribute('position', new THREE.BufferAttribute(${positions}, 3));`);
123
- if (mesh.normals) lines.push(` ${g}.setAttribute('normal', new THREE.BufferAttribute(new Float32Array([${nums(mesh.normals)}]), 3));`);
124
- lines.push(` ${g}.setIndex([${Array.from(mesh.indices).join(', ')}]);`);
123
+ const geoKey = mesh.key || mesh;
124
+ let g = bakedGeos.get(geoKey);
125
+ if (!g) {
126
+ g = fresh('g');
127
+ bakedGeos.set(geoKey, g);
128
+ const positions = `new Float32Array([${nums(mesh.positions)}])`;
129
+ defs.push(` const ${g} = new THREE.BufferGeometry();`);
130
+ defs.push(` ${g}.setAttribute('position', new THREE.BufferAttribute(${positions}, 3));`);
131
+ if (mesh.normals) defs.push(` ${g}.setAttribute('normal', new THREE.BufferAttribute(new Float32Array([${nums(mesh.normals)}]), 3));`);
132
+ defs.push(` ${g}.setIndex([${Array.from(mesh.indices).join(', ')}]);`);
133
+ }
125
134
  const m = material(node.material, node.doubleSided);
126
135
  const id = fresh('o');
127
136
  const kind = mesh.kind === 'lines' ? 'LineSegments' : 'Mesh';
@@ -289,6 +298,7 @@ export function exportThree(model) {
289
298
  ${helper}
290
299
  export function createScene() {
291
300
  const root = new THREE.Group();
301
+ ${defs.join('\n')}
292
302
  ${lines.join('\n')}
293
303
  return root;
294
304
  }
package/src/io/usd.js CHANGED
@@ -220,7 +220,7 @@ export function exportUSD(model) {
220
220
  const inst = g.matrix || matrix;
221
221
  if ((uses.get(key) || 0) > 1) {
222
222
  const id = ensureProto(shape);
223
- return ` def Xform "${name}" (\n prepend references = </World/Prototypes/${id}>\n ) {\n token visibility = "inherited"\n${indent(xformBlock(inst, ''), 12)}${indent(bind, 12)} }`;
223
+ return ` def "${name}" (\n prepend references = </World/Prototypes/${id}>\n ) {\n token visibility = "inherited"\n${indent(xformBlock(inst, ''), 12)}${indent(bind, 12)} }`;
224
224
  }
225
225
  const combined = g.matrix ? inst : combine(matrix, g.local);
226
226
  return ` def ${g.type} "${name}" {\n${indent(g.body, 12)}${indent(xformBlock(combined, ''), 12)}${indent(bind, 12)} }`;
@@ -439,6 +439,27 @@ function blockNamed(text, type, name) {
439
439
  return sliceBlock(text, open);
440
440
  }
441
441
 
442
+ /** The text of a prim's body without the bodies of the prims defined inside it. */
443
+ function ownText(body) {
444
+ let out = '';
445
+ let i = 0;
446
+ while (i < body.length) {
447
+ const at = body.indexOf('def ', i);
448
+ if (at < 0) { out += body.slice(i); break; }
449
+ out += body.slice(i, at);
450
+ const open = body.indexOf('{', at);
451
+ if (open < 0) break;
452
+ let depth = 0;
453
+ let j = open;
454
+ for (; j < body.length; j++) {
455
+ if (body[j] === '{') depth++;
456
+ else if (body[j] === '}' && --depth === 0) break;
457
+ }
458
+ i = j + 1;
459
+ }
460
+ return out;
461
+ }
462
+
442
463
  function listPrims(text) {
443
464
  const out = [];
444
465
  let i = 0;
@@ -447,22 +468,25 @@ function listPrims(text) {
447
468
  if (at < 0) break;
448
469
  const before = text.slice(i, at);
449
470
  if ((before.match(/\{/g) || []).length !== (before.match(/\}/g) || []).length) { i = at + 4; continue; }
450
- const head = /^def\s+([A-Za-z0-9_]+)\s+"([^"]+)"\s*(\([^)]*\))?/.exec(text.slice(at));
471
+ const head = /^def\s+(?:([A-Za-z0-9_]+)\s+)?"([^"]+)"\s*(\([^)]*\))?/.exec(text.slice(at));
451
472
  if (!head) { i = at + 4; continue; }
452
473
  const open = text.indexOf('{', at + head[0].length - 1);
453
474
  if (open < 0) break;
454
475
  const body = sliceBlock(text, open);
455
- out.push({ type: head[1], name: head[2], meta: head[3] || '', body });
476
+ out.push({ type: head[1] || '', name: head[2], meta: head[3] || '', body });
456
477
  i = open + body.length + 2;
457
478
  }
458
479
  return out;
459
480
  }
460
481
 
461
482
  function emitPrim(prim, lib, materials, paint, notes, parent, inherited) {
462
- const local = readXform(prim.body);
483
+ // A prim's own transform and binding, not those of the prims nested in it.
484
+ const own = ownText(prim.body);
485
+ const local = readXform(own);
463
486
  const matrix = combine(parent, local);
464
- const style = paint(prim.body, materials) || inherited || '';
465
- if (prim.type === 'Xform') {
487
+ const style = paint(own, materials) || inherited || '';
488
+ // A typeless def takes its type from the prototype it references.
489
+ if (prim.type === 'Xform' || prim.type === '') {
466
490
  const ref = /references\s*=\s*<([^>]+)>/.exec(prim.meta);
467
491
  const target = ref ? lib.get(ref[1].split('/').pop()) : null;
468
492
  if (target) return emitPrim(target, lib, materials, paint, notes, matrix, style);
package/src/io/x3d.js CHANGED
@@ -592,7 +592,7 @@ export function importX3D(text) {
592
592
  i = step.i;
593
593
  }
594
594
  materialAttrs.materials = [];
595
- const parts = nodes.map((node) => emitEs3d(node, notes)).filter(Boolean);
595
+ const parts = nodes.map((node) => emitEs3d(node, notes, defs)).filter(Boolean);
596
596
  const mats = materialAttrs.materials;
597
597
  materialAttrs.materials = [];
598
598
  const markup = `<es3d version="1">\n${mats.join('\n')}${mats.length ? '\n' : ''}${parts.join('\n')}\n</es3d>\n`;
@@ -734,7 +734,7 @@ function numAttr(list, count) {
734
734
  return nums.slice(0, count);
735
735
  }
736
736
 
737
- function emitEs3d(node, notes) {
737
+ function emitEs3d(node, notes, defs) {
738
738
  if (!node) return '';
739
739
  if (node.use) return `<use ref="${esc(node.use)}"/>`;
740
740
  const f = node.fields || {};
@@ -743,11 +743,13 @@ function emitEs3d(node, notes) {
743
743
  if (node.name === 'Transform' || node.name === 'Group' || node.name === 'Anchor' || node.name === 'Collision' || node.name === 'Switch' || node.name === 'Billboard') {
744
744
  if (node.name !== 'Transform' && node.name !== 'Group') notes.push(`<${node.name}> was written as a group.`);
745
745
  const tr = x3dTransform(f);
746
- const body = kids.map((c) => emitEs3d(c, notes)).filter(Boolean).join('\n');
746
+ const body = kids.map((c) => emitEs3d(c, notes, defs)).filter(Boolean).join('\n');
747
747
  return `<group${id}${tr}>\n${body}\n</group>`;
748
748
  }
749
749
  if (node.name === 'Shape') {
750
- const geom = f.geometry && f.geometry.name ? f.geometry : null;
750
+ // geometry USE G names a node defined earlier; draw that geometry here too.
751
+ const used = f.geometry && f.geometry.use && defs ? defs.get(f.geometry.use) : null;
752
+ const geom = f.geometry && f.geometry.name ? f.geometry : (used && used.name ? used : null);
751
753
  const style = materialAttrs(f.appearance, notes);
752
754
  if (!geom) return '';
753
755
  return emitGeom(geom, id, style, notes);