@vectojs/core 1.32.7 → 1.34.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.
@@ -14,90 +14,16 @@ import { Entity } from './Entity';
14
14
  import { IRenderer } from '../renderer/IRenderer';
15
15
  import type { WebGLDrawStats } from '../renderer/WebGLPointRenderer';
16
16
  import { type WasmModuleSource, type WasmTransformBackend } from '../wasm/backend';
17
- import { type CoreWasmRuntime } from '../wasm/runtime';
17
+ import type { CoreWasmRuntime } from '../wasm/runtime';
18
18
  import { type HitModuleSource, type HitTestBackend } from '../wasm/hit-backend';
19
19
  import { type AnimModuleSource, type AnimBackend } from '../wasm/anim-backend';
20
20
  import { type ParticleModuleSource, type ParticleBackend } from '../wasm/particle-backend';
21
- /**
22
- * A timed phase of a frame.
23
- *
24
- * `render` is the ENCLOSING phase it contains `transform`, `drawWalk` and
25
- * `flush` so it is reported without a share to avoid double-counting.
26
- * `a11ySync` and `a11yOrder` run after `render` in the frame loop, so they are
27
- * siblings of it, not children.
28
- */
29
- export type RenderPhase = 'render' | 'transform' | 'drawWalk' | 'flush' | 'a11ySync'
30
- /**
31
- * Time inside {@link Scene.syncContentGridProjection} materializing DOM
32
- * carriers, nested inside `a11ySync`.
33
- *
34
- * Split out because `a11ySync` for a streaming code block measured 1661-1875 ms
35
- * against a 210-671 ms render, and attributing that to grid materialization was
36
- * an assumption. Nothing should be optimised here on the strength of the parent
37
- * phase alone.
38
- */
39
- | 'gridMaterialize'
40
- /**
41
- * Whole of {@link Scene.syncContentProjection}, nested inside `a11ySync`.
42
- *
43
- * Measured at 99.8-99.9% of `a11ySync` for a streaming code block, so per-node
44
- * a11y attribute and geometry work is not where that phase's cost lives.
45
- */
46
- | 'contentProjection'
47
- /** Per-node a11y attribute/geometry work, excluding content projection and descendants. */
48
- | 'a11yNodes'
49
- /** Whole of `syncContentGridProjection`, of which `gridMaterialize` is one part. */
50
- | 'gridSync'
51
- /**
52
- * Synchronous part of `scheduleContentGridCalibration` — building the probe DOM.
53
- *
54
- * The measurement itself is deferred to a rAF, but the probe is constructed
55
- * here. Measured at 77-80% of `gridSync` on Chrome (3.7-4.5 ms per frame, i.e.
56
- * the entire 240Hz budget) against about 1 ms on Firefox, making it the largest
57
- * remaining cost of projecting a streaming code block once carrier reuse landed.
58
- */
59
- | 'gridCalibrateSchedule'
60
- /** The `querySelectorAll` + per-cell scan inside calibration scheduling. */
61
- | 'calibScan'
62
- /** Probe DOM construction and insertion inside calibration scheduling. */
63
- | 'calibProbeBuild' | 'a11yOrder'
64
- /** Sum of every entity's own render(), nested inside drawWalk. */
65
- | 'entityPaint';
66
- export interface RenderPhaseEntry {
67
- phase: RenderPhase;
68
- totalMs: number;
69
- calls: number;
70
- avgMs: number;
71
- /** Worst single sample — a spiky phase is a different problem from a slow one. */
72
- maxMs: number;
73
- /** Percent of the measured total, or `null` for the enclosing `render` phase. */
74
- share: number | null;
75
- }
76
- /**
77
- * Who marked the scene dirty, and why.
78
- *
79
- * Every field is optional except `reason` so a call site can be as specific as it
80
- * cheaply can — an entity id costs nothing to pass, a property name is often
81
- * already in scope.
82
- */
83
- export interface DirtySource {
84
- /** Entity id responsible, when one is. Omitted for scene-level invalidation. */
85
- entity?: string;
86
- /** Short, stable category — e.g. `'text-changed'`, `'animation'`, `'resize'`. */
87
- reason: string;
88
- /** Property that changed, when the reason alone is ambiguous. */
89
- property?: string;
90
- }
91
- /** An aggregated dirty attribution. */
92
- export interface DirtyReasonEntry {
93
- entity?: string;
94
- reason: string;
95
- property?: string;
96
- /** How many times this exact attribution was recorded. */
97
- count: number;
98
- firstFrame: number;
99
- lastFrame: number;
100
- }
21
+ import { type OverlayGeometry } from './scene/CanvasGeometry';
22
+ import { type DirtyReasonEntry, type DirtySource } from './scene/DirtyTracker';
23
+ import { type RenderPhase, type RenderPhaseEntry } from './scene/PhaseTimer';
24
+ import { type AcceleratorReason, type AcceleratorReport, type AcceleratorStatus } from './scene/WasmBackendFacade';
25
+ export type { RenderPhase, RenderPhaseEntry };
26
+ export type { DirtyReasonEntry, DirtySource };
101
27
  /**
102
28
  * Options for {@link Scene}.
103
29
  */
