@vectojs/core 1.14.0 → 1.16.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/dist/{chunk-XIEQHSBB.mjs → chunk-AQTO7OSU.mjs} +253 -496
- package/dist/{chunk-2Z23LTH3.js → chunk-DUYB4GX4.js} +310 -551
- package/dist/{chunk-BEUIB3U7.js → chunk-IYPLG4Z4.js} +93 -43
- package/dist/{chunk-L5BCKFQE.mjs → chunk-JFU56BX4.mjs} +48 -0
- package/dist/index.d.ts +4 -12
- package/dist/index.js +2155 -456
- package/dist/index.mjs +1977 -279
- package/dist/layout/index.d.ts +1 -3
- package/dist/layout.js +3 -15
- package/dist/layout.mjs +2 -16
- package/dist/renderer/CanvasRenderer.d.ts +22 -0
- package/dist/renderer/IRenderer.d.ts +13 -0
- package/dist/renderer.js +4 -3
- package/dist/renderer.mjs +1 -1
- package/dist/text/MSDFTextEntity.d.ts +30 -1
- package/dist/text/index.d.ts +1 -5
- package/dist/text.js +5 -15
- package/dist/text.mjs +6 -17
- package/dist/tree/ComputeParticleEntity.d.ts +18 -0
- package/dist/tree/DOMPortalEntity.d.ts +18 -0
- package/dist/tree/Entity.d.ts +122 -6
- package/dist/tree/Scene.d.ts +380 -0
- package/dist/wasm/anim-backend.d.ts +86 -0
- package/dist/wasm/asset.d.ts +25 -0
- package/dist/wasm/asset.js +7 -0
- package/dist/wasm/asset.mjs +5 -0
- package/dist/wasm/backend.d.ts +154 -0
- package/dist/wasm/hit-backend.d.ts +92 -0
- package/dist/wasm/hit-store.d.ts +48 -0
- package/dist/wasm/particle-backend.d.ts +104 -0
- package/dist/wasm/scene-store.d.ts +28 -0
- package/dist/wasm/soa.d.ts +146 -0
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +17 -8
- package/dist/animation/drivers.d.ts +0 -48
- package/dist/animation/easing.d.ts +0 -16
- package/dist/chunk-4AR425AR.js +0 -1121
- package/dist/chunk-BA5HUUDF.js +0 -760
- package/dist/chunk-IESDTEJ4.mjs +0 -1121
- package/dist/chunk-X7I465AQ.mjs +0 -760
- package/dist/layout/LayoutEngine.d.ts +0 -289
- package/dist/layout/LayoutWorker.d.ts +0 -23
- package/dist/layout/LayoutWorkerManager.d.ts +0 -26
- package/dist/layout/LayoutWorkerSource.d.ts +0 -1
- package/dist/layout/measure.d.ts +0 -20
- package/dist/math/SpatialHashGrid.d.ts +0 -53
- package/dist/math/SpringPhysics.d.ts +0 -13
- package/dist/text/ArabicShaper.d.ts +0 -10
- package/dist/text/BidiResolver.d.ts +0 -5
- package/dist/text/MSDFFont.d.ts +0 -129
- package/dist/text/PreparedContentGrid.d.ts +0 -60
- package/dist/text/Typography.d.ts +0 -11
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LayoutEngine,
|
|
3
|
-
LayoutResultBuffer,
|
|
4
|
-
computeLineSegments,
|
|
5
|
-
createCanvasMeasurer
|
|
6
|
-
} from "./chunk-X7I465AQ.mjs";
|
|
7
1
|
import {
|
|
8
2
|
CanvasRenderer,
|
|
9
3
|
SVGRenderer,
|
|
@@ -13,27 +7,16 @@ import {
|
|
|
13
7
|
isSafeUrl,
|
|
14
8
|
parseColorToRGBA,
|
|
15
9
|
sanitizeUrl
|
|
16
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JFU56BX4.mjs";
|
|
17
11
|
import {
|
|
18
|
-
Easing,
|
|
19
12
|
Entity,
|
|
20
|
-
MSDFFont,
|
|
21
13
|
MSDFTextEntity,
|
|
22
14
|
SVGEntity,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
cssLineBoxBaseline,
|
|
29
|
-
isTweenConfig,
|
|
30
|
-
prepareContentGrid
|
|
31
|
-
} from "./chunk-XIEQHSBB.mjs";
|
|
32
|
-
import {
|
|
33
|
-
ArabicShaper,
|
|
34
|
-
BidiResolver,
|
|
35
|
-
LayoutWorkerManager
|
|
36
|
-
} from "./chunk-IESDTEJ4.mjs";
|
|
15
|
+
VectoJSEvent
|
|
16
|
+
} from "./chunk-AQTO7OSU.mjs";
|
|
17
|
+
|
|
18
|
+
// src/tree/Scene.ts
|
|
19
|
+
import { SpringDriver, TweenDriver } from "@vectojs/animation";
|
|
37
20
|
|
|
38
21
|
// src/tree/ComputeParticleEntity.ts
|
|
39
22
|
var PARTICLE_STRIDE_FLOATS = 8;
|
|
@@ -68,6 +51,10 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
68
51
|
computeBindGroup = null;
|
|
69
52
|
/** WebGPU bind group for the render pass (usually same as compute). */
|
|
70
53
|
renderBindGroup = null;
|
|
54
|
+
/** When the last simulation step ran through the WASM backend, its fused
|
|
55
|
+
* pending-animation flag; `null` when the last step used the JS `updateCPU`
|
|
56
|
+
* path (so `hasPendingAnimations` falls back to its own scan). */
|
|
57
|
+
_wasmPending = null;
|
|
71
58
|
constructor(options = {}) {
|
|
72
59
|
super();
|
|
73
60
|
this.maxParticles = options.maxParticles ?? 1e4;
|
|
@@ -197,6 +184,9 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
197
184
|
* always return `true` and defeat the idle throttle entirely.
|
|
198
185
|
*/
|
|
199
186
|
hasPendingAnimations() {
|
|
187
|
+
if (this._wasmPending !== null) {
|
|
188
|
+
return this._wasmPending || this.pendingExplosion !== null;
|
|
189
|
+
}
|
|
200
190
|
const EPS_VELOCITY = 0.5;
|
|
201
191
|
const EPS_DISTANCE = 0.5;
|
|
202
192
|
for (let i = 0; i < this.maxParticles; i++) {
|
|
@@ -222,6 +212,7 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
222
212
|
* @param height - Boundary height.
|
|
223
213
|
*/
|
|
224
214
|
updateCPU(dt, mouseX, mouseY, width, height) {
|
|
215
|
+
this._wasmPending = null;
|
|
225
216
|
const safeDt = isNaN(dt) ? 0.016 : Math.max(0, Math.min(dt, 0.1));
|
|
226
217
|
const explosion = this.pendingExplosion;
|
|
227
218
|
const safeWidth = Math.max(1, width);
|
|
@@ -304,6 +295,37 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
304
295
|
}
|
|
305
296
|
this.pendingExplosion = null;
|
|
306
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Advance the simulation one step through the WASM particle kernel: transpose
|
|
300
|
+
* this entity's AoS buffer into the backend's SoA views, run `particle_step`,
|
|
301
|
+
* and scatter position/velocity/life back. Produces an f32 result (matching
|
|
302
|
+
* the WGSL shader) that differs from {@link updateCPU}'s f64 by <1 ULP/step —
|
|
303
|
+
* the accepted CPU-vs-GPU-class divergence. Caches the kernel's fused
|
|
304
|
+
* pending-animation flag so {@link hasPendingAnimations} needs no second scan.
|
|
305
|
+
*
|
|
306
|
+
* The backend holds one resident SoA store, so a Scene with multiple particle
|
|
307
|
+
* entities reuses it sequentially — origin is therefore re-gathered each call
|
|
308
|
+
* (not upload-once), a couple of extra f32 reads per particle.
|
|
309
|
+
*/
|
|
310
|
+
stepWithBackend(backend, dt, mouseX, mouseY, width, height) {
|
|
311
|
+
const count = this.maxParticles;
|
|
312
|
+
backend.ensure(count);
|
|
313
|
+
backend.gather(this.particleData, count, true);
|
|
314
|
+
this._wasmPending = backend.step(count, {
|
|
315
|
+
dt,
|
|
316
|
+
mouseX,
|
|
317
|
+
mouseY,
|
|
318
|
+
width,
|
|
319
|
+
height,
|
|
320
|
+
springK: this.springK,
|
|
321
|
+
damping: this.damping,
|
|
322
|
+
bounceDamping: this.bounceDamping,
|
|
323
|
+
maxVelocity: this.maxVelocity,
|
|
324
|
+
explosion: this.pendingExplosion
|
|
325
|
+
});
|
|
326
|
+
backend.scatter(this.particleData, count);
|
|
327
|
+
this.pendingExplosion = null;
|
|
328
|
+
}
|
|
307
329
|
destroy() {
|
|
308
330
|
this.destroyGPUResources();
|
|
309
331
|
super.destroy();
|
|
@@ -329,7 +351,751 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
329
351
|
}
|
|
330
352
|
};
|
|
331
353
|
|
|
354
|
+
// src/wasm/soa.ts
|
|
355
|
+
var PAD = 8;
|
|
356
|
+
function buildStore(nodes) {
|
|
357
|
+
const count = nodes.length;
|
|
358
|
+
const capacity = count + PAD;
|
|
359
|
+
const childrenOf = Array.from({ length: count }, () => []);
|
|
360
|
+
let rootInput = -1;
|
|
361
|
+
for (let k = 0; k < count; k++) {
|
|
362
|
+
const p = nodes[k].parent;
|
|
363
|
+
if (p === -1) {
|
|
364
|
+
if (rootInput !== -1) throw new Error("buildStore: more than one root (parent === -1)");
|
|
365
|
+
rootInput = k;
|
|
366
|
+
} else {
|
|
367
|
+
if (p < 0 || p >= count)
|
|
368
|
+
throw new Error(`buildStore: node ${k} has out-of-range parent ${p}`);
|
|
369
|
+
childrenOf[p].push(k);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (rootInput === -1) throw new Error("buildStore: no root (need exactly one parent === -1)");
|
|
373
|
+
const store = {
|
|
374
|
+
count,
|
|
375
|
+
capacity,
|
|
376
|
+
x: new Float64Array(capacity),
|
|
377
|
+
y: new Float64Array(capacity),
|
|
378
|
+
sx: new Float64Array(capacity),
|
|
379
|
+
sy: new Float64Array(capacity),
|
|
380
|
+
cos: new Float64Array(capacity),
|
|
381
|
+
sin: new Float64Array(capacity),
|
|
382
|
+
opacity: new Float64Array(capacity),
|
|
383
|
+
wa: new Float64Array(capacity),
|
|
384
|
+
wb: new Float64Array(capacity),
|
|
385
|
+
wc: new Float64Array(capacity),
|
|
386
|
+
wd: new Float64Array(capacity),
|
|
387
|
+
we: new Float64Array(capacity),
|
|
388
|
+
wf: new Float64Array(capacity),
|
|
389
|
+
wo: new Float64Array(capacity),
|
|
390
|
+
bx: new Float64Array(capacity),
|
|
391
|
+
by: new Float64Array(capacity),
|
|
392
|
+
bw: new Float64Array(capacity),
|
|
393
|
+
bh: new Float64Array(capacity),
|
|
394
|
+
aminx: new Float64Array(capacity),
|
|
395
|
+
aminy: new Float64Array(capacity),
|
|
396
|
+
amaxx: new Float64Array(capacity),
|
|
397
|
+
amaxy: new Float64Array(capacity),
|
|
398
|
+
runParent: new Int32Array(count),
|
|
399
|
+
runStart: new Int32Array(count),
|
|
400
|
+
runLen: new Int32Array(count),
|
|
401
|
+
runCount: 0,
|
|
402
|
+
storeIndexOf: new Int32Array(count).fill(-1)
|
|
403
|
+
};
|
|
404
|
+
store.storeIndexOf[rootInput] = 0;
|
|
405
|
+
writeInput(store, 0, nodes[rootInput]);
|
|
406
|
+
let next = 1;
|
|
407
|
+
const queue = [rootInput];
|
|
408
|
+
let head = 0;
|
|
409
|
+
while (head < queue.length) {
|
|
410
|
+
const parentInput = queue[head++];
|
|
411
|
+
const kids = childrenOf[parentInput];
|
|
412
|
+
if (kids.length === 0) continue;
|
|
413
|
+
const runStart = next;
|
|
414
|
+
for (const kid of kids) {
|
|
415
|
+
store.storeIndexOf[kid] = next;
|
|
416
|
+
writeInput(store, next, nodes[kid]);
|
|
417
|
+
next++;
|
|
418
|
+
}
|
|
419
|
+
const r = store.runCount++;
|
|
420
|
+
store.runParent[r] = store.storeIndexOf[parentInput];
|
|
421
|
+
store.runStart[r] = runStart;
|
|
422
|
+
store.runLen[r] = kids.length;
|
|
423
|
+
for (const kid of kids) queue.push(kid);
|
|
424
|
+
}
|
|
425
|
+
return store;
|
|
426
|
+
}
|
|
427
|
+
function writeInput(s, i, n) {
|
|
428
|
+
s.x[i] = n.x;
|
|
429
|
+
s.y[i] = n.y;
|
|
430
|
+
s.sx[i] = n.scaleX;
|
|
431
|
+
s.sy[i] = n.scaleY;
|
|
432
|
+
s.cos[i] = Math.cos(n.rotation);
|
|
433
|
+
s.sin[i] = Math.sin(n.rotation);
|
|
434
|
+
s.opacity[i] = n.opacity;
|
|
435
|
+
s.bx[i] = n.bx ?? 0;
|
|
436
|
+
s.by[i] = n.by ?? 0;
|
|
437
|
+
s.bw[i] = n.bw ?? 0;
|
|
438
|
+
s.bh[i] = n.bh ?? 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/wasm/scene-store.ts
|
|
442
|
+
function buildTreeStore(root) {
|
|
443
|
+
const entities = [];
|
|
444
|
+
const inputIndex = /* @__PURE__ */ new Map();
|
|
445
|
+
const collect = (e) => {
|
|
446
|
+
inputIndex.set(e, entities.length);
|
|
447
|
+
entities.push(e);
|
|
448
|
+
const kids = e.children;
|
|
449
|
+
for (let i = 0; i < kids.length; i++) collect(kids[i]);
|
|
450
|
+
};
|
|
451
|
+
collect(root);
|
|
452
|
+
const nodes = entities.map((e) => {
|
|
453
|
+
const parent = e.parent;
|
|
454
|
+
const p = parent !== null && inputIndex.has(parent) ? inputIndex.get(parent) : -1;
|
|
455
|
+
return {
|
|
456
|
+
parent: e === root ? -1 : p,
|
|
457
|
+
x: e.x,
|
|
458
|
+
y: e.y,
|
|
459
|
+
scaleX: e.scaleX,
|
|
460
|
+
scaleY: e.scaleY,
|
|
461
|
+
rotation: e.rotation,
|
|
462
|
+
opacity: e.opacity
|
|
463
|
+
};
|
|
464
|
+
});
|
|
465
|
+
const store = buildStore(nodes);
|
|
466
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
467
|
+
for (const [e, ii] of inputIndex) indexOf.set(e, store.storeIndexOf[ii]);
|
|
468
|
+
return { store, indexOf };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/wasm/backend.ts
|
|
472
|
+
var PAD2 = 8;
|
|
473
|
+
var WasmTransformBackend = class {
|
|
474
|
+
available = true;
|
|
475
|
+
ex;
|
|
476
|
+
cap = 0;
|
|
477
|
+
runCap = 0;
|
|
478
|
+
// Views over wasm linear memory, valid until the next init().
|
|
479
|
+
vx;
|
|
480
|
+
vy;
|
|
481
|
+
vsx;
|
|
482
|
+
vsy;
|
|
483
|
+
vcos;
|
|
484
|
+
vsin;
|
|
485
|
+
vop;
|
|
486
|
+
vwa;
|
|
487
|
+
vwb;
|
|
488
|
+
vwc;
|
|
489
|
+
vwd;
|
|
490
|
+
vwe;
|
|
491
|
+
vwf;
|
|
492
|
+
vwo;
|
|
493
|
+
vbx;
|
|
494
|
+
vby;
|
|
495
|
+
vbw;
|
|
496
|
+
vbh;
|
|
497
|
+
vaminx;
|
|
498
|
+
vaminy;
|
|
499
|
+
vamaxx;
|
|
500
|
+
vamaxy;
|
|
501
|
+
vrp;
|
|
502
|
+
vrs;
|
|
503
|
+
vrl;
|
|
504
|
+
constructor(instance) {
|
|
505
|
+
this.ex = instance.exports;
|
|
506
|
+
}
|
|
507
|
+
/** Compose world matrices for `store` in WASM, writing back into its
|
|
508
|
+
* `wa..wo` arrays. Result is bit-identical to `composeJS(store)`. */
|
|
509
|
+
compose(store, kernel = "simd") {
|
|
510
|
+
this.ensure(store.count, store.runCount);
|
|
511
|
+
const n = store.count;
|
|
512
|
+
this.vx.set(store.x.subarray(0, n));
|
|
513
|
+
this.vy.set(store.y.subarray(0, n));
|
|
514
|
+
this.vsx.set(store.sx.subarray(0, n));
|
|
515
|
+
this.vsy.set(store.sy.subarray(0, n));
|
|
516
|
+
this.vcos.set(store.cos.subarray(0, n));
|
|
517
|
+
this.vsin.set(store.sin.subarray(0, n));
|
|
518
|
+
this.vop.set(store.opacity.subarray(0, n));
|
|
519
|
+
const rc = store.runCount;
|
|
520
|
+
this.vrp.set(store.runParent.subarray(0, rc));
|
|
521
|
+
this.vrs.set(store.runStart.subarray(0, rc));
|
|
522
|
+
this.vrl.set(store.runLen.subarray(0, rc));
|
|
523
|
+
this.ex.set_run_count(rc);
|
|
524
|
+
if (kernel === "scalar") this.ex.compose_scalar();
|
|
525
|
+
else this.ex.compose_simd();
|
|
526
|
+
store.wa.set(this.vwa.subarray(0, n));
|
|
527
|
+
store.wb.set(this.vwb.subarray(0, n));
|
|
528
|
+
store.wc.set(this.vwc.subarray(0, n));
|
|
529
|
+
store.wd.set(this.vwd.subarray(0, n));
|
|
530
|
+
store.we.set(this.vwe.subarray(0, n));
|
|
531
|
+
store.wf.set(this.vwf.subarray(0, n));
|
|
532
|
+
store.wo.set(this.vwo.subarray(0, n));
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Run the kernel only, over data already resident in WASM memory — no upload,
|
|
536
|
+
* no readback. This is the per-frame cost the *designed* integration pays:
|
|
537
|
+
* entity accessors write `x/y/rotation` straight into the wasm input views
|
|
538
|
+
* (via {@link inputView}) and the renderer reads world matrices straight from
|
|
539
|
+
* the wasm output views (via {@link worldView}), so the batch copies in
|
|
540
|
+
* {@link compose} do not happen every frame. `compose` must have run at least
|
|
541
|
+
* once at the current capacity to size the store and set the run count; call
|
|
542
|
+
* {@link uploadRuns} after a topology change.
|
|
543
|
+
*/
|
|
544
|
+
runKernel(kernel = "simd") {
|
|
545
|
+
if (kernel === "scalar") this.ex.compose_scalar();
|
|
546
|
+
else this.ex.compose_simd();
|
|
547
|
+
}
|
|
548
|
+
/** Upload only the run table + count (topology), leaving per-entity inputs to
|
|
549
|
+
* the resident views. Call when the tree structure changes, not per frame. */
|
|
550
|
+
uploadRuns(store) {
|
|
551
|
+
this.ensure(store.count, store.runCount);
|
|
552
|
+
const rc = store.runCount;
|
|
553
|
+
this.vrp.set(store.runParent.subarray(0, rc));
|
|
554
|
+
this.vrs.set(store.runStart.subarray(0, rc));
|
|
555
|
+
this.vrl.set(store.runLen.subarray(0, rc));
|
|
556
|
+
this.ex.set_run_count(rc);
|
|
557
|
+
}
|
|
558
|
+
/** The resident wasm input views (`x,y,sx,sy,cos,sin,opacity`), valid until
|
|
559
|
+
* the next capacity growth. Writing here is what makes uploads unnecessary. */
|
|
560
|
+
inputView() {
|
|
561
|
+
return {
|
|
562
|
+
x: this.vx,
|
|
563
|
+
y: this.vy,
|
|
564
|
+
sx: this.vsx,
|
|
565
|
+
sy: this.vsy,
|
|
566
|
+
cos: this.vcos,
|
|
567
|
+
sin: this.vsin,
|
|
568
|
+
opacity: this.vop
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/** The resident wasm world-matrix output views (`wa..wo`). Reading here is
|
|
572
|
+
* what makes readback unnecessary. */
|
|
573
|
+
worldView() {
|
|
574
|
+
return {
|
|
575
|
+
wa: this.vwa,
|
|
576
|
+
wb: this.vwb,
|
|
577
|
+
wc: this.vwc,
|
|
578
|
+
wd: this.vwd,
|
|
579
|
+
we: this.vwe,
|
|
580
|
+
wf: this.vwf,
|
|
581
|
+
wo: this.vwo
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Compute world-space AABBs for `store` in WASM (G1+), writing back into its
|
|
586
|
+
* `aminx/aminy/amaxx/amaxy` arrays. Uploads the local bounds, runs the AABB
|
|
587
|
+
* pass, reads results back. Result is bit-identical to `computeAabbsJS(store)`.
|
|
588
|
+
* `compose` (or `runKernel`) must have populated the world matrices first —
|
|
589
|
+
* this pass reads them. For the resident (no-copy) integration, write bounds
|
|
590
|
+
* via {@link boundsView} and read via {@link aabbView} + call
|
|
591
|
+
* {@link runAabbs} instead.
|
|
592
|
+
*/
|
|
593
|
+
computeAabbs(store) {
|
|
594
|
+
this.ensure(store.count, store.runCount);
|
|
595
|
+
const n = store.count;
|
|
596
|
+
this.vbx.set(store.bx.subarray(0, n));
|
|
597
|
+
this.vby.set(store.by.subarray(0, n));
|
|
598
|
+
this.vbw.set(store.bw.subarray(0, n));
|
|
599
|
+
this.vbh.set(store.bh.subarray(0, n));
|
|
600
|
+
this.ex.compute_aabbs(n);
|
|
601
|
+
store.aminx.set(this.vaminx.subarray(0, n));
|
|
602
|
+
store.aminy.set(this.vaminy.subarray(0, n));
|
|
603
|
+
store.amaxx.set(this.vamaxx.subarray(0, n));
|
|
604
|
+
store.amaxy.set(this.vamaxy.subarray(0, n));
|
|
605
|
+
}
|
|
606
|
+
/** Run the AABB pass only, over `count` entities already resident in wasm
|
|
607
|
+
* memory (bounds written via {@link boundsView}, world matrices already
|
|
608
|
+
* composed). No upload/readback — the per-frame resident path. */
|
|
609
|
+
runAabbs(count) {
|
|
610
|
+
this.ex.compute_aabbs(count);
|
|
611
|
+
}
|
|
612
|
+
/** Resident wasm local-bounds input views (`bx,by,bw,bh`) for the AABB pass. */
|
|
613
|
+
boundsView() {
|
|
614
|
+
return { bx: this.vbx, by: this.vby, bw: this.vbw, bh: this.vbh };
|
|
615
|
+
}
|
|
616
|
+
/** Resident wasm world-AABB output views (`aminx,aminy,amaxx,amaxy`). */
|
|
617
|
+
aabbView() {
|
|
618
|
+
return {
|
|
619
|
+
aminx: this.vaminx,
|
|
620
|
+
aminy: this.vaminy,
|
|
621
|
+
amaxx: this.vamaxx,
|
|
622
|
+
amaxy: this.vamaxy
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
ensure(count, runCount) {
|
|
626
|
+
if (count + PAD2 <= this.cap && runCount <= this.runCap) return;
|
|
627
|
+
this.cap = count + PAD2;
|
|
628
|
+
this.runCap = Math.max(runCount, count, 1);
|
|
629
|
+
this.ex.init(count, this.runCap);
|
|
630
|
+
this.refreshViews();
|
|
631
|
+
}
|
|
632
|
+
/** Rebuild typed-array views after an init() (which may have grown, and thus
|
|
633
|
+
* detached, the memory buffer). */
|
|
634
|
+
refreshViews() {
|
|
635
|
+
const buf = this.ex.memory.buffer;
|
|
636
|
+
const cap = this.cap;
|
|
637
|
+
const rc = this.runCap;
|
|
638
|
+
const f64 = (ptr) => new Float64Array(buf, ptr, cap);
|
|
639
|
+
const i32 = (ptr) => new Int32Array(buf, ptr, rc);
|
|
640
|
+
this.vx = f64(this.ex.p_x());
|
|
641
|
+
this.vy = f64(this.ex.p_y());
|
|
642
|
+
this.vsx = f64(this.ex.p_sx());
|
|
643
|
+
this.vsy = f64(this.ex.p_sy());
|
|
644
|
+
this.vcos = f64(this.ex.p_cos());
|
|
645
|
+
this.vsin = f64(this.ex.p_sin());
|
|
646
|
+
this.vop = f64(this.ex.p_opacity());
|
|
647
|
+
this.vwa = f64(this.ex.p_wa());
|
|
648
|
+
this.vwb = f64(this.ex.p_wb());
|
|
649
|
+
this.vwc = f64(this.ex.p_wc());
|
|
650
|
+
this.vwd = f64(this.ex.p_wd());
|
|
651
|
+
this.vwe = f64(this.ex.p_we());
|
|
652
|
+
this.vwf = f64(this.ex.p_wf());
|
|
653
|
+
this.vwo = f64(this.ex.p_wo());
|
|
654
|
+
this.vbx = f64(this.ex.p_bx());
|
|
655
|
+
this.vby = f64(this.ex.p_by());
|
|
656
|
+
this.vbw = f64(this.ex.p_bw());
|
|
657
|
+
this.vbh = f64(this.ex.p_bh());
|
|
658
|
+
this.vaminx = f64(this.ex.p_aminx());
|
|
659
|
+
this.vaminy = f64(this.ex.p_aminy());
|
|
660
|
+
this.vamaxx = f64(this.ex.p_amaxx());
|
|
661
|
+
this.vamaxy = f64(this.ex.p_amaxy());
|
|
662
|
+
this.vrp = i32(this.ex.p_run_parent());
|
|
663
|
+
this.vrs = i32(this.ex.p_run_start());
|
|
664
|
+
this.vrl = i32(this.ex.p_run_len());
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
async function instantiateAsync(bytes) {
|
|
668
|
+
try {
|
|
669
|
+
const { instance } = await WebAssembly.instantiate(bytes, {});
|
|
670
|
+
return new WasmTransformBackend(instance);
|
|
671
|
+
} catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async function instantiateStreaming(source) {
|
|
676
|
+
try {
|
|
677
|
+
const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
|
|
678
|
+
if (typeof WebAssembly.instantiateStreaming === "function") {
|
|
679
|
+
const buffered = resp.clone();
|
|
680
|
+
try {
|
|
681
|
+
const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
|
|
682
|
+
return new WasmTransformBackend(instance2);
|
|
683
|
+
} catch {
|
|
684
|
+
const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
|
|
685
|
+
return new WasmTransformBackend(instance2);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
|
|
689
|
+
return new WasmTransformBackend(instance);
|
|
690
|
+
} catch {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/wasm/hit-store.ts
|
|
696
|
+
function gatherHitAABBs(root, currentFrame) {
|
|
697
|
+
const slotEntity = [];
|
|
698
|
+
const boundless = [];
|
|
699
|
+
const minxs = [];
|
|
700
|
+
const minys = [];
|
|
701
|
+
const maxxs = [];
|
|
702
|
+
const maxys = [];
|
|
703
|
+
const scratch = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
704
|
+
const visit = (node) => {
|
|
705
|
+
const index = slotEntity.length;
|
|
706
|
+
slotEntity.push(node);
|
|
707
|
+
const bounds = node.getBounds();
|
|
708
|
+
if (bounds === null) {
|
|
709
|
+
boundless.push({ entity: node, index });
|
|
710
|
+
minxs.push(0);
|
|
711
|
+
minys.push(0);
|
|
712
|
+
maxxs.push(0);
|
|
713
|
+
maxys.push(0);
|
|
714
|
+
} else {
|
|
715
|
+
if (!node._readWorldCache(currentFrame, scratch)) {
|
|
716
|
+
const t = node.getWorldTransform();
|
|
717
|
+
scratch.a = t.a;
|
|
718
|
+
scratch.b = t.b;
|
|
719
|
+
scratch.c = t.c;
|
|
720
|
+
scratch.d = t.d;
|
|
721
|
+
scratch.e = t.e;
|
|
722
|
+
scratch.f = t.f;
|
|
723
|
+
}
|
|
724
|
+
const { a, b, c, d, e, f } = scratch;
|
|
725
|
+
let minX = Infinity;
|
|
726
|
+
let minY = Infinity;
|
|
727
|
+
let maxX = -Infinity;
|
|
728
|
+
let maxY = -Infinity;
|
|
729
|
+
for (let i = 0; i < 4; i++) {
|
|
730
|
+
const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
|
|
731
|
+
const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
|
|
732
|
+
const wx = a * lx + c * ly + e;
|
|
733
|
+
const wy = b * lx + d * ly + f;
|
|
734
|
+
if (wx < minX) minX = wx;
|
|
735
|
+
if (wx > maxX) maxX = wx;
|
|
736
|
+
if (wy < minY) minY = wy;
|
|
737
|
+
if (wy > maxY) maxY = wy;
|
|
738
|
+
}
|
|
739
|
+
minxs.push(minX);
|
|
740
|
+
minys.push(minY);
|
|
741
|
+
maxxs.push(maxX);
|
|
742
|
+
maxys.push(maxY);
|
|
743
|
+
}
|
|
744
|
+
const kids = node.children;
|
|
745
|
+
for (let i = 0; i < kids.length; i++) visit(kids[i]);
|
|
746
|
+
};
|
|
747
|
+
visit(root);
|
|
748
|
+
return {
|
|
749
|
+
count: slotEntity.length,
|
|
750
|
+
slotEntity,
|
|
751
|
+
boundless,
|
|
752
|
+
minx: Float64Array.from(minxs),
|
|
753
|
+
miny: Float64Array.from(minys),
|
|
754
|
+
maxx: Float64Array.from(maxxs),
|
|
755
|
+
maxy: Float64Array.from(maxys)
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/wasm/hit-backend.ts
|
|
760
|
+
var PAD3 = 8;
|
|
761
|
+
var CELLS_PER_ENTITY_HINT = 4;
|
|
762
|
+
var HitTestBackend = class {
|
|
763
|
+
ex;
|
|
764
|
+
entityCap = 0;
|
|
765
|
+
cellCap = 0;
|
|
766
|
+
itemCap = 0;
|
|
767
|
+
vminx;
|
|
768
|
+
vminy;
|
|
769
|
+
vmaxx;
|
|
770
|
+
vmaxy;
|
|
771
|
+
vCellStart;
|
|
772
|
+
vCellCount;
|
|
773
|
+
vItems;
|
|
774
|
+
/** Grid geometry from the last {@link ensure} call. */
|
|
775
|
+
gridW = 0;
|
|
776
|
+
gridH = 0;
|
|
777
|
+
cellSize = 64;
|
|
778
|
+
constructor(instance) {
|
|
779
|
+
this.ex = instance.exports;
|
|
780
|
+
}
|
|
781
|
+
/** The resident AABB input views (`minx/miny/maxx/maxy`), valid until the
|
|
782
|
+
* next capacity growth. Writing here is what {@link build} reads from. */
|
|
783
|
+
inputView() {
|
|
784
|
+
return { minx: this.vminx, miny: this.vminy, maxx: this.vmaxx, maxy: this.vmaxy };
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Size (and grow, if needed) capacity for `count` entities over
|
|
788
|
+
* `[0,vw] x [0,vh]` at `cellSize`, and record the grid geometry
|
|
789
|
+
* {@link candidatesAt} needs. Call this BEFORE writing AABBs into
|
|
790
|
+
* {@link inputView} — a capacity growth detaches the previous views, so
|
|
791
|
+
* writing first and sizing after would write into a stale buffer.
|
|
792
|
+
*/
|
|
793
|
+
ensure(count, vw, vh, cellSize) {
|
|
794
|
+
const gw = Math.max(1, Math.ceil(vw / cellSize));
|
|
795
|
+
const gh = Math.max(1, Math.ceil(vh / cellSize));
|
|
796
|
+
this.growIfNeeded(count, gw * gh, count * CELLS_PER_ENTITY_HINT);
|
|
797
|
+
this.gridW = gw;
|
|
798
|
+
this.gridH = gh;
|
|
799
|
+
this.cellSize = cellSize;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Run the kernel's bucketing over whatever is currently resident in
|
|
803
|
+
* {@link inputView} (write the AABBs there, and call {@link ensure} first).
|
|
804
|
+
* Returns `false` if the build overflowed its item budget — the caller must
|
|
805
|
+
* not trust {@link candidatesAt} results for this build and should fall back
|
|
806
|
+
* to the JS walk instead (never return a wrong hit).
|
|
807
|
+
*/
|
|
808
|
+
runBuild(count, vw, vh, cellSize) {
|
|
809
|
+
this.ex.hit_build(count, vw, vh, cellSize);
|
|
810
|
+
return this.ex.hit_overflow() === 0;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Entity indices whose AABB overlaps the cell containing `(px, py)`, in
|
|
814
|
+
* ascending index order (scan from the end for topmost/highest-index
|
|
815
|
+
* first), or `null` if the point falls outside the built grid. This is a
|
|
816
|
+
* coarse candidate LIST, not a hit result — the caller must still confirm
|
|
817
|
+
* each candidate's AABB contains the point and re-check its precise
|
|
818
|
+
* `isPointInside`.
|
|
819
|
+
*/
|
|
820
|
+
candidatesAt(px, py) {
|
|
821
|
+
if (px < 0 || py < 0) return null;
|
|
822
|
+
const cx = Math.floor(px / this.cellSize);
|
|
823
|
+
const cy = Math.floor(py / this.cellSize);
|
|
824
|
+
if (cx < 0 || cy < 0 || cx >= this.gridW || cy >= this.gridH) return null;
|
|
825
|
+
const c = cy * this.gridW + cx;
|
|
826
|
+
if (c >= this.cellCap) return null;
|
|
827
|
+
const start = this.vCellStart[c];
|
|
828
|
+
const count = this.vCellCount[c];
|
|
829
|
+
return this.vItems.subarray(start, start + count);
|
|
830
|
+
}
|
|
831
|
+
growIfNeeded(count, cellCount, itemCount) {
|
|
832
|
+
if (count + PAD3 <= this.entityCap && cellCount <= this.cellCap && itemCount <= this.itemCap) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
this.entityCap = count + PAD3;
|
|
836
|
+
this.cellCap = Math.max(cellCount, 1);
|
|
837
|
+
this.itemCap = Math.max(itemCount, count, 1);
|
|
838
|
+
this.ex.hit_init(count, this.cellCap, this.itemCap);
|
|
839
|
+
this.refreshViews();
|
|
840
|
+
}
|
|
841
|
+
/** Rebuild typed-array views after a growing `hit_init` (which detaches the
|
|
842
|
+
* memory buffer). */
|
|
843
|
+
refreshViews() {
|
|
844
|
+
const buf = this.ex.memory.buffer;
|
|
845
|
+
const eCap = this.entityCap;
|
|
846
|
+
const cCap = this.cellCap;
|
|
847
|
+
const iCap = this.itemCap;
|
|
848
|
+
this.vminx = new Float64Array(buf, this.ex.p_h_minx(), eCap);
|
|
849
|
+
this.vminy = new Float64Array(buf, this.ex.p_h_miny(), eCap);
|
|
850
|
+
this.vmaxx = new Float64Array(buf, this.ex.p_h_maxx(), eCap);
|
|
851
|
+
this.vmaxy = new Float64Array(buf, this.ex.p_h_maxy(), eCap);
|
|
852
|
+
this.vCellStart = new Int32Array(buf, this.ex.p_h_cell_start(), cCap);
|
|
853
|
+
this.vCellCount = new Int32Array(buf, this.ex.p_h_cell_count(), cCap);
|
|
854
|
+
this.vItems = new Int32Array(buf, this.ex.p_h_items(), iCap);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
async function instantiateAsync2(bytes) {
|
|
858
|
+
try {
|
|
859
|
+
const { instance } = await WebAssembly.instantiate(bytes, {});
|
|
860
|
+
return new HitTestBackend(instance);
|
|
861
|
+
} catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function instantiateStreaming2(source) {
|
|
866
|
+
try {
|
|
867
|
+
const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
|
|
868
|
+
if (typeof WebAssembly.instantiateStreaming === "function") {
|
|
869
|
+
const buffered = resp.clone();
|
|
870
|
+
try {
|
|
871
|
+
const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
|
|
872
|
+
return new HitTestBackend(instance2);
|
|
873
|
+
} catch {
|
|
874
|
+
const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
|
|
875
|
+
return new HitTestBackend(instance2);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
|
|
879
|
+
return new HitTestBackend(instance);
|
|
880
|
+
} catch {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/wasm/anim-backend.ts
|
|
886
|
+
var PAD4 = 8;
|
|
887
|
+
var AnimBackend = class {
|
|
888
|
+
ex;
|
|
889
|
+
springCap = 0;
|
|
890
|
+
tweenCap = 0;
|
|
891
|
+
sv;
|
|
892
|
+
tv;
|
|
893
|
+
constructor(instance) {
|
|
894
|
+
this.ex = instance.exports;
|
|
895
|
+
}
|
|
896
|
+
/** The resident spring SoA input/output views, valid until the next capacity
|
|
897
|
+
* growth. Write gathered driver state here before calling {@link stepSprings}. */
|
|
898
|
+
springView() {
|
|
899
|
+
return this.sv;
|
|
900
|
+
}
|
|
901
|
+
/** The resident tween SoA input/output views, valid until the next capacity
|
|
902
|
+
* growth. Write gathered driver state here before calling {@link stepTweens}. */
|
|
903
|
+
tweenView() {
|
|
904
|
+
return this.tv;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Size (and grow, if needed) capacity for `springCount` springs and
|
|
908
|
+
* `tweenCount` tweens. Call this BEFORE writing into {@link springView}/
|
|
909
|
+
* {@link tweenView} — a capacity growth detaches the previous views, so
|
|
910
|
+
* writing first and sizing after would write into a stale buffer.
|
|
911
|
+
*/
|
|
912
|
+
ensure(springCount, tweenCount) {
|
|
913
|
+
if (springCount + PAD4 <= this.springCap && tweenCount + PAD4 <= this.tweenCap) return;
|
|
914
|
+
this.springCap = springCount + PAD4;
|
|
915
|
+
this.tweenCap = tweenCount + PAD4;
|
|
916
|
+
this.ex.anim_init(this.springCap, this.tweenCap);
|
|
917
|
+
this.refreshViews();
|
|
918
|
+
}
|
|
919
|
+
/** Advance `count` springs (from index 0) by `dtMs` milliseconds, in place. */
|
|
920
|
+
stepSprings(dtMs, count) {
|
|
921
|
+
this.ex.spring_step(dtMs / 1e3, count);
|
|
922
|
+
}
|
|
923
|
+
/** Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`. */
|
|
924
|
+
stepTweens(dtMs, count) {
|
|
925
|
+
this.ex.tween_step(dtMs, count);
|
|
926
|
+
}
|
|
927
|
+
refreshViews() {
|
|
928
|
+
const buf = this.ex.memory.buffer;
|
|
929
|
+
const sCap = this.springCap;
|
|
930
|
+
const tCap = this.tweenCap;
|
|
931
|
+
this.sv = {
|
|
932
|
+
val: new Float64Array(buf, this.ex.p_s_val(), sCap),
|
|
933
|
+
target: new Float64Array(buf, this.ex.p_s_target(), sCap),
|
|
934
|
+
vel: new Float64Array(buf, this.ex.p_s_vel(), sCap),
|
|
935
|
+
stiff: new Float64Array(buf, this.ex.p_s_stiff(), sCap),
|
|
936
|
+
damp: new Float64Array(buf, this.ex.p_s_damp(), sCap),
|
|
937
|
+
mass: new Float64Array(buf, this.ex.p_s_mass(), sCap)
|
|
938
|
+
};
|
|
939
|
+
this.tv = {
|
|
940
|
+
from: new Float64Array(buf, this.ex.p_t_from(), tCap),
|
|
941
|
+
to: new Float64Array(buf, this.ex.p_t_to(), tCap),
|
|
942
|
+
elapsed: new Float64Array(buf, this.ex.p_t_elapsed(), tCap),
|
|
943
|
+
dur: new Float64Array(buf, this.ex.p_t_dur(), tCap),
|
|
944
|
+
delay: new Float64Array(buf, this.ex.p_t_delay(), tCap),
|
|
945
|
+
ease: new Float64Array(buf, this.ex.p_t_ease(), tCap),
|
|
946
|
+
val: new Float64Array(buf, this.ex.p_t_val(), tCap)
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
async function instantiateAsync3(bytes) {
|
|
951
|
+
try {
|
|
952
|
+
const { instance } = await WebAssembly.instantiate(bytes, {});
|
|
953
|
+
return new AnimBackend(instance);
|
|
954
|
+
} catch {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async function instantiateStreaming3(source) {
|
|
959
|
+
try {
|
|
960
|
+
const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
|
|
961
|
+
if (typeof WebAssembly.instantiateStreaming === "function") {
|
|
962
|
+
const buffered = resp.clone();
|
|
963
|
+
try {
|
|
964
|
+
const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
|
|
965
|
+
return new AnimBackend(instance2);
|
|
966
|
+
} catch {
|
|
967
|
+
const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
|
|
968
|
+
return new AnimBackend(instance2);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
|
|
972
|
+
return new AnimBackend(instance);
|
|
973
|
+
} catch {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/wasm/particle-backend.ts
|
|
979
|
+
var PAD5 = 8;
|
|
980
|
+
var ParticleBackend = class {
|
|
981
|
+
ex;
|
|
982
|
+
cap = 0;
|
|
983
|
+
view;
|
|
984
|
+
constructor(instance) {
|
|
985
|
+
this.ex = instance.exports;
|
|
986
|
+
}
|
|
987
|
+
/** The resident SoA views, valid until the next capacity growth. */
|
|
988
|
+
particleView() {
|
|
989
|
+
return this.view;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Size (and grow, if needed) capacity for `count` particles. Call BEFORE
|
|
993
|
+
* writing into {@link particleView} — a growth detaches the previous views.
|
|
994
|
+
*/
|
|
995
|
+
ensure(count) {
|
|
996
|
+
if (count + PAD5 <= this.cap) return;
|
|
997
|
+
this.cap = count + PAD5;
|
|
998
|
+
this.ex.particle_init(this.cap);
|
|
999
|
+
this.refreshViews();
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Advance `count` particles one step in place. Returns `true` when at least
|
|
1003
|
+
* one live particle is still moving or off-origin beyond epsilon (the fused
|
|
1004
|
+
* `hasPendingAnimations` flag), so the caller need not re-scan the buffer.
|
|
1005
|
+
*/
|
|
1006
|
+
step(count, p) {
|
|
1007
|
+
const e = p.explosion;
|
|
1008
|
+
const flag = this.ex.particle_step(
|
|
1009
|
+
p.dt,
|
|
1010
|
+
p.mouseX,
|
|
1011
|
+
p.mouseY,
|
|
1012
|
+
p.width,
|
|
1013
|
+
p.height,
|
|
1014
|
+
p.springK,
|
|
1015
|
+
p.damping,
|
|
1016
|
+
p.bounceDamping,
|
|
1017
|
+
p.maxVelocity,
|
|
1018
|
+
e ? 1 : 0,
|
|
1019
|
+
e ? e.x : 0,
|
|
1020
|
+
e ? e.y : 0,
|
|
1021
|
+
e ? e.force : 0,
|
|
1022
|
+
count
|
|
1023
|
+
);
|
|
1024
|
+
return flag !== 0;
|
|
1025
|
+
}
|
|
1026
|
+
/** Transpose the AoS stride-8 buffer into the SoA views (position/velocity/
|
|
1027
|
+
* life every frame; origin upload-once when `withOrigin`). */
|
|
1028
|
+
gather(data, count, withOrigin) {
|
|
1029
|
+
const v = this.view;
|
|
1030
|
+
for (let i = 0; i < count; i++) {
|
|
1031
|
+
const o = i * PARTICLE_STRIDE_FLOATS;
|
|
1032
|
+
v.px[i] = data[o + PARTICLE_OFFSET_POSITION_X];
|
|
1033
|
+
v.py[i] = data[o + PARTICLE_OFFSET_POSITION_Y];
|
|
1034
|
+
v.vx[i] = data[o + PARTICLE_OFFSET_VELOCITY_X];
|
|
1035
|
+
v.vy[i] = data[o + PARTICLE_OFFSET_VELOCITY_Y];
|
|
1036
|
+
v.life[i] = data[o + PARTICLE_OFFSET_LIFE];
|
|
1037
|
+
if (withOrigin) {
|
|
1038
|
+
v.ox[i] = data[o + PARTICLE_OFFSET_ORIGIN_X];
|
|
1039
|
+
v.oy[i] = data[o + PARTICLE_OFFSET_ORIGIN_Y];
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/** Scatter the mutated position/velocity/life back into the AoS buffer. */
|
|
1044
|
+
scatter(data, count) {
|
|
1045
|
+
const v = this.view;
|
|
1046
|
+
for (let i = 0; i < count; i++) {
|
|
1047
|
+
const o = i * PARTICLE_STRIDE_FLOATS;
|
|
1048
|
+
data[o + PARTICLE_OFFSET_POSITION_X] = v.px[i];
|
|
1049
|
+
data[o + PARTICLE_OFFSET_POSITION_Y] = v.py[i];
|
|
1050
|
+
data[o + PARTICLE_OFFSET_VELOCITY_X] = v.vx[i];
|
|
1051
|
+
data[o + PARTICLE_OFFSET_VELOCITY_Y] = v.vy[i];
|
|
1052
|
+
data[o + PARTICLE_OFFSET_LIFE] = v.life[i];
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
refreshViews() {
|
|
1056
|
+
const buf = this.ex.memory.buffer;
|
|
1057
|
+
const c = this.cap;
|
|
1058
|
+
this.view = {
|
|
1059
|
+
px: new Float32Array(buf, this.ex.pp_px(), c),
|
|
1060
|
+
py: new Float32Array(buf, this.ex.pp_py(), c),
|
|
1061
|
+
vx: new Float32Array(buf, this.ex.pp_vx(), c),
|
|
1062
|
+
vy: new Float32Array(buf, this.ex.pp_vy(), c),
|
|
1063
|
+
ox: new Float32Array(buf, this.ex.pp_ox(), c),
|
|
1064
|
+
oy: new Float32Array(buf, this.ex.pp_oy(), c),
|
|
1065
|
+
life: new Float32Array(buf, this.ex.pp_life(), c)
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
async function instantiateAsync4(bytes) {
|
|
1070
|
+
try {
|
|
1071
|
+
const { instance } = await WebAssembly.instantiate(bytes, {});
|
|
1072
|
+
return new ParticleBackend(instance);
|
|
1073
|
+
} catch {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
async function instantiateStreaming4(source) {
|
|
1078
|
+
try {
|
|
1079
|
+
const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
|
|
1080
|
+
if (typeof WebAssembly.instantiateStreaming === "function") {
|
|
1081
|
+
const buffered = resp.clone();
|
|
1082
|
+
try {
|
|
1083
|
+
const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
|
|
1084
|
+
return new ParticleBackend(instance2);
|
|
1085
|
+
} catch {
|
|
1086
|
+
const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
|
|
1087
|
+
return new ParticleBackend(instance2);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
|
|
1091
|
+
return new ParticleBackend(instance);
|
|
1092
|
+
} catch {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
332
1097
|
// src/tree/Scene.ts
|
|
1098
|
+
import { clearCssLineBoxMetrics, cssLineBoxBaseline } from "@vectojs/text";
|
|
333
1099
|
var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
|
|
334
1100
|
"button",
|
|
335
1101
|
"switch",
|
|
@@ -644,6 +1410,10 @@ function extendSelection(selection, anchor, focus) {
|
|
|
644
1410
|
var Scene = class _Scene {
|
|
645
1411
|
static webglCreator = null;
|
|
646
1412
|
static webgpuManagerClass = null;
|
|
1413
|
+
/** Upper bound (ms) on a single frame's `dt`. Caps the giant elapsed gap a
|
|
1414
|
+
* backgrounded/refocused tab produces so physics advances at most one slow
|
|
1415
|
+
* frame instead of the whole idle duration (~100ms ≈ 6 frames at 60fps). */
|
|
1416
|
+
static MAX_FRAME_DT = 100;
|
|
647
1417
|
static registerWebGLPointRendererCreator(creator) {
|
|
648
1418
|
_Scene.webglCreator = creator;
|
|
649
1419
|
}
|
|
@@ -654,6 +1424,13 @@ var Scene = class _Scene {
|
|
|
654
1424
|
overlayRoot;
|
|
655
1425
|
renderer;
|
|
656
1426
|
isRunning = false;
|
|
1427
|
+
/** Whether the canvas is at least partially in the viewport. When it scrolls
|
|
1428
|
+
* fully off-screen the rAF loop pauses (stops rescheduling) instead of
|
|
1429
|
+
* burning frames on a scene nobody can see; an IntersectionObserver resumes
|
|
1430
|
+
* it on re-entry. Defaults true (and stays true where IntersectionObserver
|
|
1431
|
+
* is unavailable, e.g. SSR/jsdom, so behavior is unchanged there). */
|
|
1432
|
+
_canvasOnScreen = true;
|
|
1433
|
+
_canvasObserver = null;
|
|
657
1434
|
lastTime = 0;
|
|
658
1435
|
canvas;
|
|
659
1436
|
/**
|
|
@@ -688,12 +1465,45 @@ var Scene = class _Scene {
|
|
|
688
1465
|
maxFPS = 60;
|
|
689
1466
|
/** Whether the OS prefers-reduced-motion setting auto-caps the loop. */
|
|
690
1467
|
respectReducedMotion = true;
|
|
1468
|
+
/**
|
|
1469
|
+
* Reading direction for accessibility tab/traversal order (`'ltr'` default,
|
|
1470
|
+
* `'rtl'`). Controls the inline sort within a visual row in
|
|
1471
|
+
* {@link enforceA11yDomOrder}. Set at runtime to re-flow tab order on the
|
|
1472
|
+
* next sync (also trips a reorder).
|
|
1473
|
+
*/
|
|
1474
|
+
get readingDirection() {
|
|
1475
|
+
return this._readingDirection;
|
|
1476
|
+
}
|
|
1477
|
+
set readingDirection(dir) {
|
|
1478
|
+
if (dir !== this._readingDirection) {
|
|
1479
|
+
this._readingDirection = dir;
|
|
1480
|
+
this.a11yNeedsReorder = true;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
_readingDirection = "ltr";
|
|
691
1484
|
/** Cached media-query list; `.matches` is read live each frame. */
|
|
692
1485
|
reducedMotionQuery = null;
|
|
1486
|
+
/** Cached `(forced-colors: active)` query (Windows High Contrast etc.). A
|
|
1487
|
+
* canvas gets NO automatic forced-colors treatment from the browser (it's
|
|
1488
|
+
* opaque pixels), so components must read {@link forcedColors} and repaint
|
|
1489
|
+
* with system colors themselves; a change listener repaints idle scenes. */
|
|
1490
|
+
forcedColorsQuery = null;
|
|
1491
|
+
forcedColorsChangeHandler = null;
|
|
693
1492
|
/** True when the OS asks for reduced motion and we respect it. Read by the animation drivers. */
|
|
694
1493
|
get prefersReducedMotion() {
|
|
695
1494
|
return this.respectReducedMotion && !!this.reducedMotionQuery?.matches;
|
|
696
1495
|
}
|
|
1496
|
+
/**
|
|
1497
|
+
* True when the OS is in a forced-colors mode (Windows High Contrast, and the
|
|
1498
|
+
* `forced-colors: active` media feature generally). Canvas pixels are exempt
|
|
1499
|
+
* from the browser's forced-colors remapping, so accessible components should
|
|
1500
|
+
* read this and draw with CSS system colors (`CanvasText`, `Canvas`,
|
|
1501
|
+
* `Highlight`, …) instead of their themed palette. Re-rendered automatically
|
|
1502
|
+
* when the setting toggles.
|
|
1503
|
+
*/
|
|
1504
|
+
get forcedColors() {
|
|
1505
|
+
return !!this.forcedColorsQuery?.matches;
|
|
1506
|
+
}
|
|
697
1507
|
/**
|
|
698
1508
|
* Throttle interval (ms) for the a11y/automation shadow sync. `0` = every
|
|
699
1509
|
* frame. See {@link SceneOptions.a11ySyncInterval}.
|
|
@@ -738,7 +1548,32 @@ var Scene = class _Scene {
|
|
|
738
1548
|
frameHadAnimation = true;
|
|
739
1549
|
frameHadInteractive = true;
|
|
740
1550
|
resizeHandler;
|
|
1551
|
+
/** Active `(resolution: Ndppx)` media query watching for a runtime DPR change
|
|
1552
|
+
* (window moved between monitors, browser zoom) so the canvas backing store
|
|
1553
|
+
* can be re-scaled — otherwise it stays rasterized at the old DPR and blurs.
|
|
1554
|
+
* A resolution media query only fires when leaving its exact value, so the
|
|
1555
|
+
* handler re-arms a fresh query for the new DPR each time. */
|
|
1556
|
+
dprMediaQuery = null;
|
|
1557
|
+
/** For embedded (`disableWindowResize`) scenes: observes the canvas element so
|
|
1558
|
+
* a CSS/layout-driven size change re-runs `resize()`. A window `resize`
|
|
1559
|
+
* listener never fires for these (the window isn't what changed), so without
|
|
1560
|
+
* this an embedded canvas stayed at its initial size forever. */
|
|
1561
|
+
canvasResizeObserver = null;
|
|
1562
|
+
dprChangeHandler = null;
|
|
741
1563
|
focusedA11yElement = null;
|
|
1564
|
+
/** Last geometry `syncOverlayGeometry` wrote, so an unchanged frame can skip the
|
|
1565
|
+
* style writes entirely. Reset to `null` to force the next sync (a new overlay
|
|
1566
|
+
* layer was created and has never been positioned). */
|
|
1567
|
+
_overlayGeometry = null;
|
|
1568
|
+
/** Shadow elements the pointer is currently inside. Lets a removal that happens
|
|
1569
|
+
* mid-hover synthesize the `pointerleave` the browser never sends for a
|
|
1570
|
+
* detached element, so the entity doesn't keep its hover state. */
|
|
1571
|
+
hoveredA11yElements = /* @__PURE__ */ new WeakSet();
|
|
1572
|
+
/** Persistent tabindex=-1 element in a11yRoot. When the focused a11y mirror is
|
|
1573
|
+
* pruned (virtualization/streaming/removal) while it holds focus, we move
|
|
1574
|
+
* focus here instead of letting the browser drop it to <body> — keeping the
|
|
1575
|
+
* screen-reader virtual cursor inside the scene's a11y region. */
|
|
1576
|
+
focusSentinel = null;
|
|
742
1577
|
caretBlinkTimer = null;
|
|
743
1578
|
a11yNeedsReorder = true;
|
|
744
1579
|
portalRoot = null;
|
|
@@ -749,6 +1584,510 @@ var Scene = class _Scene {
|
|
|
749
1584
|
activePortalsPrevFrame = /* @__PURE__ */ new Set();
|
|
750
1585
|
portalEntities = /* @__PURE__ */ new Map();
|
|
751
1586
|
renderOrderCounter = 0;
|
|
1587
|
+
/**
|
|
1588
|
+
* Monotonic render-frame counter, bumped once per authoritative `render()`
|
|
1589
|
+
* pass. Entities stamp their per-frame world-matrix cache with this value and
|
|
1590
|
+
* {@link Entity.getWorldTransform} trusts that cache only while it still
|
|
1591
|
+
* matches, so a query outside the frame that produced it transparently falls
|
|
1592
|
+
* back to the ancestor walk. Public for the same reason `Entity._getTrig`/
|
|
1593
|
+
* `_setWorldCache` are: it is a cross-class render-internal contract.
|
|
1594
|
+
*/
|
|
1595
|
+
currentFrame = 0;
|
|
1596
|
+
// ── WASM transform backend (invisible accelerator) ──────────────────────────
|
|
1597
|
+
// When `_transformBackend === 'wasm'`, the main render walk sources each
|
|
1598
|
+
// entity's world matrix from an SoA store composed by `_wasm` (see
|
|
1599
|
+
// `renderNode`), instead of composing it in JS. The JS path is the permanent
|
|
1600
|
+
// fallback and the default: a null backend, a non-main renderer, or any entity
|
|
1601
|
+
// absent from the store all fall back to the JS composition, so WASM can only
|
|
1602
|
+
// ever change *how fast* a world matrix is produced, never *what* it is.
|
|
1603
|
+
_wasm = null;
|
|
1604
|
+
_transformBackend = "js";
|
|
1605
|
+
// Resident store state (Stage 3). The store layout — slot assignment + sibling
|
|
1606
|
+
// runs — depends only on tree TOPOLOGY, so it is rebuilt only when the
|
|
1607
|
+
// structure changes (add/remove/reparent bump `_structureVersion`). Between
|
|
1608
|
+
// rebuilds the per-frame cost is: gather each entity's transform into the
|
|
1609
|
+
// resident wasm input view + run the kernel — no reallocation, no readback.
|
|
1610
|
+
_treeStore = null;
|
|
1611
|
+
_slotEntity = [];
|
|
1612
|
+
// store slot -> entity (also validates slots)
|
|
1613
|
+
_wasmInputs = null;
|
|
1614
|
+
_wasmWorld = null;
|
|
1615
|
+
_structureVersion = 0;
|
|
1616
|
+
_storeStructureVersion = -1;
|
|
1617
|
+
// Cached list of ComputeParticleEntity instances in the tree, keyed by the
|
|
1618
|
+
// structure version it was gathered at. Rebuilt only on a topology change.
|
|
1619
|
+
_computeEntities = [];
|
|
1620
|
+
_computeEntitiesVersion = -1;
|
|
1621
|
+
/** Invalidate the resident WASM store layout; the next wasm-mode frame rebuilds
|
|
1622
|
+
* it. Called by `Entity.add`/`remove` (topology changes only). */
|
|
1623
|
+
markStructureChanged() {
|
|
1624
|
+
this._structureVersion++;
|
|
1625
|
+
}
|
|
1626
|
+
/** The tree's ComputeParticleEntity instances, cached per structure version so
|
|
1627
|
+
* a compute-free scene doesn't re-walk the whole tree every frame. */
|
|
1628
|
+
_computeEntitiesFor(version) {
|
|
1629
|
+
if (this._computeEntitiesVersion === version) return this._computeEntities;
|
|
1630
|
+
const list = [];
|
|
1631
|
+
const collect = (node) => {
|
|
1632
|
+
if (node instanceof ComputeParticleEntity) list.push(node);
|
|
1633
|
+
for (const child of node.children) collect(child);
|
|
1634
|
+
};
|
|
1635
|
+
collect(this.root);
|
|
1636
|
+
for (const overlay of this.overlayRoot.children) collect(overlay);
|
|
1637
|
+
this._computeEntities = list;
|
|
1638
|
+
this._computeEntitiesVersion = version;
|
|
1639
|
+
return list;
|
|
1640
|
+
}
|
|
1641
|
+
/** Which backend composes world matrices for the main render walk. */
|
|
1642
|
+
get transformBackend() {
|
|
1643
|
+
return this._transformBackend;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Install (or clear) a WASM transform backend. Passing a backend switches the
|
|
1647
|
+
* main render walk onto it; passing `null` reverts to the JS path. Synchronous
|
|
1648
|
+
* and safe to call between frames — the next `render()` picks it up. Prefer
|
|
1649
|
+
* {@link enableWasmTransforms} for the normal async hot-swap.
|
|
1650
|
+
*/
|
|
1651
|
+
setTransformBackend(backend) {
|
|
1652
|
+
this._wasm = backend;
|
|
1653
|
+
this._transformBackend = backend ? "wasm" : "js";
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Asynchronously instantiate the WASM transform core and, on success, hot-swap
|
|
1657
|
+
* the render walk onto it. Accepts whatever is convenient at the call site:
|
|
1658
|
+
*
|
|
1659
|
+
* ```ts
|
|
1660
|
+
* // The common case — a bundler-emitted, co-located asset URL:
|
|
1661
|
+
* await scene.enableWasmTransforms(new URL('./vectojs_core.wasm', import.meta.url));
|
|
1662
|
+
* // …or a path string, a Response, or raw bytes you already have:
|
|
1663
|
+
* await scene.enableWasmTransforms('/assets/vectojs_core.wasm');
|
|
1664
|
+
* await scene.enableWasmTransforms(await fetch(url));
|
|
1665
|
+
* await scene.enableWasmTransforms(myUint8Array);
|
|
1666
|
+
* ```
|
|
1667
|
+
*
|
|
1668
|
+
* A URL/Response streams (compiles while it downloads, with a buffered
|
|
1669
|
+
* fallback for a wrong MIME type); raw bytes instantiate directly. The Scene
|
|
1670
|
+
* keeps rendering on the JS path until this resolves, and stays on JS if
|
|
1671
|
+
* instantiation fails (CSP `wasm-unsafe-eval`, unsupported SIMD, corrupt or
|
|
1672
|
+
* missing bytes, a 404) — failure is the default state, not an error path.
|
|
1673
|
+
* Resolves `true` if WASM is now active, `false` if the JS path remains.
|
|
1674
|
+
*/
|
|
1675
|
+
async enableWasmTransforms(source) {
|
|
1676
|
+
const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
|
|
1677
|
+
const backend = isBytes ? await instantiateAsync(source) : await instantiateStreaming(source);
|
|
1678
|
+
if (!backend) return false;
|
|
1679
|
+
this.setTransformBackend(backend);
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1682
|
+
// ── WASM hit-test backend (invisible accelerator, G3) ───────────────────────
|
|
1683
|
+
// A separate WASM module instance from the transform backend (each crate
|
|
1684
|
+
// export lives in independent linear memory per instance, so there is no
|
|
1685
|
+
// shared-state hazard in running both) that indexes the main tree's world
|
|
1686
|
+
// AABBs into a dense viewport grid for findEntityAt. The JS depth-first walk
|
|
1687
|
+
// (findHitRecursively) is the permanent fallback: a null backend, a build
|
|
1688
|
+
// that overflows its item budget, or the overlay tree (never indexed — small
|
|
1689
|
+
// and rare, not worth accelerating) all fall through to it, so WASM can only
|
|
1690
|
+
// ever change *how fast* a hit is found, never *which* entity is returned —
|
|
1691
|
+
// every grid candidate is re-confirmed against its own precise
|
|
1692
|
+
// isPointInside before being trusted (see hit-store.ts / hit-backend.ts).
|
|
1693
|
+
_hitWasm = null;
|
|
1694
|
+
// Cache key: which frame + structure version the grid was last (successfully,
|
|
1695
|
+
// non-overflowing) built for. findEntityAt is called ad-hoc (pointer
|
|
1696
|
+
// hover/click), not every frame, so the grid is refreshed lazily on demand
|
|
1697
|
+
// rather than proactively every render() — unlike the transform store, which
|
|
1698
|
+
// every frame's draw depends on.
|
|
1699
|
+
_hitGridFrame = -1;
|
|
1700
|
+
_hitGridOk = false;
|
|
1701
|
+
_hitSlotEntity = [];
|
|
1702
|
+
_hitBoundless = [];
|
|
1703
|
+
/** Which backend answers `findEntityAt` for the main tree. */
|
|
1704
|
+
get hitTestBackend() {
|
|
1705
|
+
return this._hitWasm ? "wasm" : "js";
|
|
1706
|
+
}
|
|
1707
|
+
/** Install (or clear) a WASM hit-test backend directly. Prefer
|
|
1708
|
+
* {@link enableWasmHitTest} for the normal async hot-swap. */
|
|
1709
|
+
setHitTestBackend(backend) {
|
|
1710
|
+
this._hitWasm = backend;
|
|
1711
|
+
this._hitGridFrame = -1;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Asynchronously instantiate the WASM hit-test core and, on success, hot-swap
|
|
1715
|
+
* `findEntityAt` onto it. Accepts the same source shapes as
|
|
1716
|
+
* {@link enableWasmTransforms} (URL, path string, Response, or raw bytes).
|
|
1717
|
+
* Stays on the JS walk if instantiation fails — failure is the default
|
|
1718
|
+
* state, not an error path. Resolves `true` if WASM is now active.
|
|
1719
|
+
*/
|
|
1720
|
+
async enableWasmHitTest(source) {
|
|
1721
|
+
const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
|
|
1722
|
+
const backend = isBytes ? await instantiateAsync2(source) : await instantiateStreaming2(source);
|
|
1723
|
+
if (!backend) return false;
|
|
1724
|
+
this.setHitTestBackend(backend);
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Refresh the hit-test grid for the CURRENT tree state if it is stale (a
|
|
1729
|
+
* structural or transform change may have happened since the last build —
|
|
1730
|
+
* there is no cheap "nothing moved" shortcut for a spatial index the way
|
|
1731
|
+
* there is for the transform store's topology-only run table, since ANY
|
|
1732
|
+
* entity moving invalidates its AABB, not just add/remove/reparent; the
|
|
1733
|
+
* measured build cost is cheap enough to redo per call). Returns `false`
|
|
1734
|
+
* (grid untrustworthy — caller must use the JS walk) when there is no
|
|
1735
|
+
* backend or the build overflowed its item budget.
|
|
1736
|
+
*/
|
|
1737
|
+
_ensureHitGrid() {
|
|
1738
|
+
const backend = this._hitWasm;
|
|
1739
|
+
if (!backend) return false;
|
|
1740
|
+
if (this._hitGridFrame === this.currentFrame) return this._hitGridOk;
|
|
1741
|
+
const gathered = gatherHitAABBs(this.root, this.currentFrame);
|
|
1742
|
+
backend.ensure(gathered.count, this.width, this.height, 64);
|
|
1743
|
+
const view = backend.inputView();
|
|
1744
|
+
view.minx.set(gathered.minx.subarray(0, gathered.count));
|
|
1745
|
+
view.miny.set(gathered.miny.subarray(0, gathered.count));
|
|
1746
|
+
view.maxx.set(gathered.maxx.subarray(0, gathered.count));
|
|
1747
|
+
view.maxy.set(gathered.maxy.subarray(0, gathered.count));
|
|
1748
|
+
const ok = backend.runBuild(gathered.count, this.width, this.height, 64);
|
|
1749
|
+
this._hitSlotEntity = gathered.slotEntity;
|
|
1750
|
+
this._hitBoundless = gathered.boundless;
|
|
1751
|
+
this._hitGridFrame = this.currentFrame;
|
|
1752
|
+
this._hitGridOk = ok;
|
|
1753
|
+
return ok;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* `findEntityAt`'s WASM-accelerated path for the main tree. Scans only the
|
|
1757
|
+
* queried cell's candidates (confirming each against its own AABB and precise
|
|
1758
|
+
* `isPointInside`) merged against the (typically empty or tiny) list of
|
|
1759
|
+
* entities with no `getBounds()`, taking whichever confirmed match has the
|
|
1760
|
+
* higher pre-order index — see hit-store.ts for why that is exactly
|
|
1761
|
+
* equivalent to findHitRecursively's topmost-hit priority. Always
|
|
1762
|
+
* conclusive: returns the correct entity or `null`, never "inconclusive".
|
|
1763
|
+
*/
|
|
1764
|
+
_findEntityAtWasm(x, y) {
|
|
1765
|
+
const backend = this._hitWasm;
|
|
1766
|
+
const { minx, miny, maxx, maxy } = backend.inputView();
|
|
1767
|
+
let bestIndex = -1;
|
|
1768
|
+
let bestEntity = null;
|
|
1769
|
+
const cell = backend.candidatesAt(x, y);
|
|
1770
|
+
if (cell) {
|
|
1771
|
+
for (let k = cell.length - 1; k >= 0; k--) {
|
|
1772
|
+
const idx = cell[k];
|
|
1773
|
+
if (x < minx[idx] || x > maxx[idx] || y < miny[idx] || y > maxy[idx]) continue;
|
|
1774
|
+
const entity = this._hitSlotEntity[idx];
|
|
1775
|
+
if (entity?.isPointInside(x, y) && this.isHitEligible(entity, x, y)) {
|
|
1776
|
+
bestIndex = idx;
|
|
1777
|
+
bestEntity = entity;
|
|
1778
|
+
break;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
for (const { entity, index } of this._hitBoundless) {
|
|
1783
|
+
if (index > bestIndex && entity.isPointInside(x, y) && this.isHitEligible(entity, x, y)) {
|
|
1784
|
+
bestIndex = index;
|
|
1785
|
+
bestEntity = entity;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return bestEntity;
|
|
1789
|
+
}
|
|
1790
|
+
// ── WASM batched-animation backend (invisible accelerator, G2) ──────────────
|
|
1791
|
+
// Advances every currently-active SpringDriver/TweenDriver in one WASM call
|
|
1792
|
+
// each (spring_step/tween_step) instead of Entity.tickDrivers()'s per-driver
|
|
1793
|
+
// JS loop. The JS tick loop is the permanent fallback: a null backend, a
|
|
1794
|
+
// driver count below `animDriverGateCount`, or a TweenDriver using a custom
|
|
1795
|
+
// EasingFn (which cannot cross into WASM) all fall through to it — WASM can
|
|
1796
|
+
// only ever change *how* a driver is advanced, never *what* value it lands
|
|
1797
|
+
// on.
|
|
1798
|
+
_animWasm = null;
|
|
1799
|
+
// Entities with at least one active driver, added by Entity._spawnDriver.
|
|
1800
|
+
// Self-pruning: _tickBatchedDrivers drops an entry the first time it visits
|
|
1801
|
+
// an entity whose drivers have since all completed or been removed. This is
|
|
1802
|
+
// what lets the batch pass find its candidates in O(active drivers), not
|
|
1803
|
+
// O(tree size) — the exact mistake G3's first integrated benchmark made.
|
|
1804
|
+
_activeDriverEntities = /* @__PURE__ */ new Set();
|
|
1805
|
+
// Reused across frames instead of allocating a fresh array + N {entity,prop,
|
|
1806
|
+
// driver} objects every call — the integrated benchmark
|
|
1807
|
+
// (benchmarks/anim-wasm-scene) found that allocation churn was the
|
|
1808
|
+
// dominant integrated cost, not the wasm kernel itself. Parallel arrays,
|
|
1809
|
+
// truncated to the live count after each use so a stale tail slot never
|
|
1810
|
+
// pins a no-longer-active entity/driver in memory.
|
|
1811
|
+
_springEntities = [];
|
|
1812
|
+
_springProps = [];
|
|
1813
|
+
_springDrivers = [];
|
|
1814
|
+
_tweenEntities = [];
|
|
1815
|
+
_tweenProps = [];
|
|
1816
|
+
_tweenDrivers = [];
|
|
1817
|
+
/**
|
|
1818
|
+
* Minimum number of batchable (spring, or named-easing tween) active drivers
|
|
1819
|
+
* before a frame engages the WASM batch path at all; below it, every driver
|
|
1820
|
+
* ticks on the normal JS per-entity path, unmodified.
|
|
1821
|
+
*
|
|
1822
|
+
* Re-measured on the INTEGRATED path (benchmarks/anim-wasm-scene, real
|
|
1823
|
+
* Chrome 150 / Firefox 153, 2026-07-24 — correctness verified 0 mismatches
|
|
1824
|
+
* across all three kinds before any of these numbers were trusted): the
|
|
1825
|
+
* isolated kernel spike's "<100 drivers, wins everywhere" verdict did NOT
|
|
1826
|
+
* survive integration, and neither did the first integrated pass's single
|
|
1827
|
+
* gate-count verdict once broken out by driver kind. On Chrome, spring and
|
|
1828
|
+
* mixed drivers are a real ~1.4–2.3× win from n=128 up through the tested
|
|
1829
|
+
* ceiling of 16384, but pure-tween drivers are a LOSS at n=128 (0.71×,
|
|
1830
|
+
* i.e. ~40% slower than the JS path) and only turn net-positive around
|
|
1831
|
+
* n≈256 (1.52×). A single scalar gate can't be tight for spring/mixed
|
|
1832
|
+
* without occasionally opening early on a tween-heavy scene and making it
|
|
1833
|
+
* slower — 256 is chosen to keep the gate net-positive across all three
|
|
1834
|
+
* kinds rather than optimal for any one of them; a kind-aware gate (see
|
|
1835
|
+
* `_tickBatchedDrivers`'s per-kind arrays, which already separate spring
|
|
1836
|
+
* from tween) would recover the 128–255 spring/mixed win without the
|
|
1837
|
+
* tween regression, but that's a larger change than this measurement pass
|
|
1838
|
+
* covers. On Firefox it is a net loss at every driver count measured, up
|
|
1839
|
+
* to 16384 — not an allocation artifact (confirmed after removing all
|
|
1840
|
+
* per-frame allocation from the gather/scatter path); SpiderMonkey's
|
|
1841
|
+
* wasm-boundary/property-dispatch cost for this shape of call appears to
|
|
1842
|
+
* structurally exceed the saving, at least at the scales tested here.
|
|
1843
|
+
*
|
|
1844
|
+
* 256 is set as a Chrome-oriented default so an app that opts in (this
|
|
1845
|
+
* path is never engaged without an explicit {@link enableWasmAnimBatching}
|
|
1846
|
+
* call) sees the gate open only where it reliably helps on Chromium,
|
|
1847
|
+
* regardless of whether the scene's active drivers are spring, tween, or
|
|
1848
|
+
* a mix of both. Unlike G1 (safe to default on everywhere) and G3 (opt-in,
|
|
1849
|
+
* but a reliable win once its own gate condition holds), G2 has no
|
|
1850
|
+
* threshold that is safe on every engine — raise or lower this per your
|
|
1851
|
+
* own target browser mix and driver-kind distribution, or leave WASM
|
|
1852
|
+
* animation batching disabled entirely on a Firefox-heavy audience.
|
|
1853
|
+
*/
|
|
1854
|
+
animDriverGateCount = 256;
|
|
1855
|
+
/** Which backend advances active property drivers on the current gate
|
|
1856
|
+
* decision. Reflects only whether a backend is installed — the per-frame
|
|
1857
|
+
* gate can still choose the JS path even when this reads `'wasm'`. */
|
|
1858
|
+
get animBackend() {
|
|
1859
|
+
return this._animWasm ? "wasm" : "js";
|
|
1860
|
+
}
|
|
1861
|
+
/** Install (or clear) a WASM batched-animation backend directly. Prefer
|
|
1862
|
+
* {@link enableWasmAnimBatching} for the normal async hot-swap. */
|
|
1863
|
+
setAnimBackend(backend) {
|
|
1864
|
+
this._animWasm = backend;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Asynchronously instantiate the WASM batched-animation core and, on
|
|
1868
|
+
* success, make it available to the per-frame gate (see
|
|
1869
|
+
* {@link animDriverGateCount}). Accepts the same source shapes as
|
|
1870
|
+
* {@link enableWasmTransforms}. Stays on the JS tick loop if instantiation
|
|
1871
|
+
* fails — failure is the default state, not an error path. Resolves `true`
|
|
1872
|
+
* if WASM is now available (not necessarily active every frame).
|
|
1873
|
+
*/
|
|
1874
|
+
async enableWasmAnimBatching(source) {
|
|
1875
|
+
const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
|
|
1876
|
+
const backend = isBytes ? await instantiateAsync3(source) : await instantiateStreaming3(source);
|
|
1877
|
+
if (!backend) return false;
|
|
1878
|
+
this.setAnimBackend(backend);
|
|
1879
|
+
return true;
|
|
1880
|
+
}
|
|
1881
|
+
// ── WASM particle CPU-sim backend (invisible accelerator, G4) ───────────────
|
|
1882
|
+
// Advances a ComputeParticleEntity's whole buffer in one `particle_step` call
|
|
1883
|
+
// (spring/mouse/explosion/integrate/bounce/life), replacing the per-particle
|
|
1884
|
+
// JS `updateCPU` loop on the GPU-less fallback path. Measured ~2.1-2.5x on
|
|
1885
|
+
// Chrome and ~1.4-2.0x on Firefox including the per-frame AoS<->SoA transpose
|
|
1886
|
+
// (benchmarks/particle-wasm). f32 (matches the WGSL shader), bit-identical to
|
|
1887
|
+
// a JS f32 reference oracle; updateCPU (f64) stays the permanent fallback when
|
|
1888
|
+
// no backend is installed or a scene runs on WebGPU.
|
|
1889
|
+
_particleWasm = null;
|
|
1890
|
+
/** Which backend runs the CPU particle simulation. Reflects only whether a
|
|
1891
|
+
* backend is installed (the WebGPU compute path, when active, is used first
|
|
1892
|
+
* regardless). */
|
|
1893
|
+
get particleSimBackend() {
|
|
1894
|
+
return this._particleWasm ? "wasm" : "js";
|
|
1895
|
+
}
|
|
1896
|
+
/** Install (or clear) a WASM particle backend directly. Prefer
|
|
1897
|
+
* {@link enableWasmParticles} for the normal async hot-swap. */
|
|
1898
|
+
setParticleBackend(backend) {
|
|
1899
|
+
this._particleWasm = backend;
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Asynchronously instantiate the WASM particle core and, on success, use it
|
|
1903
|
+
* for the CPU particle fallback. Accepts the same source shapes as
|
|
1904
|
+
* {@link enableWasmTransforms}. Stays on the JS `updateCPU` path if
|
|
1905
|
+
* instantiation fails — failure is the default state, not an error path.
|
|
1906
|
+
* Resolves `true` if WASM is now active.
|
|
1907
|
+
*/
|
|
1908
|
+
async enableWasmParticles(source) {
|
|
1909
|
+
const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
|
|
1910
|
+
const backend = isBytes ? await instantiateAsync4(source) : await instantiateStreaming4(source);
|
|
1911
|
+
if (!backend) return false;
|
|
1912
|
+
this.setParticleBackend(backend);
|
|
1913
|
+
return true;
|
|
1914
|
+
}
|
|
1915
|
+
/** Internal: called by `Entity._spawnDriver` when a new property driver
|
|
1916
|
+
* starts. See {@link _activeDriverEntities}. */
|
|
1917
|
+
_registerActiveDriverEntity(entity) {
|
|
1918
|
+
this._activeDriverEntities.add(entity);
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Drop `entity` and its whole subtree from the batched-driver candidate set.
|
|
1922
|
+
* Called by {@link remove}/{@link hideOverlay} on detach: without this a
|
|
1923
|
+
* removed-but-still-animating entity stays pinned in the Set (a leak) and its
|
|
1924
|
+
* drivers keep ticking every frame even though it is off-tree. If it is later
|
|
1925
|
+
* re-added, {@link registerActiveDriverSubtree} re-registers any node that
|
|
1926
|
+
* still has live drivers, so the motion resumes.
|
|
1927
|
+
*/
|
|
1928
|
+
unregisterActiveDriverSubtree(entity) {
|
|
1929
|
+
if (this._activeDriverEntities.size === 0) return;
|
|
1930
|
+
const stack = [entity];
|
|
1931
|
+
while (stack.length > 0) {
|
|
1932
|
+
const node = stack.pop();
|
|
1933
|
+
this._activeDriverEntities.delete(node);
|
|
1934
|
+
for (const child of node.children) stack.push(child);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Re-register every node in `entity`'s subtree that still has live property
|
|
1939
|
+
* drivers. Called by {@link add}/{@link showOverlay} so re-attaching a subtree
|
|
1940
|
+
* that was removed mid-animation resumes its batched drivers (they were
|
|
1941
|
+
* dropped from the candidate set on removal, but the driver state still lives
|
|
1942
|
+
* on each entity).
|
|
1943
|
+
*/
|
|
1944
|
+
registerActiveDriverSubtree(entity) {
|
|
1945
|
+
const stack = [entity];
|
|
1946
|
+
while (stack.length > 0) {
|
|
1947
|
+
const node = stack.pop();
|
|
1948
|
+
const entries = node._driverEntries();
|
|
1949
|
+
if (entries && entries.size > 0) this._activeDriverEntities.add(node);
|
|
1950
|
+
for (const child of node.children) stack.push(child);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Advance every registered entity's active drivers for this frame, batching
|
|
1955
|
+
* whichever are batchable (`SpringDriver`; `TweenDriver` with a named
|
|
1956
|
+
* easing) through one WASM call each when the driver-count gate is open, and
|
|
1957
|
+
* ticking the rest (a `TweenDriver` using a custom `EasingFn`) directly in
|
|
1958
|
+
* JS regardless of the gate. A "claimed" entity must have ALL its drivers
|
|
1959
|
+
* advanced here so it can be safely stamped `_driversTickedFrame` — leaving
|
|
1960
|
+
* one unclaimed would silently stall it, since `tickDrivers()` skips the
|
|
1961
|
+
* whole entity once stamped.
|
|
1962
|
+
*
|
|
1963
|
+
* Must run before ANY entity's `update()`/`tickDrivers()` this frame (see
|
|
1964
|
+
* the call site in {@link render}) — the same ordering constraint G1 Stage 4
|
|
1965
|
+
* discovered: a value this pass writes must be final before anything reads
|
|
1966
|
+
* it, including the JS-mode interleaved walk and the WASM-mode transform
|
|
1967
|
+
* pre-pass.
|
|
1968
|
+
*/
|
|
1969
|
+
_tickBatchedDrivers(dt) {
|
|
1970
|
+
if (this._activeDriverEntities.size === 0) return;
|
|
1971
|
+
let batchable = 0;
|
|
1972
|
+
for (const entity of this._activeDriverEntities) {
|
|
1973
|
+
const entries = entity._driverEntries();
|
|
1974
|
+
if (!entries || entries.size === 0) {
|
|
1975
|
+
this._activeDriverEntities.delete(entity);
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
for (const driver of entries.values()) {
|
|
1979
|
+
if (driver instanceof SpringDriver) batchable++;
|
|
1980
|
+
else if (driver instanceof TweenDriver && driver.wasmEasingId !== null) batchable++;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const backend = this._animWasm;
|
|
1984
|
+
if (!backend || batchable < this.animDriverGateCount) return;
|
|
1985
|
+
const sE = this._springEntities;
|
|
1986
|
+
const sP = this._springProps;
|
|
1987
|
+
const sD = this._springDrivers;
|
|
1988
|
+
const tE = this._tweenEntities;
|
|
1989
|
+
const tP = this._tweenProps;
|
|
1990
|
+
const tD = this._tweenDrivers;
|
|
1991
|
+
let springCount = 0;
|
|
1992
|
+
let tweenCount = 0;
|
|
1993
|
+
for (const entity of this._activeDriverEntities) {
|
|
1994
|
+
const entries = entity._driverEntries();
|
|
1995
|
+
for (const [prop, driver] of entries) {
|
|
1996
|
+
if (driver instanceof SpringDriver) {
|
|
1997
|
+
sE[springCount] = entity;
|
|
1998
|
+
sP[springCount] = prop;
|
|
1999
|
+
sD[springCount] = driver;
|
|
2000
|
+
springCount++;
|
|
2001
|
+
} else if (driver instanceof TweenDriver && driver.wasmEasingId !== null) {
|
|
2002
|
+
tE[tweenCount] = entity;
|
|
2003
|
+
tP[tweenCount] = prop;
|
|
2004
|
+
tD[tweenCount] = driver;
|
|
2005
|
+
tweenCount++;
|
|
2006
|
+
} else {
|
|
2007
|
+
driver.tick(dt);
|
|
2008
|
+
entity._applyDriverTick(prop, driver);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
entity._driversTickedFrame = this.currentFrame;
|
|
2012
|
+
}
|
|
2013
|
+
sE.length = springCount;
|
|
2014
|
+
sP.length = springCount;
|
|
2015
|
+
sD.length = springCount;
|
|
2016
|
+
tE.length = tweenCount;
|
|
2017
|
+
tP.length = tweenCount;
|
|
2018
|
+
tD.length = tweenCount;
|
|
2019
|
+
backend.ensure(springCount, tweenCount);
|
|
2020
|
+
if (springCount > 0) {
|
|
2021
|
+
const sv = backend.springView();
|
|
2022
|
+
for (let i = 0; i < springCount; i++) {
|
|
2023
|
+
const phys = sD[i].physics;
|
|
2024
|
+
sv.val[i] = phys.value;
|
|
2025
|
+
sv.target[i] = phys.target;
|
|
2026
|
+
sv.vel[i] = phys.velocity;
|
|
2027
|
+
sv.stiff[i] = phys.stiffness;
|
|
2028
|
+
sv.damp[i] = phys.damping;
|
|
2029
|
+
sv.mass[i] = phys.mass;
|
|
2030
|
+
}
|
|
2031
|
+
backend.stepSprings(dt, springCount);
|
|
2032
|
+
for (let i = 0; i < springCount; i++) sD[i].syncExternal(sv.val[i], sv.vel[i]);
|
|
2033
|
+
}
|
|
2034
|
+
if (tweenCount > 0) {
|
|
2035
|
+
const tv = backend.tweenView();
|
|
2036
|
+
for (let i = 0; i < tweenCount; i++) {
|
|
2037
|
+
const d = tD[i];
|
|
2038
|
+
tv.from[i] = d.fromValue;
|
|
2039
|
+
tv.to[i] = d.target;
|
|
2040
|
+
tv.elapsed[i] = d.elapsedMs;
|
|
2041
|
+
tv.dur[i] = d.durationMs;
|
|
2042
|
+
tv.delay[i] = d.delayMs;
|
|
2043
|
+
tv.ease[i] = d.wasmEasingId;
|
|
2044
|
+
}
|
|
2045
|
+
backend.stepTweens(dt, tweenCount);
|
|
2046
|
+
for (let i = 0; i < tweenCount; i++) tD[i].syncExternal(tv.val[i], tv.elapsed[i]);
|
|
2047
|
+
}
|
|
2048
|
+
for (let i = 0; i < springCount; i++) sE[i]._applyDriverTick(sP[i], sD[i]);
|
|
2049
|
+
for (let i = 0; i < tweenCount; i++) tE[i]._applyDriverTick(tP[i], tD[i]);
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Compose the whole main tree's world matrices through the resident WASM store
|
|
2053
|
+
* and return the world-matrix views for the render walk to read. Rebuilds the
|
|
2054
|
+
* store layout (slots + runs) only when the tree structure changed since the
|
|
2055
|
+
* last rebuild; otherwise it just gathers current transforms into the resident
|
|
2056
|
+
* input view and runs the kernel. Returns `null` if there is no backend.
|
|
2057
|
+
*/
|
|
2058
|
+
_syncWasmStore() {
|
|
2059
|
+
const backend = this._wasm;
|
|
2060
|
+
if (!backend) return null;
|
|
2061
|
+
if (this._treeStore === null || this._storeStructureVersion !== this._structureVersion) {
|
|
2062
|
+
const built = buildTreeStore(this.root);
|
|
2063
|
+
const slotEntity2 = Array.from({ length: built.store.count });
|
|
2064
|
+
for (const [entity, slot] of built.indexOf) {
|
|
2065
|
+
slotEntity2[slot] = entity;
|
|
2066
|
+
entity._storeSlot = slot;
|
|
2067
|
+
}
|
|
2068
|
+
backend.uploadRuns(built.store);
|
|
2069
|
+
this._treeStore = built.store;
|
|
2070
|
+
this._slotEntity = slotEntity2;
|
|
2071
|
+
this._wasmInputs = backend.inputView();
|
|
2072
|
+
this._wasmWorld = backend.worldView();
|
|
2073
|
+
this._storeStructureVersion = this._structureVersion;
|
|
2074
|
+
}
|
|
2075
|
+
const inp = this._wasmInputs;
|
|
2076
|
+
const slotEntity = this._slotEntity;
|
|
2077
|
+
for (let slot = 1; slot < slotEntity.length; slot++) {
|
|
2078
|
+
const e = slotEntity[slot];
|
|
2079
|
+
inp.x[slot] = e.x;
|
|
2080
|
+
inp.y[slot] = e.y;
|
|
2081
|
+
inp.sx[slot] = e.scaleX;
|
|
2082
|
+
inp.sy[slot] = e.scaleY;
|
|
2083
|
+
const trig = e._getTrig();
|
|
2084
|
+
inp.cos[slot] = trig.cos;
|
|
2085
|
+
inp.sin[slot] = trig.sin;
|
|
2086
|
+
inp.opacity[slot] = e.opacity;
|
|
2087
|
+
}
|
|
2088
|
+
backend.runKernel("simd");
|
|
2089
|
+
return this._wasmWorld;
|
|
2090
|
+
}
|
|
752
2091
|
/**
|
|
753
2092
|
* Authoritative paint order for semantic nodes discovered during the main
|
|
754
2093
|
* render. A node may not have a DOM projection until the following a11y
|
|
@@ -759,6 +2098,8 @@ var Scene = class _Scene {
|
|
|
759
2098
|
// Optional WebGL point-cloud layer (see SceneOptions.pointBackend).
|
|
760
2099
|
pointRenderer = null;
|
|
761
2100
|
glCanvas = null;
|
|
2101
|
+
glContextLostHandler = null;
|
|
2102
|
+
glContextRestoredHandler = null;
|
|
762
2103
|
debugA11y;
|
|
763
2104
|
width;
|
|
764
2105
|
height;
|
|
@@ -788,6 +2129,9 @@ var Scene = class _Scene {
|
|
|
788
2129
|
mouseY = -9999;
|
|
789
2130
|
pointerMoveListener = null;
|
|
790
2131
|
pointerLeaveListener = null;
|
|
2132
|
+
/** Element the pointer listeners are bound to (parent container if present,
|
|
2133
|
+
* else the canvas). Stored so `destroy()` detaches from the same element. */
|
|
2134
|
+
pointerEventTarget = null;
|
|
791
2135
|
hasWarnedZeroSize = false;
|
|
792
2136
|
fontLoadHandler = null;
|
|
793
2137
|
// ── Dev-mode warning infrastructure ──────────────────────────────
|
|
@@ -874,8 +2218,14 @@ var Scene = class _Scene {
|
|
|
874
2218
|
this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
|
|
875
2219
|
this.contentProjectionEnabled = options.contentProjection ?? true;
|
|
876
2220
|
this.contentProjectionMargin = options.contentProjectionMargin;
|
|
2221
|
+
this.readingDirection = options.readingDirection ?? "ltr";
|
|
877
2222
|
this._devActive = _Scene._devModeDetected();
|
|
878
2223
|
this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
2224
|
+
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
2225
|
+
this.forcedColorsQuery = window.matchMedia("(forced-colors: active)");
|
|
2226
|
+
this.forcedColorsChangeHandler = () => this.markDirty();
|
|
2227
|
+
this.forcedColorsQuery.addEventListener?.("change", this.forcedColorsChangeHandler);
|
|
2228
|
+
}
|
|
879
2229
|
this.root = new class RootEntity extends Entity {
|
|
880
2230
|
isPointInside() {
|
|
881
2231
|
return false;
|
|
@@ -902,6 +2252,10 @@ var Scene = class _Scene {
|
|
|
902
2252
|
this.maxDPR
|
|
903
2253
|
);
|
|
904
2254
|
}
|
|
2255
|
+
this.renderer.onContextRestored?.(() => {
|
|
2256
|
+
this.markDirty();
|
|
2257
|
+
if (this.renderer.isContextLost?.() !== true) this.render(this.renderer);
|
|
2258
|
+
});
|
|
905
2259
|
if (typeof document !== "undefined") {
|
|
906
2260
|
this.a11yRoot = document.createElement("div");
|
|
907
2261
|
this.a11yRoot.style.position = "absolute";
|
|
@@ -913,6 +2267,15 @@ var Scene = class _Scene {
|
|
|
913
2267
|
this.a11yRoot.style.overflow = "hidden";
|
|
914
2268
|
this.a11yRoot.style.zIndex = "10";
|
|
915
2269
|
this.a11yRoot.style.userSelect = "text";
|
|
2270
|
+
this.focusSentinel = document.createElement("div");
|
|
2271
|
+
this.focusSentinel.setAttribute("data-vecto-focus-sentinel", "");
|
|
2272
|
+
this.focusSentinel.tabIndex = -1;
|
|
2273
|
+
this.focusSentinel.style.position = "absolute";
|
|
2274
|
+
this.focusSentinel.style.width = "0";
|
|
2275
|
+
this.focusSentinel.style.height = "0";
|
|
2276
|
+
this.focusSentinel.style.outline = "none";
|
|
2277
|
+
this.focusSentinel.style.overflow = "hidden";
|
|
2278
|
+
this.a11yRoot.appendChild(this.focusSentinel);
|
|
916
2279
|
this.a11yRoot.addEventListener("mousedown", (e) => {
|
|
917
2280
|
if (e.button !== 0) return;
|
|
918
2281
|
const target = e.target;
|
|
@@ -1029,6 +2392,8 @@ var Scene = class _Scene {
|
|
|
1029
2392
|
pr.resize(this.width, this.height);
|
|
1030
2393
|
this.glCanvas = gl;
|
|
1031
2394
|
this.pointRenderer = pr;
|
|
2395
|
+
this._overlayGeometry = null;
|
|
2396
|
+
this.setupGLContextRecovery(gl);
|
|
1032
2397
|
} else {
|
|
1033
2398
|
gl.remove();
|
|
1034
2399
|
}
|
|
@@ -1047,6 +2412,61 @@ var Scene = class _Scene {
|
|
|
1047
2412
|
}
|
|
1048
2413
|
this.setupEvents();
|
|
1049
2414
|
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Arm a `(resolution: Ndppx)` media query for the current devicePixelRatio and
|
|
2417
|
+
* re-apply the canvas scale when it changes. Such a query only fires when the
|
|
2418
|
+
* DPR leaves its exact value, so on each change the old query is detached and
|
|
2419
|
+
* a fresh one is armed for the new DPR. Re-runs `resize(width, height)` (which
|
|
2420
|
+
* re-scales the backing store via the renderer) so text/vectors stay crisp
|
|
2421
|
+
* after a monitor move or zoom. No-op without `matchMedia`.
|
|
2422
|
+
*/
|
|
2423
|
+
watchDevicePixelRatio() {
|
|
2424
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
2425
|
+
if (this.dprMediaQuery && this.dprChangeHandler) {
|
|
2426
|
+
this.dprMediaQuery.removeEventListener?.("change", this.dprChangeHandler);
|
|
2427
|
+
}
|
|
2428
|
+
const dpr = window.devicePixelRatio || 1;
|
|
2429
|
+
const query = window.matchMedia(`(resolution: ${dpr}dppx)`);
|
|
2430
|
+
const handler = () => {
|
|
2431
|
+
this.resize(this.width, this.height);
|
|
2432
|
+
this.watchDevicePixelRatio();
|
|
2433
|
+
};
|
|
2434
|
+
query.addEventListener?.("change", handler);
|
|
2435
|
+
this.dprMediaQuery = query;
|
|
2436
|
+
this.dprChangeHandler = handler;
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Recover the WebGL point layer from a GPU context loss (driver TDR reset,
|
|
2440
|
+
* tab backgrounded on mobile, GPU switch). Two things are required:
|
|
2441
|
+
*
|
|
2442
|
+
* 1. The `webglcontextlost` handler MUST call `preventDefault()`, or the
|
|
2443
|
+
* browser never fires `webglcontextrestored` and the layer is blank
|
|
2444
|
+
* forever. While lost, the old renderer's GL calls are silently ignored,
|
|
2445
|
+
* so we drop it and the render loop simply skips the point layer.
|
|
2446
|
+
* 2. On `webglcontextrestored`, all GL objects (programs, buffers, textures)
|
|
2447
|
+
* are gone, so we rebuild the renderer from scratch via `Scene.webglCreator`
|
|
2448
|
+
* on the same canvas, restore DPR/size, and repaint.
|
|
2449
|
+
*/
|
|
2450
|
+
setupGLContextRecovery(gl) {
|
|
2451
|
+
if (typeof gl.addEventListener !== "function") return;
|
|
2452
|
+
this.glContextLostHandler = (e) => {
|
|
2453
|
+
e.preventDefault();
|
|
2454
|
+
this.pointRenderer?.destroy();
|
|
2455
|
+
this.pointRenderer = null;
|
|
2456
|
+
};
|
|
2457
|
+
this.glContextRestoredHandler = () => {
|
|
2458
|
+
if (this.destroyed || !this.glCanvas) return;
|
|
2459
|
+
const pr = _Scene.webglCreator ? _Scene.webglCreator(this.glCanvas) : null;
|
|
2460
|
+
if (pr) {
|
|
2461
|
+
pr.maxDPR = this.maxDPR;
|
|
2462
|
+
pr.resize(this.width, this.height);
|
|
2463
|
+
this.pointRenderer = pr;
|
|
2464
|
+
this.markDirty();
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
gl.addEventListener("webglcontextlost", this.glContextLostHandler);
|
|
2468
|
+
gl.addEventListener("webglcontextrestored", this.glContextRestoredHandler);
|
|
2469
|
+
}
|
|
1050
2470
|
endContentSelectionDrag() {
|
|
1051
2471
|
this.blankRegionSelectionDrag = false;
|
|
1052
2472
|
this.contentSelectionAnchor = null;
|
|
@@ -1059,6 +2479,53 @@ var Scene = class _Scene {
|
|
|
1059
2479
|
this.endContentSelectionDrag();
|
|
1060
2480
|
selection?.removeAllRanges();
|
|
1061
2481
|
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Rebuild a content-projection element's DOM (`rebuild`) while preserving a
|
|
2484
|
+
* text selection the user made inside it. A streaming message replaces its
|
|
2485
|
+
* projection children on every appended chunk; without this, a selection in
|
|
2486
|
+
* the UNCHANGED prefix is wiped on each frame ("can't select text in a
|
|
2487
|
+
* message still receiving tokens"). We snapshot the selection's anchor/focus
|
|
2488
|
+
* as linear character offsets within `el` before the rebuild and re-resolve
|
|
2489
|
+
* them against the new DOM after, clamped to the new text length.
|
|
2490
|
+
*
|
|
2491
|
+
* Only fires when `el` owns the current selection and there is no active drag
|
|
2492
|
+
* (mid-drag the browser is authoritative). The virtualization case — where
|
|
2493
|
+
* `el` itself is removed from the DOM — is out of scope here (the node is
|
|
2494
|
+
* genuinely freed; the browser clears the selection and there is nothing to
|
|
2495
|
+
* restore against).
|
|
2496
|
+
*/
|
|
2497
|
+
preserveContentSelectionAcrossRebuild(el, rebuild) {
|
|
2498
|
+
const selection = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
2499
|
+
const owns = !!selection && !this.blankRegionSelectionDrag && ((selection.anchorNode ? el.contains(selection.anchorNode) : false) || (selection.focusNode ? el.contains(selection.focusNode) : false));
|
|
2500
|
+
if (!owns || !selection.anchorNode || !selection.focusNode) {
|
|
2501
|
+
this.releaseContentSelectionForRebuild(el);
|
|
2502
|
+
rebuild();
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
const anchorNode = selection.anchorNode;
|
|
2506
|
+
const focusNode = selection.focusNode;
|
|
2507
|
+
const anchorOffset = anchorNode instanceof Text ? projectionAbsoluteOffset(el, {
|
|
2508
|
+
node: anchorNode,
|
|
2509
|
+
offset: selection.anchorOffset
|
|
2510
|
+
}) : null;
|
|
2511
|
+
const focusOffset = focusNode instanceof Text ? projectionAbsoluteOffset(el, {
|
|
2512
|
+
node: focusNode,
|
|
2513
|
+
offset: selection.focusOffset
|
|
2514
|
+
}) : null;
|
|
2515
|
+
this.endContentSelectionDrag();
|
|
2516
|
+
selection.removeAllRanges();
|
|
2517
|
+
rebuild();
|
|
2518
|
+
if (anchorOffset === null || focusOffset === null) return;
|
|
2519
|
+
const textLen = (el.textContent ?? "").length;
|
|
2520
|
+
if (anchorOffset > textLen || focusOffset > textLen) return;
|
|
2521
|
+
const anchor = projectionCaretAt(el, anchorOffset, "forward");
|
|
2522
|
+
const focus = projectionCaretAt(el, focusOffset, "backward");
|
|
2523
|
+
if (!anchor || !focus) return;
|
|
2524
|
+
try {
|
|
2525
|
+
selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
|
|
2526
|
+
} catch {
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
1062
2529
|
/**
|
|
1063
2530
|
* Expose the underlying {@link IRenderer} for advanced direct-draw operations.
|
|
1064
2531
|
*
|
|
@@ -1087,6 +2554,7 @@ var Scene = class _Scene {
|
|
|
1087
2554
|
*/
|
|
1088
2555
|
add(entity) {
|
|
1089
2556
|
this.root.add(entity);
|
|
2557
|
+
this.registerActiveDriverSubtree(entity);
|
|
1090
2558
|
return this;
|
|
1091
2559
|
}
|
|
1092
2560
|
clearContentGridState(entityId, el) {
|
|
@@ -1109,6 +2577,7 @@ var Scene = class _Scene {
|
|
|
1109
2577
|
}
|
|
1110
2578
|
removeA11yRecursively(node) {
|
|
1111
2579
|
if (node.isDOMPortal) {
|
|
2580
|
+
node.releaseDOMBindings();
|
|
1112
2581
|
node.domElement.remove();
|
|
1113
2582
|
this.portalEntities.delete(node.id);
|
|
1114
2583
|
this.activePortalsThisFrame.delete(node.id);
|
|
@@ -1130,6 +2599,11 @@ var Scene = class _Scene {
|
|
|
1130
2599
|
this.caretBlinkTimer = null;
|
|
1131
2600
|
}
|
|
1132
2601
|
}
|
|
2602
|
+
if (this.hoveredA11yElements.has(el)) {
|
|
2603
|
+
this.hoveredA11yElements.delete(el);
|
|
2604
|
+
node.dispatchEvent(new VectoJSEvent("pointerleave", node, void 0, false));
|
|
2605
|
+
}
|
|
2606
|
+
this.preserveFocusOnRemoval(el);
|
|
1133
2607
|
el.remove();
|
|
1134
2608
|
this.a11yElements.delete(node.id);
|
|
1135
2609
|
this.a11yNeedsReorder = true;
|
|
@@ -1138,6 +2612,18 @@ var Scene = class _Scene {
|
|
|
1138
2612
|
this.removeA11yRecursively(child);
|
|
1139
2613
|
}
|
|
1140
2614
|
}
|
|
2615
|
+
/**
|
|
2616
|
+
* If `el` is about to be removed from the DOM while it holds browser focus,
|
|
2617
|
+
* move focus to the a11y focus sentinel first. Removing the active element
|
|
2618
|
+
* otherwise drops focus to `<body>`, which pulls a screen reader out of the
|
|
2619
|
+
* scene's a11y region and back to the top of the page — the classic
|
|
2620
|
+
* "lost my place on scroll/stream" bug for virtualized/recycled controls.
|
|
2621
|
+
*/
|
|
2622
|
+
preserveFocusOnRemoval(el) {
|
|
2623
|
+
if (!this.focusSentinel || typeof document === "undefined") return;
|
|
2624
|
+
if (document.activeElement !== el) return;
|
|
2625
|
+
this.focusSentinel.focus({ preventScroll: true });
|
|
2626
|
+
}
|
|
1141
2627
|
/**
|
|
1142
2628
|
* Remove a top-level entity from the scene graph and clean up its
|
|
1143
2629
|
* accessibility shadow elements recursively.
|
|
@@ -1148,6 +2634,7 @@ var Scene = class _Scene {
|
|
|
1148
2634
|
remove(entity) {
|
|
1149
2635
|
this.root.remove(entity);
|
|
1150
2636
|
this.removeA11yRecursively(entity);
|
|
2637
|
+
this.unregisterActiveDriverSubtree(entity);
|
|
1151
2638
|
return this;
|
|
1152
2639
|
}
|
|
1153
2640
|
/**
|
|
@@ -1167,6 +2654,7 @@ var Scene = class _Scene {
|
|
|
1167
2654
|
*/
|
|
1168
2655
|
showOverlay(overlay) {
|
|
1169
2656
|
this.overlayRoot.add(overlay);
|
|
2657
|
+
this.registerActiveDriverSubtree(overlay);
|
|
1170
2658
|
this.markDirty();
|
|
1171
2659
|
}
|
|
1172
2660
|
/**
|
|
@@ -1175,10 +2663,10 @@ var Scene = class _Scene {
|
|
|
1175
2663
|
hideOverlay(overlay) {
|
|
1176
2664
|
this.overlayRoot.remove(overlay);
|
|
1177
2665
|
this.removeA11yRecursively(overlay);
|
|
2666
|
+
this.unregisterActiveDriverSubtree(overlay);
|
|
1178
2667
|
this.markDirty();
|
|
1179
2668
|
}
|
|
1180
2669
|
destroyEntitySubtree(entity) {
|
|
1181
|
-
while (entity.children.length > 0) this.destroyEntitySubtree(entity.children.at(-1));
|
|
1182
2670
|
entity.destroy();
|
|
1183
2671
|
}
|
|
1184
2672
|
/**
|
|
@@ -1198,20 +2686,36 @@ var Scene = class _Scene {
|
|
|
1198
2686
|
if (typeof window !== "undefined" && !this.disableWindowResize) {
|
|
1199
2687
|
window.removeEventListener("resize", this.resizeHandler);
|
|
1200
2688
|
}
|
|
2689
|
+
if (this.canvasResizeObserver) {
|
|
2690
|
+
this.canvasResizeObserver.disconnect();
|
|
2691
|
+
this.canvasResizeObserver = null;
|
|
2692
|
+
}
|
|
2693
|
+
if (this.dprMediaQuery && this.dprChangeHandler) {
|
|
2694
|
+
this.dprMediaQuery.removeEventListener?.("change", this.dprChangeHandler);
|
|
2695
|
+
this.dprMediaQuery = null;
|
|
2696
|
+
this.dprChangeHandler = null;
|
|
2697
|
+
}
|
|
2698
|
+
if (this.forcedColorsQuery && this.forcedColorsChangeHandler) {
|
|
2699
|
+
this.forcedColorsQuery.removeEventListener?.("change", this.forcedColorsChangeHandler);
|
|
2700
|
+
this.forcedColorsQuery = null;
|
|
2701
|
+
this.forcedColorsChangeHandler = null;
|
|
2702
|
+
}
|
|
1201
2703
|
if (typeof window !== "undefined" && this.contentSelectionEndListener) {
|
|
1202
2704
|
window.removeEventListener("mouseup", this.contentSelectionEndListener);
|
|
1203
2705
|
window.removeEventListener("blur", this.contentSelectionEndListener);
|
|
1204
2706
|
this.contentSelectionEndListener = null;
|
|
1205
2707
|
}
|
|
1206
|
-
if (typeof window !== "undefined" && this.
|
|
2708
|
+
if (typeof window !== "undefined" && this.pointerEventTarget && typeof this.pointerEventTarget.removeEventListener === "function") {
|
|
1207
2709
|
if (this.pointerMoveListener) {
|
|
1208
|
-
this.
|
|
2710
|
+
this.pointerEventTarget.removeEventListener("pointermove", this.pointerMoveListener);
|
|
1209
2711
|
}
|
|
1210
2712
|
if (this.pointerLeaveListener) {
|
|
1211
|
-
this.
|
|
2713
|
+
this.pointerEventTarget.removeEventListener("pointerleave", this.pointerLeaveListener);
|
|
1212
2714
|
}
|
|
2715
|
+
this.pointerEventTarget = null;
|
|
1213
2716
|
}
|
|
1214
2717
|
this.a11yRoot?.remove();
|
|
2718
|
+
this.focusSentinel = null;
|
|
1215
2719
|
this.portalRoot?.remove();
|
|
1216
2720
|
this.a11yElements.clear();
|
|
1217
2721
|
for (const el of this.contentElements.values()) el.remove();
|
|
@@ -1225,6 +2729,16 @@ var Scene = class _Scene {
|
|
|
1225
2729
|
for (const probe of this.contentGridCalibrationProbes.values()) probe.remove();
|
|
1226
2730
|
this.contentGridCalibrationProbes.clear();
|
|
1227
2731
|
this.endContentSelectionDrag();
|
|
2732
|
+
if (this.glCanvas) {
|
|
2733
|
+
if (this.glContextLostHandler) {
|
|
2734
|
+
this.glCanvas.removeEventListener("webglcontextlost", this.glContextLostHandler);
|
|
2735
|
+
}
|
|
2736
|
+
if (this.glContextRestoredHandler) {
|
|
2737
|
+
this.glCanvas.removeEventListener("webglcontextrestored", this.glContextRestoredHandler);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
this.glContextLostHandler = null;
|
|
2741
|
+
this.glContextRestoredHandler = null;
|
|
1228
2742
|
this.pointRenderer?.destroy();
|
|
1229
2743
|
this.renderer.dispose?.();
|
|
1230
2744
|
this.glCanvas?.remove();
|
|
@@ -1247,7 +2761,20 @@ var Scene = class _Scene {
|
|
|
1247
2761
|
setupEvents() {
|
|
1248
2762
|
if (typeof window !== "undefined" && !this.disableWindowResize) {
|
|
1249
2763
|
window.addEventListener("resize", this.resizeHandler);
|
|
2764
|
+
} else if (this.disableWindowResize && typeof ResizeObserver !== "undefined" && this.canvas && typeof this.canvas.getBoundingClientRect === "function") {
|
|
2765
|
+
this.canvasResizeObserver = new ResizeObserver((entries) => {
|
|
2766
|
+
const entry = entries[0];
|
|
2767
|
+
if (!entry) return;
|
|
2768
|
+
const box = entry.contentRect;
|
|
2769
|
+
const w = Math.round(box.width);
|
|
2770
|
+
const h = Math.round(box.height);
|
|
2771
|
+
if (w > 0 && h > 0 && (w !== this.width || h !== this.height)) {
|
|
2772
|
+
this.resize(w, h);
|
|
2773
|
+
}
|
|
2774
|
+
});
|
|
2775
|
+
this.canvasResizeObserver.observe(this.canvas);
|
|
1250
2776
|
}
|
|
2777
|
+
this.watchDevicePixelRatio();
|
|
1251
2778
|
if (typeof window !== "undefined" && this.canvas && typeof this.canvas.addEventListener === "function") {
|
|
1252
2779
|
this.pointerMoveListener = (e) => {
|
|
1253
2780
|
const point = this.clientToScene(e.clientX, e.clientY);
|
|
@@ -1258,8 +2785,9 @@ var Scene = class _Scene {
|
|
|
1258
2785
|
this.mouseX = -9999;
|
|
1259
2786
|
this.mouseY = -9999;
|
|
1260
2787
|
};
|
|
1261
|
-
this.canvas.
|
|
1262
|
-
this.
|
|
2788
|
+
this.pointerEventTarget = this.canvas.parentElement ?? this.canvas;
|
|
2789
|
+
this.pointerEventTarget.addEventListener("pointermove", this.pointerMoveListener);
|
|
2790
|
+
this.pointerEventTarget.addEventListener("pointerleave", this.pointerLeaveListener);
|
|
1263
2791
|
}
|
|
1264
2792
|
}
|
|
1265
2793
|
/**
|
|
@@ -1277,6 +2805,7 @@ var Scene = class _Scene {
|
|
|
1277
2805
|
}
|
|
1278
2806
|
this.isRunning = true;
|
|
1279
2807
|
this.lastTime = typeof performance !== "undefined" ? performance.now() : 0;
|
|
2808
|
+
this.watchCanvasVisibility();
|
|
1280
2809
|
this.scheduleFrame();
|
|
1281
2810
|
const isTextFocused = this.focusedA11yElement instanceof HTMLInputElement || this.focusedA11yElement instanceof HTMLTextAreaElement;
|
|
1282
2811
|
if (isTextFocused && this.renderMode === "onDemand" && !this.caretBlinkTimer) {
|
|
@@ -1291,6 +2820,29 @@ var Scene = class _Scene {
|
|
|
1291
2820
|
requestAnimationFrame((t) => this.loop(t));
|
|
1292
2821
|
}
|
|
1293
2822
|
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Observe whether the canvas is on-screen so the rAF loop can pause when it
|
|
2825
|
+
* scrolls fully out of view (a dashboard tab, a chart below the fold) and
|
|
2826
|
+
* resume when it returns — instead of running the full update/render every
|
|
2827
|
+
* frame for a scene nobody can see. No-op (stays "on screen") where
|
|
2828
|
+
* `IntersectionObserver` is unavailable, so SSR/jsdom behavior is unchanged.
|
|
2829
|
+
*/
|
|
2830
|
+
watchCanvasVisibility() {
|
|
2831
|
+
if (this._canvasObserver || typeof IntersectionObserver === "undefined") return;
|
|
2832
|
+
if (!this.canvas || typeof this.canvas.getBoundingClientRect !== "function") return;
|
|
2833
|
+
this._canvasObserver = new IntersectionObserver((entries) => {
|
|
2834
|
+
const entry = entries[entries.length - 1];
|
|
2835
|
+
if (!entry) return;
|
|
2836
|
+
const nowOnScreen = entry.isIntersecting;
|
|
2837
|
+
const wasOffScreen = !this._canvasOnScreen;
|
|
2838
|
+
this._canvasOnScreen = nowOnScreen;
|
|
2839
|
+
if (nowOnScreen && wasOffScreen && this.isRunning) {
|
|
2840
|
+
this.lastTime = typeof performance !== "undefined" ? performance.now() : 0;
|
|
2841
|
+
this.scheduleFrame();
|
|
2842
|
+
}
|
|
2843
|
+
});
|
|
2844
|
+
this._canvasObserver.observe(this.canvas);
|
|
2845
|
+
}
|
|
1294
2846
|
/**
|
|
1295
2847
|
* Halt the render loop after the current frame completes.
|
|
1296
2848
|
*
|
|
@@ -1302,6 +2854,11 @@ var Scene = class _Scene {
|
|
|
1302
2854
|
clearInterval(this.caretBlinkTimer);
|
|
1303
2855
|
this.caretBlinkTimer = null;
|
|
1304
2856
|
}
|
|
2857
|
+
if (this._canvasObserver) {
|
|
2858
|
+
this._canvasObserver.disconnect();
|
|
2859
|
+
this._canvasObserver = null;
|
|
2860
|
+
}
|
|
2861
|
+
this._canvasOnScreen = true;
|
|
1305
2862
|
}
|
|
1306
2863
|
/**
|
|
1307
2864
|
* Manually advance the scene clock by `dt` milliseconds and render synchronously.
|
|
@@ -1392,6 +2949,7 @@ var Scene = class _Scene {
|
|
|
1392
2949
|
}
|
|
1393
2950
|
}
|
|
1394
2951
|
if (el.parentNode === this.a11yRoot) {
|
|
2952
|
+
this.preserveFocusOnRemoval(el);
|
|
1395
2953
|
this.a11yRoot.removeChild(el);
|
|
1396
2954
|
}
|
|
1397
2955
|
this.a11yElements.delete(node.id);
|
|
@@ -1426,10 +2984,12 @@ var Scene = class _Scene {
|
|
|
1426
2984
|
});
|
|
1427
2985
|
el.addEventListener("mouseenter", (e) => {
|
|
1428
2986
|
if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.2)";
|
|
2987
|
+
this.hoveredA11yElements.add(el);
|
|
1429
2988
|
node.dispatchEvent(new VectoJSEvent("hover", node, e, false));
|
|
1430
2989
|
});
|
|
1431
2990
|
el.addEventListener("mouseleave", (e) => {
|
|
1432
2991
|
if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.05)";
|
|
2992
|
+
this.hoveredA11yElements.delete(el);
|
|
1433
2993
|
node.dispatchEvent(new VectoJSEvent("pointerleave", node, e, false));
|
|
1434
2994
|
});
|
|
1435
2995
|
const capEl = el;
|
|
@@ -1493,12 +3053,18 @@ var Scene = class _Scene {
|
|
|
1493
3053
|
el.addEventListener("click", forward);
|
|
1494
3054
|
el.addEventListener("select", forward);
|
|
1495
3055
|
el.addEventListener("compositionstart", () => {
|
|
1496
|
-
composition = {
|
|
3056
|
+
composition = {
|
|
3057
|
+
start: input.selectionStart ?? input.value.length,
|
|
3058
|
+
length: 0
|
|
3059
|
+
};
|
|
1497
3060
|
forward();
|
|
1498
3061
|
});
|
|
1499
3062
|
el.addEventListener("compositionupdate", (e) => {
|
|
1500
3063
|
const data = e.data ?? "";
|
|
1501
|
-
composition = {
|
|
3064
|
+
composition = {
|
|
3065
|
+
start: composition?.start ?? 0,
|
|
3066
|
+
length: data.length
|
|
3067
|
+
};
|
|
1502
3068
|
forward();
|
|
1503
3069
|
});
|
|
1504
3070
|
el.addEventListener("compositionend", () => {
|
|
@@ -1612,6 +3178,31 @@ var Scene = class _Scene {
|
|
|
1612
3178
|
this.syncOptionalAttribute(el, "aria-activedescendant", attrs.activedescendant);
|
|
1613
3179
|
this.syncOptionalAttribute(el, "aria-valuemin", attrs.valuemin);
|
|
1614
3180
|
this.syncOptionalAttribute(el, "aria-valuemax", attrs.valuemax);
|
|
3181
|
+
this.syncOptionalAttribute(el, "aria-live", attrs.live);
|
|
3182
|
+
this.syncOptionalAttribute(
|
|
3183
|
+
el,
|
|
3184
|
+
"aria-atomic",
|
|
3185
|
+
attrs.atomic === void 0 ? void 0 : String(attrs.atomic)
|
|
3186
|
+
);
|
|
3187
|
+
this.syncOptionalAttribute(el, "aria-relevant", attrs.relevant);
|
|
3188
|
+
this.syncOptionalAttribute(el, "aria-modal", attrs.ariaModal);
|
|
3189
|
+
this.syncOptionalAttribute(el, "aria-labelledby", attrs.labelledby);
|
|
3190
|
+
this.syncOptionalAttribute(el, "aria-describedby", attrs.describedby);
|
|
3191
|
+
this.syncOptionalAttribute(
|
|
3192
|
+
el,
|
|
3193
|
+
"aria-required",
|
|
3194
|
+
attrs.required === void 0 ? void 0 : String(attrs.required)
|
|
3195
|
+
);
|
|
3196
|
+
this.syncOptionalAttribute(
|
|
3197
|
+
el,
|
|
3198
|
+
"aria-invalid",
|
|
3199
|
+
attrs.invalid === void 0 ? void 0 : String(attrs.invalid)
|
|
3200
|
+
);
|
|
3201
|
+
this.syncOptionalAttribute(
|
|
3202
|
+
el,
|
|
3203
|
+
"aria-level",
|
|
3204
|
+
attrs.level === void 0 ? void 0 : String(attrs.level)
|
|
3205
|
+
);
|
|
1615
3206
|
if (attrs.value !== void 0) {
|
|
1616
3207
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
1617
3208
|
if (el.value !== attrs.value) {
|
|
@@ -1643,13 +3234,18 @@ var Scene = class _Scene {
|
|
|
1643
3234
|
el.style.width = `${this.width}px`;
|
|
1644
3235
|
el.style.height = `${this.height}px`;
|
|
1645
3236
|
el.style.transform = "";
|
|
3237
|
+
if (el.style.display === "none") el.style.display = "";
|
|
1646
3238
|
} else {
|
|
1647
|
-
const
|
|
3239
|
+
const worldTf = node.getWorldTransform();
|
|
3240
|
+
const { a, b, c, d, e, f } = worldTf;
|
|
1648
3241
|
el.style.left = `${e + node.a11yOffsetX}px`;
|
|
1649
3242
|
el.style.top = `${f + node.a11yOffsetY}px`;
|
|
1650
3243
|
el.style.width = `${node.width}px`;
|
|
1651
3244
|
el.style.height = `${node.height}px`;
|
|
1652
3245
|
el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
3246
|
+
const visible = this.projectionBoxVisible(node, worldTf, 0);
|
|
3247
|
+
const display = visible ? "" : "none";
|
|
3248
|
+
if (el.style.display !== display) el.style.display = display;
|
|
1653
3249
|
}
|
|
1654
3250
|
}
|
|
1655
3251
|
this.syncContentProjection(node);
|
|
@@ -1717,26 +3313,24 @@ var Scene = class _Scene {
|
|
|
1717
3313
|
}
|
|
1718
3314
|
syncContentProjection(node) {
|
|
1719
3315
|
if (!this.contentProjectionEnabled || !this.a11yRoot) return;
|
|
1720
|
-
const projection = node.getContentProjection();
|
|
1721
3316
|
let el = this.contentElements.get(node.id);
|
|
1722
|
-
|
|
3317
|
+
const releaseProjectionEl = () => {
|
|
1723
3318
|
if (el) {
|
|
1724
3319
|
this.clearContentGridState(node.id, el);
|
|
1725
3320
|
el.remove();
|
|
1726
3321
|
this.contentElements.delete(node.id);
|
|
1727
3322
|
this.a11yNeedsReorder = true;
|
|
1728
3323
|
}
|
|
1729
|
-
|
|
1730
|
-
}
|
|
3324
|
+
};
|
|
1731
3325
|
const worldTf = node.getWorldTransform();
|
|
1732
3326
|
const margin = this.contentProjectionMargin ?? this.height;
|
|
1733
3327
|
if (Number.isFinite(margin) && !this.projectionBoxVisible(node, worldTf, margin)) {
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
3328
|
+
releaseProjectionEl();
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
const projection = node.getContentProjection();
|
|
3332
|
+
if (!projection || !projection.text) {
|
|
3333
|
+
releaseProjectionEl();
|
|
1740
3334
|
return;
|
|
1741
3335
|
}
|
|
1742
3336
|
if (!el) {
|
|
@@ -1776,35 +3370,48 @@ var Scene = class _Scene {
|
|
|
1776
3370
|
fallbackLineHeight: projection.lineHeight ?? 16
|
|
1777
3371
|
});
|
|
1778
3372
|
if (el.dataset.vectoProjectionLines !== signature) {
|
|
1779
|
-
this.
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
3373
|
+
this.preserveContentSelectionAcrossRebuild(el, () => {
|
|
3374
|
+
el.replaceChildren();
|
|
3375
|
+
for (let index = 0; index < lines.length; index++) {
|
|
3376
|
+
const line = lines[index];
|
|
3377
|
+
const lineElement = document.createElement("span");
|
|
3378
|
+
const lineFont = line.font ?? projection.font ?? "";
|
|
3379
|
+
const lineHeight2 = line.lineHeight ?? projection.lineHeight ?? 16;
|
|
3380
|
+
const hasPositionedRuns = !!line.runs && line.runs.some((run) => run.x !== void 0);
|
|
3381
|
+
lineElement.style.position = "absolute";
|
|
3382
|
+
lineElement.dir = hasPositionedRuns ? "ltr" : "auto";
|
|
3383
|
+
lineElement.style.left = `${line.x}px`;
|
|
3384
|
+
lineElement.style.top = `${line.y + line.baseline - cssLineBoxBaseline(lineFont, lineHeight2)}px`;
|
|
3385
|
+
lineElement.style.whiteSpace = "pre";
|
|
3386
|
+
if (lineFont) lineElement.style.font = lineFont;
|
|
3387
|
+
lineElement.style.lineHeight = `${lineHeight2}px`;
|
|
3388
|
+
const separator = line.separatorAfter ?? (index < lines.length - 1 ? "\n" : "");
|
|
3389
|
+
if (line.runs && line.runs.length > 0) {
|
|
3390
|
+
const positioned = line.runs.some((run) => run.x !== void 0);
|
|
3391
|
+
for (let runIndex = 0; runIndex < line.runs.length; runIndex++) {
|
|
3392
|
+
const run = line.runs[runIndex];
|
|
3393
|
+
const runElement = document.createElement("span");
|
|
3394
|
+
runElement.textContent = run.text + (runIndex === line.runs.length - 1 ? separator : "");
|
|
3395
|
+
if (run.font) runElement.style.font = run.font;
|
|
3396
|
+
runElement.style.lineHeight = `${lineHeight2}px`;
|
|
3397
|
+
if (positioned && run.x !== void 0) {
|
|
3398
|
+
runElement.style.position = "absolute";
|
|
3399
|
+
runElement.style.left = `${run.x - line.x}px`;
|
|
3400
|
+
runElement.style.top = "0";
|
|
3401
|
+
if (run.width !== void 0) runElement.style.width = `${run.width}px`;
|
|
3402
|
+
runElement.style.whiteSpace = "pre";
|
|
3403
|
+
runElement.style.verticalAlign = "top";
|
|
3404
|
+
runElement.style.unicodeBidi = "isolate";
|
|
3405
|
+
runElement.dir = "ltr";
|
|
3406
|
+
}
|
|
3407
|
+
lineElement.appendChild(runElement);
|
|
3408
|
+
}
|
|
3409
|
+
} else {
|
|
3410
|
+
lineElement.textContent = line.text + separator;
|
|
1802
3411
|
}
|
|
1803
|
-
|
|
1804
|
-
lineElement.textContent = line.text + separator;
|
|
3412
|
+
el.appendChild(lineElement);
|
|
1805
3413
|
}
|
|
1806
|
-
|
|
1807
|
-
}
|
|
3414
|
+
});
|
|
1808
3415
|
el.dataset.vectoProjectionLines = signature;
|
|
1809
3416
|
}
|
|
1810
3417
|
} else {
|
|
@@ -2027,7 +3634,12 @@ var Scene = class _Scene {
|
|
|
2027
3634
|
const source = document.createTextNode(sourceText);
|
|
2028
3635
|
carrier.appendChild(source);
|
|
2029
3636
|
probe.appendChild(carrier);
|
|
2030
|
-
const measurement = {
|
|
3637
|
+
const measurement = {
|
|
3638
|
+
targets: [target],
|
|
3639
|
+
targetWidth,
|
|
3640
|
+
sourceLength,
|
|
3641
|
+
source
|
|
3642
|
+
};
|
|
2031
3643
|
measurements.push(measurement);
|
|
2032
3644
|
measurementsByKey.set(measurementKey, measurement);
|
|
2033
3645
|
}
|
|
@@ -2126,6 +3738,7 @@ var Scene = class _Scene {
|
|
|
2126
3738
|
}
|
|
2127
3739
|
}
|
|
2128
3740
|
if (el.parentNode === this.a11yRoot) {
|
|
3741
|
+
this.preserveFocusOnRemoval(el);
|
|
2129
3742
|
this.a11yRoot.removeChild(el);
|
|
2130
3743
|
}
|
|
2131
3744
|
this.a11yElements.delete(id);
|
|
@@ -2135,6 +3748,7 @@ var Scene = class _Scene {
|
|
|
2135
3748
|
this.a11yNeedsReorder = true;
|
|
2136
3749
|
}
|
|
2137
3750
|
if (!this.a11yNeedsReorder) return;
|
|
3751
|
+
this.sortNormalElementsVisually();
|
|
2138
3752
|
const fullLen = this.fullViewportElements.length;
|
|
2139
3753
|
const normalLen = this.normalElements.length;
|
|
2140
3754
|
const totalLen = fullLen + normalLen;
|
|
@@ -2147,6 +3761,49 @@ var Scene = class _Scene {
|
|
|
2147
3761
|
}
|
|
2148
3762
|
this.a11yNeedsReorder = false;
|
|
2149
3763
|
}
|
|
3764
|
+
/**
|
|
3765
|
+
* Reorder `normalElements` (in place) into visual reading order using the
|
|
3766
|
+
* world positions `syncA11y` already wrote to each element's inline style
|
|
3767
|
+
* (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an
|
|
3768
|
+
* element belongs to the current row while its top is above the row's
|
|
3769
|
+
* running bottom edge), then sorted within a row by `left` — ascending for
|
|
3770
|
+
* `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the
|
|
3771
|
+
* same position keep their scene-graph (collection) order as a tiebreak.
|
|
3772
|
+
*/
|
|
3773
|
+
sortNormalElementsVisually() {
|
|
3774
|
+
const els = this.normalElements;
|
|
3775
|
+
if (els.length < 2) return;
|
|
3776
|
+
const rtl = this._readingDirection === "rtl";
|
|
3777
|
+
const topOf = (el) => Number.parseFloat(el.style.top) || 0;
|
|
3778
|
+
const leftOf = (el) => Number.parseFloat(el.style.left) || 0;
|
|
3779
|
+
const heightOf = (el) => Math.max(Number.parseFloat(el.style.height) || 0, 4);
|
|
3780
|
+
const order = els.map((el, i) => ({
|
|
3781
|
+
el,
|
|
3782
|
+
i,
|
|
3783
|
+
top: topOf(el),
|
|
3784
|
+
left: leftOf(el)
|
|
3785
|
+
}));
|
|
3786
|
+
order.sort((p, q) => p.top - q.top || p.i - q.i);
|
|
3787
|
+
const sorted = [];
|
|
3788
|
+
let rowStart = 0;
|
|
3789
|
+
let rowBottom = order.length ? order[0].top + heightOf(order[0].el) : 0;
|
|
3790
|
+
const flushRow = (end) => {
|
|
3791
|
+
const row = order.slice(rowStart, end);
|
|
3792
|
+
row.sort((p, q) => (rtl ? q.left - p.left : p.left - q.left) || p.i - q.i);
|
|
3793
|
+
for (const r of row) sorted.push(r.el);
|
|
3794
|
+
};
|
|
3795
|
+
for (let k = 1; k < order.length; k++) {
|
|
3796
|
+
if (order[k].top < rowBottom) {
|
|
3797
|
+
rowBottom = Math.max(rowBottom, order[k].top + heightOf(order[k].el));
|
|
3798
|
+
} else {
|
|
3799
|
+
flushRow(k);
|
|
3800
|
+
rowStart = k;
|
|
3801
|
+
rowBottom = order[k].top + heightOf(order[k].el);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
flushRow(order.length);
|
|
3805
|
+
for (let i = 0; i < sorted.length; i++) els[i] = sorted[i];
|
|
3806
|
+
}
|
|
2150
3807
|
/** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
|
|
2151
3808
|
syncOverlayGeometry() {
|
|
2152
3809
|
const parent = this.canvas.parentElement;
|
|
@@ -2159,6 +3816,18 @@ var Scene = class _Scene {
|
|
|
2159
3816
|
const top = (canvasRect?.top ?? 0) - (parentRect?.top ?? 0) - (parent.clientTop || 0) + parent.scrollTop;
|
|
2160
3817
|
const scaleX = this.width > 0 ? cssWidth / this.width : 1;
|
|
2161
3818
|
const scaleY = this.height > 0 ? cssHeight / this.height : 1;
|
|
3819
|
+
const prev = this._overlayGeometry;
|
|
3820
|
+
if (prev !== null && prev.left === left && prev.top === top && prev.cssWidth === cssWidth && prev.cssHeight === cssHeight && prev.width === this.width && prev.height === this.height) {
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
this._overlayGeometry = {
|
|
3824
|
+
left,
|
|
3825
|
+
top,
|
|
3826
|
+
cssWidth,
|
|
3827
|
+
cssHeight,
|
|
3828
|
+
width: this.width,
|
|
3829
|
+
height: this.height
|
|
3830
|
+
};
|
|
2162
3831
|
for (const root of [this.a11yRoot, this.portalRoot]) {
|
|
2163
3832
|
if (!root) continue;
|
|
2164
3833
|
root.style.left = `${left}px`;
|
|
@@ -2226,6 +3895,7 @@ var Scene = class _Scene {
|
|
|
2226
3895
|
if (portal.domElement.parentElement !== this.portalRoot) {
|
|
2227
3896
|
this.portalRoot.appendChild(portal.domElement);
|
|
2228
3897
|
}
|
|
3898
|
+
portal.attachDOMBindings();
|
|
2229
3899
|
if (!portal.domElement.hasAttribute("data-vecto-id")) {
|
|
2230
3900
|
portal.domElement.setAttribute("data-vecto-id", portal.id);
|
|
2231
3901
|
}
|
|
@@ -2266,6 +3936,7 @@ var Scene = class _Scene {
|
|
|
2266
3936
|
const portal = this.portalEntities.get(oldId);
|
|
2267
3937
|
if (portal) {
|
|
2268
3938
|
if (portal.domElement.parentElement === this.portalRoot && (!portal.scene || portal.scene === this)) {
|
|
3939
|
+
portal.releaseDOMBindings();
|
|
2269
3940
|
portal.domElement.remove();
|
|
2270
3941
|
}
|
|
2271
3942
|
this.portalEntities.delete(oldId);
|
|
@@ -2288,6 +3959,7 @@ var Scene = class _Scene {
|
|
|
2288
3959
|
}
|
|
2289
3960
|
loop(time) {
|
|
2290
3961
|
if (!this.isRunning) return;
|
|
3962
|
+
if (!this._canvasOnScreen) return;
|
|
2291
3963
|
let cap = this.effectiveMaxFPS();
|
|
2292
3964
|
const isIdle = !this.dirty && !this.frameHadAnimation;
|
|
2293
3965
|
if (isIdle && this.autoThrottle && this.renderMode === "always" && this.maxFPS > 0) {
|
|
@@ -2303,6 +3975,7 @@ var Scene = class _Scene {
|
|
|
2303
3975
|
const nominal = 1e3 / cap;
|
|
2304
3976
|
if (Math.abs(dt - nominal) < nominal * 0.3) dt = nominal;
|
|
2305
3977
|
}
|
|
3978
|
+
if (dt > _Scene.MAX_FRAME_DT) dt = _Scene.MAX_FRAME_DT;
|
|
2306
3979
|
this.lastTime = time;
|
|
2307
3980
|
if (this.renderMode === "onDemand" && isIdle) {
|
|
2308
3981
|
this._skippedFrames++;
|
|
@@ -2346,6 +4019,7 @@ var Scene = class _Scene {
|
|
|
2346
4019
|
* @param time - Current absolute time in milliseconds (default 0).
|
|
2347
4020
|
*/
|
|
2348
4021
|
render(renderer, dt = 0, time = 0) {
|
|
4022
|
+
if (renderer.isContextLost?.()) return;
|
|
2349
4023
|
const isMainRenderer = renderer === this.renderer;
|
|
2350
4024
|
if (isMainRenderer && this.a11yRoot && this.canvas.parentElement) {
|
|
2351
4025
|
const parentStyle = this.canvas.parentElement.style;
|
|
@@ -2355,23 +4029,13 @@ var Scene = class _Scene {
|
|
|
2355
4029
|
this.syncOverlayGeometry();
|
|
2356
4030
|
}
|
|
2357
4031
|
if (isMainRenderer) {
|
|
4032
|
+
this.currentFrame++;
|
|
2358
4033
|
this.renderOrderCounter = 0;
|
|
2359
4034
|
this.a11yRenderOrders.clear();
|
|
2360
4035
|
this.activePortalsThisFrame.clear();
|
|
4036
|
+
this._tickBatchedDrivers(dt);
|
|
2361
4037
|
}
|
|
2362
|
-
const computeEntities =
|
|
2363
|
-
const collectComputeEntities = (node) => {
|
|
2364
|
-
if (node instanceof ComputeParticleEntity) {
|
|
2365
|
-
computeEntities.push(node);
|
|
2366
|
-
}
|
|
2367
|
-
for (const child of node.children) {
|
|
2368
|
-
collectComputeEntities(child);
|
|
2369
|
-
}
|
|
2370
|
-
};
|
|
2371
|
-
collectComputeEntities(this.root);
|
|
2372
|
-
for (const overlay of this.overlayRoot.children) {
|
|
2373
|
-
collectComputeEntities(overlay);
|
|
2374
|
-
}
|
|
4038
|
+
const computeEntities = this._computeEntitiesFor(this._structureVersion);
|
|
2375
4039
|
if (computeEntities.length > 0) {
|
|
2376
4040
|
const isMainRenderPath = renderer === this.renderer;
|
|
2377
4041
|
if (isMainRenderPath && !this.device && !this.webgpuDisabled && !this.initializingWebGPU && !this.deviceLost) {
|
|
@@ -2469,7 +4133,11 @@ var Scene = class _Scene {
|
|
|
2469
4133
|
my = -9999;
|
|
2470
4134
|
}
|
|
2471
4135
|
}
|
|
2472
|
-
|
|
4136
|
+
if (this._particleWasm) {
|
|
4137
|
+
entity.stepWithBackend(this._particleWasm, dt / 1e3, mx, my, this.width, this.height);
|
|
4138
|
+
} else {
|
|
4139
|
+
entity.updateCPU(dt / 1e3, mx, my, this.width, this.height);
|
|
4140
|
+
}
|
|
2473
4141
|
}
|
|
2474
4142
|
}
|
|
2475
4143
|
} else if (isMainRenderer) {
|
|
@@ -2483,31 +4151,69 @@ var Scene = class _Scene {
|
|
|
2483
4151
|
const vh = this.height;
|
|
2484
4152
|
let walkHadAnimation = false;
|
|
2485
4153
|
let walkHadInteractive = false;
|
|
2486
|
-
const
|
|
2487
|
-
|
|
4154
|
+
const runUpdate = (node) => {
|
|
4155
|
+
const overridesUpdate = node.update !== Entity.prototype.update;
|
|
4156
|
+
let pending = node.hasPendingAnimations();
|
|
4157
|
+
if (pending || overridesUpdate) {
|
|
2488
4158
|
node.update(dt, time);
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
4159
|
+
pending = node.hasPendingAnimations();
|
|
4160
|
+
}
|
|
4161
|
+
if (pending) walkHadAnimation = true;
|
|
4162
|
+
if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
|
|
4163
|
+
if (this._devActive && this._devFrameCount % 120 === 0) {
|
|
4164
|
+
if (overridesUpdate && node.hasPendingAnimations === Entity.prototype.hasPendingAnimations) {
|
|
4165
|
+
this._devWarn(
|
|
4166
|
+
`Entity "${node.id}" overrides update() but not hasPendingAnimations(). Custom motion in update() without overriding hasPendingAnimations() causes the idle throttle to drop the animation to ~2fps. Override hasPendingAnimations() to return true while motion is in flight.`
|
|
4167
|
+
);
|
|
2497
4168
|
}
|
|
2498
4169
|
}
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
const
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
const
|
|
2509
|
-
|
|
2510
|
-
|
|
4170
|
+
};
|
|
4171
|
+
const wasmMain = isMainRenderer && this._wasm !== null && this._transformBackend === "wasm";
|
|
4172
|
+
if (wasmMain) {
|
|
4173
|
+
const updateWalk = (node) => {
|
|
4174
|
+
runUpdate(node);
|
|
4175
|
+
const kids = node.children;
|
|
4176
|
+
for (let i = 0; i < kids.length; i++) updateWalk(kids[i]);
|
|
4177
|
+
};
|
|
4178
|
+
updateWalk(this.root);
|
|
4179
|
+
for (const overlay of this.overlayRoot.children) updateWalk(overlay);
|
|
4180
|
+
}
|
|
4181
|
+
const wasmWorld = wasmMain ? this._syncWasmStore() : null;
|
|
4182
|
+
const wasmSlotEntity = this._slotEntity;
|
|
4183
|
+
const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
|
|
4184
|
+
if (isMainRenderer && !wasmMain) {
|
|
4185
|
+
runUpdate(node);
|
|
4186
|
+
}
|
|
4187
|
+
let a;
|
|
4188
|
+
let b;
|
|
4189
|
+
let c;
|
|
4190
|
+
let d;
|
|
4191
|
+
let te;
|
|
4192
|
+
let tf;
|
|
4193
|
+
const slot = node._storeSlot;
|
|
4194
|
+
if (wasmWorld !== null && slot >= 0 && wasmSlotEntity[slot] === node) {
|
|
4195
|
+
a = wasmWorld.wa[slot];
|
|
4196
|
+
b = wasmWorld.wb[slot];
|
|
4197
|
+
c = wasmWorld.wc[slot];
|
|
4198
|
+
d = wasmWorld.wd[slot];
|
|
4199
|
+
te = wasmWorld.we[slot];
|
|
4200
|
+
tf = wasmWorld.wf[slot];
|
|
4201
|
+
} else {
|
|
4202
|
+
const trig = node._getTrig();
|
|
4203
|
+
const cos = trig.cos;
|
|
4204
|
+
const sin = trig.sin;
|
|
4205
|
+
te = pa * node.x + pc * node.y + pe;
|
|
4206
|
+
tf = pb * node.x + pd * node.y + pf;
|
|
4207
|
+
const sxCos = node.scaleX * cos;
|
|
4208
|
+
const sxSin = node.scaleX * sin;
|
|
4209
|
+
const syCos = node.scaleY * cos;
|
|
4210
|
+
const sySin = node.scaleY * sin;
|
|
4211
|
+
a = pa * sxCos + pc * sySin;
|
|
4212
|
+
b = pb * sxCos + pd * sySin;
|
|
4213
|
+
c = pa * -sxSin + pc * syCos;
|
|
4214
|
+
d = pb * -sxSin + pd * syCos;
|
|
4215
|
+
}
|
|
4216
|
+
if (isMainRenderer) node._setWorldCache(a, b, c, d, te, tf, this.currentFrame);
|
|
2511
4217
|
const worldScaleX = Math.hypot(a, b);
|
|
2512
4218
|
const worldScaleY = Math.hypot(c, d);
|
|
2513
4219
|
const worldOpacity = parentOpacity * node.opacity;
|
|
@@ -2656,12 +4362,25 @@ var Scene = class _Scene {
|
|
|
2656
4362
|
this.pointRenderer.maxDPR = this.maxDPR;
|
|
2657
4363
|
this.pointRenderer.resize(width, height);
|
|
2658
4364
|
}
|
|
2659
|
-
if (this.gpuCanvas)
|
|
2660
|
-
this.gpuCanvas.width = width;
|
|
2661
|
-
this.gpuCanvas.height = height;
|
|
2662
|
-
}
|
|
4365
|
+
if (this.gpuCanvas) this.sizeGpuCanvas(this.gpuCanvas, width, height);
|
|
2663
4366
|
this.markDirty();
|
|
2664
4367
|
}
|
|
4368
|
+
/** Effective device pixel ratio, matching CanvasRenderer: real DPR clamped to
|
|
4369
|
+
* `maxDPR` when set. */
|
|
4370
|
+
effectiveDPR() {
|
|
4371
|
+
const real = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
4372
|
+
return this.maxDPR !== void 0 ? Math.min(real, this.maxDPR) : real;
|
|
4373
|
+
}
|
|
4374
|
+
/** Size the WebGPU particle canvas: backing store at logical × DPR, CSS box at
|
|
4375
|
+
* the logical size. Sizing the backing store in logical px (the old
|
|
4376
|
+
* behavior) left it rasterized at 1× and CSS-stretched — blurry on HiDPI. */
|
|
4377
|
+
sizeGpuCanvas(gpuCanvas, width, height) {
|
|
4378
|
+
const dpr = this.effectiveDPR();
|
|
4379
|
+
gpuCanvas.width = Math.max(1, Math.round(width * dpr));
|
|
4380
|
+
gpuCanvas.height = Math.max(1, Math.round(height * dpr));
|
|
4381
|
+
gpuCanvas.style.width = `${width}px`;
|
|
4382
|
+
gpuCanvas.style.height = `${height}px`;
|
|
4383
|
+
}
|
|
2665
4384
|
/**
|
|
2666
4385
|
* Gets the accessibility DOM element projected for the given entity ID.
|
|
2667
4386
|
*/
|
|
@@ -2684,6 +4403,9 @@ var Scene = class _Scene {
|
|
|
2684
4403
|
findEntityAt(x, y) {
|
|
2685
4404
|
const overlayHit = this.findHitRecursively(this.overlayRoot, x, y);
|
|
2686
4405
|
if (overlayHit) return overlayHit;
|
|
4406
|
+
if (this._hitWasm && this._ensureHitGrid()) {
|
|
4407
|
+
return this._findEntityAtWasm(x, y);
|
|
4408
|
+
}
|
|
2687
4409
|
return this.findHitRecursively(this.root, x, y);
|
|
2688
4410
|
}
|
|
2689
4411
|
/** Submit one transparent clear pass when particle content lingers on the GPU canvas. */
|
|
@@ -2718,8 +4440,7 @@ var Scene = class _Scene {
|
|
|
2718
4440
|
const device = await adapter.requestDevice();
|
|
2719
4441
|
if (typeof document !== "undefined" && !this.gpuCanvas) {
|
|
2720
4442
|
const gpuCanvas = document.createElement("canvas");
|
|
2721
|
-
gpuCanvas.width
|
|
2722
|
-
gpuCanvas.height = this.height;
|
|
4443
|
+
this.sizeGpuCanvas(gpuCanvas, this.width, this.height);
|
|
2723
4444
|
gpuCanvas.style.position = "absolute";
|
|
2724
4445
|
gpuCanvas.style.top = "0";
|
|
2725
4446
|
gpuCanvas.style.left = "0";
|
|
@@ -2729,6 +4450,7 @@ var Scene = class _Scene {
|
|
|
2729
4450
|
this.canvas.parentElement.appendChild(gpuCanvas);
|
|
2730
4451
|
}
|
|
2731
4452
|
this.gpuCanvas = gpuCanvas;
|
|
4453
|
+
this._overlayGeometry = null;
|
|
2732
4454
|
this.gpuContext = gpuCanvas.getContext("webgpu");
|
|
2733
4455
|
}
|
|
2734
4456
|
if (this.gpuContext) {
|
|
@@ -2825,19 +4547,74 @@ var Scene = class _Scene {
|
|
|
2825
4547
|
}
|
|
2826
4548
|
}
|
|
2827
4549
|
}
|
|
2828
|
-
findHitRecursively(node, x, y) {
|
|
4550
|
+
findHitRecursively(node, x, y, clip = null) {
|
|
4551
|
+
if (node.opacity <= 0) return null;
|
|
4552
|
+
let childClip = clip;
|
|
4553
|
+
if (node.clipChildren) {
|
|
4554
|
+
const box = node.getWorldBounds();
|
|
4555
|
+
childClip = clip ? intersectBounds(clip, box) : box;
|
|
4556
|
+
}
|
|
2829
4557
|
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
2830
|
-
const hit = this.findHitRecursively(node.children[i], x, y);
|
|
4558
|
+
const hit = this.findHitRecursively(node.children[i], x, y, childClip);
|
|
2831
4559
|
if (hit) return hit;
|
|
2832
4560
|
}
|
|
2833
|
-
if (node.isPointInside && node.isPointInside(x, y)) {
|
|
4561
|
+
if (node.isPointInside && node.isPointInside(x, y) && (!clip || pointInBounds(clip, x, y)) && !this.isPointerTransparent(node)) {
|
|
2834
4562
|
return node;
|
|
2835
4563
|
}
|
|
2836
4564
|
return null;
|
|
2837
4565
|
}
|
|
4566
|
+
/** Whether `node` opts out of being a pointer hit target: a disabled control
|
|
4567
|
+
* or an explicit `pointerEvents: 'none'` in its a11y attributes. Its children
|
|
4568
|
+
* are still walked (a transparent container can hold hittable descendants). */
|
|
4569
|
+
isPointerTransparent(node) {
|
|
4570
|
+
const attrs = node.getA11yAttributes();
|
|
4571
|
+
return attrs.disabled === true || attrs.pointerEvents === "none";
|
|
4572
|
+
}
|
|
4573
|
+
/**
|
|
4574
|
+
* Whether a confirmed geometric hit on `node` at world `(x, y)` is a REAL hit,
|
|
4575
|
+
* applying the same visibility/input gating as {@link findHitRecursively} but
|
|
4576
|
+
* from a flat candidate (the WASM grid has no recursion clip-stack): the node
|
|
4577
|
+
* and all ancestors are visible (`opacity > 0`), the point lies inside every
|
|
4578
|
+
* `clipChildren` ancestor's world box, and the node isn't pointer-transparent
|
|
4579
|
+
* (disabled / `pointerEvents: 'none'`). Keeps the WASM and JS hit paths in
|
|
4580
|
+
* lockstep so they return the same entity.
|
|
4581
|
+
*/
|
|
4582
|
+
isHitEligible(node, x, y) {
|
|
4583
|
+
if (this.isPointerTransparent(node)) return false;
|
|
4584
|
+
if (node.opacity <= 0) return false;
|
|
4585
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
4586
|
+
if (ancestor.opacity <= 0) return false;
|
|
4587
|
+
if (ancestor.clipChildren && ancestor.width > 0 && ancestor.height > 0) {
|
|
4588
|
+
const local = ancestor.worldToLocal(x, y);
|
|
4589
|
+
if (!local || local.x < 0 || local.y < 0 || local.x > ancestor.width || local.y > ancestor.height) {
|
|
4590
|
+
return false;
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
return true;
|
|
4595
|
+
}
|
|
2838
4596
|
};
|
|
4597
|
+
function intersectBounds(a, b) {
|
|
4598
|
+
const x = Math.max(a.x, b.x);
|
|
4599
|
+
const y = Math.max(a.y, b.y);
|
|
4600
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
4601
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
4602
|
+
return {
|
|
4603
|
+
x,
|
|
4604
|
+
y,
|
|
4605
|
+
width: Math.max(0, right - x),
|
|
4606
|
+
height: Math.max(0, bottom - y)
|
|
4607
|
+
};
|
|
4608
|
+
}
|
|
4609
|
+
function pointInBounds(b, x, y) {
|
|
4610
|
+
return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
|
|
4611
|
+
}
|
|
2839
4612
|
|
|
2840
4613
|
// src/components/TextEntity.ts
|
|
4614
|
+
import {
|
|
4615
|
+
LayoutEngine,
|
|
4616
|
+
createCanvasMeasurer
|
|
4617
|
+
} from "@vectojs/layout";
|
|
2841
4618
|
var sharedMeasurer;
|
|
2842
4619
|
function defaultMeasurer() {
|
|
2843
4620
|
if (sharedMeasurer === void 0) sharedMeasurer = createCanvasMeasurer("sans-serif");
|
|
@@ -3459,99 +5236,11 @@ var Group = class extends Entity {
|
|
|
3459
5236
|
}
|
|
3460
5237
|
};
|
|
3461
5238
|
|
|
3462
|
-
// src/
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
constructor(cellSize = 64) {
|
|
3468
|
-
this.cellSize = cellSize;
|
|
3469
|
-
}
|
|
3470
|
-
hash(cx, cy) {
|
|
3471
|
-
const x = cx < 0 ? -2 * cx - 1 : 2 * cx;
|
|
3472
|
-
const y = cy < 0 ? -2 * cy - 1 : 2 * cy;
|
|
3473
|
-
return (x + y) * (x + y + 1) / 2 + y;
|
|
3474
|
-
}
|
|
3475
|
-
cellsForAABB(x, y, w, h) {
|
|
3476
|
-
const minCx = Math.floor(x / this.cellSize);
|
|
3477
|
-
const minCy = Math.floor(y / this.cellSize);
|
|
3478
|
-
const maxCx = Math.floor((x + w) / this.cellSize);
|
|
3479
|
-
const maxCy = Math.floor((y + h) / this.cellSize);
|
|
3480
|
-
const keys = [];
|
|
3481
|
-
for (let cx = minCx; cx <= maxCx; cx++) {
|
|
3482
|
-
for (let cy = minCy; cy <= maxCy; cy++) {
|
|
3483
|
-
keys.push(this.hash(cx, cy));
|
|
3484
|
-
}
|
|
3485
|
-
}
|
|
3486
|
-
return keys;
|
|
3487
|
-
}
|
|
3488
|
-
/**
|
|
3489
|
-
* Insert or update an entity's axis-aligned bounding box in the grid.
|
|
3490
|
-
*
|
|
3491
|
-
* If the entity is already registered its old cell memberships are removed
|
|
3492
|
-
* before the new ones are computed, so this method is safe to call every
|
|
3493
|
-
* frame.
|
|
3494
|
-
*
|
|
3495
|
-
* @param id - Unique string identifier for the entity.
|
|
3496
|
-
* @param x - Left edge of the AABB in world space.
|
|
3497
|
-
* @param y - Top edge of the AABB in world space.
|
|
3498
|
-
* @param w - Width of the AABB.
|
|
3499
|
-
* @param h - Height of the AABB.
|
|
3500
|
-
*/
|
|
3501
|
-
insert(id, x, y, w, h) {
|
|
3502
|
-
this.remove(id);
|
|
3503
|
-
const keys = this.cellsForAABB(x, y, w, h);
|
|
3504
|
-
this.entityCells.set(id, keys);
|
|
3505
|
-
for (const key of keys) {
|
|
3506
|
-
if (!this.grid.has(key)) this.grid.set(key, /* @__PURE__ */ new Set());
|
|
3507
|
-
this.grid.get(key).add(id);
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
/**
|
|
3511
|
-
* Remove an entity from all grid cells it currently occupies.
|
|
3512
|
-
*
|
|
3513
|
-
* Silently does nothing if the entity is not registered.
|
|
3514
|
-
*
|
|
3515
|
-
* @param id - Unique string identifier of the entity to remove.
|
|
3516
|
-
*/
|
|
3517
|
-
remove(id) {
|
|
3518
|
-
const keys = this.entityCells.get(id);
|
|
3519
|
-
if (!keys) return;
|
|
3520
|
-
for (const key of keys) {
|
|
3521
|
-
this.grid.get(key)?.delete(id);
|
|
3522
|
-
}
|
|
3523
|
-
this.entityCells.delete(id);
|
|
3524
|
-
}
|
|
3525
|
-
/**
|
|
3526
|
-
* Return all entity IDs whose grid cells overlap the given AABB.
|
|
3527
|
-
*
|
|
3528
|
-
* Time complexity: O(k) where k is the number of cells the query AABB spans
|
|
3529
|
-
* plus the number of results — O(1) average for small, similarly-sized entities.
|
|
3530
|
-
*
|
|
3531
|
-
* @param x - Left edge of the query AABB.
|
|
3532
|
-
* @param y - Top edge of the query AABB.
|
|
3533
|
-
* @param w - Width of the query AABB.
|
|
3534
|
-
* @param h - Height of the query AABB.
|
|
3535
|
-
* @returns A `Set` of entity ID strings whose cells intersect the query region.
|
|
3536
|
-
*/
|
|
3537
|
-
query(x, y, w, h) {
|
|
3538
|
-
const result = /* @__PURE__ */ new Set();
|
|
3539
|
-
for (const key of this.cellsForAABB(x, y, w, h)) {
|
|
3540
|
-
const cell = this.grid.get(key);
|
|
3541
|
-
if (cell) for (const id of cell) result.add(id);
|
|
3542
|
-
}
|
|
3543
|
-
return result;
|
|
3544
|
-
}
|
|
3545
|
-
/**
|
|
3546
|
-
* Clear all cells and entity registrations, resetting the grid to an empty state.
|
|
3547
|
-
*
|
|
3548
|
-
* Call once per frame before re-inserting all dynamic entities.
|
|
3549
|
-
*/
|
|
3550
|
-
clear() {
|
|
3551
|
-
this.grid.clear();
|
|
3552
|
-
this.entityCells.clear();
|
|
3553
|
-
}
|
|
3554
|
-
};
|
|
5239
|
+
// src/index.ts
|
|
5240
|
+
export * from "@vectojs/layout";
|
|
5241
|
+
export * from "@vectojs/text";
|
|
5242
|
+
export * from "@vectojs/math";
|
|
5243
|
+
export * from "@vectojs/animation";
|
|
3555
5244
|
|
|
3556
5245
|
// src/tree/DOMPortalEntity.ts
|
|
3557
5246
|
var DOMPortalEntity = class extends Entity {
|
|
@@ -3559,6 +5248,7 @@ var DOMPortalEntity = class extends Entity {
|
|
|
3559
5248
|
isDOMPortal = true;
|
|
3560
5249
|
domListeners = [];
|
|
3561
5250
|
resizeObserver = null;
|
|
5251
|
+
domBound = false;
|
|
3562
5252
|
cachedWidth = 100;
|
|
3563
5253
|
cachedHeight = 100;
|
|
3564
5254
|
lastWidth = "";
|
|
@@ -3577,50 +5267,84 @@ var DOMPortalEntity = class extends Entity {
|
|
|
3577
5267
|
this.domElement.style.pointerEvents = "auto";
|
|
3578
5268
|
this.cachedWidth = parseFloat(domElement.style.width) || domElement.offsetWidth || 100;
|
|
3579
5269
|
this.cachedHeight = parseFloat(domElement.style.height) || domElement.offsetHeight || 100;
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
this.
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
5270
|
+
this.attachDOMBindings();
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
/**
|
|
5274
|
+
* Attach the ResizeObserver + DOM event listeners that bridge native DOM
|
|
5275
|
+
* events into the Vecto event system. Idempotent: safe to call every frame
|
|
5276
|
+
* from the projection path (`Scene.syncPortalGeometry`), which is what lets a
|
|
5277
|
+
* portal survive a `scene.remove()` -> re-add cycle — {@link releaseDOMBindings}
|
|
5278
|
+
* tears these down on removal and the next projection re-attaches them.
|
|
5279
|
+
*/
|
|
5280
|
+
attachDOMBindings() {
|
|
5281
|
+
if (this.domBound || typeof window === "undefined") return;
|
|
5282
|
+
this.domBound = true;
|
|
5283
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
5284
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
5285
|
+
for (const entry of entries) {
|
|
5286
|
+
this.cachedWidth = entry.contentRect.width || entry.target.offsetWidth;
|
|
5287
|
+
this.cachedHeight = entry.contentRect.height || entry.target.offsetHeight;
|
|
5288
|
+
}
|
|
5289
|
+
});
|
|
5290
|
+
this.resizeObserver.observe(this.domElement);
|
|
5291
|
+
}
|
|
5292
|
+
const events = [
|
|
5293
|
+
"click",
|
|
5294
|
+
"pointerdown",
|
|
5295
|
+
"pointerup",
|
|
5296
|
+
"pointercancel",
|
|
5297
|
+
"pointermove",
|
|
5298
|
+
"wheel"
|
|
5299
|
+
];
|
|
5300
|
+
for (const type of events) {
|
|
5301
|
+
const handler = (e) => {
|
|
5302
|
+
this.dispatchEvent(new VectoJSEvent(type, this, e));
|
|
5303
|
+
};
|
|
5304
|
+
this.domElement.addEventListener(type, handler);
|
|
5305
|
+
this.domListeners.push({ type, handler, capture: false });
|
|
5306
|
+
}
|
|
5307
|
+
const hoverEvents = [
|
|
5308
|
+
{ native: "mouseenter", vecto: "hover" },
|
|
5309
|
+
{ native: "mouseleave", vecto: "pointerleave" }
|
|
5310
|
+
];
|
|
5311
|
+
for (const { native, vecto } of hoverEvents) {
|
|
5312
|
+
const handler = (e) => {
|
|
5313
|
+
this.dispatchEvent(new VectoJSEvent(vecto, this, e, false));
|
|
5314
|
+
};
|
|
5315
|
+
this.domElement.addEventListener(native, handler);
|
|
5316
|
+
this.domListeners.push({ type: native, handler, capture: false });
|
|
5317
|
+
}
|
|
5318
|
+
const focusEvents = ["focus", "blur"];
|
|
5319
|
+
for (const type of focusEvents) {
|
|
5320
|
+
const handler = (e) => {
|
|
5321
|
+
this.dispatchEvent(new VectoJSEvent(type, this, e, true));
|
|
5322
|
+
};
|
|
5323
|
+
this.domElement.addEventListener(type, handler, true);
|
|
5324
|
+
this.domListeners.push({ type, handler, capture: true });
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
/**
|
|
5328
|
+
* Disconnect the ResizeObserver and remove the DOM event listeners, without
|
|
5329
|
+
* touching the scene graph or the element's DOM parentage. Called on the
|
|
5330
|
+
* `scene.remove()` path so a detached portal doesn't leak an observer that
|
|
5331
|
+
* keeps its element alive and firing; the element itself is removed from the
|
|
5332
|
+
* document by the scene's portal pruning. Re-attached lazily on the next
|
|
5333
|
+
* projection frame via {@link attachDOMBindings}.
|
|
5334
|
+
*/
|
|
5335
|
+
releaseDOMBindings() {
|
|
5336
|
+
if (!this.domBound) return;
|
|
5337
|
+
this.domBound = false;
|
|
5338
|
+
if (this.resizeObserver) {
|
|
5339
|
+
this.resizeObserver.disconnect();
|
|
5340
|
+
this.resizeObserver = null;
|
|
5341
|
+
}
|
|
5342
|
+
if (this.domElement) {
|
|
5343
|
+
for (const { type, handler, capture } of this.domListeners) {
|
|
5344
|
+
this.domElement.removeEventListener(type, handler, capture);
|
|
3622
5345
|
}
|
|
3623
5346
|
}
|
|
5347
|
+
this.domListeners = [];
|
|
3624
5348
|
}
|
|
3625
5349
|
isPointInside(globalX, globalY) {
|
|
3626
5350
|
const w = this.width > 0 ? this.width : this.cachedWidth;
|
|
@@ -3636,18 +5360,9 @@ var DOMPortalEntity = class extends Entity {
|
|
|
3636
5360
|
render() {
|
|
3637
5361
|
}
|
|
3638
5362
|
destroy() {
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
this.resizeObserver = null;
|
|
3643
|
-
}
|
|
3644
|
-
if (this.domElement) {
|
|
3645
|
-
for (const { type, handler, capture } of this.domListeners) {
|
|
3646
|
-
this.domElement.removeEventListener(type, handler, capture);
|
|
3647
|
-
}
|
|
3648
|
-
this.domListeners = [];
|
|
3649
|
-
this.domElement.remove();
|
|
3650
|
-
}
|
|
5363
|
+
this.releaseDOMBindings();
|
|
5364
|
+
if (typeof window !== "undefined" && this.domElement) {
|
|
5365
|
+
this.domElement.remove();
|
|
3651
5366
|
}
|
|
3652
5367
|
super.destroy();
|
|
3653
5368
|
}
|
|
@@ -3657,20 +5372,13 @@ var DOMPortalEntity = class extends Entity {
|
|
|
3657
5372
|
Scene.registerWebGLPointRendererCreator(createWebGLPointRenderer);
|
|
3658
5373
|
Scene.registerWebGPUParticleSystemManager(WebGPUParticleSystemManager);
|
|
3659
5374
|
export {
|
|
3660
|
-
ArabicShaper,
|
|
3661
|
-
BidiResolver,
|
|
3662
5375
|
CanvasRenderer,
|
|
3663
5376
|
Circle,
|
|
3664
5377
|
ComputeParticleEntity,
|
|
3665
5378
|
DOMPortalEntity,
|
|
3666
|
-
Easing,
|
|
3667
5379
|
Entity,
|
|
3668
5380
|
GridTextEntity,
|
|
3669
5381
|
Group,
|
|
3670
|
-
LayoutEngine,
|
|
3671
|
-
LayoutResultBuffer,
|
|
3672
|
-
LayoutWorkerManager,
|
|
3673
|
-
MSDFFont,
|
|
3674
5382
|
MSDFTextEntity,
|
|
3675
5383
|
PARTICLE_OFFSET_LIFE,
|
|
3676
5384
|
PARTICLE_OFFSET_ORIGIN_X,
|
|
@@ -3686,25 +5394,15 @@ export {
|
|
|
3686
5394
|
SVGEntity,
|
|
3687
5395
|
SVGRenderer,
|
|
3688
5396
|
Scene,
|
|
3689
|
-
SpatialHashGrid,
|
|
3690
5397
|
SplineEntity,
|
|
3691
|
-
SpringDriver,
|
|
3692
|
-
SpringPhysics,
|
|
3693
5398
|
TextEntity,
|
|
3694
5399
|
TextRasterCache,
|
|
3695
|
-
TweenDriver,
|
|
3696
5400
|
VectoJSEvent,
|
|
3697
5401
|
WebGPUParticleSystemManager,
|
|
3698
|
-
clearCssLineBoxMetrics,
|
|
3699
|
-
computeLineSegments,
|
|
3700
|
-
createCanvasMeasurer,
|
|
3701
5402
|
createWebGLPointRenderer,
|
|
3702
|
-
cssLineBoxBaseline,
|
|
3703
5403
|
isSafeUrl,
|
|
3704
|
-
isTweenConfig,
|
|
3705
5404
|
loadSpline,
|
|
3706
5405
|
parseColorToRGBA,
|
|
3707
5406
|
polySegmentToBezier,
|
|
3708
|
-
prepareContentGrid,
|
|
3709
5407
|
sanitizeUrl
|
|
3710
5408
|
};
|