@weasel-js/labkit 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +6 -6
  2. package/dist/_dts/{DrawCommand-uKHt4Vul.d.ts → DrawCommand-BkZztJsW.d.ts} +3 -1
  3. package/dist/_dts/{index-CFlDPeZh.d.ts → index-iAP6XbH3.d.ts} +17 -7
  4. package/dist/_dts/{useExperimentState-vrttakTt.d.ts → useTrialState-DYe2vUwN.d.ts} +62 -34
  5. package/dist/canvas/index.d.ts +1 -1
  6. package/dist/chunk-3TYUJR7Z.js +488 -0
  7. package/dist/chunk-3TYUJR7Z.js.map +1 -0
  8. package/dist/{chunk-N5KTQKQA.js → chunk-C6GJKPUI.js} +835 -770
  9. package/dist/chunk-C6GJKPUI.js.map +1 -0
  10. package/dist/{chunk-3WPOGKUP.js → chunk-DJLDIRFN.js} +543 -226
  11. package/dist/chunk-DJLDIRFN.js.map +1 -0
  12. package/dist/{chunk-73PXCRCR.js → chunk-NRKWVTVT.js} +6 -6
  13. package/dist/chunk-NRKWVTVT.js.map +1 -0
  14. package/dist/dragdrop/index.d.ts +1 -1
  15. package/dist/index.d.ts +197 -148
  16. package/dist/index.js +275 -161
  17. package/dist/index.js.map +1 -1
  18. package/dist/layers/index.d.ts +2 -2
  19. package/dist/passthrough/weasel-canvas.d.ts +1 -1
  20. package/dist/passthrough/weasel-canvas.js +1 -1
  21. package/dist/passthrough/weasel-ui.d.ts +10 -3
  22. package/dist/passthrough/weasel-ui.js +2 -2
  23. package/dist/state/index.d.ts +14 -14
  24. package/dist/state/index.js +5 -5
  25. package/dist/state/index.js.map +1 -1
  26. package/dist/styles.css +179 -14
  27. package/dist/ui/layers/index.js +3 -3
  28. package/dist/undo/index.d.ts +1 -1
  29. package/package.json +2 -1
  30. package/src/canvas/AGENTS.md +6 -2
  31. package/src/dragdrop/dragDrop.test.tsx +4 -7
  32. package/src/index.test.ts +52 -0
  33. package/src/index.ts +11 -7
  34. package/src/instrument/SineWave.smoke.test.tsx +1 -1
  35. package/src/instrument/capabilityDetector.ts +1 -1
  36. package/src/instrument/types.ts +18 -6
  37. package/src/lab/Lab.less +16 -0
  38. package/src/lab/Lab.stories.tsx +5 -5
  39. package/src/lab/Lab.test.tsx +25 -25
  40. package/src/lab/Lab.tsx +56 -44
  41. package/src/lab/LabContext.ts +12 -11
  42. package/src/lab/LabShell.tsx +1 -1
  43. package/src/lab/Workspace.less +40 -0
  44. package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +27 -11
  45. package/src/lab/Workspace.test.tsx +114 -0
  46. package/src/lab/Workspace.tsx +196 -0
  47. package/src/lab/index.ts +2 -4
  48. package/src/layers/AGENTS.md +3 -3
  49. package/src/primitives/Toolbar.stories.tsx +2 -2
  50. package/src/state/SingletonExperiment.test.tsx +8 -8
  51. package/src/state/SingletonExperiment.tsx +9 -9
  52. package/src/state/context.tsx +12 -12
  53. package/src/state/document.test.ts +369 -0
  54. package/src/state/document.ts +194 -0
  55. package/src/state/helpers.test.ts +74 -46
  56. package/src/state/helpers.ts +23 -31
  57. package/src/state/index.ts +16 -8
  58. package/src/state/store.test.ts +440 -40
  59. package/src/state/store.ts +166 -90
  60. package/src/state/types.ts +30 -10
  61. package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
  62. package/src/state/useTrialState.ts +29 -0
  63. package/src/styles.less +2 -2
  64. package/src/test-setup.ts +19 -0
  65. package/src/theme/base.less +1 -1
  66. package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
  67. package/src/{workspace → trial}/DefaultStatusBar.tsx +5 -4
  68. package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
  69. package/src/trial/Trial.canvas.test.tsx +80 -0
  70. package/src/{workspace/Workspace.less → trial/Trial.less} +1 -1
  71. package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +13 -17
  72. package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -25
  73. package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +48 -43
  74. package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +42 -42
  75. package/src/{workspace → trial}/index.ts +13 -12
  76. package/src/{workspace → trial}/slotTypes.ts +16 -16
  77. package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +60 -33
  78. package/src/trial/trialOps.ts +99 -0
  79. package/src/ui/format.test.ts +33 -0
  80. package/src/ui/format.ts +11 -0
  81. package/src/ui/properties/CurveField.tsx +6 -6
  82. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
  83. package/dist/chunk-3WPOGKUP.js.map +0 -1
  84. package/dist/chunk-73PXCRCR.js.map +0 -1
  85. package/dist/chunk-N5KTQKQA.js.map +0 -1
  86. package/dist/chunk-T7OKNJTY.js +0 -351
  87. package/dist/chunk-T7OKNJTY.js.map +0 -1
  88. package/src/lab/WorkspaceGrid.less +0 -8
  89. package/src/lab/WorkspaceGrid.test.tsx +0 -40
  90. package/src/lab/WorkspaceGrid.tsx +0 -22
  91. package/src/lab/gridDims.test.ts +0 -35
  92. package/src/lab/gridDims.ts +0 -13
  93. package/src/state/useExperimentState.ts +0 -31
  94. package/src/workspace/workspaceOps.ts +0 -86
@@ -3,7 +3,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import earcut from 'earcut';
4
4
  import polygonClipping from 'polygon-clipping';
5
5
 
6
- // ../core/dist/chunk-TR3HZN34.js
6
+ // ../core/dist/chunk-J3YC3GVF.js
7
7
 
8
8
  // ../gestures/dist/index.js
