@weasel-js/renderer 1.9.4 → 1.10.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.
@@ -1,16 +1,28 @@
1
- import { ownTextureIds, rasterizeTile, drawTileShapes, tileGeometry, tileSize, registerTextureAs, textureRegistry, unregisterTexture } from './chunk-ORBXHIRB.js';
1
+ import { ownTextureIds, rasterizeTile, drawTileShapes, tileGeometry, tileSize, registerTextureAs, textureRegistry, unregisterTexture } from './chunk-Q4SIHV7Q.js';
2
+ import { defaultOpenFill, pathFromD, boundsOfPath } from '@weasel-js/geom';
2
3
  import { tessellate } from '@weasel-js/geom/tessellate';
3
- import { createReflectable } from '@weasel-js/registry';
4
+ import { createReflectable, createEmitter } from '@weasel-js/registry';
4
5
  import { resolveStrokeWidth, strokeInsets, tessellateStroke, markerGeneration, screenSpaceKey, markerHeads, metricKey, getMarker, markerKeyOf, markerReadsKey } from '@weasel-js/stroke';
5
- import { pathFromD, boundsOfPath } from '@weasel-js/geom';
6
+ import { isTileKind, isPatternPaint, tileSpecOf, isTilePaint, patternPlacementOf } from '@weasel-js/paint';
6
7
  import { GLYPH_COVERAGE_GLSL } from '@weasel-js/font';
7
8
 
9
+ // src/cache/uploadPremultiplied.ts
10
+ function uploadPremultiplied(gl, source) {
11
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
12
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
13
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
14
+ }
15
+
8
16
  // src/cache/GLImageCache.ts
9
17
  var DEFAULT_MAX_TEXTURE_SIDE = 4096;
10
18
  var live = /* @__PURE__ */ new Set();
11
19
  function releaseImageSource(key) {
12
20
  for (const cache7 of live) cache7.release(key);
13
21
  }
