@vectojs/core 1.16.2 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -468,7 +468,158 @@ function buildTreeStore(root) {
468
468
  return { store, indexOf };
469
469
  }
470
470
 
471
+ // src/wasm/hit-store.ts
472
+ function gatherHitAABBs(root, currentFrame) {
473
+ const slotEntity = [];
474
+ const boundless = [];
475
+ const minxs = [];
476
+ const minys = [];
477
+ const maxxs = [];
478
+ const maxys = [];
479
+ const scratch = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
480
+ const visit = (node) => {
481
+ const index = slotEntity.length;
482
+ slotEntity.push(node);
483
+ const bounds = node.getBounds();
484
+ if (bounds === null) {
485
+ boundless.push({ entity: node, index });
486
+ minxs.push(0);
487
+ minys.push(0);
488
+ maxxs.push(0);
489
+ maxys.push(0);
490
+ } else {
491
+ if (!node._readWorldCache(currentFrame, scratch)) {
492
+ const t = node.getWorldTransform();
493
+ scratch.a = t.a;
494
+ scratch.b = t.b;
495
+ scratch.c = t.c;
496
+ scratch.d = t.d;
497
+ scratch.e = t.e;
498
+ scratch.f = t.f;
499
+ }
500
+ const { a, b, c, d, e, f } = scratch;
501
+ let minX = Infinity;
502
+ let minY = Infinity;
503
+ let maxX = -Infinity;
504
+ let maxY = -Infinity;
505
+ for (let i = 0; i < 4; i++) {
506
+ const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
507
+ const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
508
+ const wx = a * lx + c * ly + e;
509
+ const wy = b * lx + d * ly + f;
510
+ if (wx < minX) minX = wx;
511
+ if (wx > maxX) maxX = wx;
512
+ if (wy < minY) minY = wy;
513
+ if (wy > maxY) maxY = wy;
514
+ }
515
+ minxs.push(minX);
516
+ minys.push(minY);
517
+ maxxs.push(maxX);
518
+ maxys.push(maxY);
519
+ }
520
+ const kids = node.children;
521
+ for (let i = 0; i < kids.length; i++) visit(kids[i]);
522
+ };
523
+ visit(root);
524
+ return {
525
+ count: slotEntity.length,
526
+ slotEntity,
527
+ boundless,
528
+ minx: Float64Array.from(minxs),
529
+ miny: Float64Array.from(minys),
530
+ maxx: Float64Array.from(maxxs),
531
+ maxy: Float64Array.from(maxys)
532
+ };
533
+ }
534
+
535
+ // src/wasm/hit-store-fused.ts
536
+ function gatherHitAABBsFromStore(root, aabbs, storeSlotEntity, out) {
537
+ const slotEntity = out.slotEntity;
538
+ const boundless = out.boundless;
539
+ slotEntity.length = 0;
540
+ boundless.length = 0;
541
+ let count = 0;
542
+ let capacity = out.minx.length;
543
+ let minx = out.minx;
544
+ let miny = out.miny;
545
+ let maxx = out.maxx;
546
+ let maxy = out.maxy;
547
+ const grow = (needed) => {
548
+ if (needed <= capacity) return;
549
+ let next = capacity || 256;
550
+ while (next < needed) next *= 2;
551
+ const nminx = new Float64Array(next);
552
+ const nminy = new Float64Array(next);
553
+ const nmaxx = new Float64Array(next);
554
+ const nmaxy = new Float64Array(next);
555
+ nminx.set(minx);
556
+ nminy.set(miny);
557
+ nmaxx.set(maxx);
558
+ nmaxy.set(maxy);
559
+ minx = nminx;
560
+ miny = nminy;
561
+ maxx = nmaxx;
562
+ maxy = nmaxy;
563
+ capacity = next;
564
+ };
565
+ let bailed = false;
566
+ const visit = (node) => {
567
+ if (bailed) return;
568
+ const index = count;
569
+ slotEntity.push(node);
570
+ count++;
571
+ grow(count);
572
+ if (node.getBounds() === null) {
573
+ boundless.push({ entity: node, index });
574
+ minx[index] = 0;
575
+ miny[index] = 0;
576
+ maxx[index] = 0;
577
+ maxy[index] = 0;
578
+ } else {
579
+ const slot = node._storeSlot;
580
+ if (slot < 0 || slot >= aabbs.aminx.length || storeSlotEntity[slot] !== node) {
581
+ bailed = true;
582
+ return;
583
+ }
584
+ minx[index] = aabbs.aminx[slot];
585
+ miny[index] = aabbs.aminy[slot];
586
+ maxx[index] = aabbs.amaxx[slot];
587
+ maxy[index] = aabbs.amaxy[slot];
588
+ }
589
+ const children = node.children;
590
+ for (let i = 0; i < children.length; i++) visit(children[i]);
591
+ };
592
+ visit(root);
593
+ if (bailed) return null;
594
+ out.count = count;
595
+ out.minx = minx;
596
+ out.miny = miny;
597
+ out.maxx = maxx;
598
+ out.maxy = maxy;
599
+ return out;
600
+ }
601
+ function createHitGatherBuffer() {
602
+ return {
603
+ count: 0,
604
+ slotEntity: [],
605
+ boundless: [],
606
+ minx: new Float64Array(256),
607
+ miny: new Float64Array(256),
608
+ maxx: new Float64Array(256),
609
+ maxy: new Float64Array(256)
610
+ };
611
+ }
612
+
471
613
  // src/wasm/backend.ts
614
+ var WASM_STATUS = {
615
+ OK: 0,
616
+ /** A count exceeded what `init` allocated. */
617
+ CAPACITY: 1,
618
+ /** A kernel ran before `init`. */
619
+ UNINITIALIZED: 2,
620
+ /** A sibling run addressed a slot or parent outside the store. */
621
+ BAD_RUN: 3
622
+ };
472
623
  var PAD2 = 8;
