@vectojs/core 1.22.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-LHONR3GO.js → chunk-KEBYJVD6.js} +163 -94
- package/dist/{chunk-UPULSLKA.mjs → chunk-ME4LB2HB.mjs} +163 -94
- package/dist/index.d.ts +1 -0
- package/dist/index.js +805 -419
- package/dist/index.mjs +570 -184
- package/dist/performance/UserTiming.d.ts +44 -0
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/tree/ComputeParticleEntity.d.ts +5 -1
- package/dist/tree/Scene.d.ts +138 -1
- package/dist/wasm/anim-backend.d.ts +21 -4
- package/dist/wasm/backend.d.ts +9 -4
- package/dist/wasm/particle-backend.d.ts +12 -1
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isSafeUrl,
|
|
9
9
|
parseColorToRGBA,
|
|
10
10
|
sanitizeUrl
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ME4LB2HB.mjs";
|
|
12
12
|
import {
|
|
13
13
|
Entity,
|
|
14
14
|
MSDFTextEntity,
|
|
@@ -307,12 +307,16 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
307
307
|
* The backend holds one resident SoA store, so a Scene with multiple particle
|
|
308
308
|
* entities reuses it sequentially — origin is therefore re-gathered each call
|
|
309
309
|
* (not upload-once), a couple of extra f32 reads per particle.
|
|
310
|
+
*
|
|
311
|
+
* Returns `false` if the kernel declined the call and {@link updateCPU} ran
|
|
312
|
+
* instead, so the Scene can report which path actually simulated this frame
|
|
313
|
+
* rather than assuming an installed backend did the work.
|
|
310
314
|
*/
|
|
311
315
|
stepWithBackend(backend, dt, mouseX, mouseY, width, height) {
|
|
312
316
|
const count = this.maxParticles;
|
|
313
317
|
backend.ensure(count);
|
|
314
318
|
backend.gather(this.particleData, count, true);
|
|
315
|
-
|
|
319
|
+
const pending = backend.step(count, {
|
|
316
320
|
dt,
|
|
317
321
|
mouseX,
|
|
318
322
|
mouseY,
|
|
@@ -324,8 +328,14 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
324
328
|
maxVelocity: this.maxVelocity,
|
|
325
329
|
explosion: this.pendingExplosion
|
|
326
330
|
});
|
|
331
|
+
if (pending === null) {
|
|
332
|
+
this.updateCPU(dt, mouseX, mouseY, width, height);
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
this._wasmPending = pending;
|
|
327
336
|
backend.scatter(this.particleData, count);
|
|
328
337
|
this.pendingExplosion = null;
|
|
338
|
+
return true;
|
|
329
339
|
}
|
|
330
340
|
destroy() {
|
|
331
341
|
this.destroyGPUResources();
|
|
@@ -469,148 +479,6 @@ function buildTreeStore(root) {
|
|
|
469
479
|
return { store, indexOf };
|
|
470
480
|
}
|
|
471
481
|
|
|
472
|
-
// src/wasm/hit-store.ts
|
|
473
|
-
function gatherHitAABBs(root, currentFrame) {
|
|
474
|
-
const slotEntity = [];
|
|
475
|
-
const boundless = [];
|
|
476
|
-
const minxs = [];
|
|
477
|
-
const minys = [];
|
|
478
|
-
const maxxs = [];
|
|
479
|
-
const maxys = [];
|
|
480
|
-
const scratch = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
481
|
-
const visit = (node) => {
|
|
482
|
-
const index = slotEntity.length;
|
|
483
|
-
slotEntity.push(node);
|
|
484
|
-
const bounds = node.getBounds();
|
|
485
|
-
if (bounds === null) {
|
|
486
|
-
boundless.push({ entity: node, index });
|
|
487
|
-
minxs.push(0);
|
|
488
|
-
minys.push(0);
|
|
489
|
-
maxxs.push(0);
|
|
490
|
-
maxys.push(0);
|
|
491
|
-
} else {
|
|
492
|
-
if (!node._readWorldCache(currentFrame, scratch)) {
|
|
493
|
-
const t = node.getWorldTransform();
|
|
494
|
-
scratch.a = t.a;
|
|
495
|
-
scratch.b = t.b;
|
|
496
|
-
scratch.c = t.c;
|
|
497
|
-
scratch.d = t.d;
|
|
498
|
-
scratch.e = t.e;
|
|
499
|
-
scratch.f = t.f;
|
|
500
|
-
}
|
|
501
|
-
const { a, b, c, d, e, f } = scratch;
|
|
502
|
-
let minX = Infinity;
|
|
503
|
-
let minY = Infinity;
|
|
504
|
-
let maxX = -Infinity;
|
|
505
|
-
let maxY = -Infinity;
|
|
506
|
-
for (let i = 0; i < 4; i++) {
|
|
507
|
-
const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
|
|
508
|
-
const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
|
|
509
|
-
const wx = a * lx + c * ly + e;
|
|
510
|
-
const wy = b * lx + d * ly + f;
|
|
511
|
-
if (wx < minX) minX = wx;
|
|
512
|
-
if (wx > maxX) maxX = wx;
|
|
513
|
-
if (wy < minY) minY = wy;
|
|
514
|
-
if (wy > maxY) maxY = wy;
|
|
515
|
-
}
|
|
516
|
-
minxs.push(minX);
|
|
517
|
-
minys.push(minY);
|
|
518
|
-
maxxs.push(maxX);
|
|
519
|
-
maxys.push(maxY);
|
|
520
|
-
}
|
|
521
|
-
const kids = node.children;
|
|
522
|
-
for (let i = 0; i < kids.length; i++) visit(kids[i]);
|
|
523
|
-
};
|
|
524
|
-
visit(root);
|
|
525
|
-
return {
|
|
526
|
-
count: slotEntity.length,
|
|
527
|
-
slotEntity,
|
|
528
|
-
boundless,
|
|
529
|
-
minx: Float64Array.from(minxs),
|
|
530
|
-
miny: Float64Array.from(minys),
|
|
531
|
-
maxx: Float64Array.from(maxxs),
|
|
532
|
-
maxy: Float64Array.from(maxys)
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// src/wasm/hit-store-fused.ts
|
|
537
|
-
function gatherHitAABBsFromStore(root, aabbs, storeSlotEntity, out) {
|
|
538
|
-
const slotEntity = out.slotEntity;
|
|
539
|
-
const boundless = out.boundless;
|
|
540
|
-
slotEntity.length = 0;
|
|
541
|
-
boundless.length = 0;
|
|
542
|
-
let count = 0;
|
|
543
|
-
let capacity = out.minx.length;
|
|
544
|
-
let minx = out.minx;
|
|
545
|
-
let miny = out.miny;
|
|
546
|
-
let maxx = out.maxx;
|
|
547
|
-
let maxy = out.maxy;
|
|
548
|
-
const grow = (needed) => {
|
|
549
|
-
if (needed <= capacity) return;
|
|
550
|
-
let next = capacity || 256;
|
|
551
|
-
while (next < needed) next *= 2;
|
|
552
|
-
const nminx = new Float64Array(next);
|
|
553
|
-
const nminy = new Float64Array(next);
|
|
554
|
-
const nmaxx = new Float64Array(next);
|
|
555
|
-
const nmaxy = new Float64Array(next);
|
|
556
|
-
nminx.set(minx);
|
|
557
|
-
nminy.set(miny);
|
|
558
|
-
nmaxx.set(maxx);
|
|
559
|
-
nmaxy.set(maxy);
|
|
560
|
-
minx = nminx;
|
|
561
|
-
miny = nminy;
|
|
562
|
-
maxx = nmaxx;
|
|
563
|
-
maxy = nmaxy;
|
|
564
|
-
capacity = next;
|
|
565
|
-
};
|
|
566
|
-
let bailed = false;
|
|
567
|
-
const visit = (node) => {
|
|
568
|
-
if (bailed) return;
|
|
569
|
-
const index = count;
|
|
570
|
-
slotEntity.push(node);
|
|
571
|
-
count++;
|
|
572
|
-
grow(count);
|
|
573
|
-
if (node.getBounds() === null) {
|
|
574
|
-
boundless.push({ entity: node, index });
|
|
575
|
-
minx[index] = 0;
|
|
576
|
-
miny[index] = 0;
|
|
577
|
-
maxx[index] = 0;
|
|
578
|
-
maxy[index] = 0;
|
|
579
|
-
} else {
|
|
580
|
-
const slot = node._storeSlot;
|
|
581
|
-
if (slot < 0 || slot >= aabbs.aminx.length || storeSlotEntity[slot] !== node) {
|
|
582
|
-
bailed = true;
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
minx[index] = aabbs.aminx[slot];
|
|
586
|
-
miny[index] = aabbs.aminy[slot];
|
|
587
|
-
maxx[index] = aabbs.amaxx[slot];
|
|
588
|
-
maxy[index] = aabbs.amaxy[slot];
|
|
589
|
-
}
|
|
590
|
-
const children = node.children;
|
|
591
|
-
for (let i = 0; i < children.length; i++) visit(children[i]);
|
|
592
|
-
};
|
|
593
|
-
visit(root);
|
|
594
|
-
if (bailed) return null;
|
|
595
|
-
out.count = count;
|
|
596
|
-
out.minx = minx;
|
|
597
|
-
out.miny = miny;
|
|
598
|
-
out.maxx = maxx;
|
|
599
|
-
out.maxy = maxy;
|
|
600
|
-
return out;
|
|
601
|
-
}
|
|
602
|
-
function createHitGatherBuffer() {
|
|
603
|
-
return {
|
|
604
|
-
count: 0,
|
|
605
|
-
slotEntity: [],
|
|
606
|
-
boundless: [],
|
|
607
|
-
minx: new Float64Array(256),
|
|
608
|
-
miny: new Float64Array(256),
|
|
609
|
-
maxx: new Float64Array(256),
|
|
610
|
-
maxy: new Float64Array(256)
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
482
|
// src/wasm/backend.ts
|
|
615
483
|
var WASM_STATUS = {
|
|
616
484
|
OK: 0,
|
|
@@ -703,7 +571,10 @@ var WasmTransformBackend = class {
|
|
|
703
571
|
return status;
|
|
704
572
|
}
|
|
705
573
|
/** Upload only the run table + count (topology), leaving per-entity inputs to
|
|
706
|
-
* the resident views. Call when the tree structure changes, not per frame.
|
|
574
|
+
* the resident views. Call when the tree structure changes, not per frame.
|
|
575
|
+
* Returns `false` if the crate rejected the run count, in which case the
|
|
576
|
+
* PREVIOUS topology is still published and any kernel run against it would
|
|
577
|
+
* compose the wrong tree — the caller must not proceed. */
|
|
707
578
|
uploadRuns(store) {
|
|
708
579
|
this.ensure(store.count, store.runCount);
|
|
709
580
|
const rc = store.runCount;
|
|
@@ -711,6 +582,7 @@ var WasmTransformBackend = class {
|
|
|
711
582
|
this.vrs.set(store.runStart.subarray(0, rc));
|
|
712
583
|
this.vrl.set(store.runLen.subarray(0, rc));
|
|
713
584
|
this.lastStatus = this.ex.set_run_count(rc);
|
|
585
|
+
return this.lastStatus === WASM_STATUS.OK;
|
|
714
586
|
}
|
|
715
587
|
/**
|
|
716
588
|
* Status of the most recent kernel or run-table call. `WASM_STATUS.OK` unless
|
|
@@ -759,7 +631,8 @@ var WasmTransformBackend = class {
|
|
|
759
631
|
this.vby.set(store.by.subarray(0, n));
|
|
760
632
|
this.vbw.set(store.bw.subarray(0, n));
|
|
761
633
|
this.vbh.set(store.bh.subarray(0, n));
|
|
762
|
-
this.ex.compute_aabbs(n);
|
|
634
|
+
this.lastStatus = this.ex.compute_aabbs(n);
|
|
635
|
+
if (this.lastStatus !== WASM_STATUS.OK) return;
|
|
763
636
|
store.aminx.set(this.vaminx.subarray(0, n));
|
|
764
637
|
store.aminy.set(this.vaminy.subarray(0, n));
|
|
765
638
|
store.amaxx.set(this.vamaxx.subarray(0, n));
|
|
@@ -767,9 +640,12 @@ var WasmTransformBackend = class {
|
|
|
767
640
|
}
|
|
768
641
|
/** Run the AABB pass only, over `count` entities already resident in wasm
|
|
769
642
|
* memory (bounds written via {@link boundsView}, world matrices already
|
|
770
|
-
* composed). No upload/readback — the per-frame resident path.
|
|
643
|
+
* composed). No upload/readback — the per-frame resident path. Returns
|
|
644
|
+
* `false` if the kernel rejected `count` (beyond capacity, or uninitialized),
|
|
645
|
+
* in which case {@link aabbView} still holds the previous frame's bounds. */
|
|
771
646
|
runAabbs(count) {
|
|
772
|
-
this.ex.compute_aabbs(count);
|
|
647
|
+
this.lastStatus = this.ex.compute_aabbs(count);
|
|
648
|
+
return this.lastStatus === WASM_STATUS.OK;
|
|
773
649
|
}
|
|
774
650
|
/** Resident wasm local-bounds input views (`bx,by,bw,bh`) for the AABB pass. */
|
|
775
651
|
boundsView() {
|
|
@@ -843,6 +719,148 @@ var WasmTransformBackend = class {
|
|
|
843
719
|
}
|
|
844
720
|
};
|
|
845
721
|
|
|
722
|
+
// src/wasm/hit-store.ts
|
|
723
|
+
function gatherHitAABBs(root, currentFrame) {
|
|
724
|
+
const slotEntity = [];
|
|
725
|
+
const boundless = [];
|
|
726
|
+
const minxs = [];
|
|
727
|
+
const minys = [];
|
|
728
|
+
const maxxs = [];
|
|
729
|
+
const maxys = [];
|
|
730
|
+
const scratch = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
731
|
+
const visit = (node) => {
|
|
732
|
+
const index = slotEntity.length;
|
|
733
|
+
slotEntity.push(node);
|
|
734
|
+
const bounds = node.getBounds();
|
|
735
|
+
if (bounds === null) {
|
|
736
|
+
boundless.push({ entity: node, index });
|
|
737
|
+
minxs.push(0);
|
|
738
|
+
minys.push(0);
|
|
739
|
+
maxxs.push(0);
|
|
740
|
+
maxys.push(0);
|
|
741
|
+
} else {
|
|
742
|
+
if (!node._readWorldCache(currentFrame, scratch)) {
|
|
743
|
+
const t = node.getWorldTransform();
|
|
744
|
+
scratch.a = t.a;
|
|
745
|
+
scratch.b = t.b;
|
|
746
|
+
scratch.c = t.c;
|
|
747
|
+
scratch.d = t.d;
|
|
748
|
+
scratch.e = t.e;
|
|
749
|
+
scratch.f = t.f;
|
|
750
|
+
}
|
|
751
|
+
const { a, b, c, d, e, f } = scratch;
|
|
752
|
+
let minX = Infinity;
|
|
753
|
+
let minY = Infinity;
|
|
754
|
+
let maxX = -Infinity;
|
|
755
|
+
let maxY = -Infinity;
|
|
756
|
+
for (let i = 0; i < 4; i++) {
|
|
757
|
+
const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
|
|
758
|
+
const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
|
|
759
|
+
const wx = a * lx + c * ly + e;
|
|
760
|
+
const wy = b * lx + d * ly + f;
|
|
761
|
+
if (wx < minX) minX = wx;
|
|
762
|
+
if (wx > maxX) maxX = wx;
|
|
763
|
+
if (wy < minY) minY = wy;
|
|
764
|
+
if (wy > maxY) maxY = wy;
|
|
765
|
+
}
|
|
766
|
+
minxs.push(minX);
|
|
767
|
+
minys.push(minY);
|
|
768
|
+
maxxs.push(maxX);
|
|
769
|
+
maxys.push(maxY);
|
|
770
|
+
}
|
|
771
|
+
const kids = node.children;
|
|
772
|
+
for (let i = 0; i < kids.length; i++) visit(kids[i]);
|
|
773
|
+
};
|
|
774
|
+
visit(root);
|
|
775
|
+
return {
|
|
776
|
+
count: slotEntity.length,
|
|
777
|
+
slotEntity,
|
|
778
|
+
boundless,
|
|
779
|
+
minx: Float64Array.from(minxs),
|
|
780
|
+
miny: Float64Array.from(minys),
|
|
781
|
+
maxx: Float64Array.from(maxxs),
|
|
782
|
+
maxy: Float64Array.from(maxys)
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/wasm/hit-store-fused.ts
|
|
787
|
+
function gatherHitAABBsFromStore(root, aabbs, storeSlotEntity, out) {
|
|
788
|
+
const slotEntity = out.slotEntity;
|
|
789
|
+
const boundless = out.boundless;
|
|
790
|
+
slotEntity.length = 0;
|
|
791
|
+
boundless.length = 0;
|
|
792
|
+
let count = 0;
|
|
793
|
+
let capacity = out.minx.length;
|
|
794
|
+
let minx = out.minx;
|
|
795
|
+
let miny = out.miny;
|
|
796
|
+
let maxx = out.maxx;
|
|
797
|
+
let maxy = out.maxy;
|
|
798
|
+
const grow = (needed) => {
|
|
799
|
+
if (needed <= capacity) return;
|
|
800
|
+
let next = capacity || 256;
|
|
801
|
+
while (next < needed) next *= 2;
|
|
802
|
+
const nminx = new Float64Array(next);
|
|
803
|
+
const nminy = new Float64Array(next);
|
|
804
|
+
const nmaxx = new Float64Array(next);
|
|
805
|
+
const nmaxy = new Float64Array(next);
|
|
806
|
+
nminx.set(minx);
|
|
807
|
+
nminy.set(miny);
|
|
808
|
+
nmaxx.set(maxx);
|
|
809
|
+
nmaxy.set(maxy);
|
|
810
|
+
minx = nminx;
|
|
811
|
+
miny = nminy;
|
|
812
|
+
maxx = nmaxx;
|
|
813
|
+
maxy = nmaxy;
|
|
814
|
+
capacity = next;
|
|
815
|
+
};
|
|
816
|
+
let bailed = false;
|
|
817
|
+
const visit = (node) => {
|
|
818
|
+
if (bailed) return;
|
|
819
|
+
const index = count;
|
|
820
|
+
slotEntity.push(node);
|
|
821
|
+
count++;
|
|
822
|
+
grow(count);
|
|
823
|
+
if (node.getBounds() === null) {
|
|
824
|
+
boundless.push({ entity: node, index });
|
|
825
|
+
minx[index] = 0;
|
|
826
|
+
miny[index] = 0;
|
|
827
|
+
maxx[index] = 0;
|
|
828
|
+
maxy[index] = 0;
|
|
829
|
+
} else {
|
|
830
|
+
const slot = node._storeSlot;
|
|
831
|
+
if (slot < 0 || slot >= aabbs.aminx.length || storeSlotEntity[slot] !== node) {
|
|
832
|
+
bailed = true;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
minx[index] = aabbs.aminx[slot];
|
|
836
|
+
miny[index] = aabbs.aminy[slot];
|
|
837
|
+
maxx[index] = aabbs.amaxx[slot];
|
|
838
|
+
maxy[index] = aabbs.amaxy[slot];
|
|
839
|
+
}
|
|
840
|
+
const children = node.children;
|
|
841
|
+
for (let i = 0; i < children.length; i++) visit(children[i]);
|
|
842
|
+
};
|
|
843
|
+
visit(root);
|
|
844
|
+
if (bailed) return null;
|
|
845
|
+
out.count = count;
|
|
846
|
+
out.minx = minx;
|
|
847
|
+
out.miny = miny;
|
|
848
|
+
out.maxx = maxx;
|
|
849
|
+
out.maxy = maxy;
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
function createHitGatherBuffer() {
|
|
853
|
+
return {
|
|
854
|
+
count: 0,
|
|
855
|
+
slotEntity: [],
|
|
856
|
+
boundless: [],
|
|
857
|
+
minx: new Float64Array(256),
|
|
858
|
+
miny: new Float64Array(256),
|
|
859
|
+
maxx: new Float64Array(256),
|
|
860
|
+
maxy: new Float64Array(256)
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
846
864
|
// src/wasm/anim-backend.ts
|
|
847
865
|
var PAD3 = 8;
|
|
848
866
|
var AnimBackend = class {
|
|
@@ -877,14 +895,33 @@ var AnimBackend = class {
|
|
|
877
895
|
this.ex.anim_init(this.springCap, this.tweenCap);
|
|
878
896
|
this.refreshViews();
|
|
879
897
|
}
|
|
880
|
-
/**
|
|
898
|
+
/**
|
|
899
|
+
* Advance `count` springs (from index 0) by `dtMs` milliseconds, in place.
|
|
900
|
+
* Returns `true` when the kernel ran. `false` means it rejected the call
|
|
901
|
+
* (count beyond the capacity {@link ensure} allocated, or no `anim_init` yet)
|
|
902
|
+
* and wrote nothing, so the caller must tick those drivers in JS instead of
|
|
903
|
+
* scattering back a pack the kernel never touched. See {@link lastStatus}.
|
|
904
|
+
*/
|
|
881
905
|
stepSprings(dtMs, count) {
|
|
882
|
-
this.ex.spring_step(dtMs / 1e3, count);
|
|
906
|
+
this.lastStatus = this.ex.spring_step(dtMs / 1e3, count);
|
|
907
|
+
return this.lastStatus === WASM_STATUS.OK;
|
|
883
908
|
}
|
|
884
|
-
/**
|
|
909
|
+
/**
|
|
910
|
+
* Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`.
|
|
911
|
+
* Returns `true` when the kernel ran; `false` means it rejected the call and
|
|
912
|
+
* wrote nothing (see {@link stepSprings}). `elapsed` is kernel-side state, so
|
|
913
|
+
* a rejected tween pack must not be read back — it is unadvanced, not
|
|
914
|
+
* partially advanced.
|
|
915
|
+
*/
|
|
885
916
|
stepTweens(dtMs, count) {
|
|
886
|
-
this.ex.tween_step(dtMs, count);
|
|
917
|
+
this.lastStatus = this.ex.tween_step(dtMs, count);
|
|
918
|
+
return this.lastStatus === WASM_STATUS.OK;
|
|
887
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* Status of the most recent kernel call — `WASM_STATUS.OK` unless the kernel
|
|
922
|
+
* declined it. Mirrors {@link TransformBackend.lastStatus}.
|
|
923
|
+
*/
|
|
924
|
+
lastStatus = WASM_STATUS.OK;
|
|
888
925
|
refreshViews() {
|
|
889
926
|
const buf = this.ex.memory.buffer;
|
|
890
927
|
const sCap = this.springCap;
|
|
@@ -1035,6 +1072,12 @@ var ParticleBackend = class {
|
|
|
1035
1072
|
* Advance `count` particles one step in place. Returns `true` when at least
|
|
1036
1073
|
* one live particle is still moving or off-origin beyond epsilon (the fused
|
|
1037
1074
|
* `hasPendingAnimations` flag), so the caller need not re-scan the buffer.
|
|
1075
|
+
*
|
|
1076
|
+
* Returns `null` when the kernel REJECTED the call — `count` beyond the
|
|
1077
|
+
* capacity {@link ensure} allocated, or no `particle_init` yet. Nothing was
|
|
1078
|
+
* written, so the caller must NOT {@link scatter} (that would write the
|
|
1079
|
+
* gathered pre-step values back and freeze the simulation) and should fall
|
|
1080
|
+
* back to the JS `updateCPU` path for this frame. See {@link lastStatus}.
|
|
1038
1081
|
*/
|
|
1039
1082
|
step(count, p) {
|
|
1040
1083
|
const e = p.explosion;
|
|
@@ -1054,8 +1097,18 @@ var ParticleBackend = class {
|
|
|
1054
1097
|
e ? e.force : 0,
|
|
1055
1098
|
count
|
|
1056
1099
|
);
|
|
1100
|
+
if (flag < 0) {
|
|
1101
|
+
this.lastStatus = -flag;
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
this.lastStatus = WASM_STATUS.OK;
|
|
1057
1105
|
return flag !== 0;
|
|
1058
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Status of the most recent {@link step} — `WASM_STATUS.OK` unless the kernel
|
|
1109
|
+
* declined it. Mirrors {@link TransformBackend.lastStatus}.
|
|
1110
|
+
*/
|
|
1111
|
+
lastStatus = WASM_STATUS.OK;
|
|
1059
1112
|
/** Transpose the AoS stride-8 buffer into the SoA views (position/velocity/
|
|
1060
1113
|
* life every frame; origin upload-once when `withOrigin`). */
|
|
1061
1114
|
gather(data, count, withOrigin) {
|
|
@@ -1189,6 +1242,79 @@ async function loadCoreWasmRuntime(source) {
|
|
|
1189
1242
|
return createCoreWasmRuntime(module);
|
|
1190
1243
|
}
|
|
1191
1244
|
|
|
1245
|
+
// src/performance/UserTiming.ts
|
|
1246
|
+
var VECTO_USER_TIMING = {
|
|
1247
|
+
scene: {
|
|
1248
|
+
transform: "vecto:scene:transform",
|
|
1249
|
+
drawWalk: "vecto:scene:draw-walk",
|
|
1250
|
+
entityPaint: "vecto:scene:entity-paint",
|
|
1251
|
+
flush: "vecto:scene:flush",
|
|
1252
|
+
a11ySync: "vecto:scene:a11y-sync"
|
|
1253
|
+
},
|
|
1254
|
+
markdown: {
|
|
1255
|
+
parse: "vecto:markdown:parse"
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
var nextSpanId = 0;
|
|
1259
|
+
function beginVectoUserTiming(name) {
|
|
1260
|
+
const candidate = globalThis.performance;
|
|
1261
|
+
if (typeof candidate?.mark !== "function" || typeof candidate.measure !== "function") {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
const id = nextSpanId++;
|
|
1265
|
+
const startMark = `${name}:start:${id}`;
|
|
1266
|
+
const endMark = `${name}:end:${id}`;
|
|
1267
|
+
try {
|
|
1268
|
+
candidate.mark(startMark);
|
|
1269
|
+
return {
|
|
1270
|
+
name,
|
|
1271
|
+
startMark,
|
|
1272
|
+
endMark,
|
|
1273
|
+
performance: candidate
|
|
1274
|
+
};
|
|
1275
|
+
} catch {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function endVectoUserTiming(span) {
|
|
1280
|
+
if (!span) return;
|
|
1281
|
+
const timing = span.performance;
|
|
1282
|
+
try {
|
|
1283
|
+
timing.mark(span.endMark);
|
|
1284
|
+
timing.measure(span.name, span.startMark, span.endMark);
|
|
1285
|
+
} catch {
|
|
1286
|
+
} finally {
|
|
1287
|
+
try {
|
|
1288
|
+
timing.clearMarks?.(span.startMark);
|
|
1289
|
+
timing.clearMarks?.(span.endMark);
|
|
1290
|
+
} catch {
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function measureVectoUserTiming(name, durationMs) {
|
|
1295
|
+
const candidate = globalThis.performance;
|
|
1296
|
+
if (typeof candidate?.now !== "function" || typeof candidate.mark !== "function" || typeof candidate.measure !== "function") {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const id = nextSpanId++;
|
|
1300
|
+
const startMark = `${name}:start:${id}`;
|
|
1301
|
+
const endMark = `${name}:end:${id}`;
|
|
1302
|
+
const endTime = candidate.now();
|
|
1303
|
+
const startTime = Math.max(0, endTime - Math.max(0, durationMs));
|
|
1304
|
+
try {
|
|
1305
|
+
candidate.mark(startMark, { startTime });
|
|
1306
|
+
candidate.mark(endMark, { startTime: endTime });
|
|
1307
|
+
candidate.measure(name, startMark, endMark);
|
|
1308
|
+
} catch {
|
|
1309
|
+
} finally {
|
|
1310
|
+
try {
|
|
1311
|
+
candidate.clearMarks?.(startMark);
|
|
1312
|
+
candidate.clearMarks?.(endMark);
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1192
1318
|
// src/tree/Scene.ts
|
|
1193
1319
|
import { clearCssLineBoxMetrics, cssLineBoxBaseline } from "@vectojs/text";
|
|
1194
1320
|
var RANGE_VALUE_ROLES = /* @__PURE__ */ new Set(["slider", "spinbutton", "progressbar", "scrollbar", "meter"]);
|
|
@@ -1203,9 +1329,47 @@ var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
|
|
|
1203
1329
|
"slider",
|
|
1204
1330
|
"combobox"
|
|
1205
1331
|
]);
|
|
1332
|
+
var A11Y_REQUIRED_OWNED = /* @__PURE__ */ new Map([
|
|
1333
|
+
["grid", /* @__PURE__ */ new Set(["row", "rowgroup"])],
|
|
1334
|
+
["table", /* @__PURE__ */ new Set(["row", "rowgroup"])],
|
|
1335
|
+
["treegrid", /* @__PURE__ */ new Set(["row", "rowgroup"])],
|
|
1336
|
+
["rowgroup", /* @__PURE__ */ new Set(["row"])],
|
|
1337
|
+
["row", /* @__PURE__ */ new Set(["cell", "columnheader", "gridcell", "rowheader"])],
|
|
1338
|
+
["tablist", /* @__PURE__ */ new Set(["tab"])],
|
|
1339
|
+
["tree", /* @__PURE__ */ new Set(["treeitem", "group"])],
|
|
1340
|
+
["group", /* @__PURE__ */ new Set(["treeitem", "menuitem", "menuitemcheckbox", "menuitemradio", "option"])],
|
|
1341
|
+
["menu", /* @__PURE__ */ new Set(["menuitem", "menuitemcheckbox", "menuitemradio", "group", "separator"])],
|
|
1342
|
+
["menubar", /* @__PURE__ */ new Set(["menuitem", "menuitemcheckbox", "menuitemradio", "group", "separator"])],
|
|
1343
|
+
["listbox", /* @__PURE__ */ new Set(["option", "group"])],
|
|
1344
|
+
["list", /* @__PURE__ */ new Set(["listitem"])]
|
|
1345
|
+
]);
|
|
1206
1346
|
function isNativelyFocusable(element) {
|
|
1207
1347
|
return element instanceof HTMLButtonElement || element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLAnchorElement && element.hasAttribute("href");
|
|
1208
1348
|
}
|
|
1349
|
+
var REBASED_BOX = { left: 0, top: 0, matrix: "" };
|
|
1350
|
+
function rebaseChildBox(parent, parentOriginX, parentOriginY, child, childOriginX, childOriginY) {
|
|
1351
|
+
const det = parent.a * parent.d - parent.b * parent.c;
|
|
1352
|
+
if (!Number.isFinite(det) || Math.abs(det) < 1e-12) {
|
|
1353
|
+
REBASED_BOX.left = 0;
|
|
1354
|
+
REBASED_BOX.top = 0;
|
|
1355
|
+
REBASED_BOX.matrix = "matrix(1, 0, 0, 1, 0, 0)";
|
|
1356
|
+
return REBASED_BOX;
|
|
1357
|
+
}
|
|
1358
|
+
const ia = parent.d / det;
|
|
1359
|
+
const ib = -parent.b / det;
|
|
1360
|
+
const ic = -parent.c / det;
|
|
1361
|
+
const id = parent.a / det;
|
|
1362
|
+
const dx = childOriginX - parentOriginX;
|
|
1363
|
+
const dy = childOriginY - parentOriginY;
|
|
1364
|
+
REBASED_BOX.left = ia * dx + ic * dy;
|
|
1365
|
+
REBASED_BOX.top = ib * dx + id * dy;
|
|
1366
|
+
const a = ia * child.a + ic * child.b;
|
|
1367
|
+
const b = ib * child.a + id * child.b;
|
|
1368
|
+
const c = ia * child.c + ic * child.d;
|
|
1369
|
+
const d = ib * child.c + id * child.d;
|
|
1370
|
+
REBASED_BOX.matrix = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
1371
|
+
return REBASED_BOX;
|
|
1372
|
+
}
|
|
1209
1373
|
var REDUCED_MOTION_FPS = 30;
|
|
1210
1374
|
function parseInlinePx(value) {
|
|
1211
1375
|
if (!value || !value.endsWith("px")) return null;
|
|
@@ -1540,6 +1704,7 @@ var Scene = class _Scene {
|
|
|
1540
1704
|
/** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
|
|
1541
1705
|
static MAX_DIRTY_REASONS = 200;
|
|
1542
1706
|
_phaseTiming = false;
|
|
1707
|
+
_userTiming = false;
|
|
1543
1708
|
_phaseTotals = /* @__PURE__ */ new Map();
|
|
1544
1709
|
/**
|
|
1545
1710
|
* Start or stop per-phase render timing.
|
|
@@ -1562,6 +1727,19 @@ var Scene = class _Scene {
|
|
|
1562
1727
|
get phaseTiming() {
|
|
1563
1728
|
return this._phaseTiming;
|
|
1564
1729
|
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Enable or disable browser User Timing phase instrumentation.
|
|
1732
|
+
*
|
|
1733
|
+
* Off by default. The disabled frame path performs only boolean checks and
|
|
1734
|
+
* emits no Performance Timeline entries.
|
|
1735
|
+
*/
|
|
1736
|
+
setUserTiming(enabled) {
|
|
1737
|
+
this._userTiming = enabled;
|
|
1738
|
+
}
|
|
1739
|
+
/** Whether browser User Timing phase instrumentation is enabled. */
|
|
1740
|
+
get userTiming() {
|
|
1741
|
+
return this._userTiming;
|
|
1742
|
+
}
|
|
1565
1743
|
/**
|
|
1566
1744
|
* Accumulate one phase sample.
|
|
1567
1745
|
*
|
|
@@ -1767,6 +1945,8 @@ var Scene = class _Scene {
|
|
|
1767
1945
|
fullViewportElements = [];
|
|
1768
1946
|
normalElements = [];
|
|
1769
1947
|
activeIds = /* @__PURE__ */ new Set();
|
|
1948
|
+
/** Per-parent insertion cursor, reused by `enforceA11yDomOrder`. */
|
|
1949
|
+
a11yOrderCursors = /* @__PURE__ */ new Map();
|
|
1770
1950
|
activePortalsThisFrame = /* @__PURE__ */ new Set();
|
|
1771
1951
|
activePortalsPrevFrame = /* @__PURE__ */ new Set();
|
|
1772
1952
|
portalEntities = /* @__PURE__ */ new Map();
|
|
@@ -1866,10 +2046,13 @@ var Scene = class _Scene {
|
|
|
1866
2046
|
return true;
|
|
1867
2047
|
}
|
|
1868
2048
|
// ── WASM hit-test backend (invisible accelerator, G3) ───────────────────────
|
|
1869
|
-
//
|
|
1870
|
-
//
|
|
1871
|
-
//
|
|
1872
|
-
// AABBs into a dense viewport grid for findEntityAt.
|
|
2049
|
+
// Served by the same instance as every other accelerator (see
|
|
2050
|
+
// `_wasmRuntime`): the crate keeps transform/anim/hit/particle in distinct
|
|
2051
|
+
// statics, so one instance runs them all without aliasing. It indexes the
|
|
2052
|
+
// main tree's world AABBs into a dense viewport grid for findEntityAt. Note
|
|
2053
|
+
// that sharing one linear memory means an allocation here can grow it and
|
|
2054
|
+
// detach views built over the old buffer, so each backend re-checks buffer
|
|
2055
|
+
// identity (`revalidateViews`) before use. The JS depth-first walk
|
|
1873
2056
|
// (findHitRecursively) is the permanent fallback: a null backend, a build
|
|
1874
2057
|
// that overflows its item budget, or the overlay tree (never indexed — small
|
|
1875
2058
|
// and rare, not worth accelerating) all fall through to it, so WASM can only
|
|
@@ -1942,6 +2125,70 @@ var Scene = class _Scene {
|
|
|
1942
2125
|
get hitGatherPath() {
|
|
1943
2126
|
return this._hitFusedGather ? "fused" : "js";
|
|
1944
2127
|
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Why the transform accelerator did or did not run on the most recent frame.
|
|
2130
|
+
* Written by the render walk and `_syncWasmStore`.
|
|
2131
|
+
*/
|
|
2132
|
+
_transformReason = "not-installed";
|
|
2133
|
+
/** Why the batched-driver accelerator did or did not run. */
|
|
2134
|
+
_animReason = "not-installed";
|
|
2135
|
+
/**
|
|
2136
|
+
* Why the hit-test accelerator did or did not serve the last pointer query.
|
|
2137
|
+
* The grid is built lazily on demand, not every frame, so this describes the
|
|
2138
|
+
* most recent BUILD. Starts at `'not-installed'` because that is the truth
|
|
2139
|
+
* before a backend exists; `_ensureHitGrid` moves it to `'not-applicable'`
|
|
2140
|
+
* once one is installed but nothing has queried yet.
|
|
2141
|
+
*/
|
|
2142
|
+
_hitReason = "not-installed";
|
|
2143
|
+
/** Why the particle accelerator did or did not run. */
|
|
2144
|
+
_particleReason = "not-applicable";
|
|
2145
|
+
/** Which particle implementation actually simulated the most recent frame. */
|
|
2146
|
+
_particlePath = "none";
|
|
2147
|
+
/**
|
|
2148
|
+
* Per-frame status of every invisible accelerator: whether each is installed,
|
|
2149
|
+
* whether it actually ran on the most recent frame, and why.
|
|
2150
|
+
*
|
|
2151
|
+
* This exists because the older per-accelerator getters
|
|
2152
|
+
* ({@link transformBackend}, {@link animBackend}, {@link hitTestBackend},
|
|
2153
|
+
* {@link particleBackend}) report only that a backend is INSTALLED. Reading
|
|
2154
|
+
* `'wasm'` from one of those and concluding the accelerator is doing work is
|
|
2155
|
+
* wrong whenever a gate never opens, a kernel rejects its arguments, or a
|
|
2156
|
+
* faster backend takes the pass instead. Read {@link AcceleratorStatus.reason}
|
|
2157
|
+
* for which of those happened.
|
|
2158
|
+
*
|
|
2159
|
+
* Reflects the most recent main-renderer frame; a secondary renderer (SVG
|
|
2160
|
+
* export, offscreen snapshot) does not overwrite it.
|
|
2161
|
+
*/
|
|
2162
|
+
get accelerators() {
|
|
2163
|
+
return {
|
|
2164
|
+
transform: {
|
|
2165
|
+
available: this._wasm !== null && this._transformBackend === "wasm",
|
|
2166
|
+
activeThisFrame: this._transformReason === "active",
|
|
2167
|
+
reason: this._transformReason,
|
|
2168
|
+
path: this._transformReason === "active" ? "wasm" : "js"
|
|
2169
|
+
},
|
|
2170
|
+
animation: {
|
|
2171
|
+
available: this._animWasm !== null,
|
|
2172
|
+
activeThisFrame: this._animBatchedLastFrame,
|
|
2173
|
+
reason: this._animReason,
|
|
2174
|
+
path: this._animBatchedLastFrame ? "wasm" : "js"
|
|
2175
|
+
},
|
|
2176
|
+
hitTest: {
|
|
2177
|
+
available: this._hitWasm !== null,
|
|
2178
|
+
// The grid is built lazily on a pointer query, not every frame, so this
|
|
2179
|
+
// describes the last BUILD rather than the last frame.
|
|
2180
|
+
activeThisFrame: this._hitReason === "active",
|
|
2181
|
+
reason: this._hitReason,
|
|
2182
|
+
path: this._hitReason !== "active" ? "js" : this._hitFusedGather ? "wasm-fused" : "wasm"
|
|
2183
|
+
},
|
|
2184
|
+
particle: {
|
|
2185
|
+
available: this._particleWasm !== null || this.webgpuActive,
|
|
2186
|
+
activeThisFrame: this._particleReason === "active",
|
|
2187
|
+
reason: this._particleReason,
|
|
2188
|
+
path: this._particlePath
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
1945
2192
|
/** Which backend answers `findEntityAt` for the main tree. */
|
|
1946
2193
|
get hitTestBackend() {
|
|
1947
2194
|
return this._hitWasm ? "wasm" : "js";
|
|
@@ -1951,6 +2198,7 @@ var Scene = class _Scene {
|
|
|
1951
2198
|
setHitTestBackend(backend) {
|
|
1952
2199
|
this._hitWasm = backend;
|
|
1953
2200
|
this._hitGridFrame = -1;
|
|
2201
|
+
this._hitReason = backend ? "not-applicable" : "not-installed";
|
|
1954
2202
|
}
|
|
1955
2203
|
/**
|
|
1956
2204
|
* Asynchronously instantiate the WASM hit-test core and, on success, hot-swap
|
|
@@ -1977,7 +2225,10 @@ var Scene = class _Scene {
|
|
|
1977
2225
|
*/
|
|
1978
2226
|
_ensureHitGrid() {
|
|
1979
2227
|
const backend = this._hitWasm;
|
|
1980
|
-
if (!backend)
|
|
2228
|
+
if (!backend) {
|
|
2229
|
+
this._hitReason = "not-installed";
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
1981
2232
|
if (this._hitGridFrame === this.currentFrame) return this._hitGridOk;
|
|
1982
2233
|
let gathered = null;
|
|
1983
2234
|
if (this._wasm && this._ensureWasmAabbs()) {
|
|
@@ -2006,6 +2257,7 @@ var Scene = class _Scene {
|
|
|
2006
2257
|
this._hitBoundless = gathered.boundless;
|
|
2007
2258
|
this._hitGridFrame = this.currentFrame;
|
|
2008
2259
|
this._hitGridOk = ok;
|
|
2260
|
+
this._hitReason = ok ? "active" : "rejected";
|
|
2009
2261
|
return ok;
|
|
2010
2262
|
}
|
|
2011
2263
|
/**
|
|
@@ -2271,7 +2523,10 @@ var Scene = class _Scene {
|
|
|
2271
2523
|
* pre-pass.
|
|
2272
2524
|
*/
|
|
2273
2525
|
_tickBatchedDrivers(dt) {
|
|
2274
|
-
if (this._activeDriverEntities.size === 0)
|
|
2526
|
+
if (this._activeDriverEntities.size === 0) {
|
|
2527
|
+
this._animReason = "not-applicable";
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2275
2530
|
let springBatchable = 0;
|
|
2276
2531
|
let tweenBatchable = 0;
|
|
2277
2532
|
for (const entity of this._activeDriverEntities) {
|
|
@@ -2288,10 +2543,17 @@ var Scene = class _Scene {
|
|
|
2288
2543
|
const batchable = springBatchable + tweenBatchable;
|
|
2289
2544
|
const backend = this._animWasm;
|
|
2290
2545
|
this._animBatchedLastFrame = false;
|
|
2291
|
-
if (!backend)
|
|
2546
|
+
if (!backend) {
|
|
2547
|
+
this._animReason = "not-installed";
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2292
2550
|
const gate = springBatchable > 0 && tweenBatchable > 0 ? this.animGate.mixed : tweenBatchable > 0 ? this.animGate.tween : this.animGate.spring;
|
|
2293
|
-
if (batchable < gate)
|
|
2551
|
+
if (batchable < gate) {
|
|
2552
|
+
this._animReason = "below-gate";
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2294
2555
|
this._animBatchedLastFrame = true;
|
|
2556
|
+
this._animReason = "active";
|
|
2295
2557
|
const sE = this._springEntities;
|
|
2296
2558
|
const sP = this._springProps;
|
|
2297
2559
|
const sD = this._springDrivers;
|
|
@@ -2338,8 +2600,13 @@ var Scene = class _Scene {
|
|
|
2338
2600
|
sv.damp[i] = phys.damping;
|
|
2339
2601
|
sv.mass[i] = phys.mass;
|
|
2340
2602
|
}
|
|
2341
|
-
backend.stepSprings(dt, springCount)
|
|
2342
|
-
|
|
2603
|
+
if (backend.stepSprings(dt, springCount)) {
|
|
2604
|
+
for (let i = 0; i < springCount; i++) sD[i].syncExternal(sv.val[i], sv.vel[i]);
|
|
2605
|
+
} else {
|
|
2606
|
+
for (let i = 0; i < springCount; i++) sD[i].tick(dt);
|
|
2607
|
+
this._animReason = "rejected";
|
|
2608
|
+
this._animBatchedLastFrame = false;
|
|
2609
|
+
}
|
|
2343
2610
|
}
|
|
2344
2611
|
if (tweenCount > 0) {
|
|
2345
2612
|
const tv = backend.tweenView();
|
|
@@ -2352,8 +2619,13 @@ var Scene = class _Scene {
|
|
|
2352
2619
|
tv.delay[i] = d.delayMs;
|
|
2353
2620
|
tv.ease[i] = d.wasmEasingId;
|
|
2354
2621
|
}
|
|
2355
|
-
backend.stepTweens(dt, tweenCount)
|
|
2356
|
-
|
|
2622
|
+
if (backend.stepTweens(dt, tweenCount)) {
|
|
2623
|
+
for (let i = 0; i < tweenCount; i++) tD[i].syncExternal(tv.val[i], tv.elapsed[i]);
|
|
2624
|
+
} else {
|
|
2625
|
+
for (let i = 0; i < tweenCount; i++) tD[i].tick(dt);
|
|
2626
|
+
this._animReason = "rejected";
|
|
2627
|
+
this._animBatchedLastFrame = false;
|
|
2628
|
+
}
|
|
2357
2629
|
}
|
|
2358
2630
|
for (let i = 0; i < springCount; i++) sE[i]._applyDriverTick(sP[i], sD[i]);
|
|
2359
2631
|
for (let i = 0; i < tweenCount; i++) tE[i]._applyDriverTick(tP[i], tD[i]);
|
|
@@ -2375,7 +2647,10 @@ var Scene = class _Scene {
|
|
|
2375
2647
|
slotEntity2[slot] = entity;
|
|
2376
2648
|
entity._storeSlot = slot;
|
|
2377
2649
|
}
|
|
2378
|
-
backend.uploadRuns(built.store)
|
|
2650
|
+
if (!backend.uploadRuns(built.store)) {
|
|
2651
|
+
this._transformReason = "rejected";
|
|
2652
|
+
return null;
|
|
2653
|
+
}
|
|
2379
2654
|
this._treeStore = built.store;
|
|
2380
2655
|
this._slotEntity = slotEntity2;
|
|
2381
2656
|
this._wasmInputs = backend.inputView();
|
|
@@ -2400,8 +2675,12 @@ var Scene = class _Scene {
|
|
|
2400
2675
|
inp.sin[slot] = trig.sin;
|
|
2401
2676
|
inp.opacity[slot] = e.opacity;
|
|
2402
2677
|
}
|
|
2403
|
-
backend.runKernel("simd")
|
|
2678
|
+
if (backend.runKernel("simd") !== WASM_STATUS.OK) {
|
|
2679
|
+
this._transformReason = "rejected";
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2404
2682
|
this._wasmAabbsFresh = false;
|
|
2683
|
+
this._transformReason = "active";
|
|
2405
2684
|
return this._wasmWorld;
|
|
2406
2685
|
}
|
|
2407
2686
|
/**
|
|
@@ -2431,7 +2710,7 @@ var Scene = class _Scene {
|
|
|
2431
2710
|
bounds.bw[slot] = b ? b.width : 0;
|
|
2432
2711
|
bounds.bh[slot] = b ? b.height : 0;
|
|
2433
2712
|
}
|
|
2434
|
-
backend.runAabbs(slotEntity.length);
|
|
2713
|
+
if (!backend.runAabbs(slotEntity.length)) return false;
|
|
2435
2714
|
this._wasmAabbsFresh = true;
|
|
2436
2715
|
return true;
|
|
2437
2716
|
}
|
|
@@ -2582,6 +2861,7 @@ var Scene = class _Scene {
|
|
|
2582
2861
|
this.maxFPS = options.maxFPS ?? (isTest ? 0 : 60);
|
|
2583
2862
|
this.respectReducedMotion = options.respectReducedMotion ?? true;
|
|
2584
2863
|
this.autoThrottle = options.autoThrottle ?? true;
|
|
2864
|
+
this._userTiming = options.userTiming ?? false;
|
|
2585
2865
|
this.particleBackend = options.particleBackend ?? "auto";
|
|
2586
2866
|
this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
|
|
2587
2867
|
this.contentProjectionEnabled = options.contentProjection ?? true;
|
|
@@ -2626,6 +2906,7 @@ var Scene = class _Scene {
|
|
|
2626
2906
|
});
|
|
2627
2907
|
if (typeof document !== "undefined") {
|
|
2628
2908
|
this.a11yRoot = document.createElement("div");
|
|
2909
|
+
this.a11yRoot.setAttribute("data-vecto-a11y-root", "");
|
|
2629
2910
|
this.a11yRoot.style.position = "absolute";
|
|
2630
2911
|
this.a11yRoot.style.top = "0";
|
|
2631
2912
|
this.a11yRoot.style.left = "0";
|
|
@@ -3478,7 +3759,7 @@ var Scene = class _Scene {
|
|
|
3478
3759
|
shouldProjectA11y(node) {
|
|
3479
3760
|
return node.interactive && (node.width > 0 || node.a11yFullViewport);
|
|
3480
3761
|
}
|
|
3481
|
-
syncA11y(node) {
|
|
3762
|
+
syncA11y(node, container = null) {
|
|
3482
3763
|
if (!this.a11yRoot) return;
|
|
3483
3764
|
if (node.isDOMPortal) {
|
|
3484
3765
|
return;
|
|
@@ -3488,6 +3769,7 @@ var Scene = class _Scene {
|
|
|
3488
3769
|
return;
|
|
3489
3770
|
}
|
|
3490
3771
|
const nodeStart = this._phaseTiming ? performance.now() : 0;
|
|
3772
|
+
let childContainer = container;
|
|
3491
3773
|
if (this.shouldProjectA11y(node)) {
|
|
3492
3774
|
let el = this.a11yElements.get(node.id);
|
|
3493
3775
|
const attrs = node.getA11yAttributes();
|
|
@@ -3500,9 +3782,9 @@ var Scene = class _Scene {
|
|
|
3500
3782
|
this.caretBlinkTimer = null;
|
|
3501
3783
|
}
|
|
3502
3784
|
}
|
|
3503
|
-
if (el.parentNode
|
|
3785
|
+
if (el.parentNode) {
|
|
3504
3786
|
this.preserveFocusOnRemoval(el);
|
|
3505
|
-
|
|
3787
|
+
el.remove();
|
|
3506
3788
|
}
|
|
3507
3789
|
this.a11yElements.delete(node.id);
|
|
3508
3790
|
el = void 0;
|
|
@@ -3810,6 +4092,7 @@ var Scene = class _Scene {
|
|
|
3810
4092
|
if (el.style.boxSizing !== "border-box") el.style.boxSizing = "border-box";
|
|
3811
4093
|
if (el instanceof HTMLTextAreaElement) el.style.resize = "none";
|
|
3812
4094
|
}
|
|
4095
|
+
const nestedIn = container && attrs.role && container.owned.has(attrs.role) ? container : null;
|
|
3813
4096
|
if (node.a11yFullViewport) {
|
|
3814
4097
|
el.style.left = "0px";
|
|
3815
4098
|
el.style.top = "0px";
|
|
@@ -3820,11 +4103,36 @@ var Scene = class _Scene {
|
|
|
3820
4103
|
} else {
|
|
3821
4104
|
const worldTf = node.getWorldTransform();
|
|
3822
4105
|
const { a, b, c, d, e, f } = worldTf;
|
|
3823
|
-
|
|
3824
|
-
|
|
4106
|
+
const originX = e + node.a11yOffsetX;
|
|
4107
|
+
const originY = f + node.a11yOffsetY;
|
|
4108
|
+
const parentEl = nestedIn && nestedIn.el !== el && nestedIn.el.isConnected ? nestedIn.el : this.a11yRoot;
|
|
4109
|
+
if (el.parentNode !== parentEl) {
|
|
4110
|
+
parentEl.appendChild(el);
|
|
4111
|
+
this.a11yNeedsReorder = true;
|
|
4112
|
+
}
|
|
4113
|
+
if (parentEl === this.a11yRoot) {
|
|
4114
|
+
el.style.left = `${originX}px`;
|
|
4115
|
+
el.style.top = `${originY}px`;
|
|
4116
|
+
el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
|
|
4117
|
+
} else {
|
|
4118
|
+
const box = rebaseChildBox(
|
|
4119
|
+
nestedIn.transform,
|
|
4120
|
+
nestedIn.originX,
|
|
4121
|
+
nestedIn.originY,
|
|
4122
|
+
worldTf,
|
|
4123
|
+
originX,
|
|
4124
|
+
originY
|
|
4125
|
+
);
|
|
4126
|
+
el.style.left = `${box.left}px`;
|
|
4127
|
+
el.style.top = `${box.top}px`;
|
|
4128
|
+
el.style.transform = box.matrix;
|
|
4129
|
+
}
|
|
3825
4130
|
el.style.width = `${node.width}px`;
|
|
3826
4131
|
el.style.height = `${node.height}px`;
|
|
3827
|
-
|
|
4132
|
+
const owned = attrs.role ? A11Y_REQUIRED_OWNED.get(attrs.role) : void 0;
|
|
4133
|
+
if (owned) {
|
|
4134
|
+
childContainer = { el, owned, transform: worldTf, originX, originY };
|
|
4135
|
+
}
|
|
3828
4136
|
const visible = this.projectionBoxVisible(node, worldTf, 0);
|
|
3829
4137
|
const display = visible ? "" : "none";
|
|
3830
4138
|
if (el.style.display !== display) el.style.display = display;
|
|
@@ -3838,9 +4146,9 @@ var Scene = class _Scene {
|
|
|
3838
4146
|
} else {
|
|
3839
4147
|
this.syncContentProjection(node);
|
|
3840
4148
|
}
|
|
3841
|
-
for (const child of node.children) this.syncA11y(child);
|
|
4149
|
+
for (const child of node.children) this.syncA11y(child, childContainer);
|
|
3842
4150
|
if (node === this.root) {
|
|
3843
|
-
for (const overlay of this.overlayRoot.children) this.syncA11y(overlay);
|
|
4151
|
+
for (const overlay of this.overlayRoot.children) this.syncA11y(overlay, null);
|
|
3844
4152
|
}
|
|
3845
4153
|
}
|
|
3846
4154
|
/**
|
|
@@ -4398,9 +4706,9 @@ var Scene = class _Scene {
|
|
|
4398
4706
|
this.caretBlinkTimer = null;
|
|
4399
4707
|
}
|
|
4400
4708
|
}
|
|
4401
|
-
if (el.parentNode
|
|
4709
|
+
if (el.parentNode) {
|
|
4402
4710
|
this.preserveFocusOnRemoval(el);
|
|
4403
|
-
|
|
4711
|
+
el.remove();
|
|
4404
4712
|
}
|
|
4405
4713
|
this.a11yElements.delete(id);
|
|
4406
4714
|
}
|
|
@@ -4413,23 +4721,39 @@ var Scene = class _Scene {
|
|
|
4413
4721
|
const fullLen = this.fullViewportElements.length;
|
|
4414
4722
|
const normalLen = this.normalElements.length;
|
|
4415
4723
|
const totalLen = fullLen + normalLen;
|
|
4724
|
+
this.a11yOrderCursors.clear();
|
|
4416
4725
|
for (let i = 0; i < totalLen; i++) {
|
|
4417
4726
|
const expected = i < fullLen ? this.fullViewportElements[i] : this.normalElements[i - fullLen];
|
|
4418
|
-
const
|
|
4727
|
+
const parent = expected.parentNode;
|
|
4728
|
+
if (!parent) continue;
|
|
4729
|
+
const at = this.a11yOrderCursors.get(parent) ?? 0;
|
|
4730
|
+
this.a11yOrderCursors.set(parent, at + 1);
|
|
4731
|
+
const current = parent.childNodes[at];
|
|
4419
4732
|
if (current !== expected) {
|
|
4420
|
-
|
|
4733
|
+
parent.insertBefore(expected, current || null);
|
|
4421
4734
|
}
|
|
4422
4735
|
}
|
|
4423
4736
|
this.a11yNeedsReorder = false;
|
|
4424
4737
|
}
|
|
4425
4738
|
/**
|
|
4426
4739
|
* Reorder `normalElements` (in place) into visual reading order using the
|
|
4427
|
-
*
|
|
4740
|
+
* positions `syncA11y` already wrote to each element's inline style
|
|
4428
4741
|
* (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an
|
|
4429
4742
|
* element belongs to the current row while its top is above the row's
|
|
4430
4743
|
* running bottom edge), then sorted within a row by `left` — ascending for
|
|
4431
4744
|
* `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the
|
|
4432
4745
|
* same position keep their scene-graph (collection) order as a tiebreak.
|
|
4746
|
+
*
|
|
4747
|
+
* Those inline values are world coordinates for a top-level mirror but
|
|
4748
|
+
* PARENT-RELATIVE for a nested one, so this list mixes coordinate spaces.
|
|
4749
|
+
* That is sound because the result is only ever applied per DOM parent
|
|
4750
|
+
* ({@link enforceA11yDomOrder} advances a cursor per parent), and all of one
|
|
4751
|
+
* parent's children share one space: a `grid`'s rows are all grid-relative, a
|
|
4752
|
+
* `row`'s cells all row-relative. Comparisons ACROSS spaces do happen while
|
|
4753
|
+
* banding, but they only affect the relative order of elements in different
|
|
4754
|
+
* parents, which no `insertBefore` ever acts on. Normalizing everything back
|
|
4755
|
+
* to world coordinates here would cost a transform per element per frame to
|
|
4756
|
+
* change nothing observable.
|
|
4433
4757
|
*/
|
|
4434
4758
|
sortNormalElementsVisually() {
|
|
4435
4759
|
const els = this.normalElements;
|
|
@@ -4665,9 +4989,11 @@ var Scene = class _Scene {
|
|
|
4665
4989
|
if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
|
|
4666
4990
|
this.lastA11ySync = time;
|
|
4667
4991
|
if (hasInteractive || wantsContentSync) {
|
|
4992
|
+
const userTiming = this._userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.a11ySync) : null;
|
|
4668
4993
|
const t0 = this._phaseTiming ? performance.now() : 0;
|
|
4669
4994
|
this.syncA11y(this.root);
|
|
4670
4995
|
if (this._phaseTiming) this._recordPhase("a11ySync", performance.now() - t0);
|
|
4996
|
+
if (userTiming) endVectoUserTiming(userTiming);
|
|
4671
4997
|
}
|
|
4672
4998
|
const t1 = this._phaseTiming ? performance.now() : 0;
|
|
4673
4999
|
this.enforceA11yDomOrder();
|
|
@@ -4681,6 +5007,26 @@ var Scene = class _Scene {
|
|
|
4681
5007
|
/**
|
|
4682
5008
|
* Render the entire scene graph onto the specified renderer.
|
|
4683
5009
|
*
|
|
5010
|
+
* Main-frame causal order is a correctness contract:
|
|
5011
|
+
*
|
|
5012
|
+
* 1. Browser/input callbacks finish before the scheduled frame begins.
|
|
5013
|
+
* 2. Batched property drivers and particle simulation advance.
|
|
5014
|
+
* 3. Entity `update()` hooks run.
|
|
5015
|
+
* 4. Transform inputs are gathered and world matrices are composed.
|
|
5016
|
+
* 5. Updated world bounds are tested for culling.
|
|
5017
|
+
* 6. Visible entities paint in scene-graph order.
|
|
5018
|
+
* 7. Canvas/GPU batches flush and retained renderers present.
|
|
5019
|
+
* 8. The rAF loop synchronizes content and accessibility projections after
|
|
5020
|
+
* this method returns.
|
|
5021
|
+
*
|
|
5022
|
+
* The causal order is fixed; physical walks may stay fused. The JavaScript
|
|
5023
|
+
* transform path interleaves update → compose → cull → paint per node in
|
|
5024
|
+
* pre-order. The WASM path updates the whole tree first, then gathers and
|
|
5025
|
+
* composes it in one store pass before the same cull/paint walk. Both must
|
|
5026
|
+
* expose an update's transform mutation in that same rendered frame.
|
|
5027
|
+
* Secondary renderers are read-only snapshots: they skip simulation and
|
|
5028
|
+
* updates, then compose/cull/paint/flush the current state.
|
|
5029
|
+
*
|
|
4684
5030
|
* @param renderer - The renderer instance to draw to.
|
|
4685
5031
|
* @param dt - Delta time in milliseconds (default 0).
|
|
4686
5032
|
* @param time - Current absolute time in milliseconds (default 0).
|
|
@@ -4705,6 +5051,10 @@ var Scene = class _Scene {
|
|
|
4705
5051
|
const computeEntities = this._computeEntitiesFor(this._structureVersion);
|
|
4706
5052
|
if (computeEntities.length > 0) {
|
|
4707
5053
|
const isMainRenderPath = renderer === this.renderer;
|
|
5054
|
+
if (isMainRenderPath) {
|
|
5055
|
+
this._particleReason = this._particleWasm ? "active" : "not-installed";
|
|
5056
|
+
this._particlePath = this._particleWasm ? "wasm" : "js";
|
|
5057
|
+
}
|
|
4708
5058
|
if (isMainRenderPath && !this.device && !this.webgpuDisabled && !this.initializingWebGPU && !this.deviceLost) {
|
|
4709
5059
|
this.initializingWebGPU = true;
|
|
4710
5060
|
this.initWebGPUContext(computeEntities).then((newDevice) => {
|
|
@@ -4780,6 +5130,8 @@ var Scene = class _Scene {
|
|
|
4780
5130
|
}
|
|
4781
5131
|
this.device.queue.submit([commandEncoder.finish()]);
|
|
4782
5132
|
if (this.gpuContext) this.gpuHasContent = true;
|
|
5133
|
+
this._particleReason = "active";
|
|
5134
|
+
this._particlePath = "webgpu";
|
|
4783
5135
|
} catch (e) {
|
|
4784
5136
|
console.error("WebGPU frame execution failed. Falling back.", e);
|
|
4785
5137
|
this.deviceLost = true;
|
|
@@ -4801,13 +5153,25 @@ var Scene = class _Scene {
|
|
|
4801
5153
|
}
|
|
4802
5154
|
}
|
|
4803
5155
|
if (this._particleWasm) {
|
|
4804
|
-
entity.stepWithBackend(
|
|
5156
|
+
if (!entity.stepWithBackend(
|
|
5157
|
+
this._particleWasm,
|
|
5158
|
+
dt / 1e3,
|
|
5159
|
+
mx,
|
|
5160
|
+
my,
|
|
5161
|
+
this.width,
|
|
5162
|
+
this.height
|
|
5163
|
+
)) {
|
|
5164
|
+
this._particleReason = "rejected";
|
|
5165
|
+
this._particlePath = "js";
|
|
5166
|
+
}
|
|
4805
5167
|
} else {
|
|
4806
5168
|
entity.updateCPU(dt / 1e3, mx, my, this.width, this.height);
|
|
4807
5169
|
}
|
|
4808
5170
|
}
|
|
4809
5171
|
}
|
|
4810
5172
|
} else if (isMainRenderer) {
|
|
5173
|
+
this._particleReason = "not-applicable";
|
|
5174
|
+
this._particlePath = "none";
|
|
4811
5175
|
this.clearGPUCanvasIfStale();
|
|
4812
5176
|
}
|
|
4813
5177
|
renderer.clear();
|
|
@@ -4836,6 +5200,9 @@ var Scene = class _Scene {
|
|
|
4836
5200
|
}
|
|
4837
5201
|
};
|
|
4838
5202
|
const wasmMain = isMainRenderer && this._wasm !== null && this._transformBackend === "wasm";
|
|
5203
|
+
if (isMainRenderer) {
|
|
5204
|
+
this._transformReason = wasmMain ? "active" : "not-installed";
|
|
5205
|
+
}
|
|
4839
5206
|
if (wasmMain) {
|
|
4840
5207
|
const updateWalk = (node) => {
|
|
4841
5208
|
runUpdate(node);
|
|
@@ -4845,10 +5212,13 @@ var Scene = class _Scene {
|
|
|
4845
5212
|
updateWalk(this.root);
|
|
4846
5213
|
for (const overlay of this.overlayRoot.children) updateWalk(overlay);
|
|
4847
5214
|
}
|
|
5215
|
+
const transformTiming = this._userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.transform) : null;
|
|
4848
5216
|
const wasmT0 = this._phaseTiming ? performance.now() : 0;
|
|
4849
5217
|
const wasmWorld = wasmMain ? this._syncWasmStore() : null;
|
|
4850
5218
|
if (this._phaseTiming) this._recordPhase("transform", performance.now() - wasmT0);
|
|
5219
|
+
if (transformTiming) endVectoUserTiming(transformTiming);
|
|
4851
5220
|
const wasmSlotEntity = this._slotEntity;
|
|
5221
|
+
let userEntityPaintMs = 0;
|
|
4852
5222
|
const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
|
|
4853
5223
|
if (isMainRenderer && !wasmMain) {
|
|
4854
5224
|
runUpdate(node);
|
|
@@ -4977,10 +5347,15 @@ var Scene = class _Scene {
|
|
|
4977
5347
|
);
|
|
4978
5348
|
}
|
|
4979
5349
|
} else {
|
|
4980
|
-
if (this._phaseTiming) {
|
|
5350
|
+
if (this._userTiming || this._phaseTiming) {
|
|
4981
5351
|
const t0 = performance.now();
|
|
4982
|
-
|
|
4983
|
-
|
|
5352
|
+
try {
|
|
5353
|
+
node.render(renderer);
|
|
5354
|
+
} finally {
|
|
5355
|
+
const elapsed = performance.now() - t0;
|
|
5356
|
+
if (this._userTiming) userEntityPaintMs += elapsed;
|
|
5357
|
+
if (this._phaseTiming) this._recordPhase("entityPaint", elapsed);
|
|
5358
|
+
}
|
|
4984
5359
|
} else {
|
|
4985
5360
|
node.render(renderer);
|
|
4986
5361
|
}
|
|
@@ -4995,17 +5370,23 @@ var Scene = class _Scene {
|
|
|
4995
5370
|
renderer.flush();
|
|
4996
5371
|
renderer.restore();
|
|
4997
5372
|
};
|
|
5373
|
+
const drawTiming = this._userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.drawWalk) : null;
|
|
4998
5374
|
const drawT0 = this._phaseTiming ? performance.now() : 0;
|
|
4999
5375
|
renderNode(this.root, 1, 0, 0, 1, 0, 0, 1);
|
|
5000
5376
|
for (const overlay of this.overlayRoot.children) {
|
|
5001
5377
|
renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
|
|
5002
5378
|
}
|
|
5003
5379
|
if (this._phaseTiming) this._recordPhase("drawWalk", performance.now() - drawT0);
|
|
5380
|
+
if (drawTiming) endVectoUserTiming(drawTiming);
|
|
5381
|
+
if (this._userTiming) {
|
|
5382
|
+
measureVectoUserTiming(VECTO_USER_TIMING.scene.entityPaint, userEntityPaintMs);
|
|
5383
|
+
}
|
|
5004
5384
|
if (isMainRenderer) {
|
|
5005
5385
|
this.frameHadAnimation = walkHadAnimation;
|
|
5006
5386
|
this.frameHadInteractive = walkHadInteractive;
|
|
5007
5387
|
this.reconcilePortals();
|
|
5008
5388
|
}
|
|
5389
|
+
const flushTiming = this._userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.flush) : null;
|
|
5009
5390
|
const flushT0 = this._phaseTiming ? performance.now() : 0;
|
|
5010
5391
|
renderer.flush();
|
|
5011
5392
|
if (isMainRenderer) {
|
|
@@ -5013,6 +5394,7 @@ var Scene = class _Scene {
|
|
|
5013
5394
|
}
|
|
5014
5395
|
renderer.present?.();
|
|
5015
5396
|
if (this._phaseTiming) this._recordPhase("flush", performance.now() - flushT0);
|
|
5397
|
+
if (flushTiming) endVectoUserTiming(flushTiming);
|
|
5016
5398
|
if (this._devActive) {
|
|
5017
5399
|
this._devFrameCount++;
|
|
5018
5400
|
this._devRunChecks();
|
|
@@ -6112,11 +6494,15 @@ export {
|
|
|
6112
6494
|
SplineEntity,
|
|
6113
6495
|
TextEntity,
|
|
6114
6496
|
TextRasterCache,
|
|
6497
|
+
VECTO_USER_TIMING,
|
|
6115
6498
|
VectoJSEvent,
|
|
6116
6499
|
WebGPUParticleSystemManager,
|
|
6500
|
+
beginVectoUserTiming,
|
|
6117
6501
|
createWebGLPointRenderer,
|
|
6502
|
+
endVectoUserTiming,
|
|
6118
6503
|
isSafeUrl,
|
|
6119
6504
|
loadSpline,
|
|
6505
|
+
measureVectoUserTiming,
|
|
6120
6506
|
parseColorToRGBA,
|
|
6121
6507
|
polySegmentToBezier,
|
|
6122
6508
|
sanitizeUrl
|