@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/src/render/stage.js
CHANGED
|
@@ -20,11 +20,16 @@
|
|
|
20
20
|
// the page is idle, and read back instead of tessellated on the next visit.
|
|
21
21
|
//
|
|
22
22
|
// Reduced motion (prefers-reduced-motion) stops clock animations and the
|
|
23
|
-
// automatic orbit; scroll-driven cameras jump between stops.
|
|
23
|
+
// automatic orbit; scroll-driven cameras jump between stops. stage.reducedMotion
|
|
24
|
+
// reads that preference on each access, and the clock checks it every frame.
|
|
25
|
+
//
|
|
26
|
+
// clock: 'always' advances time even when the document has no <animate>.
|
|
27
|
+
// background: true selects that clock and leaves the camera still, for a
|
|
28
|
+
// page that moves shapes itself with stage.setTransform(id, matrix).
|
|
24
29
|
|
|
25
30
|
import { compile } from '../pipeline.js';
|
|
26
31
|
import { formatDiagnostics } from '../format/parse.js';
|
|
27
|
-
import { updateDynamic } from '../scene/flatten.js';
|
|
32
|
+
import { applyLocalMatrices, updateDynamic } from '../scene/flatten.js';
|
|
28
33
|
import { pick } from '../scene/picking.js';
|
|
29
34
|
import { createRenderer, PROGRESSIVE_BUDGET_MS } from './renderer.js';
|
|
30
35
|
import { openGeometryStore, persistGeometry } from '../cache/geometry-store.js';
|
|
@@ -32,9 +37,36 @@ import { Camera } from './camera.js';
|
|
|
32
37
|
import { OrbitControls } from './orbit-controls.js';
|
|
33
38
|
import { ScrollTimeline } from './scroll-timeline.js';
|
|
34
39
|
|
|
35
|
-
const
|
|
40
|
+
const prefersReducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
36
41
|
const idle = (fn) => (typeof requestIdleCallback === 'function' ? requestIdleCallback(fn, { timeout: 500 }) : setTimeout(fn, 16));
|
|
37
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Clock and orbit flags for createStage. background: true runs the clock
|
|
45
|
+
* and does not orbit, unless clock or autoOrbit is set explicitly.
|
|
46
|
+
* @param {object} [opts]
|
|
47
|
+
* @param {boolean} [reduced]
|
|
48
|
+
*/
|
|
49
|
+
export function stageOptions(opts = {}, reduced = false) {
|
|
50
|
+
const background = opts.background === true;
|
|
51
|
+
const always = opts.clock === 'always' || (opts.clock == null && background);
|
|
52
|
+
const autoOrbit = opts.autoOrbit !== undefined ? opts.autoOrbit !== false : !background;
|
|
53
|
+
return {
|
|
54
|
+
clock: always ? 'always' : 'document',
|
|
55
|
+
playing: opts.playing !== false && !reduced,
|
|
56
|
+
auto: autoOrbit && !reduced,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when this frame should add dt to the stage clock. */
|
|
61
|
+
export function clockAdvances(state, scene, reduced) {
|
|
62
|
+
return !!(state.playing && !reduced && (state.clock === 'always' || (scene.animation && scene.animation.time)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** True when this frame should turn the automatic orbit. */
|
|
66
|
+
export function orbitAdvances(state, reduced) {
|
|
67
|
+
return !!(state.auto && !reduced);
|
|
68
|
+
}
|
|
69
|
+
|
|
38
70
|
/** Names (name or id) on an element and its ancestors, nearest first. */
|
|
39
71
|
export function ancestorNames(doc, uid) {
|
|
40
72
|
const names = [];
|
|
@@ -57,7 +89,12 @@ const RESTORE_WAIT_MS = 50;
|
|
|
57
89
|
* @param {HTMLCanvasElement} canvas sized by its parent element
|
|
58
90
|
* @param {object} [opts]
|
|
59
91
|
* @param {boolean} [opts.playing=true] play clock animations
|
|
60
|
-
* @param {boolean} [opts.autoOrbit=true] turn slowly around the target until someone drags
|
|
92
|
+
* @param {boolean} [opts.autoOrbit=true] turn slowly around the target until someone drags.
|
|
93
|
+
* background: true starts this off
|
|
94
|
+
* @param {'always'|'document'} [opts.clock] 'always' advances time with no <animate>.
|
|
95
|
+
* background: true selects 'always'
|
|
96
|
+
* @param {boolean} [opts.background=false] a still camera and a running clock, for a script
|
|
97
|
+
* that moves shapes with setTransform. <controls mode="none"/> turns dragging off
|
|
61
98
|
* @param {number} [opts.orbitSpeed=0.22] radians per second
|
|
62
99
|
* @param {boolean} [opts.interactive=true] false: no dragging, whatever the document says
|
|
63
100
|
* @param {HTMLElement} [opts.scroll] section whose scroll position drives timeline="scroll"
|
|
@@ -80,15 +117,16 @@ export async function createStage(canvas, opts = {}) {
|
|
|
80
117
|
target.style.display = 'block';
|
|
81
118
|
const camera = new Camera();
|
|
82
119
|
const controls = new OrbitControls(target, camera);
|
|
83
|
-
const motion =
|
|
120
|
+
const motion = stageOptions(opts, prefersReducedMotion());
|
|
84
121
|
const state = {
|
|
85
122
|
result: null,
|
|
86
123
|
time: 0,
|
|
87
124
|
progress: 0,
|
|
88
125
|
last: 0,
|
|
89
126
|
disposed: false,
|
|
90
|
-
|
|
91
|
-
|
|
127
|
+
clock: motion.clock,
|
|
128
|
+
playing: motion.playing,
|
|
129
|
+
auto: motion.auto,
|
|
92
130
|
orbitSpeed: opts.orbitSpeed ?? 0.22,
|
|
93
131
|
onScreen: true,
|
|
94
132
|
// A frame is drawn only when something visible changed: the camera, the
|
|
@@ -250,6 +288,43 @@ export async function createStage(canvas, opts = {}) {
|
|
|
250
288
|
set playing(v) {
|
|
251
289
|
state.playing = !!v;
|
|
252
290
|
},
|
|
291
|
+
/** True while the visitor prefers reduced motion. The clock and the automatic orbit stay still. */
|
|
292
|
+
get reducedMotion() {
|
|
293
|
+
return prefersReducedMotion();
|
|
294
|
+
},
|
|
295
|
+
/**
|
|
296
|
+
* Replace the local matrix of each element. An entry is `[id, matrix]`
|
|
297
|
+
* or `{id, matrix}`. The matrix is column-major, 16 numbers, the same
|
|
298
|
+
* layout as matrix(). Every matrix is stored first, then each affected
|
|
299
|
+
* part of the scene is walked once. The returned array has false for an
|
|
300
|
+
* id that is missing or drawn in more than one place (a shared
|
|
301
|
+
* definition, or a copy made by repeat or scatter).
|
|
302
|
+
* @param {Array<[string, ArrayLike<number>]|{id: string, matrix: ArrayLike<number>}>} entries
|
|
303
|
+
*/
|
|
304
|
+
setTransforms(entries) {
|
|
305
|
+
const list = Array.from(entries || []);
|
|
306
|
+
const res = state.result;
|
|
307
|
+
const graph = res && res.graph;
|
|
308
|
+
const byId = graph && graph.byId;
|
|
309
|
+
const nodes = list.map((entry) => byId && byId.get(Array.isArray(entry) ? entry[0] : entry && entry.id));
|
|
310
|
+
const matrices = list.map((entry) => Array.isArray(entry) ? entry[1] : entry && entry.matrix);
|
|
311
|
+
const applied = graph && res.scene
|
|
312
|
+
? applyLocalMatrices(res.scene, graph, nodes, matrices, state.time, state.progress)
|
|
313
|
+
: list.map(() => false);
|
|
314
|
+
if (applied.some(Boolean)) state.dirty = true;
|
|
315
|
+
return applied;
|
|
316
|
+
},
|
|
317
|
+
/**
|
|
318
|
+
* Replace the local matrix of the element with this id. The matrix is
|
|
319
|
+
* column-major, 16 numbers, the same layout as matrix(). Returns false
|
|
320
|
+
* when the id is missing or that element is drawn in more than one place
|
|
321
|
+
* (a shared definition, or a copy made by repeat or scatter).
|
|
322
|
+
* @param {string} id
|
|
323
|
+
* @param {ArrayLike<number>} matrix
|
|
324
|
+
*/
|
|
325
|
+
setTransform(id, matrix) {
|
|
326
|
+
return this.setTransforms([[id, matrix]])[0] === true;
|
|
327
|
+
},
|
|
253
328
|
/** Draw the next frame even if nothing the stage tracks has changed. */
|
|
254
329
|
invalidate,
|
|
255
330
|
get result() {
|
|
@@ -320,11 +395,11 @@ export async function createStage(canvas, opts = {}) {
|
|
|
320
395
|
if (!state.width || !state.height) return;
|
|
321
396
|
const res = state.result;
|
|
322
397
|
const scene = res.scene;
|
|
323
|
-
|
|
324
|
-
if (state
|
|
398
|
+
const reduced = prefersReducedMotion();
|
|
399
|
+
if (clockAdvances(state, scene, reduced)) state.time += dt;
|
|
325
400
|
if (state.timeline && state.timeline.update(dt)) state.progress = state.timeline.value;
|
|
326
401
|
if (opts.onFrame) opts.onFrame(dt, api);
|
|
327
|
-
if (state
|
|
402
|
+
if (orbitAdvances(state, reduced)) {
|
|
328
403
|
const prev = controls.snapshot();
|
|
329
404
|
const dx = camera.position[0] - camera.target[0];
|
|
330
405
|
const dz = camera.position[2] - camera.target[2];
|
package/src/scene/flatten.js
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
//
|
|
16
16
|
// The RenderScene is renderer-agnostic; the OBJ and glTF importers produce the
|
|
17
17
|
// same structure, which lets the benchmark render all formats identically.
|
|
18
|
+
//
|
|
19
|
+
// A node the walker visits once remembers that placement: the parent matrix
|
|
20
|
+
// and the cursors at its start. applyLocalMatrices() stores every script
|
|
21
|
+
// matrix first, then rewrites each affected subtree once. A later animation
|
|
22
|
+
// walk reads the script matrix, so the document's own animation does not put
|
|
23
|
+
// the old matrix back. A node visited again, or written by the instance
|
|
24
|
+
// kernel, is shared and left alone.
|
|
18
25
|
|
|
19
26
|
import * as mat4 from '../math/mat4.js';
|
|
20
27
|
import { mulberry32 } from '../math/random.js';
|
|
@@ -160,6 +167,33 @@ class Walker {
|
|
|
160
167
|
return this.stack[depth];
|
|
161
168
|
}
|
|
162
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Remember the one placement of a node. A second visit drops it: scatter,
|
|
172
|
+
* repeat and a definition used twice do not have one matrix to replace.
|
|
173
|
+
*/
|
|
174
|
+
notePlace(node, parent, material, color, sel, lodNode, lodChild) {
|
|
175
|
+
if (node.place === 'many') return;
|
|
176
|
+
if (node.place) {
|
|
177
|
+
node.place = 'many';
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const copy = new Float64Array(16);
|
|
181
|
+
copy.set(parent);
|
|
182
|
+
node.place = {
|
|
183
|
+
parent: copy,
|
|
184
|
+
material,
|
|
185
|
+
color,
|
|
186
|
+
sel,
|
|
187
|
+
lodNode,
|
|
188
|
+
lodChild,
|
|
189
|
+
noCollide: this.noCollide,
|
|
190
|
+
rowStart: this.cursor,
|
|
191
|
+
lightStart: this.lightCursor,
|
|
192
|
+
cameraStart: this.cameraCursor,
|
|
193
|
+
switchStart: this.switchCursor,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
163
197
|
/** Local matrix of a node at the current time. */
|
|
164
198
|
localOf(node, out) {
|
|
165
199
|
if (!node.anims.length) {
|
|
@@ -177,7 +211,7 @@ class Walker {
|
|
|
177
211
|
|
|
178
212
|
walk(node, parent, material, color, sel, lodNode, lodChild, depth) {
|
|
179
213
|
if (!node || !node.visible) return;
|
|
180
|
-
|
|
214
|
+
if (!this.overwrite) this.notePlace(node, parent, material, color, sel, lodNode, lodChild);
|
|
181
215
|
const isSegmentRoot = node.anims.length > 0 && this.segmentDepth === 0 && !this.overwrite;
|
|
182
216
|
let seg = null;
|
|
183
217
|
if (isSegmentRoot) {
|
|
@@ -213,13 +247,14 @@ class Walker {
|
|
|
213
247
|
else if (node.kind === 'camera') this.emitCamera(node, parent);
|
|
214
248
|
else {
|
|
215
249
|
let world;
|
|
216
|
-
|
|
250
|
+
const scripted = node.scriptLocal;
|
|
251
|
+
if (!scripted && !node.local && !node.anims.length) {
|
|
217
252
|
// identity local transform: children write only to deeper stack slots, so the parent can be shared
|
|
218
253
|
world = parent;
|
|
219
254
|
} else {
|
|
220
255
|
world = this.mat(depth);
|
|
221
|
-
//
|
|
222
|
-
const local = node.anims.length ? this.localOf(node, this.tmp) : node.local;
|
|
256
|
+
// a script matrix replaces the element's own transform, including its <animate>
|
|
257
|
+
const local = scripted || (node.anims.length ? this.localOf(node, this.tmp) : node.local);
|
|
223
258
|
mat4.multiply(world, parent, local);
|
|
224
259
|
}
|
|
225
260
|
switch (node.kind) {
|
|
@@ -256,6 +291,12 @@ class Walker {
|
|
|
256
291
|
seg.switchEnd = this.switchCursor;
|
|
257
292
|
this.scene.dynamic.push(seg);
|
|
258
293
|
}
|
|
294
|
+
if (!this.overwrite && node.place && node.place !== 'many') {
|
|
295
|
+
node.place.rowEnd = this.cursor;
|
|
296
|
+
node.place.lightEnd = this.lightCursor;
|
|
297
|
+
node.place.cameraEnd = this.cameraCursor;
|
|
298
|
+
node.place.switchEnd = this.switchCursor;
|
|
299
|
+
}
|
|
259
300
|
}
|
|
260
301
|
|
|
261
302
|
emitShape(node, world, material, color, uid, lodNode, lodChild, depth) {
|
|
@@ -438,6 +479,7 @@ class Walker {
|
|
|
438
479
|
}
|
|
439
480
|
this.cursor = cursor;
|
|
440
481
|
this.switchCursor = sw;
|
|
482
|
+
markPlacedMany(node);
|
|
441
483
|
return true;
|
|
442
484
|
}
|
|
443
485
|
|
|
@@ -728,6 +770,168 @@ export function flattenScene(graph, opts = {}) {
|
|
|
728
770
|
return scene;
|
|
729
771
|
}
|
|
730
772
|
|
|
773
|
+
/**
|
|
774
|
+
* The instance kernel wrote every copy of this subtree, so none of those
|
|
775
|
+
* nodes has a single placement a script can replace.
|
|
776
|
+
*/
|
|
777
|
+
function markPlacedMany(node) {
|
|
778
|
+
const visit = (n) => {
|
|
779
|
+
if (!n || n.place === 'many') return;
|
|
780
|
+
n.place = 'many';
|
|
781
|
+
if (n.target) visit(n.target);
|
|
782
|
+
if (n.children) for (const c of n.children) visit(c);
|
|
783
|
+
};
|
|
784
|
+
if (node.target) visit(node.target);
|
|
785
|
+
if (node.children) for (const c of node.children) visit(c);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* The animated subtree that contains this node, if its parent matrix depends
|
|
790
|
+
* on the clock. The segment's own parent is above that animation, so rewalking
|
|
791
|
+
* the segment at the current time keeps a script matrix in the right place.
|
|
792
|
+
*/
|
|
793
|
+
function segmentFor(scene, node) {
|
|
794
|
+
for (const seg of scene.dynamic) {
|
|
795
|
+
for (let n = node; n; n = n.parent) if (n === seg.node) return seg;
|
|
796
|
+
}
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function finiteMatrix(matrix) {
|
|
801
|
+
if (!matrix || matrix.length < 16) return false;
|
|
802
|
+
for (let i = 0; i < 16; i++) if (!Number.isFinite(+matrix[i])) return false;
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function writeScriptLocal(node, matrix) {
|
|
807
|
+
let local = node.scriptLocal;
|
|
808
|
+
if (!local) local = node.scriptLocal = new Float64Array(16);
|
|
809
|
+
for (let i = 0; i < 16; i++) local[i] = +matrix[i];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function under(node, ancestor) {
|
|
813
|
+
for (let n = node; n; n = n.parent) if (n === ancestor) return true;
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** The deepest node that contains every entry, itself included. */
|
|
818
|
+
function deepestCovering(nodes) {
|
|
819
|
+
for (let p = nodes[0]; p; p = p.parent) {
|
|
820
|
+
if (nodes.every((n) => n === p || under(n, p))) return p;
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* One walk root per cluster. A shared parent of several moves is walked
|
|
827
|
+
* once. The document root is not that parent: two unrelated shapes keep
|
|
828
|
+
* two small walks.
|
|
829
|
+
*/
|
|
830
|
+
function clusterRoots(nodes, root) {
|
|
831
|
+
if (nodes.length <= 1) return nodes.slice();
|
|
832
|
+
const common = deepestCovering(nodes);
|
|
833
|
+
if (common && common !== root && common.place && common.place !== 'many') return [common];
|
|
834
|
+
const groups = new Map();
|
|
835
|
+
for (const node of nodes) {
|
|
836
|
+
let top = node;
|
|
837
|
+
for (let p = node.parent; p && p !== root && p.place && p.place !== 'many'; p = p.parent) top = p;
|
|
838
|
+
let list = groups.get(top);
|
|
839
|
+
if (!list) groups.set(top, list = []);
|
|
840
|
+
list.push(node);
|
|
841
|
+
}
|
|
842
|
+
const out = [];
|
|
843
|
+
for (const [top, members] of groups) {
|
|
844
|
+
if (members.length > 1 && top.place && top.place !== 'many' && top !== root) out.push(top);
|
|
845
|
+
else if (members.length === nodes.length) for (const member of members) out.push(member);
|
|
846
|
+
else out.push(...clusterRoots(members, root));
|
|
847
|
+
}
|
|
848
|
+
return out;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function rewalkAt(w, rowStart, lightStart, cameraStart, switchStart, noCollide, node, parent, material, color, sel, lodNode, lodChild) {
|
|
852
|
+
w.cursor = rowStart;
|
|
853
|
+
w.lightCursor = lightStart;
|
|
854
|
+
w.cameraCursor = cameraStart;
|
|
855
|
+
w.switchCursor = switchStart;
|
|
856
|
+
w.segmentDepth = 0;
|
|
857
|
+
w.noCollide = noCollide;
|
|
858
|
+
w.walk(node, parent, material, color, sel, lodNode, lodChild, 0);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Replace the local matrix of each node and rewrite the rows once per
|
|
863
|
+
* affected subtree. Each entry of the returned array is false when that
|
|
864
|
+
* node is missing, the matrix is not 16 finite numbers, or the element is
|
|
865
|
+
* shared (a definition used twice, a repeat or scatter copy, or a subtree
|
|
866
|
+
* the instance kernel expanded). Column-major, the same layout as matrix().
|
|
867
|
+
* @param {RenderScene} scene
|
|
868
|
+
* @param {object} graph
|
|
869
|
+
* @param {import('./scene-graph.js').SceneNode[]} nodes
|
|
870
|
+
* @param {ArrayLike<number>[]} matrices
|
|
871
|
+
*/
|
|
872
|
+
export function applyLocalMatrices(scene, graph, nodes, matrices, time = 0, progress = 0) {
|
|
873
|
+
const list = nodes || [];
|
|
874
|
+
const ok = new Array(list.length);
|
|
875
|
+
const accepted = [];
|
|
876
|
+
const seen = new Set();
|
|
877
|
+
for (let i = 0; i < list.length; i++) {
|
|
878
|
+
const node = list[i];
|
|
879
|
+
const matrix = matrices && matrices[i];
|
|
880
|
+
if (!scene || !graph || !node || !node.place || node.place === 'many' || !finiteMatrix(matrix)) {
|
|
881
|
+
ok[i] = false;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
writeScriptLocal(node, matrix);
|
|
885
|
+
ok[i] = true;
|
|
886
|
+
if (!seen.has(node)) {
|
|
887
|
+
seen.add(node);
|
|
888
|
+
accepted.push(node);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (!accepted.length) return ok;
|
|
892
|
+
|
|
893
|
+
const segments = [];
|
|
894
|
+
const staticNodes = [];
|
|
895
|
+
const seenSeg = new Set();
|
|
896
|
+
for (const node of accepted) {
|
|
897
|
+
const seg = segmentFor(scene, node);
|
|
898
|
+
if (seg) {
|
|
899
|
+
if (!seenSeg.has(seg)) {
|
|
900
|
+
seenSeg.add(seg);
|
|
901
|
+
segments.push(seg);
|
|
902
|
+
}
|
|
903
|
+
} else staticNodes.push(node);
|
|
904
|
+
}
|
|
905
|
+
const roots = clusterRoots(staticNodes, graph.root);
|
|
906
|
+
const w = new Walker(scene, graph, time, progress);
|
|
907
|
+
w.overwrite = true;
|
|
908
|
+
for (const node of roots) {
|
|
909
|
+
const p = node.place;
|
|
910
|
+
rewalkAt(w, p.rowStart, p.lightStart, p.cameraStart, p.switchStart, p.noCollide, node, p.parent, p.material, p.color, p.sel, p.lodNode, p.lodChild);
|
|
911
|
+
}
|
|
912
|
+
for (const seg of segments) {
|
|
913
|
+
if (roots.some((node) => under(seg.node, node))) continue;
|
|
914
|
+
rewalkAt(w, seg.start, seg.lightStart, seg.cameraStart, seg.switchStart, seg.noCollide, seg.node, seg.parent, seg.material, seg.color, seg.sel, seg.lodNode, seg.lodChild);
|
|
915
|
+
}
|
|
916
|
+
scene.version++;
|
|
917
|
+
return ok;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Replace one element's local matrix and rewrite the rows of its single
|
|
922
|
+
* placement. The matrix is column-major, the same layout as matrix().
|
|
923
|
+
* Returns false when the id is missing, the matrix is not 16 finite numbers,
|
|
924
|
+
* or the element is shared (a definition used twice, a repeat or scatter
|
|
925
|
+
* copy, or a subtree the instance kernel expanded).
|
|
926
|
+
* @param {RenderScene} scene
|
|
927
|
+
* @param {object} graph
|
|
928
|
+
* @param {import('./scene-graph.js').SceneNode} node
|
|
929
|
+
* @param {ArrayLike<number>} matrix
|
|
930
|
+
*/
|
|
931
|
+
export function applyLocalMatrix(scene, graph, node, matrix, time = 0, progress = 0) {
|
|
932
|
+
return applyLocalMatrices(scene, graph, [node], [matrix], time, progress)[0] === true;
|
|
933
|
+
}
|
|
934
|
+
|
|
731
935
|
/** Re-evaluate animated subtrees at time t and scroll progress, overwriting their rows. */
|
|
732
936
|
export function updateDynamic(scene, graph, time, progress = 0) {
|
|
733
937
|
if (!scene.dynamic.length) return false;
|
package/src/scene/scene-graph.js
CHANGED
|
@@ -37,6 +37,12 @@ export class SceneNode {
|
|
|
37
37
|
this.step = null;
|
|
38
38
|
// scatter / lod / light / camera parameters
|
|
39
39
|
this.params = null;
|
|
40
|
+
/** Column-major local matrix written by a script, or null. It wins over this node's own transform. */
|
|
41
|
+
this.scriptLocal = null;
|
|
42
|
+
/** One recorded placement, or 'many' when the node is drawn in more than one place. */
|
|
43
|
+
this.place = null;
|
|
44
|
+
/** Scene-graph parent. A shared definition has none: it is not a child of the uses that draw it. */
|
|
45
|
+
this.parent = null;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
get animated() {
|
|
@@ -65,6 +71,7 @@ export function buildSceneGraph(doc, cache, opts = {}) {
|
|
|
65
71
|
const diagnostics = [];
|
|
66
72
|
const materials = new MaterialTable();
|
|
67
73
|
const memo = new Map();
|
|
74
|
+
const byId = new Map();
|
|
68
75
|
let environment = { ...ENV_DEFAULTS, explicit: false };
|
|
69
76
|
let envFound = false;
|
|
70
77
|
let controls = null;
|
|
@@ -132,6 +139,7 @@ export function buildSceneGraph(doc, cache, opts = {}) {
|
|
|
132
139
|
memo.set(el, node);
|
|
133
140
|
stats.nodes++;
|
|
134
141
|
const v = el.values || {};
|
|
142
|
+
if (v.id !== undefined) byId.set(v.id, node);
|
|
135
143
|
if (kind !== 'camera' && kind !== 'light') {
|
|
136
144
|
const m = localMatrix(v);
|
|
137
145
|
node.local = isIdentity(m) ? null : m;
|
|
@@ -167,7 +175,10 @@ export function buildSceneGraph(doc, cache, opts = {}) {
|
|
|
167
175
|
}
|
|
168
176
|
if (c.name === 'material' || c.name === 'environment' || c.name === 'controls') continue;
|
|
169
177
|
const child = nodeFor(c, depth + 1);
|
|
170
|
-
if (child)
|
|
178
|
+
if (child) {
|
|
179
|
+
child.parent = node;
|
|
180
|
+
node.children.push(child);
|
|
181
|
+
}
|
|
171
182
|
}
|
|
172
183
|
|
|
173
184
|
switch (kind) {
|
|
@@ -298,7 +309,7 @@ export function buildSceneGraph(doc, cache, opts = {}) {
|
|
|
298
309
|
report(doc.root, 'error', 'E_TOO_MANY_INSTANCES', `The scene expands to ${expanded.toLocaleString('en-US')} instances, more than the limit of ${maxInstances.toLocaleString('en-US')}. Reduce repeat or scatter counts.`, 'render');
|
|
299
310
|
}
|
|
300
311
|
|
|
301
|
-
return { doc, root, materials, environment, controls, animation, cache, diagnostics, stats, tooLarge: expanded > maxInstances, expanded };
|
|
312
|
+
return { doc, root, materials, environment, controls, animation, cache, diagnostics, stats, tooLarge: expanded > maxInstances, expanded, byId };
|
|
302
313
|
}
|
|
303
314
|
|
|
304
315
|
/** Viewer settings from a <controls> element (angles in degrees, as written). */
|
package/src/stage.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The page entry. The published file is one minified build of this module
|
|
2
|
+
// (browser/es3d-stage.min.js, `npm run build:stage`), so a page loads the
|
|
3
|
+
// stage in a single request. createStage, ancestorNames and the placement
|
|
4
|
+
// functions a script shares with the reader are the exports.
|
|
5
|
+
|
|
6
|
+
export { createStage, ancestorNames } from './render/stage.js';
|
|
7
|
+
export { mulberry32 } from './math/random.js';
|
|
8
|
+
export { es3dSin, es3dCos } from './math/trig.js';
|