@vectojs/core 1.38.1 → 1.39.1
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/README.md +43 -135
- package/dist/{chunk-QVTCKWDO.mjs → chunk-7PYD5UDX.mjs} +93 -43
- package/dist/{chunk-LWEMD34D.mjs → chunk-AISSDI6U.mjs} +132 -33
- package/dist/{chunk-ZYYP2M5D.js → chunk-BKXERWAD.js} +181 -82
- package/dist/{chunk-SRXU3ZXK.js → chunk-POPXUPKR.js} +141 -91
- package/dist/index.js +772 -401
- package/dist/index.mjs +459 -88
- package/dist/renderer/CanvasRenderer.d.ts +6 -0
- package/dist/renderer/SVGRenderer.d.ts +17 -2
- package/dist/renderer/WebGPUParticleSystemManager.d.ts +2 -0
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/text/MSDFTextEntity.d.ts +0 -1
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +8 -2
- package/dist/tree/Scene.d.ts +123 -0
- package/dist/tree/scene/A11yProjectionManager.d.ts +12 -0
- package/dist/tree/scene/HitTester.d.ts +14 -2
- package/dist/tree/scene/WasmBackendFacade.d.ts +17 -0
- package/dist/tree/scene/keyboard.d.ts +56 -0
- package/dist/wasm/backend.d.ts +7 -7
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +8 -8
package/dist/index.mjs
CHANGED
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
parseColorToRGBA,
|
|
12
12
|
sanitizeUrl,
|
|
13
13
|
setRendererDevMode
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-AISSDI6U.mjs";
|
|
15
15
|
import {
|
|
16
16
|
Entity,
|
|
17
17
|
MSDFTextEntity,
|
|
18
18
|
SVGEntity,
|
|
19
19
|
VectoJSEvent,
|
|
20
20
|
contentLineInHint
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-7PYD5UDX.mjs";
|
|
22
22
|
|
|
23
23
|
// src/tree/ComputeParticleEntity.ts
|
|
24
24
|
var PARTICLE_STRIDE_FLOATS = 8;
|
|
@@ -224,14 +224,14 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
224
224
|
const bounceDamping = Math.max(0, Math.min(1, this.bounceDamping));
|
|
225
225
|
const maxVelocity = Math.max(1, this.maxVelocity);
|
|
226
226
|
for (let i = 0; i < this.maxParticles; i++) {
|
|
227
|
-
const offset = i *
|
|
228
|
-
let px = this.particleData[offset];
|
|
229
|
-
let py = this.particleData[offset +
|
|
230
|
-
let vx = this.particleData[offset +
|
|
231
|
-
let vy = this.particleData[offset +
|
|
232
|
-
const ox = this.particleData[offset +
|
|
233
|
-
const oy = this.particleData[offset +
|
|
234
|
-
const life = this.particleData[offset +
|
|
227
|
+
const offset = i * PARTICLE_STRIDE_FLOATS;
|
|
228
|
+
let px = this.particleData[offset + PARTICLE_OFFSET_POSITION_X];
|
|
229
|
+
let py = this.particleData[offset + PARTICLE_OFFSET_POSITION_Y];
|
|
230
|
+
let vx = this.particleData[offset + PARTICLE_OFFSET_VELOCITY_X];
|
|
231
|
+
let vy = this.particleData[offset + PARTICLE_OFFSET_VELOCITY_Y];
|
|
232
|
+
const ox = this.particleData[offset + PARTICLE_OFFSET_ORIGIN_X];
|
|
233
|
+
const oy = this.particleData[offset + PARTICLE_OFFSET_ORIGIN_Y];
|
|
234
|
+
const life = this.particleData[offset + PARTICLE_OFFSET_LIFE];
|
|
235
235
|
if (isNaN(px)) px = ox;
|
|
236
236
|
if (isNaN(py)) py = oy;
|
|
237
237
|
if (isNaN(vx)) vx = 0;
|
|
@@ -289,11 +289,11 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
289
289
|
if (life >= 0) {
|
|
290
290
|
nlife = Math.max(0, life - safeDt * 0.5);
|
|
291
291
|
}
|
|
292
|
-
this.particleData[offset] = npx;
|
|
293
|
-
this.particleData[offset +
|
|
294
|
-
this.particleData[offset +
|
|
295
|
-
this.particleData[offset +
|
|
296
|
-
this.particleData[offset +
|
|
292
|
+
this.particleData[offset + PARTICLE_OFFSET_POSITION_X] = npx;
|
|
293
|
+
this.particleData[offset + PARTICLE_OFFSET_POSITION_Y] = npy;
|
|
294
|
+
this.particleData[offset + PARTICLE_OFFSET_VELOCITY_X] = nvx;
|
|
295
|
+
this.particleData[offset + PARTICLE_OFFSET_VELOCITY_Y] = nvy;
|
|
296
|
+
this.particleData[offset + PARTICLE_OFFSET_LIFE] = nlife;
|
|
297
297
|
}
|
|
298
298
|
this.pendingExplosion = null;
|
|
299
299
|
}
|
|
@@ -593,7 +593,12 @@ var ContentGridProjector = class {
|
|
|
593
593
|
lineIndex === 0
|
|
594
594
|
);
|
|
595
595
|
const reusable = existingLines[domIndex];
|
|
596
|
-
if (reusable !== void 0 && reusable.dataset.vectoGridLineSig === lineSignature
|
|
596
|
+
if (reusable !== void 0 && reusable.dataset.vectoGridLineSig === lineSignature) {
|
|
597
|
+
const indexLabel = `${lineIndex}`;
|
|
598
|
+
if (reusable.dataset.vectoGridLine !== indexLabel) {
|
|
599
|
+
reusable.dataset.vectoGridLine = indexLabel;
|
|
600
|
+
reusable.style.top = `${(projectedLine?.y ?? lineIndex * grid.lineHeight) + baseline - cssLineBoxBaseline(lineFont, lineHeight)}px`;
|
|
601
|
+
}
|
|
597
602
|
continue;
|
|
598
603
|
}
|
|
599
604
|
if (selectionLine !== null && selectionLine === lineIndex) rebuiltSelectionLine = true;
|
|
@@ -779,6 +784,12 @@ function projectionCaretAt(root, absoluteOffset, affinity) {
|
|
|
779
784
|
}
|
|
780
785
|
|
|
781
786
|
// src/tree/scene/A11yProjectionManager.ts
|
|
787
|
+
function orderBandBottom(r) {
|
|
788
|
+
return r.container ? r.top + 4 : r.top + Math.max(Number.parseFloat(r.el.style.height) || 0, 4);
|
|
789
|
+
}
|
|
790
|
+
var byTopThenIndex = (p, q) => p.top - q.top || p.i - q.i;
|
|
791
|
+
var byLeftAscThenIndex = (p, q) => p.left - q.left || p.i - q.i;
|
|
792
|
+
var byLeftDescThenIndex = (p, q) => q.left - p.left || p.i - q.i;
|
|
782
793
|
var A11yProjectionManager = class {
|
|
783
794
|
/**
|
|
784
795
|
* Set by anything that changes which elements exist or how they nest, and
|
|
@@ -820,6 +831,18 @@ var A11yProjectionManager = class {
|
|
|
820
831
|
* implicit root region. See {@link sortNormalElementsVisually}.
|
|
821
832
|
*/
|
|
822
833
|
orderRegions = /* @__PURE__ */ new Map();
|
|
834
|
+
/**
|
|
835
|
+
* Decoration records for {@link sortNormalElementsVisually}: one per ordered
|
|
836
|
+
* element, pooled across passes and rewritten in place — the sort runs on
|
|
837
|
+
* structure-change frames, where the module's zero-GC policy applies.
|
|
838
|
+
*/
|
|
839
|
+
orderDecorated = [];
|
|
840
|
+
/** Region → decorated elements, reused by {@link sortNormalElementsVisually}. */
|
|
841
|
+
orderBucketByRegion = /* @__PURE__ */ new Map();
|
|
842
|
+
/** Pool of bucket arrays backing {@link orderBucketByRegion} across passes. */
|
|
843
|
+
orderBuckets = [];
|
|
844
|
+
/** Scratch for one visual row inside {@link sortNormalElementsVisually}. */
|
|
845
|
+
orderRowScratch = [];
|
|
823
846
|
/** Mark the projected DOM as needing a reorder on the next pass. */
|
|
824
847
|
markNeedsReorder() {
|
|
825
848
|
this.needsReorder = true;
|
|
@@ -877,7 +900,8 @@ var A11yProjectionManager = class {
|
|
|
877
900
|
this.orderCursors.set(parent, at + 1);
|
|
878
901
|
const current = parent.childNodes[at];
|
|
879
902
|
if (current !== expected) {
|
|
880
|
-
const
|
|
903
|
+
const active = document.activeElement;
|
|
904
|
+
const refocusTarget = active && (active === expected || expected.contains(active)) ? active : null;
|
|
881
905
|
if (!selectionSnapshotTaken) {
|
|
882
906
|
selectionSnapshotTaken = true;
|
|
883
907
|
const live = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
@@ -893,7 +917,7 @@ var A11yProjectionManager = class {
|
|
|
893
917
|
selectionMoved = true;
|
|
894
918
|
}
|
|
895
919
|
parent.insertBefore(expected, current || null);
|
|
896
|
-
if (
|
|
920
|
+
if (refocusTarget) refocusTarget.focus({ preventScroll: true });
|
|
897
921
|
}
|
|
898
922
|
}
|
|
899
923
|
if (selectionMoved && selection && selAnchorNode && selFocusNode) {
|
|
@@ -940,7 +964,6 @@ var A11yProjectionManager = class {
|
|
|
940
964
|
sortNormalElementsVisually(rtl) {
|
|
941
965
|
const els = this.normalElements;
|
|
942
966
|
if (els.length < 2) return;
|
|
943
|
-
const heightOf = (el) => Math.max(Number.parseFloat(el.style.height) || 0, 4);
|
|
944
967
|
const members = this.orderMembers;
|
|
945
968
|
const containers = this.orderContainers;
|
|
946
969
|
members.clear();
|
|
@@ -951,52 +974,70 @@ var A11yProjectionManager = class {
|
|
|
951
974
|
if (members.has(p)) containers.add(p);
|
|
952
975
|
}
|
|
953
976
|
}
|
|
954
|
-
const
|
|
977
|
+
const decorated = this.orderDecorated;
|
|
978
|
+
while (decorated.length < els.length) {
|
|
979
|
+
decorated.push({ el: els[0], i: 0, top: 0, left: 0, container: false });
|
|
980
|
+
}
|
|
981
|
+
for (let i = 0; i < els.length; i++) {
|
|
982
|
+
const rec = decorated[i];
|
|
955
983
|
let top = 0;
|
|
956
984
|
let left = 0;
|
|
957
|
-
for (let node =
|
|
985
|
+
for (let node = els[i]; node; node = node.parentElement) {
|
|
958
986
|
top += Number.parseFloat(node.style.top) || 0;
|
|
959
987
|
left += Number.parseFloat(node.style.left) || 0;
|
|
960
988
|
const parent = node.parentElement;
|
|
961
989
|
if (!parent || !members.has(parent)) break;
|
|
962
990
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
}
|
|
991
|
+
rec.el = els[i];
|
|
992
|
+
rec.i = i;
|
|
993
|
+
rec.top = top;
|
|
994
|
+
rec.left = left;
|
|
995
|
+
rec.container = containers.has(els[i]);
|
|
996
|
+
}
|
|
969
997
|
const regions = this.orderRegions;
|
|
970
|
-
const buckets =
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
998
|
+
const buckets = this.orderBucketByRegion;
|
|
999
|
+
const bucketPool = this.orderBuckets;
|
|
1000
|
+
let bucketsUsed = 0;
|
|
1001
|
+
buckets.clear();
|
|
1002
|
+
for (let i = 0; i < els.length; i++) {
|
|
1003
|
+
const rec = decorated[i];
|
|
1004
|
+
const key = regions.get(rec.el) ?? null;
|
|
1005
|
+
let bucket = buckets.get(key);
|
|
1006
|
+
if (!bucket) {
|
|
1007
|
+
bucket = bucketPool[bucketsUsed];
|
|
1008
|
+
if (bucket === void 0) {
|
|
1009
|
+
bucket = [];
|
|
1010
|
+
bucketPool.push(bucket);
|
|
1011
|
+
}
|
|
1012
|
+
bucketsUsed++;
|
|
1013
|
+
bucket.length = 0;
|
|
1014
|
+
buckets.set(key, bucket);
|
|
1015
|
+
}
|
|
1016
|
+
bucket.push(rec);
|
|
1017
|
+
}
|
|
1018
|
+
let out = 0;
|
|
979
1019
|
for (const order of buckets.values()) {
|
|
980
|
-
order.sort(
|
|
1020
|
+
order.sort(byTopThenIndex);
|
|
981
1021
|
let rowStart = 0;
|
|
982
|
-
let rowBottom = order.length ?
|
|
1022
|
+
let rowBottom = order.length ? orderBandBottom(order[0]) : 0;
|
|
983
1023
|
const flushRow = (end) => {
|
|
984
|
-
const row =
|
|
985
|
-
row.
|
|
986
|
-
for (
|
|
1024
|
+
const row = this.orderRowScratch;
|
|
1025
|
+
row.length = 0;
|
|
1026
|
+
for (let m = rowStart; m < end; m++) row.push(order[m]);
|
|
1027
|
+
row.sort(rtl ? byLeftDescThenIndex : byLeftAscThenIndex);
|
|
1028
|
+
for (const r of row) els[out++] = r.el;
|
|
987
1029
|
};
|
|
988
1030
|
for (let k = 1; k < order.length; k++) {
|
|
989
1031
|
if (order[k].top < rowBottom) {
|
|
990
|
-
rowBottom = Math.max(rowBottom,
|
|
1032
|
+
rowBottom = Math.max(rowBottom, orderBandBottom(order[k]));
|
|
991
1033
|
} else {
|
|
992
1034
|
flushRow(k);
|
|
993
1035
|
rowStart = k;
|
|
994
|
-
rowBottom =
|
|
1036
|
+
rowBottom = orderBandBottom(order[k]);
|
|
995
1037
|
}
|
|
996
1038
|
}
|
|
997
1039
|
flushRow(order.length);
|
|
998
1040
|
}
|
|
999
|
-
for (let i = 0; i < sorted.length; i++) els[i] = sorted[i];
|
|
1000
1041
|
}
|
|
1001
1042
|
};
|
|
1002
1043
|
|
|
@@ -1028,8 +1069,11 @@ var CanvasGeometry = class {
|
|
|
1028
1069
|
/** Effective device pixel ratio, matching CanvasRenderer: real DPR clamped to
|
|
1029
1070
|
* `maxDPR` when set. */
|
|
1030
1071
|
effectiveDPR(maxDPR) {
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1072
|
+
const raw = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1073
|
+
const real = Number.isFinite(raw) && raw > 0 ? raw : 1;
|
|
1074
|
+
if (maxDPR === void 0) return real;
|
|
1075
|
+
if (!Number.isFinite(maxDPR) || maxDPR <= 0) return real;
|
|
1076
|
+
return Math.min(real, maxDPR);
|
|
1033
1077
|
}
|
|
1034
1078
|
/** Size the WebGPU particle canvas: backing store at logical × DPR, CSS box at
|
|
1035
1079
|
* the logical size. Sizing the backing store in logical px (the old
|
|
@@ -1327,6 +1371,8 @@ var DriverTicker = class {
|
|
|
1327
1371
|
tD.length = tweenCount;
|
|
1328
1372
|
backend.ensure(springCount, tweenCount);
|
|
1329
1373
|
backend.revalidateViews();
|
|
1374
|
+
let springsRejected = false;
|
|
1375
|
+
let tweensRejected = false;
|
|
1330
1376
|
if (springCount > 0) {
|
|
1331
1377
|
const sv = backend.springView();
|
|
1332
1378
|
for (let i = 0; i < springCount; i++) {
|
|
@@ -1342,8 +1388,7 @@ var DriverTicker = class {
|
|
|
1342
1388
|
for (let i = 0; i < springCount; i++) sD[i].syncExternal(sv.val[i], sv.vel[i]);
|
|
1343
1389
|
} else {
|
|
1344
1390
|
for (let i = 0; i < springCount; i++) sD[i].tick(dt);
|
|
1345
|
-
|
|
1346
|
-
this.backends.animBatchedLastFrame = false;
|
|
1391
|
+
springsRejected = true;
|
|
1347
1392
|
}
|
|
1348
1393
|
}
|
|
1349
1394
|
if (tweenCount > 0) {
|
|
@@ -1361,10 +1406,13 @@ var DriverTicker = class {
|
|
|
1361
1406
|
for (let i = 0; i < tweenCount; i++) tD[i].syncExternal(tv.val[i], tv.elapsed[i]);
|
|
1362
1407
|
} else {
|
|
1363
1408
|
for (let i = 0; i < tweenCount; i++) tD[i].tick(dt);
|
|
1364
|
-
|
|
1365
|
-
this.backends.animBatchedLastFrame = false;
|
|
1409
|
+
tweensRejected = true;
|
|
1366
1410
|
}
|
|
1367
1411
|
}
|
|
1412
|
+
if (springsRejected || tweensRejected) {
|
|
1413
|
+
this.backends.animReason = springsRejected && tweensRejected ? "rejected" : springsRejected ? "springs-rejected" : "tweens-rejected";
|
|
1414
|
+
this.backends.animBatchedLastFrame = springCount > 0 && !springsRejected || tweenCount > 0 && !tweensRejected;
|
|
1415
|
+
}
|
|
1368
1416
|
for (let i = 0; i < springCount; i++) sE[i]._applyDriverTick(sP[i], sD[i]);
|
|
1369
1417
|
for (let i = 0; i < tweenCount; i++) tE[i]._applyDriverTick(tP[i], tD[i]);
|
|
1370
1418
|
}
|
|
@@ -1525,9 +1573,6 @@ function intersectBounds(a, b) {
|
|
|
1525
1573
|
height: Math.max(0, bottom - y)
|
|
1526
1574
|
};
|
|
1527
1575
|
}
|
|
1528
|
-
function pointInBounds(b, x, y) {
|
|
1529
|
-
return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
|
|
1530
|
-
}
|
|
1531
1576
|
var HitTester = class {
|
|
1532
1577
|
root;
|
|
1533
1578
|
overlayRoot;
|
|
@@ -1574,7 +1619,7 @@ var HitTester = class {
|
|
|
1574
1619
|
this.backends.hitReason = "not-installed";
|
|
1575
1620
|
return false;
|
|
1576
1621
|
}
|
|
1577
|
-
if (this.backends.hitGridFrame === frame) {
|
|
1622
|
+
if (this.backends.hitGridFrame === frame && this.backends.hitGridStructureVersion === this.backends.structureVersion) {
|
|
1578
1623
|
return this.backends.hitGridOk;
|
|
1579
1624
|
}
|
|
1580
1625
|
let gathered = null;
|
|
@@ -1605,6 +1650,7 @@ var HitTester = class {
|
|
|
1605
1650
|
this.slotEntity = gathered.slotEntity;
|
|
1606
1651
|
this.boundless = gathered.boundless;
|
|
1607
1652
|
this.backends.hitGridFrame = frame;
|
|
1653
|
+
this.backends.hitGridStructureVersion = this.backends.structureVersion;
|
|
1608
1654
|
this.backends.hitGridOk = ok;
|
|
1609
1655
|
this.backends.hitReason = ok ? "active" : "rejected";
|
|
1610
1656
|
return ok;
|
|
@@ -1655,7 +1701,7 @@ var HitTester = class {
|
|
|
1655
1701
|
const hit = this.findHitRecursively(node.children[i], x, y, childClip);
|
|
1656
1702
|
if (hit) return hit;
|
|
1657
1703
|
}
|
|
1658
|
-
if (node.isPointInside && node.isPointInside(x, y) && (
|
|
1704
|
+
if (node.isPointInside && node.isPointInside(x, y) && this.isInsideAllClippers(node, x, y) && !this.isPointerTransparent(node)) {
|
|
1659
1705
|
return node;
|
|
1660
1706
|
}
|
|
1661
1707
|
return null;
|
|
@@ -1667,6 +1713,25 @@ var HitTester = class {
|
|
|
1667
1713
|
const attrs = node.getA11yAttributes();
|
|
1668
1714
|
return attrs.disabled === true || attrs.pointerEvents === "none";
|
|
1669
1715
|
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Exact rotation-aware `clipChildren` test shared by BOTH hit paths: the
|
|
1718
|
+
* point must lie inside every clipChildren ancestor's local rect — the same
|
|
1719
|
+
* shape rendering clips to — not merely inside the ancestor's world AABB.
|
|
1720
|
+
* For a rotated clipper those disagree, and until both paths used the exact
|
|
1721
|
+
* rect a query's answer depended on which backend was active (#680). The
|
|
1722
|
+
* clip-stack AABB intersection in {@link findHitRecursively} remains purely
|
|
1723
|
+
* as a subtree-pruning pre-filter; this is the authoritative gate.
|
|
1724
|
+
*/
|
|
1725
|
+
isInsideAllClippers(node, x, y) {
|
|
1726
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
1727
|
+
if (!ancestor.clipChildren) continue;
|
|
1728
|
+
const local = ancestor.worldToLocal(x, y);
|
|
1729
|
+
if (!local || local.x < 0 || local.y < 0 || local.x > ancestor.width || local.y > ancestor.height) {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return true;
|
|
1734
|
+
}
|
|
1670
1735
|
/**
|
|
1671
1736
|
* Whether a confirmed geometric hit on `node` at world `(x, y)` is a REAL hit,
|
|
1672
1737
|
* applying the same visibility/input gating as {@link findHitRecursively} but
|
|
@@ -1681,14 +1746,8 @@ var HitTester = class {
|
|
|
1681
1746
|
if (node.opacity <= 0) return false;
|
|
1682
1747
|
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
1683
1748
|
if (ancestor.opacity <= 0) return false;
|
|
1684
|
-
if (ancestor.clipChildren && ancestor.width > 0 && ancestor.height > 0) {
|
|
1685
|
-
const local = ancestor.worldToLocal(x, y);
|
|
1686
|
-
if (!local || local.x < 0 || local.y < 0 || local.x > ancestor.width || local.y > ancestor.height) {
|
|
1687
|
-
return false;
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
1749
|
}
|
|
1691
|
-
return
|
|
1750
|
+
return this.isInsideAllClippers(node, x, y);
|
|
1692
1751
|
}
|
|
1693
1752
|
};
|
|
1694
1753
|
|
|
@@ -2441,7 +2500,8 @@ var WasmTransformBackend = class {
|
|
|
2441
2500
|
this.lastStatus = WASM_STATUS.CAPACITY;
|
|
2442
2501
|
return;
|
|
2443
2502
|
}
|
|
2444
|
-
const
|
|
2503
|
+
const simd = kernel === "simd" && typeof this.ex.compose_simd === "function";
|
|
2504
|
+
const status = simd ? this.ex.compose_simd() : this.ex.compose_scalar();
|
|
2445
2505
|
this.lastStatus = status;
|
|
2446
2506
|
if (status !== WASM_STATUS.OK) return;
|
|
2447
2507
|
store.wa.set(this.vwa.subarray(0, n));
|
|
@@ -2463,7 +2523,8 @@ var WasmTransformBackend = class {
|
|
|
2463
2523
|
* {@link uploadRuns} after a topology change.
|
|
2464
2524
|
*/
|
|
2465
2525
|
runKernel(kernel = "simd") {
|
|
2466
|
-
const
|
|
2526
|
+
const simd = kernel === "simd" && typeof this.ex.compose_simd === "function";
|
|
2527
|
+
const status = simd ? this.ex.compose_simd() : this.ex.compose_scalar();
|
|
2467
2528
|
this.lastStatus = status;
|
|
2468
2529
|
return status;
|
|
2469
2530
|
}
|
|
@@ -2515,21 +2576,21 @@ var WasmTransformBackend = class {
|
|
|
2515
2576
|
/**
|
|
2516
2577
|
* Compute world-space AABBs for `store` in WASM (G1+), writing back into its
|
|
2517
2578
|
* `aminx/aminy/amaxx/amaxy` arrays. Uploads the local bounds, runs the AABB
|
|
2518
|
-
* pass, reads results back. Result is bit-identical to `computeAabbsJS(store)
|
|
2519
|
-
* `compose` (or `runKernel`) must have populated the world
|
|
2520
|
-
* this pass reads them. For the resident (no-copy)
|
|
2521
|
-
* via {@link boundsView} and read via
|
|
2522
|
-
* {@link runAabbs} instead.
|
|
2579
|
+
* pass, reads results back. Result is bit-identical to `computeAabbsJS(store)`
|
|
2580
|
+
* for both kernels. `compose` (or `runKernel`) must have populated the world
|
|
2581
|
+
* matrices first — this pass reads them. For the resident (no-copy)
|
|
2582
|
+
* integration, write bounds via {@link boundsView} and read via
|
|
2583
|
+
* {@link aabbView} + call {@link runAabbs} instead.
|
|
2523
2584
|
*/
|
|
2524
|
-
computeAabbs(store) {
|
|
2585
|
+
computeAabbs(store, kernel = "simd") {
|
|
2525
2586
|
this.ensure(store.count, store.runCount);
|
|
2526
2587
|
const n = store.count;
|
|
2527
2588
|
this.vbx.set(store.bx.subarray(0, n));
|
|
2528
2589
|
this.vby.set(store.by.subarray(0, n));
|
|
2529
2590
|
this.vbw.set(store.bw.subarray(0, n));
|
|
2530
2591
|
this.vbh.set(store.bh.subarray(0, n));
|
|
2531
|
-
|
|
2532
|
-
if (
|
|
2592
|
+
const ok = this.runAabbs(n, kernel);
|
|
2593
|
+
if (!ok) return;
|
|
2533
2594
|
store.aminx.set(this.vaminx.subarray(0, n));
|
|
2534
2595
|
store.aminy.set(this.vaminy.subarray(0, n));
|
|
2535
2596
|
store.amaxx.set(this.vamaxx.subarray(0, n));
|
|
@@ -2540,8 +2601,9 @@ var WasmTransformBackend = class {
|
|
|
2540
2601
|
* composed). No upload/readback — the per-frame resident path. Returns
|
|
2541
2602
|
* `false` if the kernel rejected `count` (beyond capacity, or uninitialized),
|
|
2542
2603
|
* in which case {@link aabbView} still holds the previous frame's bounds. */
|
|
2543
|
-
runAabbs(count) {
|
|
2544
|
-
|
|
2604
|
+
runAabbs(count, kernel = "simd") {
|
|
2605
|
+
const simd = kernel === "simd" && typeof this.ex.compute_aabbs_simd === "function";
|
|
2606
|
+
this.lastStatus = simd ? this.ex.compute_aabbs_simd(count) : this.ex.compute_aabbs(count);
|
|
2545
2607
|
return this.lastStatus === WASM_STATUS.OK;
|
|
2546
2608
|
}
|
|
2547
2609
|
/** Resident wasm local-bounds input views (`bx,by,bw,bh`) for the AABB pass. */
|
|
@@ -3204,6 +3266,7 @@ var WasmBackendFacade = class {
|
|
|
3204
3266
|
setHit(backend) {
|
|
3205
3267
|
this._hit = backend;
|
|
3206
3268
|
this.hitGridFrame = -1;
|
|
3269
|
+
this.hitGridStructureVersion = -1;
|
|
3207
3270
|
this.hitReason = backend ? "not-applicable" : "not-installed";
|
|
3208
3271
|
}
|
|
3209
3272
|
/** The installed batched-animation backend, or `null` for the JS tick. */
|
|
@@ -3260,6 +3323,13 @@ var WasmBackendFacade = class {
|
|
|
3260
3323
|
// domain.
|
|
3261
3324
|
/** Frame the hit grid was last built for; `-1` forces a rebuild. */
|
|
3262
3325
|
hitGridFrame = -1;
|
|
3326
|
+
/**
|
|
3327
|
+
* Structure version the hit grid was built for; `-1` forces a rebuild.
|
|
3328
|
+
* Half of the cache key the comment above promises: without it, a pointer
|
|
3329
|
+
* query in the same frame as a structural mutation would resolve against
|
|
3330
|
+
* pre-mutation geometry while the JS walk sees live state.
|
|
3331
|
+
*/
|
|
3332
|
+
hitGridStructureVersion = -1;
|
|
3263
3333
|
/** Whether that build succeeded (did not overflow its item budget). */
|
|
3264
3334
|
hitGridOk = false;
|
|
3265
3335
|
/**
|
|
@@ -3399,6 +3469,38 @@ var WasmBackendFacade = class {
|
|
|
3399
3469
|
}
|
|
3400
3470
|
};
|
|
3401
3471
|
|
|
3472
|
+
// src/tree/scene/keyboard.ts
|
|
3473
|
+
function normalizeChord(input) {
|
|
3474
|
+
if (typeof input === "string") {
|
|
3475
|
+
const parts = input.split("+").map((p) => p.trim()).filter(Boolean);
|
|
3476
|
+
const mods = /* @__PURE__ */ new Set();
|
|
3477
|
+
let key2 = "";
|
|
3478
|
+
for (const p of parts) {
|
|
3479
|
+
const low = p.toLowerCase();
|
|
3480
|
+
if (low === "ctrl" || low === "control") mods.add("Control");
|
|
3481
|
+
else if (low === "alt") mods.add("Alt");
|
|
3482
|
+
else if (low === "shift") mods.add("Shift");
|
|
3483
|
+
else if (low === "meta" || low === "cmd" || low === "super") mods.add("Meta");
|
|
3484
|
+
else key2 = p.length === 1 ? p.toUpperCase() : p;
|
|
3485
|
+
}
|
|
3486
|
+
const ordered2 = ["Control", "Alt", "Shift", "Meta"].filter((m) => mods.has(m));
|
|
3487
|
+
return key2 ? [...ordered2, key2].join("+") : ordered2.join("+");
|
|
3488
|
+
}
|
|
3489
|
+
const ordered = [];
|
|
3490
|
+
if (input.ctrlKey) ordered.push("Control");
|
|
3491
|
+
if (input.altKey) ordered.push("Alt");
|
|
3492
|
+
if (input.shiftKey) ordered.push("Shift");
|
|
3493
|
+
if (input.metaKey) ordered.push("Meta");
|
|
3494
|
+
let key = input.key;
|
|
3495
|
+
if (key === " ") key = "Space";
|
|
3496
|
+
else if (key.length === 1) key = key.toUpperCase();
|
|
3497
|
+
if (["Control", "Alt", "Shift", "Meta"].includes(key)) {
|
|
3498
|
+
return ordered.join("+");
|
|
3499
|
+
}
|
|
3500
|
+
ordered.push(key);
|
|
3501
|
+
return ordered.join("+");
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3402
3504
|
// src/tree/Scene.ts
|
|
3403
3505
|
var RANGE_VALUE_ROLES = /* @__PURE__ */ new Set(["slider", "spinbutton", "progressbar", "scrollbar", "meter"]);
|
|
3404
3506
|
var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
|
|
@@ -3412,6 +3514,24 @@ var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
|
|
|
3412
3514
|
"slider",
|
|
3413
3515
|
"combobox"
|
|
3414
3516
|
]);
|
|
3517
|
+
var KEYBOARD_OWNING_ROLES = /* @__PURE__ */ new Set([
|
|
3518
|
+
...INTERACTIVE_A11Y_ROLES,
|
|
3519
|
+
"option",
|
|
3520
|
+
"listbox",
|
|
3521
|
+
"textbox",
|
|
3522
|
+
"searchbox",
|
|
3523
|
+
"spinbutton"
|
|
3524
|
+
]);
|
|
3525
|
+
function ownsKeyboard(el) {
|
|
3526
|
+
if (!el) return false;
|
|
3527
|
+
if (el === document.body || el === document.documentElement) return false;
|
|
3528
|
+
if (el.hasAttribute("data-vecto-a11y-root")) return false;
|
|
3529
|
+
const tag = el.tagName;
|
|
3530
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
3531
|
+
if (el.isContentEditable) return true;
|
|
3532
|
+
const role = el.getAttribute("role");
|
|
3533
|
+
return role !== null && KEYBOARD_OWNING_ROLES.has(role);
|
|
3534
|
+
}
|
|
3415
3535
|
var A11Y_REQUIRED_OWNED = /* @__PURE__ */ new Map([
|
|
3416
3536
|
["grid", /* @__PURE__ */ new Set(["row", "rowgroup"])],
|
|
3417
3537
|
["table", /* @__PURE__ */ new Set(["row", "rowgroup"])],
|
|
@@ -3950,6 +4070,9 @@ var Scene = class _Scene {
|
|
|
3950
4070
|
// server-side (e.g. headless layout / vector export) without jsdom.
|
|
3951
4071
|
a11yRoot;
|
|
3952
4072
|
a11yElements = /* @__PURE__ */ new Map();
|
|
4073
|
+
/** Elements that already have the synthetic Enter/Space activation handler
|
|
4074
|
+
* (see the syncA11y refresh pass) — install-once bookkeeping (#694). */
|
|
4075
|
+
keyboardActivatedA11y = /* @__PURE__ */ new WeakSet();
|
|
3953
4076
|
// --- domain: content-projection — projected DOM, sync state, calibration, selection ---
|
|
3954
4077
|
/** DOM nodes mirroring static text content, keyed by entity id. */
|
|
3955
4078
|
contentElements = /* @__PURE__ */ new Map();
|
|
@@ -4058,6 +4181,8 @@ var Scene = class _Scene {
|
|
|
4058
4181
|
* this an embedded canvas stayed at its initial size forever. */
|
|
4059
4182
|
canvasResizeObserver = null;
|
|
4060
4183
|
dprChangeHandler = null;
|
|
4184
|
+
dprPollInterval = null;
|
|
4185
|
+
lastDpr = 1;
|
|
4061
4186
|
// --- domain: a11y-projection — focus, overlay geometry, DOM ordering, portals ---
|
|
4062
4187
|
focusedA11yElement = null;
|
|
4063
4188
|
/**
|
|
@@ -4574,6 +4699,15 @@ var Scene = class _Scene {
|
|
|
4574
4699
|
_registerActiveDriverSubtree(entity) {
|
|
4575
4700
|
this._driverTicker.registerSubtree(entity);
|
|
4576
4701
|
}
|
|
4702
|
+
/**
|
|
4703
|
+
* The `dt` of the frame currently executing its update walk (the WASM-mode
|
|
4704
|
+
* pre-pass or the JS-mode interleaved render walk), or `null` outside it.
|
|
4705
|
+
* Read by `Entity._spawnDriver`: a driver spawned mid-walk onto an entity the
|
|
4706
|
+
* batched pass already claimed this frame must be advanced once here, or
|
|
4707
|
+
* `tickDrivers()`'s per-frame stamp would skip it a full frame — a one-frame
|
|
4708
|
+
* JS/WASM divergence. Written in {@link render} next to `_tickBatchedDrivers`.
|
|
4709
|
+
*/
|
|
4710
|
+
_updateWalkDt = null;
|
|
4577
4711
|
// --- domain: render-scheduler — batched driver tick (extraction 6), reads the anim backend ---
|
|
4578
4712
|
/**
|
|
4579
4713
|
* Advance every registered entity's active drivers for this frame, batching
|
|
@@ -4661,6 +4795,13 @@ var Scene = class _Scene {
|
|
|
4661
4795
|
/** Element the pointer listeners are bound to (parent container if present,
|
|
4662
4796
|
* else the canvas). Stored so `destroy()` detaches from the same element. */
|
|
4663
4797
|
pointerEventTarget = null;
|
|
4798
|
+
// --- scene-level keyboard channel state (see on/off/registerShortcut) ---
|
|
4799
|
+
keydownHandlers = [];
|
|
4800
|
+
keyupHandlers = [];
|
|
4801
|
+
/** Chord-normalized shortcut table, matched on keydown only. */
|
|
4802
|
+
shortcuts = [];
|
|
4803
|
+
windowKeyDownHandler = null;
|
|
4804
|
+
windowKeyUpHandler = null;
|
|
4664
4805
|
hasWarnedZeroSize = false;
|
|
4665
4806
|
/**
|
|
4666
4807
|
* Latch for {@link Scene.resize}'s invalid-dimension warning.
|
|
@@ -4796,8 +4937,13 @@ var Scene = class _Scene {
|
|
|
4796
4937
|
this.width = styleWidth ?? (canvas.width || canvas.clientWidth || 0);
|
|
4797
4938
|
this.height = styleHeight ?? (canvas.height || canvas.clientHeight || 0);
|
|
4798
4939
|
} else {
|
|
4799
|
-
|
|
4800
|
-
|
|
4940
|
+
if (typeof canvas.isConnected === "boolean" && !canvas.isConnected) {
|
|
4941
|
+
this.width = 0;
|
|
4942
|
+
this.height = 0;
|
|
4943
|
+
} else {
|
|
4944
|
+
this.width = typeof window !== "undefined" ? window.innerWidth : canvas.clientWidth || canvas.width || 800;
|
|
4945
|
+
this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
|
|
4946
|
+
}
|
|
4801
4947
|
}
|
|
4802
4948
|
const globalProcess = typeof globalThis !== "undefined" ? globalThis.process : void 0;
|
|
4803
4949
|
const isTest = globalProcess && (globalProcess.env?.NODE_ENV === "test" || globalProcess.env?.VITEST === "true");
|
|
@@ -5029,16 +5175,52 @@ var Scene = class _Scene {
|
|
|
5029
5175
|
if (this.dprMediaQuery && this.dprChangeHandler) {
|
|
5030
5176
|
this.dprMediaQuery.removeEventListener?.("change", this.dprChangeHandler);
|
|
5031
5177
|
}
|
|
5032
|
-
const
|
|
5178
|
+
const raw = window.devicePixelRatio;
|
|
5179
|
+
const dpr = Number.isFinite(raw) && raw > 0 ? raw : 1;
|
|
5033
5180
|
const query = window.matchMedia(`(resolution: ${dpr}dppx)`);
|
|
5034
5181
|
const handler = () => {
|
|
5035
|
-
|
|
5036
|
-
|
|
5182
|
+
const curRaw = window.devicePixelRatio;
|
|
5183
|
+
const cur = Number.isFinite(curRaw) && curRaw > 0 ? curRaw : 1;
|
|
5184
|
+
if (Math.abs(cur - dpr) <= 1e-3) {
|
|
5185
|
+
this.watchDevicePixelRatio();
|
|
5186
|
+
return;
|
|
5187
|
+
}
|
|
5188
|
+
this.lastDpr = cur;
|
|
5189
|
+
try {
|
|
5190
|
+
this.resize(this.width, this.height);
|
|
5191
|
+
} catch (err) {
|
|
5192
|
+
console.warn("[VectoJS] DPR resize failed", err);
|
|
5193
|
+
}
|
|
5194
|
+
try {
|
|
5195
|
+
if (this.renderer.isContextLost?.() !== true) this.render(this.renderer);
|
|
5196
|
+
} catch (err) {
|
|
5197
|
+
console.warn("[VectoJS] DPR repaint failed", err);
|
|
5198
|
+
}
|
|
5037
5199
|
this.watchDevicePixelRatio();
|
|
5038
5200
|
};
|
|
5039
5201
|
query.addEventListener?.("change", handler);
|
|
5040
5202
|
this.dprMediaQuery = query;
|
|
5041
5203
|
this.dprChangeHandler = handler;
|
|
5204
|
+
this.lastDpr = dpr;
|
|
5205
|
+
if (this.dprPollInterval === null && typeof setInterval === "function") {
|
|
5206
|
+
this.dprPollInterval = setInterval(() => {
|
|
5207
|
+
const rawPoll = window.devicePixelRatio;
|
|
5208
|
+
const curPoll = Number.isFinite(rawPoll) && rawPoll > 0 ? rawPoll : 1;
|
|
5209
|
+
if (Math.abs(curPoll - this.lastDpr) <= 1e-3) return;
|
|
5210
|
+
this.lastDpr = curPoll;
|
|
5211
|
+
try {
|
|
5212
|
+
this.resize(this.width, this.height);
|
|
5213
|
+
} catch (err) {
|
|
5214
|
+
console.warn("[VectoJS] DPR poll resize failed", err);
|
|
5215
|
+
}
|
|
5216
|
+
try {
|
|
5217
|
+
if (this.renderer.isContextLost?.() !== true) this.render(this.renderer);
|
|
5218
|
+
} catch (err) {
|
|
5219
|
+
console.warn("[VectoJS] DPR poll repaint failed", err);
|
|
5220
|
+
}
|
|
5221
|
+
this.watchDevicePixelRatio();
|
|
5222
|
+
}, 1e3);
|
|
5223
|
+
}
|
|
5042
5224
|
}
|
|
5043
5225
|
/**
|
|
5044
5226
|
* Recover the WebGL point layer from a GPU context loss (driver TDR reset,
|
|
@@ -5072,6 +5254,35 @@ var Scene = class _Scene {
|
|
|
5072
5254
|
gl.addEventListener("webglcontextlost", this.glContextLostHandler);
|
|
5073
5255
|
gl.addEventListener("webglcontextrestored", this.glContextRestoredHandler);
|
|
5074
5256
|
}
|
|
5257
|
+
/**
|
|
5258
|
+
* One-shot adoption of the window viewport once an initially-detached canvas
|
|
5259
|
+
* gains layout (#817). A full-window scene constructed before its canvas is
|
|
5260
|
+
* attached starts at 0×0, and no window `resize` fires on attachment — so
|
|
5261
|
+
* without this latch the scene would stay unsized forever. The first nonzero
|
|
5262
|
+
* layout box adopts `window.innerWidth/innerHeight` (the same sizing the
|
|
5263
|
+
* window resize handler applies, keeping the full-window contract), then the
|
|
5264
|
+
* observer disconnects: steady-state sizing stays exactly as if the canvas
|
|
5265
|
+
* had been attached before construction. An explicit user `resize()` between
|
|
5266
|
+
* construction and attachment is respected and skips adoption.
|
|
5267
|
+
*
|
|
5268
|
+
* No-op when the scene already has a size (attached at construction) or when
|
|
5269
|
+
* `ResizeObserver` is unavailable (the caller then drives sizing explicitly).
|
|
5270
|
+
*/
|
|
5271
|
+
armAttachmentViewportLatch() {
|
|
5272
|
+
if (this.width > 0 && this.height > 0) return;
|
|
5273
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
5274
|
+
const observer = new ResizeObserver((entries) => {
|
|
5275
|
+
const box = entries[0]?.contentRect;
|
|
5276
|
+
if (!box || !(box.width > 0) || !(box.height > 0)) return;
|
|
5277
|
+
observer.disconnect();
|
|
5278
|
+
this.canvasResizeObserver = null;
|
|
5279
|
+
if (this.width === 0 || this.height === 0) {
|
|
5280
|
+
this.resize(window.innerWidth, window.innerHeight);
|
|
5281
|
+
}
|
|
5282
|
+
});
|
|
5283
|
+
this.canvasResizeObserver = observer;
|
|
5284
|
+
observer.observe(this.canvas);
|
|
5285
|
+
}
|
|
5075
5286
|
// --- domain: scene-facade — renderer accessor ---
|
|
5076
5287
|
/**
|
|
5077
5288
|
* Expose the underlying {@link IRenderer} for advanced direct-draw operations.
|
|
@@ -5257,6 +5468,10 @@ var Scene = class _Scene {
|
|
|
5257
5468
|
this.dprMediaQuery = null;
|
|
5258
5469
|
this.dprChangeHandler = null;
|
|
5259
5470
|
}
|
|
5471
|
+
if (this.dprPollInterval !== null) {
|
|
5472
|
+
clearInterval(this.dprPollInterval);
|
|
5473
|
+
this.dprPollInterval = null;
|
|
5474
|
+
}
|
|
5260
5475
|
if (this.forcedColorsQuery && this.forcedColorsChangeHandler) {
|
|
5261
5476
|
this.forcedColorsQuery.removeEventListener?.("change", this.forcedColorsChangeHandler);
|
|
5262
5477
|
this.forcedColorsQuery = null;
|
|
@@ -5267,6 +5482,19 @@ var Scene = class _Scene {
|
|
|
5267
5482
|
window.removeEventListener("blur", this.contentSelectionEndListener);
|
|
5268
5483
|
this.contentSelectionEndListener = null;
|
|
5269
5484
|
}
|
|
5485
|
+
if (typeof window !== "undefined") {
|
|
5486
|
+
if (this.windowKeyDownHandler) {
|
|
5487
|
+
window.removeEventListener("keydown", this.windowKeyDownHandler);
|
|
5488
|
+
}
|
|
5489
|
+
if (this.windowKeyUpHandler) {
|
|
5490
|
+
window.removeEventListener("keyup", this.windowKeyUpHandler);
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
this.windowKeyDownHandler = null;
|
|
5494
|
+
this.windowKeyUpHandler = null;
|
|
5495
|
+
this.keydownHandlers.length = 0;
|
|
5496
|
+
this.keyupHandlers.length = 0;
|
|
5497
|
+
this.shortcuts.length = 0;
|
|
5270
5498
|
if (typeof window !== "undefined" && this.pointerEventTarget && typeof this.pointerEventTarget.removeEventListener === "function") {
|
|
5271
5499
|
if (this.pointerMoveListener) {
|
|
5272
5500
|
this.pointerEventTarget.removeEventListener("pointermove", this.pointerMoveListener);
|
|
@@ -5317,6 +5545,7 @@ var Scene = class _Scene {
|
|
|
5317
5545
|
setupEvents() {
|
|
5318
5546
|
if (typeof window !== "undefined" && !this.disableWindowResize) {
|
|
5319
5547
|
window.addEventListener("resize", this.resizeHandler);
|
|
5548
|
+
this.armAttachmentViewportLatch();
|
|
5320
5549
|
} else if (this.disableWindowResize && typeof ResizeObserver !== "undefined" && this.canvas && typeof this.canvas.getBoundingClientRect === "function") {
|
|
5321
5550
|
this.canvasResizeObserver = new ResizeObserver((entries) => {
|
|
5322
5551
|
const entry = entries[0];
|
|
@@ -5366,6 +5595,7 @@ var Scene = class _Scene {
|
|
|
5366
5595
|
this.lastTime = typeof performance !== "undefined" ? performance.now() : 0;
|
|
5367
5596
|
this.watchCanvasVisibility();
|
|
5368
5597
|
this.scheduleFrame();
|
|
5598
|
+
this.attachWindowKeyListeners();
|
|
5369
5599
|
const isTextFocused = this.focusedA11yElement instanceof HTMLInputElement || this.focusedA11yElement instanceof HTMLTextAreaElement;
|
|
5370
5600
|
if (isTextFocused && this.renderMode === "onDemand" && !this.caretBlinkTimer) {
|
|
5371
5601
|
this.caretBlinkTimer = setInterval(() => {
|
|
@@ -5433,6 +5663,132 @@ var Scene = class _Scene {
|
|
|
5433
5663
|
}
|
|
5434
5664
|
this._canvasOnScreen = true;
|
|
5435
5665
|
}
|
|
5666
|
+
// --- domain: input — scene-level keyboard channel ---
|
|
5667
|
+
/**
|
|
5668
|
+
* Register a listener on the scene-level keyboard channel.
|
|
5669
|
+
*
|
|
5670
|
+
* Mirrors {@link Entity.on} minus the options parameter (bubble-only, and
|
|
5671
|
+
* there is no capture phase for a single window tap). Handlers fire only
|
|
5672
|
+
* when ALL of these hold:
|
|
5673
|
+
*
|
|
5674
|
+
* 1. the native event was not already `defaultPrevented`,
|
|
5675
|
+
* 2. it is not an auto-repeat (`e.repeat`),
|
|
5676
|
+
* 3. `document.activeElement` does not own the keyboard
|
|
5677
|
+
* (see {@link ownsKeyboard}) — typing in an `<input>` or focusing a
|
|
5678
|
+
* slider-role element suppresses the scene channel, unconditionally,
|
|
5679
|
+
* including modifier chords.
|
|
5680
|
+
*
|
|
5681
|
+
* The listeners are window-level BUBBLE-phase taps, so an entity handler
|
|
5682
|
+
* that calls `nativeEvent.stopPropagation()` keeps the key from ever
|
|
5683
|
+
* reaching this channel. Deliberately not registered in the capture phase:
|
|
5684
|
+
* a window-capture listener would preempt Modal's document-capture trap.
|
|
5685
|
+
*
|
|
5686
|
+
* Listeners attach at `start()` and survive `stop()`; only `destroy()`
|
|
5687
|
+
* removes them.
|
|
5688
|
+
*
|
|
5689
|
+
* @param event - `'keydown'` or `'keyup'`; anything else is rejected with a
|
|
5690
|
+
* dev warning (guards JS callers — TypeScript already narrows this).
|
|
5691
|
+
* @param callback - Handler invoked with a {@link SceneKeyEvent}.
|
|
5692
|
+
* @returns `this` for method chaining.
|
|
5693
|
+
*/
|
|
5694
|
+
on(event, callback) {
|
|
5695
|
+
if (event !== "keydown" && event !== "keyup") {
|
|
5696
|
+
console.warn(
|
|
5697
|
+
`[VectoJS] Scene.on: unsupported event '${String(event)}'. Only 'keydown' and 'keyup' are supported.`
|
|
5698
|
+
);
|
|
5699
|
+
return this;
|
|
5700
|
+
}
|
|
5701
|
+
(event === "keydown" ? this.keydownHandlers : this.keyupHandlers).push(callback);
|
|
5702
|
+
return this;
|
|
5703
|
+
}
|
|
5704
|
+
/**
|
|
5705
|
+
* Remove a listener previously registered with {@link Scene.on}. Silent
|
|
5706
|
+
* no-op for unknown handlers, unsupported event names, or after
|
|
5707
|
+
* {@link Scene.destroy}.
|
|
5708
|
+
*
|
|
5709
|
+
* @returns `this` for method chaining.
|
|
5710
|
+
*/
|
|
5711
|
+
off(event, callback) {
|
|
5712
|
+
if (event !== "keydown" && event !== "keyup") {
|
|
5713
|
+
console.warn(
|
|
5714
|
+
`[VectoJS] Scene.off: unsupported event '${String(event)}'. Only 'keydown' and 'keyup' are supported.`
|
|
5715
|
+
);
|
|
5716
|
+
return this;
|
|
5717
|
+
}
|
|
5718
|
+
const handlers = event === "keydown" ? this.keydownHandlers : this.keyupHandlers;
|
|
5719
|
+
const idx = handlers.indexOf(callback);
|
|
5720
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
5721
|
+
return this;
|
|
5722
|
+
}
|
|
5723
|
+
/**
|
|
5724
|
+
* Sugar over the keyboard channel: invoke `spec.handler` whenever a
|
|
5725
|
+
* non-repeat `keydown` whose normalized chord matches `spec.chord`
|
|
5726
|
+
* (via {@link normalizeChord}) passes the same gates as {@link Scene.on} —
|
|
5727
|
+
* including the unconditional `ownsKeyboard(activeElement)` suppression.
|
|
5728
|
+
*
|
|
5729
|
+
* @param spec - `{ chord, handler }`; the chord is normalized once at
|
|
5730
|
+
* registration, so `'ctrl+n'`, `'Control+n'` and `'Ctrl+N'` are aliases.
|
|
5731
|
+
* @returns `this` for method chaining.
|
|
5732
|
+
*/
|
|
5733
|
+
registerShortcut(spec) {
|
|
5734
|
+
this.shortcuts.push({ chord: normalizeChord(spec.chord), handler: spec.handler });
|
|
5735
|
+
return this;
|
|
5736
|
+
}
|
|
5737
|
+
/**
|
|
5738
|
+
* Remove a shortcut previously registered with {@link Scene.registerShortcut}.
|
|
5739
|
+
* Matches on normalized chord + exact handler reference; silent no-op when
|
|
5740
|
+
* nothing matches.
|
|
5741
|
+
*
|
|
5742
|
+
* @param spec - The same spec object (chord spelling may differ; it is
|
|
5743
|
+
* re-normalized before matching).
|
|
5744
|
+
* @returns `this` for method chaining.
|
|
5745
|
+
*/
|
|
5746
|
+
unregisterShortcut(spec) {
|
|
5747
|
+
const chord = normalizeChord(spec.chord);
|
|
5748
|
+
const idx = this.shortcuts.findIndex((s) => s.chord === chord && s.handler === spec.handler);
|
|
5749
|
+
if (idx !== -1) this.shortcuts.splice(idx, 1);
|
|
5750
|
+
return this;
|
|
5751
|
+
}
|
|
5752
|
+
/** Attach the window bubble-phase keyboard listeners exactly once. */
|
|
5753
|
+
attachWindowKeyListeners() {
|
|
5754
|
+
if (this.windowKeyDownHandler || typeof window === "undefined") return;
|
|
5755
|
+
this.windowKeyDownHandler = (e) => this.dispatchKeyboard(e, "keydown");
|
|
5756
|
+
this.windowKeyUpHandler = (e) => this.dispatchKeyboard(e, "keyup");
|
|
5757
|
+
window.addEventListener("keydown", this.windowKeyDownHandler);
|
|
5758
|
+
window.addEventListener("keyup", this.windowKeyUpHandler);
|
|
5759
|
+
}
|
|
5760
|
+
/**
|
|
5761
|
+
* Gate a native keyboard event and fan it out to scene handlers +
|
|
5762
|
+
* chord-matched shortcuts. See {@link Scene.on} for the gate contract.
|
|
5763
|
+
*/
|
|
5764
|
+
dispatchKeyboard(e, type) {
|
|
5765
|
+
if (e.defaultPrevented) return;
|
|
5766
|
+
if (e.repeat) return;
|
|
5767
|
+
const active = typeof document !== "undefined" ? document.activeElement : null;
|
|
5768
|
+
if (ownsKeyboard(active)) return;
|
|
5769
|
+
const event = {
|
|
5770
|
+
type,
|
|
5771
|
+
key: e.key,
|
|
5772
|
+
code: e.code,
|
|
5773
|
+
repeat: e.repeat,
|
|
5774
|
+
ctrlKey: e.ctrlKey,
|
|
5775
|
+
altKey: e.altKey,
|
|
5776
|
+
shiftKey: e.shiftKey,
|
|
5777
|
+
metaKey: e.metaKey,
|
|
5778
|
+
target: e.target,
|
|
5779
|
+
nativeEvent: e,
|
|
5780
|
+
stopPropagation: () => e.stopPropagation(),
|
|
5781
|
+
preventDefault: () => e.preventDefault()
|
|
5782
|
+
};
|
|
5783
|
+
const handlers = type === "keydown" ? this.keydownHandlers : this.keyupHandlers;
|
|
5784
|
+
for (const handler of [...handlers]) handler(event);
|
|
5785
|
+
if (type === "keydown") {
|
|
5786
|
+
const chord = normalizeChord(e);
|
|
5787
|
+
for (const shortcut of [...this.shortcuts]) {
|
|
5788
|
+
if (shortcut.chord === chord) shortcut.handler(event);
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5436
5792
|
// --- domain: scene-facade — tree accessors ---
|
|
5437
5793
|
/**
|
|
5438
5794
|
* Manually advance the scene clock by `dt` milliseconds and render synchronously.
|
|
@@ -5882,14 +6238,6 @@ var Scene = class _Scene {
|
|
|
5882
6238
|
}
|
|
5883
6239
|
node.emit("blur", {});
|
|
5884
6240
|
});
|
|
5885
|
-
if (!isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role)) {
|
|
5886
|
-
el.addEventListener("keydown", (e) => {
|
|
5887
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
5888
|
-
e.preventDefault();
|
|
5889
|
-
node.dispatchEvent(new VectoJSEvent("click", node, e));
|
|
5890
|
-
}
|
|
5891
|
-
});
|
|
5892
|
-
}
|
|
5893
6241
|
if (node.a11yFullViewport) {
|
|
5894
6242
|
this.a11yRoot.insertBefore(el, this.a11yRoot.firstChild);
|
|
5895
6243
|
} else {
|
|
@@ -5908,7 +6256,24 @@ var Scene = class _Scene {
|
|
|
5908
6256
|
if (renderOrder !== void 0 && el.style.zIndex !== String(renderOrder)) {
|
|
5909
6257
|
el.style.zIndex = String(renderOrder);
|
|
5910
6258
|
}
|
|
5911
|
-
const
|
|
6259
|
+
const interactiveRole = !isNativelyFocusable(el) && !!attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role);
|
|
6260
|
+
if (interactiveRole && !this.keyboardActivatedA11y.has(el)) {
|
|
6261
|
+
el.addEventListener("keydown", (e) => {
|
|
6262
|
+
if (e.key === "Enter") {
|
|
6263
|
+
e.preventDefault();
|
|
6264
|
+
node.dispatchEvent(new VectoJSEvent("click", node, e));
|
|
6265
|
+
} else if (e.key === " ") {
|
|
6266
|
+
e.preventDefault();
|
|
6267
|
+
}
|
|
6268
|
+
});
|
|
6269
|
+
el.addEventListener("keyup", (e) => {
|
|
6270
|
+
if (e.key === " ") {
|
|
6271
|
+
node.dispatchEvent(new VectoJSEvent("click", node, e));
|
|
6272
|
+
}
|
|
6273
|
+
});
|
|
6274
|
+
this.keyboardActivatedA11y.add(el);
|
|
6275
|
+
}
|
|
6276
|
+
const implicitTabIndex = interactiveRole ? 0 : null;
|
|
5912
6277
|
const desiredTabIndex = attrs.tabIndex ?? implicitTabIndex;
|
|
5913
6278
|
if (desiredTabIndex === null) {
|
|
5914
6279
|
if (el.hasAttribute("tabindex")) el.removeAttribute("tabindex");
|
|
@@ -6850,6 +7215,7 @@ var Scene = class _Scene {
|
|
|
6850
7215
|
this.renderOrderCounter = 0;
|
|
6851
7216
|
this.a11yRenderOrders.clear();
|
|
6852
7217
|
this.activePortalsThisFrame.clear();
|
|
7218
|
+
this._updateWalkDt = dt;
|
|
6853
7219
|
this._tickBatchedDrivers(dt);
|
|
6854
7220
|
}
|
|
6855
7221
|
const computeEntities = this._computeEntitiesFor(this._structureVersion);
|
|
@@ -7020,6 +7386,7 @@ var Scene = class _Scene {
|
|
|
7020
7386
|
};
|
|
7021
7387
|
updateWalk(this.root);
|
|
7022
7388
|
for (const overlay of this.overlayRoot.children) updateWalk(overlay);
|
|
7389
|
+
this._updateWalkDt = null;
|
|
7023
7390
|
}
|
|
7024
7391
|
const transformTiming = this.phases.userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.transform) : null;
|
|
7025
7392
|
const wasmT0 = this.phases.enabled ? performance.now() : 0;
|
|
@@ -7220,6 +7587,7 @@ var Scene = class _Scene {
|
|
|
7220
7587
|
for (const overlay of this.overlayRoot.children) {
|
|
7221
7588
|
renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
|
|
7222
7589
|
}
|
|
7590
|
+
if (isMainRenderer) this._updateWalkDt = null;
|
|
7223
7591
|
if (this.phases.enabled) this.phases.record("drawWalk", performance.now() - drawT0);
|
|
7224
7592
|
if (drawTiming) endVectoUserTiming(drawTiming);
|
|
7225
7593
|
if (this.phases.userTiming) {
|
|
@@ -8365,6 +8733,7 @@ export {
|
|
|
8365
8733
|
GlyphRasterAtlas,
|
|
8366
8734
|
GridTextEntity,
|
|
8367
8735
|
Group,
|
|
8736
|
+
KEYBOARD_OWNING_ROLES,
|
|
8368
8737
|
MSDFTextEntity,
|
|
8369
8738
|
PARTICLE_OFFSET_LIFE,
|
|
8370
8739
|
PARTICLE_OFFSET_ORIGIN_X,
|
|
@@ -8396,6 +8765,8 @@ export {
|
|
|
8396
8765
|
isSafeUrl,
|
|
8397
8766
|
loadSpline,
|
|
8398
8767
|
measureVectoUserTiming,
|
|
8768
|
+
normalizeChord,
|
|
8769
|
+
ownsKeyboard,
|
|
8399
8770
|
parseColorToRGBA,
|
|
8400
8771
|
polySegmentToBezier,
|
|
8401
8772
|
sanitizeUrl,
|