@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.
Files changed (52) hide show
  1. package/dist/{chunk-XIEQHSBB.mjs → chunk-AQTO7OSU.mjs} +253 -496
  2. package/dist/{chunk-2Z23LTH3.js → chunk-DUYB4GX4.js} +310 -551
  3. package/dist/{chunk-BEUIB3U7.js → chunk-IYPLG4Z4.js} +93 -43
  4. package/dist/{chunk-L5BCKFQE.mjs → chunk-JFU56BX4.mjs} +48 -0
  5. package/dist/index.d.ts +4 -12
  6. package/dist/index.js +2155 -456
  7. package/dist/index.mjs +1977 -279
  8. package/dist/layout/index.d.ts +1 -3
  9. package/dist/layout.js +3 -15
  10. package/dist/layout.mjs +2 -16
  11. package/dist/renderer/CanvasRenderer.d.ts +22 -0
  12. package/dist/renderer/IRenderer.d.ts +13 -0
  13. package/dist/renderer.js +4 -3
  14. package/dist/renderer.mjs +1 -1
  15. package/dist/text/MSDFTextEntity.d.ts +30 -1
  16. package/dist/text/index.d.ts +1 -5
  17. package/dist/text.js +5 -15
  18. package/dist/text.mjs +6 -17
  19. package/dist/tree/ComputeParticleEntity.d.ts +18 -0
  20. package/dist/tree/DOMPortalEntity.d.ts +18 -0
  21. package/dist/tree/Entity.d.ts +122 -6
  22. package/dist/tree/Scene.d.ts +380 -0
  23. package/dist/wasm/anim-backend.d.ts +86 -0
  24. package/dist/wasm/asset.d.ts +25 -0
  25. package/dist/wasm/asset.js +7 -0
  26. package/dist/wasm/asset.mjs +5 -0
  27. package/dist/wasm/backend.d.ts +154 -0
  28. package/dist/wasm/hit-backend.d.ts +92 -0
  29. package/dist/wasm/hit-store.d.ts +48 -0
  30. package/dist/wasm/particle-backend.d.ts +104 -0
  31. package/dist/wasm/scene-store.d.ts +28 -0
  32. package/dist/wasm/soa.d.ts +146 -0
  33. package/dist/wasm/vectojs_core.wasm +0 -0
  34. package/package.json +17 -8
  35. package/dist/animation/drivers.d.ts +0 -48
  36. package/dist/animation/easing.d.ts +0 -16
  37. package/dist/chunk-4AR425AR.js +0 -1121
  38. package/dist/chunk-BA5HUUDF.js +0 -760
  39. package/dist/chunk-IESDTEJ4.mjs +0 -1121
  40. package/dist/chunk-X7I465AQ.mjs +0 -760
  41. package/dist/layout/LayoutEngine.d.ts +0 -289
  42. package/dist/layout/LayoutWorker.d.ts +0 -23
  43. package/dist/layout/LayoutWorkerManager.d.ts +0 -26
  44. package/dist/layout/LayoutWorkerSource.d.ts +0 -1
  45. package/dist/layout/measure.d.ts +0 -20
  46. package/dist/math/SpatialHashGrid.d.ts +0 -53
  47. package/dist/math/SpringPhysics.d.ts +0 -13
  48. package/dist/text/ArabicShaper.d.ts +0 -10
  49. package/dist/text/BidiResolver.d.ts +0 -5
  50. package/dist/text/MSDFFont.d.ts +0 -129
  51. package/dist/text/PreparedContentGrid.d.ts +0 -60
  52. package/dist/text/Typography.d.ts +0 -11
package/dist/index.js CHANGED
@@ -1,39 +1,23 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _createStarExport(obj) { Object.keys(obj) .filter((key) => key !== "default" && key !== "__esModule") .forEach((key) => { if (exports.hasOwnProperty(key)) { return; } Object.defineProperty(exports, key, {enumerable: true, configurable: true, get: () => obj[key]}); }); } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7; var _class8; var _class9; var _class10;const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
2
2
 
3
3
 
4
4
 
5
5
 
6
- var _chunkBA5HUUDFjs = require('./chunk-BA5HUUDF.js');
7
6
 
8
7
 
9
8
 
10
9
 
11
10
 
11
+ var _chunkIYPLG4Z4js = require('./chunk-IYPLG4Z4.js');
12
12
 
13
13
 
14
14
 
15
15
 
16
- var _chunkBEUIB3U7js = require('./chunk-BEUIB3U7.js');
17
16
 
17
+ var _chunkDUYB4GX4js = require('./chunk-DUYB4GX4.js');
18
18
 
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
- var _chunk2Z23LTH3js = require('./chunk-2Z23LTH3.js');
32
-
33
-
34
-
35
-
36
- var _chunk4AR425ARjs = require('./chunk-4AR425AR.js');
19
+ // src/tree/Scene.ts
20
+ var _animation = require('@vectojs/animation'); _createStarExport(_animation);
37
21
 
38
22
  // src/tree/ComputeParticleEntity.ts
39
23
  var PARTICLE_STRIDE_FLOATS = 8;
@@ -45,7 +29,7 @@ var PARTICLE_OFFSET_ORIGIN_X = 4;
45
29
  var PARTICLE_OFFSET_ORIGIN_Y = 5;
46
30
  var PARTICLE_OFFSET_SIZE = 6;
47
31
  var PARTICLE_OFFSET_LIFE = 7;
