@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.
@@ -253,20 +253,38 @@ var Entity = (_class2 = class {
253
253
  * nodes, so on-top components stay clickable.
254
254
  */
255
255
  __init33() {this.a11yFullViewport = false}
256
+ /**
257
+ * Hide this entity AND its whole subtree from the accessibility/automation
258
+ * projection, regardless of each node's own `interactive` flag.
259
+ *
260
+ * For a container that is logically closed while still mounted — an `Overlay`
261
+ * after `hide()`, a collapsed panel kept in the tree for its transition. Setting
262
+ * `interactive = false` on the container alone is not enough: the projection walk
263
+ * still descends, and any still-interactive child is re-created on the next
264
+ * frame. Measured before this existed: after `Popover.hide()` the popover's own
265
+ * element was gone while its button stayed projected with `tabIndex: 0` and a
266
+ * live box, so a keyboard user could Tab into a hidden popover.
267
+ *
268
+ * Deliberately NOT inferred from `opacity`: `Overlay.hide()` springs opacity
269
+ * toward 0, so mid-transition it reads nonzero (~0.26 when measured) and an
270
+ * `=== 0` test never fires; a threshold would instead silently un-project a
271
+ * faint-but-live control.
272
+ */
273
+ __init34() {this.a11yHidden = false}
256
274
  /**
257
275
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
258
276
  * rendering. Combined with translating a content child, this is how
259
277
  * scroll/overflow containers (e.g. `ScrollView`) keep their content inside a
260
278
  * fixed viewport. Off by default (children render unclipped). Canvas2D only.
261
279
  */
262
- __init34() {this.clipChildren = false}
280
+ __init35() {this.clipChildren = false}
263
281
  // Lazily allocated (see _drivers above). Most entities never register a
264
282
  // listener or an imperative animate() tween.
265
- __init35() {this.listeners = null}
283
+ __init36() {this.listeners = null}
266
284
  /** Capture-phase listeners (fired root→target before bubble). */
267
- __init36() {this.captureListeners = null}
268
- __init37() {this.animations = null}
269
- 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);
285
+ __init37() {this.captureListeners = null}
286
+ __init38() {this.animations = null}
287
+ 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);
270
288
  this.id = id || `entity_${Math.random().toString(36).substring(2, 9)}`;
271
289
  }
272
290
  /**
@@ -946,6 +964,38 @@ var Entity = (_class2 = class {
946
964
  *
947
965
  * @returns The {@link A11yAttributes} for this entity's shadow node.
948
966
  */
967
+ /**
968
+ * Describe this entity's own debug surface for DevTools.
969
+ *
970
+ * Returns `null` by default, meaning "nothing beyond the generic `Entity`
971
+ * fields the inspector already shows". Override in a component to expose the
972
+ * state that makes it inspectable — see {@link DevtoolsDescriptor}.
973
+ *
974
+ * Called only while a panel is inspecting this entity, never per frame, so it
975
+ * may compute values that would be too expensive to track continuously.
976
+ *
977
+ * @returns A descriptor, or `null` to opt out.
978
+ */
979
+ getDevtoolsDescriptor() {
980
+ return null;
981
+ }
982
+ /**
983
+ * Which of a child's properties this entity computes during layout.
984
+ *
985
+ * Returns an empty array by default, meaning "this entity does not position its
986
+ * children". A container that lays out children — `Stack`, `Table`, `Tabs` —
987
+ * overrides it so tooling can mark those values as parent-owned: editing `x` on
988
+ * a `Stack` child is reverted by the next layout, and knowing that in advance is
989
+ * the difference between a confusing tool and a correct one.
990
+ *
991
+ * @param child - The child being asked about. Containers whose control depends
992
+ * on the child (a `Table` cell versus its header) can answer per child.
993
+ * @returns Property names this entity overwrites on that child.
994
+ */
995
+ getLayoutControlledProperties(child) {
996
+ void child;
997
+ return [];
998
+ }
949
999
  getA11yAttributes() {
950
1000
  return {};
951
1001
  }