22
+ function imageSourceInLatestFrame(key) {
23
+ for (const cache7 of live) if (cache7.drewInLatestFrame(key)) return true;
24
+ return false;
25
+ }
14
26
  function maxImageTextureSide() {
15
27
  let side = Infinity;
16
28
  for (const cache7 of live) side = Math.min(side, cache7.maxTextureSide);
@@ -18,11 +30,10 @@ function maxImageTextureSide() {
18
30
  }
19
31
  var GLImageCache = class {
20
32
  /** `minification` selects the MIN_FILTER strategy for uploaded textures.
21
- * `'linear'` (default) is the screen path's existing behavior. `'mipmap'`
22
- * generates mipmaps and filters LINEAR_MIPMAP_LINEAR — required for
23
- * quality minification when a large source bitmap is drawn small (the
24
- * headless print/export path); bilinear-only minification undersamples
25
- * and produces moiré. */
33
+ * `'mipmap'` generates mipmaps and filters LINEAR_MIPMAP_LINEAR: when a
34
+ * large source bitmap is drawn small, bilinear-only minification
35
+ * undersamples and produces moiré. `'auto'` pays for that only on the
36
+ * textures a frame draws small, when it first does. */
26
37
  constructor(gl, minification = "linear") {
27
38
  this.gl = gl;
28
39
  this.minification = minification;
@@ -43,25 +54,29 @@ var GLImageCache = class {
43
54
  textures = /* @__PURE__ */ new Set();
44
55
  /**
45
56
  * MAG_FILTER each texture currently carries, so a redundant write can be
46
- * skipped.
47
- *
48
- * The batch sets this per flush — the same bitmap can be drawn at both
49
- * filters in one frame, and the value has to be right at the draw rather than
50
- * at upload. Filtering is state on the *texture object*, though, not on the
51
- * unit, so re-asserting a value it already has is a write to a live texture
52
- * for no reason. On a large sheet that is not free: a consumer's wall samples
53
- * a 5652px-square atlas, 122MB resident, and measured `sampling: 'nearest'`
54
- * costing up to 8x `'linear'` there — where the linear pass re-asserts the
55
- * upload default and the nearest pass changes state on every draw.
56
- *
57
- * Whether that is the cause is unproven (see `docs/TODO.md`), but the write
58
- * was redundant either way.
57
+ * skipped. The batch sets this per flush — the same bitmap can be drawn at
58
+ * both filters in one frame — but filtering is state on the *texture
59
+ * object*, not on the unit, so re-asserting a value it already has is a
60
+ * write to a live texture for no reason.
59
61
  */
60
62
  magFilters = /* @__PURE__ */ new WeakMap();
61
63
  finalizer;
62
64
  pendingDeletes = [];
63
- /** Delete the textures of sources collected since the last call. The
64
- * renderer calls this at the top of each `render()`. */
65
+ /** Under `'auto'`, the sources whose mipmaps are built. */
66
+ mipped = /* @__PURE__ */ new WeakSet();
67
+ frame = 0;
68
+ lastDrawn = /* @__PURE__ */ new WeakMap();
69
+ /** Open a frame: what {@link drewInLatestFrame} answers about from here on.
70
+ * The renderer calls this at the top of each `render()`. */
71
+ beginFrame() {
72
+ this.frame++;
73
+ this.drainPendingDeletes();
74
+ }
75
+ /** Whether `key` was uploaded or bound since the last {@link beginFrame}. */
76
+ drewInLatestFrame(key) {
77
+ return this.lastDrawn.get(key) === this.frame;
78
+ }
79
+ /** Delete the textures of sources collected since the last call. */
65
80
  drainPendingDeletes() {
66
81
  if (this.pendingDeletes.length === 0) return;
67
82
  for (const tex of this.pendingDeletes) this.gl.deleteTexture(tex);
@@ -76,6 +91,8 @@ var GLImageCache = class {
76
91
  this.finalizer.unregister(key);
77
92
  this.map.delete(key);
78
93
  this.magFilters.delete(key);
94
+ this.lastDrawn.delete(key);
95
+ this.mipped.delete(key);
79
96
  }
80
97
  /** Delete every texture this cache uploaded and stop answering
81
98
  * {@link releaseImageSource} and {@link maxImageTextureSide}. Idempotent. */
@@ -92,13 +109,14 @@ var GLImageCache = class {
92
109
  live.delete(this);
93
110
  }
94
111
  upload(key, source, repetition) {
112
+ this.lastDrawn.set(key, this.frame);
95
113
  const existing = this.map.get(key);
96
114
  if (existing) return existing;
97
115
  const gl = this.gl;
98
116
  const tex = gl.createTexture();
99
117
  if (!tex) throw new Error("GLImageCache: createTexture failed");
100
118
  gl.bindTexture(gl.TEXTURE_2D, tex);
101
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
119
+ uploadPremultiplied(gl, source);
102
120
  gl.texParameteri(
103
121
  gl.TEXTURE_2D,
104
122
  gl.TEXTURE_MIN_FILTER,
@@ -116,9 +134,27 @@ var GLImageCache = class {
116
134
  this.finalizer.register(key, tex, key);
117
135
  return tex;
118
136
  }
137
+ /** Whether `key` would gain from {@link buildMipmaps}: only under `'auto'`,
138
+ * and only until they are built. */
139
+ lacksMipmaps(key) {
140
+ return this.minification === "auto" && !this.mipped.has(key);
141
+ }
142
+ /** Build `key`'s mipmaps and filter minification through them from here
143
+ * on. Its texture must be uploaded. Leaves no texture bound. */
144
+ buildMipmaps(key) {
145
+ const tex = this.map.get(key);
146
+ if (!tex || this.mipped.has(key)) return;
147
+ const gl = this.gl;
148
+ gl.bindTexture(gl.TEXTURE_2D, tex);
149
+ gl.generateMipmap(gl.TEXTURE_2D);
150
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
151
+ gl.bindTexture(gl.TEXTURE_2D, null);
152
+ this.mipped.add(key);
153
+ }
119
154
  bind(key, unit) {
120
155
  const tex = this.map.get(key);
121
156
  if (!tex) throw new Error("GLImageCache: image not uploaded; call upload() first");
157
+ this.lastDrawn.set(key, this.frame);
122
158
  const gl = this.gl;
123
159
  gl.activeTexture(gl.TEXTURE0 + unit);
124
160
  gl.bindTexture(gl.TEXTURE_2D, tex);
@@ -146,10 +182,11 @@ function wrapModes(gl, rep) {
146
182
  }
147
183
  var cache = /* @__PURE__ */ new WeakMap();
148
184
  function getMesh(path) {
185
+ const openFill = defaultOpenFill();
149
186
  const cached = cache.get(path);
150
- if (cached !== void 0) return cached;
187
+ if (cached !== void 0 && cached.openFill === openFill) return cached.mesh;
151
188
  const mesh = tessellate(path);
152
- cache.set(path, mesh);
189
+ cache.set(path, { mesh, openFill });
153
190
  return mesh;
154
191
  }
155
192
  function _resetCacheForTests() {
@@ -298,11 +335,9 @@ function insetIn(length, head, ribbon) {
298
335
  function _resetStrokeMeshCacheForTests() {
299
336
  cache2 = /* @__PURE__ */ new WeakMap();
300
337
  }
301
-
302
- // src/fillInPoseFrame.ts
303
338
  function fillInPoseFrame(fill, box) {
304
339
  if (!isBuiltinKind(fill)) {
305
- return getPaintDrawKind(fill.fill)?.inPoseFrame?.(fill, box) ?? fill;
340
+ return getPaintDrawKind(fill.kind)?.inPoseFrame?.(fill, box) ?? fill;
306
341
  }
307
342
  if (isBoundsPattern(fill)) {
308
343
  const o = fill.origin ?? { x: 0, y: 0 };
@@ -313,10 +348,10 @@ function fillInPoseFrame(fill, box) {
313
348
  x: box.x + p.x * box.width,
314
349
  y: box.y + p.y * box.height
315
350
  });
316
- if (fill.fill === "linear-gradient") {
351
+ if (fill.kind === "linear-gradient") {
317
352
  return { ...fill, from: toBox(fill.from), to: toBox(fill.to), units: "local" };
318
353
  }
319
- if (fill.fill === "radial-gradient") {
354
+ if (fill.kind === "radial-gradient") {
320
355
  const scale = Math.hypot(box.width, box.height) / Math.SQRT2;
321
356
  return { ...fill, center: toBox(fill.center), radius: fill.radius * scale, units: "local" };
322
357
  }
@@ -324,45 +359,43 @@ function fillInPoseFrame(fill, box) {
324
359
  }
325
360
  function fillToBoundsFrame(fill, box) {
326
361
  if (!isBuiltinKind(fill)) {
327
- return getPaintDrawKind(fill.fill)?.toBoundsFrame?.(fill, box) ?? fill;
362
+ return getPaintDrawKind(fill.kind)?.toBoundsFrame?.(fill, box) ?? fill;
328
363
  }
329
- if (fill.fill === "pattern") {
364
+ if (isPatternPaint(fill)) {
330
365
  const o = fill.origin ?? { x: 0, y: 0 };
331
366
  return { ...fill, origin: { x: o.x - box.x, y: o.y - box.y }, units: "bounds" };
332
367
  }
333
- const isGradient = fill.fill === "linear-gradient" || fill.fill === "radial-gradient" || fill.fill === "conic-gradient";
368
+ const isGradient = fill.kind === "linear-gradient" || fill.kind === "radial-gradient" || fill.kind === "conic-gradient";
334
369
  if (!isGradient) return fill;
335
370
  if (box.width === 0 || box.height === 0) return fill;
336
371
  const fromBox = (p) => ({
337
372
  x: (p.x - box.x) / box.width,
338
373
  y: (p.y - box.y) / box.height
339
374
  });
340
- if (fill.fill === "linear-gradient") {
375
+ if (fill.kind === "linear-gradient") {
341
376
  return { ...fill, from: fromBox(fill.from), to: fromBox(fill.to), units: "bounds" };
342
377
  }
343
- if (fill.fill === "radial-gradient") {
378
+ if (fill.kind === "radial-gradient") {
344
379
  const scale = Math.hypot(box.width, box.height) / Math.SQRT2;
345
380
  return { ...fill, center: fromBox(fill.center), radius: fill.radius / scale, units: "bounds" };
346
381
  }
347
382
  return { ...fill, center: fromBox(fill.center), units: "bounds" };
348
383
  }
349
384
  function isBoundsPattern(fill) {
350
- return fill.fill === "pattern" && fill.units === "bounds";
385
+ return isPatternPaint(fill) && fill.units === "bounds";
351
386
  }
352
387
  function isBoundsGradient(fill) {
353
388
  if (fill === void 0) return false;
354
- const kind = fill.fill;
389
+ const kind = fill.kind;
355
390
  if (kind !== "linear-gradient" && kind !== "radial-gradient" && kind !== "conic-gradient") {
356
391
  return false;
357
392
  }
358
393
  return fill.units === "bounds";
359
394
  }
360
395
  function isBuiltinKind(fill) {
361
- const kind = fill.fill ?? "solid";
362
- return kind === "solid" || kind === "pattern" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient";
396
+ const kind = fill.kind ?? "solid";
397
+ return kind === "solid" || isPatternPaint(fill) || kind === "linear-gradient" || kind === "radial-gradient" || kind === "conic-gradient";
363
398
  }
364
-
365
- // src/patterns/resolveSpec.ts
366
399
  var PATTERN_TILE_CACHE_LIMIT = 256;
367
400
  var ID_PREFIX = "pattern:";
368
401
  var cache3 = /* @__PURE__ */ new Map();
@@ -376,13 +409,14 @@ function _resetPatternSpecCacheForTests() {
376
409
  cache3.clear();
377
410
  }
378
411
  function resolvePatternSpec(spec) {
379
- const id = specId(spec);
412
+ const tile = tileSpecOf(spec);
413
+ const id = specId(tile);
380
414
  const hit = cache3.get(id);
381
415
  if (hit !== void 0) {
382
416
  markUsed(id);
383
417
  return hit;
384
418
  }
385
- return build(id, spec);
419
+ return build(id, tile);
386
420
  }
387
421
  function markUsed(id) {
388
422
  const hit = cache3.get(id);
@@ -391,7 +425,7 @@ function markUsed(id) {
391
425
  cache3.set(id, hit);
392
426
  }
393
427
  function build(id, spec) {
394
- const bitmap = TILES.has(spec.tile) ? rasterizeTile({
428
+ const bitmap = isTileKind(spec.kind) ? rasterizeTile({
395
429
  size: tileSize(spec),
396
430
  draw: (oc) => drawTileShapes(oc, tileGeometry(spec))
397
431
  }) : null;
@@ -401,12 +435,6 @@ function build(id, spec) {
401
435
  while (cache3.size > PATTERN_TILE_CACHE_LIMIT) evict(cache3.keys().next().value);
402
436
  return handle;
403
437
  }
404
- var TILES = /* @__PURE__ */ new Set([
405
- "hatch",
406
- "crosshatch",
407
- "dots",
408
- "chunks"
409
- ]);
410
438
  function evict(id) {
411
439
  cache3.delete(id);
412
440
  const source = textureRegistry.get(id)?.source;
@@ -420,27 +448,29 @@ function revive(id) {
420
448
  } catch {
421
449
  return;
422
450
  }
423
- if (typeof spec !== "object" || spec === null || !("tile" in spec)) return;
451
+ if (typeof spec !== "object" || spec === null || !("kind" in spec)) return;
424
452
  build(id, spec);
425
453
  }
426
454
  ownTextureIds(ID_PREFIX, { revive, touched: markUsed });
427
- function isPatternSpec(payload) {
428
- return "tile" in payload;
429
- }
430
455
  function resolveFillPattern(fill) {
431
- if (fill.fill !== "pattern") return fill;
432
- if (!isPatternSpec(fill.pattern)) return fill;
433
- const handle = resolvePatternSpec(fill.pattern);
434
- return handle ? { ...fill, pattern: handle } : null;
456
+ if (!isTilePaint(fill)) return fill;
457
+ const texture = resolvePatternSpec(fill);
458
+ return texture ? { kind: "texture", texture, ...patternPlacementOf(fill) } : null;
435
459
  }
436
460
 
437
461
  // src/patterns/commandPaint.ts
462
+ function commandPaint(paint, box) {
463
+ return resolveFillPattern(fillInPoseFrame(paint, box));
464
+ }
438
465
  function commandFill(fill, box) {
439
- return resolveFillPattern(fillInPoseFrame(fill, box));
466
+ if (fill.paint == null) return null;
467
+ const paint = commandPaint(fill.paint, box);
468
+ if (paint === null) return null;
469
+ return paint === fill.paint ? fill : { ...fill, paint };
440
470
  }
441
471
  function commandStroke(stroke, box) {
442
472
  if (stroke.paint == null) return null;
443
- const paint = commandFill(stroke.paint, box);
473
+ const paint = commandPaint(stroke.paint, box);
444
474
  if (paint === null) return null;
445
475
  return paint === stroke.paint ? stroke : { ...stroke, paint };
446
476
  }
@@ -448,7 +478,7 @@ function resolvePaint(p, stroke, fallback, head) {
448
478
  const v = p ?? fallback;
449
479
  if (v === "none") return void 0;
450
480
  if (v === "line") return stroke.paint;
451
- return commandFill(v, boundsOfPath(head)) ?? void 0;
481
+ return commandPaint(v, boundsOfPath(head)) ?? void 0;
452
482
  }
453
483
  function headCommands(heads, stroke) {
454
484
  const out = [];
@@ -460,7 +490,7 @@ function headCommands(heads, stroke) {
460
490
  out.push({
461
491
  kind: "path",
462
492
  path,
463
- ...fill ? { fill } : {},
493
+ ...fill ? { fill: { paint: fill } } : {},
464
494
  ...outline && outline.paint ? { stroke: { paint: outline.paint, width: outline.width, cap: "round", join: "round" } } : {}
465
495
  });
466
496
  }
@@ -477,14 +507,14 @@ function readsKey(stroke) {
477
507
  key += "/";
478
508
  continue;
479
509
  }
480
- const reads = markerReadsKey(entry, stroke);
481
- if (reads === null) return null;
482
- key += `${reads}/`;
510
+ const reads2 = markerReadsKey(entry, stroke);
511
+ if (reads2 === null) return null;
512
+ key += `${reads2}/`;
483
513
  }
484
514
  return key;
485
515
  }
486
- function mapFor(path, source, reads) {
487
- if (reads !== null) {
516
+ function mapFor(path, source, reads2) {
517
+ if (reads2 !== null) {
488
518
  let map2 = byValue.get(path);
489
519
  if (map2 === void 0) byValue.set(path, map2 = /* @__PURE__ */ new Map());
490
520
  return map2;
@@ -499,8 +529,8 @@ function cachedMarkerCommands(path, source, stroke, strokeWidth, flattenToleranc
499
529
  if (stroke.markerStart === void 0 && stroke.markerMid === void 0 && stroke.markerEnd === void 0) {
500
530
  return [];
501
531
  }
502
- const reads = readsKey(stroke);
503
- const map = mapFor(path, source, reads);
532
+ const reads2 = readsKey(stroke);
533
+ const map = mapFor(path, source, reads2);
504
534
  const key = [
505
535
  strokeWidth,
506
536
  flattenTolerance ?? "",
@@ -508,7 +538,7 @@ function cachedMarkerCommands(path, source, stroke, strokeWidth, flattenToleranc
508
538
  refKey(stroke.markerStart),
509
539
  refKey(stroke.markerMid),
510
540
  refKey(stroke.markerEnd),
511
- reads ?? "",
541
+ reads2 ?? "",
512
542
  screenSpaceKey(spaces.start),
513
543
  screenSpaceKey(spaces.mid),
514
544
  screenSpaceKey(spaces.end)
@@ -575,11 +605,24 @@ function outlineStrokeMesh(glyphKey, d, emWidth, stroke, metric) {
575
605
  function _resetOutlineStrokeMeshCacheForTests() {
576
606
  cache5 = /* @__PURE__ */ new Map();
577
607
  }
608
+
609
+ // src/shaders/straightRgb.ts
610
+ var STRAIGHT_RGB_GLSL = (
611
+ /* glsl */
612
+ `
613
+ vec3 straightRgb(vec4 premultiplied) {
614
+ return premultiplied.rgb / max(premultiplied.a, 0.0001);
615
+ }
616
+ `
617
+ );
618
+
619
+ // src/shaders/batchFill.ts
578
620
  var BATCH_TEXTURE_SLOTS = 8;
579
621
  var WHITE_SLOT = 0;
580
622
  var PAINT_MODE_PLAIN = 0;
581
623
  var PAINT_MODE_RADIAL = 3;
582
624
  var PAINT_MODE_CONIC = 4;
625
+ var PAINT_MODE_IMAGE = 5;
583
626
  var INV_TAU = (1 / (2 * Math.PI)).toFixed(10);
584
627
  function packSlot(slot, mode) {
585
628
  return slot + BATCH_TEXTURE_SLOTS * mode;
@@ -651,8 +694,9 @@ ${sampleChain(BATCH_TEXTURE_SLOTS)}
651
694
  return texture(u_samplers[0], uv);
652
695
  }
653
696
  ${GLYPH_COVERAGE_GLSL}
697
+ ${STRAIGHT_RGB_GLSL}
654
698
  void main() {
655
- float isGrad = step(2.5, v_paintMode);
699
+ float isGrad = step(2.5, v_paintMode) * step(v_paintMode, ${PAINT_MODE_CONIC + 0.5});
656
700
  // A radial or conic vertex carries a gradient-space coordinate rather than a
657
701
  // texture coordinate, and its atlas row rather than an alpha. The sample is
658
702
  // split across the branch rather than the coordinate selected before it: an
@@ -674,6 +718,8 @@ void main() {
674
718
  } else {
675
719
  texel = sampleSlot(v_texSlot, v_uv);
676
720
  }
721
+ // At 0 the mix is the texel itself, so a solid still gets its color exactly.
722
+ texel.rgb = mix(texel.rgb, straightRgb(texel), step(${PAINT_MODE_IMAGE - 0.5}, v_paintMode));
677
723
  float isGlyph = step(0.5, v_paintMode) * step(v_paintMode, 2.5);
678
724
  // Unconditional, and multiplied out afterwards rather than branched around:
679
725
  // see the file header.
@@ -821,8 +867,7 @@ var DrawBatch = class {
821
867
  /** Whether the staged run is quads alone, so its indices are the canonical
822
868
  * pattern and a slot already holding that pattern needs no upload. */
823
869
  pureRects = true;
824
- /** The rect every push is cut to, or `null`. `draw.ts` sets it as rect
825
- * clips enter and leave — see `batchClip.ts`. A push returns `false`, having
870
+ /** The rect every push is cut to, or `null`. A push returns `false`, having
826
871
  * staged nothing, for geometry it cannot cut exactly; see `clipQuad`. */
827
872
  clip = null;
828
873
  /** Polygon scratch for `clip`: a quad cut by four edges has at most eight
@@ -857,8 +902,7 @@ var DrawBatch = class {
857
902
  }
858
903
  /**
859
904
  * Append one rect's four corners through `m`, all carrying `rgba` (straight
860
- * alpha). An affine maps a rect to a parallelogram, so two triangles still
861
- * cover it and the batch draws at `u_model` identity.
905
+ * alpha).
862
906
  */
863
907
  pushRect(x, y, w, h, m, r, g, b, a) {
864
908
  this.reserve(4, 6);
@@ -881,17 +925,14 @@ var DrawBatch = class {
881
925
  /**
882
926
  * Append one image quad: the destination rect `(x, y, w, h)` mapped through
883
927
  * `m`, sampling `(u0, v0)`-`(u1, v1)`, every corner carrying `post` as the
884
- * after-the-color-matrix alpha factor.
885
- *
886
- * Corners wind top-left, top-right, bottom-right, bottom-left with UVs
887
- * following — the same winding `pushRect` uses, which is what lets a run of
888
- * mixed rects and quads keep the canonical index pattern. The flips a command
889
- * asks for are already in the `u` / `v` the caller passes.
928
+ * after-the-color-matrix alpha factor and `rgb` as the scale on its texel.
929
+ * `slot` is the texture unit the corners sample.
890
930
  *
891
- * `slot` is the texture unit the corners sample, which `draw.ts` assigns per
892
- * bitmap within the run.
931
+ * Corners wind top-left, top-right, bottom-right, bottom-left — the same
932
+ * winding `pushRect` uses, which is what lets a run of mixed rects and quads
933
+ * keep the canonical index pattern.
893
934
  */
894
- pushQuad(x, y, w, h, m, u0, v0, u1, v1, post, slot) {
935
+ pushQuad(x, y, w, h, m, u0, v0, u1, v1, post, slot, r = 1, g = 1, b = 1) {
895
936
  this.reserve(4, 6);
896
937
  let i = this.nVerts * FLOATS_PER_VERTEX;
897
938
  const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
@@ -901,11 +942,11 @@ var DrawBatch = class {
901
942
  const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
902
943
  const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
903
944
  const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
904
- const P = packSlot(slot, PAINT_MODE_PLAIN);
905
- i = this.writeVertex(i, ax, ay, 1, 1, 1, 1, u0, v0, post, P);
906
- i = this.writeVertex(i, bx, by, 1, 1, 1, 1, u1, v0, post, P);
907
- i = this.writeVertex(i, cx, cy, 1, 1, 1, 1, u1, v1, post, P);
908
- this.writeVertex(i, dx, dy, 1, 1, 1, 1, u0, v1, post, P);
945
+ const P = packSlot(slot, PAINT_MODE_IMAGE);
946
+ i = this.writeVertex(i, ax, ay, r, g, b, 1, u0, v0, post, P);
947
+ i = this.writeVertex(i, bx, by, r, g, b, 1, u1, v0, post, P);
948
+ i = this.writeVertex(i, cx, cy, r, g, b, 1, u1, v1, post, P);
949
+ this.writeVertex(i, dx, dy, r, g, b, 1, u0, v1, post, P);
909
950
  this.pushQuadIndices();
910
951
  return this.clip === null || this.clipQuad(this.nVerts - 4, this.nIdx - 6, this.clip);
911
952
  }
@@ -915,20 +956,11 @@ var DrawBatch = class {
915
956
  * `rgba`.
916
957
  *
917
958
  * `mode` says which channels of that atlas carry the distance field, and
918
- * rides the vertices packed into the slot — so a run off a baked MSDF atlas
919
- * and one off the runtime canvas bake still share a draw. The synthetic-bold
920
- * threshold does not: it is `u_synthBold`, and `draw.ts` breaks the run when
921
- * it changes.
922
- *
923
- * **A synthetic oblique is sheared here rather than in the shader.** The old
924
- * text program carried the baseline per vertex and skewed against
925
- * `u_synthItalic`; the batch places its own corners, so the shear is one
926
- * multiply while they are being placed, and `tanItalic` is 0 for an upright
927
- * face. It has to happen before `m`, which is where the shader had it too.
959
+ * rides the vertices packed into the slot. The synthetic-bold threshold does
960
+ * not: it is `u_synthBold`, and the caller breaks the run when it changes.
928
961
  *
929
- * Corners wind top-left, top-right, bottom-right, bottom-left — `pushQuad`'s
930
- * winding, which is what lets a run of mixed rects, image quads and glyphs
931
- * keep the canonical index pattern.
962
+ * A synthetic oblique is sheared here, before `m`; `tanItalic` is 0 for an
963
+ * upright face. Corners wind as `pushQuad`'s do.
932
964
  */
933
965
  pushGlyph(x0, y0, x1, y1, baselineY, tanItalic, m, u0, v0, u1, v1, r, g, b, a, slot, mode) {
934
966
  this.reserve(4, 6);
@@ -955,16 +987,10 @@ var DrawBatch = class {
955
987
  * through `m`, sampling the ramp atlas at `slot`.
956
988
  *
957
989
  * `uv` gives each channel of `a_uv` as an affine function of the coordinates
958
- * the corners arrive in, which is what lets a gradient ride the vertices at
959
- * all: a linear one's ramp position is affine in position outright, and a
960
- * radial or conic one's gradient-space *coordinate* is, even though the ramp
961
- * position it yields is not. Either way the rasterizer's interpolation across
962
- * a triangle is exact. `mode` says which of the two the fragment shader is
963
- * looking at, and `post` carries the atlas row for the modes that read it —
964
- * see `shaders/batchFill.ts`.
965
- *
966
- * Values outside 0..1 are the sampler's business; the atlas clamps to the
967
- * edge texel, which is what the gradient shader's own `clamp` did.
990
+ * the corners arrive in. `mode` says whether that is a ramp position or a
991
+ * gradient-space coordinate, and `post` carries the atlas row for the modes
992
+ * that read it — see `shaders/batchFill.ts`. Values outside 0..1 clamp to
993
+ * the atlas's edge texel.
968
994
  */
969
995
  pushGradientRect(x, y, w, h, m, uv, post, slot, mode, r, g, b, a) {
970
996
  this.reserve(4, 6);
@@ -1121,9 +1147,7 @@ var DrawBatch = class {
1121
1147
  * subpixel grid the rasterizer snaps to, and the shortened edge then covers
1122
1148
  * different samples than the whole one did; along an axis the cut edge is
1123
1149
  * the same line exactly. So a slanted quad that crosses is rolled back and
1124
- * refused, for `draw.ts` to clip through the stencil instead.
1125
- *
1126
- * A cut axis-aligned quad is still four corners, so the run stays pure.
1150
+ * refused, for the caller to clip through the stencil instead.
1127
1151
  */
1128
1152
  clipQuad(v0, i0, rect) {
1129
1153
  const v = this.verts;
@@ -1276,14 +1300,16 @@ var DrawBatch = class {
1276
1300
  gl.deleteVertexArray(set.vao);
1277
1301
  }
1278
1302
  };
1279
-
1280
- // src/images/imageCache.ts
1281
1303
  var MAX_VECTOR_RASTER_PIXELS = 4096 * 4096;
1282
1304
  var SHRINK_SLACK = 4;
1283
1305
  var FALLBACK_WIDTH = 300;
1284
1306
  var FALLBACK_HEIGHT = 150;
1307
+ var DEFAULT_BUDGET_BYTES = 256 * 1024 * 1024;
1285
1308
  var cache6 = /* @__PURE__ */ new Map();
1286
- var subscribers = /* @__PURE__ */ new Set();
1309
+ var budgetBytes = DEFAULT_BUDGET_BYTES;
1310
+ var reads = 0;
1311
+ var readsAtLastPass = 0;
1312
+ var ready = createEmitter();
1287
1313
  var decodedVectors = /* @__PURE__ */ new Map();
1288
1314
  function isVectorImageSrc(src) {
1289
1315
  if (/^data:image\/svg\+xml[;,]/i.test(src)) return true;
@@ -1328,10 +1354,30 @@ var defaultLoader = async (src, size) => {
1328
1354
  };
1329
1355
  var loader = defaultLoader;
1330
1356
  function notify() {
1331
- for (const cb of subscribers) cb();
1357
+ ready.emit();
1358
+ }
1359
+ var bytesOf = (bitmap) => bitmap ? bitmap.width * bitmap.height * 4 : 0;
1360
+ function enforceBudget(landed) {
1361
+ let total = 0;
1362
+ for (const entry of cache6.values()) total += bytesOf(entry.bitmap);
1363
+ if (total > budgetBytes) {
1364
+ const idle = [...cache6].filter(([src, entry]) => src !== landed && entry.bitmap !== void 0 && entry.readAt <= readsAtLastPass && !imageSourceInLatestFrame(entry.bitmap));
1365
+ idle.sort((a, b) => a[1].readAt - b[1].readAt);
1366
+ for (const [src, entry] of idle) {
1367
+ if (total <= budgetBytes) break;
1368
+ total -= bytesOf(entry.bitmap);
1369
+ releaseImage(src);
1370
+ }
1371
+ }
1372
+ readsAtLastPass = reads;
1373
+ }
1374
+ function setImageCacheBudget(bytes) {
1375
+ const previous = budgetBytes;
1376
+ budgetBytes = bytes;
1377
+ return previous;
1332
1378
  }
1333
1379
  function startLoad2(src) {
1334
- const entry = { status: "loading" };
1380
+ const entry = { status: "loading", readAt: ++reads };
1335
1381
  cache6.set(src, entry);
1336
1382
  loader(src).then(
1337
1383
  (bitmap) => {
@@ -1339,13 +1385,15 @@ function startLoad2(src) {
1339
1385
  cache6.set(src, {
1340
1386
  status: "ready",
1341
1387
  bitmap,
1388
+ readAt: entry.readAt,
1342
1389
  ...isVectorImageSrc(src) ? { vector: { natural: { width: bitmap.width, height: bitmap.height }, scale: 1, failed: /* @__PURE__ */ new Set(), floor: 0 } } : {}
1343
1390
  });
1391
+ enforceBudget(src);
1344
1392
  notify();
1345
1393
  },
1346
1394
  () => {
1347
1395
  if (cache6.get(src) !== entry) return;
1348
- cache6.set(src, { status: "error" });
1396
+ cache6.set(src, { status: "error", readAt: entry.readAt });
1349
1397
  notify();
1350
1398
  }
1351
1399
  );
@@ -1384,6 +1432,7 @@ function maybeReraster(src, entry, v, drawnAt) {
1384
1432
  v.shrunkTo = grow ? void 0 : target;
1385
1433
  v.scale = target;
1386
1434
  if (old && old !== bitmap) releaseImageSource(old);
1435
+ enforceBudget(src);
1387
1436
  notify();
1388
1437
  },
1389
1438
  () => {
@@ -1399,6 +1448,7 @@ function getImageBitmap(src, drawnAt) {
1399
1448
  startLoad2(src);
1400
1449
  return void 0;
1401
1450
  }
1451
+ entry.readAt = ++reads;
1402
1452
  if (drawnAt && entry.vector) maybeReraster(src, entry, entry.vector, drawnAt);
1403
1453
  return entry.bitmap;
1404
1454
  }
@@ -1417,21 +1467,21 @@ function retryImage(src) {
1417
1467
  notify();
1418
1468
  }
1419
1469
  function subscribeImageReady(cb) {
1420
- subscribers.add(cb);
1421
- return () => {
1422
- subscribers.delete(cb);
1423
- };
1470
+ return ready.subscribe(cb);
1424
1471
  }
1425
1472
  function __setImageLoaderForTests(fn) {
1426
1473
  loader = fn;
1427
1474
  }
1428
1475
  function _resetImageCacheForTests() {
1429
1476
  cache6.clear();
1430
- subscribers.clear();
1477
+ ready = createEmitter();
1431
1478
  decodedVectors.clear();
1432
1479
  loader = defaultLoader;
1480
+ budgetBytes = DEFAULT_BUDGET_BYTES;
1481
+ reads = 0;
1482
+ readsAtLastPass = 0;
1433
1483
  }
1434
1484
 
1435
- export { BATCH_ATTRIBUTE_LOCATIONS, BATCH_FILL_ATTRIBUTES, BATCH_FILL_UNIFORMS, BATCH_FRAG_SRC, BATCH_TEXTURE_SLOTS, BATCH_VERT_SRC, DrawBatch, FLOATS_PER_VERTEX, GLImageCache, PAINT_MODE_CONIC, PAINT_MODE_PLAIN, PAINT_MODE_RADIAL, __setImageLoaderForTests, _resetCacheForTests, _resetImageCacheForTests, _resetMarkerCommandCacheForTests, _resetOutlineMeshCacheForTests, _resetOutlineStrokeMeshCacheForTests, _resetPaintDrawKindsForTests, _resetPatternSpecCacheForTests, _resetProgramRegistryForTests, _resetStrokeMeshCacheForTests, axisAlignedClipRect, cachedMarkerCommands, commandFill, commandStroke, fillInPoseFrame, fillToBoundsFrame, getImageBitmap, getMesh, getPaintDrawKind, getProgramSource, imageStatus, intersectClipRects, isPaintKindKnown, isPatternSpec, isVectorImageSrc, outlineMesh, outlineStrokeMesh, paintDrawKindRegistry, programSourceRegistry, quantizeEmWidth, quantizeStrokeScale, registerPaintDrawKind, registerPaintDrawKindLoader, registerProgram, releaseImage, releaseImageSource, resolveFillPattern, resolvePatternSpec, retryImage, strokeMesh, subscribeImageReady, warmPaintKinds };
1436
- //# sourceMappingURL=chunk-VHQJ56XQ.js.map
1437
- //# sourceMappingURL=chunk-VHQJ56XQ.js.map
1485
+ export { BATCH_ATTRIBUTE_LOCATIONS, BATCH_FILL_ATTRIBUTES, BATCH_FILL_UNIFORMS, BATCH_FRAG_SRC, BATCH_TEXTURE_SLOTS, BATCH_VERT_SRC, DrawBatch, FLOATS_PER_VERTEX, GLImageCache, PAINT_MODE_CONIC, PAINT_MODE_PLAIN, PAINT_MODE_RADIAL, STRAIGHT_RGB_GLSL, __setImageLoaderForTests, _resetCacheForTests, _resetImageCacheForTests, _resetMarkerCommandCacheForTests, _resetOutlineMeshCacheForTests, _resetOutlineStrokeMeshCacheForTests, _resetPaintDrawKindsForTests, _resetPatternSpecCacheForTests, _resetProgramRegistryForTests, _resetStrokeMeshCacheForTests, axisAlignedClipRect, cachedMarkerCommands, commandFill, commandPaint, commandStroke, fillInPoseFrame, fillToBoundsFrame, getImageBitmap, getMesh, getPaintDrawKind, getProgramSource, imageStatus, intersectClipRects, isPaintKindKnown, isVectorImageSrc, outlineMesh, outlineStrokeMesh, paintDrawKindRegistry, programSourceRegistry, quantizeEmWidth, quantizeStrokeScale, registerPaintDrawKind, registerPaintDrawKindLoader, registerProgram, releaseImage, releaseImageSource, resolveFillPattern, resolvePatternSpec, retryImage, setImageCacheBudget, strokeMesh, subscribeImageReady, uploadPremultiplied, warmPaintKinds };
1486
+ //# sourceMappingURL=chunk-LFD47OV2.js.map
1487
+ //# sourceMappingURL=chunk-LFD47OV2.js.map