@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/dist/renderer.js CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
 
12
12
 
13
- var _chunkKRUDTGNRjs = require('./chunk-KRUDTGNR.js');
13
+ var _chunkBKXERWADjs = require('./chunk-BKXERWAD.js');
14
14
 
15
15
 
16
16
 
@@ -22,4 +22,4 @@ var _chunkKRUDTGNRjs = require('./chunk-KRUDTGNR.js');
22
22
 
23
23
 
24
24
 
25
- exports.CanvasRenderer = _chunkKRUDTGNRjs.CanvasRenderer; exports.GlyphRasterAtlas = _chunkKRUDTGNRjs.GlyphRasterAtlas; exports.SVGRenderer = _chunkKRUDTGNRjs.SVGRenderer; exports.TextRasterCache = _chunkKRUDTGNRjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkKRUDTGNRjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkKRUDTGNRjs.createWebGLPointRenderer; exports.installRendererDevTraps = _chunkKRUDTGNRjs.installRendererDevTraps; exports.isRendererDevMode = _chunkKRUDTGNRjs.isRendererDevMode; exports.parseColorToRGBA = _chunkKRUDTGNRjs.parseColorToRGBA; exports.setRendererDevMode = _chunkKRUDTGNRjs.setRendererDevMode;
25
+ exports.CanvasRenderer = _chunkBKXERWADjs.CanvasRenderer; exports.GlyphRasterAtlas = _chunkBKXERWADjs.GlyphRasterAtlas; exports.SVGRenderer = _chunkBKXERWADjs.SVGRenderer; exports.TextRasterCache = _chunkBKXERWADjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkBKXERWADjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkBKXERWADjs.createWebGLPointRenderer; exports.installRendererDevTraps = _chunkBKXERWADjs.installRendererDevTraps; exports.isRendererDevMode = _chunkBKXERWADjs.isRendererDevMode; exports.parseColorToRGBA = _chunkBKXERWADjs.parseColorToRGBA; exports.setRendererDevMode = _chunkBKXERWADjs.setRendererDevMode;
package/dist/renderer.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  isRendererDevMode,
10
10
  parseColorToRGBA,
11
11
  setRendererDevMode
12
- } from "./chunk-RUM2KEDI.mjs";
12
+ } from "./chunk-AISSDI6U.mjs";
13
13
  export {
14
14
  CanvasRenderer,
15
15
  GlyphRasterAtlas,
package/dist/text.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkPOPXUPKRjs = require('./chunk-POPXUPKR.js');
5
+ var _chunkHC5EZOZJjs = require('./chunk-HC5EZOZJ.js');
6
6
 
7
7
  // src/text/index.ts
8
8
  var _text = require('@vectojs/text'); _createStarExport(_text);
9
9
 
10
10
 
11
11
 
12
- exports.MSDFTextEntity = _chunkPOPXUPKRjs.MSDFTextEntity; exports.SVGEntity = _chunkPOPXUPKRjs.SVGEntity;
12
+ exports.MSDFTextEntity = _chunkHC5EZOZJjs.MSDFTextEntity; exports.SVGEntity = _chunkHC5EZOZJjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MSDFTextEntity,
3
3
  SVGEntity
4
- } from "./chunk-7PYD5UDX.mjs";
4
+ } from "./chunk-CIRZ3S2Z.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -502,6 +502,20 @@ export interface ListenerOptions {
502
502
  /** Register the listener for the capture phase (root→target) instead of bubble. */
503
503
  capture?: boolean;
504
504
  }
505
+ /**
506
+ * Origin transport of a dispatched {@link VectoJSEvent} (input-dispatch-contract
507
+ * v2 §4 rule 1: source attribution; gesture stickiness and single delivery are
508
+ * judged against this field).
509
+ *
510
+ * `'mirror'` is the a11y-shadow transport that ships today; `'canvas'` and
511
+ * `'at'` are reserved for the canvas-first dispatcher and AT-synthesized
512
+ * reflection. `'dom'` is owned by `@vectojs/dom` (RFC2): the live projected
513
+ * element counts as a materialized target at its point, so its bridge tags
514
+ * every event it forwards. The open tail leaves room for future backends
515
+ * without touching this type. `undefined` means "unattributed" (legacy call
516
+ * sites and synthetic dispatches predate the contract).
517
+ */
518
+ export type VectoEventSource = 'mirror' | 'canvas' | 'at' | (string & {});
505
519
  /**
506
520
  * A propagating event dispatched through the entity tree by
507
521
  * {@link Entity.dispatchEvent} (DOM-like capture + bubble).
@@ -523,10 +537,15 @@ export declare class VectoJSEvent<N = unknown> {
523
537
  readonly nativeEvent: N | undefined;
524
538
  /** Whether the event bubbles past its target (capture always runs). */
