@vectojs/core 1.39.0 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -135
- package/dist/{chunk-RUM2KEDI.mjs → chunk-AISSDI6U.mjs} +15 -8
- package/dist/{chunk-KRUDTGNR.js → chunk-BKXERWAD.js} +15 -8
- package/dist/{chunk-7PYD5UDX.mjs → chunk-CIRZ3S2Z.mjs} +48 -3
- package/dist/{chunk-POPXUPKR.js → chunk-HC5EZOZJ.js} +80 -35
- package/dist/components/VirtualizedSetAggregate.d.ts +110 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1130 -216
- package/dist/index.mjs +927 -13
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +59 -1
- package/dist/tree/Scene.d.ts +212 -1
- package/dist/tree/scene/HitResult.d.ts +50 -0
- package/dist/tree/scene/HitTester.d.ts +30 -0
- package/dist/tree/scene/ProjectionBackend.d.ts +64 -0
- package/dist/tree/scene/ProjectionPolicy.d.ts +162 -0
- package/dist/tree/scene/SemanticProjectionPolicy.d.ts +119 -0
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
parseColorToRGBA,
|
|
12
12
|
sanitizeUrl,
|
|
13
13
|
setRendererDevMode
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-AISSDI6U.mjs";
|
|
15
15
|
import {
|
|
16
16
|
Entity,
|
|
17
17
|
MSDFTextEntity,
|
|
18
18
|
SVGEntity,
|
|
19
19
|
VectoJSEvent,
|
|
20
20
|
contentLineInHint
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-CIRZ3S2Z.mjs";
|
|
22
22
|
|
|
23
23
|
// src/tree/ComputeParticleEntity.ts
|
|
24
24
|
var PARTICLE_STRIDE_FLOATS = 8;
|
|
@@ -1069,8 +1069,11 @@ var CanvasGeometry = class {
|
|
|
1069
1069
|
/** Effective device pixel ratio, matching CanvasRenderer: real DPR clamped to
|
|
1070
1070
|
* `maxDPR` when set. */
|
|
1071
1071
|
effectiveDPR(maxDPR) {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1072
|
+
const raw = typeof window !== "undefined" ? window.devicePixelRatio : 1;
|
|
1073
|
+
const real = Number.isFinite(raw) && raw > 0 ? raw : 1;
|
|
1074
|
+
if (maxDPR === void 0) return real;
|
|
1075
|
+
if (!Number.isFinite(maxDPR) || maxDPR <= 0) return real;
|
|
1076
|
+
return Math.min(real, maxDPR);
|
|
1074
1077
|
}
|
|
1075
1078
|
/** Size the WebGPU particle canvas: backing store at logical × DPR, CSS box at
|
|
1076
1079
|
* the logical size. Sizing the backing store in logical px (the old
|
|
@@ -1595,6 +1598,62 @@ var HitTester = class {
|
|
|
1595
1598
|
findEntityAt(x, y, frame, width, height) {
|
|
1596
1599
|
const overlayHit = this.findHitRecursively(this.overlayRoot, x, y);
|
|
1597
1600
|
if (overlayHit) return overlayHit;
|
|
1601
|
+
return this.findMainTreeHit(x, y, frame, width, height);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* The merged hit list (RFC5 §2, CTX-0600): the canvas spatial test plus
|
|
1605
|
+
* caller-observed DOM-native candidates (mirror / portal / `dom-visual`
|
|
1606
|
+
* extension point for CTX-0598) as ONE ordered candidate list.
|
|
1607
|
+
*
|
|
1608
|
+
* Query API only — no dispatch change: `findEntityAt` keeps its single
|
|
1609
|
+
* topmost answer byte-for-byte. Coordinates enter in scene space (callers
|
|
1610
|
+
* map client coordinates first, e.g. `Scene.findHitsAtClient`); backend
|
|
1611
|
+
* attribution follows. DOM candidates pass through the same
|
|
1612
|
+
* `disabled` / `pointerEvents: 'none'` predicate as the canvas paths
|
|
1613
|
+
* (RFC5 §2 rule 4); the browser's hit test is their geometric authority,
|
|
1614
|
+
* so scene-space `isPointInside` is not re-checked.
|
|
1615
|
+
*
|
|
1616
|
+
* Overlay order is authoritative (RFC5 §2 rule 2): overlay-subtree
|
|
1617
|
+
* candidates sort above main-tree candidates, DOM-native backends above
|
|
1618
|
+
* canvas within one subtree. Sorted descending by `priority`; ties keep
|
|
1619
|
+
* insertion order (canvas overlay, canvas main, then DOM in caller order).
|
|
1620
|
+
*/
|
|
1621
|
+
findHitsAt(x, y, frame, width, height, domCandidates = []) {
|
|
1622
|
+
const results = [];
|
|
1623
|
+
const overlayHit = this.findHitRecursively(this.overlayRoot, x, y);
|
|
1624
|
+
if (overlayHit) results.push(this.toHitResult(overlayHit, "canvas", x, y));
|
|
1625
|
+
const mainHit = this.findMainTreeHit(x, y, frame, width, height);
|
|
1626
|
+
if (mainHit) results.push(this.toHitResult(mainHit, "canvas", x, y));
|
|
1627
|
+
for (const candidate of domCandidates) {
|
|
1628
|
+
if (!this.isHitEligible(candidate.node, x, y)) continue;
|
|
1629
|
+
results.push(this.toHitResult(candidate.node, candidate.backend, x, y));
|
|
1630
|
+
}
|
|
1631
|
+
results.sort((a, b) => b.priority - a.priority);
|
|
1632
|
+
return results;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Attribute one candidate: scene-space point plus node-local point, with
|
|
1636
|
+
* the compositor-layer priority (overlay subtree above main tree,
|
|
1637
|
+
* DOM-native backends above canvas within one subtree).
|
|
1638
|
+
*/
|
|
1639
|
+
toHitResult(node, backend, x, y) {
|
|
1640
|
+
return {
|
|
1641
|
+
node,
|
|
1642
|
+
backend,
|
|
1643
|
+
localPoint: node.worldToLocal(x, y),
|
|
1644
|
+
worldPoint: { x, y },
|
|
1645
|
+
priority: (this.isInOverlaySubtree(node) ? 2 : 0) + (backend === "canvas" ? 0 : 1)
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
/** Whether `node` lives under the overlay root (drawn above the main tree). */
|
|
1649
|
+
isInOverlaySubtree(node) {
|
|
1650
|
+
for (let current = node; current; current = current.parent) {
|
|
1651
|
+
if (current === this.overlayRoot) return true;
|
|
1652
|
+
}
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
/** Main-tree arm of {@link findEntityAt}, shared with {@link findHitsAt}. */
|
|
1656
|
+
findMainTreeHit(x, y, frame, width, height) {
|
|
1598
1657
|
if (this.backends.hit && this.ensureHitGrid(frame, width, height)) {
|
|
1599
1658
|
return this.findEntityAtWasm(x, y);
|
|
1600
1659
|
}
|
|
@@ -3498,6 +3557,207 @@ function normalizeChord(input) {
|
|
|
3498
3557
|
return ordered.join("+");
|
|
3499
3558
|
}
|
|
3500
3559
|
|
|
3560
|
+
// src/tree/scene/SemanticProjectionPolicy.ts
|
|
3561
|
+
var DEFAULT_SEMANTIC_PROJECTION_POLICY = {
|
|
3562
|
+
choose: () => "project"
|
|
3563
|
+
};
|
|
3564
|
+
function getSemanticProjectionCapabilities() {
|
|
3565
|
+
return {
|
|
3566
|
+
htmlInCanvas: supportsHTMLInCanvas(),
|
|
3567
|
+
canvasTextRecovery: false
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
var CONTROL_SEMANTIC_ROLES = /* @__PURE__ */ new Set([
|
|
3571
|
+
"button",
|
|
3572
|
+
"link",
|
|
3573
|
+
"checkbox",
|
|
3574
|
+
"radio",
|
|
3575
|
+
"switch",
|
|
3576
|
+
"slider",
|
|
3577
|
+
"spinbutton",
|
|
3578
|
+
"combobox",
|
|
3579
|
+
"listbox",
|
|
3580
|
+
"option",
|
|
3581
|
+
"menuitem",
|
|
3582
|
+
"menuitemcheckbox",
|
|
3583
|
+
"menuitemradio",
|
|
3584
|
+
"tab",
|
|
3585
|
+
"textbox",
|
|
3586
|
+
"searchbox",
|
|
3587
|
+
"progressbar",
|
|
3588
|
+
"scrollbar",
|
|
3589
|
+
"meter"
|
|
3590
|
+
]);
|
|
3591
|
+
function isDeferrableSemanticNode(node) {
|
|
3592
|
+
const attrs = node.getA11yAttributes();
|
|
3593
|
+
if (attrs.tag !== void 0 && attrs.tag !== "div") return false;
|
|
3594
|
+
if (attrs.role !== void 0 && CONTROL_SEMANTIC_ROLES.has(attrs.role)) return false;
|
|
3595
|
+
if (typeof attrs.tabIndex === "number" && attrs.tabIndex >= 0) return false;
|
|
3596
|
+
const projection = node.getContentProjection?.();
|
|
3597
|
+
if (projection?.selectable) return false;
|
|
3598
|
+
return true;
|
|
3599
|
+
}
|
|
3600
|
+
function describeHTMLInCanvasSupport() {
|
|
3601
|
+
if (typeof document === "undefined") return { supported: false, reason: "no-dom" };
|
|
3602
|
+
try {
|
|
3603
|
+
const canvas = document.createElement("canvas");
|
|
3604
|
+
const ctx = canvas.getContext("2d");
|
|
3605
|
+
const native2D = typeof CanvasRenderingContext2D !== "undefined" && ctx instanceof CanvasRenderingContext2D;
|
|
3606
|
+
if (native2D && typeof ctx["drawElementImage"] === "function") {
|
|
3607
|
+
return { supported: true };
|
|
3608
|
+
}
|
|
3609
|
+
} catch {
|
|
3610
|
+
return { supported: false, reason: "no-backend" };
|
|
3611
|
+
}
|
|
3612
|
+
return { supported: false, reason: "no-backend" };
|
|
3613
|
+
}
|
|
3614
|
+
function supportsHTMLInCanvas() {
|
|
3615
|
+
return describeHTMLInCanvasSupport().supported;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
// src/tree/scene/ProjectionPolicy.ts
|
|
3619
|
+
var DEFAULT_PROJECTION_CAPABILITIES = {
|
|
3620
|
+
/** Static text (short): findability already covered by content projection. */
|
|
3621
|
+
text: {
|
|
3622
|
+
domSupported: true,
|
|
3623
|
+
nativeValue: "medium",
|
|
3624
|
+
domCost: "low",
|
|
3625
|
+
canvasCost: "low",
|
|
3626
|
+
autoDefault: "canvas"
|
|
3627
|
+
},
|
|
3628
|
+
/** Long/selectable text, Markdown blocks: selection, Ctrl+F, translation. */
|
|
3629
|
+
prose: {
|
|
3630
|
+
domSupported: true,
|
|
3631
|
+
nativeValue: "high",
|
|
3632
|
+
domCost: "medium",
|
|
3633
|
+
canvasCost: "high",
|
|
3634
|
+
autoDefault: "dom"
|
|
3635
|
+
},
|
|
3636
|
+
/** Code blocks: selectable source, but canvas carriers already serve selection — rests canvas until measured (RFC4 §6 hybrid mapping). */
|
|
3637
|
+
code: {
|
|
3638
|
+
domSupported: true,
|
|
3639
|
+
nativeValue: "medium",
|
|
3640
|
+
domCost: "medium",
|
|
3641
|
+
canvasCost: "high",
|
|
3642
|
+
autoDefault: "canvas"
|
|
3643
|
+
},
|
|
3644
|
+
/** Text input / editable: IME, caret, clipboard, BiDi come free in DOM. */
|
|
3645
|
+
input: {
|
|
3646
|
+
domSupported: true,
|
|
3647
|
+
nativeValue: "high",
|
|
3648
|
+
domCost: "low",
|
|
3649
|
+
canvasCost: "prohibitive",
|
|
3650
|
+
autoDefault: "dom"
|
|
3651
|
+
},
|
|
3652
|
+
/** Button / Link: cheap either way, native wins on focus + AT clicks. */
|
|
3653
|
+
button: {
|
|
3654
|
+
domSupported: true,
|
|
3655
|
+
nativeValue: "medium",
|
|
3656
|
+
domCost: "low",
|
|
3657
|
+
canvasCost: "low",
|
|
3658
|
+
autoDefault: "dom"
|
|
3659
|
+
},
|
|
3660
|
+
link: {
|
|
3661
|
+
domSupported: true,
|
|
3662
|
+
nativeValue: "medium",
|
|
3663
|
+
domCost: "low",
|
|
3664
|
+
canvasCost: "low",
|
|
3665
|
+
autoDefault: "dom"
|
|
3666
|
+
},
|
|
3667
|
+
/** Select / Dropdown / Checkbox / Radio: popup, keyboard, form semantics. */
|
|
3668
|
+
select: {
|
|
3669
|
+
domSupported: true,
|
|
3670
|
+
nativeValue: "high",
|
|
3671
|
+
domCost: "low",
|
|
3672
|
+
canvasCost: "high",
|
|
3673
|
+
autoDefault: "dom"
|
|
3674
|
+
},
|
|
3675
|
+
/** Container / layout group: no direct representation, children negotiate. */
|
|
3676
|
+
container: {
|
|
3677
|
+
domSupported: true,
|
|
3678
|
+
nativeValue: "none",
|
|
3679
|
+
domCost: "n/a",
|
|
3680
|
+
canvasCost: "low",
|
|
3681
|
+
autoDefault: "canvas"
|
|
3682
|
+
},
|
|
3683
|
+
/** Transform / camera rig: owns space, not pixels — never materialized. */
|
|
3684
|
+
transform: {
|
|
3685
|
+
domSupported: true,
|
|
3686
|
+
nativeValue: "none",
|
|
3687
|
+
domCost: "n/a",
|
|
3688
|
+
canvasCost: "n/a",
|
|
3689
|
+
autoDefault: "canvas"
|
|
3690
|
+
},
|
|
3691
|
+
/** Image / video frame: blit is cheap, one element per item is not. */
|
|
3692
|
+
image: {
|
|
3693
|
+
domSupported: true,
|
|
3694
|
+
nativeValue: "low",
|
|
3695
|
+
domCost: "medium",
|
|
3696
|
+
canvasCost: "low",
|
|
3697
|
+
autoDefault: "canvas"
|
|
3698
|
+
},
|
|
3699
|
+
/** Particles / danmaku / chart glyphs: bulk stays canvas, unconditionally. */
|
|
3700
|
+
particles: {
|
|
3701
|
+
domSupported: true,
|
|
3702
|
+
nativeValue: "none",
|
|
3703
|
+
domCost: "prohibitive",
|
|
3704
|
+
canvasCost: "low",
|
|
3705
|
+
autoDefault: "canvas"
|
|
3706
|
+
},
|
|
3707
|
+
/** Graph nodes/edges (interactive, moderate count): canvas until measured. */
|
|
3708
|
+
graph: {
|
|
3709
|
+
domSupported: true,
|
|
3710
|
+
nativeValue: "low",
|
|
3711
|
+
domCost: "medium",
|
|
3712
|
+
canvasCost: "medium",
|
|
3713
|
+
autoDefault: "canvas"
|
|
3714
|
+
},
|
|
3715
|
+
/** Custom shader / GPU particles: no DOM representation exists. */
|
|
3716
|
+
shader: {
|
|
3717
|
+
domSupported: false,
|
|
3718
|
+
nativeValue: "none",
|
|
3719
|
+
domCost: "unsupported",
|
|
3720
|
+
canvasCost: "low",
|
|
3721
|
+
autoDefault: "canvas"
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
var UNKNOWN_PROJECTION_CAPABILITY = {
|
|
3725
|
+
domSupported: false,
|
|
3726
|
+
nativeValue: "none",
|
|
3727
|
+
domCost: "unsupported",
|
|
3728
|
+
canvasCost: "low",
|
|
3729
|
+
autoDefault: "canvas"
|
|
3730
|
+
};
|
|
3731
|
+
var PROJECTION_AUTO_HYSTERESIS_FRAMES = 3;
|
|
3732
|
+
var PROJECTION_AUTO_DOM_BUDGET = 500;
|
|
3733
|
+
function resolveProjection(want, cap, ctx) {
|
|
3734
|
+
if (want === "canvas") return { resolved: "canvas", reason: null };
|
|
3735
|
+
if (!ctx.hasDOM || !ctx.domBackendMounted) {
|
|
3736
|
+
return { resolved: "canvas", reason: "no-dom-backend" };
|
|
3737
|
+
}
|
|
3738
|
+
if (!cap.domSupported) return { resolved: "canvas", reason: "unsupported-kind" };
|
|
3739
|
+
if (want === "dom") {
|
|
3740
|
+
if (cap.domCost === "prohibitive") return { resolved: "canvas", reason: "prohibitive-cost" };
|
|
3741
|
+
return { resolved: "dom", reason: null };
|
|
3742
|
+
}
|
|
3743
|
+
if (cap.nativeValue === "high" && cap.domCost !== "prohibitive") {
|
|
3744
|
+
return { resolved: "dom", reason: null };
|
|
3745
|
+
}
|
|
3746
|
+
if (cap.canvasCost === "prohibitive" && cap.domSupported) {
|
|
3747
|
+
return { resolved: "dom", reason: null };
|
|
3748
|
+
}
|
|
3749
|
+
if (cap.autoDefault === "dom") return { resolved: "dom", reason: null };
|
|
3750
|
+
return { resolved: "canvas", reason: null };
|
|
3751
|
+
}
|
|
3752
|
+
function hysteresisVote(current, desired, consecutive, hysteresisFrames) {
|
|
3753
|
+
if (current === void 0 || current === desired) {
|
|
3754
|
+
return { resolved: desired, consecutive: 0, flipped: false };
|
|
3755
|
+
}
|
|
3756
|
+
const next = consecutive + 1;
|
|
3757
|
+
if (next >= hysteresisFrames) return { resolved: desired, consecutive: 0, flipped: true };
|
|
3758
|
+
return { resolved: current, consecutive: next, flipped: false };
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3501
3761
|
// src/tree/Scene.ts
|
|
3502
3762
|
var RANGE_VALUE_ROLES = /* @__PURE__ */ new Set(["slider", "spinbutton", "progressbar", "scrollbar", "meter"]);
|
|
3503
3763
|
var INTERACTIVE_A11Y_ROLES = /* @__PURE__ */ new Set([
|
|
@@ -3521,7 +3781,8 @@ var KEYBOARD_OWNING_ROLES = /* @__PURE__ */ new Set([
|
|
|
3521
3781
|
]);
|
|
3522
3782
|
function ownsKeyboard(el) {
|
|
3523
3783
|
if (!el) return false;
|
|
3524
|
-
if (el === document.body || el === document.documentElement)
|
|
3784
|
+
if (typeof document !== "undefined" && (el === document.body || el === document.documentElement))
|
|
3785
|
+
return false;
|
|
3525
3786
|
if (el.hasAttribute("data-vecto-a11y-root")) return false;
|
|
3526
3787
|
const tag = el.tagName;
|
|
3527
3788
|
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
@@ -3561,6 +3822,7 @@ var SCENE_OPTION_KEYS = [
|
|
|
3561
3822
|
"renderer",
|
|
3562
3823
|
"renderMode",
|
|
3563
3824
|
"respectReducedMotion",
|
|
3825
|
+
"semanticProjectionPolicy",
|
|
3564
3826
|
"userTiming"
|
|
3565
3827
|
];
|
|
3566
3828
|
var SCENE_FIELD_NOT_OPTION = {
|
|
@@ -4058,6 +4320,26 @@ var Scene = class _Scene {
|
|
|
4058
4320
|
* frame. See {@link SceneOptions.a11ySyncInterval}.
|
|
4059
4321
|
*/
|
|
4060
4322
|
a11ySyncInterval = 0;
|
|
4323
|
+
/**
|
|
4324
|
+
* Per-node semantic projection policy (RFC3 §5, CTX-0599). Consulted once
|
|
4325
|
+
* per node by {@link shouldProjectA11y}; the default projects everything
|
|
4326
|
+
* the legacy predicate projects, so reassigning nothing changes nothing.
|
|
4327
|
+
* See {@link SceneOptions.semanticProjectionPolicy}.
|
|
4328
|
+
*/
|
|
4329
|
+
semanticProjectionPolicy = DEFAULT_SEMANTIC_PROJECTION_POLICY;
|
|
4330
|
+
/**
|
|
4331
|
+
* Consecutive syncs an `'auto'`-policy node must keep voting for the other
|
|
4332
|
+
* backend before its sticky resolution flips (RFC4 §3 rule 3, CTX-0601).
|
|
4333
|
+
* Documented tunable — see {@link PROJECTION_AUTO_HYSTERESIS_FRAMES}.
|
|
4334
|
+
*/
|
|
4335
|
+
projectionHysteresisFrames = PROJECTION_AUTO_HYSTERESIS_FRAMES;
|
|
4336
|
+
/**
|
|
4337
|
+
* Maximum `'auto'`-resolved DOM residents per scene per frame (RFC4 §4
|
|
4338
|
+
* particle-row backstop: bulk-count nodes stay canvas). Explicit `'dom'`
|
|
4339
|
+
* requests bypass it. Documented tunable — see
|
|
4340
|
+
* {@link PROJECTION_AUTO_DOM_BUDGET}.
|
|
4341
|
+
*/
|
|
4342
|
+
projectionAutoDomBudget = PROJECTION_AUTO_DOM_BUDGET;
|
|
4061
4343
|
/** Timestamp of the last a11y sync, for throttling. */
|
|
4062
4344
|
lastA11ySync = -Infinity;
|
|
4063
4345
|
/** True if we skipped an a11y sync during animation and need to sync when at rest. */
|
|
@@ -4178,6 +4460,8 @@ var Scene = class _Scene {
|
|
|
4178
4460
|
* this an embedded canvas stayed at its initial size forever. */
|
|
4179
4461
|
canvasResizeObserver = null;
|
|
4180
4462
|
dprChangeHandler = null;
|
|
4463
|
+
dprPollInterval = null;
|
|
4464
|
+
lastDpr = 1;
|
|
4181
4465
|
// --- domain: a11y-projection — focus, overlay geometry, DOM ordering, portals ---
|
|
4182
4466
|
focusedA11yElement = null;
|
|
4183
4467
|
/**
|
|
@@ -4286,6 +4570,30 @@ var Scene = class _Scene {
|
|
|
4286
4570
|
activePortalsThisFrame = /* @__PURE__ */ new Set();
|
|
4287
4571
|
activePortalsPrevFrame = /* @__PURE__ */ new Set();
|
|
4288
4572
|
portalEntities = /* @__PURE__ */ new Map();
|
|
4573
|
+
/**
|
|
4574
|
+
* Generic projection backends (RFC1 §5 vocabulary, `tree/scene/ProjectionBackend.ts`).
|
|
4575
|
+
* Render-independent: the interface exchanges only node identity, world
|
|
4576
|
+
* matrix, and lifecycle calls, so registering a DOM backend adds no
|
|
4577
|
+
* `HTMLElement` surface to core. `@vectojs/dom` owns the only implementation.
|
|
4578
|
+
*/
|
|
4579
|
+
projectionBackends = [];
|
|
4580
|
+
/** Per-frame seen-sets driving {@link pruneProjectionBackends} (portal-precedent). */
|
|
4581
|
+
domSeenPrevFrame = /* @__PURE__ */ new Set();
|
|
4582
|
+
domSeenThisFrame = /* @__PURE__ */ new Set();
|
|
4583
|
+
domSeenNodes = /* @__PURE__ */ new Map();
|
|
4584
|
+
/**
|
|
4585
|
+
* Negotiation state (RFC4 §3, CTX-0601): last resolution per node (the
|
|
4586
|
+
* per-scene queryable surface behind {@link getProjectionResolutions}),
|
|
4587
|
+
* hysteresis votes, per-kind capability overrides, and explicit gesture
|
|
4588
|
+
* pins. Plain data only — no `HTMLElement` surface in core.
|
|
4589
|
+
*/
|
|
4590
|
+
projectionResolutions = /* @__PURE__ */ new Map();
|
|
4591
|
+
projectionHysteresis = /* @__PURE__ */ new Map();
|
|
4592
|
+
projectionCapabilityOverrides = /* @__PURE__ */ new Map();
|
|
4593
|
+
projectionGesturePins = /* @__PURE__ */ new Map();
|
|
4594
|
+
/** Frame id the `'auto'` DOM budget count belongs to (bulk backstop). */
|
|
4595
|
+
projectionBudgetFrame = -1;
|
|
4596
|
+
projectionDomAutoCount = 0;
|
|
4289
4597
|
renderOrderCounter = 0;
|
|
4290
4598
|
// --- domain: render-scheduler — authoritative frame counter ---
|
|
4291
4599
|
/**
|
|
@@ -4932,8 +5240,13 @@ var Scene = class _Scene {
|
|
|
4932
5240
|
this.width = styleWidth ?? (canvas.width || canvas.clientWidth || 0);
|
|
4933
5241
|
this.height = styleHeight ?? (canvas.height || canvas.clientHeight || 0);
|
|
4934
5242
|
} else {
|
|
4935
|
-
|
|
4936
|
-
|
|
5243
|
+
if (typeof canvas.isConnected === "boolean" && !canvas.isConnected) {
|
|
5244
|
+
this.width = 0;
|
|
5245
|
+
this.height = 0;
|
|
5246
|
+
} else {
|
|
5247
|
+
this.width = typeof window !== "undefined" ? window.innerWidth : canvas.clientWidth || canvas.width || 800;
|
|
5248
|
+
this.height = typeof window !== "undefined" ? window.innerHeight : canvas.clientHeight || canvas.height || 600;
|
|
5249
|
+
}
|
|
4937
5250
|
}
|
|
4938
5251
|
const globalProcess = typeof globalThis !== "undefined" ? globalThis.process : void 0;
|
|
4939
5252
|
const isTest = globalProcess && (globalProcess.env?.NODE_ENV === "test" || globalProcess.env?.VITEST === "true");
|
|
@@ -4944,6 +5257,9 @@ var Scene = class _Scene {
|
|
|
4944
5257
|
this.phases.userTiming = options.userTiming ?? false;
|
|
4945
5258
|
this.particleBackend = options.particleBackend ?? "auto";
|
|
4946
5259
|
this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
|
|
5260
|
+
this.semanticProjectionPolicy = options.semanticProjectionPolicy ?? DEFAULT_SEMANTIC_PROJECTION_POLICY;
|
|
5261
|
+
this.projectionHysteresisFrames = options.projectionHysteresisFrames ?? PROJECTION_AUTO_HYSTERESIS_FRAMES;
|
|
5262
|
+
this.projectionAutoDomBudget = options.projectionAutoDomBudget ?? PROJECTION_AUTO_DOM_BUDGET;
|
|
4947
5263
|
this.contentProjectionEnabled = options.contentProjection ?? true;
|
|
4948
5264
|
this.contentProjectionMargin = options.contentProjectionMargin;
|
|
4949
5265
|
this.contentSemanticMargin = options.contentSemanticMargin;
|
|
@@ -5165,16 +5481,52 @@ var Scene = class _Scene {
|
|
|
5165
5481
|
if (this.dprMediaQuery && this.dprChangeHandler) {
|
|
5166
5482
|
this.dprMediaQuery.removeEventListener?.("change", this.dprChangeHandler);
|
|
5167
5483
|
}
|
|
5168
|
-
const
|
|
5484
|
+
const raw = window.devicePixelRatio;
|
|
5485
|
+
const dpr = Number.isFinite(raw) && raw > 0 ? raw : 1;
|
|
5169
5486
|
const query = window.matchMedia(`(resolution: ${dpr}dppx)`);
|
|
5170
5487
|
const handler = () => {
|
|
5171
|
-
|
|
5172
|
-
|
|
5488
|
+
const curRaw = window.devicePixelRatio;
|
|
5489
|
+
const cur = Number.isFinite(curRaw) && curRaw > 0 ? curRaw : 1;
|
|
5490
|
+
if (Math.abs(cur - dpr) <= 1e-3) {
|
|
5491
|
+
this.watchDevicePixelRatio();
|
|
5492
|
+
return;
|
|
5493
|
+
}
|
|
5494
|
+
this.lastDpr = cur;
|
|
5495
|
+
try {
|
|
5496
|
+
this.resize(this.width, this.height);
|
|
5497
|
+
} catch (err) {
|
|
5498
|
+
console.warn("[VectoJS] DPR resize failed", err);
|
|
5499
|
+
}
|
|
5500
|
+
try {
|
|
5501
|
+
if (this.renderer.isContextLost?.() !== true) this.render(this.renderer);
|
|
5502
|
+
} catch (err) {
|
|
5503
|
+
console.warn("[VectoJS] DPR repaint failed", err);
|
|
5504
|
+
}
|
|
5173
5505
|
this.watchDevicePixelRatio();
|
|
5174
5506
|
};
|
|
5175
5507
|
query.addEventListener?.("change", handler);
|
|
5176
5508
|
this.dprMediaQuery = query;
|
|
5177
5509
|
this.dprChangeHandler = handler;
|
|
5510
|
+
this.lastDpr = dpr;
|
|
5511
|
+
if (this.dprPollInterval === null && typeof setInterval === "function") {
|
|
5512
|
+
this.dprPollInterval = setInterval(() => {
|
|
5513
|
+
const rawPoll = window.devicePixelRatio;
|
|
5514
|
+
const curPoll = Number.isFinite(rawPoll) && rawPoll > 0 ? rawPoll : 1;
|
|
5515
|
+
if (Math.abs(curPoll - this.lastDpr) <= 1e-3) return;
|
|
5516
|
+
this.lastDpr = curPoll;
|
|
5517
|
+
try {
|
|
5518
|
+
this.resize(this.width, this.height);
|
|
5519
|
+
} catch (err) {
|
|
5520
|
+
console.warn("[VectoJS] DPR poll resize failed", err);
|
|
5521
|
+
}
|
|
5522
|
+
try {
|
|
5523
|
+
if (this.renderer.isContextLost?.() !== true) this.render(this.renderer);
|
|
5524
|
+
} catch (err) {
|
|
5525
|
+
console.warn("[VectoJS] DPR poll repaint failed", err);
|
|
5526
|
+
}
|
|
5527
|
+
this.watchDevicePixelRatio();
|
|
5528
|
+
}, 1e3);
|
|
5529
|
+
}
|
|
5178
5530
|
}
|
|
5179
5531
|
/**
|
|
5180
5532
|
* Recover the WebGL point layer from a GPU context loss (driver TDR reset,
|
|
@@ -5208,6 +5560,35 @@ var Scene = class _Scene {
|
|
|
5208
5560
|
gl.addEventListener("webglcontextlost", this.glContextLostHandler);
|
|
5209
5561
|
gl.addEventListener("webglcontextrestored", this.glContextRestoredHandler);
|
|
5210
5562
|
}
|
|
5563
|
+
/**
|
|
5564
|
+
* One-shot adoption of the window viewport once an initially-detached canvas
|
|
5565
|
+
* gains layout (#817). A full-window scene constructed before its canvas is
|
|
5566
|
+
* attached starts at 0×0, and no window `resize` fires on attachment — so
|
|
5567
|
+
* without this latch the scene would stay unsized forever. The first nonzero
|
|
5568
|
+
* layout box adopts `window.innerWidth/innerHeight` (the same sizing the
|
|
5569
|
+
* window resize handler applies, keeping the full-window contract), then the
|
|
5570
|
+
* observer disconnects: steady-state sizing stays exactly as if the canvas
|
|
5571
|
+
* had been attached before construction. An explicit user `resize()` between
|
|
5572
|
+
* construction and attachment is respected and skips adoption.
|
|
5573
|
+
*
|
|
5574
|
+
* No-op when the scene already has a size (attached at construction) or when
|
|
5575
|
+
* `ResizeObserver` is unavailable (the caller then drives sizing explicitly).
|
|
5576
|
+
*/
|
|
5577
|
+
armAttachmentViewportLatch() {
|
|
5578
|
+
if (this.width > 0 && this.height > 0) return;
|
|
5579
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
5580
|
+
const observer = new ResizeObserver((entries) => {
|
|
5581
|
+
const box = entries[0]?.contentRect;
|
|
5582
|
+
if (!box || !(box.width > 0) || !(box.height > 0)) return;
|
|
5583
|
+
observer.disconnect();
|
|
5584
|
+
this.canvasResizeObserver = null;
|
|
5585
|
+
if (this.width === 0 || this.height === 0) {
|
|
5586
|
+
this.resize(window.innerWidth, window.innerHeight);
|
|
5587
|
+
}
|
|
5588
|
+
});
|
|
5589
|
+
this.canvasResizeObserver = observer;
|
|
5590
|
+
observer.observe(this.canvas);
|
|
5591
|
+
}
|
|
5211
5592
|
// --- domain: scene-facade — renderer accessor ---
|
|
5212
5593
|
/**
|
|
5213
5594
|
* Expose the underlying {@link IRenderer} for advanced direct-draw operations.
|
|
@@ -5230,6 +5611,37 @@ var Scene = class _Scene {
|
|
|
5230
5611
|
findEntityAt(x, y) {
|
|
5231
5612
|
return this._hitTester.findEntityAt(x, y, this.currentFrame, this.width, this.height);
|
|
5232
5613
|
}
|
|
5614
|
+
// --- domain: hit-test — merged HitResult query (RFC5 §2, CTX-0600) ---
|
|
5615
|
+
/**
|
|
5616
|
+
* The merged hit list for a scene-space point: the canvas spatial test
|
|
5617
|
+
* plus caller-observed DOM-native candidates (mirror / portal /
|
|
5618
|
+
* `dom-visual` extension point for CTX-0598) as ONE ordered candidate
|
|
5619
|
+
* list (`HitResult`, overlay order authoritative).
|
|
5620
|
+
*
|
|
5621
|
+
* Query API alongside {@link findEntityAt} — no dispatch change:
|
|
5622
|
+
* `findEntityAt` keeps its single topmost answer byte-for-byte, keyboard
|
|
5623
|
+
* and AT flows are untouched. For browser pointer coordinates use
|
|
5624
|
+
* {@link findHitsAtClient}, which maps through {@link clientToScene} first.
|
|
5625
|
+
*/
|
|
5626
|
+
findHitsAt(x, y, domCandidates = []) {
|
|
5627
|
+
return this._hitTester.findHitsAt(
|
|
5628
|
+
x,
|
|
5629
|
+
y,
|
|
5630
|
+
this.currentFrame,
|
|
5631
|
+
this.width,
|
|
5632
|
+
this.height,
|
|
5633
|
+
domCandidates
|
|
5634
|
+
);
|
|
5635
|
+
}
|
|
5636
|
+
/**
|
|
5637
|
+
* {@link findHitsAt} for browser viewport coordinates: maps through
|
|
5638
|
+
* {@link clientToScene} so coordinates enter in scene space (RFC5 §2
|
|
5639
|
+
* rule 3) and backend attribution follows.
|
|
5640
|
+
*/
|
|
5641
|
+
findHitsAtClient(clientX, clientY, domCandidates = []) {
|
|
5642
|
+
const point = this.clientToScene(clientX, clientY);
|
|
5643
|
+
return this.findHitsAt(point.x, point.y, domCandidates);
|
|
5644
|
+
}
|
|
5233
5645
|
// --- domain: hit-test — client-to-scene mapping ---
|
|
5234
5646
|
/** Convert browser viewport coordinates into this Scene's logical coordinates. */
|
|
5235
5647
|
clientToScene(clientX, clientY) {
|
|
@@ -5270,6 +5682,15 @@ var Scene = class _Scene {
|
|
|
5270
5682
|
this.activePortalsThisFrame.delete(node.id);
|
|
5271
5683
|
this.activePortalsPrevFrame.delete(node.id);
|
|
5272
5684
|
}
|
|
5685
|
+
if (node.domPolicy === "dom" || node.domResident) {
|
|
5686
|
+
for (const backend of this.projectionBackends) backend.unmount(node);
|
|
5687
|
+
this.domSeenThisFrame.delete(node.id);
|
|
5688
|
+
this.domSeenPrevFrame.delete(node.id);
|
|
5689
|
+
this.domSeenNodes.delete(node.id);
|
|
5690
|
+
}
|
|
5691
|
+
this.projectionResolutions.delete(node.id);
|
|
5692
|
+
this.projectionHysteresis.delete(node.id);
|
|
5693
|
+
this.projectionGesturePins.delete(node.id);
|
|
5273
5694
|
const contentEl = this.contentElements.get(node.id);
|
|
5274
5695
|
if (contentEl) {
|
|
5275
5696
|
this._contentProjection.clearGridState(node.id, contentEl);
|
|
@@ -5393,6 +5814,10 @@ var Scene = class _Scene {
|
|
|
5393
5814
|
this.dprMediaQuery = null;
|
|
5394
5815
|
this.dprChangeHandler = null;
|
|
5395
5816
|
}
|
|
5817
|
+
if (this.dprPollInterval !== null) {
|
|
5818
|
+
clearInterval(this.dprPollInterval);
|
|
5819
|
+
this.dprPollInterval = null;
|
|
5820
|
+
}
|
|
5396
5821
|
if (this.forcedColorsQuery && this.forcedColorsChangeHandler) {
|
|
5397
5822
|
this.forcedColorsQuery.removeEventListener?.("change", this.forcedColorsChangeHandler);
|
|
5398
5823
|
this.forcedColorsQuery = null;
|
|
@@ -5466,6 +5891,7 @@ var Scene = class _Scene {
|
|
|
5466
5891
|
setupEvents() {
|
|
5467
5892
|
if (typeof window !== "undefined" && !this.disableWindowResize) {
|
|
5468
5893
|
window.addEventListener("resize", this.resizeHandler);
|
|
5894
|
+
this.armAttachmentViewportLatch();
|
|
5469
5895
|
} else if (this.disableWindowResize && typeof ResizeObserver !== "undefined" && this.canvas && typeof this.canvas.getBoundingClientRect === "function") {
|
|
5470
5896
|
this.canvasResizeObserver = new ResizeObserver((entries) => {
|
|
5471
5897
|
const entry = entries[0];
|
|
@@ -5874,14 +6300,47 @@ var Scene = class _Scene {
|
|
|
5874
6300
|
shouldProjectA11y(node) {
|
|
5875
6301
|
if (!node.interactive) return false;
|
|
5876
6302
|
if (!(node.width > 0 || node.a11yFullViewport)) return false;
|
|
6303
|
+
if (node.domPolicy === "dom" || this.resolveProjectionFor(node) === "dom") return false;
|
|
5877
6304
|
switch (node.a11yProjection) {
|
|
5878
6305
|
case "never":
|
|
5879
6306
|
return false;
|
|
5880
6307
|
case "onDemand":
|
|
5881
|
-
return this.a11yEngaged(node);
|
|
6308
|
+
return this.a11yEngaged(node) && this.resolveSemanticProjection(node);
|
|
5882
6309
|
default:
|
|
5883
|
-
return
|
|
6310
|
+
return this.resolveSemanticProjection(node);
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
/**
|
|
6314
|
+
* Policy half of {@link shouldProjectA11y} (RFC3 §5, CTX-0599).
|
|
6315
|
+
*
|
|
6316
|
+
* The legacy gates above (interactive, box, `a11yProjection` engagement)
|
|
6317
|
+
* are unchanged; this only maps a {@link SemanticProjectionPolicy} decision
|
|
6318
|
+
* onto project/suppress. Out of the box the default policy returns
|
|
6319
|
+
* `'project'`, so behaviour is identical to having no policy.
|
|
6320
|
+
*
|
|
6321
|
+
* `'defer-to-browser'` always falls back to projection today: no deferral
|
|
6322
|
+
* backend exists (`supportsHTMLInCanvas()` is `false`), so deferring would
|
|
6323
|
+
* silently drop semantics — the exact failure RFC §3 rules out. When a
|
|
6324
|
+
* backend lands, only allow-listed plain display text
|
|
6325
|
+
* ({@link isDeferrableSemanticNode}; never controls) may actually defer. A
|
|
6326
|
+
* throwing policy likewise falls back to projection: a policy must never be
|
|
6327
|
+
* able to drop semantics by accident.
|
|
6328
|
+
*/
|
|
6329
|
+
resolveSemanticProjection(node) {
|
|
6330
|
+
const policy = this.semanticProjectionPolicy ?? DEFAULT_SEMANTIC_PROJECTION_POLICY;
|
|
6331
|
+
let decision;
|
|
6332
|
+
try {
|
|
6333
|
+
decision = policy.choose(node, getSemanticProjectionCapabilities(), {
|
|
6334
|
+
hasDOM: this.a11yRoot !== null
|
|
6335
|
+
});
|
|
6336
|
+
} catch {
|
|
6337
|
+
decision = "project";
|
|
6338
|
+
}
|
|
6339
|
+
if (decision === "never") return false;
|
|
6340
|
+
if (decision === "defer-to-browser" && isDeferrableSemanticNode(node) && supportsHTMLInCanvas()) {
|
|
6341
|
+
return false;
|
|
5884
6342
|
}
|
|
6343
|
+
return true;
|
|
5885
6344
|
}
|
|
5886
6345
|
/**
|
|
5887
6346
|
* Whether an `a11yProjection: 'onDemand'` entity is currently engaged enough to
|
|
@@ -6051,14 +6510,17 @@ var Scene = class _Scene {
|
|
|
6051
6510
|
if (e.target === capEl && typeof capEl.setPointerCapture === "function") {
|
|
6052
6511
|
capEl.setPointerCapture(e.pointerId);
|
|
6053
6512
|
}
|
|
6513
|
+
this.pinProjectionForGesture(node);
|
|
6054
6514
|
node.dispatchEvent(new VectoJSEvent("pointerdown", node, e));
|
|
6055
6515
|
});
|
|
6056
6516
|
el.addEventListener("pointerup", (e) => {
|
|
6057
6517
|
releasePointer(e);
|
|
6518
|
+
this.unpinProjectionForGesture(node);
|
|
6058
6519
|
node.dispatchEvent(new VectoJSEvent("pointerup", node, e));
|
|
6059
6520
|
});
|
|
6060
6521
|
el.addEventListener("pointercancel", (e) => {
|
|
6061
6522
|
releasePointer(e);
|
|
6523
|
+
this.unpinProjectionForGesture(node);
|
|
6062
6524
|
node.dispatchEvent(new VectoJSEvent("pointercancel", node, e));
|
|
6063
6525
|
});
|
|
6064
6526
|
el.addEventListener(
|
|
@@ -7019,6 +7481,197 @@ var Scene = class _Scene {
|
|
|
7019
7481
|
this.activePortalsPrevFrame = new Set(this.activePortalsThisFrame);
|
|
7020
7482
|
this.activePortalsThisFrame.clear();
|
|
7021
7483
|
}
|
|
7484
|
+
/**
|
|
7485
|
+
* Register a generic projection backend (RFC1 §5, RFC2 P1 CTX-0598).
|
|
7486
|
+
* Registration is idempotent per backend instance. Core drives
|
|
7487
|
+
* mount/update/unmount from the render walk; the backend owns all
|
|
7488
|
+
* medium-specific state.
|
|
7489
|
+
*/
|
|
7490
|
+
addProjectionBackend(backend) {
|
|
7491
|
+
if (!this.projectionBackends.includes(backend)) this.projectionBackends.push(backend);
|
|
7492
|
+
return this;
|
|
7493
|
+
}
|
|
7494
|
+
/**
|
|
7495
|
+
* Remove a previously registered projection backend by instance or
|
|
7496
|
+
* {@link ProjectionBackendKind | kind}. Removing does not unmount resident
|
|
7497
|
+
* nodes — unmount the backend first if teardown order matters.
|
|
7498
|
+
*/
|
|
7499
|
+
removeProjectionBackend(backend) {
|
|
7500
|
+
this.projectionBackends = this.projectionBackends.filter(
|
|
7501
|
+
(b) => typeof backend === "string" ? b.kind !== backend : b !== backend
|
|
7502
|
+
);
|
|
7503
|
+
return this;
|
|
7504
|
+
}
|
|
7505
|
+
/**
|
|
7506
|
+
* Scene-level projection capabilities (RFC4 §3 rule 2, CTX-0601).
|
|
7507
|
+
*
|
|
7508
|
+
* Plain-data feature detection in the shape of the proposed
|
|
7509
|
+
* `scene.inputCapabilities` (input-dispatch-contract-v2 §4): apps and
|
|
7510
|
+
* devtools query where a node _would_ materialize and why, without touching
|
|
7511
|
+
* any backend. Additive only — no existing surface moves.
|
|
7512
|
+
*/
|
|
7513
|
+
get projectionCapabilities() {
|
|
7514
|
+
return {
|
|
7515
|
+
hasDOM: typeof document !== "undefined",
|
|
7516
|
+
domBackendMounted: this.projectionBackends.some((b) => b.kind === "dom"),
|
|
7517
|
+
backends: this.projectionBackends.map((b) => b.kind),
|
|
7518
|
+
autoHysteresisFrames: this.projectionHysteresisFrames,
|
|
7519
|
+
autoDomBudget: this.projectionAutoDomBudget
|
|
7520
|
+
};
|
|
7521
|
+
}
|
|
7522
|
+
/**
|
|
7523
|
+
* Register (or replace) the Capability Matrix row for one `domKind`
|
|
7524
|
+
* (RFC4 §4, CTX-0601). Custom kinds start at the unknown-kind default
|
|
7525
|
+
* (`unsupported-kind` → canvas); registering a row is how a custom backend
|
|
7526
|
+
* kind opts into `'auto'` resolution and how tests drive oscillating costs.
|
|
7527
|
+
*/
|
|
7528
|
+
registerProjectionCapability(domKind, row) {
|
|
7529
|
+
this.projectionCapabilityOverrides.set(domKind, row);
|
|
7530
|
+
return this;
|
|
7531
|
+
}
|
|
7532
|
+
/** Matrix row for a node's `domKind` (override wins, unknown → canvas). */
|
|
7533
|
+
getProjectionCapability(node) {
|
|
7534
|
+
return this.projectionCapabilityOverrides.get(node.domKind) ?? DEFAULT_PROJECTION_CAPABILITIES[node.domKind] ?? UNKNOWN_PROJECTION_CAPABILITY;
|
|
7535
|
+
}
|
|
7536
|
+
/**
|
|
7537
|
+
* Pin a node's backend for the duration of an active gesture (RFC4 §5 +
|
|
7538
|
+
* input-dispatch-contract-v2 §4 gesture stickiness: no mid-gesture handoff).
|
|
7539
|
+
* The a11y-mirror pointerdown/up/cancel listeners maintain this; DOM-side
|
|
7540
|
+
* gestures are consulted through
|
|
7541
|
+
* {@link ProjectionBackend.hasActiveGesture | backend.hasActiveGesture}.
|
|
7542
|
+
* Refcounted so multi-pointer gestures on one node unpin exactly once.
|
|
7543
|
+
*/
|
|
7544
|
+
pinProjectionForGesture(node) {
|
|
7545
|
+
this.projectionGesturePins.set(node.id, (this.projectionGesturePins.get(node.id) ?? 0) + 1);
|
|
7546
|
+
}
|
|
7547
|
+
/** Release one gesture pin taken by {@link pinProjectionForGesture}. */
|
|
7548
|
+
unpinProjectionForGesture(node) {
|
|
7549
|
+
const count = (this.projectionGesturePins.get(node.id) ?? 0) - 1;
|
|
7550
|
+
if (count <= 0) this.projectionGesturePins.delete(node.id);
|
|
7551
|
+
else this.projectionGesturePins.set(node.id, count);
|
|
7552
|
+
}
|
|
7553
|
+
/** Whether `node` currently owns an active gesture on a core mirror. */
|
|
7554
|
+
isProjectionPinned(node) {
|
|
7555
|
+
return (this.projectionGesturePins.get(node.id) ?? 0) > 0;
|
|
7556
|
+
}
|
|
7557
|
+
/**
|
|
7558
|
+
* Last negotiation for one node, if it was ever resolved on this scene.
|
|
7559
|
+
* Accepts the node or its id; returns the stored record (not a copy — treat
|
|
7560
|
+
* as read-only).
|
|
7561
|
+
*/
|
|
7562
|
+
getProjectionResolution(node) {
|
|
7563
|
+
return this.projectionResolutions.get(typeof node === "string" ? node : node.id);
|
|
7564
|
+
}
|
|
7565
|
+
/** Every node's last negotiation, in first-resolution order. */
|
|
7566
|
+
getProjectionResolutions() {
|
|
7567
|
+
return [...this.projectionResolutions.values()];
|
|
7568
|
+
}
|
|
7569
|
+
/**
|
|
7570
|
+
* Resolve one node's `domPolicy` to its effective backend (RFC4 §3, CTX-0601).
|
|
7571
|
+
*
|
|
7572
|
+
* Memoized per main frame (`currentFrame`, bumped once per authoritative
|
|
7573
|
+
* render): the render walk and the later a11y sync must agree within a
|
|
7574
|
+
* frame, or a node could lose its mirror the same frame it gains an element.
|
|
7575
|
+
* `'auto'` applies, in order: no-DOM short-circuit → pure negotiation →
|
|
7576
|
+
* gesture/focus hard bars (a pinned node keeps its current backend, never
|
|
7577
|
+
* flips mid-gesture, focus is preserved-or-moved never dropped to `body` per
|
|
7578
|
+
* input-dispatch-contract-v2 §4) → hysteresis stickiness → bulk budget.
|
|
7579
|
+
* Every outcome is recorded on the per-scene queryable surface
|
|
7580
|
+
* ({@link getProjectionResolutions}) with its reason — fallbacks are
|
|
7581
|
+
* reported, not silent (§3 rule 2).
|
|
7582
|
+
*/
|
|
7583
|
+
resolveProjectionFor(node) {
|
|
7584
|
+
const want = node.domPolicy;
|
|
7585
|
+
if (this.projectionBackends.length === 0 && want === "canvas") return "canvas";
|
|
7586
|
+
const memo = this.projectionHysteresis.get(node.id);
|
|
7587
|
+
if (memo && memo.lastFrame === this.currentFrame) return memo.resolved;
|
|
7588
|
+
const hasDOM = typeof document !== "undefined";
|
|
7589
|
+
const domBackendMounted = this.projectionBackends.some((b) => b.kind === "dom");
|
|
7590
|
+
const outcome = resolveProjection(want, this.getProjectionCapability(node), {
|
|
7591
|
+
hasDOM,
|
|
7592
|
+
domBackendMounted
|
|
7593
|
+
});
|
|
7594
|
+
let resolved = outcome.resolved;
|
|
7595
|
+
let reason = outcome.reason;
|
|
7596
|
+
if (want === "auto" && memo !== void 0) {
|
|
7597
|
+
if (resolved !== memo.resolved) {
|
|
7598
|
+
const gesturePinned = this.isProjectionPinned(node) || this.projectionBackends.some((b) => b.hasActiveGesture?.(node) === true);
|
|
7599
|
+
if (gesturePinned) {
|
|
7600
|
+
resolved = memo.resolved;
|
|
7601
|
+
reason = "active-gesture";
|
|
7602
|
+
} else if (this.ownsProjectionFocus(node)) {
|
|
7603
|
+
resolved = memo.resolved;
|
|
7604
|
+
reason = "focus-pinned";
|
|
7605
|
+
} else {
|
|
7606
|
+
const vote = hysteresisVote(
|
|
7607
|
+
memo.resolved,
|
|
7608
|
+
resolved,
|
|
7609
|
+
memo.consecutive,
|
|
7610
|
+
this.projectionHysteresisFrames
|
|
7611
|
+
);
|
|
7612
|
+
resolved = vote.resolved;
|
|
7613
|
+
memo.consecutive = vote.consecutive;
|
|
7614
|
+
reason = vote.flipped ? reason : "hysteresis";
|
|
7615
|
+
}
|
|
7616
|
+
} else {
|
|
7617
|
+
memo.consecutive = 0;
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
if (want === "auto" && resolved === "dom") {
|
|
7621
|
+
if (this.projectionBudgetFrame !== this.currentFrame) {
|
|
7622
|
+
this.projectionBudgetFrame = this.currentFrame;
|
|
7623
|
+
this.projectionDomAutoCount = 0;
|
|
7624
|
+
}
|
|
7625
|
+
this.projectionDomAutoCount += 1;
|
|
7626
|
+
if (this.projectionDomAutoCount > this.projectionAutoDomBudget) {
|
|
7627
|
+
resolved = "canvas";
|
|
7628
|
+
reason = "bulk-budget";
|
|
7629
|
+
}
|
|
7630
|
+
}
|
|
7631
|
+
this.projectionHysteresis.set(node.id, {
|
|
7632
|
+
resolved,
|
|
7633
|
+
consecutive: memo?.consecutive ?? 0,
|
|
7634
|
+
lastFrame: this.currentFrame
|
|
7635
|
+
});
|
|
7636
|
+
const prev = this.projectionResolutions.get(node.id);
|
|
7637
|
+
if (!prev || prev.want !== want || prev.resolved !== resolved || prev.reason !== reason) {
|
|
7638
|
+
this.projectionResolutions.set(node.id, { nodeId: node.id, want, resolved, reason });
|
|
7639
|
+
}
|
|
7640
|
+
return resolved;
|
|
7641
|
+
}
|
|
7642
|
+
/**
|
|
7643
|
+
* Whether `node`'s a11y mirror currently owns browser focus (RFC4 §5 focus
|
|
7644
|
+
* rule, input-dispatch-contract-v2 §4: focus stays DOM-based; flipping
|
|
7645
|
+
* `projection` must preserve or deliberately move focus, never drop it to
|
|
7646
|
+
* `body`). A focused node keeps its backend until blur — the actual move, if
|
|
7647
|
+
* any, goes through the existing sentinels (`preserveFocusOnRemoval`, the
|
|
7648
|
+
* DOM backend's own fallback), never a bare removal.
|
|
7649
|
+
*/
|
|
7650
|
+
ownsProjectionFocus(node) {
|
|
7651
|
+
if (typeof document === "undefined") return false;
|
|
7652
|
+
const mirror = this.a11yElements.get(node.id);
|
|
7653
|
+
if (!mirror || mirror !== this.focusedA11yElement) return false;
|
|
7654
|
+
return document.activeElement === mirror;
|
|
7655
|
+
}
|
|
7656
|
+
/**
|
|
7657
|
+
* RFC2 P1 (CTX-0598): unmount DOM-projection residents the walk did not see
|
|
7658
|
+
* this frame (viewport-culled, removed without `remove()`, policy-flipped).
|
|
7659
|
+
* Mirrors {@link reconcilePortals} above; unmount is idempotent per the
|
|
7660
|
+
* `ProjectionBackend` contract so notifying every backend is safe.
|
|
7661
|
+
*/
|
|
7662
|
+
pruneProjectionBackends() {
|
|
7663
|
+
for (const id of this.domSeenPrevFrame) {
|
|
7664
|
+
if (!this.domSeenThisFrame.has(id)) {
|
|
7665
|
+
const node = this.domSeenNodes.get(id);
|
|
7666
|
+
if (node) {
|
|
7667
|
+
for (const backend of this.projectionBackends) backend.unmount(node);
|
|
7668
|
+
this.domSeenNodes.delete(id);
|
|
7669
|
+
}
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
this.domSeenPrevFrame = new Set(this.domSeenThisFrame);
|
|
7673
|
+
this.domSeenThisFrame.clear();
|
|
7674
|
+
}
|
|
7022
7675
|
// --- domain: render-scheduler — the loop and the render walk ---
|
|
7023
7676
|
/**
|
|
7024
7677
|
* The frame-rate cap actually in effect: the explicit {@link maxFPS}, further
|
|
@@ -7096,6 +7749,12 @@ var Scene = class _Scene {
|
|
|
7096
7749
|
/**
|
|
7097
7750
|
* Render the entire scene graph onto the specified renderer.
|
|
7098
7751
|
*
|
|
7752
|
+
* This is the `CanvasProjection` row of {@link ProjectionBackend}:
|
|
7753
|
+
* the per-frame materialization of scene semantics into pixels through the
|
|
7754
|
+
* backend-agnostic `IRenderer` contract. (The `A11yProjection` /
|
|
7755
|
+
* `ContentProjection` rows run on the same cadence via {@link Scene.syncA11y}
|
|
7756
|
+
* below; the `DOMProjection` row does not exist yet — RFC2/CTX-0598.)
|
|
7757
|
+
*
|
|
7099
7758
|
* Main-frame causal order is a correctness contract:
|
|
7100
7759
|
*
|
|
7101
7760
|
* 1. Browser/input callbacks finish before the scheduled frame begins.
|
|
@@ -7388,6 +8047,16 @@ var Scene = class _Scene {
|
|
|
7388
8047
|
visible = maxX >= 0 && minX <= vw && maxY >= 0 && minY <= vh;
|
|
7389
8048
|
}
|
|
7390
8049
|
if (!visible && node.children.length === 0) return;
|
|
8050
|
+
if (isMainRenderer && this.projectionBackends.length > 0 && this.resolveProjectionFor(node) === "dom" && visible) {
|
|
8051
|
+
for (const backend of this.projectionBackends) {
|
|
8052
|
+
backend.update(node, { a, b, c, d, e: te, f: tf });
|
|
8053
|
+
}
|
|
8054
|
+
if (node.domResident) {
|
|
8055
|
+
this.domSeenThisFrame.add(node.id);
|
|
8056
|
+
this.domSeenNodes.set(node.id, node);
|
|
8057
|
+
if (node.children.length === 0) return;
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
7391
8060
|
if (node.children.length === 0 && node.scaleX === node.scaleY) {
|
|
7392
8061
|
const bc = node.getBatchCircle();
|
|
7393
8062
|
if (bc) {
|
|
@@ -7517,6 +8186,7 @@ var Scene = class _Scene {
|
|
|
7517
8186
|
this.frameHadAnimation = walkHadAnimation;
|
|
7518
8187
|
this.frameHadInteractive = walkHadInteractive;
|
|
7519
8188
|
this.reconcilePortals();
|
|
8189
|
+
this.pruneProjectionBackends();
|
|
7520
8190
|
}
|
|
7521
8191
|
const flushTiming = this.phases.userTiming ? beginVectoUserTiming(VECTO_USER_TIMING.scene.flush) : null;
|
|
7522
8192
|
const flushT0 = this.phases.enabled ? performance.now() : 0;
|
|
@@ -7953,6 +8623,238 @@ var TextEntity = class extends Entity {
|
|
|
7953
8623
|
}
|
|
7954
8624
|
};
|
|
7955
8625
|
|
|
8626
|
+
// src/components/VirtualizedSetAggregate.ts
|
|
8627
|
+
var VirtualizedSetAggregate = class extends Entity {
|
|
8628
|
+
_items;
|
|
8629
|
+
_label;
|
|
8630
|
+
_role;
|
|
8631
|
+
_itemRole;
|
|
8632
|
+
_rowHeight;
|
|
8633
|
+
_visibleCapacity;
|
|
8634
|
+
_onActivate;
|
|
8635
|
+
/** First item index currently bound to the pool (the scroll window). */
|
|
8636
|
+
_visibleStart = 0;
|
|
8637
|
+
/** Item id owning the roving tab stop / keyboard focus. */
|
|
8638
|
+
_activeId = null;
|
|
8639
|
+
_selectedId = null;
|
|
8640
|
+
/** One focusable hotspot per visible slot; re-bound, never rebuilt per item. */
|
|
8641
|
+
_hotspots = [];
|
|
8642
|
+
constructor(opts = {}) {
|
|
8643
|
+
super();
|
|
8644
|
+
this._items = opts.items ?? [];
|
|
8645
|
+
this._label = opts.label ?? "Items";
|
|
8646
|
+
this._role = opts.role ?? "list";
|
|
8647
|
+
this._itemRole = opts.itemRole ?? "listitem";
|
|
8648
|
+
this._rowHeight = opts.rowHeight ?? 28;
|
|
8649
|
+
this._visibleCapacity = opts.visibleCapacity;
|
|
8650
|
+
this._onActivate = opts.onActivate;
|
|
8651
|
+
this.width = opts.width ?? 0;
|
|
8652
|
+
this.height = opts.height ?? 0;
|
|
8653
|
+
this.interactive = true;
|
|
8654
|
+
this._syncHotspots();
|
|
8655
|
+
}
|
|
8656
|
+
/** Full item count — the number the container label states. */
|
|
8657
|
+
get itemCount() {
|
|
8658
|
+
return this._items.length;
|
|
8659
|
+
}
|
|
8660
|
+
/** Live pool size (bounded by the visible capacity, never by the item count). */
|
|
8661
|
+
get poolSize() {
|
|
8662
|
+
return this._hotspots.length;
|
|
8663
|
+
}
|
|
8664
|
+
/** First item index currently bound to the pool. */
|
|
8665
|
+
get visibleStart() {
|
|
8666
|
+
return this._visibleStart;
|
|
8667
|
+
}
|
|
8668
|
+
getA11yAttributes() {
|
|
8669
|
+
return {
|
|
8670
|
+
role: this._role,
|
|
8671
|
+
label: `${this._label}, ${this._items.length} items`,
|
|
8672
|
+
pointerEvents: "none"
|
|
8673
|
+
};
|
|
8674
|
+
}
|
|
8675
|
+
render(_renderer) {
|
|
8676
|
+
}
|
|
8677
|
+
/** Axis-aligned hit-test against the container box (the `UIComponent` default). */
|
|
8678
|
+
isPointInside(globalX, globalY) {
|
|
8679
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
8680
|
+
if (!local) return false;
|
|
8681
|
+
return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
|
|
8682
|
+
}
|
|
8683
|
+
/** Replace the item set; the pool re-binds and the label count follows. */
|
|
8684
|
+
setItems(items) {
|
|
8685
|
+
this._items = items;
|
|
8686
|
+
if (this._activeId !== null && !items.some((item) => item.id === this._activeId)) {
|
|
8687
|
+
this._activeId = null;
|
|
8688
|
+
}
|
|
8689
|
+
if (this._selectedId !== null && !items.some((item) => item.id === this._selectedId)) {
|
|
8690
|
+
this._selectedId = null;
|
|
8691
|
+
}
|
|
8692
|
+
this._visibleStart = Math.max(0, Math.min(this._visibleStart, Math.max(0, items.length - 1)));
|
|
8693
|
+
this._syncHotspots();
|
|
8694
|
+
this.scene?.markDirty();
|
|
8695
|
+
}
|
|
8696
|
+
/** Move the pool window to start at `start` (the scroll position). */
|
|
8697
|
+
setVisibleStart(start) {
|
|
8698
|
+
const clamped = Math.max(0, Math.min(start, Math.max(0, this._items.length - 1)));
|
|
8699
|
+
if (clamped === this._visibleStart) return;
|
|
8700
|
+
this._visibleStart = clamped;
|
|
8701
|
+
this._syncHotspots();
|
|
8702
|
+
this.scene?.markDirty();
|
|
8703
|
+
}
|
|
8704
|
+
/**
|
|
8705
|
+
* Keep one hotspot per visible slot, positioned over it. The pool is sized
|
|
8706
|
+
* to the viewport (capacity), each slot re-bound to whatever item currently
|
|
8707
|
+
* occupies it — the `Tree._syncHotspots` shape, minus tree structure.
|
|
8708
|
+
*/
|
|
8709
|
+
_syncHotspots() {
|
|
8710
|
+
const capacity = this._visibleCapacity ?? Math.max(1, Math.ceil(this.height / this._rowHeight));
|
|
8711
|
+
const need = Math.max(0, Math.min(capacity, this._items.length - this._visibleStart));
|
|
8712
|
+
while (this._hotspots.length < need) {
|
|
8713
|
+
const hotspot = new AggregateItemHotspot(this);
|
|
8714
|
+
this._hotspots.push(hotspot);
|
|
8715
|
+
this.add(hotspot);
|
|
8716
|
+
}
|
|
8717
|
+
while (this._hotspots.length > need) {
|
|
8718
|
+
const hotspot = this._hotspots.pop();
|
|
8719
|
+
this.scene?.detachA11y(hotspot);
|
|
8720
|
+
this.remove(hotspot);
|
|
8721
|
+
}
|
|
8722
|
+
for (let slot = 0; slot < need; slot++) {
|
|
8723
|
+
const index = this._visibleStart + slot;
|
|
8724
|
+
const hotspot = this._hotspots[slot];
|
|
8725
|
+
hotspot.bind(index, this._items[index]);
|
|
8726
|
+
hotspot.x = 0;
|
|
8727
|
+
hotspot.y = slot * this._rowHeight;
|
|
8728
|
+
hotspot.width = this.width;
|
|
8729
|
+
hotspot.height = this._rowHeight;
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
8732
|
+
/** Whether `id` owns the roving tab stop: active, else selected, else first. */
|
|
8733
|
+
isTabStop(id) {
|
|
8734
|
+
const anchor = (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) ?? this._items[0]?.id;
|
|
8735
|
+
return id === anchor;
|
|
8736
|
+
}
|
|
8737
|
+
isSelected(id) {
|
|
8738
|
+
return this._selectedId === id;
|
|
8739
|
+
}
|
|
8740
|
+
/** Item role the hotspots announce (kept on the parent: one source of truth). */
|
|
8741
|
+
get itemRole() {
|
|
8742
|
+
return this._itemRole;
|
|
8743
|
+
}
|
|
8744
|
+
/** Activate an item (pointer tap-through or Enter/Space): selects and notifies. */
|
|
8745
|
+
activateItem(id, focusIt = false) {
|
|
8746
|
+
const index = this._items.findIndex((item) => item.id === id);
|
|
8747
|
+
if (index === -1) return;
|
|
8748
|
+
this._activeId = id;
|
|
8749
|
+
this._selectedId = id;
|
|
8750
|
+
this._onActivate?.(this._items[index], index);
|
|
8751
|
+
if (focusIt) this.focusItem(id);
|
|
8752
|
+
this.scene?.markDirty();
|
|
8753
|
+
}
|
|
8754
|
+
/** Move keyboard focus to the hotspot currently bound to `id`, if any. */
|
|
8755
|
+
focusItem(id) {
|
|
8756
|
+
this._hotspots.find((hotspot) => hotspot.itemId === id)?.focus();
|
|
8757
|
+
}
|
|
8758
|
+
/**
|
|
8759
|
+
* Aggregate keyboard model: Up/Down move the active item (scrolling the pool
|
|
8760
|
+
* window so it stays bound), Home/End jump, Enter/Space activate. The active
|
|
8761
|
+
* item is focused as it moves so focus is never stranded on a re-bound slot.
|
|
8762
|
+
*/
|
|
8763
|
+
handleItemKey(e, id) {
|
|
8764
|
+
const index = this._items.findIndex((item) => item.id === id);
|
|
8765
|
+
if (index === -1) return;
|
|
8766
|
+
let next = -1;
|
|
8767
|
+
switch (e.key) {
|
|
8768
|
+
case "ArrowDown":
|
|
8769
|
+
next = Math.min(index + 1, this._items.length - 1);
|
|
8770
|
+
break;
|
|
8771
|
+
case "ArrowUp":
|
|
8772
|
+
next = Math.max(index - 1, 0);
|
|
8773
|
+
break;
|
|
8774
|
+
case "Home":
|
|
8775
|
+
next = 0;
|
|
8776
|
+
break;
|
|
8777
|
+
case "End":
|
|
8778
|
+
next = this._items.length - 1;
|
|
8779
|
+
break;
|
|
8780
|
+
case "Enter":
|
|
8781
|
+
case " ":
|
|
8782
|
+
e.preventDefault();
|
|
8783
|
+
this.activateItem(id, true);
|
|
8784
|
+
return;
|
|
8785
|
+
default:
|
|
8786
|
+
return;
|
|
8787
|
+
}
|
|
8788
|
+
e.preventDefault();
|
|
8789
|
+
this._activeId = this._items[next].id;
|
|
8790
|
+
this._ensureVisible(next);
|
|
8791
|
+
this.focusItem(this._activeId);
|
|
8792
|
+
this.scene?.markDirty();
|
|
8793
|
+
}
|
|
8794
|
+
/** Scroll the pool window just enough to keep `index` bound. */
|
|
8795
|
+
_ensureVisible(index) {
|
|
8796
|
+
const capacity = this._visibleCapacity ?? Math.max(1, Math.ceil(this.height / this._rowHeight));
|
|
8797
|
+
if (index < this._visibleStart) this.setVisibleStart(index);
|
|
8798
|
+
else if (index >= this._visibleStart + capacity) {
|
|
8799
|
+
this.setVisibleStart(index - capacity + 1);
|
|
8800
|
+
} else {
|
|
8801
|
+
this._syncHotspots();
|
|
8802
|
+
}
|
|
8803
|
+
}
|
|
8804
|
+
update(_dt, _time) {
|
|
8805
|
+
super.update(_dt, _time);
|
|
8806
|
+
this._syncHotspots();
|
|
8807
|
+
}
|
|
8808
|
+
};
|
|
8809
|
+
var AggregateItemHotspot = class extends Entity {
|
|
8810
|
+
constructor(aggregate) {
|
|
8811
|
+
super();
|
|
8812
|
+
this.aggregate = aggregate;
|
|
8813
|
+
this.interactive = true;
|
|
8814
|
+
this.on("click", () => this.aggregate.activateItem(this.itemId, true));
|
|
8815
|
+
this.on("keydown", (e) => this.aggregate.handleItemKey(e, this.itemId));
|
|
8816
|
+
}
|
|
8817
|
+
aggregate;
|
|
8818
|
+
itemId = "";
|
|
8819
|
+
_index = 0;
|
|
8820
|
+
_label = "";
|
|
8821
|
+
bind(index, item) {
|
|
8822
|
+
this._index = index;
|
|
8823
|
+
this.itemId = item.id;
|
|
8824
|
+
this._label = item.label;
|
|
8825
|
+
}
|
|
8826
|
+
/** The aggregate positions and sizes one pooled hotspot per visible slot. */
|
|
8827
|
+
getLayoutControlledProperties() {
|
|
8828
|
+
return ["x", "y", "width", "height"];
|
|
8829
|
+
}
|
|
8830
|
+
getA11yAttributes() {
|
|
8831
|
+
return {
|
|
8832
|
+
role: this.aggregate.itemRole,
|
|
8833
|
+
label: this._label,
|
|
8834
|
+
// 1-based position within the FULL set, not the pool: without these a
|
|
8835
|
+
// virtualized window announces the pool position as the set position.
|
|
8836
|
+
posInSet: this._index + 1,
|
|
8837
|
+
setSize: this.aggregate.itemCount,
|
|
8838
|
+
selected: this.aggregate.isSelected(this.itemId),
|
|
8839
|
+
// Roving tabindex: only the active item is a tab stop; arrows move within.
|
|
8840
|
+
tabIndex: this.aggregate.isTabStop(this.itemId) ? 0 : -1,
|
|
8841
|
+
// The owner keeps the pointer (canvas handling or an underlying
|
|
8842
|
+
// selectable-text mirror); this hotspot exists for semantics + keyboard
|
|
8843
|
+
// focus, so it opts out of hit-testing. Keyboard focus and
|
|
8844
|
+
// AT-synthesized `click` still work under `pointer-events:none`.
|
|
8845
|
+
pointerEvents: "none"
|
|
8846
|
+
};
|
|
8847
|
+
}
|
|
8848
|
+
render() {
|
|
8849
|
+
}
|
|
8850
|
+
/** Axis-aligned hit-test against the hotspot slot (the `UIComponent` default). */
|
|
8851
|
+
isPointInside(globalX, globalY) {
|
|
8852
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
8853
|
+
if (!local) return false;
|
|
8854
|
+
return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
|
|
8855
|
+
}
|
|
8856
|
+
};
|
|
8857
|
+
|
|
7956
8858
|
// src/components/GridTextEntity.ts
|
|
7957
8859
|
var GridTextEntity = class extends Entity {
|
|
7958
8860
|
fontSize;
|
|
@@ -8648,6 +9550,8 @@ export {
|
|
|
8648
9550
|
Circle,
|
|
8649
9551
|
ComputeParticleEntity,
|
|
8650
9552
|
DEFAULT_CONTENT_SEMANTIC_BUDGET,
|
|
9553
|
+
DEFAULT_PROJECTION_CAPABILITIES,
|
|
9554
|
+
DEFAULT_SEMANTIC_PROJECTION_POLICY,
|
|
8651
9555
|
DOMPortalEntity,
|
|
8652
9556
|
Entity,
|
|
8653
9557
|
GlyphRasterAtlas,
|
|
@@ -8664,6 +9568,8 @@ export {
|
|
|
8664
9568
|
PARTICLE_OFFSET_VELOCITY_X,
|
|
8665
9569
|
PARTICLE_OFFSET_VELOCITY_Y,
|
|
8666
9570
|
PARTICLE_STRIDE_FLOATS,
|
|
9571
|
+
PROJECTION_AUTO_DOM_BUDGET,
|
|
9572
|
+
PROJECTION_AUTO_HYSTERESIS_FRAMES,
|
|
8667
9573
|
REDUCED_MOTION_FPS,
|
|
8668
9574
|
Rect,
|
|
8669
9575
|
SCENE_OPTION_KEYS,
|
|
@@ -8673,14 +9579,20 @@ export {
|
|
|
8673
9579
|
SplineEntity,
|
|
8674
9580
|
TextEntity,
|
|
8675
9581
|
TextRasterCache,
|
|
9582
|
+
UNKNOWN_PROJECTION_CAPABILITY,
|
|
8676
9583
|
VECTO_USER_TIMING,
|
|
8677
9584
|
VectoJSEvent,
|
|
9585
|
+
VirtualizedSetAggregate,
|
|
8678
9586
|
WebGPUParticleSystemManager,
|
|
8679
9587
|
beginVectoUserTiming,
|
|
8680
9588
|
contentLineInHint,
|
|
8681
9589
|
createWebGLPointRenderer,
|
|
9590
|
+
describeHTMLInCanvasSupport,
|
|
8682
9591
|
endVectoUserTiming,
|
|
9592
|
+
getSemanticProjectionCapabilities,
|
|
9593
|
+
hysteresisVote,
|
|
8683
9594
|
installRendererDevTraps,
|
|
9595
|
+
isDeferrableSemanticNode,
|
|
8684
9596
|
isRendererDevMode,
|
|
8685
9597
|
isSafeUrl,
|
|
8686
9598
|
loadSpline,
|
|
@@ -8689,6 +9601,8 @@ export {
|
|
|
8689
9601
|
ownsKeyboard,
|
|
8690
9602
|
parseColorToRGBA,
|
|
8691
9603
|
polySegmentToBezier,
|
|
9604
|
+
resolveProjection,
|
|
8692
9605
|
sanitizeUrl,
|
|
8693
|
-
setRendererDevMode
|
|
9606
|
+
setRendererDevMode,
|
|
9607
|
+
supportsHTMLInCanvas
|
|
8694
9608
|
};
|