@weasel-js/labkit 1.0.2 → 1.0.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 (111) hide show
  1. package/dist/_dts/{DrawCommand-CitWPxMY.d.ts → DrawCommand-BkZztJsW.d.ts} +59 -1
  2. package/dist/_dts/{index-C6Yze7sQ.d.ts → index-CFlDPeZh.d.ts} +30 -1
  3. package/dist/_dts/{types-Si4Fw-1F.d.ts → types-x92Kfeme.d.ts} +14 -0
  4. package/dist/_dts/{useExperimentState-CJn2hHzd.d.ts → useExperimentState-D7EQnnwJ.d.ts} +47 -0
  5. package/dist/canvas/index.d.ts +13 -2
  6. package/dist/canvas/index.js +2 -2
  7. package/dist/{chunk-54IQ2DX7.js → chunk-574LJAV4.js} +3 -3
  8. package/dist/chunk-574LJAV4.js.map +1 -0
  9. package/dist/{chunk-VLAHRJOC.js → chunk-5R2ATYPJ.js} +2 -2
  10. package/dist/{chunk-VLAHRJOC.js.map → chunk-5R2ATYPJ.js.map} +1 -1
  11. package/dist/{chunk-R4TACNW7.js → chunk-BAPZPDDA.js} +631 -591
  12. package/dist/chunk-BAPZPDDA.js.map +1 -0
  13. package/dist/{chunk-PWC7AQZM.js → chunk-CPUJ3QXL.js} +2 -2
  14. package/dist/chunk-CPUJ3QXL.js.map +1 -0
  15. package/dist/{chunk-2ZRE7WGQ.js → chunk-DEWXYFEU.js} +6 -6
  16. package/dist/chunk-DEWXYFEU.js.map +1 -0
  17. package/dist/{chunk-3P56ZCCJ.js → chunk-DWV7SFKR.js} +601 -269
  18. package/dist/chunk-DWV7SFKR.js.map +1 -0
  19. package/dist/{chunk-53XSBIUK.js → chunk-KSTEW2AF.js} +18 -2
  20. package/dist/chunk-KSTEW2AF.js.map +1 -0
  21. package/dist/{chunk-HXZHVU4G.js → chunk-PMAU3SEE.js} +3 -3
  22. package/dist/chunk-PMAU3SEE.js.map +1 -0
  23. package/dist/{chunk-7BKDG73Z.js → chunk-RL2LOLNI.js} +2 -2
  24. package/dist/chunk-RL2LOLNI.js.map +1 -0
  25. package/dist/{chunk-2QNYYL3V.js → chunk-SFL7NFKN.js} +2 -2
  26. package/dist/chunk-SFL7NFKN.js.map +1 -0
  27. package/dist/controls/index.d.ts +4 -2
  28. package/dist/controls/index.js +1 -1
  29. package/dist/dragdrop/index.d.ts +2 -2
  30. package/dist/index.d.ts +180 -15
  31. package/dist/index.js +146 -37
  32. package/dist/index.js.map +1 -1
  33. package/dist/layers/index.d.ts +6 -3
  34. package/dist/layers/index.js +1 -1
  35. package/dist/passthrough/weasel-canvas.d.ts +5 -1
  36. package/dist/passthrough/weasel-canvas.js +1 -1
  37. package/dist/passthrough/weasel-ui.d.ts +444 -7
  38. package/dist/passthrough/weasel-ui.js +2 -2
  39. package/dist/primitives/index.d.ts +13 -0
  40. package/dist/primitives/index.js +2 -2
  41. package/dist/state/index.d.ts +16 -3
  42. package/dist/state/index.js +2 -2
  43. package/dist/state/index.js.map +1 -1
  44. package/dist/styles.css +163 -6
  45. package/dist/ui/layers/index.d.ts +5 -0
  46. package/dist/ui/layers/index.js +3 -3
  47. package/dist/undo/index.d.ts +17 -2
  48. package/dist/undo/index.js +1 -1
  49. package/package.json +2 -1
  50. package/src/canvas/CanvasStack.tsx +4 -0
  51. package/src/canvas/CanvasStackContext.ts +3 -0
  52. package/src/canvas/canvasCoords.ts +2 -0
  53. package/src/canvas/useLayerScheduler.ts +2 -0
  54. package/src/controls/ControlPanel.tsx +2 -0
  55. package/src/controls/types.ts +14 -0
  56. package/src/instrument/capabilityDetector.ts +3 -0
  57. package/src/instrument/defineInstrument.ts +2 -0
  58. package/src/instrument/types.ts +29 -0
  59. package/src/instrument/validateConfigSchema.ts +5 -0
  60. package/src/lab/Lab.tsx +17 -2
  61. package/src/lab/LabContext.ts +6 -0
  62. package/src/lab/LabShell.tsx +4 -0
  63. package/src/lab/WorkspaceGrid.less +37 -5
  64. package/src/lab/WorkspaceGrid.stories.tsx +16 -0
  65. package/src/lab/WorkspaceGrid.test.tsx +84 -10
  66. package/src/lab/WorkspaceGrid.tsx +188 -12
  67. package/src/lab/index.ts +0 -2
  68. package/src/layers/LayerList.tsx +3 -0
  69. package/src/primitives/FpsMeter.tsx +1 -0
  70. package/src/primitives/ScaleIndicator.tsx +4 -0
  71. package/src/primitives/Sidebar.tsx +3 -0
  72. package/src/primitives/StatusBar.tsx +3 -0
  73. package/src/primitives/Toolbar.tsx +6 -0
  74. package/src/state/SingletonExperiment.tsx +1 -0
  75. package/src/state/adapters.ts +8 -0
  76. package/src/state/context.tsx +9 -0
  77. package/src/state/helpers.ts +13 -1
  78. package/src/state/store.test.ts +2 -1
  79. package/src/state/store.ts +25 -0
  80. package/src/state/types.ts +20 -0
  81. package/src/state/useExperimentState.ts +3 -0
  82. package/src/test-setup.ts +19 -0
  83. package/src/ui/layers/LayerStack.tsx +5 -0
  84. package/src/ui/properties/CurveField.tsx +4 -1
  85. package/src/ui/properties/EffectCard.tsx +12 -0
  86. package/src/ui/properties/PropertyGroup.tsx +1 -0
  87. package/src/ui/properties/PropertyPanel.tsx +28 -0
  88. package/src/undo/eventBus.ts +4 -0
  89. package/src/undo/undoStack.ts +11 -0
  90. package/src/workspace/DefaultSidebar.tsx +3 -0
  91. package/src/workspace/DefaultStatusBar.tsx +3 -0
  92. package/src/workspace/DefaultToolbar.tsx +3 -0
  93. package/src/workspace/Workspace.stories.tsx +1 -0
  94. package/src/workspace/Workspace.test.tsx +1 -0
  95. package/src/workspace/Workspace.tsx +3 -0
  96. package/src/workspace/WorkspaceChrome.tsx +4 -0
  97. package/src/workspace/index.ts +1 -0
  98. package/src/workspace/slotTypes.ts +9 -0
  99. package/src/workspace/workspaceOps.test.ts +34 -1
  100. package/src/workspace/workspaceOps.ts +24 -0
  101. package/dist/chunk-2QNYYL3V.js.map +0 -1
  102. package/dist/chunk-2ZRE7WGQ.js.map +0 -1
  103. package/dist/chunk-3P56ZCCJ.js.map +0 -1
  104. package/dist/chunk-53XSBIUK.js.map +0 -1
  105. package/dist/chunk-54IQ2DX7.js.map +0 -1
  106. package/dist/chunk-7BKDG73Z.js.map +0 -1
  107. package/dist/chunk-HXZHVU4G.js.map +0 -1
  108. package/dist/chunk-PWC7AQZM.js.map +0 -1
  109. package/dist/chunk-R4TACNW7.js.map +0 -1
  110. package/src/lab/gridDims.test.ts +0 -35
  111. package/src/lab/gridDims.ts +0 -11
