@weasel-js/core 1.4.3 → 1.4.4

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +367 -0
  2. package/dist/{pointSnapToGrid-D1bRdq-j.d.ts → autoPoseDescriptor-DF1SnnSx.d.ts} +18 -3
  3. package/dist/{builtins-CtGORLCG.d.ts → builtins-BXFBXegF.d.ts} +200 -100
  4. package/dist/{chunk-ORZNKVXM.js → chunk-2VXGHUVL.js} +3 -3
  5. package/dist/{chunk-ORZNKVXM.js.map → chunk-2VXGHUVL.js.map} +1 -1
  6. package/dist/{chunk-LEURURX3.js → chunk-BL65SHCX.js} +17 -5
  7. package/dist/chunk-BL65SHCX.js.map +1 -0
  8. package/dist/{chunk-BLJNRMKB.js → chunk-PRGBGMH3.js} +3 -3
  9. package/dist/{chunk-BLJNRMKB.js.map → chunk-PRGBGMH3.js.map} +1 -1
  10. package/dist/{chunk-WYPSGIYS.js → chunk-R3AWPTLZ.js} +1773 -1209
  11. package/dist/chunk-R3AWPTLZ.js.map +1 -0
  12. package/dist/{chunk-SBFC6J3C.js → chunk-WPM42WJP.js} +3 -3
  13. package/dist/{chunk-SBFC6J3C.js.map → chunk-WPM42WJP.js.map} +1 -1
  14. package/dist/clipboard.d.ts +2 -2
  15. package/dist/clone.d.ts +2 -2
  16. package/dist/{geometry-CHR36Ub_.d.ts → geometry-6fCNhAux.d.ts} +1 -1
  17. package/dist/{grid-Bw8cSde-.d.ts → grid-0Pbn5B2C.d.ts} +1 -14
  18. package/dist/index.d.ts +231 -74
  19. package/dist/index.js +5 -5
  20. package/dist/insert.d.ts +3 -3
  21. package/dist/insert.js +1 -1
  22. package/dist/move.d.ts +4 -4
  23. package/dist/move.js +2 -2
  24. package/dist/{options-DYfUlZxk.d.ts → options-DbYLImvq.d.ts} +1 -1
  25. package/dist/{registry-DnoTWGED.d.ts → registry-BY-wI9gm.d.ts} +4 -4
  26. package/dist/renderer.d.ts +2 -2
  27. package/dist/renderer.js +5 -5
  28. package/dist/resize.d.ts +5 -4
  29. package/dist/resize.js +2 -2
  30. package/dist/routing.d.ts +5 -5
  31. package/dist/routing.js +1 -1
  32. package/dist/{types-CoVTbo_y.d.ts → types-DEALFt5F.d.ts} +1 -6
  33. package/dist/{types-H7o6MaPo.d.ts → types-bcc7jcUy.d.ts} +96 -14
  34. package/dist/{types-BJqsTlXl.d.ts → types-ei3UMl9R.d.ts} +1 -1
  35. package/package.json +9 -9
  36. package/dist/chunk-LEURURX3.js.map +0 -1
  37. package/dist/chunk-WYPSGIYS.js.map +0 -1
@@ -1,13 +1,13 @@
1
- import { ActionDisabledReason, useOptionalDepRegistry, DepRegistryProvider, useActionsRegistry, ActionsScope, ActionsProvider, useDepRegistry, reportRouteConflicts, buildDepsFromRequires, useDepSource, actionBindings } from './chunk-LEURURX3.js';
2
- import { scratchKey, createReparentOp, createTransformOp, snap, getScratch, setScratch } from './chunk-ORZNKVXM.js';
3
- import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-BLJNRMKB.js';
4
- import { AUTO_POSE_DESCRIPTOR, remapRotatedLeaf, PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M, resolveUnit, polylineFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, polygonFromPoints, RECT_POSE_DESCRIPTOR, PathBuilder, pathPoseDescriptor, withinPxRadius, boundsOfPath, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, pointInPath, strokeHitTest, transformPath, translatePath, pxExtent, withinPxBox, flattenCubicWithArcLen, flattenQuadraticWithArcLen, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-SBFC6J3C.js';
1
+ import { ActionDisabledReason, useOptionalDepRegistry, DepRegistryProvider, useActionsRegistry, ActionsScope, ActionsProvider, useDepRegistry, reportRouteConflicts, buildDepsFromRequires, useDepSource, actionBindings } from './chunk-BL65SHCX.js';
2
+ import { scratchKey, createReparentOp, createTransformOp, snap, getScratch, setScratch } from './chunk-2VXGHUVL.js';
3
+ import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-PRGBGMH3.js';
4
+ import { AUTO_POSE_DESCRIPTOR, remapRotatedLeaf, PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M, resolveUnit, polylineFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, polygonFromPoints, RECT_POSE_DESCRIPTOR, PathBuilder, pathPoseDescriptor, withinPxRadius, boundsOfPath, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, pointInPath, strokeHitTest, transformPath, translatePath, pxExtent, withinPxBox, flattenCubicWithArcLen, flattenQuadraticWithArcLen, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-WPM42WJP.js';
5
5
  import { dispatchApplyBatch, dlog, dwarn, usePointerContext, PointerContextProvider, extractWeaselClipboardFromSvg, parseWeaselClipboardText, materializePaste, WEASEL_CLIPBOARD_MIME, WEASEL_CLIPBOARD_MIME_WEB, sniffWeaselClipboardText, applyOpsTo } from './chunk-T3UQ3F6R.js';
6
6
  import { createSetSelectionOp } from './chunk-DOBSZOPR.js';
7
7
  import { registerOpFactory, createInsertOp, createDeleteOp, rebuildOp } from './chunk-4RJP2N2L.js';
8
8
  import { getTexture, chunks, dots, crosshatch, hatch } from './chunk-BJN5ZRIJ.js';
9
9
  import { createContext, createElement, forwardRef, useMemo, useRef, useCallback, useState, useEffect, useImperativeHandle, useSyncExternalStore, useLayoutEffect, useContext, useId, useReducer } from 'react';
10
- import { subscribeGlyphReady, DEFAULT_BAKE_BUDGET, TEXT_VERT_SRC, TEXT_FRAG_SRC, TEXT_SDF_UNIFORMS, TEXT_SDF_ATTRIBUTES, TEXT_FRAG_R8_SRC, markAllFontsNotUploaded, resetBakeBudget, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey } from '@weasel-js/font';
10
+ import { GLYPH_COVERAGE_GLSL, subscribeGlyphReady, DEFAULT_BAKE_BUDGET, markAllFontsNotUploaded, resetBakeBudget, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey, GLYPH_MODE_R8, GLYPH_MODE_MSDF } from '@weasel-js/font';
11
11
  export { canQueryLocalFonts, canQueryLocalFonts as canQueryLocalFonts2, enableLocalFontOutlines, enableLocalFontOutlines as enableLocalFontOutlines2, hasFontOutlines, hasFontOutlines as hasFontOutlines2, isCanvasFont, isCanvasFont as isCanvasFont2, listFontOutlines, listFontOutlines as listFontOutlines2, outlineStatus, outlineStatus as outlineStatus2, registerCanvasFont, registerCanvasFont as registerCanvasFont2, registerFont, registerFont as registerFont2, registerFontOutlines, registerFontOutlines as registerFontOutlines2, subscribeGlyphReady, subscribeGlyphReady as subscribeGlyphReady2, unregisterCanvasFont, unregisterCanvasFont as unregisterCanvasFont2, unregisterFontOutlines, unregisterFontOutlines as unregisterFontOutlines2 } from '@weasel-js/font';
12
12
  import { resolveTextStyle, resolveAlign, resolveRuns, cachedLayoutRuns, verticalAlignOffset, runsToPlainText, toRuns, fontString, textLineBoxes } from '@weasel-js/text';
13
13
  export { DEFAULT_TEXT_STYLE, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, cachedLayoutRuns, createMarkdownRenderer, fontString, layoutMarkdown, layoutRuns, markdownToRuns, measureText, measureTextBounds, measuredWidth, resolveAlign, resolveRuns, resolveTextStyle, runsToMarkdown, runsToPlainText, textLineBoxes, toRuns, verticalAlignOffset } from '@weasel-js/text';
@@ -25,7 +25,7 @@ import { createHistory } from '@weasel-js/history';
25
25
  export * from '@weasel-js/history';
26
26
 
27
27
  // src/version.ts
28
- var VERSION = "1.4.3" ;
28
+ var VERSION = "1.4.4" ;
29
29
 
30
30
  // src/features/grid/roundToCell.ts
31
31
  function roundToCell(value, cellSize) {
@@ -258,63 +258,135 @@ void main() {
258
258
  }
259
259
  `
260
260
  );
261
- var IMAGE_VOPACITY_VERT_SRC = (
261
+ var IMAGE_FILL_UNIFORMS = [
262
+ "u_proj",
263
+ "u_model",
264
+ "u_sampler",
265
+ "u_opacity",
266
+ "u_alpha",
267
+ "u_colorMatrix",
268
+ "u_colorBias"
269
+ ];
270
+ var IMAGE_FILL_ATTRIBUTES = ["a_position", "a_uv"];
271
+ var BATCH_TEXTURE_SLOTS = 8;
272
+ var WHITE_SLOT = 0;
273
+ var PAINT_MODE_PLAIN = 0;
274
+ var PAINT_MODE_RADIAL = 3;
275
+ var PAINT_MODE_CONIC = 4;
276
+ var INV_TAU = (1 / (2 * Math.PI)).toFixed(10);
277
+ function packSlot(slot, mode) {
278
+ return slot + BATCH_TEXTURE_SLOTS * mode;
279
+ }
280
+ function sampleChain(slots) {
281
+ const arms = [];
282
+ for (let i = 1; i < slots; i++) {
283
+ arms.push(` if (slot == ${i}) return texture(u_samplers[${i}], uv);`);
284
+ }
285
+ return arms.join("\n");
286
+ }
287
+ var BATCH_VERT_SRC = (
262
288
  /* glsl */
263
289
  `#version 300 es
264
290
  in vec2 a_position;
291
+ in vec4 a_vertexColor;
265
292
  in vec2 a_uv;
266
- in float a_opacity;
293
+ in float a_post;
294
+ in float a_texSlot;
267
295
  uniform mat3 u_proj;
268
296
  uniform mat3 u_model;
297
+ out vec4 v_vertexColor;
269
298
  out vec2 v_uv;
270
- out float v_opacity;
299
+ out float v_post;
300
+ flat out int v_texSlot;
301
+ flat out float v_paintMode;
271
302
  void main() {
272
303
  vec3 screen = u_model * vec3(a_position, 1.0);
273
- vec3 clip = u_proj * vec3(screen.xy, 1.0);
304
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
274
305
  gl_Position = vec4(clip.xy, 0.0, 1.0);
306
+ v_vertexColor = a_vertexColor;
275
307
  v_uv = a_uv;
276
- v_opacity = a_opacity;
308
+ v_post = a_post;
309
+ // slot + 8 * mode, unpacked \u2014 see the file header for why they share a float.
310
+ int packed = int(a_texSlot + 0.5);
311
+ v_texSlot = packed % ${BATCH_TEXTURE_SLOTS};
312
+ v_paintMode = float(packed / ${BATCH_TEXTURE_SLOTS});
277
313
  }
278
314
  `
279
315
  );
280
- var IMAGE_VOPACITY_FRAG_SRC = (
316
+ var BATCH_FRAG_SRC = (
281
317
  /* glsl */
282
318
  `#version 300 es
283
319
  precision highp float;
320
+ in vec4 v_vertexColor;
284
321
  in vec2 v_uv;
285
- in float v_opacity;
286
- uniform sampler2D u_sampler;
322
+ in float v_post;
323
+ flat in int v_texSlot;
324
+ flat in float v_paintMode;
325
+ uniform sampler2D u_samplers[${BATCH_TEXTURE_SLOTS}];
326
+ uniform vec4 u_color;
287
327
  uniform float u_alpha;
328
+ uniform float u_synthBold;
288
329
  uniform mat4 u_colorMatrix;
289
330
  uniform vec4 u_colorBias;
290
331
  out vec4 outColor;
332
+
333
+ vec4 sampleSlot(int slot, vec2 uv) {
334
+ ${sampleChain(BATCH_TEXTURE_SLOTS)}
335
+ return texture(u_samplers[0], uv);
336
+ }
337
+ ${GLYPH_COVERAGE_GLSL}
291
338
  void main() {
292
- vec4 texel = texture(u_sampler, v_uv);
293
- vec4 mapped = clamp(u_colorMatrix * texel + u_colorBias, 0.0, 1.0);
294
- float a = mapped.a * v_opacity * u_alpha;
339
+ float isGrad = step(2.5, v_paintMode);
340
+ // A radial or conic vertex carries a gradient-space coordinate rather than a
341
+ // texture coordinate, and its atlas row rather than an alpha. The sample is
342
+ // split across the branch rather than the coordinate selected before it: an
343
+ // arm that reads v_uv straight is a fetch the hardware can schedule against a
344
+ // varying, and one reading a coordinate the shader computed is not. Selecting
345
+ // the coordinate first and sampling once costs 65% of a fragment that is not
346
+ // a gradient; splitting it costs 5% (tests/perf/fill-rate.spec.ts).
347
+ //
348
+ // Branching at all is safe here where it would not be around the glyph math:
349
+ // v_paintMode is flat, so every fragment of a quad takes the same arm, and
350
+ // nothing in either arm is a derivative.
351
+ vec4 texel;
352
+ if (isGrad > 0.5) {
353
+ float t = v_paintMode > ${PAINT_MODE_CONIC - 0.5}
354
+ ? fract(atan(v_uv.y, v_uv.x) * ${INV_TAU})
355
+ : length(v_uv);
356
+ texel = sampleSlot(v_texSlot, vec2(t, v_post));
357
+ } else {
358
+ texel = sampleSlot(v_texSlot, v_uv);
359
+ }
360
+ float isGlyph = step(0.5, v_paintMode) * step(v_paintMode, 2.5);
361
+ // Unconditional, and multiplied out afterwards rather than branched around:
362
+ // see the file header.
363
+ float coverage = glyphCoverage(texel, v_paintMode, u_synthBold);
364
+ // A glyph's texel is a distance field, so it takes no part in the color; a
365
+ // plain vertex multiplies its texel in as it always did.
366
+ vec4 src = mix(texel, vec4(1.0), isGlyph) * u_color * v_vertexColor;
367
+ vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);
368
+ float a = mapped.a * u_alpha * mix(v_post, 1.0, isGrad) * mix(1.0, coverage, isGlyph);
295
369
  outColor = vec4(mapped.rgb * a, a);
296
370
  }
297
371
  `
298
372
  );
299
- var IMAGE_FILL_UNIFORMS = [
373
+ var BATCH_FILL_UNIFORMS = [
300
374
  "u_proj",
301
375
  "u_model",
302
- "u_sampler",
303
- "u_opacity",
376
+ "u_samplers",
377
+ "u_color",
304
378
  "u_alpha",
379
+ "u_synthBold",
305
380
  "u_colorMatrix",
306
381
  "u_colorBias"
307
382
  ];
308
- var IMAGE_FILL_ATTRIBUTES = ["a_position", "a_uv"];
309
- var IMAGE_VOPACITY_UNIFORMS = [
310
- "u_proj",
311
- "u_model",
312
- "u_sampler",
313
- "u_alpha",
314
- "u_colorMatrix",
315
- "u_colorBias"
383
+ var BATCH_FILL_ATTRIBUTES = [
384
+ "a_position",
385
+ "a_vertexColor",
386
+ "a_uv",
387
+ "a_post",
388
+ "a_texSlot"
316
389
  ];
317
- var IMAGE_VOPACITY_ATTRIBUTES = ["a_position", "a_uv", "a_opacity"];
318
390
 
319
391
  // src/renderer/shaders/gradFill.ts
320
392
  var GRAD_VERT_SRC = (
@@ -340,7 +412,10 @@ var GRAD_FRAG_SRC = (
340
412
  precision highp float;
341
413
  in vec2 v_world;
342
414
  uniform sampler2D u_ramp;
415
+ uniform float u_rampV;
343
416
  uniform float u_alpha;
417
+ uniform mat4 u_colorMatrix;
418
+ uniform vec4 u_colorBias;
344
419
  uniform float u_opacity;
345
420
  uniform int u_gradKind;
346
421
  uniform vec2 u_gradP0;
@@ -364,9 +439,14 @@ void main() {
364
439
  t = fract(a / (2.0 * PI));
365
440
  }
366
441
  t = clamp(t, 0.0, 1.0);
367
- vec4 rampColor = texture(u_ramp, vec2(t, 0.5));
368
- float a = rampColor.a * u_opacity * u_alpha;
369
- outColor = vec4(rampColor.rgb * a, a);
442
+ vec4 rampColor = texture(u_ramp, vec2(t, u_rampV));
443
+ // Fill opacity into the alpha channel before the matrix, which is where the
444
+ // batch program's vertex alpha sits \u2014 the two have to agree, since a linear
445
+ // gradient goes through that one and a radial through this.
446
+ vec4 src = vec4(rampColor.rgb, rampColor.a * u_opacity);
447
+ vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);
448
+ float a = mapped.a * u_alpha;
449
+ outColor = vec4(mapped.rgb * a, a);
370
450
  }
371
451
  `
372
452
  );
@@ -375,8 +455,11 @@ var GRAD_FILL_UNIFORMS = [
375
455
  "u_model",
376
456
  "u_worldInv",
377
457
  "u_ramp",
458
+ "u_rampV",
378
459
  "u_alpha",
379
460
  "u_opacity",
461
+ "u_colorMatrix",
462
+ "u_colorBias",
380
463
  "u_gradKind",
381
464
  "u_gradP0",
382
465
  "u_gradDir",
@@ -684,6 +767,23 @@ var GLImageCache = class {
684
767
  gl;
685
768
  minification;
686
769
  map = /* @__PURE__ */ new WeakMap();
770
+ /**
771
+ * MAG_FILTER each texture currently carries, so a redundant write can be
772
+ * skipped.
773
+ *
774
+ * The batch sets this per flush — the same bitmap can be drawn at both
775
+ * filters in one frame, and the value has to be right at the draw rather than
776
+ * at upload. Filtering is state on the *texture object*, though, not on the
777
+ * unit, so re-asserting a value it already has is a write to a live texture
778
+ * for no reason. On a large sheet that is not free: a consumer's wall samples
779
+ * a 5652px-square atlas, 122MB resident, and measured `sampling: 'nearest'`
780
+ * costing up to 8x `'linear'` there — where the linear pass re-asserts the
781
+ * upload default and the nearest pass changes state on every draw.
782
+ *
783
+ * Whether that is the cause is unproven (see `docs/TODO.md`), but the write
784
+ * was redundant either way.
785
+ */
786
+ magFilters = /* @__PURE__ */ new WeakMap();
687
787
  upload(key, source, repetition) {
688
788
  const existing = this.map.get(key);
689
789
  if (existing) return existing;
@@ -698,6 +798,7 @@ var GLImageCache = class {
698
798
  this.minification === "mipmap" ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR
699
799
  );
700
800
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
801
+ this.magFilters.set(key, gl.LINEAR);
701
802
  const [wrapS, wrapT] = wrapModes(gl, repetition);
702
803
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);
703
804
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);
@@ -713,6 +814,14 @@ var GLImageCache = class {
713
814
  gl.activeTexture(gl.TEXTURE0 + unit2);
714
815
  gl.bindTexture(gl.TEXTURE_2D, tex);
715
816
  }
817
+ /** Set `key`'s MAG_FILTER, skipping the call when it already holds that
818
+ * value. Its texture must be bound to the active unit — callers pair this
819
+ * with `bind`. */
820
+ setMagFilter(key, filter) {
821
+ if (this.magFilters.get(key) === filter) return;
822
+ this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, filter);
823
+ this.magFilters.set(key, filter);
824
+ }
716
825
  };
