@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.
@@ -964,6 +964,38 @@ var Entity = (_class2 = class {
964
964
  *
965
965
  * @returns The {@link A11yAttributes} for this entity's shadow node.
966
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
+ }
967
999
  getA11yAttributes() {
968
1000
  return {};
969
1001
  }
@@ -962,6 +962,38 @@ var Entity = class {
962
962
  *
963
963
  * @returns The {@link A11yAttributes} for this entity's shadow node.
964
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
+ }
965
997
  getA11yAttributes() {
966
998
  return {};
967
999
  }
@@ -1,6 +1,21 @@
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
+ function emptyDrawCounters() {
5
+ return {
6
+ fills: 0,
7
+ strokes: 0,
8
+ texts: 0,
9
+ images: 0,
10
+ circles: 0,
11
+ flushes: 0,
12
+ saves: 0,
13
+ restores: 0,
14
+ clips: 0,
15
+ stateSwitches: 0,
16
+ overdrawRatio: 0
17
+ };
18
+ }
4
19
  var TWO_PI = Math.PI * 2;
5
20
  function getDevicePixelRatio() {
6
21
  return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
@@ -41,6 +56,17 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
41
56
  __init6() {this.batchCount = 0}
42
57
  __init7() {this._cachedFont = ""}
43
58
  __init8() {this._cachedFill = ""}
59
+ /** Backend discriminator; see {@link IRenderer.kind}. */
60
+ __init9() {this.kind = "canvas2d"}
61
+ /**
62
+ * Draw counters, allocated only once counting is enabled.
63
+ *
64
+ * Null when off, so the guard on every op is a single null test and an inactive
65
+ * renderer carries no counter object at all.
66
+ */
67
+ __init10() {this.counters = null}
68
+ /** Accumulated primitive area, kept separately so the ratio is derived on read. */
69
+ __init11() {this.drawnArea = 0}
44
70
  /**
45
71
  * @param canvas - The target canvas. Its backing store is resized to the
46
72
  * logical size × devicePixelRatio.
@@ -50,7 +76,7 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
50
76
  * set) so the canvas's own dimensions aren't clobbered by the window's.
51
77
  * @param maxDPR - See {@link maxDPR}.
52
78
  */
53
- constructor(canvas, size, maxDPR) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);
79
+ constructor(canvas, size, maxDPR) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);
54
80
  this.maxDPR = maxDPR;
55
81
  const dpr = this.effectiveDPR();
56
82
  this.width = _nullishCoalesce(_optionalChain([size, 'optionalAccess', _ => _.width]), () => ( (typeof window !== "undefined" ? window.innerWidth : canvas.width || 0)));
@@ -137,19 +163,47 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
137
163
  return this.contextLost;
138
164
  }
139
165
  /** @inheritdoc */
166
+ /** @inheritdoc */
167
+ setDrawCounters(enabled) {
168
+ if (!enabled) {
169
+ this.counters = null;
170
+ this.drawnArea = 0;
171
+ return;
172
+ }
173
+ if (!this.counters) this.counters = emptyDrawCounters();
174
+ }
175
+ /** @inheritdoc */
176
+ getDrawCounters() {
177
+ if (!this.counters) return null;
178
+ const area = this.width * this.height;
179
+ return {
180
+ ...this.counters,
181
+ // Derived on read rather than maintained: the ratio is only meaningful
182
+ // against the current surface size, which can change under resize.
183
+ overdrawRatio: area > 0 ? Math.round(this.drawnArea / area * 100) / 100 : 0
184
+ };
185
+ }
186
+ /** @inheritdoc */
187
+ clearDrawCounters() {
188
+ if (this.counters) this.counters = emptyDrawCounters();
189
+ this.drawnArea = 0;
190
+ }
140
191
  clear() {
141
192
  if (this.contextLost) return;
142
193
  this.flush();
194
+ if (this.counters) this.drawnArea = 0;
143
195
  this.ctx.clearRect(0, 0, this.width, this.height);
144
196
  }
145
197
  /** @inheritdoc */
