@vectojs/core 1.39.1 → 1.40.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.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _createStarExport(obj) { Object.keys(obj) .filter((key) => key !== "default" && key !== "__esModule") .forEach((key) => { if (exports.hasOwnProperty(key)) { return; } Object.defineProperty(exports, key, {enumerable: true, configurable: true, get: () => obj[key]}); }); } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7; var _class8; var _class9; var _class10; var _class11; var _class12; var _class13; var _class14; var _class15; var _class16; var _class17; var _class18; var _class19;const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _createStarExport(obj) { Object.keys(obj) .filter((key) => key !== "default" && key !== "__esModule") .forEach((key) => { if (exports.hasOwnProperty(key)) { return; } Object.defineProperty(exports, key, {enumerable: true, configurable: true, get: () => obj[key]}); }); } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7; var _class8; var _class9; var _class10; var _class11; var _class12; var _class13; var _class14; var _class15; var _class16; var _class17; var _class18; var _class19; var _class20; var _class21;const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
2
2
 
3
3
 
4
4
 
@@ -19,7 +19,7 @@ var _chunkBKXERWADjs = require('./chunk-BKXERWAD.js');
19
19
 
20
20
 
21
21
 
22
- var _chunkPOPXUPKRjs = require('./chunk-POPXUPKR.js');
22
+ var _chunkHC5EZOZJjs = require('./chunk-HC5EZOZJ.js');
23
23
 
24
24
  // src/tree/ComputeParticleEntity.ts
25
25
  var PARTICLE_STRIDE_FLOATS = 8;
@@ -31,7 +31,7 @@ var PARTICLE_OFFSET_ORIGIN_X = 4;
31
31
  var PARTICLE_OFFSET_ORIGIN_Y = 5;
32
32
  var PARTICLE_OFFSET_SIZE = 6;
33
33
  var PARTICLE_OFFSET_LIFE = 7;
