@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.
- package/README.md +24 -0
- package/browser/es3d-stage.min.js +243 -10232
- package/browser/es3d.min.js +534 -0
- package/docs/STAGE.md +95 -0
- package/package.json +6 -3
- package/src/browser.js +16 -0
- package/src/cache/geometry-store.js +56 -18
- package/src/geometry/geometry-cache.js +93 -8
- package/src/io/compression.js +4 -1
- package/src/io/convert.js +14 -1
- package/src/pipeline.js +2 -1
- package/src/render/compile-worker.js +143 -0
- package/src/render/frame-builder.js +114 -77
- package/src/render/geometry-pool.js +103 -41
- package/src/render/renderer.js +28 -2
- package/src/render/stage.js +305 -50
- package/src/render/webgl2/webgl2-backend.js +68 -16
- package/src/render/webgpu/webgpu-backend.js +42 -11
- package/src/render/worker-boot.js +14 -0
- package/src/scene/fragments.js +527 -0
- package/src/scene/picking.js +31 -2
- package/src/scene/prepared.js +398 -0
- package/src/scene/scene-graph.js +1 -1
- package/src/scene/scene-set.js +251 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
// Fragments: complete ES3D documents shown inside a stage's document.
|
|
2
|
+
//
|
|
3
|
+
// const tile = stage.add(source, { id: '16/32748/21789' });
|
|
4
|
+
// await tile.ready; // compiled and placed
|
|
5
|
+
// tile.visible = false; // kept, not drawn
|
|
6
|
+
// tile.replace(newSource); // only this fragment is rebuilt
|
|
7
|
+
// tile.remove();
|
|
8
|
+
//
|
|
9
|
+
// Every fragment is a conforming version-1 document, so any reader can open
|
|
10
|
+
// it on its own; the stage only draws several of them together. Each keeps
|
|
11
|
+
// its own compile result: its own ids (fragment.setTransforms, <use>), its
|
|
12
|
+
// own scene graph and animation. Drawing goes through a SceneSet
|
|
13
|
+
// (scene-set.js): materials with identical parameters are one material for
|
|
14
|
+
// the whole stage, and one GeometryCache serves the host and every fragment,
|
|
15
|
+
// each building under its own owner (geometry-cache.js), so a shape repeated
|
|
16
|
+
// across tiles is generated and uploaded once.
|
|
17
|
+
//
|
|
18
|
+
// Changes are queued: applyQueued(budget) compiles and places them in the
|
|
19
|
+
// stage's frame loop until the budget (ms) is spent, and the rest waits for
|
|
20
|
+
// the next frame. A change already started finishes over budget. Removing and
|
|
21
|
+
// hiding are applied at once: they cost what the fragment's own geometry and
|
|
22
|
+
// material lists do, and a page expects a removed tile gone in the next frame.
|
|
23
|
+
//
|
|
24
|
+
// This module has no DOM: the stage (render/stage.js) connects it to the
|
|
25
|
+
// renderer and the frame loop, and the tests drive it directly.
|
|
26
|
+
|
|
27
|
+
import { compile, now as clockNow } from '../pipeline.js';
|
|
28
|
+
import { parse, formatDiagnostics } from '../format/parse.js';
|
|
29
|
+
import { GeometryCache, DOCUMENT } from '../geometry/geometry-cache.js';
|
|
30
|
+
import { applyLocalMatrices, updateDynamic } from './flatten.js';
|
|
31
|
+
import { ancestorNames } from './picking.js';
|
|
32
|
+
import { SceneSet, ScenePart } from './scene-set.js';
|
|
33
|
+
import { Prepared, hostOnlyWarnings, sceneFromPayload } from './prepared.js';
|
|
34
|
+
|
|
35
|
+
const isThenable = (v) => !!v && typeof v.then === 'function';
|
|
36
|
+
|
|
37
|
+
/** One document shown inside the stage's document. */
|
|
38
|
+
export class Fragment {
|
|
39
|
+
constructor(owner, id) {
|
|
40
|
+
this.stage = owner;
|
|
41
|
+
this.id = id;
|
|
42
|
+
this.part = new ScenePart(id, 'fragment');
|
|
43
|
+
this.result = null; // the placed compile result (a prepared one compiles its graph on demand)
|
|
44
|
+
this.owner = null; // its cache owner
|
|
45
|
+
this.source = null;
|
|
46
|
+
this.warnings = [];
|
|
47
|
+
this.tasks = []; // queued changes, oldest first
|
|
48
|
+
this.removed = false;
|
|
49
|
+
this.time = NaN; // clock its animated rows were last evaluated at
|
|
50
|
+
this.progress = NaN;
|
|
51
|
+
/** @type {Promise<{ok: boolean, diagnostics: object[], fragment: string}>} */
|
|
52
|
+
this.ready = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Compiled and drawn (or hidden) in the stage. */
|
|
56
|
+
get placed() {
|
|
57
|
+
return !!this.part.scene;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get visible() {
|
|
61
|
+
return this.part.visible;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Hide or show it. Nothing is recompiled; a hidden fragment draws no rows at all. */
|
|
65
|
+
set visible(v) {
|
|
66
|
+
if (this.removed) return;
|
|
67
|
+
this.stage.set.setVisible(this.part, v);
|
|
68
|
+
this.stage.changed = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Diagnostics of the content on screen (compile's and the stage's warnings). */
|
|
72
|
+
get diagnostics() {
|
|
73
|
+
return this.result ? [...this.result.diagnostics, ...this.warnings] : [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Show other content in this fragment's place, in its turn. Bad input keeps
|
|
78
|
+
* the content on screen. Resolves when the new content is placed or failed.
|
|
79
|
+
* @param {string|ArrayBuffer|Uint8Array|Prepared|Promise<Prepared>} source
|
|
80
|
+
* @param {{immediate?: boolean}} [opts]
|
|
81
|
+
*/
|
|
82
|
+
replace(source, opts = {}) {
|
|
83
|
+
if (this.removed) return Promise.resolve({ ok: false, diagnostics: [], fragment: this.id, removed: true });
|
|
84
|
+
this.ready = this.stage.enqueue(this, source, !!opts.immediate);
|
|
85
|
+
return this.ready;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Replace the local matrix of elements of this fragment, by their ids in
|
|
90
|
+
* this fragment's document. Same entries, rules and return values as
|
|
91
|
+
* stage.setTransforms: false for an id that is missing or drawn in more
|
|
92
|
+
* than one place.
|
|
93
|
+
* @param {Array<[string, ArrayLike<number>]|{id: string, matrix: ArrayLike<number>}>} entries
|
|
94
|
+
*/
|
|
95
|
+
setTransforms(entries) {
|
|
96
|
+
const list = Array.from(entries || []);
|
|
97
|
+
if (this.removed || !this.result) return list.map(() => false);
|
|
98
|
+
const res = this.stage.materialize(this);
|
|
99
|
+
const byId = res.graph.byId;
|
|
100
|
+
const nodes = list.map((entry) => byId.get(Array.isArray(entry) ? entry[0] : entry && entry.id));
|
|
101
|
+
const matrices = list.map((entry) => (Array.isArray(entry) ? entry[1] : entry && entry.matrix));
|
|
102
|
+
const { time, progress } = this.stage.clock();
|
|
103
|
+
const applied = applyLocalMatrices(res.scene, res.graph, nodes, matrices, time, progress);
|
|
104
|
+
if (applied.some(Boolean)) {
|
|
105
|
+
this.stage.set.invalidateBounds();
|
|
106
|
+
this.stage.changed = true;
|
|
107
|
+
}
|
|
108
|
+
return applied;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** setTransforms for one id; true when it was applied. */
|
|
112
|
+
setTransform(id, matrix) {
|
|
113
|
+
return this.setTransforms([[id, matrix]])[0] === true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Take it out of the stage. Its rows and the geometry only it used are freed. */
|
|
117
|
+
remove() {
|
|
118
|
+
this.stage.removeFragment(this);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The fragments of one stage and the scene set that draws them with the host.
|
|
124
|
+
*/
|
|
125
|
+
export class FragmentStage {
|
|
126
|
+
/**
|
|
127
|
+
* @param {object} [o]
|
|
128
|
+
* @param {GeometryCache} [o.cache] shared with the host document
|
|
129
|
+
* @param {() => {time: number, progress: number}} [o.clock]
|
|
130
|
+
* @param {(message: string, result: object) => void} [o.report] errors and stage warnings
|
|
131
|
+
* @param {{compile: Function}|null} [o.compiler] a CompilerClient (compile-worker.js); null compiles here
|
|
132
|
+
* @param {(owner: *) => void} [o.forget] an owner's geometry is gone (stop storing it)
|
|
133
|
+
* @param {(cache: GeometryCache, owner: *) => Promise|null} [o.restore] read an owner's stored
|
|
134
|
+
* meshes before it is placed (the stage's geometry store); null: place at once
|
|
135
|
+
* @param {() => number} [o.now]
|
|
136
|
+
*/
|
|
137
|
+
constructor(o = {}) {
|
|
138
|
+
this.cache = o.cache || new GeometryCache();
|
|
139
|
+
this.clock = o.clock || (() => ({ time: 0, progress: 0 }));
|
|
140
|
+
this.report = o.report || null;
|
|
141
|
+
this.compiler = o.compiler || null;
|
|
142
|
+
this.forget = o.forget || null;
|
|
143
|
+
this.restore = o.restore || null;
|
|
144
|
+
this.now = o.now || clockNow;
|
|
145
|
+
this.set = new SceneSet();
|
|
146
|
+
this.host = this.set.insert(new ScenePart(null, 'host'));
|
|
147
|
+
this.hostResult = null;
|
|
148
|
+
this.hostOwner = DOCUMENT;
|
|
149
|
+
this.byId = new Map();
|
|
150
|
+
this.queue = [];
|
|
151
|
+
this.waiting = 0; // changes waiting for a worker's result
|
|
152
|
+
this.nextId = 1;
|
|
153
|
+
this.changed = false; // something to draw since the stage last looked (takeChanged)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Fragments in the order added. */
|
|
157
|
+
get list() {
|
|
158
|
+
return [...this.byId.values()];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Changes not yet placed: queued ones and those waiting for a worker. */
|
|
162
|
+
get queued() {
|
|
163
|
+
return this.queue.length;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get pending() {
|
|
167
|
+
return this.queue.length > 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** True once if anything changed since the last call. */
|
|
171
|
+
takeChanged() {
|
|
172
|
+
const c = this.changed;
|
|
173
|
+
this.changed = false;
|
|
174
|
+
return c;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
newOwner(kind) {
|
|
178
|
+
return { owner: kind };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- the host ---------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Draw a compiled document as the host. `owner` holds its geometry; the
|
|
185
|
+
* previous host's owner is released.
|
|
186
|
+
*/
|
|
187
|
+
setHost(result, owner) {
|
|
188
|
+
this.set.place(this.host, result.scene);
|
|
189
|
+
this.adoptHost(result, owner);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Record the host without drawing it (a stage that has no fragment yet). */
|
|
193
|
+
adoptHost(result, owner) {
|
|
194
|
+
if (this.hostOwner !== owner) this.release(this.hostOwner);
|
|
195
|
+
this.hostResult = result;
|
|
196
|
+
this.hostOwner = owner;
|
|
197
|
+
this.changed = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
release(owner) {
|
|
201
|
+
if (owner == null) return;
|
|
202
|
+
this.cache.release(owner);
|
|
203
|
+
if (this.forget && owner !== DOCUMENT) this.forget(owner);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The compile result a prepared document stands for, with its geometry in
|
|
208
|
+
* this stage's cache. A prepared document is placed once.
|
|
209
|
+
* @param {Prepared} prepared
|
|
210
|
+
* @returns {{result: object, owner: *}}
|
|
211
|
+
*/
|
|
212
|
+
fromPrepared(prepared, kind = 'fragment') {
|
|
213
|
+
if (prepared.used) throw new Error('A prepared document can be shown or added once; prepare it again.');
|
|
214
|
+
prepared.used = true;
|
|
215
|
+
prepared.claimed = true;
|
|
216
|
+
if (!prepared.ok) {
|
|
217
|
+
return { result: { ok: false, doc: null, graph: null, scene: null, cache: this.cache, diagnostics: prepared.documentDiagnostics, timings: prepared.timings, lineIndex: prepared.lineIndex }, owner: null };
|
|
218
|
+
}
|
|
219
|
+
if (prepared.result) return { result: prepared.result, owner: prepared.owner };
|
|
220
|
+
const owner = this.newOwner(kind);
|
|
221
|
+
if (prepared.payload.main) {
|
|
222
|
+
// the worker could not carry it (animation, textures): compile it here
|
|
223
|
+
const { time, progress } = this.clock();
|
|
224
|
+
return { result: compile(prepared.source, { cache: this.cache, owner, time, progress }), owner };
|
|
225
|
+
}
|
|
226
|
+
const source = prepared.source;
|
|
227
|
+
const cache = this.cache;
|
|
228
|
+
let local = null;
|
|
229
|
+
// a level the worker did not send (its key was sent for another document
|
|
230
|
+
// and evicted here since) is generated from this document, compiled here once
|
|
231
|
+
const real = (key) => {
|
|
232
|
+
if (!local) {
|
|
233
|
+
local = new GeometryCache();
|
|
234
|
+
local.quality = cache.quality;
|
|
235
|
+
compile(source, { cache: local });
|
|
236
|
+
}
|
|
237
|
+
const e = local.entries.get(key);
|
|
238
|
+
if (!e) throw new Error(`The document has no geometry ${key}.`);
|
|
239
|
+
return e.desc;
|
|
240
|
+
};
|
|
241
|
+
const scene = sceneFromPayload(prepared.payload, cache, owner, real);
|
|
242
|
+
prepared.payload = null; // its buffers are the scene's rows now
|
|
243
|
+
let doc = null;
|
|
244
|
+
const result = {
|
|
245
|
+
ok: true,
|
|
246
|
+
scene,
|
|
247
|
+
cache,
|
|
248
|
+
graph: null, // compiled on demand (FragmentStage.materialize)
|
|
249
|
+
diagnostics: prepared.documentDiagnostics,
|
|
250
|
+
timings: prepared.timings,
|
|
251
|
+
prepared: true,
|
|
252
|
+
source,
|
|
253
|
+
// parsed on first use, for the names of a picked element
|
|
254
|
+
get doc() {
|
|
255
|
+
if (!doc) doc = parse(source);
|
|
256
|
+
return doc;
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
return { result, owner };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- fragments --------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Add a fragment. Its content is compiled and placed in its turn (or at
|
|
266
|
+
* once with immediate: true); until then it draws nothing.
|
|
267
|
+
* @param {string|ArrayBuffer|Uint8Array|Prepared|Promise<Prepared>} source
|
|
268
|
+
* @param {{id?: string, immediate?: boolean}} [opts]
|
|
269
|
+
*/
|
|
270
|
+
add(source, opts = {}) {
|
|
271
|
+
let id = opts.id != null ? String(opts.id) : null;
|
|
272
|
+
if (id === null) {
|
|
273
|
+
while (this.byId.has(`fragment-${this.nextId}`)) this.nextId++;
|
|
274
|
+
id = `fragment-${this.nextId++}`;
|
|
275
|
+
}
|
|
276
|
+
if (this.byId.has(id)) throw new Error(`The stage has a fragment "${id}" already. Replace it, or remove it first.`);
|
|
277
|
+
const frag = new Fragment(this, id);
|
|
278
|
+
this.byId.set(id, frag);
|
|
279
|
+
this.set.insert(frag.part);
|
|
280
|
+
frag.ready = this.enqueue(frag, source, !!opts.immediate);
|
|
281
|
+
return frag;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Queue new content for a fragment. A change not yet started gives way to it. */
|
|
285
|
+
enqueue(frag, input, immediate) {
|
|
286
|
+
this.claim(input);
|
|
287
|
+
let settle;
|
|
288
|
+
const promise = new Promise((r) => (settle = r));
|
|
289
|
+
const task = { frag, input, ready: !isThenable(input), cancelled: false, finished: false, immediate, placing: null, resolve: null };
|
|
290
|
+
task.resolve = (v) => {
|
|
291
|
+
task.finished = true;
|
|
292
|
+
settle(v);
|
|
293
|
+
};
|
|
294
|
+
// older changes of this fragment that have not started resolve with this one
|
|
295
|
+
for (const old of frag.tasks) {
|
|
296
|
+
this.cancel(old);
|
|
297
|
+
promise.then(old.resolve);
|
|
298
|
+
}
|
|
299
|
+
frag.tasks = [task];
|
|
300
|
+
if (isThenable(input)) {
|
|
301
|
+
this.waiting++;
|
|
302
|
+
input.then(
|
|
303
|
+
(v) => {
|
|
304
|
+
task.input = v;
|
|
305
|
+
},
|
|
306
|
+
(err) => {
|
|
307
|
+
task.input = null;
|
|
308
|
+
task.error = err;
|
|
309
|
+
},
|
|
310
|
+
).then(() => {
|
|
311
|
+
this.waiting--;
|
|
312
|
+
task.ready = true;
|
|
313
|
+
if (task.cancelled) this.discard(task.input);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (immediate && task.ready) this.run(task);
|
|
317
|
+
else this.queue.push(task);
|
|
318
|
+
return promise;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// a prepared document becomes one fragment's rows: using it twice would share them
|
|
322
|
+
claim(input) {
|
|
323
|
+
if (!(input instanceof Prepared)) return;
|
|
324
|
+
if (input.claimed) throw new Error('A prepared document can be shown or added once; prepare it again.');
|
|
325
|
+
input.claimed = true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
cancel(task) {
|
|
329
|
+
task.cancelled = true;
|
|
330
|
+
task.finished = true;
|
|
331
|
+
if (task.placing) this.release(task.placing.owner);
|
|
332
|
+
const k = this.queue.indexOf(task);
|
|
333
|
+
if (k >= 0) this.queue.splice(k, 1);
|
|
334
|
+
if (task.ready) this.discard(task.input);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// a prepared document compiled here holds geometry until it is used
|
|
338
|
+
discard(input) {
|
|
339
|
+
if (input instanceof Prepared && !input.used && input.owner) {
|
|
340
|
+
input.used = true;
|
|
341
|
+
this.release(input.owner);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Apply queued changes until `budget` ms are spent (at least one per call).
|
|
347
|
+
* @returns {{applied: number, changeMs: number, compileMs: number}}
|
|
348
|
+
*/
|
|
349
|
+
applyQueued(budget = Infinity) {
|
|
350
|
+
const t0 = this.now();
|
|
351
|
+
let applied = 0;
|
|
352
|
+
let compileMs = 0;
|
|
353
|
+
while (this.queue.length) {
|
|
354
|
+
if (applied && this.now() - t0 >= budget) break;
|
|
355
|
+
const k = this.queue.findIndex((t) => t.ready);
|
|
356
|
+
if (k < 0) break;
|
|
357
|
+
const task = this.queue.splice(k, 1)[0];
|
|
358
|
+
compileMs += this.run(task);
|
|
359
|
+
applied++;
|
|
360
|
+
}
|
|
361
|
+
return { applied, changeMs: applied ? this.now() - t0 : 0, compileMs };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Compile and place one change. Returns the time spent compiling (ms). */
|
|
365
|
+
run(task) {
|
|
366
|
+
const frag = task.frag;
|
|
367
|
+
frag.tasks = frag.tasks.filter((t) => t !== task);
|
|
368
|
+
if (task.cancelled || frag.removed) return 0;
|
|
369
|
+
if (task.placing) {
|
|
370
|
+
// compiled earlier; its stored meshes have been read
|
|
371
|
+
const p = task.placing;
|
|
372
|
+
task.placing = null;
|
|
373
|
+
this.place(frag, p.result, p.owner, p.warnings, p.input);
|
|
374
|
+
task.resolve({ ok: true, diagnostics: frag.diagnostics, fragment: frag.id });
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
const t0 = this.now();
|
|
378
|
+
let result, owner, warnings, lineIndex;
|
|
379
|
+
const input = task.input;
|
|
380
|
+
try {
|
|
381
|
+
if (input instanceof Prepared) {
|
|
382
|
+
({ result, owner } = this.fromPrepared(input, 'fragment'));
|
|
383
|
+
warnings = result.ok ? input.hostOnly : [];
|
|
384
|
+
lineIndex = input.lineIndex;
|
|
385
|
+
} else if (input == null) {
|
|
386
|
+
result = { ok: false, diagnostics: [], error: task.error };
|
|
387
|
+
} else {
|
|
388
|
+
owner = this.newOwner('fragment');
|
|
389
|
+
const { time, progress } = this.clock();
|
|
390
|
+
result = compile(input, { cache: this.cache, owner, time, progress });
|
|
391
|
+
warnings = result.ok ? hostOnlyWarnings(result) : [];
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
result = { ok: false, diagnostics: [], error: err };
|
|
395
|
+
}
|
|
396
|
+
const ms = this.now() - t0;
|
|
397
|
+
if (!result.ok) {
|
|
398
|
+
if (owner) this.release(owner);
|
|
399
|
+
const msg = result.error ? String(result.error && result.error.message ? result.error.message : result.error) : formatDiagnostics({ diagnostics: result.diagnostics, lineIndex: lineIndex || (result.doc && result.doc.lineIndex) });
|
|
400
|
+
if (this.report) this.report(msg, { ...result, fragment: frag.id });
|
|
401
|
+
task.resolve({ ok: false, diagnostics: result.diagnostics, fragment: frag.id });
|
|
402
|
+
return ms;
|
|
403
|
+
}
|
|
404
|
+
// meshes made here may be stored from an earlier visit: read them before
|
|
405
|
+
// the first frame draws this fragment (a prepared one brought its meshes)
|
|
406
|
+
const wait = this.restore && !task.immediate && !result.prepared ? this.restore(this.cache, owner) : null;
|
|
407
|
+
if (wait) {
|
|
408
|
+
task.placing = { result, owner, warnings, input };
|
|
409
|
+
task.ready = false;
|
|
410
|
+
frag.tasks.push(task);
|
|
411
|
+
this.queue.push(task);
|
|
412
|
+
this.waiting++;
|
|
413
|
+
Promise.resolve(wait).catch(() => {}).then(() => {
|
|
414
|
+
this.waiting--;
|
|
415
|
+
task.ready = true;
|
|
416
|
+
});
|
|
417
|
+
return ms;
|
|
418
|
+
}
|
|
419
|
+
this.place(frag, result, owner, warnings, input);
|
|
420
|
+
task.resolve({ ok: true, diagnostics: frag.diagnostics, fragment: frag.id });
|
|
421
|
+
return ms;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
place(frag, result, owner, warnings, source, quiet = false) {
|
|
425
|
+
const before = frag.owner;
|
|
426
|
+
this.set.place(frag.part, result.scene);
|
|
427
|
+
frag.result = result;
|
|
428
|
+
frag.owner = owner;
|
|
429
|
+
frag.source = source instanceof Prepared ? source.source : source;
|
|
430
|
+
frag.warnings = warnings || [];
|
|
431
|
+
const { time, progress } = this.clock();
|
|
432
|
+
frag.time = time;
|
|
433
|
+
frag.progress = progress;
|
|
434
|
+
if (before && before !== owner) this.release(before);
|
|
435
|
+
this.changed = true;
|
|
436
|
+
if (!quiet && frag.warnings.length && this.report) {
|
|
437
|
+
this.report(formatDiagnostics({ diagnostics: frag.warnings, lineIndex: result.doc && !result.prepared ? result.doc.lineIndex : null }), { ok: true, diagnostics: frag.warnings, fragment: frag.id });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* A fragment's full compile result, with its scene graph. A prepared
|
|
443
|
+
* fragment has none yet: its document is compiled here now and drawn in
|
|
444
|
+
* its place (the same rows), once.
|
|
445
|
+
*/
|
|
446
|
+
materialize(frag) {
|
|
447
|
+
const res = frag.result;
|
|
448
|
+
if (res.graph) return res;
|
|
449
|
+
const owner = this.newOwner('fragment');
|
|
450
|
+
const { time, progress } = this.clock();
|
|
451
|
+
const full = compile(frag.source, { cache: this.cache, owner, time, progress });
|
|
452
|
+
this.place(frag, full, owner, frag.warnings, frag.source, true);
|
|
453
|
+
return full;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
removeFragment(frag) {
|
|
457
|
+
if (frag.removed) return;
|
|
458
|
+
frag.removed = true;
|
|
459
|
+
for (const t of frag.tasks) {
|
|
460
|
+
this.cancel(t);
|
|
461
|
+
t.resolve({ ok: false, diagnostics: [], fragment: frag.id, removed: true });
|
|
462
|
+
}
|
|
463
|
+
frag.tasks = [];
|
|
464
|
+
this.set.remove(frag.part);
|
|
465
|
+
this.release(frag.owner);
|
|
466
|
+
frag.owner = null;
|
|
467
|
+
this.byId.delete(frag.id);
|
|
468
|
+
this.changed = true;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Remove every fragment. */
|
|
472
|
+
clear() {
|
|
473
|
+
for (const f of this.list) this.removeFragment(f);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Compile a document for add() or show(), in the worker where there is one.
|
|
478
|
+
* @param {string|ArrayBuffer|Uint8Array} source
|
|
479
|
+
* @param {{as?: 'fragment'|'document'}} [opts]
|
|
480
|
+
* @returns {Promise<Prepared>}
|
|
481
|
+
*/
|
|
482
|
+
prepare(source, opts = {}) {
|
|
483
|
+
const as = opts.as === 'document' ? 'document' : 'fragment';
|
|
484
|
+
const { time, progress } = this.clock();
|
|
485
|
+
const here = () => {
|
|
486
|
+
const owner = this.newOwner(as);
|
|
487
|
+
const result = compile(source, { cache: this.cache, owner, time, progress });
|
|
488
|
+
if (!result.ok) this.release(owner);
|
|
489
|
+
return new Prepared({ as, source, result, owner: result.ok ? owner : null, hostOnly: result.ok ? hostOnlyWarnings(result) : [] });
|
|
490
|
+
};
|
|
491
|
+
if (!this.compiler) return Promise.resolve(here());
|
|
492
|
+
return this.compiler.compile(source, { time, progress, quality: this.cache.quality }).then((payload) => (payload ? new Prepared({ as, source, payload }) : here()));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// --- per frame ----------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Evaluate the animated rows of the drawn fragments at this clock (the
|
|
499
|
+
* stage animates the host as it always has). True when any row moved.
|
|
500
|
+
*/
|
|
501
|
+
updateDynamic(time, progress) {
|
|
502
|
+
let any = false;
|
|
503
|
+
for (const f of this.byId.values()) {
|
|
504
|
+
const r = f.result;
|
|
505
|
+
if (!r || !f.part.drawn || !r.scene.dynamic.length || !r.graph) continue;
|
|
506
|
+
if (f.time === time && f.progress === progress) continue;
|
|
507
|
+
updateDynamic(r.scene, r.graph, time, progress);
|
|
508
|
+
f.time = time;
|
|
509
|
+
f.progress = progress;
|
|
510
|
+
any = true;
|
|
511
|
+
}
|
|
512
|
+
if (any) this.set.invalidateBounds();
|
|
513
|
+
return any;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Names (name or id) of a picked element and its ancestors, from its own document. */
|
|
517
|
+
names(part, uid) {
|
|
518
|
+
if (!part || part === this.host) return ancestorNames(this.hostResult && this.hostResult.doc, uid);
|
|
519
|
+
const f = this.byId.get(part.id);
|
|
520
|
+
return f && f.result ? ancestorNames(f.result.doc, uid) : [];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
dispose() {
|
|
524
|
+
this.clear();
|
|
525
|
+
if (this.compiler && this.compiler.dispose) this.compiler.dispose();
|
|
526
|
+
}
|
|
527
|
+
}
|
package/src/scene/picking.js
CHANGED
|
@@ -84,10 +84,13 @@ function rayTriangles(o, d, mesh, best, facing = 0) {
|
|
|
84
84
|
* @param {{isVisible?: (i:number)=>boolean, maxDistance?: number, skipFlags?: number, facing?: boolean}} [opts]
|
|
85
85
|
* skipFlags: instances with any of these flag bits are ignored. facing: only
|
|
86
86
|
* surfaces whose front side faces the origin (double-sided geometry always counts).
|
|
87
|
-
* @returns {{index:number, uid:number, distance:number, point:number[], normal:number[]}|null}
|
|
88
|
-
* normal is the world-space surface normal turned against the ray.
|
|
87
|
+
* @returns {{index:number, uid:number, distance:number, point:number[], normal:number[], part?: object}|null}
|
|
88
|
+
* normal is the world-space surface normal turned against the ray. A SceneSet
|
|
89
|
+
* (scene-set.js) is searched part by part: isVisible(i, partScene) gets the
|
|
90
|
+
* part's own scene, and the hit names the part it is in (index is that part's row).
|
|
89
91
|
*/
|
|
90
92
|
export function raycast(scene, meshOf, origin, dir, opts = {}) {
|
|
93
|
+
if (scene && scene.parts) return raycastParts(scene, meshOf, origin, dir, opts);
|
|
91
94
|
const { isVisible = null, maxDistance = Infinity, skipFlags = 0, facing = false } = opts;
|
|
92
95
|
const cands = [];
|
|
93
96
|
const S = scene.spheres;
|
|
@@ -147,6 +150,20 @@ export function raycast(scene, meshOf, origin, dir, opts = {}) {
|
|
|
147
150
|
};
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
/** Nearest hit over the drawn parts of a scene set. */
|
|
154
|
+
function raycastParts(set, meshOf, origin, dir, opts) {
|
|
155
|
+
let best = null;
|
|
156
|
+
for (const part of set.parts) {
|
|
157
|
+
if (!part.scene || !part.visible) continue;
|
|
158
|
+
const P = part.scene;
|
|
159
|
+
const isVisible = opts.isVisible ? (i) => opts.isVisible(i, P) : null;
|
|
160
|
+
const maxDistance = best ? best.distance : opts.maxDistance ?? Infinity;
|
|
161
|
+
const hit = raycast(P, meshOf, origin, dir, { ...opts, isVisible, maxDistance });
|
|
162
|
+
if (hit && (!best || hit.distance < best.distance)) best = { ...hit, part };
|
|
163
|
+
}
|
|
164
|
+
return best;
|
|
165
|
+
}
|
|
166
|
+
|
|
150
167
|
/**
|
|
151
168
|
* Element under a ray (for selection).
|
|
152
169
|
* @param {import('./flatten.js').RenderScene} scene
|
|
@@ -159,3 +176,15 @@ export function raycast(scene, meshOf, origin, dir, opts = {}) {
|
|
|
159
176
|
export function pick(scene, meshOf, origin, dir, isVisible = null) {
|
|
160
177
|
return raycast(scene, meshOf, origin, dir, { isVisible });
|
|
161
178
|
}
|
|
179
|
+
|
|
180
|
+
/** Names (name or id) on an element and its ancestors, nearest first. */
|
|
181
|
+
export function ancestorNames(doc, uid) {
|
|
182
|
+
const names = [];
|
|
183
|
+
let el = doc && doc.byUid.get(uid);
|
|
184
|
+
while (el) {
|
|
185
|
+
const n = el.values && (el.values.name || el.values.id);
|
|
186
|
+
if (n) names.push(n);
|
|
187
|
+
el = el.parent;
|
|
188
|
+
}
|
|
189
|
+
return names;
|
|
190
|
+
}
|