@@ -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-W2WB4352.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-W2WB4352.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-K5JJ6WRB.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-6SHZHXXR.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-UB6A6L77.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-Z3KNFTTS.js
1680
+ // ../core/dist/chunk-J7LUY47N.js
1648
1681
  function lockAspectWithModifier(opts = {}) {
1649
1682
  const { key = "shift" } = opts;
1650
1683
  return {
@@ -1840,7 +1873,7 @@ function materializePaste(adapter, snapshot, opts) {
1840
1873
  return { ops, newIds, created };
1841
1874
  }
1842
1875
 
1843
- // ../core/dist/chunk-IO5Z75X4.js
1876
+ // ../core/dist/chunk-HGF5X6MZ.js
1844
1877
  var counter = 0;
1845
1878
  var registry = /* @__PURE__ */ new Map();
1846
1879
  function getTexture(id) {
@@ -1998,7 +2031,7 @@ function chunks(params) {
1998
2031
  return build({ tile: "chunks", ...params });
1999
2032
  }
2000
2033
 
2001
- // ../font/dist/index.js
2034
+ // ../font/dist/chunk-3RSTZDXH.js
2002
2035
  var subscribers = /* @__PURE__ */ new Set();
2003
2036
  var generation = 0;
2004
2037
  function glyphGeneration() {
@@ -2642,6 +2675,8 @@ function warnOnce(key, message) {
2642
2675
  warned.add(key);
2643
2676
  console.warn(message);
2644
2677
  }
2678
+
2679
+ // ../font/dist/index.js
2645
2680
  var TEXT_VERT_SRC = (
2646
2681
  /* glsl */
2647
2682
  `#version 300 es
@@ -2738,7 +2773,7 @@ var TEXT_SDF_UNIFORMS = [
2738
2773
  ];
2739
2774
  var TEXT_SDF_ATTRIBUTES = ["a_position", "a_uv", "a_baselineY"];
2740
2775
 
2741
- // ../modes/dist/chunk-KILROWYB.js
2776
+ // ../modes/dist/chunk-YAJYQMBG.js
2742
2777
  var NORMAL = {
2743
2778
  allows: [
2744
2779
  "creates-selection",
@@ -2848,6 +2883,7 @@ function createHistory(adapter, options = {}) {
2848
2883
  const logger = options.debug ?? SILENT;
2849
2884
  let nextEntryId = 1;
2850
2885
  let version = 0;
2886
+ let coalesceAnchorVersion = -1;
2851
2887
  const listeners = /* @__PURE__ */ new Set();
2852
2888
  function bump() {
2853
2889
  version++;
@@ -2888,6 +2924,7 @@ function createHistory(adapter, options = {}) {
2888
2924
  }
2889
2925
  function canCoalesce(top, incoming) {
2890
2926
  if (coalesceWindowMs <= 0) return false;
2927
+ if (version !== coalesceAnchorVersion) return false;
2891
2928
  if (now() - top.timestamp > coalesceWindowMs) return false;
2892
2929
  if (top.forwardOps.length === 0 || incoming.length === 0) return false;
2893
2930
  if (top.forwardOps.length !== incoming.length) return false;
@@ -2925,9 +2962,9 @@ function createHistory(adapter, options = {}) {
2925
2962
  for (const id of incoming) merged.add(id);
2926
2963
  top.touchedIds = merged;
2927
2964
  }
2928
- dropRedo();
2929
2965
  logger.log(`coalesce '${label}' into entry id=${top.id} (${ops.length} ops)`);
2930
2966
  bump();
2967
+ coalesceAnchorVersion = version;
2931
2968
  return;
2932
2969
  }
2933
2970
  logger.log(`push '${label}' (${ops.length} ops)`);
@@ -2935,6 +2972,7 @@ function createHistory(adapter, options = {}) {
2935
2972
  dropRedo();
2936
2973
  enforceLimit();
2937
2974
  bump();
2975
+ coalesceAnchorVersion = version;
2938
2976
  }
2939
2977
  return {
2940
2978
  apply(op, label) {
@@ -3040,7 +3078,11 @@ function createHistory(adapter, options = {}) {
3040
3078
  return j;
3041
3079
  },
3042
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
+ }
3043
3084
  _resumeJournalInternal(journal);
3085
+ activeJournal = journal;
3044
3086
  },
3045
3087
  restore(snapshot) {
3046
3088
  undoStack.length = 0;
@@ -3121,9 +3163,8 @@ function serialToEntry(se, custom, logger) {
3121
3163
  label: se.label,
3122
3164
  forwardOps,
3123
3165
  baseOps,
3124
- // Restored entries inherit a "now" timestamp coalesce eligibility is
3125
- // a within-session concept and a restored entry shouldn't merge with a
3126
- // 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.
3127
3168
  timestamp: 0,
3128
3169
  // Re-derive touchedIds from the rebuilt ops rather than trying to
3129
3170
  // round-trip the Set through the serialized form (Sets aren't JSON-safe).
@@ -3131,7 +3172,7 @@ function serialToEntry(se, custom, logger) {
3131
3172
  };
3132
3173
  }
3133
3174
 
3134
- // ../core/dist/chunk-PI56ORHE.js
3175
+ // ../core/dist/chunk-AYGSXNY3.js
3135
3176
  function worldToScreen(worldX, worldY, view) {
3136
3177
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
3137
3178
  }
@@ -3568,13 +3609,20 @@ var GLTextureCache = class {
3568
3609
  }
3569
3610
  gl;
3570
3611
  map = /* @__PURE__ */ new Map();
3612
+ wraps = /* @__PURE__ */ new Map();
3571
3613
  has(id) {
3572
3614
  return this.map.has(id);
3573
3615
  }
3574
- /** Uploads once per id; `wrap` therefore applies on first upload only.
3575
- * 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. */
3576
3620
  upload(id, source, wrap = "clamp") {
3577
- 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
+ }
3578
3626
  const gl = this.gl;
3579
3627
  const tex = gl.createTexture();
3580
3628
  if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
@@ -3587,8 +3635,18 @@ var GLTextureCache = class {
3587
3635
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
3588
3636
  gl.bindTexture(gl.TEXTURE_2D, null);
3589
3637
  this.map.set(id, tex);
3638
+ this.wraps.set(id, wrap);
3590
3639
  return id;
3591
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
+ }
3592
3650
  /** Create a single-channel R8 texture from raw bytes (full upload).
3593
3651
  * No-op if `id` already exists. Same LINEAR/CLAMP params as `upload`;
3594
3652
  * UNPACK_ALIGNMENT dropped to 1 for non-4-aligned row widths. */
@@ -3607,6 +3665,7 @@ var GLTextureCache = class {
3607
3665
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3608
3666
  gl.bindTexture(gl.TEXTURE_2D, null);
3609
3667
  this.map.set(id, tex);
3668
+ this.wraps.set(id, "clamp");
3610
3669
  }
3611
3670
  /** Patch a rect of an existing R8 texture with tightly-packed w×h bytes. */
3612
3671
  subImageR8(id, x, y, w, h, data) {
@@ -3638,6 +3697,7 @@ var GLTextureCache = class {
3638
3697
  gl.deleteTexture(tex);
3639
3698
  }
3640
3699
  this.map.clear();
3700
+ this.wraps.clear();
3641
3701
  }
3642
3702
  };
3643
3703
  var GLImageCache = class {
@@ -4287,6 +4347,13 @@ var GroupState = class {
4287
4347
  this.alphaStack.push(nextAlpha);
4288
4348
  this.colorMatrixStack.push(nextCM);
4289
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
+ }
4290
4357
  pop() {
4291
4358
  if (this.transformStack.length <= 1) {
4292
4359
  throw new Error("GroupState.pop: cannot pop root frame");
@@ -4934,16 +5001,28 @@ function verticalAlignOffset(align, boxHeight, textHeight) {
4934
5001
  return align === "center" ? slack / 2 : slack;
4935
5002
  }
4936
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
+ }
4937
5013
  function tokenize(d) {
4938
5014
  const out = [];
4939
5015
  let i = 0;
4940
5016
  while (i < d.length) {
4941
- const ch = d[i];
4942
- if (/[A-Za-z]/.test(ch)) {
5017
+ if (isCommandLetter(d, i)) {
4943
5018
  let j = i + 1;
4944
- while (j < d.length && !/[A-Za-z]/.test(d[j])) j++;
5019
+ while (j < d.length && !isCommandLetter(d, j)) j++;
4945
5020
  const argSlice = d.slice(i + 1, j);
4946
- 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
+ });
4947
5026
  i = j;
4948
5027
  } else {
4949
5028
  i++;
@@ -4961,6 +5040,30 @@ function readNumbers(s) {
4961
5040
  }
4962
5041
  return out;
4963
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
+ }
4964
5067
  var ARG_COUNTS = {
4965
5068
  M: 2,
4966
5069
  L: 2,
@@ -5151,9 +5254,9 @@ function arcToCubics(x1, y1, x2, y2, rx, ry, xAxisRotationDeg, largeArc, sweep,
5151
5254
  const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
5152
5255
  const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
5153
5256
  const angle = (ux, uy, vx, vy) => {
5154
- const dot = ux * vx + uy * vy;
5257
+ const dot2 = ux * vx + uy * vy;
5155
5258
  const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
5156
- 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)));
5157
5260
  if (ux * vy - uy * vx < 0) a = -a;
5158
5261
  return a;
5159
5262
  };
@@ -5865,42 +5968,42 @@ var MAX_VERTICES_PER_BATCH = 32768;
5865
5968
  var FLOATS_PER_VERTEX = 6;
5866
5969
  var INITIAL_VERTICES = 256;
5867
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
+ }
5868
5980
  var SolidBatch = class {
5869
5981
  gl;
5870
- vao;
5871
- vbo;
5872
- 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;
5873
5991
  verts = new Float32Array(INITIAL_VERTICES * FLOATS_PER_VERTEX);
5874
5992
  idx = new Uint32Array(INITIAL_INDICES);
5875
5993
  nVerts = 0;
5876
5994
  nIdx = 0;
5877
- /** What the GPU buffers are currently sized for. */
5878
- vboVerts = 0;
5879
- 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;
5880
5998
  constructor(gl, prog) {
5881
5999
  const aPos = prog.attribute("a_position");
5882
6000
  const aColor = prog.attribute("a_vertexColor");
5883
6001
  if (aPos === void 0 || aColor === void 0) {
5884
6002
  throw new Error("SolidBatch: vertex-color program is missing a_position / a_vertexColor");
5885
6003
  }
5886
- const vao = gl.createVertexArray();
5887
- const vbo = gl.createBuffer();
5888
- const ibo = gl.createBuffer();
5889
- if (!vao || !vbo || !ibo) throw new Error("SolidBatch: failed to create GL objects");
5890
6004
  this.gl = gl;
5891
- this.vao = vao;
5892
- this.vbo = vbo;
5893
- this.ibo = ibo;
5894
- const stride = FLOATS_PER_VERTEX * 4;
5895
- gl.bindVertexArray(vao);
5896
- gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
5897
- gl.enableVertexAttribArray(aPos);
5898
- gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, stride, 0);
5899
- gl.enableVertexAttribArray(aColor);
5900
- gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 8);
5901
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
5902
- gl.bindVertexArray(null);
5903
- this.growGpu(INITIAL_VERTICES, INITIAL_INDICES);
6005
+ this.aPos = aPos;
6006
+ this.aColor = aColor;
5904
6007
  }
5905
6008
  get length() {
5906
6009
  return this.nIdx;
@@ -5970,6 +6073,7 @@ var SolidBatch = class {
5970
6073
  const srcIdx = mesh.indices;
5971
6074
  const n = src.length >> 1;
5972
6075
  this.reserve(n, srcIdx.length);
6076
+ this.pureRects = false;
5973
6077
  const v = this.verts;
5974
6078
  let i = this.nVerts * FLOATS_PER_VERTEX;
5975
6079
  const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
@@ -5990,67 +6094,109 @@ var SolidBatch = class {
5990
6094
  this.nVerts += n;
5991
6095
  this.nIdx += srcIdx.length;
5992
6096
  }
5993
- /** Upload the staged geometry and bind the batch VAO. Returns the index
5994
- * 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`. */
5995
6099
  uploadAndBind() {
5996
6100
  const gl = this.gl;
5997
- if (this.nVerts > this.vboVerts || this.nIdx > this.iboIdx) {
5998
- this.growGpu(this.nVerts, this.nIdx);
5999
- }
6000
- gl.bindVertexArray(this.vao);
6001
- 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);
6002
6104
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nVerts * FLOATS_PER_VERTEX);
6003
- 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
+ }
6004
6110
  return this.nIdx;
6005
6111
  }
6006
6112
  reset() {
6007
6113
  this.nVerts = 0;
6008
6114
  this.nIdx = 0;
6115
+ this.pureRects = true;
6009
6116
  }
6010
6117
  dispose() {
6011
- const gl = this.gl;
6012
- gl.deleteBuffer(this.vbo);
6013
- gl.deleteBuffer(this.ibo);
6014
- 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);
6015
6123
  }
6016
6124
  /** Grow the CPU arrays so `vertices` / `indices` more fit. */
6017
6125
  reserve(vertices, indices) {
6018
6126
  const needVerts = (this.nVerts + vertices) * FLOATS_PER_VERTEX;
6019
6127
  if (needVerts > this.verts.length) {
6020
- let len = this.verts.length;
6021
- while (len < needVerts) len *= 2;
6022
- const grown = new Float32Array(len);
6128
+ const grown = new Float32Array(doubledTo(this.verts.length, needVerts));
6023
6129
  grown.set(this.verts);
6024
6130
  this.verts = grown;
6025
6131
  }
6026
6132
  const needIdx = this.nIdx + indices;
6027
6133
  if (needIdx > this.idx.length) {
6028
- let len = this.idx.length;
6029
- while (len < needIdx) len *= 2;
6030
- const grown = new Uint32Array(len);
6134
+ const grown = new Uint32Array(doubledTo(this.idx.length, needIdx));
6031
6135
  grown.set(this.idx);
6032
6136
  this.idx = grown;
6033
6137
  }
6034
6138
  }
6035
- /** Size both GPU buffers for at least what is staged, doubling to amortize. */
6036
- 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() {
6037
6149
  const gl = this.gl;
6038
- if (vertices > this.vboVerts) {
6039
- let capacity = Math.max(this.vboVerts, INITIAL_VERTICES);
6040
- while (capacity < vertices) capacity *= 2;
6041
- gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
6042
- gl.bufferData(gl.ARRAY_BUFFER, capacity * FLOATS_PER_VERTEX * 4, gl.DYNAMIC_DRAW);
6043
- this.vboVerts = capacity;
6044
- }
6045
- if (indices > this.iboIdx) {
6046
- let capacity = Math.max(this.iboIdx, INITIAL_INDICES);
6047
- while (capacity < indices) capacity *= 2;
6048
- gl.bindVertexArray(this.vao);
6049
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo);
6050
- 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);
6051
6171
  gl.bindVertexArray(null);
6052
- this.iboIdx = capacity;
6172
+ set.indexRects = -1;
6053
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);
6054
6200
  }
