@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.js +717 -429
- package/dist/index.mjs +554 -266
- package/dist/tree/Scene.d.ts +133 -2
- package/dist/wasm/backend.d.ts +37 -1
- package/dist/wasm/hit-store-fused.d.ts +54 -0
- package/dist/wasm/runtime.d.ts +76 -0
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +1 -1
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
|
-
|
|
525
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
|
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 +
|
|
984
|
+
if (count + PAD4 <= this.entityCap && cellCount <= this.cellCap && itemCount <= this.itemCap) {
|
|
833
985
|
return;
|
|
834
986
|
}
|
|
835
|
-
this.entityCap = count +
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1084
|
-
return new ParticleBackend(instance2);
|
|
1130
|
+
return await hit;
|
|
1085
1131
|
} catch {
|
|
1086
|
-
|
|
1087
|
-
return
|
|
1132
|
+
moduleCache.delete(key);
|
|
1133
|
+
return null;
|
|
1088
1134
|
}
|
|
1089
1135
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
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
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
|
1722
|
-
|
|
1723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
|
1910
|
-
|
|
1911
|
-
|
|
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
|
|
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)
|
|
1980
|
-
else if (driver instanceof TweenDriver && driver.wasmEasingId !== null)
|
|
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
|
-
|
|
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
|
|
2418
|
+
let projectableCount = 0;
|
|
2165
2419
|
const walk = (node) => {
|
|
2166
|
-
if (
|
|
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 >
|
|
2426
|
+
if (shadowCount > projectableCount) {
|
|
2173
2427
|
this._devWarn(
|
|
2174
|
-
`a11yElements (${shadowCount}) exceeds
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 &&
|
|
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);
|