473
624
  var WasmTransformBackend = class {
474
625
  available = true;
@@ -520,9 +671,13 @@ var WasmTransformBackend = class {
520
671
  this.vrp.set(store.runParent.subarray(0, rc));
521
672
  this.vrs.set(store.runStart.subarray(0, rc));
522
673
  this.vrl.set(store.runLen.subarray(0, rc));
523
- this.ex.set_run_count(rc);
524
- if (kernel === "scalar") this.ex.compose_scalar();
525
- else this.ex.compose_simd();
674
+ if (this.ex.set_run_count(rc) !== WASM_STATUS.OK) {
675
+ this.lastStatus = WASM_STATUS.CAPACITY;
676
+ return;
677
+ }
678
+ const status = kernel === "scalar" ? this.ex.compose_scalar() : this.ex.compose_simd();
679
+ this.lastStatus = status;
680
+ if (status !== WASM_STATUS.OK) return;
526
681
  store.wa.set(this.vwa.subarray(0, n));
527
682
  store.wb.set(this.vwb.subarray(0, n));
528
683
  store.wc.set(this.vwc.subarray(0, n));
@@ -542,8 +697,9 @@ var WasmTransformBackend = class {
542
697
  * {@link uploadRuns} after a topology change.
543
698
  */
544
699
  runKernel(kernel = "simd") {
545
- if (kernel === "scalar") this.ex.compose_scalar();
546
- else this.ex.compose_simd();
700
+ const status = kernel === "scalar" ? this.ex.compose_scalar() : this.ex.compose_simd();
701
+ this.lastStatus = status;
702
+ return status;
547
703
  }
548
704
  /** Upload only the run table + count (topology), leaving per-entity inputs to
549
705
  * the resident views. Call when the tree structure changes, not per frame. */
@@ -553,8 +709,13 @@ var WasmTransformBackend = class {
553
709
  this.vrp.set(store.runParent.subarray(0, rc));
554
710
  this.vrs.set(store.runStart.subarray(0, rc));
555
711
  this.vrl.set(store.runLen.subarray(0, rc));
556
- this.ex.set_run_count(rc);
712
+ this.lastStatus = this.ex.set_run_count(rc);
557
713
  }
714
+ /**
715
+ * Status of the most recent kernel or run-table call. `WASM_STATUS.OK` unless
716
+ * the crate rejected its arguments, in which case that call was a no-op.
717
+ */
718
+ lastStatus = WASM_STATUS.OK;
558
719
  /** The resident wasm input views (`x,y,sx,sy,cos,sin,opacity`), valid until
559
720
  * the next capacity growth. Writing here is what makes uploads unnecessary. */
560
721
  inputView() {
@@ -622,6 +783,22 @@ var WasmTransformBackend = class {
622
783
  amaxy: this.vamaxy
623
784
  };
624
785
  }
786
+ /**
787
+ * Re-create the typed-array views if the memory buffer they were built over has
788
+ * been detached.
789
+ *
790
+ * Necessary because all backends of a Scene now share one instance, and
791
+ * therefore one linear memory: another backend's allocation (notably
792
+ * `hit_init`, which allocates its own grid arrays) can grow the memory and
793
+ * detach every view built over the old buffer. A detached `Float64Array` reads
794
+ * as length 0 and silently returns `undefined` for every index, so without this
795
+ * the transform store appears empty rather than failing loudly.
796
+ */
797
+ revalidateViews() {
798
+ if (this.cap === 0) return;
799
+ if (this.vwa.length === this.cap && this.vwa.buffer === this.ex.memory.buffer) return;
800
+ this.refreshViews();
801
+ }
625
802
  ensure(count, runCount) {
626
803
  if (count + PAD2 <= this.cap && runCount <= this.runCap) return;
627
804
  this.cap = count + PAD2;
@@ -664,100 +841,75 @@ var WasmTransformBackend = class {
664
841
  this.vrl = i32(this.ex.p_run_len());
665
842
  }
666
843
  };
667
- async function instantiateAsync(bytes) {
668
- try {
669
- const { instance } = await WebAssembly.instantiate(bytes, {});
670
- return new WasmTransformBackend(instance);
671
- } catch {
672
- return null;
844
+
845
+ // src/wasm/anim-backend.ts
846
+ var PAD3 = 8;
847
+ var AnimBackend = class {
848
+ ex;
849
+ springCap = 0;
850
+ tweenCap = 0;
851
+ sv;
852
+ tv;
853
+ constructor(instance) {
854
+ this.ex = instance.exports;
673
855
  }
674
- }
675
- async function instantiateStreaming(source) {
676
- try {
677
- const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
678
- if (typeof WebAssembly.instantiateStreaming === "function") {
679
- const buffered = resp.clone();
680
- try {
681
- const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
682
- return new WasmTransformBackend(instance2);
683
- } catch {
684
- const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
685
- return new WasmTransformBackend(instance2);
686
- }
687
- }
688
- const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
689
- return new WasmTransformBackend(instance);
690
- } catch {
691
- return null;
856
+ /** The resident spring SoA input/output views, valid until the next capacity
857
+ * growth. Write gathered driver state here before calling {@link stepSprings}. */
858
+ springView() {
859
+ return this.sv;
692
860
  }
693
- }
694
-
695
- // src/wasm/hit-store.ts
696
- function gatherHitAABBs(root, currentFrame) {
697
- const slotEntity = [];
698
- const boundless = [];
699
- const minxs = [];
700
- const minys = [];
701
- const maxxs = [];
702
- const maxys = [];
703
- const scratch = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
704
- const visit = (node) => {
705
- const index = slotEntity.length;
706
- slotEntity.push(node);
707
- const bounds = node.getBounds();
708
- if (bounds === null) {
709
- boundless.push({ entity: node, index });
710
- minxs.push(0);
711
- minys.push(0);
712
- maxxs.push(0);
713
- maxys.push(0);
714
- } else {
715
- if (!node._readWorldCache(currentFrame, scratch)) {
716
- const t = node.getWorldTransform();
717
- scratch.a = t.a;
718
- scratch.b = t.b;
719
- scratch.c = t.c;
720
- scratch.d = t.d;
721
- scratch.e = t.e;
722
- scratch.f = t.f;
723
- }
724
- const { a, b, c, d, e, f } = scratch;
725
- let minX = Infinity;
726
- let minY = Infinity;
727
- let maxX = -Infinity;
728
- let maxY = -Infinity;
729
- for (let i = 0; i < 4; i++) {
730
- const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
731
- const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
732
- const wx = a * lx + c * ly + e;
733
- const wy = b * lx + d * ly + f;
734
- if (wx < minX) minX = wx;
735
- if (wx > maxX) maxX = wx;
736
- if (wy < minY) minY = wy;
737
- if (wy > maxY) maxY = wy;
738
- }
739
- minxs.push(minX);
740
- minys.push(minY);
741
- maxxs.push(maxX);
742
- maxys.push(maxY);
743
- }
744
- const kids = node.children;
745
- for (let i = 0; i < kids.length; i++) visit(kids[i]);
746
- };
747
- visit(root);
748
- return {
749
- count: slotEntity.length,
750
- slotEntity,
751
- boundless,
752
- minx: Float64Array.from(minxs),
753
- miny: Float64Array.from(minys),
754
- maxx: Float64Array.from(maxxs),
755
- maxy: Float64Array.from(maxys)
756
- };
757
- }
861
+ /** The resident tween SoA input/output views, valid until the next capacity
862
+ * growth. Write gathered driver state here before calling {@link stepTweens}. */
863
+ tweenView() {
864
+ return this.tv;
865
+ }
866
+ /**
867
+ * Size (and grow, if needed) capacity for `springCount` springs and
868
+ * `tweenCount` tweens. Call this BEFORE writing into {@link springView}/
869
+ * {@link tweenView} — a capacity growth detaches the previous views, so
870
+ * writing first and sizing after would write into a stale buffer.
871
+ */
872
+ ensure(springCount, tweenCount) {
873
+ if (springCount + PAD3 <= this.springCap && tweenCount + PAD3 <= this.tweenCap) return;
874
+ this.springCap = springCount + PAD3;
875
+ this.tweenCap = tweenCount + PAD3;
876
+ this.ex.anim_init(this.springCap, this.tweenCap);
877
+ this.refreshViews();
878
+ }
879
+ /** Advance `count` springs (from index 0) by `dtMs` milliseconds, in place. */
880
+ stepSprings(dtMs, count) {
881
+ this.ex.spring_step(dtMs / 1e3, count);
882
+ }
883
+ /** Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`. */
884
+ stepTweens(dtMs, count) {
885
+ this.ex.tween_step(dtMs, count);
886
+ }
887
+ refreshViews() {
888
+ const buf = this.ex.memory.buffer;
889
+ const sCap = this.springCap;
890
+ const tCap = this.tweenCap;
891
+ this.sv = {
892
+ val: new Float64Array(buf, this.ex.p_s_val(), sCap),
893
+ target: new Float64Array(buf, this.ex.p_s_target(), sCap),
894
+ vel: new Float64Array(buf, this.ex.p_s_vel(), sCap),
895
+ stiff: new Float64Array(buf, this.ex.p_s_stiff(), sCap),
896
+ damp: new Float64Array(buf, this.ex.p_s_damp(), sCap),
897
+ mass: new Float64Array(buf, this.ex.p_s_mass(), sCap)
898
+ };
899
+ this.tv = {
900
+ from: new Float64Array(buf, this.ex.p_t_from(), tCap),
901
+ to: new Float64Array(buf, this.ex.p_t_to(), tCap),
902
+ elapsed: new Float64Array(buf, this.ex.p_t_elapsed(), tCap),
903
+ dur: new Float64Array(buf, this.ex.p_t_dur(), tCap),
904
+ delay: new Float64Array(buf, this.ex.p_t_delay(), tCap),
905
+ ease: new Float64Array(buf, this.ex.p_t_ease(), tCap),
906
+ val: new Float64Array(buf, this.ex.p_t_val(), tCap)
907
+ };
908
+ }
909
+ };
758
910
 
759
911
  // src/wasm/hit-backend.ts
760
- var PAD3 = 8;
912
+ var PAD4 = 8;
761
913
  var CELLS_PER_ENTITY_HINT = 4;
762
914
  var HitTestBackend = class {
763
915
  ex;
@@ -829,10 +981,10 @@ var HitTestBackend = class {
829
981
  return this.vItems.subarray(start, start + count);
830
982
  }
831
983
  growIfNeeded(count, cellCount, itemCount) {
832
- if (count + PAD3 <= this.entityCap && cellCount <= this.cellCap && itemCount <= this.itemCap) {
984
+ if (count + PAD4 <= this.entityCap && cellCount <= this.cellCap && itemCount <= this.itemCap) {
833
985
  return;
834
986
  }
835
- this.entityCap = count + PAD3;
987
+ this.entityCap = count + PAD4;
836
988
  this.cellCap = Math.max(cellCount, 1);
837
989
  this.itemCap = Math.max(itemCount, count, 1);
838
990
  this.ex.hit_init(count, this.cellCap, this.itemCap);
@@ -854,126 +1006,6 @@ var HitTestBackend = class {
854
1006
  this.vItems = new Int32Array(buf, this.ex.p_h_items(), iCap);
855
1007
  }
856
1008
  };
857
- async function instantiateAsync2(bytes) {
858
- try {
859
- const { instance } = await WebAssembly.instantiate(bytes, {});
860
- return new HitTestBackend(instance);
861
- } catch {
862
- return null;
863
- }
864
- }
865
- async function instantiateStreaming2(source) {
866
- try {
867
- const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
868
- if (typeof WebAssembly.instantiateStreaming === "function") {
869
- const buffered = resp.clone();
870
- try {
871
- const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
872
- return new HitTestBackend(instance2);
873
- } catch {
874
- const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
875
- return new HitTestBackend(instance2);
876
- }
877
- }
878
- const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
879
- return new HitTestBackend(instance);
880
- } catch {
881
- return null;
882
- }
883
- }
884
-
885
- // src/wasm/anim-backend.ts
886
- var PAD4 = 8;
887
- var AnimBackend = class {
888
- ex;
889
- springCap = 0;
890
- tweenCap = 0;
891
- sv;
892
- tv;
893
- constructor(instance) {
894
- this.ex = instance.exports;
895
- }
896
- /** The resident spring SoA input/output views, valid until the next capacity
897
- * growth. Write gathered driver state here before calling {@link stepSprings}. */
898
- springView() {
899
- return this.sv;
900
- }
901
- /** The resident tween SoA input/output views, valid until the next capacity
902
- * growth. Write gathered driver state here before calling {@link stepTweens}. */
903
- tweenView() {
904
- return this.tv;
905
- }
906
- /**
907
- * Size (and grow, if needed) capacity for `springCount` springs and
908
- * `tweenCount` tweens. Call this BEFORE writing into {@link springView}/
909
- * {@link tweenView} — a capacity growth detaches the previous views, so
910
- * writing first and sizing after would write into a stale buffer.
911
- */
912
- ensure(springCount, tweenCount) {
913
- if (springCount + PAD4 <= this.springCap && tweenCount + PAD4 <= this.tweenCap) return;
914
- this.springCap = springCount + PAD4;
915
- this.tweenCap = tweenCount + PAD4;
916
- this.ex.anim_init(this.springCap, this.tweenCap);
917
- this.refreshViews();
918
- }
919
- /** Advance `count` springs (from index 0) by `dtMs` milliseconds, in place. */
920
- stepSprings(dtMs, count) {
921
- this.ex.spring_step(dtMs / 1e3, count);
922
- }
923
- /** Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`. */
924
- stepTweens(dtMs, count) {
925
- this.ex.tween_step(dtMs, count);
926
- }
927
- refreshViews() {
928
- const buf = this.ex.memory.buffer;
929
- const sCap = this.springCap;
930
- const tCap = this.tweenCap;
931
- this.sv = {
932
- val: new Float64Array(buf, this.ex.p_s_val(), sCap),
933
- target: new Float64Array(buf, this.ex.p_s_target(), sCap),
934
- vel: new Float64Array(buf, this.ex.p_s_vel(), sCap),
935
- stiff: new Float64Array(buf, this.ex.p_s_stiff(), sCap),
936
- damp: new Float64Array(buf, this.ex.p_s_damp(), sCap),
937
- mass: new Float64Array(buf, this.ex.p_s_mass(), sCap)
938
- };
939
- this.tv = {
940
- from: new Float64Array(buf, this.ex.p_t_from(), tCap),
941
- to: new Float64Array(buf, this.ex.p_t_to(), tCap),
942
- elapsed: new Float64Array(buf, this.ex.p_t_elapsed(), tCap),
943
- dur: new Float64Array(buf, this.ex.p_t_dur(), tCap),
944
- delay: new Float64Array(buf, this.ex.p_t_delay(), tCap),
945
- ease: new Float64Array(buf, this.ex.p_t_ease(), tCap),
946
- val: new Float64Array(buf, this.ex.p_t_val(), tCap)
947
- };
948
- }
949
- };
950
- async function instantiateAsync3(bytes) {
951
- try {
952
- const { instance } = await WebAssembly.instantiate(bytes, {});
953
- return new AnimBackend(instance);
954
- } catch {
955
- return null;
956
- }
957
- }
958
- async function instantiateStreaming3(source) {
959
- try {
960
- const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
961
- if (typeof WebAssembly.instantiateStreaming === "function") {
962
- const buffered = resp.clone();
963
- try {
964
- const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
965
- return new AnimBackend(instance2);
966
- } catch {
967
- const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
968
- return new AnimBackend(instance2);
969
- }
970
- }
971
- const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
972
- return new AnimBackend(instance);
973
- } catch {
974
- return null;
975
- }
976
- }
977
1009
 
978
1010
  // src/wasm/particle-backend.ts
979
1011
  var PAD5 = 8;
@@ -1066,33 +1098,95 @@ var ParticleBackend = class {
1066
1098
  };
1067
1099
  }
1068
1100
  };
1069
- async function instantiateAsync4(bytes) {
1070
- try {
1071
- const { instance } = await WebAssembly.instantiate(bytes, {});
1072
- return new ParticleBackend(instance);
1073
- } catch {
1074
- return null;
1101
+
1102
+ // src/wasm/runtime.ts
1103
+ var moduleCache = /* @__PURE__ */ new Map();
1104
+ function cacheKey(source) {
1105
+ if (typeof source === "string") return source;
1106
+ if (source instanceof URL) return source.href;
1107
+ return null;
1108
+ }
1109
+ async function compile(source) {
1110
+ if (source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
1111
+ return new WebAssembly.Module(source);
1112
+ }
1113
+ const response = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
1114
+ if (typeof WebAssembly.compileStreaming === "function") {
1115
+ const buffered = response.clone();
1116
+ try {
1117
+ return await WebAssembly.compileStreaming(response);
1118
+ } catch {
1119
+ return await WebAssembly.compile(await buffered.arrayBuffer());
1120
+ }
1075
1121
  }
1122
+ return await WebAssembly.compile(await response.arrayBuffer());
1076
1123
  }
1077
- async function instantiateStreaming4(source) {
1078
- try {
1079
- const resp = typeof source === "string" || source instanceof URL ? await fetch(String(source)) : await source;
1080
- if (typeof WebAssembly.instantiateStreaming === "function") {
1081
- const buffered = resp.clone();
1124
+ async function loadCoreWasmModule(source) {
1125
+ const key = cacheKey(source);
1126
+ if (key !== null) {
1127
+ const hit = moduleCache.get(key);
1128
+ if (hit) {
1082
1129
  try {
1083
- const { instance: instance2 } = await WebAssembly.instantiateStreaming(resp, {});
1084
- return new ParticleBackend(instance2);
1130
+ return await hit;
1085
1131
  } catch {
1086
- const { instance: instance2 } = await WebAssembly.instantiate(await buffered.arrayBuffer(), {});
1087
- return new ParticleBackend(instance2);
1132
+ moduleCache.delete(key);
1133
+ return null;
1088
1134
  }
1089
1135
  }
1090
- const { instance } = await WebAssembly.instantiate(await resp.arrayBuffer(), {});
1091
- return new ParticleBackend(instance);
1136
+ }
1137
+ const pending = compile(source);
1138
+ if (key !== null) moduleCache.set(key, pending);
1139
+ try {
1140
+ return await pending;
1092
1141
  } catch {
1142
+ if (key !== null) moduleCache.delete(key);
1093
1143
  return null;
1094
1144
  }
1095
1145
  }
1146
+ var CoreWasmRuntime = class {
1147
+ instance;
1148
+ transformBackend = null;
1149
+ animBackend = null;
1150
+ hitBackend = null;
1151
+ particleBackendInstance = null;
1152
+ constructor(instance) {
1153
+ this.instance = instance;
1154
+ }
1155
+ /**
1156
+ * Backends are constructed lazily and memoised: each one's constructor calls
1157
+ * into the instance to size its store and build typed-array views, so building
1158
+ * all four up front would pay for accelerators the Scene never enables.
1159
+ */
1160
+ transform() {
1161
+ if (!this.transformBackend) this.transformBackend = new WasmTransformBackend(this.instance);
1162
+ return this.transformBackend;
1163
+ }
1164
+ anim() {
1165
+ if (!this.animBackend) this.animBackend = new AnimBackend(this.instance);
1166
+ return this.animBackend;
1167
+ }
1168
+ hit() {
1169
+ if (!this.hitBackend) this.hitBackend = new HitTestBackend(this.instance);
1170
+ return this.hitBackend;
1171
+ }
1172
+ particle() {
1173
+ if (!this.particleBackendInstance)
1174
+ this.particleBackendInstance = new ParticleBackend(this.instance);
1175
+ return this.particleBackendInstance;
1176
+ }
1177
+ };
1178
+ function createCoreWasmRuntime(module) {
1179
+ try {
1180
+ return new CoreWasmRuntime(new WebAssembly.Instance(module, {}));
1181
+ } catch {
1182
+ return null;
1183
+ }
1184
+ }
1185
+ async function loadCoreWasmRuntime(source) {
1186
+ const module = await loadCoreWasmModule(source);
1187
+ if (!module) return null;
1188
+ return createCoreWasmRuntime(module);
1189
+ }
1096
1190
 
1097
1191
  // src/tree/Scene.ts
1098
1192
  import { clearCssLineBoxMetrics, cssLineBoxBaseline } from "@vectojs/text";
@@ -1673,10 +1767,9 @@ var Scene = class _Scene {
1673
1767
  * Resolves `true` if WASM is now active, `false` if the JS path remains.
1674
1768
  */
1675
1769
  async enableWasmTransforms(source) {
1676
- const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
1677
- const backend = isBytes ? await instantiateAsync(source) : await instantiateStreaming(source);
1678
- if (!backend) return false;
1679
- this.setTransformBackend(backend);
1770
+ const runtime = await this.ensureWasmRuntime(source);
1771
+ if (!runtime) return false;
1772
+ this.setTransformBackend(runtime.transform());
1680
1773
  return true;
1681
1774
  }
1682
1775
  // ── WASM hit-test backend (invisible accelerator, G3) ───────────────────────
@@ -1690,6 +1783,44 @@ var Scene = class _Scene {
1690
1783
  // ever change *how fast* a hit is found, never *which* entity is returned —
1691
1784
  // every grid candidate is re-confirmed against its own precise
1692
1785
  // isPointInside before being trusted (see hit-store.ts / hit-backend.ts).
1786
+ /**
1787
+ * The one WASM instance this Scene's accelerators share.
1788
+ *
1789
+ * Each `enableWasm*` used to instantiate the binary itself, so enabling all
1790
+ * four compiled the same module four times and held four linear memories. The
1791
+ * Rust crate already keeps transform/anim/hit/particle in separate statics, so
1792
+ * one instance serves all of them without aliasing. The compiled module is
1793
+ * cached globally; the instance is per-Scene, which is the isolation that
1794
+ * actually matters.
1795
+ */
1796
+ _wasmRuntime = null;
1797
+ /**
1798
+ * Load (or reuse) this Scene's shared WASM runtime.
1799
+ *
1800
+ * Returns `null` on any failure — CSP `wasm-unsafe-eval`, a 404, corrupt bytes,
1801
+ * unsupported SIMD — so every caller keeps its JS path. Failure is the default
1802
+ * state here, not an error path.
1803
+ */
1804
+ async ensureWasmRuntime(source) {
1805
+ if (this._wasmRuntime) return this._wasmRuntime;
1806
+ const runtime = await loadCoreWasmRuntime(source);
1807
+ if (!runtime) return null;
1808
+ if (this._wasmRuntime) return this._wasmRuntime;
1809
+ this._wasmRuntime = runtime;
1810
+ return runtime;
1811
+ }
1812
+ /**
1813
+ * Install a pre-built runtime, so several Scenes can share one compile while
1814
+ * each keeps its own stores. Pass `null` to detach (backends already installed
1815
+ * keep working; only subsequent `enableWasm*` calls re-load).
1816
+ */
1817
+ setWasmRuntime(runtime) {
1818
+ this._wasmRuntime = runtime;
1819
+ }
1820
+ /** The shared WASM runtime, if one has been loaded. */
1821
+ get wasmRuntime() {
1822
+ return this._wasmRuntime;
1823
+ }
1693
1824
  _hitWasm = null;
1694
1825
  // Cache key: which frame + structure version the grid was last (successfully,
1695
1826
  // non-overflowing) built for. findEntityAt is called ad-hoc (pointer
@@ -1700,6 +1831,24 @@ var Scene = class _Scene {
1700
1831
  _hitGridOk = false;
1701
1832
  _hitSlotEntity = [];
1702
1833
  _hitBoundless = [];
1834
+ /** Reused buffer for the fused gather, so a pointer query allocates nothing. */
1835
+ _hitGatherBuffer = null;
1836
+ /**
1837
+ * Whether the last grid build sourced its AABBs from the WASM transform store
1838
+ * rather than recomputing them in JS. Diagnostic only — both paths must
1839
+ * produce the same entity for a given point.
1840
+ */
1841
+ _hitFusedGather = false;
1842
+ /**
1843
+ * Whether `compute_aabbs` has run against the current frame's world matrices.
1844
+ * The AABB pass is only meaningful after a `compose_*`, so the fused gather
1845
+ * must not read the views before then.
1846
+ */
1847
+ _wasmAabbsFresh = false;
1848
+ /** Did the last hit-grid build use the fused (WASM-store) gather? */
1849
+ get hitGatherPath() {
1850
+ return this._hitFusedGather ? "fused" : "js";
1851
+ }
1703
1852
  /** Which backend answers `findEntityAt` for the main tree. */
1704
1853
  get hitTestBackend() {
1705
1854
  return this._hitWasm ? "wasm" : "js";
@@ -1718,10 +1867,9 @@ var Scene = class _Scene {
1718
1867
  * state, not an error path. Resolves `true` if WASM is now active.
1719
1868
  */
1720
1869
  async enableWasmHitTest(source) {
1721
- const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
1722
- const backend = isBytes ? await instantiateAsync2(source) : await instantiateStreaming2(source);
1723
- if (!backend) return false;
1724
- this.setHitTestBackend(backend);
1870
+ const runtime = await this.ensureWasmRuntime(source);
1871
+ if (!runtime) return false;
1872
+ this.setHitTestBackend(runtime.hit());
1725
1873
  return true;
1726
1874
  }
1727
1875
  /**
@@ -1738,7 +1886,22 @@ var Scene = class _Scene {
1738
1886
  const backend = this._hitWasm;
1739
1887
  if (!backend) return false;
1740
1888
  if (this._hitGridFrame === this.currentFrame) return this._hitGridOk;
1741
- const gathered = gatherHitAABBs(this.root, this.currentFrame);
1889
+ let gathered = null;
1890
+ if (this._wasm && this._ensureWasmAabbs()) {
1891
+ this._hitGatherBuffer ??= createHitGatherBuffer();
1892
+ this._wasm.revalidateViews();
1893
+ gathered = gatherHitAABBsFromStore(
1894
+ this.root,
1895
+ this._wasm.aabbView(),
1896
+ this._slotEntity,
1897
+ this._hitGatherBuffer
1898
+ );
1899
+ if (gathered) this._hitFusedGather = true;
1900
+ }
1901
+ if (!gathered) {
1902
+ this._hitFusedGather = false;
1903
+ gathered = gatherHitAABBs(this.root, this.currentFrame);
1904
+ }
1742
1905
  backend.ensure(gathered.count, this.width, this.height, 64);
1743
1906
  const view = backend.inputView();
1744
1907
  view.minx.set(gathered.minx.subarray(0, gathered.count));
@@ -1851,7 +2014,57 @@ var Scene = class _Scene {
1851
2014
  * own target browser mix and driver-kind distribution, or leave WASM
1852
2015
  * animation batching disabled entirely on a Firefox-heavy audience.
1853
2016
  */
1854
- animDriverGateCount = 256;
2017
+ /**
2018
+ * Back-compat alias for {@link animGate}. Reading it returns the tween gate
2019
+ * (the conservative one the single knob used to represent); writing it sets all
2020
+ * three, so code that tuned one number keeps behaving as before.
2021
+ *
2022
+ * Prefer {@link animGate} — a single threshold cannot be right for both kinds,
2023
+ * which is why this exists as an alias rather than the primary control.
2024
+ */
2025
+ get animDriverGateCount() {
2026
+ return this.animGate.tween;
2027
+ }
2028
+ set animDriverGateCount(n) {
2029
+ this.animGate = { spring: n, tween: n, mixed: n };
2030
+ }
2031
+ /**
2032
+ * Per-kind driver gates, in active batchable drivers.
2033
+ *
2034
+ * Measured on the integrated path (`benchmarks/anim-wasm-scene`, real Chrome
2035
+ * 150 / Firefox 153): spring and mixed workloads are a ~1.4-2.3x win from 128
2036
+ * drivers up through 16384, while pure tween is a **0.71x loss** at 128 and
2037
+ * only turns net-positive near 256. One scalar threshold therefore had to be
2038
+ * set for the worst kind, discarding the 128-255 spring win to avoid making a
2039
+ * tween-heavy scene slower.
2040
+ *
2041
+ * Firefox is a net loss at every count measured up to 16384 — not an
2042
+ * allocation artifact (confirmed after removing all per-frame allocation from
2043
+ * gather/scatter); SpiderMonkey's wasm-boundary cost for this call shape
2044
+ * appears to structurally exceed the saving at these scales. These defaults are
2045
+ * Chrome-oriented; on a Firefox-heavy audience, leave
2046
+ * {@link enableWasmAnimBatching} off entirely rather than tuning these.
2047
+ *
2048
+ * Setting {@link animDriverGateCount} overwrites all three, so existing code
2049
+ * that tuned the single knob keeps working unchanged.
2050
+ */
2051
+ _animBatchedLastFrame = false;
2052
+ /**
2053
+ * Whether the WASM batch path actually ran on the most recent frame.
2054
+ *
2055
+ * Distinct from {@link animBackend}, which reports only that a backend is
2056
+ * installed — a gate below the driver count means the frame still ticked in JS.
2057
+ * Conflating the two makes it easy to believe an accelerator is active when it
2058
+ * never opens.
2059
+ */
2060
+ get animBatchedLastFrame() {
2061
+ return this._animBatchedLastFrame;
2062
+ }
2063
+ animGate = {
2064
+ spring: 128,
2065
+ tween: 256,
2066
+ mixed: 128
2067
+ };
1855
2068
  /** Which backend advances active property drivers on the current gate
1856
2069
  * decision. Reflects only whether a backend is installed — the per-frame
1857
2070
  * gate can still choose the JS path even when this reads `'wasm'`. */
@@ -1872,10 +2085,9 @@ var Scene = class _Scene {
1872
2085
  * if WASM is now available (not necessarily active every frame).
1873
2086
  */
1874
2087
  async enableWasmAnimBatching(source) {
1875
- const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
1876
- const backend = isBytes ? await instantiateAsync3(source) : await instantiateStreaming3(source);
1877
- if (!backend) return false;
1878
- this.setAnimBackend(backend);
2088
+ const runtime = await this.ensureWasmRuntime(source);
2089
+ if (!runtime) return false;
2090
+ this.setAnimBackend(runtime.anim());
1879
2091
  return true;
1880
2092
  }
1881
2093
  // ── WASM particle CPU-sim backend (invisible accelerator, G4) ───────────────
@@ -1906,10 +2118,9 @@ var Scene = class _Scene {
1906
2118
  * Resolves `true` if WASM is now active.
1907
2119
  */
1908
2120
  async enableWasmParticles(source) {
1909
- const isBytes = source instanceof ArrayBuffer || ArrayBuffer.isView(source);
1910
- const backend = isBytes ? await instantiateAsync4(source) : await instantiateStreaming4(source);
1911
- if (!backend) return false;
1912
- this.setParticleBackend(backend);
2121
+ const runtime = await this.ensureWasmRuntime(source);
2122
+ if (!runtime) return false;
2123
+ this.setParticleBackend(runtime.particle());
1913
2124
  return true;
1914
2125
  }
1915
2126
  /** Internal: called by `Entity._spawnDriver` when a new property driver
@@ -1968,7 +2179,8 @@ var Scene = class _Scene {
1968
2179
  */
1969
2180
  _tickBatchedDrivers(dt) {
1970
2181
  if (this._activeDriverEntities.size === 0) return;
1971
- let batchable = 0;
2182
+ let springBatchable = 0;
2183
+ let tweenBatchable = 0;
1972
2184
  for (const entity of this._activeDriverEntities) {
1973
2185
  const entries = entity._driverEntries();
1974
2186
  if (!entries || entries.size === 0) {
@@ -1976,12 +2188,17 @@ var Scene = class _Scene {
1976
2188
  continue;
1977
2189
  }
1978
2190
  for (const driver of entries.values()) {
1979
- if (driver instanceof SpringDriver) batchable++;
1980
- else if (driver instanceof TweenDriver && driver.wasmEasingId !== null) batchable++;
2191
+ if (driver instanceof SpringDriver) springBatchable++;
2192
+ else if (driver instanceof TweenDriver && driver.wasmEasingId !== null) tweenBatchable++;
1981
2193
  }
1982
2194
  }
2195
+ const batchable = springBatchable + tweenBatchable;
1983
2196
  const backend = this._animWasm;
1984
- if (!backend || batchable < this.animDriverGateCount) return;
2197
+ this._animBatchedLastFrame = false;
2198
+ if (!backend) return;
2199
+ const gate = springBatchable > 0 && tweenBatchable > 0 ? this.animGate.mixed : tweenBatchable > 0 ? this.animGate.tween : this.animGate.spring;
2200
+ if (batchable < gate) return;
2201
+ this._animBatchedLastFrame = true;
1985
2202
  const sE = this._springEntities;
1986
2203
  const sP = this._springProps;
1987
2204
  const sD = this._springDrivers;
@@ -2072,6 +2289,11 @@ var Scene = class _Scene {
2072
2289
  this._wasmWorld = backend.worldView();
2073
2290
  this._storeStructureVersion = this._structureVersion;
2074
2291
  }
2292
+ backend.revalidateViews();
2293
+ if (this._wasmInputs && this._wasmInputs.x.length === 0) {
2294
+ this._wasmInputs = backend.inputView();
2295
+ this._wasmWorld = backend.worldView();
2296
+ }
2075
2297
  const inp = this._wasmInputs;
2076
2298
  const slotEntity = this._slotEntity;
2077
2299
  for (let slot = 1; slot < slotEntity.length; slot++) {
@@ -2086,8 +2308,40 @@ var Scene = class _Scene {
2086
2308
  inp.opacity[slot] = e.opacity;
2087
2309
  }
2088
2310
  backend.runKernel("simd");
2311
+ this._wasmAabbsFresh = false;
2089
2312
  return this._wasmWorld;
2090
2313
  }
2314
+ /**
2315
+ * Run the WASM world-AABB pass over the current frame's world matrices, so the
2316
+ * fused hit gather can read AABBs straight out of the store.
2317
+ *
2318
+ * Local bounds are uploaded here rather than in the per-frame transform sync
2319
+ * because `getBounds()` is a virtual call that allocates a rect on most
2320
+ * entities — paying it every frame for a query that may never come would move
2321
+ * cost onto the render path to save it on hover. Returns `false` if any entity
2322
+ * cannot supply bounds through the store, so the caller uses the JS gather.
2323
+ */
2324
+ _ensureWasmAabbs() {
2325
+ const backend = this._wasm;
2326
+ const store = this._treeStore;
2327
+ if (!backend || !store) return false;
2328
+ if (this._wasmAabbsFresh) return true;
2329
+ backend.revalidateViews();
2330
+ const bounds = backend.boundsView();
2331
+ const slotEntity = this._slotEntity;
2332
+ for (let slot = 0; slot < slotEntity.length; slot++) {
2333
+ const e = slotEntity[slot];
2334
+ if (!e) continue;
2335
+ const b = e.getBounds();
2336
+ bounds.bx[slot] = b ? b.x : 0;
2337
+ bounds.by[slot] = b ? b.y : 0;
2338
+ bounds.bw[slot] = b ? b.width : 0;
2339
+ bounds.bh[slot] = b ? b.height : 0;
2340
+ }
2341
+ backend.runAabbs(slotEntity.length);
2342
+ this._wasmAabbsFresh = true;
2343
+ return true;
2344
+ }
2091
2345
  /**
2092
2346
  * Authoritative paint order for semantic nodes discovered during the main
2093
2347
  * render. A node may not have a DOM projection until the following a11y
@@ -2161,17 +2415,17 @@ var Scene = class _Scene {
2161
2415
  this._devFrameCount++;
2162
2416
  if (this._devFrameCount % 120 !== 0) return;
2163
2417
  if (this.a11yElements) {
2164
- let interactiveCount = 0;
2418
+ let projectableCount = 0;
2165
2419
  const walk = (node) => {
2166
- if (node.interactive && node.width > 0) interactiveCount++;
2420
+ if (this.shouldProjectA11y(node)) projectableCount++;
2167
2421
  for (const c of node.children) walk(c);
2168
2422
  };
2169
2423
  walk(this.root);
2170
2424
  for (const c of this.overlayRoot.children) walk(c);
2171
2425
  const shadowCount = this.a11yElements.size;
2172
- if (shadowCount > interactiveCount + 2) {
2426
+ if (shadowCount > projectableCount) {
2173
2427
  this._devWarn(
2174
- `a11yElements (${shadowCount}) exceeds interactive entities (${interactiveCount}). Call scene.detachA11y(entity) before removing interactive children from the tree, or their shadow nodes leak.`
2428
+ `a11yElements (${shadowCount}) exceeds projectable entities (${projectableCount}). Call scene.detachA11y(entity) before removing interactive children from the tree, or their shadow nodes leak.`
2175
2429
  );
2176
2430
  }
2177
2431
  }
@@ -2642,7 +2896,19 @@ var Scene = class _Scene {
2642
2896
  * without removing it from the scene graph. Components that manage dynamic
2643
2897
  * interactive *child* entities (e.g. a {@link Entity}'s per-link hotspots) call
2644
2898
  * this before discarding those children so their shadow `<a>`/controls don't
2645
- * leak (the per-frame `syncA11y` only creates/updates, it never prunes).
2899
+ * leak.
2900
+ *
2901
+ * `syncA11y` itself only creates and updates, never prunes — but it is always
2902
+ * followed by `enforceA11yDomOrder`, whose prune pass removes any element
2903
+ * whose entity is no longer reachable in the tree or no longer satisfies
2904
+ * {@link shouldProjectA11y}. So an entity that is `remove()`d, or whose
2905
+ * `interactive` flips to `false`, has its element torn down on the next synced
2906
+ * frame without any explicit call.
2907
+ *
2908
+ * This method is for the case that pass cannot see: a child dropped from a
2909
+ * component's own bookkeeping while still parented, or one discarded before
2910
+ * the next sync runs. Calling it is always safe and is the right habit for
2911
+ * pooled children.
2646
2912
  *
2647
2913
  * @param entity - The subtree whose shadow nodes should be removed.
2648
2914
  */
@@ -2931,12 +3197,34 @@ var Scene = class _Scene {
2931
3197
  }
2932
3198
  if (element.getAttribute(name) !== value) element.setAttribute(name, value);
2933
3199
  }
3200
+ /**
3201
+ * Whether `node` should have an a11y shadow element projected for it.
3202
+ *
3203
+ * The single authority for that decision. It was previously inlined verbatim
3204
+ * at four call sites — `syncA11y` (create/update), `enforceA11yDomOrder`
3205
+ * (which ids survive pruning), `getA11yTree` (the public snapshot) and
3206
+ * `render` (z-index / reading-order assignment). Four copies of one predicate
3207
+ * is a standing correctness hazard: if any of them drifts, elements either
3208
+ * leak (created but never marked active, so pruned every frame and rebuilt) or
3209
+ * go missing from the semantic tree while still present in the DOM.
3210
+ *
3211
+ * A box is required because a zero-size element is unfocusable and
3212
+ * unhittable; `a11yFullViewport` is the deliberate exception, since those
3213
+ * nodes are boundless interaction surfaces mounted behind everything else.
3214
+ *
3215
+ * Keep this the only place the rule is written. A planned per-entity
3216
+ * `a11yProjection` mode ('eager' | 'onDemand' | 'never') extends exactly this
3217
+ * predicate, which is only tractable while it has one home.
3218
+ */
3219
+ shouldProjectA11y(node) {
3220
+ return node.interactive && (node.width > 0 || node.a11yFullViewport);
3221
+ }
2934
3222
  syncA11y(node) {
2935
3223
  if (!this.a11yRoot) return;
2936
3224
  if (node.isDOMPortal) {
2937
3225
  return;
2938
3226
  }
2939
- if (node.interactive && (node.width > 0 || node.a11yFullViewport)) {
3227
+ if (this.shouldProjectA11y(node)) {
2940
3228
  let el = this.a11yElements.get(node.id);
2941
3229
  const attrs = node.getA11yAttributes();
2942
3230
  const expectedTag = attrs.tag || "div";
@@ -3712,7 +4000,7 @@ var Scene = class _Scene {
3712
4000
  if (node.a11yFullViewport) this.fullViewportElements.push(contentEl);
3713
4001
  else this.normalElements.push(contentEl);
3714
4002
  }
3715
- if (node.interactive && (node.width > 0 || node.a11yFullViewport)) {
4003
+ if (this.shouldProjectA11y(node)) {
3716
4004
  const el = this.a11yElements.get(node.id);
3717
4005
  if (el) {
3718
4006
  this.activeIds.add(node.id);
@@ -3851,7 +4139,7 @@ var Scene = class _Scene {
3851
4139
  const traverse = (node, parentNode) => {
3852
4140
  if (node.isDOMPortal) return;
3853
4141
  let currentA11yNode = null;
3854
- if (node.interactive && (node.width > 0 || node.a11yFullViewport)) {
4142
+ if (this.shouldProjectA11y(node)) {
3855
4143
  const el = this.a11yElements.get(node.id);
3856
4144
  if (el) {
3857
4145
  const attrs = node.getA11yAttributes();
@@ -4221,7 +4509,7 @@ var Scene = class _Scene {
4221
4509
  const orthogonalTolerance = Math.max(1, worldScaleX * worldScaleY) * 1e-6;
4222
4510
  const isSimilarityTransform = Number.isFinite(worldScaleX) && Number.isFinite(worldScaleY) && Math.abs(worldScaleX - worldScaleY) <= scaleTolerance && Math.abs(a * c + b * d) <= orthogonalTolerance;
4223
4511
  const a11yEl = isMainRenderer ? this.a11yElements.get(node.id) : void 0;
4224
- const willProjectA11y = isMainRenderer && node.interactive && (node.width > 0 || node.a11yFullViewport);
4512
+ const willProjectA11y = isMainRenderer && this.shouldProjectA11y(node);
4225
4513
  if (a11yEl || willProjectA11y) {
4226
4514
  const renderOrder = this.renderOrderCounter++;
4227
4515
  if (willProjectA11y) this.a11yRenderOrders.set(node.id, renderOrder);