6055
6201
  };
6056
6202
  var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
@@ -6073,6 +6219,25 @@ function sameValues(prev, next) {
6073
6219
  for (let i = 0; i < prev.length; i++) if (prev[i] !== next[i]) return false;
6074
6220
  return true;
6075
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
+ }
6076
6241
  var BATCH_MODEL = mat3.identity();
6077
6242
  function projFor(ctx) {
6078
6243
  let proj = FRAME_PROJ.get(ctx);
@@ -6365,8 +6530,8 @@ function flushSolids(ctx) {
6365
6530
  gl.useProgram(prog.handle);
6366
6531
  const indexCount = batch.uploadAndBind();
6367
6532
  setProjAndModel(ctx, prog, BATCH_MODEL);
6368
- gl.uniform4f(prog.uniform("u_color"), 1, 1, 1, 1);
6369
- gl.uniform1f(prog.uniform("u_alpha"), staged.alpha);
6533
+ setColorUniform(ctx, prog, 1, 1, 1, 1);
6534
+ setAlphaUniform(ctx, prog, staged.alpha);
6370
6535
  setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
6371
6536
  applyClipTest(ctx, staged.clipDepth);
6372
6537
  gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
@@ -6431,10 +6596,9 @@ function setProjAndModel(ctx, prog, model = ctx.state.transform) {
6431
6596
  }
6432
6597
  }
6433
6598
  function setSolidPaintUniforms(ctx, prog, color, opacity) {
6434
- const gl = ctx.gl;
6435
6599
  const [r, g, b, a] = resolveColor(color);
6436
- gl.uniform4f(prog.uniform("u_color"), r, g, b, a * (opacity ?? 1));
6437
- 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);
6438
6602
  }
