@vectojs/core 1.19.0 → 1.21.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.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * A texture atlas of rasterized glyphs, for grids that draw the *same small set
3
+ * of glyphs* thousands of times per frame.
4
+ *
5
+ * Named `GlyphRasterAtlas` rather than `GlyphAtlas` because `@vectojs/layout`
6
+ * already exports a `GlyphAtlas` interface — a map of grapheme to *vector* path
7
+ * metrics — and the core barrel re-exports that package, so the shorter name is a
8
+ * hard collision. The distinction is also worth keeping: that one holds path data
9
+ * for measuring, this one holds pixels for blitting.
10
+ *
11
+ * ## Why this exists alongside {@link TextRasterCache}
12
+ *
13
+ * Both replace per-cell `fillText` with a bitmap blit. The difference is where
14
+ * the pixels live, and measurement says that difference decides whether the idea
15
+ * works at all.
16
+ *
17
+ * `TextRasterCache` allocates **one canvas per cached run**. A warm cache for a
18
+ * syntax-highlighted code grid holds a few hundred of them (glyphs x theme
19
+ * colours), so a frame blits from a few hundred distinct source textures and the
20
+ * GPU re-binds on nearly every call. Measured on real hardware, that per-source
21
+ * cost is invisible at 2k cells and dominant at 40k: Chrome went 1.82x at 2k to
22
+ * **0.87x at 40k** — slower than the `fillText` it replaced. Its per-call cost
23
+ * grows with cell count (1.22 -> 2.89 us) rather than staying flat.
24
+ *
25
+ * This atlas keeps every glyph in **one** canvas and selects with a source rect,
26
+ * so the source texture never changes. Same call count, same pixels, same
27
+ * geometry — and per-call cost is flat as the grid grows (Chrome 1.10 -> 1.11
28
+ * us), giving **1.90-2.27x over `fillText` on both engines at every size**.
29
+ * Full data: `vectojs-docs/forge/baselines/raster-cache-findings.md`.
30
+ *
31
+ * The win comes from *reuse*, so this is for bounded glyph sets: a monospace code
32
+ * grid, a terminal, a data grid, a numeric HUD. Prose is the wrong customer —
33
+ * every run is distinct, so an atlas is pure overhead (use `RichText`'s coalesced
34
+ * runs there instead).
35
+ *
36
+ * ## Requires a source-rect blit
37
+ *
38
+ * Selecting one slot needs {@link IRenderer.drawImageRect}, which is optional:
39
+ * `CanvasRenderer` implements it, `SVGRenderer` deliberately does not (an SVG
40
+ * blit embeds its source as a data URL, so a per-cell sub-rect would inline the
41
+ * whole atlas thousands of times — and vector text is the correct output for a
42
+ * vector export anyway). Callers must keep their `fillText` path for renderers
43
+ * that lack it:
44
+ *
45
+ * ```ts
46
+ * const slot = atlas.get(font, color, glyph);
47
+ * if (slot && r.drawImageRect) {
48
+ * r.drawImageRect(atlas.source, slot.sx, slot.sy, slot.sw, slot.sh,
49
+ * x - slot.offsetX, baselineY - slot.offsetY, slot.w, slot.h);
50
+ * } else {
51
+ * r.fillText(glyph, x, baselineY, font, color);
52
+ * }
53
+ * ```
54
+ */
55
+ /** Where one glyph lives in the atlas, and how to blit it at a baseline. */
56
+ export interface GlyphSlot {
57
+ /** Source X in atlas *device* pixels. */
58
+ sx: number;
59
+ /** Source Y in atlas *device* pixels. */
60
+ sy: number;
61
+ /** Source width in atlas *device* pixels. */
62
+ sw: number;
63
+ /** Source height in atlas *device* pixels. */
64
+ sh: number;
65
+ /** Destination width in CSS pixels. */
66
+ w: number;
67
+ /** Destination height in CSS pixels. */
68
+ h: number;
69
+ /** Left inset (CSS px) of the glyph origin inside the slot. */
70
+ offsetX: number;
71
+ /** Distance (CSS px) from the slot top down to the text baseline. */
72
+ offsetY: number;
73
+ /** The cluster these pixels represent. */
74
+ glyph: string;
75
+ /** The CSS font shorthand these pixels were rasterized with. */
76
+ font: string;
77
+ /** Advance width (CSS px) of the cluster, i.e. `measureText().width`. */
78
+ advance: number;
79
+ /**
80
+ * Ink extent left of the glyph origin (CSS px), from `actualBoundingBoxLeft`.
81
+ *
82
+ * Carried on the slot so a blit can be mapped back to the same geometry a
83
+ * `fillText` would have produced. Without it, instrumentation that traces draw
84
+ * calls to verify grid positioning (`e2e/text-projection.e2e.ts`) can see only
85
+ * a destination rect and cannot recover where the glyph origin sat inside it.
86
+ */
87
+ left: number;
88
+ /** Ink extent right of the glyph origin (CSS px), from `actualBoundingBoxRight`. */
89
+ right: number;
90
+ }
91
+ /** Instrumentation counters, e.g. to surface a HUD hit rate. */
92
+ export interface GlyphRasterAtlasStats {
93
+ /** Requests served from an existing slot. */
94
+ hits: number;
95
+ /** Requests that had to rasterize. */
96
+ misses: number;
97
+ /** Glyphs currently resident. */
98
+ size: number;
99
+ /**
100
+ * Times the atlas filled up and was reset.
101
+ *
102
+ * Steady-state thrash means the glyph set is unbounded for the configured
103
+ * size, and the atlas is doing net harm — every reset re-rasterizes everything.
104
+ * A caller that watches this can fall back to `fillText` permanently.
105
+ */
106
+ resets: number;
107
+ }
108
+ /** Options for {@link GlyphRasterAtlas}. */
109
+ export interface GlyphRasterAtlasOptions {
110
+ /**
111
+ * Device-pixel-ratio to rasterize at. Slots record device pixels while `w`/`h`
112
+ * stay in CSS pixels, so the blit is DPR-correct without caller arithmetic.
113
+ * Default `1`.
114
+ */
115
+ dpr?: number;
116
+ /**
117
+ * Max atlas edge in device pixels, capped at 8192 — comfortably inside the
118
+ * lowest common `maxTextureSize` while leaving room for thousands of glyphs.
119
+ * Exceeding a browser's real limit yields a silently blank canvas, so this is
120
+ * clamped rather than trusted. Default `2048`.
121
+ */
122
+ maxSize?: number;
123
+ }
124
+ /**
125
+ * A glyph atlas. Create one per renderer/scene — instances share no state, so
126
+ * multiple scenes or an SSR pass never collide.
127
+ */
128
+ export declare class GlyphRasterAtlas {
129
+ private readonly slots;
130
+ private readonly dpr;
131
+ private readonly maxSize;
132
+ private canvas;
133
+ private ctx;
134
+ /**
135
+ * Shelf packing: glyphs land left-to-right on a row, then a new row starts.
136
+ * A monospace grid produces near-uniform widths, so shelves waste very little
137
+ * and cost one comparison per insert — a real 2D packer would buy nothing here.
138
+ */
139
+ private penX;
140
+ private penY;
141
+ private rowHeight;
142
+ private _hits;
143
+ private _misses;
144
+ private _resets;
145
+ constructor(options?: GlyphRasterAtlasOptions);
146
+ /** Live instrumentation snapshot. */
147
+ get stats(): GlyphRasterAtlasStats;
148
+ /**
149
+ * The atlas canvas, to pass as the blit source.
150
+ *
151
+ * `null` until the first successful {@link get}, and in any non-DOM context.
152
+ */
153
+ get source(): HTMLCanvasElement | null;
154
+ private ensureCanvas;
155
+ /**
156
+ * Look up a glyph, rasterizing it into the atlas on first request.
157
+ *
158
+ * @param font - Full CSS `font` shorthand, used for measuring and painting.
159
+ * @param color - CSS color baked into the pixels.
160
+ * @param glyph - A single grapheme cluster. Long strings are rejected
161
+ * (`null`): they defeat the atlas's fixed-slot packing and belong in
162
+ * `fillText` or {@link TextRasterCache}.
163
+ * @returns The slot, or `null` when the caller must fall back to `fillText`
164
+ * (headless, unrasterizable, or too large to pack).
165
+ */
166
+ get(font: string, color: string, glyph: string): GlyphSlot | null;
167
+ /**
168
+ * Find the slot occupying a source position, or `null`.
169
+ *
170
+ * The inverse of {@link get}: it maps a blit back to the glyph it drew. Exists
171
+ * for instrumentation — a test or devtool that traces `drawImage` calls sees
172
+ * only a source rect, and needs this to recover which cluster was painted and
173
+ * with what metrics. Linear over resident slots, so it is a diagnostic, not a
174
+ * per-frame call.
175
+ */
176
+ slotAt(sx: number, sy: number): GlyphSlot | null;
177
+ /**
178
+ * Drop every glyph and reuse the canvas.
179
+ *
180
+ * Call after a font or theme change: slots are keyed by `(font, color, glyph)`
181
+ * so stale entries are never *returned* wrongly, but they do occupy space.
182
+ */
183
+ reset(): void;
184
+ /** Release the backing canvas and all slots. */
185
+ destroy(): void;
186
+ }
@@ -115,6 +115,37 @@ export interface IRenderer {
115
115
  * @param dh - Destination height.
116
116
  */
117
117
  drawImage(source: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;
118
+ /**
119
+ * Draw a sub-rectangle of an image source — the 9-argument `drawImage`.
120
+ *
121
+ * **Optional.** Callers must feature-detect and keep a fallback path:
122
+ *
123
+ * ```ts
124
+ * if (r.drawImageRect) r.drawImageRect(atlas, sx, sy, sw, sh, dx, dy, dw, dh);
125
+ * else r.fillText(glyph, x, baselineY, font, color);
126
+ * ```
127
+ *
128
+ * This exists for texture atlases (see `GlyphRasterAtlas`), where selecting one slot
129
+ * out of a shared canvas is what makes the blit cheap: a per-source-canvas
130
+ * cache re-binds a different texture almost every call and measured *slower*
131
+ * than the `fillText` it replaced on Chrome at scale, while atlas blits stay
132
+ * flat and run ~2x faster on both engines.
133
+ *
134
+ * `SVGRenderer` deliberately omits it: an SVG image embeds its source as a data
135
+ * URL, so a per-cell sub-rect would inline the entire atlas once per cell —
136
+ * and vector text is the correct output for a vector export regardless.
137
+ *
138
+ * @param source - The image source.
139
+ * @param sx - Source X, in source-image pixels.
140
+ * @param sy - Source Y, in source-image pixels.
141
+ * @param sw - Source width, in source-image pixels.
142
+ * @param sh - Source height, in source-image pixels.
143
+ * @param dx - Destination X.
144
+ * @param dy - Destination Y.
145
+ * @param dw - Destination width.
146
+ * @param dh - Destination height.
147
+ */
148
+ drawImageRect?(source: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
118
149
  /**
119
150
  * Fill the current path with the given color or gradient.
120
151
  *
@@ -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 _chunkGKSCJ6AFjs = require('./chunk-GKSCJ6AF.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 = _chunkGKSCJ6AFjs.CanvasRenderer; exports.GlyphRasterAtlas = _chunkGKSCJ6AFjs.GlyphRasterAtlas; exports.SVGRenderer = _chunkGKSCJ6AFjs.SVGRenderer; exports.TextRasterCache = _chunkGKSCJ6AFjs.TextRasterCache; exports.WebGPUParticleSystemManager = _chunkGKSCJ6AFjs.WebGPUParticleSystemManager; exports.createWebGLPointRenderer = _chunkGKSCJ6AFjs.createWebGLPointRenderer; exports.parseColorToRGBA = _chunkGKSCJ6AFjs.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-RTENOAYT.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 _chunkIA3KW4CGjs = require('./chunk-IA3KW4CG.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 = _chunkIA3KW4CGjs.MSDFTextEntity; exports.SVGEntity = _chunkIA3KW4CGjs.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-XWBVBXFZ.mjs";
4
+ } from "./chunk-FRMLD4PP.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -159,6 +159,105 @@ 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 and cascades into a
242
+ * large diff. A key that survives reordering — a row id, a message id — keeps
243
+ * the diff proportional to what actually changed. Most relevant to
244
+ * `VirtualList` and `Table`, where recycling moves entities constantly.
245
+ */
246
+ devtoolsKey?: string;
247
+ }
248
+ /**
249
+ * Entity properties a parent computes for its children.
250
+ *
251
+ * Editing one of these on a child is silently reverted by the next layout pass,
252
+ * which looks like the editor being broken rather than the value being owned
253
+ * elsewhere. A container declares what it controls so a tool can say so up front
254
+ * instead of letting a user discover it by watching their change disappear.
255
+ *
256
+ * Declared by the parent rather than detected by the tool: only the container
257
+ * knows whether it writes `x` unconditionally, and a table of container types
258
+ * inside DevTools would gate every new layout component on a debug-tool change.
259
+ */
260
+ export type LayoutControlledProperty = 'x' | 'y' | 'width' | 'height' | 'scaleX' | 'scaleY' | 'rotation' | 'opacity';
162
261
  export interface A11yAttributes {
163
262
  /** Shadow element tag to create. Defaults to `'div'`. */
164
263
  tag?: 'div' | 'a' | 'button' | 'img' | 'input' | 'textarea';
@@ -416,6 +515,24 @@ export declare abstract class Entity {
416
515
  * nodes, so on-top components stay clickable.
417
516
  */
418
517
  a11yFullViewport: boolean;
518
+ /**
519
+ * Hide this entity AND its whole subtree from the accessibility/automation
520
+ * projection, regardless of each node's own `interactive` flag.
521
+ *
522
+ * For a container that is logically closed while still mounted — an `Overlay`
523
+ * after `hide()`, a collapsed panel kept in the tree for its transition. Setting
524
+ * `interactive = false` on the container alone is not enough: the projection walk
525
+ * still descends, and any still-interactive child is re-created on the next
526
+ * frame. Measured before this existed: after `Popover.hide()` the popover's own
527
+ * element was gone while its button stayed projected with `tabIndex: 0` and a
528
+ * live box, so a keyboard user could Tab into a hidden popover.
529
+ *
530
+ * Deliberately NOT inferred from `opacity`: `Overlay.hide()` springs opacity
531
+ * toward 0, so mid-transition it reads nonzero (~0.26 when measured) and an
532
+ * `=== 0` test never fires; a threshold would instead silently un-project a
533
+ * faint-but-live control.
534
+ */
535
+ a11yHidden: boolean;
419
536
  /**
420
537
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
421
538
  * rendering. Combined with translating a content child, this is how
@@ -708,6 +825,33 @@ export declare abstract class Entity {
708
825
  *
709
826
  * @returns The {@link A11yAttributes} for this entity's shadow node.
710
827
  */
828
+ /**
829
+ * Describe this entity's own debug surface for DevTools.
830
+ *
831
+ * Returns `null` by default, meaning "nothing beyond the generic `Entity`
832
+ * fields the inspector already shows". Override in a component to expose the
833
+ * state that makes it inspectable — see {@link DevtoolsDescriptor}.
834
+ *
835
+ * Called only while a panel is inspecting this entity, never per frame, so it
836
+ * may compute values that would be too expensive to track continuously.
837
+ *
838
+ * @returns A descriptor, or `null` to opt out.
839
+ */
840
+ getDevtoolsDescriptor(): DevtoolsDescriptor | null;
841
+ /**
842
+ * Which of a child's properties this entity computes during layout.
843
+ *
844
+ * Returns an empty array by default, meaning "this entity does not position its
845
+ * children". A container that lays out children — `Stack`, `Table`, `Tabs` —
846
+ * overrides it so tooling can mark those values as parent-owned: editing `x` on
847
+ * a `Stack` child is reverted by the next layout, and knowing that in advance is
848
+ * the difference between a confusing tool and a correct one.
849
+ *
850
+ * @param child - The child being asked about. Containers whose control depends
851
+ * on the child (a `Table` cell versus its header) can answer per child.
852
+ * @returns Property names this entity overwrites on that child.
853
+ */
854
+ getLayoutControlledProperties(child: Entity): ReadonlyArray<LayoutControlledProperty>;
711
855
  getA11yAttributes(): A11yAttributes;
712
856
  /**
713
857
  * Local-space axis-aligned bounding box of what this entity's {@link render}