@vectojs/core 1.38.1 → 1.39.1

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.
@@ -65,7 +65,8 @@ function emptyDrawCounters() {
65
65
  }
66
66
  var TWO_PI = Math.PI * 2;
67
67
  function getDevicePixelRatio() {
68
- return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
68
+ const raw = typeof window !== "undefined" ? window.devicePixelRatio : 1;
69
+ return Number.isFinite(raw) && raw > 0 ? raw : 1;
69
70
  }
70
71
  var CanvasRenderer = (_class = class _CanvasRenderer {
71
72
 
@@ -109,17 +110,28 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
109
110
  __init7() {this.batchCount = 0}
110
111
  __init8() {this._cachedFont = ""}
111
112
  __init9() {this._cachedFill = ""}
113
+ // Stroke-side counterparts of the fill/font caches, read by stroke()'s
114
+ // style-elision branch. Reset wherever the context state can change behind
115
+ // our back: restore(), resize(), contextrestored, dispose().
116
+ __init10() {this._cachedStroke = ""}
117
+ __init11() {this._cachedLineWidth = -1}
118
+ __init12() {this._cachedLineCap = ""}
119
+ __init13() {this._cachedLineJoin = ""}
120
+ // Context-loss listeners, held for removal in dispose() so a canvas that
121
+ // outlives the renderer cannot retain it through these closures.
122
+
123
+
112
124
  /** Backend discriminator; see {@link IRenderer.kind}. */
113
- __init10() {this.kind = "canvas2d"}
125
+ __init14() {this.kind = "canvas2d"}
114
126
  /**
115
127
  * Draw counters, allocated only once counting is enabled.
116
128
  *
117
129
  * Null when off, so the guard on every op is a single null test and an inactive
118
130
  * renderer carries no counter object at all.
119
131
  */
120
- __init11() {this.counters = null}
132
+ __init15() {this.counters = null}
121
133
  /** Accumulated primitive area, kept separately so the ratio is derived on read. */
122
- __init12() {this.drawnArea = 0}
134
+ __init16() {this.drawnArea = 0}
123
135
  /**
124
136
  * @param canvas - The target canvas. Its backing store is resized to the
125
137
  * logical size × devicePixelRatio.
@@ -129,7 +141,7 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
129
141
  * set) so the canvas's own dimensions aren't clobbered by the window's.
130
142
  * @param maxDPR - See {@link maxDPR}.
131
143
  */
132
- 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);_class.prototype.__init12.call(this);
144
+ 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);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);_class.prototype.__init16.call(this);
133
145
  this.maxDPR = maxDPR;
134
146
  const dpr = this.effectiveDPR();
135
147
  this.appliedDPR = dpr;
@@ -162,11 +174,11 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
162
174
  */
163
175
  setupContextLossRecovery() {
164
176
  if (typeof this.canvas.addEventListener !== "function") return;
165
- this.canvas.addEventListener("contextlost", (e) => {
177
+ this._onContextLost = (e) => {
166
178
  e.preventDefault();
167
179
  this.contextLost = true;
168
- });
169
- this.canvas.addEventListener("contextrestored", () => {
180
+ };
181
+ this._onContextRestored = () => {
170
182
  const ctx = this.canvas.getContext("2d");
171
183
  if (!ctx) return;
172
184
  this.ctx = ctx;
@@ -176,11 +188,17 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
176
188
  ctx.scale(restoredDPR, restoredDPR);
177
189
  this._cachedFont = "";
178
190
  this._cachedFill = "";
191
+ this._cachedStroke = "";
192
+ this._cachedLineWidth = -1;
193
+ this._cachedLineCap = "";
194
+ this._cachedLineJoin = "";
179
195
  this.batchActive = false;
180
196
  this.batchCount = 0;
181
197
  this.contextLost = false;
182
198
  _optionalChain([this, 'access', _8 => _8.contextRestoredCb, 'optionalCall', _9 => _9()]);
183
- });
199
+ };
200
+ this.canvas.addEventListener("contextlost", this._onContextLost);
201
+ this.canvas.addEventListener("contextrestored", this._onContextRestored);
184
202
  }