6439
6603
  function drawPathFillByKind(ctx, fill, handle) {
6440
6604
  const kind = fill.fill ?? "solid";
@@ -6480,7 +6644,7 @@ function drawPathFillPattern(ctx, fill, handle) {
6480
6644
  ctx.textureCache.bind(tex.id, 0);
6481
6645
  gl.uniform1i(ctx.patternFill.uniform("u_sampler"), 0);
6482
6646
  gl.uniform1f(ctx.patternFill.uniform("u_opacity"), fill.opacity ?? 1);
6483
- gl.uniform1f(ctx.patternFill.uniform("u_alpha"), ctx.state.alpha);
6647
+ setAlphaUniform(ctx, ctx.patternFill, ctx.state.alpha);
6484
6648
  applyClipTest(ctx);
6485
6649
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
6486
6650
  gl.bindVertexArray(null);
@@ -6504,7 +6668,7 @@ function drawPathFillGradient(ctx, fill, handle) {
6504
6668
  gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
6505
6669
  ctx.gradRampCache.bind(key, 0);
6506
6670
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
6507
- gl.uniform1f(ctx.gradFill.uniform("u_alpha"), ctx.state.alpha);
6671
+ setAlphaUniform(ctx, ctx.gradFill, ctx.state.alpha);
6508
6672
  gl.uniform1f(ctx.gradFill.uniform("u_opacity"), fill.opacity ?? 1);
6509
6673
  if (fill.fill === "linear-gradient") {
6510
6674
  gl.uniform1i(ctx.gradFill.uniform("u_gradKind"), 0);
@@ -6654,6 +6818,7 @@ function drawPathStrokeUnclipped(ctx, cmd) {
6654
6818
  gl.enableVertexAttribArray(aVColorLoc);
6655
6819
  gl.vertexAttribPointer(aVColorLoc, 4, gl.FLOAT, false, 0, 0);
6656
6820
  }
6821
+ applyClipTest(ctx);
6657
6822
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
6658
6823
  gl.bindVertexArray(null);
6659
6824
  gl.deleteBuffer(colorVbo);
@@ -6769,7 +6934,7 @@ function drawText(ctx, cmd) {
6769
6934
  preparedPrograms.add(prog);
6770
6935
  setProjAndModel(ctx, prog);
6771
6936
  setColorMatrixUniforms(ctx, prog);
6772
- gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
6937
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
6773
6938
  }
6774
6939
  drawTextGroup(ctx, group, prog, dx, dy);
6775
6940
  }
@@ -6887,8 +7052,8 @@ function drawTextDecorations(ctx, decorations, dx, dy) {
6887
7052
  if (!ibo) throw new Error("drawTextDecorations: createBuffer (IBO) returned null");
6888
7053
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
6889
7054
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.DYNAMIC_DRAW);
6890
- gl.uniform4f(prog.uniform("u_color"), rgba[0], rgba[1], rgba[2], rgba[3]);
6891
- 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);
6892
7057
  gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_INT, 0);
6893
7058
  gl.bindVertexArray(null);
6894
7059
  gl.deleteVertexArray(vao);
@@ -6972,7 +7137,7 @@ function drawTextGroup(ctx, group, prog, dx, dy) {
6972
7137
  if ("color" in group.fill) {
6973
7138
  [r, g, b, a] = resolveColor(group.fill.color);
6974
7139
  }
6975
- gl.uniform4f(prog.uniform("u_color"), r, g, b, a);
7140
+ setColorUniform(ctx, prog, r, g, b, a);
6976
7141
  const synthBoldAmount = group.synthetic.bold ? 0.08 : 0;
6977
7142
  const uSynthBold = prog.uniform("u_synthBold");
6978
7143
  if (uSynthBold !== void 0) gl.uniform1f(uSynthBold, synthBoldAmount);
@@ -6988,70 +7153,98 @@ function drawTextGroup(ctx, group, prog, dx, dy) {
6988
7153
  gl.deleteBuffer(vbo);
6989
7154
  gl.deleteBuffer(ibo);
6990
7155
  }
7156
+ var IMAGE_QUAD_RING_SIZE = 64;
7157
+ var IMAGE_QUAD_RINGS = /* @__PURE__ */ new WeakMap();
7158
+ var IMAGE_QUAD_INDICES = new Uint32Array([0, 1, 2, 1, 3, 2]);
7159
+ var IMAGE_QUAD_STRIDE = 16;
7160
+ function imageQuadRing(gl, prog) {
7161
+ const existing = IMAGE_QUAD_RINGS.get(prog);
7162
+ if (existing) return existing;
7163
+ const aPosLoc = prog.attribute("a_position");
7164
+ const aUvLoc = prog.attribute("a_uv");
7165
+ const ibo = gl.createBuffer();
7166
+ if (!ibo) throw new Error("drawImage: createBuffer (IBO) returned null");
7167
+ const vaos = [];
7168
+ const vbos = [];
7169
+ for (let slot = 0; slot < IMAGE_QUAD_RING_SIZE; slot++) {
7170
+ const vao = gl.createVertexArray();
7171
+ const vbo = gl.createBuffer();
7172
+ if (!vao) throw new Error("drawImage: createVertexArray returned null");
7173
+ if (!vbo) throw new Error("drawImage: createBuffer returned null");
7174
+ gl.bindVertexArray(vao);
7175
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
7176
+ gl.bufferData(gl.ARRAY_BUFFER, 4 * IMAGE_QUAD_STRIDE, gl.DYNAMIC_DRAW);
7177
+ if (aPosLoc !== void 0) {
7178
+ gl.enableVertexAttribArray(aPosLoc);
7179
+ gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, IMAGE_QUAD_STRIDE, 0);
7180
+ }
7181
+ if (aUvLoc !== void 0) {
7182
+ gl.enableVertexAttribArray(aUvLoc);
7183
+ gl.vertexAttribPointer(aUvLoc, 2, gl.FLOAT, false, IMAGE_QUAD_STRIDE, 8);
7184
+ }
7185
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
7186
+ if (slot === 0) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, IMAGE_QUAD_INDICES, gl.STATIC_DRAW);
7187
+ gl.bindVertexArray(null);
7188
+ vaos.push(vao);
7189
+ vbos.push(vbo);
7190
+ }
7191
+ const ring = { vaos, vbos, ibo, next: 0 };
7192
+ IMAGE_QUAD_RINGS.set(prog, ring);
7193
+ return ring;
7194
+ }
7195
+ function disposeImageQuads(gl, prog) {
7196
+ const ring = IMAGE_QUAD_RINGS.get(prog);
7197
+ if (!ring) return;
7198
+ for (const vao of ring.vaos) gl.deleteVertexArray(vao);
7199
+ for (const vbo of ring.vbos) gl.deleteBuffer(vbo);
7200
+ gl.deleteBuffer(ring.ibo);
7201
+ IMAGE_QUAD_RINGS.delete(prog);
7202
+ }
7203
+ var IMAGE_QUAD_VERTICES = new Float32Array(16);
6991
7204
  function drawImage(ctx, cmd) {
6992
7205
  ctx.imageCache.upload(cmd.image, cmd.image);
6993
7206
  const gl = ctx.gl;
6994
- gl.useProgram(ctx.imageFill.handle);
7207
+ const prog = ctx.imageFill;
7208
+ gl.useProgram(prog.handle);
6995
7209
  const x0 = cmd.x, y0 = cmd.y;
6996
7210
  const x1 = cmd.x + cmd.w, y1 = cmd.y + cmd.h;
6997
- const vertices = new Float32Array([
6998
- x0,
6999
- y0,
7000
- 0,
7001
- 0,
7002
- x1,
7003
- y0,
7004
- 1,
7005
- 0,
7006
- x0,
7007
- y1,
7008
- 0,
7009
- 1,
7010
- x1,
7011
- y1,
7012
- 1,
7013
- 1
7014
- ]);
7015
- const indices = new Uint32Array([0, 1, 2, 1, 3, 2]);
7016
- const vao = gl.createVertexArray();
7017
- if (!vao) throw new Error("drawImage: createVertexArray returned null");
7018
- gl.bindVertexArray(vao);
7019
- const vbo = gl.createBuffer();
7020
- if (!vbo) throw new Error("drawImage: createBuffer returned null");
7021
- gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
7022
- gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.DYNAMIC_DRAW);
7023
- const stride = 16;
7024
- const aPosLoc = ctx.imageFill.attribute("a_position");
7025
- const aUvLoc = ctx.imageFill.attribute("a_uv");
7026
- if (aPosLoc !== void 0) {
7027
- gl.enableVertexAttribArray(aPosLoc);
7028
- gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, stride, 0);
7029
- }
7030
- if (aUvLoc !== void 0) {
7031
- gl.enableVertexAttribArray(aUvLoc);
7032
- gl.vertexAttribPointer(aUvLoc, 2, gl.FLOAT, false, stride, 8);
7033
- }
7034
- const ibo = gl.createBuffer();
7035
- if (!ibo) throw new Error("drawImage: createBuffer (IBO) returned null");
7036
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
7037
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.DYNAMIC_DRAW);
7038
- setProjAndModel(ctx, ctx.imageFill);
7039
- setColorMatrixUniforms(ctx, ctx.imageFill);
7211
+ const v = IMAGE_QUAD_VERTICES;
7212
+ v[0] = x0;
7213
+ v[1] = y0;
7214
+ v[2] = 0;
7215
+ v[3] = 0;
7216
+ v[4] = x1;
7217
+ v[5] = y0;
7218
+ v[6] = 1;
7219
+ v[7] = 0;
7220
+ v[8] = x0;
7221
+ v[9] = y1;
7222
+ v[10] = 0;
7223
+ v[11] = 1;
7224
+ v[12] = x1;
7225
+ v[13] = y1;
7226
+ v[14] = 1;
7227
+ v[15] = 1;
7228
+ const ring = imageQuadRing(gl, prog);
7229
+ const slot = ring.next;
7230
+ ring.next = (slot + 1) % IMAGE_QUAD_RING_SIZE;
7231
+ gl.bindVertexArray(ring.vaos[slot]);
7232
+ gl.bindBuffer(gl.ARRAY_BUFFER, ring.vbos[slot]);
7233
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, v);
7234
+ setProjAndModel(ctx, prog);
7235
+ setColorMatrixUniforms(ctx, prog);
7040
7236
  ctx.imageCache.bind(cmd.image, 0);
