@vectojs/core 1.20.0 → 1.22.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.
@@ -12,7 +12,70 @@
12
12
  * r.fill('#38bdf8');
13
13
  * }
14
14
  */
15
+ /**
16
+ * Per-backend draw counters, for a DevTools GPU readout.
17
+ *
18
+ * Opt-in via {@link IRenderer.setDrawCounters}: when off, counting compiles to a
19
+ * single boolean test per op, matching how `Scene` gates its phase timing and
20
+ * dirty tracking. Totals are cumulative until cleared rather than per-frame,
21
+ * because a per-frame log answers the question less directly at far more memory —
22
+ * the same reasoning `Scene.renderPhases` documents.
23
+ */
24
+ export interface DrawCounters {
25
+ /** `fill()` calls, each committing one path. */
26
+ fills: number;
27
+ /** `stroke()` calls. */
28
+ strokes: number;
29
+ /** `fillText()` calls. */
30
+ texts: number;
31
+ /** `drawImage`/`drawImageRect` blits. */
32
+ images: number;
33
+ /** `fillCircle()` calls — batched, so this exceeds the fills they produce. */
34
+ circles: number;
35
+ /** Batch commits from {@link IRenderer.flush}. */
36
+ flushes: number;
37
+ /** `save()` calls. */
38
+ saves: number;
39
+ /** `restore()` calls. */
40
+ restores: number;
41
+ /** `clip()` calls. */
42
+ clips: number;
43
+ /**
44
+ * Times a font or fill style actually changed, as opposed to being re-set to
45
+ * the value it already had.
46
+ *
47
+ * The renderer already elides redundant sets; this counts the ones that got
48
+ * through, which is the number that costs anything.
49
+ */
50
+ stateSwitches: number;
51
+ /**
52
+ * Sum of drawn primitive areas divided by canvas area.
53
+ *
54
+ * A PROXY for overdraw, not a measurement: Canvas2D exposes no pixel-coverage
55
+ * readback, so this counts area submitted, ignoring clipping and off-screen
56
+ * rejection. It overstates, sometimes badly. Useful as a trend between two
57
+ * states of the same scene; meaningless as an absolute.
58
+ */
59
+ overdrawRatio: number;
60
+ }
15
61
  export interface IRenderer {
62
+ /**
63
+ * Stable backend identifier.
64
+ *
65
+ * A discriminator rather than `constructor.name`, which minifies to something
66
+ * unusable in a production bundle — and a debug tool that cannot name the
67
+ * backend in the build where it matters is not much of a debug tool.
68
+ */
69
+ readonly kind?: 'canvas2d' | 'svg' | 'three' | string;
70
+ /**
71
+ * Enable or disable draw counting. Optional; a backend that cannot count omits
72
+ * it and a reader treats the absence as "not available".
73
+ */
74
+ setDrawCounters?(enabled: boolean): void;
75
+ /** Current counter totals, or null when counting is off. */
76
+ getDrawCounters?(): DrawCounters | null;
77
+ /** Zero the totals without disabling counting. */
78
+ clearDrawCounters?(): void;
16
79
  /** Clear the entire drawing surface to transparent / background color. */
17
80
  clear(): void;
18
81
  /** Push the current transform + state onto the renderer's stack. */
@@ -115,6 +178,37 @@ export interface IRenderer {
115
178
  * @param dh - Destination height.
116
179
  */
117
180
  drawImage(source: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
181
+ /**
182
+ * Draw a sub-rectangle of an image source — the 9-argument `drawImage`.
183
+ *
184
+ * **Optional.** Callers must feature-detect and keep a fallback path:
185
+ *
186
+ * ```ts
187
+ * if (r.drawImageRect) r.drawImageRect(atlas, sx, sy, sw, sh, dx, dy, dw, dh);
188
+ * else r.fillText(glyph, x, baselineY, font, color);
189
+ * ```
190
+ *
191
+ * This exists for texture atlases (see `GlyphRasterAtlas`), where selecting one slot
192
+ * out of a shared canvas is what makes the blit cheap: a per-source-canvas
193
+ * cache re-binds a different texture almost every call and measured *slower*
194
+ * than the `fillText` it replaced on Chrome at scale, while atlas blits stay
195
+ * flat and run ~2x faster on both engines.
196
+ *
197
+ * `SVGRenderer` deliberately omits it: an SVG image embeds its source as a data
198
+ * URL, so a per-cell sub-rect would inline the entire atlas once per cell —
199
+ * and vector text is the correct output for a vector export regardless.
200
+ *
201
+ * @param source - The image source.
202
+ * @param sx - Source X, in source-image pixels.
203
+ * @param sy - Source Y, in source-image pixels.
204
+ * @param sw - Source width, in source-image pixels.
205
+ * @param sh - Source height, in source-image pixels.
206
+ * @param dx - Destination X.
207
+ * @param dy - Destination Y.
208
+ * @param dw - Destination width.
209
+ * @param dh - Destination height.
210
+ */
211
+ drawImageRect?(source: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
118
212
  /**
119
213
  * Fill the current path with the given color or gradient.
120
214
  *
@@ -13,6 +13,8 @@ export interface SVGLinearGradient {
13
13
  createMatrix: number[];
14
14
  }
15
15
  export declare class SVGRenderer implements IRenderer {
16
+ /** Backend discriminator; see {@link IRenderer.kind}. */
17
+ readonly kind = "svg";
16
18
  private width;
17
19
  private height;
18
20
  private buffer;
@@ -4,6 +4,29 @@
4
4
  * render `getBatchCircle()` / `getBatchRect()` entities — the point-cloud /
5
5
  * particle case where Canvas2D tops out at ~7 fps for 100k primitives.
6
6
  */
7
+ /** Per-frame and cumulative WebGL draw accounting. */
8
+ export interface WebGLDrawStats {
9
+ /** Draw calls issued for the last completed frame. */
10
+ drawCalls: number;
11
+ /** Cumulative draw calls since creation. */
12
+ totalDrawCalls: number;
13
+ /** Cumulative mid-frame MSDF atlas switches, each costing an extra draw. */
14
+ atlasSwitches: number;
15
+ /** Programs compiled at creation — a fixed capability, not a per-frame cost. */
16
+ programs: number;
17
+ /** Textures currently allocated (colour atlas and/or MSDF atlas). */
18
+ textures: number;
19
+ /**
20
+ * Circles routed to the quad path rather than `gl.POINTS`, cumulatively.
21
+ *
22
+ * A circle takes the quad path when it could clip off-viewport or exceeds the
23
+ * driver's maximum aliased point size. A high share means the POINTS fast path
24
+ * is not being used and each circle costs four vertices instead of one.
25
+ */
26
+ circleQuadFallbacks: number;
27
+ /** Circles drawn through the `gl.POINTS` fast path, cumulatively. */
28
+ circlePoints: number;
29
+ }
7
30
  export interface PointRenderer {
8
31
  /** Resize the backing buffer + GL viewport to a logical `w × h` (DPR applied). */
9
32
  resize(width: number, height: number): void;
@@ -21,6 +44,16 @@ export interface PointRenderer {
21
44
  maxDPR?: number;
22
45
  /** Begin a frame: reset the accumulated primitive buffers. */
23
46
  begin(): void;
47
+ /**
48
+ * Draw-call counters for the most recent frame, plus cumulative totals.
49
+ *
50
+ * Batching here is by primitive type — one draw per active type — so draw calls
51
+ * and batches are the same number, and both are bounded at five plus one per
52
+ * mid-frame MSDF atlas switch. That switch is the only variable term and the
53
+ * only thing worth watching: it forces a commit of glyphs batched against the
54
+ * previous font.
55
+ */
56
+ stats?(): WebGLDrawStats;
24
57
  /** Add one circle in world (CSS-pixel) coordinates; `alpha` multiplies the color's. */
25
58
  addCircle(x: number, y: number, radius: number, color: string, alpha?: number): void;
26
59
  /**
@@ -4,4 +4,5 @@ export * from './WebGLPointRenderer';
4
4
  export * from './WebGPUParticleSystemManager';
5
5
  export * from './IRenderer';
6
6
  export * from './colorParse';
7
+ export * from './GlyphRasterAtlas';
7
8
  export * from './TextRasterCache';
package/dist/renderer.js CHANGED
@@ -6,12 +6,14 @@
6
6
 
7
7
 
8
8
 
9
- var _chunkL4SWVP2Hjs = require('./chunk-L4SWVP2H.js');
10
9
 
10
+ var _chunkLHONR3GOjs = require('./chunk-LHONR3GO.js');
11
11
 
12
12
 
13
13
 
14
14
 
15
15
 
16
16
 
17
- exports.CanvasRenderer = _chunkL4SWVP2Hjs.CanvasRenderer; exports.SVGRenderer = _chunkL4SWVP2Hjs.SVGRenderer; exports.TextRasterCache = _chunkL4SWVP2Hjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkL4SWVP2Hjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkL4SWVP2Hjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkL4SWVP2Hjs.parseColorToRGBA;
17
+
18
+
19
+ exports.CanvasRenderer = _chunkLHONR3GOjs.CanvasRenderer; exports.GlyphRasterAtlas = _chunkLHONR3GOjs.GlyphRasterAtlas; exports.SVGRenderer = _chunkLHONR3GOjs.SVGRenderer; exports.TextRasterCache = _chunkLHONR3GOjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkLHONR3GOjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkLHONR3GOjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkLHONR3GOjs.parseColorToRGBA;
package/dist/renderer.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  CanvasRenderer,
3
+ GlyphRasterAtlas,
3
4
  SVGRenderer,
4
5
  TextRasterCache,
5
6
  WebGPUParticleSystemManager,
6
7
  createWebGLPointRenderer,
7
8
  parseColorToRGBA
8
- } from "./chunk-QS3CUV7H.mjs";
9
+ } from "./chunk-UPULSLKA.mjs";
9
10
  export {
10
11
  CanvasRenderer,
12
+ GlyphRasterAtlas,
11
13
  SVGRenderer,
12
14
  TextRasterCache,
13
15
  WebGPUParticleSystemManager,
package/dist/text.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkYLH7F4ZVjs = require('./chunk-YLH7F4ZV.js');
5
+ var _chunkAGP4VLF4js = require('./chunk-AGP4VLF4.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 = _chunkYLH7F4ZVjs.MSDFTextEntity; exports.SVGEntity = _chunkYLH7F4ZVjs.SVGEntity;
12
+ exports.MSDFTextEntity = _chunkAGP4VLF4js.MSDFTextEntity; exports.SVGEntity = _chunkAGP4VLF4js.SVGEntity;
package/dist/text.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MSDFTextEntity,
3
3
  SVGEntity
4
- } from "./chunk-TJCXB2F6.mjs";
4
+ } from "./chunk-FRMLD4PP.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -159,6 +159,107 @@ export interface TextInputStyle {
159
159
  * to create and label the shadow DOM node (e.g. a real `<button>` or `<a href>`)
160
160
  * so the canvas stays accessible and clickable by automation/agents.
161
161
  */
162
+ /**
163
+ * One value in a {@link DevtoolsDescriptor} group.
164
+ *
165
+ * JSON-safe by construction: DevTools serializes descriptors to render a panel,
166
+ * to write a snapshot, and to cross a `postMessage` bridge, so a value that
167
+ * cannot survive `structuredClone` is a bug rather than a limitation.
168
+ */
169
+ export interface DevtoolsField {
170
+ /** Field name as shown in the inspector, e.g. `'scrollTop'`. */
171
+ label: string;
172
+ /** Current value. Keep to primitives, or short arrays/records of primitives. */
173
+ value: string | number | boolean | null | ReadonlyArray<string | number> | Record<string, string | number | boolean>;
174
+ /**
175
+ * Optional one-line explanation, shown as a tooltip.
176
+ *
177
+ * Worth spending: a reader looking at `visibleRange: [12, 34]` cannot tell
178
+ * whether the bounds are inclusive without being told.
179
+ */
180
+ hint?: string;
181
+ /**
182
+ * Mark a value that reflects derived or externally-owned state, so the panel
183
+ * can show it as read-only rather than inviting an edit that will be silently
184
+ * reverted. A `Stack`-laid-out child's `x` is the canonical example.
185
+ */
186
+ readOnly?: boolean;
187
+ }
188
+ /**
189
+ * A component's self-description for DevTools.
190
+ *
191
+ * Without this, the inspector can only show generic `Entity` properties —
192
+ * position, size, opacity — so everything that makes a component a component is
193
+ * invisible: `Input.value`, `Slider.min`/`max`, `ScrollView.scrollTop`,
194
+ * `VirtualList.visibleRange`, a `Markdown` block's token counts. The alternative
195
+ * is DevTools carrying a table of component types, which inverts the dependency
196
+ * (a debug tool would gate every new component) and breaks under minified builds
197
+ * where `constructor.name` is unreliable.
198
+ *
199
+ * Implement {@link Entity.getDevtoolsDescriptor} to opt in. Cost is paid only
200
+ * when a panel actually inspects the entity, so a descriptor may compute values
201
+ * it would not compute per frame.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * public override getDevtoolsDescriptor(): DevtoolsDescriptor {
206
+ * return {
207
+ * kind: 'ScrollView',
208
+ * groups: [{
209
+ * label: 'Scroll',
210
+ * fields: [
211
+ * { label: 'scrollTop', value: this.scrollTop },
212
+ * { label: 'contentHeight', value: this.contentHeight, readOnly: true },
213
+ * ],
214
+ * }],
215
+ * };
216
+ * }
217
+ * ```
218
+ */
219
+ export interface DevtoolsDescriptor {
220
+ /**
221
+ * Component kind for display, e.g. `'VirtualList'`.
222
+ *
223
+ * Provided explicitly rather than read from `constructor.name`, which minifies
224
+ * to something meaningless in a production bundle.
225
+ */
226
+ kind: string;
227
+ /** Grouped fields, rendered as sections in the order given. */
228
+ groups: ReadonlyArray<{
229
+ label: string;
230
+ fields: ReadonlyArray<DevtoolsField>;
231
+ }>;
232
+ /**
233
+ * Free-form notes: a caveat, a known-slow path, a link to a doc section.
234
+ * Rendered under the groups.
235
+ */
236
+ notes?: ReadonlyArray<string>;
237
+ /**
238
+ * Stable identity for snapshot diffing, independent of tree position.
239
+ *
240
+ * Snapshot paths are structural indices (`root > Card[0] > Text[2]`), so
241
+ * inserting at the head of a list renames every sibling, so the diff describes
242
+ * different nodes than the ones that changed. A key that survives reordering — a
243
+ * row id, a message id — keeps each entry attributed to the node it belongs to.
244
+ * Most relevant to `VirtualList` and `Table`, where recycling moves entities
245
+ * constantly.
246
+
247
+ */
248
+ devtoolsKey?: string;
249
+ }
250
+ /**
251
+ * Entity properties a parent computes for its children.
252
+ *
253
+ * Editing one of these on a child is silently reverted by the next layout pass,
254
+ * which looks like the editor being broken rather than the value being owned
255
+ * elsewhere. A container declares what it controls so a tool can say so up front
256
+ * instead of letting a user discover it by watching their change disappear.
257
+ *
258
+ * Declared by the parent rather than detected by the tool: only the container
259
+ * knows whether it writes `x` unconditionally, and a table of container types
260
+ * inside DevTools would gate every new layout component on a debug-tool change.
261
+ */
262
+ export type LayoutControlledProperty = 'x' | 'y' | 'width' | 'height' | 'scaleX' | 'scaleY' | 'rotation' | 'opacity';
162
263
  export interface A11yAttributes {
163
264
  /** Shadow element tag to create. Defaults to `'div'`. */
164
265
  tag?: 'div' | 'a' | 'button' | 'img' | 'input' | 'textarea';
@@ -726,6 +827,33 @@ export declare abstract class Entity {
726
827
  *
727
828
  * @returns The {@link A11yAttributes} for this entity's shadow node.
728
829
  */
830
+ /**
831
+ * Describe this entity's own debug surface for DevTools.
832
+ *
833
+ * Returns `null` by default, meaning "nothing beyond the generic `Entity`
834
+ * fields the inspector already shows". Override in a component to expose the
835
+ * state that makes it inspectable — see {@link DevtoolsDescriptor}.
836
+ *
837
+ * Called only while a panel is inspecting this entity, never per frame, so it
838
+ * may compute values that would be too expensive to track continuously.
839
+ *
840
+ * @returns A descriptor, or `null` to opt out.
841
+ */
842
+ getDevtoolsDescriptor(): DevtoolsDescriptor | null;
843
+ /**
844
+ * Which of a child's properties this entity computes during layout.
845
+ *
846
+ * Returns an empty array by default, meaning "this entity does not position its
847
+ * children". A container that lays out children — `Stack`, `Table`, `Tabs` —
848
+ * overrides it so tooling can mark those values as parent-owned: editing `x` on
849
+ * a `Stack` child is reverted by the next layout, and knowing that in advance is
850
+ * the difference between a confusing tool and a correct one.
851
+ *
852
+ * @param child - The child being asked about. Containers whose control depends
853
+ * on the child (a `Table` cell versus its header) can answer per child.
854
+ * @returns Property names this entity overwrites on that child.
855
+ */
856
+ getLayoutControlledProperties(child: Entity): ReadonlyArray<LayoutControlledProperty>;
729
857
  getA11yAttributes(): A11yAttributes;
730
858
  /**
731
859
  * Local-space axis-aligned bounding box of what this entity's {@link render}
@@ -12,6 +12,7 @@ export interface IWebGPUParticleSystemManager {
12
12
  }
13
13
  import { Entity } from './Entity';
14
14
  import { IRenderer } from '../renderer/IRenderer';
15
+ import type { WebGLDrawStats } from '../renderer/WebGLPointRenderer';
15
16
  import { type WasmModuleSource, type WasmTransformBackend } from '../wasm/backend';
16
17
  import { type CoreWasmRuntime } from '../wasm/runtime';
17
18
  import { type HitModuleSource, type HitTestBackend } from '../wasm/hit-backend';
@@ -25,7 +26,41 @@ import { type ParticleModuleSource, type ParticleBackend } from '../wasm/particl
25
26
  * `a11ySync` and `a11yOrder` run after `render` in the frame loop, so they are
26
27
  * siblings of it, not children.
27
28
  */
28
- export type RenderPhase = 'render' | 'transform' | 'drawWalk' | 'flush' | 'a11ySync' | 'a11yOrder'
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'
29
64
  /** Sum of every entity's own render(), nested inside drawWalk. */
30
65
  | 'entityPaint';
31
66
  export interface RenderPhaseEntry {
@@ -358,6 +393,29 @@ export declare class Scene {
358
393
  private contentGridCalibrationFrames;
359
394
  /** Detached, untransformed font probes used by the cold calibration pass. */
360
395
  private contentGridCalibrationProbes;
396
+ /**
397
+ * Monotonic stamp identifying the conditions grid cells were calibrated under.
398
+ *
399
+ * Calibration measures the difference between the advance the canvas grid assigns
400
+ * a cluster and the width the browser lays it out at, then writes a per-cell
401
+ * `scaleX`. That result stays valid until the font or the page scale changes, and
402
+ * it lives on the cell element — so a cell carrying this stamp needs no further
403
+ * work.
404
+ *
405
+ * The scan that feeds calibration was O(cells) on every revision bump: for a
406
+ * streaming code block it re-derived a measurement key for every cell in the
407
+ * block each frame in order to produce only ~20 distinct keys, costing about
408
+ * 2.5 ms/frame after the `style.font` fix and still over half of `a11ySync`. Since
409
+ * carrier reuse (#244) leaves untouched lines — and therefore their calibrated
410
+ * transforms — in place, cells stamped with the current generation can simply be
411
+ * skipped, making the scan O(new cells) instead.
412
+ *
413
+ * A plain incrementing integer rather than the descriptive calibration key,
414
+ * because it goes into an attribute selector and must not need escaping.
415
+ */
416
+ private contentGridCalibrationGeneration;
417
+ /** The `(fontEpoch, pageScale)` pair the current generation corresponds to. */
418
+ private contentGridCalibrationStamp;
361
419
  /** Invalidates grid font calibration after browser font availability changes. */
362
420
  private contentFontEpoch;
363
421
  /** Cached Canvas-to-client scale for the current font/viewport epoch. */
@@ -751,6 +809,23 @@ export declare class Scene {
751
809
  particleBackend: 'auto' | 'webgpu' | 'cpu';
752
810
  private _webgpuDisabled;
753
811
  get webgpuDisabled(): boolean;
812
+ /**
813
+ * Draw accounting for the WebGL point layer, or null when that layer is not in
814
+ * use.
815
+ *
816
+ * Null and all-zero mean different things: null is "this backend is not
817
+ * running", zero is "it ran and drew nothing". A readout that conflates them
818
+ * sends someone looking for a performance problem in a backend that was never
819
+ * active.
820
+ */
821
+ get webglDrawStats(): WebGLDrawStats | null;
822
+ /**
823
+ * Whether a WebGPU device is currently live for particle compute.
824
+ *
825
+ * The WebGPU path only activates when a `ComputeParticleEntity` is present, so
826
+ * most scenes never touch it.
827
+ */
828
+ get webgpuActive(): boolean;
754
829
  set webgpuDisabled(value: boolean);
755
830
  private recoveryTimerId;
756
831
  private manager;
@@ -800,6 +875,15 @@ export declare class Scene {
800
875
  */
801
876
  private setupGLContextRecovery;
802
877
  private endContentSelectionDrag;
878
+ /**
879
+ * Index of the carrier line currently holding a selection inside `el`, or
880
+ * `null`.
881
+ *
882
+ * Lets a partial re-materialization decide whether the user's selection is even
883
+ * affected. Checks the tracked anchor first (it survives a drag) and falls back
884
+ * to the live DOM selection.
885
+ */
886
+ private contentGridSelectionLine;
803
887
  private releaseContentSelectionForRebuild;
804
888
  /**
805
889
  * Rebuild a content-projection element's DOM (`rebuild`) while preserving a
@@ -836,6 +920,17 @@ export declare class Scene {
836
920
  * @example scene.add(new CircleEntity());
837
921
  */
838
922
  add(entity: Entity): this;
923
+ /**
924
+ * Reset per-grid calibration and bookkeeping before a (re)materialization.
925
+ *
926
+ * @param entityId - Owning entity, keyed into the calibration maps.
927
+ * @param el - The projection element.
928
+ * @param releaseSelection - Whether to drop a selection this element owns.
929
+ * Pass `false` when carrier lines are being reused: the selection's DOM nodes
930
+ * survive the pass, so tearing it down would wipe a user's selection on every
931
+ * streamed chunk — the exact bug `preserveContentSelectionAcrossRebuild`
932
+ * exists to prevent on the non-grid path.
933
+ */
839
934
  private clearContentGridState;
840
935
  /**
841
936
  * Drop any projected elements under `node` without touching the entity tree.
@@ -933,6 +1028,26 @@ export declare class Scene {
933
1028
  get rootEntity(): Entity;
934
1029
  /** The overlay layer root (see {@link showOverlay}), read-only for tooling. */
935
1030
  get overlayRootEntity(): Entity;
1031
+ /**
1032
+ * Advance and render exactly one frame, synchronously.
1033
+ *
1034
+ * This renders UNCONDITIONALLY: it consults neither {@link renderMode} nor
1035
+ * {@link dirty}, and it does not apply the `always`-mode idle auto-throttle.
1036
+ * That is deliberate — a deterministic driver (video export, a test, a
1037
+ * fixed-step benchmark) asks for a frame because it wants that frame, not a
1038
+ * scheduler opinion about whether it is needed.
1039
+ *
1040
+ * The consequence is a measurement footgun worth stating explicitly: a
1041
+ * benchmark that drives frames through `step()` CANNOT observe frame skipping,
1042
+ * so `always` and `onDemand` produce byte-identical draw counts through this
1043
+ * path. An investigation into whether `onDemand` skips redundant repaints once
1044
+ * concluded "it does not" on exactly that basis; on the real rAF loop the same
1045
+ * workload rendered ~1.0 frames per content change. To measure anything about
1046
+ * scheduling, use {@link start} and let `requestAnimationFrame` drive.
1047
+ *
1048
+ * @param dt Seconds to advance. Not clamped by `MAX_FRAME_DT` — the caller
1049
+ * chooses the step, since determinism is the point.
1050
+ */
936
1051
  step(dt: number): void;
937
1052
  /**
938
1053
  * Mark the scene as needing a redraw on the next frame.
@@ -941,6 +1056,22 @@ export declare class Scene {
941
1056
  * entity state outside of {@link Entity.animate} so the change is rendered.
942
1057
  */
943
1058
  markDirty(source?: DirtySource): void;
1059
+ /**
1060
+ * Increments whenever the tree's shape changes: add, remove or reparent.
1061
+ *
1062
+ * Already maintained for the resident WASM transform store (see
1063
+ * {@link markStructureChanged}, called from `Entity.add`/`remove`), and exposed
1064
+ * here because a cache of the tree's shape — a DevTools tree model, a serialized
1065
+ * snapshot — is valid exactly as long as this value is unchanged. Comparing it is
1066
+ * O(1) against re-walking the tree, which is what it replaces: DevTools rebuilt
1067
+ * both trees on a fixed 500 ms interval, a constant cost proportional to entity
1068
+ * count, purely because it had no way to ask whether the shape had changed.
1069
+ *
1070
+ * Property changes do NOT bump it. Moving or restyling an entity leaves the
1071
+ * shape intact, so a consumer that also cares about values must read those
1072
+ * directly rather than rebuilding a tree.
1073
+ */
1074
+ get structureVersion(): number;
944
1075
  /**
945
1076
  * Record who marked the scene dirty and why.
946
1077
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.20.0",
3
+ "version": "1.22.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@vectojs/animation": "^0.1.1",
68
- "@vectojs/layout": "^0.3.0",
68
+ "@vectojs/layout": "^0.4.0",
69
69
  "@vectojs/math": "^0.1.1",
70
70
  "@vectojs/text": "^0.2.0"
71
71
  },