@@ -301,60 +227,7 @@ export interface SceneOptions {
301
227
  export declare const SCENE_OPTION_KEYS: readonly ['a11ySyncInterval', 'autoThrottle', 'contentProjection', 'contentProjectionMargin', 'contentSemanticBudget', 'contentSemanticMargin', 'debugA11y', 'disableWindowResize', 'maxDPR', 'maxFPS', 'particleBackend', 'pointBackend', 'readingDirection', 'renderer', 'renderMode', 'respectReducedMotion', 'userTiming'];
302
228
  /** Frame-rate the loop is capped to when the OS requests reduced motion. */
303
229
  export declare const REDUCED_MOTION_FPS = 30;
304
- /**
305
- * Why an accelerator did or did not run on the most recent frame.
306
- *
307
- * `'active'` is the only value that means the accelerator ran. Everything else
308
- * is a distinct decline, kept separate because they call for different actions:
309
- * `'not-installed'` means enable it, `'below-gate'` means the workload is too
310
- * small to be worth it (working as designed), and `'rejected'` means the kernel
311
- * refused its arguments — a fault worth reporting, not a tuning outcome.
312
- */
313
- export type AcceleratorReason =
314
- /** Ran on this frame. */
315
- 'active'
316
- /** No backend installed; the JS path is the permanent fallback. */
317
- | 'not-installed'
318
- /** Installed, but the per-frame gate chose JS (workload below threshold). */
319
- | 'below-gate'
320
- /** Installed and gated in, but the kernel rejected the call and wrote nothing. */
321
- | 'rejected'
322
- /** Not applicable to this pass (e.g. a non-main renderer, or nothing to do). */
323
- | 'not-applicable';
324
- /**
325
- * One accelerator's per-frame status, read from {@link Scene.accelerators}.
326
- *
327
- * The pair exists because `available` and `activeThisFrame` genuinely differ:
328
- * before this shape, `transformBackend`/`animBackend` reported only that a
329
- * backend was *installed*, which invites concluding an accelerator is doing work
330
- * when its gate never opens. Read `activeThisFrame` for what actually happened
331
- * and `reason` for why.
332
- */
333
- export interface AcceleratorStatus {
334
- /** A backend is installed and could run, gate permitting. */
335
- available: boolean;
336
- /** It ran on the most recent frame. */
337
- activeThisFrame: boolean;
338
- /** Why it did or did not run. */
339
- reason: AcceleratorReason;
340
- /** Which implementation actually did the work on the most recent frame. */
341
- path: string;
342
- }
343
- /**
344
- * Per-frame status of every invisible accelerator, read from
345
- * {@link Scene.accelerators}. Each is independent: a scene can compose
346
- * transforms in WASM while ticking drivers in JS.
347
- */
348
- export interface AcceleratorReport {
349
- /** World-matrix composition (`compose_simd`). */
350
- transform: AcceleratorStatus;
351
- /** Batched property drivers (`spring_step`/`tween_step`). */
352
- animation: AcceleratorStatus;
353
- /** Hit-test broad phase (`hit_build`/`hit_query`) and its gather source. */
354
- hitTest: AcceleratorStatus;
355
- /** Particle simulation — WebGPU compute, the WASM CPU kernel, or JS. */
356
- particle: AcceleratorStatus;
357
- }
230
+ export type { AcceleratorReason, AcceleratorStatus, AcceleratorReport };
358
231
  /**
359
232
  * Live render-loop telemetry, read from {@link Scene.frameStats}. See that
360
233
  * getter for how each field is measured.
@@ -470,11 +343,16 @@ export declare class Scene {
470
343
  * event-driven UIs where idle frames should cost ~0.
471
344
  */
472
345
  renderMode: 'always' | 'onDemand';
473
- /** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
474
- private static readonly MAX_DIRTY_REASONS;
475
- private _phaseTiming;
476
- private _userTiming;
477
- private _phaseTotals;
346
+ /**
347
+ * Per-phase frame timing and the browser User Timing flag.
348
+ *
349
+ * Extracted ahead of extraction 3 because the content-grid calibration pass is
350
+ * instrumented (`calibScan`, `calibProbeBuild`) and could not move while its
351
+ * only remaining dependency was a `Scene` private. Nine methods across four
352
+ * domains write phases, so this is a shared leaf rather than any one domain's
353
+ * property — see `DEC-0021`.
354
+ */
355
+ private readonly phases;
478
356
  /**
479
357
  * Start or stop per-phase render timing.
480
358
  *
@@ -500,15 +378,6 @@ export declare class Scene {
500
378
  setUserTiming(enabled: boolean): void;
501
379
  /** Whether browser User Timing phase instrumentation is enabled. */
502
380
  get userTiming(): boolean;
503
- /**
504
- * Accumulate one phase sample.
505
- *
506
- * Totals rather than a per-frame log: the question is always "which phase owns
507
- * the frame", and a log of thousands of samples answers it less directly while
508
- * costing far more memory. `maxMs` is kept because a phase that is cheap on
509
- * average but spikes is a different problem from one that is uniformly slow.
510
- */
511
- private _recordPhase;
512
381
  /**
513
382
  * Recorded phase timings, most expensive first, with each phase's share of the
514
383
  * measured total.
@@ -519,9 +388,24 @@ export declare class Scene {
519
388
  get renderPhases(): RenderPhaseEntry[];
520
389
  /** Drop recorded phase timings, keeping timing enabled. */
521
390
  clearRenderPhases(): void;
522
- private _dirtyTracking;
523
- private _dirtyReasons;
524
- private dirty;
391
+ /**
392
+ * The dirty flag and its opt-in attribution (extraction 6, `DEC-0025`).
393
+ *
394
+ * A field initializer rather than a constructor assignment: it needs no
395
+ * injected input, so it follows {@link phases} and {@link a11yOrder} rather
396
+ * than the definite-assignment collaborators.
397
+ */
398
+ private readonly _dirty;
399
+ /**
400
+ * The redraw-pending flag, under its original name.
401
+ *
402
+ * `Scene.test.ts:2153` assigns `false` and `:2158` reads it, and the suite is
403
+ * unedited (`DEC-0019` rule 4). `private` is correct here rather than
404
+ * `protected` — unlike the extraction 1/3/4 accessors, this one still has
405
+ * in-class readers (`loop`, `step`, `frameStats`).
406
+ */
407
+ private get dirty();
408
+ private set dirty(value);
525
409
  /** Whether to throttle rendering to 2 FPS when the scene is static to save power. */
526
410
  autoThrottle: boolean;
527
411
  /** Wall-clock ms spent inside the last `render()` call. */
@@ -594,33 +478,6 @@ export declare class Scene {
594
478
  * {@link Entity.getContentEpoch}. (carryctx CTX-0199)
595
479
  */
596
480
  private contentSyncState;
597
- /** Pending cold font-calibration frame per projected grid entity. */
598
- private contentGridCalibrationFrames;
599
- /** Detached, untransformed font probes used by the cold calibration pass. */
600
- private contentGridCalibrationProbes;
601
- /**
602
- * Monotonic stamp identifying the conditions grid cells were calibrated under.
603
- *
604
- * Calibration measures the difference between the advance the canvas grid assigns
605
- * a cluster and the width the browser lays it out at, then writes a per-cell
606
- * `scaleX`. That result stays valid until the font or the page scale changes, and
607
- * it lives on the cell element — so a cell carrying this stamp needs no further
608
- * work.
609
- *
610
- * The scan that feeds calibration was O(cells) on every revision bump: for a
611
- * streaming code block it re-derived a measurement key for every cell in the
612
- * block each frame in order to produce only ~20 distinct keys, costing about
613
- * 2.5 ms/frame after the `style.font` fix and still over half of `a11ySync`. Since
614
- * carrier reuse (#244) leaves untouched lines — and therefore their calibrated
615
- * transforms — in place, cells stamped with the current generation can simply be
616
- * skipped, making the scan O(new cells) instead.
617
- *
618
- * A plain incrementing integer rather than the descriptive calibration key,
619
- * because it goes into an attribute selector and must not need escaping.
620
- */
621
- private contentGridCalibrationGeneration;
622
- /** The `(fontEpoch, pageScale)` pair the current generation corresponds to. */
623
- private contentGridCalibrationStamp;
624
481
  /** Invalidates grid font calibration after browser font availability changes. */
625
482
  private contentFontEpoch;
626
483
  /**
@@ -681,37 +538,6 @@ export declare class Scene {
681
538
  private contentSemanticBudget;
682
539
  private contentSemanticBudgetLeft;
683
540
  private contentSemanticDeferred;
684
- /**
685
- * Per-sync memo of "does the document hold a selection at all".
686
- *
687
- * Reading ANY property of a `Selection` (`anchorNode`, `rangeCount`, `type`,
688
- * `isCollapsed`) forces a synchronous layout, because Blink validates the
689
- * selection against current box geometry before answering. Measured in real
690
- * Chrome against a 1000-carrier subtree with layout dirtied between reads:
691
- * `anchorNode` 0.5ms, `rangeCount` 0.4ms, `type` 0.5ms, `isCollapsed` 0.5ms —
692
- * all indistinguishable from `offsetHeight` (0.5ms), against a 0ms floor for
693
- * mutating without reading. So there is no cheap property to probe with; the
694
- * only way to avoid the layout is to not touch the object at all.
695
- *
696
- * Materializing a block rebuilds its carriers, which asks whether the rebuild
697
- * would destroy a selection. Once per block, that read cost a forced layout
698
- * over the whole (and growing) projection subtree, which is what made
699
- * per-block cost rise with resident count: profiled at 1973 forced layouts
700
- * totalling 633ms of an 847ms 1000-block drain (75%).
701
- *
702
- * A selection is a single document-wide object and a sync walk cannot yield to
703
- * the user, so its presence cannot change mid-walk. Resolving it once per walk
704
- * turns O(blocks) forced layouts into O(1). `null` = not yet resolved.
705
- */
706
- private contentSelectionPresentThisSync;
707
- /**
708
- * True while a text-selection drag that started on a projection's blank
709
- * region (no text node under the press) is being driven manually — the
710
- * browser has no native anchor for it, so mousemove extends the Selection
711
- * from the position we resolved ourselves.
712
- */
713
- private blankRegionSelectionDrag;
714
- private contentSelectionAnchor;
715
541
  private contentSelectionEndListener;
716
542
  private frameHadAnimation;
717
543
  private frameHadInteractive;
@@ -729,10 +555,25 @@ export declare class Scene {
729
555
  private canvasResizeObserver;
730
556
  private dprChangeHandler;
731
557
  private focusedA11yElement;
732
- /** Last geometry `syncOverlayGeometry` wrote, so an unchanged frame can skip the
733
- * style writes entirely. Reset to `null` to force the next sync (a new overlay
734
- * layer was created and has never been positioned). */
735
- private _overlayGeometry;
558
+ /**
559
+ * Canvas box geometry: the CSS↔logical mapping, overlay layer alignment, and
560
+ * the DPR math (extraction 5, `DEC-0024`).
561
+ *
562
+ * Definite-assignment because it holds `a11yRoot` and `portalRoot`, which the
563
+ * constructor only decides partway through — the same shape as
564
+ * {@link _contentProjection}.
565
+ */
566
+ private _geometry;
567
+ /**
568
+ * The overlay memo, for the unedited suite.
569
+ *
570
+ * `OverlayGeometrySkip.test.ts:75,143` assigns `null` to force the next sync,
571
+ * so the setter delegates to `invalidateOverlay()`. `protected` rather than
572
+ * `private` because it has no in-class reader and `noUnusedLocals` fails the
573
+ * build on a private with none (`DEC-0019` rule 4).
574
+ */
575
+ protected get _overlayGeometry(): OverlayGeometry | null;
576
+ protected set _overlayGeometry(value: OverlayGeometry | null);
736
577
  /** Shadow elements the pointer is currently inside. Lets a removal that happens
737
578
  * mid-hover synthesize the `pointerleave` the browser never sends for a
738
579
  * detached element, so the entity doesn't keep its hover state. */
@@ -751,33 +592,60 @@ export declare class Scene {
751
592
  * screen-reader virtual cursor inside the scene's a11y region. */
752
593
  private focusSentinel;
753
594
  private caretBlinkTimer;
754
- a11yNeedsReorder: boolean;
755
- private portalRoot;
756
- private fullViewportElements;
757
- private normalElements;
758
- private activeIds;
759
- /** Per-parent insertion cursor, reused by `enforceA11yDomOrder`. */
760
- private a11yOrderCursors;
761
- /** Membership set for the elements being ordered, reused per reorder pass. */
762
- private a11yOrderMembers;
763
595
  /**
764
- * Elements that are an *ancestor* of another ordered element, reused per pass.
596
+ * The projected DOM's ordering engine the reorder flag, the per-pass scratch
597
+ * collections, the visual reading-order sort and the cursor-based
598
+ * `insertBefore` pass (extraction 2, `DEC-0020`).
599
+ *
600
+ * `a11yNeedsReorder` and `enforceA11yDomOrder` keep their names on `Scene` and
601
+ * delegate here; the collect-and-prune walk stays behind because it needs
602
+ * {@link shouldProjectA11y} and the focus/caret state.
603
+ */
604
+ private readonly a11yOrder;
605
+ /**
606
+ * The content projection's selection preservation and grid calibration
607
+ * (extraction 3, `DEC-0022`).
765
608
  *
766
- * A composite widget's container (a `grid` around its rows, a `tree` around its
767
- * items) spans every descendant row, so it must not extend a visual row band —
768
- * see {@link sortNormalElementsVisually}.
609
+ * Definite-assignment because it needs `a11yRoot`, which the constructor
610
+ * creates partway through the same shape as {@link _wasmBackend}. Every
611
+ * constructor use is inside a deferred event listener, so it is always
612
+ * assigned before anything can reach it.
769
613
  */
770
- private a11yOrderContainers;
614
+ private _contentProjection;
615
+ /** Grid carrier materialization (the projection walk's separable leaf). */
616
+ private _gridProjector;
771
617
  /**
772
- * Nearest `clipChildren` ancestor per ordered element — its *region* — reused
773
- * per pass. Written by `enforceA11yDomOrder`'s collect walk, which already has
774
- * the entity in hand, so a region costs one comparison per node rather than an
775
- * ancestor walk per element.
618
+ * Pending grid-calibration frames, keyed by entity id.
776
619
  *
777
- * Absent means the element sits under no clipping ancestor and belongs to the
778
- * implicit root region. See {@link sortNormalElementsVisually}.
620
+ * Delegates to {@link ContentProjectionManager}. Kept on `Scene` under its
621
+ * original name because the text-projection e2e reads
622
+ * `scene.contentGridCalibrationFrames.size` to assert calibration is not left
623
+ * in flight after a rebuild. `protected` rather than `private`: there is no
624
+ * in-class reader, and `private` plus no reader fails `noUnusedLocals`
625
+ * (`DEC-0019` rule 4).
779
626
  */
780
- private a11yOrderRegions;
627
+ protected get contentGridCalibrationFrames(): ReadonlyMap<string, number>;
628
+ /**
629
+ * Index of the carrier line holding a selection inside `el`, or `null`.
630
+ *
631
+ * Delegates to {@link ContentProjectionManager}. Kept under its original name
632
+ * because `ContentGridSelectionWindow.test.ts` calls it through a cast to
633
+ * assert the selection-window behaviour. `protected` per `DEC-0019` rule 4 —
634
+ * the in-class callers now go through the manager directly.
635
+ */
636
+ protected contentGridSelectionLine(el: HTMLElement): number | null;
637
+ /**
638
+ * Whether the projected a11y DOM needs reordering on the next pass.
639
+ *
640
+ * Delegates to {@link A11yProjectionManager}. Kept on `Scene` under its
641
+ * original name — and with a setter — because `Entity` assigns to
642
+ * `scene.a11yNeedsReorder` as a public cross-class contract (`Entity.ts` sets
643
+ * it when `interactive` flips and when a child is added or removed), and the
644
+ * unedited suite writes it directly too.
645
+ */
646
+ get a11yNeedsReorder(): boolean;
647
+ set a11yNeedsReorder(value: boolean);
648
+ private portalRoot;
781
649
  private activePortalsThisFrame;
782
650
  private activePortalsPrevFrame;
783
651
  private portalEntities;
@@ -791,22 +659,58 @@ export declare class Scene {
791
659
  * `_setWorldCache` are: it is a cross-class render-internal contract.
792
660
  */
793
661
  currentFrame: number;
794
- private _wasm;
795
- private _transformBackend;
796
- private _treeStore;
797
- private _slotEntity;
798
- private _wasmInputs;
799
- private _wasmWorld;
800
- private _structureVersion;
801
- private _storeStructureVersion;
802
- /**
803
- * Consecutive `uploadRuns` rejections. Reset by any success and by
804
- * {@link setTransformBackend}; at {@link WASM_UPLOAD_REJECT_LIMIT} the backend
805
- * mode flips to `'js'` permanently.
806
- */
807
- private _wasmUploadRejections;
808
- /** Latch for the permanent-fallback warning, which fires from a per-frame path. */
809
- private hasWarnedWasmUploadFallback;
662
+ /**
663
+ * The four invisible WASM accelerators and the resident transform store.
664
+ *
665
+ * Extraction 1 of this file's decomposition. Every public member below keeps
666
+ * its name, signature and behaviour and delegates here, so the public API is
667
+ * byte-identical; only where the state lives changed.
668
+ *
669
+ * Constructed in the constructor rather than initialized here because it needs
670
+ * {@link root}, which is itself assigned there.
671
+ */
672
+ private _wasmBackend;
673
+ /**
674
+ * The transform backend object itself.
675
+ *
676
+ * Kept under its original name and delegating, rather than deleted:
677
+ * `test/wasm/scene-accelerators.test.ts` reaches for `scene._wasm` to
678
+ * monkey-patch a kernel into rejecting, and that unedited suite is this
679
+ * refactor's gate — rewriting it to match the new shape would be marking our
680
+ * own homework. Same reasoning for `_animWasm`, `_wasmUploadRejections`,
681
+ * `_storeStructureVersion` and `_structureVersion` below.
682
+ *
683
+ * `protected` rather than `private` since extraction 4: the hit-grid build was
684
+ * this getter's last in-class reader, and `private` plus no reader fails
685
+ * `noUnusedLocals` (`DEC-0019` rule 4).
686
+ * @internal
687
+ */
688
+ protected get _wasm(): WasmTransformBackend | null;
689
+ /** @internal Read by the WASM anim suites; see {@link _wasm}. */
690
+ private get _animWasm();
691
+ /**
692
+ * @internal
693
+ * `protected`, not `private`, and that is the one deliberate access change in
694
+ * this extraction. Both of these are read by a test and by nothing inside this
695
+ * class, so `private` makes them dead code and `noUnusedLocals` rejects the
696
+ * build. `protected` is the honest encoding of "exists to be read from
697
+ * outside, never from in here" — and it is invisible to API consumers, who
698
+ * cannot reach a protected member either. `Scene` has no subclasses in this
699
+ * repo and is not documented as subclassable.
700
+ *
701
+ * Read by the run-table fallback suite; see {@link _wasm}.
702
+ */
703
+ protected get _wasmUploadRejections(): number;
704
+ /** @internal Read by the resident-store suite; see {@link _wasmUploadRejections}. */
705
+ protected get _storeStructureVersion(): number;
706
+ /**
707
+ * Tree topology version, bumped by every add/remove/reparent.
708
+ *
709
+ * Lives on the facade because the resident store layout is keyed by it, but is
710
+ * read here by the compute-entity cache below and by `Scene.test.ts`.
711
+ * @internal
712
+ */
713
+ private get _structureVersion();
810
714
  private _computeEntities;
811
715
  private _computeEntitiesVersion;
812
716
  /** Invalidate the resident WASM store layout; the next wasm-mode frame rebuilds
@@ -845,25 +749,6 @@ export declare class Scene {
845
749
  * Resolves `true` if WASM is now active, `false` if the JS path remains.
846
750
  */
847
751
  enableWasmTransforms(source: WasmModuleSource): Promise<boolean>;
848
- /**
849
- * The one WASM instance this Scene's accelerators share.
850
- *
851
- * Each `enableWasm*` used to instantiate the binary itself, so enabling all
852
- * four compiled the same module four times and held four linear memories. The
853
- * Rust crate already keeps transform/anim/hit/particle in separate statics, so
854
- * one instance serves all of them without aliasing. The compiled module is
855
- * cached globally; the instance is per-Scene, which is the isolation that
856
- * actually matters.
857
- */
858
- private _wasmRuntime;
859
- /**
860
- * Load (or reuse) this Scene's shared WASM runtime.
861
- *
862
- * Returns `null` on any failure — CSP `wasm-unsafe-eval`, a 404, corrupt bytes,
863
- * unsupported SIMD — so every caller keeps its JS path. Failure is the default
864
- * state here, not an error path.
865
- */
866
- private ensureWasmRuntime;
867
752
  /**
868
753
  * Install a pre-built runtime, so several Scenes can share one compile while
869
754
  * each keeps its own stores. Pass `null` to detach (backends already installed
@@ -872,46 +757,17 @@ export declare class Scene {
872
757
  setWasmRuntime(runtime: CoreWasmRuntime | null): void;
873
758
  /** The shared WASM runtime, if one has been loaded. */
874
759
  get wasmRuntime(): CoreWasmRuntime | null;
875
- private _hitWasm;
876
- private _hitGridFrame;
877
- private _hitGridOk;
878
- private _hitSlotEntity;
879
- private _hitBoundless;
880
- /** Reused buffer for the fused gather, so a pointer query allocates nothing. */
881
- private _hitGatherBuffer;
882
- /**
883
- * Whether the last grid build sourced its AABBs from the WASM transform store
884
- * rather than recomputing them in JS. Diagnostic only — both paths must
885
- * produce the same entity for a given point.
886
- */
887
- private _hitFusedGather;
888
760
  /**
889
- * Whether `compute_aabbs` has run against the current frame's world matrices.
890
- * The AABB pass is only meaningful after a `compose_*`, so the fused gather
891
- * must not read the views before then.
761
+ * The pointer hit-test: the WASM broad-phase grid, the permanent JS
762
+ * depth-first walk, and the eligibility gating that keeps the two in lockstep
763
+ * (extraction 4, `DEC-0023`).
764
+ *
765
+ * Definite-assignment because it needs {@link _wasmBackend}, which is built
766
+ * partway through the constructor.
892
767
  */
893
- private _wasmAabbsFresh;
768
+ private _hitTester;
894
769
  /** Did the last hit-grid build use the fused (WASM-store) gather? */
895
770
  get hitGatherPath(): 'fused' | 'js';
896
- /**
897
- * Why the transform accelerator did or did not run on the most recent frame.
898
- * Written by the render walk and `_syncWasmStore`.
899
- */
900
- private _transformReason;
901
- /** Why the batched-driver accelerator did or did not run. */
902
- private _animReason;
903
- /**
904
- * Why the hit-test accelerator did or did not serve the last pointer query.
905
- * The grid is built lazily on demand, not every frame, so this describes the
906
- * most recent BUILD. Starts at `'not-installed'` because that is the truth
907
- * before a backend exists; `_ensureHitGrid` moves it to `'not-applicable'`
908
- * once one is installed but nothing has queried yet.
909
- */
910
- private _hitReason;
911
- /** Why the particle accelerator did or did not run. */
912
- private _particleReason;
913
- /** Which particle implementation actually simulated the most recent frame. */
914
- private _particlePath;
915
771
  /**
916
772
  * Per-frame status of every invisible accelerator: whether each is installed,
917
773
  * whether it actually ran on the most recent frame, and why.
@@ -926,6 +782,10 @@ export declare class Scene {
926
782
  *
927
783
  * Reflects the most recent main-renderer frame; a secondary renderer (SVG
928
784
  * export, offscreen snapshot) does not overwrite it.
785
+ *
786
+ * `webgpuActive` is handed to the facade rather than read by it: WebGPU device
787
+ * state belongs to the context/resize domain (extraction 5), and this getter
788
+ * is the only place the two domains have to agree.
929
789
  */
930
790
  get accelerators(): AcceleratorReport;
931
791
  /** Which backend answers `findEntityAt` for the main tree. */
@@ -942,34 +802,23 @@ export declare class Scene {
942
802
  */
943
803
  enableWasmHitTest(source: HitModuleSource): Promise<boolean>;
944
804
  /**
945
- * Refresh the hit-test grid for the CURRENT tree state if it is stale (a
946
- * structural or transform change may have happened since the last build —
947
- * there is no cheap "nothing moved" shortcut for a spatial index the way
948
- * there is for the transform store's topology-only run table, since ANY
949
- * entity moving invalidates its AABB, not just add/remove/reparent; the
950
- * measured build cost is cheap enough to redo per call). Returns `false`
951
- * (grid untrustworthy — caller must use the JS walk) when there is no
952
- * backend or the build overflowed its item budget.
953
- */
954
- private _ensureHitGrid;
955
- /**
956
- * `findEntityAt`'s WASM-accelerated path for the main tree. Scans only the
957
- * queried cell's candidates (confirming each against its own AABB and precise
958
- * `isPointInside`) merged against the (typically empty or tiny) list of
959
- * entities with no `getBounds()`, taking whichever confirmed match has the
960
- * higher pre-order index — see hit-store.ts for why that is exactly
961
- * equivalent to findHitRecursively's topmost-hit priority. Always
962
- * conclusive: returns the correct entity or `null`, never "inconclusive".
963
- */
964
- private _findEntityAtWasm;
965
- private _animWasm;
966
- private _activeDriverEntities;
967
- private _springEntities;
968
- private _springProps;
969
- private _springDrivers;
970
- private _tweenEntities;
971
- private _tweenProps;
972
- private _tweenDrivers;
805
+ * The batched driver tick and its candidate registry (extraction 6,
806
+ * `DEC-0025`).
807
+ *
808
+ * Definite-assignment because it holds {@link _wasmBackend}, which is built
809
+ * partway through the constructor the same shape as {@link _hitTester}.
810
+ */
811
+ private _driverTicker;
812
+ /**
813
+ * The candidate set, for the unedited suite.
814
+ *
815
+ * `test/wasm/scene-anim-batch.test.ts` reads it through a cast at four sites to
816
+ * assert the registry self-prunes, unregisters a removed subtree, and
817
+ * re-registers a re-added one. `protected` rather than `private` because it has
818
+ * no in-class reader and `noUnusedLocals` fails the build on a private with
819
+ * none (`DEC-0019` rule 4).
820
+ */
821
+ protected get _activeDriverEntities(): Set<Entity>;
973
822
  /**
974
823
  * Minimum number of batchable (spring, or named-easing tween) active drivers
975
824
  * before a frame engages the WASM batch path at all; below it, every driver
@@ -1037,7 +886,6 @@ export declare class Scene {
1037
886
  * Setting {@link animDriverGateCount} overwrites all three, so existing code
1038
887
  * that tuned the single knob keeps working unchanged.
1039
888
  */
1040
- private _animBatchedLastFrame;
1041
889
  /**
1042
890
  * Whether the WASM batch path actually ran on the most recent frame.
1043
891
  *
@@ -1068,7 +916,6 @@ export declare class Scene {
1068
916
  * if WASM is now available (not necessarily active every frame).
1069
917
  */
1070
918
  enableWasmAnimBatching(source: AnimModuleSource): Promise<boolean>;
1071
- private _particleWasm;
1072
919
  /** Which backend runs the CPU particle simulation. Reflects only whether a
1073
920
  * backend is installed (the WebGPU compute path, when active, is used first
1074
921
  * regardless). */
@@ -1121,25 +968,6 @@ export declare class Scene {
1121
968
  * pre-pass.
1122
969
  */
1123
970
  private _tickBatchedDrivers;
1124
- /**
1125
- * Compose the whole main tree's world matrices through the resident WASM store
1126
- * and return the world-matrix views for the render walk to read. Rebuilds the
1127
- * store layout (slots + runs) only when the tree structure changed since the
1128
- * last rebuild; otherwise it just gathers current transforms into the resident
1129
- * input view and runs the kernel. Returns `null` if there is no backend.
1130
- */
1131
- private _syncWasmStore;
1132
- /**
1133
- * Run the WASM world-AABB pass over the current frame's world matrices, so the
1134
- * fused hit gather can read AABBs straight out of the store.
1135
- *
1136
- * Local bounds are uploaded here rather than in the per-frame transform sync
1137
- * because `getBounds()` is a virtual call that allocates a rect on most
1138
- * entities — paying it every frame for a query that may never come would move
1139
- * cost onto the render path to save it on hover. Returns `false` if any entity
1140
- * cannot supply bounds through the store, so the caller uses the JS gather.
1141
- */
1142
- private _ensureWasmAabbs;
1143
971
  /**
1144
972
  * Authoritative paint order for semantic nodes discovered during the main
1145
973
  * render. A node may not have a DOM projection until the following a11y
@@ -1263,48 +1091,22 @@ export declare class Scene {
1263
1091
  * on the same canvas, restore DPR/size, and repaint.
1264
1092
  */
1265
1093
  private setupGLContextRecovery;
1266
- private endContentSelectionDrag;
1267
- /**
1268
- * Index of the carrier line currently holding a selection inside `el`, or
1269
- * `null`.
1270
- *
1271
- * Lets a partial re-materialization decide whether the user's selection is even
1272
- * affected. Checks the tracked anchor first (it survives a drag) and falls back
1273
- * to the live DOM selection.
1274
- */
1275
- private contentGridSelectionLine;
1276
- /**
1277
- * Does the document hold a selection right now, memoized for this sync walk?
1278
- *
1279
- * Pays one forced layout per walk instead of one per rebuilt element — see
1280
- * {@link Scene.contentSelectionPresentThisSync} for the measurements. When the
1281
- * answer is `false` no element can own a selection, so every per-element
1282
- * ownership test can be skipped without touching the object.
1283
- */
1284
- private contentSelectionPresent;
1285
- private releaseContentSelectionForRebuild;
1286
- /**
1287
- * Rebuild a content-projection element's DOM (`rebuild`) while preserving a
1288
- * text selection the user made inside it. A streaming message replaces its
1289
- * projection children on every appended chunk; without this, a selection in
1290
- * the UNCHANGED prefix is wiped on each frame ("can't select text in a
1291
- * message still receiving tokens"). We snapshot the selection's anchor/focus
1292
- * as linear character offsets within `el` before the rebuild and re-resolve
1293
- * them against the new DOM after, clamped to the new text length.
1294
- *
1295
- * Only fires when `el` owns the current selection and there is no active drag
1296
- * (mid-drag the browser is authoritative). The virtualization case — where
1297
- * `el` itself is removed from the DOM — is out of scope here (the node is
1298
- * genuinely freed; the browser clears the selection and there is nothing to
1299
- * restore against).
1300
- */
1301
- private preserveContentSelectionAcrossRebuild;
1302
1094
  /**
1303
1095
  * Expose the underlying {@link IRenderer} for advanced direct-draw operations.
1304
1096
  *
1305
1097
  * @returns The active renderer instance.
1306
1098
  */
1307
1099
  getRenderer(): IRenderer;
1100
+ /**
1101
+ * Finds the topmost interactive entity at the given coordinates.
1102
+ *
1103
+ * Delegates to {@link HitTester}. The frame stamp and the logical size are
1104
+ * passed in rather than reached for: `currentFrame` is the scheduler's
1105
+ * (extraction 6) and `width`/`height` are mutated by `resize`
1106
+ * (extraction 5), so neither can be captured at construction
1107
+ * (`DEC-0019` rule 5).
1108
+ */
1109
+ findEntityAt(x: number, y: number): Entity | null;
1308
1110
  /** Convert browser viewport coordinates into this Scene's logical coordinates. */
1309
1111
  clientToScene(clientX: number, clientY: number): {
1310
1112
  x: number;
@@ -1318,18 +1120,6 @@ export declare class Scene {
1318
1120
  * @example scene.add(new CircleEntity());
1319
1121
  */
1320
1122
  add(entity: Entity): this;
1321
- /**
1322
- * Reset per-grid calibration and bookkeeping before a (re)materialization.
1323
- *
1324
- * @param entityId - Owning entity, keyed into the calibration maps.
1325
- * @param el - The projection element.
1326
- * @param releaseSelection - Whether to drop a selection this element owns.
1327
- * Pass `false` when carrier lines are being reused: the selection's DOM nodes
1328
- * survive the pass, so tearing it down would wipe a user's selection on every
1329
- * streamed chunk — the exact bug `preserveContentSelectionAcrossRebuild`
1330
- * exists to prevent on the non-grid path.
1331
- */
1332
- private clearContentGridState;
1333
1123
  /**
1334
1124
  * Drop any projected elements under `node` without touching the entity tree.
1335
1125
  *
@@ -1484,13 +1274,6 @@ export declare class Scene {
1484
1274
  * directly rather than rebuilding a tree.
1485
1275
  */
1486
1276
  get structureVersion(): number;
1487
- /**
1488
- * Record who marked the scene dirty and why.
1489
- *
1490
- * Kept separate from {@link markDirty} so the hot path is not a function call
1491
- * with a branch — V8 inlines the one-field version reliably.
1492
- */
1493
- private recordDirtyReason;
1494
1277
  /**
1495
1278
  * Start or stop recording dirty attributions.
1496
1279
  *
@@ -1668,49 +1451,15 @@ export declare class Scene {
1668
1451
  */
1669
1452
  private projectionVisibleLocalYBand;
1670
1453
  private syncContentProjection;
1671
- /**
1672
- * Materialize a prepared grid in logical source order while positioning each
1673
- * carrier from the shared canvas geometry. Browser font measurement happens
1674
- * later in one cold read/write batch, never inside projection synchronization.
1675
- */
1676
- private syncContentGridProjection;
1677
1454
  private getContentMetricScaleX;
1678
- private scheduleContentGridCalibration;
1679
1455
  private enforceA11yDomOrder;
1680
1456
  /**
1681
- * Reorder `normalElements` (in place) into visual reading order using the
1682
- * positions `syncA11y` already wrote to each element's inline style
1683
- * (`top`/`left`/`height`). Elements are grouped into rows top-to-bottom (an
1684
- * element belongs to the current row while its top is above the row's
1685
- * running bottom edge), then sorted within a row by `left` ascending for
1686
- * `'ltr'`, descending for `'rtl'`. The sort is stable, so entities at the
1687
- * same position keep their scene-graph (collection) order as a tiebreak.
1688
- *
1689
- * Those inline values are world coordinates for a top-level mirror but
1690
- * PARENT-RELATIVE for a nested one, so this list mixes coordinate spaces.
1691
- * That is sound because the result is only ever applied per DOM parent
1692
- * ({@link enforceA11yDomOrder} advances a cursor per parent), and all of one
1693
- * parent's children share one space: a `grid`'s rows are all grid-relative, a
1694
- * `row`'s cells all row-relative. Comparisons ACROSS spaces do happen while
1695
- * banding, but they only affect the relative order of elements in different
1696
- * parents, which no `insertBefore` ever acts on. Normalizing everything back
1697
- * to world coordinates here would cost a transform per element per frame to
1698
- * change nothing observable.
1699
- *
1700
- * Banding runs **per region** — per nearest `clipChildren` ancestor, recorded
1701
- * by {@link enforceA11yDomOrder}'s collect walk — rather than once over the
1702
- * whole scene. Purely visual banding is right for a screen reader but wrong
1703
- * for selection: a DOM `Selection` covers everything between anchor and focus
1704
- * in DOM order, so under one global banding a vertical drag through a
1705
- * transcript also swallowed a sidebar whose headings happened to fall in the
1706
- * same rows. Regions are laid out side by side, so ordering region-major keeps
1707
- * each one a contiguous DOM run and a drag stays inside it, while reading
1708
- * order *within* a region is unchanged. Regions are emitted in the order their
1709
- * clipper is first reached by the depth-first walk, so a screen reader still
1710
- * meets them in the author's declared order.
1711
- */
1712
- private sortNormalElementsVisually;
1713
- /** Keep DOM/WebGL overlay layers aligned with the canvas's CSS box. */
1457
+ * Keep DOM/WebGL overlay layers aligned with the canvas's CSS box.
1458
+ *
1459
+ * The logical size and the lazily-created layers are threaded through rather
1460
+ * than held: `resize` mutates `width`/`height`, and `gpuCanvas` does not exist
1461
+ * until the WebGPU particle path first runs (`DEC-0019` rule 5).
1462
+ */
1714
1463
  private syncOverlayGeometry;
1715
1464
  getA11yTree(): A11yTreeNode[];
1716
1465
  private renderPortalDOM;
@@ -1762,9 +1511,6 @@ export declare class Scene {
1762
1511
  * {@link start} instead, and rejecting it here would contradict that.
1763
1512
  */
1764
1513
  resize(width: number, height: number): void;
1765
- /** Effective device pixel ratio, matching CanvasRenderer: real DPR clamped to
1766
- * `maxDPR` when set. */
1767
- private effectiveDPR;
1768
1514
  /** Size the WebGPU particle canvas: backing store at logical × DPR, CSS box at
1769
1515
  * the logical size. Sizing the backing store in logical px (the old
1770
1516
  * behavior) left it rasterized at 1× and CSS-stretched — blurry on HiDPI. */
@@ -1779,29 +1525,10 @@ export declare class Scene {
1779
1525
  * Gets the root entity of the scene.
1780
1526
  */
1781
1527
  getRoot(): Entity;
1782
- /**
1783
- * Finds the topmost interactive entity at the given coordinates.
1784
- */
1785
- findEntityAt(x: number, y: number): Entity | null;
1786
1528
  /** Submit one transparent clear pass when particle content lingers on the GPU canvas. */
1787
1529
  private clearGPUCanvasIfStale;
1788
1530
  private initWebGPUContext;
1789
1531
  private setupDeviceLostHandler;
1790
1532
  private recreateWebGPUDeviceWithRetry;
1791
1533
  private renderCPUParticles;
1792
- private findHitRecursively;
1793
- /** Whether `node` opts out of being a pointer hit target: a disabled control
1794
- * or an explicit `pointerEvents: 'none'` in its a11y attributes. Its children
1795
- * are still walked (a transparent container can hold hittable descendants). */
1796
- private isPointerTransparent;
1797
- /**
1798
- * Whether a confirmed geometric hit on `node` at world `(x, y)` is a REAL hit,
1799
- * applying the same visibility/input gating as {@link findHitRecursively} but
1800
- * from a flat candidate (the WASM grid has no recursion clip-stack): the node
1801
- * and all ancestors are visible (`opacity > 0`), the point lies inside every
1802
- * `clipChildren` ancestor's world box, and the node isn't pointer-transparent
1803
- * (disabled / `pointerEvents: 'none'`). Keeps the WASM and JS hit paths in
1804
- * lockstep so they return the same entity.
1805
- */
1806
- private isHitEligible;
1807
1534
  }