7041
7237
  gl.texParameteri(
7042
7238
  gl.TEXTURE_2D,
7043
7239
  gl.TEXTURE_MAG_FILTER,
7044
7240
  cmd.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
7045
7241
  );
7046
- gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
7047
- gl.uniform1f(ctx.imageFill.uniform("u_opacity"), cmd.opacity ?? 1);
7048
- gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
7242
+ gl.uniform1i(prog.uniform("u_sampler"), 0);
7243
+ gl.uniform1f(prog.uniform("u_opacity"), cmd.opacity ?? 1);
7244
+ setAlphaUniform(ctx, prog, ctx.state.alpha);
7049
7245
  applyClipTest(ctx);
7050
7246
  gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, 0);
7051
7247
  gl.bindVertexArray(null);
7052
- gl.deleteVertexArray(vao);
7053
- gl.deleteBuffer(vbo);
7054
- gl.deleteBuffer(ibo);
7055
7248
  }
7056
7249
  var CUSTOM_VERT_SRC = (
7057
7250
  /* glsl */
@@ -7263,6 +7456,8 @@ var WeaselRenderer = class {
7263
7456
  const program = new ShaderProgram(this.gl, vertSrc, fragSrc);
7264
7457
  program.lookupUniforms([...CUSTOM_KIT_UNIFORMS, ...extractUniformNames(fragSrc)]);
7265
7458
  program.lookupAttributes(CUSTOM_ATTRIBUTES);
7459
+ const previous = this.programRegistry.get(handle.id);
7460
+ if (previous) this.gl.deleteProgram(previous.handle);
7266
7461
  this.programRegistry.set(handle.id, program);
7267
7462
  }
7268
7463
  applyViewport() {
@@ -7360,7 +7555,7 @@ var WeaselRenderer = class {
7360
7555
  }
7361
7556
  this.meshCache.freeTransient();
7362
7557
  this.meshCache.drainPendingDeletes();
7363
- 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]) {
7364
7559
  gl.deleteProgram(prog.handle);
7365
7560
  }
