@es3d/es3d 1.1.2 → 1.2.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.
@@ -0,0 +1,398 @@
1
+ // A document compiled somewhere else (a worker), ready to place.
2
+ //
3
+ // compile() makes objects that cannot cross to another thread: the scene
4
+ // graph's geometry entries hold generator closures over the parsed tree. So a
5
+ // worker runs the whole pipeline and also generates the meshes, and sends
6
+ // back only plain data, most of it as transferred buffers (nothing copied):
7
+ //
8
+ // - the instance table (RenderScene rows, exact length),
9
+ // - each geometry's descriptor fields (key, identity, levels, bounds) as
10
+ // columns and, for keys this worker has not sent before, its generated
11
+ // meshes with the diagnostics generation reported. A level identical to
12
+ // the one before it (a straight-sided extrusion has four) is sent once
13
+ // and shared, and every mesh of the document is packed into four buffers:
14
+ // a few transfers, not four per level (thousands for a large tile, each
15
+ // with a cost of its own when the message arrives).
16
+ // - material parameters, lights, cameras, lod switches, bounds,
17
+ // - the diagnostics as plain fields (Diagnostic is rebuilt here, so the
18
+ // codes, messages and locations are exactly those compile() reports).
19
+ //
20
+ // The main thread turns that back into a RenderScene whose entries live in
21
+ // the stage's GeometryCache (resultFromPayload): an entry whose key the cache
22
+ // has is reused, a new one adopts the worker's meshes the way the persistent
23
+ // store's are adopted, and a level the worker did not send is generated from
24
+ // the document on first use. The parsed tree and the scene graph are never
25
+ // sent: unpacking them would cost the main thread about what parsing does.
26
+ // When a page needs them (setTransforms on that document, or names for a
27
+ // pick), the document is compiled on the main thread then.
28
+ //
29
+ // A document the payload cannot carry (one with <animate>, which updateDynamic
30
+ // rewalks through the graph every frame, or with textures) comes back as
31
+ // `main: true`, and the main thread compiles it as it would without workers.
32
+
33
+ import { compile, elementDiagnostic } from '../pipeline.js';
34
+ import { Diagnostic, LineIndex } from '../format/errors.js';
35
+ import { GeometryCache } from '../geometry/geometry-cache.js';
36
+ import { Mesh } from '../geometry/mesh.js';
37
+ import { Material } from '../material/materials.js';
38
+ import { RenderScene } from './flatten.js';
39
+
40
+ /**
41
+ * Code of the stage's own warning for an element a fragment cannot set.
42
+ * It is not a format diagnostic: a document that has these elements is valid,
43
+ * and opened on its own it uses them.
44
+ */
45
+ export const HOST_ONLY = 'STAGE_HOST_ONLY';
46
+
47
+ const HOST_ONLY_ELEMENTS = {
48
+ environment: 'the environment',
49
+ camera: 'a camera',
50
+ controls: 'the controls',
51
+ };
52
+
53
+ /**
54
+ * Warnings for the <environment>, <camera> and <controls> elements of a
55
+ * document shown as a fragment: they belong to the host and are ignored.
56
+ * @param {object} result a compile() result
57
+ * @returns {Diagnostic[]}
58
+ */
59
+ export function hostOnlyWarnings(result) {
60
+ const doc = result && result.doc;
61
+ if (!doc || !doc.root) return [];
62
+ const out = [];
63
+ const visit = (el) => {
64
+ if (el.unknown) return;
65
+ const what = HOST_ONLY_ELEMENTS[el.name];
66
+ if (what) {
67
+ out.push(elementDiagnostic(doc, {
68
+ severity: 'warning',
69
+ phase: 'render',
70
+ code: HOST_ONLY,
71
+ message: `A fragment cannot set ${what}: the stage's document does. This element is ignored.`,
72
+ element: el,
73
+ }));
74
+ }
75
+ for (const c of el.children) if (c.type === 'element') visit(c);
76
+ };
77
+ visit(doc.root);
78
+ return out;
79
+ }
80
+
81
+ const DIAGNOSTIC_FIELDS = ['severity', 'phase', 'code', 'message', 'offset', 'length', 'suggestion', 'hint', 'path', 'line', 'column', 'uid'];
82
+
83
+ /** A Diagnostic as plain fields (structured-clone safe). */
84
+ export function plainDiagnostic(d) {
85
+ const o = {};
86
+ for (const k of DIAGNOSTIC_FIELDS) o[k] = d[k] === undefined ? null : d[k];
87
+ return o;
88
+ }
89
+
90
+ /** The Diagnostic those fields describe, located as it was. */
91
+ export function diagnosticFrom(o) {
92
+ const d = new Diagnostic(o);
93
+ d.line = o.line ?? 0;
94
+ d.column = o.column ?? 0;
95
+ return d;
96
+ }
97
+
98
+ const KINDS = ['triangles', 'lines'];
99
+ const kindCode = (k) => Math.max(0, KINDS.indexOf(k));
100
+ const sameArray = (a, b) => {
101
+ if (a.length !== b.length) return false;
102
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
103
+ return true;
104
+ };
105
+ const sameMesh = (a, b) => a.kind === b.kind && sameArray(a.indices, b.indices) && sameArray(a.positions, b.positions) && sameArray(a.normals, b.normals) && sameArray(a.uvs, b.uvs);
106
+
107
+ /**
108
+ * Geometry descriptors as columns, and the meshes of keys not sent before
109
+ * packed into four buffers. levels[g * maxLods + l] is the mesh index of
110
+ * level l of geometry g, -1 when it is not sent.
111
+ */
112
+ function packGeometries(entries, cache, sent) {
113
+ const n = entries.length;
114
+ const maxLods = entries.reduce((m, e) => Math.max(m, e.lods), 1);
115
+ const keys = new Array(n);
116
+ const identities = new Array(n);
117
+ const lods = new Uint8Array(n);
118
+ const flags = new Uint8Array(n); // 1 double-sided, 2 has bounds, 4 failed somewhere
119
+ const kinds = new Uint8Array(n);
120
+ const bounds = new Float64Array(n * 6);
121
+ const deviation = new Float64Array(n * maxLods);
122
+ const devLength = new Uint8Array(n);
123
+ const levels = new Int32Array(n * maxLods).fill(-1);
124
+ const reports = []; // [g, l, reports] for levels that reported something
125
+ const failed = []; // [g, l]
126
+ const meshes = [];
127
+ entries.forEach((e, g) => {
128
+ keys[g] = e.key;
129
+ identities[g] = e.identity;
130
+ lods[g] = e.lods;
131
+ kinds[g] = kindCode(e.kind);
132
+ flags[g] = (e.doubleSided ? 1 : 0) | (e.bounds ? 2 : 0);
133
+ if (e.bounds) bounds.set([...e.bounds.min, ...e.bounds.max], g * 6);
134
+ devLength[g] = Math.min(e.lodDeviation.length, maxLods);
135
+ for (let l = 0; l < devLength[g]; l++) deviation[g * maxLods + l] = e.lodDeviation[l];
136
+ const tag = `${cache.quality}:${e.key}`;
137
+ if (sent && sent.has(tag)) return;
138
+ let prev = null;
139
+ for (let l = 0; l < e.lods; l++) {
140
+ const m = cache.mesh(e, l);
141
+ if (e.reports[l]) reports.push([g, l, e.reports[l]]);
142
+ if (e.failedLods & (1 << l)) failed.push([g, l]);
143
+ if (prev && sameMesh(prev, m)) {
144
+ levels[g * maxLods + l] = levels[g * maxLods + l - 1];
145
+ continue;
146
+ }
147
+ levels[g * maxLods + l] = meshes.length;
148
+ meshes.push(m);
149
+ prev = m;
150
+ }
151
+ if (sent) sent.add(tag);
152
+ });
153
+ // one buffer per attribute; a mesh is a range of each (offsets in elements)
154
+ const table = new Uint32Array(meshes.length * 8);
155
+ const meshKinds = new Uint8Array(meshes.length);
156
+ const total = [0, 0, 0, 0];
157
+ meshes.forEach((m, k) => {
158
+ const arrays = [m.positions, m.normals, m.uvs, m.indices];
159
+ arrays.forEach((a, j) => {
160
+ table[k * 8 + j * 2] = total[j];
161
+ table[k * 8 + j * 2 + 1] = a.length;
162
+ total[j] += a.length;
163
+ });
164
+ meshKinds[k] = kindCode(m.kind);
165
+ });
166
+ const positions = new Float32Array(total[0]);
167
+ const normals = new Float32Array(total[1]);
168
+ const uvs = new Float32Array(total[2]);
169
+ const indices = new Uint32Array(total[3]);
170
+ meshes.forEach((m, k) => {
171
+ positions.set(m.positions, table[k * 8]);
172
+ normals.set(m.normals, table[k * 8 + 2]);
173
+ uvs.set(m.uvs, table[k * 8 + 4]);
174
+ indices.set(m.indices, table[k * 8 + 6]);
175
+ });
176
+ const columns = { keys, identities, lods, flags, kinds, bounds, deviation, devLength, levels, maxLods, reports, failed };
177
+ const packed = { table, meshKinds, positions, normals, uvs, indices };
178
+ const buffers = [lods, flags, kinds, bounds, deviation, devLength, levels, table, meshKinds, positions, normals, uvs, indices].map((a) => a.buffer);
179
+ return { columns, packed, buffers };
180
+ }
181
+
182
+ /** The meshes of a packed payload, as views into its four buffers. */
183
+ function unpackMeshes(packed) {
184
+ const { table, meshKinds, positions, normals, uvs, indices } = packed;
185
+ const out = new Array(meshKinds.length);
186
+ for (let k = 0; k < out.length; k++) {
187
+ const o = k * 8;
188
+ out[k] = new Mesh({
189
+ positions: positions.subarray(table[o], table[o] + table[o + 1]),
190
+ normals: normals.subarray(table[o + 2], table[o + 2] + table[o + 3]),
191
+ uvs: uvs.subarray(table[o + 4], table[o + 4] + table[o + 5]),
192
+ indices: indices.subarray(table[o + 6], table[o + 6] + table[o + 7]),
193
+ kind: KINDS[meshKinds[k]],
194
+ });
195
+ }
196
+ return out;
197
+ }
198
+
199
+ const textured = (m) => {
200
+ const p = m.params;
201
+ return !!(p.map || p.normalMap || p.occlusionMap || p.metallicRoughnessMap || p.emissiveMap);
202
+ };
203
+
204
+ // A typed array that owns its whole buffer can be transferred; a view into a
205
+ // larger one (a WebAssembly heap, a shared pool) is copied first.
206
+ const own = (a) => (a.byteOffset === 0 && a.byteLength === a.buffer.byteLength ? a : a.slice());
207
+
208
+ /**
209
+ * Compile a document and describe the result as transferable data. Runs in
210
+ * the worker (compile-worker.js); tests run it on one thread.
211
+ * @param {string|ArrayBuffer|Uint8Array} input
212
+ * @param {{time?: number, progress?: number, quality?: number, sent?: Set<string>}} [opts]
213
+ * sent: keys (with quality) whose meshes this worker already sent; their meshes are not sent again
214
+ * @returns {{message: object, transfer: ArrayBuffer[]}}
215
+ */
216
+ export function preparePayload(input, opts = {}) {
217
+ const cache = new GeometryCache();
218
+ if (opts.quality) cache.quality = opts.quality;
219
+ const result = compile(input, { cache, time: opts.time || 0, progress: opts.progress || 0 });
220
+ const lineIndex = result.doc && result.doc.lineIndex;
221
+ const base = {
222
+ ok: result.ok,
223
+ diagnostics: result.diagnostics.map(plainDiagnostic),
224
+ hostOnly: result.ok ? hostOnlyWarnings(result).map(plainDiagnostic) : [],
225
+ timings: result.timings,
226
+ // the text diagnostics point into, when it is not the input itself (decoded bytes)
227
+ lineText: lineIndex && typeof lineIndex.text === 'string' && lineIndex.text !== input ? lineIndex.text : null,
228
+ lineIndex: !!lineIndex,
229
+ };
230
+ if (!result.ok) return { message: base, transfer: [] };
231
+ const scene = result.scene;
232
+ if (scene.dynamic.length || scene.materials.some(textured)) return { message: { ...base, main: true }, transfer: [] };
233
+
234
+ const transfer = new Set();
235
+ const take = (a) => {
236
+ const o = own(a);
237
+ transfer.add(o.buffer);
238
+ return o;
239
+ };
240
+ const n = scene.count;
241
+ const rows = {
242
+ count: n,
243
+ matrices: take(scene.matrices.slice(0, n * 16)),
244
+ geom: take(scene.geom.slice(0, n)),
245
+ mat: take(scene.mat.slice(0, n)),
246
+ uid: take(scene.uid.slice(0, n)),
247
+ lodNode: take(scene.lodNode.slice(0, n)),
248
+ lodChild: take(scene.lodChild.slice(0, n)),
249
+ flags: take(scene.flags.slice(0, n)),
250
+ spheres: take(scene.spheres.slice(0, n * 4)),
251
+ };
252
+ const sent = opts.sent;
253
+ const geometries = packGeometries(scene.geometries, cache, sent);
254
+ for (const b of geometries.buffers) transfer.add(b);
255
+ const message = {
256
+ ...base,
257
+ scene: {
258
+ rows,
259
+ geometries: geometries.columns,
260
+ meshes: geometries.packed,
261
+ materials: scene.materials.map((m) => ({ params: m.params, name: m.name })),
262
+ lights: scene.lights,
263
+ cameras: scene.cameras,
264
+ lodSwitches: scene.lodSwitches,
265
+ environment: scene.environment,
266
+ controls: scene.controls,
267
+ animation: scene.animation,
268
+ bounds: scene.bounds,
269
+ },
270
+ };
271
+ return { message, transfer: [...transfer] };
272
+ }
273
+
274
+ /**
275
+ * A document compiled by prepare(), ready for stage.add() or stage.show().
276
+ * `ok` and `diagnostics` are known at once; diagnostics are those compile()
277
+ * reports (and, for `as: 'fragment'`, the stage's warnings for elements a
278
+ * fragment cannot set).
279
+ */
280
+ export class Prepared {
281
+ /**
282
+ * @param {object} o
283
+ * @param {'fragment'|'document'} o.as
284
+ * @param {string|ArrayBuffer|Uint8Array} o.source
285
+ * @param {object|null} [o.payload] from preparePayload (a worker)
286
+ * @param {object|null} [o.result] a compile() result made on this thread
287
+ * @param {*} [o.owner] the cache owner holding that result's geometry
288
+ * @param {Diagnostic[]} [o.hostOnly]
289
+ */
290
+ constructor(o) {
291
+ this.as = o.as || 'fragment';
292
+ this.source = o.source;
293
+ this.payload = o.payload || null;
294
+ this.result = o.result || null;
295
+ this.owner = o.owner ?? null;
296
+ this.used = false;
297
+ const p = this.payload;
298
+ if (p) {
299
+ this.ok = p.ok;
300
+ this.documentDiagnostics = p.diagnostics.map(diagnosticFrom);
301
+ this.hostOnly = p.hostOnly.map(diagnosticFrom);
302
+ this.timings = p.timings;
303
+ this.lineText = p.lineText;
304
+ this.hasLineIndex = p.lineIndex;
305
+ } else {
306
+ this.ok = this.result.ok;
307
+ this.documentDiagnostics = this.result.diagnostics;
308
+ this.hostOnly = o.hostOnly || [];
309
+ this.timings = this.result.timings;
310
+ this.lineText = null;
311
+ this.hasLineIndex = !!(this.result.doc && this.result.doc.lineIndex);
312
+ }
313
+ this.lineIndexCache = undefined;
314
+ }
315
+
316
+ /** Diagnostics as the stage reports them for this use. */
317
+ get diagnostics() {
318
+ return this.as === 'fragment' ? [...this.documentDiagnostics, ...this.hostOnly] : this.documentDiagnostics;
319
+ }
320
+
321
+ /** Line positions for formatDiagnostics (made on first use; errors only need them). */
322
+ get lineIndex() {
323
+ if (this.lineIndexCache !== undefined) return this.lineIndexCache;
324
+ if (this.result && this.result.doc) return (this.lineIndexCache = this.result.doc.lineIndex);
325
+ const text = this.lineText || (typeof this.source === 'string' ? this.source : null);
326
+ return (this.lineIndexCache = this.hasLineIndex && text ? new LineIndex(text) : null);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Rebuild a worker's scene on this thread, with its geometry in `cache` held
332
+ * by `owner`. Costs what the scene's rows and geometry list are: the rows are
333
+ * the transferred arrays themselves.
334
+ * @param {object} payload a preparePayload message with ok and scene
335
+ * @param {GeometryCache} cache
336
+ * @param {*} owner
337
+ * @param {(key: string) => object} realDescriptor descriptor of a key, from the document compiled
338
+ * here, for a level the worker did not send (called on first use only)
339
+ * @returns {import('./flatten.js').RenderScene}
340
+ */
341
+ export function sceneFromPayload(payload, cache, owner, realDescriptor) {
342
+ const p = payload.scene;
343
+ const scene = new RenderScene(1);
344
+ const r = p.rows;
345
+ scene.count = r.count;
346
+ scene.capacity = r.count;
347
+ scene.matrices = r.matrices;
348
+ scene.geom = r.geom;
349
+ scene.mat = r.mat;
350
+ scene.uid = r.uid;
351
+ scene.lodNode = r.lodNode;
352
+ scene.lodChild = r.lodChild;
353
+ scene.flags = r.flags;
354
+ scene.spheres = r.spheres;
355
+ const c = p.geometries;
356
+ const meshes = unpackMeshes(p.meshes);
357
+ const M = c.maxLods;
358
+ const reports = new Map(c.reports.map(([g, l, r]) => [g * M + l, r]));
359
+ const failed = new Set(c.failed.map(([g, l]) => g * M + l));
360
+ cache.beginBuild(owner);
361
+ for (let g = 0; g < c.keys.length; g++) {
362
+ const key = c.keys[g];
363
+ const lodDeviation = Array.from(c.deviation.subarray(g * M, g * M + c.devLength[g]));
364
+ const b = c.bounds;
365
+ const desc = {
366
+ key,
367
+ identity: c.identities[g],
368
+ kind: KINDS[c.kinds[g]],
369
+ lods: c.lods[g],
370
+ doubleSided: !!(c.flags[g] & 1),
371
+ bounds: c.flags[g] & 2 ? { min: [b[g * 6], b[g * 6 + 1], b[g * 6 + 2]], max: [b[g * 6 + 3], b[g * 6 + 4], b[g * 6 + 5]] } : null,
372
+ lodDeviation,
373
+ generate: (lod, quality, report) => realDescriptor(key).generate(lod, quality, report),
374
+ };
375
+ const entry = cache.acquire(desc, null);
376
+ for (let l = 0; l < desc.lods; l++) {
377
+ const k = c.levels[g * M + l];
378
+ if (k < 0 || entry.meshes[l]) continue;
379
+ if (failed.has(g * M + l)) {
380
+ entry.failed = true;
381
+ entry.failedLods |= 1 << l;
382
+ }
383
+ cache.adopt(entry, l, meshes[k], reports.get(g * M + l) || null);
384
+ }
385
+ scene.geometryIndex(entry);
386
+ }
387
+ cache.endBuild();
388
+ scene.materials = p.materials.map((m, i) => new Material(m.params, i, m.name));
389
+ scene.lights = p.lights;
390
+ scene.cameras = p.cameras;
391
+ scene.lodSwitches = p.lodSwitches;
392
+ scene.environment = p.environment;
393
+ scene.controls = p.controls;
394
+ scene.animation = p.animation;
395
+ scene.bounds = p.bounds;
396
+ scene.version = 1;
397
+ return scene;
398
+ }
@@ -108,7 +108,7 @@ export function buildSceneGraph(doc, cache, opts = {}) {
108
108
  };
109
109
  collect(doc.root);
110
110
 
111
- cache.beginBuild();
111
+ cache.beginBuild(opts.owner);
112
112
 
113
113
  const nodeFor = (el, depth = 0) => {
114
114
  if (memo.has(el)) return memo.get(el);
@@ -0,0 +1,251 @@
1
+ // A scene set: the instance tables a stage draws together, the host
2
+ // document's and one per fragment (stage.add).
3
+ //
4
+ // Each part keeps the RenderScene that compile() made for it, unchanged, so
5
+ // setTransforms, animation and ids work inside a part exactly as in a single
6
+ // document. Rows refer to their own document's geometry and material lists;
7
+ // the set keeps one list of each for all parts and a small map per part from
8
+ // its indices to the set's. The frame builder and picking read every placed,
9
+ // visible part through those maps (frame-builder.js, picking.js), so adding,
10
+ // hiding or removing a part never copies, moves or renumbers anyone's rows.
11
+ //
12
+ // Materials with identical parameters are one material across all parts (the
13
+ // specification asks readers to deduplicate them), and a geometry entry the
14
+ // host and several tiles use is one entry. Both lists count their users: a
15
+ // slot no part uses is freed, and its geometry is reported to the renderer
16
+ // (takeReleased) so the pool can repack once enough of it is unused.
17
+ //
18
+ // While nothing but the host is placed, the order of both lists is the order
19
+ // the host's own lists have, and parts placed in turn extend them in the order
20
+ // one merged document would: the picture is the same as showing that document.
21
+
22
+ import { Material } from '../material/materials.js';
23
+
24
+ const EMPTY_BOUNDS = () => ({ min: [-1, -1, -1], max: [1, 1, 1] });
25
+
26
+ /** One document drawn by a scene set: the host, or a fragment. */
27
+ export class ScenePart {
28
+ /**
29
+ * @param {string|null} id fragment id (null for the host)
30
+ * @param {'host'|'fragment'} kind
31
+ */
32
+ constructor(id, kind = 'fragment') {
33
+ this.id = id;
34
+ this.kind = kind;
35
+ /** @type {import('./flatten.js').RenderScene|null} null until placed */
36
+ this.scene = null;
37
+ this.geomMap = null; // Uint32Array: part geometry index -> set index
38
+ this.matMap = null; // Uint32Array: part material index -> set index
39
+ this.visible = true;
40
+ // renderer.selection holds the host's element uids; fragments have their own
41
+ this.selectable = kind === 'host';
42
+ }
43
+
44
+ /** Placed and drawn. */
45
+ get drawn() {
46
+ return !!(this.scene && this.visible);
47
+ }
48
+ }
49
+
50
+ export class SceneSet {
51
+ constructor() {
52
+ /** @type {ScenePart[]} host first, then fragments in the order added */
53
+ this.parts = [];
54
+ this.host = null;
55
+ this.geometries = [];
56
+ this.geomIndex = new Map(); // entry -> set index
57
+ this.geomUsers = [];
58
+ this.freeGeometries = [];
59
+ this.materials = [];
60
+ this.matIndex = new Map(); // material key -> set index
61
+ this.matUsers = [];
62
+ this.freeMaterials = [];
63
+ this.released = []; // geometry entries no part uses any more
64
+ this.version = 0; // bumps when parts are placed, hidden or removed
65
+ this.boundsDirty = true;
66
+ this.cachedBounds = EMPTY_BOUNDS();
67
+ }
68
+
69
+ /** Add a part at the end (fragments) or at the front (the host). */
70
+ insert(part) {
71
+ if (part.kind === 'host') {
72
+ if (this.host) throw new Error('A scene set has one host.');
73
+ this.host = part;
74
+ this.parts.unshift(part);
75
+ } else this.parts.push(part);
76
+ this.changed();
77
+ return part;
78
+ }
79
+
80
+ /** Take a part out of the set and free what only it used. */
81
+ remove(part) {
82
+ const i = this.parts.indexOf(part);
83
+ if (i < 0) return false;
84
+ this.unplace(part);
85
+ this.parts.splice(i, 1);
86
+ if (part === this.host) this.host = null;
87
+ this.changed();
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Draw a compiled scene as this part (replacing what it drew before).
93
+ * Costs what the part's own geometry and material lists do.
94
+ * @param {ScenePart} part
95
+ * @param {import('./flatten.js').RenderScene} scene
96
+ */
97
+ place(part, scene) {
98
+ const geomMap = new Uint32Array(scene.geometries.length);
99
+ for (let k = 0; k < scene.geometries.length; k++) geomMap[k] = this.useGeometry(scene.geometries[k]);
100
+ const matMap = new Uint32Array(scene.materials.length);
101
+ for (let k = 0; k < scene.materials.length; k++) matMap[k] = this.useMaterial(scene.materials[k]);
102
+ // release the old content after taking the new, so what both use stays
103
+ this.unplace(part);
104
+ part.scene = scene;
105
+ part.geomMap = geomMap;
106
+ part.matMap = matMap;
107
+ this.changed();
108
+ }
109
+
110
+ /** Stop drawing a part's content and free what only it used. */
111
+ unplace(part) {
112
+ if (!part.scene) return;
113
+ for (const gi of part.geomMap) this.dropGeometry(gi);
114
+ for (const mi of part.matMap) this.dropMaterial(mi);
115
+ part.scene = null;
116
+ part.geomMap = null;
117
+ part.matMap = null;
118
+ this.changed();
119
+ }
120
+
121
+ setVisible(part, visible) {
122
+ const v = !!visible;
123
+ if (part.visible === v) return;
124
+ part.visible = v;
125
+ this.changed();
126
+ }
127
+
128
+ /** Something that changes bounds, lights or the drawn rows happened. */
129
+ changed() {
130
+ this.version++;
131
+ this.boundsDirty = true;
132
+ }
133
+
134
+ /** Bounds moved inside a part (setTransforms, animation). */
135
+ invalidateBounds() {
136
+ this.boundsDirty = true;
137
+ }
138
+
139
+ useGeometry(entry) {
140
+ let i = this.geomIndex.get(entry);
141
+ if (i === undefined) {
142
+ i = this.freeGeometries.length ? this.freeGeometries.pop() : this.geometries.length;
143
+ this.geometries[i] = entry;
144
+ this.geomUsers[i] = 0;
145
+ this.geomIndex.set(entry, i);
146
+ }
147
+ this.geomUsers[i]++;
148
+ return i;
149
+ }
150
+
151
+ dropGeometry(i) {
152
+ if (--this.geomUsers[i] > 0) return;
153
+ const entry = this.geometries[i];
154
+ this.geomIndex.delete(entry);
155
+ this.geometries[i] = null;
156
+ this.freeGeometries.push(i);
157
+ this.released.push(entry);
158
+ }
159
+
160
+ useMaterial(m) {
161
+ let i = this.matIndex.get(m.key);
162
+ if (i === undefined) {
163
+ i = this.freeMaterials.length ? this.freeMaterials.pop() : this.materials.length;
164
+ this.materials[i] = new Material(m.params, i, m.name);
165
+ this.matUsers[i] = 0;
166
+ this.matIndex.set(m.key, i);
167
+ }
168
+ this.matUsers[i]++;
169
+ return i;
170
+ }
171
+
172
+ dropMaterial(i) {
173
+ if (--this.matUsers[i] > 0) return;
174
+ this.matIndex.delete(this.materials[i].key);
175
+ this.materials[i] = null;
176
+ this.freeMaterials.push(i);
177
+ }
178
+
179
+ /** Geometry entries no part uses since the last call. */
180
+ takeReleased() {
181
+ const r = this.released;
182
+ this.released = [];
183
+ return r;
184
+ }
185
+
186
+ /** Placed, visible parts in drawing order. */
187
+ drawnParts() {
188
+ const out = [];
189
+ for (const p of this.parts) if (p.scene && p.visible) out.push(p);
190
+ return out;
191
+ }
192
+
193
+ /** Instance rows drawn. */
194
+ get count() {
195
+ let n = 0;
196
+ for (const p of this.parts) if (p.scene && p.visible) n += p.scene.count;
197
+ return n;
198
+ }
199
+
200
+ /** Union of the drawn parts' bounds, recomputed when something changed. */
201
+ get bounds() {
202
+ if (!this.boundsDirty) return this.cachedBounds;
203
+ const min = [Infinity, Infinity, Infinity];
204
+ const max = [-Infinity, -Infinity, -Infinity];
205
+ for (const p of this.parts) {
206
+ // an empty part reports the ±1 stand-in flattenScene gives an empty scene
207
+ if (!p.scene || !p.visible || !p.scene.count) continue;
208
+ const b = p.scene.bounds;
209
+ for (let k = 0; k < 3; k++) {
210
+ if (b.min[k] < min[k]) min[k] = b.min[k];
211
+ if (b.max[k] > max[k]) max[k] = b.max[k];
212
+ }
213
+ }
214
+ this.cachedBounds = min[0] <= max[0] ? { min, max } : EMPTY_BOUNDS();
215
+ this.boundsDirty = false;
216
+ return this.cachedBounds;
217
+ }
218
+
219
+ /** Lights of the drawn parts, host first, as one merged document lists them. */
220
+ get lights() {
221
+ const parts = this.parts;
222
+ if (parts.length === 1 && parts[0].scene && parts[0].visible) return parts[0].scene.lights;
223
+ const out = [];
224
+ for (const p of parts) if (p.scene && p.visible) for (const l of p.scene.lights) out.push(l);
225
+ return out;
226
+ }
227
+
228
+ /** Clocks the drawn parts' animations use. */
229
+ get animation() {
230
+ const a = { time: false, scroll: false };
231
+ for (const p of this.parts) {
232
+ if (!p.scene || !p.visible) continue;
233
+ if (p.scene.animation.time) a.time = true;
234
+ if (p.scene.animation.scroll) a.scroll = true;
235
+ }
236
+ return a;
237
+ }
238
+
239
+ // the host's alone: a fragment's <environment>, <camera> and <controls> are ignored
240
+ get environment() {
241
+ return this.host && this.host.scene ? this.host.scene.environment : null;
242
+ }
243
+
244
+ get controls() {
245
+ return this.host && this.host.scene ? this.host.scene.controls : null;
246
+ }
247
+
248
+ get cameras() {
249
+ return this.host && this.host.scene ? this.host.scene.cameras : [];
250
+ }
251
+ }