@@ -1041,15 +1091,15 @@ var MSDFTextEntity = (_class3 = class extends Entity {
1041
1091
  // joined by soft hyphens (U+00AD); the worker then treats those as break
1042
1092
  // opportunities. `text` keeps the original string for a11y/content
1043
1093
  // projection; `layoutText` is the soft-hyphen-annotated string sent to layout.
1044
- __init38() {this.hyphenator = null}
1045
- __init39() {this.layoutText = ""}
1046
- __init40() {this.text = ""}
1047
- __init41() {this.lastRenderedSeqId = 0}
1048
- __init42() {this.rgbColorCache = /* @__PURE__ */ new Map()}
1049
- __init43() {this.fontStringCache = []}
1050
- __init44() {this.layoutResult = null}
1094
+ __init39() {this.hyphenator = null}
1095
+ __init40() {this.layoutText = ""}
1096
+ __init41() {this.text = ""}
1097
+ __init42() {this.lastRenderedSeqId = 0}
1098
+ __init43() {this.rgbColorCache = /* @__PURE__ */ new Map()}
1099
+ __init44() {this.fontStringCache = []}
1100
+ __init45() {this.layoutResult = null}
1051
1101
  constructor(text, options) {
1052
- super();_class3.prototype.__init38.call(this);_class3.prototype.__init39.call(this);_class3.prototype.__init40.call(this);_class3.prototype.__init41.call(this);_class3.prototype.__init42.call(this);_class3.prototype.__init43.call(this);_class3.prototype.__init44.call(this);;
1102
+ super();_class3.prototype.__init39.call(this);_class3.prototype.__init40.call(this);_class3.prototype.__init41.call(this);_class3.prototype.__init42.call(this);_class3.prototype.__init43.call(this);_class3.prototype.__init44.call(this);_class3.prototype.__init45.call(this);;
1053
1103
  this.font = options.font;
1054
1104
  this.texture = options.texture;
1055
1105
  this.fallbackFont = _nullishCoalesce(options.fallbackFont, () => ( "sans-serif"));
@@ -1269,19 +1319,19 @@ function readSvgAttribute(source, name) {
1269
1319
  return null;
1270
1320
  }
1271
1321
  var SVGEntity = (_class4 = class extends Entity {
1272
- __init45() {this.svgSource = ""}
1273
- __init46() {this.imageBitmap = null}
1274
- __init47() {this.imageElement = null}
1275
- __init48() {this.blobURL = null}
1276
- __init49() {this.currentImg = null}
1277
- __init50() {this.lodTimeout = null}
1278
- __init51() {this.cachedDoc = null}
1279
- __init52() {this.baseWidth = 100}
1280
- __init53() {this.baseHeight = 100}
1281
- __init54() {this.lastRasterizedScale = 1}
1282
- __init55() {this.targetScale = 1}
1322
+ __init46() {this.svgSource = ""}
1323
+ __init47() {this.imageBitmap = null}
1324
+ __init48() {this.imageElement = null}
1325
+ __init49() {this.blobURL = null}
1326
+ __init50() {this.currentImg = null}
1327
+ __init51() {this.lodTimeout = null}
1328
+ __init52() {this.cachedDoc = null}
1329
+ __init53() {this.baseWidth = 100}
1330
+ __init54() {this.baseHeight = 100}
1331
+ __init55() {this.lastRasterizedScale = 1}
1332
+ __init56() {this.targetScale = 1}
1283
1333
  constructor(svgSource, id) {
1284
- super(id);_class4.prototype.__init45.call(this);_class4.prototype.__init46.call(this);_class4.prototype.__init47.call(this);_class4.prototype.__init48.call(this);_class4.prototype.__init49.call(this);_class4.prototype.__init50.call(this);_class4.prototype.__init51.call(this);_class4.prototype.__init52.call(this);_class4.prototype.__init53.call(this);_class4.prototype.__init54.call(this);_class4.prototype.__init55.call(this);;
1334
+ super(id);_class4.prototype.__init46.call(this);_class4.prototype.__init47.call(this);_class4.prototype.__init48.call(this);_class4.prototype.__init49.call(this);_class4.prototype.__init50.call(this);_class4.prototype.__init51.call(this);_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);;
1285
1335
  this.setSVGSource(svgSource);
1286
1336
  }
1287
1337
  setSVGSource(svgSource) {
@@ -251,6 +251,24 @@ var Entity = class {
251
251
  * nodes, so on-top components stay clickable.
252
252
  */
253
253
  a11yFullViewport = false;
254
+ /**
255
+ * Hide this entity AND its whole subtree from the accessibility/automation
256
+ * projection, regardless of each node's own `interactive` flag.
257
+ *
258
+ * For a container that is logically closed while still mounted — an `Overlay`
259
+ * after `hide()`, a collapsed panel kept in the tree for its transition. Setting
260
+ * `interactive = false` on the container alone is not enough: the projection walk
261
+ * still descends, and any still-interactive child is re-created on the next
262
+ * frame. Measured before this existed: after `Popover.hide()` the popover's own
263
+ * element was gone while its button stayed projected with `tabIndex: 0` and a
264
+ * live box, so a keyboard user could Tab into a hidden popover.
265
+ *
266
+ * Deliberately NOT inferred from `opacity`: `Overlay.hide()` springs opacity
267
+ * toward 0, so mid-transition it reads nonzero (~0.26 when measured) and an
268
+ * `=== 0` test never fires; a threshold would instead silently un-project a
269
+ * faint-but-live control.
270
+ */
271
+ a11yHidden = false;
254
272
  /**
255
273
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
256
274
  * rendering. Combined with translating a content child, this is how
@@ -944,6 +962,38 @@ var Entity = class {
944
962
  *
945
963
  * @returns The {@link A11yAttributes} for this entity's shadow node.
946
964
  */
965
+ /**
966
+ * Describe this entity's own debug surface for DevTools.
967
+ *
968
+ * Returns `null` by default, meaning "nothing beyond the generic `Entity`
969
+ * fields the inspector already shows". Override in a component to expose the
970
+ * state that makes it inspectable — see {@link DevtoolsDescriptor}.
971
+ *
972
+ * Called only while a panel is inspecting this entity, never per frame, so it
973
+ * may compute values that would be too expensive to track continuously.
974
+ *
975
+ * @returns A descriptor, or `null` to opt out.
976
+ */
977
+ getDevtoolsDescriptor() {
978
+ return null;
979
+ }
980
+ /**
981
+ * Which of a child's properties this entity computes during layout.
982
+ *
983
+ * Returns an empty array by default, meaning "this entity does not position its
984
+ * children". A container that lays out children — `Stack`, `Table`, `Tabs` —
985
+ * overrides it so tooling can mark those values as parent-owned: editing `x` on
986
+ * a `Stack` child is reverted by the next layout, and knowing that in advance is
987
+ * the difference between a confusing tool and a correct one.
988
+ *
989
+ * @param child - The child being asked about. Containers whose control depends
990
+ * on the child (a `Table` cell versus its header) can answer per child.
991
+ * @returns Property names this entity overwrites on that child.
992
+ */
993
+ getLayoutControlledProperties(child) {
994
+ void child;
995
+ return [];
996
+ }
947
997
  getA11yAttributes() {
948
998
  return {};
949
999
  }
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4;const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5;const __vecto_cjs_url=require("url").pathToFileURL(__filename).href;
2
2
 
3
3
  // src/renderer/CanvasRenderer.ts
4
4
  var TWO_PI = Math.PI * 2;
@@ -212,6 +212,11 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
212
212
  this.ctx.drawImage(source, dx, dy, dw, dh);
213
213
  }
214
214
  /** @inheritdoc */
215
+ drawImageRect(source, sx, sy, sw, sh, dx, dy, dw, dh) {
216
+ this.flush();
217
+ this.ctx.drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh);
218
+ }
219
+ /** @inheritdoc */
215
220
  fillCircle(cx, cy, radius, color, alpha = 1) {
216
221
  if (this.batchActive && (color !== this.batchColor || alpha !== this.batchAlpha)) {
217
222
  this.flush();
@@ -1809,15 +1814,201 @@ var WebGPUParticleSystemManager = (_class3 = class {
1809
1814
  }
1810
1815
  }, _class3);
1811
1816
 
1817
+ // src/renderer/GlyphRasterAtlas.ts
1818
+ var HARD_MAX_SIZE = 8192;
1819
+ var PAD = 2;
1820
+ var GlyphRasterAtlas = (_class4 = class {
1821
+ __init35() {this.slots = /* @__PURE__ */ new Map()}
1822
+
1823
+
1824
+ __init36() {this.canvas = null}
1825
+ __init37() {this.ctx = null}
1826
+ /**
1827
+ * Shelf packing: glyphs land left-to-right on a row, then a new row starts.
1828
+ * A monospace grid produces near-uniform widths, so shelves waste very little
1829
+ * and cost one comparison per insert — a real 2D packer would buy nothing here.
1830
+ */
1831
+ __init38() {this.penX = PAD}
1832
+ __init39() {this.penY = PAD}
1833
+ __init40() {this.rowHeight = 0}
1834
+ __init41() {this._hits = 0}
1835
+ __init42() {this._misses = 0}
1836
+ __init43() {this._resets = 0}
1837
+ constructor(options = {}) {;_class4.prototype.__init35.call(this);_class4.prototype.__init36.call(this);_class4.prototype.__init37.call(this);_class4.prototype.__init38.call(this);_class4.prototype.__init39.call(this);_class4.prototype.__init40.call(this);_class4.prototype.__init41.call(this);_class4.prototype.__init42.call(this);_class4.prototype.__init43.call(this);
1838
+ this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
1839
+ this.maxSize = Math.min(HARD_MAX_SIZE, Math.max(256, _nullishCoalesce(options.maxSize, () => ( 2048))));
1840
+ }
1841
+ /** Live instrumentation snapshot. */
1842
+ get stats() {
1843
+ return {
1844
+ hits: this._hits,
1845
+ misses: this._misses,
1846
+ size: this.slots.size,
1847
+ resets: this._resets
1848
+ };
1849
+ }
1850
+ /**
1851
+ * The atlas canvas, to pass as the blit source.
1852
+ *
1853
+ * `null` until the first successful {@link get}, and in any non-DOM context.
1854
+ */
1855
+ get source() {
1856
+ return this.canvas;
1857
+ }
1858
+ ensureCanvas() {
1859
+ if (this.ctx) return true;
1860
+ if (typeof document === "undefined") return false;
1861
+ const canvas = document.createElement("canvas");
1862
+ canvas.width = this.maxSize;
1863
+ canvas.height = this.maxSize;
1864
+ const ctx = canvas.getContext("2d");
1865
+ if (!ctx) return false;
1866
+ this.canvas = canvas;
1867
+ this.ctx = ctx;
1868
+ return true;
1869
+ }
1870
+ /**
1871
+ * Look up a glyph, rasterizing it into the atlas on first request.
1872
+ *
1873
+ * @param font - Full CSS `font` shorthand, used for measuring and painting.
1874
+ * @param color - CSS color baked into the pixels.
1875
+ * @param glyph - A single grapheme cluster. Long strings are rejected
1876
+ * (`null`): they defeat the atlas's fixed-slot packing and belong in
1877
+ * `fillText` or {@link TextRasterCache}.
1878
+ * @returns The slot, or `null` when the caller must fall back to `fillText`
1879
+ * (headless, unrasterizable, or too large to pack).
1880
+ */
1881
+ get(font, color, glyph) {
1882
+ const key = font + "\0" + color + "\0" + glyph;
1883
+ const existing = this.slots.get(key);
1884
+ if (existing !== void 0) {
1885
+ if (existing) this._hits++;
1886
+ return existing;
1887
+ }
1888
+ this._misses++;
1889
+ if (glyph.length === 0 || glyph.length > 8) {
1890
+ this.slots.set(key, null);
1891
+ return null;
1892
+ }
1893
+ if (!this.ensureCanvas()) return null;
1894
+ const ctx = this.ctx;
1895
+ ctx.font = font;
1896
+ const metrics = ctx.measureText(glyph);
1897
+ const advance = metrics.width;
1898
+ const ascent = metrics.actualBoundingBoxAscent || metrics.fontBoundingBoxAscent || advance * 0.9;
1899
+ const descent = metrics.actualBoundingBoxDescent || metrics.fontBoundingBoxDescent || advance * 0.25;
1900
+ const left = metrics.actualBoundingBoxLeft || 0;
1901
+ const right = metrics.actualBoundingBoxRight || advance;
1902
+ const offsetX = Math.ceil(left) + PAD;
1903
+ const offsetY = Math.ceil(ascent) + PAD;
1904
+ const w = offsetX + Math.ceil(right) + PAD;
1905
+ const h = offsetY + Math.ceil(descent) + PAD;
1906
+ if (!(w > 0) || !(h > 0)) {
1907
+ this.slots.set(key, null);
1908
+ return null;
1909
+ }
1910
+ const dpr = this.dpr;
1911
+ const dw = Math.ceil(w * dpr);
1912
+ const dh = Math.ceil(h * dpr);
1913
+ if (dw + PAD * 2 > this.maxSize || dh + PAD * 2 > this.maxSize) {
1914
+ this.slots.set(key, null);
1915
+ return null;
1916
+ }
1917
+ if (this.penX + dw + PAD > this.maxSize) {
1918
+ this.penX = PAD;
1919
+ this.penY += this.rowHeight + PAD;
1920
+ this.rowHeight = 0;
1921
+ }
1922
+ if (this.penY + dh + PAD > this.maxSize) {
1923
+ this.reset();
1924
+ if (!this.ensureCanvas()) return null;
1925
+ this.ctx.font = font;
1926
+ }
1927
+ const sx = this.penX;
1928
+ const sy = this.penY;
1929
+ const c = this.ctx;
1930
+ c.save();
1931
+ c.translate(sx, sy);
1932
+ if (dpr !== 1) c.scale(dpr, dpr);
1933
+ c.font = font;
1934
+ c.textBaseline = "alphabetic";
1935
+ c.fillStyle = color;
1936
+ c.fillText(glyph, offsetX, offsetY);
1937
+ c.restore();
1938
+ this.penX += dw + PAD;
1939
+ if (dh > this.rowHeight) this.rowHeight = dh;
1940
+ const slot = {
1941
+ sx,
1942
+ sy,
1943
+ sw: dw,
1944
+ sh: dh,
1945
+ w,
1946
+ h,
1947
+ offsetX,
1948
+ offsetY,
1949
+ glyph,
1950
+ font,
1951
+ advance,
1952
+ left,
1953
+ right
1954
+ };
1955
+ this.slots.set(key, slot);
1956
+ return slot;
1957
+ }
1958
+ /**
1959
+ * Find the slot occupying a source position, or `null`.
1960
+ *
1961
+ * The inverse of {@link get}: it maps a blit back to the glyph it drew. Exists
1962
+ * for instrumentation — a test or devtool that traces `drawImage` calls sees
1963
+ * only a source rect, and needs this to recover which cluster was painted and
1964
+ * with what metrics. Linear over resident slots, so it is a diagnostic, not a
1965
+ * per-frame call.
1966
+ */
1967
+ slotAt(sx, sy) {
1968
+ for (const slot of this.slots.values()) {
1969
+ if (slot && slot.sx === sx && slot.sy === sy) return slot;
1970
+ }
1971
+ return null;
1972
+ }
1973
+ /**
1974
+ * Drop every glyph and reuse the canvas.
1975
+ *
1976
+ * Call after a font or theme change: slots are keyed by `(font, color, glyph)`
1977
+ * so stale entries are never *returned* wrongly, but they do occupy space.
1978
+ */
1979
+ reset() {
1980
+ this.slots.clear();
1981
+ this.penX = PAD;
1982
+ this.penY = PAD;
1983
+ this.rowHeight = 0;
1984
+ this._resets++;
1985
+ if (this.ctx && this.canvas) {
1986
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1987
+ }
1988
+ }
1989
+ /** Release the backing canvas and all slots. */
1990
+ destroy() {
1991
+ this.slots.clear();
1992
+ this.canvas = null;
1993
+ this.ctx = null;
1994
+ this.penX = PAD;
1995
+ this.penY = PAD;
1996
+ this.rowHeight = 0;
1997
+ this._hits = 0;
1998
+ this._misses = 0;
1999
+ this._resets = 0;
2000
+ }
2001
+ }, _class4);
2002
+
1812
2003
  // src/renderer/TextRasterCache.ts
1813
- var TextRasterCache = (_class4 = class {
1814
- __init35() {this.cache = /* @__PURE__ */ new Map()}
2004
+ var TextRasterCache = (_class5 = class {
2005
+ __init44() {this.cache = /* @__PURE__ */ new Map()}
1815
2006
 
1816
2007
 
1817
- __init36() {this.scratchCtx = null}
1818
- __init37() {this._hits = 0}
1819
- __init38() {this._misses = 0}
1820
- constructor(options = {}) {;_class4.prototype.__init35.call(this);_class4.prototype.__init36.call(this);_class4.prototype.__init37.call(this);_class4.prototype.__init38.call(this);
2008
+ __init45() {this.scratchCtx = null}
2009
+ __init46() {this._hits = 0}
2010
+ __init47() {this._misses = 0}
2011
+ constructor(options = {}) {;_class5.prototype.__init44.call(this);_class5.prototype.__init45.call(this);_class5.prototype.__init46.call(this);_class5.prototype.__init47.call(this);
1821
2012
  this.maxEntries = Math.max(1, _nullishCoalesce(options.maxEntries, () => ( 4096)));
1822
2013
  this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
1823
2014
  }
@@ -1892,7 +2083,8 @@ var TextRasterCache = (_class4 = class {
1892
2083
  this._hits = 0;
1893
2084
  this._misses = 0;
1894
2085
  }
1895
- }, _class4);
2086
+ }, _class5);
2087
+
1896
2088
 
1897
2089
 
1898
2090
 
@@ -1903,4 +2095,4 @@ var TextRasterCache = (_class4 = class {
1903
2095
 
1904
2096
 
1905
2097
 
1906
- exports.CanvasRenderer = CanvasRenderer; exports.sanitizeUrl = sanitizeUrl; exports.isSafeUrl = isSafeUrl; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager; exports.TextRasterCache = TextRasterCache;
2098
+ exports.CanvasRenderer = CanvasRenderer; exports.sanitizeUrl = sanitizeUrl; exports.isSafeUrl = isSafeUrl; exports.SVGRenderer = SVGRenderer; exports.parseColorToRGBA = parseColorToRGBA; exports.createWebGLPointRenderer = createWebGLPointRenderer; exports.WebGPUParticleSystemManager = WebGPUParticleSystemManager; exports.GlyphRasterAtlas = GlyphRasterAtlas; exports.TextRasterCache = TextRasterCache;
@@ -210,6 +210,11 @@ var CanvasRenderer = class _CanvasRenderer {
210
210
  this.ctx.drawImage(source, dx, dy, dw, dh);
211
211
  }
212
212
  /** @inheritdoc */
213
+ drawImageRect(source, sx, sy, sw, sh, dx, dy, dw, dh) {
214
+ this.flush();
215
+ this.ctx.drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh);
216
+ }
217
+ /** @inheritdoc */
213
218
  fillCircle(cx, cy, radius, color, alpha = 1) {
214
219
  if (this.batchActive && (color !== this.batchColor || alpha !== this.batchAlpha)) {
215
220
  this.flush();
@@ -1807,6 +1812,192 @@ var WebGPUParticleSystemManager = class {
1807
1812
  }
1808
1813
  };
1809
1814
 
1815
+ // src/renderer/GlyphRasterAtlas.ts
1816
+ var HARD_MAX_SIZE = 8192;
1817
+ var PAD = 2;
1818
+ var GlyphRasterAtlas = class {
1819
+ slots = /* @__PURE__ */ new Map();
1820
+ dpr;
1821
+ maxSize;
1822
+ canvas = null;
1823
+ ctx = null;
1824
+ /**
1825
+ * Shelf packing: glyphs land left-to-right on a row, then a new row starts.
1826
+ * A monospace grid produces near-uniform widths, so shelves waste very little
1827
+ * and cost one comparison per insert — a real 2D packer would buy nothing here.
1828
+ */
1829
+ penX = PAD;
1830
+ penY = PAD;
1831
+ rowHeight = 0;
1832
+ _hits = 0;
1833
+ _misses = 0;
1834
+ _resets = 0;
1835
+ constructor(options = {}) {
1836
+ this.dpr = Math.max(1, options.dpr ?? 1);
1837
+ this.maxSize = Math.min(HARD_MAX_SIZE, Math.max(256, options.maxSize ?? 2048));
1838
+ }
1839
+ /** Live instrumentation snapshot. */
1840
+ get stats() {
1841
+ return {
1842
+ hits: this._hits,
1843
+ misses: this._misses,
1844
+ size: this.slots.size,
1845
+ resets: this._resets
1846
+ };
1847
+ }
1848
+ /**
1849
+ * The atlas canvas, to pass as the blit source.
1850
+ *
1851
+ * `null` until the first successful {@link get}, and in any non-DOM context.
1852
+ */
1853
+ get source() {
1854
+ return this.canvas;
1855
+ }
1856
+ ensureCanvas() {
1857
+ if (this.ctx) return true;
1858
+ if (typeof document === "undefined") return false;
1859
+ const canvas = document.createElement("canvas");
1860
+ canvas.width = this.maxSize;
1861
+ canvas.height = this.maxSize;
1862
+ const ctx = canvas.getContext("2d");
1863
+ if (!ctx) return false;
1864
+ this.canvas = canvas;
1865
+ this.ctx = ctx;
1866
+ return true;
1867
+ }
1868
+ /**
1869
+ * Look up a glyph, rasterizing it into the atlas on first request.
1870
+ *
1871
+ * @param font - Full CSS `font` shorthand, used for measuring and painting.
1872
+ * @param color - CSS color baked into the pixels.
1873
+ * @param glyph - A single grapheme cluster. Long strings are rejected
1874
+ * (`null`): they defeat the atlas's fixed-slot packing and belong in
1875
+ * `fillText` or {@link TextRasterCache}.
1876
+ * @returns The slot, or `null` when the caller must fall back to `fillText`
1877
+ * (headless, unrasterizable, or too large to pack).
1878
+ */
1879
+ get(font, color, glyph) {
1880
+ const key = font + "\0" + color + "\0" + glyph;
1881
+ const existing = this.slots.get(key);
1882
+ if (existing !== void 0) {
1883
+ if (existing) this._hits++;
1884
+ return existing;
1885
+ }
1886
+ this._misses++;
1887
+ if (glyph.length === 0 || glyph.length > 8) {
1888
+ this.slots.set(key, null);
1889
+ return null;
1890
+ }
1891
+ if (!this.ensureCanvas()) return null;
1892
+ const ctx = this.ctx;
1893
+ ctx.font = font;
1894
+ const metrics = ctx.measureText(glyph);
1895
+ const advance = metrics.width;
1896
+ const ascent = metrics.actualBoundingBoxAscent || metrics.fontBoundingBoxAscent || advance * 0.9;
1897
+ const descent = metrics.actualBoundingBoxDescent || metrics.fontBoundingBoxDescent || advance * 0.25;
1898
+ const left = metrics.actualBoundingBoxLeft || 0;
1899
+ const right = metrics.actualBoundingBoxRight || advance;
1900
+ const offsetX = Math.ceil(left) + PAD;
1901
+ const offsetY = Math.ceil(ascent) + PAD;
1902
+ const w = offsetX + Math.ceil(right) + PAD;
1903
+ const h = offsetY + Math.ceil(descent) + PAD;
1904
+ if (!(w > 0) || !(h > 0)) {
1905
+ this.slots.set(key, null);
1906
+ return null;
1907
+ }
1908
+ const dpr = this.dpr;
1909
+ const dw = Math.ceil(w * dpr);
1910
+ const dh = Math.ceil(h * dpr);
1911
+ if (dw + PAD * 2 > this.maxSize || dh + PAD * 2 > this.maxSize) {
1912
+ this.slots.set(key, null);
1913
+ return null;
1914
+ }
1915
+ if (this.penX + dw + PAD > this.maxSize) {
1916
+ this.penX = PAD;
1917
+ this.penY += this.rowHeight + PAD;
1918
+ this.rowHeight = 0;
1919
+ }
1920
+ if (this.penY + dh + PAD > this.maxSize) {
1921
+ this.reset();
1922
+ if (!this.ensureCanvas()) return null;
1923
+ this.ctx.font = font;
1924
+ }
1925
+ const sx = this.penX;
1926
+ const sy = this.penY;
1927
+ const c = this.ctx;
1928
+ c.save();
1929
+ c.translate(sx, sy);
1930
+ if (dpr !== 1) c.scale(dpr, dpr);
1931
+ c.font = font;
1932
+ c.textBaseline = "alphabetic";
1933
+ c.fillStyle = color;
1934
+ c.fillText(glyph, offsetX, offsetY);
1935
+ c.restore();
1936
+ this.penX += dw + PAD;
1937
+ if (dh > this.rowHeight) this.rowHeight = dh;
1938
+ const slot = {
1939
+ sx,
1940
+ sy,
1941
+ sw: dw,
1942
+ sh: dh,
1943
+ w,
1944
+ h,
1945
+ offsetX,
1946
+ offsetY,
1947
+ glyph,
1948
+ font,
1949
+ advance,
1950
+ left,
1951
+ right
1952
+ };
1953
+ this.slots.set(key, slot);
1954
+ return slot;
1955
+ }
1956
+ /**
1957
+ * Find the slot occupying a source position, or `null`.
1958
+ *
1959
+ * The inverse of {@link get}: it maps a blit back to the glyph it drew. Exists
1960
+ * for instrumentation — a test or devtool that traces `drawImage` calls sees
1961
+ * only a source rect, and needs this to recover which cluster was painted and
1962
+ * with what metrics. Linear over resident slots, so it is a diagnostic, not a
1963
+ * per-frame call.
1964
+ */
1965
+ slotAt(sx, sy) {
1966
+ for (const slot of this.slots.values()) {
1967
+ if (slot && slot.sx === sx && slot.sy === sy) return slot;
1968
+ }
1969
+ return null;
1970
+ }
1971
+ /**
1972
+ * Drop every glyph and reuse the canvas.
1973
+ *
1974
+ * Call after a font or theme change: slots are keyed by `(font, color, glyph)`
1975
+ * so stale entries are never *returned* wrongly, but they do occupy space.
1976
+ */
1977
+ reset() {
1978
+ this.slots.clear();
1979
+ this.penX = PAD;
1980
+ this.penY = PAD;
1981
+ this.rowHeight = 0;
1982
+ this._resets++;
1983
+ if (this.ctx && this.canvas) {
1984
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1985
+ }
1986
+ }
1987
+ /** Release the backing canvas and all slots. */
1988
+ destroy() {
1989
+ this.slots.clear();
1990
+ this.canvas = null;
1991
+ this.ctx = null;
1992
+ this.penX = PAD;
1993
+ this.penY = PAD;
1994
+ this.rowHeight = 0;
1995
+ this._hits = 0;
1996
+ this._misses = 0;
1997
+ this._resets = 0;
1998
+ }
1999
+ };
2000
+
1810
2001
  // src/renderer/TextRasterCache.ts
1811
2002
  var TextRasterCache = class {
1812
2003
  cache = /* @__PURE__ */ new Map();
@@ -1900,5 +2091,6 @@ export {
1900
2091
  parseColorToRGBA,
1901
2092
  createWebGLPointRenderer,
1902
2093
  WebGPUParticleSystemManager,
2094
+ GlyphRasterAtlas,
1903
2095
  TextRasterCache
1904
2096
  };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './renderer/WebGLPointRenderer';
5
5
  export * from './renderer/WebGPUParticleSystemManager';
6
6
  export * from './renderer/colorParse';
7
7
  export * from './renderer/url';
8
+ export * from './renderer/GlyphRasterAtlas';
8
9
  export * from './renderer/TextRasterCache';
9
10
  export * from './tree/Entity';
10
11
  export * from './tree/Scene';