34
- var ComputeParticleEntity = (_class = class extends _chunkPOPXUPKRjs.Entity {
34
+ var ComputeParticleEntity = (_class = class extends _chunkHC5EZOZJjs.Entity {
35
35
 
36
36
 
37
37
 
@@ -1599,6 +1599,62 @@ var HitTester = (_class6 = class {
1599
1599
  findEntityAt(x, y, frame, width, height) {
1600
1600
  const overlayHit = this.findHitRecursively(this.overlayRoot, x, y);
1601
1601
  if (overlayHit) return overlayHit;
1602
+ return this.findMainTreeHit(x, y, frame, width, height);
1603
+ }
1604
+ /**
1605
+ * The merged hit list (RFC5 §2, CTX-0600): the canvas spatial test plus
1606
+ * caller-observed DOM-native candidates (mirror / portal / `dom-visual`
1607
+ * extension point for CTX-0598) as ONE ordered candidate list.
1608
+ *
1609
+ * Query API only — no dispatch change: `findEntityAt` keeps its single
1610
+ * topmost answer byte-for-byte. Coordinates enter in scene space (callers
1611
+ * map client coordinates first, e.g. `Scene.findHitsAtClient`); backend
1612
+ * attribution follows. DOM candidates pass through the same
1613
+ * `disabled` / `pointerEvents: 'none'` predicate as the canvas paths
1614
+ * (RFC5 §2 rule 4); the browser's hit test is their geometric authority,
1615
+ * so scene-space `isPointInside` is not re-checked.
1616
+ *
1617
+ * Overlay order is authoritative (RFC5 §2 rule 2): overlay-subtree
1618
+ * candidates sort above main-tree candidates, DOM-native backends above
1619
+ * canvas within one subtree. Sorted descending by `priority`; ties keep
1620
+ * insertion order (canvas overlay, canvas main, then DOM in caller order).
1621
+ */
1622
+ findHitsAt(x, y, frame, width, height, domCandidates = []) {
1623
+ const results = [];
1624
+ const overlayHit = this.findHitRecursively(this.overlayRoot, x, y);
1625
+ if (overlayHit) results.push(this.toHitResult(overlayHit, "canvas", x, y));
1626
+ const mainHit = this.findMainTreeHit(x, y, frame, width, height);
1627
+ if (mainHit) results.push(this.toHitResult(mainHit, "canvas", x, y));
1628
+ for (const candidate of domCandidates) {
1629
+ if (!this.isHitEligible(candidate.node, x, y)) continue;
1630
+ results.push(this.toHitResult(candidate.node, candidate.backend, x, y));
1631
+ }
1632
+ results.sort((a, b) => b.priority - a.priority);
1633
+ return results;
1634
+ }
1635
+ /**
1636
+ * Attribute one candidate: scene-space point plus node-local point, with
1637
+ * the compositor-layer priority (overlay subtree above main tree,
1638
+ * DOM-native backends above canvas within one subtree).
1639
+ */
1640
+ toHitResult(node, backend, x, y) {
1641
+ return {
1642
+ node,
1643
+ backend,
1644
+ localPoint: node.worldToLocal(x, y),
1645
+ worldPoint: { x, y },
1646
+ priority: (this.isInOverlaySubtree(node) ? 2 : 0) + (backend === "canvas" ? 0 : 1)
1647
+ };
1648
+ }
1649
+ /** Whether `node` lives under the overlay root (drawn above the main tree). */
1650
+ isInOverlaySubtree(node) {
1651
+ for (let current = node; current; current = current.parent) {
1652
+ if (current === this.overlayRoot) return true;
1653
+ }
1654
+ return false;
1655
+ }
1656
+ /** Main-tree arm of {@link findEntityAt}, shared with {@link findHitsAt}. */
1657
+ findMainTreeHit(x, y, frame, width, height) {
1602
1658
  if (this.backends.hit && this.ensureHitGrid(frame, width, height)) {
1603
1659
  return this.findEntityAtWasm(x, y);
1604
1660
  }
@@ -3502,6 +3558,207 @@ function normalizeChord(input) {
3502
3558
  return ordered.join("+");
3503
3559
  }
3504
3560
 
3561
+ // src/tree/scene/SemanticProjectionPolicy.ts
3562
+ var DEFAULT_SEMANTIC_PROJECTION_POLICY = {
3563
+ choose: () => "project"
3564
+ };
3565
+ function getSemanticProjectionCapabilities() {
3566
+ return {
3567
+ htmlInCanvas: supportsHTMLInCanvas(),
3568
+ canvasTextRecovery: false
3569
+ };
3570
+ }
3571
+ var CONTROL_SEMANTIC_ROLES = /* @__PURE__ */ new Set([
3572
+ "button",
3573
+ "link",
3574
+ "checkbox",
3575
+ "radio",
3576
+ "switch",
3577
+ "slider",
3578
+ "spinbutton",
3579
+ "combobox",
3580
+ "listbox",
3581
+ "option",
3582
+ "menuitem",
3583
+ "menuitemcheckbox",
3584
+ "menuitemradio",
3585
+ "tab",
3586
+ "textbox",
3587
+ "searchbox",
3588
+ "progressbar",
3589
+ "scrollbar",
3590
+ "meter"
3591
+ ]);
3592
+ function isDeferrableSemanticNode(node) {
3593
+ const attrs = node.getA11yAttributes();
3594
+ if (attrs.tag !== void 0 && attrs.tag !== "div") return false;
3595
+ if (attrs.role !== void 0 && CONTROL_SEMANTIC_ROLES.has(attrs.role)) return false;
3596
+ if (typeof attrs.tabIndex === "number" && attrs.tabIndex >= 0) return false;
3597
+ const projection = _optionalChain([node, 'access', _86 => _86.getContentProjection, 'optionalCall', _87 => _87()]);
3598
+ if (_optionalChain([projection, 'optionalAccess', _88 => _88.selectable])) return false;
3599
+ return true;
3600
+ }
3601
+ function describeHTMLInCanvasSupport() {
3602
+ if (typeof document === "undefined") return { supported: false, reason: "no-dom" };
3603
+ try {
3604
+ const canvas = document.createElement("canvas");
3605
+ const ctx = canvas.getContext("2d");
3606
+ const native2D = typeof CanvasRenderingContext2D !== "undefined" && ctx instanceof CanvasRenderingContext2D;
3607
+ if (native2D && typeof ctx["drawElementImage"] === "function") {
3608
+ return { supported: true };
3609
+ }
3610
+ } catch (e15) {
3611
+ return { supported: false, reason: "no-backend" };
3612
+ }
3613
+ return { supported: false, reason: "no-backend" };
3614
+ }
3615
+ function supportsHTMLInCanvas() {
3616
+ return describeHTMLInCanvasSupport().supported;
3617
+ }
3618
+
3619
+ // src/tree/scene/ProjectionPolicy.ts
3620
+ var DEFAULT_PROJECTION_CAPABILITIES = {
3621
+ /** Static text (short): findability already covered by content projection. */
3622
+ text: {
3623
+ domSupported: true,
3624
+ nativeValue: "medium",
3625
+ domCost: "low",
3626
+ canvasCost: "low",
3627
+ autoDefault: "canvas"
3628
+ },
3629
+ /** Long/selectable text, Markdown blocks: selection, Ctrl+F, translation. */
3630
+ prose: {
3631
+ domSupported: true,
3632
+ nativeValue: "high",
3633
+ domCost: "medium",
3634
+ canvasCost: "high",
3635
+ autoDefault: "dom"
3636
+ },
3637
+ /** Code blocks: selectable source, but canvas carriers already serve selection — rests canvas until measured (RFC4 §6 hybrid mapping). */
3638
+ code: {
3639
+ domSupported: true,
3640
+ nativeValue: "medium",
3641
+ domCost: "medium",
3642
+ canvasCost: "high",
3643
+ autoDefault: "canvas"
3644
+ },
3645
+ /** Text input / editable: IME, caret, clipboard, BiDi come free in DOM. */
3646
+ input: {
3647
+ domSupported: true,
3648
+ nativeValue: "high",
3649
+ domCost: "low",
3650
+ canvasCost: "prohibitive",
3651
+ autoDefault: "dom"
3652
+ },
3653
+ /** Button / Link: cheap either way, native wins on focus + AT clicks. */
3654
+ button: {
3655
+ domSupported: true,
3656
+ nativeValue: "medium",
3657
+ domCost: "low",
3658
+ canvasCost: "low",
3659
+ autoDefault: "dom"
3660
+ },
3661
+ link: {
3662
+ domSupported: true,
3663
+ nativeValue: "medium",
3664
+ domCost: "low",
3665
+ canvasCost: "low",
3666
+ autoDefault: "dom"
3667
+ },
3668
+ /** Select / Dropdown / Checkbox / Radio: popup, keyboard, form semantics. */
3669
+ select: {
3670
+ domSupported: true,
3671
+ nativeValue: "high",
3672
+ domCost: "low",
3673
+ canvasCost: "high",
3674
+ autoDefault: "dom"
3675
+ },
3676
+ /** Container / layout group: no direct representation, children negotiate. */
3677
+ container: {
3678
+ domSupported: true,
3679
+ nativeValue: "none",
3680
+ domCost: "n/a",
3681
+ canvasCost: "low",
3682
+ autoDefault: "canvas"
3683
+ },
3684
+ /** Transform / camera rig: owns space, not pixels — never materialized. */
3685
+ transform: {
3686
+ domSupported: true,
3687
+ nativeValue: "none",
3688
+ domCost: "n/a",
3689
+ canvasCost: "n/a",
3690
+ autoDefault: "canvas"
3691
+ },
3692
+ /** Image / video frame: blit is cheap, one element per item is not. */
3693
+ image: {
3694
+ domSupported: true,
3695
+ nativeValue: "low",
3696
+ domCost: "medium",
3697
+ canvasCost: "low",
3698
+ autoDefault: "canvas"
3699
+ },
3700
+ /** Particles / danmaku / chart glyphs: bulk stays canvas, unconditionally. */
3701
+ particles: {
3702
+ domSupported: true,
3703
+ nativeValue: "none",
3704
+ domCost: "prohibitive",
3705
+ canvasCost: "low",
3706
+ autoDefault: "canvas"
3707
+ },
3708
+ /** Graph nodes/edges (interactive, moderate count): canvas until measured. */
3709
+ graph: {
3710
+ domSupported: true,
3711
+ nativeValue: "low",
3712
+ domCost: "medium",
3713
+ canvasCost: "medium",
3714
+ autoDefault: "canvas"
3715
+ },
3716
+ /** Custom shader / GPU particles: no DOM representation exists. */
3717
+ shader: {
3718
+ domSupported: false,
3719
+ nativeValue: "none",
3720
+ domCost: "unsupported",
3721
+ canvasCost: "low",
3722
+ autoDefault: "canvas"
3723
+ }
3724
+ };
3725
+ var UNKNOWN_PROJECTION_CAPABILITY = {
3726
+ domSupported: false,
3727
+ nativeValue: "none",
3728
+ domCost: "unsupported",
3729
+ canvasCost: "low",
3730
+ autoDefault: "canvas"
3731
+ };
3732
+ var PROJECTION_AUTO_HYSTERESIS_FRAMES = 3;
3733
+ var PROJECTION_AUTO_DOM_BUDGET = 500;
3734
+ function resolveProjection(want, cap, ctx) {
3735
+ if (want === "canvas") return { resolved: "canvas", reason: null };
3736
+ if (!ctx.hasDOM || !ctx.domBackendMounted) {
3737
+ return { resolved: "canvas", reason: "no-dom-backend" };
3738
+ }
3739
+ if (!cap.domSupported) return { resolved: "canvas", reason: "unsupported-kind" };
3740
+ if (want === "dom") {
3741
+ if (cap.domCost === "prohibitive") return { resolved: "canvas", reason: "prohibitive-cost" };
3742
+ return { resolved: "dom", reason: null };
3743
+ }
3744
+ if (cap.nativeValue === "high" && cap.domCost !== "prohibitive") {
3745
+ return { resolved: "dom", reason: null };
3746
+ }
3747
+ if (cap.canvasCost === "prohibitive" && cap.domSupported) {
3748
+ return { resolved: "dom", reason: null };
3749
+ }
3750
+ if (cap.autoDefault === "dom") return { resolved: "dom", reason: null };
3751
+ return { resolved: "canvas", reason: null };
3752
+ }
3753
+ function hysteresisVote(current, desired, consecutive, hysteresisFrames) {
3754
+ if (current === void 0 || current === desired) {
3755
+ return { resolved: desired, consecutive: 0, flipped: false };
3756
+ }
3757
+ const next = consecutive + 1;
3758
+ if (next >= hysteresisFrames) return { resolved: desired, consecutive: 0, flipped: true };
3759
+ return { resolved: current, consecutive: next, flipped: false };
3760
+ }
3761
+
3505
3762
  // src/tree/Scene.ts
3506
3763
  var RANGE_VALUE_ROLES = /* @__PURE__ */ new Set(["slider", "spinbutton", "progressbar", "scrollbar", "meter"]);
3507
3764
  var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
@@ -3525,7 +3782,8 @@ var KEYBOARD_OWNING_ROLES = /* @__PURE__ */ new Set([
3525
3782
  ]);
3526
3783
  function ownsKeyboard(el) {
3527
3784
  if (!el) return false;
3528
- if (el === document.body || el === document.documentElement) return false;
3785
+ if (typeof document !== "undefined" && (el === document.body || el === document.documentElement))
3786
+ return false;
3529
3787
  if (el.hasAttribute("data-vecto-a11y-root")) return false;
3530
3788
  const tag = el.tagName;
3531
3789
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
@@ -3561,10 +3819,13 @@ var SCENE_OPTION_KEYS = [
3561
3819
  "maxFPS",
3562
3820
  "particleBackend",
3563
3821
  "pointBackend",
3822
+ "projectionAutoDomBudget",
3823
+ "projectionHysteresisFrames",
3564
3824
  "readingDirection",
3565
3825
  "renderer",
3566
3826
  "renderMode",
3567
3827
  "respectReducedMotion",
3828
+ "semanticProjectionPolicy",
3568
3829
  "userTiming"
3569
3830
  ];
3570
3831
  var SCENE_FIELD_NOT_OPTION = {
@@ -3681,9 +3942,9 @@ function parseCssMatrix(transform) {
3681
3942
  }
3682
3943
  function clientToGridLocal(contentEl, canvas, clientX, clientY) {
3683
3944
  const line = contentEl.querySelector("[data-vecto-grid-line]");
3684
- const originMarker = _optionalChain([line, 'optionalAccess', _86 => _86.querySelector, 'call', _87 => _87('[data-vecto-grid-basis="origin"]')]);
3685
- const xMarker = _optionalChain([line, 'optionalAccess', _88 => _88.querySelector, 'call', _89 => _89('[data-vecto-grid-basis="x"]')]);
3686
- const yMarker = _optionalChain([line, 'optionalAccess', _90 => _90.querySelector, 'call', _91 => _91('[data-vecto-grid-basis="y"]')]);
3945
+ const originMarker = _optionalChain([line, 'optionalAccess', _89 => _89.querySelector, 'call', _90 => _90('[data-vecto-grid-basis="origin"]')]);
3946
+ const xMarker = _optionalChain([line, 'optionalAccess', _91 => _91.querySelector, 'call', _92 => _92('[data-vecto-grid-basis="x"]')]);
3947
+ const yMarker = _optionalChain([line, 'optionalAccess', _93 => _93.querySelector, 'call', _94 => _94('[data-vecto-grid-basis="y"]')]);
3687
3948
  if (line && originMarker && xMarker && yMarker) {
3688
3949
  const origin = originMarker.getBoundingClientRect();
3689
3950
  const xPoint = xMarker.getBoundingClientRect();
@@ -3767,7 +4028,7 @@ function nearestTextPositionInLine(line, x, y) {
3767
4028
  nearest = { position: { node, offset }, distance: candidate.distance };
3768
4029
  }
3769
4030
  }
3770
- return _nullishCoalesce(_optionalChain([nearest, 'optionalAccess', _92 => _92.position]), () => ( null));
4031
+ return _nullishCoalesce(_optionalChain([nearest, 'optionalAccess', _95 => _95.position]), () => ( null));
3771
4032
  }
3772
4033
  function nearestTextPositionInProjection(contentEl, canvas, x, y, eventTarget) {
3773
4034
  if (contentEl.dataset.vectoContentGrid !== void 0) {
@@ -3781,7 +4042,7 @@ function nearestTextPositionInProjection(contentEl, canvas, x, y, eventTarget) {
3781
4042
  }
3782
4043
  targetLine = targetLine.parentElement;
3783
4044
  }
3784
- if (_optionalChain([targetLine, 'optionalAccess', _93 => _93.parentElement]) === contentEl) {
4045
+ if (_optionalChain([targetLine, 'optionalAccess', _96 => _96.parentElement]) === contentEl) {
3785
4046
  return nearestTextPositionInLine(targetLine, x, y);
3786
4047
  }
3787
4048
  let bestLine = null;
@@ -3843,13 +4104,13 @@ function extendSelection(selection, anchor, focus) {
3843
4104
  try {
3844
4105
  selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
3845
4106
  return;
3846
- } catch (e15) {
4107
+ } catch (e16) {
3847
4108
  }
3848
4109
  try {
3849
4110
  selection.collapse(anchor.node, anchor.offset);
3850
4111
  selection.extend(focus.node, focus.offset);
3851
4112
  return;
3852
- } catch (e16) {
4113
+ } catch (e17) {
3853
4114
  }
3854
4115
  const anchorRange = document.createRange();
3855
4116
  anchorRange.setStart(anchor.node, anchor.offset);
@@ -4043,7 +4304,7 @@ var Scene = (_class15 = class _Scene {
4043
4304
  __init108() {this.forcedColorsChangeHandler = null}
4044
4305
  /** True when the OS asks for reduced motion and we respect it. Read by the animation drivers. */
4045
4306
  get prefersReducedMotion() {
4046
- return this.respectReducedMotion && !!_optionalChain([this, 'access', _94 => _94.reducedMotionQuery, 'optionalAccess', _95 => _95.matches]);
4307
+ return this.respectReducedMotion && !!_optionalChain([this, 'access', _97 => _97.reducedMotionQuery, 'optionalAccess', _98 => _98.matches]);
4047
4308
  }
4048
4309
  /**
4049
4310
  * True when the OS is in a forced-colors mode (Windows High Contrast, and the
@@ -4054,7 +4315,7 @@ var Scene = (_class15 = class _Scene {
4054
4315
  * when the setting toggles.
4055
4316
  */
4056
4317
  get forcedColors() {
4057
- return !!_optionalChain([this, 'access', _96 => _96.forcedColorsQuery, 'optionalAccess', _97 => _97.matches]);
4318
+ return !!_optionalChain([this, 'access', _99 => _99.forcedColorsQuery, 'optionalAccess', _100 => _100.matches]);
4058
4319
  }
4059
4320
  // --- domain: a11y-projection — sync throttle and shadow-DOM state ---
4060
4321
  /**
@@ -4062,21 +4323,41 @@ var Scene = (_class15 = class _Scene {
4062
4323
  * frame. See {@link SceneOptions.a11ySyncInterval}.
4063
4324
  */
4064
4325
  __init109() {this.a11ySyncInterval = 0}
4326
+ /**
4327
+ * Per-node semantic projection policy (RFC3 §5, CTX-0599). Consulted once
4328
+ * per node by {@link shouldProjectA11y}; the default projects everything
4329
+ * the legacy predicate projects, so reassigning nothing changes nothing.
4330
+ * See {@link SceneOptions.semanticProjectionPolicy}.
4331
+ */
4332
+ __init110() {this.semanticProjectionPolicy = DEFAULT_SEMANTIC_PROJECTION_POLICY}
4333
+ /**
4334
+ * Consecutive syncs an `'auto'`-policy node must keep voting for the other
4335
+ * backend before its sticky resolution flips (RFC4 §3 rule 3, CTX-0601).
4336
+ * Documented tunable — see {@link PROJECTION_AUTO_HYSTERESIS_FRAMES}.
4337
+ */
4338
+ __init111() {this.projectionHysteresisFrames = PROJECTION_AUTO_HYSTERESIS_FRAMES}
4339
+ /**
4340
+ * Maximum `'auto'`-resolved DOM residents per scene per frame (RFC4 §4
4341
+ * particle-row backstop: bulk-count nodes stay canvas). Explicit `'dom'`
4342
+ * requests bypass it. Documented tunable — see
4343
+ * {@link PROJECTION_AUTO_DOM_BUDGET}.
4344
+ */
4345
+ __init112() {this.projectionAutoDomBudget = PROJECTION_AUTO_DOM_BUDGET}
4065
4346
  /** Timestamp of the last a11y sync, for throttling. */
4066
- __init110() {this.lastA11ySync = -Infinity}
4347
+ __init113() {this.lastA11ySync = -Infinity}
4067
4348
  /** True if we skipped an a11y sync during animation and need to sync when at rest. */
4068
- __init111() {this.a11yPendingSyncAfterAnimation = false}
4349
+ __init114() {this.a11yPendingSyncAfterAnimation = false}
4069
4350
  // A11y / Automation Layer. `null` in non-DOM (SSR/Node) environments — the
4070
4351
  // whole projection degrades to a no-op so the engine's logic stays usable
4071
4352
  // server-side (e.g. headless layout / vector export) without jsdom.
4072
4353
 
4073
- __init112() {this.a11yElements = /* @__PURE__ */ new Map()}
4354
+ __init115() {this.a11yElements = /* @__PURE__ */ new Map()}
4074
4355
  /** Elements that already have the synthetic Enter/Space activation handler
4075
4356
  * (see the syncA11y refresh pass) — install-once bookkeeping (#694). */
4076
- __init113() {this.keyboardActivatedA11y = /* @__PURE__ */ new WeakSet()}
4357
+ __init116() {this.keyboardActivatedA11y = /* @__PURE__ */ new WeakSet()}
4077
4358
  // --- domain: content-projection — projected DOM, sync state, calibration, selection ---
4078
4359
  /** DOM nodes mirroring static text content, keyed by entity id. */
4079
- __init114() {this.contentElements = /* @__PURE__ */ new Map()}
4360
+ __init117() {this.contentElements = /* @__PURE__ */ new Map()}
4080
4361
  /**
4081
4362
  * What the last completed content-projection sync was built from, per entity.
4082
4363
  *
@@ -4085,9 +4366,9 @@ var Scene = (_class15 = class _Scene {
4085
4366
  * build. Only populated for entities that opt in via
4086
4367
  * {@link Entity.getContentEpoch}. (carryctx CTX-0199)
4087
4368
  */
4088
- __init115() {this.contentSyncState = /* @__PURE__ */ new Map()}
4369
+ __init118() {this.contentSyncState = /* @__PURE__ */ new Map()}
4089
4370
  /** Invalidates grid font calibration after browser font availability changes. */
4090
- __init116() {this.contentFontEpoch = 0}
4371
+ __init119() {this.contentFontEpoch = 0}
4091
4372
  /**
4092
4373
  * Bumped whenever the viewport itself changes shape, which re-tiers blocks
4093
4374
  * without moving any of them.
@@ -4100,7 +4381,7 @@ var Scene = (_class15 = class _Scene {
4100
4381
  * carriers it no longer needs. Scrolling needs no such epoch: it moves the root,
4101
4382
  * so every block's parent world transform changes and the check fails honestly.
4102
4383
  */
4103
- __init117() {this.contentViewportEpoch = 0}
4384
+ __init120() {this.contentViewportEpoch = 0}
4104
4385
  /**
4105
4386
  * One-entry memo for a parent's world transform, held as scalars.
4106
4387
  *
@@ -4115,16 +4396,16 @@ var Scene = (_class15 = class _Scene {
4115
4396
  * sync, so a memo can never survive into a frame in which the parent has
4116
4397
  * moved.
4117
4398
  */
4118
- __init118() {this._pwNode = null}
4119
- __init119() {this._pwSerial = -1}
4120
- __init120() {this._pwa = 1}
4121
- __init121() {this._pwb = 0}
4122
- __init122() {this._pwc = 0}
4123
- __init123() {this._pwd = 1}
4124
- __init124() {this._pwe = 0}
4125
- __init125() {this._pwf = 0}
4399
+ __init121() {this._pwNode = null}
4400
+ __init122() {this._pwSerial = -1}
4401
+ __init123() {this._pwa = 1}
4402
+ __init124() {this._pwb = 0}
4403
+ __init125() {this._pwc = 0}
4404
+ __init126() {this._pwd = 1}
4405
+ __init127() {this._pwe = 0}
4406
+ __init128() {this._pwf = 0}
4126
4407
  /** Incremented once per top-level `syncA11y`, invalidating `_pwNode`. */
4127
- __init126() {this._syncSerial = 0}
4408
+ __init129() {this._syncSerial = 0}
4128
4409
  /**
4129
4410
  * Disables the settled-walk fast path in {@link syncContentProjection}.
4130
4411
  *
@@ -4136,38 +4417,38 @@ var Scene = (_class15 = class _Scene {
4136
4417
  * Not part of the public API and not documented as an option: turning it on only
4137
4418
  * makes a settled document slower, never more correct.
4138
4419
  */
4139
- __init127() {this.disableSettledFastPath = false}
4420
+ __init130() {this.disableSettledFastPath = false}
4140
4421
  /** Cached Canvas-to-client scale for the current font/viewport epoch. */
4141
- __init128() {this.contentMetricScaleEpoch = -1}
4142
- __init129() {this.contentMetricScaleX = 1}
4143
- __init130() {this.contentProjectionEnabled = true}
4422
+ __init131() {this.contentMetricScaleEpoch = -1}
4423
+ __init132() {this.contentMetricScaleX = 1}
4424
+ __init133() {this.contentProjectionEnabled = true}
4144
4425
  // Virtualization margin (px) for the content-projection CARRIER band;
4145
4426
  // `undefined` → one viewport height, resolved at sync time. `Infinity` is
4146
4427
  // unsupported here: it unwindows every carrier, which is O(total glyphs).
4147
- __init131() {this.contentProjectionMargin = void 0}
4428
+ __init134() {this.contentProjectionMargin = void 0}
4148
4429
  // Virtualization margin (px) for the SEMANTIC tier — whether a block has
4149
4430
  // any projected DOM. `undefined` → falls back to contentProjectionMargin, so
4150
4431
  // the default keeps one gate. `Infinity` = every block keeps resident text.
4151
- __init132() {this.contentSemanticMargin = void 0}
4432
+ __init135() {this.contentSemanticMargin = void 0}
4152
4433
  // How many coarse-tier blocks may be MATERIALIZED per sync, spreading the
4153
4434
  // resident tier's document-open cost across frames. `Infinity` = one
4154
4435
  // synchronous pass.
4155
- __init133() {this.contentSemanticBudget = DEFAULT_CONTENT_SEMANTIC_BUDGET}
4436
+ __init136() {this.contentSemanticBudget = DEFAULT_CONTENT_SEMANTIC_BUDGET}
4156
4437
  // Remaining materializations in the CURRENT sync. Reset at the start of each
4157
4438
  // a11y walk; decremented per coarse block that creates its element.
4158
- __init134() {this.contentSemanticBudgetLeft = 0}
4439
+ __init137() {this.contentSemanticBudgetLeft = 0}
4159
4440
  // Set when a sync deferred at least one block, so the scene knows to keep
4160
4441
  // drawing frames until the resident tier is complete. Without it a static
4161
4442
  // scene in `onDemand` mode would stop rendering with the document half
4162
4443
  // materialized and never finish.
4163
- __init135() {this.contentSemanticDeferred = false}
4164
- __init136() {this.contentSelectionEndListener = null}
4444
+ __init138() {this.contentSemanticDeferred = false}
4445
+ __init139() {this.contentSelectionEndListener = null}
4165
4446
  // --- domain: render-scheduler — per-frame walk flags ---
4166
4447
  // Animation/interactive flags collected during the render walk (tree-walk
4167
4448
  // fusion): the loop reads last frame's answers instead of re-walking the
4168
4449
  // tree up to 4× per tick. Start true so the first tick stays conservative.
4169
- __init137() {this.frameHadAnimation = true}
4170
- __init138() {this.frameHadInteractive = true}
4450
+ __init140() {this.frameHadAnimation = true}
4451
+ __init141() {this.frameHadInteractive = true}
4171
4452
  // --- domain: context-and-resize — resize handler, DPR query, canvas observer ---
4172
4453
 
4173
4454
  /** Active `(resolution: Ndppx)` media query watching for a runtime DPR change
@@ -4175,17 +4456,17 @@ var Scene = (_class15 = class _Scene {
4175
4456
  * can be re-scaled — otherwise it stays rasterized at the old DPR and blurs.
4176
4457
  * A resolution media query only fires when leaving its exact value, so the
4177
4458
  * handler re-arms a fresh query for the new DPR each time. */
4178
- __init139() {this.dprMediaQuery = null}
4459
+ __init142() {this.dprMediaQuery = null}
4179
4460
  /** For embedded (`disableWindowResize`) scenes: observes the canvas element so
4180
4461
  * a CSS/layout-driven size change re-runs `resize()`. A window `resize`
4181
4462
  * listener never fires for these (the window isn't what changed), so without
4182
4463
  * this an embedded canvas stayed at its initial size forever. */
4183
- __init140() {this.canvasResizeObserver = null}
4184
- __init141() {this.dprChangeHandler = null}
4185
- __init142() {this.dprPollInterval = null}
4186
- __init143() {this.lastDpr = 1}
4464
+ __init143() {this.canvasResizeObserver = null}
4465
+ __init144() {this.dprChangeHandler = null}
4466
+ __init145() {this.dprPollInterval = null}
4467
+ __init146() {this.lastDpr = 1}
4187
4468
  // --- domain: a11y-projection — focus, overlay geometry, DOM ordering, portals ---
4188
- __init144() {this.focusedA11yElement = null}
4469
+ __init147() {this.focusedA11yElement = null}
4189
4470
  /**
4190
4471
  * Canvas box geometry: the CSS↔logical mapping, overlay layer alignment, and
4191
4472
  * the DPR math (extraction 5, `DEC-0024`).
@@ -4212,7 +4493,7 @@ var Scene = (_class15 = class _Scene {
4212
4493
  /** Shadow elements the pointer is currently inside. Lets a removal that happens
4213
4494
  * mid-hover synthesize the `pointerleave` the browser never sends for a
4214
4495
  * detached element, so the entity doesn't keep its hover state. */
4215
- __init145() {this.hoveredA11yElements = /* @__PURE__ */ new WeakSet()}
4496
+ __init148() {this.hoveredA11yElements = /* @__PURE__ */ new WeakSet()}
4216
4497
  /**
4217
4498
  * Entity ids the application has pinned via {@link requestA11yProjection}.
4218
4499
  *
@@ -4220,13 +4501,13 @@ var Scene = (_class15 = class _Scene {
4220
4501
  * a stale id simply never matches. Cleared per-entity by
4221
4502
  * {@link releaseA11yProjection}.
4222
4503
  */
4223
- __init146() {this.a11yProjectionRequests = /* @__PURE__ */ new Set()}
4504
+ __init149() {this.a11yProjectionRequests = /* @__PURE__ */ new Set()}
4224
4505
  /** Persistent tabindex=-1 element in a11yRoot. When the focused a11y mirror is
4225
4506
  * pruned (virtualization/streaming/removal) while it holds focus, we move
4226
4507
  * focus here instead of letting the browser drop it to <body> — keeping the
4227
4508
  * screen-reader virtual cursor inside the scene's a11y region. */
4228
- __init147() {this.focusSentinel = null}
4229
- __init148() {this.caretBlinkTimer = null}
4509
+ __init150() {this.focusSentinel = null}
4510
+ __init151() {this.caretBlinkTimer = null}
4230
4511
  /**
4231
4512
  * The projected DOM's ordering engine — the reorder flag, the per-pass scratch
4232
4513
  * collections, the visual reading-order sort and the cursor-based
@@ -4236,7 +4517,7 @@ var Scene = (_class15 = class _Scene {
4236
4517
  * delegate here; the collect-and-prune walk stays behind because it needs
4237
4518
  * {@link shouldProjectA11y} and the focus/caret state.
4238
4519
  */
4239
- __init149() {this.a11yOrder = new A11yProjectionManager()}
4520
+ __init152() {this.a11yOrder = new A11yProjectionManager()}
4240
4521
  /**
4241
4522
  * The content projection's selection preservation and grid calibration
4242
4523
  * (extraction 3, `DEC-0022`).
@@ -4288,11 +4569,35 @@ var Scene = (_class15 = class _Scene {
4288
4569
  set a11yNeedsReorder(value) {
4289
4570
  this.a11yOrder.needsReorder = value;
4290
4571
  }
4291
- __init150() {this.portalRoot = null}
4292
- __init151() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
4293
- __init152() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
4294
- __init153() {this.portalEntities = /* @__PURE__ */ new Map()}
4295
- __init154() {this.renderOrderCounter = 0}
4572
+ __init153() {this.portalRoot = null}
4573
+ __init154() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
4574
+ __init155() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
4575
+ __init156() {this.portalEntities = /* @__PURE__ */ new Map()}
4576
+ /**
4577
+ * Generic projection backends (RFC1 §5 vocabulary, `tree/scene/ProjectionBackend.ts`).
4578
+ * Render-independent: the interface exchanges only node identity, world
4579
+ * matrix, and lifecycle calls, so registering a DOM backend adds no
4580
+ * `HTMLElement` surface to core. `@vectojs/dom` owns the only implementation.
4581
+ */
4582
+ __init157() {this.projectionBackends = []}
4583
+ /** Per-frame seen-sets driving {@link pruneProjectionBackends} (portal-precedent). */
4584
+ __init158() {this.domSeenPrevFrame = /* @__PURE__ */ new Set()}
4585
+ __init159() {this.domSeenThisFrame = /* @__PURE__ */ new Set()}
4586
+ __init160() {this.domSeenNodes = /* @__PURE__ */ new Map()}
4587
+ /**
4588
+ * Negotiation state (RFC4 §3, CTX-0601): last resolution per node (the
4589
+ * per-scene queryable surface behind {@link getProjectionResolutions}),
4590
+ * hysteresis votes, per-kind capability overrides, and explicit gesture
4591
+ * pins. Plain data only — no `HTMLElement` surface in core.
4592
+ */
4593
+ __init161() {this.projectionResolutions = /* @__PURE__ */ new Map()}
4594
+ __init162() {this.projectionHysteresis = /* @__PURE__ */ new Map()}
4595
+ __init163() {this.projectionCapabilityOverrides = /* @__PURE__ */ new Map()}
4596
+ __init164() {this.projectionGesturePins = /* @__PURE__ */ new Map()}
4597
+ /** Frame id the `'auto'` DOM budget count belongs to (bulk backstop). */
4598
+ __init165() {this.projectionBudgetFrame = -1}
4599
+ __init166() {this.projectionDomAutoCount = 0}
4600
+ __init167() {this.renderOrderCounter = 0}
4296
4601
  // --- domain: render-scheduler — authoritative frame counter ---
4297
4602
  /**
4298
4603
  * Monotonic render-frame counter, bumped once per authoritative `render()`
@@ -4302,7 +4607,7 @@ var Scene = (_class15 = class _Scene {
4302
4607
  * back to the ancestor walk. Public for the same reason `Entity._getTrig`/
4303
4608
  * `_setWorldCache` are: it is a cross-class render-internal contract.
4304
4609
  */
4305
- __init155() {this.currentFrame = 0}
4610
+ __init168() {this.currentFrame = 0}
4306
4611
  // --- domain: wasm-backend-facade — EXTRACTED (see scene/WasmBackendFacade.ts) ---
4307
4612
  /**
4308
4613
  * The four invisible WASM accelerators and the resident transform store.
@@ -4372,8 +4677,8 @@ var Scene = (_class15 = class _Scene {
4372
4677
  // Stays on `Scene`: it walks `root` *and* `overlayRoot` for entities the
4373
4678
  // render walk consumes, so only its cache key is store state. The facade owns
4374
4679
  // the key; the walk stays here.
4375
- __init156() {this._computeEntities = []}
4376
- __init157() {this._computeEntitiesVersion = -1}
4680
+ __init169() {this._computeEntities = []}
4681
+ __init170() {this._computeEntitiesVersion = -1}
4377
4682
  /** Invalidate the resident WASM store layout; the next wasm-mode frame rebuilds
4378
4683
  * it. Called by `Entity.add`/`remove` (topology changes only). */
4379
4684
  markStructureChanged() {
@@ -4612,7 +4917,7 @@ var Scene = (_class15 = class _Scene {
4612
4917
  get animBatchedLastFrame() {
4613
4918
  return this._wasmBackend.animBatchedLastFrame;
4614
4919
  }
4615
- __init158() {this.animGate = {
4920
+ __init171() {this.animGate = {
4616
4921
  spring: 128,
4617
4922
  tween: 256,
4618
4923
  mixed: 128
@@ -4708,7 +5013,7 @@ var Scene = (_class15 = class _Scene {
4708
5013
  * `tickDrivers()`'s per-frame stamp would skip it a full frame — a one-frame
4709
5014
  * JS/WASM divergence. Written in {@link render} next to `_tickBatchedDrivers`.
4710
5015
  */
4711
- __init159() {this._updateWalkDt = null}
5016
+ __init172() {this._updateWalkDt = null}
4712
5017
  // --- domain: render-scheduler — batched driver tick (extraction 6), reads the anim backend ---
4713
5018
  /**
4714
5019
  * Advance every registered entity's active drivers for this frame, batching
@@ -4736,25 +5041,25 @@ var Scene = (_class15 = class _Scene {
4736
5041
  * sync, so retaining the order prevents a newly opened overlay from spending
4737
5042
  * its first frame below previously projected controls.
4738
5043
  */
4739
- __init160() {this.a11yRenderOrders = /* @__PURE__ */ new Map()}
5044
+ __init173() {this.a11yRenderOrders = /* @__PURE__ */ new Map()}
4740
5045
  // --- domain: context-and-resize — GL/GPU canvases, device state, pointer, size warnings ---
4741
5046
  // Optional WebGL point-cloud layer (see SceneOptions.pointBackend).
4742
- __init161() {this.pointRenderer = null}
4743
- __init162() {this.glCanvas = null}
4744
- __init163() {this.glContextLostHandler = null}
4745
- __init164() {this.glContextRestoredHandler = null}
5047
+ __init174() {this.pointRenderer = null}
5048
+ __init175() {this.glCanvas = null}
5049
+ __init176() {this.glContextLostHandler = null}
5050
+ __init177() {this.glContextRestoredHandler = null}
4746
5051
 
4747
5052
 
4748
5053
 
4749
- __init165() {this.disableWindowResize = false}
5054
+ __init178() {this.disableWindowResize = false}
4750
5055
  /** See {@link SceneOptions.maxDPR}. `undefined` = uncapped (real DPR). */
4751
5056
 
4752
5057
  // WebGPU properties
4753
- __init166() {this.destroyed = false}
4754
- __init167() {this.device = null}
4755
- __init168() {this.deviceLost = false}
4756
- __init169() {this.particleBackend = "auto"}
4757
- __init170() {this._webgpuDisabled = false}
5058
+ __init179() {this.destroyed = false}
5059
+ __init180() {this.device = null}
5060
+ __init181() {this.deviceLost = false}
5061
+ __init182() {this.particleBackend = "auto"}
5062
+ __init183() {this._webgpuDisabled = false}
4758
5063
  get webgpuDisabled() {
4759
5064
  return this._webgpuDisabled || this.particleBackend === "cpu";
4760
5065
  }
@@ -4768,7 +5073,7 @@ var Scene = (_class15 = class _Scene {
4768
5073
  * active.
4769
5074
  */
4770
5075
  get webglDrawStats() {
4771
- return _nullishCoalesce(_optionalChain([this, 'access', _98 => _98.pointRenderer, 'optionalAccess', _99 => _99.stats, 'optionalCall', _100 => _100()]), () => ( null));
5076
+ return _nullishCoalesce(_optionalChain([this, 'access', _101 => _101.pointRenderer, 'optionalAccess', _102 => _102.stats, 'optionalCall', _103 => _103()]), () => ( null));
4772
5077
  }
4773
5078
  /**
4774
5079
  * Whether a WebGPU device is currently live for particle compute.
@@ -4782,28 +5087,28 @@ var Scene = (_class15 = class _Scene {
4782
5087
  set webgpuDisabled(value) {
4783
5088
  this._webgpuDisabled = value;
4784
5089
  }
4785
- __init171() {this.recoveryTimerId = null}
4786
- __init172() {this.manager = null}
4787
- __init173() {this.initializingWebGPU = false}
4788
- __init174() {this.gpuCanvas = null}
4789
- __init175() {this.gpuContext = null}
5090
+ __init184() {this.recoveryTimerId = null}
5091
+ __init185() {this.manager = null}
5092
+ __init186() {this.initializingWebGPU = false}
5093
+ __init187() {this.gpuCanvas = null}
5094
+ __init188() {this.gpuContext = null}
4790
5095
  /** True while the GPU canvas holds a presented particle frame (needs clearing when they leave). */
4791
- __init176() {this.gpuHasContent = false}
4792
- __init177() {this.mouseX = -9999}
4793
- __init178() {this.mouseY = -9999}
4794
- __init179() {this.pointerMoveListener = null}
4795
- __init180() {this.pointerLeaveListener = null}
5096
+ __init189() {this.gpuHasContent = false}
5097
+ __init190() {this.mouseX = -9999}
5098
+ __init191() {this.mouseY = -9999}
5099
+ __init192() {this.pointerMoveListener = null}
5100
+ __init193() {this.pointerLeaveListener = null}
4796
5101
  /** Element the pointer listeners are bound to (parent container if present,
4797
5102
  * else the canvas). Stored so `destroy()` detaches from the same element. */
4798
- __init181() {this.pointerEventTarget = null}
5103
+ __init194() {this.pointerEventTarget = null}
4799
5104
  // --- scene-level keyboard channel state (see on/off/registerShortcut) ---
4800
- __init182() {this.keydownHandlers = []}
4801
- __init183() {this.keyupHandlers = []}
5105
+ __init195() {this.keydownHandlers = []}
5106
+ __init196() {this.keyupHandlers = []}
4802
5107
  /** Chord-normalized shortcut table, matched on keydown only. */
4803
- __init184() {this.shortcuts = []}
4804
- __init185() {this.windowKeyDownHandler = null}
4805
- __init186() {this.windowKeyUpHandler = null}
4806
- __init187() {this.hasWarnedZeroSize = false}
5108
+ __init197() {this.shortcuts = []}
5109
+ __init198() {this.windowKeyDownHandler = null}
5110
+ __init199() {this.windowKeyUpHandler = null}
5111
+ __init200() {this.hasWarnedZeroSize = false}
4807
5112
  /**
4808
5113
  * Latch for {@link Scene.resize}'s invalid-dimension warning.
4809
5114
  *
@@ -4813,8 +5118,8 @@ var Scene = (_class15 = class _Scene {
4813
5118
  * because `resize()` is commonly driven from a `ResizeObserver`, which would
4814
5119
  * otherwise warn on every frame of a drag.
4815
5120
  */
4816
- __init188() {this.hasWarnedInvalidResize = false}
4817
- __init189() {this.fontLoadHandler = null}
5121
+ __init201() {this.hasWarnedInvalidResize = false}
5122
+ __init202() {this.fontLoadHandler = null}
4818
5123
  // --- domain: scene-facade — dev-mode diagnostics ---
4819
5124
  // ── Dev-mode warning infrastructure ──────────────────────────────
4820
5125
  //
@@ -4844,12 +5149,12 @@ var Scene = (_class15 = class _Scene {
4844
5149
  static _devModeDetected() {
4845
5150
  if (_Scene._devMode) return true;
4846
5151
  const gp = typeof globalThis !== "undefined" ? globalThis : void 0;
4847
- if (_optionalChain([gp, 'optionalAccess', _101 => _101.__DEV__])) return true;
4848
- if (_optionalChain([gp, 'optionalAccess', _102 => _102.process, 'optionalAccess', _103 => _103.env, 'optionalAccess', _104 => _104.NODE_ENV]) === "development") return true;
5152
+ if (_optionalChain([gp, 'optionalAccess', _104 => _104.__DEV__])) return true;
5153
+ if (_optionalChain([gp, 'optionalAccess', _105 => _105.process, 'optionalAccess', _106 => _106.env, 'optionalAccess', _107 => _107.NODE_ENV]) === "development") return true;
4849
5154
  return false;
4850
5155
  }
4851
5156
 
4852
- __init190() {this._devFrameCount = 0}
5157
+ __init203() {this._devFrameCount = 0}
4853
5158
  _devWarn(message) {
4854
5159
  if (!this._devActive) return;
4855
5160
  console.warn(`[vectojs/dev] ${message}`);
@@ -4907,9 +5212,9 @@ var Scene = (_class15 = class _Scene {
4907
5212
  let checked = 0;
4908
5213
  const walkProjections = (node) => {
4909
5214
  if (checked > 10) return;
4910
- const proj = _optionalChain([node, 'access', _105 => _105.getContentProjection, 'optionalCall', _106 => _106()]);
4911
- if (_optionalChain([proj, 'optionalAccess', _107 => _107.text]) && proj.selectable !== false) {
4912
- const el = _optionalChain([this, 'access', _108 => _108.contentElements, 'optionalAccess', _109 => _109.get, 'call', _110 => _110(node.id)]);
5215
+ const proj = _optionalChain([node, 'access', _108 => _108.getContentProjection, 'optionalCall', _109 => _109()]);
5216
+ if (_optionalChain([proj, 'optionalAccess', _110 => _110.text]) && proj.selectable !== false) {
5217
+ const el = _optionalChain([this, 'access', _111 => _111.contentElements, 'optionalAccess', _112 => _112.get, 'call', _113 => _113(node.id)]);
4913
5218
  if (el) {
4914
5219
  const projectedText = el.textContent || "";
4915
5220
  const windowed = el.dataset.vectoProjectionWindow !== void 0;
@@ -4927,14 +5232,14 @@ var Scene = (_class15 = class _Scene {
4927
5232
  walkProjections(this.root);
4928
5233
  }
4929
5234
  // --- domain: scene-facade — construction (wires every collaborator) ---
4930
- constructor(canvas, options = {}) {;_class15.prototype.__init88.call(this);_class15.prototype.__init89.call(this);_class15.prototype.__init90.call(this);_class15.prototype.__init91.call(this);_class15.prototype.__init92.call(this);_class15.prototype.__init93.call(this);_class15.prototype.__init94.call(this);_class15.prototype.__init95.call(this);_class15.prototype.__init96.call(this);_class15.prototype.__init97.call(this);_class15.prototype.__init98.call(this);_class15.prototype.__init99.call(this);_class15.prototype.__init100.call(this);_class15.prototype.__init101.call(this);_class15.prototype.__init102.call(this);_class15.prototype.__init103.call(this);_class15.prototype.__init104.call(this);_class15.prototype.__init105.call(this);_class15.prototype.__init106.call(this);_class15.prototype.__init107.call(this);_class15.prototype.__init108.call(this);_class15.prototype.__init109.call(this);_class15.prototype.__init110.call(this);_class15.prototype.__init111.call(this);_class15.prototype.__init112.call(this);_class15.prototype.__init113.call(this);_class15.prototype.__init114.call(this);_class15.prototype.__init115.call(this);_class15.prototype.__init116.call(this);_class15.prototype.__init117.call(this);_class15.prototype.__init118.call(this);_class15.prototype.__init119.call(this);_class15.prototype.__init120.call(this);_class15.prototype.__init121.call(this);_class15.prototype.__init122.call(this);_class15.prototype.__init123.call(this);_class15.prototype.__init124.call(this);_class15.prototype.__init125.call(this);_class15.prototype.__init126.call(this);_class15.prototype.__init127.call(this);_class15.prototype.__init128.call(this);_class15.prototype.__init129.call(this);_class15.prototype.__init130.call(this);_class15.prototype.__init131.call(this);_class15.prototype.__init132.call(this);_class15.prototype.__init133.call(this);_class15.prototype.__init134.call(this);_class15.prototype.__init135.call(this);_class15.prototype.__init136.call(this);_class15.prototype.__init137.call(this);_class15.prototype.__init138.call(this);_class15.prototype.__init139.call(this);_class15.prototype.__init140.call(this);_class15.prototype.__init141.call(this);_class15.prototype.__init142.call(this);_class15.prototype.__init143.call(this);_class15.prototype.__init144.call(this);_class15.prototype.__init145.call(this);_class15.prototype.__init146.call(this);_class15.prototype.__init147.call(this);_class15.prototype.__init148.call(this);_class15.prototype.__init149.call(this);_class15.prototype.__init150.call(this);_class15.prototype.__init151.call(this);_class15.prototype.__init152.call(this);_class15.prototype.__init153.call(this);_class15.prototype.__init154.call(this);_class15.prototype.__init155.call(this);_class15.prototype.__init156.call(this);_class15.prototype.__init157.call(this);_class15.prototype.__init158.call(this);_class15.prototype.__init159.call(this);_class15.prototype.__init160.call(this);_class15.prototype.__init161.call(this);_class15.prototype.__init162.call(this);_class15.prototype.__init163.call(this);_class15.prototype.__init164.call(this);_class15.prototype.__init165.call(this);_class15.prototype.__init166.call(this);_class15.prototype.__init167.call(this);_class15.prototype.__init168.call(this);_class15.prototype.__init169.call(this);_class15.prototype.__init170.call(this);_class15.prototype.__init171.call(this);_class15.prototype.__init172.call(this);_class15.prototype.__init173.call(this);_class15.prototype.__init174.call(this);_class15.prototype.__init175.call(this);_class15.prototype.__init176.call(this);_class15.prototype.__init177.call(this);_class15.prototype.__init178.call(this);_class15.prototype.__init179.call(this);_class15.prototype.__init180.call(this);_class15.prototype.__init181.call(this);_class15.prototype.__init182.call(this);_class15.prototype.__init183.call(this);_class15.prototype.__init184.call(this);_class15.prototype.__init185.call(this);_class15.prototype.__init186.call(this);_class15.prototype.__init187.call(this);_class15.prototype.__init188.call(this);_class15.prototype.__init189.call(this);_class15.prototype.__init190.call(this);
5235
+ constructor(canvas, options = {}) {;_class15.prototype.__init88.call(this);_class15.prototype.__init89.call(this);_class15.prototype.__init90.call(this);_class15.prototype.__init91.call(this);_class15.prototype.__init92.call(this);_class15.prototype.__init93.call(this);_class15.prototype.__init94.call(this);_class15.prototype.__init95.call(this);_class15.prototype.__init96.call(this);_class15.prototype.__init97.call(this);_class15.prototype.__init98.call(this);_class15.prototype.__init99.call(this);_class15.prototype.__init100.call(this);_class15.prototype.__init101.call(this);_class15.prototype.__init102.call(this);_class15.prototype.__init103.call(this);_class15.prototype.__init104.call(this);_class15.prototype.__init105.call(this);_class15.prototype.__init106.call(this);_class15.prototype.__init107.call(this);_class15.prototype.__init108.call(this);_class15.prototype.__init109.call(this);_class15.prototype.__init110.call(this);_class15.prototype.__init111.call(this);_class15.prototype.__init112.call(this);_class15.prototype.__init113.call(this);_class15.prototype.__init114.call(this);_class15.prototype.__init115.call(this);_class15.prototype.__init116.call(this);_class15.prototype.__init117.call(this);_class15.prototype.__init118.call(this);_class15.prototype.__init119.call(this);_class15.prototype.__init120.call(this);_class15.prototype.__init121.call(this);_class15.prototype.__init122.call(this);_class15.prototype.__init123.call(this);_class15.prototype.__init124.call(this);_class15.prototype.__init125.call(this);_class15.prototype.__init126.call(this);_class15.prototype.__init127.call(this);_class15.prototype.__init128.call(this);_class15.prototype.__init129.call(this);_class15.prototype.__init130.call(this);_class15.prototype.__init131.call(this);_class15.prototype.__init132.call(this);_class15.prototype.__init133.call(this);_class15.prototype.__init134.call(this);_class15.prototype.__init135.call(this);_class15.prototype.__init136.call(this);_class15.prototype.__init137.call(this);_class15.prototype.__init138.call(this);_class15.prototype.__init139.call(this);_class15.prototype.__init140.call(this);_class15.prototype.__init141.call(this);_class15.prototype.__init142.call(this);_class15.prototype.__init143.call(this);_class15.prototype.__init144.call(this);_class15.prototype.__init145.call(this);_class15.prototype.__init146.call(this);_class15.prototype.__init147.call(this);_class15.prototype.__init148.call(this);_class15.prototype.__init149.call(this);_class15.prototype.__init150.call(this);_class15.prototype.__init151.call(this);_class15.prototype.__init152.call(this);_class15.prototype.__init153.call(this);_class15.prototype.__init154.call(this);_class15.prototype.__init155.call(this);_class15.prototype.__init156.call(this);_class15.prototype.__init157.call(this);_class15.prototype.__init158.call(this);_class15.prototype.__init159.call(this);_class15.prototype.__init160.call(this);_class15.prototype.__init161.call(this);_class15.prototype.__init162.call(this);_class15.prototype.__init163.call(this);_class15.prototype.__init164.call(this);_class15.prototype.__init165.call(this);_class15.prototype.__init166.call(this);_class15.prototype.__init167.call(this);_class15.prototype.__init168.call(this);_class15.prototype.__init169.call(this);_class15.prototype.__init170.call(this);_class15.prototype.__init171.call(this);_class15.prototype.__init172.call(this);_class15.prototype.__init173.call(this);_class15.prototype.__init174.call(this);_class15.prototype.__init175.call(this);_class15.prototype.__init176.call(this);_class15.prototype.__init177.call(this);_class15.prototype.__init178.call(this);_class15.prototype.__init179.call(this);_class15.prototype.__init180.call(this);_class15.prototype.__init181.call(this);_class15.prototype.__init182.call(this);_class15.prototype.__init183.call(this);_class15.prototype.__init184.call(this);_class15.prototype.__init185.call(this);_class15.prototype.__init186.call(this);_class15.prototype.__init187.call(this);_class15.prototype.__init188.call(this);_class15.prototype.__init189.call(this);_class15.prototype.__init190.call(this);_class15.prototype.__init191.call(this);_class15.prototype.__init192.call(this);_class15.prototype.__init193.call(this);_class15.prototype.__init194.call(this);_class15.prototype.__init195.call(this);_class15.prototype.__init196.call(this);_class15.prototype.__init197.call(this);_class15.prototype.__init198.call(this);_class15.prototype.__init199.call(this);_class15.prototype.__init200.call(this);_class15.prototype.__init201.call(this);_class15.prototype.__init202.call(this);_class15.prototype.__init203.call(this);
4931
5236
  this.canvas = canvas;
4932
5237
  this.debugA11y = _nullishCoalesce(options.debugA11y, () => ( false));
4933
5238
  this.disableWindowResize = _nullishCoalesce(options.disableWindowResize, () => ( false));
4934
5239
  this.maxDPR = options.maxDPR;
4935
5240
  if (this.disableWindowResize) {
4936
- const styleWidth = parseInlinePx(_optionalChain([canvas, 'access', _111 => _111.style, 'optionalAccess', _112 => _112.width]));
4937
- const styleHeight = parseInlinePx(_optionalChain([canvas, 'access', _113 => _113.style, 'optionalAccess', _114 => _114.height]));
5241
+ const styleWidth = parseInlinePx(_optionalChain([canvas, 'access', _114 => _114.style, 'optionalAccess', _115 => _115.width]));
5242
+ const styleHeight = parseInlinePx(_optionalChain([canvas, 'access', _116 => _116.style, 'optionalAccess', _117 => _117.height]));
4938
5243
  this.width = _nullishCoalesce(styleWidth, () => ( (canvas.width || canvas.clientWidth || 0)));
4939
5244
  this.height = _nullishCoalesce(styleHeight, () => ( (canvas.height || canvas.clientHeight || 0)));
4940
5245
  } else {
@@ -4947,7 +5252,7 @@ var Scene = (_class15 = class _Scene {
4947
5252
  }
4948
5253
  }
4949
5254
  const globalProcess = typeof globalThis !== "undefined" ? globalThis.process : void 0;
4950
- const isTest = globalProcess && (_optionalChain([globalProcess, 'access', _115 => _115.env, 'optionalAccess', _116 => _116.NODE_ENV]) === "test" || _optionalChain([globalProcess, 'access', _117 => _117.env, 'optionalAccess', _118 => _118.VITEST]) === "true");
5255
+ const isTest = globalProcess && (_optionalChain([globalProcess, 'access', _118 => _118.env, 'optionalAccess', _119 => _119.NODE_ENV]) === "test" || _optionalChain([globalProcess, 'access', _120 => _120.env, 'optionalAccess', _121 => _121.VITEST]) === "true");
4951
5256
  this.maxFPS = _nullishCoalesce(options.maxFPS, () => ( (isTest ? 0 : 60)));
4952
5257
  this.respectReducedMotion = _nullishCoalesce(options.respectReducedMotion, () => ( true));
4953
5258
  this.autoThrottle = _nullishCoalesce(options.autoThrottle, () => ( true));
@@ -4955,6 +5260,9 @@ var Scene = (_class15 = class _Scene {
4955
5260
  this.phases.userTiming = _nullishCoalesce(options.userTiming, () => ( false));
4956
5261
  this.particleBackend = _nullishCoalesce(options.particleBackend, () => ( "auto"));
4957
5262
  this.a11ySyncInterval = _nullishCoalesce(options.a11ySyncInterval, () => ( 0));
5263
+ this.semanticProjectionPolicy = _nullishCoalesce(options.semanticProjectionPolicy, () => ( DEFAULT_SEMANTIC_PROJECTION_POLICY));
5264
+ this.projectionHysteresisFrames = _nullishCoalesce(options.projectionHysteresisFrames, () => ( PROJECTION_AUTO_HYSTERESIS_FRAMES));
5265
+ this.projectionAutoDomBudget = _nullishCoalesce(options.projectionAutoDomBudget, () => ( PROJECTION_AUTO_DOM_BUDGET));
4958
5266
  this.contentProjectionEnabled = _nullishCoalesce(options.contentProjection, () => ( true));
4959
5267
  this.contentProjectionMargin = options.contentProjectionMargin;
4960
5268
  this.contentSemanticMargin = options.contentSemanticMargin;
@@ -4968,9 +5276,9 @@ var Scene = (_class15 = class _Scene {
4968
5276
  if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
4969
5277
  this.forcedColorsQuery = window.matchMedia("(forced-colors: active)");
4970
5278
  this.forcedColorsChangeHandler = () => this.markDirty();
4971
- _optionalChain([this, 'access', _119 => _119.forcedColorsQuery, 'access', _120 => _120.addEventListener, 'optionalCall', _121 => _121("change", this.forcedColorsChangeHandler)]);
5279
+ _optionalChain([this, 'access', _122 => _122.forcedColorsQuery, 'access', _123 => _123.addEventListener, 'optionalCall', _124 => _124("change", this.forcedColorsChangeHandler)]);
4972
5280
  }
4973
- this.root = new class RootEntity extends _chunkPOPXUPKRjs.Entity {
5281
+ this.root = new class RootEntity extends _chunkHC5EZOZJjs.Entity {
4974
5282
  isPointInside() {
4975
5283
  return false;
4976
5284
  }
@@ -4980,7 +5288,7 @@ var Scene = (_class15 = class _Scene {
4980
5288
  }("root");
4981
5289
  this.root._scene = this;
4982
5290
  this._wasmBackend = new WasmBackendFacade(this.root);
4983
- this.overlayRoot = new class OverlayRoot extends _chunkPOPXUPKRjs.Entity {
5291
+ this.overlayRoot = new class OverlayRoot extends _chunkHC5EZOZJjs.Entity {
4984
5292
  isPointInside() {
4985
5293
  return false;
4986
5294
  }
@@ -4999,9 +5307,9 @@ var Scene = (_class15 = class _Scene {
4999
5307
  this.maxDPR
5000
5308
  );
5001
5309
  }
5002
- _optionalChain([this, 'access', _122 => _122.renderer, 'access', _123 => _123.onContextRestored, 'optionalCall', _124 => _124(() => {
5310
+ _optionalChain([this, 'access', _125 => _125.renderer, 'access', _126 => _126.onContextRestored, 'optionalCall', _127 => _127(() => {
5003
5311
  this.markDirty();
5004
- if (_optionalChain([this, 'access', _125 => _125.renderer, 'access', _126 => _126.isContextLost, 'optionalCall', _127 => _127()]) !== true) this.render(this.renderer);
5312
+ if (_optionalChain([this, 'access', _128 => _128.renderer, 'access', _129 => _129.isContextLost, 'optionalCall', _130 => _130()]) !== true) this.render(this.renderer);
5005
5313
  })]);
5006
5314
  if (typeof document !== "undefined") {
5007
5315
  this.a11yRoot = document.createElement("div");
@@ -5174,7 +5482,7 @@ var Scene = (_class15 = class _Scene {
5174
5482
  watchDevicePixelRatio() {
5175
5483
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
5176
5484
  if (this.dprMediaQuery && this.dprChangeHandler) {
5177
- _optionalChain([this, 'access', _128 => _128.dprMediaQuery, 'access', _129 => _129.removeEventListener, 'optionalCall', _130 => _130("change", this.dprChangeHandler)]);
5485
+ _optionalChain([this, 'access', _131 => _131.dprMediaQuery, 'access', _132 => _132.removeEventListener, 'optionalCall', _133 => _133("change", this.dprChangeHandler)]);
5178
5486
  }
5179
5487
  const raw = window.devicePixelRatio;
5180
5488
  const dpr = Number.isFinite(raw) && raw > 0 ? raw : 1;
@@ -5193,13 +5501,13 @@ var Scene = (_class15 = class _Scene {
5193
5501
  console.warn("[VectoJS] DPR resize failed", err);
5194
5502
  }
5195
5503
  try {
5196
- if (_optionalChain([this, 'access', _131 => _131.renderer, 'access', _132 => _132.isContextLost, 'optionalCall', _133 => _133()]) !== true) this.render(this.renderer);
5504
+ if (_optionalChain([this, 'access', _134 => _134.renderer, 'access', _135 => _135.isContextLost, 'optionalCall', _136 => _136()]) !== true) this.render(this.renderer);
5197
5505
  } catch (err) {
5198
5506
  console.warn("[VectoJS] DPR repaint failed", err);
5199
5507
  }
5200
5508
  this.watchDevicePixelRatio();
5201
5509
  };
5202
- _optionalChain([query, 'access', _134 => _134.addEventListener, 'optionalCall', _135 => _135("change", handler)]);
5510
+ _optionalChain([query, 'access', _137 => _137.addEventListener, 'optionalCall', _138 => _138("change", handler)]);
5203
5511
  this.dprMediaQuery = query;
5204
5512
  this.dprChangeHandler = handler;
5205
5513
  this.lastDpr = dpr;
@@ -5215,7 +5523,7 @@ var Scene = (_class15 = class _Scene {
5215
5523
  console.warn("[VectoJS] DPR poll resize failed", err);
5216
5524
  }
5217
5525
  try {
5218
- if (_optionalChain([this, 'access', _136 => _136.renderer, 'access', _137 => _137.isContextLost, 'optionalCall', _138 => _138()]) !== true) this.render(this.renderer);
5526
+ if (_optionalChain([this, 'access', _139 => _139.renderer, 'access', _140 => _140.isContextLost, 'optionalCall', _141 => _141()]) !== true) this.render(this.renderer);
5219
5527
  } catch (err) {
5220
5528
  console.warn("[VectoJS] DPR poll repaint failed", err);
5221
5529
  }
@@ -5239,7 +5547,7 @@ var Scene = (_class15 = class _Scene {
5239
5547
  if (typeof gl.addEventListener !== "function") return;
5240
5548
  this.glContextLostHandler = (e) => {
5241
5549
  e.preventDefault();
5242
- _optionalChain([this, 'access', _139 => _139.pointRenderer, 'optionalAccess', _140 => _140.destroy, 'call', _141 => _141()]);
5550
+ _optionalChain([this, 'access', _142 => _142.pointRenderer, 'optionalAccess', _143 => _143.destroy, 'call', _144 => _144()]);
5243
5551
  this.pointRenderer = null;
5244
5552
  };
5245
5553
  this.glContextRestoredHandler = () => {
@@ -5273,7 +5581,7 @@ var Scene = (_class15 = class _Scene {
5273
5581
  if (this.width > 0 && this.height > 0) return;
5274
5582
  if (typeof ResizeObserver === "undefined") return;
5275
5583
  const observer = new ResizeObserver((entries) => {
5276
- const box = _optionalChain([entries, 'access', _142 => _142[0], 'optionalAccess', _143 => _143.contentRect]);
5584
+ const box = _optionalChain([entries, 'access', _145 => _145[0], 'optionalAccess', _146 => _146.contentRect]);
5277
5585
  if (!box || !(box.width > 0) || !(box.height > 0)) return;
5278
5586
  observer.disconnect();
5279
5587
  this.canvasResizeObserver = null;
@@ -5306,6 +5614,37 @@ var Scene = (_class15 = class _Scene {
5306
5614
  findEntityAt(x, y) {
5307
5615
  return this._hitTester.findEntityAt(x, y, this.currentFrame, this.width, this.height);
5308
5616
  }
5617
+ // --- domain: hit-test — merged HitResult query (RFC5 §2, CTX-0600) ---
5618
+ /**
5619
+ * The merged hit list for a scene-space point: the canvas spatial test
5620
+ * plus caller-observed DOM-native candidates (mirror / portal /
5621
+ * `dom-visual` extension point for CTX-0598) as ONE ordered candidate
5622
+ * list (`HitResult`, overlay order authoritative).
5623
+ *
5624
+ * Query API alongside {@link findEntityAt} — no dispatch change:
5625
+ * `findEntityAt` keeps its single topmost answer byte-for-byte, keyboard
5626
+ * and AT flows are untouched. For browser pointer coordinates use
5627
+ * {@link findHitsAtClient}, which maps through {@link clientToScene} first.
5628
+ */
5629
+ findHitsAt(x, y, domCandidates = []) {
5630
+ return this._hitTester.findHitsAt(
5631
+ x,
5632
+ y,
5633
+ this.currentFrame,
5634
+ this.width,
5635
+ this.height,
5636
+ domCandidates
5637
+ );
5638
+ }
5639
+ /**
5640
+ * {@link findHitsAt} for browser viewport coordinates: maps through
5641
+ * {@link clientToScene} so coordinates enter in scene space (RFC5 §2
5642
+ * rule 3) and backend attribution follows.
5643
+ */
5644
+ findHitsAtClient(clientX, clientY, domCandidates = []) {
5645
+ const point = this.clientToScene(clientX, clientY);
5646
+ return this.findHitsAt(point.x, point.y, domCandidates);
5647
+ }
5309
5648
  // --- domain: hit-test — client-to-scene mapping ---
5310
5649
  /** Convert browser viewport coordinates into this Scene's logical coordinates. */
5311
5650
  clientToScene(clientX, clientY) {
@@ -5346,6 +5685,15 @@ var Scene = (_class15 = class _Scene {
5346
5685
  this.activePortalsThisFrame.delete(node.id);
5347
5686
  this.activePortalsPrevFrame.delete(node.id);
5348
5687
  }
5688
+ if (node.domPolicy === "dom" || node.domResident) {
5689
+ for (const backend of this.projectionBackends) backend.unmount(node);
5690
+ this.domSeenThisFrame.delete(node.id);
5691
+ this.domSeenPrevFrame.delete(node.id);
5692
+ this.domSeenNodes.delete(node.id);
5693
+ }
5694
+ this.projectionResolutions.delete(node.id);
5695
+ this.projectionHysteresis.delete(node.id);
5696
+ this.projectionGesturePins.delete(node.id);
5349
5697
  const contentEl = this.contentElements.get(node.id);
5350
5698
  if (contentEl) {
5351
5699
  this._contentProjection.clearGridState(node.id, contentEl);
@@ -5365,7 +5713,7 @@ var Scene = (_class15 = class _Scene {
5365
5713
  }
5366
5714
  if (this.hoveredA11yElements.has(el)) {
5367
5715
  this.hoveredA11yElements.delete(el);
5368
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("pointerleave", node, void 0, false));
5716
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("pointerleave", node, void 0, false));
5369
5717
  }
5370
5718
  this.preserveFocusOnRemoval(el);
5371
5719
  el.remove();
@@ -5457,6 +5805,15 @@ var Scene = (_class15 = class _Scene {
5457
5805
  while (this.overlayRoot.children.length > 0) {
5458
5806
  this.destroyEntitySubtree(this.overlayRoot.children.at(-1));
5459
5807
  }
5808
+ for (const node of this.domSeenNodes.values()) {
5809
+ for (const backend of this.projectionBackends) backend.unmount(node);
5810
+ }
5811
+ this.domSeenNodes.clear();
5812
+ this.domSeenThisFrame.clear();
5813
+ this.domSeenPrevFrame.clear();
5814
+ this.projectionResolutions.clear();
5815
+ this.projectionHysteresis.clear();
5816
+ this.projectionGesturePins.clear();
5460
5817
  if (typeof window !== "undefined" && !this.disableWindowResize) {
5461
5818
  window.removeEventListener("resize", this.resizeHandler);
5462
5819
  }
@@ -5465,7 +5822,7 @@ var Scene = (_class15 = class _Scene {
5465
5822
  this.canvasResizeObserver = null;
5466
5823
  }
5467
5824
  if (this.dprMediaQuery && this.dprChangeHandler) {
5468
- _optionalChain([this, 'access', _144 => _144.dprMediaQuery, 'access', _145 => _145.removeEventListener, 'optionalCall', _146 => _146("change", this.dprChangeHandler)]);
5825
+ _optionalChain([this, 'access', _147 => _147.dprMediaQuery, 'access', _148 => _148.removeEventListener, 'optionalCall', _149 => _149("change", this.dprChangeHandler)]);
5469
5826
  this.dprMediaQuery = null;
5470
5827
  this.dprChangeHandler = null;
5471
5828
  }
@@ -5474,7 +5831,7 @@ var Scene = (_class15 = class _Scene {
5474
5831
  this.dprPollInterval = null;
5475
5832
  }
5476
5833
  if (this.forcedColorsQuery && this.forcedColorsChangeHandler) {
5477
- _optionalChain([this, 'access', _147 => _147.forcedColorsQuery, 'access', _148 => _148.removeEventListener, 'optionalCall', _149 => _149("change", this.forcedColorsChangeHandler)]);
5834
+ _optionalChain([this, 'access', _150 => _150.forcedColorsQuery, 'access', _151 => _151.removeEventListener, 'optionalCall', _152 => _152("change", this.forcedColorsChangeHandler)]);
5478
5835
  this.forcedColorsQuery = null;
5479
5836
  this.forcedColorsChangeHandler = null;
5480
5837
  }
@@ -5505,9 +5862,9 @@ var Scene = (_class15 = class _Scene {
5505
5862
  }
5506
5863
  this.pointerEventTarget = null;
5507
5864
  }
5508
- _optionalChain([this, 'access', _150 => _150.a11yRoot, 'optionalAccess', _151 => _151.remove, 'call', _152 => _152()]);
5865
+ _optionalChain([this, 'access', _153 => _153.a11yRoot, 'optionalAccess', _154 => _154.remove, 'call', _155 => _155()]);
5509
5866
  this.focusSentinel = null;
5510
- _optionalChain([this, 'access', _153 => _153.portalRoot, 'optionalAccess', _154 => _154.remove, 'call', _155 => _155()]);
5867
+ _optionalChain([this, 'access', _156 => _156.portalRoot, 'optionalAccess', _157 => _157.remove, 'call', _158 => _158()]);
5511
5868
  this.a11yElements.clear();
5512
5869
  for (const el of this.contentElements.values()) el.remove();
5513
5870
  this.contentElements.clear();
@@ -5523,10 +5880,10 @@ var Scene = (_class15 = class _Scene {
5523
5880
  }
5524
5881
  this.glContextLostHandler = null;
5525
5882
  this.glContextRestoredHandler = null;
5526
- _optionalChain([this, 'access', _156 => _156.pointRenderer, 'optionalAccess', _157 => _157.destroy, 'call', _158 => _158()]);
5527
- _optionalChain([this, 'access', _159 => _159.renderer, 'access', _160 => _160.dispose, 'optionalCall', _161 => _161()]);
5528
- _optionalChain([this, 'access', _162 => _162.glCanvas, 'optionalAccess', _163 => _163.remove, 'call', _164 => _164()]);
5529
- _optionalChain([this, 'access', _165 => _165.gpuCanvas, 'optionalAccess', _166 => _166.remove, 'call', _167 => _167()]);
5883
+ _optionalChain([this, 'access', _159 => _159.pointRenderer, 'optionalAccess', _160 => _160.destroy, 'call', _161 => _161()]);
5884
+ _optionalChain([this, 'access', _162 => _162.renderer, 'access', _163 => _163.dispose, 'optionalCall', _164 => _164()]);
5885
+ _optionalChain([this, 'access', _165 => _165.glCanvas, 'optionalAccess', _166 => _166.remove, 'call', _167 => _167()]);
5886
+ _optionalChain([this, 'access', _168 => _168.gpuCanvas, 'optionalAccess', _169 => _169.remove, 'call', _170 => _170()]);
5530
5887
  this.gpuCanvas = null;
5531
5888
  this.gpuContext = null;
5532
5889
  if (this.recoveryTimerId) {
@@ -5538,7 +5895,7 @@ var Scene = (_class15 = class _Scene {
5538
5895
  this.manager = null;
5539
5896
  }
5540
5897
  if (this.device) {
5541
- _optionalChain([this, 'access', _168 => _168.device, 'access', _169 => _169.destroy, 'optionalCall', _170 => _170()]);
5898
+ _optionalChain([this, 'access', _171 => _171.device, 'access', _172 => _172.destroy, 'optionalCall', _173 => _173()]);
5542
5899
  this.device = null;
5543
5900
  }
5544
5901
  }
@@ -5955,14 +6312,47 @@ var Scene = (_class15 = class _Scene {
5955
6312
  shouldProjectA11y(node) {
5956
6313
  if (!node.interactive) return false;
5957
6314
  if (!(node.width > 0 || node.a11yFullViewport)) return false;
6315
+ if (this.resolveProjectionFor(node) === "dom") return false;
5958
6316
  switch (node.a11yProjection) {
5959
6317
  case "never":
5960
6318
  return false;
5961
6319
  case "onDemand":
5962
- return this.a11yEngaged(node);
6320
+ return this.a11yEngaged(node) && this.resolveSemanticProjection(node);
5963
6321
  default:
5964
- return true;
6322
+ return this.resolveSemanticProjection(node);
6323
+ }
6324
+ }
6325
+ /**
6326
+ * Policy half of {@link shouldProjectA11y} (RFC3 §5, CTX-0599).
6327
+ *
6328
+ * The legacy gates above (interactive, box, `a11yProjection` engagement)
6329
+ * are unchanged; this only maps a {@link SemanticProjectionPolicy} decision
6330
+ * onto project/suppress. Out of the box the default policy returns
6331
+ * `'project'`, so behaviour is identical to having no policy.
6332
+ *
6333
+ * `'defer-to-browser'` always falls back to projection today: no deferral
6334
+ * backend exists (`supportsHTMLInCanvas()` is `false`), so deferring would
6335
+ * silently drop semantics — the exact failure RFC §3 rules out. When a
6336
+ * backend lands, only allow-listed plain display text
6337
+ * ({@link isDeferrableSemanticNode}; never controls) may actually defer. A
6338
+ * throwing policy likewise falls back to projection: a policy must never be
6339
+ * able to drop semantics by accident.
6340
+ */
6341
+ resolveSemanticProjection(node) {
6342
+ const policy = _nullishCoalesce(this.semanticProjectionPolicy, () => ( DEFAULT_SEMANTIC_PROJECTION_POLICY));
6343
+ let decision;
6344
+ try {
6345
+ decision = policy.choose(node, getSemanticProjectionCapabilities(), {
6346
+ hasDOM: this.a11yRoot !== null
6347
+ });
6348
+ } catch (e18) {
6349
+ decision = "project";
5965
6350
  }
6351
+ if (decision === "never") return false;
6352
+ if (decision === "defer-to-browser" && isDeferrableSemanticNode(node) && supportsHTMLInCanvas()) {
6353
+ return false;
6354
+ }
6355
+ return true;
5966
6356
  }
5967
6357
  /**
5968
6358
  * Whether an `a11yProjection: 'onDemand'` entity is currently engaged enough to
@@ -5991,7 +6381,7 @@ var Scene = (_class15 = class _Scene {
5991
6381
  if (this.mouseX > -9e3 && !this.projectsSelectableText(node)) {
5992
6382
  if (node.isPointInside(this.mouseX, this.mouseY)) return true;
5993
6383
  }
5994
- const existing = _optionalChain([this, 'access', _171 => _171.a11yElements, 'optionalAccess', _172 => _172.get, 'call', _173 => _173(node.id)]);
6384
+ const existing = _optionalChain([this, 'access', _174 => _174.a11yElements, 'optionalAccess', _175 => _175.get, 'call', _176 => _176(node.id)]);
5995
6385
  if (existing && typeof document !== "undefined" && document.activeElement === existing) {
5996
6386
  return true;
5997
6387
  }
@@ -6005,8 +6395,8 @@ var Scene = (_class15 = class _Scene {
6005
6395
  * selection.
6006
6396
  */
6007
6397
  projectsSelectableText(node) {
6008
- const projection = _optionalChain([node, 'access', _174 => _174.getContentProjection, 'optionalCall', _175 => _175()]);
6009
- return !!_optionalChain([projection, 'optionalAccess', _176 => _176.text]) && projection.selectable !== false;
6398
+ const projection = _optionalChain([node, 'access', _177 => _177.getContentProjection, 'optionalCall', _178 => _178()]);
6399
+ return !!_optionalChain([projection, 'optionalAccess', _179 => _179.text]) && projection.selectable !== false;
6010
6400
  }
6011
6401
  /**
6012
6402
  * Keep `entity`'s a11y shadow node projected while it has
@@ -6101,20 +6491,20 @@ var Scene = (_class15 = class _Scene {
6101
6491
  el.style.background = "transparent";
6102
6492
  }
6103
6493
  el.addEventListener("click", (e) => {
6104
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("click", node, e));
6494
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("click", node, e));
6105
6495
  });
6106
6496
  el.addEventListener("dblclick", (e) => {
6107
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("dblclick", node, e));
6497
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("dblclick", node, e));
6108
6498
  });
6109
6499
  el.addEventListener("mouseenter", (e) => {
6110
6500
  if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.2)";
6111
6501
  this.hoveredA11yElements.add(el);
6112
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("hover", node, e, false));
6502
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("hover", node, e, false));
6113
6503
  });
6114
6504
  el.addEventListener("mouseleave", (e) => {
6115
6505
  if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.05)";
6116
6506
  this.hoveredA11yElements.delete(el);
6117
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("pointerleave", node, e, false));
6507
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("pointerleave", node, e, false));
6118
6508
  });
6119
6509
  const capEl = el;
6120
6510
  const releasePointer = (event) => {
@@ -6132,24 +6522,27 @@ var Scene = (_class15 = class _Scene {
6132
6522
  if (e.target === capEl && typeof capEl.setPointerCapture === "function") {
6133
6523
  capEl.setPointerCapture(e.pointerId);
6134
6524
  }
6135
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("pointerdown", node, e));
6525
+ this.pinProjectionForGesture(node);
6526
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("pointerdown", node, e));
6136
6527
  });
6137
6528
  el.addEventListener("pointerup", (e) => {
6138
6529
  releasePointer(e);
6139
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("pointerup", node, e));
6530
+ this.unpinProjectionForGesture(node);
6531
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("pointerup", node, e));
6140
6532
  });
6141
6533
  el.addEventListener("pointercancel", (e) => {
6142
6534
  releasePointer(e);
6143
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("pointercancel", node, e));
6535
+ this.unpinProjectionForGesture(node);
6536
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("pointercancel", node, e));
6144
6537
  });
6145
6538
  el.addEventListener(
6146
6539
  "pointermove",
6147
- (e) => node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("pointermove", node, e))
6540
+ (e) => node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("pointermove", node, e))
6148
6541
  );
6149
6542
  el.addEventListener(
6150
6543
  "wheel",
6151
6544
  (e) => {
6152
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("wheel", node, e));
6545
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("wheel", node, e));
6153
6546
  },
6154
6547
  { passive: false }
6155
6548
  );
@@ -6174,10 +6567,10 @@ var Scene = (_class15 = class _Scene {
6174
6567
  );
6175
6568
  emitInitialScroll = emitScroll;
6176
6569
  el.addEventListener("keydown", (e) => {
6177
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("keydown", node, e));
6570
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("keydown", node, e));
6178
6571
  });
6179
6572
  el.addEventListener("keyup", (e) => {
6180
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("keyup", node, e));
6573
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("keyup", node, e));
6181
6574
  });
6182
6575
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
6183
6576
  const input = el;
@@ -6208,7 +6601,7 @@ var Scene = (_class15 = class _Scene {
6208
6601
  el.addEventListener("compositionupdate", (e) => {
6209
6602
  const data = _nullishCoalesce(e.data, () => ( ""));
6210
6603
  composition = {
6211
- start: _nullishCoalesce(_optionalChain([composition, 'optionalAccess', _177 => _177.start]), () => ( 0)),
6604
+ start: _nullishCoalesce(_optionalChain([composition, 'optionalAccess', _180 => _180.start]), () => ( 0)),
6212
6605
  length: data.length
6213
6606
  };
6214
6607
  forward();
@@ -6262,14 +6655,14 @@ var Scene = (_class15 = class _Scene {
6262
6655
  el.addEventListener("keydown", (e) => {
6263
6656
  if (e.key === "Enter") {
6264
6657
  e.preventDefault();
6265
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("click", node, e));
6658
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("click", node, e));
6266
6659
  } else if (e.key === " ") {
6267
6660
  e.preventDefault();
6268
6661
  }
6269
6662
  });
6270
6663
  el.addEventListener("keyup", (e) => {
6271
6664
  if (e.key === " ") {
6272
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("click", node, e));
6665
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("click", node, e));
6273
6666
  }
6274
6667
  });
6275
6668
  this.keyboardActivatedA11y.add(el);
@@ -6459,7 +6852,7 @@ var Scene = (_class15 = class _Scene {
6459
6852
  const display = visible ? "" : "none";
6460
6853
  if (el.style.display !== display) el.style.display = display;
6461
6854
  }
6462
- _optionalChain([emitInitialScroll, 'optionalCall', _178 => _178()]);
6855
+ _optionalChain([emitInitialScroll, 'optionalCall', _181 => _181()]);
6463
6856
  }
6464
6857
  if (this.phases.enabled) {
6465
6858
  const projectionStart = performance.now();
@@ -6689,7 +7082,7 @@ var Scene = (_class15 = class _Scene {
6689
7082
  el.addEventListener(
6690
7083
  "wheel",
6691
7084
  (e2) => {
6692
- node.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)("wheel", node, e2));
7085
+ node.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)("wheel", node, e2));
6693
7086
  },
6694
7087
  { passive: false }
6695
7088
  );
@@ -6920,8 +7313,8 @@ var Scene = (_class15 = class _Scene {
6920
7313
  next.f = f2;
6921
7314
  next.tier = tier;
6922
7315
  next.hasBand = lineBand !== null;
6923
- next.bandMin = _nullishCoalesce(_optionalChain([lineBand, 'optionalAccess', _179 => _179.minY]), () => ( 0));
6924
- next.bandMax = _nullishCoalesce(_optionalChain([lineBand, 'optionalAccess', _180 => _180.maxY]), () => ( 0));
7316
+ next.bandMin = _nullishCoalesce(_optionalChain([lineBand, 'optionalAccess', _182 => _182.minY]), () => ( 0));
7317
+ next.bandMax = _nullishCoalesce(_optionalChain([lineBand, 'optionalAccess', _183 => _183.maxY]), () => ( 0));
6925
7318
  next.visible = visible;
6926
7319
  next.width = node.width;
6927
7320
  next.height = node.height;
@@ -7100,6 +7493,197 @@ var Scene = (_class15 = class _Scene {
7100
7493
  this.activePortalsPrevFrame = new Set(this.activePortalsThisFrame);
7101
7494
  this.activePortalsThisFrame.clear();
7102
7495
  }
7496
+ /**
7497
+ * Register a generic projection backend (RFC1 §5, RFC2 P1 CTX-0598).
7498
+ * Registration is idempotent per backend instance. Core drives
7499
+ * mount/update/unmount from the render walk; the backend owns all
7500
+ * medium-specific state.
7501
+ */
7502
+ addProjectionBackend(backend) {
7503
+ if (!this.projectionBackends.includes(backend)) this.projectionBackends.push(backend);
7504
+ return this;
7505
+ }
7506
+ /**
7507
+ * Remove a previously registered projection backend by instance or
7508
+ * {@link ProjectionBackendKind | kind}. Removing does not unmount resident
7509
+ * nodes — unmount the backend first if teardown order matters.
7510
+ */
7511
+ removeProjectionBackend(backend) {
7512
+ this.projectionBackends = this.projectionBackends.filter(
7513
+ (b) => typeof backend === "string" ? b.kind !== backend : b !== backend
7514
+ );
7515
+ return this;
7516
+ }
7517
+ /**
7518
+ * Scene-level projection capabilities (RFC4 §3 rule 2, CTX-0601).
7519
+ *
7520
+ * Plain-data feature detection in the shape of the proposed
7521
+ * `scene.inputCapabilities` (input-dispatch-contract-v2 §4): apps and
7522
+ * devtools query where a node _would_ materialize and why, without touching
7523
+ * any backend. Additive only — no existing surface moves.
7524
+ */
7525
+ get projectionCapabilities() {
7526
+ return {
7527
+ hasDOM: typeof document !== "undefined",
7528
+ domBackendMounted: this.projectionBackends.some((b) => b.kind === "dom"),
7529
+ backends: this.projectionBackends.map((b) => b.kind),
7530
+ autoHysteresisFrames: this.projectionHysteresisFrames,
7531
+ autoDomBudget: this.projectionAutoDomBudget
7532
+ };
7533
+ }
7534
+ /**
7535
+ * Register (or replace) the Capability Matrix row for one `domKind`
7536
+ * (RFC4 §4, CTX-0601). Custom kinds start at the unknown-kind default
7537
+ * (`unsupported-kind` → canvas); registering a row is how a custom backend
7538
+ * kind opts into `'auto'` resolution and how tests drive oscillating costs.
7539
+ */
7540
+ registerProjectionCapability(domKind, row) {
7541
+ this.projectionCapabilityOverrides.set(domKind, row);
7542
+ return this;
7543
+ }
7544
+ /** Matrix row for a node's `domKind` (override wins, unknown → canvas). */
7545
+ getProjectionCapability(node) {
7546
+ return _nullishCoalesce(_nullishCoalesce(this.projectionCapabilityOverrides.get(node.domKind), () => ( DEFAULT_PROJECTION_CAPABILITIES[node.domKind])), () => ( UNKNOWN_PROJECTION_CAPABILITY));
7547
+ }
7548
+ /**
7549
+ * Pin a node's backend for the duration of an active gesture (RFC4 §5 +
7550
+ * input-dispatch-contract-v2 §4 gesture stickiness: no mid-gesture handoff).
7551
+ * The a11y-mirror pointerdown/up/cancel listeners maintain this; DOM-side
7552
+ * gestures are consulted through
7553
+ * {@link ProjectionBackend.hasActiveGesture | backend.hasActiveGesture}.
7554
+ * Refcounted so multi-pointer gestures on one node unpin exactly once.
7555
+ */
7556
+ pinProjectionForGesture(node) {
7557
+ this.projectionGesturePins.set(node.id, (_nullishCoalesce(this.projectionGesturePins.get(node.id), () => ( 0))) + 1);
7558
+ }
7559
+ /** Release one gesture pin taken by {@link pinProjectionForGesture}. */
7560
+ unpinProjectionForGesture(node) {
7561
+ const count = (_nullishCoalesce(this.projectionGesturePins.get(node.id), () => ( 0))) - 1;
7562
+ if (count <= 0) this.projectionGesturePins.delete(node.id);
7563
+ else this.projectionGesturePins.set(node.id, count);
7564
+ }
7565
+ /** Whether `node` currently owns an active gesture on a core mirror. */
7566
+ isProjectionPinned(node) {
7567
+ return (_nullishCoalesce(this.projectionGesturePins.get(node.id), () => ( 0))) > 0;
7568
+ }
7569
+ /**
7570
+ * Last negotiation for one node, if it was ever resolved on this scene.
7571
+ * Accepts the node or its id; returns the stored record (not a copy — treat
7572
+ * as read-only).
7573
+ */
7574
+ getProjectionResolution(node) {
7575
+ return this.projectionResolutions.get(typeof node === "string" ? node : node.id);
7576
+ }
7577
+ /** Every node's last negotiation, in first-resolution order. */
7578
+ getProjectionResolutions() {
7579
+ return [...this.projectionResolutions.values()];
7580
+ }
7581
+ /**
7582
+ * Resolve one node's `domPolicy` to its effective backend (RFC4 §3, CTX-0601).
7583
+ *
7584
+ * Memoized per main frame (`currentFrame`, bumped once per authoritative
7585
+ * render): the render walk and the later a11y sync must agree within a
7586
+ * frame, or a node could lose its mirror the same frame it gains an element.
7587
+ * `'auto'` applies, in order: no-DOM short-circuit → pure negotiation →
7588
+ * gesture/focus hard bars (a pinned node keeps its current backend, never
7589
+ * flips mid-gesture, focus is preserved-or-moved never dropped to `body` per
7590
+ * input-dispatch-contract-v2 §4) → hysteresis stickiness → bulk budget.
7591
+ * Every outcome is recorded on the per-scene queryable surface
7592
+ * ({@link getProjectionResolutions}) with its reason — fallbacks are
7593
+ * reported, not silent (§3 rule 2).
7594
+ */
7595
+ resolveProjectionFor(node) {
7596
+ const want = node.domPolicy;
7597
+ if (this.projectionBackends.length === 0 && want === "canvas") return "canvas";
7598
+ const memo = this.projectionHysteresis.get(node.id);
7599
+ if (memo && memo.lastFrame === this.currentFrame) return memo.resolved;
7600
+ const hasDOM = typeof document !== "undefined";
7601
+ const domBackendMounted = this.projectionBackends.some((b) => b.kind === "dom");
7602
+ const outcome = resolveProjection(want, this.getProjectionCapability(node), {
7603
+ hasDOM,
7604
+ domBackendMounted
7605
+ });
7606
+ let resolved = outcome.resolved;
7607
+ let reason = outcome.reason;
7608
+ if (want === "auto" && memo !== void 0) {
7609
+ if (resolved !== memo.resolved) {
7610
+ const gesturePinned = this.isProjectionPinned(node) || this.projectionBackends.some((b) => _optionalChain([b, 'access', _184 => _184.hasActiveGesture, 'optionalCall', _185 => _185(node)]) === true);
7611
+ if (gesturePinned) {
7612
+ resolved = memo.resolved;
7613
+ reason = "active-gesture";
7614
+ } else if (this.ownsProjectionFocus(node)) {
7615
+ resolved = memo.resolved;
7616
+ reason = "focus-pinned";
7617
+ } else {
7618
+ const vote = hysteresisVote(
7619
+ memo.resolved,
7620
+ resolved,
7621
+ memo.consecutive,
7622
+ this.projectionHysteresisFrames
7623
+ );
7624
+ resolved = vote.resolved;
7625
+ memo.consecutive = vote.consecutive;
7626
+ reason = vote.flipped ? reason : "hysteresis";
7627
+ }
7628
+ } else {
7629
+ memo.consecutive = 0;
7630
+ }
7631
+ }
7632
+ if (want === "auto" && resolved === "dom") {
7633
+ if (this.projectionBudgetFrame !== this.currentFrame) {
7634
+ this.projectionBudgetFrame = this.currentFrame;
7635
+ this.projectionDomAutoCount = 0;
7636
+ }
7637
+ this.projectionDomAutoCount += 1;
7638
+ if (this.projectionDomAutoCount > this.projectionAutoDomBudget) {
7639
+ resolved = "canvas";
7640
+ reason = "bulk-budget";
7641
+ }
7642
+ }
7643
+ this.projectionHysteresis.set(node.id, {
7644
+ resolved,
7645
+ consecutive: _nullishCoalesce(_optionalChain([memo, 'optionalAccess', _186 => _186.consecutive]), () => ( 0)),
7646
+ lastFrame: this.currentFrame
7647
+ });
7648
+ const prev = this.projectionResolutions.get(node.id);
7649
+ if (!prev || prev.want !== want || prev.resolved !== resolved || prev.reason !== reason) {
7650
+ this.projectionResolutions.set(node.id, { nodeId: node.id, want, resolved, reason });
7651
+ }
7652
+ return resolved;
7653
+ }
7654
+ /**
7655
+ * Whether `node`'s a11y mirror currently owns browser focus (RFC4 §5 focus
7656
+ * rule, input-dispatch-contract-v2 §4: focus stays DOM-based; flipping
7657
+ * `projection` must preserve or deliberately move focus, never drop it to
7658
+ * `body`). A focused node keeps its backend until blur — the actual move, if
7659
+ * any, goes through the existing sentinels (`preserveFocusOnRemoval`, the
7660
+ * DOM backend's own fallback), never a bare removal.
7661
+ */
7662
+ ownsProjectionFocus(node) {
7663
+ if (typeof document === "undefined") return false;
7664
+ const mirror = this.a11yElements.get(node.id);
7665
+ if (!mirror || mirror !== this.focusedA11yElement) return false;
7666
+ return document.activeElement === mirror;
7667
+ }
7668
+ /**
7669
+ * RFC2 P1 (CTX-0598): unmount DOM-projection residents the walk did not see
7670
+ * this frame (viewport-culled, removed without `remove()`, policy-flipped).
7671
+ * Mirrors {@link reconcilePortals} above; unmount is idempotent per the
7672
+ * `ProjectionBackend` contract so notifying every backend is safe.
7673
+ */
7674
+ pruneProjectionBackends() {
7675
+ for (const id of this.domSeenPrevFrame) {
7676
+ if (!this.domSeenThisFrame.has(id)) {
7677
+ const node = this.domSeenNodes.get(id);
7678
+ if (node) {
7679
+ for (const backend of this.projectionBackends) backend.unmount(node);
7680
+ this.domSeenNodes.delete(id);
7681
+ }
7682
+ }
7683
+ }
7684
+ this.domSeenPrevFrame = new Set(this.domSeenThisFrame);
7685
+ this.domSeenThisFrame.clear();
7686
+ }
7103
7687
  // --- domain: render-scheduler — the loop and the render walk ---
7104
7688
  /**
7105
7689
  * The frame-rate cap actually in effect: the explicit {@link maxFPS}, further
@@ -7107,7 +7691,7 @@ var Scene = (_class15 = class _Scene {
7107
7691
  * (and {@link respectReducedMotion} is on). `0` means uncapped.
7108
7692
  */
7109
7693
  effectiveMaxFPS() {
7110
- const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _181 => _181.reducedMotionQuery, 'optionalAccess', _182 => _182.matches]);
7694
+ const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _187 => _187.reducedMotionQuery, 'optionalAccess', _188 => _188.matches]);
7111
7695
  if (reduced)
7112
7696
  return this.maxFPS > 0 ? Math.min(this.maxFPS, REDUCED_MOTION_FPS) : REDUCED_MOTION_FPS;
7113
7697
  return this.maxFPS;
@@ -7177,6 +7761,13 @@ var Scene = (_class15 = class _Scene {
7177
7761
  /**
7178
7762
  * Render the entire scene graph onto the specified renderer.
7179
7763
  *
7764
+ * This is the `CanvasProjection` row of {@link ProjectionBackend}:
7765
+ * the per-frame materialization of scene semantics into pixels through the
7766
+ * backend-agnostic `IRenderer` contract. (The `A11yProjection` /
7767
+ * `ContentProjection` rows run on the same cadence via {@link Scene.syncA11y}
7768
+ * below; the `DOMProjection` row lives in `@vectojs/dom` and is driven from
7769
+ * the render walk further below.)
7770
+ *
7180
7771
  * Main-frame causal order is a correctness contract:
7181
7772
  *
7182
7773
  * 1. Browser/input callbacks finish before the scheduled frame begins.
@@ -7202,7 +7793,7 @@ var Scene = (_class15 = class _Scene {
7202
7793
  * @param time - Current absolute time in milliseconds (default 0).
7203
7794
  */
7204
7795
  render(renderer, dt = 0, time = 0) {
7205
- if (_optionalChain([renderer, 'access', _183 => _183.isContextLost, 'optionalCall', _184 => _184()])) return;
7796
+ if (_optionalChain([renderer, 'access', _189 => _189.isContextLost, 'optionalCall', _190 => _190()])) return;
7206
7797
  const isMainRenderer = renderer === this.renderer;
7207
7798
  if (isMainRenderer && this.a11yRoot && this.canvas.parentElement) {
7208
7799
  const parentStyle = this.canvas.parentElement.style;
@@ -7352,14 +7943,14 @@ var Scene = (_class15 = class _Scene {
7352
7943
  }
7353
7944
  renderer.clear();
7354
7945
  if (isMainRenderer) {
7355
- _optionalChain([this, 'access', _185 => _185.pointRenderer, 'optionalAccess', _186 => _186.begin, 'call', _187 => _187()]);
7946
+ _optionalChain([this, 'access', _191 => _191.pointRenderer, 'optionalAccess', _192 => _192.begin, 'call', _193 => _193()]);
7356
7947
  }
7357
7948
  const vw = this.width;
7358
7949
  const vh = this.height;
7359
7950
  let walkHadAnimation = false;
7360
7951
  let walkHadInteractive = false;
7361
7952
  const runUpdate = (node) => {
7362
- const overridesUpdate = node.update !== _chunkPOPXUPKRjs.Entity.prototype.update;
7953
+ const overridesUpdate = node.update !== _chunkHC5EZOZJjs.Entity.prototype.update;
7363
7954
  let pending = node.hasPendingAnimations();
7364
7955
  if (pending || overridesUpdate) {
7365
7956
  node.update(dt, time);
@@ -7368,7 +7959,7 @@ var Scene = (_class15 = class _Scene {
7368
7959
  if (pending) walkHadAnimation = true;
7369
7960
  if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
7370
7961
  if (this._devActive && this._devFrameCount % 120 === 0) {
7371
- if (overridesUpdate && node.hasPendingAnimations === _chunkPOPXUPKRjs.Entity.prototype.hasPendingAnimations) {
7962
+ if (overridesUpdate && node.hasPendingAnimations === _chunkHC5EZOZJjs.Entity.prototype.hasPendingAnimations) {
7372
7963
  this._devWarn(
7373
7964
  `Entity "${node.id}" overrides update() but not hasPendingAnimations(). Custom motion in update() without overriding hasPendingAnimations() causes the idle throttle to slow the animation. Override hasPendingAnimations() to return true while motion is in flight.`
7374
7965
  );
@@ -7469,6 +8060,16 @@ var Scene = (_class15 = class _Scene {
7469
8060
  visible = maxX >= 0 && minX <= vw && maxY >= 0 && minY <= vh;
7470
8061
  }
7471
8062
  if (!visible && node.children.length === 0) return;
8063
+ if (isMainRenderer && this.projectionBackends.length > 0 && this.resolveProjectionFor(node) === "dom" && visible) {
8064
+ for (const backend of this.projectionBackends) {
8065
+ backend.update(node, { a, b, c, d, e: te, f: tf });
8066
+ }
8067
+ if (node.domResident) {
8068
+ this.domSeenThisFrame.add(node.id);
8069
+ this.domSeenNodes.set(node.id, node);
8070
+ if (node.children.length === 0) return;
8071
+ }
8072
+ }
7472
8073
  if (node.children.length === 0 && node.scaleX === node.scaleY) {
7473
8074
  const bc = node.getBatchCircle();
7474
8075
  if (bc) {
@@ -7598,14 +8199,15 @@ var Scene = (_class15 = class _Scene {
7598
8199
  this.frameHadAnimation = walkHadAnimation;
7599
8200
  this.frameHadInteractive = walkHadInteractive;
7600
8201
  this.reconcilePortals();
8202
+ this.pruneProjectionBackends();
7601
8203
  }
7602
8204
  const flushTiming = this.phases.userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.flush) : null;
7603
8205
  const flushT0 = this.phases.enabled ? performance.now() : 0;
7604
8206
  renderer.flush();
7605
8207
  if (isMainRenderer) {
7606
- _optionalChain([this, 'access', _188 => _188.pointRenderer, 'optionalAccess', _189 => _189.flush, 'call', _190 => _190()]);
8208
+ _optionalChain([this, 'access', _194 => _194.pointRenderer, 'optionalAccess', _195 => _195.flush, 'call', _196 => _196()]);
7607
8209
  }
7608
- _optionalChain([renderer, 'access', _191 => _191.present, 'optionalCall', _192 => _192()]);
8210
+ _optionalChain([renderer, 'access', _197 => _197.present, 'optionalCall', _198 => _198()]);
7609
8211
  if (this.phases.enabled) this.phases.record("flush", performance.now() - flushT0);
7610
8212
  if (flushTiming) endVectoUserTiming(flushTiming);
7611
8213
  if (this._devActive) {
@@ -7696,7 +8298,7 @@ var Scene = (_class15 = class _Scene {
7696
8298
  });
7697
8299
  pass.end();
7698
8300
  this.device.queue.submit([encoder.finish()]);
7699
- } catch (e17) {
8301
+ } catch (e19) {
7700
8302
  }
7701
8303
  this.gpuHasContent = false;
7702
8304
  }
@@ -7831,20 +8433,20 @@ function defaultMeasurer() {
7831
8433
  sharedMeasurer ??= _layout.resolveGlyphMeasurer.call(void 0, "sans-serif");
7832
8434
  return sharedMeasurer;
7833
8435
  }
7834
- var TextEntity = (_class16 = class extends _chunkPOPXUPKRjs.Entity {
8436
+ var TextEntity = (_class16 = class extends _chunkHC5EZOZJjs.Entity {
7835
8437
 
7836
8438
 
7837
8439
 
7838
8440
 
7839
- __init191() {this.nodes = []}
8441
+ __init204() {this.nodes = []}
7840
8442
 
7841
- __init192() {this.fillStyle = "#94a3b8"}
7842
- __init193() {this.strokeStyle = null}
7843
- __init194() {this.hoveredFillStyle = "#ffffff"}
7844
- __init195() {this.lineWidth = 1}
7845
- __init196() {this.isHovered = false}
8443
+ __init205() {this.fillStyle = "#94a3b8"}
8444
+ __init206() {this.strokeStyle = null}
8445
+ __init207() {this.hoveredFillStyle = "#ffffff"}
8446
+ __init208() {this.lineWidth = 1}
8447
+ __init209() {this.isHovered = false}
7846
8448
  /** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
7847
- __init197() {this.contentEpoch = 0}
8449
+ __init210() {this.contentEpoch = 0}
7848
8450
  /**
7849
8451
  * Visual lines with real canvas geometry, rebuilt by {@link applyLayout}
7850
8452
  * exactly when `this.nodes` changes.
@@ -7856,9 +8458,9 @@ var TextEntity = (_class16 = class extends _chunkPOPXUPKRjs.Entity {
7856
8458
  * ~0.35 em per line for a 24 px font, Firefox). Per-line carriers pin the DOM
7857
8459
  * baseline to the canvas baseline by construction, same as `ui/Text`.
7858
8460
  */
7859
- __init198() {this.projectionLines = []}
8461
+ __init211() {this.projectionLines = []}
7860
8462
  constructor(text, atlas, maxWidth, fontSize = 24) {
7861
- super();_class16.prototype.__init191.call(this);_class16.prototype.__init192.call(this);_class16.prototype.__init193.call(this);_class16.prototype.__init194.call(this);_class16.prototype.__init195.call(this);_class16.prototype.__init196.call(this);_class16.prototype.__init197.call(this);_class16.prototype.__init198.call(this);;
8463
+ super();_class16.prototype.__init204.call(this);_class16.prototype.__init205.call(this);_class16.prototype.__init206.call(this);_class16.prototype.__init207.call(this);_class16.prototype.__init208.call(this);_class16.prototype.__init209.call(this);_class16.prototype.__init210.call(this);_class16.prototype.__init211.call(this);;
7862
8464
  this.text = text;
7863
8465
  this.atlas = atlas;
7864
8466
  this.fontSize = fontSize;
@@ -8034,18 +8636,250 @@ var TextEntity = (_class16 = class extends _chunkPOPXUPKRjs.Entity {
8034
8636
  }
8035
8637
  }, _class16);
8036
8638
 
8639
+ // src/components/VirtualizedSetAggregate.ts
8640
+ var VirtualizedSetAggregate = (_class17 = class extends _chunkHC5EZOZJjs.Entity {
8641
+
8642
+
8643
+
8644
+
8645
+
8646
+
8647
+
8648
+ /** First item index currently bound to the pool (the scroll window). */
8649
+ __init212() {this._visibleStart = 0}
8650
+ /** Item id owning the roving tab stop / keyboard focus. */
8651
+ __init213() {this._activeId = null}
8652
+ __init214() {this._selectedId = null}
8653
+ /** One focusable hotspot per visible slot; re-bound, never rebuilt per item. */
8654
+ __init215() {this._hotspots = []}
8655
+ constructor(opts = {}) {
8656
+ super();_class17.prototype.__init212.call(this);_class17.prototype.__init213.call(this);_class17.prototype.__init214.call(this);_class17.prototype.__init215.call(this);;
8657
+ this._items = _nullishCoalesce(opts.items, () => ( []));
8658
+ this._label = _nullishCoalesce(opts.label, () => ( "Items"));
8659
+ this._role = _nullishCoalesce(opts.role, () => ( "list"));
8660
+ this._itemRole = _nullishCoalesce(opts.itemRole, () => ( "listitem"));
8661
+ this._rowHeight = _nullishCoalesce(opts.rowHeight, () => ( 28));
8662
+ this._visibleCapacity = opts.visibleCapacity;
8663
+ this._onActivate = opts.onActivate;
8664
+ this.width = _nullishCoalesce(opts.width, () => ( 0));
8665
+ this.height = _nullishCoalesce(opts.height, () => ( 0));
8666
+ this.interactive = true;
8667
+ this._syncHotspots();
8668
+ }
8669
+ /** Full item count — the number the container label states. */
8670
+ get itemCount() {
8671
+ return this._items.length;
8672
+ }
8673
+ /** Live pool size (bounded by the visible capacity, never by the item count). */
8674
+ get poolSize() {
8675
+ return this._hotspots.length;
8676
+ }
8677
+ /** First item index currently bound to the pool. */
8678
+ get visibleStart() {
8679
+ return this._visibleStart;
8680
+ }
8681
+ getA11yAttributes() {
8682
+ return {
8683
+ role: this._role,
8684
+ label: `${this._label}, ${this._items.length} items`,
8685
+ pointerEvents: "none"
8686
+ };
8687
+ }
8688
+ render(_renderer) {
8689
+ }
8690
+ /** Axis-aligned hit-test against the container box (the `UIComponent` default). */
8691
+ isPointInside(globalX, globalY) {
8692
+ const local = this.worldToLocal(globalX, globalY);
8693
+ if (!local) return false;
8694
+ return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
8695
+ }
8696
+ /** Replace the item set; the pool re-binds and the label count follows. */
8697
+ setItems(items) {
8698
+ this._items = items;
8699
+ if (this._activeId !== null && !items.some((item) => item.id === this._activeId)) {
8700
+ this._activeId = null;
8701
+ }
8702
+ if (this._selectedId !== null && !items.some((item) => item.id === this._selectedId)) {
8703
+ this._selectedId = null;
8704
+ }
8705
+ this._visibleStart = Math.max(0, Math.min(this._visibleStart, Math.max(0, items.length - 1)));
8706
+ this._syncHotspots();
8707
+ _optionalChain([this, 'access', _199 => _199.scene, 'optionalAccess', _200 => _200.markDirty, 'call', _201 => _201()]);
8708
+ }
8709
+ /** Move the pool window to start at `start` (the scroll position). */
8710
+ setVisibleStart(start) {
8711
+ const clamped = Math.max(0, Math.min(start, Math.max(0, this._items.length - 1)));
8712
+ if (clamped === this._visibleStart) return;
8713
+ this._visibleStart = clamped;
8714
+ this._syncHotspots();
8715
+ _optionalChain([this, 'access', _202 => _202.scene, 'optionalAccess', _203 => _203.markDirty, 'call', _204 => _204()]);
8716
+ }
8717
+ /**
8718
+ * Keep one hotspot per visible slot, positioned over it. The pool is sized
8719
+ * to the viewport (capacity), each slot re-bound to whatever item currently
8720
+ * occupies it — the `Tree._syncHotspots` shape, minus tree structure.
8721
+ */
8722
+ _syncHotspots() {
8723
+ const capacity = _nullishCoalesce(this._visibleCapacity, () => ( Math.max(1, Math.ceil(this.height / this._rowHeight))));
8724
+ const need = Math.max(0, Math.min(capacity, this._items.length - this._visibleStart));
8725
+ while (this._hotspots.length < need) {
8726
+ const hotspot = new AggregateItemHotspot(this);
8727
+ this._hotspots.push(hotspot);
8728
+ this.add(hotspot);
8729
+ }
8730
+ while (this._hotspots.length > need) {
8731
+ const hotspot = this._hotspots.pop();
8732
+ _optionalChain([this, 'access', _205 => _205.scene, 'optionalAccess', _206 => _206.detachA11y, 'call', _207 => _207(hotspot)]);
8733
+ this.remove(hotspot);
8734
+ }
8735
+ for (let slot = 0; slot < need; slot++) {
8736
+ const index = this._visibleStart + slot;
8737
+ const hotspot = this._hotspots[slot];
8738
+ hotspot.bind(index, this._items[index]);
8739
+ hotspot.x = 0;
8740
+ hotspot.y = slot * this._rowHeight;
8741
+ hotspot.width = this.width;
8742
+ hotspot.height = this._rowHeight;
8743
+ }
8744
+ }
8745
+ /** Whether `id` owns the roving tab stop: active, else selected, else first. */
8746
+ isTabStop(id) {
8747
+ const anchor = _nullishCoalesce(_nullishCoalesce((this._activeId !== null && this._items.some((item) => item.id === this._activeId) ? this._activeId : null), () => ( (this._selectedId !== null && this._items.some((item) => item.id === this._selectedId) ? this._selectedId : null))), () => ( _optionalChain([this, 'access', _208 => _208._items, 'access', _209 => _209[0], 'optionalAccess', _210 => _210.id])));
8748
+ return id === anchor;
8749
+ }
8750
+ isSelected(id) {
8751
+ return this._selectedId === id;
8752
+ }
8753
+ /** Item role the hotspots announce (kept on the parent: one source of truth). */
8754
+ get itemRole() {
8755
+ return this._itemRole;
8756
+ }
8757
+ /** Activate an item (pointer tap-through or Enter/Space): selects and notifies. */
8758
+ activateItem(id, focusIt = false) {
8759
+ const index = this._items.findIndex((item) => item.id === id);
8760
+ if (index === -1) return;
8761
+ this._activeId = id;
8762
+ this._selectedId = id;
8763
+ _optionalChain([this, 'access', _211 => _211._onActivate, 'optionalCall', _212 => _212(this._items[index], index)]);
8764
+ if (focusIt) this.focusItem(id);
8765
+ _optionalChain([this, 'access', _213 => _213.scene, 'optionalAccess', _214 => _214.markDirty, 'call', _215 => _215()]);
8766
+ }
8767
+ /** Move keyboard focus to the hotspot currently bound to `id`, if any. */
8768
+ focusItem(id) {
8769
+ _optionalChain([this, 'access', _216 => _216._hotspots, 'access', _217 => _217.find, 'call', _218 => _218((hotspot) => hotspot.itemId === id), 'optionalAccess', _219 => _219.focus, 'call', _220 => _220()]);
8770
+ }
8771
+ /**
8772
+ * Aggregate keyboard model: Up/Down move the active item (scrolling the pool
8773
+ * window so it stays bound), Home/End jump, Enter/Space activate. The active
8774
+ * item is focused as it moves so focus is never stranded on a re-bound slot.
8775
+ */
8776
+ handleItemKey(e, id) {
8777
+ const index = this._items.findIndex((item) => item.id === id);
8778
+ if (index === -1) return;
8779
+ let next = -1;
8780
+ switch (e.key) {
8781
+ case "ArrowDown":
8782
+ next = Math.min(index + 1, this._items.length - 1);
8783
+ break;
8784
+ case "ArrowUp":
8785
+ next = Math.max(index - 1, 0);
8786
+ break;
8787
+ case "Home":
8788
+ next = 0;
8789
+ break;
8790
+ case "End":
8791
+ next = this._items.length - 1;
8792
+ break;
8793
+ case "Enter":
8794
+ case " ":
8795
+ e.preventDefault();
8796
+ this.activateItem(id, true);
8797
+ return;
8798
+ default:
8799
+ return;
8800
+ }
8801
+ e.preventDefault();
8802
+ this._activeId = this._items[next].id;
8803
+ this._ensureVisible(next);
8804
+ this.focusItem(this._activeId);
8805
+ _optionalChain([this, 'access', _221 => _221.scene, 'optionalAccess', _222 => _222.markDirty, 'call', _223 => _223()]);
8806
+ }
8807
+ /** Scroll the pool window just enough to keep `index` bound. */
8808
+ _ensureVisible(index) {
8809
+ const capacity = _nullishCoalesce(this._visibleCapacity, () => ( Math.max(1, Math.ceil(this.height / this._rowHeight))));
8810
+ if (index < this._visibleStart) this.setVisibleStart(index);
8811
+ else if (index >= this._visibleStart + capacity) {
8812
+ this.setVisibleStart(index - capacity + 1);
8813
+ } else {
8814
+ this._syncHotspots();
8815
+ }
8816
+ }
8817
+ update(_dt, _time) {
8818
+ super.update(_dt, _time);
8819
+ this._syncHotspots();
8820
+ }
8821
+ }, _class17);
8822
+ var AggregateItemHotspot = (_class18 = class extends _chunkHC5EZOZJjs.Entity {
8823
+ constructor(aggregate) {
8824
+ super();_class18.prototype.__init216.call(this);_class18.prototype.__init217.call(this);_class18.prototype.__init218.call(this);;
8825
+ this.aggregate = aggregate;
8826
+ this.interactive = true;
8827
+ this.on("click", () => this.aggregate.activateItem(this.itemId, true));
8828
+ this.on("keydown", (e) => this.aggregate.handleItemKey(e, this.itemId));
8829
+ }
8830
+
8831
+ __init216() {this.itemId = ""}
8832
+ __init217() {this._index = 0}
8833
+ __init218() {this._label = ""}
8834
+ bind(index, item) {
8835
+ this._index = index;
8836
+ this.itemId = item.id;
8837
+ this._label = item.label;
8838
+ }
8839
+ /** The aggregate positions and sizes one pooled hotspot per visible slot. */
8840
+ getLayoutControlledProperties() {
8841
+ return ["x", "y", "width", "height"];
8842
+ }
8843
+ getA11yAttributes() {
8844
+ return {
8845
+ role: this.aggregate.itemRole,
8846
+ label: this._label,
8847
+ // 1-based position within the FULL set, not the pool: without these a
8848
+ // virtualized window announces the pool position as the set position.
8849
+ posInSet: this._index + 1,
8850
+ setSize: this.aggregate.itemCount,
8851
+ selected: this.aggregate.isSelected(this.itemId),
8852
+ // Roving tabindex: only the active item is a tab stop; arrows move within.
8853
+ tabIndex: this.aggregate.isTabStop(this.itemId) ? 0 : -1,
8854
+ // The owner keeps the pointer (canvas handling or an underlying
8855
+ // selectable-text mirror); this hotspot exists for semantics + keyboard
8856
+ // focus, so it opts out of hit-testing. Keyboard focus and
8857
+ // AT-synthesized `click` still work under `pointer-events:none`.
8858
+ pointerEvents: "none"
8859
+ };
8860
+ }
8861
+ render() {
8862
+ }
8863
+ /** Axis-aligned hit-test against the hotspot slot (the `UIComponent` default). */
8864
+ isPointInside(globalX, globalY) {
8865
+ const local = this.worldToLocal(globalX, globalY);
8866
+ if (!local) return false;
8867
+ return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
8868
+ }
8869
+ }, _class18);
8870
+
8037
8871
  // src/components/GridTextEntity.ts
8038
- var GridTextEntity = (_class17 = class extends _chunkPOPXUPKRjs.Entity {
8872
+ var GridTextEntity = (_class19 = class extends _chunkHC5EZOZJjs.Entity {
8039
8873
 
8040
- __init199() {this.fillStyle = "#ffffff"}
8041
- __init200() {this.grid = []}
8874
+ __init219() {this.fillStyle = "#ffffff"}
8875
+ __init220() {this.grid = []}
8042
8876
  // Array of rows
8043
- __init201() {this.cols = 0}
8044
- __init202() {this.rows = 0}
8877
+ __init221() {this.cols = 0}
8878
+ __init222() {this.rows = 0}
8045
8879
 
8046
8880
 
8047
8881
  constructor(_atlas, fontSize = 10) {
8048
- super();_class17.prototype.__init199.call(this);_class17.prototype.__init200.call(this);_class17.prototype.__init201.call(this);_class17.prototype.__init202.call(this);;
8882
+ super();_class19.prototype.__init219.call(this);_class19.prototype.__init220.call(this);_class19.prototype.__init221.call(this);_class19.prototype.__init222.call(this);;
8049
8883
  this.fontSize = fontSize;
8050
8884
  this.charWidth = fontSize * 1;
8051
8885
  this.charHeight = fontSize * 1.1;
@@ -8054,7 +8888,7 @@ var GridTextEntity = (_class17 = class extends _chunkPOPXUPKRjs.Entity {
8054
8888
  updateGrid(ascii) {
8055
8889
  this.grid = ascii;
8056
8890
  this.rows = ascii.length;
8057
- this.cols = ascii.reduce((widest, row) => Math.max(widest, _nullishCoalesce(_optionalChain([row, 'optionalAccess', _193 => _193.length]), () => ( 0))), 0);
8891
+ this.cols = ascii.reduce((widest, row) => Math.max(widest, _nullishCoalesce(_optionalChain([row, 'optionalAccess', _224 => _224.length]), () => ( 0))), 0);
8058
8892
  }
8059
8893
  isPointInside(_globalX, _globalY) {
8060
8894
  return false;
@@ -8076,7 +8910,7 @@ var GridTextEntity = (_class17 = class extends _chunkPOPXUPKRjs.Entity {
8076
8910
  }
8077
8911
  }
8078
8912
  }
8079
- }, _class17);
8913
+ }, _class19);
8080
8914
 
8081
8915
  // src/components/SplineEntity.ts
8082
8916
  function polySegmentToBezier(seg) {
@@ -8129,7 +8963,7 @@ function distSqToSegment(px, py, x1, y1, x2, y2) {
8129
8963
  const ey = py - cy;
8130
8964
  return ex * ex + ey * ey;
8131
8965
  }
8132
- var SplineEntity = (_class18 = class extends _chunkPOPXUPKRjs.Entity {
8966
+ var SplineEntity = (_class20 = class extends _chunkHC5EZOZJjs.Entity {
8133
8967
 
8134
8968
 
8135
8969
 
@@ -8137,17 +8971,17 @@ var SplineEntity = (_class18 = class extends _chunkPOPXUPKRjs.Entity {
8137
8971
 
8138
8972
 
8139
8973
 
8140
- __init203() {this.offscreen = null}
8141
- __init204() {this.baked = false}
8974
+ __init223() {this.offscreen = null}
8975
+ __init224() {this.baked = false}
8142
8976
  /** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
8143
- __init205() {this.bakedWidth = 0}
8144
- __init206() {this.bakedHeight = 0}
8977
+ __init225() {this.bakedWidth = 0}
8978
+ __init226() {this.bakedHeight = 0}
8145
8979
  /**
8146
8980
  * Device-pixel ratio the bitmap was rasterized at. Tracked so a DPR change
8147
8981
  * (browser zoom, a monitor move, a renderer whose clamp changed) re-bakes
8148
8982
  * instead of blitting a bitmap at the wrong device density forever.
8149
8983
  */
8150
- __init207() {this.bakedDPR = 0}
8984
+ __init227() {this.bakedDPR = 0}
8151
8985
  /**
8152
8986
  * Gradient strokes can't be baked to a solid-color bitmap; they render
8153
8987
  * per-frame. Derived from `_doc`, so it is NOT readonly — assigning a new
@@ -8155,7 +8989,7 @@ var SplineEntity = (_class18 = class extends _chunkPOPXUPKRjs.Entity {
8155
8989
  */
8156
8990
 
8157
8991
  /** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
8158
- __init208() {this.polylines = null}
8992
+ __init228() {this.polylines = null}
8159
8993
  /**
8160
8994
  * The spline document to render. Assigning a new document invalidates all
8161
8995
  * caches (baked canvas, flattened polylines, bounds).
@@ -8189,9 +9023,9 @@ var SplineEntity = (_class18 = class extends _chunkPOPXUPKRjs.Entity {
8189
9023
  * local bounds after painting the curves. Useful for drag feedback and
8190
9024
  * debugging hit areas. Defaults to `false`.
8191
9025
  */
8192
- __init209() {this.showBounds = false}
9026
+ __init229() {this.showBounds = false}
8193
9027
  constructor(doc, opts = {}) {
8194
- super();_class18.prototype.__init203.call(this);_class18.prototype.__init204.call(this);_class18.prototype.__init205.call(this);_class18.prototype.__init206.call(this);_class18.prototype.__init207.call(this);_class18.prototype.__init208.call(this);_class18.prototype.__init209.call(this);;
9028
+ super();_class20.prototype.__init223.call(this);_class20.prototype.__init224.call(this);_class20.prototype.__init225.call(this);_class20.prototype.__init226.call(this);_class20.prototype.__init227.call(this);_class20.prototype.__init228.call(this);_class20.prototype.__init229.call(this);;
8195
9029
  this._doc = doc;
8196
9030
  this._lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
8197
9031
  this.cache = _nullishCoalesce(opts.cache, () => ( true));
@@ -8211,7 +9045,7 @@ var SplineEntity = (_class18 = class extends _chunkPOPXUPKRjs.Entity {
8211
9045
  */
8212
9046
  computeContainsGradient() {
8213
9047
  const isGradient = (c) => c !== null && !Array.isArray(c);
8214
- return (_nullishCoalesce(_optionalChain([this, 'access', _194 => _194._doc, 'access', _195 => _195.equations, 'optionalAccess', _196 => _196.some, 'call', _197 => _197((eq) => isGradient(eq.color_rgb))]), () => ( false))) || (_nullishCoalesce(_optionalChain([this, 'access', _198 => _198._doc, 'access', _199 => _199.paths, 'optionalAccess', _200 => _200.some, 'call', _201 => _201((p) => isGradient(p.color_rgb))]), () => ( false)));
9048
+ return (_nullishCoalesce(_optionalChain([this, 'access', _225 => _225._doc, 'access', _226 => _226.equations, 'optionalAccess', _227 => _227.some, 'call', _228 => _228((eq) => isGradient(eq.color_rgb))]), () => ( false))) || (_nullishCoalesce(_optionalChain([this, 'access', _229 => _229._doc, 'access', _230 => _230.paths, 'optionalAccess', _231 => _231.some, 'call', _232 => _232((p) => isGradient(p.color_rgb))]), () => ( false)));
8215
9049
  }
8216
9050
  /** Clear cached baked canvas and hit-test polylines. */
8217
9051
  invalidateCache() {
@@ -8450,14 +9284,14 @@ var SplineEntity = (_class18 = class extends _chunkPOPXUPKRjs.Entity {
8450
9284
  r.stroke("rgba(0, 150, 255, 0.8)", 2);
8451
9285
  }
8452
9286
  }
8453
- }, _class18);
9287
+ }, _class20);
8454
9288
  async function loadSpline(url) {
8455
9289
  const res = await fetch(url);
8456
9290
  return await res.json();
8457
9291
  }
8458
9292
 
8459
9293
  // src/components/Rect.ts
8460
- var Rect = class extends _chunkPOPXUPKRjs.Entity {
9294
+ var Rect = class extends _chunkHC5EZOZJjs.Entity {
8461
9295
 
8462
9296
 
8463
9297
 
@@ -8513,7 +9347,7 @@ var Rect = class extends _chunkPOPXUPKRjs.Entity {
8513
9347
  };
8514
9348
 
8515
9349
  // src/components/Circle.ts
8516
- var Circle = class extends _chunkPOPXUPKRjs.Entity {
9350
+ var Circle = class extends _chunkHC5EZOZJjs.Entity {
8517
9351
 
8518
9352
 
8519
9353
 
@@ -8577,7 +9411,7 @@ var Circle = class extends _chunkPOPXUPKRjs.Entity {
8577
9411
  };
8578
9412
 
8579
9413
  // src/components/Group.ts
8580
- var Group = class extends _chunkPOPXUPKRjs.Entity {
9414
+ var Group = class extends _chunkHC5EZOZJjs.Entity {
8581
9415
  constructor(...children) {
8582
9416
  super();
8583
9417
  if (children.length > 0) this.add(...children);
@@ -8596,21 +9430,21 @@ var _math = require('@vectojs/math'); _createStarExport(_math);
8596
9430
 
8597
9431
 
8598
9432
  // src/tree/DOMPortalEntity.ts
8599
- var DOMPortalEntity = (_class19 = class extends _chunkPOPXUPKRjs.Entity {
9433
+ var DOMPortalEntity = (_class21 = class extends _chunkHC5EZOZJjs.Entity {
8600
9434
 
8601
- __init210() {this.isDOMPortal = true}
8602
- __init211() {this.domListeners = []}
8603
- __init212() {this.resizeObserver = null}
8604
- __init213() {this.domBound = false}
8605
- __init214() {this.cachedWidth = 100}
8606
- __init215() {this.cachedHeight = 100}
8607
- __init216() {this.lastWidth = ""}
8608
- __init217() {this.lastHeight = ""}
8609
- __init218() {this.lastTransform = ""}
8610
- __init219() {this.lastZIndex = ""}
8611
- __init220() {this.lastOpacity = ""}
9435
+ __init230() {this.isDOMPortal = true}
9436
+ __init231() {this.domListeners = []}
9437
+ __init232() {this.resizeObserver = null}
9438
+ __init233() {this.domBound = false}
9439
+ __init234() {this.cachedWidth = 100}
9440
+ __init235() {this.cachedHeight = 100}
9441
+ __init236() {this.lastWidth = ""}
9442
+ __init237() {this.lastHeight = ""}
9443
+ __init238() {this.lastTransform = ""}
9444
+ __init239() {this.lastZIndex = ""}
9445
+ __init240() {this.lastOpacity = ""}
8612
9446
  constructor(domElement, width, height, id) {
8613
- super(id);_class19.prototype.__init210.call(this);_class19.prototype.__init211.call(this);_class19.prototype.__init212.call(this);_class19.prototype.__init213.call(this);_class19.prototype.__init214.call(this);_class19.prototype.__init215.call(this);_class19.prototype.__init216.call(this);_class19.prototype.__init217.call(this);_class19.prototype.__init218.call(this);_class19.prototype.__init219.call(this);_class19.prototype.__init220.call(this);;
9447
+ super(id);_class21.prototype.__init230.call(this);_class21.prototype.__init231.call(this);_class21.prototype.__init232.call(this);_class21.prototype.__init233.call(this);_class21.prototype.__init234.call(this);_class21.prototype.__init235.call(this);_class21.prototype.__init236.call(this);_class21.prototype.__init237.call(this);_class21.prototype.__init238.call(this);_class21.prototype.__init239.call(this);_class21.prototype.__init240.call(this);;
8614
9448
  this.domElement = domElement;
8615
9449
  this.width = _nullishCoalesce(width, () => ( 0));
8616
9450
  this.height = _nullishCoalesce(height, () => ( 0));
@@ -8652,7 +9486,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkPOPXUPKRjs.Entity {
8652
9486
  ];
8653
9487
  for (const type of events) {
8654
9488
  const handler = (e) => {
8655
- this.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)(type, this, e));
9489
+ this.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)(type, this, e));
8656
9490
  };
8657
9491
  this.domElement.addEventListener(type, handler);
8658
9492
  this.domListeners.push({ type, handler, capture: false });
@@ -8663,7 +9497,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkPOPXUPKRjs.Entity {
8663
9497
  ];
8664
9498
  for (const { native, vecto } of hoverEvents) {
8665
9499
  const handler = (e) => {
8666
- this.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)(vecto, this, e, false));
9500
+ this.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)(vecto, this, e, false));
8667
9501
  };
8668
9502
  this.domElement.addEventListener(native, handler);
8669
9503
  this.domListeners.push({ type: native, handler, capture: false });
@@ -8671,7 +9505,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkPOPXUPKRjs.Entity {
8671
9505
  const focusEvents = ["focus", "blur"];
8672
9506
  for (const type of focusEvents) {
8673
9507
  const handler = (e) => {
8674
- this.dispatchEvent(new (0, _chunkPOPXUPKRjs.VectoJSEvent)(type, this, e, true));
9508
+ this.dispatchEvent(new (0, _chunkHC5EZOZJjs.VectoJSEvent)(type, this, e, true));
8675
9509
  };
8676
9510
  this.domElement.addEventListener(type, handler, true);
8677
9511
  this.domListeners.push({ type, handler, capture: true });
@@ -8719,7 +9553,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkPOPXUPKRjs.Entity {
8719
9553
  }
8720
9554
  super.destroy();
8721
9555
  }
8722
- }, _class19);
9556
+ }, _class21);
8723
9557
 
8724
9558
  // src/index.ts
8725
9559
  Scene.registerWebGLPointRendererCreator(_chunkBKXERWADjs.createWebGLPointRenderer);
@@ -8772,4 +9606,16 @@ Scene.registerWebGPUParticleSystemManager(_chunkBKXERWADjs.WebGPUParticleSystemM
8772
9606
 
8773
9607
 
8774
9608
 
8775
- exports.CanvasRenderer = _chunkBKXERWADjs.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DEFAULT_CONTENT_SEMANTIC_BUDGET = DEFAULT_CONTENT_SEMANTIC_BUDGET; exports.DOMPortalEntity = DOMPortalEntity; exports.Entity = _chunkPOPXUPKRjs.Entity; exports.GlyphRasterAtlas = _chunkBKXERWADjs.GlyphRasterAtlas; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.KEYBOARD_OWNING_ROLES = KEYBOARD_OWNING_ROLES; exports.MSDFTextEntity = _chunkPOPXUPKRjs.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.Rect = Rect; exports.SCENE_OPTION_KEYS = SCENE_OPTION_KEYS; exports.SVGEntity = _chunkPOPXUPKRjs.SVGEntity; exports.SVGRenderer = _chunkBKXERWADjs.SVGRenderer; exports.Scene = Scene; exports.SplineEntity = SplineEntity; exports.TextEntity = TextEntity; exports.TextRasterCache = _chunkBKXERWADjs.TextRasterCache; exports.VECTO_USER_TIMING = VECTO_USER_TIMING; exports.VectoJSEvent = _chunkPOPXUPKRjs.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkBKXERWADjs.WebGPUParticleSystemManager; exports.beginVectoUserTiming = beginVectoUserTiming; exports.contentLineInHint = _chunkPOPXUPKRjs.contentLineInHint; exports.createWebGLPointRenderer = _chunkBKXERWADjs.createWebGLPointRenderer; exports.endVectoUserTiming = endVectoUserTiming; exports.installRendererDevTraps = _chunkBKXERWADjs.installRendererDevTraps; exports.isRendererDevMode = _chunkBKXERWADjs.isRendererDevMode; exports.isSafeUrl = _chunkBKXERWADjs.isSafeUrl; exports.loadSpline = loadSpline; exports.measureVectoUserTiming = measureVectoUserTiming; exports.normalizeChord = normalizeChord; exports.ownsKeyboard = ownsKeyboard; exports.parseColorToRGBA = _chunkBKXERWADjs.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.sanitizeUrl = _chunkBKXERWADjs.sanitizeUrl; exports.setRendererDevMode = _chunkBKXERWADjs.setRendererDevMode;
9609
+
9610
+
9611
+
9612
+
9613
+
9614
+
9615
+
9616
+
9617
+
9618
+
9619
+
9620
+
9621
+ exports.CanvasRenderer = _chunkBKXERWADjs.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DEFAULT_CONTENT_SEMANTIC_BUDGET = DEFAULT_CONTENT_SEMANTIC_BUDGET; exports.DEFAULT_PROJECTION_CAPABILITIES = DEFAULT_PROJECTION_CAPABILITIES; exports.DEFAULT_SEMANTIC_PROJECTION_POLICY = DEFAULT_SEMANTIC_PROJECTION_POLICY; exports.DOMPortalEntity = DOMPortalEntity; exports.Entity = _chunkHC5EZOZJjs.Entity; exports.GlyphRasterAtlas = _chunkBKXERWADjs.GlyphRasterAtlas; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.KEYBOARD_OWNING_ROLES = KEYBOARD_OWNING_ROLES; exports.MSDFTextEntity = _chunkHC5EZOZJjs.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.PROJECTION_AUTO_DOM_BUDGET = PROJECTION_AUTO_DOM_BUDGET; exports.PROJECTION_AUTO_HYSTERESIS_FRAMES = PROJECTION_AUTO_HYSTERESIS_FRAMES; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.Rect = Rect; exports.SCENE_OPTION_KEYS = SCENE_OPTION_KEYS; exports.SVGEntity = _chunkHC5EZOZJjs.SVGEntity; exports.SVGRenderer = _chunkBKXERWADjs.SVGRenderer; exports.Scene = Scene; exports.SplineEntity = SplineEntity; exports.TextEntity = TextEntity; exports.TextRasterCache = _chunkBKXERWADjs.TextRasterCache; exports.UNKNOWN_PROJECTION_CAPABILITY = UNKNOWN_PROJECTION_CAPABILITY; exports.VECTO_USER_TIMING = VECTO_USER_TIMING; exports.VectoJSEvent = _chunkHC5EZOZJjs.VectoJSEvent; exports.VirtualizedSetAggregate = VirtualizedSetAggregate; exports.WebGPUParticleSystemManager = _chunkBKXERWADjs.WebGPUParticleSystemManager; exports.beginVectoUserTiming = beginVectoUserTiming; exports.contentLineInHint = _chunkHC5EZOZJjs.contentLineInHint; exports.createWebGLPointRenderer = _chunkBKXERWADjs.createWebGLPointRenderer; exports.describeHTMLInCanvasSupport = describeHTMLInCanvasSupport; exports.endVectoUserTiming = endVectoUserTiming; exports.getSemanticProjectionCapabilities = getSemanticProjectionCapabilities; exports.hysteresisVote = hysteresisVote; exports.installRendererDevTraps = _chunkBKXERWADjs.installRendererDevTraps; exports.isDeferrableSemanticNode = isDeferrableSemanticNode; exports.isRendererDevMode = _chunkBKXERWADjs.isRendererDevMode; exports.isSafeUrl = _chunkBKXERWADjs.isSafeUrl; exports.loadSpline = loadSpline; exports.measureVectoUserTiming = measureVectoUserTiming; exports.normalizeChord = normalizeChord; exports.ownsKeyboard = ownsKeyboard; exports.parseColorToRGBA = _chunkBKXERWADjs.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.resolveProjection = resolveProjection; exports.sanitizeUrl = _chunkBKXERWADjs.sanitizeUrl; exports.setRendererDevMode = _chunkBKXERWADjs.setRendererDevMode; exports.supportsHTMLInCanvas = supportsHTMLInCanvas;