@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 CHANGED
@@ -1,18 +1,12 @@
1
1
  # @vectojs/core
2
2
 
3
- > Scene, layout, interaction, text, rendering, and semantic projection for canvas-native interfaces.
4
-
5
- [![npm](https://img.shields.io/npm/v/@vectojs/core?color=22d3ee)](https://www.npmjs.com/package/@vectojs/core)
6
- [![CI](https://github.com/vectojs/vectojs/actions/workflows/ci.yml/badge.svg)](https://github.com/vectojs/vectojs/actions/workflows/ci.yml)
7
- [![MIT](https://img.shields.io/badge/license-MIT-6366f1.svg)](https://github.com/vectojs/vectojs/blob/main/LICENSE)
8
-
9
- `@vectojs/core` is the runtime beneath VectoJS. It owns the retained `Scene`/`Entity` tree, affine
10
- transforms, render scheduling, layout, text flow, spatial hit-testing, event propagation, renderer
11
- backends, and the accessibility/automation projection layer.
12
-
13
- [Core guide](https://vectojs.org/learn/core-scene/) ·
14
- [API reference](https://vectojs.org/reference/core-api/) ·
15
- [Main repository](https://github.com/vectojs/vectojs)
3
+ `@vectojs/core` is the runtime at the root of the VectoJS dependency graph: a retained
4
+ `Scene`/`Entity` scene graph (the Virtual Math Tree) that renders to a single canvas, with
5
+ transforms, render scheduling, spatial hit-testing, DOM-like event propagation, layout and text
6
+ engines, Canvas/SVG/WebGL/WebGPU renderer backends, and the accessibility/automation projection
7
+ layer. It is also the composition point of the framework — it depends on and re-exports
8
+ `@vectojs/layout`, `@vectojs/text`, `@vectojs/math`, and `@vectojs/animation`, so `@vectojs/ui`
9
+ and every higher-level package build directly on it.
16
10
 
17
11
  ## Install
18
12
 
@@ -20,7 +14,7 @@ backends, and the accessibility/automation projection layer.
20
14
  bun add @vectojs/core
21
15
  ```
22
16
 
23
- ## Minimal scene
17
+ ## Usage
24
18
 
25
19
  ```ts
26
20
  import { Entity, type IRenderer, Scene } from '@vectojs/core';
@@ -57,124 +51,38 @@ scene.add(new Dot().setPosition(80, 80));
57
51
  scene.start();
58
52
  ```
59
53
 
60
- ## Runtime building blocks
61
-
62
- | Area | Main APIs | Purpose |
63
- | ----------- | ----------------------------------------------- | ---------------------------------------------------------------------- |
64
- | Scene graph | `Scene`, `Entity` | ownership, transforms, lifecycle, update/render traversal |
65
- | Layout | `LayoutEngine`, layout subpath | prepared text/rich-text layout, wrapping, exclusions, reusable buffers |
66
- | Interaction | entity events, `SpatialHashGrid` | hit-testing and DOM-like capture/bubble dispatch |
67
- | Text | `TextEntity`, `MSDFTextEntity`, `SplineEntity` | Canvas text, GPU text, and mathematical curves |
68
- | Rendering | `IRenderer`, `CanvasRenderer`, renderer subpath | backend-neutral drawing contract and concrete renderers |
69
- | GPU paths | WebGL point batching, WebGPU particles | high-volume points/rects and compute-driven particles |
70
- | Semantics | `A11yAttributes`, Scene projection | role/name/state, native inputs, screen readers, Playwright/agents |
71
- | Animation | `animate`, springs, transitions, `Scene.step()` | real-time or deterministic fixed-step motion |
72
-
73
- ## Scene lifecycle
74
-
75
- ```ts
76
- const scene = new Scene(canvas, {
77
- maxFPS: 60,
78
- pointBackend: 'canvas', // or 'webgl'
79
- particleBackend: 'cpu', // or 'webgpu' when supported
80
- });
81
- scene.renderMode = 'onDemand'; // redraw only when dirty
82
-
83
- scene.resize(width, height); // logical CSS pixels
84
- scene.markDirty();
85
- scene.start();
86
- scene.stop();
87
- scene.step(1000 / 60); // deterministic single step
88
- scene.destroy(); // release renderers, workers, observers, and semantic DOM
89
- ```
90
-
91
- Always call `destroy()` when a framework component unmounts. A `Scene` owns browser observers,
92
- renderer resources, layout work, and projected DOM nodes.
93
-
94
- ## Package entry points
95
-
96
- ```ts
97
- import { Scene, Entity } from '@vectojs/core';
98
- import { LayoutEngine } from '@vectojs/core/layout';
99
- import type { IRenderer } from '@vectojs/core/renderer';
100
- import { TextEntity } from '@vectojs/core/text';
101
- ```
102
-
103
- Both ESM and CommonJS outputs are published with TypeScript declarations.
104
-
105
- ## Accessibility and automation
106
-
107
- Canvas pixels have no semantics. Interactive entities can implement `getA11yAttributes()`; the
108
- Scene projects transparent DOM nodes over their world-space bounds and forwards native input back
109
- into the VectoJS event system.
110
-
111
- This projection is intentionally thin. Applications still own accessible names, keyboard behavior,
112
- focus order, contrast, and correct control semantics. See the
113
- [accessibility guide](https://vectojs.org/learn/accessibility/).
114
-
115
- Non-control workspaces that own keyboard shortcuts can opt into focus order
116
- explicitly: return `{ role: 'region', label: 'Canvas workspace', tabIndex: 0 }`.
117
- The Scene refreshes the projected `tabindex` when attributes change. Keep native
118
- text inputs and editable content in charge of their own editing shortcuts.
119
-
120
- Interactive projected nodes capture the active pointer on `pointerdown` and route
121
- `pointermove`, `pointerup`, and `pointercancel` through normal VMT capture/bubble propagation.
122
- Treat `pointercancel` as rollback: discard transient gesture state and do not create durable
123
- history. Pointer capture is released safely on both completion paths.
124
-
125
- ## Static content projection
126
-
127
- Canvas-rendered text can opt into browser-native find, translation, selection, and copy without
128
- turning the application into a DOM layout. Override `getContentProjection()` on the owning Entity;
129
- the Scene creates a transparent, position-synchronized mirror while the VMT remains authoritative:
130
-
131
- ```ts
132
- class SelectableLabel extends Entity {
133
- getContentProjection() {
134
- return {
135
- text: 'Selectable canvas text',
136
- font: '18px Inter',
137
- lineHeight: 24,
138
- selectable: true,
139
- };
140
- }
141
- }
142
- ```
143
-
144
- `selectable` controls whether the mirror receives pointer input. The Scene keeps projection order
145
- aligned with VMT order, removes descendant mirrors with their subtree, and hides mirrors fully
146
- outside the viewport or a `clipChildren` ancestor. Tooling can inspect the currently materialized
147
- node with `scene.getContentElement(entityId)`. Virtualized or non-materialized off-viewport text is
148
- not searchable until the application brings it into the active scene.
149
-
150
- Code-like renderers can compile their logical source once with `prepareContentGrid()` and return the
151
- same immutable plan as `ContentProjection.grid`. The plan retains UTF-16 source ranges, legal
152
- grapheme carets, CR/LF ownership, tab stops, wide CJK/emoji advances, Arabic shaping, and Unicode
153
- bidi positions. Scene projects those cells in logical source order, performs font calibration in a
154
- cold offscreen batch, and uses the plan's local geometry for pointer selection even when the entity
155
- is rotated, scaled, or the page is zoomed.
156
-
157
- Selection routing preserves forward/reverse drag direction, Shift extension, word and line
158
- selection, and exact clipboard source. Projection rebuild/removal/destroy paths release active
159
- selection and pending calibration ownership before replacing DOM carriers.
160
-
161
- ## Performance model
162
-
163
- Useful levers include on-demand rendering, viewport culling, spatial hashing, prepared text layout,
164
- typed reusable buffers, batched WebGL points, and optional WebGPU particle compute. None makes every
165
- workload allocation-free or GPU-bound; profile the renderer and entity types used by your app.
166
-
167
- Run the repository benchmarks with `bun run benchmark`, `bun run compare:dom`, and
168
- `bun run compare`. Prepared-grid scaling can be measured independently with
169
- `bun run --cwd packages/core benchmark:grid`; benchmark timing is release evidence rather than a
170
- wall-clock CI gate.
171
-
172
- ## Related packages
173
-
174
- - [`@vectojs/ui`](https://github.com/vectojs/vectojs/tree/main/packages/ui) — high-level accessible components
175
- - [`@vectojs/three`](https://github.com/vectojs/vectojs/tree/main/packages/three) — Three.js/WebXR projection and raycast routing
176
- - [`@vectojs/video-exporter`](https://github.com/vectojs/vectojs/tree/main/packages/video-exporter) — deterministic H.264 capture
177
-
178
- ## License
179
-
180
- [MIT](https://github.com/vectojs/vectojs/blob/main/LICENSE) © 2026 Xuepoo
54
+ ## Highlights
55
+
56
+ - Retained `Scene`/`Entity` tree with affine transforms, capture/bubble event dispatch,
57
+ viewport culling, and dirty-flag `renderMode: 'onDemand'` rendering; `scene.step(dt)` drives a
58
+ deterministic frame for tests and video export.
59
+ - Backend-neutral `IRenderer` drawing contract with modular backends: `CanvasRenderer`,
60
+ `SVGRenderer`, batched WebGL points (`WebGLPointRenderer`), and WebGPU particle compute
61
+ (`WebGPUParticleSystemManager`) registered on load via `Scene.register*`, selected through
62
+ `pointBackend` / `particleBackend` options.
63
+ - Optional Rust WASM kernels (`crates/vectojs-core-rs`) hot-swapped per subsystem with
64
+ `scene.enableWasmTransforms / enableWasmHitTest / enableWasmAnimBatching / enableWasmParticles`;
65
+ fallible exports return `WASM_STATUS` codes (`OK`/`CAPACITY`/`UNINITIALIZED`/`BAD_RUN`/
66
+ `OVERFLOW`) so any rejected batch degrades to the JS path instead of rendering half-written state.
67
+ - Semantic accessibility projection: entities implementing `getA11yAttributes()` get transparent,
68
+ position-synchronized DOM mirrors for screen readers, keyboard users, Playwright, and AI agents;
69
+ static text opts into browser-native selection/find/copy through `getContentProjection()` and
70
+ Core's prepared content grid (`prepareContentGrid()`).
71
+ - Entity-based text renderers stay here because they extend `Entity`: `TextEntity`,
72
+ `GridTextEntity`, GPU-resolved `MSDFTextEntity` (off-thread layout via `LayoutWorkerManager`),
73
+ `SVGEntity`, and `DOMPortalEntity`.
74
+ - The standalone engines are re-exported from this barrel and remain available as subpaths, so
75
+ existing imports keep working:
76
+ `@vectojs/core/layout`, `@vectojs/core/text`, `@vectojs/core/renderer`.
77
+ - Lifecycle ownership is explicit: a `Scene` owns renderers, workers, observers, and projected DOM
78
+ nodes; `scene.destroy()` releases all of them.
79
+
80
+ > Documents @vectojs/core@1.39.0.
81
+
82
+ ## Documentation
83
+
84
+ - [Core Scene architecture](https://vectojs.org/learn/core-scene/)
85
+ - [`@vectojs/core` API reference](https://vectojs.org/reference/core-api/)
86
+ - [`Entity` reference](https://vectojs.org/reference/core-entity/)
87
+ - [Renderers reference](https://vectojs.org/reference/core-renderer/)
88
+ - [Accessibility & automation guide](https://vectojs.org/learn/accessibility/)
@@ -63,7 +63,8 @@ function emptyDrawCounters() {
63
63
  }
64
64
  var TWO_PI = Math.PI * 2;
65
65
  function getDevicePixelRatio() {
66
- return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
66
+ const raw = typeof window !== "undefined" ? window.devicePixelRatio : 1;
67
+ return Number.isFinite(raw) && raw > 0 ? raw : 1;
67
68
  }
68
69
  var CanvasRenderer = class _CanvasRenderer {
69
70
  ctx;
@@ -209,7 +210,9 @@ var CanvasRenderer = class _CanvasRenderer {
209
210
  /** Real `devicePixelRatio`, clamped to {@link maxDPR} when set. */
210
211
  effectiveDPR() {
211
212
  const real = getDevicePixelRatio();
212
- return this.maxDPR !== void 0 ? Math.min(real, this.maxDPR) : real;
213
+ if (this.maxDPR === void 0) return real;
214
+ if (!Number.isFinite(this.maxDPR) || this.maxDPR <= 0) return real;
215
+ return Math.min(real, this.maxDPR);
213
216
  }
214
217
  /**
215
218
  * @inheritdoc
@@ -240,14 +243,18 @@ var CanvasRenderer = class _CanvasRenderer {
240
243
  */
241
244
  resize(width, height) {
242
245
  const dpr = this.effectiveDPR();
246
+ const safeWidth = Number.isFinite(width) && width >= 0 ? width : this.width;
247
+ const safeHeight = Number.isFinite(height) && height >= 0 ? height : this.height;
248
+ const backingW = Number.isFinite(safeWidth * dpr) ? Math.max(1, Math.round(safeWidth * dpr)) : 1;
249
+ const backingH = Number.isFinite(safeHeight * dpr) ? Math.max(1, Math.round(safeHeight * dpr)) : 1;
243
250
  this.appliedDPR = dpr;
244
- this.width = width;
245
- this.height = height;
246
- this.ctx.canvas.width = width * dpr;
247
- this.ctx.canvas.height = height * dpr;
251
+ this.width = safeWidth;
252
+ this.height = safeHeight;
253
+ this.ctx.canvas.width = backingW;
254
+ this.ctx.canvas.height = backingH;
248
255
  if (this.ctx.canvas.style) {
249
- this.ctx.canvas.style.width = `${width}px`;
250
- this.ctx.canvas.style.height = `${height}px`;
256
+ this.ctx.canvas.style.width = `${safeWidth}px`;
257
+ this.ctx.canvas.style.height = `${safeHeight}px`;
251
258
  }
252
259
  this.ctx.scale(dpr, dpr);
253
260
  this._cachedFont = "";
@@ -65,7 +65,8 @@ function emptyDrawCounters() {
65
65
  }
66
66
  var TWO_PI = Math.PI * 2;
67
67
  function getDevicePixelRatio() {
68
- return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
68
+ const raw = typeof window !== "undefined" ? window.devicePixelRatio : 1;
69
+ return Number.isFinite(raw) && raw > 0 ? raw : 1;
69
70
  }
70
71
  var CanvasRenderer = (_class = class _CanvasRenderer {
71
72
 
@@ -211,7 +212,9 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
211
212
  /** Real `devicePixelRatio`, clamped to {@link maxDPR} when set. */
212
213
  effectiveDPR() {
213
214
  const real = getDevicePixelRatio();
214
- return this.maxDPR !== void 0 ? Math.min(real, this.maxDPR) : real;
215
+ if (this.maxDPR === void 0) return real;
216
+ if (!Number.isFinite(this.maxDPR) || this.maxDPR <= 0) return real;
217
+ return Math.min(real, this.maxDPR);
215
218
  }
216
219
  /**
217
220
  * @inheritdoc
@@ -242,14 +245,18 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
242
245
  */
243
246
  resize(width, height) {
244
247
  const dpr = this.effectiveDPR();
248
+ const safeWidth = Number.isFinite(width) && width >= 0 ? width : this.width;
249
+ const safeHeight = Number.isFinite(height) && height >= 0 ? height : this.height;
250
+ const backingW = Number.isFinite(safeWidth * dpr) ? Math.max(1, Math.round(safeWidth * dpr)) : 1;
251
+ const backingH = Number.isFinite(safeHeight * dpr) ? Math.max(1, Math.round(safeHeight * dpr)) : 1;
245
252
  this.appliedDPR = dpr;
246
- this.width = width;
247
- this.height = height;
248
- this.ctx.canvas.width = width * dpr;
249
- this.ctx.canvas.height = height * dpr;
253
+ this.width = safeWidth;
254
+ this.height = safeHeight;
255
+ this.ctx.canvas.width = backingW;
256
+ this.ctx.canvas.height = backingH;
250
257
  if (this.ctx.canvas.style) {
251
- this.ctx.canvas.style.width = `${width}px`;
252
- this.ctx.canvas.style.height = `${height}px`;
258
+ this.ctx.canvas.style.width = `${safeWidth}px`;
259
+ this.ctx.canvas.style.height = `${safeHeight}px`;
253
260
  }
254
261
  this.ctx.scale(dpr, dpr);
255
262
  this._cachedFont = "";
@@ -27,16 +27,22 @@ var VectoJSEvent = class {
27
27
  nativeEvent;
28
28
  /** Whether the event bubbles past its target (capture always runs). */
29
29
  bubbles;
30
+ /**
31
+ * Which transport delivered this event ({@link VectoEventSource}).
32
+ * `undefined` for unattributed legacy/synthetic dispatches.
33
+ */
34
+ source;
30
35
  explicitScenePoint;
31
36
  stopped = false;
32
37
  stoppedImmediate = false;
33
- constructor(type, target, nativeEvent, bubbles = true, scenePoint) {
38
+ constructor(type, target, nativeEvent, bubbles = true, scenePoint, source) {
34
39
  this.type = type;
35
40
  this.target = target;
36
41
  this.currentTarget = target;
37
42
  this.nativeEvent = nativeEvent;
38
43
  this.bubbles = bubbles;
39
44
  this.explicitScenePoint = scenePoint;
45
+ this.source = source;
40
46
  }
41
47
  /** Stop the event from reaching the next node in the propagation path. */
42
48
  stopPropagation() {
@@ -330,6 +336,41 @@ var Entity = class {
330
336
  * faint-but-live control.
331
337
  */
332
338
  a11yHidden = false;
339
+ /**
340
+ * Opt-in to the DOM visual projection (RFC2, implemented by `@vectojs/dom`).
341
+ * Plain data only — core never materializes an element from this; a
342
+ * registered `ProjectionBackend` does.
343
+ *
344
+ * Name mapping: this field IS the RFC4 §2 `projection` policy
345
+ * (`ProjectionPolicy`: `'canvas' | 'dom' | 'auto'` in
346
+ * `tree/scene/ProjectionPolicy.ts`) — predates the RFC, so the field keeps
347
+ * its name and the RFC spelling lives on the value type.
348
+ *
349
+ * - `'canvas'` (default): today's rendering. Zero behavior change.
350
+ * - `'dom'`: the node materializes as a live `HTMLElement` positioned by its
351
+ * world matrix. The live element replaces the transparent a11y mirror
352
+ * (same single-delivery reasoning as `DOMPortalEntity`), so opting in
353
+ * requires a DOM backend and a DOM environment for the AT representation.
354
+ * - `'auto'`: negotiated per node per frame by `Scene.resolveProjectionFor`
355
+ * (RFC4 §3: explicit beats automatic, fallbacks reported with reasons,
356
+ * hysteresis against flip-flop, no-DOM short-circuits to canvas).
357
+ */
358
+ domPolicy = "canvas";
359
+ /**
360
+ * Backend-side creation hint for a `'dom'`-policy node (tag/content mapping
361
+ * key, e.g. `'text' | 'button' | 'input' | 'container' | 'transform'`). Plain
362
+ * string so custom nodes need no DOM types in core; `@vectojs/dom` owns the
363
+ * registry. `''` means a plain positioned `div` with no content sync.
364
+ */
365
+ domKind = "";
366
+ /**
367
+ * Internal cache owned by the DOM backend: true while the node has a live
368
+ * projected element. The render walk reads it to skip canvas paint for
369
+ * DOM-resident nodes (so a mounted node is never double-drawn) and falls
370
+ * through to canvas while false (SSR / no backend registered). Do not set
371
+ * by hand — the backend sets it on mount and clears it on unmount.
372
+ */
373
+ domResident = false;
333
374
  /**
334
375
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
335
376
  * rendering. Combined with translating a content child, this is how
@@ -631,8 +672,7 @@ var Entity = class {
631
672
  (e) => new Promise((resolve) => {
632
673
  this._spawnDriver(e[0], e[1], cfg);
633
674
  const d = this._drivers?.get(e[0]);
634
- if (!d)
635
- resolve();
675
+ if (!d) resolve();
636
676
  else d.onDone = resolve;
637
677
  })
638
678
  )
@@ -822,6 +862,7 @@ var Entity = class {
822
862
  el.focus();
823
863
  return;
824
864
  }
865
+ if (typeof requestAnimationFrame === "undefined") return;
825
866
  requestAnimationFrame(() => {
826
867
  const retry = this.scene?.getA11yElement(this.id);
827
868
  if (retry) retry.focus();
@@ -1188,6 +1229,10 @@ var Entity = class {
1188
1229
  * `selectable` is set — natively selectable. Returns `null` by default.
1189
1230
  * Read on the a11y sync cadence, so text changes propagate automatically.
1190
1231
  *
1232
+ * This is the descriptor half of the `ContentProjection` row of
1233
+ * `ProjectionBackend` (`tree/scene/ProjectionBackend.ts`): the entity
1234
+ * describes *what* to project, the scene owns *how* it is materialized.
1235
+ *
1191
1236
  * @param hint - Optional advice about which part of the entity is worth
1192
1237
  * describing. Purely an optimization: ignoring it is always correct, which
1193
1238
  * is why it is a parameter rather than a required contract change. See
@@ -29,16 +29,22 @@ var VectoJSEvent = (_class = class {
29
29
 
30
30
  /** Whether the event bubbles past its target (capture always runs). */
31
31
 
32
+ /**
33
+ * Which transport delivered this event ({@link VectoEventSource}).
34
+ * `undefined` for unattributed legacy/synthetic dispatches.
35
+ */
36
+
32
37
 
33
38
  __init() {this.stopped = false}
34
39
  __init2() {this.stoppedImmediate = false}
35
- constructor(type, target, nativeEvent, bubbles = true, scenePoint) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);
40
+ constructor(type, target, nativeEvent, bubbles = true, scenePoint, source) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);
36
41
  this.type = type;
37
42
  this.target = target;
38
43
  this.currentTarget = target;
39
44
  this.nativeEvent = nativeEvent;
40
45
  this.bubbles = bubbles;
41
46
  this.explicitScenePoint = scenePoint;
47
+ this.source = source;
42
48
  }
43
49
  /** Stop the event from reaching the next node in the propagation path. */
44
50
  stopPropagation() {
@@ -332,13 +338,48 @@ var Entity = (_class2 = class {
332
338
  * faint-but-live control.
333
339
  */
334
340
  __init36() {this.a11yHidden = false}
341
+ /**
342
+ * Opt-in to the DOM visual projection (RFC2, implemented by `@vectojs/dom`).
343
+ * Plain data only — core never materializes an element from this; a
344
+ * registered `ProjectionBackend` does.
345
+ *
346
+ * Name mapping: this field IS the RFC4 §2 `projection` policy
347
+ * (`ProjectionPolicy`: `'canvas' | 'dom' | 'auto'` in
348
+ * `tree/scene/ProjectionPolicy.ts`) — predates the RFC, so the field keeps
349
+ * its name and the RFC spelling lives on the value type.
350
+ *
351
+ * - `'canvas'` (default): today's rendering. Zero behavior change.
352
+ * - `'dom'`: the node materializes as a live `HTMLElement` positioned by its
353
+ * world matrix. The live element replaces the transparent a11y mirror
354
+ * (same single-delivery reasoning as `DOMPortalEntity`), so opting in
355
+ * requires a DOM backend and a DOM environment for the AT representation.
356
+ * - `'auto'`: negotiated per node per frame by `Scene.resolveProjectionFor`
357
+ * (RFC4 §3: explicit beats automatic, fallbacks reported with reasons,
358
+ * hysteresis against flip-flop, no-DOM short-circuits to canvas).
359
+ */
360
+ __init37() {this.domPolicy = "canvas"}
361
+ /**
362
+ * Backend-side creation hint for a `'dom'`-policy node (tag/content mapping
363
+ * key, e.g. `'text' | 'button' | 'input' | 'container' | 'transform'`). Plain
364
+ * string so custom nodes need no DOM types in core; `@vectojs/dom` owns the
365
+ * registry. `''` means a plain positioned `div` with no content sync.
366
+ */
367
+ __init38() {this.domKind = ""}
368
+ /**
369
+ * Internal cache owned by the DOM backend: true while the node has a live
370
+ * projected element. The render walk reads it to skip canvas paint for
371
+ * DOM-resident nodes (so a mounted node is never double-drawn) and falls
372
+ * through to canvas while false (SSR / no backend registered). Do not set
373
+ * by hand — the backend sets it on mount and clears it on unmount.
374
+ */
375
+ __init39() {this.domResident = false}
335
376
  /**
336
377
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
337
378
  * rendering. Combined with translating a content child, this is how
338
379
  * scroll/overflow containers (e.g. `ScrollView`) keep their content inside a
339
380
  * fixed viewport. Off by default (children render unclipped). Canvas2D only.
340
381
  */
341
- __init37() {this.clipChildren = false}
382
+ __init40() {this.clipChildren = false}
342
383
  /**
343
384
  * Group this subtree's projected text into its own accessibility **region**,
344
385
  * without clipping anything.
@@ -365,14 +406,14 @@ var Entity = (_class2 = class {
365
406
  *
366
407
  * Off by default. Regions nest: the nearest enclosing region wins.
367
408
  */
368
- __init38() {this.a11yRegion = false}
409
+ __init41() {this.a11yRegion = false}
369
410
  // Lazily allocated (see _drivers above). Most entities never register a
370
411
  // listener or an imperative animate() tween.
371
- __init39() {this.listeners = null}
412
+ __init42() {this.listeners = null}
372
413
  /** Capture-phase listeners (fired root→target before bubble). */
373
- __init40() {this.captureListeners = null}
374
- __init41() {this.animations = null}
375
- constructor(id) {;_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);
414
+ __init43() {this.captureListeners = null}
415
+ __init44() {this.animations = null}
416
+ constructor(id) {;_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);
376
417
  this.id = id || `entity_${Math.random().toString(36).substring(2, 9)}`;
377
418
  }
378
419
  /**
@@ -633,8 +674,7 @@ var Entity = (_class2 = class {
633
674
  (e) => new Promise((resolve) => {
634
675
  this._spawnDriver(e[0], e[1], cfg);
635
676
  const d = _optionalChain([this, 'access', _84 => _84._drivers, 'optionalAccess', _85 => _85.get, 'call', _86 => _86(e[0])]);
636
- if (!d)
637
- resolve();
677
+ if (!d) resolve();
638
678
  else d.onDone = resolve;
639
679
  })
640
680
  )
@@ -824,6 +864,7 @@ var Entity = (_class2 = class {
824
864
  el.focus();
825
865
  return;
826
866
  }
867
+ if (typeof requestAnimationFrame === "undefined") return;
827
868
  requestAnimationFrame(() => {
828
869
  const retry = _optionalChain([this, 'access', _109 => _109.scene, 'optionalAccess', _110 => _110.getA11yElement, 'call', _111 => _111(this.id)]);
829
870
  if (retry) retry.focus();
@@ -1190,6 +1231,10 @@ var Entity = (_class2 = class {
1190
1231
  * `selectable` is set — natively selectable. Returns `null` by default.
1191
1232
  * Read on the a11y sync cadence, so text changes propagate automatically.
1192
1233
  *
1234
+ * This is the descriptor half of the `ContentProjection` row of
1235
+ * `ProjectionBackend` (`tree/scene/ProjectionBackend.ts`): the entity
1236
+ * describes *what* to project, the scene owns *how* it is materialized.
1237
+ *
1193
1238
  * @param hint - Optional advice about which part of the entity is worth
1194
1239
  * describing. Purely an optimization: ignoring it is always correct, which
1195
1240
  * is why it is a parameter rather than a required contract change. See
@@ -1267,27 +1312,27 @@ var MSDFTextEntity = (_class3 = class extends Entity {
1267
1312
  // joined by soft hyphens (U+00AD); the worker then treats those as break
1268
1313
  // opportunities. `text` keeps the original string for a11y/content
1269
1314
  // projection; `layoutText` is the soft-hyphen-annotated string sent to layout.
1270
- __init42() {this.hyphenator = null}
1271
- __init43() {this.layoutText = ""}
1272
- __init44() {this.text = ""}
1273
- __init45() {this.lastRenderedSeqId = 0}
1315
+ __init45() {this.hyphenator = null}
1316
+ __init46() {this.layoutText = ""}
1317
+ __init47() {this.text = ""}
1318
+ __init48() {this.lastRenderedSeqId = 0}
1274
1319
  /** Bumped by {@link queueLayout}; read by `Scene` to skip an unchanged sync. */
1275
- __init46() {this.contentEpoch = 0}
1320
+ __init49() {this.contentEpoch = 0}
1276
1321
  // Atlas-decode subscription (see watchAtlasDecode). Held so `destroy()` can
1277
1322
  // release it: the handler closes over `this`, so leaving it attached to a
1278
1323
  // long-lived shared atlas image would retain the whole entity.
1279
- __init47() {this.atlasDecodeTarget = null}
1280
- __init48() {this.atlasDecodeHandler = null}
1281
- __init49() {this.fontStringCache = []}
1282
- __init50() {this.layoutResult = null}
1324
+ __init50() {this.atlasDecodeTarget = null}
1325
+ __init51() {this.atlasDecodeHandler = null}
1326
+ __init52() {this.fontStringCache = []}
1327
+ __init53() {this.layoutResult = null}
1283
1328
  /**
1284
1329
  * Visual rows rebuilt from {@link layoutResult} (see
1285
1330
  * {@link rebuildProjectionLines}). Empty until a layout reply lands and the
1286
1331
  * reply's shaped glyphs can be mapped back to the source text 1:1.
1287
1332
  */
1288
- __init51() {this.projectionLines = []}
1333
+ __init54() {this.projectionLines = []}
1289
1334
  constructor(text, options) {
1290
- super();_class3.prototype.__init42.call(this);_class3.prototype.__init43.call(this);_class3.prototype.__init44.call(this);_class3.prototype.__init45.call(this);_class3.prototype.__init46.call(this);_class3.prototype.__init47.call(this);_class3.prototype.__init48.call(this);_class3.prototype.__init49.call(this);_class3.prototype.__init50.call(this);_class3.prototype.__init51.call(this);;
1335
+ super();_class3.prototype.__init45.call(this);_class3.prototype.__init46.call(this);_class3.prototype.__init47.call(this);_class3.prototype.__init48.call(this);_class3.prototype.__init49.call(this);_class3.prototype.__init50.call(this);_class3.prototype.__init51.call(this);_class3.prototype.__init52.call(this);_class3.prototype.__init53.call(this);_class3.prototype.__init54.call(this);;
1291
1336
  this.font = options.font;
1292
1337
  this.texture = options.texture;
1293
1338
  this.fallbackFont = _nullishCoalesce(options.fallbackFont, () => ( "sans-serif"));
@@ -1671,23 +1716,23 @@ var SVGEntity = (_class4 = class extends Entity {
1671
1716
  * rasterized. Set to `'transparent'` to opt out and keep the box empty.
1672
1717
  * Default `'rgba(248,113,113,0.9)'`.
1673
1718
  */
1674
- __init52() {this.fallbackStroke = "rgba(248,113,113,0.9)"}
1719
+ __init55() {this.fallbackStroke = "rgba(248,113,113,0.9)"}
1675
1720
  /** Fill behind the fallback marker. Default `'rgba(248,113,113,0.12)'`. */
1676
- __init53() {this.fallbackFill = "rgba(248,113,113,0.12)"}
1677
- __init54() {this.svgSource = ""}
1678
- __init55() {this.imageBitmap = null}
1679
- __init56() {this.imageElement = null}
1680
- __init57() {this.blobURL = null}
1681
- __init58() {this.currentImg = null}
1682
- __init59() {this.lodTimeout = null}
1683
- __init60() {this.rasterFailed = false}
1684
- __init61() {this.cachedDoc = null}
1685
- __init62() {this.baseWidth = 100}
1686
- __init63() {this.baseHeight = 100}
1687
- __init64() {this.lastRasterizedScale = 1}
1688
- __init65() {this.targetScale = 1}
1721
+ __init56() {this.fallbackFill = "rgba(248,113,113,0.12)"}
1722
+ __init57() {this.svgSource = ""}
1723
+ __init58() {this.imageBitmap = null}
1724
+ __init59() {this.imageElement = null}
1725
+ __init60() {this.blobURL = null}
1726
+ __init61() {this.currentImg = null}
1727
+ __init62() {this.lodTimeout = null}
1728
+ __init63() {this.rasterFailed = false}
1729
+ __init64() {this.cachedDoc = null}
1730
+ __init65() {this.baseWidth = 100}
1731
+ __init66() {this.baseHeight = 100}
1732
+ __init67() {this.lastRasterizedScale = 1}
1733
+ __init68() {this.targetScale = 1}
1689
1734
  constructor(svgSource, id) {
1690
- super(id);_class4.prototype.__init52.call(this);_class4.prototype.__init53.call(this);_class4.prototype.__init54.call(this);_class4.prototype.__init55.call(this);_class4.prototype.__init56.call(this);_class4.prototype.__init57.call(this);_class4.prototype.__init58.call(this);_class4.prototype.__init59.call(this);_class4.prototype.__init60.call(this);_class4.prototype.__init61.call(this);_class4.prototype.__init62.call(this);_class4.prototype.__init63.call(this);_class4.prototype.__init64.call(this);_class4.prototype.__init65.call(this);;
1735
+ super(id);_class4.prototype.__init55.call(this);_class4.prototype.__init56.call(this);_class4.prototype.__init57.call(this);_class4.prototype.__init58.call(this);_class4.prototype.__init59.call(this);_class4.prototype.__init60.call(this);_class4.prototype.__init61.call(this);_class4.prototype.__init62.call(this);_class4.prototype.__init63.call(this);_class4.prototype.__init64.call(this);_class4.prototype.__init65.call(this);_class4.prototype.__init66.call(this);_class4.prototype.__init67.call(this);_class4.prototype.__init68.call(this);;
1691
1736
  this.setSVGSource(svgSource);
1692
1737
  }
1693
1738
  setSVGSource(svgSource) {