7366
7561
  for (const prog of this.programRegistry.values()) {
@@ -7371,6 +7566,7 @@ var WeaselRenderer = class {
7371
7566
  this.gradRampCache.free();
7372
7567
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
7373
7568
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
7569
+ disposeImageQuads(gl, this.imageFill);
7374
7570
  this.solidBatch.dispose();
7375
7571
  }
7376
7572
  /**
@@ -7386,6 +7582,7 @@ var WeaselRenderer = class {
7386
7582
  const gl = this.gl;
7387
7583
  this.meshCache.drainPendingDeletes();
7388
7584
  resetBakeBudget(this.bakeBudget);
7585
+ this.groupState.reset();
7389
7586
  gl.stencilMask(255);
7390
7587
  gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
7391
7588
  const ctx = {
@@ -10955,7 +11152,7 @@ var reorderBackwardAction = {
10955
11152
  }
10956
11153
  ],
10957
11154
  eligible: { capability: "edits-page" },
10958
- requires: ["selection", "scene"],
11155
+ requires: ["selection", "scene", "applyOps"],
10959
11156
  invoker: {
10960
11157
  timing: "immediate",
10961
11158
  run: (deps, params) => {
@@ -11095,6 +11292,7 @@ function makeAlignAction(edge) {
11095
11292
  icon: ICON_FOR[edge],
11096
11293
  group: "align",
11097
11294
  eligible: { capability: "transforms-selection" },
11295
+ requires: ["selection", "scene"],
11098
11296
  // No default keybindings — six edges/centers don't fit a clean default
11099
11297
  // chord set. Wire bindings explicitly via the actions registry override map.
11100
11298
  invoker: {
@@ -11106,7 +11304,14 @@ function makeAlignAction(edge) {
11106
11304
  alignSelection(selection, scene, edge);
11107
11305
  }
11108
11306
  },
11109
- enabled: () => ActionDisabledReason.SelectionRequired
11307
+ // Deps-aware, matching `alignSelection`'s own `ids.length < 2` guard: a
11308
+ // constant disabled reason greys the entry out forever (see
11309
+ // `requiresSelection`).
11310
+ enabled: (deps) => {
11311
+ const selection = deps?.selection;
11312
+ const count = selection?.get().length ?? 0;
11313
+ return count >= 2 ? true : ActionDisabledReason.SelectionRequired;
11314
+ }
11110
11315
  };
11111
11316
  }
11112
11317
  var alignLeftAction = makeAlignAction("left");
@@ -11187,6 +11392,7 @@ function makeDistributeAction(axis) {
11187
11392
  icon: ICON_FOR2[axis],
11188
11393
  group: "distribute",
11189
11394
  eligible: { capability: "transforms-selection" },
11395
+ requires: ["selection", "scene"],
11190
11396
  // No default keybindings. Wire bindings explicitly via the actions registry.
11191
11397
  invoker: {
11192
11398
  timing: "immediate",
@@ -11197,7 +11403,14 @@ function makeDistributeAction(axis) {
11197
11403
  distributeSelection(selection, scene, axis);
11198
11404
  }
11199
11405
  },
11200
- enabled: () => ActionDisabledReason.SelectionRequired
11406
+ // Deps-aware, matching `distributeSelection`'s own `ids.length < 3` guard:
11407
+ // a constant disabled reason greys the entry out forever (see
11408
+ // `requiresSelection`).
11409
+ enabled: (deps) => {
11410
+ const selection = deps?.selection;
11411
+ const count = selection?.get().length ?? 0;
11412
+ return count >= 3 ? true : ActionDisabledReason.SelectionRequired;
11413
+ }
11201
11414
  };
11202
11415
  }
11203
11416
  var distributeHorizontalAction = makeDistributeAction("x");
@@ -11279,7 +11492,91 @@ function pathToMultiPolygon(path, opts = {}) {
11279
11492
  }
11280
11493
  finalizeCurrent();
11281
11494
  if (rings.length === 0) return [];
11282
- return rings.map((r) => [r]);
11495
+ return groupRings(rings, path.fillRule);
11496
+ }
11497
+ function ringSignedArea(ring) {
11498
+ let a = 0;
11499
+ for (let i = 0, n = ring.length - 1; i < n; i++) {
11500
+ a += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];
11501
+ }
11502
+ return a * 0.5;
11503
+ }
11504
+ function ringBox(ring) {
11505
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
11506
+ for (const [x, y] of ring) {
11507
+ if (x < minX) minX = x;
11508
+ if (x > maxX) maxX = x;
11509
+ if (y < minY) minY = y;
11510
+ if (y > maxY) maxY = y;
11511
+ }
11512
+ return [minX, minY, maxX, maxY];
11513
+ }
11514
+ function pointInRing(ring, px, py) {
11515
+ let inside = false;
11516
+ for (let i = 0, n = ring.length - 1; i < n; i++) {
11517
+ const [ax, ay] = ring[i];
11518
+ const [bx, by] = ring[i + 1];
11519
+ if (ay > py !== by > py && px < (bx - ax) * (py - ay) / (by - ay) + ax) {
11520
+ inside = !inside;
11521
+ }
11522
+ }
11523
+ return inside;
11524
+ }
11525
+ function ringContains(outer, outerBox, inner, innerBox) {
11526
+ if (innerBox[0] < outerBox[0] || innerBox[1] < outerBox[1] || innerBox[2] > outerBox[2] || innerBox[3] > outerBox[3]) return false;
11527
+ const n = inner.length - 1;
11528
+ let votes = 0;
11529
+ for (const k of [0, Math.floor(n / 3), Math.floor(2 * n / 3)]) {
11530
+ if (pointInRing(outer, inner[k][0], inner[k][1])) votes++;
11531
+ }
11532
+ return votes >= 2;
11533
+ }
11534
+ function groupRings(rings, fillRule) {
11535
+ const n = rings.length;
11536
+ if (n === 1) return [[rings[0]]];
11537
+ const boxes = rings.map(ringBox);
11538
+ const orient = rings.map((r) => ringSignedArea(r) >= 0 ? 1 : -1);
11539
+ const parent = new Array(n).fill(-1);
11540
+ const children = rings.map(() => []);
11541
+ for (let i = 0; i < n; i++) {
11542
+ let best = -1;
11543
+ let bestArea = Infinity;
11544
+ for (let j = 0; j < n; j++) {
11545
+ if (i === j) continue;
11546
+ if (!ringContains(rings[j], boxes[j], rings[i], boxes[i])) continue;
11547
+ const area = (boxes[j][2] - boxes[j][0]) * (boxes[j][3] - boxes[j][1]);
11548
+ if (area < bestArea) {
11549
+ bestArea = area;
11550
+ best = j;
11551
+ }
11552
+ }
11553
+ parent[i] = best;
11554
+ }
11555
+ for (let i = 0; i < n; i++) if (parent[i] >= 0) children[parent[i]].push(i);
11556
+ const filled = new Array(n);
11557
+ for (let i = 0; i < n; i++) {
11558
+ let depth = 0;
11559
+ let winding = 0;
11560
+ for (let a = i; a >= 0; a = parent[a]) {
11561
+ winding += orient[a];
11562
+ if (a !== i) depth++;
11563
+ }
11564
+ filled[i] = fillRule === "evenodd" ? depth % 2 === 0 : winding !== 0;
11565
+ }
11566
+ const out = [];
11567
+ for (let i = 0; i < n; i++) {
11568
+ if (!filled[i]) continue;
11569
+ if (parent[i] >= 0 && filled[parent[i]]) continue;
11570
+ const polygon = [rings[i]];
11571
+ const queue = [...children[i]];
11572
+ while (queue.length > 0) {
11573
+ const c = queue.pop();
11574
+ if (filled[c]) queue.push(...children[c]);
11575
+ else polygon.push(rings[c]);
11576
+ }
11577
+ out.push(polygon);
11578
+ }
11579
+ return out;
11283
11580
  }
11284
11581
  function multiPolygonToPath(mp) {
11285
11582
  let nCmds = 0;
@@ -11592,6 +11889,7 @@ function makePathfinderAction(op) {
11592
11889
  icon: ICON_FOR3[op],
11593
11890
  group: "pathfinder",
11594
11891
  eligible: { capability: "creates-shapes" },
11892
+ requires: ["selection", "booleansAdapter"],
11595
11893
  // No default keybindings — there's no industry-standard chord set for
11596
11894
  // boolean ops. Wire bindings explicitly via the actions registry override map.
11597
11895
  invoker: {
@@ -13884,10 +14182,11 @@ function createDispatcher(opts) {
13884
14182
  uiOngoingByAction.set(actionId, gestureId);
13885
14183
  notify2();
13886
14184
  let ended = false;
14185
+ const live = () => !ended && inFlightHandles.get(gestureId) === handle;
13887
14186
  return {
13888
14187
  gestureId,
13889
14188
  update(nextParams) {
13890
- if (ended) return;
14189
+ if (!live()) return;
13891
14190
  if (!handle.onMove) return;
13892
14191
  try {
13893
14192
  handle.onMove(buildUiInvocationCtx(deps, nextParams));
@@ -13897,7 +14196,10 @@ function createDispatcher(opts) {
13897
14196
  notify2();
13898
14197
  },
13899
14198
  end(reason) {
13900
- if (ended) return;
14199
+ if (!live()) {
14200
+ ended = true;
14201
+ return;
14202
+ }
13901
14203
  ended = true;
13902
14204
  if (handle.onEnd) {
13903
14205
  try {
@@ -15111,6 +15413,7 @@ function useTools(opts) {
15111
15413
  );
15112
15414
  const active = contributions.focused;
15113
15415
  const getActiveOverlays = contributions.overlays;
15416
+ const registryKey = Object.keys(slotted.registry).sort().join("|") + "#" + slotted.ambient.map((t) => t.id).join("|");
15114
15417
  return useMemo(
15115
15418
  () => ({
15116
15419
  active,
@@ -15123,7 +15426,7 @@ function useTools(opts) {
15123
15426
  has,
15124
15427
  getActiveOverlays
15125
15428
  }),
15126
- [active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays]
15429
+ [active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays, registryKey]
15127
15430
  );
15128
15431
  }
15129
15432
  var TOOL_ACTIVATE_ID = "tool.activate";
@@ -16514,8 +16817,17 @@ function composeAffordanceLayer(id, label, affordances) {
16514
16817
  // `buildAffordanceAt` share one implementation rather than two that can
16515
16818
  // drift. This wrapper lifts the region's declared cursor and claim onto
16516
16819
  // the binding, the way `buildAffordanceAt` lifts them onto `AffordanceHit`.
16517
- hitTest: (wx, wy, state, view, _dims, isVisible) => {
16518
- const hit = hitAffordanceRegions(affordances, wx, wy, state, view, isVisible);
16820
+ hitTest: (wx, wy, data, view, _dims, isVisible) => {
16821
+ const state = asChromeState(data);
16822
+ if (!state) return null;
16823
+ const hit = hitAffordanceRegions(
16824
+ affordances,
16825
+ wx,
16826
+ wy,
16827
+ state,
16828
+ view,
16829
+ isVisible ?? asIsVisible(data) ?? void 0
16830
+ );
16519
16831
  if (!hit) return null;
16520
16832
  const { cursor, strength, claimedKinds } = hit.region;
16521
16833
  return {
@@ -17118,6 +17430,10 @@ function usePenTool(options) {
17118
17430
  onEnd: (endCtx, reason) => {
17119
17431
  const se = s();
17120
17432
  if (reason === "cancel") {
17433
+ if (se.current && se.draggingHandleAt === se.current.anchors.length - 1) {
17434
+ se.current.anchors.pop();
17435
+ if (se.current.anchors.length === 0) se.current = null;
17436
+ }
17121
17437
  se.draggingHandleAt = null;
17122
17438
  forceRenderRef.current();
17123
17439
  return;
@@ -18696,7 +19012,6 @@ function CanvasInner(props, ref) {
18696
19012
  shaders,
18697
19013
  previewIdsExtra,
18698
19014
  previewPoseExtra,
18699
- previewBoundsExtra,
18700
19015
  gestureSource,
18701
19016
  viewport,
18702
19017
  backgroundFill,
@@ -18722,6 +19037,7 @@ function CanvasInner(props, ref) {
18722
19037
  debugSinkRefForCtx.current = debugSink;
18723
19038
  const adapter = adapterProp;
18724
19039
  const canvasRef = useRef(null);
19040
+ const helpersForLayersRef = useRef(null);
18725
19041
  const dimsRef = useRef({ width, height });
18726
19042
  dimsRef.current = { width, height };
18727
19043
  const getIsVisibleRef = useRef(getIsVisible);
@@ -18744,7 +19060,7 @@ function CanvasInner(props, ref) {
18744
19060
  const isVisible = getIsVisibleRef.current?.() ?? alwaysVisible;
18745
19061
  for (const layer of layers2) {
18746
19062
  if (!layer.hitTest) continue;
18747
- const hit = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
19063
+ const hit = layer.hitTest(worldX, worldY, helpersForLayersRef.current, view, dims, isVisible);
18748
19064
  if (hit) return { layerId: layer.id, hit };
18749
19065
  }
18750
19066
  return null;
@@ -18899,8 +19215,8 @@ function CanvasInner(props, ref) {
18899
19215
  };
18900
19216
  const selectedIdsForWiring = effectiveSelection.current;
18901
19217
  const multiActive2 = selectedIdsForWiring.length > 1;
18902
- const previewExtraRef = useRef({ previewPoseExtra, previewBoundsExtra });
18903
- previewExtraRef.current = { previewPoseExtra, previewBoundsExtra };
19218
+ const previewExtraRef = useRef({ previewPoseExtra });
19219
+ previewExtraRef.current = { previewPoseExtra };
18904
19220
  const effectiveBoundsOf = useMemo(() => {
18905
19221
  return boundsOf2 ?? baseBoundsOf;
18906
19222
  }, [boundsOf2, baseBoundsOf]);
@@ -18919,10 +19235,6 @@ function CanvasInner(props, ref) {
18919
19235
  if (p != null) return geometry.getBounds(p);
18920
19236
  }
18921
19237
  const extras = previewExtraRef.current;
18922
- if (extras.previewBoundsExtra) {
18923
- const b = extras.previewBoundsExtra(id);
18924
- if (b) return b;
18925
- }
18926
19238
  if (extras.previewPoseExtra) {
18927
19239
  const p = extras.previewPoseExtra(id);
18928
19240
  if (p != null) return geometry.getBounds(p);
@@ -18963,10 +19275,6 @@ function CanvasInner(props, ref) {
18963
19275
  const p = firstPreviewPose(tools, id);
18964
19276
  if (p != null) return geometry.getBounds(p);
18965
19277
  }
18966
- if (previewBoundsExtra) {
18967
- const b = previewBoundsExtra(id);
18968
- if (b) return b;
18969
- }
18970
19278
  if (previewPoseExtra) {
18971
19279
  const p = previewPoseExtra(id);
18972
19280
  if (p != null) return geometry.getBounds(p);
@@ -19002,6 +19310,7 @@ function CanvasInner(props, ref) {
19002
19310
  getDebug: () => debugSink,
19003
19311
  getIsVisible: () => getIsVisibleRef.current?.() ?? alwaysVisible
19004
19312
  };
19313
+ helpersForLayersRef.current = helpersForLayers;
19005
19314
  if (helpersRef) helpersRef.current = helpersForLayers;
19006
19315
  const pointerDownRef = useRef(false);
19007
19316
  const handlePointerDown = (e) => {
@@ -19183,6 +19492,11 @@ function CanvasInner(props, ref) {
19183
19492
  );
19184
19493
  renderer.render(commands, viewToMat3(effectiveView));
19185
19494
  }, [layersWithDebug, width, height, effectiveView, debugSink, redrawNonce, dprProp]);
19495
+ useEffect(() => () => {
19496
+ glRendererRef.current?.dispose();
19497
+ glRendererRef.current = null;
19498
+ lastResizeRef.current = null;
19499
+ }, []);
19186
19500
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
19187
19501
  useEffect(() => {
19188
19502
  const renderer = glRendererRef.current;
@@ -20611,6 +20925,89 @@ function poseContainsRotated(pose, wx, wy, tolerance) {
20611
20925
  }
20612
20926
  return poseContains(pose, wx, wy, tolerance);
20613
20927
  }
20928
+ function hiddenLayerIds(layers) {
20929
+ const out = /* @__PURE__ */ new Set();
20930
+ for (const layer of layers ?? []) {
20931
+ if (layer.visible === false) out.add(layer.id);
20932
+ }
20933
+ return out;
20934
+ }
20935
+ function hitTestArea(scene, bounds) {
20936
+ const { x, y, width: w, height: h } = bounds;
20937
+ const area = [x, y, x + w, y, x + w, y + h, x, y + h];
20938
+ return hitTestAreaPolygon(scene, area, { x, y, width: w, height: h }, true);
20939
+ }
20940
+ function hitTestAreaPolygon(scene, area, areaBounds, areaIsRect = false) {
20941
+ const ab = areaBounds ?? boundsOf(area);
20942
+ if (!ab) return [];
20943
+ const hits = [];
20944
+ const hidden = hiddenLayerIds(scene.layers);
20945
+ const order = scene.renderOrderNodes();
20946
+ for (let i = 0; i < order.length; i++) {
20947
+ const node = order[i];
20948
+ if (node.kind === "container") continue;
20949
+ if (hidden.size > 0 && hidden.has(node.layer)) continue;
20950
+ const pose = node.pose;
20951
+ const silhouette = pose !== null && typeof pose === "object" && pose.kind === "polygon";
20952
+ const b = silhouette ? nodeMemo(node, "aabb", pose, () => aabbOfPose(pose)) : aabbOfPose(pose);
20953
+ if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) {
20954
+ continue;
20955
+ }
20956
+ 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) {
20957
+ continue;
20958
+ }
20959
+ 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) {
20960
+ hits.push(node.id);
20961
+ continue;
20962
+ }
20963
+ if (silhouette) {
20964
+ if (silhouetteOverlapsArea(pose.coords, area)) {
20965
+ hits.push(node.id);
20966
+ }
20967
+ continue;
20968
+ }
20969
+ const drawn = findShapeSilhouette(node, pose);
20970
+ if (drawn?.kind === "polygon") {
20971
+ if (silhouetteOverlapsArea(drawn.coords, area)) hits.push(node.id);
20972
+ continue;
20973
+ }
20974
+ hits.push(node.id);
20975
+ }
20976
+ return hits;
20977
+ }
20978
+ function silhouetteOverlapsArea(sil, area) {
20979
+ const ns = sil.length >> 1;
20980
+ const na = area.length >> 1;
20981
+ if (ns < 1 || na < 3) return false;
20982
+ for (let i = 0; i < ns; i++) {
20983
+ if (pointInPolygon(area, sil[i * 2], sil[i * 2 + 1])) return true;
20984
+ }
20985
+ for (let i = 0; i < na; i++) {
20986
+ if (pointInPolygon(sil, area[i * 2], area[i * 2 + 1])) return true;
20987
+ }
20988
+ for (let i = 0, j = ns - 1; i < ns; j = i++) {
20989
+ const ax = sil[j * 2], ay = sil[j * 2 + 1];
20990
+ const bx = sil[i * 2], by = sil[i * 2 + 1];
20991
+ for (let k = 0, l = na - 1; k < na; l = k++) {
20992
+ const cx = area[l * 2], cy = area[l * 2 + 1];
20993
+ const dx = area[k * 2], dy = area[k * 2 + 1];
20994
+ if (segmentsCross(ax, ay, bx, by, cx, cy, dx, dy)) return true;
20995
+ }
20996
+ }
20997
+ return false;
20998
+ }
20999
+ function boundsOf(coords) {
21000
+ if (coords.length < 2) return null;
21001
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
21002
+ for (let i = 0; i + 1 < coords.length; i += 2) {
21003
+ const x = coords[i], y = coords[i + 1];
21004
+ if (x < minX) minX = x;
21005
+ if (y < minY) minY = y;
21006
+ if (x > maxX) maxX = x;
21007
+ if (y > maxY) maxY = y;
21008
+ }
21009
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
21010
+ }
20614
21011
  var DEFAULT_PICK_TOLERANCE_PX = 4;