185
203
  /**
186
204
  * Expose the underlying `CanvasRenderingContext2D` for operations not
@@ -194,7 +212,9 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
194
212
  /** Real `devicePixelRatio`, clamped to {@link maxDPR} when set. */
195
213
  effectiveDPR() {
196
214
  const real = getDevicePixelRatio();
197
- return this.maxDPR !== void 0 ? Math.min(real, this.maxDPR) : real;
215
+ if (this.maxDPR === void 0) return real;
216
+ if (!Number.isFinite(this.maxDPR) || this.maxDPR <= 0) return real;
217
+ return Math.min(real, this.maxDPR);
198
218
  }
199
219
  /**
200
220
  * @inheritdoc
@@ -225,16 +245,26 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
225
245
  */
226
246
  resize(width, height) {
227
247
  const dpr = this.effectiveDPR();
248
+ const safeWidth = Number.isFinite(width) && width >= 0 ? width : this.width;
249
+ const safeHeight = Number.isFinite(height) && height >= 0 ? height : this.height;
250
+ const backingW = Number.isFinite(safeWidth * dpr) ? Math.max(1, Math.round(safeWidth * dpr)) : 1;
251
+ const backingH = Number.isFinite(safeHeight * dpr) ? Math.max(1, Math.round(safeHeight * dpr)) : 1;
228
252
  this.appliedDPR = dpr;
229
- this.width = width;
230
- this.height = height;
231
- this.ctx.canvas.width = width * dpr;
232
- this.ctx.canvas.height = height * dpr;
233
- this.ctx.canvas.style.width = `${width}px`;
234
- this.ctx.canvas.style.height = `${height}px`;
253
+ this.width = safeWidth;
254
+ this.height = safeHeight;
255
+ this.ctx.canvas.width = backingW;
256
+ this.ctx.canvas.height = backingH;
257
+ if (this.ctx.canvas.style) {
258
+ this.ctx.canvas.style.width = `${safeWidth}px`;
259
+ this.ctx.canvas.style.height = `${safeHeight}px`;
260
+ }
235
261
  this.ctx.scale(dpr, dpr);
236
262
  this._cachedFont = "";
237
263
  this._cachedFill = "";
264
+ this._cachedStroke = "";
265
+ this._cachedLineWidth = -1;
266
+ this._cachedLineCap = "";
267
+ this._cachedLineJoin = "";
238
268
  this.batchActive = false;
239
269
  this.batchCount = 0;
240
270
  }
@@ -288,6 +318,10 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
288
318
  this.ctx.restore();
289
319
  this._cachedFont = "";
290
320
  this._cachedFill = "";
321
+ this._cachedStroke = "";
322
+ this._cachedLineWidth = -1;
323
+ this._cachedLineCap = "";
324
+ this._cachedLineJoin = "";
291
325
  }
292
326
  /** @inheritdoc */
293
327
  translate(x, y) {
@@ -412,10 +446,17 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
412
446
  stroke(color, lineWidth = 1) {
413
447
  this.flush();
414
448
  if (this.counters) this.counters.strokes++;
415
- this.ctx.strokeStyle = color;
416
- this.ctx.lineWidth = lineWidth;
417
- this.ctx.lineCap = "round";
418
- this.ctx.lineJoin = "round";
449
+ if (this._cachedStroke !== color || this._cachedLineWidth !== lineWidth || this._cachedLineCap !== "round" || this._cachedLineJoin !== "round") {
450
+ if (this.counters) this.counters.stateSwitches++;
451
+ this.ctx.strokeStyle = color;
452
+ this.ctx.lineWidth = lineWidth;
453
+ this.ctx.lineCap = "round";
454
+ this.ctx.lineJoin = "round";
455
+ this._cachedStroke = color;
456
+ this._cachedLineWidth = lineWidth;
457
+ this._cachedLineCap = "round";
458
+ this._cachedLineJoin = "round";
459
+ }
419
460
  this.ctx.stroke();
420
461
  }
421
462
  /** @inheritdoc */
@@ -454,6 +495,17 @@ var CanvasRenderer = (_class = class _CanvasRenderer {
454
495
  this.batchActive = false;
455
496
  this._cachedFont = "";
456
497
  this._cachedFill = "";
498
+ this._cachedStroke = "";
499
+ this._cachedLineWidth = -1;
500
+ this._cachedLineCap = "";
501
+ this._cachedLineJoin = "";
502
+ if (typeof this.canvas.removeEventListener === "function") {
503
+ if (this._onContextLost) this.canvas.removeEventListener("contextlost", this._onContextLost);
504
+ if (this._onContextRestored)
505
+ this.canvas.removeEventListener("contextrestored", this._onContextRestored);
506
+ }
507
+ this._onContextLost = void 0;
508
+ this._onContextRestored = void 0;
457
509
  }
458
510
  }, _class.__initStatic(), _class);