9
9
  var GESTURE_DESCRIPTORS = [
@@ -60,9 +60,8 @@ function formatRoute(r) {
60
60
  const desc = getGestureDescriptor(r.gesture);
61
61
  const phaseStr = `[${r.phases.map(formatPhaseAtom).join(",")}]`;
62
62
  let out = `${phaseStr} ${r.gesture}`;
63
- if (desc.arg) {
64
- const isDefault = r.arg === void 0 || desc.arg.default !== void 0 && r.arg === desc.arg.default;
65
- if (!isDefault) out += `(${r.arg})`;
63
+ if (desc.arg && r.arg !== void 0 && r.arg !== (desc.arg.default ?? "*")) {
64
+ out += `(${r.arg})`;
66
65
  }
67
66
  if (desc.hasTarget && r.target !== void 0 && r.target !== "*") {
68
67
  out += ` => ${r.target}`;
@@ -288,7 +287,7 @@ function matchSpec(e, spec, isMac, phaseCtx) {
288
287
  }
289
288
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
290
289
 
291
- // ../core/dist/chunk-TR3HZN34.js
290
+ // ../core/dist/chunk-J3YC3GVF.js
292
291
  var DepRegistryContext = createContext(null);
293
292
  function DepRegistryProvider({ children }) {
294
293
  const sourcesRef = useRef(/* @__PURE__ */ new Map());
@@ -520,7 +519,8 @@ function ActionsProvider({ children }) {
520
519
  const disp = dispatcherRef.current;
521
520
  if (!disp) return null;
522
521
  const r = wiredDepRegRef.current ?? depRegRef.current;
523
- const deps = r ? {
522
+ const a = actionsRef.current.get(id);
523
+ const deps = !r ? {} : a?.requires ? buildDepsFromRequires(a, r) : {
524
524
  selection: r.get("selection"),
525
525
  scene: r.get("scene"),
526
526
  history: r.get("history"),
@@ -528,7 +528,7 @@ function ActionsProvider({ children }) {
528
528
  pointer: r.get("pointer"),
529
529
  activeTool: r.get("activeTool"),
530
530
  booleansAdapter: r.get("booleansAdapter")
531
- } : {};
531
+ };
532
532
  return disp.beginUiOngoing(id, deps, params);
533
533
  }
534
534
  };
@@ -699,7 +699,7 @@ function reportRouteConflicts(scopes, warn = (m) => console.warn(m)) {
699
699
  return conflicts;
700
700
  }
701
701
 
702
- // ../geom/dist/chunk-UULRZUHO.js
702
+ // ../geom/dist/chunk-KU7FXMOA.js
703
703
  function cubicEvalAt(x0, y0, x1, y1, x2, y2, x3, y3, t) {
704
704
  const u = 1 - t;
705
705
  const a = u * u * u, b = 3 * u * u * t, c = 3 * u * t * t, d = t * t * t;
@@ -753,6 +753,12 @@ function cubicBounds(x0, y0, x1, y1, x2, y2, x3, y3) {
753
753
  }
754
754
 
755
755
  // ../geom/dist/index.js
756
+ function dot(ax, ay, bx, by) {
757
+ return ax * bx + ay * by;
758
+ }
759
+ function len2(x, y) {
760
+ return x * x + y * y;
761
+ }
756
762
  function sign(n) {
757
763
  return n > 0 ? 1 : n < 0 ? -1 : 0;
758
764
  }
@@ -804,6 +810,15 @@ function segmentsCross(ax, ay, bx, by, cx, cy, dx, dy) {
804
810
  const d4 = sign((bx - ax) * (dy - ay) - (by - ay) * (dx - ax));
805
811
  return d1 !== d2 && d3 !== d4;
806
812
  }
813
+ function pointSegmentDist2(px, py, ax, ay, bx, by) {
814
+ const vx = bx - ax, vy = by - ay;
815
+ const wx = px - ax, wy = py - ay;
816
+ const vv = len2(vx, vy);
817
+ let t = vv === 0 ? 0 : dot(wx, wy, vx, vy) / vv;
818
+ t = t < 0 ? 0 : t > 1 ? 1 : t;
819
+ const dx = px - (ax + t * vx), dy = py - (ay + t * vy);
820
+ return len2(dx, dy);
821
+ }
807
822
  function transformCoords(coords, m) {
808
823
  const out = new Float64Array(coords.length);
809
824
  const a = m[0], b = m[1], c = m[2], d = m[3], e = m[4], f = m[5];
@@ -815,7 +830,7 @@ function transformCoords(coords, m) {
815
830
  return out;
816
831
  }
817
832
 
818
- // ../core/dist/chunk-U45SODC2.js
833
+ // ../core/dist/chunk-4Z6J4IVH.js
819
834
  function resolveUnit(v, unitSystem) {
820
835
  if (typeof v === "number") return v;
821
836
  if (!unitSystem) {
@@ -900,19 +915,36 @@ function boundsOfPath(path) {
900
915
  return { kind: "rect", x: minX, y: minY, width: maxX - minX, height: maxY - minY };
901
916
  }
902
917
  var DEFAULT_FLATTEN_TOLERANCE = 0.5;
918
+ var MIN_FLATTEN_TOLERANCE = 1e-6;
919
+ function usableTolerance(tolerance) {
920
+ return tolerance > 0 ? tolerance : MIN_FLATTEN_TOLERANCE;
921
+ }
922
+ function finite6(a, b, c, d, e, f) {
923
+ return Number.isFinite(a) && Number.isFinite(b) && Number.isFinite(c) && Number.isFinite(d) && Number.isFinite(e) && Number.isFinite(f);
924
+ }
925
+ function finite8(a, b, c, d, e, f, g, h) {
926
+ return finite6(a, b, c, d, e, f) && Number.isFinite(g) && Number.isFinite(h);
927
+ }
903
928
  function distPointToLine(px, py, ax, ay, bx, by) {
904
929
  const dx = bx - ax;
905
930
  const dy = by - ay;
906
- const len2 = dx * dx + dy * dy;
907
- if (len2 === 0) {
931
+ const len22 = dx * dx + dy * dy;
932
+ if (len22 === 0) {
908
933
  const ex = px - ax;
909
934
  const ey = py - ay;
910
935
  return Math.sqrt(ex * ex + ey * ey);
911
936
  }
912
937
  const cross = (px - ax) * dy - (py - ay) * dx;
913
- return Math.abs(cross) / Math.sqrt(len2);
938
+ return Math.abs(cross) / Math.sqrt(len22);
914
939
  }
915
940
  function flattenCubic2(x0, y0, x1, y1, x2, y2, x3, y3, tolerance, out) {
941
+ if (!finite8(x0, y0, x1, y1, x2, y2, x3, y3)) {
942
+ out.push(x3, y3);
943
+ return;
944
+ }
945
+ flattenCubicRec(x0, y0, x1, y1, x2, y2, x3, y3, usableTolerance(tolerance), out);
946
+ }
947
+ function flattenCubicRec(x0, y0, x1, y1, x2, y2, x3, y3, tolerance, out) {
916
948
  const d1 = distPointToLine(x1, y1, x0, y0, x3, y3);
917
949
  const d2 = distPointToLine(x2, y2, x0, y0, x3, y3);
918
950
  if (Math.max(d1, d2) <= tolerance) {
@@ -925,10 +957,17 @@ function flattenCubic2(x0, y0, x1, y1, x2, y2, x3, y3, tolerance, out) {
925
957
  const x012 = (x01 + x12) * 0.5, y012 = (y01 + y12) * 0.5;
926
958
  const x123 = (x12 + x23) * 0.5, y123 = (y12 + y23) * 0.5;
927
959
  const x0123 = (x012 + x123) * 0.5, y0123 = (y012 + y123) * 0.5;
928
- flattenCubic2(x0, y0, x01, y01, x012, y012, x0123, y0123, tolerance, out);
929
- flattenCubic2(x0123, y0123, x123, y123, x23, y23, x3, y3, tolerance, out);
960
+ flattenCubicRec(x0, y0, x01, y01, x012, y012, x0123, y0123, tolerance, out);
961
+ flattenCubicRec(x0123, y0123, x123, y123, x23, y23, x3, y3, tolerance, out);
930
962
  }
931
963
  function flattenQuadratic(x0, y0, x1, y1, x2, y2, tolerance, out) {
964
+ if (!finite6(x0, y0, x1, y1, x2, y2)) {
965
+ out.push(x2, y2);
966
+ return;
967
+ }
968
+ flattenQuadraticRec(x0, y0, x1, y1, x2, y2, usableTolerance(tolerance), out);
969
+ }
970
+ function flattenQuadraticRec(x0, y0, x1, y1, x2, y2, tolerance, out) {
932
971
  const d = distPointToLine(x1, y1, x0, y0, x2, y2);
933
972
  if (d <= tolerance) {
934
973
  out.push(x2, y2);
@@ -937,11 +976,16 @@ function flattenQuadratic(x0, y0, x1, y1, x2, y2, tolerance, out) {
937
976
  const x01 = (x0 + x1) * 0.5, y01 = (y0 + y1) * 0.5;
938
977
  const x12 = (x1 + x2) * 0.5, y12 = (y1 + y2) * 0.5;
939
978
  const x012 = (x01 + x12) * 0.5, y012 = (y01 + y12) * 0.5;
940
- flattenQuadratic(x0, y0, x01, y01, x012, y012, tolerance, out);
941
- flattenQuadratic(x012, y012, x12, y12, x2, y2, tolerance, out);
979
+ flattenQuadraticRec(x0, y0, x01, y01, x012, y012, tolerance, out);
980
+ flattenQuadraticRec(x012, y012, x12, y12, x2, y2, tolerance, out);
942
981
  }
943
982
  function flattenCubicWithArcLen(x0, y0, x1, y1, x2, y2, x3, y3, tolerance, out, arcOut) {
944
- return flattenCubicArcRec(x0, y0, x1, y1, x2, y2, x3, y3, tolerance, out, arcOut, 0);
983
+ if (!finite8(x0, y0, x1, y1, x2, y2, x3, y3)) {
984
+ out.push(x3, y3);
985
+ arcOut.push(0);
986
+ return 0;
987
+ }
988
+ return flattenCubicArcRec(x0, y0, x1, y1, x2, y2, x3, y3, usableTolerance(tolerance), out, arcOut, 0);
945
989
  }
946
990
  function flattenCubicArcRec(x0, y0, x1, y1, x2, y2, x3, y3, tolerance, out, arcOut, accum) {
947
991
  const d1 = distPointToLine(x1, y1, x0, y0, x3, y3);
@@ -966,7 +1010,12 @@ function flattenCubicArcRec(x0, y0, x1, y1, x2, y2, x3, y3, tolerance, out, arcO
966
1010
  return accum;
967
1011
  }
968
1012
  function flattenQuadraticWithArcLen(x0, y0, x1, y1, x2, y2, tolerance, out, arcOut) {
969
- return flattenQuadraticArcRec(x0, y0, x1, y1, x2, y2, tolerance, out, arcOut, 0);
1013
+ if (!finite6(x0, y0, x1, y1, x2, y2)) {
1014
+ out.push(x2, y2);
1015
+ arcOut.push(0);
1016
+ return 0;
1017
+ }
1018
+ return flattenQuadraticArcRec(x0, y0, x1, y1, x2, y2, usableTolerance(tolerance), out, arcOut, 0);
970
1019
  }
971
1020
  function flattenQuadraticArcRec(x0, y0, x1, y1, x2, y2, tolerance, out, arcOut, accum) {
972
1021
  const d = distPointToLine(x1, y1, x0, y0, x2, y2);
@@ -1159,22 +1208,6 @@ function polylineWithinThreshold(path, px, py, t2, tolerance) {
1159
1208
  if (subOpen && checkSegments(false)) return true;
1160
1209
  return false;
1161
1210
  }
1162
- function pointSegmentDist2(px, py, ax, ay, bx, by) {
1163
- const dx = bx - ax;
1164
- const dy = by - ay;
1165
- const len2 = dx * dx + dy * dy;
1166
- if (len2 === 0) {
1167
- const ex2 = px - ax, ey2 = py - ay;
1168
- return ex2 * ex2 + ey2 * ey2;
1169
- }
1170
- let t = ((px - ax) * dx + (py - ay) * dy) / len2;
1171
- if (t < 0) t = 0;
1172
- else if (t > 1) t = 1;
1173
- const cx = ax + t * dx;
1174
- const cy = ay + t * dy;
1175
- const ex = px - cx, ey = py - cy;
1176
- return ex * ex + ey * ey;
1177
- }
1178
1211
  var PathBuilder = class _PathBuilder {
1179
1212
  cmds = [];
1180
1213
  xs = [];
@@ -1563,7 +1596,7 @@ function createDeleteOp(args) {
1563
1596
  }
1564
1597
  registerOpFactory("delete", (args) => createDeleteOp(args));
1565
1598
 
1566
- // ../core/dist/chunk-3KGMJMOS.js
1599
+ // ../core/dist/chunk-CSMF654J.js
1567
1600
  function createTransformOp(args) {
1568
1601
  const { id, from, to, label, coalesceKey = `transform:${id}` } = args;
1569
1602
  return {
@@ -1644,7 +1677,7 @@ function snap(strategy, opts = {}) {
1644
1677
  };
1645
1678
  }
1646
1679
 
1647
- // ../core/dist/chunk-3JYFAQIB.js
1680
+ // ../core/dist/chunk-J7LUY47N.js
1648
1681
  function lockAspectWithModifier(opts = {}) {
1649
1682
  const { key = "shift" } = opts;
1650
1683
  return {
@@ -2850,6 +2883,7 @@ function createHistory(adapter, options = {}) {
2850
2883
  const logger = options.debug ?? SILENT;
2851
2884
  let nextEntryId = 1;
2852
2885
  let version = 0;
2886
+ let coalesceAnchorVersion = -1;
2853
2887
  const listeners = /* @__PURE__ */ new Set();
2854
2888
  function bump() {
2855
2889
  version++;
@@ -2890,6 +2924,7 @@ function createHistory(adapter, options = {}) {
2890
2924
  }
2891
2925
  function canCoalesce(top, incoming) {
2892
2926
  if (coalesceWindowMs <= 0) return false;
2927
+ if (version !== coalesceAnchorVersion) return false;
2893
2928
  if (now() - top.timestamp > coalesceWindowMs) return false;
2894
2929
  if (top.forwardOps.length === 0 || incoming.length === 0) return false;
2895
2930
  if (top.forwardOps.length !== incoming.length) return false;
@@ -2927,9 +2962,9 @@ function createHistory(adapter, options = {}) {
2927
2962
  for (const id of incoming) merged.add(id);
2928
2963
  top.touchedIds = merged;
2929
2964
  }
2930
- dropRedo();
2931
2965
  logger.log(`coalesce '${label}' into entry id=${top.id} (${ops.length} ops)`);
2932
2966
  bump();
2967
+ coalesceAnchorVersion = version;
2933
2968
  return;
2934
2969
  }
2935
2970
  logger.log(`push '${label}' (${ops.length} ops)`);
@@ -2937,6 +2972,7 @@ function createHistory(adapter, options = {}) {
2937
2972
  dropRedo();
2938
2973
  enforceLimit();
2939
2974
  bump();
2975
+ coalesceAnchorVersion = version;
2940
2976
  }
2941
2977
  return {
2942
2978
  apply(op, label) {
@@ -3042,7 +3078,11 @@ function createHistory(adapter, options = {}) {
3042
3078
  return j;
3043
3079
  },
3044
3080
  resumeJournal(journal) {
3081
+ if (activeJournal !== null && activeJournal !== journal && activeJournal.isActive()) {
3082
+ throw new Error("A journal is already active \u2014 commit, cancel, or suspend it first");
3083
+ }
3045
3084
  _resumeJournalInternal(journal);
3085
+ activeJournal = journal;
3046
3086
  },
3047
3087
  restore(snapshot) {
3048
3088
  undoStack.length = 0;
@@ -3123,9 +3163,8 @@ function serialToEntry(se, custom, logger) {
3123
3163
  label: se.label,
3124
3164
  forwardOps,
3125
3165
  baseOps,
3126
- // Restored entries inherit a "now" timestamp coalesce eligibility is
3127
- // a within-session concept and a restored entry shouldn't merge with a
3128
- // freshly-typed one regardless of when it was originally pushed.
3166
+ // Coalescing is a within-session concept; a restored entry is never a
3167
+ // coalesce anchor, so its timestamp only has to be non-null.
3129
3168
  timestamp: 0,
3130
3169
  // Re-derive touchedIds from the rebuilt ops rather than trying to
3131
3170
  // round-trip the Set through the serialized form (Sets aren't JSON-safe).
@@ -3133,7 +3172,7 @@ function serialToEntry(se, custom, logger) {
3133
3172
  };
3134
3173
  }
3135
3174
 
3136
- // ../core/dist/chunk-SMCZRSYB.js
3175
+ // ../core/dist/chunk-3B4QEB2G.js
3137
3176
  function worldToScreen(worldX, worldY, view) {
3138
3177
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
3139
3178
  }
@@ -3570,13 +3609,20 @@ var GLTextureCache = class {
3570
3609
  }
3571
3610
  gl;
3572
3611
  map = /* @__PURE__ */ new Map();
3612
+ wraps = /* @__PURE__ */ new Map();
3573
3613
  has(id) {
3574
3614
  return this.map.has(id);
3575
3615
  }
3576
- /** Uploads once per id; `wrap` therefore applies on first upload only.
3577
- * An id is either a pattern tile or a shader texture, never both. */
3616
+ /** Uploads the pixels once per id. `wrap` is re-applied whenever it differs
3617
+ * from what the id was last uploaded under: the same registered image can
3618
+ * be a clamped shader texture in one draw and a repeating pattern tile in
3619
+ * the next, and first-upload-wins gave the second one the first one's mode. */
3578
3620
  upload(id, source, wrap = "clamp") {
3579
- if (this.map.has(id)) return id;
3621
+ const cached2 = this.map.get(id);
3622
+ if (cached2) {
3623
+ if (this.wraps.get(id) !== wrap) this.applyWrap(id, cached2, wrap);
3624
+ return id;
3625
+ }
3580
3626
  const gl = this.gl;
3581
3627
  const tex = gl.createTexture();
3582
3628
  if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
@@ -3589,8 +3635,18 @@ var GLTextureCache = class {
3589
3635
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
3590
3636
  gl.bindTexture(gl.TEXTURE_2D, null);
3591
3637
  this.map.set(id, tex);
3638
+ this.wraps.set(id, wrap);
3592
3639
  return id;
3593
3640
  }
3641
+ applyWrap(id, tex, wrap) {
3642
+ const gl = this.gl;
3643
+ const mode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
3644
+ gl.bindTexture(gl.TEXTURE_2D, tex);
3645
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, mode);
3646
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, mode);
3647
+ gl.bindTexture(gl.TEXTURE_2D, null);
3648
+ this.wraps.set(id, wrap);
3649
+ }
3594
3650
  /** Create a single-channel R8 texture from raw bytes (full upload).
3595
3651
  * No-op if `id` already exists. Same LINEAR/CLAMP params as `upload`;
3596
3652
  * UNPACK_ALIGNMENT dropped to 1 for non-4-aligned row widths. */
@@ -3609,6 +3665,7 @@ var GLTextureCache = class {
3609
3665
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3610
3666
  gl.bindTexture(gl.TEXTURE_2D, null);
3611
3667
  this.map.set(id, tex);
3668
+ this.wraps.set(id, "clamp");
3612
3669
  }
3613
3670
  /** Patch a rect of an existing R8 texture with tightly-packed w×h bytes. */
3614
3671
  subImageR8(id, x, y, w, h, data) {
@@ -3640,6 +3697,7 @@ var GLTextureCache = class {
3640
3697
  gl.deleteTexture(tex);
3641
3698
  }
3642
3699
  this.map.clear();
3700
+ this.wraps.clear();
3643
3701
  }
3644
3702
  };
3645
3703
  var GLImageCache = class {
@@ -4289,6 +4347,13 @@ var GroupState = class {
4289
4347
  this.alphaStack.push(nextAlpha);
4290
4348
  this.colorMatrixStack.push(nextCM);
4291
4349
  }
4350
+ /** Drop every pushed frame, leaving the root. A frame that throws part-way
4351
+ * down the tree never pops, and the next frame would draw under leftovers. */
4352
+ reset() {
4353
+ this.transformStack.length = 1;
4354
+ this.alphaStack.length = 1;
4355
+ this.colorMatrixStack.length = 1;
4356
+ }
4292
4357
  pop() {
4293
4358
  if (this.transformStack.length <= 1) {
4294
4359
  throw new Error("GroupState.pop: cannot pop root frame");
@@ -4936,16 +5001,28 @@ function verticalAlignOffset(align, boxHeight, textHeight) {
4936
5001
  return align === "center" ? slack / 2 : slack;
4937
5002
  }
4938
5003
  var NUM_RE = /[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g;
5004
+ var NUM_RE_AT = /[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/y;
5005
+ function isCommandLetter(d, j) {
5006
+ const c = d[j];
5007
+ if (!/[A-Za-z]/.test(c)) return false;
5008
+ if (c !== "e" && c !== "E") return true;
5009
+ if (!/[\d.]/.test(d[j - 1] ?? "")) return true;
5010
+ const k = d[j + 1] === "+" || d[j + 1] === "-" ? j + 2 : j + 1;
5011
+ return !/\d/.test(d[k] ?? "");
5012
+ }
4939
5013
  function tokenize(d) {
4940
5014
  const out = [];
4941
5015
  let i = 0;
4942
5016
  while (i < d.length) {
4943
- const ch = d[i];
4944
- if (/[A-Za-z]/.test(ch)) {
5017
+ if (isCommandLetter(d, i)) {
4945
5018
  let j = i + 1;
4946
- while (j < d.length && !/[A-Za-z]/.test(d[j])) j++;
5019
+ while (j < d.length && !isCommandLetter(d, j)) j++;
4947
5020
  const argSlice = d.slice(i + 1, j);
4948
- out.push({ cmd: ch, args: readNumbers(argSlice) });
5021
+ const cmd = d[i];
5022
+ out.push({
5023
+ cmd,
5024
+ args: cmd === "A" || cmd === "a" ? readArcArgs(argSlice) : readNumbers(argSlice)
5025
+ });
4949
5026
  i = j;
4950
5027
  } else {
4951
5028
  i++;
@@ -4963,6 +5040,30 @@ function readNumbers(s) {
4963
5040
  }
4964
5041
  return out;
4965
5042
  }
5043
+ function readArcArgs(s) {
5044
+ const out = [];
5045
+ let i = 0;
5046
+ while (i < s.length) {
5047
+ const c = s[i];
5048
+ if (c === "," || c === " " || c === " " || c === "\n" || c === "\r") {
5049
+ i++;
5050
+ continue;
5051
+ }
5052
+ const slot = out.length % 7;
5053
+ if (slot === 3 || slot === 4) {
5054
+ if (c !== "0" && c !== "1") return out;
5055
+ out.push(c === "1" ? 1 : 0);
5056
+ i++;
5057
+ continue;
5058
+ }
5059
+ NUM_RE_AT.lastIndex = i;
5060
+ const m = NUM_RE_AT.exec(s);
5061
+ if (!m) return out;
5062
+ out.push(Number(m[0]));
5063
+ i = NUM_RE_AT.lastIndex;
5064
+ }
5065
+ return out;
5066
+ }
4966
5067
  var ARG_COUNTS = {
4967
5068
  M: 2,
4968
5069
  L: 2,
@@ -5153,9 +5254,9 @@ function arcToCubics(x1, y1, x2, y2, rx, ry, xAxisRotationDeg, largeArc, sweep,
5153
5254
  const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
5154
5255
  const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
5155
5256
  const angle = (ux, uy, vx, vy) => {
5156
- const dot = ux * vx + uy * vy;
5257
+ const dot2 = ux * vx + uy * vy;
5157
5258
  const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
5158
- let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
5259
+ let a = Math.acos(Math.max(-1, Math.min(1, dot2 / len)));
5159
5260
  if (ux * vy - uy * vx < 0) a = -a;
5160
5261
  return a;
5161
5262
  };
@@ -5867,42 +5968,42 @@ var MAX_VERTICES_PER_BATCH = 32768;
5867
5968
  var FLOATS_PER_VERTEX = 6;
5868
5969
  var INITIAL_VERTICES = 256;
5869
5970
  var INITIAL_INDICES = 384;
5971
+ var SOLID_RING_SIZE = 64;
5972
+ var SOLID_RING_SLOT_VERTICES = 1024;
5973
+ var SOLID_RING_SLOT_INDICES = 3072;
5974
+ var SOLID_LARGE_RING_SIZE = 4;
5975
+ function doubledTo(from, need) {
5976
+ let capacity = from;
5977
+ while (capacity < need) capacity *= 2;
5978
+ return capacity;
5979
+ }
5870
5980
  var SolidBatch = class {
5871
5981
  gl;
5872
- vao;
5873
- vbo;
5874
- ibo;
5982
+ aPos;
5983
+ aColor;
5984
+ /** Cycled per flush, and created on first use so a renderer that flushes
5985
+ * rarely allocates as few slots as it flushes. */
5986
+ ring = new Array(SOLID_RING_SIZE).fill(void 0);
5987
+ next = 0;
5988
+ /** The same, for flushes past a slot's capacity; these sets grow to fit. */
5989
+ largeRing = new Array(SOLID_LARGE_RING_SIZE).fill(void 0);
5990
+ nextLarge = 0;
5875
5991
  verts = new Float32Array(INITIAL_VERTICES * FLOATS_PER_VERTEX);
5876
5992
  idx = new Uint32Array(INITIAL_INDICES);
5877
5993
  nVerts = 0;
5878
5994
  nIdx = 0;
5879
- /** What the GPU buffers are currently sized for. */
5880
- vboVerts = 0;
5881
- iboIdx = 0;
5995
+ /** Whether the staged run is rects alone, so its indices are the canonical
5996
+ * quad pattern and a slot already holding that pattern needs no upload. */
5997
+ pureRects = true;
5882
5998
  constructor(gl, prog) {
5883
5999
  const aPos = prog.attribute("a_position");
5884
6000
  const aColor = prog.attribute("a_vertexColor");
5885
6001
  if (aPos === void 0 || aColor === void 0) {
5886
6002
  throw new Error("SolidBatch: vertex-color program is missing a_position / a_vertexColor");
5887
6003
  }
5888
- const vao = gl.createVertexArray();
5889
- const vbo = gl.createBuffer();
5890
- const ibo = gl.createBuffer();
5891
- if (!vao || !vbo || !ibo) throw new Error("SolidBatch: failed to create GL objects");
5892
6004
  this.gl = gl;
5893
- this.vao = vao;
5894
- this.vbo = vbo;
5895
- this.ibo = ibo;
5896
- const stride = FLOATS_PER_VERTEX * 4;
5897
- gl.bindVertexArray(vao);
5898
- gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
5899
- gl.enableVertexAttribArray(aPos);
5900
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, stride, 0);
5901
- gl.enableVertexAttribArray(aColor);
5902
- gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 8);
5903
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
5904
- gl.bindVertexArray(null);
5905
- this.growGpu(INITIAL_VERTICES, INITIAL_INDICES);
6005
+ this.aPos = aPos;
6006
+ this.aColor = aColor;
5906
6007
  }
5907
6008
  get length() {
5908
6009
  return this.nIdx;
@@ -5972,6 +6073,7 @@ var SolidBatch = class {
5972
6073
  const srcIdx = mesh.indices;
5973
6074
  const n = src.length >> 1;
5974
6075
  this.reserve(n, srcIdx.length);
6076
+ this.pureRects = false;
5975
6077
  const v = this.verts;
5976
6078
  let i = this.nVerts * FLOATS_PER_VERTEX;
5977
6079
  const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
@@ -5992,67 +6094,109 @@ var SolidBatch = class {
5992
6094
  this.nVerts += n;
5993
6095
  this.nIdx += srcIdx.length;
5994
6096
  }
5995
- /** Upload the staged geometry and bind the batch VAO. Returns the index
5996
- * count for the caller's `drawElements`. */
6097
+ /** Upload the staged geometry into the next set of buffers and bind its VAO.
6098
+ * Returns the index count for the caller's `drawElements`. */
5997
6099
  uploadAndBind() {
5998
6100
  const gl = this.gl;
5999
- if (this.nVerts > this.vboVerts || this.nIdx > this.iboIdx) {
6000
- this.growGpu(this.nVerts, this.nIdx);
6001
- }
6002
- gl.bindVertexArray(this.vao);
6003
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
6101
+ const set = this.nVerts <= SOLID_RING_SLOT_VERTICES && this.nIdx <= SOLID_RING_SLOT_INDICES ? this.nextRingSlot() : this.nextLargeSlot();
6102
+ gl.bindVertexArray(set.vao);
6103
+ gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
6004
6104
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nVerts * FLOATS_PER_VERTEX);
6005
- gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, this.idx, 0, this.nIdx);
6105
+ const rects = this.pureRects ? this.nVerts >> 2 : -1;
6106
+ if (rects < 0 || rects !== set.indexRects) {
6107
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, this.idx, 0, this.nIdx);
6108
+ set.indexRects = rects;
6109
+ }
6006
6110
  return this.nIdx;
6007
6111
  }
6008
6112
  reset() {
6009
6113
  this.nVerts = 0;
6010
6114
  this.nIdx = 0;
6115
+ this.pureRects = true;
6011
6116
  }
6012
6117
  dispose() {
6013
- const gl = this.gl;
6014
- gl.deleteBuffer(this.vbo);
6015
- gl.deleteBuffer(this.ibo);
6016
- gl.deleteVertexArray(this.vao);
6118
+ for (const set of [...this.ring, ...this.largeRing]) {
6119
+ if (set) this.deleteSet(set);
6120
+ }
6121
+ this.ring.fill(void 0);
6122
+ this.largeRing.fill(void 0);
6017
6123
  }
6018
6124
  /** Grow the CPU arrays so `vertices` / `indices` more fit. */
6019
6125
  reserve(vertices, indices) {
6020
6126
  const needVerts = (this.nVerts + vertices) * FLOATS_PER_VERTEX;
6021
6127
  if (needVerts > this.verts.length) {
6022
- let len = this.verts.length;
6023
- while (len < needVerts) len *= 2;
6024
- const grown = new Float32Array(len);
6128
+ const grown = new Float32Array(doubledTo(this.verts.length, needVerts));
6025
6129
  grown.set(this.verts);
6026
6130
  this.verts = grown;
6027
6131
  }
6028
6132
  const needIdx = this.nIdx + indices;
6029
6133
  if (needIdx > this.idx.length) {
6030
- let len = this.idx.length;
6031
- while (len < needIdx) len *= 2;
6032
- const grown = new Uint32Array(len);
6134
+ const grown = new Uint32Array(doubledTo(this.idx.length, needIdx));
6033
6135
  grown.set(this.idx);
6034
6136
  this.idx = grown;
6035
6137
  }
6036
6138
  }
6037
- /** Size both GPU buffers for at least what is staged, doubling to amortize. */
6038
- growGpu(vertices, indices) {
6139
+ nextRingSlot() {
6140
+ const slot = this.next;
6141
+ this.next = (slot + 1) % SOLID_RING_SIZE;
6142
+ const existing = this.ring[slot];
6143
+ if (existing) return existing;
6144
+ const set = this.createSet(SOLID_RING_SLOT_VERTICES, SOLID_RING_SLOT_INDICES);
6145
+ this.ring[slot] = set;
6146
+ return set;
6147
+ }
6148
+ nextLargeSlot() {
6039
6149
  const gl = this.gl;
6040
- if (vertices > this.vboVerts) {
6041
- let capacity = Math.max(this.vboVerts, INITIAL_VERTICES);
6042
- while (capacity < vertices) capacity *= 2;
6043
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
6044
- gl.bufferData(gl.ARRAY_BUFFER, capacity * FLOATS_PER_VERTEX * 4, gl.DYNAMIC_DRAW);
6045
- this.vboVerts = capacity;
6046
- }
6047
- if (indices > this.iboIdx) {
6048
- let capacity = Math.max(this.iboIdx, INITIAL_INDICES);
6049
- while (capacity < indices) capacity *= 2;
6050
- gl.bindVertexArray(this.vao);
6051
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo);
6052
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, capacity * 4, gl.DYNAMIC_DRAW);
6150
+ const slot = this.nextLarge;
6151
+ this.nextLarge = (slot + 1) % SOLID_LARGE_RING_SIZE;
6152
+ let set = this.largeRing[slot];
6153
+ if (!set) {
6154
+ set = this.createSet(
6155
+ doubledTo(SOLID_RING_SLOT_VERTICES, this.nVerts),
6156
+ doubledTo(SOLID_RING_SLOT_INDICES, this.nIdx)
6157
+ );
6158
+ this.largeRing[slot] = set;
6159
+ return set;
6160
+ }
6161
+ if (this.nVerts > set.vertexCapacity) {
6162
+ set.vertexCapacity = doubledTo(set.vertexCapacity, this.nVerts);
6163
+ gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
6164
+ gl.bufferData(gl.ARRAY_BUFFER, set.vertexCapacity * FLOATS_PER_VERTEX * 4, gl.DYNAMIC_DRAW);
6165
+ }
6166
+ if (this.nIdx > set.indexCapacity) {
6167
+ set.indexCapacity = doubledTo(set.indexCapacity, this.nIdx);
6168
+ gl.bindVertexArray(set.vao);
6169
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, set.ibo);
6170
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, set.indexCapacity * 4, gl.DYNAMIC_DRAW);
6053
6171
  gl.bindVertexArray(null);
6054
- this.iboIdx = capacity;
6172
+ set.indexRects = -1;
6055
6173
  }
6174
+ return set;
6175
+ }
6176
+ createSet(vertices, indices) {
6177
+ const gl = this.gl;
6178
+ const vao = gl.createVertexArray();
6179
+ const vbo = gl.createBuffer();
6180
+ const ibo = gl.createBuffer();
6181
+ if (!vao || !vbo || !ibo) throw new Error("SolidBatch: failed to create GL objects");
6182
+ const stride = FLOATS_PER_VERTEX * 4;
6183
+ gl.bindVertexArray(vao);
6184
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
6185
+ gl.bufferData(gl.ARRAY_BUFFER, vertices * stride, gl.DYNAMIC_DRAW);
6186
+ gl.enableVertexAttribArray(this.aPos);
6187
+ gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, stride, 0);
6188
+ gl.enableVertexAttribArray(this.aColor);
6189
+ gl.vertexAttribPointer(this.aColor, 4, gl.FLOAT, false, stride, 8);
6190
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
6191
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices * 4, gl.DYNAMIC_DRAW);
6192
+ gl.bindVertexArray(null);
6193
+ return { vao, vbo, ibo, vertexCapacity: vertices, indexCapacity: indices, indexRects: -1 };
6194
+ }
6195
+ deleteSet(set) {
6196
+ const gl = this.gl;
6197
+ gl.deleteBuffer(set.vbo);
6198
+ gl.deleteBuffer(set.ibo);
6199
+ gl.deleteVertexArray(set.vao);
6056
6200
  }
6057
6201
  };
6058
6202
  var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
@@ -6075,6 +6219,25 @@ function sameValues(prev, next) {
6075
6219
  for (let i = 0; i < prev.length; i++) if (prev[i] !== next[i]) return false;
6076
6220
  return true;
6077
6221
  }
6222
+ function setColorUniform(ctx, prog, r, g, b, a) {
6223
+ const loc = prog.uniform("u_color");
6224
+ if (loc === void 0) return;
6225
+ const uploaded = uploadedFor(ctx, prog);
6226
+ const prev = uploaded.color;
6227
+ if (prev !== void 0 && prev[0] === r && prev[1] === g && prev[2] === b && prev[3] === a) {
6228
+ return;
6229
+ }
6230
+ ctx.gl.uniform4f(loc, r, g, b, a);
6231
+ uploaded.color = [r, g, b, a];
6232
+ }
6233
+ function setAlphaUniform(ctx, prog, alpha) {
6234
+ const loc = prog.uniform("u_alpha");
6235
+ if (loc === void 0) return;
6236
+ const uploaded = uploadedFor(ctx, prog);
6237
+ if (uploaded.alpha === alpha) return;
6238
+ ctx.gl.uniform1f(loc, alpha);
6239
+ uploaded.alpha = alpha;
6240
+ }
6078
6241
  var BATCH_MODEL = mat3.identity();
6079
6242
  function projFor(ctx) {
6080
6243
  let proj = FRAME_PROJ.get(ctx);
@@ -6367,8 +6530,8 @@ function flushSolids(ctx) {
6367
6530
  gl.useProgram(prog.handle);
6368
6531
  const indexCount = batch.uploadAndBind();
6369
6532
  setProjAndModel(ctx, prog, BATCH_MODEL);
6370
- gl.uniform4f(prog.uniform("u_color"), 1, 1, 1, 1);
6371
- gl.uniform1f(prog.uniform("u_alpha"), staged.alpha);
6533
+ setColorUniform(ctx, prog, 1, 1, 1, 1);
6534
+ setAlphaUniform(ctx, prog, staged.alpha);
6372
6535
  setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
6373
6536
  applyClipTest(ctx, staged.clipDepth);
6374
6537
  gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
@@ -6433,10 +6596,9 @@ function setProjAndModel(ctx, prog, model = ctx.state.transform) {
6433
6596
  }
6434
6597
  }
6435
6598
  function setSolidPaintUniforms(ctx, prog, color, opacity) {
6436
- const gl = ctx.gl;
6437
6599
  const [r, g, b, a] = resolveColor(color);
6438
- gl.uniform4f(prog.uniform("u_color"), r, g, b, a * (opacity ?? 1));
6439
- gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
6600
+ setColorUniform(ctx, prog, r, g, b, a * (opacity ?? 1));
6601
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
6440
6602
  }
6441
6603
  function drawPathFillByKind(ctx, fill, handle) {
6442
6604
  const kind = fill.fill ?? "solid";
@@ -6482,7 +6644,7 @@ function drawPathFillPattern(ctx, fill, handle) {
6482
6644
  ctx.textureCache.bind(tex.id, 0);
6483
6645
  gl.uniform1i(ctx.patternFill.uniform("u_sampler"), 0);
6484
6646
  gl.uniform1f(ctx.patternFill.uniform("u_opacity"), fill.opacity ?? 1);
6485
- gl.uniform1f(ctx.patternFill.uniform("u_alpha"), ctx.state.alpha);
6647
+ setAlphaUniform(ctx, ctx.patternFill, ctx.state.alpha);
6486
6648
  applyClipTest(ctx);
6487
6649
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
6488
6650
  gl.bindVertexArray(null);
@@ -6506,7 +6668,7 @@ function drawPathFillGradient(ctx, fill, handle) {
6506
6668
  gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
6507
6669
  ctx.gradRampCache.bind(key, 0);
6508
6670
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
6509
- gl.uniform1f(ctx.gradFill.uniform("u_alpha"), ctx.state.alpha);
6671
+ setAlphaUniform(ctx, ctx.gradFill, ctx.state.alpha);
6510
6672
  gl.uniform1f(ctx.gradFill.uniform("u_opacity"), fill.opacity ?? 1);
6511
6673
  if (fill.fill === "linear-gradient") {
6512
6674
  gl.uniform1i(ctx.gradFill.uniform("u_gradKind"), 0);
@@ -6656,6 +6818,7 @@ function drawPathStrokeUnclipped(ctx, cmd) {
6656
6818
  gl.enableVertexAttribArray(aVColorLoc);
6657
6819
  gl.vertexAttribPointer(aVColorLoc, 4, gl.FLOAT, false, 0, 0);
6658
6820
  }
6821
+ applyClipTest(ctx);
6659
6822
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
6660
6823
  gl.bindVertexArray(null);
6661
6824
  gl.deleteBuffer(colorVbo);
@@ -6771,7 +6934,7 @@ function drawText(ctx, cmd) {
6771
6934
  preparedPrograms.add(prog);
6772
6935
  setProjAndModel(ctx, prog);
6773
6936
  setColorMatrixUniforms(ctx, prog);
6774
- gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
6937
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
6775
6938
  }
6776
6939
  drawTextGroup(ctx, group, prog, dx, dy);
6777
6940
  }
@@ -6889,8 +7052,8 @@ function drawTextDecorations(ctx, decorations, dx, dy) {
6889
7052
  if (!ibo) throw new Error("drawTextDecorations: createBuffer (IBO) returned null");
6890
7053
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
6891
7054
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.DYNAMIC_DRAW);
6892
- gl.uniform4f(prog.uniform("u_color"), rgba[0], rgba[1], rgba[2], rgba[3]);
6893
- gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
7055
+ setColorUniform(ctx, prog, rgba[0], rgba[1], rgba[2], rgba[3]);
7056
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
6894
7057
  gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0);
6895
7058
  gl.bindVertexArray(null);
6896
7059
  gl.deleteVertexArray(vao);
@@ -6974,7 +7137,7 @@ function drawTextGroup(ctx, group, prog, dx, dy) {
6974
7137
  if ("color" in group.fill) {
6975
7138
  [r, g, b, a] = resolveColor(group.fill.color);
6976
7139
  }
6977
- gl.uniform4f(prog.uniform("u_color"), r, g, b, a);
7140
+ setColorUniform(ctx, prog, r, g, b, a);
6978
7141
  const synthBoldAmount = group.synthetic.bold ? 0.08 : 0;
6979
7142
  const uSynthBold = prog.uniform("u_synthBold");
6980
7143
  if (uSynthBold !== void 0) gl.uniform1f(uSynthBold, synthBoldAmount);
@@ -7078,7 +7241,7 @@ function drawImage(ctx, cmd) {
7078
7241
  );
7079
7242
  gl.uniform1i(prog.uniform("u_sampler"), 0);
7080
7243
  gl.uniform1f(prog.uniform("u_opacity"), cmd.opacity ?? 1);
7081
- gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
7244
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
7082
7245
  applyClipTest(ctx);
7083
7246
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, 0);
7084
7247
  gl.bindVertexArray(null);
@@ -7293,6 +7456,8 @@ var WeaselRenderer = class {
7293
7456
  const program = new ShaderProgram(this.gl, vertSrc, fragSrc);
7294
7457
  program.lookupUniforms([...CUSTOM_KIT_UNIFORMS, ...extractUniformNames(fragSrc)]);
7295
7458
  program.lookupAttributes(CUSTOM_ATTRIBUTES);
7459
+ const previous = this.programRegistry.get(handle.id);
7460
+ if (previous) this.gl.deleteProgram(previous.handle);
7296
7461
  this.programRegistry.set(handle.id, program);
7297
7462
  }
7298
7463
  applyViewport() {
@@ -7390,7 +7555,7 @@ var WeaselRenderer = class {
7390
7555
  }
7391
7556
  this.meshCache.freeTransient();
7392
7557
  this.meshCache.drainPendingDeletes();
7393
- for (const prog of [this.pathFill, this.pathFillVColor, this.textSdf, this.textSdfR8, this.imageFill, this.gradFill]) {
7558
+ for (const prog of [this.pathFill, this.pathFillVColor, this.textSdf, this.textSdfR8, this.imageFill, this.gradFill, this.patternFill]) {
7394
7559
  gl.deleteProgram(prog.handle);
7395
7560
  }
7396
7561
  for (const prog of this.programRegistry.values()) {
@@ -7417,6 +7582,7 @@ var WeaselRenderer = class {
7417
7582
  const gl = this.gl;
7418
7583
  this.meshCache.drainPendingDeletes();
7419
7584
  resetBakeBudget(this.bakeBudget);
7585
+ this.groupState.reset();
7420
7586
  gl.stencilMask(255);
7421
7587
  gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
7422
7588
  const ctx = {
@@ -7886,7 +8052,7 @@ var IDENTITY_POSE_COMPOSITION = {
7886
8052
  compose: (_parent, child) => child,
7887
8053
  decompose: (_parent, world) => world
7888
8054
  };
7889
- function composeWorldPose(adapter, id, compose) {
8055
+ function composeWorldPose(adapter, id, compose2) {
7890
8056
  const chain = [id];
7891
8057
  const seen = /* @__PURE__ */ new Set([id]);
7892
8058
  let cursor = adapter.getParent(id);
@@ -7898,16 +8064,16 @@ function composeWorldPose(adapter, id, compose) {
7898
8064
  }
7899
8065
  let world = adapter.getPose(chain[chain.length - 1]);
7900
8066
  for (let i = chain.length - 2; i >= 0; i--) {
7901
- world = compose(world, adapter.getPose(chain[i]));
8067
+ world = compose2(world, adapter.getPose(chain[i]));
7902
8068
  }
7903
8069
  return world;
7904
8070
  }
7905
8071
  function translateRectPose(pose, dx, dy) {
7906
8072
  return { ...pose, x: pose.x + dx, y: pose.y + dy };
7907
8073
  }
7908
- function rebaseLocalPose(adapter, worldPose, newParentId, compose, decompose) {
8074
+ function rebaseLocalPose(adapter, worldPose, newParentId, compose2, decompose) {
7909
8075
  if (newParentId === null) return worldPose;
7910
- const parentWorld = composeWorldPose(adapter, newParentId, compose);
8076
+ const parentWorld = composeWorldPose(adapter, newParentId, compose2);
7911
8077
  return decompose(parentWorld, worldPose);
7912
8078
  }
7913
8079
  function translatePoseGeneric(pose, dx, dy, projection) {
@@ -10802,9 +10968,17 @@ function arraysEqual(a, b) {
10802
10968
  return true;
10803
10969
  }
10804
10970
  function createPartitionedReorderOp(args) {
10805
- const { ids, fn, label } = args;
10806
- let restore = null;
10971
+ const { ids, direction, fn, label, prevOrders } = args;
10972
+ const argsForSerial = {
10973
+ ids,
10974
+ direction,
10975
+ label,
10976
+ prevOrders: prevOrders ? prevOrders.map((e) => ({ ...e, before: e.before.slice() })) : void 0
10977
+ };
10978
+ let restore = prevOrders ? prevOrders.map((e) => ({ ...e, before: e.before.slice() })) : null;
10807
10979
  return {
10980
+ name: "reorder",
10981
+ args: argsForSerial,
10808
10982
  label,
10809
10983
  apply(adapter) {
10810
10984
  const a = adapter;
@@ -10827,6 +11001,7 @@ function createPartitionedReorderOp(args) {
10827
11001
  mutated = true;
10828
11002
  }
10829
11003
  restore = snapshots;
11004
+ argsForSerial.prevOrders = snapshots.map((e) => ({ ...e, before: e.before.slice() }));
10830
11005
  return mutated;
10831
11006
  },
10832
11007
  invert() {
@@ -10858,10 +11033,17 @@ var REORDER_DIRECTIONS = {
10858
11033
  back: { fn: sendToBack, defaultLabel: "Send to back" }
10859
11034
  };
10860
11035
  function createReorderOp(args) {
10861
- const { ids, direction, label } = args;
11036
+ const { ids, direction, label, prevOrders } = args;
10862
11037
  const { fn, defaultLabel } = REORDER_DIRECTIONS[direction];
10863
- return createPartitionedReorderOp({ ids, fn, label: label ?? defaultLabel });
11038
+ return createPartitionedReorderOp({
11039
+ ids,
11040
+ direction,
11041
+ fn,
11042
+ label: label ?? defaultLabel,
11043
+ prevOrders
11044
+ });
10864
11045
  }
11046
+ registerOpFactory("reorder", (args) => createReorderOp(args));
10865
11047
  function createMoveToIndexOp(args) {
10866
11048
  const { ids, parentId, index, label } = args;
10867
11049
  const argsForSerial = {
@@ -10986,7 +11168,7 @@ var reorderBackwardAction = {
10986
11168
  }
10987
11169
  ],
10988
11170
  eligible: { capability: "edits-page" },
10989
- requires: ["selection", "scene"],
11171
+ requires: ["selection", "scene", "applyOps"],
10990
11172
  invoker: {
10991
11173
  timing: "immediate",
10992
11174
  run: (deps, params) => {
@@ -11126,6 +11308,7 @@ function makeAlignAction(edge) {
11126
11308
  icon: ICON_FOR[edge],
11127
11309
  group: "align",
11128
11310
  eligible: { capability: "transforms-selection" },
11311
+ requires: ["selection", "scene"],
11129
11312
  // No default keybindings — six edges/centers don't fit a clean default
11130
11313
  // chord set. Wire bindings explicitly via the actions registry override map.
11131
11314
  invoker: {
@@ -11137,7 +11320,14 @@ function makeAlignAction(edge) {
11137
11320
  alignSelection(selection, scene, edge);
11138
11321
  }
11139
11322
  },
11140
- enabled: () => ActionDisabledReason.SelectionRequired
11323
+ // Deps-aware, matching `alignSelection`'s own `ids.length < 2` guard: a
11324
+ // constant disabled reason greys the entry out forever (see
11325
+ // `requiresSelection`).
11326
+ enabled: (deps) => {
11327
+ const selection = deps?.selection;
11328
+ const count = selection?.get().length ?? 0;
11329
+ return count >= 2 ? true : ActionDisabledReason.SelectionRequired;
11330
+ }
11141
11331
  };
11142
11332
  }
11143
11333
  var alignLeftAction = makeAlignAction("left");
@@ -11218,6 +11408,7 @@ function makeDistributeAction(axis) {
11218
11408
  icon: ICON_FOR2[axis],
11219
11409
  group: "distribute",
11220
11410
  eligible: { capability: "transforms-selection" },
11411
+ requires: ["selection", "scene"],
11221
11412
  // No default keybindings. Wire bindings explicitly via the actions registry.
11222
11413
  invoker: {
11223
11414
  timing: "immediate",
@@ -11228,7 +11419,14 @@ function makeDistributeAction(axis) {
11228
11419
  distributeSelection(selection, scene, axis);
11229
11420
  }
11230
11421
  },
11231
- enabled: () => ActionDisabledReason.SelectionRequired
11422
+ // Deps-aware, matching `distributeSelection`'s own `ids.length < 3` guard:
11423
+ // a constant disabled reason greys the entry out forever (see
11424
+ // `requiresSelection`).
11425
+ enabled: (deps) => {
11426
+ const selection = deps?.selection;
11427
+ const count = selection?.get().length ?? 0;
11428
+ return count >= 3 ? true : ActionDisabledReason.SelectionRequired;
11429
+ }
11232
11430
  };
11233
11431
  }
11234
11432
  var distributeHorizontalAction = makeDistributeAction("x");
@@ -11310,7 +11508,91 @@ function pathToMultiPolygon(path, opts = {}) {
11310
11508
  }
11311
11509
  finalizeCurrent();
11312
11510
  if (rings.length === 0) return [];
11313
- return rings.map((r) => [r]);
11511
+ return groupRings(rings, path.fillRule);
11512
+ }
11513
+ function ringSignedArea(ring) {
11514
+ let a = 0;
11515
+ for (let i = 0, n = ring.length - 1; i < n; i++) {
11516
+ a += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
11517
+ }
11518
+ return a * 0.5;
11519
+ }
11520
+ function ringBox(ring) {
11521
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
11522
+ for (const [x, y] of ring) {
11523
+ if (x < minX) minX = x;
11524
+ if (x > maxX) maxX = x;
11525
+ if (y < minY) minY = y;
11526
+ if (y > maxY) maxY = y;
11527
+ }
11528
+ return [minX, minY, maxX, maxY];
11529
+ }
11530
+ function pointInRing(ring, px, py) {
11531
+ let inside = false;
11532
+ for (let i = 0, n = ring.length - 1; i < n; i++) {
11533
+ const [ax, ay] = ring[i];
11534
+ const [bx, by] = ring[i + 1];
11535
+ if (ay > py !== by > py && px < (bx - ax) * (py - ay) / (by - ay) + ax) {
11536
+ inside = !inside;
11537
+ }
11538
+ }
11539
+ return inside;
11540
+ }
11541
+ function ringContains(outer, outerBox, inner, innerBox) {
11542
+ if (innerBox[0] < outerBox[0] || innerBox[1] < outerBox[1] || innerBox[2] > outerBox[2] || innerBox[3] > outerBox[3]) return false;
11543
+ const n = inner.length - 1;
11544
+ let votes = 0;
11545
+ for (const k of [0, Math.floor(n / 3), Math.floor(2 * n / 3)]) {
11546
+ if (pointInRing(outer, inner[k][0], inner[k][1])) votes++;
11547
+ }
11548
+ return votes >= 2;
11549
+ }
11550
+ function groupRings(rings, fillRule) {
11551
+ const n = rings.length;
11552
+ if (n === 1) return [[rings[0]]];
11553
+ const boxes = rings.map(ringBox);
11554
+ const orient = rings.map((r) => ringSignedArea(r) >= 0 ? 1 : -1);
11555
+ const parent = new Array(n).fill(-1);
11556
+ const children = rings.map(() => []);
11557
+ for (let i = 0; i < n; i++) {
11558
+ let best = -1;
11559
+ let bestArea = Infinity;
11560
+ for (let j = 0; j < n; j++) {
11561
+ if (i === j) continue;
11562
+ if (!ringContains(rings[j], boxes[j], rings[i], boxes[i])) continue;
11563
+ const area = (boxes[j][2] - boxes[j][0]) * (boxes[j][3] - boxes[j][1]);
11564
+ if (area < bestArea) {
11565
+ bestArea = area;
11566
+ best = j;
11567
+ }
11568
+ }
11569
+ parent[i] = best;
11570
+ }
11571
+ for (let i = 0; i < n; i++) if (parent[i] >= 0) children[parent[i]].push(i);
11572
+ const filled = new Array(n);
11573
+ for (let i = 0; i < n; i++) {
11574
+ let depth = 0;
11575
+ let winding = 0;
11576
+ for (let a = i; a >= 0; a = parent[a]) {
11577
+ winding += orient[a];
11578
+ if (a !== i) depth++;
11579
+ }
11580
+ filled[i] = fillRule === "evenodd" ? depth % 2 === 0 : winding !== 0;
11581
+ }
11582
+ const out = [];
11583
+ for (let i = 0; i < n; i++) {
11584
+ if (!filled[i]) continue;
11585
+ if (parent[i] >= 0 && filled[parent[i]]) continue;
11586
+ const polygon = [rings[i]];
11587
+ const queue = [...children[i]];
11588
+ while (queue.length > 0) {
11589
+ const c = queue.pop();
11590
+ if (filled[c]) queue.push(...children[c]);
11591
+ else polygon.push(rings[c]);
11592
+ }
11593
+ out.push(polygon);
11594
+ }
11595
+ return out;
11314
11596
  }
11315
11597
  function multiPolygonToPath(mp) {
11316
11598
  let nCmds = 0;
@@ -11623,6 +11905,7 @@ function makePathfinderAction(op) {
11623
11905
  icon: ICON_FOR3[op],
11624
11906
  group: "pathfinder",
11625
11907
  eligible: { capability: "creates-shapes" },
11908
+ requires: ["selection", "booleansAdapter"],
11626
11909
  // No default keybindings — there's no industry-standard chord set for
11627
11910
  // boolean ops. Wire bindings explicitly via the actions registry override map.
11628
11911
  invoker: {
@@ -13915,10 +14198,11 @@ function createDispatcher(opts) {
13915
14198
  uiOngoingByAction.set(actionId, gestureId);
13916
14199
  notify2();
13917
14200
  let ended = false;
14201
+ const live = () => !ended && inFlightHandles.get(gestureId) === handle;
13918
14202
  return {
13919
14203
  gestureId,
13920
14204
  update(nextParams) {
13921
- if (ended) return;
14205
+ if (!live()) return;
13922
14206
  if (!handle.onMove) return;
13923
14207
  try {
13924
14208
  handle.onMove(buildUiInvocationCtx(deps, nextParams));
@@ -13928,7 +14212,10 @@ function createDispatcher(opts) {
13928
14212
  notify2();
13929
14213
  },
13930
14214
  end(reason) {
13931
- if (ended) return;
14215
+ if (!live()) {
14216
+ ended = true;
14217
+ return;
14218
+ }
13932
14219
  ended = true;
13933
14220
  if (handle.onEnd) {
13934
14221
  try {
@@ -15142,6 +15429,7 @@ function useTools(opts) {
15142
15429
  );
15143
15430
  const active = contributions.focused;
15144
15431
  const getActiveOverlays = contributions.overlays;
15432
+ const registryKey = Object.keys(slotted.registry).sort().join("|") + "#" + slotted.ambient.map((t) => t.id).join("|");
15145
15433
  return useMemo(
15146
15434
  () => ({
15147
15435
  active,
@@ -15154,7 +15442,7 @@ function useTools(opts) {
15154
15442
  has,
15155
15443
  getActiveOverlays
15156
15444
  }),
15157
- [active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays]
15445
+ [active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays, registryKey]
15158
15446
  );
15159
15447
  }
15160
15448
  var TOOL_ACTIVATE_ID = "tool.activate";
@@ -16545,8 +16833,17 @@ function composeAffordanceLayer(id, label, affordances) {
16545
16833
  // `buildAffordanceAt` share one implementation rather than two that can
16546
16834
  // drift. This wrapper lifts the region's declared cursor and claim onto
16547
16835
  // the binding, the way `buildAffordanceAt` lifts them onto `AffordanceHit`.
16548
- hitTest: (wx, wy, state, view, _dims, isVisible) => {
16549
- const hit = hitAffordanceRegions(affordances, wx, wy, state, view, isVisible);
16836
+ hitTest: (wx, wy, data, view, _dims, isVisible) => {
16837
+ const state = asChromeState(data);
16838
+ if (!state) return null;
16839
+ const hit = hitAffordanceRegions(
16840
+ affordances,
16841
+ wx,
16842
+ wy,
16843
+ state,
16844
+ view,
16845
+ isVisible ?? asIsVisible(data) ?? void 0
16846
+ );
16550
16847
  if (!hit) return null;
16551
16848
  const { cursor, strength, claimedKinds } = hit.region;
16552
16849
  return {
@@ -17149,6 +17446,10 @@ function usePenTool(options) {
17149
17446
  onEnd: (endCtx, reason) => {
17150
17447
  const se = s();
17151
17448
  if (reason === "cancel") {
17449
+ if (se.current && se.draggingHandleAt === se.current.anchors.length - 1) {
17450
+ se.current.anchors.pop();
17451
+ if (se.current.anchors.length === 0) se.current = null;
17452
+ }
17152
17453
  se.draggingHandleAt = null;
17153
17454
  forceRenderRef.current();
17154
17455
  return;
@@ -17991,7 +18292,7 @@ function makeContainerAwareBoundsResolver(getPose, getBounds, getChildren, isCon
17991
18292
  const visit = (nid) => {
17992
18293
  const kids = getChildren(nid);
17993
18294
  if (kids.length === 0) {
17994
- out.push(nid);
18295
+ if (!isContainer(nid)) out.push(nid);
17995
18296
  return;
17996
18297
  }
17997
18298
  for (const k of kids) visit(k);
@@ -18727,7 +19028,6 @@ function CanvasInner(props, ref) {
18727
19028
  shaders,
18728
19029
  previewIdsExtra,
18729
19030
  previewPoseExtra,
18730
- previewBoundsExtra,
18731
19031
  gestureSource,
18732
19032
  viewport,
18733
19033
  backgroundFill,
@@ -18753,6 +19053,7 @@ function CanvasInner(props, ref) {
18753
19053
  debugSinkRefForCtx.current = debugSink;
18754
19054
  const adapter = adapterProp;
18755
19055
  const canvasRef = useRef(null);
19056
+ const helpersForLayersRef = useRef(null);
18756
19057
  const dimsRef = useRef({ width, height });
18757
19058
  dimsRef.current = { width, height };
18758
19059
  const getIsVisibleRef = useRef(getIsVisible);
@@ -18775,7 +19076,7 @@ function CanvasInner(props, ref) {
18775
19076
  const isVisible = getIsVisibleRef.current?.() ?? alwaysVisible;
18776
19077
  for (const layer of layers2) {
18777
19078
  if (!layer.hitTest) continue;
18778
- const hit = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
19079
+ const hit = layer.hitTest(worldX, worldY, helpersForLayersRef.current, view, dims, isVisible);
18779
19080
  if (hit) return { layerId: layer.id, hit };
18780
19081
  }
18781
19082
  return null;
@@ -18930,8 +19231,8 @@ function CanvasInner(props, ref) {
18930
19231
  };
18931
19232
  const selectedIdsForWiring = effectiveSelection.current;
18932
19233
  const multiActive2 = selectedIdsForWiring.length > 1;
18933
- const previewExtraRef = useRef({ previewPoseExtra, previewBoundsExtra });
18934
- previewExtraRef.current = { previewPoseExtra, previewBoundsExtra };
19234
+ const previewExtraRef = useRef({ previewPoseExtra });
19235
+ previewExtraRef.current = { previewPoseExtra };
18935
19236
  const effectiveBoundsOf = useMemo(() => {
18936
19237
  return boundsOf2 ?? baseBoundsOf;
18937
19238
  }, [boundsOf2, baseBoundsOf]);
@@ -18950,10 +19251,6 @@ function CanvasInner(props, ref) {
18950
19251
  if (p != null) return geometry.getBounds(p);
18951
19252
  }
18952
19253
  const extras = previewExtraRef.current;
18953
- if (extras.previewBoundsExtra) {
18954
- const b = extras.previewBoundsExtra(id);
18955
- if (b) return b;
18956
- }
18957
19254
  if (extras.previewPoseExtra) {
18958
19255
  const p = extras.previewPoseExtra(id);
18959
19256
  if (p != null) return geometry.getBounds(p);
@@ -18994,10 +19291,6 @@ function CanvasInner(props, ref) {
18994
19291
  const p = firstPreviewPose(tools, id);
18995
19292
  if (p != null) return geometry.getBounds(p);
18996
19293
  }
18997
- if (previewBoundsExtra) {
18998
- const b = previewBoundsExtra(id);
18999
- if (b) return b;
19000
- }
19001
19294
  if (previewPoseExtra) {
19002
19295
  const p = previewPoseExtra(id);
19003
19296
  if (p != null) return geometry.getBounds(p);
@@ -19033,6 +19326,7 @@ function CanvasInner(props, ref) {
19033
19326
  getDebug: () => debugSink,
19034
19327
  getIsVisible: () => getIsVisibleRef.current?.() ?? alwaysVisible
19035
19328
  };
19329
+ helpersForLayersRef.current = helpersForLayers;
19036
19330
  if (helpersRef) helpersRef.current = helpersForLayers;
19037
19331
  const pointerDownRef = useRef(false);
19038
19332
  const handlePointerDown = (e) => {
@@ -19214,6 +19508,11 @@ function CanvasInner(props, ref) {
19214
19508
  );
19215
19509
  renderer.render(commands, viewToMat3(effectiveView));
19216
19510
  }, [layersWithDebug, width, height, effectiveView, debugSink, redrawNonce, dprProp]);
19511
+ useEffect(() => () => {
19512
+ glRendererRef.current?.dispose();
19513
+ glRendererRef.current = null;
19514
+ lastResizeRef.current = null;
19515
+ }, []);
19217
19516
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
19218
19517
  useEffect(() => {
19219
19518
  const renderer = glRendererRef.current;
@@ -20642,6 +20941,89 @@ function poseContainsRotated(pose, wx, wy, tolerance) {
20642
20941
  }
20643
20942
  return poseContains(pose, wx, wy, tolerance);
20644
20943
  }
20944
+ function hiddenLayerIds(layers) {
20945
+ const out = /* @__PURE__ */ new Set();
20946
+ for (const layer of layers ?? []) {
20947
+ if (layer.visible === false) out.add(layer.id);
20948
+ }
20949
+ return out;
20950
+ }
20951
+ function hitTestArea(scene, bounds) {
20952
+ const { x, y, width: w, height: h } = bounds;
20953
+ const area = [x, y, x + w, y, x + w, y + h, x, y + h];
20954
+ return hitTestAreaPolygon(scene, area, { x, y, width: w, height: h }, true);
20955
+ }
20956
+ function hitTestAreaPolygon(scene, area, areaBounds, areaIsRect = false) {
20957
+ const ab = areaBounds ?? boundsOf(area);
20958
+ if (!ab) return [];
20959
+ const hits = [];
20960
+ const hidden = hiddenLayerIds(scene.layers);
20961
+ const order = scene.renderOrderNodes();
20962
+ for (let i = 0; i < order.length; i++) {
20963
+ const node = order[i];
20964
+ if (node.kind === "container") continue;
20965
+ if (hidden.size > 0 && hidden.has(node.layer)) continue;
20966
+ const pose = node.pose;
20967
+ const silhouette = pose !== null && typeof pose === "object" && pose.kind === "polygon";
20968
+ const b = silhouette ? nodeMemo(node, "aabb", pose, () => aabbOfPose(pose)) : aabbOfPose(pose);
20969
+ if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) {
20970
+ continue;
20971
+ }
20972
+ if (b.x >= ab.x + ab.width || b.x + b.width <= ab.x || b.y >= ab.y + ab.height || b.y + b.height <= ab.y) {
20973
+ continue;
20974
+ }
20975
+ if (areaIsRect && b.x >= ab.x && b.y >= ab.y && b.x + b.width <= ab.x + ab.width && b.y + b.height <= ab.y + ab.height) {
20976
+ hits.push(node.id);
20977
+ continue;
20978
+ }
20979
+ if (silhouette) {
20980
+ if (silhouetteOverlapsArea(pose.coords, area)) {
20981
+ hits.push(node.id);
20982
+ }
20983
+ continue;
20984
+ }
20985
+ const drawn = findShapeSilhouette(node, pose);
20986
+ if (drawn?.kind === "polygon") {
20987
+ if (silhouetteOverlapsArea(drawn.coords, area)) hits.push(node.id);
20988
+ continue;
20989
+ }
20990
+ hits.push(node.id);
20991
+ }
20992
+ return hits;
20993
+ }
20994
+ function silhouetteOverlapsArea(sil, area) {
20995
+ const ns = sil.length >> 1;
20996
+ const na = area.length >> 1;
20997
+ if (ns < 1 || na < 3) return false;
20998
+ for (let i = 0; i < ns; i++) {
20999
+ if (pointInPolygon(area, sil[i * 2], sil[i * 2 + 1])) return true;
21000
+ }
21001
+ for (let i = 0; i < na; i++) {
21002
+ if (pointInPolygon(sil, area[i * 2], area[i * 2 + 1])) return true;
21003
+ }
21004
+ for (let i = 0, j = ns - 1; i < ns; j = i++) {
21005
+ const ax = sil[j * 2], ay = sil[j * 2 + 1];
21006
+ const bx = sil[i * 2], by = sil[i * 2 + 1];
21007
+ for (let k = 0, l = na - 1; k < na; l = k++) {
21008
+ const cx = area[l * 2], cy = area[l * 2 + 1];
21009
+ const dx = area[k * 2], dy = area[k * 2 + 1];
21010
+ if (segmentsCross(ax, ay, bx, by, cx, cy, dx, dy)) return true;
21011
+ }
21012
+ }
21013
+ return false;
21014
+ }
21015
+ function boundsOf(coords) {
21016
+ if (coords.length < 2) return null;
21017
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
21018
+ for (let i = 0; i + 1 < coords.length; i += 2) {
21019
+ const x = coords[i], y = coords[i + 1];
21020
+ if (x < minX) minX = x;
21021
+ if (y < minY) minY = y;
21022
+ if (x > maxX) maxX = x;
21023
+ if (y > maxY) maxY = y;
21024
+ }
21025
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
21026
+ }
20645
21027
  var DEFAULT_PICK_TOLERANCE_PX = 4;
20646
21028
  function useSceneSelectTool(args) {
20647
21029
  const { scene, selection, geometry, selectTool: opts, insertTool, layouts, getView } = args;
@@ -20739,7 +21121,9 @@ function useSceneSelectTool(args) {
20739
21121
  }
20740
21122
  const tolerance = pickTolerancePx / meanScale(getView?.()?.scale ?? { x: 1, y: 1 });
20741
21123
  const out = [];
21124
+ const hidden = hiddenLayerIds(scene.layers);
20742
21125
  for (const n of scene.renderOrderNodes()) {
21126
+ if (hidden.size > 0 && hidden.has(n.layer)) continue;
20743
21127
  if (!poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
20744
21128
  if (shapePicking && !shapeCoversPoint(n, n.pose, wx, wy, { tolerance })) continue;
20745
21129
  out.push(n.id);
@@ -21623,8 +22007,10 @@ function createPathAnchorAffordances(getAnchorState, opts = {}) {
21623
22007
  const anchors = {
21624
22008
  id: PATH_ANCHOR_CHROME_ID,
21625
22009
  regions(state) {
22010
+ const editingId = getAnchorState()?.editingId ?? null;
21626
22011
  const out = [];
21627
22012
  for (const { id, path } of editablePaths(state)) {
22013
+ if (editingId !== null && id !== editingId) continue;
21628
22014
  for (const a of enumerateAnchors(path)) {
21629
22015
  out.push(point(
21630
22016
  `${id}:anchor:${a.anchorIndex}`,
@@ -21879,80 +22265,6 @@ function DepRegistryProviderIfRoot({ children }) {
21879
22265
  if (parent) return /* @__PURE__ */ jsx(Fragment, { children });
21880
22266
  return /* @__PURE__ */ jsx(DepRegistryProvider, { children });
21881
22267
  }
21882
- function hitTestArea(scene, bounds) {
21883
- const { x, y, width: w, height: h } = bounds;
21884
- const area = [x, y, x + w, y, x + w, y + h, x, y + h];
21885
- return hitTestAreaPolygon(scene, area, { x, y, width: w, height: h }, true);
21886
- }
21887
- function hitTestAreaPolygon(scene, area, areaBounds, areaIsRect = false) {
21888
- const ab = areaBounds ?? boundsOf(area);
21889
- if (!ab) return [];
21890
- const hits = [];
21891
- const order = scene.renderOrderNodes();
21892
- for (let i = 0; i < order.length; i++) {
21893
- const node = order[i];
21894
- if (node.kind === "container") continue;
21895
- const pose = node.pose;
21896
- const silhouette = pose !== null && typeof pose === "object" && pose.kind === "polygon";
21897
- const b = silhouette ? nodeMemo(node, "aabb", pose, () => aabbOfPose(pose)) : aabbOfPose(pose);
21898
- if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) {
21899
- continue;
21900
- }
21901
- if (b.x >= ab.x + ab.width || b.x + b.width <= ab.x || b.y >= ab.y + ab.height || b.y + b.height <= ab.y) {
21902
- continue;
21903
- }
21904
- if (areaIsRect && b.x >= ab.x && b.y >= ab.y && b.x + b.width <= ab.x + ab.width && b.y + b.height <= ab.y + ab.height) {
21905
- hits.push(node.id);
21906
- continue;
21907
- }
21908
- if (silhouette) {
21909
- if (silhouetteOverlapsArea(pose.coords, area)) {
21910
- hits.push(node.id);
21911
- }
21912
- continue;
21913
- }
21914
- const drawn = findShapeSilhouette(node, pose);
21915
- if (drawn?.kind === "polygon") {
21916
- if (silhouetteOverlapsArea(drawn.coords, area)) hits.push(node.id);
21917
- continue;
21918
- }
21919
- hits.push(node.id);
21920
- }
21921
- return hits;
21922
- }
21923
- function silhouetteOverlapsArea(sil, area) {
21924
- const ns = sil.length >> 1;
21925
- const na = area.length >> 1;
21926
- if (ns < 1 || na < 3) return false;
21927
- for (let i = 0; i < ns; i++) {
21928
- if (pointInPolygon(area, sil[i * 2], sil[i * 2 + 1])) return true;
21929
- }
21930
- for (let i = 0; i < na; i++) {
21931
- if (pointInPolygon(sil, area[i * 2], area[i * 2 + 1])) return true;
21932
- }
21933
- for (let i = 0, j = ns - 1; i < ns; j = i++) {
21934
- const ax = sil[j * 2], ay = sil[j * 2 + 1];
21935
- const bx = sil[i * 2], by = sil[i * 2 + 1];
21936
- for (let k = 0, l = na - 1; k < na; l = k++) {
21937
- const cx = area[l * 2], cy = area[l * 2 + 1];
21938
- const dx = area[k * 2], dy = area[k * 2 + 1];
21939
- if (segmentsCross(ax, ay, bx, by, cx, cy, dx, dy)) return true;
21940
- }
21941
- }
21942
- return false;
21943
- }
21944
- function boundsOf(coords) {
21945
- if (coords.length < 2) return null;
21946
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
21947
- for (let i = 0; i + 1 < coords.length; i += 2) {
21948
- const x = coords[i], y = coords[i + 1];
21949
- if (x < minX) minX = x;
21950
- if (y < minY) minY = y;
21951
- if (x > maxX) maxX = x;
21952
- if (y > maxY) maxY = y;
21953
- }
21954
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
21955
- }
21956
22268
  function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize) {
21957
22269
  const viewApiRef = useRef({
21958
22270
  get: () => currentViewRef.current,
@@ -22724,6 +23036,7 @@ function useActionsPropResolver(actions) {
22724
23036
  }
22725
23037
  if (!prop) return;
22726
23038
  const unregisters = [];
23039
+ const restores = [];
22727
23040
  const warnedIds = /* @__PURE__ */ new Set();
22728
23041
  for (const [slotId, entry] of Object.entries(prop)) {
22729
23042
  if (entry === null) {
@@ -22751,10 +23064,14 @@ function useActionsPropResolver(actions) {
22751
23064
  }
22752
23065
  const { id: _drop, ...rest } = entry;
22753
23066
  const merged = { ...existing, ...rest };
23067
+ restores.push(() => {
23068
+ reg.register(existing);
23069
+ });
22754
23070
  unregisters.push(reg.register(merged));
22755
23071
  }
22756
23072
  return () => {
22757
23073
  for (const u of unregisters) u();
23074
+ for (const r of restores) r();
22758
23075
  };
22759
23076
  }, [reg, actions]);
22760
23077
  }
@@ -24506,5 +24823,5 @@ function oklchToOklab(L, C, h) {
24506
24823
  }
24507
24824
 
24508
24825
  export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleContribution, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
24509
- //# sourceMappingURL=chunk-3WPOGKUP.js.map
24510
- //# sourceMappingURL=chunk-3WPOGKUP.js.map
24826
+ //# sourceMappingURL=chunk-DJLDIRFN.js.map
24827
+ //# sourceMappingURL=chunk-DJLDIRFN.js.map