146
198
  save() {
147
199
  this.flush();
200
+ if (this.counters) this.counters.saves++;
148
201
  this.ctx.save();
149
202
  }
150
203
  /** @inheritdoc */
151
204
  restore() {
152
205
  this.flush();
206
+ if (this.counters) this.counters.restores++;
153
207
  this.ctx.restore();
154
208
  this._cachedFont = "";
155
209
  this._cachedFill = "";
@@ -173,6 +227,7 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
173
227
  /** @inheritdoc */
174
228
  clip(x, y, width, height) {
175
229
  this.flush();
230
+ if (this.counters) this.counters.clips++;
176
231
  this.ctx.beginPath();
177
232
  this.ctx.rect(x, y, width, height);
178
233
  this.ctx.clip();
@@ -209,9 +264,22 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
209
264
  /** @inheritdoc */
210
265
  drawImage(source, dx, dy, dw, dh) {
211
266
  this.flush();
267
+ if (this.counters) {
268
+ this.counters.images++;
269
+ this.drawnArea += Math.abs(dw * dh);
270
+ }
212
271
  this.ctx.drawImage(source, dx, dy, dw, dh);
213
272
  }
214
273
  /** @inheritdoc */
274
+ drawImageRect(source, sx, sy, sw, sh, dx, dy, dw, dh) {
275
+ this.flush();
276
+ if (this.counters) {
277
+ this.counters.images++;
278
+ this.drawnArea += Math.abs(dw * dh);
279
+ }
280
+ this.ctx.drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh);
281
+ }
282
+ /** @inheritdoc */
215
283
  fillCircle(cx, cy, radius, color, alpha = 1) {
216
284
  if (this.batchActive && (color !== this.batchColor || alpha !== this.batchAlpha)) {
217
285
  this.flush();
@@ -222,6 +290,10 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
222
290
  this.batchColor = color;
223
291
  this.batchAlpha = alpha;
224
292
  }
293
+ if (this.counters) {
294
+ this.counters.circles++;
295
+ this.drawnArea += Math.PI * radius * radius;
296
+ }
225
297
  this.ctx.moveTo(cx + radius, cy);
226
298
  this.ctx.arc(cx, cy, radius, 0, TWO_PI);
227
299
  this.batchCount++;
@@ -230,6 +302,7 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
230
302
  /** @inheritdoc */
231
303
  flush() {
232
304
  if (!this.batchActive) return;
305
+ if (this.counters) this.counters.flushes++;
233
306
  this.ctx.globalAlpha = this.batchAlpha;
234
307
  this.ctx.fillStyle = this.batchColor;
235
308
  this.ctx.fill();
@@ -240,7 +313,9 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
240
313
  /** @inheritdoc */
241
314
  fill(color) {
242
315
  this.flush();
316
+ if (this.counters) this.counters.fills++;
243
317
  if (this._cachedFill !== color) {
318
+ if (this.counters) this.counters.stateSwitches++;
244
319
  this.ctx.fillStyle = color;
245
320
  this._cachedFill = color;
246
321
  }
@@ -249,6 +324,7 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
249
324
  /** @inheritdoc */
250
325
  stroke(color, lineWidth = 1) {
251
326
  this.flush();
327
+ if (this.counters) this.counters.strokes++;
252
328
  this.ctx.strokeStyle = color;
253
329
  this.ctx.lineWidth = lineWidth;
254
330
  this.ctx.lineCap = "round";
@@ -258,11 +334,14 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
258
334
  /** @inheritdoc */
259
335
  fillText(text, x, y, font, color) {
260
336
  this.flush();
337
+ if (this.counters) this.counters.texts++;
261
338
  if (this._cachedFont !== font) {
339
+ if (this.counters) this.counters.stateSwitches++;
262
340
  this.ctx.font = font;
263
341
  this._cachedFont = font;
264
342
  }
265
343
  if (this._cachedFill !== color) {
344
+ if (this.counters) this.counters.stateSwitches++;
266
345
  this.ctx.fillStyle = color;
267
346
  this._cachedFill = color;
268
347
  }
@@ -373,41 +452,43 @@ function fontFamilyFromShorthand(font, sizeToken) {
373
452
  return family || "sans-serif";
374
453
  }
375
454
  var SVGRenderer = (_class2 = class {
455
+ /** Backend discriminator; see {@link IRenderer.kind}. */
456
+ __init12() {this.kind = "svg"}
376
457
 
377
458
 
378
- __init9() {this.buffer = []}
379
- __init10() {this.defsBuffer = []}
380
- __init11() {this.currentPath = []}
459
+ __init13() {this.buffer = []}
460
+ __init14() {this.defsBuffer = []}
461
+ __init15() {this.currentPath = []}
381
462
  // Inline matrix states (3x3 representation)
382
463
  // [a, b, c, d, e, f] corresponding to:
383
464
  // | a c e |
384
465
  // | b d f |
385
466
  // | 0 0 1 |
386
- __init12() {this.mStack = []}
387
- __init13() {this.ma = 1}
388
- __init14() {this.mb = 0}
389
- __init15() {this.mc = 0}
390
- __init16() {this.md = 1}
391
- __init17() {this.me = 0}
392
- __init18() {this.mf = 0}
467
+ __init16() {this.mStack = []}
468
+ __init17() {this.ma = 1}
469
+ __init18() {this.mb = 0}
470
+ __init19() {this.mc = 0}
471
+ __init20() {this.md = 1}
472
+ __init21() {this.me = 0}
473
+ __init22() {this.mf = 0}
393
474
  // Alpha stack
394
- __init19() {this.alphaStack = []}
395
- __init20() {this.globalAlpha = 1}
475
+ __init23() {this.alphaStack = []}
476
+ __init24() {this.globalAlpha = 1}
396
477
  // Clipping stack
397
- __init21() {this.clipDepthStack = []}
398
- __init22() {this.clipDepth = 0}
399
- __init23() {this.clipCounter = 0}
478
+ __init25() {this.clipDepthStack = []}
479
+ __init26() {this.clipDepth = 0}
480
+ __init27() {this.clipCounter = 0}
400
481
  // uniquely identifies clipPaths
401
482
  // Batch circles states (local coordinates)
402
- __init24() {this.batchCircles = []}
403
- __init25() {this.batchMatrix = [1, 0, 0, 1, 0, 0]}
404
- __init26() {this.batchColor = ""}
405
- __init27() {this.batchAlpha = 1}
406
- __init28() {this.batchActive = false}
483
+ __init28() {this.batchCircles = []}
484
+ __init29() {this.batchMatrix = [1, 0, 0, 1, 0, 0]}
485
+ __init30() {this.batchColor = ""}
486
+ __init31() {this.batchAlpha = 1}
487
+ __init32() {this.batchActive = false}
407
488
  // Cache for generated gradient defs
408
- __init29() {this.gradientCounter = 0}
409
- __init30() {this.gradientCache = /* @__PURE__ */ new Map()}
410
- constructor(width, height) {;_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);
489
+ __init33() {this.gradientCounter = 0}
490
+ __init34() {this.gradientCache = /* @__PURE__ */ new Map()}
491
+ constructor(width, height) {;_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);
411
492
  this.width = width;
412
493
  this.height = height;
413
494
  }
@@ -1225,6 +1306,12 @@ function createWebGLPointRenderer(canvas) {
1225
1306
  let spriteCount = 0;
1226
1307
  let glyphData = new Float32Array(FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD * 1024);
1227
1308
  let glyphCount = 0;
1309
+ let frameDrawCalls = 0;
1310
+ let lastFrameDrawCalls = 0;
1311
+ let totalDrawCalls = 0;
1312
+ let atlasSwitches = 0;
1313
+ let circleQuadFallbacks = 0;
1314
+ let circlePoints = 0;
1228
1315
  let circleQuadData = new Float32Array(FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD * 64);
1229
1316
  let circleQuadCount = 0;
1230
1317
  let logicalW = 0;
@@ -1247,6 +1334,7 @@ function createWebGLPointRenderer(canvas) {
1247
1334
  gl.uniform1f(gURange, distanceRange);
1248
1335
  ensureQuadIndices(glyphCount);
1249
1336
  gl.drawElements(gl.TRIANGLES, glyphCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
1337
+ frameDrawCalls++;
1250
1338
  gl.bindVertexArray(null);
1251
1339
  glyphCount = 0;
1252
1340
  };
@@ -1263,7 +1351,23 @@ function createWebGLPointRenderer(canvas) {
1263
1351
  canvas.style.height = `${height}px`;
1264
1352
  gl.viewport(0, 0, canvas.width, canvas.height);
1265
1353
  },
1354
+ stats() {
1355
+ return {
1356
+ drawCalls: lastFrameDrawCalls,
1357
+ totalDrawCalls,
1358
+ atlasSwitches,
1359
+ // Fixed at creation: five programs compiled once, never per frame, so
1360
+ // these are capability facts rather than counters.
1361
+ programs: 5,
1362
+ textures: (texture ? 1 : 0) + (msdfTexture ? 1 : 0),
1363
+ circleQuadFallbacks,
1364
+ circlePoints
1365
+ };
1366
+ },
1266
1367
  begin() {
1368
+ lastFrameDrawCalls = frameDrawCalls;
1369
+ totalDrawCalls += frameDrawCalls;
1370
+ frameDrawCalls = 0;
1267
1371
  pointCount = 0;
1268
1372
  rectCount = 0;
1269
1373
  spriteCount = 0;
@@ -1316,6 +1420,7 @@ function createWebGLPointRenderer(canvas) {
1316
1420
  distanceRange = range;
1317
1421
  return;
1318
1422
  }
1423
+ atlasSwitches++;
1319
1424
  drawGlyphs();
1320
1425
  distanceRange = range;
1321
1426
  if (!msdfTexture) {
@@ -1358,6 +1463,7 @@ function createWebGLPointRenderer(canvas) {
1358
1463
  addCircle(x, y, radius, color, alpha = 1) {
1359
1464
  const needsQuad = logicalW > 0 && (x < radius || y < radius || x > logicalW - radius || y > logicalH - radius) || radius * 2 * dpr > maxPointSize;
1360
1465
  if (needsQuad) {
1466
+ circleQuadFallbacks++;
1361
1467
  const stride = FLOATS_PER_SPRITE_VERT * VERTS_PER_QUAD;
1362
1468
  circleQuadData = grow(circleQuadData, (circleQuadCount + 1) * stride);
1363
1469
  const rgba = parseColorToRGBA(color);
@@ -1382,6 +1488,7 @@ function createWebGLPointRenderer(canvas) {
1382
1488
  circleQuadCount++;
1383
1489
  return;
1384
1490
  }
1491
+ circlePoints++;
1385
1492
  pointData = grow(pointData, (pointCount + 1) * FLOATS_PER_POINT);
1386
1493
  const [r, g, b, a] = parseColorToRGBA(color);
1387
1494
  const o = pointCount * FLOATS_PER_POINT;
@@ -1423,6 +1530,7 @@ function createWebGLPointRenderer(canvas) {
1423
1530
  gl.uniform2f(rURes, logicalW, logicalH);
1424
1531
  ensureQuadIndices(rectCount);
1425
1532
  gl.drawElements(gl.TRIANGLES, rectCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
1533
+ frameDrawCalls++;
1426
1534
  }
1427
1535
  if (pointCount > 0) {
1428
1536
  gl.useProgram(pointProgram);
@@ -1436,6 +1544,7 @@ function createWebGLPointRenderer(canvas) {
1436
1544
  gl.uniform2f(pURes, logicalW, logicalH);
1437
1545
  gl.uniform1f(pUDpr, dpr);
1438
1546
  gl.drawArrays(gl.POINTS, 0, pointCount);
1547
+ frameDrawCalls++;
1439
1548
  }
1440
1549
  if (circleQuadCount > 0) {
1441
1550
  const floats = circleQuadCount * VERTS_PER_QUAD * FLOATS_PER_SPRITE_VERT;
@@ -1446,6 +1555,7 @@ function createWebGLPointRenderer(canvas) {
1446
1555
  gl.uniform2f(cURes, logicalW, logicalH);
1447
1556
  ensureQuadIndices(circleQuadCount);
1448
1557
  gl.drawElements(gl.TRIANGLES, circleQuadCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
1558
+ frameDrawCalls++;
1449
1559
  }
1450
1560
  if (spriteCount > 0 && texture) {
1451
1561
  const floats = spriteCount * VERTS_PER_QUAD * FLOATS_PER_SPRITE_VERT;
@@ -1459,6 +1569,7 @@ function createWebGLPointRenderer(canvas) {
1459
1569
  gl.uniform2f(sURes, logicalW, logicalH);
1460
1570
  ensureQuadIndices(spriteCount);
1461
1571
  gl.drawElements(gl.TRIANGLES, spriteCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
1572
+ frameDrawCalls++;
1462
1573
  }
1463
1574
  gl.bindVertexArray(null);
1464
1575
  drawGlyphs();
@@ -1664,11 +1775,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
1664
1775
  `;
1665
1776
  var WebGPUParticleSystemManager = (_class3 = class {
1666
1777
 
1667
- __init31() {this.computePipeline = null}
1668
- __init32() {this.renderPipeline = null}
1669
- __init33() {this.computeBindGroupLayout = null}
1670
- __init34() {this.renderBindGroupLayout = null}
1671
- constructor(device) {;_class3.prototype.__init31.call(this);_class3.prototype.__init32.call(this);_class3.prototype.__init33.call(this);_class3.prototype.__init34.call(this);
1778
+ __init35() {this.computePipeline = null}
1779
+ __init36() {this.renderPipeline = null}
1780
+ __init37() {this.computeBindGroupLayout = null}
1781
+ __init38() {this.renderBindGroupLayout = null}
1782
+ constructor(device) {;_class3.prototype.__init35.call(this);_class3.prototype.__init36.call(this);_class3.prototype.__init37.call(this);_class3.prototype.__init38.call(this);
1672
1783
  this.device = device;
1673
1784
  }
1674
1785
  initPipelines(format) {
@@ -1809,15 +1920,201 @@ var WebGPUParticleSystemManager = (_class3 = class {
1809
1920
  }
1810
1921
  }, _class3);
1811
1922
 
1923
+ // src/renderer/GlyphRasterAtlas.ts
1924
+ var HARD_MAX_SIZE = 8192;
1925
+ var PAD = 2;
1926
+ var GlyphRasterAtlas = (_class4 = class {
1927
+ __init39() {this.slots = /* @__PURE__ */ new Map()}
1928
+
1929
+
1930
+ __init40() {this.canvas = null}
1931
+ __init41() {this.ctx = null}
1932
+ /**
1933
+ * Shelf packing: glyphs land left-to-right on a row, then a new row starts.
1934
+ * A monospace grid produces near-uniform widths, so shelves waste very little
1935
+ * and cost one comparison per insert — a real 2D packer would buy nothing here.
1936
+ */
1937
+ __init42() {this.penX = PAD}
1938
+ __init43() {this.penY = PAD}
1939
+ __init44() {this.rowHeight = 0}
1940
+ __init45() {this._hits = 0}
1941
+ __init46() {this._misses = 0}
1942
+ __init47() {this._resets = 0}
1943
+ constructor(options = {}) {;_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);_class4.prototype.__init44.call(this);_class4.prototype.__init45.call(this);_class4.prototype.__init46.call(this);_class4.prototype.__init47.call(this);
1944
+ this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
1945
+ this.maxSize = Math.min(HARD_MAX_SIZE, Math.max(256, _nullishCoalesce(options.maxSize, () => ( 2048))));
1946
+ }
1947
+ /** Live instrumentation snapshot. */
1948
+ get stats() {
1949
+ return {
1950
+ hits: this._hits,
1951
+ misses: this._misses,
1952
+ size: this.slots.size,
1953
+ resets: this._resets
1954
+ };
1955
+ }
1956
+ /**
1957
+ * The atlas canvas, to pass as the blit source.
1958
+ *
1959
+ * `null` until the first successful {@link get}, and in any non-DOM context.
1960
+ */
1961
+ get source() {
1962
+ return this.canvas;
1963
+ }
1964
+ ensureCanvas() {
1965
+ if (this.ctx) return true;
1966
+ if (typeof document === "undefined") return false;
1967
+ const canvas = document.createElement("canvas");
1968
+ canvas.width = this.maxSize;
1969
+ canvas.height = this.maxSize;
1970
+ const ctx = canvas.getContext("2d");
1971
+ if (!ctx) return false;
1972
+ this.canvas = canvas;
1973
+ this.ctx = ctx;
1974
+ return true;
1975
+ }
1976
+ /**
1977
+ * Look up a glyph, rasterizing it into the atlas on first request.
1978
+ *
1979
+ * @param font - Full CSS `font` shorthand, used for measuring and painting.
1980
+ * @param color - CSS color baked into the pixels.
1981
+ * @param glyph - A single grapheme cluster. Long strings are rejected
1982
+ * (`null`): they defeat the atlas's fixed-slot packing and belong in
1983
+ * `fillText` or {@link TextRasterCache}.
1984
+ * @returns The slot, or `null` when the caller must fall back to `fillText`
1985
+ * (headless, unrasterizable, or too large to pack).
1986
+ */
1987
+ get(font, color, glyph) {
1988
+ const key = font + "\0" + color + "\0" + glyph;
1989
+ const existing = this.slots.get(key);
1990
+ if (existing !== void 0) {
1991
+ if (existing) this._hits++;
1992
+ return existing;
1993
+ }
1994
+ this._misses++;
1995
+ if (glyph.length === 0 || glyph.length > 8) {
1996
+ this.slots.set(key, null);
1997
+ return null;
1998
+ }
1999
+ if (!this.ensureCanvas()) return null;
2000
+ const ctx = this.ctx;
2001
+ ctx.font = font;
2002
+ const metrics = ctx.measureText(glyph);
2003
+ const advance = metrics.width;
2004
+ const ascent = metrics.actualBoundingBoxAscent || metrics.fontBoundingBoxAscent || advance * 0.9;
2005
+ const descent = metrics.actualBoundingBoxDescent || metrics.fontBoundingBoxDescent || advance * 0.25;
2006
+ const left = metrics.actualBoundingBoxLeft || 0;
2007
+ const right = metrics.actualBoundingBoxRight || advance;
2008
+ const offsetX = Math.ceil(left) + PAD;
2009
+ const offsetY = Math.ceil(ascent) + PAD;
2010
+ const w = offsetX + Math.ceil(right) + PAD;
2011
+ const h = offsetY + Math.ceil(descent) + PAD;
2012
+ if (!(w > 0) || !(h > 0)) {
2013
+ this.slots.set(key, null);
2014
+ return null;
2015
+ }
2016
+ const dpr = this.dpr;
2017
+ const dw = Math.ceil(w * dpr);
2018
+ const dh = Math.ceil(h * dpr);
2019
+ if (dw + PAD * 2 > this.maxSize || dh + PAD * 2 > this.maxSize) {
2020
+ this.slots.set(key, null);
2021
+ return null;
2022
+ }
2023
+ if (this.penX + dw + PAD > this.maxSize) {
2024
+ this.penX = PAD;
2025
+ this.penY += this.rowHeight + PAD;
2026
+ this.rowHeight = 0;
2027
+ }
2028
+ if (this.penY + dh + PAD > this.maxSize) {
2029
+ this.reset();
2030
+ if (!this.ensureCanvas()) return null;
2031
+ this.ctx.font = font;
2032
+ }
2033
+ const sx = this.penX;
2034
+ const sy = this.penY;
2035
+ const c = this.ctx;
2036
+ c.save();
2037
+ c.translate(sx, sy);
2038
+ if (dpr !== 1) c.scale(dpr, dpr);
2039
+ c.font = font;
2040
+ c.textBaseline = "alphabetic";
2041
+ c.fillStyle = color;
2042
+ c.fillText(glyph, offsetX, offsetY);
2043
+ c.restore();
2044
+ this.penX += dw + PAD;
2045
+ if (dh > this.rowHeight) this.rowHeight = dh;
2046
+ const slot = {
2047
+ sx,
2048
+ sy,
2049
+ sw: dw,
2050
+ sh: dh,
2051
+ w,
2052
+ h,
2053
+ offsetX,
2054
+ offsetY,
2055
+ glyph,
2056
+ font,
2057
+ advance,
2058
+ left,
2059
+ right
2060
+ };
2061
+ this.slots.set(key, slot);
2062
+ return slot;
2063
+ }
2064
+ /**
2065
+ * Find the slot occupying a source position, or `null`.
2066
+ *
2067
+ * The inverse of {@link get}: it maps a blit back to the glyph it drew. Exists
2068
+ * for instrumentation — a test or devtool that traces `drawImage` calls sees
2069
+ * only a source rect, and needs this to recover which cluster was painted and
2070
+ * with what metrics. Linear over resident slots, so it is a diagnostic, not a
2071
+ * per-frame call.
2072
+ */
2073
+ slotAt(sx, sy) {
2074
+ for (const slot of this.slots.values()) {
2075
+ if (slot && slot.sx === sx && slot.sy === sy) return slot;
2076
+ }
2077
+ return null;
2078
+ }
2079
+ /**
2080
+ * Drop every glyph and reuse the canvas.
2081
+ *
2082
+ * Call after a font or theme change: slots are keyed by `(font, color, glyph)`
2083
+ * so stale entries are never *returned* wrongly, but they do occupy space.
2084
+ */
2085
+ reset() {
2086
+ this.slots.clear();
2087
+ this.penX = PAD;
2088
+ this.penY = PAD;
2089
+ this.rowHeight = 0;
2090
+ this._resets++;
2091
+ if (this.ctx && this.canvas) {
2092
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
2093
+ }
2094
+ }
2095
+ /** Release the backing canvas and all slots. */
2096
+ destroy() {
2097
+ this.slots.clear();
2098
+ this.canvas = null;
2099
+ this.ctx = null;
2100
+ this.penX = PAD;
2101
+ this.penY = PAD;
2102
+ this.rowHeight = 0;
2103
+ this._hits = 0;
2104
+ this._misses = 0;
2105
+ this._resets = 0;
2106
+ }
2107
+ }, _class4);
2108
+
1812
2109
  // src/renderer/TextRasterCache.ts
1813
- var TextRasterCache = (_class4 = class {
1814
- __init35() {this.cache = /* @__PURE__ */ new Map()}
2110
+ var TextRasterCache = (_class5 = class {
2111
+ __init48() {this.cache = /* @__PURE__ */ new Map()}
1815
2112
 
1816
2113
 
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);
2114
+ __init49() {this.scratchCtx = null}
2115
+ __init50() {this._hits = 0}
2116
+ __init51() {this._misses = 0}
2117
+ constructor(options = {}) {;_class5.prototype.__init48.call(this);_class5.prototype.__init49.call(this);_class5.prototype.__init50.call(this);_class5.prototype.__init51.call(this);
1821
2118
  this.maxEntries = Math.max(1, _nullishCoalesce(options.maxEntries, () => ( 4096)));
1822
2119
  this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
1823
2120
  }
@@ -1892,7 +2189,8 @@ var TextRasterCache = (_class4 = class {
1892
2189
  this._hits = 0;
1893
2190
  this._misses = 0;
1894
2191
  }
1895
- }, _class4);
2192
+ }, _class5);
2193
+
1896
2194
 
1897
2195
 
1898
2196
 
@@ -1903,4 +2201,4 @@ var TextRasterCache = (_class4 = class {
1903
2201
 
1904
2202
 
1905
2203
 
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;
2204
+ 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;