459
511
 
@@ -476,9 +528,13 @@ function hasSafeSchemeOrIsRelative(url) {
476
528
  return SAFE_SCHEMES.has(`${candidate.toLowerCase()}:`);
477
529
  }
478
530
  var CHAR_REF = /&(?:#\d+|#x[\da-f]+|colon|tab|newline);/i;
531
+ var MAX_CODE_POINT = 1114111;
532
+ function decodeCodePoint(value) {
533
+ return value <= MAX_CODE_POINT ? String.fromCodePoint(value) : "\uFFFD";
534
+ }
479
535
  function decodeCharacterReferences(value) {
480
536
  if (value.indexOf("&") < 0 || !CHAR_REF.test(value)) return value;
481
- return value.replace(/&#x([\da-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10))).replace(/&colon;/gi, ":").replace(/&tab;/gi, " ").replace(/&newline;/gi, "\n");
537
+ return value.replace(/&#x([\da-f]+);/gi, (_, hex) => decodeCodePoint(parseInt(hex, 16))).replace(/&#(\d+);/g, (_, dec) => decodeCodePoint(parseInt(dec, 10))).replace(/&colon;/gi, ":").replace(/&tab;/gi, " ").replace(/&newline;/gi, "\n");
482
538
  }
483
539
  function sanitizeUrl(href) {
484
540
  if (typeof href !== "string") return "";
@@ -545,44 +601,47 @@ function fontFamilyFromShorthand(font, sizeToken) {
545
601
  }
546
602
  var SVGRenderer = (_class2 = class {
547
603
  /** Backend discriminator; see {@link IRenderer.kind}. */
548
- __init13() {this.kind = "svg"}
604
+ __init17() {this.kind = "svg"}
549
605
 
550
606
 
551
- __init14() {this.buffer = []}
552
- __init15() {this.defsBuffer = []}
553
- __init16() {this.currentPath = []}
607
+ __init18() {this.buffer = []}
608
+ __init19() {this.defsBuffer = []}
609
+ __init20() {this.currentPath = []}
554
610
  // Inline matrix states (3x3 representation)
555
611
  // [a, b, c, d, e, f] corresponding to:
556
612
  // | a c e |
557
613
  // | b d f |
558
614
  // | 0 0 1 |
559
- __init17() {this.mStack = []}
560
- __init18() {this.ma = 1}
561
- __init19() {this.mb = 0}
562
- __init20() {this.mc = 0}
563
- __init21() {this.md = 1}
564
- __init22() {this.me = 0}
565
- __init23() {this.mf = 0}
615
+ __init21() {this.mStack = []}
616
+ __init22() {this.ma = 1}
617
+ __init23() {this.mb = 0}
618
+ __init24() {this.mc = 0}
619
+ __init25() {this.md = 1}
620
+ __init26() {this.me = 0}
621
+ __init27() {this.mf = 0}
566
622
  // Alpha stack
567
- __init24() {this.alphaStack = []}
568
- __init25() {this.globalAlpha = 1}
623
+ __init28() {this.alphaStack = []}
624
+ __init29() {this.globalAlpha = 1}
569
625
  // Clipping stack
570
- __init26() {this.clipDepthStack = []}
571
- __init27() {this.clipDepth = 0}
572
- __init28() {this.clipCounter = 0}
626
+ __init30() {this.clipDepthStack = []}
627
+ __init31() {this.clipDepth = 0}
628
+ __init32() {this.clipCounter = 0}
573
629
  // uniquely identifies clipPaths
574
630
  // Batch circles states (local coordinates)
575
- __init29() {this.batchCircles = []}
576
- __init30() {this.batchMatrix = [1, 0, 0, 1, 0, 0]}
577
- __init31() {this.batchColor = ""}
578
- __init32() {this.batchAlpha = 1}
579
- __init33() {this.batchActive = false}
631
+ __init33() {this.batchCircles = []}
632
+ __init34() {this.batchMatrix = [1, 0, 0, 1, 0, 0]}
633
+ __init35() {this.batchColor = ""}
634
+ __init36() {this.batchAlpha = 1}
635
+ __init37() {this.batchActive = false}
580
636
  // Cache for generated gradient defs
581
- __init34() {this.gradientCounter = 0}
582
- __init35() {this.gradientCache = /* @__PURE__ */ new Map()}
583
- constructor(width, height) {;_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);
637
+ __init38() {this.gradientCounter = 0}
638
+ __init39() {this.gradientCache = /* @__PURE__ */ new Map()}
639
+ /** Root font size used to resolve `em`/`rem` font sizes (default 16px). */
640
+
641
+ constructor(width, height, options) {;_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);
584
642
  this.width = width;
585
643
  this.height = height;
644
+ this.rootFontSize = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _10 => _10.rootFontSize]), () => ( 16));
586
645
  installRendererDevTraps(this, "SVGRenderer");
587
646
  }
588
647
  clear() {
@@ -770,12 +829,22 @@ var SVGRenderer = (_class2 = class {
770
829
  `<path d="${dStr}" transform="${transformStr}" fill="none" stroke="${strokeVal}" stroke-width="${lineWidth}" stroke-opacity="${this.globalAlpha}" />`
771
830
  );
772
831
  }
832
+ /**
833
+ * Emit a `<text>` element for a text run.
834
+ *
835
+ * Font-size unit handling: `px` sizes pass through; `em` and `rem` are both
836
+ * scaled against the renderer's `rootFontSize` (constructor option, default
837
+ * 16 — matching the browser default, not necessarily the host page's root,
838
+ * so pass `{ rootFontSize }` when exporting against a non-default root).
839
+ * A percentage size (`'100% Inter'`) has no absolute meaning in this
840
+ * context and silently falls back to the default 16px font size.
841
+ */
773
842
  fillText(text, x, y, font, color) {
774
843
  this.flush();
775
844
  const sizeToken = parseFontSizeToken(font);
776
845
  let fontSize = sizeToken ? sizeToken.value : 16;
777
- if (sizeToken && sizeToken.unit !== "px") {
778
- fontSize = fontSize * 16;
846
+ if (sizeToken && (sizeToken.unit === "em" || sizeToken.unit === "rem")) {
847
+ fontSize = fontSize * this.rootFontSize;
779
848
  }
780
849
  const lowerFont = font.toLowerCase();
781
850
  const fontStyle = lowerFont.includes("italic") ? "italic" : lowerFont.includes("oblique") ? "oblique" : "normal";
@@ -805,8 +874,8 @@ var SVGRenderer = (_class2 = class {
805
874
  }
806
875
  drawImage(source, dx, dy, dw, dh) {
807
876
  this.flush();
808
- const fromCanvas2 = typeof _optionalChain([source, 'optionalAccess', _10 => _10.toDataURL]) === "function";
809
- const rawHref = fromCanvas2 ? source.toDataURL() : _optionalChain([source, 'optionalAccess', _11 => _11.src]) || "";
877
+ const fromCanvas2 = typeof _optionalChain([source, 'optionalAccess', _11 => _11.toDataURL]) === "function";
878
+ const rawHref = fromCanvas2 ? source.toDataURL() : _optionalChain([source, 'optionalAccess', _12 => _12.src]) || "";
810
879
  const href = fromCanvas2 && this.isSafeRasterDataUrl(rawHref) ? rawHref : sanitizeUrl(String(rawHref));
811
880
  const transformStr = `matrix(${this.ma},${this.mb},${this.mc},${this.md},${this.me},${this.mf})`;
812
881
  if (href) {
@@ -957,11 +1026,11 @@ var SVGRenderer = (_class2 = class {
957
1026
  }
958
1027
  /**
959
1028
  * SVGRenderer accumulates strings in memory; nothing external is allocated.
960
- * Drop the buffers for GC and become idempotent.
1029
+ * Drop the buffers for GC and become idempotent. `clear()` already empties
1030
+ * the gradient cache, so no second clear is needed here.
961
1031
  */
962
1032
  dispose() {
963
1033
  this.clear();
964
- this.gradientCache.clear();
965
1034
  }
966
1035
  }, _class2);
967
1036
 
@@ -1333,13 +1402,22 @@ function createWebGLPointRenderer(canvas) {
1333
1402
  premultipliedAlpha: false
1334
1403
  });
1335
1404
  if (!gl) return null;
1336
- const pointProgram = link(gl, POINT_VERT, POINT_FRAG);
1337
- const rectProgram = link(gl, RECT_VERT, RECT_FRAG);
1338
- const spriteProgram = link(gl, SPRITE_VERT, SPRITE_FRAG);
1339
- const msdfProgram = link(gl, SPRITE_VERT, MSDF_FRAG);
1340
- const circleQuadProgram = link(gl, SPRITE_VERT, CIRCLE_QUAD_FRAG);
1341
- if (!pointProgram || !rectProgram || !spriteProgram || !msdfProgram || !circleQuadProgram)
1405
+ const programs = [];
1406
+ const linkTracked = (vsSrc, fsSrc) => {
1407
+ const program = link(gl, vsSrc, fsSrc);
1408
+ if (program) programs.push(program);
1409
+ return program;
1410
+ };
1411
+ const pointProgram = linkTracked(POINT_VERT, POINT_FRAG);
1412
+ const rectProgram = linkTracked(RECT_VERT, RECT_FRAG);
1413
+ const spriteProgram = linkTracked(SPRITE_VERT, SPRITE_FRAG);
1414
+ const msdfProgram = linkTracked(SPRITE_VERT, MSDF_FRAG);
1415
+ const circleQuadProgram = linkTracked(SPRITE_VERT, CIRCLE_QUAD_FRAG);
1416
+ if (!pointProgram || !rectProgram || !spriteProgram || !msdfProgram || !circleQuadProgram) {
1417
+ for (const program of programs) gl.deleteProgram(program);
1418
+ _optionalChain([gl, 'access', _13 => _13.getExtension, 'call', _14 => _14("WEBGL_lose_context"), 'optionalAccess', _15 => _15.loseContext, 'call', _16 => _16()]);
1342
1419
  return null;
1420
+ }
1343
1421
  gl.enable(gl.BLEND);
1344
1422
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1345
1423
  const pAPos = gl.getAttribLocation(pointProgram, "a_pos");
@@ -1519,6 +1597,7 @@ function createWebGLPointRenderer(canvas) {
1519
1597
  ensureQuadIndices(spriteCount);
1520
1598
  gl.drawElements(gl.TRIANGLES, spriteCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
1521
1599
  frameDrawCalls++;
1600
+ gl.bindVertexArray(null);
1522
1601
  spriteCount = 0;
1523
1602
  };
1524
1603
  const drawGlyphs = () => {
@@ -1797,6 +1876,7 @@ function createWebGLPointRenderer(canvas) {
1797
1876
  }
1798
1877
 
1799
1878
  // src/renderer/WebGPUParticleSystemManager.ts
1879
+ var recordComputePassScratch = new Float32Array(20);
1800
1880
  var COMPUTE_SHADER = `
1801
1881
  struct Particle {
1802
1882
  position: vec2<f32>,
@@ -1971,16 +2051,22 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
1971
2051
  `;
1972
2052
  var WebGPUParticleSystemManager = (_class3 = class {
1973
2053
 
1974
- __init36() {this.computePipeline = null}
1975
- __init37() {this.renderPipeline = null}
1976
- __init38() {this.computeBindGroupLayout = null}
1977
- __init39() {this.renderBindGroupLayout = null}
1978
- constructor(device) {;_class3.prototype.__init36.call(this);_class3.prototype.__init37.call(this);_class3.prototype.__init38.call(this);_class3.prototype.__init39.call(this);
2054
+ __init40() {this.computePipeline = null}
2055
+ __init41() {this.renderPipeline = null}
2056
+ // Held so destroy() can release them — modules were previously unreachable
2057
+ // after initPipelines and lived until context loss (#684).
2058
+ __init42() {this.computeModule = null}
2059
+ __init43() {this.renderModule = null}
2060
+ __init44() {this.computeBindGroupLayout = null}
2061
+ __init45() {this.renderBindGroupLayout = null}
2062
+ constructor(device) {;_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);
1979
2063
  this.device = device;
1980
2064
  }
1981
2065
  initPipelines(format) {
1982
- const computeModule = this.device.createShaderModule({ code: COMPUTE_SHADER });
1983
- const renderModule = this.device.createShaderModule({ code: RENDER_SHADER });
2066
+ this.computeModule = this.device.createShaderModule({ code: COMPUTE_SHADER });
2067
+ this.renderModule = this.device.createShaderModule({ code: RENDER_SHADER });
2068
+ const computeModule = this.computeModule;
2069
+ const renderModule = this.renderModule;
1984
2070
  this.computeBindGroupLayout = this.device.createBindGroupLayout({
1985
2071
  entries: [
1986
2072
  {
@@ -2039,6 +2125,8 @@ var WebGPUParticleSystemManager = (_class3 = class {
2039
2125
  });
2040
2126
  }
2041
2127
  setupEntityResources(entity) {
2128
+ releaseGpuObject(entity.gpuStorageBuffer);
2129
+ releaseGpuObject(entity.gpuUniformBuffer);
2042
2130
  const storageSize = entity.maxParticles * 32;
2043
2131
  entity.gpuStorageBuffer = this.device.createBuffer({
2044
2132
  size: storageSize,
@@ -2065,7 +2153,7 @@ var WebGPUParticleSystemManager = (_class3 = class {
2065
2153
  }
2066
2154
  recordComputePass(pass, entity, dt, mouseX, mouseY, width, height) {
2067
2155
  if (!this.computePipeline || !entity.computeBindGroup) return;
2068
- const uniformArray = new Float32Array(20);
2156
+ const uniformArray = recordComputePassScratch;
2069
2157
  const color = parseColorToRGBA(entity.baseColor);
2070
2158
  let opacity = 1;
2071
2159
  for (let current = entity; current; current = current.parent) {
@@ -2109,34 +2197,45 @@ var WebGPUParticleSystemManager = (_class3 = class {
2109
2197
  pass.draw(6, entity.maxParticles);
2110
2198
  }
2111
2199
  destroy() {
2200
+ releaseGpuObject(this.computePipeline);
2201
+ releaseGpuObject(this.renderPipeline);
2202
+ releaseGpuObject(this.computeModule);
2203
+ releaseGpuObject(this.renderModule);
2112
2204
  this.computePipeline = null;
2113
2205
  this.renderPipeline = null;
2206
+ this.computeModule = null;
2207
+ this.renderModule = null;
2114
2208
  this.computeBindGroupLayout = null;
2115
2209
  this.renderBindGroupLayout = null;
2116
2210
  }
2117
2211
  }, _class3);
2212
+ function releaseGpuObject(obj) {
2213
+ if (obj && typeof obj.destroy === "function") {
2214
+ obj.destroy();
2215
+ }
2216
+ }
2118
2217
 
2119
2218
  // src/renderer/GlyphRasterAtlas.ts
2120
2219
  var HARD_MAX_SIZE = 8192;
2121
2220
  var PAD = 2;
2122
2221
  var GlyphRasterAtlas = (_class4 = class {
2123
- __init40() {this.slots = /* @__PURE__ */ new Map()}
2222
+ __init46() {this.slots = /* @__PURE__ */ new Map()}
2124
2223
 
2125
2224
 
2126
- __init41() {this.canvas = null}
2127
- __init42() {this.ctx = null}
2225
+ __init47() {this.canvas = null}
2226
+ __init48() {this.ctx = null}
2128
2227
  /**
2129
2228
  * Shelf packing: glyphs land left-to-right on a row, then a new row starts.
2130
2229
  * A monospace grid produces near-uniform widths, so shelves waste very little
2131
2230
  * and cost one comparison per insert — a real 2D packer would buy nothing here.
2132
2231
  */
2133
- __init43() {this.penX = PAD}
2134
- __init44() {this.penY = PAD}
2135
- __init45() {this.rowHeight = 0}
2136
- __init46() {this._hits = 0}
2137
- __init47() {this._misses = 0}
2138
- __init48() {this._resets = 0}
2139
- constructor(options = {}) {;_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);_class4.prototype.__init48.call(this);
2232
+ __init49() {this.penX = PAD}
2233
+ __init50() {this.penY = PAD}
2234
+ __init51() {this.rowHeight = 0}
2235
+ __init52() {this._hits = 0}
2236
+ __init53() {this._misses = 0}
2237
+ __init54() {this._resets = 0}
2238
+ constructor(options = {}) {;_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);
2140
2239
  this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
2141
2240
  this.maxSize = Math.min(HARD_MAX_SIZE, Math.max(256, _nullishCoalesce(options.maxSize, () => ( 2048))));
2142
2241
  }
@@ -2195,7 +2294,7 @@ var GlyphRasterAtlas = (_class4 = class {
2195
2294
  * (headless, unrasterizable, or too large to pack).
2196
2295
  */
2197
2296
  get(font, color, glyph) {
2198
- const key = font + "\0" + color + "\0" + glyph;
2297
+ const key = `${font.length}\0${font}${color.length}\0${color}${glyph.length}\0${glyph}`;
2199
2298
  const existing = this.slots.get(key);
2200
2299
  if (existing !== void 0) {
2201
2300
  if (existing) this._hits++;
@@ -2318,13 +2417,13 @@ var GlyphRasterAtlas = (_class4 = class {
2318
2417
 
2319
2418
  // src/renderer/TextRasterCache.ts
2320
2419
  var TextRasterCache = (_class5 = class {
2321
- __init49() {this.cache = /* @__PURE__ */ new Map()}
2420
+ __init55() {this.cache = /* @__PURE__ */ new Map()}
2322
2421
 
2323
2422
 
2324
- __init50() {this.scratchCtx = null}
2325
- __init51() {this._hits = 0}
2326
- __init52() {this._misses = 0}
2327
- constructor(options = {}) {;_class5.prototype.__init49.call(this);_class5.prototype.__init50.call(this);_class5.prototype.__init51.call(this);_class5.prototype.__init52.call(this);
2423
+ __init56() {this.scratchCtx = null}
2424
+ __init57() {this._hits = 0}
2425
+ __init58() {this._misses = 0}
2426
+ constructor(options = {}) {;_class5.prototype.__init55.call(this);_class5.prototype.__init56.call(this);_class5.prototype.__init57.call(this);_class5.prototype.__init58.call(this);
2328
2427
  this.maxEntries = Math.max(1, _nullishCoalesce(options.maxEntries, () => ( 4096)));
2329
2428
  this.dpr = Math.max(1, _nullishCoalesce(options.dpr, () => ( 1)));
2330
2429
  }
@@ -2349,7 +2448,7 @@ var TextRasterCache = (_class5 = class {
2349
2448
  * fall back to {@link IRenderer.fillText}).
2350
2449
  */
2351
2450
  get(font, color, text) {
2352
- const key = font + "\0" + color + "\0" + text;
2451
+ const key = `${font.length}\0${font}${color.length}\0${color}${text.length}\0${text}`;
2353
2452
  const hit = this.cache.get(key);
2354
2453
  if (hit) {
2355
2454
  this._hits++;