20615
21012
  function useSceneSelectTool(args) {
20616
21013
  const { scene, selection, geometry, selectTool: opts, insertTool, layouts, getView } = args;
@@ -20708,7 +21105,9 @@ function useSceneSelectTool(args) {
20708
21105
  }
20709
21106
  const tolerance = pickTolerancePx / meanScale(getView?.()?.scale ?? { x: 1, y: 1 });
20710
21107
  const out = [];
21108
+ const hidden = hiddenLayerIds(scene.layers);
20711
21109
  for (const n of scene.renderOrderNodes()) {
21110
+ if (hidden.size > 0 && hidden.has(n.layer)) continue;
20712
21111
  if (!poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
20713
21112
  if (shapePicking && !shapeCoversPoint(n, n.pose, wx, wy, { tolerance })) continue;
20714
21113
  out.push(n.id);
@@ -21592,8 +21991,10 @@ function createPathAnchorAffordances(getAnchorState, opts = {}) {
21592
21991
  const anchors = {
21593
21992
  id: PATH_ANCHOR_CHROME_ID,
21594
21993
  regions(state) {
21994
+ const editingId = getAnchorState()?.editingId ?? null;
21595
21995
  const out = [];
21596
21996
  for (const { id, path } of editablePaths(state)) {
21997
+ if (editingId !== null && id !== editingId) continue;
21597
21998
  for (const a of enumerateAnchors(path)) {
21598
21999
  out.push(point(
21599
22000
  `${id}:anchor:${a.anchorIndex}`,
@@ -21848,80 +22249,6 @@ function DepRegistryProviderIfRoot({ children }) {
21848
22249
  if (parent) return /* @__PURE__ */ jsx(Fragment, { children });
21849
22250
  return /* @__PURE__ */ jsx(DepRegistryProvider, { children });
21850
22251
  }
21851
- function hitTestArea(scene, bounds) {
21852
- const { x, y, width: w, height: h } = bounds;
21853
- const area = [x, y, x + w, y, x + w, y + h, x, y + h];
21854
- return hitTestAreaPolygon(scene, area, { x, y, width: w, height: h }, true);
21855
- }
21856
- function hitTestAreaPolygon(scene, area, areaBounds, areaIsRect = false) {
21857
- const ab = areaBounds ?? boundsOf(area);
21858
- if (!ab) return [];
21859
- const hits = [];
21860
- const order = scene.renderOrderNodes();
21861
- for (let i = 0; i < order.length; i++) {
21862
- const node = order[i];
21863
- if (node.kind === "container") continue;
21864
- const pose = node.pose;
21865
- const silhouette = pose !== null && typeof pose === "object" && pose.kind === "polygon";
21866
- const b = silhouette ? nodeMemo(node, "aabb", pose, () => aabbOfPose(pose)) : aabbOfPose(pose);
21867
- if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) {
21868
- continue;
21869
- }
21870
- 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) {
21871
- continue;
21872
- }
21873
- 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) {
21874
- hits.push(node.id);
21875
- continue;
21876
- }
21877
- if (silhouette) {
21878
- if (silhouetteOverlapsArea(pose.coords, area)) {
21879
- hits.push(node.id);
21880
- }
21881
- continue;
21882
- }
21883
- const drawn = findShapeSilhouette(node, pose);
21884
- if (drawn?.kind === "polygon") {
21885
- if (silhouetteOverlapsArea(drawn.coords, area)) hits.push(node.id);
21886
- continue;
21887
- }
21888
- hits.push(node.id);
21889
- }
21890
- return hits;
21891
- }
21892
- function silhouetteOverlapsArea(sil, area) {
21893
- const ns = sil.length >> 1;
21894
- const na = area.length >> 1;
21895
- if (ns < 1 || na < 3) return false;
21896
- for (let i = 0; i < ns; i++) {
21897
- if (pointInPolygon(area, sil[i * 2], sil[i * 2 + 1])) return true;
21898
- }
21899
- for (let i = 0; i < na; i++) {
21900
- if (pointInPolygon(sil, area[i * 2], area[i * 2 + 1])) return true;
21901
- }
21902
- for (let i = 0, j = ns - 1; i < ns; j = i++) {
21903
- const ax = sil[j * 2], ay = sil[j * 2 + 1];
21904
- const bx = sil[i * 2], by = sil[i * 2 + 1];
21905
- for (let k = 0, l = na - 1; k < na; l = k++) {
21906
- const cx = area[l * 2], cy = area[l * 2 + 1];
21907
- const dx = area[k * 2], dy = area[k * 2 + 1];
21908
- if (segmentsCross(ax, ay, bx, by, cx, cy, dx, dy)) return true;
21909
- }
21910
- }
21911
- return false;
21912
- }
21913
- function boundsOf(coords) {
21914
- if (coords.length < 2) return null;
21915
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
21916
- for (let i = 0; i + 1 < coords.length; i += 2) {
21917
- const x = coords[i], y = coords[i + 1];
21918
- if (x < minX) minX = x;
21919
- if (y < minY) minY = y;
21920
- if (x > maxX) maxX = x;
21921
- if (y > maxY) maxY = y;
21922
- }
21923
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
21924
- }
21925
22252
  function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize) {
21926
22253
  const viewApiRef = useRef({
21927
22254
  get: () => currentViewRef.current,
@@ -22693,6 +23020,7 @@ function useActionsPropResolver(actions) {
22693
23020
  }
22694
23021
  if (!prop) return;
22695
23022
  const unregisters = [];
23023
+ const restores = [];
22696
23024
  const warnedIds = /* @__PURE__ */ new Set();
22697
23025
  for (const [slotId, entry] of Object.entries(prop)) {
22698
23026
  if (entry === null) {
@@ -22720,10 +23048,14 @@ function useActionsPropResolver(actions) {
22720
23048
  }
22721
23049
  const { id: _drop, ...rest } = entry;
22722
23050
  const merged = { ...existing, ...rest };
23051
+ restores.push(() => {
23052
+ reg.register(existing);
23053
+ });
22723
23054
  unregisters.push(reg.register(merged));
22724
23055
  }
22725
23056
  return () => {
22726
23057
  for (const u of unregisters) u();
23058
+ for (const r of restores) r();
22727
23059
  };
22728
23060
  }, [reg, actions]);
22729
23061
  }
@@ -24475,5 +24807,5 @@ function oklchToOklab(L, C, h) {
24475
24807
  }
24476
24808
 
24477
24809
  export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleContribution, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
24478
- //# sourceMappingURL=chunk-3P56ZCCJ.js.map
24479
- //# sourceMappingURL=chunk-3P56ZCCJ.js.map
24810
+ //# sourceMappingURL=chunk-DWV7SFKR.js.map
24811
+ //# sourceMappingURL=chunk-DWV7SFKR.js.map