48
- var ComputeParticleEntity = (_class = class extends _chunk2Z23LTH3js.Entity {
32
+ var ComputeParticleEntity = (_class = class extends _chunkDUYB4GX4js.Entity {
49
33
 
50
34
 
51
35
 
@@ -68,8 +52,12 @@ var ComputeParticleEntity = (_class = class extends _chunk2Z23LTH3js.Entity {
68
52
  __init5() {this.computeBindGroup = null}
69
53
  /** WebGPU bind group for the render pass (usually same as compute). */
70
54
  __init6() {this.renderBindGroup = null}
55
+ /** When the last simulation step ran through the WASM backend, its fused
56
+ * pending-animation flag; `null` when the last step used the JS `updateCPU`
57
+ * path (so `hasPendingAnimations` falls back to its own scan). */
58
+ __init7() {this._wasmPending = null}
71
59
  constructor(options = {}) {
72
- super();_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);;
60
+ super();_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);;
73
61
  this.maxParticles = _nullishCoalesce(options.maxParticles, () => ( 1e4));
74
62
  this.springK = _nullishCoalesce(options.springK, () => ( 0.05));
75
63
  this.damping = _nullishCoalesce(options.damping, () => ( 0.95));
@@ -197,6 +185,9 @@ var ComputeParticleEntity = (_class = class extends _chunk2Z23LTH3js.Entity {
197
185
  * always return `true` and defeat the idle throttle entirely.
198
186
  */
199
187
  hasPendingAnimations() {
188
+ if (this._wasmPending !== null) {
189
+ return this._wasmPending || this.pendingExplosion !== null;
190
+ }
200
191
  const EPS_VELOCITY = 0.5;
201
192
  const EPS_DISTANCE = 0.5;
202
193
  for (let i = 0; i < this.maxParticles; i++) {
@@ -222,6 +213,7 @@ var ComputeParticleEntity = (_class = class extends _chunk2Z23LTH3js.Entity {
222
213
  * @param height - Boundary height.
223
214
  */
224
215
  updateCPU(dt, mouseX, mouseY, width, height) {
216
+ this._wasmPending = null;
225
217
  const safeDt = isNaN(dt) ? 0.016 : Math.max(0, Math.min(dt, 0.1));
226
218
  const explosion = this.pendingExplosion;
227
219
  const safeWidth = Math.max(1, width);
@@ -304,6 +296,37 @@ var ComputeParticleEntity = (_class = class extends _chunk2Z23LTH3js.Entity {
304
296
  }
305
297
  this.pendingExplosion = null;
306
298
  }
299
+ /**
300
+ * Advance the simulation one step through the WASM particle kernel: transpose
301
+ * this entity's AoS buffer into the backend's SoA views, run `particle_step`,
302
+ * and scatter position/velocity/life back. Produces an f32 result (matching
303
+ * the WGSL shader) that differs from {@link updateCPU}'s f64 by <1 ULP/step —
304
+ * the accepted CPU-vs-GPU-class divergence. Caches the kernel's fused
305
+ * pending-animation flag so {@link hasPendingAnimations} needs no second scan.
306
+ *
307
+ * The backend holds one resident SoA store, so a Scene with multiple particle
308
+ * entities reuses it sequentially — origin is therefore re-gathered each call
309
+ * (not upload-once), a couple of extra f32 reads per particle.
310
+ */
311
+ stepWithBackend(backend, dt, mouseX, mouseY, width, height) {
312
+ const count = this.maxParticles;
313
+ backend.ensure(count);
314
+ backend.gather(this.particleData, count, true);
315
+ this._wasmPending = backend.step(count, {
316
+ dt,
317
+ mouseX,
318
+ mouseY,
319
+ width,
320
+ height,
321
+ springK: this.springK,
322
+ damping: this.damping,
323
+ bounceDamping: this.bounceDamping,
324
+ maxVelocity: this.maxVelocity,
325
+ explosion: this.pendingExplosion
326
+ });
327
+ backend.scatter(this.particleData, count);
328
+ this.pendingExplosion = null;
329
+ }
307
330
  destroy() {
308
331
  this.destroyGPUResources();
309
332
  super.destroy();
@@ -329,7 +352,751 @@ var ComputeParticleEntity = (_class = class extends _chunk2Z23LTH3js.Entity {
329
352
  }
330
353
  }, _class);
331
354
 
355
+ // src/wasm/soa.ts
356
+ var PAD = 8;
357
+ function buildStore(nodes) {
358
+ const count = nodes.length;
359
+ const capacity = count + PAD;
360
+ const childrenOf = Array.from({ length: count }, () => []);
361
+ let rootInput = -1;
362
+ for (let k = 0; k < count; k++) {
363
+ const p = nodes[k].parent;
364
+ if (p === -1) {
365
+ if (rootInput !== -1) throw new Error("buildStore: more than one root (parent === -1)");
366
+ rootInput = k;
367
+ } else {
368
+ if (p < 0 || p >= count)
369
+ throw new Error(`buildStore: node ${k} has out-of-range parent ${p}`);
370
+ childrenOf[p].push(k);
371
+ }
372
+ }
373
+ if (rootInput === -1) throw new Error("buildStore: no root (need exactly one parent === -1)");
374
+ const store = {
375
+ count,
376
+ capacity,
377
+ x: new Float64Array(capacity),
378
+ y: new Float64Array(capacity),
379
+ sx: new Float64Array(capacity),
380
+ sy: new Float64Array(capacity),
381
+ cos: new Float64Array(capacity),
382
+ sin: new Float64Array(capacity),
383
+ opacity: new Float64Array(capacity),
384
+ wa: new Float64Array(capacity),
385
+ wb: new Float64Array(capacity),
386
+ wc: new Float64Array(capacity),
387
+ wd: new Float64Array(capacity),
388
+ we: new Float64Array(capacity),
389
+ wf: new Float64Array(capacity),
390
+ wo: new Float64Array(capacity),
391
+ bx: new Float64Array(capacity),
392
+ by: new Float64Array(capacity),
393
+ bw: new Float64Array(capacity),
394
+ bh: new Float64Array(capacity),
395
+ aminx: new Float64Array(capacity),
396
+ aminy: new Float64Array(capacity),
397
+ amaxx: new Float64Array(capacity),
398
+ amaxy: new Float64Array(capacity),
399
+ runParent: new Int32Array(count),
400
+ runStart: new Int32Array(count),
401
+ runLen: new Int32Array(count),
402
+ runCount: 0,
403
+ storeIndexOf: new Int32Array(count).fill(-1)
404
+ };
405
+ store.storeIndexOf[rootInput] = 0;
406
+ writeInput(store, 0, nodes[rootInput]);
407
+ let next = 1;
408
+ const queue = [rootInput];
409
+ let head = 0;
410
+ while (head < queue.length) {
411
+ const parentInput = queue[head++];
412
+ const kids = childrenOf[parentInput];
413
+ if (kids.length === 0) continue;
414
+ const runStart = next;
415
+ for (const kid of kids) {
416
+ store.storeIndexOf[kid] = next;
417
+ writeInput(store, next, nodes[kid]);
418
+ next++;
419
+ }
420
+ const r = store.runCount++;
421
+ store.runParent[r] = store.storeIndexOf[parentInput];
422
+ store.runStart[r] = runStart;
423
+ store.runLen[r] = kids.length;
424
+ for (const kid of kids) queue.push(kid);
425
+ }
426
+ return store;
427
+ }
428
+ function writeInput(s, i, n) {
429
+ s.x[i] = n.x;
430
+ s.y[i] = n.y;
431
+ s.sx[i] = n.scaleX;
432
+ s.sy[i] = n.scaleY;
433
+ s.cos[i] = Math.cos(n.rotation);
434
+ s.sin[i] = Math.sin(n.rotation);
435
+ s.opacity[i] = n.opacity;
436
+ s.bx[i] = _nullishCoalesce(n.bx, () => ( 0));
437
+ s.by[i] = _nullishCoalesce(n.by, () => ( 0));
438
+ s.bw[i] = _nullishCoalesce(n.bw, () => ( 0));
439
+ s.bh[i] = _nullishCoalesce(n.bh, () => ( 0));
440
+ }
441
+
442
+ // src/wasm/scene-store.ts
443
+ function buildTreeStore(root) {
444
+ const entities = [];
445
+ const inputIndex = /* @__PURE__ */ new Map();
446
+ const collect = (e) => {
447
+ inputIndex.set(e, entities.length);
448
+ entities.push(e);
449
+ const kids = e.children;
450
+ for (let i = 0; i < kids.length; i++) collect(kids[i]);
451
+ };
452
+ collect(root);
453
+ const nodes = entities.map((e) => {
454
+ const parent = e.parent;
455
+ const p = parent !== null && inputIndex.has(parent) ? inputIndex.get(parent) : -1;
456
+ return {
457
+ parent: e === root ? -1 : p,
458
+ x: e.x,
459
+ y: e.y,
460
+ scaleX: e.scaleX,
461
+ scaleY: e.scaleY,
462
+ rotation: e.rotation,
463
+ opacity: e.opacity
464
+ };
465
+ });
466
+ const store = buildStore(nodes);
467
+ const indexOf = /* @__PURE__ */ new Map();
468
+ for (const [e, ii] of inputIndex) indexOf.set(e, store.storeIndexOf[ii]);
469
+ return { store, indexOf };
470
+ }
471
+
472
+ // src/wasm/backend.ts
473
+ var PAD2 = 8;
474
+ var WasmTransformBackend = (_class2 = class {
475
+ __init8() {this.available = true}
476
+
477
+ __init9() {this.cap = 0}
478
+ __init10() {this.runCap = 0}
479
+ // Views over wasm linear memory, valid until the next init().
480
+
481
+
482
+
483
+
484
+
485
+
486
+
487
+
488
+
489
+
490
+
491
+
492
+
493
+
494
+
495
+
496
+
497
+
498
+
499
+
500
+
501
+
502
+
503
+
504
+
505
+ constructor(instance) {;_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);
506
+ this.ex = instance.exports;
507
+ }
508
+ /** Compose world matrices for `store` in WASM, writing back into its
509
+ * `wa..wo` arrays. Result is bit-identical to `composeJS(store)`. */
510
+ compose(store, kernel = "simd") {
511
+ this.ensure(store.count, store.runCount);
512
+ const n = store.count;
513
+ this.vx.set(store.x.subarray(0, n));
514
+ this.vy.set(store.y.subarray(0, n));
515
+ this.vsx.set(store.sx.subarray(0, n));
516
+ this.vsy.set(store.sy.subarray(0, n));
517
+ this.vcos.set(store.cos.subarray(0, n));
518
+ this.vsin.set(store.sin.subarray(0, n));
519
+ this.vop.set(store.opacity.subarray(0, n));
520
+ const rc = store.runCount;
521
+ this.vrp.set(store.runParent.subarray(0, rc));
522
+ this.vrs.set(store.runStart.subarray(0, rc));
523
+ this.vrl.set(store.runLen.subarray(0, rc));
524
+ this.ex.set_run_count(rc);
525
+ if (kernel === "scalar") this.ex.compose_scalar();
526
+ else this.ex.compose_simd();
527
+ store.wa.set(this.vwa.subarray(0, n));
528
+ store.wb.set(this.vwb.subarray(0, n));
529
+ store.wc.set(this.vwc.subarray(0, n));
530
+ store.wd.set(this.vwd.subarray(0, n));
531
+ store.we.set(this.vwe.subarray(0, n));
532
+ store.wf.set(this.vwf.subarray(0, n));
533
+ store.wo.set(this.vwo.subarray(0, n));
534
+ }
535
+ /**
536
+ * Run the kernel only, over data already resident in WASM memory — no upload,
537
+ * no readback. This is the per-frame cost the *designed* integration pays:
538
+ * entity accessors write `x/y/rotation` straight into the wasm input views
539
+ * (via {@link inputView}) and the renderer reads world matrices straight from
540
+ * the wasm output views (via {@link worldView}), so the batch copies in
541
+ * {@link compose} do not happen every frame. `compose` must have run at least
542
+ * once at the current capacity to size the store and set the run count; call
543
+ * {@link uploadRuns} after a topology change.
544
+ */
545
+ runKernel(kernel = "simd") {
546
+ if (kernel === "scalar") this.ex.compose_scalar();
547
+ else this.ex.compose_simd();
548
+ }
549
+ /** Upload only the run table + count (topology), leaving per-entity inputs to
550
+ * the resident views. Call when the tree structure changes, not per frame. */
551
+ uploadRuns(store) {
552
+ this.ensure(store.count, store.runCount);
553
+ const rc = store.runCount;
554
+ this.vrp.set(store.runParent.subarray(0, rc));
555
+ this.vrs.set(store.runStart.subarray(0, rc));
556
+ this.vrl.set(store.runLen.subarray(0, rc));
557
+ this.ex.set_run_count(rc);
558
+ }
559
+ /** The resident wasm input views (`x,y,sx,sy,cos,sin,opacity`), valid until
560
+ * the next capacity growth. Writing here is what makes uploads unnecessary. */
561
+ inputView() {
562
+ return {
563
+ x: this.vx,
564
+ y: this.vy,
565
+ sx: this.vsx,
566
+ sy: this.vsy,
567
+ cos: this.vcos,
568
+ sin: this.vsin,
569
+ opacity: this.vop
570
+ };
571
+ }
572
+ /** The resident wasm world-matrix output views (`wa..wo`). Reading here is
573
+ * what makes readback unnecessary. */
574
+ worldView() {
575
+ return {
576
+ wa: this.vwa,
577
+ wb: this.vwb,
578
+ wc: this.vwc,
579
+ wd: this.vwd,
580
+ we: this.vwe,
581
+ wf: this.vwf,
582
+ wo: this.vwo
583
+ };
584
+ }
585
+ /**
586
+ * Compute world-space AABBs for `store` in WASM (G1+), writing back into its
587
+ * `aminx/aminy/amaxx/amaxy` arrays. Uploads the local bounds, runs the AABB
588
+ * pass, reads results back. Result is bit-identical to `computeAabbsJS(store)`.
589
+ * `compose` (or `runKernel`) must have populated the world matrices first —
590
+ * this pass reads them. For the resident (no-copy) integration, write bounds
591
+ * via {@link boundsView} and read via {@link aabbView} + call
592
+ * {@link runAabbs} instead.
593
+ */
594
+ computeAabbs(store) {
595
+ this.ensure(store.count, store.runCount);
596
+ const n = store.count;
597
+ this.vbx.set(store.bx.subarray(0, n));
598
+ this.vby.set(store.by.subarray(0, n));
599
+ this.vbw.set(store.bw.subarray(0, n));
600
+ this.vbh.set(store.bh.subarray(0, n));
601
+ this.ex.compute_aabbs(n);
602
+ store.aminx.set(this.vaminx.subarray(0, n));
603
+ store.aminy.set(this.vaminy.subarray(0, n));
604
+ store.amaxx.set(this.vamaxx.subarray(0, n));
605
+ store.amaxy.set(this.vamaxy.subarray(0, n));
606
+ }
607
+ /** Run the AABB pass only, over `count` entities already resident in wasm
608
+ * memory (bounds written via {@link boundsView}, world matrices already
609
+ * composed). No upload/readback — the per-frame resident path. */
610
+ runAabbs(count) {
611
+ this.ex.compute_aabbs(count);
612
+ }
613
+ /** Resident wasm local-bounds input views (`bx,by,bw,bh`) for the AABB pass. */
614
+ boundsView() {
615
+ return { bx: this.vbx, by: this.vby, bw: this.vbw, bh: this.vbh };
616
+ }
617
+ /** Resident wasm world-AABB output views (`aminx,aminy,amaxx,amaxy`). */
618
+ aabbView() {
619
+ return {
620
+ aminx: this.vaminx,
621
+ aminy: this.vaminy,
622
+ amaxx: this.vamaxx,
623
+ amaxy: this.vamaxy
624
+ };
625
+ }
626
+ ensure(count, runCount) {
627
+ if (count + PAD2 <= this.cap && runCount <= this.runCap) return;
628
+ this.cap = count + PAD2;
629
+ this.runCap = Math.max(runCount, count, 1);
630
+ this.ex.init(count, this.runCap);
631
+ this.refreshViews();
632
+ }
633
+ /** Rebuild typed-array views after an init() (which may have grown, and thus
634
+ * detached, the memory buffer). */
635
+ refreshViews() {
636
+ const buf = this.ex.memory.buffer;
637
+ const cap = this.cap;
638
+ const rc = this.runCap;
639
+ const f64 = (ptr) => new Float64Array(buf, ptr, cap);
640
+ const i32 = (ptr) => new Int32Array(buf, ptr, rc);
641
+ this.vx = f64(this.ex.p_x());
642
+ this.vy = f64(this.ex.p_y());
643
+ this.vsx = f64(this.ex.p_sx());
644
+ this.vsy = f64(this.ex.p_sy());
645
+ this.vcos = f64(this.ex.p_cos());
646
+ this.vsin = f64(this.ex.p_sin());
647
+ this.vop = f64(this.ex.p_opacity());
648
+ this.vwa = f64(this.ex.p_wa());
649
+ this.vwb = f64(this.ex.p_wb());
650
+ this.vwc = f64(this.ex.p_wc());
651
+ this.vwd = f64(this.ex.p_wd());
652
+ this.vwe = f64(this.ex.p_we());
653
+ this.vwf = f64(this.ex.p_wf());
654
+ this.vwo = f64(this.ex.p_wo());
655
+ this.vbx = f64(this.ex.p_bx());
656
+ this.vby = f64(this.ex.p_by());
657
+ this.vbw = f64(this.ex.p_bw());
658
+ this.vbh = f64(this.ex.p_bh());
659
+ this.vaminx = f64(this.ex.p_aminx());
660
+ this.vaminy = f64(this.ex.p_aminy());
661
+ this.vamaxx = f64(this.ex.p_amaxx());
662
+ this.vamaxy = f64(this.ex.p_amaxy());
663
+ this.vrp = i32(this.ex.p_run_parent());
664
+ this.vrs = i32(this.ex.p_run_start());
665
+ this.vrl = i32(this.ex.p_run_len());
666
+ }
667
+ }, _class2);
668
+ async function instantiateAsync(bytes) {
669
+ try {
670
+ const { instance } = await WebAssembly.instantiate(bytes, {});
671
+ return new WasmTransformBackend(instance);
672
+ } catch (e3) {
673
+ return null;
674
+ }
675
+ }
676
+ async function instantiateStreaming(source) {
677
+ try {
678
+ const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
679
+ if (typeof WebAssembly.instantiateStreaming === "function") {
680
+ const buffered = resp.clone();
681
+ try {
682
+ const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
683
+ return new WasmTransformBackend(instance2);
684
+ } catch (e4) {
685
+ const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
686
+ return new WasmTransformBackend(instance2);
687
+ }
688
+ }
689
+ const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
690
+ return new WasmTransformBackend(instance);
691
+ } catch (e5) {
692
+ return null;
693
+ }
694
+ }
695
+
696
+ // src/wasm/hit-store.ts
697
+ function gatherHitAABBs(root, currentFrame) {
698
+ const slotEntity = [];
699
+ const boundless = [];
700
+ const minxs = [];
701
+ const minys = [];
702
+ const maxxs = [];
703
+ const maxys = [];
704
+ const scratch = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
705
+ const visit = (node) => {
706
+ const index = slotEntity.length;
707
+ slotEntity.push(node);
708
+ const bounds = node.getBounds();
709
+ if (bounds === null) {
710
+ boundless.push({ entity: node, index });
711
+ minxs.push(0);
712
+ minys.push(0);
713
+ maxxs.push(0);
714
+ maxys.push(0);
715
+ } else {
716
+ if (!node._readWorldCache(currentFrame, scratch)) {
717
+ const t = node.getWorldTransform();
718
+ scratch.a = t.a;
719
+ scratch.b = t.b;
720
+ scratch.c = t.c;
721
+ scratch.d = t.d;
722
+ scratch.e = t.e;
723
+ scratch.f = t.f;
724
+ }
725
+ const { a, b, c, d, e, f } = scratch;
726
+ let minX = Infinity;
727
+ let minY = Infinity;
728
+ let maxX = -Infinity;
729
+ let maxY = -Infinity;
730
+ for (let i = 0; i < 4; i++) {
731
+ const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
732
+ const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
733
+ const wx = a * lx + c * ly + e;
734
+ const wy = b * lx + d * ly + f;
735
+ if (wx < minX) minX = wx;
736
+ if (wx > maxX) maxX = wx;
737
+ if (wy < minY) minY = wy;
738
+ if (wy > maxY) maxY = wy;
739
+ }
740
+ minxs.push(minX);
741
+ minys.push(minY);
742
+ maxxs.push(maxX);
743
+ maxys.push(maxY);
744
+ }
745
+ const kids = node.children;
746
+ for (let i = 0; i < kids.length; i++) visit(kids[i]);
747
+ };
748
+ visit(root);
749
+ return {
750
+ count: slotEntity.length,
751
+ slotEntity,
752
+ boundless,
753
+ minx: Float64Array.from(minxs),
754
+ miny: Float64Array.from(minys),
755
+ maxx: Float64Array.from(maxxs),
756
+ maxy: Float64Array.from(maxys)
757
+ };
758
+ }
759
+
760
+ // src/wasm/hit-backend.ts
761
+ var PAD3 = 8;
762
+ var CELLS_PER_ENTITY_HINT = 4;
763
+ var HitTestBackend = (_class3 = class {
764
+
765
+ __init11() {this.entityCap = 0}
766
+ __init12() {this.cellCap = 0}
767
+ __init13() {this.itemCap = 0}
768
+
769
+
770
+
771
+
772
+
773
+
774
+
775
+ /** Grid geometry from the last {@link ensure} call. */
776
+ __init14() {this.gridW = 0}
777
+ __init15() {this.gridH = 0}
778
+ __init16() {this.cellSize = 64}
779
+ constructor(instance) {;_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);_class3.prototype.__init16.call(this);
780
+ this.ex = instance.exports;
781
+ }
782
+ /** The resident AABB input views (`minx/miny/maxx/maxy`), valid until the
783
+ * next capacity growth. Writing here is what {@link build} reads from. */
784
+ inputView() {
785
+ return { minx: this.vminx, miny: this.vminy, maxx: this.vmaxx, maxy: this.vmaxy };
786
+ }
787
+ /**
788
+ * Size (and grow, if needed) capacity for `count` entities over
789
+ * `[0,vw] x [0,vh]` at `cellSize`, and record the grid geometry
790
+ * {@link candidatesAt} needs. Call this BEFORE writing AABBs into
791
+ * {@link inputView} — a capacity growth detaches the previous views, so
792
+ * writing first and sizing after would write into a stale buffer.
793
+ */
794
+ ensure(count, vw, vh, cellSize) {
795
+ const gw = Math.max(1, Math.ceil(vw / cellSize));
796
+ const gh = Math.max(1, Math.ceil(vh / cellSize));
797
+ this.growIfNeeded(count, gw * gh, count * CELLS_PER_ENTITY_HINT);
798
+ this.gridW = gw;
799
+ this.gridH = gh;
800
+ this.cellSize = cellSize;
801
+ }
802
+ /**
803
+ * Run the kernel's bucketing over whatever is currently resident in
804
+ * {@link inputView} (write the AABBs there, and call {@link ensure} first).
805
+ * Returns `false` if the build overflowed its item budget — the caller must
806
+ * not trust {@link candidatesAt} results for this build and should fall back
807
+ * to the JS walk instead (never return a wrong hit).
808
+ */
809
+ runBuild(count, vw, vh, cellSize) {
810
+ this.ex.hit_build(count, vw, vh, cellSize);
811
+ return this.ex.hit_overflow() === 0;
812
+ }
813
+ /**
814
+ * Entity indices whose AABB overlaps the cell containing `(px, py)`, in
815
+ * ascending index order (scan from the end for topmost/highest-index
816
+ * first), or `null` if the point falls outside the built grid. This is a
817
+ * coarse candidate LIST, not a hit result — the caller must still confirm
818
+ * each candidate's AABB contains the point and re-check its precise
819
+ * `isPointInside`.
820
+ */
821
+ candidatesAt(px, py) {
822
+ if (px < 0 || py < 0) return null;
823
+ const cx = Math.floor(px / this.cellSize);
824
+ const cy = Math.floor(py / this.cellSize);
825
+ if (cx < 0 || cy < 0 || cx >= this.gridW || cy >= this.gridH) return null;
826
+ const c = cy * this.gridW + cx;
827
+ if (c >= this.cellCap) return null;
828
+ const start = this.vCellStart[c];
829
+ const count = this.vCellCount[c];
830
+ return this.vItems.subarray(start, start + count);
831
+ }
832
+ growIfNeeded(count, cellCount, itemCount) {
833
+ if (count + PAD3 <= this.entityCap && cellCount <= this.cellCap && itemCount <= this.itemCap) {
834
+ return;
835
+ }
836
+ this.entityCap = count + PAD3;
837
+ this.cellCap = Math.max(cellCount, 1);
838
+ this.itemCap = Math.max(itemCount, count, 1);
839
+ this.ex.hit_init(count, this.cellCap, this.itemCap);
840
+ this.refreshViews();
841
+ }
842
+ /** Rebuild typed-array views after a growing `hit_init` (which detaches the
843
+ * memory buffer). */
844
+ refreshViews() {
845
+ const buf = this.ex.memory.buffer;
846
+ const eCap = this.entityCap;
847
+ const cCap = this.cellCap;
848
+ const iCap = this.itemCap;
849
+ this.vminx = new Float64Array(buf, this.ex.p_h_minx(), eCap);
850
+ this.vminy = new Float64Array(buf, this.ex.p_h_miny(), eCap);
851
+ this.vmaxx = new Float64Array(buf, this.ex.p_h_maxx(), eCap);
852
+ this.vmaxy = new Float64Array(buf, this.ex.p_h_maxy(), eCap);
853
+ this.vCellStart = new Int32Array(buf, this.ex.p_h_cell_start(), cCap);
854
+ this.vCellCount = new Int32Array(buf, this.ex.p_h_cell_count(), cCap);
855
+ this.vItems = new Int32Array(buf, this.ex.p_h_items(), iCap);
856
+ }
857
+ }, _class3);
858
+ async function instantiateAsync2(bytes) {
859
+ try {
860
+ const { instance } = await WebAssembly.instantiate(bytes, {});
861
+ return new HitTestBackend(instance);
862
+ } catch (e6) {
863
+ return null;
864
+ }
865
+ }
866
+ async function instantiateStreaming2(source) {
867
+ try {
868
+ const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
869
+ if (typeof WebAssembly.instantiateStreaming === "function") {
870
+ const buffered = resp.clone();
871
+ try {
872
+ const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
873
+ return new HitTestBackend(instance2);
874
+ } catch (e7) {
875
+ const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
876
+ return new HitTestBackend(instance2);
877
+ }
878
+ }
879
+ const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
880
+ return new HitTestBackend(instance);
881
+ } catch (e8) {
882
+ return null;
883
+ }
884
+ }
885
+
886
+ // src/wasm/anim-backend.ts
887
+ var PAD4 = 8;
888
+ var AnimBackend = (_class4 = class {
889
+
890
+ __init17() {this.springCap = 0}
891
+ __init18() {this.tweenCap = 0}
892
+
893
+
894
+ constructor(instance) {;_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);
895
+ this.ex = instance.exports;
896
+ }
897
+ /** The resident spring SoA input/output views, valid until the next capacity
898
+ * growth. Write gathered driver state here before calling {@link stepSprings}. */
899
+ springView() {
900
+ return this.sv;
901
+ }
902
+ /** The resident tween SoA input/output views, valid until the next capacity
903
+ * growth. Write gathered driver state here before calling {@link stepTweens}. */
904
+ tweenView() {
905
+ return this.tv;
906
+ }
907
+ /**
908
+ * Size (and grow, if needed) capacity for `springCount` springs and
909
+ * `tweenCount` tweens. Call this BEFORE writing into {@link springView}/
910
+ * {@link tweenView} — a capacity growth detaches the previous views, so
911
+ * writing first and sizing after would write into a stale buffer.
912
+ */
913
+ ensure(springCount, tweenCount) {
914
+ if (springCount + PAD4 <= this.springCap && tweenCount + PAD4 <= this.tweenCap) return;
915
+ this.springCap = springCount + PAD4;
916
+ this.tweenCap = tweenCount + PAD4;
917
+ this.ex.anim_init(this.springCap, this.tweenCap);
918
+ this.refreshViews();
919
+ }
920
+ /** Advance `count` springs (from index 0) by `dtMs` milliseconds, in place. */
921
+ stepSprings(dtMs, count) {
922
+ this.ex.spring_step(dtMs / 1e3, count);
923
+ }
924
+ /** Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`. */
925
+ stepTweens(dtMs, count) {
926
+ this.ex.tween_step(dtMs, count);
927
+ }
928
+ refreshViews() {
929
+ const buf = this.ex.memory.buffer;
930
+ const sCap = this.springCap;
931
+ const tCap = this.tweenCap;
932
+ this.sv = {
933
+ val: new Float64Array(buf, this.ex.p_s_val(), sCap),
934
+ target: new Float64Array(buf, this.ex.p_s_target(), sCap),
935
+ vel: new Float64Array(buf, this.ex.p_s_vel(), sCap),
936
+ stiff: new Float64Array(buf, this.ex.p_s_stiff(), sCap),
937
+ damp: new Float64Array(buf, this.ex.p_s_damp(), sCap),
938
+ mass: new Float64Array(buf, this.ex.p_s_mass(), sCap)
939
+ };
940
+ this.tv = {
941
+ from: new Float64Array(buf, this.ex.p_t_from(), tCap),
942
+ to: new Float64Array(buf, this.ex.p_t_to(), tCap),
943
+ elapsed: new Float64Array(buf, this.ex.p_t_elapsed(), tCap),
944
+ dur: new Float64Array(buf, this.ex.p_t_dur(), tCap),
945
+ delay: new Float64Array(buf, this.ex.p_t_delay(), tCap),
946
+ ease: new Float64Array(buf, this.ex.p_t_ease(), tCap),
947
+ val: new Float64Array(buf, this.ex.p_t_val(), tCap)
948
+ };
949
+ }
950
+ }, _class4);
951
+ async function instantiateAsync3(bytes) {
952
+ try {
953
+ const { instance } = await WebAssembly.instantiate(bytes, {});
954
+ return new AnimBackend(instance);
955
+ } catch (e9) {
956
+ return null;
957
+ }
958
+ }
959
+ async function instantiateStreaming3(source) {
960
+ try {
961
+ const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
962
+ if (typeof WebAssembly.instantiateStreaming === "function") {
963
+ const buffered = resp.clone();
964
+ try {
965
+ const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
966
+ return new AnimBackend(instance2);
967
+ } catch (e10) {
968
+ const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
969
+ return new AnimBackend(instance2);
970
+ }
971
+ }
972
+ const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
973
+ return new AnimBackend(instance);
974
+ } catch (e11) {
975
+ return null;
976
+ }
977
+ }
978
+
979
+ // src/wasm/particle-backend.ts
980
+ var PAD5 = 8;
981
+ var ParticleBackend = (_class5 = class {
982
+
983
+ __init19() {this.cap = 0}
984
+
985
+ constructor(instance) {;_class5.prototype.__init19.call(this);
986
+ this.ex = instance.exports;
987
+ }
988
+ /** The resident SoA views, valid until the next capacity growth. */
989
+ particleView() {
990
+ return this.view;
991
+ }
992
+ /**
993
+ * Size (and grow, if needed) capacity for `count` particles. Call BEFORE
994
+ * writing into {@link particleView} — a growth detaches the previous views.
995
+ */
996
+ ensure(count) {
997
+ if (count + PAD5 <= this.cap) return;
998
+ this.cap = count + PAD5;
999
+ this.ex.particle_init(this.cap);
1000
+ this.refreshViews();
1001
+ }
1002
+ /**
1003
+ * Advance `count` particles one step in place. Returns `true` when at least
1004
+ * one live particle is still moving or off-origin beyond epsilon (the fused
1005
+ * `hasPendingAnimations` flag), so the caller need not re-scan the buffer.
1006
+ */
1007
+ step(count, p) {
1008
+ const e = p.explosion;
1009
+ const flag = this.ex.particle_step(
1010
+ p.dt,
1011
+ p.mouseX,
1012
+ p.mouseY,
1013
+ p.width,
1014
+ p.height,
1015
+ p.springK,
1016
+ p.damping,
1017
+ p.bounceDamping,
1018
+ p.maxVelocity,
1019
+ e ? 1 : 0,
1020
+ e ? e.x : 0,
1021
+ e ? e.y : 0,
1022
+ e ? e.force : 0,
1023
+ count
1024
+ );
1025
+ return flag !== 0;
1026
+ }
1027
+ /** Transpose the AoS stride-8 buffer into the SoA views (position/velocity/
1028
+ * life every frame; origin upload-once when `withOrigin`). */
1029
+ gather(data, count, withOrigin) {
1030
+ const v = this.view;
1031
+ for (let i = 0; i < count; i++) {
1032
+ const o = i * PARTICLE_STRIDE_FLOATS;
1033
+ v.px[i] = data[o + PARTICLE_OFFSET_POSITION_X];
1034
+ v.py[i] = data[o + PARTICLE_OFFSET_POSITION_Y];
1035
+ v.vx[i] = data[o + PARTICLE_OFFSET_VELOCITY_X];
1036
+ v.vy[i] = data[o + PARTICLE_OFFSET_VELOCITY_Y];
1037
+ v.life[i] = data[o + PARTICLE_OFFSET_LIFE];
1038
+ if (withOrigin) {
1039
+ v.ox[i] = data[o + PARTICLE_OFFSET_ORIGIN_X];
1040
+ v.oy[i] = data[o + PARTICLE_OFFSET_ORIGIN_Y];
1041
+ }
1042
+ }
1043
+ }
1044
+ /** Scatter the mutated position/velocity/life back into the AoS buffer. */
1045
+ scatter(data, count) {
1046
+ const v = this.view;
1047
+ for (let i = 0; i < count; i++) {
1048
+ const o = i * PARTICLE_STRIDE_FLOATS;
1049
+ data[o + PARTICLE_OFFSET_POSITION_X] = v.px[i];
1050
+ data[o + PARTICLE_OFFSET_POSITION_Y] = v.py[i];
1051
+ data[o + PARTICLE_OFFSET_VELOCITY_X] = v.vx[i];
1052
+ data[o + PARTICLE_OFFSET_VELOCITY_Y] = v.vy[i];
1053
+ data[o + PARTICLE_OFFSET_LIFE] = v.life[i];
1054
+ }
1055
+ }
1056
+ refreshViews() {
1057
+ const buf = this.ex.memory.buffer;
1058
+ const c = this.cap;
1059
+ this.view = {
1060
+ px: new Float32Array(buf, this.ex.pp_px(), c),
1061
+ py: new Float32Array(buf, this.ex.pp_py(), c),
1062
+ vx: new Float32Array(buf, this.ex.pp_vx(), c),
1063
+ vy: new Float32Array(buf, this.ex.pp_vy(), c),
1064
+ ox: new Float32Array(buf, this.ex.pp_ox(), c),
1065
+ oy: new Float32Array(buf, this.ex.pp_oy(), c),
1066
+ life: new Float32Array(buf, this.ex.pp_life(), c)
1067
+ };
1068
+ }
1069
+ }, _class5);
1070
+ async function instantiateAsync4(bytes) {
1071
+ try {
1072
+ const { instance } = await WebAssembly.instantiate(bytes, {});
1073
+ return new ParticleBackend(instance);
1074
+ } catch (e12) {
1075
+ return null;
1076
+ }
1077
+ }
1078
+ async function instantiateStreaming4(source) {
1079
+ try {
1080
+ const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
1081
+ if (typeof WebAssembly.instantiateStreaming === "function") {
1082
+ const buffered = resp.clone();
1083
+ try {
1084
+ const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
1085
+ return new ParticleBackend(instance2);
1086
+ } catch (e13) {
1087
+ const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
1088
+ return new ParticleBackend(instance2);
1089
+ }
1090
+ }
1091
+ const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
1092
+ return new ParticleBackend(instance);
1093
+ } catch (e14) {
1094
+ return null;
1095
+ }
1096
+ }
1097
+
332
1098
  // src/tree/Scene.ts
1099
+ var _text = require('@vectojs/text'); _createStarExport(_text);
333
1100
  var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
334
1101
  "button",
335
1102
  "switch",
@@ -616,13 +1383,13 @@ function extendSelection(selection, anchor, focus) {
616
1383
  try {
617
1384
  selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
618
1385
  return;
619
- } catch (e3) {
1386
+ } catch (e15) {
620
1387
  }
621
1388
  try {
622
1389
  selection.collapse(anchor.node, anchor.offset);
623
1390
  selection.extend(focus.node, focus.offset);
624
1391
  return;
625
- } catch (e4) {
1392
+ } catch (e16) {
626
1393
  }
627
1394
  const anchorRange = document.createRange();
628
1395
  anchorRange.setStart(anchor.node, anchor.offset);
@@ -641,9 +1408,13 @@ function extendSelection(selection, anchor, focus) {
641
1408
  selection.removeAllRanges();
642
1409
  selection.addRange(range);
643
1410
  }
644
- var Scene = (_class2 = class _Scene {
1411
+ var Scene = (_class6 = class _Scene {
645
1412
  static __initStatic() {this.webglCreator = null}
646
1413
  static __initStatic2() {this.webgpuManagerClass = null}
1414
+ /** Upper bound (ms) on a single frame's `dt`. Caps the giant elapsed gap a
1415
+ * backgrounded/refocused tab produces so physics advances at most one slow
1416
+ * frame instead of the whole idle duration (~100ms ≈ 6 frames at 60fps). */
1417
+ static __initStatic3() {this.MAX_FRAME_DT = 100}
647
1418
  static registerWebGLPointRendererCreator(creator) {
648
1419
  _Scene.webglCreator = creator;
649
1420
  }
@@ -653,8 +1424,15 @@ var Scene = (_class2 = class _Scene {
653
1424
 
654
1425
 
655
1426
 
656
- __init7() {this.isRunning = false}
657
- __init8() {this.lastTime = 0}
1427
+ __init20() {this.isRunning = false}
1428
+ /** Whether the canvas is at least partially in the viewport. When it scrolls
1429
+ * fully off-screen the rAF loop pauses (stops rescheduling) instead of
1430
+ * burning frames on a scene nobody can see; an IntersectionObserver resumes
1431
+ * it on re-entry. Defaults true (and stays true where IntersectionObserver
1432
+ * is unavailable, e.g. SSR/jsdom, so behavior is unchanged there). */
1433
+ __init21() {this._canvasOnScreen = true}
1434
+ __init22() {this._canvasObserver = null}
1435
+ __init23() {this.lastTime = 0}
658
1436
 
659
1437
  /**
660
1438
  * Redraw strategy:
@@ -663,133 +1441,700 @@ var Scene = (_class2 = class _Scene {
663
1441
  * {@link markDirty}) or while an animation is pending. Ideal for static /
664
1442
  * event-driven UIs where idle frames should cost ~0.
665
1443
  */
666
- __init9() {this.renderMode = "always"}
667
- __init10() {this.dirty = true}
1444
+ __init24() {this.renderMode = "always"}
1445
+ __init25() {this.dirty = true}
668
1446
  /** Whether to throttle rendering to 2 FPS when the scene is static to save power. */
669
- __init11() {this.autoThrottle = true}
1447
+ __init26() {this.autoThrottle = true}
670
1448
  // --- Frame telemetry (read via `frameStats`) ---------------------------
671
1449
  /** Wall-clock ms spent inside the last `render()` call. */
672
- __init12() {this._lastFrameMs = 0}
1450
+ __init27() {this._lastFrameMs = 0}
673
1451
  /** Rolling exponential average of rendered-frame intervals, in ms. */
674
- __init13() {this._avgFrameIntervalMs = 0}
1452
+ __init28() {this._avgFrameIntervalMs = 0}
675
1453
  /** dt (ms) handed to the last rendered frame. */
676
- __init14() {this._lastDt = 0}
1454
+ __init29() {this._lastDt = 0}
677
1455
  /** Count of frames actually rendered since the loop started. */
678
- __init15() {this._renderedFrames = 0}
1456
+ __init30() {this._renderedFrames = 0}
679
1457
  /** Count of rAF ticks skipped (idle / capped) since the loop started. */
680
- __init16() {this._skippedFrames = 0}
1458
+ __init31() {this._skippedFrames = 0}
681
1459
  /** `time` of the previous *rendered* frame, for interval measurement. */
682
- __init17() {this._lastRenderTick = 0}
1460
+ __init32() {this._lastRenderTick = 0}
683
1461
  /**
684
1462
  * Frame-rate cap (power saving). `0` = uncapped (native refresh). When set,
685
1463
  * the loop renders at most `maxFPS` times per second; animations still run,
686
1464
  * just less often. See {@link SceneOptions.maxFPS}.
687
1465
  */
688
- __init18() {this.maxFPS = 60}
1466
+ __init33() {this.maxFPS = 60}
689
1467
  /** Whether the OS prefers-reduced-motion setting auto-caps the loop. */
690
- __init19() {this.respectReducedMotion = true}
1468
+ __init34() {this.respectReducedMotion = true}
1469
+ /**
1470
+ * Reading direction for accessibility tab/traversal order (`'ltr'` default,
1471
+ * `'rtl'`). Controls the inline sort within a visual row in
1472
+ * {@link enforceA11yDomOrder}. Set at runtime to re-flow tab order on the
1473
+ * next sync (also trips a reorder).
1474
+ */
1475
+ get readingDirection() {
1476
+ return this._readingDirection;
1477
+ }
1478
+ set readingDirection(dir) {
1479
+ if (dir !== this._readingDirection) {
1480
+ this._readingDirection = dir;
1481
+ this.a11yNeedsReorder = true;
1482
+ }
1483
+ }
1484
+ __init35() {this._readingDirection = "ltr"}
691
1485
  /** Cached media-query list; `.matches` is read live each frame. */
692
- __init20() {this.reducedMotionQuery = null}
1486
+ __init36() {this.reducedMotionQuery = null}
1487
+ /** Cached `(forced-colors: active)` query (Windows High Contrast etc.). A
1488
+ * canvas gets NO automatic forced-colors treatment from the browser (it's
1489
+ * opaque pixels), so components must read {@link forcedColors} and repaint
1490
+ * with system colors themselves; a change listener repaints idle scenes. */
1491
+ __init37() {this.forcedColorsQuery = null}
1492
+ __init38() {this.forcedColorsChangeHandler = null}
693
1493
  /** True when the OS asks for reduced motion and we respect it. Read by the animation drivers. */
694
1494
  get prefersReducedMotion() {
695
1495
  return this.respectReducedMotion && !!_optionalChain([this, 'access', _21 => _21.reducedMotionQuery, 'optionalAccess', _22 => _22.matches]);
696
1496
  }
1497
+ /**
1498
+ * True when the OS is in a forced-colors mode (Windows High Contrast, and the
1499
+ * `forced-colors: active` media feature generally). Canvas pixels are exempt
1500
+ * from the browser's forced-colors remapping, so accessible components should
1501
+ * read this and draw with CSS system colors (`CanvasText`, `Canvas`,
1502
+ * `Highlight`, …) instead of their themed palette. Re-rendered automatically
1503
+ * when the setting toggles.
1504
+ */
1505
+ get forcedColors() {
1506
+ return !!_optionalChain([this, 'access', _23 => _23.forcedColorsQuery, 'optionalAccess', _24 => _24.matches]);
1507
+ }
697
1508
  /**
698
1509
  * Throttle interval (ms) for the a11y/automation shadow sync. `0` = every
699
1510
  * frame. See {@link SceneOptions.a11ySyncInterval}.
700
1511
  */
701
- __init21() {this.a11ySyncInterval = 0}
1512
+ __init39() {this.a11ySyncInterval = 0}
702
1513
  /** Timestamp of the last a11y sync, for throttling. */
703
- __init22() {this.lastA11ySync = -Infinity}
1514
+ __init40() {this.lastA11ySync = -Infinity}
704
1515
  /** True if we skipped an a11y sync during animation and need to sync when at rest. */
705
- __init23() {this.a11yPendingSyncAfterAnimation = false}
1516
+ __init41() {this.a11yPendingSyncAfterAnimation = false}
706
1517
  // A11y / Automation Layer. `null` in non-DOM (SSR/Node) environments — the
707
1518
  // whole projection degrades to a no-op so the engine's logic stays usable
708
1519
  // server-side (e.g. headless layout / vector export) without jsdom.
709
1520
 
710
- __init24() {this.a11yElements = /* @__PURE__ */ new Map()}
1521
+ __init42() {this.a11yElements = /* @__PURE__ */ new Map()}
711
1522
  /** DOM nodes mirroring static text content, keyed by entity id. */
712
- __init25() {this.contentElements = /* @__PURE__ */ new Map()}
1523
+ __init43() {this.contentElements = /* @__PURE__ */ new Map()}
713
1524
  /** Pending cold font-calibration frame per projected grid entity. */
714
- __init26() {this.contentGridCalibrationFrames = /* @__PURE__ */ new Map()}
1525
+ __init44() {this.contentGridCalibrationFrames = /* @__PURE__ */ new Map()}
715
1526
  /** Detached, untransformed font probes used by the cold calibration pass. */
716
- __init27() {this.contentGridCalibrationProbes = /* @__PURE__ */ new Map()}
1527
+ __init45() {this.contentGridCalibrationProbes = /* @__PURE__ */ new Map()}
717
1528
  /** Invalidates grid font calibration after browser font availability changes. */
718
- __init28() {this.contentFontEpoch = 0}
1529
+ __init46() {this.contentFontEpoch = 0}
719
1530
  /** Cached Canvas-to-client scale for the current font/viewport epoch. */
720
- __init29() {this.contentMetricScaleEpoch = -1}
721
- __init30() {this.contentMetricScaleX = 1}
722
- __init31() {this.contentProjectionEnabled = true}
1531
+ __init47() {this.contentMetricScaleEpoch = -1}
1532
+ __init48() {this.contentMetricScaleX = 1}
1533
+ __init49() {this.contentProjectionEnabled = true}
723
1534
  // Virtualization margin (px) for content projection; `undefined` → one
724
1535
  // viewport height, resolved at sync time. `Infinity` = materialize everything.
725
- __init32() {this.contentProjectionMargin = void 0}
1536
+ __init50() {this.contentProjectionMargin = void 0}
726
1537
  /**
727
1538
  * True while a text-selection drag that started on a projection's blank
728
1539
  * region (no text node under the press) is being driven manually — the
729
1540
  * browser has no native anchor for it, so mousemove extends the Selection
730
1541
  * from the position we resolved ourselves.
731
1542
  */
732
- __init33() {this.blankRegionSelectionDrag = false}
733
- __init34() {this.contentSelectionAnchor = null}
734
- __init35() {this.contentSelectionEndListener = null}
1543
+ __init51() {this.blankRegionSelectionDrag = false}
1544
+ __init52() {this.contentSelectionAnchor = null}
1545
+ __init53() {this.contentSelectionEndListener = null}
735
1546
  // Animation/interactive flags collected during the render walk (tree-walk
736
1547
  // fusion): the loop reads last frame's answers instead of re-walking the
737
1548
  // tree up to 4× per tick. Start true so the first tick stays conservative.
738
- __init36() {this.frameHadAnimation = true}
739
- __init37() {this.frameHadInteractive = true}
740
-
741
- __init38() {this.focusedA11yElement = null}
742
- __init39() {this.caretBlinkTimer = null}
743
- __init40() {this.a11yNeedsReorder = true}
744
- __init41() {this.portalRoot = null}
745
- __init42() {this.fullViewportElements = []}
746
- __init43() {this.normalElements = []}
747
- __init44() {this.activeIds = /* @__PURE__ */ new Set()}
748
- __init45() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
749
- __init46() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
750
- __init47() {this.portalEntities = /* @__PURE__ */ new Map()}
751
- __init48() {this.renderOrderCounter = 0}
1549
+ __init54() {this.frameHadAnimation = true}
1550
+ __init55() {this.frameHadInteractive = true}
1551
+
1552
+ /** Active `(resolution: Ndppx)` media query watching for a runtime DPR change
1553
+ * (window moved between monitors, browser zoom) so the canvas backing store
1554
+ * can be re-scaled — otherwise it stays rasterized at the old DPR and blurs.
1555
+ * A resolution media query only fires when leaving its exact value, so the
1556
+ * handler re-arms a fresh query for the new DPR each time. */
1557
+ __init56() {this.dprMediaQuery = null}
1558
+ /** For embedded (`disableWindowResize`) scenes: observes the canvas element so
1559
+ * a CSS/layout-driven size change re-runs `resize()`. A window `resize`
1560
+ * listener never fires for these (the window isn't what changed), so without
1561
+ * this an embedded canvas stayed at its initial size forever. */
1562
+ __init57() {this.canvasResizeObserver = null}
1563
+ __init58() {this.dprChangeHandler = null}
1564
+ __init59() {this.focusedA11yElement = null}
1565
+ /** Last geometry `syncOverlayGeometry` wrote, so an unchanged frame can skip the
1566
+ * style writes entirely. Reset to `null` to force the next sync (a new overlay
1567
+ * layer was created and has never been positioned). */
1568
+ __init60() {this._overlayGeometry = null}
1569
+ /** Shadow elements the pointer is currently inside. Lets a removal that happens
1570
+ * mid-hover synthesize the `pointerleave` the browser never sends for a
1571
+ * detached element, so the entity doesn't keep its hover state. */
1572
+ __init61() {this.hoveredA11yElements = /* @__PURE__ */ new WeakSet()}
1573
+ /** Persistent tabindex=-1 element in a11yRoot. When the focused a11y mirror is
1574
+ * pruned (virtualization/streaming/removal) while it holds focus, we move
1575
+ * focus here instead of letting the browser drop it to <body> — keeping the
1576
+ * screen-reader virtual cursor inside the scene's a11y region. */
1577
+ __init62() {this.focusSentinel = null}
1578
+ __init63() {this.caretBlinkTimer = null}
1579
+ __init64() {this.a11yNeedsReorder = true}
1580
+ __init65() {this.portalRoot = null}
1581
+ __init66() {this.fullViewportElements = []}
1582
+ __init67() {this.normalElements = []}
1583
+ __init68() {this.activeIds = /* @__PURE__ */ new Set()}
1584
+ __init69() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
1585
+ __init70() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
1586
+ __init71() {this.portalEntities = /* @__PURE__ */ new Map()}
1587
+ __init72() {this.renderOrderCounter = 0}
1588
+ /**
1589
+ * Monotonic render-frame counter, bumped once per authoritative `render()`
1590
+ * pass. Entities stamp their per-frame world-matrix cache with this value and
1591
+ * {@link Entity.getWorldTransform} trusts that cache only while it still
1592
+ * matches, so a query outside the frame that produced it transparently falls
1593
+ * back to the ancestor walk. Public for the same reason `Entity._getTrig`/
1594
+ * `_setWorldCache` are: it is a cross-class render-internal contract.
1595
+ */
1596
+ __init73() {this.currentFrame = 0}
1597
+ // ── WASM transform backend (invisible accelerator) ──────────────────────────
1598
+ // When `_transformBackend === 'wasm'`, the main render walk sources each
1599
+ // entity's world matrix from an SoA store composed by `_wasm` (see
1600
+ // `renderNode`), instead of composing it in JS. The JS path is the permanent
1601
+ // fallback and the default: a null backend, a non-main renderer, or any entity
1602
+ // absent from the store all fall back to the JS composition, so WASM can only
1603
+ // ever change *how fast* a world matrix is produced, never *what* it is.
1604
+ __init74() {this._wasm = null}
1605
+ __init75() {this._transformBackend = "js"}
1606
+ // Resident store state (Stage 3). The store layout — slot assignment + sibling
1607
+ // runs — depends only on tree TOPOLOGY, so it is rebuilt only when the
1608
+ // structure changes (add/remove/reparent bump `_structureVersion`). Between
1609
+ // rebuilds the per-frame cost is: gather each entity's transform into the
1610
+ // resident wasm input view + run the kernel — no reallocation, no readback.
1611
+ __init76() {this._treeStore = null}
1612
+ __init77() {this._slotEntity = []}
1613
+ // store slot -> entity (also validates slots)
1614
+ __init78() {this._wasmInputs = null}
1615
+ __init79() {this._wasmWorld = null}
1616
+ __init80() {this._structureVersion = 0}
1617
+ __init81() {this._storeStructureVersion = -1}
1618
+ // Cached list of ComputeParticleEntity instances in the tree, keyed by the
1619
+ // structure version it was gathered at. Rebuilt only on a topology change.
1620
+ __init82() {this._computeEntities = []}
1621
+ __init83() {this._computeEntitiesVersion = -1}
1622
+ /** Invalidate the resident WASM store layout; the next wasm-mode frame rebuilds
1623
+ * it. Called by `Entity.add`/`remove` (topology changes only). */
1624
+ markStructureChanged() {
1625
+ this._structureVersion++;
1626
+ }
1627
+ /** The tree's ComputeParticleEntity instances, cached per structure version so
1628
+ * a compute-free scene doesn't re-walk the whole tree every frame. */
1629
+ _computeEntitiesFor(version) {
1630
+ if (this._computeEntitiesVersion === version) return this._computeEntities;
1631
+ const list = [];
1632
+ const collect = (node) => {
1633
+ if (node instanceof ComputeParticleEntity) list.push(node);
1634
+ for (const child of node.children) collect(child);
1635
+ };
1636
+ collect(this.root);
1637
+ for (const overlay of this.overlayRoot.children) collect(overlay);
1638
+ this._computeEntities = list;
1639
+ this._computeEntitiesVersion = version;
1640
+ return list;
1641
+ }
1642
+ /** Which backend composes world matrices for the main render walk. */
1643
+ get transformBackend() {
1644
+ return this._transformBackend;
1645
+ }
1646
+ /**
1647
+ * Install (or clear) a WASM transform backend. Passing a backend switches the
1648
+ * main render walk onto it; passing `null` reverts to the JS path. Synchronous
1649
+ * and safe to call between frames — the next `render()` picks it up. Prefer
1650
+ * {@link enableWasmTransforms} for the normal async hot-swap.
1651
+ */
1652
+ setTransformBackend(backend) {
1653
+ this._wasm = backend;
1654
+ this._transformBackend = backend ? "wasm" : "js";
1655
+ }
1656
+ /**
1657
+ * Asynchronously instantiate the WASM transform core and, on success, hot-swap
1658
+ * the render walk onto it. Accepts whatever is convenient at the call site:
1659
+ *
1660
+ * ```ts
1661
+ * // The common case — a bundler-emitted, co-located asset URL:
1662
+ * await scene.enableWasmTransforms(new URL('./vectojs_core.wasm', import.meta.url));
1663
+ * // …or a path string, a Response, or raw bytes you already have:
1664
+ * await scene.enableWasmTransforms('/assets/vectojs_core.wasm');
1665
+ * await scene.enableWasmTransforms(await fetch(url));
1666
+ * await scene.enableWasmTransforms(myUint8Array);
1667
+ * ```
1668
+ *
1669
+ * A URL/Response streams (compiles while it downloads, with a buffered
1670
+ * fallback for a wrong MIME type); raw bytes instantiate directly. The Scene
1671
+ * keeps rendering on the JS path until this resolves, and stays on JS if
1672
+ * instantiation fails (CSP `wasm-unsafe-eval`, unsupported SIMD, corrupt or
1673
+ * missing bytes, a 404) — failure is the default state, not an error path.
1674
+ * Resolves `true` if WASM is now active, `false` if the JS path remains.
1675
+ */
1676
+ async enableWasmTransforms(source) {
1677
+ const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
1678
+ const backend = isBytes ? await instantiateAsync(source) : await instantiateStreaming(source);
1679
+ if (!backend) return false;
1680
+ this.setTransformBackend(backend);
1681
+ return true;
1682
+ }
1683
+ // ── WASM hit-test backend (invisible accelerator, G3) ───────────────────────
1684
+ // A separate WASM module instance from the transform backend (each crate
1685
+ // export lives in independent linear memory per instance, so there is no
1686
+ // shared-state hazard in running both) that indexes the main tree's world
1687
+ // AABBs into a dense viewport grid for findEntityAt. The JS depth-first walk
1688
+ // (findHitRecursively) is the permanent fallback: a null backend, a build
1689
+ // that overflows its item budget, or the overlay tree (never indexed — small
1690
+ // and rare, not worth accelerating) all fall through to it, so WASM can only
1691
+ // ever change *how fast* a hit is found, never *which* entity is returned —
1692
+ // every grid candidate is re-confirmed against its own precise
1693
+ // isPointInside before being trusted (see hit-store.ts / hit-backend.ts).
1694
+ __init84() {this._hitWasm = null}
1695
+ // Cache key: which frame + structure version the grid was last (successfully,
1696
+ // non-overflowing) built for. findEntityAt is called ad-hoc (pointer
1697
+ // hover/click), not every frame, so the grid is refreshed lazily on demand
1698
+ // rather than proactively every render() — unlike the transform store, which
1699
+ // every frame's draw depends on.
1700
+ __init85() {this._hitGridFrame = -1}
1701
+ __init86() {this._hitGridOk = false}
1702
+ __init87() {this._hitSlotEntity = []}
1703
+ __init88() {this._hitBoundless = []}
1704
+ /** Which backend answers `findEntityAt` for the main tree. */
1705
+ get hitTestBackend() {
1706
+ return this._hitWasm ? "wasm" : "js";
1707
+ }
1708
+ /** Install (or clear) a WASM hit-test backend directly. Prefer
1709
+ * {@link enableWasmHitTest} for the normal async hot-swap. */
1710
+ setHitTestBackend(backend) {
1711
+ this._hitWasm = backend;
1712
+ this._hitGridFrame = -1;
1713
+ }
1714
+ /**
1715
+ * Asynchronously instantiate the WASM hit-test core and, on success, hot-swap
1716
+ * `findEntityAt` onto it. Accepts the same source shapes as
1717
+ * {@link enableWasmTransforms} (URL, path string, Response, or raw bytes).
1718
+ * Stays on the JS walk if instantiation fails — failure is the default
1719
+ * state, not an error path. Resolves `true` if WASM is now active.
1720
+ */
1721
+ async enableWasmHitTest(source) {
1722
+ const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
1723
+ const backend = isBytes ? await instantiateAsync2(source) : await instantiateStreaming2(source);
1724
+ if (!backend) return false;
1725
+ this.setHitTestBackend(backend);
1726
+ return true;
1727
+ }
1728
+ /**
1729
+ * Refresh the hit-test grid for the CURRENT tree state if it is stale (a
1730
+ * structural or transform change may have happened since the last build —
1731
+ * there is no cheap "nothing moved" shortcut for a spatial index the way
1732
+ * there is for the transform store's topology-only run table, since ANY
1733
+ * entity moving invalidates its AABB, not just add/remove/reparent; the
1734
+ * measured build cost is cheap enough to redo per call). Returns `false`
1735
+ * (grid untrustworthy — caller must use the JS walk) when there is no
1736
+ * backend or the build overflowed its item budget.
1737
+ */
1738
+ _ensureHitGrid() {
1739
+ const backend = this._hitWasm;
1740
+ if (!backend) return false;
1741
+ if (this._hitGridFrame === this.currentFrame) return this._hitGridOk;
1742
+ const gathered = gatherHitAABBs(this.root, this.currentFrame);
1743
+ backend.ensure(gathered.count, this.width, this.height, 64);
1744
+ const view = backend.inputView();
1745
+ view.minx.set(gathered.minx.subarray(0, gathered.count));
1746
+ view.miny.set(gathered.miny.subarray(0, gathered.count));
1747
+ view.maxx.set(gathered.maxx.subarray(0, gathered.count));
1748
+ view.maxy.set(gathered.maxy.subarray(0, gathered.count));
1749
+ const ok = backend.runBuild(gathered.count, this.width, this.height, 64);
1750
+ this._hitSlotEntity = gathered.slotEntity;
1751
+ this._hitBoundless = gathered.boundless;
1752
+ this._hitGridFrame = this.currentFrame;
1753
+ this._hitGridOk = ok;
1754
+ return ok;
1755
+ }
1756
+ /**
1757
+ * `findEntityAt`'s WASM-accelerated path for the main tree. Scans only the
1758
+ * queried cell's candidates (confirming each against its own AABB and precise
1759
+ * `isPointInside`) merged against the (typically empty or tiny) list of
1760
+ * entities with no `getBounds()`, taking whichever confirmed match has the
1761
+ * higher pre-order index — see hit-store.ts for why that is exactly
1762
+ * equivalent to findHitRecursively's topmost-hit priority. Always
1763
+ * conclusive: returns the correct entity or `null`, never "inconclusive".
1764
+ */
1765
+ _findEntityAtWasm(x, y) {
1766
+ const backend = this._hitWasm;
1767
+ const { minx, miny, maxx, maxy } = backend.inputView();
1768
+ let bestIndex = -1;
1769
+ let bestEntity = null;
1770
+ const cell = backend.candidatesAt(x, y);
1771
+ if (cell) {
1772
+ for (let k = cell.length - 1; k >= 0; k--) {
1773
+ const idx = cell[k];
1774
+ if (x < minx[idx] || x > maxx[idx] || y < miny[idx] || y > maxy[idx]) continue;
1775
+ const entity = this._hitSlotEntity[idx];
1776
+ if (_optionalChain([entity, 'optionalAccess', _25 => _25.isPointInside, 'call', _26 => _26(x, y)]) && this.isHitEligible(entity, x, y)) {
1777
+ bestIndex = idx;
1778
+ bestEntity = entity;
1779
+ break;
1780
+ }
1781
+ }
1782
+ }
1783
+ for (const { entity, index } of this._hitBoundless) {
1784
+ if (index > bestIndex && entity.isPointInside(x, y) && this.isHitEligible(entity, x, y)) {
1785
+ bestIndex = index;
1786
+ bestEntity = entity;
1787
+ }
1788
+ }
1789
+ return bestEntity;
1790
+ }
1791
+ // ── WASM batched-animation backend (invisible accelerator, G2) ──────────────
1792
+ // Advances every currently-active SpringDriver/TweenDriver in one WASM call
1793
+ // each (spring_step/tween_step) instead of Entity.tickDrivers()'s per-driver
1794
+ // JS loop. The JS tick loop is the permanent fallback: a null backend, a
1795
+ // driver count below `animDriverGateCount`, or a TweenDriver using a custom
1796
+ // EasingFn (which cannot cross into WASM) all fall through to it — WASM can
1797
+ // only ever change *how* a driver is advanced, never *what* value it lands
1798
+ // on.
1799
+ __init89() {this._animWasm = null}
1800
+ // Entities with at least one active driver, added by Entity._spawnDriver.
1801
+ // Self-pruning: _tickBatchedDrivers drops an entry the first time it visits
1802
+ // an entity whose drivers have since all completed or been removed. This is
1803
+ // what lets the batch pass find its candidates in O(active drivers), not
1804
+ // O(tree size) — the exact mistake G3's first integrated benchmark made.
1805
+ __init90() {this._activeDriverEntities = /* @__PURE__ */ new Set()}
1806
+ // Reused across frames instead of allocating a fresh array + N {entity,prop,
1807
+ // driver} objects every call — the integrated benchmark
1808
+ // (benchmarks/anim-wasm-scene) found that allocation churn was the
1809
+ // dominant integrated cost, not the wasm kernel itself. Parallel arrays,
1810
+ // truncated to the live count after each use so a stale tail slot never
1811
+ // pins a no-longer-active entity/driver in memory.
1812
+ __init91() {this._springEntities = []}
1813
+ __init92() {this._springProps = []}
1814
+ __init93() {this._springDrivers = []}
1815
+ __init94() {this._tweenEntities = []}
1816
+ __init95() {this._tweenProps = []}
1817
+ __init96() {this._tweenDrivers = []}
1818
+ /**
1819
+ * Minimum number of batchable (spring, or named-easing tween) active drivers
1820
+ * before a frame engages the WASM batch path at all; below it, every driver
1821
+ * ticks on the normal JS per-entity path, unmodified.
1822
+ *
1823
+ * Re-measured on the INTEGRATED path (benchmarks/anim-wasm-scene, real
1824
+ * Chrome 150 / Firefox 153, 2026-07-24 — correctness verified 0 mismatches
1825
+ * across all three kinds before any of these numbers were trusted): the
1826
+ * isolated kernel spike's "<100 drivers, wins everywhere" verdict did NOT
1827
+ * survive integration, and neither did the first integrated pass's single
1828
+ * gate-count verdict once broken out by driver kind. On Chrome, spring and
1829
+ * mixed drivers are a real ~1.4–2.3× win from n=128 up through the tested
1830
+ * ceiling of 16384, but pure-tween drivers are a LOSS at n=128 (0.71×,
1831
+ * i.e. ~40% slower than the JS path) and only turn net-positive around
1832
+ * n≈256 (1.52×). A single scalar gate can't be tight for spring/mixed
1833
+ * without occasionally opening early on a tween-heavy scene and making it
1834
+ * slower — 256 is chosen to keep the gate net-positive across all three
1835
+ * kinds rather than optimal for any one of them; a kind-aware gate (see
1836
+ * `_tickBatchedDrivers`'s per-kind arrays, which already separate spring
1837
+ * from tween) would recover the 128–255 spring/mixed win without the
1838
+ * tween regression, but that's a larger change than this measurement pass
1839
+ * covers. On Firefox it is a net loss at every driver count measured, up
1840
+ * to 16384 — not an allocation artifact (confirmed after removing all
1841
+ * per-frame allocation from the gather/scatter path); SpiderMonkey's
1842
+ * wasm-boundary/property-dispatch cost for this shape of call appears to
1843
+ * structurally exceed the saving, at least at the scales tested here.
1844
+ *
1845
+ * 256 is set as a Chrome-oriented default so an app that opts in (this
1846
+ * path is never engaged without an explicit {@link enableWasmAnimBatching}
1847
+ * call) sees the gate open only where it reliably helps on Chromium,
1848
+ * regardless of whether the scene's active drivers are spring, tween, or
1849
+ * a mix of both. Unlike G1 (safe to default on everywhere) and G3 (opt-in,
1850
+ * but a reliable win once its own gate condition holds), G2 has no
1851
+ * threshold that is safe on every engine — raise or lower this per your
1852
+ * own target browser mix and driver-kind distribution, or leave WASM
1853
+ * animation batching disabled entirely on a Firefox-heavy audience.
1854
+ */
1855
+ __init97() {this.animDriverGateCount = 256}
1856
+ /** Which backend advances active property drivers on the current gate
1857
+ * decision. Reflects only whether a backend is installed — the per-frame
1858
+ * gate can still choose the JS path even when this reads `'wasm'`. */
1859
+ get animBackend() {
1860
+ return this._animWasm ? "wasm" : "js";
1861
+ }
1862
+ /** Install (or clear) a WASM batched-animation backend directly. Prefer
1863
+ * {@link enableWasmAnimBatching} for the normal async hot-swap. */
1864
+ setAnimBackend(backend) {
1865
+ this._animWasm = backend;
1866
+ }
1867
+ /**
1868
+ * Asynchronously instantiate the WASM batched-animation core and, on
1869
+ * success, make it available to the per-frame gate (see
1870
+ * {@link animDriverGateCount}). Accepts the same source shapes as
1871
+ * {@link enableWasmTransforms}. Stays on the JS tick loop if instantiation
1872
+ * fails — failure is the default state, not an error path. Resolves `true`
1873
+ * if WASM is now available (not necessarily active every frame).
1874
+ */
1875
+ async enableWasmAnimBatching(source) {
1876
+ const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
1877
+ const backend = isBytes ? await instantiateAsync3(source) : await instantiateStreaming3(source);
1878
+ if (!backend) return false;
1879
+ this.setAnimBackend(backend);
1880
+ return true;
1881
+ }
1882
+ // ── WASM particle CPU-sim backend (invisible accelerator, G4) ───────────────
1883
+ // Advances a ComputeParticleEntity's whole buffer in one `particle_step` call
1884
+ // (spring/mouse/explosion/integrate/bounce/life), replacing the per-particle
1885
+ // JS `updateCPU` loop on the GPU-less fallback path. Measured ~2.1-2.5x on
1886
+ // Chrome and ~1.4-2.0x on Firefox including the per-frame AoS<->SoA transpose
1887
+ // (benchmarks/particle-wasm). f32 (matches the WGSL shader), bit-identical to
1888
+ // a JS f32 reference oracle; updateCPU (f64) stays the permanent fallback when
1889
+ // no backend is installed or a scene runs on WebGPU.
1890
+ __init98() {this._particleWasm = null}
1891
+ /** Which backend runs the CPU particle simulation. Reflects only whether a
1892
+ * backend is installed (the WebGPU compute path, when active, is used first
1893
+ * regardless). */
1894
+ get particleSimBackend() {
1895
+ return this._particleWasm ? "wasm" : "js";
1896
+ }
1897
+ /** Install (or clear) a WASM particle backend directly. Prefer
1898
+ * {@link enableWasmParticles} for the normal async hot-swap. */
1899
+ setParticleBackend(backend) {
1900
+ this._particleWasm = backend;
1901
+ }
1902
+ /**
1903
+ * Asynchronously instantiate the WASM particle core and, on success, use it
1904
+ * for the CPU particle fallback. Accepts the same source shapes as
1905
+ * {@link enableWasmTransforms}. Stays on the JS `updateCPU` path if
1906
+ * instantiation fails — failure is the default state, not an error path.
1907
+ * Resolves `true` if WASM is now active.
1908
+ */
1909
+ async enableWasmParticles(source) {
1910
+ const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
1911
+ const backend = isBytes ? await instantiateAsync4(source) : await instantiateStreaming4(source);
1912
+ if (!backend) return false;
1913
+ this.setParticleBackend(backend);
1914
+ return true;
1915
+ }
1916
+ /** Internal: called by `Entity._spawnDriver` when a new property driver
1917
+ * starts. See {@link _activeDriverEntities}. */
1918
+ _registerActiveDriverEntity(entity) {
1919
+ this._activeDriverEntities.add(entity);
1920
+ }
1921
+ /**
1922
+ * Drop `entity` and its whole subtree from the batched-driver candidate set.
1923
+ * Called by {@link remove}/{@link hideOverlay} on detach: without this a
1924
+ * removed-but-still-animating entity stays pinned in the Set (a leak) and its
1925
+ * drivers keep ticking every frame even though it is off-tree. If it is later
1926
+ * re-added, {@link registerActiveDriverSubtree} re-registers any node that
1927
+ * still has live drivers, so the motion resumes.
1928
+ */
1929
+ unregisterActiveDriverSubtree(entity) {
1930
+ if (this._activeDriverEntities.size === 0) return;
1931
+ const stack = [entity];
1932
+ while (stack.length > 0) {
1933
+ const node = stack.pop();
1934
+ this._activeDriverEntities.delete(node);
1935
+ for (const child of node.children) stack.push(child);
1936
+ }
1937
+ }
1938
+ /**
1939
+ * Re-register every node in `entity`'s subtree that still has live property
1940
+ * drivers. Called by {@link add}/{@link showOverlay} so re-attaching a subtree
1941
+ * that was removed mid-animation resumes its batched drivers (they were
1942
+ * dropped from the candidate set on removal, but the driver state still lives
1943
+ * on each entity).
1944
+ */
1945
+ registerActiveDriverSubtree(entity) {
1946
+ const stack = [entity];
1947
+ while (stack.length > 0) {
1948
+ const node = stack.pop();
1949
+ const entries = node._driverEntries();
1950
+ if (entries && entries.size > 0) this._activeDriverEntities.add(node);
1951
+ for (const child of node.children) stack.push(child);
1952
+ }
1953
+ }
1954
+ /**
1955
+ * Advance every registered entity's active drivers for this frame, batching
1956
+ * whichever are batchable (`SpringDriver`; `TweenDriver` with a named
1957
+ * easing) through one WASM call each when the driver-count gate is open, and
1958
+ * ticking the rest (a `TweenDriver` using a custom `EasingFn`) directly in
1959
+ * JS regardless of the gate. A "claimed" entity must have ALL its drivers
1960
+ * advanced here so it can be safely stamped `_driversTickedFrame` — leaving
1961
+ * one unclaimed would silently stall it, since `tickDrivers()` skips the
1962
+ * whole entity once stamped.
1963
+ *
1964
+ * Must run before ANY entity's `update()`/`tickDrivers()` this frame (see
1965
+ * the call site in {@link render}) — the same ordering constraint G1 Stage 4
1966
+ * discovered: a value this pass writes must be final before anything reads
1967
+ * it, including the JS-mode interleaved walk and the WASM-mode transform
1968
+ * pre-pass.
1969
+ */
1970
+ _tickBatchedDrivers(dt) {
1971
+ if (this._activeDriverEntities.size === 0) return;
1972
+ let batchable = 0;
1973
+ for (const entity of this._activeDriverEntities) {
1974
+ const entries = entity._driverEntries();
1975
+ if (!entries || entries.size === 0) {
1976
+ this._activeDriverEntities.delete(entity);
1977
+ continue;
1978
+ }
1979
+ for (const driver of entries.values()) {
1980
+ if (driver instanceof _animation.SpringDriver) batchable++;
1981
+ else if (driver instanceof _animation.TweenDriver && driver.wasmEasingId !== null) batchable++;
1982
+ }
1983
+ }
1984
+ const backend = this._animWasm;
1985
+ if (!backend || batchable < this.animDriverGateCount) return;
1986
+ const sE = this._springEntities;
1987
+ const sP = this._springProps;
1988
+ const sD = this._springDrivers;
1989
+ const tE = this._tweenEntities;
1990
+ const tP = this._tweenProps;
1991
+ const tD = this._tweenDrivers;
1992
+ let springCount = 0;
1993
+ let tweenCount = 0;
1994
+ for (const entity of this._activeDriverEntities) {
1995
+ const entries = entity._driverEntries();
1996
+ for (const [prop, driver] of entries) {
1997
+ if (driver instanceof _animation.SpringDriver) {
1998
+ sE[springCount] = entity;
1999
+ sP[springCount] = prop;
2000
+ sD[springCount] = driver;
2001
+ springCount++;
2002
+ } else if (driver instanceof _animation.TweenDriver && driver.wasmEasingId !== null) {
2003
+ tE[tweenCount] = entity;
2004
+ tP[tweenCount] = prop;
2005
+ tD[tweenCount] = driver;
2006
+ tweenCount++;
2007
+ } else {
2008
+ driver.tick(dt);
2009
+ entity._applyDriverTick(prop, driver);
2010
+ }
2011
+ }
2012
+ entity._driversTickedFrame = this.currentFrame;
2013
+ }
2014
+ sE.length = springCount;
2015
+ sP.length = springCount;
2016
+ sD.length = springCount;
2017
+ tE.length = tweenCount;
2018
+ tP.length = tweenCount;
2019
+ tD.length = tweenCount;
2020
+ backend.ensure(springCount, tweenCount);
2021
+ if (springCount > 0) {
2022
+ const sv = backend.springView();
2023
+ for (let i = 0; i < springCount; i++) {
2024
+ const phys = sD[i].physics;
2025
+ sv.val[i] = phys.value;
2026
+ sv.target[i] = phys.target;
2027
+ sv.vel[i] = phys.velocity;
2028
+ sv.stiff[i] = phys.stiffness;
2029
+ sv.damp[i] = phys.damping;
2030
+ sv.mass[i] = phys.mass;
2031
+ }
2032
+ backend.stepSprings(dt, springCount);
2033
+ for (let i = 0; i < springCount; i++) sD[i].syncExternal(sv.val[i], sv.vel[i]);
2034
+ }
2035
+ if (tweenCount > 0) {
2036
+ const tv = backend.tweenView();
2037
+ for (let i = 0; i < tweenCount; i++) {
2038
+ const d = tD[i];
2039
+ tv.from[i] = d.fromValue;
2040
+ tv.to[i] = d.target;
2041
+ tv.elapsed[i] = d.elapsedMs;
2042
+ tv.dur[i] = d.durationMs;
2043
+ tv.delay[i] = d.delayMs;
2044
+ tv.ease[i] = d.wasmEasingId;
2045
+ }
2046
+ backend.stepTweens(dt, tweenCount);
2047
+ for (let i = 0; i < tweenCount; i++) tD[i].syncExternal(tv.val[i], tv.elapsed[i]);
2048
+ }
2049
+ for (let i = 0; i < springCount; i++) sE[i]._applyDriverTick(sP[i], sD[i]);
2050
+ for (let i = 0; i < tweenCount; i++) tE[i]._applyDriverTick(tP[i], tD[i]);
2051
+ }
2052
+ /**
2053
+ * Compose the whole main tree's world matrices through the resident WASM store
2054
+ * and return the world-matrix views for the render walk to read. Rebuilds the
2055
+ * store layout (slots + runs) only when the tree structure changed since the
2056
+ * last rebuild; otherwise it just gathers current transforms into the resident
2057
+ * input view and runs the kernel. Returns `null` if there is no backend.
2058
+ */
2059
+ _syncWasmStore() {
2060
+ const backend = this._wasm;
2061
+ if (!backend) return null;
2062
+ if (this._treeStore === null || this._storeStructureVersion !== this._structureVersion) {
2063
+ const built = buildTreeStore(this.root);
2064
+ const slotEntity2 = Array.from({ length: built.store.count });
2065
+ for (const [entity, slot] of built.indexOf) {
2066
+ slotEntity2[slot] = entity;
2067
+ entity._storeSlot = slot;
2068
+ }
2069
+ backend.uploadRuns(built.store);
2070
+ this._treeStore = built.store;
2071
+ this._slotEntity = slotEntity2;
2072
+ this._wasmInputs = backend.inputView();
2073
+ this._wasmWorld = backend.worldView();
2074
+ this._storeStructureVersion = this._structureVersion;
2075
+ }
2076
+ const inp = this._wasmInputs;
2077
+ const slotEntity = this._slotEntity;
2078
+ for (let slot = 1; slot < slotEntity.length; slot++) {
2079
+ const e = slotEntity[slot];
2080
+ inp.x[slot] = e.x;
2081
+ inp.y[slot] = e.y;
2082
+ inp.sx[slot] = e.scaleX;
2083
+ inp.sy[slot] = e.scaleY;
2084
+ const trig = e._getTrig();
2085
+ inp.cos[slot] = trig.cos;
2086
+ inp.sin[slot] = trig.sin;
2087
+ inp.opacity[slot] = e.opacity;
2088
+ }
2089
+ backend.runKernel("simd");
2090
+ return this._wasmWorld;
2091
+ }
752
2092
  /**
753
2093
  * Authoritative paint order for semantic nodes discovered during the main
754
2094
  * render. A node may not have a DOM projection until the following a11y
755
2095
  * sync, so retaining the order prevents a newly opened overlay from spending
756
2096
  * its first frame below previously projected controls.
757
2097
  */
758
- __init49() {this.a11yRenderOrders = /* @__PURE__ */ new Map()}
2098
+ __init99() {this.a11yRenderOrders = /* @__PURE__ */ new Map()}
759
2099
  // Optional WebGL point-cloud layer (see SceneOptions.pointBackend).
760
- __init50() {this.pointRenderer = null}
761
- __init51() {this.glCanvas = null}
2100
+ __init100() {this.pointRenderer = null}
2101
+ __init101() {this.glCanvas = null}
2102
+ __init102() {this.glContextLostHandler = null}
2103
+ __init103() {this.glContextRestoredHandler = null}
762
2104
 
763
2105
 
764
2106
 
765
- __init52() {this.disableWindowResize = false}
2107
+ __init104() {this.disableWindowResize = false}
766
2108
  /** See {@link SceneOptions.maxDPR}. `undefined` = uncapped (real DPR). */
767
2109
 
768
2110
  // WebGPU properties
769
- __init53() {this.destroyed = false}
770
- __init54() {this.device = null}
771
- __init55() {this.deviceLost = false}
772
- __init56() {this.particleBackend = "auto"}
773
- __init57() {this._webgpuDisabled = false}
2111
+ __init105() {this.destroyed = false}
2112
+ __init106() {this.device = null}
2113
+ __init107() {this.deviceLost = false}
2114
+ __init108() {this.particleBackend = "auto"}
2115
+ __init109() {this._webgpuDisabled = false}
774
2116
  get webgpuDisabled() {
775
2117
  return this._webgpuDisabled || this.particleBackend === "cpu";
776
2118
  }
777
2119
  set webgpuDisabled(value) {
778
2120
  this._webgpuDisabled = value;
779
2121
  }
780
- __init58() {this.recoveryTimerId = null}
781
- __init59() {this.manager = null}
782
- __init60() {this.initializingWebGPU = false}
783
- __init61() {this.gpuCanvas = null}
784
- __init62() {this.gpuContext = null}
2122
+ __init110() {this.recoveryTimerId = null}
2123
+ __init111() {this.manager = null}
2124
+ __init112() {this.initializingWebGPU = false}
2125
+ __init113() {this.gpuCanvas = null}
2126
+ __init114() {this.gpuContext = null}
785
2127
  /** True while the GPU canvas holds a presented particle frame (needs clearing when they leave). */
786
- __init63() {this.gpuHasContent = false}
787
- __init64() {this.mouseX = -9999}
788
- __init65() {this.mouseY = -9999}
789
- __init66() {this.pointerMoveListener = null}
790
- __init67() {this.pointerLeaveListener = null}
791
- __init68() {this.hasWarnedZeroSize = false}
792
- __init69() {this.fontLoadHandler = null}
2128
+ __init115() {this.gpuHasContent = false}
2129
+ __init116() {this.mouseX = -9999}
2130
+ __init117() {this.mouseY = -9999}
2131
+ __init118() {this.pointerMoveListener = null}
2132
+ __init119() {this.pointerLeaveListener = null}
2133
+ /** Element the pointer listeners are bound to (parent container if present,
2134
+ * else the canvas). Stored so `destroy()` detaches from the same element. */
2135
+ __init120() {this.pointerEventTarget = null}
2136
+ __init121() {this.hasWarnedZeroSize = false}
2137
+ __init122() {this.fontLoadHandler = null}
793
2138
  // ── Dev-mode warning infrastructure ──────────────────────────────
794
2139
  //
795
2140
  // Enable with `Scene.devMode = true` or by setting `globalThis.__DEV__`.
@@ -798,16 +2143,16 @@ var Scene = (_class2 = class _Scene {
798
2143
  // Checks run once every ~120 frames (~2s at 60fps) to keep overhead
799
2144
  // negligible even when dev mode is on.
800
2145
  /** Toggle development-mode runtime warnings globally. */
801
- static __initStatic3() {this.devMode = false}
2146
+ static __initStatic4() {this.devMode = false}
802
2147
  static _devModeDetected() {
803
2148
  if (_Scene.devMode) return true;
804
2149
  const gp = typeof globalThis !== "undefined" ? globalThis : void 0;
805
- if (_optionalChain([gp, 'optionalAccess', _23 => _23.__DEV__])) return true;
806
- if (_optionalChain([gp, 'optionalAccess', _24 => _24.process, 'optionalAccess', _25 => _25.env, 'optionalAccess', _26 => _26.NODE_ENV]) === "development") return true;
2150
+ if (_optionalChain([gp, 'optionalAccess', _27 => _27.__DEV__])) return true;
2151
+ if (_optionalChain([gp, 'optionalAccess', _28 => _28.process, 'optionalAccess', _29 => _29.env, 'optionalAccess', _30 => _30.NODE_ENV]) === "development") return true;
807
2152
  return false;
808
2153
  }
809
2154
 
810
- __init70() {this._devFrameCount = 0}
2155
+ __init123() {this._devFrameCount = 0}
811
2156
  _devWarn(message) {
812
2157
  if (!this._devActive) return;
813
2158
  console.warn(`[vectojs/dev] ${message}`);
@@ -834,9 +2179,9 @@ var Scene = (_class2 = class _Scene {
834
2179
  let checked = 0;
835
2180
  const walkProjections = (node) => {
836
2181
  if (checked > 10) return;
837
- const proj = _optionalChain([node, 'access', _27 => _27.getContentProjection, 'optionalCall', _28 => _28()]);
838
- if (_optionalChain([proj, 'optionalAccess', _29 => _29.text]) && proj.selectable !== false) {
839
- const el = _optionalChain([this, 'access', _30 => _30.contentElements, 'optionalAccess', _31 => _31.get, 'call', _32 => _32(node.id)]);
2182
+ const proj = _optionalChain([node, 'access', _31 => _31.getContentProjection, 'optionalCall', _32 => _32()]);
2183
+ if (_optionalChain([proj, 'optionalAccess', _33 => _33.text]) && proj.selectable !== false) {
2184
+ const el = _optionalChain([this, 'access', _34 => _34.contentElements, 'optionalAccess', _35 => _35.get, 'call', _36 => _36(node.id)]);
840
2185
  if (el) {
841
2186
  const projectedText = el.textContent || "";
842
2187
  if (projectedText !== "" && projectedText !== proj.text) {
@@ -851,14 +2196,14 @@ var Scene = (_class2 = class _Scene {
851
2196
  };
852
2197
  walkProjections(this.root);
853
2198
  }
854
- constructor(canvas, options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);_class2.prototype.__init49.call(this);_class2.prototype.__init50.call(this);_class2.prototype.__init51.call(this);_class2.prototype.__init52.call(this);_class2.prototype.__init53.call(this);_class2.prototype.__init54.call(this);_class2.prototype.__init55.call(this);_class2.prototype.__init56.call(this);_class2.prototype.__init57.call(this);_class2.prototype.__init58.call(this);_class2.prototype.__init59.call(this);_class2.prototype.__init60.call(this);_class2.prototype.__init61.call(this);_class2.prototype.__init62.call(this);_class2.prototype.__init63.call(this);_class2.prototype.__init64.call(this);_class2.prototype.__init65.call(this);_class2.prototype.__init66.call(this);_class2.prototype.__init67.call(this);_class2.prototype.__init68.call(this);_class2.prototype.__init69.call(this);_class2.prototype.__init70.call(this);
2199
+ constructor(canvas, options = {}) {;_class6.prototype.__init20.call(this);_class6.prototype.__init21.call(this);_class6.prototype.__init22.call(this);_class6.prototype.__init23.call(this);_class6.prototype.__init24.call(this);_class6.prototype.__init25.call(this);_class6.prototype.__init26.call(this);_class6.prototype.__init27.call(this);_class6.prototype.__init28.call(this);_class6.prototype.__init29.call(this);_class6.prototype.__init30.call(this);_class6.prototype.__init31.call(this);_class6.prototype.__init32.call(this);_class6.prototype.__init33.call(this);_class6.prototype.__init34.call(this);_class6.prototype.__init35.call(this);_class6.prototype.__init36.call(this);_class6.prototype.__init37.call(this);_class6.prototype.__init38.call(this);_class6.prototype.__init39.call(this);_class6.prototype.__init40.call(this);_class6.prototype.__init41.call(this);_class6.prototype.__init42.call(this);_class6.prototype.__init43.call(this);_class6.prototype.__init44.call(this);_class6.prototype.__init45.call(this);_class6.prototype.__init46.call(this);_class6.prototype.__init47.call(this);_class6.prototype.__init48.call(this);_class6.prototype.__init49.call(this);_class6.prototype.__init50.call(this);_class6.prototype.__init51.call(this);_class6.prototype.__init52.call(this);_class6.prototype.__init53.call(this);_class6.prototype.__init54.call(this);_class6.prototype.__init55.call(this);_class6.prototype.__init56.call(this);_class6.prototype.__init57.call(this);_class6.prototype.__init58.call(this);_class6.prototype.__init59.call(this);_class6.prototype.__init60.call(this);_class6.prototype.__init61.call(this);_class6.prototype.__init62.call(this);_class6.prototype.__init63.call(this);_class6.prototype.__init64.call(this);_class6.prototype.__init65.call(this);_class6.prototype.__init66.call(this);_class6.prototype.__init67.call(this);_class6.prototype.__init68.call(this);_class6.prototype.__init69.call(this);_class6.prototype.__init70.call(this);_class6.prototype.__init71.call(this);_class6.prototype.__init72.call(this);_class6.prototype.__init73.call(this);_class6.prototype.__init74.call(this);_class6.prototype.__init75.call(this);_class6.prototype.__init76.call(this);_class6.prototype.__init77.call(this);_class6.prototype.__init78.call(this);_class6.prototype.__init79.call(this);_class6.prototype.__init80.call(this);_class6.prototype.__init81.call(this);_class6.prototype.__init82.call(this);_class6.prototype.__init83.call(this);_class6.prototype.__init84.call(this);_class6.prototype.__init85.call(this);_class6.prototype.__init86.call(this);_class6.prototype.__init87.call(this);_class6.prototype.__init88.call(this);_class6.prototype.__init89.call(this);_class6.prototype.__init90.call(this);_class6.prototype.__init91.call(this);_class6.prototype.__init92.call(this);_class6.prototype.__init93.call(this);_class6.prototype.__init94.call(this);_class6.prototype.__init95.call(this);_class6.prototype.__init96.call(this);_class6.prototype.__init97.call(this);_class6.prototype.__init98.call(this);_class6.prototype.__init99.call(this);_class6.prototype.__init100.call(this);_class6.prototype.__init101.call(this);_class6.prototype.__init102.call(this);_class6.prototype.__init103.call(this);_class6.prototype.__init104.call(this);_class6.prototype.__init105.call(this);_class6.prototype.__init106.call(this);_class6.prototype.__init107.call(this);_class6.prototype.__init108.call(this);_class6.prototype.__init109.call(this);_class6.prototype.__init110.call(this);_class6.prototype.__init111.call(this);_class6.prototype.__init112.call(this);_class6.prototype.__init113.call(this);_class6.prototype.__init114.call(this);_class6.prototype.__init115.call(this);_class6.prototype.__init116.call(this);_class6.prototype.__init117.call(this);_class6.prototype.__init118.call(this);_class6.prototype.__init119.call(this);_class6.prototype.__init120.call(this);_class6.prototype.__init121.call(this);_class6.prototype.__init122.call(this);_class6.prototype.__init123.call(this);
855
2200
  this.canvas = canvas;
856
2201
  this.debugA11y = _nullishCoalesce(options.debugA11y, () => ( false));
857
2202
  this.disableWindowResize = _nullishCoalesce(options.disableWindowResize, () => ( false));
858
2203
  this.maxDPR = options.maxDPR;
859
2204
  if (this.disableWindowResize) {
860
- const styleWidth = parseInlinePx(_optionalChain([canvas, 'access', _33 => _33.style, 'optionalAccess', _34 => _34.width]));
861
- const styleHeight = parseInlinePx(_optionalChain([canvas, 'access', _35 => _35.style, 'optionalAccess', _36 => _36.height]));
2205
+ const styleWidth = parseInlinePx(_optionalChain([canvas, 'access', _37 => _37.style, 'optionalAccess', _38 => _38.width]));
2206
+ const styleHeight = parseInlinePx(_optionalChain([canvas, 'access', _39 => _39.style, 'optionalAccess', _40 => _40.height]));
862
2207
  this.width = _nullishCoalesce(styleWidth, () => ( (canvas.width || canvas.clientWidth || 0)));
863
2208
  this.height = _nullishCoalesce(styleHeight, () => ( (canvas.height || canvas.clientHeight || 0)));
864
2209
  } else {
@@ -866,7 +2211,7 @@ var Scene = (_class2 = class _Scene {
866
2211
  this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
867
2212
  }
868
2213
  const globalProcess = typeof globalThis !== "undefined" ? globalThis.process : void 0;
869
- const isTest = globalProcess && (_optionalChain([globalProcess, 'access', _37 => _37.env, 'optionalAccess', _38 => _38.NODE_ENV]) === "test" || _optionalChain([globalProcess, 'access', _39 => _39.env, 'optionalAccess', _40 => _40.VITEST]) === "true");
2214
+ const isTest = globalProcess && (_optionalChain([globalProcess, 'access', _41 => _41.env, 'optionalAccess', _42 => _42.NODE_ENV]) === "test" || _optionalChain([globalProcess, 'access', _43 => _43.env, 'optionalAccess', _44 => _44.VITEST]) === "true");
870
2215
  this.maxFPS = _nullishCoalesce(options.maxFPS, () => ( (isTest ? 0 : 60)));
871
2216
  this.respectReducedMotion = _nullishCoalesce(options.respectReducedMotion, () => ( true));
872
2217
  this.autoThrottle = _nullishCoalesce(options.autoThrottle, () => ( true));
@@ -874,9 +2219,15 @@ var Scene = (_class2 = class _Scene {
874
2219
  this.a11ySyncInterval = _nullishCoalesce(options.a11ySyncInterval, () => ( 0));
875
2220
  this.contentProjectionEnabled = _nullishCoalesce(options.contentProjection, () => ( true));
876
2221
  this.contentProjectionMargin = options.contentProjectionMargin;
2222
+ this.readingDirection = _nullishCoalesce(options.readingDirection, () => ( "ltr"));
877
2223
  this._devActive = _Scene._devModeDetected();
878
2224
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
879
- this.root = new class RootEntity extends _chunk2Z23LTH3js.Entity {
2225
+ if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
2226
+ this.forcedColorsQuery = window.matchMedia("(forced-colors: active)");
2227
+ this.forcedColorsChangeHandler = () => this.markDirty();
2228
+ _optionalChain([this, 'access', _45 => _45.forcedColorsQuery, 'access', _46 => _46.addEventListener, 'optionalCall', _47 => _47("change", this.forcedColorsChangeHandler)]);
2229
+ }
2230
+ this.root = new class RootEntity extends _chunkDUYB4GX4js.Entity {
880
2231
  isPointInside() {
881
2232
  return false;
882
2233
  }
@@ -885,7 +2236,7 @@ var Scene = (_class2 = class _Scene {
885
2236
  }
886
2237
  }("root");
887
2238
  this.root._scene = this;
888
- this.overlayRoot = new class OverlayRoot extends _chunk2Z23LTH3js.Entity {
2239
+ this.overlayRoot = new class OverlayRoot extends _chunkDUYB4GX4js.Entity {
889
2240
  isPointInside() {
890
2241
  return false;
891
2242
  }
@@ -896,12 +2247,16 @@ var Scene = (_class2 = class _Scene {
896
2247
  if (options.renderer) {
897
2248
  this.renderer = options.renderer;
898
2249
  } else {
899
- this.renderer = new (0, _chunkBEUIB3U7js.CanvasRenderer)(
2250
+ this.renderer = new (0, _chunkIYPLG4Z4js.CanvasRenderer)(
900
2251
  canvas,
901
2252
  this.disableWindowResize ? { width: this.width, height: this.height } : void 0,
902
2253
  this.maxDPR
903
2254
  );
904
2255
  }
2256
+ _optionalChain([this, 'access', _48 => _48.renderer, 'access', _49 => _49.onContextRestored, 'optionalCall', _50 => _50(() => {
2257
+ this.markDirty();
2258
+ if (_optionalChain([this, 'access', _51 => _51.renderer, 'access', _52 => _52.isContextLost, 'optionalCall', _53 => _53()]) !== true) this.render(this.renderer);
2259
+ })]);
905
2260
  if (typeof document !== "undefined") {
906
2261
  this.a11yRoot = document.createElement("div");
907
2262
  this.a11yRoot.style.position = "absolute";
@@ -913,6 +2268,15 @@ var Scene = (_class2 = class _Scene {
913
2268
  this.a11yRoot.style.overflow = "hidden";
914
2269
  this.a11yRoot.style.zIndex = "10";
915
2270
  this.a11yRoot.style.userSelect = "text";
2271
+ this.focusSentinel = document.createElement("div");
2272
+ this.focusSentinel.setAttribute("data-vecto-focus-sentinel", "");
2273
+ this.focusSentinel.tabIndex = -1;
2274
+ this.focusSentinel.style.position = "absolute";
2275
+ this.focusSentinel.style.width = "0";
2276
+ this.focusSentinel.style.height = "0";
2277
+ this.focusSentinel.style.outline = "none";
2278
+ this.focusSentinel.style.overflow = "hidden";
2279
+ this.a11yRoot.appendChild(this.focusSentinel);
916
2280
  this.a11yRoot.addEventListener("mousedown", (e) => {
917
2281
  if (e.button !== 0) return;
918
2282
  const target = e.target;
@@ -1029,6 +2393,8 @@ var Scene = (_class2 = class _Scene {
1029
2393
  pr.resize(this.width, this.height);
1030
2394
  this.glCanvas = gl;
1031
2395
  this.pointRenderer = pr;
2396
+ this._overlayGeometry = null;
2397
+ this.setupGLContextRecovery(gl);
1032
2398
  } else {
1033
2399
  gl.remove();
1034
2400
  }
@@ -1038,7 +2404,7 @@ var Scene = (_class2 = class _Scene {
1038
2404
  };
1039
2405
  if (typeof document !== "undefined" && document.fonts) {
1040
2406
  this.fontLoadHandler = () => {
1041
- _chunk2Z23LTH3js.clearCssLineBoxMetrics.call(void 0, );
2407
+ _text.clearCssLineBoxMetrics.call(void 0, );
1042
2408
  this.contentFontEpoch++;
1043
2409
  this.markDirty();
1044
2410
  };
@@ -1047,6 +2413,61 @@ var Scene = (_class2 = class _Scene {
1047
2413
  }
1048
2414
  this.setupEvents();
1049
2415
  }
2416
+ /**
2417
+ * Arm a `(resolution: Ndppx)` media query for the current devicePixelRatio and
2418
+ * re-apply the canvas scale when it changes. Such a query only fires when the
2419
+ * DPR leaves its exact value, so on each change the old query is detached and
2420
+ * a fresh one is armed for the new DPR. Re-runs `resize(width, height)` (which
2421
+ * re-scales the backing store via the renderer) so text/vectors stay crisp
2422
+ * after a monitor move or zoom. No-op without `matchMedia`.
2423
+ */
2424
+ watchDevicePixelRatio() {
2425
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
2426
+ if (this.dprMediaQuery && this.dprChangeHandler) {
2427
+ _optionalChain([this, 'access', _54 => _54.dprMediaQuery, 'access', _55 => _55.removeEventListener, 'optionalCall', _56 => _56("change", this.dprChangeHandler)]);
2428
+ }
2429
+ const dpr = window.devicePixelRatio || 1;
2430
+ const query = window.matchMedia(`(resolution: ${dpr}dppx)`);
2431
+ const handler = () => {
2432
+ this.resize(this.width, this.height);
2433
+ this.watchDevicePixelRatio();
2434
+ };
2435
+ _optionalChain([query, 'access', _57 => _57.addEventListener, 'optionalCall', _58 => _58("change", handler)]);
2436
+ this.dprMediaQuery = query;
2437
+ this.dprChangeHandler = handler;
2438
+ }
2439
+ /**
2440
+ * Recover the WebGL point layer from a GPU context loss (driver TDR reset,
2441
+ * tab backgrounded on mobile, GPU switch). Two things are required:
2442
+ *
2443
+ * 1. The `webglcontextlost` handler MUST call `preventDefault()`, or the
2444
+ * browser never fires `webglcontextrestored` and the layer is blank
2445
+ * forever. While lost, the old renderer's GL calls are silently ignored,
2446
+ * so we drop it and the render loop simply skips the point layer.
2447
+ * 2. On `webglcontextrestored`, all GL objects (programs, buffers, textures)
2448
+ * are gone, so we rebuild the renderer from scratch via `Scene.webglCreator`
2449
+ * on the same canvas, restore DPR/size, and repaint.
2450
+ */
2451
+ setupGLContextRecovery(gl) {
2452
+ if (typeof gl.addEventListener !== "function") return;
2453
+ this.glContextLostHandler = (e) => {
2454
+ e.preventDefault();
2455
+ _optionalChain([this, 'access', _59 => _59.pointRenderer, 'optionalAccess', _60 => _60.destroy, 'call', _61 => _61()]);
2456
+ this.pointRenderer = null;
2457
+ };
2458
+ this.glContextRestoredHandler = () => {
2459
+ if (this.destroyed || !this.glCanvas) return;
2460
+ const pr = _Scene.webglCreator ? _Scene.webglCreator(this.glCanvas) : null;
2461
+ if (pr) {
2462
+ pr.maxDPR = this.maxDPR;
2463
+ pr.resize(this.width, this.height);
2464
+ this.pointRenderer = pr;
2465
+ this.markDirty();
2466
+ }
2467
+ };
2468
+ gl.addEventListener("webglcontextlost", this.glContextLostHandler);
2469
+ gl.addEventListener("webglcontextrestored", this.glContextRestoredHandler);
2470
+ }
1050
2471
  endContentSelectionDrag() {
1051
2472
  this.blankRegionSelectionDrag = false;
1052
2473
  this.contentSelectionAnchor = null;
@@ -1054,10 +2475,57 @@ var Scene = (_class2 = class _Scene {
1054
2475
  }
1055
2476
  releaseContentSelectionForRebuild(el) {
1056
2477
  const selection = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
1057
- const ownsSelection = this.contentSelectionAnchor && el.contains(this.contentSelectionAnchor.node) || (_optionalChain([selection, 'optionalAccess', _41 => _41.anchorNode]) ? el.contains(selection.anchorNode) : false) || (_optionalChain([selection, 'optionalAccess', _42 => _42.focusNode]) ? el.contains(selection.focusNode) : false);
2478
+ const ownsSelection = this.contentSelectionAnchor && el.contains(this.contentSelectionAnchor.node) || (_optionalChain([selection, 'optionalAccess', _62 => _62.anchorNode]) ? el.contains(selection.anchorNode) : false) || (_optionalChain([selection, 'optionalAccess', _63 => _63.focusNode]) ? el.contains(selection.focusNode) : false);
1058
2479
  if (!ownsSelection) return;
1059
2480
  this.endContentSelectionDrag();
1060
- _optionalChain([selection, 'optionalAccess', _43 => _43.removeAllRanges, 'call', _44 => _44()]);
2481
+ _optionalChain([selection, 'optionalAccess', _64 => _64.removeAllRanges, 'call', _65 => _65()]);
2482
+ }
2483
+ /**
2484
+ * Rebuild a content-projection element's DOM (`rebuild`) while preserving a
2485
+ * text selection the user made inside it. A streaming message replaces its
2486
+ * projection children on every appended chunk; without this, a selection in
2487
+ * the UNCHANGED prefix is wiped on each frame ("can't select text in a
2488
+ * message still receiving tokens"). We snapshot the selection's anchor/focus
2489
+ * as linear character offsets within `el` before the rebuild and re-resolve
2490
+ * them against the new DOM after, clamped to the new text length.
2491
+ *
2492
+ * Only fires when `el` owns the current selection and there is no active drag
2493
+ * (mid-drag the browser is authoritative). The virtualization case — where
2494
+ * `el` itself is removed from the DOM — is out of scope here (the node is
2495
+ * genuinely freed; the browser clears the selection and there is nothing to
2496
+ * restore against).
2497
+ */
2498
+ preserveContentSelectionAcrossRebuild(el, rebuild) {
2499
+ const selection = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
2500
+ const owns = !!selection && !this.blankRegionSelectionDrag && ((selection.anchorNode ? el.contains(selection.anchorNode) : false) || (selection.focusNode ? el.contains(selection.focusNode) : false));
2501
+ if (!owns || !selection.anchorNode || !selection.focusNode) {
2502
+ this.releaseContentSelectionForRebuild(el);
2503
+ rebuild();
2504
+ return;
2505
+ }
2506
+ const anchorNode = selection.anchorNode;
2507
+ const focusNode = selection.focusNode;
2508
+ const anchorOffset = anchorNode instanceof Text ? projectionAbsoluteOffset(el, {
2509
+ node: anchorNode,
2510
+ offset: selection.anchorOffset
2511
+ }) : null;
2512
+ const focusOffset = focusNode instanceof Text ? projectionAbsoluteOffset(el, {
2513
+ node: focusNode,
2514
+ offset: selection.focusOffset
2515
+ }) : null;
2516
+ this.endContentSelectionDrag();
2517
+ selection.removeAllRanges();
2518
+ rebuild();
2519
+ if (anchorOffset === null || focusOffset === null) return;
2520
+ const textLen = (_nullishCoalesce(el.textContent, () => ( ""))).length;
2521
+ if (anchorOffset > textLen || focusOffset > textLen) return;
2522
+ const anchor = projectionCaretAt(el, anchorOffset, "forward");
2523
+ const focus = projectionCaretAt(el, focusOffset, "backward");
2524
+ if (!anchor || !focus) return;
2525
+ try {
2526
+ selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
2527
+ } catch (e17) {
2528
+ }
1061
2529
  }
1062
2530
  /**
1063
2531
  * Expose the underlying {@link IRenderer} for advanced direct-draw operations.
@@ -1069,7 +2537,7 @@ var Scene = (_class2 = class _Scene {
1069
2537
  }
1070
2538
  /** Convert browser viewport coordinates into this Scene's logical coordinates. */
1071
2539
  clientToScene(clientX, clientY) {
1072
- const rect = _optionalChain([this, 'access', _45 => _45.canvas, 'access', _46 => _46.getBoundingClientRect, 'optionalCall', _47 => _47()]);
2540
+ const rect = _optionalChain([this, 'access', _66 => _66.canvas, 'access', _67 => _67.getBoundingClientRect, 'optionalCall', _68 => _68()]);
1073
2541
  if (!rect) return { x: clientX, y: clientY };
1074
2542
  const cssWidth = rect.width || this.canvas.clientWidth || this.width;
1075
2543
  const cssHeight = rect.height || this.canvas.clientHeight || this.height;
@@ -1087,6 +2555,7 @@ var Scene = (_class2 = class _Scene {
1087
2555
  */
1088
2556
  add(entity) {
1089
2557
  this.root.add(entity);
2558
+ this.registerActiveDriverSubtree(entity);
1090
2559
  return this;
1091
2560
  }
1092
2561
  clearContentGridState(entityId, el) {
@@ -1095,7 +2564,7 @@ var Scene = (_class2 = class _Scene {
1095
2564
  cancelAnimationFrame(calibrationFrame);
1096
2565
  }
1097
2566
  this.contentGridCalibrationFrames.delete(entityId);
1098
- _optionalChain([this, 'access', _48 => _48.contentGridCalibrationProbes, 'access', _49 => _49.get, 'call', _50 => _50(entityId), 'optionalAccess', _51 => _51.remove, 'call', _52 => _52()]);
2567
+ _optionalChain([this, 'access', _69 => _69.contentGridCalibrationProbes, 'access', _70 => _70.get, 'call', _71 => _71(entityId), 'optionalAccess', _72 => _72.remove, 'call', _73 => _73()]);
1099
2568
  this.contentGridCalibrationProbes.delete(entityId);
1100
2569
  delete el.dataset.vectoGridCalibrationPending;
1101
2570
  delete el.dataset.vectoGridCalibration;
@@ -1109,6 +2578,7 @@ var Scene = (_class2 = class _Scene {
1109
2578
  }
1110
2579
  removeA11yRecursively(node) {
1111
2580
  if (node.isDOMPortal) {
2581
+ node.releaseDOMBindings();
1112
2582
  node.domElement.remove();
1113
2583
  this.portalEntities.delete(node.id);
1114
2584
  this.activePortalsThisFrame.delete(node.id);
@@ -1130,6 +2600,11 @@ var Scene = (_class2 = class _Scene {
1130
2600
  this.caretBlinkTimer = null;
1131
2601
  }
1132
2602
  }
2603
+ if (this.hoveredA11yElements.has(el)) {
2604
+ this.hoveredA11yElements.delete(el);
2605
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("pointerleave", node, void 0, false));
2606
+ }
2607
+ this.preserveFocusOnRemoval(el);
1133
2608
  el.remove();
1134
2609
  this.a11yElements.delete(node.id);
1135
2610
  this.a11yNeedsReorder = true;
@@ -1138,6 +2613,18 @@ var Scene = (_class2 = class _Scene {
1138
2613
  this.removeA11yRecursively(child);
1139
2614
  }
1140
2615
  }
2616
+ /**
2617
+ * If `el` is about to be removed from the DOM while it holds browser focus,
2618
+ * move focus to the a11y focus sentinel first. Removing the active element
2619
+ * otherwise drops focus to `<body>`, which pulls a screen reader out of the
2620
+ * scene's a11y region and back to the top of the page — the classic
2621
+ * "lost my place on scroll/stream" bug for virtualized/recycled controls.
2622
+ */
2623
+ preserveFocusOnRemoval(el) {
2624
+ if (!this.focusSentinel || typeof document === "undefined") return;
2625
+ if (document.activeElement !== el) return;
2626
+ this.focusSentinel.focus({ preventScroll: true });
2627
+ }
1141
2628
  /**
1142
2629
  * Remove a top-level entity from the scene graph and clean up its
1143
2630
  * accessibility shadow elements recursively.
@@ -1148,6 +2635,7 @@ var Scene = (_class2 = class _Scene {
1148
2635
  remove(entity) {
1149
2636
  this.root.remove(entity);
1150
2637
  this.removeA11yRecursively(entity);
2638
+ this.unregisterActiveDriverSubtree(entity);
1151
2639
  return this;
1152
2640
  }
1153
2641
  /**
@@ -1167,6 +2655,7 @@ var Scene = (_class2 = class _Scene {
1167
2655
  */
1168
2656
  showOverlay(overlay) {
1169
2657
  this.overlayRoot.add(overlay);
2658
+ this.registerActiveDriverSubtree(overlay);
1170
2659
  this.markDirty();
1171
2660
  }
1172
2661
  /**
@@ -1175,10 +2664,10 @@ var Scene = (_class2 = class _Scene {
1175
2664
  hideOverlay(overlay) {
1176
2665
  this.overlayRoot.remove(overlay);
1177
2666
  this.removeA11yRecursively(overlay);
2667
+ this.unregisterActiveDriverSubtree(overlay);
1178
2668
  this.markDirty();
1179
2669
  }
1180
2670
  destroyEntitySubtree(entity) {
1181
- while (entity.children.length > 0) this.destroyEntitySubtree(entity.children.at(-1));
1182
2671
  entity.destroy();
1183
2672
  }
1184
2673
  /**
@@ -1198,21 +2687,37 @@ var Scene = (_class2 = class _Scene {
1198
2687
  if (typeof window !== "undefined" && !this.disableWindowResize) {
1199
2688
  window.removeEventListener("resize", this.resizeHandler);
1200
2689
  }
2690
+ if (this.canvasResizeObserver) {
2691
+ this.canvasResizeObserver.disconnect();
2692
+ this.canvasResizeObserver = null;
2693
+ }
2694
+ if (this.dprMediaQuery && this.dprChangeHandler) {
2695
+ _optionalChain([this, 'access', _74 => _74.dprMediaQuery, 'access', _75 => _75.removeEventListener, 'optionalCall', _76 => _76("change", this.dprChangeHandler)]);
2696
+ this.dprMediaQuery = null;
2697
+ this.dprChangeHandler = null;
2698
+ }
2699
+ if (this.forcedColorsQuery && this.forcedColorsChangeHandler) {
2700
+ _optionalChain([this, 'access', _77 => _77.forcedColorsQuery, 'access', _78 => _78.removeEventListener, 'optionalCall', _79 => _79("change", this.forcedColorsChangeHandler)]);
2701
+ this.forcedColorsQuery = null;
2702
+ this.forcedColorsChangeHandler = null;
2703
+ }
1201
2704
  if (typeof window !== "undefined" && this.contentSelectionEndListener) {
1202
2705
  window.removeEventListener("mouseup", this.contentSelectionEndListener);
1203
2706
  window.removeEventListener("blur", this.contentSelectionEndListener);
1204
2707
  this.contentSelectionEndListener = null;
1205
2708
  }
1206
- if (typeof window !== "undefined" && this.canvas && typeof this.canvas.removeEventListener === "function") {
2709
+ if (typeof window !== "undefined" && this.pointerEventTarget && typeof this.pointerEventTarget.removeEventListener === "function") {
1207
2710
  if (this.pointerMoveListener) {
1208
- this.canvas.removeEventListener("pointermove", this.pointerMoveListener);
2711
+ this.pointerEventTarget.removeEventListener("pointermove", this.pointerMoveListener);
1209
2712
  }
1210
2713
  if (this.pointerLeaveListener) {
1211
- this.canvas.removeEventListener("pointerleave", this.pointerLeaveListener);
2714
+ this.pointerEventTarget.removeEventListener("pointerleave", this.pointerLeaveListener);
1212
2715
  }
2716
+ this.pointerEventTarget = null;
1213
2717
  }
1214
- _optionalChain([this, 'access', _53 => _53.a11yRoot, 'optionalAccess', _54 => _54.remove, 'call', _55 => _55()]);
1215
- _optionalChain([this, 'access', _56 => _56.portalRoot, 'optionalAccess', _57 => _57.remove, 'call', _58 => _58()]);
2718
+ _optionalChain([this, 'access', _80 => _80.a11yRoot, 'optionalAccess', _81 => _81.remove, 'call', _82 => _82()]);
2719
+ this.focusSentinel = null;
2720
+ _optionalChain([this, 'access', _83 => _83.portalRoot, 'optionalAccess', _84 => _84.remove, 'call', _85 => _85()]);
1216
2721
  this.a11yElements.clear();
1217
2722
  for (const el of this.contentElements.values()) el.remove();
1218
2723
  this.contentElements.clear();
@@ -1225,10 +2730,20 @@ var Scene = (_class2 = class _Scene {
1225
2730
  for (const probe of this.contentGridCalibrationProbes.values()) probe.remove();
1226
2731
  this.contentGridCalibrationProbes.clear();
1227
2732
  this.endContentSelectionDrag();
1228
- _optionalChain([this, 'access', _59 => _59.pointRenderer, 'optionalAccess', _60 => _60.destroy, 'call', _61 => _61()]);
1229
- _optionalChain([this, 'access', _62 => _62.renderer, 'access', _63 => _63.dispose, 'optionalCall', _64 => _64()]);
1230
- _optionalChain([this, 'access', _65 => _65.glCanvas, 'optionalAccess', _66 => _66.remove, 'call', _67 => _67()]);
1231
- _optionalChain([this, 'access', _68 => _68.gpuCanvas, 'optionalAccess', _69 => _69.remove, 'call', _70 => _70()]);
2733
+ if (this.glCanvas) {
2734
+ if (this.glContextLostHandler) {
2735
+ this.glCanvas.removeEventListener("webglcontextlost", this.glContextLostHandler);
2736
+ }
2737
+ if (this.glContextRestoredHandler) {
2738
+ this.glCanvas.removeEventListener("webglcontextrestored", this.glContextRestoredHandler);
2739
+ }
2740
+ }
2741
+ this.glContextLostHandler = null;
2742
+ this.glContextRestoredHandler = null;
2743
+ _optionalChain([this, 'access', _86 => _86.pointRenderer, 'optionalAccess', _87 => _87.destroy, 'call', _88 => _88()]);
2744
+ _optionalChain([this, 'access', _89 => _89.renderer, 'access', _90 => _90.dispose, 'optionalCall', _91 => _91()]);
2745
+ _optionalChain([this, 'access', _92 => _92.glCanvas, 'optionalAccess', _93 => _93.remove, 'call', _94 => _94()]);
2746
+ _optionalChain([this, 'access', _95 => _95.gpuCanvas, 'optionalAccess', _96 => _96.remove, 'call', _97 => _97()]);
1232
2747
  this.gpuCanvas = null;
1233
2748
  this.gpuContext = null;
1234
2749
  if (this.recoveryTimerId) {
@@ -1240,14 +2755,27 @@ var Scene = (_class2 = class _Scene {
1240
2755
  this.manager = null;
1241
2756
  }
1242
2757
  if (this.device) {
1243
- _optionalChain([this, 'access', _71 => _71.device, 'access', _72 => _72.destroy, 'optionalCall', _73 => _73()]);
2758
+ _optionalChain([this, 'access', _98 => _98.device, 'access', _99 => _99.destroy, 'optionalCall', _100 => _100()]);
1244
2759
  this.device = null;
1245
2760
  }
1246
2761
  }
1247
2762
  setupEvents() {
1248
2763
  if (typeof window !== "undefined" && !this.disableWindowResize) {
1249
2764
  window.addEventListener("resize", this.resizeHandler);
2765
+ } else if (this.disableWindowResize && typeof ResizeObserver !== "undefined" && this.canvas && typeof this.canvas.getBoundingClientRect === "function") {
2766
+ this.canvasResizeObserver = new ResizeObserver((entries) => {
2767
+ const entry = entries[0];
2768
+ if (!entry) return;
2769
+ const box = entry.contentRect;
2770
+ const w = Math.round(box.width);
2771
+ const h = Math.round(box.height);
2772
+ if (w > 0 && h > 0 && (w !== this.width || h !== this.height)) {
2773
+ this.resize(w, h);
2774
+ }
2775
+ });
2776
+ this.canvasResizeObserver.observe(this.canvas);
1250
2777
  }
2778
+ this.watchDevicePixelRatio();
1251
2779
  if (typeof window !== "undefined" && this.canvas && typeof this.canvas.addEventListener === "function") {
1252
2780
  this.pointerMoveListener = (e) => {
1253
2781
  const point = this.clientToScene(e.clientX, e.clientY);
@@ -1258,8 +2786,9 @@ var Scene = (_class2 = class _Scene {
1258
2786
  this.mouseX = -9999;
1259
2787
  this.mouseY = -9999;
1260
2788
  };
1261
- this.canvas.addEventListener("pointermove", this.pointerMoveListener);
1262
- this.canvas.addEventListener("pointerleave", this.pointerLeaveListener);
2789
+ this.pointerEventTarget = _nullishCoalesce(this.canvas.parentElement, () => ( this.canvas));
2790
+ this.pointerEventTarget.addEventListener("pointermove", this.pointerMoveListener);
2791
+ this.pointerEventTarget.addEventListener("pointerleave", this.pointerLeaveListener);
1263
2792
  }
1264
2793
  }
1265
2794
  /**
@@ -1277,6 +2806,7 @@ var Scene = (_class2 = class _Scene {
1277
2806
  }
1278
2807
  this.isRunning = true;
1279
2808
  this.lastTime = typeof performance !== "undefined" ? performance.now() : 0;
2809
+ this.watchCanvasVisibility();
1280
2810
  this.scheduleFrame();
1281
2811
  const isTextFocused = this.focusedA11yElement instanceof HTMLInputElement || this.focusedA11yElement instanceof HTMLTextAreaElement;
1282
2812
  if (isTextFocused && this.renderMode === "onDemand" && !this.caretBlinkTimer) {
@@ -1291,6 +2821,29 @@ var Scene = (_class2 = class _Scene {
1291
2821
  requestAnimationFrame((t) => this.loop(t));
1292
2822
  }
1293
2823
  }
2824
+ /**
2825
+ * Observe whether the canvas is on-screen so the rAF loop can pause when it
2826
+ * scrolls fully out of view (a dashboard tab, a chart below the fold) and
2827
+ * resume when it returns — instead of running the full update/render every
2828
+ * frame for a scene nobody can see. No-op (stays "on screen") where
2829
+ * `IntersectionObserver` is unavailable, so SSR/jsdom behavior is unchanged.
2830
+ */
2831
+ watchCanvasVisibility() {
2832
+ if (this._canvasObserver || typeof IntersectionObserver === "undefined") return;
2833
+ if (!this.canvas || typeof this.canvas.getBoundingClientRect !== "function") return;
2834
+ this._canvasObserver = new IntersectionObserver((entries) => {
2835
+ const entry = entries[entries.length - 1];
2836
+ if (!entry) return;
2837
+ const nowOnScreen = entry.isIntersecting;
2838
+ const wasOffScreen = !this._canvasOnScreen;
2839
+ this._canvasOnScreen = nowOnScreen;
2840
+ if (nowOnScreen && wasOffScreen && this.isRunning) {
2841
+ this.lastTime = typeof performance !== "undefined" ? performance.now() : 0;
2842
+ this.scheduleFrame();
2843
+ }
2844
+ });
2845
+ this._canvasObserver.observe(this.canvas);
2846
+ }
1294
2847
  /**
1295
2848
  * Halt the render loop after the current frame completes.
1296
2849
  *
@@ -1302,6 +2855,11 @@ var Scene = (_class2 = class _Scene {
1302
2855
  clearInterval(this.caretBlinkTimer);
1303
2856
  this.caretBlinkTimer = null;
1304
2857
  }
2858
+ if (this._canvasObserver) {
2859
+ this._canvasObserver.disconnect();
2860
+ this._canvasObserver = null;
2861
+ }
2862
+ this._canvasOnScreen = true;
1305
2863
  }
1306
2864
  /**
1307
2865
  * Manually advance the scene clock by `dt` milliseconds and render synchronously.
@@ -1392,6 +2950,7 @@ var Scene = (_class2 = class _Scene {
1392
2950
  }
1393
2951
  }
1394
2952
  if (el.parentNode === this.a11yRoot) {
2953
+ this.preserveFocusOnRemoval(el);
1395
2954
  this.a11yRoot.removeChild(el);
1396
2955
  }
1397
2956
  this.a11yElements.delete(node.id);
@@ -1419,18 +2978,20 @@ var Scene = (_class2 = class _Scene {
1419
2978
  el.style.background = "transparent";
1420
2979
  }
1421
2980
  el.addEventListener("click", (e) => {
1422
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("click", node, e));
2981
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("click", node, e));
1423
2982
  });
1424
2983
  el.addEventListener("dblclick", (e) => {
1425
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("dblclick", node, e));
2984
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("dblclick", node, e));
1426
2985
  });
1427
2986
  el.addEventListener("mouseenter", (e) => {
1428
2987
  if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.2)";
1429
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("hover", node, e, false));
2988
+ this.hoveredA11yElements.add(el);
2989
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("hover", node, e, false));
1430
2990
  });
1431
2991
  el.addEventListener("mouseleave", (e) => {
1432
2992
  if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.05)";
1433
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("pointerleave", node, e, false));
2993
+ this.hoveredA11yElements.delete(el);
2994
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("pointerleave", node, e, false));
1434
2995
  });
1435
2996
  const capEl = el;
1436
2997
  const releasePointer = (event) => {
@@ -1446,32 +3007,32 @@ var Scene = (_class2 = class _Scene {
1446
3007
  };
1447
3008
  el.addEventListener("pointerdown", (e) => {
1448
3009
  if (typeof capEl.setPointerCapture === "function") capEl.setPointerCapture(e.pointerId);
1449
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("pointerdown", node, e));
3010
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("pointerdown", node, e));
1450
3011
  });
1451
3012
  el.addEventListener("pointerup", (e) => {
1452
3013
  releasePointer(e);
1453
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("pointerup", node, e));
3014
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("pointerup", node, e));
1454
3015
  });
1455
3016
  el.addEventListener("pointercancel", (e) => {
1456
3017
  releasePointer(e);
1457
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("pointercancel", node, e));
3018
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("pointercancel", node, e));
1458
3019
  });
1459
3020
  el.addEventListener(
1460
3021
  "pointermove",
1461
- (e) => node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("pointermove", node, e))
3022
+ (e) => node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("pointermove", node, e))
1462
3023
  );
1463
3024
  el.addEventListener(
1464
3025
  "wheel",
1465
3026
  (e) => {
1466
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("wheel", node, e));
3027
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("wheel", node, e));
1467
3028
  },
1468
3029
  { passive: false }
1469
3030
  );
1470
3031
  el.addEventListener("keydown", (e) => {
1471
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("keydown", node, e));
3032
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("keydown", node, e));
1472
3033
  });
1473
3034
  el.addEventListener("keyup", (e) => {
1474
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("keyup", node, e));
3035
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("keyup", node, e));
1475
3036
  });
1476
3037
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1477
3038
  const input = el;
@@ -1493,12 +3054,18 @@ var Scene = (_class2 = class _Scene {
1493
3054
  el.addEventListener("click", forward);
1494
3055
  el.addEventListener("select", forward);
1495
3056
  el.addEventListener("compositionstart", () => {
1496
- composition = { start: _nullishCoalesce(input.selectionStart, () => ( input.value.length)), length: 0 };
3057
+ composition = {
3058
+ start: _nullishCoalesce(input.selectionStart, () => ( input.value.length)),
3059
+ length: 0
3060
+ };
1497
3061
  forward();
1498
3062
  });
1499
3063
  el.addEventListener("compositionupdate", (e) => {
1500
3064
  const data = _nullishCoalesce(e.data, () => ( ""));
1501
- composition = { start: _nullishCoalesce(_optionalChain([composition, 'optionalAccess', _74 => _74.start]), () => ( 0)), length: data.length };
3065
+ composition = {
3066
+ start: _nullishCoalesce(_optionalChain([composition, 'optionalAccess', _101 => _101.start]), () => ( 0)),
3067
+ length: data.length
3068
+ };
1502
3069
  forward();
1503
3070
  });
1504
3071
  el.addEventListener("compositionend", () => {
@@ -1531,7 +3098,7 @@ var Scene = (_class2 = class _Scene {
1531
3098
  el.addEventListener("keydown", (e) => {
1532
3099
  if (e.key === "Enter" || e.key === " ") {
1533
3100
  e.preventDefault();
1534
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("click", node, e));
3101
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("click", node, e));
1535
3102
  }
1536
3103
  });
1537
3104
  }
@@ -1569,7 +3136,7 @@ var Scene = (_class2 = class _Scene {
1569
3136
  this.syncOptionalAttribute(
1570
3137
  el,
1571
3138
  "href",
1572
- attrs.href === void 0 ? void 0 : _chunkBEUIB3U7js.sanitizeUrl.call(void 0, attrs.href)
3139
+ attrs.href === void 0 ? void 0 : _chunkIYPLG4Z4js.sanitizeUrl.call(void 0, attrs.href)
1573
3140
  );
1574
3141
  this.syncOptionalAttribute(el, "target", attrs.target);
1575
3142
  }
@@ -1612,6 +3179,31 @@ var Scene = (_class2 = class _Scene {
1612
3179
  this.syncOptionalAttribute(el, "aria-activedescendant", attrs.activedescendant);
1613
3180
  this.syncOptionalAttribute(el, "aria-valuemin", attrs.valuemin);
1614
3181
  this.syncOptionalAttribute(el, "aria-valuemax", attrs.valuemax);
3182
+ this.syncOptionalAttribute(el, "aria-live", attrs.live);
3183
+ this.syncOptionalAttribute(
3184
+ el,
3185
+ "aria-atomic",
3186
+ attrs.atomic === void 0 ? void 0 : String(attrs.atomic)
3187
+ );
3188
+ this.syncOptionalAttribute(el, "aria-relevant", attrs.relevant);
3189
+ this.syncOptionalAttribute(el, "aria-modal", attrs.ariaModal);
3190
+ this.syncOptionalAttribute(el, "aria-labelledby", attrs.labelledby);
3191
+ this.syncOptionalAttribute(el, "aria-describedby", attrs.describedby);
3192
+ this.syncOptionalAttribute(
3193
+ el,
3194
+ "aria-required",
3195
+ attrs.required === void 0 ? void 0 : String(attrs.required)
3196
+ );
3197
+ this.syncOptionalAttribute(
3198
+ el,
3199
+ "aria-invalid",
3200
+ attrs.invalid === void 0 ? void 0 : String(attrs.invalid)
3201
+ );
3202
+ this.syncOptionalAttribute(
3203
+ el,
3204
+ "aria-level",
3205
+ attrs.level === void 0 ? void 0 : String(attrs.level)
3206
+ );
1615
3207
  if (attrs.value !== void 0) {
1616
3208
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1617
3209
  if (el.value !== attrs.value) {
@@ -1643,13 +3235,18 @@ var Scene = (_class2 = class _Scene {
1643
3235
  el.style.width = `${this.width}px`;
1644
3236
  el.style.height = `${this.height}px`;
1645
3237
  el.style.transform = "";
3238
+ if (el.style.display === "none") el.style.display = "";
1646
3239
  } else {
1647
- const { a, b, c, d, e, f } = node.getWorldTransform();
3240
+ const worldTf = node.getWorldTransform();
3241
+ const { a, b, c, d, e, f } = worldTf;
1648
3242
  el.style.left = `${e + node.a11yOffsetX}px`;
1649
3243
  el.style.top = `${f + node.a11yOffsetY}px`;
1650
3244
  el.style.width = `${node.width}px`;
1651
3245
  el.style.height = `${node.height}px`;
1652
3246
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
3247
+ const visible = this.projectionBoxVisible(node, worldTf, 0);
3248
+ const display = visible ? "" : "none";
3249
+ if (el.style.display !== display) el.style.display = display;
1653
3250
  }
1654
3251
  }
1655
3252
  this.syncContentProjection(node);
@@ -1717,26 +3314,24 @@ var Scene = (_class2 = class _Scene {
1717
3314
  }
1718
3315
  syncContentProjection(node) {
1719
3316
  if (!this.contentProjectionEnabled || !this.a11yRoot) return;
1720
- const projection = node.getContentProjection();
1721
3317
  let el = this.contentElements.get(node.id);
1722
- if (!projection || !projection.text) {
3318
+ const releaseProjectionEl = () => {
1723
3319
  if (el) {
1724
3320
  this.clearContentGridState(node.id, el);
1725
3321
  el.remove();
1726
3322
  this.contentElements.delete(node.id);
1727
3323
  this.a11yNeedsReorder = true;
1728
3324
  }
1729
- return;
1730
- }
3325
+ };
1731
3326
  const worldTf = node.getWorldTransform();
1732
3327
  const margin = _nullishCoalesce(this.contentProjectionMargin, () => ( this.height));
1733
3328
  if (Number.isFinite(margin) && !this.projectionBoxVisible(node, worldTf, margin)) {
1734
- if (el) {
1735
- this.clearContentGridState(node.id, el);
1736
- el.remove();
1737
- this.contentElements.delete(node.id);
1738
- this.a11yNeedsReorder = true;
1739
- }
3329
+ releaseProjectionEl();
3330
+ return;
3331
+ }
3332
+ const projection = node.getContentProjection();
3333
+ if (!projection || !projection.text) {
3334
+ releaseProjectionEl();
1740
3335
  return;
1741
3336
  }
1742
3337
  if (!el) {
@@ -1755,7 +3350,7 @@ var Scene = (_class2 = class _Scene {
1755
3350
  el.addEventListener(
1756
3351
  "wheel",
1757
3352
  (e2) => {
1758
- node.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)("wheel", node, e2));
3353
+ node.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)("wheel", node, e2));
1759
3354
  },
1760
3355
  { passive: false }
1761
3356
  );
@@ -1776,35 +3371,48 @@ var Scene = (_class2 = class _Scene {
1776
3371
  fallbackLineHeight: _nullishCoalesce(projection.lineHeight, () => ( 16))
1777
3372
  });
1778
3373
  if (el.dataset.vectoProjectionLines !== signature) {
1779
- this.releaseContentSelectionForRebuild(el);
1780
- el.replaceChildren();
1781
- for (let index = 0; index < lines.length; index++) {
1782
- const line = lines[index];
1783
- const lineElement = document.createElement("span");
1784
- const lineFont = _nullishCoalesce(_nullishCoalesce(line.font, () => ( projection.font)), () => ( ""));
1785
- const lineHeight2 = _nullishCoalesce(_nullishCoalesce(line.lineHeight, () => ( projection.lineHeight)), () => ( 16));
1786
- lineElement.style.position = "absolute";
1787
- lineElement.dir = "auto";
1788
- lineElement.style.left = `${line.x}px`;
1789
- lineElement.style.top = `${line.y + line.baseline - _chunk2Z23LTH3js.cssLineBoxBaseline.call(void 0, lineFont, lineHeight2)}px`;
1790
- lineElement.style.whiteSpace = "pre";
1791
- if (lineFont) lineElement.style.font = lineFont;
1792
- lineElement.style.lineHeight = `${lineHeight2}px`;
1793
- const separator = _nullishCoalesce(line.separatorAfter, () => ( (index < lines.length - 1 ? "\n" : "")));
1794
- if (line.runs && line.runs.length > 0) {
1795
- for (let runIndex = 0; runIndex < line.runs.length; runIndex++) {
1796
- const run = line.runs[runIndex];
1797
- const runElement = document.createElement("span");
1798
- runElement.textContent = run.text + (runIndex === line.runs.length - 1 ? separator : "");
1799
- if (run.font) runElement.style.font = run.font;
1800
- runElement.style.lineHeight = `${lineHeight2}px`;
1801
- lineElement.appendChild(runElement);
3374
+ this.preserveContentSelectionAcrossRebuild(el, () => {
3375
+ el.replaceChildren();
3376
+ for (let index = 0; index < lines.length; index++) {
3377
+ const line = lines[index];
3378
+ const lineElement = document.createElement("span");
3379
+ const lineFont = _nullishCoalesce(_nullishCoalesce(line.font, () => ( projection.font)), () => ( ""));
3380
+ const lineHeight2 = _nullishCoalesce(_nullishCoalesce(line.lineHeight, () => ( projection.lineHeight)), () => ( 16));
3381
+ const hasPositionedRuns = !!line.runs && line.runs.some((run) => run.x !== void 0);
3382
+ lineElement.style.position = "absolute";
3383
+ lineElement.dir = hasPositionedRuns ? "ltr" : "auto";
3384
+ lineElement.style.left = `${line.x}px`;
3385
+ lineElement.style.top = `${line.y + line.baseline - _text.cssLineBoxBaseline.call(void 0, lineFont, lineHeight2)}px`;
3386
+ lineElement.style.whiteSpace = "pre";
3387
+ if (lineFont) lineElement.style.font = lineFont;
3388
+ lineElement.style.lineHeight = `${lineHeight2}px`;
3389
+ const separator = _nullishCoalesce(line.separatorAfter, () => ( (index < lines.length - 1 ? "\n" : "")));
3390
+ if (line.runs && line.runs.length > 0) {
3391
+ const positioned = line.runs.some((run) => run.x !== void 0);
3392
+ for (let runIndex = 0; runIndex < line.runs.length; runIndex++) {
3393
+ const run = line.runs[runIndex];
3394
+ const runElement = document.createElement("span");
3395
+ runElement.textContent = run.text + (runIndex === line.runs.length - 1 ? separator : "");
3396
+ if (run.font) runElement.style.font = run.font;
3397
+ runElement.style.lineHeight = `${lineHeight2}px`;
3398
+ if (positioned && run.x !== void 0) {
3399
+ runElement.style.position = "absolute";
3400
+ runElement.style.left = `${run.x - line.x}px`;
3401
+ runElement.style.top = "0";
3402
+ if (run.width !== void 0) runElement.style.width = `${run.width}px`;
3403
+ runElement.style.whiteSpace = "pre";
3404
+ runElement.style.verticalAlign = "top";
3405
+ runElement.style.unicodeBidi = "isolate";
3406
+ runElement.dir = "ltr";
3407
+ }
3408
+ lineElement.appendChild(runElement);
3409
+ }
3410
+ } else {
3411
+ lineElement.textContent = line.text + separator;
1802
3412
  }
1803
- } else {
1804
- lineElement.textContent = line.text + separator;
3413
+ el.appendChild(lineElement);
1805
3414
  }
1806
- el.appendChild(lineElement);
1807
- }
3415
+ });
1808
3416
  el.dataset.vectoProjectionLines = signature;
1809
3417
  }
1810
3418
  } else {
@@ -1838,7 +3446,7 @@ var Scene = (_class2 = class _Scene {
1838
3446
  const { a, b, c, d, e, f } = worldTf;
1839
3447
  const contentX = _nullishCoalesce(projection.contentX, () => ( 0));
1840
3448
  const contentY = _nullishCoalesce(projection.contentY, () => ( 0));
1841
- const baselineOffset = lines && lines.length > 0 ? 0 : projection.baseline === void 0 ? 0 : projection.baseline - _chunk2Z23LTH3js.cssLineBoxBaseline.call(void 0, font, _nullishCoalesce(projection.lineHeight, () => ( 16)));
3449
+ const baselineOffset = lines && lines.length > 0 ? 0 : projection.baseline === void 0 ? 0 : projection.baseline - _text.cssLineBoxBaseline.call(void 0, font, _nullishCoalesce(projection.lineHeight, () => ( 16)));
1842
3450
  const localY = contentY + baselineOffset;
1843
3451
  el.style.left = `${e + a * contentX + c * localY}px`;
1844
3452
  el.style.top = `${f + b * contentX + d * localY}px`;
@@ -1867,15 +3475,15 @@ var Scene = (_class2 = class _Scene {
1867
3475
  for (let lineIndex = 0; lineIndex < grid.lines.length; lineIndex++) {
1868
3476
  const gridLine = grid.lines[lineIndex];
1869
3477
  const projectedLine = projectionLines[lineIndex];
1870
- const lineHeight = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _75 => _75.lineHeight]), () => ( grid.lineHeight));
1871
- const baseline = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _76 => _76.baseline]), () => ( grid.baseline));
1872
- const lineFont = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _77 => _77.font]), () => ( grid.font));
3478
+ const lineHeight = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _102 => _102.lineHeight]), () => ( grid.lineHeight));
3479
+ const baseline = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _103 => _103.baseline]), () => ( grid.baseline));
3480
+ const lineFont = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _104 => _104.font]), () => ( grid.font));
1873
3481
  const lineElement = document.createElement("span");
1874
3482
  lineElement.dir = "ltr";
1875
3483
  lineElement.dataset.vectoGridLine = `${lineIndex}`;
1876
3484
  lineElement.style.position = "absolute";
1877
- lineElement.style.left = `${_nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _78 => _78.x]), () => ( 0))}px`;
1878
- lineElement.style.top = `${(_nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _79 => _79.y]), () => ( lineIndex * grid.lineHeight))) + baseline - _chunk2Z23LTH3js.cssLineBoxBaseline.call(void 0, lineFont, lineHeight)}px`;
3485
+ lineElement.style.left = `${_nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _105 => _105.x]), () => ( 0))}px`;
3486
+ lineElement.style.top = `${(_nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _106 => _106.y]), () => ( lineIndex * grid.lineHeight))) + baseline - _text.cssLineBoxBaseline.call(void 0, lineFont, lineHeight)}px`;
1879
3487
  lineElement.style.width = `${gridLine.width}px`;
1880
3488
  lineElement.style.height = `${lineHeight}px`;
1881
3489
  lineElement.style.whiteSpace = "pre";
@@ -1971,7 +3579,7 @@ var Scene = (_class2 = class _Scene {
1971
3579
  if (previous !== void 0 && typeof cancelAnimationFrame === "function") {
1972
3580
  cancelAnimationFrame(previous);
1973
3581
  }
1974
- _optionalChain([this, 'access', _80 => _80.contentGridCalibrationProbes, 'access', _81 => _81.get, 'call', _82 => _82(entityId), 'optionalAccess', _83 => _83.remove, 'call', _84 => _84()]);
3582
+ _optionalChain([this, 'access', _107 => _107.contentGridCalibrationProbes, 'access', _108 => _108.get, 'call', _109 => _109(entityId), 'optionalAccess', _110 => _110.remove, 'call', _111 => _111()]);
1975
3583
  this.contentGridCalibrationProbes.delete(entityId);
1976
3584
  const calibrationStart = typeof performance !== "undefined" ? performance.now() : 0;
1977
3585
  const probe = document.createElement("div");
@@ -2001,7 +3609,7 @@ var Scene = (_class2 = class _Scene {
2001
3609
  const sourceLength = Number(_nullishCoalesce(target.dataset.vectoGridSourceLength, () => ( 0)));
2002
3610
  const targetWidth = Number(_nullishCoalesce(target.dataset.vectoGridAdvance, () => ( 0)));
2003
3611
  if (sourceLength <= 0 || targetWidth <= 0) continue;
2004
- const sourceText = _nullishCoalesce(_optionalChain([target, 'access', _85 => _85.textContent, 'optionalAccess', _86 => _86.slice, 'call', _87 => _87(0, sourceLength)]), () => ( ""));
3612
+ const sourceText = _nullishCoalesce(_optionalChain([target, 'access', _112 => _112.textContent, 'optionalAccess', _113 => _113.slice, 'call', _114 => _114(0, sourceLength)]), () => ( ""));
2005
3613
  if (!sourceText) continue;
2006
3614
  const measurementKey = JSON.stringify([
2007
3615
  target.style.font,
@@ -2027,7 +3635,12 @@ var Scene = (_class2 = class _Scene {
2027
3635
  const source = document.createTextNode(sourceText);
2028
3636
  carrier.appendChild(source);
2029
3637
  probe.appendChild(carrier);
2030
- const measurement = { targets: [target], targetWidth, sourceLength, source };
3638
+ const measurement = {
3639
+ targets: [target],
3640
+ targetWidth,
3641
+ sourceLength,
3642
+ source
3643
+ };
2031
3644
  measurements.push(measurement);
2032
3645
  measurementsByKey.set(measurementKey, measurement);
2033
3646
  }
@@ -2126,6 +3739,7 @@ var Scene = (_class2 = class _Scene {
2126
3739
  }
2127
3740
  }
2128
3741
  if (el.parentNode === this.a11yRoot) {
3742
+ this.preserveFocusOnRemoval(el);
2129
3743
  this.a11yRoot.removeChild(el);
2130
3744
  }
2131
3745
  this.a11yElements.delete(id);
@@ -2135,6 +3749,7 @@ var Scene = (_class2 = class _Scene {
2135
3749
  this.a11yNeedsReorder = true;
2136
3750
  }
2137
3751
  if (!this.a11yNeedsReorder) return;
3752
+ this.sortNormalElementsVisually();
2138
3753
  const fullLen = this.fullViewportElements.length;
2139
3754
  const normalLen = this.normalElements.length;
2140
3755
  const totalLen = fullLen + normalLen;
@@ -2147,18 +3762,73 @@ var Scene = (_class2 = class _Scene {
2147
3762
  }
2148
3763
  this.a11yNeedsReorder = false;
2149
3764
  }
3765
+ /**
3766
+ * Reorder `normalElements` (in place) into visual reading order using the
3767
+ * world positions `syncA11y` already wrote to each element's inline style
3768
+ * (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an
3769
+ * element belongs to the current row while its top is above the row's
3770
+ * running bottom edge), then sorted within a row by `left` — ascending for
3771
+ * `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the
3772
+ * same position keep their scene-graph (collection) order as a tiebreak.
3773
+ */
3774
+ sortNormalElementsVisually() {
3775
+ const els = this.normalElements;
3776
+ if (els.length < 2) return;
3777
+ const rtl = this._readingDirection === "rtl";
3778
+ const topOf = (el) => Number.parseFloat(el.style.top) || 0;
3779
+ const leftOf = (el) => Number.parseFloat(el.style.left) || 0;
3780
+ const heightOf = (el) => Math.max(Number.parseFloat(el.style.height) || 0, 4);
3781
+ const order = els.map((el, i) => ({
3782
+ el,
3783
+ i,
3784
+ top: topOf(el),
3785
+ left: leftOf(el)
3786
+ }));
3787
+ order.sort((p, q) => p.top - q.top || p.i - q.i);
3788
+ const sorted = [];
3789
+ let rowStart = 0;
3790
+ let rowBottom = order.length ? order[0].top + heightOf(order[0].el) : 0;
3791
+ const flushRow = (end) => {
3792
+ const row = order.slice(rowStart, end);
3793
+ row.sort((p, q) => (rtl ? q.left - p.left : p.left - q.left) || p.i - q.i);
3794
+ for (const r of row) sorted.push(r.el);
3795
+ };
3796
+ for (let k = 1; k < order.length; k++) {
3797
+ if (order[k].top < rowBottom) {
3798
+ rowBottom = Math.max(rowBottom, order[k].top + heightOf(order[k].el));
3799
+ } else {
3800
+ flushRow(k);
3801
+ rowStart = k;
3802
+ rowBottom = order[k].top + heightOf(order[k].el);
3803
+ }
3804
+ }
3805
+ flushRow(order.length);
3806
+ for (let i = 0; i < sorted.length; i++) els[i] = sorted[i];
3807
+ }
2150
3808
  /** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
2151
3809
  syncOverlayGeometry() {
2152
3810
  const parent = this.canvas.parentElement;
2153
3811
  if (!parent) return;
2154
- const canvasRect = _optionalChain([this, 'access', _88 => _88.canvas, 'access', _89 => _89.getBoundingClientRect, 'optionalCall', _90 => _90()]);
2155
- const parentRect = _optionalChain([parent, 'access', _91 => _91.getBoundingClientRect, 'optionalCall', _92 => _92()]);
2156
- const cssWidth = _optionalChain([canvasRect, 'optionalAccess', _93 => _93.width]) || this.canvas.clientWidth || this.width;
2157
- const cssHeight = _optionalChain([canvasRect, 'optionalAccess', _94 => _94.height]) || this.canvas.clientHeight || this.height;
2158
- const left = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _95 => _95.left]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _96 => _96.left]), () => ( 0))) - (parent.clientLeft || 0) + parent.scrollLeft;
2159
- const top = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _97 => _97.top]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _98 => _98.top]), () => ( 0))) - (parent.clientTop || 0) + parent.scrollTop;
3812
+ const canvasRect = _optionalChain([this, 'access', _115 => _115.canvas, 'access', _116 => _116.getBoundingClientRect, 'optionalCall', _117 => _117()]);
3813
+ const parentRect = _optionalChain([parent, 'access', _118 => _118.getBoundingClientRect, 'optionalCall', _119 => _119()]);
3814
+ const cssWidth = _optionalChain([canvasRect, 'optionalAccess', _120 => _120.width]) || this.canvas.clientWidth || this.width;
3815
+ const cssHeight = _optionalChain([canvasRect, 'optionalAccess', _121 => _121.height]) || this.canvas.clientHeight || this.height;
3816
+ const left = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _122 => _122.left]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _123 => _123.left]), () => ( 0))) - (parent.clientLeft || 0) + parent.scrollLeft;
3817
+ const top = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _124 => _124.top]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _125 => _125.top]), () => ( 0))) - (parent.clientTop || 0) + parent.scrollTop;
2160
3818
  const scaleX = this.width > 0 ? cssWidth / this.width : 1;
2161
3819
  const scaleY = this.height > 0 ? cssHeight / this.height : 1;
3820
+ const prev = this._overlayGeometry;
3821
+ if (prev !== null && prev.left === left && prev.top === top && prev.cssWidth === cssWidth && prev.cssHeight === cssHeight && prev.width === this.width && prev.height === this.height) {
3822
+ return;
3823
+ }
3824
+ this._overlayGeometry = {
3825
+ left,
3826
+ top,
3827
+ cssWidth,
3828
+ cssHeight,
3829
+ width: this.width,
3830
+ height: this.height
3831
+ };
2162
3832
  for (const root of [this.a11yRoot, this.portalRoot]) {
2163
3833
  if (!root) continue;
2164
3834
  root.style.left = `${left}px`;
@@ -2226,6 +3896,7 @@ var Scene = (_class2 = class _Scene {
2226
3896
  if (portal.domElement.parentElement !== this.portalRoot) {
2227
3897
  this.portalRoot.appendChild(portal.domElement);
2228
3898
  }
3899
+ portal.attachDOMBindings();
2229
3900
  if (!portal.domElement.hasAttribute("data-vecto-id")) {
2230
3901
  portal.domElement.setAttribute("data-vecto-id", portal.id);
2231
3902
  }
@@ -2266,6 +3937,7 @@ var Scene = (_class2 = class _Scene {
2266
3937
  const portal = this.portalEntities.get(oldId);
2267
3938
  if (portal) {
2268
3939
  if (portal.domElement.parentElement === this.portalRoot && (!portal.scene || portal.scene === this)) {
3940
+ portal.releaseDOMBindings();
2269
3941
  portal.domElement.remove();
2270
3942
  }
2271
3943
  this.portalEntities.delete(oldId);
@@ -2281,13 +3953,14 @@ var Scene = (_class2 = class _Scene {
2281
3953
  * (and {@link respectReducedMotion} is on). `0` means uncapped.
2282
3954
  */
2283
3955
  effectiveMaxFPS() {
2284
- const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _99 => _99.reducedMotionQuery, 'optionalAccess', _100 => _100.matches]);
3956
+ const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _126 => _126.reducedMotionQuery, 'optionalAccess', _127 => _127.matches]);
2285
3957
  if (reduced)
2286
3958
  return this.maxFPS > 0 ? Math.min(this.maxFPS, REDUCED_MOTION_FPS) : REDUCED_MOTION_FPS;
2287
3959
  return this.maxFPS;
2288
3960
  }
2289
3961
  loop(time) {
2290
3962
  if (!this.isRunning) return;
3963
+ if (!this._canvasOnScreen) return;
2291
3964
  let cap = this.effectiveMaxFPS();
2292
3965
  const isIdle = !this.dirty && !this.frameHadAnimation;
2293
3966
  if (isIdle && this.autoThrottle && this.renderMode === "always" && this.maxFPS > 0) {
@@ -2303,6 +3976,7 @@ var Scene = (_class2 = class _Scene {
2303
3976
  const nominal = 1e3 / cap;
2304
3977
  if (Math.abs(dt - nominal) < nominal * 0.3) dt = nominal;
2305
3978
  }
3979
+ if (dt > _Scene.MAX_FRAME_DT) dt = _Scene.MAX_FRAME_DT;
2306
3980
  this.lastTime = time;
2307
3981
  if (this.renderMode === "onDemand" && isIdle) {
2308
3982
  this._skippedFrames++;
@@ -2346,6 +4020,7 @@ var Scene = (_class2 = class _Scene {
2346
4020
  * @param time - Current absolute time in milliseconds (default 0).
2347
4021
  */
2348
4022
  render(renderer, dt = 0, time = 0) {
4023
+ if (_optionalChain([renderer, 'access', _128 => _128.isContextLost, 'optionalCall', _129 => _129()])) return;
2349
4024
  const isMainRenderer = renderer === this.renderer;
2350
4025
  if (isMainRenderer && this.a11yRoot && this.canvas.parentElement) {
2351
4026
  const parentStyle = this.canvas.parentElement.style;
@@ -2355,23 +4030,13 @@ var Scene = (_class2 = class _Scene {
2355
4030
  this.syncOverlayGeometry();
2356
4031
  }
2357
4032
  if (isMainRenderer) {
4033
+ this.currentFrame++;
2358
4034
  this.renderOrderCounter = 0;
2359
4035
  this.a11yRenderOrders.clear();
2360
4036
  this.activePortalsThisFrame.clear();
4037
+ this._tickBatchedDrivers(dt);
2361
4038
  }
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
- }
4039
+ const computeEntities = this._computeEntitiesFor(this._structureVersion);
2375
4040
  if (computeEntities.length > 0) {
2376
4041
  const isMainRenderPath = renderer === this.renderer;
2377
4042
  if (isMainRenderPath && !this.device && !this.webgpuDisabled && !this.initializingWebGPU && !this.deviceLost) {
@@ -2469,7 +4134,11 @@ var Scene = (_class2 = class _Scene {
2469
4134
  my = -9999;
2470
4135
  }
2471
4136
  }
2472
- entity.updateCPU(dt / 1e3, mx, my, this.width, this.height);
4137
+ if (this._particleWasm) {
4138
+ entity.stepWithBackend(this._particleWasm, dt / 1e3, mx, my, this.width, this.height);
4139
+ } else {
4140
+ entity.updateCPU(dt / 1e3, mx, my, this.width, this.height);
4141
+ }
2473
4142
  }
2474
4143
  }
2475
4144
  } else if (isMainRenderer) {
@@ -2477,37 +4146,75 @@ var Scene = (_class2 = class _Scene {
2477
4146
  }
2478
4147
  renderer.clear();
2479
4148
  if (isMainRenderer) {
2480
- _optionalChain([this, 'access', _101 => _101.pointRenderer, 'optionalAccess', _102 => _102.begin, 'call', _103 => _103()]);
4149
+ _optionalChain([this, 'access', _130 => _130.pointRenderer, 'optionalAccess', _131 => _131.begin, 'call', _132 => _132()]);
2481
4150
  }
2482
4151
  const vw = this.width;
2483
4152
  const vh = this.height;
2484
4153
  let walkHadAnimation = false;
2485
4154
  let walkHadInteractive = false;
2486
- const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
2487
- if (isMainRenderer) {
4155
+ const runUpdate = (node) => {
4156
+ const overridesUpdate = node.update !== _chunkDUYB4GX4js.Entity.prototype.update;
4157
+ let pending = node.hasPendingAnimations();
4158
+ if (pending || overridesUpdate) {
2488
4159
  node.update(dt, time);
2489
- if (!walkHadAnimation && node.hasPendingAnimations()) walkHadAnimation = true;
2490
- if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
2491
- if (this._devActive && this._devFrameCount % 120 === 0) {
2492
- if (node.update !== _chunk2Z23LTH3js.Entity.prototype.update && node.hasPendingAnimations === _chunk2Z23LTH3js.Entity.prototype.hasPendingAnimations) {
2493
- this._devWarn(
2494
- `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.`
2495
- );
2496
- }
4160
+ pending = node.hasPendingAnimations();
4161
+ }
4162
+ if (pending) walkHadAnimation = true;
4163
+ if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
4164
+ if (this._devActive && this._devFrameCount % 120 === 0) {
4165
+ if (overridesUpdate && node.hasPendingAnimations === _chunkDUYB4GX4js.Entity.prototype.hasPendingAnimations) {
4166
+ this._devWarn(
4167
+ `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.`
4168
+ );
2497
4169
  }
2498
4170
  }
2499
- const cos = Math.cos(node.rotation);
2500
- const sin = Math.sin(node.rotation);
2501
- const te = pa * node.x + pc * node.y + pe;
2502
- const tf = pb * node.x + pd * node.y + pf;
2503
- const sxCos = node.scaleX * cos;
2504
- const sxSin = node.scaleX * sin;
2505
- const syCos = node.scaleY * cos;
2506
- const sySin = node.scaleY * sin;
2507
- const a = pa * sxCos + pc * sySin;
2508
- const b = pb * sxCos + pd * sySin;
2509
- const c = pa * -sxSin + pc * syCos;
2510
- const d = pb * -sxSin + pd * syCos;
4171
+ };
4172
+ const wasmMain = isMainRenderer && this._wasm !== null && this._transformBackend === "wasm";
4173
+ if (wasmMain) {
4174
+ const updateWalk = (node) => {
4175
+ runUpdate(node);
4176
+ const kids = node.children;
4177
+ for (let i = 0; i < kids.length; i++) updateWalk(kids[i]);
4178
+ };
4179
+ updateWalk(this.root);
4180
+ for (const overlay of this.overlayRoot.children) updateWalk(overlay);
4181
+ }
4182
+ const wasmWorld = wasmMain ? this._syncWasmStore() : null;
4183
+ const wasmSlotEntity = this._slotEntity;
4184
+ const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
4185
+ if (isMainRenderer && !wasmMain) {
4186
+ runUpdate(node);
4187
+ }
4188
+ let a;
4189
+ let b;
4190
+ let c;
4191
+ let d;
4192
+ let te;
4193
+ let tf;
4194
+ const slot = node._storeSlot;
4195
+ if (wasmWorld !== null && slot >= 0 && wasmSlotEntity[slot] === node) {
4196
+ a = wasmWorld.wa[slot];
4197
+ b = wasmWorld.wb[slot];
4198
+ c = wasmWorld.wc[slot];
4199
+ d = wasmWorld.wd[slot];
4200
+ te = wasmWorld.we[slot];
4201
+ tf = wasmWorld.wf[slot];
4202
+ } else {
4203
+ const trig = node._getTrig();
4204
+ const cos = trig.cos;
4205
+ const sin = trig.sin;
4206
+ te = pa * node.x + pc * node.y + pe;
4207
+ tf = pb * node.x + pd * node.y + pf;
4208
+ const sxCos = node.scaleX * cos;
4209
+ const sxSin = node.scaleX * sin;
4210
+ const syCos = node.scaleY * cos;
4211
+ const sySin = node.scaleY * sin;
4212
+ a = pa * sxCos + pc * sySin;
4213
+ b = pb * sxCos + pd * sySin;
4214
+ c = pa * -sxSin + pc * syCos;
4215
+ d = pb * -sxSin + pd * syCos;
4216
+ }
4217
+ if (isMainRenderer) node._setWorldCache(a, b, c, d, te, tf, this.currentFrame);
2511
4218
  const worldScaleX = Math.hypot(a, b);
2512
4219
  const worldScaleY = Math.hypot(c, d);
2513
4220
  const worldOpacity = parentOpacity * node.opacity;
@@ -2625,9 +4332,9 @@ var Scene = (_class2 = class _Scene {
2625
4332
  }
2626
4333
  renderer.flush();
2627
4334
  if (isMainRenderer) {
2628
- _optionalChain([this, 'access', _104 => _104.pointRenderer, 'optionalAccess', _105 => _105.flush, 'call', _106 => _106()]);
4335
+ _optionalChain([this, 'access', _133 => _133.pointRenderer, 'optionalAccess', _134 => _134.flush, 'call', _135 => _135()]);
2629
4336
  }
2630
- _optionalChain([renderer, 'access', _107 => _107.present, 'optionalCall', _108 => _108()]);
4337
+ _optionalChain([renderer, 'access', _136 => _136.present, 'optionalCall', _137 => _137()]);
2631
4338
  if (this._devActive) {
2632
4339
  this._devFrameCount++;
2633
4340
  this._devRunChecks();
@@ -2637,7 +4344,7 @@ var Scene = (_class2 = class _Scene {
2637
4344
  * Export the current scene state to a lightweight, flat SVG XML string.
2638
4345
  */
2639
4346
  toSVG() {
2640
- const renderer = new (0, _chunkBEUIB3U7js.SVGRenderer)(this.width, this.height);
4347
+ const renderer = new (0, _chunkIYPLG4Z4js.SVGRenderer)(this.width, this.height);
2641
4348
  this.render(renderer, 0, 0);
2642
4349
  return renderer.toXMLString();
2643
4350
  }
@@ -2656,12 +4363,25 @@ var Scene = (_class2 = class _Scene {
2656
4363
  this.pointRenderer.maxDPR = this.maxDPR;
2657
4364
  this.pointRenderer.resize(width, height);
2658
4365
  }
2659
- if (this.gpuCanvas) {
2660
- this.gpuCanvas.width = width;
2661
- this.gpuCanvas.height = height;
2662
- }
4366
+ if (this.gpuCanvas) this.sizeGpuCanvas(this.gpuCanvas, width, height);
2663
4367
  this.markDirty();
2664
4368
  }
4369
+ /** Effective device pixel ratio, matching CanvasRenderer: real DPR clamped to
4370
+ * `maxDPR` when set. */
4371
+ effectiveDPR() {
4372
+ const real = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
4373
+ return this.maxDPR !== void 0 ? Math.min(real, this.maxDPR) : real;
4374
+ }
4375
+ /** Size the WebGPU particle canvas: backing store at logical × DPR, CSS box at
4376
+ * the logical size. Sizing the backing store in logical px (the old
4377
+ * behavior) left it rasterized at 1× and CSS-stretched — blurry on HiDPI. */
4378
+ sizeGpuCanvas(gpuCanvas, width, height) {
4379
+ const dpr = this.effectiveDPR();
4380
+ gpuCanvas.width = Math.max(1, Math.round(width * dpr));
4381
+ gpuCanvas.height = Math.max(1, Math.round(height * dpr));
4382
+ gpuCanvas.style.width = `${width}px`;
4383
+ gpuCanvas.style.height = `${height}px`;
4384
+ }
2665
4385
  /**
2666
4386
  * Gets the accessibility DOM element projected for the given entity ID.
2667
4387
  */
@@ -2684,6 +4404,9 @@ var Scene = (_class2 = class _Scene {
2684
4404
  findEntityAt(x, y) {
2685
4405
  const overlayHit = this.findHitRecursively(this.overlayRoot, x, y);
2686
4406
  if (overlayHit) return overlayHit;
4407
+ if (this._hitWasm && this._ensureHitGrid()) {
4408
+ return this._findEntityAtWasm(x, y);
4409
+ }
2687
4410
  return this.findHitRecursively(this.root, x, y);
2688
4411
  }
2689
4412
  /** Submit one transparent clear pass when particle content lingers on the GPU canvas. */
@@ -2703,7 +4426,7 @@ var Scene = (_class2 = class _Scene {
2703
4426
  });
2704
4427
  pass.end();
2705
4428
  this.device.queue.submit([encoder.finish()]);
2706
- } catch (e5) {
4429
+ } catch (e18) {
2707
4430
  }
2708
4431
  this.gpuHasContent = false;
2709
4432
  }
@@ -2718,8 +4441,7 @@ var Scene = (_class2 = class _Scene {
2718
4441
  const device = await adapter.requestDevice();
2719
4442
  if (typeof document !== "undefined" && !this.gpuCanvas) {
2720
4443
  const gpuCanvas = document.createElement("canvas");
2721
- gpuCanvas.width = this.width;
2722
- gpuCanvas.height = this.height;
4444
+ this.sizeGpuCanvas(gpuCanvas, this.width, this.height);
2723
4445
  gpuCanvas.style.position = "absolute";
2724
4446
  gpuCanvas.style.top = "0";
2725
4447
  gpuCanvas.style.left = "0";
@@ -2729,6 +4451,7 @@ var Scene = (_class2 = class _Scene {
2729
4451
  this.canvas.parentElement.appendChild(gpuCanvas);
2730
4452
  }
2731
4453
  this.gpuCanvas = gpuCanvas;
4454
+ this._overlayGeometry = null;
2732
4455
  this.gpuContext = gpuCanvas.getContext("webgpu");
2733
4456
  }
2734
4457
  if (this.gpuContext) {
@@ -2825,42 +4548,97 @@ var Scene = (_class2 = class _Scene {
2825
4548
  }
2826
4549
  }
2827
4550
  }
2828
- findHitRecursively(node, x, y) {
4551
+ findHitRecursively(node, x, y, clip = null) {
4552
+ if (node.opacity <= 0) return null;
4553
+ let childClip = clip;
4554
+ if (node.clipChildren) {
4555
+ const box = node.getWorldBounds();
4556
+ childClip = clip ? intersectBounds(clip, box) : box;
4557
+ }
2829
4558
  for (let i = node.children.length - 1; i >= 0; i--) {
2830
- const hit = this.findHitRecursively(node.children[i], x, y);
4559
+ const hit = this.findHitRecursively(node.children[i], x, y, childClip);
2831
4560
  if (hit) return hit;
2832
4561
  }
2833
- if (node.isPointInside && node.isPointInside(x, y)) {
4562
+ if (node.isPointInside && node.isPointInside(x, y) && (!clip || pointInBounds(clip, x, y)) && !this.isPointerTransparent(node)) {
2834
4563
  return node;
2835
4564
  }
2836
4565
  return null;
2837
4566
  }
2838
- }, _class2.__initStatic(), _class2.__initStatic2(), _class2.__initStatic3(), _class2);
4567
+ /** Whether `node` opts out of being a pointer hit target: a disabled control
4568
+ * or an explicit `pointerEvents: 'none'` in its a11y attributes. Its children
4569
+ * are still walked (a transparent container can hold hittable descendants). */
4570
+ isPointerTransparent(node) {
4571
+ const attrs = node.getA11yAttributes();
4572
+ return attrs.disabled === true || attrs.pointerEvents === "none";
4573
+ }
4574
+ /**
4575
+ * Whether a confirmed geometric hit on `node` at world `(x, y)` is a REAL hit,
4576
+ * applying the same visibility/input gating as {@link findHitRecursively} but
4577
+ * from a flat candidate (the WASM grid has no recursion clip-stack): the node
4578
+ * and all ancestors are visible (`opacity > 0`), the point lies inside every
4579
+ * `clipChildren` ancestor's world box, and the node isn't pointer-transparent
4580
+ * (disabled / `pointerEvents: 'none'`). Keeps the WASM and JS hit paths in
4581
+ * lockstep so they return the same entity.
4582
+ */
4583
+ isHitEligible(node, x, y) {
4584
+ if (this.isPointerTransparent(node)) return false;
4585
+ if (node.opacity <= 0) return false;
4586
+ for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
4587
+ if (ancestor.opacity <= 0) return false;
4588
+ if (ancestor.clipChildren && ancestor.width > 0 && ancestor.height > 0) {
4589
+ const local = ancestor.worldToLocal(x, y);
4590
+ if (!local || local.x < 0 || local.y < 0 || local.x > ancestor.width || local.y > ancestor.height) {
4591
+ return false;
4592
+ }
4593
+ }
4594
+ }
4595
+ return true;
4596
+ }
4597
+ }, _class6.__initStatic(), _class6.__initStatic2(), _class6.__initStatic3(), _class6.__initStatic4(), _class6);
4598
+ function intersectBounds(a, b) {
4599
+ const x = Math.max(a.x, b.x);
4600
+ const y = Math.max(a.y, b.y);
4601
+ const right = Math.min(a.x + a.width, b.x + b.width);
4602
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
4603
+ return {
4604
+ x,
4605
+ y,
4606
+ width: Math.max(0, right - x),
4607
+ height: Math.max(0, bottom - y)
4608
+ };
4609
+ }
4610
+ function pointInBounds(b, x, y) {
4611
+ return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
4612
+ }
2839
4613
 
2840
4614
  // src/components/TextEntity.ts
4615
+
4616
+
4617
+
4618
+ var _layout = require('@vectojs/layout'); _createStarExport(_layout);
2841
4619
  var sharedMeasurer;
2842
4620
  function defaultMeasurer() {
2843
- if (sharedMeasurer === void 0) sharedMeasurer = _chunkBA5HUUDFjs.createCanvasMeasurer.call(void 0, "sans-serif");
4621
+ if (sharedMeasurer === void 0) sharedMeasurer = _layout.createCanvasMeasurer.call(void 0, "sans-serif");
2844
4622
  return sharedMeasurer;
2845
4623
  }
2846
- var TextEntity = (_class3 = class extends _chunk2Z23LTH3js.Entity {
4624
+ var TextEntity = (_class7 = class extends _chunkDUYB4GX4js.Entity {
2847
4625
 
2848
4626
 
2849
4627
 
2850
4628
 
2851
- __init71() {this.nodes = []}
4629
+ __init124() {this.nodes = []}
2852
4630
 
2853
- __init72() {this.fillStyle = "#94a3b8"}
2854
- __init73() {this.strokeStyle = null}
2855
- __init74() {this.hoveredFillStyle = "#ffffff"}
2856
- __init75() {this.lineWidth = 1}
2857
- __init76() {this.isHovered = false}
4631
+ __init125() {this.fillStyle = "#94a3b8"}
4632
+ __init126() {this.strokeStyle = null}
4633
+ __init127() {this.hoveredFillStyle = "#ffffff"}
4634
+ __init128() {this.lineWidth = 1}
4635
+ __init129() {this.isHovered = false}
2858
4636
  constructor(text, atlas, maxWidth, fontSize = 24) {
2859
- super();_class3.prototype.__init71.call(this);_class3.prototype.__init72.call(this);_class3.prototype.__init73.call(this);_class3.prototype.__init74.call(this);_class3.prototype.__init75.call(this);_class3.prototype.__init76.call(this);;
4637
+ super();_class7.prototype.__init124.call(this);_class7.prototype.__init125.call(this);_class7.prototype.__init126.call(this);_class7.prototype.__init127.call(this);_class7.prototype.__init128.call(this);_class7.prototype.__init129.call(this);;
2860
4638
  this.text = text;
2861
4639
  this.atlas = atlas;
2862
4640
  this.fontSize = fontSize;
2863
- this.layout = new (0, _chunkBA5HUUDFjs.LayoutEngine)(maxWidth, 1e4, defaultMeasurer());
4641
+ this.layout = new (0, _layout.LayoutEngine)(maxWidth, 1e4, defaultMeasurer());
2864
4642
  this.prepared = this.layout.prepare(this.text, this.atlas, this.fontSize);
2865
4643
  this.applyLayout();
2866
4644
  this.interactive = true;
@@ -2966,20 +4744,20 @@ var TextEntity = (_class3 = class extends _chunk2Z23LTH3js.Entity {
2966
4744
  renderer.restore();
2967
4745
  }
2968
4746
  }
2969
- }, _class3);
4747
+ }, _class7);
2970
4748
 
2971
4749
  // src/components/GridTextEntity.ts
2972
- var GridTextEntity = (_class4 = class extends _chunk2Z23LTH3js.Entity {
4750
+ var GridTextEntity = (_class8 = class extends _chunkDUYB4GX4js.Entity {
2973
4751
 
2974
- __init77() {this.fillStyle = "#ffffff"}
2975
- __init78() {this.grid = []}
4752
+ __init130() {this.fillStyle = "#ffffff"}
4753
+ __init131() {this.grid = []}
2976
4754
  // Array of rows
2977
- __init79() {this.cols = 0}
2978
- __init80() {this.rows = 0}
4755
+ __init132() {this.cols = 0}
4756
+ __init133() {this.rows = 0}
2979
4757
 
2980
4758
 
2981
4759
  constructor(_atlas, fontSize = 10) {
2982
- super();_class4.prototype.__init77.call(this);_class4.prototype.__init78.call(this);_class4.prototype.__init79.call(this);_class4.prototype.__init80.call(this);;
4760
+ super();_class8.prototype.__init130.call(this);_class8.prototype.__init131.call(this);_class8.prototype.__init132.call(this);_class8.prototype.__init133.call(this);;
2983
4761
  this.fontSize = fontSize;
2984
4762
  this.charWidth = fontSize * 1;
2985
4763
  this.charHeight = fontSize * 1.1;
@@ -2988,7 +4766,7 @@ var GridTextEntity = (_class4 = class extends _chunk2Z23LTH3js.Entity {
2988
4766
  updateGrid(ascii) {
2989
4767
  this.grid = ascii;
2990
4768
  this.rows = ascii.length;
2991
- this.cols = _optionalChain([ascii, 'access', _109 => _109[0], 'optionalAccess', _110 => _110.length]) || 0;
4769
+ this.cols = _optionalChain([ascii, 'access', _138 => _138[0], 'optionalAccess', _139 => _139.length]) || 0;
2992
4770
  }
2993
4771
  isPointInside(_globalX, _globalY) {
2994
4772
  return false;
@@ -3010,7 +4788,7 @@ var GridTextEntity = (_class4 = class extends _chunk2Z23LTH3js.Entity {
3010
4788
  }
3011
4789
  }
3012
4790
  }
3013
- }, _class4);
4791
+ }, _class8);
3014
4792
 
3015
4793
  // src/components/SplineEntity.ts
3016
4794
  function polySegmentToBezier(seg) {
@@ -3063,7 +4841,7 @@ function distSqToSegment(px, py, x1, y1, x2, y2) {
3063
4841
  const ey = py - cy;
3064
4842
  return ex * ex + ey * ey;
3065
4843
  }
3066
- var SplineEntity = (_class5 = class extends _chunk2Z23LTH3js.Entity {
4844
+ var SplineEntity = (_class9 = class extends _chunkDUYB4GX4js.Entity {
3067
4845
 
3068
4846
 
3069
4847
 
@@ -3071,23 +4849,23 @@ var SplineEntity = (_class5 = class extends _chunk2Z23LTH3js.Entity {
3071
4849
 
3072
4850
 
3073
4851
 
3074
- __init81() {this.offscreen = null}
3075
- __init82() {this.baked = false}
4852
+ __init134() {this.offscreen = null}
4853
+ __init135() {this.baked = false}
3076
4854
  /** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
3077
- __init83() {this.bakedWidth = 0}
3078
- __init84() {this.bakedHeight = 0}
4855
+ __init136() {this.bakedWidth = 0}
4856
+ __init137() {this.bakedHeight = 0}
3079
4857
  /** Gradient strokes can't be baked to a solid-color bitmap; they render per-frame. */
3080
4858
 
3081
4859
  /** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
3082
- __init85() {this.polylines = null}
4860
+ __init138() {this.polylines = null}
3083
4861
  /**
3084
4862
  * When `true`, the renderer draws a rounded-rect outline of the entity's
3085
4863
  * local bounds after painting the curves. Useful for drag feedback and
3086
4864
  * debugging hit areas. Defaults to `false`.
3087
4865
  */
3088
- __init86() {this.showBounds = false}
4866
+ __init139() {this.showBounds = false}
3089
4867
  constructor(doc, opts = {}) {
3090
- super();_class5.prototype.__init81.call(this);_class5.prototype.__init82.call(this);_class5.prototype.__init83.call(this);_class5.prototype.__init84.call(this);_class5.prototype.__init85.call(this);_class5.prototype.__init86.call(this);;
4868
+ super();_class9.prototype.__init134.call(this);_class9.prototype.__init135.call(this);_class9.prototype.__init136.call(this);_class9.prototype.__init137.call(this);_class9.prototype.__init138.call(this);_class9.prototype.__init139.call(this);;
3091
4869
  this.doc = doc;
3092
4870
  this.lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
3093
4871
  this.cache = _nullishCoalesce(opts.cache, () => ( true));
@@ -3098,7 +4876,7 @@ var SplineEntity = (_class5 = class extends _chunk2Z23LTH3js.Entity {
3098
4876
  this.width = this.bounds.width;
3099
4877
  this.height = this.bounds.height;
3100
4878
  const isGradient = (c) => c !== null && !Array.isArray(c);
3101
- this.containsGradient = (_nullishCoalesce(_optionalChain([this, 'access', _111 => _111.doc, 'access', _112 => _112.equations, 'optionalAccess', _113 => _113.some, 'call', _114 => _114((eq) => isGradient(eq.color_rgb))]), () => ( false))) || (_nullishCoalesce(_optionalChain([this, 'access', _115 => _115.doc, 'access', _116 => _116.paths, 'optionalAccess', _117 => _117.some, 'call', _118 => _118((p) => isGradient(p.color_rgb))]), () => ( false)));
4879
+ this.containsGradient = (_nullishCoalesce(_optionalChain([this, 'access', _140 => _140.doc, 'access', _141 => _141.equations, 'optionalAccess', _142 => _142.some, 'call', _143 => _143((eq) => isGradient(eq.color_rgb))]), () => ( false))) || (_nullishCoalesce(_optionalChain([this, 'access', _144 => _144.doc, 'access', _145 => _145.paths, 'optionalAccess', _146 => _146.some, 'call', _147 => _147((p) => isGradient(p.color_rgb))]), () => ( false)));
3102
4880
  this.interactive = true;
3103
4881
  }
3104
4882
  computeBounds() {
@@ -3331,14 +5109,14 @@ var SplineEntity = (_class5 = class extends _chunk2Z23LTH3js.Entity {
3331
5109
  r.stroke("rgba(0, 150, 255, 0.8)", 2);
3332
5110
  }
3333
5111
  }
3334
- }, _class5);
5112
+ }, _class9);
3335
5113
  async function loadSpline(url) {
3336
5114
  const res = await fetch(url);
3337
5115
  return await res.json();
3338
5116
  }
3339
5117
 
3340
5118
  // src/components/Rect.ts
3341
- var Rect = class extends _chunk2Z23LTH3js.Entity {
5119
+ var Rect = class extends _chunkDUYB4GX4js.Entity {
3342
5120
 
3343
5121
 
3344
5122
 
@@ -3387,7 +5165,7 @@ var Rect = class extends _chunk2Z23LTH3js.Entity {
3387
5165
  };
3388
5166
 
3389
5167
  // src/components/Circle.ts
3390
- var Circle = class extends _chunk2Z23LTH3js.Entity {
5168
+ var Circle = class extends _chunkDUYB4GX4js.Entity {
3391
5169
 
3392
5170
 
3393
5171
 
@@ -3447,7 +5225,7 @@ var Circle = class extends _chunk2Z23LTH3js.Entity {
3447
5225
  };
3448
5226
 
3449
5227
  // src/components/Group.ts
3450
- var Group = class extends _chunk2Z23LTH3js.Entity {
5228
+ var Group = class extends _chunkDUYB4GX4js.Entity {
3451
5229
  constructor(...children) {
3452
5230
  super();
3453
5231
  if (children.length > 0) this.add(...children);
@@ -3459,115 +5237,28 @@ var Group = class extends _chunk2Z23LTH3js.Entity {
3459
5237
  }
3460
5238
  };
3461
5239
 
3462
- // src/math/SpatialHashGrid.ts
3463
- var SpatialHashGrid = (_class6 = class {
3464
-
3465
- __init87() {this.grid = /* @__PURE__ */ new Map()}
3466
- __init88() {this.entityCells = /* @__PURE__ */ new Map()}
3467
- constructor(cellSize = 64) {;_class6.prototype.__init87.call(this);_class6.prototype.__init88.call(this);
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
- _optionalChain([this, 'access', _119 => _119.grid, 'access', _120 => _120.get, 'call', _121 => _121(key), 'optionalAccess', _122 => _122.delete, 'call', _123 => _123(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
- }, _class6);
5240
+ // src/index.ts
5241
+
5242
+
5243
+ var _math = require('@vectojs/math'); _createStarExport(_math);
5244
+
3555
5245
 
3556
5246
  // src/tree/DOMPortalEntity.ts
3557
- var DOMPortalEntity = (_class7 = class extends _chunk2Z23LTH3js.Entity {
3558
-
3559
- __init89() {this.isDOMPortal = true}
3560
- __init90() {this.domListeners = []}
3561
- __init91() {this.resizeObserver = null}
3562
- __init92() {this.cachedWidth = 100}
3563
- __init93() {this.cachedHeight = 100}
3564
- __init94() {this.lastWidth = ""}
3565
- __init95() {this.lastHeight = ""}
3566
- __init96() {this.lastTransform = ""}
3567
- __init97() {this.lastZIndex = ""}
3568
- __init98() {this.lastOpacity = ""}
5247
+ var DOMPortalEntity = (_class10 = class extends _chunkDUYB4GX4js.Entity {
5248
+
5249
+ __init140() {this.isDOMPortal = true}
5250
+ __init141() {this.domListeners = []}
5251
+ __init142() {this.resizeObserver = null}
5252
+ __init143() {this.domBound = false}
5253
+ __init144() {this.cachedWidth = 100}
5254
+ __init145() {this.cachedHeight = 100}
5255
+ __init146() {this.lastWidth = ""}
5256
+ __init147() {this.lastHeight = ""}
5257
+ __init148() {this.lastTransform = ""}
5258
+ __init149() {this.lastZIndex = ""}
5259
+ __init150() {this.lastOpacity = ""}
3569
5260
  constructor(domElement, width, height, id) {
3570
- super(id);_class7.prototype.__init89.call(this);_class7.prototype.__init90.call(this);_class7.prototype.__init91.call(this);_class7.prototype.__init92.call(this);_class7.prototype.__init93.call(this);_class7.prototype.__init94.call(this);_class7.prototype.__init95.call(this);_class7.prototype.__init96.call(this);_class7.prototype.__init97.call(this);_class7.prototype.__init98.call(this);;
5261
+ super(id);_class10.prototype.__init140.call(this);_class10.prototype.__init141.call(this);_class10.prototype.__init142.call(this);_class10.prototype.__init143.call(this);_class10.prototype.__init144.call(this);_class10.prototype.__init145.call(this);_class10.prototype.__init146.call(this);_class10.prototype.__init147.call(this);_class10.prototype.__init148.call(this);_class10.prototype.__init149.call(this);_class10.prototype.__init150.call(this);;
3571
5262
  this.domElement = domElement;
3572
5263
  this.width = _nullishCoalesce(width, () => ( 0));
3573
5264
  this.height = _nullishCoalesce(height, () => ( 0));
@@ -3577,50 +5268,84 @@ var DOMPortalEntity = (_class7 = class extends _chunk2Z23LTH3js.Entity {
3577
5268
  this.domElement.style.pointerEvents = "auto";
3578
5269
  this.cachedWidth = parseFloat(domElement.style.width) || domElement.offsetWidth || 100;
3579
5270
  this.cachedHeight = parseFloat(domElement.style.height) || domElement.offsetHeight || 100;
3580
- if (typeof ResizeObserver !== "undefined") {
3581
- this.resizeObserver = new ResizeObserver((entries) => {
3582
- for (const entry of entries) {
3583
- this.cachedWidth = entry.contentRect.width || entry.target.offsetWidth;
3584
- this.cachedHeight = entry.contentRect.height || entry.target.offsetHeight;
3585
- }
3586
- });
3587
- this.resizeObserver.observe(this.domElement);
3588
- }
3589
- const events = [
3590
- "click",
3591
- "pointerdown",
3592
- "pointerup",
3593
- "pointercancel",
3594
- "pointermove",
3595
- "wheel"
3596
- ];
3597
- for (const type of events) {
3598
- const handler = (e) => {
3599
- this.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)(type, this, e));
3600
- };
3601
- this.domElement.addEventListener(type, handler);
3602
- this.domListeners.push({ type, handler, capture: false });
3603
- }
3604
- const hoverEvents = [
3605
- { native: "mouseenter", vecto: "hover" },
3606
- { native: "mouseleave", vecto: "pointerleave" }
3607
- ];
3608
- for (const { native, vecto } of hoverEvents) {
3609
- const handler = (e) => {
3610
- this.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)(vecto, this, e, false));
3611
- };
3612
- this.domElement.addEventListener(native, handler);
3613
- this.domListeners.push({ type: native, handler, capture: false });
3614
- }
3615
- const focusEvents = ["focus", "blur"];
3616
- for (const type of focusEvents) {
3617
- const handler = (e) => {
3618
- this.dispatchEvent(new (0, _chunk2Z23LTH3js.VectoJSEvent)(type, this, e, true));
3619
- };
3620
- this.domElement.addEventListener(type, handler, true);
3621
- this.domListeners.push({ type, handler, capture: true });
5271
+ this.attachDOMBindings();
5272
+ }
5273
+ }
5274
+ /**
5275
+ * Attach the ResizeObserver + DOM event listeners that bridge native DOM
5276
+ * events into the Vecto event system. Idempotent: safe to call every frame
5277
+ * from the projection path (`Scene.syncPortalGeometry`), which is what lets a
5278
+ * portal survive a `scene.remove()` -> re-add cycle — {@link releaseDOMBindings}
5279
+ * tears these down on removal and the next projection re-attaches them.
5280
+ */
5281
+ attachDOMBindings() {
5282
+ if (this.domBound || typeof window === "undefined") return;
5283
+ this.domBound = true;
5284
+ if (typeof ResizeObserver !== "undefined") {
5285
+ this.resizeObserver = new ResizeObserver((entries) => {
5286
+ for (const entry of entries) {
5287
+ this.cachedWidth = entry.contentRect.width || entry.target.offsetWidth;
5288
+ this.cachedHeight = entry.contentRect.height || entry.target.offsetHeight;
5289
+ }
5290
+ });
5291
+ this.resizeObserver.observe(this.domElement);
5292
+ }
5293
+ const events = [
5294
+ "click",
5295
+ "pointerdown",
5296
+ "pointerup",
5297
+ "pointercancel",
5298
+ "pointermove",
5299
+ "wheel"
5300
+ ];
5301
+ for (const type of events) {
5302
+ const handler = (e) => {
5303
+ this.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)(type, this, e));
5304
+ };
5305
+ this.domElement.addEventListener(type, handler);
5306
+ this.domListeners.push({ type, handler, capture: false });
5307
+ }
5308
+ const hoverEvents = [
5309
+ { native: "mouseenter", vecto: "hover" },
5310
+ { native: "mouseleave", vecto: "pointerleave" }
5311
+ ];
5312
+ for (const { native, vecto } of hoverEvents) {
5313
+ const handler = (e) => {
5314
+ this.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)(vecto, this, e, false));
5315
+ };
5316
+ this.domElement.addEventListener(native, handler);
5317
+ this.domListeners.push({ type: native, handler, capture: false });
5318
+ }
5319
+ const focusEvents = ["focus", "blur"];
5320
+ for (const type of focusEvents) {
5321
+ const handler = (e) => {
5322
+ this.dispatchEvent(new (0, _chunkDUYB4GX4js.VectoJSEvent)(type, this, e, true));
5323
+ };
5324
+ this.domElement.addEventListener(type, handler, true);
5325
+ this.domListeners.push({ type, handler, capture: true });
5326
+ }
5327
+ }
5328
+ /**
5329
+ * Disconnect the ResizeObserver and remove the DOM event listeners, without
5330
+ * touching the scene graph or the element's DOM parentage. Called on the
5331
+ * `scene.remove()` path so a detached portal doesn't leak an observer that
5332
+ * keeps its element alive and firing; the element itself is removed from the
5333
+ * document by the scene's portal pruning. Re-attached lazily on the next
5334
+ * projection frame via {@link attachDOMBindings}.
5335
+ */
5336
+ releaseDOMBindings() {
5337
+ if (!this.domBound) return;
5338
+ this.domBound = false;
5339
+ if (this.resizeObserver) {
5340
+ this.resizeObserver.disconnect();
5341
+ this.resizeObserver = null;
5342
+ }
5343
+ if (this.domElement) {
5344
+ for (const { type, handler, capture } of this.domListeners) {
5345
+ this.domElement.removeEventListener(type, handler, capture);
3622
5346
  }
3623
5347
  }
5348
+ this.domListeners = [];
3624
5349
  }
3625
5350
  isPointInside(globalX, globalY) {
3626
5351
  const w = this.width > 0 ? this.width : this.cachedWidth;
@@ -3636,43 +5361,17 @@ var DOMPortalEntity = (_class7 = class extends _chunk2Z23LTH3js.Entity {
3636
5361
  render() {
3637
5362
  }
3638
5363
  destroy() {
3639
- if (typeof window !== "undefined") {
3640
- if (this.resizeObserver) {
3641
- this.resizeObserver.disconnect();
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
- }
5364
+ this.releaseDOMBindings();
5365
+ if (typeof window !== "undefined" && this.domElement) {
5366
+ this.domElement.remove();
3651
5367
  }
3652
5368
  super.destroy();
3653
5369
  }
3654
- }, _class7);
5370
+ }, _class10);
3655
5371
 
3656
5372
  // src/index.ts
3657
- Scene.registerWebGLPointRendererCreator(_chunkBEUIB3U7js.createWebGLPointRenderer);
3658
- Scene.registerWebGPUParticleSystemManager(_chunkBEUIB3U7js.WebGPUParticleSystemManager);
3659
-
3660
-
3661
-
3662
-
3663
-
3664
-
3665
-
3666
-
3667
-
3668
-
3669
-
3670
-
3671
-
3672
-
3673
-
3674
-
3675
-
5373
+ Scene.registerWebGLPointRendererCreator(_chunkIYPLG4Z4js.createWebGLPointRenderer);
5374
+ Scene.registerWebGPUParticleSystemManager(_chunkIYPLG4Z4js.WebGPUParticleSystemManager);
3676
5375
 
3677
5376
 
3678
5377
 
@@ -3707,4 +5406,4 @@ Scene.registerWebGPUParticleSystemManager(_chunkBEUIB3U7js.WebGPUParticleSystemM
3707
5406
 
3708
5407
 
3709
5408
 
3710
- exports.ArabicShaper = _chunk4AR425ARjs.ArabicShaper; exports.BidiResolver = _chunk4AR425ARjs.BidiResolver; exports.CanvasRenderer = _chunkBEUIB3U7js.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DOMPortalEntity = DOMPortalEntity; exports.Easing = _chunk2Z23LTH3js.Easing; exports.Entity = _chunk2Z23LTH3js.Entity; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.LayoutEngine = _chunkBA5HUUDFjs.LayoutEngine; exports.LayoutResultBuffer = _chunkBA5HUUDFjs.LayoutResultBuffer; exports.LayoutWorkerManager = _chunk4AR425ARjs.LayoutWorkerManager; exports.MSDFFont = _chunk2Z23LTH3js.MSDFFont; exports.MSDFTextEntity = _chunk2Z23LTH3js.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.Rect = Rect; exports.SVGEntity = _chunk2Z23LTH3js.SVGEntity; exports.SVGRenderer = _chunkBEUIB3U7js.SVGRenderer; exports.Scene = Scene; exports.SpatialHashGrid = SpatialHashGrid; exports.SplineEntity = SplineEntity; exports.SpringDriver = _chunk2Z23LTH3js.SpringDriver; exports.SpringPhysics = _chunk2Z23LTH3js.SpringPhysics; exports.TextEntity = TextEntity; exports.TextRasterCache = _chunkBEUIB3U7js.TextRasterCache; exports.TweenDriver = _chunk2Z23LTH3js.TweenDriver; exports.VectoJSEvent = _chunk2Z23LTH3js.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkBEUIB3U7js.WebGPUParticleSystemManager; exports.clearCssLineBoxMetrics = _chunk2Z23LTH3js.clearCssLineBoxMetrics; exports.computeLineSegments = _chunkBA5HUUDFjs.computeLineSegments; exports.createCanvasMeasurer = _chunkBA5HUUDFjs.createCanvasMeasurer; exports.createWebGLPointRenderer = _chunkBEUIB3U7js.createWebGLPointRenderer; exports.cssLineBoxBaseline = _chunk2Z23LTH3js.cssLineBoxBaseline; exports.isSafeUrl = _chunkBEUIB3U7js.isSafeUrl; exports.isTweenConfig = _chunk2Z23LTH3js.isTweenConfig; exports.loadSpline = loadSpline; exports.parseColorToRGBA = _chunkBEUIB3U7js.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.prepareContentGrid = _chunk2Z23LTH3js.prepareContentGrid; exports.sanitizeUrl = _chunkBEUIB3U7js.sanitizeUrl;
5409
+ exports.CanvasRenderer = _chunkIYPLG4Z4js.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DOMPortalEntity = DOMPortalEntity; exports.Entity = _chunkDUYB4GX4js.Entity; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.MSDFTextEntity = _chunkDUYB4GX4js.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.Rect = Rect; exports.SVGEntity = _chunkDUYB4GX4js.SVGEntity; exports.SVGRenderer = _chunkIYPLG4Z4js.SVGRenderer; exports.Scene = Scene; exports.SplineEntity = SplineEntity; exports.TextEntity = TextEntity; exports.TextRasterCache = _chunkIYPLG4Z4js.TextRasterCache; exports.VectoJSEvent = _chunkDUYB4GX4js.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkIYPLG4Z4js.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkIYPLG4Z4js.createWebGLPointRenderer; exports.isSafeUrl = _chunkIYPLG4Z4js.isSafeUrl; exports.loadSpline = loadSpline; exports.parseColorToRGBA = _chunkIYPLG4Z4js.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.sanitizeUrl = _chunkIYPLG4Z4js.sanitizeUrl;