525
539
  readonly bubbles: boolean;
540
+ /**
541
+ * Which transport delivered this event ({@link VectoEventSource}).
542
+ * `undefined` for unattributed legacy/synthetic dispatches.
543
+ */
544
+ readonly source: VectoEventSource | undefined;
526
545
  private readonly explicitScenePoint;
527
546
  private stopped;
528
547
  private stoppedImmediate;
529
- constructor(type: VectoEvent, target: Entity, nativeEvent?: N, bubbles?: boolean, scenePoint?: Point);
548
+ constructor(type: VectoEvent, target: Entity, nativeEvent?: N, bubbles?: boolean, scenePoint?: Point, source?: VectoEventSource);
530
549
  /** Stop the event from reaching the next node in the propagation path. */
531
550
  stopPropagation(): void;
532
551
  /** Stop propagation AND skip any remaining listeners on the current node. */
@@ -712,6 +731,41 @@ export declare abstract class Entity {
712
731
  * faint-but-live control.
713
732
  */
714
733
  a11yHidden: boolean;
734
+ /**
735
+ * Opt-in to the DOM visual projection (RFC2, implemented by `@vectojs/dom`).
736
+ * Plain data only — core never materializes an element from this; a
737
+ * registered `ProjectionBackend` does.
738
+ *
739
+ * Name mapping: this field IS the RFC4 §2 `projection` policy
740
+ * (`ProjectionPolicy`: `'canvas' | 'dom' | 'auto'` in
741
+ * `tree/scene/ProjectionPolicy.ts`) — predates the RFC, so the field keeps
742
+ * its name and the RFC spelling lives on the value type.
743
+ *
744
+ * - `'canvas'` (default): today's rendering. Zero behavior change.
745
+ * - `'dom'`: the node materializes as a live `HTMLElement` positioned by its
746
+ * world matrix. The live element replaces the transparent a11y mirror
747
+ * (same single-delivery reasoning as `DOMPortalEntity`), so opting in
748
+ * requires a DOM backend and a DOM environment for the AT representation.
749
+ * - `'auto'`: negotiated per node per frame by `Scene.resolveProjectionFor`
750
+ * (RFC4 §3: explicit beats automatic, fallbacks reported with reasons,
751
+ * hysteresis against flip-flop, no-DOM short-circuits to canvas).
752
+ */
753
+ domPolicy: 'canvas' | 'dom' | 'auto';
754
+ /**
755
+ * Backend-side creation hint for a `'dom'`-policy node (tag/content mapping
756
+ * key, e.g. `'text' | 'button' | 'input' | 'container' | 'transform'`). Plain
757
+ * string so custom nodes need no DOM types in core; `@vectojs/dom` owns the
758
+ * registry. `''` means a plain positioned `div` with no content sync.
759
+ */
760
+ domKind: string;
761
+ /**
762
+ * Internal cache owned by the DOM backend: true while the node has a live
763
+ * projected element. The render walk reads it to skip canvas paint for
764
+ * DOM-resident nodes (so a mounted node is never double-drawn) and falls
765
+ * through to canvas while false (SSR / no backend registered). Do not set
766
+ * by hand — the backend sets it on mount and clears it on unmount.
767
+ */
768
+ domResident: boolean;
715
769
  /**
716
770
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
717
771
  * rendering. Combined with translating a content child, this is how
@@ -1123,6 +1177,10 @@ export declare abstract class Entity {
1123
1177
  * `selectable` is set — natively selectable. Returns `null` by default.
1124
1178
  * Read on the a11y sync cadence, so text changes propagate automatically.
1125
1179
  *
1180
+ * This is the descriptor half of the `ContentProjection` row of
1181
+ * `ProjectionBackend` (`tree/scene/ProjectionBackend.ts`): the entity
1182
+ * describes *what* to project, the scene owns *how* it is materialized.
1183
+ *
1126
1184
  * @param hint - Optional advice about which part of the entity is worth
1127
1185
  * describing. Purely an optimization: ignoring it is always correct, which
1128
1186
  * is why it is a parameter rather than a required contract change. See
@@ -13,6 +13,7 @@ export interface IWebGPUParticleSystemManager {
13
13
  import { Entity } from './Entity';
14
14
  import { IRenderer } from '../renderer/IRenderer';
15
15
  import type { WebGLDrawStats } from '../renderer/WebGLPointRenderer';
16
+ import type { ProjectionBackend, ProjectionBackendKind } from './scene/ProjectionBackend';
16
17
  import { type WasmModuleSource, type WasmTransformBackend } from '../wasm/backend';
17
18
  import type { CoreWasmRuntime } from '../wasm/runtime';
18
19
  import { type HitModuleSource, type HitTestBackend } from '../wasm/hit-backend';
@@ -20,9 +21,12 @@ import { type AnimModuleSource, type AnimBackend } from '../wasm/anim-backend';
20
21
  import { type ParticleModuleSource, type ParticleBackend } from '../wasm/particle-backend';
21
22
  import { type OverlayGeometry } from './scene/CanvasGeometry';
22
23
  import { type DirtyReasonEntry, type DirtySource } from './scene/DirtyTracker';
24
+ import type { DomHitCandidate, HitResult } from './scene/HitResult';
23
25
  import { type RenderPhase, type RenderPhaseEntry } from './scene/PhaseTimer';
24
26
  import { type AcceleratorReason, type AcceleratorReport, type AcceleratorStatus } from './scene/WasmBackendFacade';
25
27
  import { type SceneKeyEvent, type SceneShortcutSpec } from './scene/keyboard';
28
+ import { type SemanticProjectionPolicy } from './scene/SemanticProjectionPolicy';
29
+ import { type ProjectionCapabilityRow, type ProjectionResolution, type SceneProjectionCapabilities } from './scene/ProjectionPolicy';
26
30
  export type { RenderPhase, RenderPhaseEntry };
27
31
  export { normalizeChord } from './scene/keyboard';
28
32
  export type { SceneKeyEvent, SceneShortcutSpec } from './scene/keyboard';
@@ -96,6 +100,28 @@ export interface SceneOptions {
96
100
  * Also settable later via {@link Scene.a11ySyncInterval}.
97
101
  */
98
102
  a11ySyncInterval?: number;
103
+ /**
104
+ * Per-node semantic projection policy (RFC3 §5, CTX-0599). Consulted by the
105
+ * single per-node decision point (`shouldProjectA11y`); the framework-known
106
+ * default projects everything the legacy predicate projects, so omitting
107
+ * this changes nothing. Also settable later via
108
+ * {@link Scene.semanticProjectionPolicy}.
109
+ */
110
+ semanticProjectionPolicy?: SemanticProjectionPolicy;
111
+ /**
112
+ * Consecutive syncs an `'auto'`-policy node must keep voting for the other
113
+ * backend before its sticky resolution flips (RFC4 §3 rule 3). Defaults to
114
+ * {@link PROJECTION_AUTO_HYSTERESIS_FRAMES}. Also settable later via
115
+ * {@link Scene.projectionHysteresisFrames}.
116
+ */
117
+ projectionHysteresisFrames?: number;
118
+ /**
119
+ * Maximum `'auto'`-resolved DOM residents per scene per frame (RFC4 §4
120
+ * particle-row backstop). Explicit `'dom'` requests bypass it. Defaults to
121
+ * {@link PROJECTION_AUTO_DOM_BUDGET}. Also settable later via
122
+ * {@link Scene.projectionAutoDomBudget}.
123
+ */
124
+ projectionAutoDomBudget?: number;
99
125
  /**
100
126
  * Custom renderer implementation (e.g., ThreeRenderer from @vectojs/three).
101
127
  * If provided, this renderer will be used for drawing rather than the default CanvasRenderer.
@@ -261,7 +287,7 @@ export interface SceneOptions {
261
287
  * against. A new option must be added here too — the test suite asserts the two
262
288
  * stay in sync.
263
289
  */
264
- export declare const SCENE_OPTION_KEYS: readonly ['a11ySyncInterval', 'autoThrottle', 'contentProjection', 'contentProjectionMargin', 'contentSemanticBudget', 'contentSemanticMargin', 'debugA11y', 'disableWindowResize', 'idleFPS', 'maxDPR', 'maxFPS', 'particleBackend', 'pointBackend', 'readingDirection', 'renderer', 'renderMode', 'respectReducedMotion', 'userTiming'];
290
+ export declare const SCENE_OPTION_KEYS: readonly ['a11ySyncInterval', 'autoThrottle', 'contentProjection', 'contentProjectionMargin', 'contentSemanticBudget', 'contentSemanticMargin', 'debugA11y', 'disableWindowResize', 'idleFPS', 'maxDPR', 'maxFPS', 'particleBackend', 'pointBackend', 'readingDirection', 'renderer', 'renderMode', 'respectReducedMotion', 'semanticProjectionPolicy', 'userTiming'];
265
291
  /** Frame-rate the loop is capped to when the OS requests reduced motion. */
266
292
  export declare const REDUCED_MOTION_FPS = 30;
267
293
  export type { AcceleratorReason, AcceleratorStatus, AcceleratorReport };
@@ -507,6 +533,26 @@ export declare class Scene {
507
533
  * frame. See {@link SceneOptions.a11ySyncInterval}.
508
534
  */
509
535
  a11ySyncInterval: number;
536
+ /**
537
+ * Per-node semantic projection policy (RFC3 §5, CTX-0599). Consulted once
538
+ * per node by {@link shouldProjectA11y}; the default projects everything
539
+ * the legacy predicate projects, so reassigning nothing changes nothing.
540
+ * See {@link SceneOptions.semanticProjectionPolicy}.
541
+ */
542
+ semanticProjectionPolicy: SemanticProjectionPolicy;
543
+ /**
544
+ * Consecutive syncs an `'auto'`-policy node must keep voting for the other
545
+ * backend before its sticky resolution flips (RFC4 §3 rule 3, CTX-0601).
546
+ * Documented tunable — see {@link PROJECTION_AUTO_HYSTERESIS_FRAMES}.
547
+ */
548
+ projectionHysteresisFrames: number;
549
+ /**
550
+ * Maximum `'auto'`-resolved DOM residents per scene per frame (RFC4 §4
551
+ * particle-row backstop: bulk-count nodes stay canvas). Explicit `'dom'`
552
+ * requests bypass it. Documented tunable — see
553
+ * {@link PROJECTION_AUTO_DOM_BUDGET}.
554
+ */
555
+ projectionAutoDomBudget: number;
510
556
  /** Timestamp of the last a11y sync, for throttling. */
511
557
  private lastA11ySync;
512
558
  /** True if we skipped an a11y sync during animation and need to sync when at rest. */
@@ -603,6 +649,8 @@ export declare class Scene {
603
649
  * this an embedded canvas stayed at its initial size forever. */
604
650
  private canvasResizeObserver;
605
651
  private dprChangeHandler;
652
+ private dprPollInterval;
653
+ private lastDpr;
606
654
  private focusedA11yElement;
607
655
  /**
608
656
  * Canvas box geometry: the CSS↔logical mapping, overlay layer alignment, and
@@ -698,6 +746,30 @@ export declare class Scene {
698
746
  private activePortalsThisFrame;
699
747
  private activePortalsPrevFrame;
700
748
  private portalEntities;
749
+ /**
750
+ * Generic projection backends (RFC1 §5 vocabulary, `tree/scene/ProjectionBackend.ts`).
751
+ * Render-independent: the interface exchanges only node identity, world
752
+ * matrix, and lifecycle calls, so registering a DOM backend adds no
753
+ * `HTMLElement` surface to core. `@vectojs/dom` owns the only implementation.
754
+ */
755
+ private projectionBackends;
756
+ /** Per-frame seen-sets driving {@link pruneProjectionBackends} (portal-precedent). */
757
+ private domSeenPrevFrame;
758
+ private domSeenThisFrame;
759
+ private domSeenNodes;
760
+ /**
761
+ * Negotiation state (RFC4 §3, CTX-0601): last resolution per node (the
762
+ * per-scene queryable surface behind {@link getProjectionResolutions}),
763
+ * hysteresis votes, per-kind capability overrides, and explicit gesture
764
+ * pins. Plain data only — no `HTMLElement` surface in core.
765
+ */
766
+ private projectionResolutions;
767
+ private projectionHysteresis;
768
+ private projectionCapabilityOverrides;
769
+ private projectionGesturePins;
770
+ /** Frame id the `'auto'` DOM budget count belongs to (bulk backstop). */
771
+ private projectionBudgetFrame;
772
+ private projectionDomAutoCount;
701
773
  private renderOrderCounter;
702
774
  /**
703
775
  * Monotonic render-frame counter, bumped once per authoritative `render()`
@@ -1155,6 +1227,21 @@ export declare class Scene {
1155
1227
  * on the same canvas, restore DPR/size, and repaint.
1156
1228
  */
1157
1229
  private setupGLContextRecovery;
1230
+ /**
1231
+ * One-shot adoption of the window viewport once an initially-detached canvas
1232
+ * gains layout (#817). A full-window scene constructed before its canvas is
1233
+ * attached starts at 0×0, and no window `resize` fires on attachment — so
1234
+ * without this latch the scene would stay unsized forever. The first nonzero
1235
+ * layout box adopts `window.innerWidth/innerHeight` (the same sizing the
1236
+ * window resize handler applies, keeping the full-window contract), then the
1237
+ * observer disconnects: steady-state sizing stays exactly as if the canvas
1238
+ * had been attached before construction. An explicit user `resize()` between
1239
+ * construction and attachment is respected and skips adoption.
1240
+ *
1241
+ * No-op when the scene already has a size (attached at construction) or when
1242
+ * `ResizeObserver` is unavailable (the caller then drives sizing explicitly).
1243
+ */
1244
+ private armAttachmentViewportLatch;
1158
1245
  /**
1159
1246
  * Expose the underlying {@link IRenderer} for advanced direct-draw operations.
1160
1247
  *
@@ -1171,6 +1258,24 @@ export declare class Scene {
1171
1258
  * (`DEC-0019` rule 5).
1172
1259
  */
1173
1260
  findEntityAt(x: number, y: number): Entity | null;
1261
+ /**
1262
+ * The merged hit list for a scene-space point: the canvas spatial test
1263
+ * plus caller-observed DOM-native candidates (mirror / portal /
1264
+ * `dom-visual` extension point for CTX-0598) as ONE ordered candidate
1265
+ * list (`HitResult`, overlay order authoritative).
1266
+ *
1267
+ * Query API alongside {@link findEntityAt} — no dispatch change:
1268
+ * `findEntityAt` keeps its single topmost answer byte-for-byte, keyboard
1269
+ * and AT flows are untouched. For browser pointer coordinates use
1270
+ * {@link findHitsAtClient}, which maps through {@link clientToScene} first.
1271
+ */
1272
+ findHitsAt(x: number, y: number, domCandidates?: DomHitCandidate[]): HitResult[];
1273
+ /**
1274
+ * {@link findHitsAt} for browser viewport coordinates: maps through
1275
+ * {@link clientToScene} so coordinates enter in scene space (RFC5 §2
1276
+ * rule 3) and backend attribution follows.
1277
+ */
1278
+ findHitsAtClient(clientX: number, clientY: number, domCandidates?: DomHitCandidate[]): HitResult[];
1174
1279
  /** Convert browser viewport coordinates into this Scene's logical coordinates. */
1175
1280
  clientToScene(clientX: number, clientY: number): {
1176
1281
  x: number;
@@ -1464,6 +1569,23 @@ export declare class Scene {
1464
1569
  * predicate, which is only tractable while it has one home.
1465
1570
  */
1466
1571
  private shouldProjectA11y;
1572
+ /**
1573
+ * Policy half of {@link shouldProjectA11y} (RFC3 §5, CTX-0599).
1574
+ *
1575
+ * The legacy gates above (interactive, box, `a11yProjection` engagement)
1576
+ * are unchanged; this only maps a {@link SemanticProjectionPolicy} decision
1577
+ * onto project/suppress. Out of the box the default policy returns
1578
+ * `'project'`, so behaviour is identical to having no policy.
1579
+ *
1580
+ * `'defer-to-browser'` always falls back to projection today: no deferral
1581
+ * backend exists (`supportsHTMLInCanvas()` is `false`), so deferring would
1582
+ * silently drop semantics — the exact failure RFC §3 rules out. When a
1583
+ * backend lands, only allow-listed plain display text
1584
+ * ({@link isDeferrableSemanticNode}; never controls) may actually defer. A
1585
+ * throwing policy likewise falls back to projection: a policy must never be
1586
+ * able to drop semantics by accident.
1587
+ */
1588
+ private resolveSemanticProjection;
1467
1589
  /**
1468
1590
  * Whether an `a11yProjection: 'onDemand'` entity is currently engaged enough to
1469
1591
  * deserve a shadow node.
@@ -1593,6 +1715,89 @@ export declare class Scene {
1593
1715
  getA11yTree(): A11yTreeNode[];
1594
1716
  private renderPortalDOM;
1595
1717
  private reconcilePortals;
1718
+ /**
1719
+ * Register a generic projection backend (RFC1 §5, RFC2 P1 CTX-0598).
1720
+ * Registration is idempotent per backend instance. Core drives
1721
+ * mount/update/unmount from the render walk; the backend owns all
1722
+ * medium-specific state.
1723
+ */
1724
+ addProjectionBackend(backend: ProjectionBackend): this;
1725
+ /**
1726
+ * Remove a previously registered projection backend by instance or
1727
+ * {@link ProjectionBackendKind | kind}. Removing does not unmount resident
1728
+ * nodes — unmount the backend first if teardown order matters.
1729
+ */
1730
+ removeProjectionBackend(backend: ProjectionBackend | ProjectionBackendKind): this;
1731
+ /**
1732
+ * Scene-level projection capabilities (RFC4 §3 rule 2, CTX-0601).
1733
+ *
1734
+ * Plain-data feature detection in the shape of the proposed
1735
+ * `scene.inputCapabilities` (input-dispatch-contract-v2 §4): apps and
1736
+ * devtools query where a node _would_ materialize and why, without touching
1737
+ * any backend. Additive only — no existing surface moves.
1738
+ */
1739
+ get projectionCapabilities(): SceneProjectionCapabilities;
1740
+ /**
1741
+ * Register (or replace) the Capability Matrix row for one `domKind`
1742
+ * (RFC4 §4, CTX-0601). Custom kinds start at the unknown-kind default
1743
+ * (`unsupported-kind` → canvas); registering a row is how a custom backend
1744
+ * kind opts into `'auto'` resolution and how tests drive oscillating costs.
1745
+ */
1746
+ registerProjectionCapability(domKind: string, row: ProjectionCapabilityRow): this;
1747
+ /** Matrix row for a node's `domKind` (override wins, unknown → canvas). */
1748
+ getProjectionCapability(node: Entity): ProjectionCapabilityRow;
1749
+ /**
1750
+ * Pin a node's backend for the duration of an active gesture (RFC4 §5 +
1751
+ * input-dispatch-contract-v2 §4 gesture stickiness: no mid-gesture handoff).
1752
+ * The a11y-mirror pointerdown/up/cancel listeners maintain this; DOM-side
1753
+ * gestures are consulted through
1754
+ * {@link ProjectionBackend.hasActiveGesture | backend.hasActiveGesture}.
1755
+ * Refcounted so multi-pointer gestures on one node unpin exactly once.
1756
+ */
1757
+ pinProjectionForGesture(node: Entity): void;
1758
+ /** Release one gesture pin taken by {@link pinProjectionForGesture}. */
1759
+ unpinProjectionForGesture(node: Entity): void;
1760
+ /** Whether `node` currently owns an active gesture on a core mirror. */
1761
+ isProjectionPinned(node: Entity): boolean;
1762
+ /**
1763
+ * Last negotiation for one node, if it was ever resolved on this scene.
1764
+ * Accepts the node or its id; returns the stored record (not a copy — treat
1765
+ * as read-only).
1766
+ */
1767
+ getProjectionResolution(node: Entity | string): ProjectionResolution | undefined;
1768
+ /** Every node's last negotiation, in first-resolution order. */
1769
+ getProjectionResolutions(): readonly ProjectionResolution[];
1770
+ /**
1771
+ * Resolve one node's `domPolicy` to its effective backend (RFC4 §3, CTX-0601).
1772
+ *
1773
+ * Memoized per main frame (`currentFrame`, bumped once per authoritative
1774
+ * render): the render walk and the later a11y sync must agree within a
1775
+ * frame, or a node could lose its mirror the same frame it gains an element.
1776
+ * `'auto'` applies, in order: no-DOM short-circuit → pure negotiation →
1777
+ * gesture/focus hard bars (a pinned node keeps its current backend, never
1778
+ * flips mid-gesture, focus is preserved-or-moved never dropped to `body` per
1779
+ * input-dispatch-contract-v2 §4) → hysteresis stickiness → bulk budget.
1780
+ * Every outcome is recorded on the per-scene queryable surface
1781
+ * ({@link getProjectionResolutions}) with its reason — fallbacks are
1782
+ * reported, not silent (§3 rule 2).
1783
+ */
1784
+ resolveProjectionFor(node: Entity): 'canvas' | 'dom';
1785
+ /**
1786
+ * Whether `node`'s a11y mirror currently owns browser focus (RFC4 §5 focus
1787
+ * rule, input-dispatch-contract-v2 §4: focus stays DOM-based; flipping
1788
+ * `projection` must preserve or deliberately move focus, never drop it to
1789
+ * `body`). A focused node keeps its backend until blur — the actual move, if
1790
+ * any, goes through the existing sentinels (`preserveFocusOnRemoval`, the
1791
+ * DOM backend's own fallback), never a bare removal.
1792
+ */
1793
+ private ownsProjectionFocus;
1794
+ /**
1795
+ * RFC2 P1 (CTX-0598): unmount DOM-projection residents the walk did not see
1796
+ * this frame (viewport-culled, removed without `remove()`, policy-flipped).
1797
+ * Mirrors {@link reconcilePortals} above; unmount is idempotent per the
1798
+ * `ProjectionBackend` contract so notifying every backend is safe.
1799
+ */
1800
+ private pruneProjectionBackends;
1596
1801
  /**
1597
1802
  * The frame-rate cap actually in effect: the explicit {@link maxFPS}, further
1598
1803
  * lowered to {@link REDUCED_MOTION_FPS} when the OS requests reduced motion
@@ -1603,6 +1808,12 @@ export declare class Scene {
1603
1808
  /**
1604
1809
  * Render the entire scene graph onto the specified renderer.
1605
1810
  *
1811
+ * This is the `CanvasProjection` row of {@link ProjectionBackend}:
1812
+ * the per-frame materialization of scene semantics into pixels through the
1813
+ * backend-agnostic `IRenderer` contract. (The `A11yProjection` /
1814
+ * `ContentProjection` rows run on the same cadence via {@link Scene.syncA11y}
1815
+ * below; the `DOMProjection` row does not exist yet — RFC2/CTX-0598.)
1816
+ *
1606
1817
  * Main-frame causal order is a correctness contract:
1607
1818
  *
1608
1819
  * 1. Browser/input callbacks finish before the scheduled frame begins.
@@ -0,0 +1,50 @@
1
+ import type { Entity, Point } from '../Entity';
2
+ /**
3
+ * Which medium painted the hit node (RFC5 §2, CTX-0600).
4
+ *
5
+ * - `'canvas'` — the canvas spatial test (`Scene.findEntityAt`).
6
+ * - `'mirror'` — an `a11yRoot` mirror element under the cursor.
7
+ * - `'portal'` — a `portalRoot` element (`DOMPortalEntity` bridge precedent,
8
+ * `tree/DOMPortalEntity.ts:64-99`).
9
+ * - `'dom-visual'` — extension point for CTX-0598's DOM backend: accepted and
10
+ * attributed through the same predicate path today, without depending on
11
+ * that package existing yet.
12
+ */
13
+ export type HitBackend = 'canvas' | 'dom-visual' | 'mirror' | 'portal';
14
+ /**
15
+ * One candidate in the merged hit list (RFC5 §2).
16
+ *
17
+ * Coordinates enter in scene space and attribution follows (RFC5 §2 rule 3):
18
+ * `worldPoint` is the queried scene-space point, `localPoint` is that point
19
+ * in the node's local space (`Entity.worldToLocal`, `null` when the
20
+ * accumulated transform is singular).
21
+ *
22
+ * `priority` is compositor-layer order, then paint order (RFC5 §2 rule 2 and
23
+ * §3): the overlay subtree sorts above the main tree, and within one subtree
24
+ * DOM-native backends sort above canvas. Computed as
25
+ * `(inOverlaySubtree ? 2 : 0) + (backend === 'canvas' ? 0 : 1)`, so
26
+ * overlay-DOM (3) > overlay-canvas (2) > main-DOM (1) > main-canvas (0).
27
+ * Sort descending; ties keep insertion order (stable sort).
28
+ */
29
+ export interface HitResult {
30
+ node: Entity;
31
+ backend: HitBackend;
32
+ localPoint: Point | null;
33
+ worldPoint: Point;
34
+ priority: number;
35
+ }
36
+ /**
37
+ * A caller-observed DOM-native hit offered to the merge
38
+ * (`HitTester.findHitsAt`). The browser's own hit test over the
39
+ * `pointer-events: auto` element is the geometric authority, so the merge
40
+ * gates the owning node with the same visibility/input predicate as the
41
+ * canvas paths (`disabled` / `pointerEvents: 'none'` exclusion, opacity,
42
+ * `clipChildren`) but does not re-check scene-space `isPointInside`.
43
+ *
44
+ * `'canvas'` is excluded: canvas candidates come from the spatial test, never
45
+ * from the caller.
46
+ */
47
+ export interface DomHitCandidate {
48
+ node: Entity;
49
+ backend: Exclude<HitBackend, 'canvas'>;
50
+ }
@@ -56,6 +56,7 @@
56
56
  * captured at construction without going stale.
57
57
  */
58
58
  import type { Bounds, Entity } from '../Entity';
59
+ import type { DomHitCandidate, HitResult } from './HitResult';
59
60
  import type { WasmBackendFacade } from './WasmBackendFacade';
60
61
  export declare class HitTester {
61
62
  private readonly root;
@@ -76,6 +77,35 @@ export declare class HitTester {
76
77
  * @param height - Scene logical height, for the grid's extent.
77
78
  */
78
79
  findEntityAt(x: number, y: number, frame: number, width: number, height: number): Entity | null;
80
+ /**
81
+ * The merged hit list (RFC5 §2, CTX-0600): the canvas spatial test plus
82
+ * caller-observed DOM-native candidates (mirror / portal / `dom-visual`
83
+ * extension point for CTX-0598) as ONE ordered candidate list.
84
+ *
85
+ * Query API only — no dispatch change: `findEntityAt` keeps its single
86
+ * topmost answer byte-for-byte. Coordinates enter in scene space (callers
87
+ * map client coordinates first, e.g. `Scene.findHitsAtClient`); backend
88
+ * attribution follows. DOM candidates pass through the same
89
+ * `disabled` / `pointerEvents: 'none'` predicate as the canvas paths
90
+ * (RFC5 §2 rule 4); the browser's hit test is their geometric authority,
91
+ * so scene-space `isPointInside` is not re-checked.
92
+ *
93
+ * Overlay order is authoritative (RFC5 §2 rule 2): overlay-subtree
94
+ * candidates sort above main-tree candidates, DOM-native backends above
95
+ * canvas within one subtree. Sorted descending by `priority`; ties keep
96
+ * insertion order (canvas overlay, canvas main, then DOM in caller order).
97
+ */
98
+ findHitsAt(x: number, y: number, frame: number, width: number, height: number, domCandidates?: DomHitCandidate[]): HitResult[];
99
+ /**
100
+ * Attribute one candidate: scene-space point plus node-local point, with
101
+ * the compositor-layer priority (overlay subtree above main tree,
102
+ * DOM-native backends above canvas within one subtree).
103
+ */
104
+ private toHitResult;
105
+ /** Whether `node` lives under the overlay root (drawn above the main tree). */
106
+ private isInOverlaySubtree;
107
+ /** Main-tree arm of {@link findEntityAt}, shared with {@link findHitsAt}. */
108
+ private findMainTreeHit;
79
109
  /**
80
110
  * Refresh the hit-test grid for the CURRENT tree state if it is stale (a
81
111
  * structural or transform change may have happened since the last build —
@@ -0,0 +1,64 @@
1
+ import type { AffineTransform, Entity } from '../Entity';
2
+ /**
3
+ * Stable backend id for telemetry and capability negotiation (cf. RFC4).
4
+ *
5
+ * The three literals name the projections that exist today; the open-ended
6
+ * tail leaves room for RFC2+ backends without touching this type. `'content'`
7
+ * extends the RFC1 sketch's `canvas | dom | a11y` union: the sketch's table
8
+ * already lists `ContentProjection` as a first-class row, so the kind union
9
+ * names it too.
10
+ */
11
+ export type ProjectionBackendKind = 'canvas' | 'dom' | 'a11y' | 'content' | (string & {});
12
+ /**
13
+ * A materialization of scene nodes in one medium: canvas pixels, an
14
+ * `HTMLElement` subtree, an accessibility node, or a future backend's own
15
+ * representation.
16
+ *
17
+ * This is vocabulary, not machinery (RFC1 §5, CTX-0597): it names the
18
+ * mount/update/unmount shape the existing projections already follow so
19
+ * RFC2 (DOM projection), RFC3 (semantic/a11y projection), and RFC4
20
+ * (projection policy) can each name which row they implement or consume.
21
+ * Nothing implements this interface yet — the rows below map each proposed
22
+ * backend onto the code that performs its role today, with the invariant
23
+ * from RFC1 §2: two projections of the same node must agree on geometry
24
+ * (world matrix), visibility, and lifecycle state, while each keeps its own
25
+ * medium-specific representation.
26
+ *
27
+ * | Backend (proposed name) | Exists today as | Mount site |
28
+ * | ----------------------- | --------------- | ---------- |
29
+ * | `CanvasProjection` (`'canvas'`) | the main render pass via `IRenderer` | `Scene.render(renderer)` (`tree/Scene.ts`) → per-entity `Entity.render` (`tree/Entity.ts`) |
30
+ * | `A11yProjection` (`'a11y'`) | `A11yProjectionManager` + `a11yRoot` mirrors | mirror creation in the `Scene.syncA11y` walk (`tree/Scene.ts`); ordering in `A11yProjectionManager` |
31
+ * | `ContentProjection` (`'content'`) | `ContentProjectionManager` + `Entity.getContentProjection` | `Scene.syncContentProjection`, driven on the a11y sync cadence (`tree/Scene.ts`) |
32
+ * | `DOMProjection` (`'dom'`) | **does not exist** — RFC2's work item (CTX-0598) | — (`DOMPortalEntity` is the pilot consumer, not the backend) |
33
+ *
34
+ * Open points deliberately left to RFC2/RFC4 (RFC1 §5): whether `update`
35
+ * receives the full matrix or a decomposed transform, how z-order/layering
36
+ * composes canvas pixels under/over DOM elements, and event forwarding
37
+ * ownership (constrained by `forge/input-dispatch-contract-v2-design.md` §4).
38
+ */
39
+ export interface ProjectionBackend {
40
+ /** Stable backend id for telemetry and capability negotiation (cf. RFC4). */
41
+ readonly kind: ProjectionBackendKind;
42
+ /** Create backend state for a newly materialized node. Idempotent. */
43
+ mount(node: Entity): void;
44
+ /**
45
+ * Reconcile backend state after a layout/transform/visibility change.
46
+ *
47
+ * Takes the node's {@link Entity.getWorldTransform | world transform} as
48
+ * {@link AffineTransform} rather than the sketch's `Float64Array`: that is
49
+ * what the scene computes today, and it converts losslessly to the
50
+ * `matrix3d()` a DOM backend wants. The matrix-vs-decomposed question stays
51
+ * open for RFC2/RFC4 per RFC1 §5.
52
+ */
53
+ update(node: Entity, worldMatrix: AffineTransform): void;
54
+ /** Tear down backend state on removal or policy change. Idempotent. */
55
+ unmount(node: Entity): void;
56
+ /**
57
+ * Whether `node` currently owns an active gesture on this backend (RFC4 §5,
58
+ * CTX-0601). Consulted by the scene's capability negotiation alongside its
59
+ * own mirror pins: a pinned node keeps its backend until gesture end —
60
+ * input-dispatch-contract-v2 §4 gesture stickiness, no mid-gesture handoff.
61
+ * Optional so test fakes stay three methods; absent means "no gestures".
62
+ */
63
+ hasActiveGesture?(node: Entity): boolean;
64
+ }