717
826
  function wrapModes(gl, rep) {
718
827
  switch (rep) {
@@ -1194,14 +1303,16 @@ function gradientForBounds(kind, bounds, stops, units) {
1194
1303
  return { fill: "conic-gradient", center, angle: 0, stops, units };
1195
1304
  }
1196
1305
 
1197
- // src/renderer/cache/GradientRampCache.ts
1198
- var RAMP_SIZE = 256;
1306
+ // src/renderer/cache/GradientRampAtlas.ts
1307
+ var RAMP_WIDTH = 256;
1308
+ var INITIAL_ROWS = 16;
1309
+ var RAMP_ATLAS_MAX_ROWS = 1024;
1199
1310
  function buildGradientRamp(stops) {
1200
- const data = new Uint8ClampedArray(RAMP_SIZE * 4);
1311
+ const data = new Uint8ClampedArray(RAMP_WIDTH * 4);
1201
1312
  if (stops.length === 0) return data;
1202
1313
  const resolved = resolveGradientStops(stops);
1203
- for (let i = 0; i < RAMP_SIZE; i++) {
1204
- const [r, g, b, a] = sampleResolvedStops(resolved, i / (RAMP_SIZE - 1));
1314
+ for (let i = 0; i < RAMP_WIDTH; i++) {
1315
+ const [r, g, b, a] = sampleResolvedStops(resolved, i / (RAMP_WIDTH - 1));
1205
1316
  data[i * 4] = r * 255;
1206
1317
  data[i * 4 + 1] = g * 255;
1207
1318
  data[i * 4 + 2] = b * 255;
@@ -1209,51 +1320,76 @@ function buildGradientRamp(stops) {
1209
1320
  }
1210
1321
  return data;
1211
1322
  }
1212
- var GradientRampCache = class {
1323
+ var GradientRampAtlas = class {
1213
1324
  constructor(gl) {
1214
1325
  this.gl = gl;
1215
1326
  }
1216
1327
  gl;
1217
- map = /* @__PURE__ */ new Map();
1328
+ /** Row per stop list, in least-recently-used order: a hit re-inserts, so the
1329
+ * first entry is the row a full atlas recycles. */
1330
+ rowByKey = /* @__PURE__ */ new Map();
1331
+ texture = null;
1332
+ rows = 0;
1333
+ /** Mirror of the texture, so growth can respecify it without re-baking every
1334
+ * ramp it already holds. */
1335
+ pixels = new Uint8ClampedArray(0);
1218
1336
  totalQueries = 0;
1219
1337
  cacheHits = 0;
1338
+ /** Rows the atlas currently holds — its texture height. */
1339
+ get height() {
1340
+ return this.rows;
1341
+ }
1342
+ /**
1343
+ * Bake `stops` into a row and return it, reusing the row an identical stop
1344
+ * list already holds.
1345
+ *
1346
+ * **A returned row outlives the frame only while the atlas has room.** Past
1347
+ * `RAMP_ATLAS_MAX_ROWS` an upload recycles the least recently used row, which
1348
+ * rewrites texels a row handed out earlier still names. Nothing that defers
1349
+ * its draw past this call may hold a row across another `upload` without
1350
+ * arranging to be flushed first.
1351
+ */
1220
1352
  upload(stops) {
1221
1353
  const key = JSON.stringify(stops);
1222
1354
  this.totalQueries++;
1223
- if (this.map.has(key)) {
1355
+ const existing = this.rowByKey.get(key);
1356
+ if (existing !== void 0) {
1224
1357
  this.cacheHits++;
1225
- return key;
1358
+ this.rowByKey.delete(key);
1359
+ this.rowByKey.set(key, existing);
1360
+ return existing;
1226
1361
  }
1227
- const ramp = buildGradientRamp(stops);
1228
- const gl = this.gl;
1229
- const tex = gl.createTexture();
1230
- if (!tex) throw new Error("GradientRampCache: createTexture failed");
1231
- gl.bindTexture(gl.TEXTURE_2D, tex);
1232
- gl.texImage2D(
1233
- gl.TEXTURE_2D,
1234
- 0,
1235
- gl.RGBA,
1236
- RAMP_SIZE,
1237
- 1,
1238
- 0,
1239
- gl.RGBA,
1240
- gl.UNSIGNED_BYTE,
1241
- ramp
1242
- );
1243
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1244
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1245
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1246
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1247
- gl.bindTexture(gl.TEXTURE_2D, null);
1248
- this.map.set(key, tex);
1249
- return key;
1362
+ const row = this.claimRow();
1363
+ this.writeRow(row, buildGradientRamp(stops));
1364
+ this.rowByKey.set(key, row);
1365
+ return row;
1250
1366
  }
1251
- bind(key, unit2) {
1252
- const tex = this.map.get(key);
1253
- if (!tex) throw new Error(`GradientRampCache: key "${key}" not uploaded`);
1367
+ /**
1368
+ * Whether uploading `stops` would move where existing rows sit — by growing
1369
+ * the atlas, which changes every row's `v`, or by recycling one, which
1370
+ * rewrites its texels.
1371
+ *
1372
+ * Anything holding a row past this call asks first and gets itself out of
1373
+ * the way, because neither can be undone once it has happened.
1374
+ */
1375
+ wouldReshape(stops) {
1376
+ if (this.rowByKey.size === 0) return false;
1377
+ return !this.rowByKey.has(JSON.stringify(stops)) && this.rowByKey.size >= this.rows;
1378
+ }
1379
+ /**
1380
+ * The `v` a row is sampled at — its center.
1381
+ *
1382
+ * The center is load-bearing: the atlas filters LINEAR, so a `v` anywhere
1383
+ * else blends the ramp beside it into this one. At the center the neighbor's
1384
+ * weight is exactly zero.
1385
+ */
1386
+ rowV(row) {
1387
+ return (row + 0.5) / (this.rows || 1);
1388
+ }
1389
+ bind(unit2) {
1254
1390
  const gl = this.gl;
1255
1391
  gl.activeTexture(gl.TEXTURE0 + unit2);
1256
- gl.bindTexture(gl.TEXTURE_2D, tex);
1392
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
1257
1393
  }
1258
1394
  hitRate() {
1259
1395
  if (this.totalQueries === 0) return 0;
@@ -1264,19 +1400,79 @@ var GradientRampCache = class {
1264
1400
  this.cacheHits = 0;
1265
1401
  }
1266
1402
  /**
1267
- * Delete every uploaded GL ramp texture and clear the map. Called by
1268
- * `WeaselRenderer.dispose()`. Only the GL textures are owned resources —
1269
- * `buildGradientRamp`'s CPU-side `Uint8ClampedArray` is transient per
1270
- * `upload()` call and already gone by the time a ramp is cached. The
1271
- * cache is unusable but refillable afterward (stats are left as-is; call
1403
+ * Delete the atlas texture and forget every row in it. Called by
1404
+ * `WeaselRenderer.dispose()`. The CPU mirror goes with it, so the atlas is
1405
+ * unusable but refillable afterward (stats are left as-is; call
1272
1406
  * `resetStats()` separately if desired).
1273
1407
  */
1274
1408
  free() {
1409
+ if (this.texture) this.gl.deleteTexture(this.texture);
1410
+ this.texture = null;
1411
+ this.rows = 0;
1412
+ this.pixels = new Uint8ClampedArray(0);
1413
+ this.rowByKey.clear();
1414
+ }
1415
+ /** The row the next ramp is written to: a free one, a taller atlas, or the
1416
+ * least recently used row of a full one. */
1417
+ claimRow() {
1418
+ if (this.rowByKey.size < this.rows) return this.rowByKey.size;
1419
+ if (this.rows < RAMP_ATLAS_MAX_ROWS) {
1420
+ this.resize(this.rows === 0 ? INITIAL_ROWS : this.rows * 2);
1421
+ return this.rowByKey.size;
1422
+ }
1423
+ const [lruKey, lruRow] = this.rowByKey.entries().next().value;
1424
+ this.rowByKey.delete(lruKey);
1425
+ return lruRow;
1426
+ }
1427
+ /** Grow to `rows`, keeping every row at the index it already had — a row
1428
+ * index is a stable name, and callers hold them. */
1429
+ resize(rows) {
1275
1430
  const gl = this.gl;
1276
- for (const tex of this.map.values()) {
1277
- gl.deleteTexture(tex);
1431
+ const next = new Uint8ClampedArray(RAMP_WIDTH * rows * 4);
1432
+ next.set(this.pixels);
1433
+ this.pixels = next;
1434
+ this.rows = rows;
1435
+ if (!this.texture) {
1436
+ const tex = gl.createTexture();
1437
+ if (!tex) throw new Error("GradientRampAtlas: createTexture failed");
1438
+ this.texture = tex;
1439
+ gl.bindTexture(gl.TEXTURE_2D, tex);
1440
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1441
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1442
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1443
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1444
+ } else {
1445
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
1278
1446
  }
1279
- this.map.clear();
1447
+ gl.texImage2D(
1448
+ gl.TEXTURE_2D,
1449
+ 0,
1450
+ gl.RGBA,
1451
+ RAMP_WIDTH,
1452
+ rows,
1453
+ 0,
1454
+ gl.RGBA,
1455
+ gl.UNSIGNED_BYTE,
1456
+ this.pixels
1457
+ );
1458
+ gl.bindTexture(gl.TEXTURE_2D, null);
1459
+ }
1460
+ writeRow(row, ramp2) {
1461
+ const gl = this.gl;
1462
+ this.pixels.set(ramp2, row * RAMP_WIDTH * 4);
1463
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
1464
+ gl.texSubImage2D(
1465
+ gl.TEXTURE_2D,
1466
+ 0,
1467
+ 0,
1468
+ row,
1469
+ RAMP_WIDTH,
1470
+ 1,
1471
+ gl.RGBA,
1472
+ gl.UNSIGNED_BYTE,
1473
+ ramp2
1474
+ );
1475
+ gl.bindTexture(gl.TEXTURE_2D, null);
1280
1476
  }
1281
1477
  };
1282
1478
 
@@ -3076,47 +3272,78 @@ function strokeMesh(path, stroke, flattenTolerance) {
3076
3272
  return mesh;
3077
3273
  }
3078
3274
 
3079
- // src/renderer/solidBatch.ts
3275
+ // src/renderer/drawBatch.ts
3080
3276
  var MAX_VERTICES_PER_BATCH = 32768;
3081
- var FLOATS_PER_VERTEX = 6;
3277
+ var FLOATS_PER_VERTEX = 10;
3082
3278
  var INITIAL_VERTICES = 256;
3083
3279
  var INITIAL_INDICES = 384;
3280
+ var WHITE_U = 0.5;
3281
+ var WHITE_V = 0.5;
3282
+ var PLAIN_WHITE = packSlot(WHITE_SLOT, PAINT_MODE_PLAIN);
3084
3283
  var SOLID_RING_SIZE = 64;
3085
- var SOLID_RING_SLOT_VERTICES = 1024;
3086
- var SOLID_RING_SLOT_INDICES = 3072;
3284
+ var BATCH_TIERS = [
3285
+ { vertices: 16, indices: 24 },
3286
+ { vertices: 1024, indices: 3072 }
3287
+ ];
3288
+ var SOLID_RING_SLOT_VERTICES = BATCH_TIERS[BATCH_TIERS.length - 1].vertices;
3087
3289
  var SOLID_LARGE_RING_SIZE = 4;
3088
3290
  function doubledTo(from, need) {
3089
3291
  let capacity = from;
3090
3292
  while (capacity < need) capacity *= 2;
3091
3293
  return capacity;
3092
3294
  }
3093
- var SolidBatch = class {
3295
+ function quadIndices(quads) {
3296
+ const out = new Uint32Array(quads * 6);
3297
+ for (let q = 0; q < quads; q++) {
3298
+ const b = q * 4;
3299
+ const j = q * 6;
3300
+ out[j] = b;
3301
+ out[j + 1] = b + 1;
3302
+ out[j + 2] = b + 2;
3303
+ out[j + 3] = b;
3304
+ out[j + 4] = b + 2;
3305
+ out[j + 5] = b + 3;
3306
+ }
3307
+ return out;
3308
+ }
3309
+ var DrawBatch = class {
3094
3310
  gl;
3095
3311
  aPos;
3096
3312
  aColor;
3097
- /** Cycled per flush, and created on first use so a renderer that flushes
3098
- * rarely allocates as few slots as it flushes. */
3099
- ring = new Array(SOLID_RING_SIZE).fill(void 0);
3100
- next = 0;
3101
- /** The same, for flushes past a slot's capacity; these sets grow to fit. */
3313
+ aUv;
3314
+ aPost;
3315
+ aSlot;
3316
+ /** One ring per tier, cycled per flush. Slots are created on first use, so a
3317
+ * renderer that flushes rarely allocates as few as it flushes. */
3318
+ rings = BATCH_TIERS.map(() => new Array(SOLID_RING_SIZE).fill(void 0));
3319
+ nextInRing = BATCH_TIERS.map(() => 0);
3320
+ /** The same, for flushes past the largest tier; these sets grow to fit. */
3102
3321
  largeRing = new Array(SOLID_LARGE_RING_SIZE).fill(void 0);
3103
3322
  nextLarge = 0;
3104
3323
  verts = new Float32Array(INITIAL_VERTICES * FLOATS_PER_VERTEX);
3105
3324
  idx = new Uint32Array(INITIAL_INDICES);
3106
3325
  nVerts = 0;
3107
3326
  nIdx = 0;
3108
- /** Whether the staged run is rects alone, so its indices are the canonical
3109
- * quad pattern and a slot already holding that pattern needs no upload. */
3327
+ /** Whether the staged run is quads alone, so its indices are the canonical
3328
+ * pattern and a slot already holding that pattern needs no upload. */
3110
3329
  pureRects = true;
3111
3330
  constructor(gl, prog) {
3112
3331
  const aPos = prog.attribute("a_position");
3113
3332
  const aColor = prog.attribute("a_vertexColor");
3114
- if (aPos === void 0 || aColor === void 0) {
3115
- throw new Error("SolidBatch: vertex-color program is missing a_position / a_vertexColor");
3333
+ const aUv = prog.attribute("a_uv");
3334
+ const aPost = prog.attribute("a_post");
3335
+ const aSlot = prog.attribute("a_texSlot");
3336
+ if (aPos === void 0 || aColor === void 0 || aUv === void 0 || aPost === void 0 || aSlot === void 0) {
3337
+ throw new Error(
3338
+ "DrawBatch: batch program is missing a_position / a_vertexColor / a_uv / a_post / a_texSlot"
3339
+ );
3116
3340
  }
3117
3341
  this.gl = gl;
3118
3342
  this.aPos = aPos;
3119
3343
  this.aColor = aColor;
3344
+ this.aUv = aUv;
3345
+ this.aPost = aPost;
3346
+ this.aSlot = aSlot;
3120
3347
  }
3121
3348
  get length() {
3122
3349
  return this.nIdx;
@@ -3132,7 +3359,6 @@ var SolidBatch = class {
3132
3359
  */
3133
3360
  pushRect(x, y, w, h, m, r, g, b, a) {
3134
3361
  this.reserve(4, 6);
3135
- const v = this.verts;
3136
3362
  let i = this.nVerts * FLOATS_PER_VERTEX;
3137
3363
  const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
3138
3364
  const x1 = x + w;
@@ -3141,40 +3367,161 @@ var SolidBatch = class {
3141
3367
  const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
3142
3368
  const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
3143
3369
  const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
3144
- v[i++] = ax;
3145
- v[i++] = ay;
3146
- v[i++] = r;
3147
- v[i++] = g;
3148
- v[i++] = b;
3149
- v[i++] = a;
3150
- v[i++] = bx;
3151
- v[i++] = by;
3152
- v[i++] = r;
3153
- v[i++] = g;
3154
- v[i++] = b;
3155
- v[i++] = a;
3156
- v[i++] = cx;
3157
- v[i++] = cy;
3158
- v[i++] = r;
3159
- v[i++] = g;
3160
- v[i++] = b;
3161
- v[i++] = a;
3162
- v[i++] = dx;
3163
- v[i++] = dy;
3164
- v[i++] = r;
3165
- v[i++] = g;
3166
- v[i++] = b;
3167
- v[i++] = a;
3370
+ const P = PLAIN_WHITE;
3371
+ i = this.writeVertex(i, ax, ay, r, g, b, a, WHITE_U, WHITE_V, 1, P);
3372
+ i = this.writeVertex(i, bx, by, r, g, b, a, WHITE_U, WHITE_V, 1, P);
3373
+ i = this.writeVertex(i, cx, cy, r, g, b, a, WHITE_U, WHITE_V, 1, P);
3374
+ this.writeVertex(i, dx, dy, r, g, b, a, WHITE_U, WHITE_V, 1, P);
3375
+ this.pushQuadIndices();
3376
+ }
3377
+ /**
3378
+ * Append one image quad: the destination rect `(x, y, w, h)` mapped through
3379
+ * `m`, sampling `(u0, v0)`-`(u1, v1)`, every corner carrying `post` as the
3380
+ * after-the-color-matrix alpha factor.
3381
+ *
3382
+ * Corners wind top-left, top-right, bottom-right, bottom-left with UVs
3383
+ * following — the same winding `pushRect` uses, which is what lets a run of
3384
+ * mixed rects and quads keep the canonical index pattern. The flips a command
3385
+ * asks for are already in the `u` / `v` the caller passes.
3386
+ *
3387
+ * `slot` is the texture unit the corners sample, which `draw.ts` assigns per
3388
+ * bitmap within the run.
3389
+ */
3390
+ pushQuad(x, y, w, h, m, u0, v0, u1, v1, post, slot) {
3391
+ this.reserve(4, 6);
3392
+ let i = this.nVerts * FLOATS_PER_VERTEX;
3393
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
3394
+ const x1 = x + w;
3395
+ const y1 = y + h;
3396
+ const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
3397
+ const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
3398
+ const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
3399
+ const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
3400
+ const P = packSlot(slot, PAINT_MODE_PLAIN);
3401
+ i = this.writeVertex(i, ax, ay, 1, 1, 1, 1, u0, v0, post, P);
3402
+ i = this.writeVertex(i, bx, by, 1, 1, 1, 1, u1, v0, post, P);
3403
+ i = this.writeVertex(i, cx, cy, 1, 1, 1, 1, u1, v1, post, P);
3404
+ this.writeVertex(i, dx, dy, 1, 1, 1, 1, u0, v1, post, P);
3405
+ this.pushQuadIndices();
3406
+ }
3407
+ /**
3408
+ * Append one glyph quad: the box `(x0, y0)`-`(x1, y1)` mapped through `m`,
3409
+ * sampling `(u0, v0)`-`(u1, v1)` of the font atlas at `slot`, painted in
3410
+ * `rgba`.
3411
+ *
3412
+ * `mode` says which channels of that atlas carry the distance field, and
3413
+ * rides the vertices packed into the slot — so a run off a baked MSDF atlas
3414
+ * and one off the runtime canvas bake still share a draw. The synthetic-bold
3415
+ * threshold does not: it is `u_synthBold`, and `draw.ts` breaks the run when
3416
+ * it changes.
3417
+ *
3418
+ * **A synthetic oblique is sheared here rather than in the shader.** The old
3419
+ * text program carried the baseline per vertex and skewed against
3420
+ * `u_synthItalic`; the batch places its own corners, so the shear is one
3421
+ * multiply while they are being placed, and `tanItalic` is 0 for an upright
3422
+ * face. It has to happen before `m`, which is where the shader had it too.
3423
+ *
3424
+ * Corners wind top-left, top-right, bottom-right, bottom-left — `pushQuad`'s
3425
+ * winding, which is what lets a run of mixed rects, image quads and glyphs
3426
+ * keep the canonical index pattern.
3427
+ */
3428
+ pushGlyph(x0, y0, x1, y1, baselineY, tanItalic, m, u0, v0, u1, v1, r, g, b, a, slot, mode) {
3429
+ this.reserve(4, 6);
3430
+ let i = this.nVerts * FLOATS_PER_VERTEX;
3431
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
3432
+ const topShift = (baselineY - y0) * tanItalic;
3433
+ const botShift = (baselineY - y1) * tanItalic;
3434
+ const tx0 = x0 + topShift, tx1 = x1 + topShift;
3435
+ const bx0 = x0 + botShift, bx1 = x1 + botShift;
3436
+ const ax = ma * tx0 + mc * y0 + mtx, ay = mb * tx0 + md * y0 + mty;
3437
+ const bx = ma * tx1 + mc * y0 + mtx, by = mb * tx1 + md * y0 + mty;
3438
+ const cx = ma * bx1 + mc * y1 + mtx, cy = mb * bx1 + md * y1 + mty;
3439
+ const dx = ma * bx0 + mc * y1 + mtx, dy = mb * bx0 + md * y1 + mty;
3440
+ const P = packSlot(slot, mode);
3441
+ i = this.writeVertex(i, ax, ay, r, g, b, a, u0, v0, 1, P);
3442
+ i = this.writeVertex(i, bx, by, r, g, b, a, u1, v0, 1, P);
3443
+ i = this.writeVertex(i, cx, cy, r, g, b, a, u1, v1, 1, P);
3444
+ this.writeVertex(i, dx, dy, r, g, b, a, u0, v1, 1, P);
3445
+ this.pushQuadIndices();
3446
+ }
3447
+ /**
3448
+ * Append one rect filled by a gradient: the corners of `(x, y, w, h)` mapped
3449
+ * through `m`, sampling the ramp atlas at `slot`.
3450
+ *
3451
+ * `uv` gives each channel of `a_uv` as an affine function of the coordinates
3452
+ * the corners arrive in, which is what lets a gradient ride the vertices at
3453
+ * all: a linear one's ramp position is affine in position outright, and a
3454
+ * radial or conic one's gradient-space *coordinate* is, even though the ramp
3455
+ * position it yields is not. Either way the rasterizer's interpolation across
3456
+ * a triangle is exact. `mode` says which of the two the fragment shader is
3457
+ * looking at, and `post` carries the atlas row for the modes that read it —
3458
+ * see `shaders/batchFill.ts`.
3459
+ *
3460
+ * Values outside 0..1 are the sampler's business; the atlas clamps to the
3461
+ * edge texel, which is what the gradient shader's own `clamp` did.
3462
+ */
3463
+ pushGradientRect(x, y, w, h, m, uv, post, slot, mode, r, g, b, a) {
3464
+ this.reserve(4, 6);
3465
+ let i = this.nVerts * FLOATS_PER_VERTEX;
3466
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
3467
+ const x1 = x + w;
3468
+ const y1 = y + h;
3469
+ const P = packSlot(slot, mode);
3470
+ const corner = (cx, cy) => {
3471
+ const at = i;
3472
+ i = this.writeVertex(
3473
+ at,
3474
+ ma * cx + mc * cy + mtx,
3475
+ mb * cx + md * cy + mty,
3476
+ r,
3477
+ g,
3478
+ b,
3479
+ a,
3480
+ uv.ux * cx + uv.uy * cy + uv.u0,
3481
+ uv.vx * cx + uv.vy * cy + uv.v0,
3482
+ post,
3483
+ P
3484
+ );
3485
+ return i;
3486
+ };
3487
+ corner(x, y);
3488
+ corner(x1, y);
3489
+ corner(x1, y1);
3490
+ corner(x, y1);
3491
+ this.pushQuadIndices();
3492
+ }
3493
+ /** `pushMesh` for a mesh filled by a gradient — see `pushGradientRect` for
3494
+ * what `uv`, `post` and `mode` carry. */
3495
+ pushGradientMesh(mesh, m, uv, post, slot, mode, r, g, b, a) {
3496
+ const src = mesh.vertices;
3497
+ const srcIdx = mesh.indices;
3498
+ const n = src.length >> 1;
3499
+ this.reserve(n, srcIdx.length);
3500
+ this.pureRects = false;
3501
+ const v = this.verts;
3502
+ let i = this.nVerts * FLOATS_PER_VERTEX;
3503
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
3504
+ const P = packSlot(slot, mode);
3505
+ for (let k = 0; k < n; k++) {
3506
+ const x = src[k * 2];
3507
+ const y = src[k * 2 + 1];
3508
+ v[i++] = ma * x + mc * y + mtx;
3509
+ v[i++] = mb * x + md * y + mty;
3510
+ v[i++] = r;
3511
+ v[i++] = g;
3512
+ v[i++] = b;
3513
+ v[i++] = a;
3514
+ v[i++] = uv.ux * x + uv.uy * y + uv.u0;
3515
+ v[i++] = uv.vx * x + uv.vy * y + uv.v0;
3516
+ v[i++] = post;
3517
+ v[i++] = P;
3518
+ }
3168
3519
  const base = this.nVerts;
3520
+ const out = this.idx;
3169
3521
  let j = this.nIdx;
3170
- this.idx[j++] = base;
3171
- this.idx[j++] = base + 1;
3172
- this.idx[j++] = base + 2;
3173
- this.idx[j++] = base;
3174
- this.idx[j++] = base + 2;
3175
- this.idx[j++] = base + 3;
3176
- this.nVerts += 4;
3177
- this.nIdx += 6;
3522
+ for (let k = 0; k < srcIdx.length; k++) out[j++] = base + srcIdx[k];
3523
+ this.nVerts += n;
3524
+ this.nIdx += srcIdx.length;
3178
3525
  }
3179
3526
  /**
3180
3527
  * Append a tessellated mesh through `m`, all vertices carrying `rgba`. The
@@ -3199,6 +3546,10 @@ var SolidBatch = class {
3199
3546
  v[i++] = g;
3200
3547
  v[i++] = b;
3201
3548
  v[i++] = a;
3549
+ v[i++] = WHITE_U;
3550
+ v[i++] = WHITE_V;
3551
+ v[i++] = 1;
3552
+ v[i++] = PLAIN_WHITE;
3202
3553
  }
3203
3554
  const base = this.nVerts;
3204
3555
  const out = this.idx;
@@ -3211,14 +3562,20 @@ var SolidBatch = class {
3211
3562
  * Returns the index count for the caller's `drawElements`. */
3212
3563
  uploadAndBind() {
3213
3564
  const gl = this.gl;
3214
- const set = this.nVerts <= SOLID_RING_SLOT_VERTICES && this.nIdx <= SOLID_RING_SLOT_INDICES ? this.nextRingSlot() : this.nextLargeSlot();
3565
+ const tier = BATCH_TIERS.findIndex(
3566
+ (t) => this.nVerts <= t.vertices && this.nIdx <= t.indices
3567
+ );
3568
+ const set = tier < 0 ? this.nextLargeSlot() : this.nextRingSlot(tier);
3215
3569
  gl.bindVertexArray(set.vao);
3216
3570
  gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
3217
3571
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nVerts * FLOATS_PER_VERTEX);
3218
- const rects = this.pureRects ? this.nVerts >> 2 : -1;
3219
- if (rects < 0 || rects !== set.indexRects) {
3572
+ if (!this.pureRects) {
3220
3573
  gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, this.idx, 0, this.nIdx);
3221
- set.indexRects = rects;
3574
+ set.canonicalQuads = 0;
3575
+ } else if (set.canonicalQuads < this.nVerts >> 2) {
3576
+ const quads = Math.min(set.vertexCapacity >> 2, Math.floor(set.indexCapacity / 6));
3577
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, quadIndices(quads));
3578
+ set.canonicalQuads = quads;
3222
3579
  }
3223
3580
  return this.nIdx;
3224
3581
  }
@@ -3228,12 +3585,39 @@ var SolidBatch = class {
3228
3585
  this.pureRects = true;
3229
3586
  }
3230
3587
  dispose() {
3231
- for (const set of [...this.ring, ...this.largeRing]) {
3588
+ for (const set of [...this.rings.flat(), ...this.largeRing]) {
3232
3589
  if (set) this.deleteSet(set);
3233
3590
  }
3234
- this.ring.fill(void 0);
3591
+ for (const ring of this.rings) ring.fill(void 0);
3235
3592
  this.largeRing.fill(void 0);
3236
3593
  }
3594
+ writeVertex(i, x, y, r, g, b, a, u, v, post, packed) {
3595
+ const out = this.verts;
3596
+ out[i] = x;
3597
+ out[i + 1] = y;
3598
+ out[i + 2] = r;
3599
+ out[i + 3] = g;
3600
+ out[i + 4] = b;
3601
+ out[i + 5] = a;
3602
+ out[i + 6] = u;
3603
+ out[i + 7] = v;
3604
+ out[i + 8] = post;
3605
+ out[i + 9] = packed;
3606
+ return i + FLOATS_PER_VERTEX;
3607
+ }
3608
+ /** Two triangles over the four corners just written. */
3609
+ pushQuadIndices() {
3610
+ const base = this.nVerts;
3611
+ let j = this.nIdx;
3612
+ this.idx[j++] = base;
3613
+ this.idx[j++] = base + 1;
3614
+ this.idx[j++] = base + 2;
3615
+ this.idx[j++] = base;
3616
+ this.idx[j++] = base + 2;
3617
+ this.idx[j++] = base + 3;
3618
+ this.nVerts += 4;
3619
+ this.nIdx += 6;
3620
+ }
3237
3621
  /** Grow the CPU arrays so `vertices` / `indices` more fit. */
3238
3622
  reserve(vertices, indices) {
3239
3623
  const needVerts = (this.nVerts + vertices) * FLOATS_PER_VERTEX;
@@ -3249,13 +3633,14 @@ var SolidBatch = class {
3249
3633
  this.idx = grown;
3250
3634
  }
3251
3635
  }
3252
- nextRingSlot() {
3253
- const slot = this.next;
3254
- this.next = (slot + 1) % SOLID_RING_SIZE;
3255
- const existing = this.ring[slot];
3636
+ nextRingSlot(tier) {
3637
+ const ring = this.rings[tier];
3638
+ const slot = this.nextInRing[tier];
3639
+ this.nextInRing[tier] = (slot + 1) % ring.length;
3640
+ const existing = ring[slot];
3256
3641
  if (existing) return existing;
3257
- const set = this.createSet(SOLID_RING_SLOT_VERTICES, SOLID_RING_SLOT_INDICES);
3258
- this.ring[slot] = set;
3642
+ const set = this.createSet(BATCH_TIERS[tier].vertices, BATCH_TIERS[tier].indices);
3643
+ ring[slot] = set;
3259
3644
  return set;
3260
3645
  }
3261
3646
  nextLargeSlot() {
@@ -3266,7 +3651,7 @@ var SolidBatch = class {
3266
3651
  if (!set) {
3267
3652
  set = this.createSet(
3268
3653
  doubledTo(SOLID_RING_SLOT_VERTICES, this.nVerts),
3269
- doubledTo(SOLID_RING_SLOT_INDICES, this.nIdx)
3654
+ doubledTo(BATCH_TIERS[BATCH_TIERS.length - 1].indices, this.nIdx)
3270
3655
  );
3271
3656
  this.largeRing[slot] = set;
3272
3657
  return set;
@@ -3282,7 +3667,7 @@ var SolidBatch = class {
3282
3667
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, set.ibo);
3283
3668
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, set.indexCapacity * 4, gl.DYNAMIC_DRAW);
3284
3669
  gl.bindVertexArray(null);
3285
- set.indexRects = -1;
3670
+ set.canonicalQuads = 0;
3286
3671
  }
3287
3672
  return set;
3288
3673
  }
@@ -3291,7 +3676,7 @@ var SolidBatch = class {
3291
3676
  const vao = gl.createVertexArray();
3292
3677
  const vbo = gl.createBuffer();
3293
3678
  const ibo = gl.createBuffer();
3294
- if (!vao || !vbo || !ibo) throw new Error("SolidBatch: failed to create GL objects");
3679
+ if (!vao || !vbo || !ibo) throw new Error("DrawBatch: failed to create GL objects");
3295
3680
  const stride = FLOATS_PER_VERTEX * 4;
3296
3681
  gl.bindVertexArray(vao);
3297
3682
  gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
@@ -3300,10 +3685,16 @@ var SolidBatch = class {
3300
3685
  gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, stride, 0);
3301
3686
  gl.enableVertexAttribArray(this.aColor);
3302
3687
  gl.vertexAttribPointer(this.aColor, 4, gl.FLOAT, false, stride, 8);
3688
+ gl.enableVertexAttribArray(this.aUv);
3689
+ gl.vertexAttribPointer(this.aUv, 2, gl.FLOAT, false, stride, 24);
3690
+ gl.enableVertexAttribArray(this.aPost);
3691
+ gl.vertexAttribPointer(this.aPost, 1, gl.FLOAT, false, stride, 32);
3692
+ gl.enableVertexAttribArray(this.aSlot);
3693
+ gl.vertexAttribPointer(this.aSlot, 1, gl.FLOAT, false, stride, 36);
3303
3694
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
3304
3695
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices * 4, gl.DYNAMIC_DRAW);
3305
3696
  gl.bindVertexArray(null);
3306
- return { vao, vbo, ibo, vertexCapacity: vertices, indexCapacity: indices, indexRects: -1 };
3697
+ return { vao, vbo, ibo, vertexCapacity: vertices, indexCapacity: indices, canonicalQuads: 0 };
3307
3698
  }
3308
3699
  deleteSet(set) {
3309
3700
  const gl = this.gl;
@@ -3313,222 +3704,18 @@ var SolidBatch = class {
3313
3704
  }
3314
3705
  };
3315
3706
 
3316
- // src/renderer/imageBatch.ts
3317
- var MAX_IMAGE_VERTICES_PER_BATCH = 32768;
3318
- var FLOATS_PER_VERTEX2 = 5;
3319
- var INITIAL_QUADS = 64;
3320
- var TIERS = [
3321
- { quads: 1, slots: 64 },
3322
- { quads: 16, slots: 64 },
3323
- { quads: 256, slots: 16 }
3324
- ];
3325
- TIERS[0].slots;
3326
- var IMAGE_LARGE_RING_SIZE = 4;
3327
- function doubledTo2(from, need) {
3328
- let capacity = from;
3329
- while (capacity < need) capacity *= 2;
3330
- return capacity;
3707
+ // src/renderer/shaders/registerProgram.ts
3708
+ var registry = /* @__PURE__ */ new Map();
3709
+ function getProgramSource(id) {
3710
+ return registry.get(id) ?? null;
3331
3711
  }
3332
- function quadIndices(quads) {
3333
- const out = new Uint32Array(quads * 6);
3334
- for (let q = 0; q < quads; q++) {
3335
- const b = q * 4;
3336
- const j = q * 6;
3337
- out[j] = b;
3338
- out[j + 1] = b + 1;
3339
- out[j + 2] = b + 2;
3340
- out[j + 3] = b;
3341
- out[j + 4] = b + 2;
3342
- out[j + 5] = b + 3;
3712
+ var isDev = () => typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
3713
+ function registerProgram(id, vert, frag) {
3714
+ if (registry.has(id) && !isDev()) {
3715
+ throw new Error(`weasel registerProgram: duplicate program id "${id}". In production, re-registration is not allowed. Pass a unique id or call in dev mode.`);
3343
3716
  }
3344
- return out;
3345
- }
3346
- var ImageBatch = class {
3347
- gl;
3348
- aPos;
3349
- aUv;
3350
- aOpacity;
3351
- /** One ring per tier, cycled per flush. Slots are created on first use, so a
3352
- * renderer that flushes rarely allocates as few as it flushes. */
3353
- rings = TIERS.map((t) => new Array(t.slots).fill(void 0));
3354
- nextInRing = TIERS.map(() => 0);
3355
- /** For flushes past the largest tier; these sets grow to fit. */
3356
- largeRing = new Array(IMAGE_LARGE_RING_SIZE).fill(void 0);
3357
- nextLarge = 0;
3358
- verts = new Float32Array(INITIAL_QUADS * 4 * FLOATS_PER_VERTEX2);
3359
- nQuads = 0;
3360
- constructor(gl, prog) {
3361
- const aPos = prog.attribute("a_position");
3362
- const aUv = prog.attribute("a_uv");
3363
- const aOpacity = prog.attribute("a_opacity");
3364
- if (aPos === void 0 || aUv === void 0 || aOpacity === void 0) {
3365
- throw new Error(
3366
- "ImageBatch: vertex-opacity program is missing a_position / a_uv / a_opacity"
3367
- );
3368
- }
3369
- this.gl = gl;
3370
- this.aPos = aPos;
3371
- this.aUv = aUv;
3372
- this.aOpacity = aOpacity;
3373
- }
3374
- /** Indices staged — what a caller passes to `drawElements`. */
3375
- get length() {
3376
- return this.nQuads * 6;
3377
- }
3378
- get quads() {
3379
- return this.nQuads;
3380
- }
3381
- /** Whether staging one more quad would put the run past the per-flush cap. */
3382
- wouldOverflow() {
3383
- return (this.nQuads + 1) * 4 > MAX_IMAGE_VERTICES_PER_BATCH;
3384
- }
3385
- /**
3386
- * Append one image quad: the destination rect `(x, y, w, h)` mapped through
3387
- * `m`, sampling `(u0, v0)`-`(u1, v1)`, every corner carrying `opacity`.
3388
- *
3389
- * An affine maps a rect to a parallelogram, so two triangles still cover it
3390
- * and the batch draws at `u_model` identity. Corners wind top-left,
3391
- * top-right, bottom-right, bottom-left, with UVs following — the flips a
3392
- * command asks for are already in the `u`/`v` the caller passes.
3393
- */
3394
- pushQuad(x, y, w, h, m, u0, v0, u1, v1, opacity) {
3395
- this.reserve();
3396
- const v = this.verts;
3397
- let i = this.nQuads * 4 * FLOATS_PER_VERTEX2;
3398
- const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
3399
- const x1 = x + w;
3400
- const y1 = y + h;
3401
- const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
3402
- const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
3403
- const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
3404
- const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
3405
- v[i++] = ax;
3406
- v[i++] = ay;
3407
- v[i++] = u0;
3408
- v[i++] = v0;
3409
- v[i++] = opacity;
3410
- v[i++] = bx;
3411
- v[i++] = by;
3412
- v[i++] = u1;
3413
- v[i++] = v0;
3414
- v[i++] = opacity;
3415
- v[i++] = cx;
3416
- v[i++] = cy;
3417
- v[i++] = u1;
3418
- v[i++] = v1;
3419
- v[i++] = opacity;
3420
- v[i++] = dx;
3421
- v[i++] = dy;
3422
- v[i++] = u0;
3423
- v[i++] = v1;
3424
- v[i++] = opacity;
3425
- this.nQuads += 1;
3426
- }
3427
- /** Upload the staged geometry into the next set of buffers and bind its VAO.
3428
- * Returns the index count for the caller's `drawElements`. */
3429
- uploadAndBind() {
3430
- const gl = this.gl;
3431
- const tier = TIERS.findIndex((t) => this.nQuads <= t.quads);
3432
- const set = tier < 0 ? this.nextLargeSlot() : this.nextRingSlot(tier);
3433
- gl.bindVertexArray(set.vao);
3434
- gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
3435
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nQuads * 4 * FLOATS_PER_VERTEX2);
3436
- return this.nQuads * 6;
3437
- }
3438
- reset() {
3439
- this.nQuads = 0;
3440
- }
3441
- dispose() {
3442
- for (const set of [...this.rings.flat(), ...this.largeRing]) {
3443
- if (set) this.deleteSet(set);
3444
- }
3445
- for (const ring of this.rings) ring.fill(void 0);
3446
- this.largeRing.fill(void 0);
3447
- }
3448
- /** Grow the CPU arrays so one more quad fits. */
3449
- reserve() {
3450
- const need = (this.nQuads + 1) * 4 * FLOATS_PER_VERTEX2;
3451
- if (need <= this.verts.length) return;
3452
- const grown = new Float32Array(doubledTo2(this.verts.length, need));
3453
- grown.set(this.verts);
3454
- this.verts = grown;
3455
- }
3456
- nextRingSlot(tier) {
3457
- const ring = this.rings[tier];
3458
- const slot = this.nextInRing[tier];
3459
- this.nextInRing[tier] = (slot + 1) % ring.length;
3460
- const existing = ring[slot];
3461
- if (existing) return existing;
3462
- const set = this.createSet(TIERS[tier].quads);
3463
- ring[slot] = set;
3464
- return set;
3465
- }
3466
- nextLargeSlot() {
3467
- const gl = this.gl;
3468
- const slot = this.nextLarge;
3469
- this.nextLarge = (slot + 1) % IMAGE_LARGE_RING_SIZE;
3470
- let set = this.largeRing[slot];
3471
- if (!set) {
3472
- set = this.createSet(doubledTo2(TIERS[TIERS.length - 1].quads, this.nQuads));
3473
- this.largeRing[slot] = set;
3474
- return set;
3475
- }
3476
- if (this.nQuads > set.quadCapacity) {
3477
- set.quadCapacity = doubledTo2(set.quadCapacity, this.nQuads);
3478
- gl.bindVertexArray(set.vao);
3479
- gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
3480
- gl.bufferData(
3481
- gl.ARRAY_BUFFER,
3482
- set.quadCapacity * 4 * FLOATS_PER_VERTEX2 * 4,
3483
- gl.DYNAMIC_DRAW
3484
- );
3485
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, set.ibo);
3486
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices(set.quadCapacity), gl.STATIC_DRAW);
3487
- gl.bindVertexArray(null);
3488
- }
3489
- return set;
3490
- }
3491
- createSet(quads) {
3492
- const gl = this.gl;
3493
- const vao = gl.createVertexArray();
3494
- const vbo = gl.createBuffer();
3495
- const ibo = gl.createBuffer();
3496
- if (!vao || !vbo || !ibo) throw new Error("ImageBatch: failed to create GL objects");
3497
- const stride = FLOATS_PER_VERTEX2 * 4;
3498
- gl.bindVertexArray(vao);
3499
- gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
3500
- gl.bufferData(gl.ARRAY_BUFFER, quads * 4 * stride, gl.DYNAMIC_DRAW);
3501
- gl.enableVertexAttribArray(this.aPos);
3502
- gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, stride, 0);
3503
- gl.enableVertexAttribArray(this.aUv);
3504
- gl.vertexAttribPointer(this.aUv, 2, gl.FLOAT, false, stride, 8);
3505
- gl.enableVertexAttribArray(this.aOpacity);
3506
- gl.vertexAttribPointer(this.aOpacity, 1, gl.FLOAT, false, stride, 16);
3507
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
3508
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices(quads), gl.STATIC_DRAW);
3509
- gl.bindVertexArray(null);
3510
- return { vao, vbo, ibo, quadCapacity: quads };
3511
- }
3512
- deleteSet(set) {
3513
- const gl = this.gl;
3514
- gl.deleteBuffer(set.vbo);
3515
- gl.deleteBuffer(set.ibo);
3516
- gl.deleteVertexArray(set.vao);
3517
- }
3518
- };
3519
-
3520
- // src/renderer/shaders/registerProgram.ts
3521
- var registry = /* @__PURE__ */ new Map();
3522
- function getProgramSource(id) {
3523
- return registry.get(id) ?? null;
3524
- }
3525
- var isDev = () => typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
3526
- function registerProgram(id, vert, frag) {
3527
- if (registry.has(id) && !isDev()) {
3528
- throw new Error(`weasel registerProgram: duplicate program id "${id}". In production, re-registration is not allowed. Pass a unique id or call in dev mode.`);
3529
- }
3530
- registry.set(id, { vert, frag });
3531
- return { id };
3717
+ registry.set(id, { vert, frag });
3718
+ return { id };
3532
3719
  }
3533
3720
 
3534
3721
  // src/renderer/effects/effectPrelude.ts
@@ -3668,14 +3855,13 @@ function dispatch(ctx, cmd) {
3668
3855
  case "path":
3669
3856
  return drawPath(ctx, cmd);
3670
3857
  case "text":
3671
- flushBatches(ctx);
3672
3858
  return drawText(ctx, cmd);
3673
3859
  case "image":
3674
3860
  return drawImage(ctx, cmd);
3675
3861
  case "sprites":
3676
3862
  return drawSprites(ctx, cmd);
3677
3863
  case "shader":
3678
- flushBatches(ctx);
3864
+ flushBatch(ctx);
3679
3865
  return drawShader(ctx, cmd);
3680
3866
  }
3681
3867
  }
@@ -3792,7 +3978,7 @@ function drawShader(ctx, cmd) {
3792
3978
  }
3793
3979
  function drawGroupWithEffects(ctx, cmd, targets, width, height) {
3794
3980
  const gl = ctx.gl;
3795
- flushBatches(ctx);
3981
+ flushBatch(ctx);
3796
3982
  const parentTarget = ctx.renderTarget ?? null;
3797
3983
  const parentClipDepth = ctx.clipDepth;
3798
3984
  const scissorWasOn = gl.isEnabled(gl.SCISSOR_TEST);
@@ -3806,7 +3992,7 @@ function drawGroupWithEffects(ctx, cmd, targets, width, height) {
3806
3992
  gl.stencilMask(255);
3807
3993
  gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
3808
3994
  for (const child of cmd.children) dispatch(ctx, child);
3809
- flushBatches(ctx);
3995
+ flushBatch(ctx);
3810
3996
  gl.disable(gl.BLEND);
3811
3997
  for (const effect of cmd.effects) {
3812
3998
  const program = ctx.ensureProgram?.(effect.program.id) ?? null;
@@ -3934,12 +4120,14 @@ function drawPathFill(ctx, cmd) {
3934
4120
  const isSolid = fill.fill === void 0 || fill.fill === "solid";
3935
4121
  const hasVColors = !!(cmd.vertexColors && cmd.vertexColors.length > 0);
3936
4122
  const solid2 = fill;
3937
- if (isSolid && !hasVColors && cmd.path.kind === "rect") {
3938
- pushRect(ctx, cmd.path, solid2);
4123
+ const paint = batchPaint(ctx, fill, hasVColors);
4124
+ if (paint !== void 0 && cmd.path.kind === "rect") {
4125
+ if (paint.kind === "ramp") pushRampRect(ctx, cmd.path, paint);
4126
+ else pushRect(ctx, cmd.path, paint);
3939
4127
  return;
3940
4128
  }
3941
4129
  const mesh = fillMesh(ctx, cmd.path);
3942
- if (tryStageSolid(ctx, mesh, isSolid && !hasVColors ? solid2 : void 0)) return;
4130
+ if (tryStageFill(ctx, mesh, paint)) return;
3943
4131
  const handle = meshHandle(ctx, mesh);
3944
4132
  if (isSolid && hasVColors) {
3945
4133
  drawPathFillVColor(ctx, cmd, solid2, handle);
@@ -3949,43 +4137,120 @@ function drawPathFill(ctx, cmd) {
3949
4137
  drawPathFillByKind(ctx, fill, handle);
3950
4138
  }
3951
4139
  }
4140
+ var SAMPLER_UNITS = new Int32Array(
4141
+ Array.from({ length: BATCH_TEXTURE_SLOTS }, (_, i) => i)
4142
+ );
3952
4143
  function isIdentityColorMatrix(cm) {
3953
4144
  return cm === IDENTITY_COLOR_MATRIX || sameValues(IDENTITY_COLOR_MATRIX, cm);
3954
4145
  }
3955
- function stagedStateIsLive(ctx, staged) {
4146
+ function stagedStateIsLive(ctx, staged, image, sampling, synthBold = 0) {
3956
4147
  if (staged.clipDepth !== ctx.clipDepth) return false;
4148
+ if (staged.synthBold !== synthBold) return false;
3957
4149
  if (!staged.foldsAlpha && staged.alpha !== ctx.state.alpha) return false;
4150
+ if (image !== void 0 && slotForBitmap(staged, image, sampling) < 0) return false;
3958
4151
  const colorMatrix = ctx.state.colorMatrix;
3959
4152
  return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
3960
4153
  }
4154
+ function slotForBitmap(staged, image, sampling) {
4155
+ for (let i = 0; i < staged.textures.length; i++) {
4156
+ const entry = staged.textures[i];
4157
+ if (entry.kind !== "bitmap" || entry.image !== image) continue;
4158
+ return entry.sampling === sampling ? i + 1 : -1;
4159
+ }
4160
+ return nextFreeSlot(staged);
4161
+ }
4162
+ function slotForRamps(staged) {
4163
+ for (let i = 0; i < staged.textures.length; i++) {
4164
+ if (staged.textures[i].kind === "ramps") return i + 1;
4165
+ }
4166
+ return nextFreeSlot(staged);
4167
+ }
4168
+ function slotForAtlas(staged, id) {
4169
+ for (let i = 0; i < staged.textures.length; i++) {
4170
+ const entry = staged.textures[i];
4171
+ if (entry.kind === "atlas" && entry.id === id) return i + 1;
4172
+ }
4173
+ return nextFreeSlot(staged);
4174
+ }
4175
+ function nextFreeSlot(staged) {
4176
+ return staged.textures.length + 1 < BATCH_TEXTURE_SLOTS ? staged.textures.length + 1 : -1;
4177
+ }
3961
4178
  var MAX_BATCHED_MESH_VERTICES = 256;
3962
4179
  function canBatchMesh(mesh) {
3963
4180
  return !mesh.requiresStencil && mesh.vertices.length >> 1 <= MAX_BATCHED_MESH_VERTICES;
3964
4181
  }
3965
4182
  function stageSolid(ctx, vertices) {
3966
- flushImages(ctx);
3967
- if (ctx.solidState !== void 0 && !stagedStateIsLive(ctx, ctx.solidState)) flushSolids(ctx);
3968
- if (ctx.solidBatch.wouldOverflow(vertices)) flushSolids(ctx);
3969
- if (ctx.solidState === void 0) {
4183
+ if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState)) flushBatch(ctx);
4184
+ if (ctx.drawBatch.wouldOverflow(vertices)) flushBatch(ctx);
4185
+ return openRun(ctx);
4186
+ }
4187
+ function openRun(ctx) {
4188
+ if (ctx.batchState === void 0) {
3970
4189
  const colorMatrix = ctx.state.colorMatrix;
3971
4190
  const foldsAlpha = isIdentityColorMatrix(colorMatrix);
3972
- ctx.solidState = {
4191
+ ctx.batchState = {
3973
4192
  alpha: foldsAlpha ? 1 : ctx.state.alpha,
3974
4193
  colorMatrix,
3975
4194
  clipDepth: ctx.clipDepth,
3976
- foldsAlpha
4195
+ synthBold: 0,
4196
+ foldsAlpha,
4197
+ textures: []
3977
4198
  };
3978
4199
  }
3979
- return ctx.solidState;
4200
+ return ctx.batchState;
4201
+ }
4202
+ function stageImage(ctx, image, sampling) {
4203
+ if (ctx.batchState !== void 0 && !stagedStateIsLive(ctx, ctx.batchState, image, sampling)) {
4204
+ flushBatch(ctx);
4205
+ }
4206
+ if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
4207
+ const staged = openRun(ctx);
4208
+ let slot = slotForBitmap(staged, image, sampling);
4209
+ if (slot > staged.textures.length) {
4210
+ staged.textures.push({ kind: "bitmap", image, sampling });
4211
+ slot = staged.textures.length;
4212
+ }
4213
+ return { staged, slot };
4214
+ }
4215
+ function stageGlyphs(ctx, atlasId, synthBold) {
4216
+ if (ctx.batchState !== void 0 && (!stagedStateIsLive(ctx, ctx.batchState, void 0, void 0, synthBold) || slotForAtlas(ctx.batchState, atlasId) < 0)) {
4217
+ flushBatch(ctx);
4218
+ }
4219
+ if (ctx.drawBatch.wouldOverflow(4)) flushBatch(ctx);
4220
+ const staged = openRun(ctx);
4221
+ staged.synthBold = synthBold;
4222
+ let slot = slotForAtlas(staged, atlasId);
4223
+ if (slot > staged.textures.length) {
4224
+ staged.textures.push({ kind: "atlas", id: atlasId });
4225
+ slot = staged.textures.length;
4226
+ }
4227
+ return { staged, slot };
4228
+ }
4229
+ function stageRamps(ctx, stops, vertices) {
4230
+ if (ctx.gradRamps.wouldReshape(stops)) flushBatch(ctx);
4231
+ if (ctx.batchState !== void 0 && (!stagedStateIsLive(ctx, ctx.batchState) || slotForRamps(ctx.batchState) < 0)) {
4232
+ flushBatch(ctx);
4233
+ }
4234
+ if (ctx.drawBatch.wouldOverflow(vertices)) flushBatch(ctx);
4235
+ const staged = openRun(ctx);
4236
+ let slot = slotForRamps(staged);
4237
+ if (slot > staged.textures.length) {
4238
+ staged.textures.push({ kind: "ramps" });
4239
+ slot = staged.textures.length;
4240
+ }
4241
+ return { staged, slot, rowV: ctx.gradRamps.rowV(ctx.gradRamps.upload(stops)) };
4242
+ }
4243
+ function stagedAlpha(ctx, staged, opacity) {
4244
+ return (opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1);
3980
4245
  }
3981
4246
  function stagedColor(ctx, staged, paint) {
3982
4247
  const [r, g, b, a] = resolveColor(paint.color);
3983
- return [r, g, b, a * (paint.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1)];
4248
+ return [r, g, b, a * stagedAlpha(ctx, staged, paint.opacity)];
3984
4249
  }
3985
4250
  function pushRect(ctx, rect, fill) {
3986
4251
  const staged = stageSolid(ctx, 4);
3987
4252
  const [r, g, b, a] = stagedColor(ctx, staged, fill);
3988
- ctx.solidBatch.pushRect(
4253
+ ctx.drawBatch.pushRect(
3989
4254
  rect.x,
3990
4255
  rect.y,
3991
4256
  rect.width,
@@ -4000,83 +4265,162 @@ function pushRect(ctx, rect, fill) {
4000
4265
  function pushMesh(ctx, mesh, paint) {
4001
4266
  const staged = stageSolid(ctx, mesh.vertices.length >> 1);
4002
4267
  const [r, g, b, a] = stagedColor(ctx, staged, paint);
4003
- ctx.solidBatch.pushMesh(mesh, ctx.state.transform, r, g, b, a);
4268
+ ctx.drawBatch.pushMesh(mesh, ctx.state.transform, r, g, b, a);
4269
+ }
4270
+ function batchPaint(ctx, fill, hasVColors) {
4271
+ if (hasVColors) return void 0;
4272
+ const kind = fill.fill ?? "solid";
4273
+ if (kind === "solid") {
4274
+ const solid2 = fill;
4275
+ return { kind: "solid", color: solid2.color, opacity: solid2.opacity };
4276
+ }
4277
+ if (kind === "linear-gradient") {
4278
+ const grad = fill;
4279
+ return ramp(grad, PAINT_MODE_PLAIN, linearRampUV(ctx, grad));
4280
+ }
4281
+ if (kind === "radial-gradient") {
4282
+ const grad = fill;
4283
+ return ramp(grad, PAINT_MODE_RADIAL, radialRampUV(ctx, grad));
4284
+ }
4285
+ if (kind === "conic-gradient") {
4286
+ const grad = fill;
4287
+ return ramp(grad, PAINT_MODE_CONIC, conicRampUV(ctx, grad));
4288
+ }
4289
+ return void 0;
4004
4290
  }
4005
- function tryStageSolid(ctx, mesh, paint) {
4291
+ function ramp(fill, mode, uv) {
4292
+ return { kind: "ramp", stops: fill.stops, opacity: fill.opacity, mode, uv };
4293
+ }
4294
+ function gradientMap(ctx, units) {
4295
+ return mat3.multiply(gradientSpaceInverse(ctx, units), ctx.state.transform);
4296
+ }
4297
+ function linearRampUV(ctx, fill) {
4298
+ const g = gradientMap(ctx, fill.units);
4299
+ const dx = fill.to.x - fill.from.x;
4300
+ const dy = fill.to.y - fill.from.y;
4301
+ const len = Math.hypot(dx, dy) || 1;
4302
+ const k = 1 / (len * len);
4303
+ return {
4304
+ ux: (g[0] * dx + g[1] * dy) * k,
4305
+ uy: (g[3] * dx + g[4] * dy) * k,
4306
+ u0: ((g[6] - fill.from.x) * dx + (g[7] - fill.from.y) * dy) * k,
4307
+ vx: 0,
4308
+ vy: 0,
4309
+ v0: 0
4310
+ };
4311
+ }
4312
+ function radialRampUV(ctx, fill) {
4313
+ const g = gradientMap(ctx, fill.units);
4314
+ const k = 1 / Math.max(fill.radius, 1e-4);
4315
+ return {
4316
+ ux: g[0] * k,
4317
+ uy: g[3] * k,
4318
+ u0: (g[6] - fill.center.x) * k,
4319
+ vx: g[1] * k,
4320
+ vy: g[4] * k,
4321
+ v0: (g[7] - fill.center.y) * k
4322
+ };
4323
+ }
4324
+ function conicRampUV(ctx, fill) {
4325
+ const g = gradientMap(ctx, fill.units);
4326
+ const cos = Math.cos(fill.angle);
4327
+ const sin = Math.sin(fill.angle);
4328
+ const dx0 = g[6] - fill.center.x;
4329
+ const dy0 = g[7] - fill.center.y;
4330
+ return {
4331
+ ux: cos * g[0] + sin * g[1],
4332
+ uy: cos * g[3] + sin * g[4],
4333
+ u0: cos * dx0 + sin * dy0,
4334
+ vx: cos * g[1] - sin * g[0],
4335
+ vy: cos * g[4] - sin * g[3],
4336
+ v0: cos * dy0 - sin * dx0
4337
+ };
4338
+ }
4339
+ function rampVertices(paint, rowV) {
4340
+ return paint.mode === PAINT_MODE_PLAIN ? { uv: { ...paint.uv, v0: rowV }, post: 1 } : { uv: paint.uv, post: rowV };
4341
+ }
4342
+ function pushRampRect(ctx, rect, paint) {
4343
+ const { staged, slot, rowV } = stageRamps(ctx, paint.stops, 4);
4344
+ const { uv, post } = rampVertices(paint, rowV);
4345
+ ctx.drawBatch.pushGradientRect(
4346
+ rect.x,
4347
+ rect.y,
4348
+ rect.width,
4349
+ rect.height,
4350
+ ctx.state.transform,
4351
+ uv,
4352
+ post,
4353
+ slot,
4354
+ paint.mode,
4355
+ 1,
4356
+ 1,
4357
+ 1,
4358
+ stagedAlpha(ctx, staged, paint.opacity)
4359
+ );
4360
+ }
4361
+ function pushRampMesh(ctx, mesh, paint) {
4362
+ const { staged, slot, rowV } = stageRamps(ctx, paint.stops, mesh.vertices.length >> 1);
4363
+ const { uv, post } = rampVertices(paint, rowV);
4364
+ ctx.drawBatch.pushGradientMesh(
4365
+ mesh,
4366
+ ctx.state.transform,
4367
+ uv,
4368
+ post,
4369
+ slot,
4370
+ paint.mode,
4371
+ 1,
4372
+ 1,
4373
+ 1,
4374
+ stagedAlpha(ctx, staged, paint.opacity)
4375
+ );
4376
+ }
4377
+ function tryStageFill(ctx, mesh, paint) {
4006
4378
  if (paint !== void 0 && canBatchMesh(mesh)) {
4007
- pushMesh(ctx, mesh, paint);
4379
+ if (paint.kind === "ramp") pushRampMesh(ctx, mesh, paint);
4380
+ else pushMesh(ctx, mesh, paint);
4008
4381
  return true;
4009
4382
  }
4010
- flushBatches(ctx);
4383
+ flushBatch(ctx);
4011
4384
  return false;
4012
4385
  }
4013
- function flushSolids(ctx) {
4014
- const batch = ctx.solidBatch;
4015
- const staged = ctx.solidState;
4386
+ function flushBatch(ctx) {
4387
+ const batch = ctx.drawBatch;
4388
+ const staged = ctx.batchState;
4016
4389
  if (batch.length === 0 || staged === void 0) return;
4017
4390
  const gl = ctx.gl;
4018
- const prog = ctx.pathFillVColor;
4391
+ const prog = ctx.batchFill;
4019
4392
  gl.useProgram(prog.handle);
4020
4393
  const indexCount = batch.uploadAndBind();
4021
4394
  setProjAndModel(ctx, prog, BATCH_MODEL);
4022
4395
  setColorUniform(ctx, prog, 1, 1, 1, 1);
4023
4396
  setAlphaUniform(ctx, prog, staged.alpha);
4397
+ gl.uniform1f(prog.uniform("u_synthBold"), staged.synthBold);
4024
4398
  setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
4025
- applyClipTest(ctx, staged.clipDepth);
4026
- gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
4027
- gl.bindVertexArray(null);
4028
- batch.reset();
4029
- ctx.solidState = void 0;
4030
- }
4031
- function stagedImageStateIsLive(ctx, staged, image, sampling) {
4032
- if (staged.image !== image) return false;
4033
- if (staged.sampling !== sampling) return false;
4034
- if (staged.clipDepth !== ctx.clipDepth) return false;
4035
- const colorMatrix = ctx.state.colorMatrix;
4036
- return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
4037
- }
4038
- function stageImage(ctx, image, sampling) {
4039
- flushSolids(ctx);
4040
- if (ctx.imageState !== void 0 && !stagedImageStateIsLive(ctx, ctx.imageState, image, sampling)) {
4041
- flushImages(ctx);
4042
- }
4043
- if (ctx.imageBatch.wouldOverflow()) flushImages(ctx);
4044
- if (ctx.imageState === void 0) {
4045
- ctx.imageState = {
4046
- image,
4047
- sampling,
4048
- colorMatrix: ctx.state.colorMatrix,
4049
- clipDepth: ctx.clipDepth
4050
- };
4399
+ gl.activeTexture(gl.TEXTURE0);
4400
+ gl.bindTexture(gl.TEXTURE_2D, ctx.whiteTexture);
4401
+ for (let i = 0; i < staged.textures.length; i++) {
4402
+ const entry = staged.textures[i];
4403
+ if (entry.kind === "atlas") {
4404
+ ctx.textureCache.bind(entry.id, i + 1);
4405
+ continue;
4406
+ }
4407
+ if (entry.kind === "ramps") {
4408
+ ctx.gradRamps.bind(i + 1);
4409
+ continue;
4410
+ }
4411
+ ctx.imageCache.bind(entry.image, i + 1);
4412
+ ctx.imageCache.setMagFilter(
4413
+ entry.image,
4414
+ entry.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
4415
+ );
4051
4416
  }
4052
- }
4053
- function flushImages(ctx) {
4054
- const batch = ctx.imageBatch;
4055
- const staged = ctx.imageState;
4056
- if (batch.length === 0 || staged === void 0) return;
4057
- const gl = ctx.gl;
4058
- const prog = ctx.imageFillVOpacity;
4059
- gl.useProgram(prog.handle);
4060
- const indexCount = batch.uploadAndBind();
4061
- setProjAndModel(ctx, prog, BATCH_MODEL);
4062
- setAlphaUniform(ctx, prog, 1);
4063
- setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
4064
- ctx.imageCache.bind(staged.image, 0);
4065
- gl.texParameteri(
4066
- gl.TEXTURE_2D,
4067
- gl.TEXTURE_MAG_FILTER,
4068
- staged.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
4069
- );
4070
- gl.uniform1i(prog.uniform("u_sampler"), 0);
4417
+ gl.uniform1iv(prog.uniform("u_samplers"), SAMPLER_UNITS);
4071
4418
  applyClipTest(ctx, staged.clipDepth);
4072
4419
  gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
4420
+ gl.activeTexture(gl.TEXTURE0);
4073
4421
  gl.bindVertexArray(null);
4074
4422
  batch.reset();
4075
- ctx.imageState = void 0;
4076
- }
4077
- function flushBatches(ctx) {
4078
- flushSolids(ctx);
4079
- flushImages(ctx);
4423
+ ctx.batchState = void 0;
4080
4424
  }
4081
4425
  function expandAnchorColors(perAnchor, handle) {
4082
4426
  const aA = handle.anchorA;
@@ -4161,8 +4505,9 @@ function paintBindContext(ctx) {
4161
4505
  setProjAndModel: (prog) => setProjAndModel(ctx, prog),
4162
4506
  spaceInverse: (units) => gradientSpaceInverse(ctx, units),
4163
4507
  bindRamp: (stops, unit2) => {
4164
- const key = ctx.gradRampCache.upload(stops);
4165
- ctx.gradRampCache.bind(key, unit2);
4508
+ const row = ctx.gradRamps.upload(stops);
4509
+ ctx.gradRamps.bind(unit2);
4510
+ return ctx.gradRamps.rowV(row);
4166
4511
  }
4167
4512
  };
4168
4513
  BIND_CONTEXTS.set(ctx, made);
@@ -4222,12 +4567,14 @@ function gradientSpaceInverse(ctx, units) {
4222
4567
  }
4223
4568
  function bindPathFillGradient(ctx, fill) {
4224
4569
  const gl = ctx.gl;
4225
- const key = ctx.gradRampCache.upload(fill.stops);
4570
+ const row = ctx.gradRamps.upload(fill.stops);
4226
4571
  gl.useProgram(ctx.gradFill.handle);
4227
4572
  setProjAndModel(ctx, ctx.gradFill);
4228
4573
  gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
4229
- ctx.gradRampCache.bind(key, 0);
4574
+ setColorMatrixUniforms(ctx, ctx.gradFill);
4575
+ ctx.gradRamps.bind(0);
4230
4576
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
4577
+ gl.uniform1f(ctx.gradFill.uniform("u_rampV"), ctx.gradRamps.rowV(row));
4231
4578
  setAlphaUniform(ctx, ctx.gradFill, ctx.state.alpha);
4232
4579
  gl.uniform1f(ctx.gradFill.uniform("u_opacity"), fill.opacity ?? 1);
4233
4580
  if (fill.fill === "linear-gradient") {
@@ -4281,7 +4628,7 @@ function rasterizePathToStencil(ctx, path) {
4281
4628
  gl.bindVertexArray(null);
4282
4629
  }
4283
4630
  function pushClip(ctx, path, newDepth) {
4284
- flushBatches(ctx);
4631
+ flushBatch(ctx);
4285
4632
  const gl = ctx.gl;
4286
4633
  const ancestors = ancestorMask(newDepth - 1);
4287
4634
  const newBit = 1 << newDepth;
@@ -4296,7 +4643,7 @@ function pushClip(ctx, path, newDepth) {
4296
4643
  gl.stencilMask(255);
4297
4644
  }
4298
4645
  function popClip(ctx, path, oldDepth) {
4299
- flushBatches(ctx);
4646
+ flushBatch(ctx);
4300
4647
  const gl = ctx.gl;
4301
4648
  gl.enable(gl.STENCIL_TEST);
4302
4649
  const oldBit = 1 << oldDepth + 1;
@@ -4343,7 +4690,7 @@ function drawPathStroke(ctx, rawCmd) {
4343
4690
  const stroke = cmd.stroke;
4344
4691
  const align = stroke.align ?? "center";
4345
4692
  if (cmd.path.kind === "polygon" && align !== "center") {
4346
- flushBatches(ctx);
4693
+ flushBatch(ctx);
4347
4694
  drawPathStrokeStenciled(ctx, cmd, align);
4348
4695
  return;
4349
4696
  }
@@ -4357,7 +4704,7 @@ function drawPathStrokeUnclipped(ctx, cmd) {
4357
4704
  const mesh = strokeMesh(cmd.path, stroke, ctx.flattenTolerance);
4358
4705
  if (mesh.indices.length === 0) return;
4359
4706
  const hasVColors = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
4360
- if (tryStageSolid(ctx, mesh, isSolid && !hasVColors ? solid2 : void 0)) return;
4707
+ if (tryStageFill(ctx, mesh, batchPaint(ctx, paint, hasVColors))) return;
4361
4708
  const handle = hasVColors ? ctx.meshCache.uploadTransient(mesh) : ctx.meshCache.uploadRecurring(mesh);
4362
4709
  if (!isSolid && !hasVColors) {
4363
4710
  drawPathFillByKind(ctx, paint, handle);
@@ -4492,28 +4839,9 @@ function drawText(ctx, cmd) {
4492
4839
  if (laid.groups.length === 0 && laid.decorations.length === 0) return;
4493
4840
  const dx = cmd.x;
4494
4841
  const dy = cmd.y + verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
4495
- const gl = ctx.gl;
4496
- applyClipTest(ctx);
4497
- const preparedPrograms = /* @__PURE__ */ new Set();
4498
- let currentProg = null;
4499
4842
  for (const group of laid.groups) {
4500
- if (group.source === "outline") {
4501
- drawTextOutlineGroup(ctx, group, dx, dy);
4502
- currentProg = null;
4503
- continue;
4504
- }
4505
- const prog = group.source === "canvas" ? ctx.textSdfR8 : ctx.textSdf;
4506
- if (prog !== currentProg) {
4507
- gl.useProgram(prog.handle);
4508
- currentProg = prog;
4509
- }
4510
- if (!preparedPrograms.has(prog)) {
4511
- preparedPrograms.add(prog);
4512
- setProjAndModel(ctx, prog);
4513
- setColorMatrixUniforms(ctx, prog);
4514
- setAlphaUniform(ctx, prog, ctx.state.alpha);
4515
- }
4516
- drawTextGroup(ctx, group, prog, dx, dy);
4843
+ if (group.source === "outline") drawTextOutlineGroup(ctx, group, dx, dy);
4844
+ else drawTextGroup(ctx, group, dx, dy);
4517
4845
  }
4518
4846
  drawTextDecorations(ctx, laid.decorations, dx, dy);
4519
4847
  }
@@ -4522,14 +4850,16 @@ function drawTextOutlineGroup(ctx, group, dx, dy) {
4522
4850
  const fill = group.fill;
4523
4851
  if (fill !== null) {
4524
4852
  const mesh = outlineGroupMesh(group, dx, dy);
4525
- if (mesh) drawPathFillByKind(ctx, fill, ctx.meshCache.uploadTransient(mesh));
4853
+ if (mesh) drawOutlineMesh(ctx, fill, mesh);
4526
4854
  }
4527
4855
  const strokePaint = group.stroke?.paint;
4528
4856
  if (!strokePaint) return;
4529
4857
  const ribbon = outlineGroupStrokeMesh(group, dx, dy, mat3.meanScaleOf(ctx.state.transform));
4530
- if (ribbon) {
4531
- drawPathFillByKind(ctx, strokePaint, ctx.meshCache.uploadTransient(ribbon));
4532
- }
4858
+ if (ribbon) drawOutlineMesh(ctx, strokePaint, ribbon);
4859
+ }
4860
+ function drawOutlineMesh(ctx, fill, mesh) {
4861
+ if (tryStageFill(ctx, mesh, batchPaint(ctx, fill, false))) return;
4862
+ drawPathFillByKind(ctx, fill, ctx.meshCache.uploadTransient(mesh));
4533
4863
  }
4534
4864
  function outlineGroupMesh(group, dx, dy) {
4535
4865
  return mergeGlyphMeshes(group, dx, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
@@ -4573,208 +4903,69 @@ function mergeGlyphMeshes(group, dx, dy, meshFor) {
4573
4903
  return { vertices, indices };
4574
4904
  }
4575
4905
  function drawTextDecorations(ctx, decorations, dx, dy) {
4576
- if (decorations.length === 0) return;
4577
- const batches = /* @__PURE__ */ new Map();
4578
4906
  for (const d of decorations) {
4579
- const rgba = "color" in d.fill ? resolveColor(d.fill.color) : [0, 0, 0, 1];
4580
- const key = rgba.join(",");
4581
- let batch = batches.get(key);
4582
- if (!batch) {
4583
- batch = { rgba, rects: [] };
4584
- batches.set(key, batch);
4585
- }
4586
- batch.rects.push(d);
4587
- }
4588
- const gl = ctx.gl;
4589
- const prog = ctx.pathFill;
4590
- gl.useProgram(prog.handle);
4591
- setProjAndModel(ctx, prog);
4592
- setColorMatrixUniforms(ctx, prog);
4593
- applyClipTest(ctx);
4594
- for (const { rgba, rects } of batches.values()) {
4595
- const floats = rects.length * 4 * 2;
4596
- if (TEXT_RULE_VERTICES.length < floats) TEXT_RULE_VERTICES = new Float32Array(grownTo(floats));
4597
- const vertices = TEXT_RULE_VERTICES;
4598
- let vi = 0;
4599
- for (const d of rects) {
4600
- const dx0 = d.x0 + dx, dx1 = d.x1 + dx;
4601
- const dy0 = d.y0 + dy, dy1 = d.y1 + dy;
4602
- vertices[vi++] = dx0;
4603
- vertices[vi++] = dy0;
4604
- vertices[vi++] = dx1;
4605
- vertices[vi++] = dy0;
4606
- vertices[vi++] = dx0;
4607
- vertices[vi++] = dy1;
4608
- vertices[vi++] = dx1;
4609
- vertices[vi++] = dy1;
4610
- }
4611
- const ring = textQuadRing(gl, prog, TEXT_RULE_STRIDE, (loc) => {
4612
- const aPos = loc("a_position");
4613
- if (aPos !== void 0) {
4614
- gl.enableVertexAttribArray(aPos);
4615
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, TEXT_RULE_STRIDE, 0);
4616
- }
4617
- });
4618
- ensureQuadIndices(gl, ring, rects.length);
4619
- bindTextQuadSlot(gl, ring, vertices.subarray(0, floats));
4620
- setColorUniform(ctx, prog, rgba[0], rgba[1], rgba[2], rgba[3]);
4621
- setAlphaUniform(ctx, prog, ctx.state.alpha);
4622
- gl.drawElements(gl.TRIANGLES, rects.length * 6, gl.UNSIGNED_INT, 0);
4623
- gl.bindVertexArray(null);
4907
+ const [r, g, b, a] = "color" in d.fill ? resolveColor(d.fill.color) : [0, 0, 0, 1];
4908
+ const staged = stageSolid(ctx, 4);
4909
+ ctx.drawBatch.pushRect(
4910
+ d.x0 + dx,
4911
+ d.y0 + dy,
4912
+ d.x1 - d.x0,
4913
+ d.y1 - d.y0,
4914
+ ctx.state.transform,
4915
+ r,
4916
+ g,
4917
+ b,
4918
+ a * (staged.foldsAlpha ? ctx.state.alpha : 1)
4919
+ );
4624
4920
  }
4625
4921
  }
4626
- function drawTextGroup(ctx, group, prog, dx, dy) {
4922
+ var SYNTH_BOLD_AMOUNT = 0.08;
4923
+ function drawTextGroup(ctx, group, dx, dy) {
4627
4924
  if (group.source === "canvas") {
4628
4925
  if (!syncDynamicPageTexture(ctx.textureCache, group.page)) return;
4629
4926
  } else {
4630
4927
  if (!ensureFontTexture(group.family, group.weight, group.style, ctx.textureCache)) return;
4631
4928
  }
4632
4929
  if (group.quads.length === 0) return;
4633
- const gl = ctx.gl;
4634
- const floats = group.quads.length * 4 * 5;
4635
- if (TEXT_GROUP_VERTICES.length < floats) TEXT_GROUP_VERTICES = new Float32Array(grownTo(floats));
4636
- const vertices = TEXT_GROUP_VERTICES;
4637
- let vi = 0;
4930
+ const atlasId = group.source === "canvas" ? dynamicPageTextureId(group.page) : textureCacheKey(group.family, group.weight, group.style);
4931
+ const mode = group.source === "canvas" ? GLYPH_MODE_R8 : GLYPH_MODE_MSDF;
4932
+ const bold = group.synthetic.bold ? SYNTH_BOLD_AMOUNT : 0;
4933
+ const tanItalic = group.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
4934
+ const color = group.fill !== null && "color" in group.fill ? resolveColor(group.fill.color) : [0, 0, 0, 1];
4935
+ const batch = ctx.drawBatch;
4936
+ const m = ctx.state.transform;
4937
+ let { staged, slot } = stageGlyphs(ctx, atlasId, bold);
4938
+ let alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
4638
4939
  for (const q of group.quads) {
4639
- const x0 = q.x0 + dx, x1 = q.x1 + dx;
4640
- const y0 = q.y0 + dy, y1 = q.y1 + dy, by = q.baselineY + dy;
4641
- vertices[vi++] = x0;
4642
- vertices[vi++] = y0;
4643
- vertices[vi++] = q.u0;
4644
- vertices[vi++] = q.v0;
4645
- vertices[vi++] = by;
4646
- vertices[vi++] = x1;
4647
- vertices[vi++] = y0;
4648
- vertices[vi++] = q.u1;
4649
- vertices[vi++] = q.v0;
4650
- vertices[vi++] = by;
4651
- vertices[vi++] = x0;
4652
- vertices[vi++] = y1;
4653
- vertices[vi++] = q.u0;
4654
- vertices[vi++] = q.v1;
4655
- vertices[vi++] = by;
4656
- vertices[vi++] = x1;
4657
- vertices[vi++] = y1;
4658
- vertices[vi++] = q.u1;
4659
- vertices[vi++] = q.v1;
4660
- vertices[vi++] = by;
4661
- }
4662
- const ring = textQuadRing(gl, prog, TEXT_GROUP_STRIDE, (loc) => {
4663
- const aPos = loc("a_position");
4664
- const aUv = loc("a_uv");
4665
- const aBase = loc("a_baselineY");
4666
- if (aPos !== void 0) {
4667
- gl.enableVertexAttribArray(aPos);
4668
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, TEXT_GROUP_STRIDE, 0);
4669
- }
4670
- if (aUv !== void 0) {
4671
- gl.enableVertexAttribArray(aUv);
4672
- gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, TEXT_GROUP_STRIDE, 8);
4673
- }
4674
- if (aBase !== void 0) {
4675
- gl.enableVertexAttribArray(aBase);
4676
- gl.vertexAttribPointer(aBase, 1, gl.FLOAT, false, TEXT_GROUP_STRIDE, 16);
4677
- }
4678
- });
4679
- ensureQuadIndices(gl, ring, group.quads.length);
4680
- bindTextQuadSlot(gl, ring, vertices.subarray(0, floats));
4681
- let r = 0, g = 0, b = 0, a = 1;
4682
- if (group.fill !== null && "color" in group.fill) {
4683
- [r, g, b, a] = resolveColor(group.fill.color);
4684
- }
4685
- setColorUniform(ctx, prog, r, g, b, a);
4686
- const synthBoldAmount = group.synthetic.bold ? 0.08 : 0;
4687
- const uSynthBold = prog.uniform("u_synthBold");
4688
- if (uSynthBold !== void 0) gl.uniform1f(uSynthBold, synthBoldAmount);
4689
- const synthItalicAmount = group.synthetic.italic ? SYNTHETIC_ITALIC_RADIANS : 0;
4690
- const uSynthItalic = prog.uniform("u_synthItalic");
4691
- if (uSynthItalic !== void 0) gl.uniform1f(uSynthItalic, synthItalicAmount);
4692
- const texId = group.source === "canvas" ? dynamicPageTextureId(group.page) : textureCacheKey(group.family, group.weight, group.style);
4693
- ctx.textureCache.bind(texId, 0);
4694
- gl.uniform1i(prog.uniform("u_atlas"), 0);
4695
- gl.drawElements(gl.TRIANGLES, group.quads.length * 6, gl.UNSIGNED_INT, 0);
4696
- gl.bindVertexArray(null);
4697
- }
4698
- var TEXT_QUAD_RING_SIZE = 64;
4699
- var TEXT_QUAD_RINGS = /* @__PURE__ */ new WeakMap();
4700
- function grownTo(need) {
4701
- let n = 256;
4702
- while (n < need) n *= 2;
4703
- return n;
4704
- }
4705
- function textQuadRing(gl, prog, stride, configureAttribs) {
4706
- const existing = TEXT_QUAD_RINGS.get(prog);
4707
- if (existing) return existing;
4708
- const ibo = gl.createBuffer();
4709
- if (!ibo) throw new Error("textQuadRing: createBuffer (IBO) returned null");
4710
- const slots = [];
4711
- for (let i = 0; i < TEXT_QUAD_RING_SIZE; i++) {
4712
- const vao = gl.createVertexArray();
4713
- const vbo = gl.createBuffer();
4714
- if (!vao) throw new Error("textQuadRing: createVertexArray returned null");
4715
- if (!vbo) throw new Error("textQuadRing: createBuffer returned null");
4716
- const bytes = grownTo(64 * 4 * stride);
4717
- gl.bindVertexArray(vao);
4718
- gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
4719
- gl.bufferData(gl.ARRAY_BUFFER, bytes, gl.DYNAMIC_DRAW);
4720
- configureAttribs((name) => prog.attribute(name));
4721
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
4722
- gl.bindVertexArray(null);
4723
- slots.push({ vao, vbo, bytes });
4724
- }
4725
- const ring = { slots, ibo, quads: 0, next: 0 };
4726
- TEXT_QUAD_RINGS.set(prog, ring);
4727
- return ring;
4728
- }
4729
- var TEXT_QUAD_INDICES = new Uint32Array(0);
4730
- var TEXT_GROUP_STRIDE = 20;
4731
- var TEXT_RULE_STRIDE = 8;
4732
- var TEXT_GROUP_VERTICES = new Float32Array(0);
4733
- var TEXT_RULE_VERTICES = new Float32Array(0);
4734
- function ensureQuadIndices(gl, ring, quads) {
4735
- if (quads <= ring.quads) return;
4736
- const grown = grownTo(quads);
4737
- if (TEXT_QUAD_INDICES.length < grown * 6) TEXT_QUAD_INDICES = new Uint32Array(grown * 6);
4738
- const idx = TEXT_QUAD_INDICES;
4739
- let ii = ring.quads * 6;
4740
- for (let q = ring.quads; q < grown; q++) {
4741
- const base = q * 4;
4742
- idx[ii++] = base;
4743
- idx[ii++] = base + 1;
4744
- idx[ii++] = base + 2;
4745
- idx[ii++] = base + 1;
4746
- idx[ii++] = base + 3;
4747
- idx[ii++] = base + 2;
4748
- }
4749
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ring.ibo);
4750
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, idx.subarray(0, grown * 6), gl.STATIC_DRAW);
4751
- ring.quads = grown;
4752
- }
4753
- function bindTextQuadSlot(gl, ring, vertices) {
4754
- const slot = ring.slots[ring.next];
4755
- ring.next = (ring.next + 1) % TEXT_QUAD_RING_SIZE;
4756
- gl.bindVertexArray(slot.vao);
4757
- gl.bindBuffer(gl.ARRAY_BUFFER, slot.vbo);
4758
- const bytes = vertices.byteLength;
4759
- if (bytes > slot.bytes) {
4760
- slot.bytes = grownTo(bytes);
4761
- gl.bufferData(gl.ARRAY_BUFFER, slot.bytes, gl.DYNAMIC_DRAW);
4762
- }
4763
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, vertices);
4764
- }
4765
- function disposeTextQuads(gl, prog) {
4766
- const ring = TEXT_QUAD_RINGS.get(prog);
4767
- if (!ring) return;
4768
- for (const slot of ring.slots) {
4769
- gl.deleteVertexArray(slot.vao);
4770
- gl.deleteBuffer(slot.vbo);
4771
- }
4772
- gl.deleteBuffer(ring.ibo);
4773
- TEXT_QUAD_RINGS.delete(prog);
4940
+ if (batch.wouldOverflow(4)) {
4941
+ flushBatch(ctx);
4942
+ ({ staged, slot } = stageGlyphs(ctx, atlasId, bold));
4943
+ alpha = color[3] * (staged.foldsAlpha ? ctx.state.alpha : 1);
4944
+ }
4945
+ batch.pushGlyph(
4946
+ q.x0 + dx,
4947
+ q.y0 + dy,
4948
+ q.x1 + dx,
4949
+ q.y1 + dy,
4950
+ q.baselineY + dy,
4951
+ tanItalic,
4952
+ m,
4953
+ q.u0,
4954
+ q.v0,
4955
+ q.u1,
4956
+ q.v1,
4957
+ color[0],
4958
+ color[1],
4959
+ color[2],
4960
+ alpha,
4961
+ slot,
4962
+ mode
4963
+ );
4964
+ }
4774
4965
  }
4775
4966
  function drawImage(ctx, cmd) {
4776
4967
  ctx.imageCache.upload(cmd.image, cmd.image);
4777
- stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
4968
+ const { staged, slot } = stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
4778
4969
  const src = cmd.source;
4779
4970
  let u0 = 0, v0 = 0, u1 = 1, v1 = 1;
4780
4971
  if (src) {
@@ -4794,7 +4985,7 @@ function drawImage(ctx, cmd) {
4794
4985
  v0 = v1;
4795
4986
  v1 = t;
4796
4987
  }
4797
- ctx.imageBatch.pushQuad(
4988
+ ctx.drawBatch.pushQuad(
4798
4989
  cmd.x,
4799
4990
  cmd.y,
4800
4991
  cmd.w,
@@ -4804,7 +4995,11 @@ function drawImage(ctx, cmd) {
4804
4995
  v0,
4805
4996
  u1,
4806
4997
  v1,
4807
- (cmd.opacity ?? 1) * ctx.state.alpha
4998
+ // Group alpha rides the vertices only where it rode the solid colors too:
4999
+ // with a real color matrix in the run it is `u_alpha`, and folding it here
5000
+ // as well would apply it twice.
5001
+ (cmd.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1),
5002
+ slot
4808
5003
  );
4809
5004
  }
4810
5005
  function drawSprites(ctx, cmd) {
@@ -4813,16 +5008,17 @@ function drawSprites(ctx, cmd) {
4813
5008
  if (count === 0) return;
4814
5009
  ctx.imageCache.upload(cmd.image, cmd.image);
4815
5010
  const sampling = cmd.sampling ?? "linear";
4816
- stageImage(ctx, cmd.image, sampling);
4817
- const batch = ctx.imageBatch;
5011
+ let { staged, slot } = stageImage(ctx, cmd.image, sampling);
5012
+ const batch = ctx.drawBatch;
4818
5013
  const m = ctx.state.transform;
4819
- const groupAlpha = ctx.state.alpha;
5014
+ let groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
4820
5015
  const tw = cmd.image.width;
4821
5016
  const th = cmd.image.height;
4822
5017
  for (let s = 0, i = 0; s < count; s++, i += SPRITE_STRIDE) {
4823
- if (batch.wouldOverflow()) {
4824
- flushImages(ctx);
4825
- stageImage(ctx, cmd.image, sampling);
5018
+ if (batch.wouldOverflow(4)) {
5019
+ flushBatch(ctx);
5020
+ ({ staged, slot } = stageImage(ctx, cmd.image, sampling));
5021
+ groupAlpha = staged.foldsAlpha ? ctx.state.alpha : 1;
4826
5022
  }
4827
5023
  const sx = data[i + 4], sy = data[i + 5], sw = data[i + 6], sh = data[i + 7];
4828
5024
  batch.pushQuad(
@@ -4835,7 +5031,8 @@ function drawSprites(ctx, cmd) {
4835
5031
  sy / th,
4836
5032
  (sx + sw) / tw,
4837
5033
  (sy + sh) / th,
4838
- data[i + 8] * groupAlpha
5034
+ data[i + 8] * groupAlpha,
5035
+ slot
4839
5036
  );
4840
5037
  }
4841
5038
  }
@@ -4997,21 +5194,21 @@ var WeaselRenderer = class {
4997
5194
  gl;
4998
5195
  pathFill;
4999
5196
  pathFillVColor;
5000
- textSdf;
5001
- textSdfR8;
5002
5197
  imageFill;
5003
- imageFillVOpacity;
5198
+ batchFill;
5004
5199
  gradFill;
5005
5200
  patternFill;
5006
5201
  meshCache;
5007
5202
  textureCache;
5008
5203
  imageCache;
5009
- gradRampCache;
5204
+ gradRamps;
5010
5205
  programRegistry = /* @__PURE__ */ new Map();
5011
5206
  quadVbo = null;
5012
5207
  quadIbo = null;
5013
- solidBatch;
5014
- imageBatch;
5208
+ drawBatch;
5209
+ /** 1x1 white, so a batch flush sampling no image still samples something —
5210
+ * see `shaders/batchFill.ts`. */
5211
+ whiteTexture = null;
5015
5212
  groupState = new GroupState();
5016
5213
  widthCss;
5017
5214
  heightCss;
@@ -5071,22 +5268,12 @@ var WeaselRenderer = class {
5071
5268
  this.pathFillVColor = new ShaderProgram(this.gl, VCOLOR_VERT_SRC, VCOLOR_FRAG_SRC);
5072
5269
  this.pathFillVColor.lookupUniforms(PATH_FILL_UNIFORMS);
5073
5270
  this.pathFillVColor.lookupAttributes(PATH_FILL_VCOLOR_ATTRIBUTES);
5074
- this.textSdf = new ShaderProgram(this.gl, TEXT_VERT_SRC, TEXT_FRAG_SRC);
5075
- this.textSdf.lookupUniforms(TEXT_SDF_UNIFORMS);
5076
- this.textSdf.lookupAttributes(TEXT_SDF_ATTRIBUTES);
5077
- this.textSdfR8 = new ShaderProgram(this.gl, TEXT_VERT_SRC, TEXT_FRAG_R8_SRC);
5078
- this.textSdfR8.lookupUniforms(TEXT_SDF_UNIFORMS);
5079
- this.textSdfR8.lookupAttributes(TEXT_SDF_ATTRIBUTES);
5080
5271
  this.imageFill = new ShaderProgram(this.gl, IMAGE_VERT_SRC, IMAGE_FRAG_SRC);
5081
5272
  this.imageFill.lookupUniforms(IMAGE_FILL_UNIFORMS);
5082
5273
  this.imageFill.lookupAttributes(IMAGE_FILL_ATTRIBUTES);
5083
- this.imageFillVOpacity = new ShaderProgram(
5084
- this.gl,
5085
- IMAGE_VOPACITY_VERT_SRC,
5086
- IMAGE_VOPACITY_FRAG_SRC
5087
- );
5088
- this.imageFillVOpacity.lookupUniforms(IMAGE_VOPACITY_UNIFORMS);
5089
- this.imageFillVOpacity.lookupAttributes(IMAGE_VOPACITY_ATTRIBUTES);
5274
+ this.batchFill = new ShaderProgram(this.gl, BATCH_VERT_SRC, BATCH_FRAG_SRC);
5275
+ this.batchFill.lookupUniforms(BATCH_FILL_UNIFORMS);
5276
+ this.batchFill.lookupAttributes(BATCH_FILL_ATTRIBUTES);
5090
5277
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
5091
5278
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
5092
5279
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
@@ -5098,10 +5285,10 @@ var WeaselRenderer = class {
5098
5285
  this.meshCache = new GLMeshCache(this.gl, aPos);
5099
5286
  this.textureCache = new GLTextureCache(this.gl);
5100
5287
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
5101
- this.gradRampCache = new GradientRampCache(this.gl);
5288
+ this.gradRamps = new GradientRampAtlas(this.gl);
5102
5289
  this.uploadQuadGeometry();
5103
- this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
5104
- this.imageBatch = new ImageBatch(this.gl, this.imageFillVOpacity);
5290
+ this.drawBatch = new DrawBatch(this.gl, this.batchFill);
5291
+ this.whiteTexture = createWhiteTexture(this.gl);
5105
5292
  }
5106
5293
  uploadQuadGeometry() {
5107
5294
  const gl = this.gl;
@@ -5217,22 +5404,12 @@ var WeaselRenderer = class {
5217
5404
  this.pathFillVColor = new ShaderProgram(this.gl, VCOLOR_VERT_SRC, VCOLOR_FRAG_SRC);
5218
5405
  this.pathFillVColor.lookupUniforms(PATH_FILL_UNIFORMS);
5219
5406
  this.pathFillVColor.lookupAttributes(PATH_FILL_VCOLOR_ATTRIBUTES);
5220
- this.textSdf = new ShaderProgram(this.gl, TEXT_VERT_SRC, TEXT_FRAG_SRC);
5221
- this.textSdf.lookupUniforms(TEXT_SDF_UNIFORMS);
5222
- this.textSdf.lookupAttributes(TEXT_SDF_ATTRIBUTES);
5223
- this.textSdfR8 = new ShaderProgram(this.gl, TEXT_VERT_SRC, TEXT_FRAG_R8_SRC);
5224
- this.textSdfR8.lookupUniforms(TEXT_SDF_UNIFORMS);
5225
- this.textSdfR8.lookupAttributes(TEXT_SDF_ATTRIBUTES);
5226
5407
  this.imageFill = new ShaderProgram(this.gl, IMAGE_VERT_SRC, IMAGE_FRAG_SRC);
5227
5408
  this.imageFill.lookupUniforms(IMAGE_FILL_UNIFORMS);
5228
5409
  this.imageFill.lookupAttributes(IMAGE_FILL_ATTRIBUTES);
5229
- this.imageFillVOpacity = new ShaderProgram(
5230
- this.gl,
5231
- IMAGE_VOPACITY_VERT_SRC,
5232
- IMAGE_VOPACITY_FRAG_SRC
5233
- );
5234
- this.imageFillVOpacity.lookupUniforms(IMAGE_VOPACITY_UNIFORMS);
5235
- this.imageFillVOpacity.lookupAttributes(IMAGE_VOPACITY_ATTRIBUTES);
5410
+ this.batchFill = new ShaderProgram(this.gl, BATCH_VERT_SRC, BATCH_FRAG_SRC);
5411
+ this.batchFill.lookupUniforms(BATCH_FILL_UNIFORMS);
5412
+ this.batchFill.lookupAttributes(BATCH_FILL_ATTRIBUTES);
5236
5413
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
5237
5414
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
5238
5415
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
@@ -5244,11 +5421,11 @@ var WeaselRenderer = class {
5244
5421
  this.meshCache = new GLMeshCache(this.gl, aPos);
5245
5422
  this.textureCache = new GLTextureCache(this.gl);
5246
5423
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
5247
- this.gradRampCache = new GradientRampCache(this.gl);
5424
+ this.gradRamps = new GradientRampAtlas(this.gl);
5248
5425
  markAllFontsNotUploaded();
5249
5426
  this.uploadQuadGeometry();
5250
- this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
5251
- this.imageBatch = new ImageBatch(this.gl, this.imageFillVOpacity);
5427
+ this.drawBatch = new DrawBatch(this.gl, this.batchFill);
5428
+ this.whiteTexture = createWhiteTexture(this.gl);
5252
5429
  for (const id of this.programRegistry.keys()) {
5253
5430
  const src = getProgramSource(id);
5254
5431
  if (!src) continue;
@@ -5269,7 +5446,7 @@ var WeaselRenderer = class {
5269
5446
  * Scope: built-in shader programs, any consumer-registered programs, the
5270
5447
  * shared quad/rect geometry, any in-flight transient meshes, and the
5271
5448
  * enumerable Map-keyed caches (`GLTextureCache` atlas/image textures,
5272
- * `GradientRampCache` ramp textures) ARE freed.
5449
+ * `GradientRampAtlas`'s ramp texture) ARE freed.
5273
5450
  *
5274
5451
  * NOT freed: `GLImageCache` (bitmap/pattern textures) and `GLMeshCache`'s
5275
5452
  * persistent per-Path mesh cache are keyed by `WeakMap`, not enumerable,
@@ -5297,7 +5474,7 @@ var WeaselRenderer = class {
5297
5474
  }
5298
5475
  this.meshCache.freeTransient();
5299
5476
  this.meshCache.drainPendingDeletes();
5300
- for (const prog of [this.pathFill, this.pathFillVColor, this.textSdf, this.textSdfR8, this.imageFill, this.imageFillVOpacity, this.gradFill, this.patternFill]) {
5477
+ for (const prog of [this.pathFill, this.pathFillVColor, this.imageFill, this.batchFill, this.gradFill, this.patternFill]) {
5301
5478
  gl.deleteProgram(prog.handle);
5302
5479
  }
5303
5480
  for (const prog of this.programRegistry.values()) {
@@ -5305,12 +5482,12 @@ var WeaselRenderer = class {
5305
5482
  }
5306
5483
  this.programRegistry.clear();
5307
5484
  this.textureCache.free();
5308
- this.gradRampCache.free();
5485
+ this.gradRamps.free();
5309
5486
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
5310
5487
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
5311
- for (const prog of [this.textSdf, this.textSdfR8, this.pathFill]) disposeTextQuads(gl, prog);
5312
- this.solidBatch.dispose();
5313
- this.imageBatch.dispose();
5488
+ this.drawBatch.dispose();
5489
+ if (this.whiteTexture) gl.deleteTexture(this.whiteTexture);
5490
+ this.whiteTexture = null;
5314
5491
  this.effectTargets.releaseAll();
5315
5492
  }
5316
5493
  /**
@@ -5337,22 +5514,20 @@ var WeaselRenderer = class {
5337
5514
  gl,
5338
5515
  pathFill: this.pathFill,
5339
5516
  pathFillVColor: this.pathFillVColor,
5340
- textSdf: this.textSdf,
5341
- textSdfR8: this.textSdfR8,
5342
5517
  imageFill: this.imageFill,
5343
- imageFillVOpacity: this.imageFillVOpacity,
5518
+ batchFill: this.batchFill,
5344
5519
  gradFill: this.gradFill,
5345
5520
  patternFill: this.patternFill,
5346
5521
  meshCache: this.meshCache,
5347
5522
  textureCache: this.textureCache,
5348
5523
  imageCache: this.imageCache,
5349
- gradRampCache: this.gradRampCache,
5524
+ gradRamps: this.gradRamps,
5350
5525
  programRegistry: this.programRegistry,
5351
5526
  ensureProgram: (id) => this.ensureProgram(id),
5352
5527
  quadVbo: this.quadVbo,
5353
5528
  quadIbo: this.quadIbo,
5354
- solidBatch: this.solidBatch,
5355
- imageBatch: this.imageBatch,
5529
+ drawBatch: this.drawBatch,
5530
+ whiteTexture: this.whiteTexture,
5356
5531
  state: this.groupState,
5357
5532
  widthCss: this.widthCss,
5358
5533
  heightCss: this.heightCss,
@@ -5367,7 +5542,7 @@ var WeaselRenderer = class {
5367
5542
  restoreTargetRect: () => this.applyTarget()
5368
5543
  };
5369
5544
  for (const cmd of commands) dispatch(ctx, cmd);
5370
- flushBatches(ctx);
5545
+ flushBatch(ctx);
5371
5546
  this.meshCache.freeTransient();
5372
5547
  }
5373
5548
  resize(dims) {
@@ -5397,28 +5572,16 @@ var WeaselRenderer = class {
5397
5572
  return this.pathFillVColor;
5398
5573
  }
5399
5574
  /** @internal */
5400
- _solidBatch() {
5401
- return this.solidBatch;
5402
- }
5403
- /** @internal */
5404
- _imageBatch() {
5405
- return this.imageBatch;
5406
- }
5407
- /** @internal */
5408
- _textSdf() {
5409
- return this.textSdf;
5410
- }
5411
- /** @internal */
5412
- _textSdfR8() {
5413
- return this.textSdfR8;
5575
+ _drawBatch() {
5576
+ return this.drawBatch;
5414
5577
  }
5415
5578
  /** @internal */
5416
5579
  _imageFill() {
5417
5580
  return this.imageFill;
5418
5581
  }
5419
5582
  /** @internal */
5420
- _imageFillVOpacity() {
5421
- return this.imageFillVOpacity;
5583
+ _batchFill() {
5584
+ return this.batchFill;
5422
5585
  }
5423
5586
  /** @internal */
5424
5587
  _gradFill() {
@@ -5441,8 +5604,8 @@ var WeaselRenderer = class {
5441
5604
  return this.imageCache;
5442
5605
  }
5443
5606
  /** @internal */
5444
- _gradRampCache() {
5445
- return this.gradRampCache;
5607
+ _gradRamps() {
5608
+ return this.gradRamps;
5446
5609
  }
5447
5610
  /** @internal */
5448
5611
  _groupState() {
@@ -5461,6 +5624,28 @@ var WeaselRenderer = class {
5461
5624
  return this.dpr;
5462
5625
  }
5463
5626
  };
5627
+ function createWhiteTexture(gl) {
5628
+ const tex = gl.createTexture();
5629
+ if (!tex) return null;
5630
+ gl.bindTexture(gl.TEXTURE_2D, tex);
5631
+ gl.texImage2D(
5632
+ gl.TEXTURE_2D,
5633
+ 0,
5634
+ gl.RGBA,
5635
+ 1,
5636
+ 1,
5637
+ 0,
5638
+ gl.RGBA,
5639
+ gl.UNSIGNED_BYTE,
5640
+ new Uint8Array([255, 255, 255, 255])
5641
+ );
5642
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
5643
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
5644
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
5645
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
5646
+ gl.bindTexture(gl.TEXTURE_2D, null);
5647
+ return tex;
5648
+ }
5464
5649
 
5465
5650
  // src/renderer/spriteSheet.ts
5466
5651
  function frameRect(sheet, index) {
@@ -5940,6 +6125,109 @@ function createParallaxLayer(opts) {
5940
6125
  }
5941
6126
  };
5942
6127
  }
6128
+
6129
+ // src/features/simulation/types.ts
6130
+ var DEFAULT_ALPHA_DECAY = 1 - Math.pow(1e-3, 1 / 300);
6131
+ var DEFAULT_ALPHA_MIN = 1e-3;
6132
+ var DEFAULT_VELOCITY_DECAY = 0.4;
6133
+
6134
+ // src/features/simulation/createSimulation.ts
6135
+ function createSimulation(opts) {
6136
+ let nodes = opts.nodes;
6137
+ let forces = opts.forces ?? [];
6138
+ let alpha = opts.alpha ?? 1;
6139
+ let alphaMin = opts.alphaMin ?? DEFAULT_ALPHA_MIN;
6140
+ let alphaDecay = opts.alphaDecay ?? DEFAULT_ALPHA_DECAY;
6141
+ let alphaTarget = opts.alphaTarget ?? 0;
6142
+ let velocityDecay = opts.velocityDecay ?? DEFAULT_VELOCITY_DECAY;
6143
+ const random = opts.random ?? Math.random;
6144
+ function prime(list) {
6145
+ list.forEach((n, i) => {
6146
+ n.index = i;
6147
+ if (n.vx == null) n.vx = 0;
6148
+ if (n.vy == null) n.vy = 0;
6149
+ });
6150
+ }
6151
+ prime(nodes);
6152
+ for (const f of forces) f.initialize?.(nodes, random);
6153
+ function step() {
6154
+ alpha += (alphaTarget - alpha) * alphaDecay;
6155
+ for (const f of forces) f(alpha);
6156
+ const friction = 1 - velocityDecay;
6157
+ for (const n of nodes) {
6158
+ if (n.fx == null) {
6159
+ n.vx = (n.vx ?? 0) * friction;
6160
+ if (!Number.isFinite(n.vx)) n.vx = 0;
6161
+ n.x += n.vx;
6162
+ if (!Number.isFinite(n.x)) n.x = 0;
6163
+ } else {
6164
+ n.x = n.fx;
6165
+ n.vx = 0;
6166
+ }
6167
+ if (n.fy == null) {
6168
+ n.vy = (n.vy ?? 0) * friction;
6169
+ if (!Number.isFinite(n.vy)) n.vy = 0;
6170
+ n.y += n.vy;
6171
+ if (!Number.isFinite(n.y)) n.y = 0;
6172
+ } else {
6173
+ n.y = n.fy;
6174
+ n.vy = 0;
6175
+ }
6176
+ }
6177
+ }
6178
+ const core = {
6179
+ get nodes() {
6180
+ return nodes;
6181
+ },
6182
+ setNodes(next) {
6183
+ nodes = next;
6184
+ prime(nodes);
6185
+ for (const f of forces) f.initialize?.(nodes, random);
6186
+ return core;
6187
+ },
6188
+ setForces(next) {
6189
+ const old = forces;
6190
+ forces = next;
6191
+ for (const f of next) {
6192
+ if (!old.includes(f)) f.initialize?.(nodes, random);
6193
+ }
6194
+ return core;
6195
+ },
6196
+ alpha(value) {
6197
+ if (value === void 0) return alpha;
6198
+ alpha = value;
6199
+ return core;
6200
+ },
6201
+ alphaTarget(value) {
6202
+ if (value === void 0) return alphaTarget;
6203
+ alphaTarget = value;
6204
+ return core;
6205
+ },
6206
+ alphaDecay(value) {
6207
+ if (value === void 0) return alphaDecay;
6208
+ alphaDecay = value;
6209
+ return core;
6210
+ },
6211
+ alphaMin(value) {
6212
+ if (value === void 0) return alphaMin;
6213
+ alphaMin = value;
6214
+ return core;
6215
+ },
6216
+ velocityDecay(value) {
6217
+ if (value === void 0) return velocityDecay;
6218
+ velocityDecay = value;
6219
+ return core;
6220
+ },
6221
+ tick(iterations = 1) {
6222
+ for (let i = 0; i < iterations; i++) step();
6223
+ return core;
6224
+ },
6225
+ isSettled() {
6226
+ return alpha < alphaMin && alphaTarget === 0;
6227
+ }
6228
+ };
6229
+ return core;
6230
+ }
5943
6231
  var documentHidden = () => typeof document !== "undefined" && document.hidden;
5944
6232
  var resolveTarget = (target) => typeof target === "function" ? target() : target?.current ?? null;
5945
6233
  function useVisibleRaf(frame, options = {}) {
@@ -6034,75 +6322,32 @@ function useVisibleRaf(frame, options = {}) {
6034
6322
  return useMemo(() => ({ request, cancel, isVisible }), [request, cancel, isVisible]);
6035
6323
  }
6036
6324
 
6037
- // src/features/simulation/types.ts
6038
- var DEFAULT_ALPHA_DECAY = 1 - Math.pow(1e-3, 1 / 300);
6039
- var DEFAULT_ALPHA_MIN = 1e-3;
6040
- var DEFAULT_VELOCITY_DECAY = 0.4;
6041
-
6042
6325
  // src/features/simulation/useSimulation.ts
6043
6326
  function useSimulation(opts) {
6044
6327
  const optsRef = useRef(opts);
6045
6328
  optsRef.current = opts;
6046
- const nodesRef = useRef(opts.nodes);
6047
- const forcesRef = useRef(opts.forces ?? []);
6048
- const alphaRef = useRef(opts.alpha ?? 1);
6049
- const alphaMinRef = useRef(opts.alphaMin ?? DEFAULT_ALPHA_MIN);
6050
- const alphaDecayRef = useRef(opts.alphaDecay ?? DEFAULT_ALPHA_DECAY);
6051
- const alphaTargetRef = useRef(opts.alphaTarget ?? 0);
6052
- const velocityDecayRef = useRef(opts.velocityDecay ?? DEFAULT_VELOCITY_DECAY);
6329
+ const coreRef = useRef(null);
6330
+ coreRef.current ??= createSimulation(opts);
6331
+ const core = coreRef.current;
6053
6332
  const endedRef = useRef(false);
6054
6333
  const mountedRef = useRef(true);
6055
- const clockRef = useRef(null);
6056
- if (clockRef.current === null) {
6057
- clockRef.current = {
6058
- requestFrame: opts.requestFrame ?? ((cb) => requestAnimationFrame(cb)),
6059
- cancelFrame: opts.cancelFrame ?? ((h) => cancelAnimationFrame(h))
6060
- };
6061
- }
6062
6334
  const frameLoop = useVisibleRaf(
6063
6335
  () => {
6064
6336
  rafTick();
6065
6337
  },
6066
6338
  {
6067
- requestFrame: clockRef.current.requestFrame,
6068
- cancelFrame: clockRef.current.cancelFrame
6339
+ requestFrame: opts.requestFrame,
6340
+ cancelFrame: opts.cancelFrame
6069
6341
  }
6070
6342
  );
6071
- const tickOnce = () => {
6072
- alphaRef.current += (alphaTargetRef.current - alphaRef.current) * alphaDecayRef.current;
6073
- const forces = forcesRef.current;
6074
- for (const f of forces) f(alphaRef.current);
6075
- const friction = 1 - velocityDecayRef.current;
6076
- const nodes = nodesRef.current;
6077
- for (const n of nodes) {
6078
- if (n.fx == null) {
6079
- n.vx = (n.vx ?? 0) * friction;
6080
- if (!Number.isFinite(n.vx)) n.vx = 0;
6081
- n.x += n.vx;
6082
- if (!Number.isFinite(n.x)) n.x = 0;
6083
- } else {
6084
- n.x = n.fx;
6085
- n.vx = 0;
6086
- }
6087
- if (n.fy == null) {
6088
- n.vy = (n.vy ?? 0) * friction;
6089
- if (!Number.isFinite(n.vy)) n.vy = 0;
6090
- n.y += n.vy;
6091
- if (!Number.isFinite(n.y)) n.y = 0;
6092
- } else {
6093
- n.y = n.fy;
6094
- n.vy = 0;
6095
- }
6096
- }
6097
- };
6098
6343
  const stopRaf = () => {
6099
6344
  frameLoop.cancel();
6100
6345
  };
6101
6346
  const rafTick = () => {
6102
6347
  if (!mountedRef.current) return;
6103
- tickOnce();
6104
- optsRef.current.onTick?.(nodesRef.current);
6105
- if (alphaRef.current < alphaMinRef.current) {
6348
+ core.tick();
6349
+ optsRef.current.onTick?.(core.nodes);
6350
+ if (core.alpha() < core.alphaMin()) {
6106
6351
  stopRaf();
6107
6352
  if (!endedRef.current) {
6108
6353
  endedRef.current = true;
@@ -6116,17 +6361,9 @@ function useSimulation(opts) {
6116
6361
  if (!mountedRef.current) return;
6117
6362
  frameLoop.request();
6118
6363
  };
6119
- const initOnceRef = useRef(false);
6120
- if (!initOnceRef.current) {
6121
- initOnceRef.current = true;
6122
- nodesRef.current.forEach((n, i) => {
6123
- n.index = i;
6124
- if (n.vx == null) n.vx = 0;
6125
- if (n.vy == null) n.vy = 0;
6126
- });
6127
- for (const f of forcesRef.current) {
6128
- f.initialize?.(nodesRef.current, Math.random);
6129
- }
6364
+ const startedRef = useRef(false);
6365
+ if (!startedRef.current) {
6366
+ startedRef.current = true;
6130
6367
  startRaf();
6131
6368
  }
6132
6369
  useEffect(() => {
@@ -6139,80 +6376,195 @@ function useSimulation(opts) {
6139
6376
  const handle = useMemo(() => {
6140
6377
  const h = {
6141
6378
  get nodes() {
6142
- return nodesRef.current;
6379
+ return core.nodes;
6143
6380
  },
6144
6381
  setNodes(newNodes) {
6145
- nodesRef.current = newNodes;
6146
- newNodes.forEach((n, i) => {
6147
- n.index = i;
6148
- if (n.vx == null) n.vx = 0;
6149
- if (n.vy == null) n.vy = 0;
6150
- });
6151
- for (const f of forcesRef.current) {
6152
- f.initialize?.(newNodes, Math.random);
6153
- }
6382
+ core.setNodes(newNodes);
6154
6383
  return h;
6155
6384
  },
6156
6385
  setForces(newForces) {
6157
- const old = forcesRef.current;
6158
- forcesRef.current = newForces;
6159
- for (const f of newForces) {
6160
- if (!old.includes(f)) {
6161
- f.initialize?.(nodesRef.current, Math.random);
6162
- }
6163
- }
6386
+ core.setForces(newForces);
6164
6387
  return h;
6165
6388
  },
6166
6389
  alpha(value) {
6167
- if (value === void 0) return alphaRef.current;
6168
- alphaRef.current = value;
6169
- if (value >= alphaMinRef.current) endedRef.current = false;
6390
+ if (value === void 0) return core.alpha();
6391
+ core.alpha(value);
6392
+ if (value >= core.alphaMin()) endedRef.current = false;
6170
6393
  return h;
6171
6394
  },
6172
6395
  alphaTarget(value) {
6173
- if (value === void 0) return alphaTargetRef.current;
6174
- alphaTargetRef.current = value;
6396
+ if (value === void 0) return core.alphaTarget();
6397
+ core.alphaTarget(value);
6175
6398
  if (value > 0) endedRef.current = false;
6176
6399
  return h;
6177
6400
  },
6178
6401
  alphaDecay(value) {
6179
- if (value === void 0) return alphaDecayRef.current;
6180
- alphaDecayRef.current = value;
6402
+ if (value === void 0) return core.alphaDecay();
6403
+ core.alphaDecay(value);
6181
6404
  return h;
6182
6405
  },
6183
6406
  alphaMin(value) {
6184
- if (value === void 0) return alphaMinRef.current;
6185
- alphaMinRef.current = value;
6407
+ if (value === void 0) return core.alphaMin();
6408
+ core.alphaMin(value);
6186
6409
  return h;
6187
6410
  },
6188
6411
  velocityDecay(value) {
6189
- if (value === void 0) return velocityDecayRef.current;
6190
- velocityDecayRef.current = value;
6412
+ if (value === void 0) return core.velocityDecay();
6413
+ core.velocityDecay(value);
6191
6414
  return h;
6192
6415
  },
6193
6416
  restart() {
6194
- if (alphaRef.current < alphaMinRef.current) {
6195
- alphaRef.current = 1;
6417
+ if (core.alpha() < core.alphaMin()) core.alpha(1);
6418
+ endedRef.current = false;
6419
+ startRaf();
6420
+ return h;
6421
+ },
6422
+ stop() {
6423
+ stopRaf();
6424
+ return h;
6425
+ },
6426
+ tick(iterations = 1) {
6427
+ core.tick(iterations);
6428
+ return h;
6429
+ },
6430
+ isSettled() {
6431
+ return core.isSettled();
6432
+ }
6433
+ };
6434
+ return h;
6435
+ }, []);
6436
+ return handle;
6437
+ }
6438
+
6439
+ // src/interactions/actions/previewOverrides.ts
6440
+ function syncPreviewOverrides(state) {
6441
+ const { overrides } = state.scene;
6442
+ for (const [id, pose] of state.previews) {
6443
+ const existing = state.overrideEntries.get(id);
6444
+ if (existing) {
6445
+ existing.pose = pose;
6446
+ continue;
6447
+ }
6448
+ const entry = { pose };
6449
+ state.overrideEntries.set(id, entry);
6450
+ overrides.set(id, entry);
6451
+ }
6452
+ for (const id of state.overrideEntries.keys()) {
6453
+ if (state.previews.has(id)) continue;
6454
+ state.overrideEntries.delete(id);
6455
+ overrides.clear(id);
6456
+ }
6457
+ overrides.commit();
6458
+ }
6459
+ function dropPreviewOverrides(state) {
6460
+ if (state.overrideEntries.size === 0) return;
6461
+ for (const id of state.overrideEntries.keys()) state.scene.overrides.clear(id);
6462
+ state.overrideEntries.clear();
6463
+ state.scene.overrides.commit();
6464
+ }
6465
+
6466
+ // src/features/poseRun/usePoseRun.ts
6467
+ var EMPTY = /* @__PURE__ */ new Map();
6468
+ function usePoseRun(opts) {
6469
+ const optsRef = useRef(opts);
6470
+ optsRef.current = opts;
6471
+ const state = useRef({
6472
+ previews: /* @__PURE__ */ new Map(),
6473
+ overrideEntries: /* @__PURE__ */ new Map()
6474
+ });
6475
+ const running = useRef(false);
6476
+ const frame = useRef(0);
6477
+ const mounted = useRef(true);
6478
+ const loop = useVisibleRaf(() => {
6479
+ tick();
6480
+ }, {
6481
+ requestFrame: opts.requestFrame,
6482
+ cancelFrame: opts.cancelFrame
6483
+ });
6484
+ const syncState = () => ({
6485
+ scene: optsRef.current.scene,
6486
+ previews: state.current.previews,
6487
+ overrideEntries: state.current.overrideEntries
6488
+ });
6489
+ const pinnedNow = () => {
6490
+ const { overrides } = optsRef.current.scene;
6491
+ let pinned;
6492
+ for (const id of overrides.ids()) {
6493
+ const entry = overrides.get(id);
6494
+ if (entry === void 0 || entry === state.current.overrideEntries.get(id)) continue;
6495
+ if (entry.pose === void 0) continue;
6496
+ (pinned ??= /* @__PURE__ */ new Map()).set(id, entry.pose);
6497
+ }
6498
+ return pinned ?? EMPTY;
6499
+ };
6500
+ const commit = () => {
6501
+ const { previews } = state.current;
6502
+ const scene = optsRef.current.scene;
6503
+ let moved = 0;
6504
+ if (previews.size > 0) {
6505
+ scene.batch(optsRef.current.label ?? "Layout", () => {
6506
+ for (const [id, pose] of previews) {
6507
+ if (scene.get(id) === void 0) continue;
6508
+ scene.setPose(id, pose);
6509
+ moved++;
6196
6510
  }
6197
- endedRef.current = false;
6198
- startRaf();
6199
- return h;
6200
- },
6201
- stop() {
6202
- stopRaf();
6203
- return h;
6204
- },
6205
- tick(iterations = 1) {
6206
- for (let i = 0; i < iterations; i++) tickOnce();
6207
- return h;
6208
- },
6209
- isSettled() {
6210
- return alphaRef.current < alphaMinRef.current && alphaTargetRef.current === 0;
6211
- }
6511
+ });
6512
+ }
6513
+ dropPreviewOverrides(syncState());
6514
+ previews.clear();
6515
+ optsRef.current.onCommit?.(moved);
6516
+ };
6517
+ const halt = () => {
6518
+ running.current = false;
6519
+ loop.cancel();
6520
+ };
6521
+ const tick = () => {
6522
+ if (!mounted.current || !running.current) return;
6523
+ const pinned = pinnedNow();
6524
+ const { poses, done } = optsRef.current.step({ pinned, frame: frame.current });
6525
+ frame.current++;
6526
+ state.current.previews.clear();
6527
+ for (const [id, pose] of poses) {
6528
+ if (pinned.has(id)) continue;
6529
+ state.current.previews.set(id, pose);
6530
+ }
6531
+ syncPreviewOverrides(syncState());
6532
+ if (done) {
6533
+ halt();
6534
+ commit();
6535
+ return;
6536
+ }
6537
+ loop.request();
6538
+ };
6539
+ useEffect(() => {
6540
+ mounted.current = true;
6541
+ return () => {
6542
+ mounted.current = false;
6543
+ loop.cancel();
6544
+ dropPreviewOverrides(syncState());
6545
+ state.current.previews.clear();
6212
6546
  };
6213
- return h;
6214
- }, []);
6215
- return handle;
6547
+ }, [loop]);
6548
+ return useMemo(() => ({
6549
+ start() {
6550
+ if (running.current || !mounted.current) return;
6551
+ running.current = true;
6552
+ frame.current = 0;
6553
+ loop.request();
6554
+ },
6555
+ stop() {
6556
+ if (!running.current) return;
6557
+ halt();
6558
+ commit();
6559
+ },
6560
+ cancel() {
6561
+ halt();
6562
+ dropPreviewOverrides(syncState());
6563
+ state.current.previews.clear();
6564
+ },
6565
+ isRunning: () => running.current
6566
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6567
+ }), [loop]);
6216
6568
  }
6217
6569
 
6218
6570
  // src/core/device/profile.ts
@@ -7162,31 +7514,158 @@ function resolveParams(params) {
7162
7514
  return typeof params === "function" ? params() : params;
7163
7515
  }
7164
7516
 
7165
- // src/interactions/actions/previewOverrides.ts
7166
- function syncPreviewOverrides(state) {
7167
- const { overrides } = state.scene;
7168
- for (const [id, pose] of state.previews) {
7169
- const existing = state.overrideEntries.get(id);
7170
- if (existing) {
7171
- existing.pose = pose;
7172
- continue;
7517
+ // src/core/scene/dependents.ts
7518
+ var NONE = Object.freeze([]);
7519
+ function dependencyIdsOf(node, childrenOf) {
7520
+ const deps = node.dependsOn;
7521
+ if (deps === void 0) return NONE;
7522
+ return deps === "children" ? childrenOf(node.id) : deps;
7523
+ }
7524
+ function createDependentsIndex() {
7525
+ const forward = /* @__PURE__ */ new Map();
7526
+ const reverse = /* @__PURE__ */ new Map();
7527
+ function detachOwnDeps(id) {
7528
+ const old = reverse.get(id);
7529
+ if (old === void 0) return;
7530
+ for (const dep of old) {
7531
+ const set = forward.get(dep);
7532
+ if (set === void 0) continue;
7533
+ set.delete(id);
7534
+ if (set.size === 0) forward.delete(dep);
7173
7535
  }
7174
- const entry = { pose };
7175
- state.overrideEntries.set(id, entry);
7176
- overrides.set(id, entry);
7536
+ reverse.delete(id);
7177
7537
  }
7178
- for (const id of state.overrideEntries.keys()) {
7179
- if (state.previews.has(id)) continue;
7180
- state.overrideEntries.delete(id);
7181
- overrides.clear(id);
7538
+ return {
7539
+ add(id, deps) {
7540
+ detachOwnDeps(id);
7541
+ if (deps.length === 0) return;
7542
+ reverse.set(id, [...deps]);
7543
+ for (const dep of deps) {
7544
+ let set = forward.get(dep);
7545
+ if (set === void 0) {
7546
+ set = /* @__PURE__ */ new Set();
7547
+ forward.set(dep, set);
7548
+ }
7549
+ set.add(id);
7550
+ }
7551
+ },
7552
+ remove(id) {
7553
+ detachOwnDeps(id);
7554
+ },
7555
+ clear() {
7556
+ forward.clear();
7557
+ reverse.clear();
7558
+ },
7559
+ isEmpty() {
7560
+ return forward.size === 0;
7561
+ },
7562
+ dependentsOf(id) {
7563
+ const set = forward.get(id);
7564
+ return set === void 0 ? NONE : [...set];
7565
+ },
7566
+ transitiveDependentsOf(id) {
7567
+ const direct = forward.get(id);
7568
+ if (direct === void 0) return NONE;
7569
+ const out = /* @__PURE__ */ new Set();
7570
+ const stack = [...direct];
7571
+ while (stack.length > 0) {
7572
+ const next = stack.pop();
7573
+ if (out.has(next)) continue;
7574
+ out.add(next);
7575
+ for (const d of forward.get(next) ?? []) stack.push(d);
7576
+ }
7577
+ return out;
7578
+ }
7579
+ };
7580
+ }
7581
+ function sameDependsOn(a, b) {
7582
+ if (a === b) return true;
7583
+ if (a === void 0 || b === void 0) return false;
7584
+ if (a === "children" || b === "children") return false;
7585
+ if (a.length !== b.length) return false;
7586
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
7587
+ return true;
7588
+ }
7589
+
7590
+ // src/core/scene/derivedPath.ts
7591
+ var SLOT = "kit:derivedPath";
7592
+ var resolving = /* @__PURE__ */ new Set();
7593
+ var cycleHits = 0;
7594
+ function resolveDerivedPath(node, depOf2, childrenOf) {
7595
+ const derivePath = node.derivePath;
7596
+ if (derivePath === void 0) return null;
7597
+ const ids = dependencyIdsOf(node, childrenOf);
7598
+ if (ids.length === 0) return null;
7599
+ if (resolving.has(node.id)) {
7600
+ cycleHits++;
7601
+ return null;
7602
+ }
7603
+ resolving.add(node.id);
7604
+ const hitsBefore = cycleHits;
7605
+ try {
7606
+ const value = nodeMemo(
7607
+ node,
7608
+ SLOT,
7609
+ node.pose,
7610
+ () => derivePath(node, ids.map((id) => depOf2(id)))
7611
+ );
7612
+ if (cycleHits !== hitsBefore) dropPoseKeyedMemoSlots(node);
7613
+ return value;
7614
+ } finally {
7615
+ resolving.delete(node.id);
7182
7616
  }
7183
- overrides.commit();
7184
7617
  }
7185
- function dropPreviewOverrides(state) {
7186
- if (state.overrideEntries.size === 0) return;
7187
- for (const id of state.overrideEntries.keys()) state.scene.overrides.clear(id);
7188
- state.overrideEntries.clear();
7189
- state.scene.overrides.commit();
7618
+
7619
+ // src/core/scene/effectivePose.ts
7620
+ var SLOT2 = "kit:derivedPose";
7621
+ var resolving2 = /* @__PURE__ */ new Set();
7622
+ var cycleHits2 = 0;
7623
+ function derivedPose(source, node) {
7624
+ const derive = node.derivePose;
7625
+ if (derive === void 0) return null;
7626
+ const ids = dependencyIdsOf(node, source.childrenOf);
7627
+ if (ids.length === 0) return null;
7628
+ if (resolving2.has(node.id)) {
7629
+ cycleHits2++;
7630
+ return null;
7631
+ }
7632
+ resolving2.add(node.id);
7633
+ const hitsBefore = cycleHits2;
7634
+ try {
7635
+ const value = nodeMemo(
7636
+ node,
7637
+ SLOT2,
7638
+ node.pose,
7639
+ () => derive(node, ids.map((id) => derivedDepOf(source, id)))
7640
+ );
7641
+ if (cycleHits2 !== hitsBefore) dropPoseKeyedMemoSlots(node);
7642
+ return value;
7643
+ } finally {
7644
+ resolving2.delete(node.id);
7645
+ }
7646
+ }
7647
+ function derivedDepOf(source, id) {
7648
+ const node = source.get(id);
7649
+ if (node === void 0) return void 0;
7650
+ return {
7651
+ node,
7652
+ pose: effectivePose(source, node),
7653
+ get path() {
7654
+ return resolveDerivedPath(
7655
+ node,
7656
+ (depId) => derivedDepOf(source, depId),
7657
+ (depId) => source.childrenOf(depId)
7658
+ );
7659
+ }
7660
+ };
7661
+ }
7662
+ function documentPose(source, node) {
7663
+ return derivedPose(source, node) ?? node.pose;
7664
+ }
7665
+ function effectivePose(source, node) {
7666
+ const override = source.overrides.get(node.id)?.pose;
7667
+ if (override !== void 0) return override;
7668
+ return derivedPose(source, node) ?? node.pose;
7190
7669
  }
7191
7670
 
7192
7671
  // src/core/scene/types.ts
@@ -7218,6 +7697,86 @@ function geometryDataOp(projection, node, m, label) {
7218
7697
  return createSetDataOp({ id: node.id, from: node.data, to: next, label, coalesceKey: `setData:${node.id}` });
7219
7698
  }
7220
7699
 
7700
+ // src/core/scene/nodeFnFields.ts
7701
+ var NODE_FN_FIELDS = [
7702
+ {
7703
+ field: "clipFromPose",
7704
+ opKey: "clipKey",
7705
+ jsonKey: "clipFromPoseKey",
7706
+ containersOnly: true,
7707
+ lost: "clip"
7708
+ },
7709
+ {
7710
+ field: "derivePath",
7711
+ opKey: "derivePathKey",
7712
+ jsonKey: "derivePathKey",
7713
+ containersOnly: false,
7714
+ lost: "derived path"
7715
+ },
7716
+ {
7717
+ field: "derivePose",
7718
+ opKey: "derivePoseKey",
7719
+ jsonKey: "derivePoseKey",
7720
+ containersOnly: false,
7721
+ lost: "derived pose"
7722
+ }
7723
+ ];
7724
+ function createNodeFnFields(registry2) {
7725
+ return NODE_FN_FIELDS.map((spec) => {
7726
+ const table = registry2[spec.field];
7727
+ const reverse = /* @__PURE__ */ new Map();
7728
+ if (table) for (const [key, fn] of Object.entries(table)) reverse.set(fn, key);
7729
+ const pending = /* @__PURE__ */ new Map();
7730
+ const applies = (node) => !spec.containersOnly || node.kind === "container";
7731
+ return {
7732
+ spec,
7733
+ keyOf: (fn) => reverse.get(fn),
7734
+ fnOf: (key) => table?.[key],
7735
+ read: (node) => applies(node) ? node[spec.field] : void 0,
7736
+ write: (node, fn) => {
7737
+ if (applies(node)) node[spec.field] = fn;
7738
+ },
7739
+ pending
7740
+ };
7741
+ });
7742
+ }
7743
+ function restoreFn(field, node, key, site, warn) {
7744
+ const fn = field.fnOf(key);
7745
+ if (fn === void 0) {
7746
+ warn(
7747
+ "scene",
7748
+ `${site}: ${field.spec.opKey} "${key}" not in this scene's registry \u2014 node "${node.id}" restored without its ${field.spec.lost}. Register a function with this key in the registry option to restore it.`
7749
+ );
7750
+ return;
7751
+ }
7752
+ field.write(node, fn);
7753
+ field.pending.set(node.id, fn);
7754
+ }
7755
+ function fnKeysOf(fields, node, which, onUnregistered) {
7756
+ const out = {};
7757
+ for (const field of fields) {
7758
+ const fn = field.read(node);
7759
+ if (fn === void 0) continue;
7760
+ const key = field.keyOf(fn);
7761
+ if (key === void 0) {
7762
+ onUnregistered?.(field);
7763
+ continue;
7764
+ }
7765
+ out[field.spec[which]] = key;
7766
+ }
7767
+ return out;
7768
+ }
7769
+ function fnFieldsOfNode(node) {
7770
+ const src = node;
7771
+ const out = {};
7772
+ for (const spec of NODE_FN_FIELDS) {
7773
+ if (spec.containersOnly && node.kind !== "container") continue;
7774
+ const fn = src[spec.field];
7775
+ if (fn !== void 0) out[spec.field] = fn;
7776
+ }
7777
+ return out;
7778
+ }
7779
+
7221
7780
  // src/interactions/actions/move/gestureAdapter.ts
7222
7781
  function moveGestureAdapter(scene) {
7223
7782
  return {
@@ -7225,7 +7784,7 @@ function moveGestureAdapter(scene) {
7225
7784
  getNodes: () => {
7226
7785
  return [...scene.renderOrderNodes()];
7227
7786
  },
7228
- getPose: (id) => scene.get(asNodeId(id)).pose,
7787
+ getPose: (id) => documentPose(scene, scene.get(asNodeId(id))),
7229
7788
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
7230
7789
  setPose: (id, pose) => scene.setPose(asNodeId(id), pose),
7231
7790
  setParent: (id, parentId) => scene.move(asNodeId(id), parentId === null ? null : asNodeId(parentId)),
@@ -7240,8 +7799,7 @@ function moveGestureAdapter(scene) {
7240
7799
  ...index !== void 0 ? { index } : {},
7241
7800
  ...node.parent !== null ? { parent: node.parent } : {},
7242
7801
  ...node.dependsOn !== void 0 ? { dependsOn: node.dependsOn } : {},
7243
- ...node.derivePath !== void 0 ? { derivePath: node.derivePath } : {},
7244
- ...node.kind === "container" && node.clipFromPose !== void 0 ? { clipFromPose: node.clipFromPose } : {}
7802
+ ...fnFieldsOfNode(node)
7245
7803
  }),
7246
7804
  getChildren: (parentId) => parentId === null ? [...scene.roots] : [...scene.childrenOf(asNodeId(parentId))],
7247
7805
  setChildOrder: (parentId, ids) => {
@@ -7489,7 +8047,7 @@ function unreleasedIds(scratch, released) {
7489
8047
  }
7490
8048
  function scenePoseAdapter(scene) {
7491
8049
  return {
7492
- getPose: (id) => scene.get(id).pose,
8050
+ getPose: (id) => documentPose(scene, scene.get(id)),
7493
8051
  getParent: (id) => scene.get(id)?.parent ?? null
7494
8052
  };
7495
8053
  }
@@ -7902,7 +8460,7 @@ function defaultCommitAdapter(scene) {
7902
8460
  getNodes: () => {
7903
8461
  return [...scene.renderOrderNodes()];
7904
8462
  },
7905
- getPose: (id) => scene.get(asNodeId(id)).pose,
8463
+ getPose: (id) => documentPose(scene, scene.get(asNodeId(id))),
7906
8464
  getSelection: () => [...scene.getSelection()],
7907
8465
  setSelection: (ids) => scene.setSelection(ids.map(asNodeId)),
7908
8466
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -7936,12 +8494,8 @@ function defaultCommitAdapter(scene) {
7936
8494
  // source removal) re-inserts at the recorded z-order slot instead
7937
8495
  // of appending at the end.
7938
8496
  ...index !== void 0 ? { index } : {},
7939
- // The function-valued fields travel with the node, or undo of a delete
7940
- // brings an edge back as a static path that never follows its endpoints
7941
- // again, and a clipped container back with nothing clipped.
7942
8497
  ...node.dependsOn !== void 0 ? { dependsOn: node.dependsOn } : {},
7943
- ...node.derivePath !== void 0 ? { derivePath: node.derivePath } : {},
7944
- ...node.kind === "container" && node.clipFromPose !== void 0 ? { clipFromPose: node.clipFromPose } : {}
8498
+ ...fnFieldsOfNode(node)
7945
8499
  })
7946
8500
  };
7947
8501
  }
@@ -10272,6 +10826,26 @@ var duplicateAction = {
10272
10826
  enabled: requiresSelection
10273
10827
  };
10274
10828
 
10829
+ // src/core/scene/kitRegistry.ts
10830
+ var UNION_OF_CHILDREN = "kit:unionOfChildren";
10831
+ function unionOfChildren(_node, deps) {
10832
+ const poses = [];
10833
+ for (const d of deps) {
10834
+ if (d !== void 0) poses.push(d.pose);
10835
+ }
10836
+ if (poses.length === 0) return null;
10837
+ return unionAABB(poses);
10838
+ }
10839
+ function withKitRegistry(registry2) {
10840
+ return {
10841
+ ...registry2,
10842
+ derivePose: {
10843
+ [UNION_OF_CHILDREN]: unionOfChildren,
10844
+ ...registry2.derivePose
10845
+ }
10846
+ };
10847
+ }
10848
+
10275
10849
  // src/interactions/actions/defaults/group.ts
10276
10850
  var containerIdCounter = 0;
10277
10851
  function freshContainerId() {
@@ -10309,7 +10883,9 @@ var groupAction = {
10309
10883
  layer,
10310
10884
  pose,
10311
10885
  data: {},
10312
- parent: parent ?? null
10886
+ parent: parent ?? null,
10887
+ dependsOn: "children",
10888
+ derivePose: unionOfChildren
10313
10889
  },
10314
10890
  label: "Group"
10315
10891
  })
@@ -12727,7 +13303,7 @@ function useStandardActions(opts) {
12727
13303
  }, [reg, depReg, excludeKey]);
12728
13304
  }
12729
13305
  var IDENTITY_EXPAND2 = (ids) => ids;
12730
- var EMPTY = Object.freeze([]);
13306
+ var EMPTY2 = Object.freeze([]);
12731
13307
  function useResizePolicy(options) {
12732
13308
  const optsRef = useRef(options);
12733
13309
  optsRef.current = options;
@@ -12735,7 +13311,7 @@ function useResizePolicy(options) {
12735
13311
  const o = optsRef.current;
12736
13312
  return {
12737
13313
  constraints: o.constraints ?? DEFAULT_RESIZE_BEHAVIORS,
12738
- pointSnap: o.pointSnap ?? EMPTY,
13314
+ pointSnap: o.pointSnap ?? EMPTY2,
12739
13315
  expandIds: o.expandIds ?? IDENTITY_EXPAND2,
12740
13316
  projection: o.projection ?? AUTO_POSE_DESCRIPTOR
12741
13317
  };
@@ -17348,7 +17924,7 @@ var TEXT_PAINTER = {
17348
17924
  const p = pose;
17349
17925
  const y = p.y;
17350
17926
  const paint = { fill: d.fill, stroke: resolveNodeStroke(d.stroke) ?? void 0 };
17351
- return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, d.style, void 0, p.height, void 0, paint)] : [textCommand(p.x, y, d.text, d.style, void 0, p.height, void 0, paint)];
17927
+ return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, d.style, void 0, p.height, d.verticalAlign, paint)] : [textCommand(p.x, y, d.text, d.style, void 0, p.height, d.verticalAlign, paint)];
17352
17928
  }),
17353
17929
  // The pose is a *wrap box*, not a bounding box — "Away" in a 300-unit box
17354
17930
  // leaves most of it empty, and a pose-rect silhouette claims all of it. The
@@ -17372,7 +17948,11 @@ var TEXT_PAINTER = {
17372
17948
  height: p.height,
17373
17949
  text: d.text,
17374
17950
  runs: d.runs,
17375
- style: d.style
17951
+ style: d.style,
17952
+ // `textLineBoxes` applies the same shift `drawText` does, so picking
17953
+ // follows a centered or bottom-aligned block down its box instead of
17954
+ // staying where a top-aligned one would have drawn.
17955
+ verticalAlign: d.verticalAlign
17376
17956
  },
17377
17957
  { maxWidth: Infinity }
17378
17958
  );
@@ -17607,12 +18187,15 @@ var DERIVED_PAINTER = {
17607
18187
  const declared = resolveNodeFill(d?.fill, strokeSpec === null ? DEFAULT_SHAPE_FILL : null);
17608
18188
  const fill = declared && resolveFillPattern(fillInPoseFrame(declared, box));
17609
18189
  const stroke = strokeSpec && strokeInPoseFrame(strokeSpec, box);
17610
- return [{
18190
+ const cmd = {
17611
18191
  kind: "path",
17612
18192
  path,
17613
18193
  ...fill ? { fill } : {},
17614
18194
  ...stroke ? { stroke } : {}
17615
- }];
18195
+ };
18196
+ if (!stroke) return [cmd];
18197
+ const width = resolveStrokeWidth(stroke.width ?? 1, 1);
18198
+ return [cmd, ...markerDrawCommands(path, stroke, width, void 0)];
17616
18199
  },
17617
18200
  // The derived path *is* the silhouette, and it is already absolute — the
17618
18201
  // pose is a placeholder, so nothing here can be recovered from it. Without
@@ -17656,95 +18239,27 @@ function registerBuiltInShapePainters() {
17656
18239
  registerNodeShape(PATH_PAINTER);
17657
18240
  registerNodeShape(SHAPE_PAINTER);
17658
18241
  registerNodeShape(IMAGE_PAINTER);
17659
- registerNodeShape(RECT_FALLBACK_PAINTER);
17660
- }
17661
- registerBuiltInShapePainters();
17662
-
17663
- // src/core/scene/poseOverrides.ts
17664
- function createPoseOverrides(getNode, onInvalidate) {
17665
- const entries = /* @__PURE__ */ new Map();
17666
- const listeners = /* @__PURE__ */ new Set();
17667
- let generation2 = 0;
17668
- function invalidate(id) {
17669
- const node = getNode(id);
17670
- if (node) dropPoseKeyedMemoSlots(node);
17671
- onInvalidate?.(id);
17672
- }
17673
- function published() {
17674
- generation2++;
17675
- for (const listener of listeners) listener();
17676
- }
17677
- return {
17678
- set(id, entry) {
17679
- entries.set(id, entry);
17680
- invalidate(id);
17681
- published();
17682
- },
17683
- get(id) {
17684
- return entries.get(id);
17685
- },
17686
- has(id) {
17687
- return entries.has(id);
17688
- },
17689
- ids() {
17690
- return [...entries.keys()];
17691
- },
17692
- clear(id) {
17693
- if (!entries.delete(id)) return;
17694
- invalidate(id);
17695
- published();
17696
- },
17697
- clearAll() {
17698
- if (entries.size === 0) return;
17699
- for (const id of entries.keys()) invalidate(id);
17700
- entries.clear();
17701
- published();
17702
- },
17703
- commit() {
17704
- for (const id of entries.keys()) invalidate(id);
17705
- published();
17706
- },
17707
- subscribe(fn) {
17708
- listeners.add(fn);
17709
- return () => {
17710
- listeners.delete(fn);
17711
- };
17712
- },
17713
- getGeneration() {
17714
- return generation2;
17715
- }
17716
- };
17717
- }
17718
- function effectivePose(overrides, node) {
17719
- return overrides.get(node.id)?.pose ?? node.pose;
17720
- }
17721
-
17722
- // src/canvas/derivedPath.ts
17723
- var SLOT = "kit:derivedPath";
17724
- function resolveDerivedPath(node, poseOf) {
17725
- const deps = node.dependsOn;
17726
- const derivePath = node.derivePath;
17727
- if (deps === void 0 || deps.length === 0 || derivePath === void 0) return null;
17728
- return nodeMemo(
17729
- node,
17730
- SLOT,
17731
- node.pose,
17732
- () => derivePath(node, deps.map((id) => poseOf(id)))
17733
- );
18242
+ registerNodeShape(RECT_FALLBACK_PAINTER);
17734
18243
  }
17735
- function scenePoseLookup(scene) {
17736
- return (id) => {
17737
- const node = scene.get(id);
17738
- if (node === void 0) return void 0;
17739
- return scene.overrides.get(id)?.pose ?? node.pose;
17740
- };
18244
+ registerBuiltInShapePainters();
18245
+
18246
+ // src/canvas/derivedPath.ts
18247
+ function sceneDepLookup(scene) {
18248
+ return (id) => derivedDepOf(scene, id);
17741
18249
  }
17742
18250
  function withDerivedPaths(scene, drawOne) {
17743
- const poseOf = scenePoseLookup(scene);
18251
+ const depOf2 = sceneDepLookup(scene);
18252
+ const childrenOf = (id) => scene.childrenOf(id);
17744
18253
  return (node, pose, view, ctx) => {
17745
- const deps = node.dependsOn;
17746
- if (deps === void 0 || deps.length === 0) return drawOne(node, pose, view, ctx);
17747
- return drawOne(node, pose, view, { ...ctx, derivedPath: resolveDerivedPath(node, poseOf) });
18254
+ if (node.dependsOn === void 0) return drawOne(node, pose, view, ctx);
18255
+ return drawOne(node, pose, view, {
18256
+ ...ctx,
18257
+ derivedPath: resolveDerivedPath(
18258
+ node,
18259
+ depOf2,
18260
+ childrenOf
18261
+ )
18262
+ });
17748
18263
  };
17749
18264
  }
17750
18265
 
@@ -17776,6 +18291,7 @@ function pickWalk(src, q) {
17776
18291
  };
17777
18292
  for (const node of src.order()) {
17778
18293
  if (!includeContainers && node.kind === "container") continue;
18294
+ if (node.pickable === false) continue;
17779
18295
  if (node.layer !== void 0 && src.layerIsPainted?.(node.layer) === false) continue;
17780
18296
  if (src.alphaOf !== void 0 && src.alphaOf(node.id) <= 0) continue;
17781
18297
  const pose = src.poseOf(node);
@@ -17813,14 +18329,23 @@ function scenePickSource(scene, opts = {}) {
17813
18329
  }
17814
18330
  }
17815
18331
  const readAlpha = alphaOf ?? (enumerable ? faded ? (id) => faded.has(id) ? 0 : 1 : void 0 : (id) => scene.overrides.get(asNodeId(id))?.alpha ?? 1);
17816
- const poseOf = getPose ? (node) => getPose(node.id) : (node) => effectivePose(scene.overrides, node);
18332
+ const poseOf = getPose ? (node) => getPose(node.id) : (node) => effectivePose(scene, node);
18333
+ const childrenOf = (id) => scene.childrenOf(id);
18334
+ const depOf2 = (id) => {
18335
+ const dep = scene.get(id);
18336
+ if (dep === void 0) return void 0;
18337
+ return {
18338
+ node: dep,
18339
+ pose: poseOf(dep),
18340
+ get path() {
18341
+ return resolveDerivedPath(dep, depOf2, childrenOf);
18342
+ }
18343
+ };
18344
+ };
17817
18345
  const derivedPathOf = (node) => {
17818
18346
  const n = node;
17819
- if (n.dependsOn === void 0 || n.dependsOn.length === 0) return null;
17820
- return resolveDerivedPath(n, (id) => {
17821
- const dep = scene.get(id);
17822
- return dep === void 0 ? void 0 : poseOf(dep);
17823
- });
18347
+ if (n.dependsOn === void 0) return null;
18348
+ return resolveDerivedPath(n, depOf2, childrenOf);
17824
18349
  };
17825
18350
  return {
17826
18351
  order: () => scene.renderOrderNodes(),
@@ -19938,8 +20463,8 @@ function useFrameLoop(paint, options = {}) {
19938
20463
  }, []);
19939
20464
  return { requestRedraw, subscribeFrame };
19940
20465
  }
19941
- var EMPTY2 = [];
19942
- var EMPTY_SNAPSHOT = () => EMPTY2;
20466
+ var EMPTY3 = [];
20467
+ var EMPTY_SNAPSHOT = () => EMPTY3;
19943
20468
  var NEVER_CHANGES = () => () => {
19944
20469
  };
19945
20470
  function useSelection(opts = {}) {
@@ -21724,71 +22249,67 @@ function composeAlphaFor(scene, alphaFor) {
21724
22249
  return alphaFor ? (id) => alphaFor(id) * overrideAlphaFor(id) : overrideAlphaFor;
21725
22250
  }
21726
22251
  function wireSceneSlotToScene(slot, scene, alphaFor) {
21727
- const poseOf = scenePoseLookup(scene);
22252
+ const depOf2 = sceneDepLookup(scene);
21728
22253
  return {
21729
22254
  ...slot,
21730
22255
  drawOne: withDerivedPaths(scene, slot.drawOne),
21731
- derivedPathOf: (node) => resolveDerivedPath(node, poseOf),
22256
+ derivedPathOf: (node) => resolveDerivedPath(node, depOf2, (id) => scene.childrenOf(id)),
21732
22257
  alphaFor: composeAlphaFor(scene, alphaFor)
21733
22258
  };
21734
22259
  }
21735
22260
 
21736
- // src/core/scene/dependents.ts
21737
- var NONE = Object.freeze([]);
21738
- function createDependentsIndex() {
21739
- const forward = /* @__PURE__ */ new Map();
21740
- const reverse = /* @__PURE__ */ new Map();
21741
- function detachOwnDeps(id) {
21742
- const old = reverse.get(id);
21743
- if (old === void 0) return;
21744
- for (const dep of old) {
21745
- const set = forward.get(dep);
21746
- if (set === void 0) continue;
21747
- set.delete(id);
21748
- if (set.size === 0) forward.delete(dep);
21749
- }
21750
- reverse.delete(id);
22261
+ // src/core/scene/poseOverrides.ts
22262
+ function createPoseOverrides(getNode, onInvalidate) {
22263
+ const entries = /* @__PURE__ */ new Map();
22264
+ const listeners = /* @__PURE__ */ new Set();
22265
+ let generation2 = 0;
22266
+ function invalidate(id) {
22267
+ const node = getNode(id);
22268
+ if (node) dropPoseKeyedMemoSlots(node);
22269
+ onInvalidate?.(id);
22270
+ }
22271
+ function published() {
22272
+ generation2++;
22273
+ for (const listener of listeners) listener();
21751
22274
  }
21752
22275
  return {
21753
- add(id, deps) {
21754
- detachOwnDeps(id);
21755
- if (deps.length === 0) return;
21756
- reverse.set(id, [...deps]);
21757
- for (const dep of deps) {
21758
- let set = forward.get(dep);
21759
- if (set === void 0) {
21760
- set = /* @__PURE__ */ new Set();
21761
- forward.set(dep, set);
21762
- }
21763
- set.add(id);
21764
- }
22276
+ set(id, entry) {
22277
+ entries.set(id, entry);
22278
+ invalidate(id);
22279
+ published();
21765
22280
  },
21766
- remove(id) {
21767
- detachOwnDeps(id);
22281
+ get(id) {
22282
+ return entries.get(id);
21768
22283
  },
21769
- clear() {
21770
- forward.clear();
21771
- reverse.clear();
22284
+ has(id) {
22285
+ return entries.has(id);
21772
22286
  },
21773
- isEmpty() {
21774
- return forward.size === 0;
22287
+ ids() {
22288
+ return [...entries.keys()];
21775
22289
  },
21776
- dependentsOf(id) {
21777
- const set = forward.get(id);
21778
- return set === void 0 ? NONE : [...set];
22290
+ clear(id) {
22291
+ if (!entries.delete(id)) return;
22292
+ invalidate(id);
22293
+ published();
21779
22294
  },
21780
- transitiveDependentsOf(id) {
21781
- const direct = forward.get(id);
21782
- if (direct === void 0) return NONE;
21783
- const out = /* @__PURE__ */ new Set();
21784
- const stack = [...direct];
21785
- while (stack.length > 0) {
21786
- const next = stack.pop();
21787
- if (out.has(next)) continue;
21788
- out.add(next);
21789
- for (const d of forward.get(next) ?? []) stack.push(d);
21790
- }
21791
- return out;
22295
+ clearAll() {
22296
+ if (entries.size === 0) return;
22297
+ for (const id of entries.keys()) invalidate(id);
22298
+ entries.clear();
22299
+ published();
22300
+ },
22301
+ commit() {
22302
+ for (const id of entries.keys()) invalidate(id);
22303
+ published();
22304
+ },
22305
+ subscribe(fn) {
22306
+ listeners.add(fn);
22307
+ return () => {
22308
+ listeners.delete(fn);
22309
+ };
22310
+ },
22311
+ getGeneration() {
22312
+ return generation2;
21792
22313
  }
21793
22314
  };
21794
22315
  }
@@ -21814,23 +22335,13 @@ function createScene(options) {
21814
22335
  layers: [],
21815
22336
  layerIndex: /* @__PURE__ */ new Map()
21816
22337
  };
21817
- const registry2 = options.registry ?? {};
21818
- const reverseClipFromPose = /* @__PURE__ */ new Map();
21819
- if (registry2.clipFromPose) {
21820
- for (const [key, fn] of Object.entries(registry2.clipFromPose)) {
21821
- reverseClipFromPose.set(fn, key);
21822
- }
21823
- }
21824
- const reverseDerivePath = /* @__PURE__ */ new Map();
21825
- if (registry2.derivePath) {
21826
- for (const [key, fn] of Object.entries(registry2.derivePath)) {
21827
- reverseDerivePath.set(fn, key);
21828
- }
21829
- }
21830
- const pendingClipPatches = /* @__PURE__ */ new Map();
21831
- const pendingDerivePathPatches = /* @__PURE__ */ new Map();
22338
+ const registry2 = withKitRegistry(options.registry ?? {});
22339
+ const fnFields = createNodeFnFields(registry2);
22340
+ const fnField = (name) => fnFields.find((f) => f.spec.field === name);
21832
22341
  const dependents = createDependentsIndex();
22342
+ const childDerived = /* @__PURE__ */ new Set();
21833
22343
  function invalidateDependents(id) {
22344
+ invalidateChildDerivedFrom(state.nodes.get(id)?.parent ?? null);
21834
22345
  if (dependents.isEmpty()) return;
21835
22346
  const subtree = [id];
21836
22347
  descendants(id, subtree);
@@ -21841,6 +22352,35 @@ function createScene(options) {
21841
22352
  }
21842
22353
  }
21843
22354
  }
22355
+ function invalidateChildDerivedFrom(start) {
22356
+ if (childDerived.size === 0) return;
22357
+ let cursor = start;
22358
+ while (cursor !== null) {
22359
+ const node = state.nodes.get(cursor);
22360
+ if (node === void 0) return;
22361
+ if (childDerived.has(cursor)) {
22362
+ dropPoseKeyedMemoSlots(node);
22363
+ for (const dep of dependents.transitiveDependentsOf(cursor)) {
22364
+ const d = state.nodes.get(dep);
22365
+ if (d !== void 0) dropPoseKeyedMemoSlots(d);
22366
+ }
22367
+ }
22368
+ cursor = node.parent;
22369
+ }
22370
+ }
22371
+ function trackChildDerived(id, dependsOn) {
22372
+ if (dependsOn === "children") childDerived.add(id);
22373
+ else childDerived.delete(id);
22374
+ }
22375
+ function retarget(id, dependsOn) {
22376
+ const node = requireNode(id);
22377
+ if (dependsOn === void 0) delete node.dependsOn;
22378
+ else node.dependsOn = dependsOn;
22379
+ dependents.add(id, dependsOn === void 0 || dependsOn === "children" ? [] : dependsOn);
22380
+ trackChildDerived(id, dependsOn);
22381
+ dropPoseKeyedMemoSlots(requireNode(id));
22382
+ invalidateDependents(id);
22383
+ }
21844
22384
  for (let i = 0; i < options.systemLayers.length; i++) {
21845
22385
  const spec = options.systemLayers[i];
21846
22386
  if (state.layerIndex.has(spec.id)) {
@@ -21927,13 +22467,12 @@ function createScene(options) {
21927
22467
  for (const listener of listeners) listener();
21928
22468
  }
21929
22469
  function pruneCacheForDroppedOps(ops) {
21930
- if (pendingClipPatches.size === 0 && pendingDerivePathPatches.size === 0) return;
22470
+ if (fnFields.every((f) => f.pending.size === 0)) return;
21931
22471
  for (const op of ops) {
21932
22472
  if (op.name !== "kit:add") continue;
21933
22473
  const id = op.args?.id;
21934
22474
  if (id && !state.nodes.has(id)) {
21935
- pendingClipPatches.delete(id);
21936
- pendingDerivePathPatches.delete(id);
22475
+ for (const field of fnFields) field.pending.delete(id);
21937
22476
  }
21938
22477
  }
21939
22478
  }
@@ -22014,39 +22553,25 @@ function createScene(options) {
22014
22553
  }
22015
22554
  return { ids, removing };
22016
22555
  }
22017
- function patchClipFromPose(spec, id, { cacheForRedo }) {
22018
- if (spec.kind === "container" && spec.clipFromPose !== void 0) {
22019
- state.nodes.get(id).clipFromPose = spec.clipFromPose;
22020
- if (cacheForRedo) {
22021
- pendingClipPatches.set(id, spec.clipFromPose);
22022
- }
22556
+ function patchNodeFunctions(spec, id, { cacheForRedo }) {
22557
+ const node = state.nodes.get(id);
22558
+ if (node === void 0) return;
22559
+ for (const field of fnFields) {
22560
+ const fn = spec[field.spec.field];
22561
+ if (fn === void 0) continue;
22562
+ if (field.spec.containersOnly && spec.kind !== "container") continue;
22563
+ field.write(node, fn);
22564
+ if (cacheForRedo) field.pending.set(id, fn);
22023
22565
  }
22024
22566
  }
22025
- function patchDerivePath(spec, id, { cacheForRedo }) {
22026
- if (spec.derivePath === void 0) return;
22027
- state.nodes.get(id).derivePath = spec.derivePath;
22028
- if (cacheForRedo) pendingDerivePathPatches.set(id, spec.derivePath);
22029
- }
22030
22567
  function restoreNodeFunctions(node, keys) {
22031
22568
  if (!keys) return;
22032
- if (node.derivePath === void 0 && keys.derivePathKey !== void 0) {
22033
- const fn = registry2.derivePath?.[keys.derivePathKey];
22034
- if (fn) {
22035
- node.derivePath = fn;
22036
- pendingDerivePathPatches.set(node.id, fn);
22037
- } else {
22038
- dwarn("scene", `kit:remove: derivePathKey "${keys.derivePathKey}" not in this scene's registry \u2014 node "${node.id}" restored without derived geometry. Register a function with this key in the registry option to restore it.`);
22039
- }
22040
- }
22041
- if (node.kind !== "container" || keys.clipKey === void 0) return;
22042
- const container = node;
22043
- if (container.clipFromPose !== void 0) return;
22044
- const clip = registry2.clipFromPose?.[keys.clipKey];
22045
- if (clip) {
22046
- container.clipFromPose = clip;
22047
- pendingClipPatches.set(node.id, clip);
22048
- } else {
22049
- dwarn("scene", `kit:remove: clipKey "${keys.clipKey}" not in this scene's registry \u2014 container "${node.id}" restored without clip. Register a function with this key in the registry option to restore the clip.`);
22569
+ const target = node;
22570
+ for (const field of fnFields) {
22571
+ if (field.read(target) !== void 0) continue;
22572
+ const key = keys[field.spec.opKey];
22573
+ if (key === void 0) continue;
22574
+ restoreFn(field, target, key, "kit:remove", dwarn);
22050
22575
  }
22051
22576
  }
22052
22577
  function registerKitOp(kind, handler) {
@@ -22060,33 +22585,19 @@ function createScene(options) {
22060
22585
  const node = p.kind === "container" ? { kind: "container", id: p.id, layer: p.layer, pose: p.pose, data: p.data, parent: p.parent, children: [] } : { kind: "leaf", id: p.id, layer: p.layer, pose: p.pose, data: p.data, parent: p.parent };
22061
22586
  state.nodes.set(p.id, node);
22062
22587
  attach(p.id, p.parent, p.index);
22063
- if (p.kind === "container") {
22064
- const cached = pendingClipPatches.get(p.id);
22065
- if (cached) {
22066
- node.clipFromPose = cached;
22067
- } else if (p.clipKey !== void 0) {
22068
- const fn = registry2.clipFromPose?.[p.clipKey];
22069
- if (fn) {
22070
- node.clipFromPose = fn;
22071
- pendingClipPatches.set(p.id, fn);
22072
- } else {
22073
- dwarn("scene", `kit:add: clipKey "${p.clipKey}" not in this scene's registry \u2014 container "${p.id}" restored without clip. Register a function with this key in the registry option to restore the clip.`);
22074
- }
22075
- }
22076
- }
22077
22588
  if (p.dependsOn !== void 0) node.dependsOn = p.dependsOn;
22078
- dependents.add(p.id, p.dependsOn ?? []);
22079
- const cachedDerivePath = pendingDerivePathPatches.get(p.id);
22080
- if (cachedDerivePath) {
22081
- node.derivePath = cachedDerivePath;
22082
- } else if (p.derivePathKey !== void 0) {
22083
- const fn = registry2.derivePath?.[p.derivePathKey];
22084
- if (fn) {
22085
- node.derivePath = fn;
22086
- pendingDerivePathPatches.set(p.id, fn);
22087
- } else {
22088
- dwarn("scene", `kit:add: derivePathKey "${p.derivePathKey}" not in this scene's registry \u2014 node "${p.id}" restored without derived geometry. Register a function with this key in the registry option to restore it.`);
22589
+ if (p.pickable !== void 0) node.pickable = p.pickable;
22590
+ dependents.add(p.id, p.dependsOn === "children" ? [] : p.dependsOn ?? []);
22591
+ trackChildDerived(p.id, p.dependsOn);
22592
+ const target = node;
22593
+ for (const field of fnFields) {
22594
+ const cached = field.pending.get(p.id);
22595
+ if (cached !== void 0) {
22596
+ field.write(target, cached);
22597
+ continue;
22089
22598
  }
22599
+ const key = p[field.spec.opKey];
22600
+ if (key !== void 0) restoreFn(field, target, key, "kit:add", dwarn);
22090
22601
  }
22091
22602
  invalidateDependents(p.id);
22092
22603
  },
@@ -22094,6 +22605,8 @@ function createScene(options) {
22094
22605
  detach(p.id);
22095
22606
  state.nodes.delete(p.id);
22096
22607
  dependents.remove(p.id);
22608
+ childDerived.delete(p.id);
22609
+ invalidateChildDerivedFrom(p.parent);
22097
22610
  invalidateDependents(p.id);
22098
22611
  }
22099
22612
  });
@@ -22103,7 +22616,9 @@ function createScene(options) {
22103
22616
  for (const n of p.nodes) {
22104
22617
  state.nodes.delete(n.id);
22105
22618
  dependents.remove(n.id);
22619
+ childDerived.delete(n.id);
22106
22620
  }
22621
+ for (const d of p.detached) invalidateChildDerivedFrom(d.parent);
22107
22622
  },
22108
22623
  revert: (p) => {
22109
22624
  const keys = new Map((p.fnKeys ?? []).map((k) => [k.id, k]));
@@ -22111,9 +22626,11 @@ function createScene(options) {
22111
22626
  const clone = n.kind === "container" ? { ...n, children: [...n.children] } : { ...n };
22112
22627
  restoreNodeFunctions(clone, keys.get(n.id));
22113
22628
  state.nodes.set(n.id, clone);
22114
- dependents.add(n.id, clone.dependsOn ?? []);
22629
+ dependents.add(n.id, clone.dependsOn === "children" ? [] : clone.dependsOn ?? []);
22630
+ trackChildDerived(n.id, clone.dependsOn);
22115
22631
  }
22116
22632
  for (const d of p.detached) attach(d.id, d.parent, d.index);
22633
+ for (const d of p.detached) invalidateChildDerivedFrom(d.parent);
22117
22634
  }
22118
22635
  });
22119
22636
  registerKitOp("kit:setPose", {
@@ -22129,9 +22646,11 @@ function createScene(options) {
22129
22646
  registerKitOp("kit:setData", {
22130
22647
  apply: (p) => {
22131
22648
  requireNode(p.id).data = p.to;
22649
+ invalidateDependents(p.id);
22132
22650
  },
22133
22651
  revert: (p) => {
22134
22652
  requireNode(p.id).data = p.from;
22653
+ invalidateDependents(p.id);
22135
22654
  }
22136
22655
  });
22137
22656
  registerKitOp("kit:setLayer", {
@@ -22139,21 +22658,33 @@ function createScene(options) {
22139
22658
  requireLayerIndex(p.to);
22140
22659
  requireNode(p.id).layer = p.to;
22141
22660
  invalidateOrder();
22661
+ invalidateDependents(p.id);
22142
22662
  },
22143
22663
  revert: (p) => {
22144
22664
  requireNode(p.id).layer = p.from;
22145
22665
  invalidateOrder();
22666
+ invalidateDependents(p.id);
22667
+ }
22668
+ });
22669
+ registerKitOp("kit:setDependsOn", {
22670
+ apply: (p) => {
22671
+ retarget(p.id, p.to);
22672
+ },
22673
+ revert: (p) => {
22674
+ retarget(p.id, p.from);
22146
22675
  }
22147
22676
  });
22148
22677
  registerKitOp("kit:move", {
22149
22678
  apply: (p) => {
22150
22679
  detach(p.id);
22151
22680
  attach(p.id, p.toParent, p.toIndex);
22681
+ invalidateChildDerivedFrom(p.fromParent);
22152
22682
  invalidateDependents(p.id);
22153
22683
  },
22154
22684
  revert: (p) => {
22155
22685
  detach(p.id);
22156
22686
  attach(p.id, p.fromParent, p.fromIndex);
22687
+ invalidateChildDerivedFrom(p.toParent);
22157
22688
  invalidateDependents(p.id);
22158
22689
  }
22159
22690
  });
@@ -22426,8 +22957,6 @@ function createScene(options) {
22426
22957
  }
22427
22958
  const sibs = siblingsOf(parent);
22428
22959
  const index = spec.index ?? sibs.length;
22429
- const clipKey = spec.kind === "container" && spec.clipFromPose !== void 0 ? reverseClipFromPose.get(spec.clipFromPose) : void 0;
22430
- const derivePathKey = spec.derivePath !== void 0 ? reverseDerivePath.get(spec.derivePath) : void 0;
22431
22960
  executeAndLog("kit:add", {
22432
22961
  id,
22433
22962
  kind: spec.kind,
@@ -22436,12 +22965,11 @@ function createScene(options) {
22436
22965
  data: spec.data,
22437
22966
  parent,
22438
22967
  index,
22439
- ...clipKey !== void 0 ? { clipKey } : {},
22440
22968
  ...spec.dependsOn !== void 0 ? { dependsOn: spec.dependsOn } : {},
22441
- ...derivePathKey !== void 0 ? { derivePathKey } : {}
22969
+ ...spec.pickable !== void 0 ? { pickable: spec.pickable } : {},
22970
+ ...fnKeysOf(fnFields, { ...spec, id, kind: spec.kind }, "opKey")
22442
22971
  }, `add ${spec.kind}`);
22443
- patchClipFromPose(spec, id, { cacheForRedo: true });
22444
- patchDerivePath(spec, id, { cacheForRedo: true });
22972
+ patchNodeFunctions(spec, id, { cacheForRedo: true });
22445
22973
  return id;
22446
22974
  },
22447
22975
  remove(id) {
@@ -22460,13 +22988,8 @@ function createScene(options) {
22460
22988
  const detached = snapshot.filter((n) => n.parent === null || !removing.has(n.parent)).map((n) => ({ id: n.id, parent: n.parent, index: siblingsOf(n.parent).indexOf(n.id) })).sort((l, r) => l.index - r.index);
22461
22989
  for (const nid of ids) overrides.clear(nid);
22462
22990
  const fnKeys = snapshot.flatMap((n) => {
22463
- const derivePathKey = n.derivePath !== void 0 ? reverseDerivePath.get(n.derivePath) : void 0;
22464
- const clipKey = n.kind === "container" && n.clipFromPose !== void 0 ? reverseClipFromPose.get(n.clipFromPose) : void 0;
22465
- return derivePathKey === void 0 && clipKey === void 0 ? [] : [{
22466
- id: n.id,
22467
- ...derivePathKey !== void 0 ? { derivePathKey } : {},
22468
- ...clipKey !== void 0 ? { clipKey } : {}
22469
- }];
22991
+ const keys = fnKeysOf(fnFields, n, "opKey");
22992
+ return Object.keys(keys).length === 0 ? [] : [{ id: n.id, ...keys }];
22470
22993
  });
22471
22994
  const payload = {
22472
22995
  nodes: snapshot,
@@ -22516,6 +23039,15 @@ function createScene(options) {
22516
23039
  });
22517
23040
  }
22518
23041
  },
23042
+ setDependsOn(id, dependsOn) {
23043
+ const node = requireNode(id);
23044
+ if (sameDependsOn(node.dependsOn, dependsOn)) return;
23045
+ executeAndLog(
23046
+ "kit:setDependsOn",
23047
+ { id, from: node.dependsOn, to: dependsOn },
23048
+ "setDependsOn"
23049
+ );
23050
+ },
22519
23051
  move(id, parent, index) {
22520
23052
  const node = requireNode(id);
22521
23053
  if (parent !== null) {
@@ -22744,25 +23276,17 @@ function createScene(options) {
22744
23276
  data: n.data
22745
23277
  };
22746
23278
  if (n.parent != null) out.parent = n.parent;
22747
- if (n.kind === "container" && n.clipFromPose) {
22748
- const key = reverseClipFromPose.get(n.clipFromPose);
22749
- if (!key) {
22750
- throw new Error(
22751
- `Scene.toJSON: container '${id}' has clipFromPose but no matching registry key. The function must be registered via createScene's registry option to round-trip.`
22752
- );
22753
- }
22754
- out.clipFromPoseKey = key;
22755
- }
22756
- if (n.dependsOn && n.dependsOn.length > 0) out.dependsOn = n.dependsOn;
22757
- if (n.derivePath) {
22758
- const key = reverseDerivePath.get(n.derivePath);
22759
- if (!key) {
23279
+ if (n.dependsOn === "children") out.dependsOn = "children";
23280
+ else if (n.dependsOn && n.dependsOn.length > 0) out.dependsOn = n.dependsOn;
23281
+ if (n.pickable === false) out.pickable = false;
23282
+ Object.assign(
23283
+ out,
23284
+ fnKeysOf(fnFields, n, "jsonKey", (field) => {
22760
23285
  throw new Error(
22761
- `Scene.toJSON: node '${id}' has derivePath but no matching registry key. The function must be registered via createScene's registry option to round-trip.`
23286
+ `Scene.toJSON: node '${id}' has ${field.spec.field} but no matching registry key. The function must be registered via createScene's registry option to round-trip.`
22762
23287
  );
22763
- }
22764
- out.derivePathKey = key;
22765
- }
23288
+ })
23289
+ );
22766
23290
  nodes.push(out);
22767
23291
  }
22768
23292
  const systemLayers = state.layers.map((l) => {
@@ -22794,8 +23318,7 @@ function createScene(options) {
22794
23318
  state.layerIndex.set(spec.id, i);
22795
23319
  }
22796
23320
  history.clear();
22797
- pendingClipPatches.clear();
22798
- pendingDerivePathPatches.clear();
23321
+ for (const field of fnFields) field.pending.clear();
22799
23322
  currentBatch = null;
22800
23323
  batchDepth = 0;
22801
23324
  batchDirty = false;
@@ -22833,18 +23356,19 @@ function createScene(options) {
22833
23356
  data: spec.data,
22834
23357
  parent,
22835
23358
  index,
22836
- ...spec.dependsOn !== void 0 ? { dependsOn: spec.dependsOn } : {}
23359
+ ...spec.dependsOn !== void 0 ? { dependsOn: spec.dependsOn } : {},
23360
+ ...spec.pickable !== void 0 ? { pickable: spec.pickable } : {}
22837
23361
  });
22838
- patchClipFromPose(spec, id, { cacheForRedo: false });
22839
- patchDerivePath(spec, id, { cacheForRedo: false });
23362
+ patchNodeFunctions(spec, id, { cacheForRedo: false });
22840
23363
  }
22841
23364
  }
22842
23365
  if (options.initial) {
22843
23366
  applyConstructionSpecs(options.initial);
22844
23367
  notify2();
22845
23368
  }
22846
- scene.__clipCacheSize = () => pendingClipPatches.size;
22847
- scene.__derivePathCacheSize = () => pendingDerivePathPatches.size;
23369
+ scene.__clipCacheSize = () => fnField("clipFromPose").pending.size;
23370
+ scene.__derivePathCacheSize = () => fnField("derivePath").pending.size;
23371
+ scene.__derivePoseCacheSize = () => fnField("derivePose").pending.size;
22848
23372
  return scene;
22849
23373
  }
22850
23374
  function specsFromSerialized(json, registry2) {
@@ -22860,24 +23384,21 @@ function specsFromSerialized(json, registry2) {
22860
23384
  data: n.data
22861
23385
  };
22862
23386
  if (n.parent !== void 0) spec.parent = n.parent;
22863
- if (n.clipFromPoseKey !== void 0) {
22864
- const fn = registry2.clipFromPose?.[n.clipFromPoseKey];
22865
- if (!fn) {
22866
- throw new Error(
22867
- `Scene: unknown clipFromPose key '${n.clipFromPoseKey}'. Register a function with this key in the registry option.`
22868
- );
22869
- }
22870
- spec.clipFromPose = fn;
22871
- }
22872
- if (n.dependsOn !== void 0) spec.dependsOn = n.dependsOn;
22873
- if (n.derivePathKey !== void 0) {
22874
- const fn = registry2.derivePath?.[n.derivePathKey];
23387
+ if (n.pickable !== void 0) spec.pickable = n.pickable;
23388
+ if (n.dependsOn !== void 0) {
23389
+ spec.dependsOn = n.dependsOn === "children" ? "children" : n.dependsOn;
23390
+ }
23391
+ const serialized = n;
23392
+ for (const field of NODE_FN_FIELDS) {
23393
+ const key = serialized[field.jsonKey];
23394
+ if (key === void 0) continue;
23395
+ const fn = registry2[field.field]?.[key];
22875
23396
  if (!fn) {
22876
23397
  throw new Error(
22877
- `Scene: unknown derivePath key '${n.derivePathKey}'. Register a function with this key in the registry option.`
23398
+ `Scene: unknown ${field.field} key '${key}'. Register a function with this key in the registry option.`
22878
23399
  );
22879
23400
  }
22880
- spec.derivePath = fn;
23401
+ spec[field.field] = fn;
22881
23402
  }
22882
23403
  return spec;
22883
23404
  });
@@ -23864,7 +24385,7 @@ function sceneToAdapter(scene, options = {}) {
23864
24385
  const nid = asNodeId(id);
23865
24386
  const n = scene.get(nid);
23866
24387
  if (!n) throw new Error(`sceneToAdapter: unknown node "${id}"`);
23867
- return effectivePose(scene.overrides, { id: nid, pose: n.pose });
24388
+ return effectivePose(scene, n);
23868
24389
  },
23869
24390
  getParent(id) {
23870
24391
  const n = scene.get(asNodeId(id));
@@ -23949,11 +24470,10 @@ function sceneToAdapter(scene, options = {}) {
23949
24470
  id: node.id,
23950
24471
  ...index !== void 0 ? { index } : {},
23951
24472
  ...node.parent !== null ? { parent: node.parent } : {},
23952
- ...node.kind === "container" && node.clipFromPose ? { clipFromPose: node.clipFromPose } : {},
23953
24473
  // Derivation travels with the node, or undo of a delete brings an
23954
24474
  // edge back as a static path that never follows its endpoints again.
23955
24475
  ...node.dependsOn !== void 0 ? { dependsOn: node.dependsOn } : {},
23956
- ...node.derivePath !== void 0 ? { derivePath: node.derivePath } : {}
24476
+ ...fnFieldsOfNode(node)
23957
24477
  });
23958
24478
  },
23959
24479
  removeNode(id) {
@@ -24183,20 +24703,13 @@ function useSceneSelectTool(args) {
24183
24703
  };
24184
24704
  }, [scene, commitInsert, insertLayer, layouts, selection]);
24185
24705
  const wiredMoveOptions = useMemo(() => {
24186
- const defaultCascade = (id) => {
24187
- const n = scene.get(asNodeId(id));
24188
- return n ? n.pose : null;
24189
- };
24190
- const merged = {
24191
- cascadeWorldPose: defaultCascade,
24192
- ...moveOptions ?? {}
24193
- };
24706
+ const merged = { ...moveOptions ?? {} };
24194
24707
  if (snap2) {
24195
24708
  const existing = merged.behaviors ?? [];
24196
24709
  merged.behaviors = [snap(snap2), ...existing];
24197
24710
  }
24198
24711
  return merged;
24199
- }, [scene, moveOptions, snap2]);
24712
+ }, [moveOptions, snap2]);
24200
24713
  const wiredHitBody = useMemo(() => {
24201
24714
  return (wx, wy, camera) => {
24202
24715
  if (pickEveryProp) {
@@ -24339,7 +24852,7 @@ function usePreviewGhostLayer(args) {
24339
24852
  }
24340
24853
  if (idSet.size === 0) return [];
24341
24854
  const sc = sceneRef.current;
24342
- const poseOf = scenePoseLookup(sc);
24855
+ const depOf2 = sceneDepLookup(sc);
24343
24856
  const previewPoseFor = (id) => previewPoseIn(sources, id);
24344
24857
  const previewDataFor = (id) => previewDataIn(sources, id);
24345
24858
  const buildSubtree = (id) => {
@@ -24362,7 +24875,7 @@ function usePreviewGhostLayer(args) {
24362
24875
  const clip = findShapeSilhouette(
24363
24876
  effNode,
24364
24877
  effPose,
24365
- { derivedPath: resolveDerivedPath(effNode, poseOf) }
24878
+ { derivedPath: resolveDerivedPath(effNode, depOf2, (nid) => sc.childrenOf(nid)) }
24366
24879
  );
24367
24880
  if (clip) group.clip = clip;
24368
24881
  }
@@ -27396,12 +27909,12 @@ function sceneAsHierarchy(scene) {
27396
27909
  const node = scene.get(parentId);
27397
27910
  return node && node.kind === "container" ? node.children : [];
27398
27911
  },
27399
- getPose: (id) => scene.overrides.get(id)?.pose ?? scene.get(id).pose
27912
+ getPose: (id) => effectivePose(scene, scene.get(id))
27400
27913
  };
27401
27914
  }
27402
27915
  function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor) {
27403
27916
  const derived = withDerivedPaths(scene, drawOne);
27404
- const poseLookup = scenePoseLookup(scene);
27917
+ const depLookup = sceneDepLookup(scene);
27405
27918
  const wrappedDrawOne = (node, pose, v) => {
27406
27919
  const overrideAlpha = scene.overrides.get(node.id)?.alpha ?? 1;
27407
27920
  return wrapNodeOutput(
@@ -27417,7 +27930,8 @@ function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor) {
27417
27930
  void 0,
27418
27931
  ((node) => resolveDerivedPath(
27419
27932
  node,
27420
- poseLookup
27933
+ depLookup,
27934
+ (id) => scene.childrenOf(id)
27421
27935
  ))
27422
27936
  );
27423
27937
  if (extraCommands && extraCommands.length > 0) {
@@ -27511,6 +28025,44 @@ function SceneViewCanvasInner(props) {
27511
28025
  );
27512
28026
  }
27513
28027
  var SceneViewCanvas = SceneViewCanvasInner;
28028
+ var NEVER_SUBSCRIBE = () => () => {
28029
+ };
28030
+ var DEFAULT_LAYER = "default";
28031
+ function isTrivialOptions(options) {
28032
+ return !!options && typeof options === "object" && "items" in options && !("systemLayers" in options);
28033
+ }
28034
+ function useScene(options) {
28035
+ const sceneRef = useRef(null);
28036
+ if (sceneRef.current === null) {
28037
+ if (isTrivialOptions(options)) {
28038
+ const scene2 = createScene({
28039
+ systemLayers: [{ id: DEFAULT_LAYER }],
28040
+ historyLimit: options.historyLimit,
28041
+ generateId: options.generateId,
28042
+ initial: options.items.map((item) => ({
28043
+ kind: "leaf",
28044
+ layer: DEFAULT_LAYER,
28045
+ pose: item,
28046
+ data: item,
28047
+ id: asNodeId(item.id)
28048
+ }))
28049
+ });
28050
+ sceneRef.current = scene2;
28051
+ } else {
28052
+ sceneRef.current = createScene(
28053
+ options
28054
+ );
28055
+ }
28056
+ }
28057
+ const scene = sceneRef.current;
28058
+ const subscribed = options?.subscribe !== false;
28059
+ useSyncExternalStore(
28060
+ subscribed ? scene.subscribe : NEVER_SUBSCRIBE,
28061
+ scene.getVersion,
28062
+ scene.getVersion
28063
+ );
28064
+ return scene;
28065
+ }
27514
28066
 
27515
28067
  // src/canvas/minimapMath.ts
27516
28068
  var DEFAULT_FIT_PADDING = 8;
@@ -27547,7 +28099,7 @@ function sceneLeafBounds(scene, poseBounds) {
27547
28099
  let any = false;
27548
28100
  for (const node of scene.nodes.values()) {
27549
28101
  if (node.kind !== "leaf") continue;
27550
- const b = poseBounds(node.pose);
28102
+ const b = poseBounds(documentPose(scene, node));
27551
28103
  if (b.x < minX) minX = b.x;
27552
28104
  if (b.y < minY) minY = b.y;
27553
28105
  const right = b.x + b.width;
@@ -28920,9 +29472,10 @@ function useSceneTextEdit(scene, container, options = {}) {
28920
29472
  return get ? get(node.data) : { fill: node.data.fill, stroke: node.data.stroke };
28921
29473
  },
28922
29474
  getScreenPose: (id) => {
28923
- const node = sceneRef.current.get(asNodeId(id));
29475
+ const scene2 = sceneRef.current;
29476
+ const node = scene2.get(asNodeId(id));
28924
29477
  if (!node) return null;
28925
- const pose = node.pose;
29478
+ const pose = effectivePose(scene2, node);
28926
29479
  const style = optsRef.current.getStyle ? optsRef.current.getStyle(node.data) : node.data.style;
28927
29480
  const view = resolveView2(optsRef.current.view);
28928
29481
  const zoom = view ? view.scale.x : 1;
@@ -28980,11 +29533,12 @@ function useSceneTextEdit(scene, container, options = {}) {
28980
29533
  for (let i = order.length - 1; i >= 0; i--) {
28981
29534
  const node = order[i];
28982
29535
  const text = readText(node.data);
29536
+ const at = effectivePose(sceneRef.current, node);
28983
29537
  const pose = {
28984
- x: node.pose.x,
28985
- y: node.pose.y,
28986
- width: node.pose.width,
28987
- height: node.pose.height,
29538
+ x: at.x,
29539
+ y: at.y,
29540
+ width: at.width,
29541
+ height: at.height,
28988
29542
  text,
28989
29543
  runs: readRuns(node.data),
28990
29544
  style: readStyle(node.data)
@@ -29218,6 +29772,54 @@ function polygonPathDistance(path, px, py) {
29218
29772
  }
29219
29773
  return best === Infinity ? Infinity : Math.sqrt(best);
29220
29774
  }
29775
+
29776
+ // src/features/paths/pathAt.ts
29777
+ function pointAlongPath(path, t, opts = {}) {
29778
+ const polylines = extractPolylines(
29779
+ path,
29780
+ opts.flattenTolerance === void 0 ? {} : { flattenTolerance: opts.flattenTolerance }
29781
+ );
29782
+ const segments = [];
29783
+ let total = 0;
29784
+ let first;
29785
+ for (const pl of polylines) {
29786
+ const count = pl.points.length / 2;
29787
+ if (count === 0) continue;
29788
+ first ??= { x: pl.points[0], y: pl.points[1] };
29789
+ const last2 = pl.closed ? count : count - 1;
29790
+ for (let i = 0; i < last2; i++) {
29791
+ const j = (i + 1) % count;
29792
+ const ax = pl.points[i * 2];
29793
+ const ay = pl.points[i * 2 + 1];
29794
+ const bx = pl.points[j * 2];
29795
+ const by = pl.points[j * 2 + 1];
29796
+ const len = Math.hypot(bx - ax, by - ay);
29797
+ if (len === 0) continue;
29798
+ segments.push({ ax, ay, bx, by, len });
29799
+ total += len;
29800
+ }
29801
+ }
29802
+ if (first === void 0) return null;
29803
+ if (segments.length === 0) return { point: first, tangent: { x: 1, y: 0 } };
29804
+ const target = Math.min(Math.max(t, 0), 1) * total;
29805
+ let walked = 0;
29806
+ for (const seg of segments) {
29807
+ if (walked + seg.len < target) {
29808
+ walked += seg.len;
29809
+ continue;
29810
+ }
29811
+ const u = (target - walked) / seg.len;
29812
+ return {
29813
+ point: { x: seg.ax + (seg.bx - seg.ax) * u, y: seg.ay + (seg.by - seg.ay) * u },
29814
+ tangent: { x: (seg.bx - seg.ax) / seg.len, y: (seg.by - seg.ay) / seg.len }
29815
+ };
29816
+ }
29817
+ const last = segments[segments.length - 1];
29818
+ return {
29819
+ point: { x: last.bx, y: last.by },
29820
+ tangent: { x: (last.bx - last.ax) / last.len, y: (last.by - last.ay) / last.len }
29821
+ };
29822
+ }
29221
29823
  var PLACEHOLDER_FILL = { color: "#ffffff" };
29222
29824
  var PLACEHOLDER_STROKE = { paint: { color: "#ffffff" }, width: 1 };
29223
29825
  function resolveOverride(base, override, tMs) {
@@ -30149,44 +30751,6 @@ function useArrayAdapter(options) {
30149
30751
  ref.current = items;
30150
30752
  return arrayAdapter({ ref, ...rest });
30151
30753
  }
30152
- var NEVER_SUBSCRIBE = () => () => {
30153
- };
30154
- var DEFAULT_LAYER = "default";
30155
- function isTrivialOptions(options) {
30156
- return !!options && typeof options === "object" && "items" in options && !("systemLayers" in options);
30157
- }
30158
- function useScene(options) {
30159
- const sceneRef = useRef(null);
30160
- if (sceneRef.current === null) {
30161
- if (isTrivialOptions(options)) {
30162
- const scene2 = createScene({
30163
- systemLayers: [{ id: DEFAULT_LAYER }],
30164
- historyLimit: options.historyLimit,
30165
- generateId: options.generateId,
30166
- initial: options.items.map((item) => ({
30167
- kind: "leaf",
30168
- layer: DEFAULT_LAYER,
30169
- pose: item,
30170
- data: item,
30171
- id: asNodeId(item.id)
30172
- }))
30173
- });
30174
- sceneRef.current = scene2;
30175
- } else {
30176
- sceneRef.current = createScene(
30177
- options
30178
- );
30179
- }
30180
- }
30181
- const scene = sceneRef.current;
30182
- const subscribed = options?.subscribe !== false;
30183
- useSyncExternalStore(
30184
- subscribed ? scene.subscribe : NEVER_SUBSCRIBE,
30185
- scene.getVersion,
30186
- scene.getVersion
30187
- );
30188
- return scene;
30189
- }
30190
30754
  function useGuides(initial = []) {
30191
30755
  const [guides, setGuides] = useState(initial);
30192
30756
  const ref = useRef(initial);
@@ -31814,6 +32378,6 @@ function mergeContributions(...bundles) {
31814
32378
  return out;
31815
32379
  }
31816
32380
 
31817
- export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, ArrowIcon, BUILTIN_MARKERS, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, CanvasView, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_STROKES, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_SHAPE_FILL, DEFAULT_STROKE_COLOR, DEFAULT_VELOCITY_DECAY, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EFFECT_KIT_UNIFORMS, EFFECT_VERT_SRC, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_JOINT, IDENTITY_POSE_COMPOSITION, ImageIcon, IntersectIcon, KIT_SHAPE_KINDS, KIT_STANDARD_ACTION_IDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PAINTED_CURSOR_LAYER_ID, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SPRITE_STRIDE, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TOOL_PREF_KINDS, TextIcon, UnionIcon, UnknownIcon, VERSION, VIEW_ANIMATION_KEY, WHEEL_ZOOM_STEP, WeaselProvider, WeaselRenderer, _resetMarkersForTests, _resetPaintKindsForTests, aabbCenter, actionIs, actionShortcuts, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, asPaint, axisAlignedBounds, bezierCubic, bezierQuadratic, blendPoses, blur, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView, computeFitViewport, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMoveToIndexOp, createNodeProperties, createNodeRouting, createPaintedCursorLayer, createPaintedCursorState, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createVelocityTracker, createViewResolver, createViewportLayer, cubicBezierEasing, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, defineTool, defineViewportTool, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, drawOneLayer, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, forEachCoalesced, frameRect, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getMarker, getNodeShapes, getPaintKind, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hostAnchorCss, hostAnchorRect, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, interpolateView, isBuiltinToolPref, isEditableTarget, isGradientFill, isLayerPainted, isLayerVisible, isPatternSpec, keySpecShortcut, lassoSelectAction, lerpColorArray, lerpOklab, lerpOklch, linear, listMarkers, listPaintKinds, liveScope, makePinchZoomAction, makeViewportWheelPanAction, makeViewportZoomAction, markerDrawCommands, markerInset, markerKeyOf, markerSites, mat3, matchAlignment, matchesKeyBinding, meanScale, mergeAlphaFromPrev, mergeContributions, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, openPointerSession, or, paintAlpha, paintKindOf, paintWithAlpha, paintWithColor, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerEffect, registerMarker, registerNodeShape, registerPaintKind, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveEasing, resolveFillPattern, resolveMarkerSize, resolveParams, resolvePatternSpec, resolveSkeleton, resolveStrokeWidth, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, sampleGradientStops, sampleTrack, sceneFromJSON, sceneNodeClientRect, sceneSelectionStore, sceneToAdapter, scopeBindings, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, sliceAction, snapPoint, solid, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, strokeInsets, strokeOf, strokeWith, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textCommandFromRuns, tileGrid, toHex8, translatePoseViaDescriptor, translateRectPose, trimPolyline, tweenPose, tweenVertexColors, unionAABB, unionBounds, unionBoundsPath, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHostAnchor, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useNodeOverlayFrame, useOptionalActiveToolContext, usePenTool, usePencilTool, usePinchGesture, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useVisibleRaf, viewToMat3, viewToTransform, viewportDragPanAction, viewportWheelPanAction, viewportZoomAction, viewportsAt, vignette, visualBoundsViaDescriptor, wheelPan, wheelZoom, wheelZoomFactor, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
31818
- //# sourceMappingURL=chunk-WYPSGIYS.js.map
31819
- //# sourceMappingURL=chunk-WYPSGIYS.js.map
32381
+ export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, ArrowIcon, BUILTIN_MARKERS, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, CanvasView, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_STROKES, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_SHAPE_FILL, DEFAULT_STROKE_COLOR, DEFAULT_VELOCITY_DECAY, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EFFECT_KIT_UNIFORMS, EFFECT_VERT_SRC, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_JOINT, IDENTITY_POSE_COMPOSITION, ImageIcon, IntersectIcon, KIT_SHAPE_KINDS, KIT_STANDARD_ACTION_IDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PAINTED_CURSOR_LAYER_ID, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SPRITE_STRIDE, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TOOL_PREF_KINDS, TextIcon, UNION_OF_CHILDREN, UnionIcon, UnknownIcon, VERSION, VIEW_ANIMATION_KEY, WHEEL_ZOOM_STEP, WeaselProvider, WeaselRenderer, _resetMarkersForTests, _resetPaintKindsForTests, aabbCenter, actionIs, actionShortcuts, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, asPaint, axisAlignedBounds, bezierCubic, bezierQuadratic, blendPoses, blur, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView, computeFitViewport, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMoveToIndexOp, createNodeProperties, createNodeRouting, createPaintedCursorLayer, createPaintedCursorState, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createPoseOverrides, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createSimulation, createTextLayer, createVelocityTracker, createViewResolver, createViewportLayer, cubicBezierEasing, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, defineTool, defineViewportTool, deriveAlignmentGuides, deriveParallaxView, derivedDepOf, derivedPose, describeRule, documentPose, domPositionToCharOffset, domToRuns, drawLayers, drawOneLayer, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, effectivePose, enterTextEditAction, enumerateAnchors, evaluate, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, forEachCoalesced, frameRect, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getMarker, getNodeShapes, getPaintKind, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hostAnchorCss, hostAnchorRect, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, interpolateView, isBuiltinToolPref, isEditableTarget, isGradientFill, isLayerPainted, isLayerVisible, isPatternSpec, keySpecShortcut, lassoSelectAction, lerpColorArray, lerpOklab, lerpOklch, linear, listMarkers, listPaintKinds, liveScope, makePinchZoomAction, makeViewportWheelPanAction, makeViewportZoomAction, markerDrawCommands, markerInset, markerKeyOf, markerSites, mat3, matchAlignment, matchesKeyBinding, meanScale, mergeAlphaFromPrev, mergeContributions, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, openPointerSession, or, paintAlpha, paintKindOf, paintWithAlpha, paintWithColor, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointAlongPath, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerEffect, registerMarker, registerNodeShape, registerPaintKind, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveEasing, resolveFillPattern, resolveMarkerSize, resolveParams, resolvePatternSpec, resolveSkeleton, resolveStrokeWidth, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, sampleGradientStops, sampleTrack, sceneFromJSON, sceneNodeClientRect, sceneSelectionStore, sceneToAdapter, scopeBindings, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, sliceAction, snapPoint, solid, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, strokeInsets, strokeOf, strokeWith, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textCommandFromRuns, tileGrid, toHex8, translatePoseViaDescriptor, translateRectPose, trimPolyline, tweenPose, tweenVertexColors, unionAABB, unionBounds, unionBoundsPath, unionOfChildren, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHostAnchor, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useNodeOverlayFrame, useOptionalActiveToolContext, usePenTool, usePencilTool, usePinchGesture, usePointerStylus, usePolygonTool, usePoseRun, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useVisibleRaf, viewToMat3, viewToTransform, viewportDragPanAction, viewportWheelPanAction, viewportZoomAction, viewportsAt, vignette, visualBoundsViaDescriptor, wheelPan, wheelZoom, wheelZoomFactor, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
32382
+ //# sourceMappingURL=chunk-R3AWPTLZ.js.map
32383
+ //# sourceMappingURL=chunk-R3AWPTLZ.js.map