@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/dist/index.mjs CHANGED
@@ -11,14 +11,14 @@ import {
11
11
  parseColorToRGBA,
12
12
  sanitizeUrl,
13
13
  setRendererDevMode
14
- } from "./chunk-LWEMD34D.mjs";
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-QVTCKWDO.mjs";
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 * 8;
228
- let px = this.particleData[offset];
229
- let py = this.particleData[offset + 1];
230
- let vx = this.particleData[offset + 2];
231
- let vy = this.particleData[offset + 3];
232
- const ox = this.particleData[offset + 4];
233
- const oy = this.particleData[offset + 5];
234
- const life = this.particleData[offset + 7];
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 + 1] = npy;
294
- this.particleData[offset + 2] = nvx;
295
- this.particleData[offset + 3] = nvy;
296
- this.particleData[offset + 7] = nlife;
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 && reusable.dataset.vectoGridLine === `${lineIndex}`) {
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 refocus = document.activeElement === expected;
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 (refocus) expected.focus({ preventScroll: true });
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 absolute = (el) => {
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 = el; node; node = node.parentElement) {
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
- return { top, left };
964
- };
965
- const decorated = els.map((el, i) => {
966
- const { top, left } = absolute(el);
967
- return { el, i, top, left, container: containers.has(el) };
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 = /* @__PURE__ */ new Map();
971
- for (const d of decorated) {
972
- const key = regions.get(d.el) ?? null;
973
- const bucket = buckets.get(key);
974
- if (bucket) bucket.push(d);
975
- else buckets.set(key, [d]);
976
- }
977
- const bandBottom = (r) => r.top + (r.container ? 4 : heightOf(r.el));
978
- const sorted = [];
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((p, q) => p.top - q.top || p.i - q.i);
1020
+ order.sort(byTopThenIndex);
981
1021
  let rowStart = 0;
982
- let rowBottom = order.length ? bandBottom(order[0]) : 0;
1022
+ let rowBottom = order.length ? orderBandBottom(order[0]) : 0;
983
1023
  const flushRow = (end) => {
984
- const row = order.slice(rowStart, end);
985
- row.sort((p, q) => (rtl ? q.left - p.left : p.left - q.left) || p.i - q.i);
986
- for (const r of row) sorted.push(r.el);
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, bandBottom(order[k]));
1032
+ rowBottom = Math.max(rowBottom, orderBandBottom(order[k]));
991
1033
  } else {
992
1034
  flushRow(k);
993
1035
  rowStart = k;
994
- rowBottom = bandBottom(order[k]);
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 real = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
1032
- return maxDPR !== void 0 ? Math.min(real, maxDPR) : real;
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
- this.backends.animReason = "rejected";
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
- this.backends.animReason = "rejected";
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) && (!clip || pointInBounds(clip, x, y)) && !this.isPointerTransparent(node)) {
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 true;
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 status = kernel === "scalar" ? this.ex.compose_scalar() : this.ex.compose_simd();
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 status = kernel === "scalar" ? this.ex.compose_scalar() : this.ex.compose_simd();
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 matrices first —
2520
- * this pass reads them. For the resident (no-copy) integration, write bounds
2521
- * via {@link boundsView} and read via {@link aabbView} + call
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
- this.lastStatus = this.ex.compute_aabbs(n);
2532
- if (this.lastStatus !== WASM_STATUS.OK) return;
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
- this.lastStatus = this.ex.compute_aabbs(count);
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
- this.width = typeof window !== "undefined" ? window.innerWidth : canvas.clientWidth || canvas.width || 800;
4800
- this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
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 dpr = window.devicePixelRatio || 1;
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
- this.resize(this.width, this.height);
5036
- if (this.renderer.isContextLost?.() !== true) this.render(this.renderer);
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 implicitTabIndex = !isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role) ? 0 : null;
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,