@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 +1 -1
- package/src/io/threejs.js +19 -9
- package/src/io/usd.js +30 -6
- package/src/io/x3d.js +6 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@es3d/es3d",
|
|
3
|
-
"version": "1.1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
465
|
-
|
|
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
|
-
|
|
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);
|