@weasel-js/labkit 1.4.0 → 1.4.2

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 (133) hide show
  1. package/README.md +8 -0
  2. package/dist/_dts/{CanvasStackContext-D6M0o8cQ.d.ts → CanvasStackContext-kjILVnPj.d.ts} +1 -1
  3. package/dist/_dts/PrefsForm.d-CgxUequc.d.ts +20 -0
  4. package/dist/_dts/{frac-QS7XzvpG.d.ts → frac-A4R6v4ld.d.ts} +36 -13
  5. package/dist/_dts/{index-B2X21G1c.d.ts → index-CsU9WhjM.d.ts} +90 -30
  6. package/dist/_dts/{types-ChVJJHvk.d.ts → types-BbXpvQa8.d.ts} +20 -1
  7. package/dist/_dts/{types-C1Zw7s5P.d.ts → types-lg4TSCb2.d.ts} +2 -1
  8. package/dist/_dts/{useTrialState-Cdm13fvl.d.ts → useTrialState-DKjqpv20.d.ts} +5 -1
  9. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +5041 -0
  10. package/dist/canvas/index.d.ts +16 -16
  11. package/dist/canvas/index.js +2 -2
  12. package/dist/chrome/index.d.ts +15 -9
  13. package/dist/chrome/index.js +5 -5
  14. package/dist/{chunk-7VW3S44V.js → chunk-54ZWZ5FQ.js} +908 -301
  15. package/dist/chunk-54ZWZ5FQ.js.map +1 -0
  16. package/dist/{chunk-Y2HK44TG.js → chunk-D5KQ5OY6.js} +3 -3
  17. package/dist/{chunk-Y2HK44TG.js.map → chunk-D5KQ5OY6.js.map} +1 -1
  18. package/dist/{chunk-VHFQKAUL.js → chunk-FJG4PHTL.js} +83 -75
  19. package/dist/chunk-FJG4PHTL.js.map +1 -0
  20. package/dist/{chunk-LN6JDUGB.js → chunk-HXXTULDN.js} +2 -2
  21. package/dist/{chunk-LN6JDUGB.js.map → chunk-HXXTULDN.js.map} +1 -1
  22. package/dist/{chunk-L5WBGUVR.js → chunk-ISSVF5PT.js} +2753 -2654
  23. package/dist/chunk-ISSVF5PT.js.map +1 -0
  24. package/dist/chunk-NSOVI3AZ.js +170 -0
  25. package/dist/chunk-NSOVI3AZ.js.map +1 -0
  26. package/dist/{chunk-YQGQUBKA.js → chunk-TN7YSJVU.js} +62 -60
  27. package/dist/chunk-TN7YSJVU.js.map +1 -0
  28. package/dist/{chunk-4AMN4MMB.js → chunk-UDXOYZEC.js} +53 -39
  29. package/dist/chunk-UDXOYZEC.js.map +1 -0
  30. package/dist/{chunk-LJSIUFYD.js → chunk-ULDW42CR.js} +23 -2
  31. package/dist/chunk-ULDW42CR.js.map +1 -0
  32. package/dist/{chunk-QR5V3AFG.js → chunk-UTOEDPNU.js} +5 -5
  33. package/dist/{chunk-QR5V3AFG.js.map → chunk-UTOEDPNU.js.map} +1 -1
  34. package/dist/{chunk-UD7TEHZT.js → chunk-W2FJR5FF.js} +35 -22
  35. package/dist/chunk-W2FJR5FF.js.map +1 -0
  36. package/dist/{chunk-B5VWJBWM.js → chunk-WS6ZRV75.js} +3 -3
  37. package/dist/{chunk-B5VWJBWM.js.map → chunk-WS6ZRV75.js.map} +1 -1
  38. package/dist/{chunk-W3KSUXXR.js → chunk-XKENZTNE.js} +45 -14
  39. package/dist/chunk-XKENZTNE.js.map +1 -0
  40. package/dist/controls/index.d.ts +4 -3
  41. package/dist/controls/index.js +3 -3
  42. package/dist/dragdrop/index.d.ts +8 -8
  43. package/dist/dragdrop/index.js +2 -2
  44. package/dist/index.d.ts +75 -110
  45. package/dist/index.js +148 -115
  46. package/dist/index.js.map +1 -1
  47. package/dist/job/index.js +1 -1
  48. package/dist/layers/index.d.ts +26 -11
  49. package/dist/layers/index.js +3 -3
  50. package/dist/loupe/index.d.ts +7 -13
  51. package/dist/loupe/index.js +2 -2
  52. package/dist/passthrough/weasel-canvas.d.ts +3 -343
  53. package/dist/passthrough/weasel-canvas.js +1 -1
  54. package/dist/passthrough/weasel-ui.d.ts +123 -3088
  55. package/dist/passthrough/weasel-ui.js +2 -2
  56. package/dist/primitives/index.js +4 -4
  57. package/dist/state/index.d.ts +3 -3
  58. package/dist/state/index.js +2 -2
  59. package/dist/styles.css +30 -3
  60. package/dist/surface/index.js +2 -2
  61. package/dist/ui/layers/index.d.ts +22 -9
  62. package/dist/ui/layers/index.js +2 -2
  63. package/dist/undo/index.d.ts +6 -5
  64. package/package.json +8 -8
  65. package/src/annotations/AnnotationTargets.tsx +6 -1
  66. package/src/annotations/Annotations.overlay.test.tsx +2 -0
  67. package/src/annotations/types.ts +4 -1
  68. package/src/canvas/CanvasStack.tsx +8 -13
  69. package/src/canvas/useOrbit.test.ts +97 -1
  70. package/src/canvas/useOrbit.ts +41 -34
  71. package/src/canvas/usePanZoom.test.ts +106 -1
  72. package/src/canvas/usePanZoom.ts +54 -36
  73. package/src/chrome/ChromeRegions.stories.tsx +42 -1
  74. package/src/chrome/builtins.test.ts +4 -0
  75. package/src/chrome/builtins.tsx +18 -0
  76. package/src/chrome/index.ts +1 -1
  77. package/src/chrome/regions/SidebarRegion.tsx +7 -6
  78. package/src/chrome/regions/TitleBarRegion.test.tsx +64 -0
  79. package/src/chrome/regions/TitleBarRegion.tsx +16 -7
  80. package/src/chrome/regions/ToolbarRegion.test.tsx +14 -0
  81. package/src/chrome/regions/ToolbarRegion.tsx +2 -2
  82. package/src/chrome/regions/ViewportRegion.tsx +2 -2
  83. package/src/chrome/regions/regions.test.tsx +45 -1
  84. package/src/chrome/types.ts +21 -3
  85. package/src/controls/ControlPanel.test.tsx +38 -0
  86. package/src/controls/ControlPanel.tsx +51 -6
  87. package/src/dragdrop/DragDropRuntime.tsx +74 -61
  88. package/src/dragdrop/Palette.tsx +4 -3
  89. package/src/dragdrop/dragDrop.test.tsx +35 -12
  90. package/src/index.ts +2 -1
  91. package/src/instrument/types.ts +4 -5
  92. package/src/job/useJob.ts +1 -1
  93. package/src/lab/Lab.test.tsx +7 -0
  94. package/src/lab/Lab.tsx +2 -2
  95. package/src/lab/LabContext.ts +5 -1
  96. package/src/lab/LabShell.less +17 -2
  97. package/src/lab/Workspace.tsx +1 -1
  98. package/src/layers/AGENTS.md +29 -7
  99. package/src/layers/LayerList.less +16 -0
  100. package/src/layers/LayerList.stories.tsx +66 -0
  101. package/src/layers/LayerList.test.tsx +185 -2
  102. package/src/layers/LayerList.tsx +221 -74
  103. package/src/layers/index.ts +1 -1
  104. package/src/passthrough/weasel-ui.ts +5 -0
  105. package/src/primitives/FloatingPanel.test.tsx +87 -0
  106. package/src/primitives/FloatingPanel.tsx +59 -41
  107. package/src/state/store.test.ts +78 -0
  108. package/src/state/store.ts +27 -0
  109. package/src/state/types.ts +20 -0
  110. package/src/trial/Trial.less +4 -0
  111. package/src/trial/Trial.targets.test.tsx +96 -0
  112. package/src/trial/Trial.test.tsx +171 -14
  113. package/src/trial/Trial.tsx +4 -1
  114. package/src/trial/TrialChrome.tsx +22 -2
  115. package/src/trial/TrialTitleBar.tsx +7 -3
  116. package/src/trial/index.ts +1 -0
  117. package/src/trial/trialOps.test.ts +49 -1
  118. package/src/trial/trialOps.ts +28 -6
  119. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +0 -564
  120. package/dist/_dts/PrefsForm-BkUJZx0A.d.ts +0 -204
  121. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +0 -21
  122. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +0 -87
  123. package/dist/_dts/types-C-gh9Ap-.d.ts +0 -695
  124. package/dist/chunk-4AMN4MMB.js.map +0 -1
  125. package/dist/chunk-7VW3S44V.js.map +0 -1
  126. package/dist/chunk-L5WBGUVR.js.map +0 -1
  127. package/dist/chunk-LJSIUFYD.js.map +0 -1
  128. package/dist/chunk-UD7TEHZT.js.map +0 -1
  129. package/dist/chunk-VHFQKAUL.js.map +0 -1
  130. package/dist/chunk-W3KSUXXR.js.map +0 -1
  131. package/dist/chunk-XCBCZEBO.js +0 -94
  132. package/dist/chunk-XCBCZEBO.js.map +0 -1
  133. package/dist/chunk-YQGQUBKA.js.map +0 -1
@@ -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-UTIFY2UU.js
6
+ // ../core/dist/chunk-TORKRNFY.js
7
7
 
8
8
  // ../gestures/dist/index.js
9
9
  var GESTURE_DESCRIPTORS = [
@@ -287,7 +287,7 @@ function matchSpec(e, spec, isMac, phaseCtx) {
287
287
  }
288
288
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
289
289
 
290
- // ../core/dist/chunk-UTIFY2UU.js
290
+ // ../core/dist/chunk-TORKRNFY.js
291
291
  var DepRegistryContext = createContext(null);
292
292
  function DepRegistryProvider({ children }) {
293
293
  const sourcesRef = useRef(/* @__PURE__ */ new Map());
@@ -437,6 +437,7 @@ function warnSharedScope() {
437
437
  var ActionsContext = createContext(null);
438
438
  function ActionsProvider({ children }) {
439
439
  const actionsRef = useRef(/* @__PURE__ */ new Map());
440
+ const liveAction = (id) => actionsRef.current.get(id)?.at(-1);
440
441
  const versionRef = useRef(0);
441
442
  const cachedRef = useRef([]);
442
443
  const cachedVerRef = useRef(-1);
@@ -451,7 +452,9 @@ function ActionsProvider({ children }) {
451
452
  const snapshot = () => {
452
453
  const v = versionRef.current;
453
454
  if (cachedVerRef.current === v) return cachedRef.current;
454
- const out = Object.freeze(Array.from(actionsRef.current.values()));
455
+ const out = Object.freeze(
456
+ Array.from(actionsRef.current.values(), (stack) => stack[stack.length - 1])
457
+ );
455
458
  cachedRef.current = out;
456
459
  cachedVerRef.current = v;
457
460
  return out;
@@ -469,16 +472,23 @@ function ActionsProvider({ children }) {
469
472
  register: (action) => {
470
473
  validateActionId(action.id);
471
474
  validateActionDefaultBinding(action);
472
- actionsRef.current.set(action.id, action);
475
+ const stack = actionsRef.current.get(action.id);
476
+ if (stack) stack.push(action);
477
+ else actionsRef.current.set(action.id, [action]);
473
478
  versionRef.current++;
474
479
  notify2();
480
+ let released = false;
475
481
  return () => {
482
+ if (released) return;
483
+ released = true;
476
484
  const cur = actionsRef.current.get(action.id);
477
- if (cur === action) {
478
- actionsRef.current.delete(action.id);
479
- versionRef.current++;
480
- notify2();
481
- }
485
+ if (!cur) return;
486
+ const i = cur.lastIndexOf(action);
487
+ if (i === -1) return;
488
+ cur.splice(i, 1);
489
+ if (cur.length === 0) actionsRef.current.delete(action.id);
490
+ versionRef.current++;
491
+ notify2();
482
492
  };
483
493
  },
484
494
  unregister: (id) => {
@@ -489,7 +499,7 @@ function ActionsProvider({ children }) {
489
499
  },
490
500
  list: () => snapshot(),
491
501
  trigger: (id, params) => {
492
- const a = actionsRef.current.get(id);
502
+ const a = liveAction(id);
493
503
  if (!a) return false;
494
504
  try {
495
505
  if (a.invoker && a.invoker.timing === "immediate") {
@@ -536,7 +546,7 @@ function ActionsProvider({ children }) {
536
546
  const disp = dispatcherRef.current;
537
547
  if (!disp) return null;
538
548
  const r = wiredDepRegRef.current ?? depRegRef.current;
539
- const a = actionsRef.current.get(id);
549
+ const a = liveAction(id);
540
550
  const deps = !r ? {} : a?.requires ? buildDepsFromRequires(a, r) : {
541
551
  selection: r.get("selection"),
542
552
  scene: r.get("scene"),
@@ -2968,7 +2978,7 @@ var TEXT_SDF_UNIFORMS = [
2968
2978
  ];
2969
2979
  var TEXT_SDF_ATTRIBUTES = ["a_position", "a_uv", "a_baselineY"];
2970
2980
 
2971
- // ../text/dist/chunk-BYL6KOD4.js
2981
+ // ../text/dist/chunk-7ZHZKQEH.js
2972
2982
  function resolveAlign(align, direction) {
2973
2983
  if (align === "start") return direction === "rtl" ? "right" : "left";
2974
2984
  if (align === "end") return direction === "rtl" ? "left" : "right";
@@ -3538,7 +3548,7 @@ function layoutRuns(runs, opts) {
3538
3548
  cells[pos] = cell;
3539
3549
  const step = e.advance + e.tracking;
3540
3550
  const scale2 = e.fontSize / e.metrics.size;
3541
- const baselineY = lineBaselineY - e.run.baselineShift;
3551
+ const baselineY = lineBaselineY - (e.run.baselineShift ?? 0);
3542
3552
  if (e.run.underline || e.run.strikethrough || e.run.overline) {
3543
3553
  if (span !== null && span.underline === e.run.underline && span.strikethrough === e.run.strikethrough && span.overline === e.run.overline && span.fontSize === e.fontSize && span.baselineY === baselineY && sameFill(span.fill, e.run.fill)) {
3544
3554
  span.x1 = penX + step;
@@ -4660,7 +4670,7 @@ function serialToEntry(se, custom, logger) {
4660
4670
  };
4661
4671
  }
4662
4672
 
4663
- // ../core/dist/chunk-HYJL7OOJ.js
4673
+ // ../core/dist/chunk-KIVJXUYE.js
4664
4674
  function worldToScreen(worldX, worldY, view) {
4665
4675
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
4666
4676
  }
@@ -4833,6 +4843,44 @@ void main() {
4833
4843
  }
4834
4844
  `
4835
4845
  );
4846
+ var IMAGE_VOPACITY_VERT_SRC = (
4847
+ /* glsl */
4848
+ `#version 300 es
4849
+ in vec2 a_position;
4850
+ in vec2 a_uv;
4851
+ in float a_opacity;
4852
+ uniform mat3 u_proj;
4853
+ uniform mat3 u_model;
4854
+ out vec2 v_uv;
4855
+ out float v_opacity;
4856
+ void main() {
4857
+ vec3 screen = u_model * vec3(a_position, 1.0);
4858
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
4859
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
4860
+ v_uv = a_uv;
4861
+ v_opacity = a_opacity;
4862
+ }
4863
+ `
4864
+ );
4865
+ var IMAGE_VOPACITY_FRAG_SRC = (
4866
+ /* glsl */
4867
+ `#version 300 es
4868
+ precision highp float;
4869
+ in vec2 v_uv;
4870
+ in float v_opacity;
4871
+ uniform sampler2D u_sampler;
4872
+ uniform float u_alpha;
4873
+ uniform mat4 u_colorMatrix;
4874
+ uniform vec4 u_colorBias;
4875
+ out vec4 outColor;
4876
+ void main() {
4877
+ vec4 texel = texture(u_sampler, v_uv);
4878
+ vec4 mapped = clamp(u_colorMatrix * texel + u_colorBias, 0.0, 1.0);
4879
+ float a = mapped.a * v_opacity * u_alpha;
4880
+ outColor = vec4(mapped.rgb * a, a);
4881
+ }
4882
+ `
4883
+ );
4836
4884
  var IMAGE_FILL_UNIFORMS = [
4837
4885
  "u_proj",
4838
4886
  "u_model",
@@ -4843,6 +4891,15 @@ var IMAGE_FILL_UNIFORMS = [
4843
4891
  "u_colorBias"
4844
4892
  ];
4845
4893
  var IMAGE_FILL_ATTRIBUTES = ["a_position", "a_uv"];
4894
+ var IMAGE_VOPACITY_UNIFORMS = [
4895
+ "u_proj",
4896
+ "u_model",
4897
+ "u_sampler",
4898
+ "u_alpha",
4899
+ "u_colorMatrix",
4900
+ "u_colorBias"
4901
+ ];
4902
+ var IMAGE_VOPACITY_ATTRIBUTES = ["a_position", "a_uv", "a_opacity"];
4846
4903
  var GRAD_VERT_SRC = (
4847
4904
  /* glsl */
4848
4905
  `#version 300 es
@@ -5963,6 +6020,7 @@ seedBuiltins();
5963
6020
  function getPaintKind(kind) {
5964
6021
  return KINDS.get(kind ?? "solid");
5965
6022
  }
6023
+ var SPRITE_STRIDE = 9;
5966
6024
  function tessellate(path, opts = {}) {
5967
6025
  if (path.kind === "rect") return tessellateRect(path);
5968
6026
  return tessellatePolygon(path, opts);
@@ -7645,6 +7703,208 @@ var SolidBatch = class {
7645
7703
  gl.deleteVertexArray(set.vao);
7646
7704
  }
7647
7705
  };
7706
+ var MAX_IMAGE_VERTICES_PER_BATCH = 32768;
7707
+ var FLOATS_PER_VERTEX2 = 5;
7708
+ var INITIAL_QUADS = 64;
7709
+ var TIERS = [
7710
+ { quads: 1, slots: 64 },
7711
+ { quads: 16, slots: 64 },
7712
+ { quads: 256, slots: 16 }
7713
+ ];
7714
+ TIERS[0].slots;
7715
+ var IMAGE_LARGE_RING_SIZE = 4;
7716
+ function doubledTo2(from, need) {
7717
+ let capacity = from;
7718
+ while (capacity < need) capacity *= 2;
7719
+ return capacity;
7720
+ }
7721
+ function quadIndices(quads) {
7722
+ const out = new Uint32Array(quads * 6);
7723
+ for (let q = 0; q < quads; q++) {
7724
+ const b = q * 4;
7725
+ const j = q * 6;
7726
+ out[j] = b;
7727
+ out[j + 1] = b + 1;
7728
+ out[j + 2] = b + 2;
7729
+ out[j + 3] = b;
7730
+ out[j + 4] = b + 2;
7731
+ out[j + 5] = b + 3;
7732
+ }
7733
+ return out;
7734
+ }
7735
+ var ImageBatch = class {
7736
+ gl;
7737
+ aPos;
7738
+ aUv;
7739
+ aOpacity;
7740
+ /** One ring per tier, cycled per flush. Slots are created on first use, so a
7741
+ * renderer that flushes rarely allocates as few as it flushes. */
7742
+ rings = TIERS.map((t) => new Array(t.slots).fill(void 0));
7743
+ nextInRing = TIERS.map(() => 0);
7744
+ /** For flushes past the largest tier; these sets grow to fit. */
7745
+ largeRing = new Array(IMAGE_LARGE_RING_SIZE).fill(void 0);
7746
+ nextLarge = 0;
7747
+ verts = new Float32Array(INITIAL_QUADS * 4 * FLOATS_PER_VERTEX2);
7748
+ nQuads = 0;
7749
+ constructor(gl, prog) {
7750
+ const aPos = prog.attribute("a_position");
7751
+ const aUv = prog.attribute("a_uv");
7752
+ const aOpacity = prog.attribute("a_opacity");
7753
+ if (aPos === void 0 || aUv === void 0 || aOpacity === void 0) {
7754
+ throw new Error(
7755
+ "ImageBatch: vertex-opacity program is missing a_position / a_uv / a_opacity"
7756
+ );
7757
+ }
7758
+ this.gl = gl;
7759
+ this.aPos = aPos;
7760
+ this.aUv = aUv;
7761
+ this.aOpacity = aOpacity;
7762
+ }
7763
+ /** Indices staged — what a caller passes to `drawElements`. */
7764
+ get length() {
7765
+ return this.nQuads * 6;
7766
+ }
7767
+ get quads() {
7768
+ return this.nQuads;
7769
+ }
7770
+ /** Whether staging one more quad would put the run past the per-flush cap. */
7771
+ wouldOverflow() {
7772
+ return (this.nQuads + 1) * 4 > MAX_IMAGE_VERTICES_PER_BATCH;
7773
+ }
7774
+ /**
7775
+ * Append one image quad: the destination rect `(x, y, w, h)` mapped through
7776
+ * `m`, sampling `(u0, v0)`-`(u1, v1)`, every corner carrying `opacity`.
7777
+ *
7778
+ * An affine maps a rect to a parallelogram, so two triangles still cover it
7779
+ * and the batch draws at `u_model` identity. Corners wind top-left,
7780
+ * top-right, bottom-right, bottom-left, with UVs following — the flips a
7781
+ * command asks for are already in the `u`/`v` the caller passes.
7782
+ */
7783
+ pushQuad(x, y, w, h, m, u0, v0, u1, v1, opacity) {
7784
+ this.reserve();
7785
+ const v = this.verts;
7786
+ let i = this.nQuads * 4 * FLOATS_PER_VERTEX2;
7787
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
7788
+ const x1 = x + w;
7789
+ const y1 = y + h;
7790
+ const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
7791
+ const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
7792
+ const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
7793
+ const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
7794
+ v[i++] = ax;
7795
+ v[i++] = ay;
7796
+ v[i++] = u0;
7797
+ v[i++] = v0;
7798
+ v[i++] = opacity;
7799
+ v[i++] = bx;
7800
+ v[i++] = by;
7801
+ v[i++] = u1;
7802
+ v[i++] = v0;
7803
+ v[i++] = opacity;
7804
+ v[i++] = cx;
7805
+ v[i++] = cy;
7806
+ v[i++] = u1;
7807
+ v[i++] = v1;
7808
+ v[i++] = opacity;
7809
+ v[i++] = dx;
7810
+ v[i++] = dy;
7811
+ v[i++] = u0;
7812
+ v[i++] = v1;
7813
+ v[i++] = opacity;
7814
+ this.nQuads += 1;
7815
+ }
7816
+ /** Upload the staged geometry into the next set of buffers and bind its VAO.
7817
+ * Returns the index count for the caller's `drawElements`. */
7818
+ uploadAndBind() {
7819
+ const gl = this.gl;
7820
+ const tier = TIERS.findIndex((t) => this.nQuads <= t.quads);
7821
+ const set = tier < 0 ? this.nextLargeSlot() : this.nextRingSlot(tier);
7822
+ gl.bindVertexArray(set.vao);
7823
+ gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
7824
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nQuads * 4 * FLOATS_PER_VERTEX2);
7825
+ return this.nQuads * 6;
7826
+ }
7827
+ reset() {
7828
+ this.nQuads = 0;
7829
+ }
7830
+ dispose() {
7831
+ for (const set of [...this.rings.flat(), ...this.largeRing]) {
7832
+ if (set) this.deleteSet(set);
7833
+ }
7834
+ for (const ring of this.rings) ring.fill(void 0);
7835
+ this.largeRing.fill(void 0);
7836
+ }
7837
+ /** Grow the CPU arrays so one more quad fits. */
7838
+ reserve() {
7839
+ const need = (this.nQuads + 1) * 4 * FLOATS_PER_VERTEX2;
7840
+ if (need <= this.verts.length) return;
7841
+ const grown = new Float32Array(doubledTo2(this.verts.length, need));
7842
+ grown.set(this.verts);
7843
+ this.verts = grown;
7844
+ }
7845
+ nextRingSlot(tier) {
7846
+ const ring = this.rings[tier];
7847
+ const slot = this.nextInRing[tier];
7848
+ this.nextInRing[tier] = (slot + 1) % ring.length;
7849
+ const existing = ring[slot];
7850
+ if (existing) return existing;
7851
+ const set = this.createSet(TIERS[tier].quads);
7852
+ ring[slot] = set;
7853
+ return set;
7854
+ }
7855
+ nextLargeSlot() {
7856
+ const gl = this.gl;
7857
+ const slot = this.nextLarge;
7858
+ this.nextLarge = (slot + 1) % IMAGE_LARGE_RING_SIZE;
7859
+ let set = this.largeRing[slot];
7860
+ if (!set) {
7861
+ set = this.createSet(doubledTo2(TIERS[TIERS.length - 1].quads, this.nQuads));
7862
+ this.largeRing[slot] = set;
7863
+ return set;
7864
+ }
7865
+ if (this.nQuads > set.quadCapacity) {
7866
+ set.quadCapacity = doubledTo2(set.quadCapacity, this.nQuads);
7867
+ gl.bindVertexArray(set.vao);
7868
+ gl.bindBuffer(gl.ARRAY_BUFFER, set.vbo);
7869
+ gl.bufferData(
7870
+ gl.ARRAY_BUFFER,
7871
+ set.quadCapacity * 4 * FLOATS_PER_VERTEX2 * 4,
7872
+ gl.DYNAMIC_DRAW
7873
+ );
7874
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, set.ibo);
7875
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices(set.quadCapacity), gl.STATIC_DRAW);
7876
+ gl.bindVertexArray(null);
7877
+ }
7878
+ return set;
7879
+ }
7880
+ createSet(quads) {
7881
+ const gl = this.gl;
7882
+ const vao = gl.createVertexArray();
7883
+ const vbo = gl.createBuffer();
7884
+ const ibo = gl.createBuffer();
7885
+ if (!vao || !vbo || !ibo) throw new Error("ImageBatch: failed to create GL objects");
7886
+ const stride = FLOATS_PER_VERTEX2 * 4;
7887
+ gl.bindVertexArray(vao);
7888
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
7889
+ gl.bufferData(gl.ARRAY_BUFFER, quads * 4 * stride, gl.DYNAMIC_DRAW);
7890
+ gl.enableVertexAttribArray(this.aPos);
7891
+ gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, stride, 0);
7892
+ gl.enableVertexAttribArray(this.aUv);
7893
+ gl.vertexAttribPointer(this.aUv, 2, gl.FLOAT, false, stride, 8);
7894
+ gl.enableVertexAttribArray(this.aOpacity);
7895
+ gl.vertexAttribPointer(this.aOpacity, 1, gl.FLOAT, false, stride, 16);
7896
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
7897
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, quadIndices(quads), gl.STATIC_DRAW);
7898
+ gl.bindVertexArray(null);
7899
+ return { vao, vbo, ibo, quadCapacity: quads };
7900
+ }
7901
+ deleteSet(set) {
7902
+ const gl = this.gl;
7903
+ gl.deleteBuffer(set.vbo);
7904
+ gl.deleteBuffer(set.ibo);
7905
+ gl.deleteVertexArray(set.vao);
7906
+ }
7907
+ };
7648
7908
  var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
7649
7909
  var FRAME_PROJ = /* @__PURE__ */ new WeakMap();
7650
7910
  function uploadedFor(ctx, prog) {
@@ -7738,13 +7998,14 @@ function dispatch(ctx, cmd) {
7738
7998
  case "path":
7739
7999
  return drawPath(ctx, cmd);
7740
8000
  case "text":
7741
- flushSolids(ctx);
8001
+ flushBatches(ctx);
7742
8002
  return drawText(ctx, cmd);
7743
8003
  case "image":
7744
- flushSolids(ctx);
7745
8004
  return drawImage(ctx, cmd);
8005
+ case "sprites":
8006
+ return drawSprites(ctx, cmd);
7746
8007
  case "shader":
7747
- flushSolids(ctx);
8008
+ flushBatches(ctx);
7748
8009
  return drawShader(ctx, cmd);
7749
8010
  }
7750
8011
  }
@@ -7921,6 +8182,7 @@ function canBatchMesh(mesh) {
7921
8182
  return !mesh.requiresStencil && mesh.vertices.length >> 1 <= MAX_BATCHED_MESH_VERTICES;
7922
8183
  }
7923
8184
  function stageSolid(ctx, vertices) {
8185
+ flushImages(ctx);
7924
8186
  if (ctx.solidState !== void 0 && !stagedStateIsLive(ctx, ctx.solidState)) flushSolids(ctx);
7925
8187
  if (ctx.solidBatch.wouldOverflow(vertices)) flushSolids(ctx);
7926
8188
  if (ctx.solidState === void 0) {
@@ -7964,7 +8226,7 @@ function tryStageSolid(ctx, mesh, paint) {
7964
8226
  pushMesh(ctx, mesh, paint);
7965
8227
  return true;
7966
8228
  }
7967
- flushSolids(ctx);
8229
+ flushBatches(ctx);
7968
8230
  return false;
7969
8231
  }
7970
8232
  function flushSolids(ctx) {
@@ -7985,6 +8247,56 @@ function flushSolids(ctx) {
7985
8247
  batch.reset();
7986
8248
  ctx.solidState = void 0;
7987
8249
  }
8250
+ function stagedImageStateIsLive(ctx, staged, image, sampling) {
8251
+ if (staged.image !== image) return false;
8252
+ if (staged.sampling !== sampling) return false;
8253
+ if (staged.clipDepth !== ctx.clipDepth) return false;
8254
+ const colorMatrix = ctx.state.colorMatrix;
8255
+ return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
8256
+ }
8257
+ function stageImage(ctx, image, sampling) {
8258
+ flushSolids(ctx);
8259
+ if (ctx.imageState !== void 0 && !stagedImageStateIsLive(ctx, ctx.imageState, image, sampling)) {
8260
+ flushImages(ctx);
8261
+ }
8262
+ if (ctx.imageBatch.wouldOverflow()) flushImages(ctx);
8263
+ if (ctx.imageState === void 0) {
8264
+ ctx.imageState = {
8265
+ image,
8266
+ sampling,
8267
+ colorMatrix: ctx.state.colorMatrix,
8268
+ clipDepth: ctx.clipDepth
8269
+ };
8270
+ }
8271
+ }
8272
+ function flushImages(ctx) {
8273
+ const batch = ctx.imageBatch;
8274
+ const staged = ctx.imageState;
8275
+ if (batch.length === 0 || staged === void 0) return;
8276
+ const gl = ctx.gl;
8277
+ const prog = ctx.imageFillVOpacity;
8278
+ gl.useProgram(prog.handle);
8279
+ const indexCount = batch.uploadAndBind();
8280
+ setProjAndModel(ctx, prog, BATCH_MODEL);
8281
+ setAlphaUniform(ctx, prog, 1);
8282
+ setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
8283
+ ctx.imageCache.bind(staged.image, 0);
8284
+ gl.texParameteri(
8285
+ gl.TEXTURE_2D,
8286
+ gl.TEXTURE_MAG_FILTER,
8287
+ staged.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
8288
+ );
8289
+ gl.uniform1i(prog.uniform("u_sampler"), 0);
8290
+ applyClipTest(ctx, staged.clipDepth);
8291
+ gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
8292
+ gl.bindVertexArray(null);
8293
+ batch.reset();
8294
+ ctx.imageState = void 0;
8295
+ }
8296
+ function flushBatches(ctx) {
8297
+ flushSolids(ctx);
8298
+ flushImages(ctx);
8299
+ }
7988
8300
  function expandAnchorColors(perAnchor, handle) {
7989
8301
  const aA = handle.anchorA;
7990
8302
  const aB = handle.anchorB;
@@ -8188,7 +8500,7 @@ function rasterizePathToStencil(ctx, path) {
8188
8500
  gl.bindVertexArray(null);
8189
8501
  }
8190
8502
  function pushClip(ctx, path, newDepth) {
8191
- flushSolids(ctx);
8503
+ flushBatches(ctx);
8192
8504
  const gl = ctx.gl;
8193
8505
  const ancestors = ancestorMask(newDepth - 1);
8194
8506
  const newBit = 1 << newDepth;
@@ -8203,7 +8515,7 @@ function pushClip(ctx, path, newDepth) {
8203
8515
  gl.stencilMask(255);
8204
8516
  }
8205
8517
  function popClip(ctx, path, oldDepth) {
8206
- flushSolids(ctx);
8518
+ flushBatches(ctx);
8207
8519
  const gl = ctx.gl;
8208
8520
  gl.enable(gl.STENCIL_TEST);
8209
8521
  const oldBit = 1 << oldDepth + 1;
@@ -8250,7 +8562,7 @@ function drawPathStroke(ctx, rawCmd) {
8250
8562
  const stroke = cmd.stroke;
8251
8563
  const align = stroke.align ?? "center";
8252
8564
  if (cmd.path.kind === "polygon" && align !== "center") {
8253
- flushSolids(ctx);
8565
+ flushBatches(ctx);
8254
8566
  drawPathStrokeStenciled(ctx, cmd, align);
8255
8567
  return;
8256
8568
  }
@@ -8598,45 +8910,6 @@ function drawTextGroup(ctx, group, prog, dx, dy) {
8598
8910
  gl.drawElements(gl.TRIANGLES, group.quads.length * 6, gl.UNSIGNED_INT, 0);
8599
8911
  gl.bindVertexArray(null);
8600
8912
  }
8601
- var IMAGE_QUAD_RING_SIZE = 64;
8602
- var IMAGE_QUAD_RINGS = /* @__PURE__ */ new WeakMap();
8603
- var IMAGE_QUAD_INDICES = new Uint32Array([0, 1, 2, 1, 3, 2]);
8604
- var IMAGE_QUAD_STRIDE = 16;
8605
- function imageQuadRing(gl, prog) {
8606
- const existing = IMAGE_QUAD_RINGS.get(prog);
8607
- if (existing) return existing;
8608
- const aPosLoc = prog.attribute("a_position");
8609
- const aUvLoc = prog.attribute("a_uv");
8610
- const ibo = gl.createBuffer();
8611
- if (!ibo) throw new Error("drawImage: createBuffer (IBO) returned null");
8612
- const vaos = [];
8613
- const vbos = [];
8614
- for (let slot = 0; slot < IMAGE_QUAD_RING_SIZE; slot++) {
8615
- const vao = gl.createVertexArray();
8616
- const vbo = gl.createBuffer();
8617
- if (!vao) throw new Error("drawImage: createVertexArray returned null");
8618
- if (!vbo) throw new Error("drawImage: createBuffer returned null");
8619
- gl.bindVertexArray(vao);
8620
- gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
8621
- gl.bufferData(gl.ARRAY_BUFFER, 4 * IMAGE_QUAD_STRIDE, gl.DYNAMIC_DRAW);
8622
- if (aPosLoc !== void 0) {
8623
- gl.enableVertexAttribArray(aPosLoc);
8624
- gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, IMAGE_QUAD_STRIDE, 0);
8625
- }
8626
- if (aUvLoc !== void 0) {
8627
- gl.enableVertexAttribArray(aUvLoc);
8628
- gl.vertexAttribPointer(aUvLoc, 2, gl.FLOAT, false, IMAGE_QUAD_STRIDE, 8);
8629
- }
8630
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
8631
- if (slot === 0) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, IMAGE_QUAD_INDICES, gl.STATIC_DRAW);
8632
- gl.bindVertexArray(null);
8633
- vaos.push(vao);
8634
- vbos.push(vbo);
8635
- }
8636
- const ring = { vaos, vbos, ibo, next: 0 };
8637
- IMAGE_QUAD_RINGS.set(prog, ring);
8638
- return ring;
8639
- }
8640
8913
  var TEXT_QUAD_RING_SIZE = 64;
8641
8914
  var TEXT_QUAD_RINGS = /* @__PURE__ */ new WeakMap();
8642
8915
  function grownTo(need) {
@@ -8714,22 +8987,9 @@ function disposeTextQuads(gl, prog) {
8714
8987
  gl.deleteBuffer(ring.ibo);
8715
8988
  TEXT_QUAD_RINGS.delete(prog);
8716
8989
  }
8717
- function disposeImageQuads(gl, prog) {
8718
- const ring = IMAGE_QUAD_RINGS.get(prog);
8719
- if (!ring) return;
8720
- for (const vao of ring.vaos) gl.deleteVertexArray(vao);
8721
- for (const vbo of ring.vbos) gl.deleteBuffer(vbo);
8722
- gl.deleteBuffer(ring.ibo);
8723
- IMAGE_QUAD_RINGS.delete(prog);
8724
- }
8725
- var IMAGE_QUAD_VERTICES = new Float32Array(16);
8726
8990
  function drawImage(ctx, cmd) {
8727
8991
  ctx.imageCache.upload(cmd.image, cmd.image);
8728
- const gl = ctx.gl;
8729
- const prog = ctx.imageFill;
8730
- gl.useProgram(prog.handle);
8731
- const x0 = cmd.x, y0 = cmd.y;
8732
- const x1 = cmd.x + cmd.w, y1 = cmd.y + cmd.h;
8992
+ stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
8733
8993
  const src = cmd.source;
8734
8994
  let u0 = 0, v0 = 0, u1 = 1, v1 = 1;
8735
8995
  if (src) {
@@ -8749,43 +9009,50 @@ function drawImage(ctx, cmd) {
8749
9009
  v0 = v1;
8750
9010
  v1 = t;
8751
9011
  }
8752
- const v = IMAGE_QUAD_VERTICES;
8753
- v[0] = x0;
8754
- v[1] = y0;
8755
- v[2] = u0;
8756
- v[3] = v0;
8757
- v[4] = x1;
8758
- v[5] = y0;
8759
- v[6] = u1;
8760
- v[7] = v0;
8761
- v[8] = x0;
8762
- v[9] = y1;
8763
- v[10] = u0;
8764
- v[11] = v1;
8765
- v[12] = x1;
8766
- v[13] = y1;
8767
- v[14] = u1;
8768
- v[15] = v1;
8769
- const ring = imageQuadRing(gl, prog);
8770
- const slot = ring.next;
8771
- ring.next = (slot + 1) % IMAGE_QUAD_RING_SIZE;
8772
- gl.bindVertexArray(ring.vaos[slot]);
8773
- gl.bindBuffer(gl.ARRAY_BUFFER, ring.vbos[slot]);
8774
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, v);
8775
- setProjAndModel(ctx, prog);
8776
- setColorMatrixUniforms(ctx, prog);
8777
- ctx.imageCache.bind(cmd.image, 0);
8778
- gl.texParameteri(
8779
- gl.TEXTURE_2D,
8780
- gl.TEXTURE_MAG_FILTER,
8781
- cmd.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
9012
+ ctx.imageBatch.pushQuad(
9013
+ cmd.x,
9014
+ cmd.y,
9015
+ cmd.w,
9016
+ cmd.h,
9017
+ ctx.state.transform,
9018
+ u0,
9019
+ v0,
9020
+ u1,
9021
+ v1,
9022
+ (cmd.opacity ?? 1) * ctx.state.alpha
8782
9023
  );
8783
- gl.uniform1i(prog.uniform("u_sampler"), 0);
8784
- gl.uniform1f(prog.uniform("u_opacity"), cmd.opacity ?? 1);
8785
- setAlphaUniform(ctx, prog, ctx.state.alpha);
8786
- applyClipTest(ctx);
8787
- gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, 0);
8788
- gl.bindVertexArray(null);
9024
+ }
9025
+ function drawSprites(ctx, cmd) {
9026
+ const data = cmd.sprites;
9027
+ const count = Math.floor(data.length / SPRITE_STRIDE);
9028
+ if (count === 0) return;
9029
+ ctx.imageCache.upload(cmd.image, cmd.image);
9030
+ const sampling = cmd.sampling ?? "linear";
9031
+ stageImage(ctx, cmd.image, sampling);
9032
+ const batch = ctx.imageBatch;
9033
+ const m = ctx.state.transform;
9034
+ const groupAlpha = ctx.state.alpha;
9035
+ const tw = cmd.image.width;
9036
+ const th = cmd.image.height;
9037
+ for (let s = 0, i = 0; s < count; s++, i += SPRITE_STRIDE) {
9038
+ if (batch.wouldOverflow()) {
9039
+ flushImages(ctx);
9040
+ stageImage(ctx, cmd.image, sampling);
9041
+ }
9042
+ const sx = data[i + 4], sy = data[i + 5], sw = data[i + 6], sh = data[i + 7];
9043
+ batch.pushQuad(
9044
+ data[i],
9045
+ data[i + 1],
9046
+ data[i + 2],
9047
+ data[i + 3],
9048
+ m,
9049
+ sx / tw,
9050
+ sy / th,
9051
+ (sx + sw) / tw,
9052
+ (sy + sh) / th,
9053
+ data[i + 8] * groupAlpha
9054
+ );
9055
+ }
8789
9056
  }
8790
9057
  var CUSTOM_VERT_SRC = (
8791
9058
  /* glsl */
@@ -8874,6 +9141,7 @@ var WeaselRenderer = class {
8874
9141
  textSdf;
8875
9142
  textSdfR8;
8876
9143
  imageFill;
9144
+ imageFillVOpacity;
8877
9145
  gradFill;
8878
9146
  patternFill;
8879
9147
  meshCache;
@@ -8884,6 +9152,7 @@ var WeaselRenderer = class {
8884
9152
  quadVbo = null;
8885
9153
  quadIbo = null;
8886
9154
  solidBatch;
9155
+ imageBatch;
8887
9156
  groupState = new GroupState();
8888
9157
  widthCss;
8889
9158
  heightCss;
@@ -8948,6 +9217,13 @@ var WeaselRenderer = class {
8948
9217
  this.imageFill = new ShaderProgram(this.gl, IMAGE_VERT_SRC, IMAGE_FRAG_SRC);
8949
9218
  this.imageFill.lookupUniforms(IMAGE_FILL_UNIFORMS);
8950
9219
  this.imageFill.lookupAttributes(IMAGE_FILL_ATTRIBUTES);
9220
+ this.imageFillVOpacity = new ShaderProgram(
9221
+ this.gl,
9222
+ IMAGE_VOPACITY_VERT_SRC,
9223
+ IMAGE_VOPACITY_FRAG_SRC
9224
+ );
9225
+ this.imageFillVOpacity.lookupUniforms(IMAGE_VOPACITY_UNIFORMS);
9226
+ this.imageFillVOpacity.lookupAttributes(IMAGE_VOPACITY_ATTRIBUTES);
8951
9227
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
8952
9228
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
8953
9229
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
@@ -8962,6 +9238,7 @@ var WeaselRenderer = class {
8962
9238
  this.gradRampCache = new GradientRampCache(this.gl);
8963
9239
  this.uploadQuadGeometry();
8964
9240
  this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
9241
+ this.imageBatch = new ImageBatch(this.gl, this.imageFillVOpacity);
8965
9242
  }
8966
9243
  uploadQuadGeometry() {
8967
9244
  const gl = this.gl;
@@ -9085,6 +9362,13 @@ var WeaselRenderer = class {
9085
9362
  this.imageFill = new ShaderProgram(this.gl, IMAGE_VERT_SRC, IMAGE_FRAG_SRC);
9086
9363
  this.imageFill.lookupUniforms(IMAGE_FILL_UNIFORMS);
9087
9364
  this.imageFill.lookupAttributes(IMAGE_FILL_ATTRIBUTES);
9365
+ this.imageFillVOpacity = new ShaderProgram(
9366
+ this.gl,
9367
+ IMAGE_VOPACITY_VERT_SRC,
9368
+ IMAGE_VOPACITY_FRAG_SRC
9369
+ );
9370
+ this.imageFillVOpacity.lookupUniforms(IMAGE_VOPACITY_UNIFORMS);
9371
+ this.imageFillVOpacity.lookupAttributes(IMAGE_VOPACITY_ATTRIBUTES);
9088
9372
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
9089
9373
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
9090
9374
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
@@ -9099,6 +9383,7 @@ var WeaselRenderer = class {
9099
9383
  this.gradRampCache = new GradientRampCache(this.gl);
9100
9384
  this.uploadQuadGeometry();
9101
9385
  this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
9386
+ this.imageBatch = new ImageBatch(this.gl, this.imageFillVOpacity);
9102
9387
  for (const id of this.programRegistry.keys()) {
9103
9388
  const src = getProgramSource(id);
9104
9389
  if (!src) continue;
@@ -9147,7 +9432,7 @@ var WeaselRenderer = class {
9147
9432
  }
9148
9433
  this.meshCache.freeTransient();
9149
9434
  this.meshCache.drainPendingDeletes();
9150
- for (const prog of [this.pathFill, this.pathFillVColor, this.textSdf, this.textSdfR8, this.imageFill, this.gradFill, this.patternFill]) {
9435
+ for (const prog of [this.pathFill, this.pathFillVColor, this.textSdf, this.textSdfR8, this.imageFill, this.imageFillVOpacity, this.gradFill, this.patternFill]) {
9151
9436
  gl.deleteProgram(prog.handle);
9152
9437
  }
9153
9438
  for (const prog of this.programRegistry.values()) {
@@ -9158,9 +9443,9 @@ var WeaselRenderer = class {
9158
9443
  this.gradRampCache.free();
9159
9444
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
9160
9445
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
9161
- disposeImageQuads(gl, this.imageFill);
9162
9446
  for (const prog of [this.textSdf, this.textSdfR8, this.pathFill]) disposeTextQuads(gl, prog);
9163
9447
  this.solidBatch.dispose();
9448
+ this.imageBatch.dispose();
9164
9449
  }
9165
9450
  /**
9166
9451
  * Draw one frame.
@@ -9189,6 +9474,7 @@ var WeaselRenderer = class {
9189
9474
  textSdf: this.textSdf,
9190
9475
  textSdfR8: this.textSdfR8,
9191
9476
  imageFill: this.imageFill,
9477
+ imageFillVOpacity: this.imageFillVOpacity,
9192
9478
  gradFill: this.gradFill,
9193
9479
  patternFill: this.patternFill,
9194
9480
  meshCache: this.meshCache,
@@ -9200,6 +9486,7 @@ var WeaselRenderer = class {
9200
9486
  quadVbo: this.quadVbo,
9201
9487
  quadIbo: this.quadIbo,
9202
9488
  solidBatch: this.solidBatch,
9489
+ imageBatch: this.imageBatch,
9203
9490
  state: this.groupState,
9204
9491
  widthCss: this.widthCss,
9205
9492
  heightCss: this.heightCss,
@@ -9209,7 +9496,7 @@ var WeaselRenderer = class {
9209
9496
  viewMatrix: viewMatrix2
9210
9497
  };
9211
9498
  for (const cmd of commands) dispatch(ctx, cmd);
9212
- flushSolids(ctx);
9499
+ flushBatches(ctx);
9213
9500
  this.meshCache.freeTransient();
9214
9501
  }
9215
9502
  resize(dims) {
@@ -9243,6 +9530,10 @@ var WeaselRenderer = class {
9243
9530
  return this.solidBatch;
9244
9531
  }
9245
9532
  /** @internal */
9533
+ _imageBatch() {
9534
+ return this.imageBatch;
9535
+ }
9536
+ /** @internal */
9246
9537
  _textSdf() {
9247
9538
  return this.textSdf;
9248
9539
  }
@@ -9255,6 +9546,10 @@ var WeaselRenderer = class {
9255
9546
  return this.imageFill;
9256
9547
  }
9257
9548
  /** @internal */
9549
+ _imageFillVOpacity() {
9550
+ return this.imageFillVOpacity;
9551
+ }
9552
+ /** @internal */
9258
9553
  _gradFill() {
9259
9554
  return this.gradFill;
9260
9555
  }
@@ -9850,6 +10145,116 @@ function clientToCanvas(canvas, clientX, clientY) {
9850
10145
  function clientToCanvasRect(rect, clientX, clientY) {
9851
10146
  return [clientX - rect.left, clientY - rect.top];
9852
10147
  }
10148
+ var LOST_CAPTURE_EVENT = "lostpointercapture";
10149
+ function reportsButtons(down) {
10150
+ return !!down.buttons;
10151
+ }
10152
+ function isMissedRelease(move) {
10153
+ return move.buttons === 0;
10154
+ }
10155
+ function openPointerSession(origin, down, cb, opts = {}) {
10156
+ const pointerId = down.pointerId;
10157
+ const doc = origin.ownerDocument ?? document;
10158
+ const buttonsReported = reportsButtons(down);
10159
+ let active = true;
10160
+ const teardown = () => {
10161
+ active = false;
10162
+ doc.removeEventListener("pointermove", onMove, true);
10163
+ doc.removeEventListener("pointerup", onUp, true);
10164
+ doc.removeEventListener("pointercancel", onPointerCancel, true);
10165
+ doc.removeEventListener("pointerdown", onRepress, true);
10166
+ origin.removeEventListener(LOST_CAPTURE_EVENT, onLostCapture);
10167
+ if (opts.capture !== false) {
10168
+ try {
10169
+ origin.releasePointerCapture?.(pointerId);
10170
+ } catch {
10171
+ }
10172
+ }
10173
+ };
10174
+ const mine = (e) => e.pointerId === pointerId;
10175
+ const end = (e) => {
10176
+ if (!active) return;
10177
+ teardown();
10178
+ cb.onEnd?.(e);
10179
+ };
10180
+ const abort = (reason) => {
10181
+ if (!active) return;
10182
+ teardown();
10183
+ cb.onCancel?.(reason);
10184
+ };
10185
+ function onMove(e) {
10186
+ if (!active || !mine(e)) return;
10187
+ if (buttonsReported && isMissedRelease(e)) {
10188
+ end(e);
10189
+ return;
10190
+ }
10191
+ cb.onMove?.(e);
10192
+ }
10193
+ function onUp(e) {
10194
+ if (mine(e)) end(e);
10195
+ }
10196
+ function onPointerCancel(e) {
10197
+ if (mine(e)) abort("pointercancel");
10198
+ }
10199
+ function onRepress(e) {
10200
+ if (mine(e)) abort("superseded");
10201
+ }
10202
+ function onLostCapture(e) {
10203
+ if (e.pointerId !== pointerId) return;
10204
+ if (!origin.isConnected) abort("lostcapture");
10205
+ }
10206
+ if (opts.capture !== false) {
10207
+ try {
10208
+ origin.setPointerCapture?.(pointerId);
10209
+ } catch {
10210
+ }
10211
+ }
10212
+ doc.addEventListener("pointermove", onMove, true);
10213
+ doc.addEventListener("pointerup", onUp, true);
10214
+ doc.addEventListener("pointercancel", onPointerCancel, true);
10215
+ doc.addEventListener("pointerdown", onRepress, true);
10216
+ origin.addEventListener(LOST_CAPTURE_EVENT, onLostCapture);
10217
+ return {
10218
+ pointerId,
10219
+ get active() {
10220
+ return active;
10221
+ },
10222
+ cancel: () => abort("aborted")
10223
+ };
10224
+ }
10225
+ function startThresholdDrag(e, opts) {
10226
+ const startX = e.clientX;
10227
+ const startY = e.clientY;
10228
+ const threshold = opts.threshold ?? 4;
10229
+ let activated = false;
10230
+ const maybeActivate = (ev) => {
10231
+ if (activated) return;
10232
+ const dx = ev.clientX - startX;
10233
+ const dy = ev.clientY - startY;
10234
+ if (dx * dx + dy * dy < threshold * threshold) return;
10235
+ activated = true;
10236
+ opts.onActivate?.(ev);
10237
+ };
10238
+ const session = openPointerSession(opts.origin ?? e.currentTarget, e, {
10239
+ onMove: (ev) => {
10240
+ maybeActivate(ev);
10241
+ if (activated) opts.onMove(ev);
10242
+ },
10243
+ onEnd: (ev) => {
10244
+ if (activated) opts.onCommit(ev);
10245
+ else opts.onClick?.(ev);
10246
+ },
10247
+ onCancel: () => {
10248
+ opts.onCancel?.();
10249
+ }
10250
+ });
10251
+ return {
10252
+ isDragging: () => activated,
10253
+ cancel: () => {
10254
+ session.cancel();
10255
+ }
10256
+ };
10257
+ }
9853
10258
  var BASE_PX = {
9854
10259
  handle: 8,
9855
10260
  anchor: 8,
@@ -11609,6 +12014,28 @@ var cloneAction = {
11609
12014
  },
11610
12015
  enabled: () => true
11611
12016
  };
12017
+ function createVelocityTracker() {
12018
+ let samples = [];
12019
+ return {
12020
+ record(dx, dy, t) {
12021
+ samples.push({ dx, dy, t });
12022
+ const cutoff = t - 100;
12023
+ samples = samples.filter((s) => s.t >= cutoff);
12024
+ },
12025
+ getVelocity() {
12026
+ const s = samples;
12027
+ if (s.length < 2) return { vx: 0, vy: 0 };
12028
+ const dt = s[s.length - 1].t - s[0].t;
12029
+ if (dt === 0) return { vx: 0, vy: 0 };
12030
+ const totalDx = s.slice(1).reduce((acc, p) => acc + p.dx, 0);
12031
+ const totalDy = s.slice(1).reduce((acc, p) => acc + p.dy, 0);
12032
+ return { vx: totalDx / dt, vy: totalDy / dt };
12033
+ },
12034
+ reset() {
12035
+ samples = [];
12036
+ }
12037
+ };
12038
+ }
11612
12039
  var viewportDragPanAction = {
11613
12040
  id: "viewport.dragPan",
11614
12041
  label: "Drag to pan viewport",
@@ -11626,17 +12053,37 @@ var viewportDragPanAction = {
11626
12053
  requires: ["view"],
11627
12054
  invoker: {
11628
12055
  timing: "ongoing",
11629
- start(ctx) {
12056
+ start(ctx, opts) {
11630
12057
  const view = ctx.deps.view;
11631
12058
  if (!view) return {};
11632
12059
  const startView = view.get();
11633
- const scratch = { startView, view };
12060
+ const params = resolveParams(opts?.params) ?? {};
12061
+ const inertia = params.inertia === false ? void 0 : params.inertia;
12062
+ const scratch = {
12063
+ startView,
12064
+ view,
12065
+ axis: params.axis ?? "both",
12066
+ inertia,
12067
+ // Only track when a coast could actually happen, so the common
12068
+ // no-inertia drag does no per-move work.
12069
+ tracker: inertia && view.decay ? createVelocityTracker() : void 0,
12070
+ lastScreen: { x: 0, y: 0 }
12071
+ };
11634
12072
  return {
11635
12073
  kind: "pan",
11636
12074
  onMove(moveCtx) {
11637
12075
  if (!moveCtx.drag) return;
11638
12076
  const screen = moveCtx.drag.screenDelta ?? moveCtx.drag.delta;
11639
- const { x: screenDx, y: screenDy } = screen;
12077
+ const screenDx = scratch.axis === "y" ? 0 : screen.x;
12078
+ const screenDy = scratch.axis === "x" ? 0 : screen.y;
12079
+ if (scratch.tracker) {
12080
+ scratch.tracker.record(
12081
+ screenDx - scratch.lastScreen.x,
12082
+ screenDy - scratch.lastScreen.y,
12083
+ performance.now()
12084
+ );
12085
+ scratch.lastScreen = { x: screenDx, y: screenDy };
12086
+ }
11640
12087
  const sv = scratch.startView;
11641
12088
  scratch.view.set({
11642
12089
  ...sv,
@@ -11644,7 +12091,24 @@ var viewportDragPanAction = {
11644
12091
  y: sv.y - screenDy / sv.scale.y
11645
12092
  });
11646
12093
  },
11647
- onEnd() {
12094
+ onEnd(_endCtx, reason) {
12095
+ const { tracker, inertia: inertia2, view: v } = scratch;
12096
+ if (reason === "cancel" || !tracker || !inertia2 || !v.decay) return;
12097
+ const { vx, vy } = tracker.getVelocity();
12098
+ const scale2 = scratch.startView.scale;
12099
+ v.decay({
12100
+ // Screen px/ms → world units/ms, matching the onMove conversion.
12101
+ velocity: { vx: -vx / scale2.x, vy: -vy / scale2.y },
12102
+ friction: inertia2.friction,
12103
+ minSpeed: inertia2.minSpeed,
12104
+ boundary: inertia2.boundary,
12105
+ viewBounds: inertia2.bounds,
12106
+ initialPosition: { x: v.get().x, y: v.get().y },
12107
+ onTick: (dx, dy) => {
12108
+ const cur = v.get();
12109
+ v.set({ ...cur, x: cur.x + dx, y: cur.y + dy });
12110
+ }
12111
+ });
11648
12112
  }
11649
12113
  };
11650
12114
  }
@@ -11745,6 +12209,52 @@ function makeViewportZoomAction(opts = {}) {
11745
12209
  };
11746
12210
  }
11747
12211
  makeViewportZoomAction();
12212
+ var viewportWheelPanAction = {
12213
+ id: "viewport.wheelPan",
12214
+ label: "Pan (wheel)",
12215
+ group: "viewport",
12216
+ /**
12217
+ * Plain wheel → pan; Shift+wheel → horizontal pan (axis swap).
12218
+ * Cmd+wheel is claimed by `viewportZoomAction`. The matcher's strict
12219
+ * modifier check keeps the bindings exclusive: omitting `mods` forbids
12220
+ * any modifier, and `mods: { shift: true }` forbids the others.
12221
+ */
12222
+ defaultBinding: [
12223
+ { spec: { kind: "wheel" }, opts: {} },
12224
+ { spec: { kind: "wheel", mods: { shift: true } }, opts: { params: { swapAxis: true } } }
12225
+ ],
12226
+ requires: ["view"],
12227
+ invoker: {
12228
+ timing: "immediate",
12229
+ run(deps, params) {
12230
+ const view = deps.view;
12231
+ if (!view) return;
12232
+ const axis = params?.axis ?? "both";
12233
+ const deltaX = params?.deltaX ?? 0;
12234
+ const deltaY = params?.deltaY ?? 0;
12235
+ const current = view.get();
12236
+ const swap = params?.swapAxis === true;
12237
+ const dx = swap ? (deltaX !== 0 ? deltaX : deltaY) / current.scale.x : deltaX / current.scale.x;
12238
+ const dy = swap ? 0 : deltaY / current.scale.y;
12239
+ view.set({
12240
+ ...current,
12241
+ x: current.x + (axis === "y" ? 0 : dx),
12242
+ y: current.y + (axis === "x" ? 0 : dy)
12243
+ });
12244
+ }
12245
+ },
12246
+ enabled: () => true
12247
+ };
12248
+ function makeViewportWheelPanAction(opts = {}) {
12249
+ const axis = opts.axis ?? "both";
12250
+ return {
12251
+ ...viewportWheelPanAction,
12252
+ defaultBinding: [
12253
+ { spec: { kind: "wheel" }, opts: { params: { axis } } },
12254
+ { spec: { kind: "wheel", mods: { shift: true } }, opts: { params: { swapAxis: true, axis } } }
12255
+ ]
12256
+ };
12257
+ }
11748
12258
  function bodyPredicate(fn) {
11749
12259
  return Object.assign(fn, { readsAffordance: false });
11750
12260
  }
@@ -16120,7 +16630,7 @@ function withViewDeps(base, overlay) {
16120
16630
  };
16121
16631
  }
16122
16632
  function computeMultiTouchGeometry(positions) {
16123
- const pts = [...positions.values()];
16633
+ const pts = [...positions];
16124
16634
  if (pts.length < 2) {
16125
16635
  return { centroid: { x: 0, y: 0 }, spread: 0 };
16126
16636
  }
@@ -16247,13 +16757,13 @@ function useGestureDispatcher(opts) {
16247
16757
  return result;
16248
16758
  };
16249
16759
  const heldKeys = /* @__PURE__ */ new Set();
16250
- const activePointers = /* @__PURE__ */ new Set();
16251
- const pointerPositions = /* @__PURE__ */ new Map();
16760
+ const held = /* @__PURE__ */ new Map();
16761
+ const heldGeometry = () => computeMultiTouchGeometry(
16762
+ [...held.values()].map((p) => p.pos)
16763
+ );
16252
16764
  let multiTouchStart = null;
16253
16765
  const TAP_THRESHOLD_PX = 8;
16254
16766
  const DRAG_THRESHOLD_PX = 4;
16255
- const bufferedDown = /* @__PURE__ */ new Map();
16256
- const lastPointerDown = /* @__PURE__ */ new Map();
16257
16767
  const LONG_PRESS_MS = 500;
16258
16768
  const longPressTimers = /* @__PURE__ */ new Map();
16259
16769
  const cancelLongPress = (pointerId) => {
@@ -16268,7 +16778,7 @@ function useGestureDispatcher(opts) {
16268
16778
  longPressTimers.clear();
16269
16779
  };
16270
16780
  const fireLongPress = (pointerId) => {
16271
- const down = lastPointerDown.get(pointerId);
16781
+ const down = held.get(pointerId)?.down;
16272
16782
  if (!down) return;
16273
16783
  const shared = {
16274
16784
  target: down.target,
@@ -16390,13 +16900,8 @@ function useGestureDispatcher(opts) {
16390
16900
  };
16391
16901
  const onPointerDown = (e) => {
16392
16902
  if ((e.button ?? 0) !== 0) return;
16903
+ if (!canvas) return;
16393
16904
  routeDown(e.pointerId, e.clientX, e.clientY);
16394
- activePointers.add(e.pointerId);
16395
- pointerPositions.set(e.pointerId, { x: e.clientX, y: e.clientY });
16396
- try {
16397
- canvas?.setPointerCapture?.(e.pointerId);
16398
- } catch {
16399
- }
16400
16905
  const worldPoint = { x: e.clientX, y: e.clientY };
16401
16906
  const affordance = target().affordanceAt?.(worldPoint) ?? void 0;
16402
16907
  const body = target().classifyTarget?.(worldPoint);
@@ -16420,23 +16925,35 @@ function useGestureDispatcher(opts) {
16420
16925
  ...bodyTarget !== void 0 ? { bodyTarget } : {},
16421
16926
  ...bodyKind !== void 0 ? { bodyKind } : {}
16422
16927
  };
16423
- bufferedDown.set(e.pointerId, { ev, clientX: e.clientX, clientY: e.clientY });
16424
- dispatch2({ ...ev, stage: "press" });
16425
- lastPointerDown.set(e.pointerId, {
16426
- target: e.target,
16427
- clientX: e.clientX,
16428
- clientY: e.clientY,
16429
- worldX: w.x,
16430
- worldY: w.y,
16431
- affordance,
16432
- bodyTarget,
16433
- bodyKind,
16434
- altKey: e.altKey,
16435
- ctrlKey: e.ctrlKey,
16436
- metaKey: e.metaKey,
16437
- shiftKey: e.shiftKey
16928
+ held.set(e.pointerId, {
16929
+ session: openPointerSession(canvas, e, {
16930
+ onMove: onPointerMove,
16931
+ onEnd: onPointerUp,
16932
+ onCancel: () => {
16933
+ onPointerCancel(e.pointerId);
16934
+ }
16935
+ }),
16936
+ pos: { x: e.clientX, y: e.clientY },
16937
+ // Deferred until the first past-threshold pointermove: a bare
16938
+ // press-and-release should fire `click`, not start a drag action.
16939
+ buffered: { ev, clientX: e.clientX, clientY: e.clientY },
16940
+ down: {
16941
+ target: e.target,
16942
+ clientX: e.clientX,
16943
+ clientY: e.clientY,
16944
+ worldX: w.x,
16945
+ worldY: w.y,
16946
+ affordance,
16947
+ bodyTarget,
16948
+ bodyKind,
16949
+ altKey: e.altKey,
16950
+ ctrlKey: e.ctrlKey,
16951
+ metaKey: e.metaKey,
16952
+ shiftKey: e.shiftKey
16953
+ }
16438
16954
  });
16439
- if ((e.pointerType === "touch" || e.pointerType === "pen") && activePointers.size === 1) {
16955
+ dispatch2({ ...ev, stage: "press" });
16956
+ if ((e.pointerType === "touch" || e.pointerType === "pen") && held.size === 1) {
16440
16957
  cancelLongPress(e.pointerId);
16441
16958
  longPressTimers.set(
16442
16959
  e.pointerId,
@@ -16447,18 +16964,18 @@ function useGestureDispatcher(opts) {
16447
16964
  }, LONG_PRESS_MS)
16448
16965
  );
16449
16966
  }
16450
- if (activePointers.size >= 2) {
16967
+ if (held.size >= 2) {
16451
16968
  cancelAllLongPress();
16452
- for (const id of activePointers) bufferedDown.delete(id);
16453
- const { centroid, spread } = computeMultiTouchGeometry(pointerPositions);
16969
+ for (const p of held.values()) p.buffered = null;
16970
+ const { centroid, spread } = heldGeometry();
16454
16971
  if (!multiTouchStart) {
16455
- multiTouchStart = { fingers: activePointers.size, centroid: { ...centroid } };
16972
+ multiTouchStart = { fingers: held.size, centroid: { ...centroid } };
16456
16973
  } else {
16457
- multiTouchStart.fingers = activePointers.size;
16974
+ multiTouchStart.fingers = held.size;
16458
16975
  }
16459
16976
  const mt = {
16460
16977
  kind: "multitouch",
16461
- fingers: activePointers.size,
16978
+ fingers: held.size,
16462
16979
  altKey: e.altKey,
16463
16980
  ctrlKey: e.ctrlKey,
16464
16981
  metaKey: e.metaKey,
@@ -16471,25 +16988,24 @@ function useGestureDispatcher(opts) {
16471
16988
  };
16472
16989
  const onPointerMove = (e) => {
16473
16990
  routeAt(e.pointerId, e.clientX, e.clientY);
16474
- if (activePointers.has(e.pointerId)) {
16475
- pointerPositions.set(e.pointerId, { x: e.clientX, y: e.clientY });
16476
- }
16477
- const buffered = bufferedDown.get(e.pointerId);
16478
- if (buffered) {
16991
+ const rec = held.get(e.pointerId);
16992
+ if (rec) rec.pos = { x: e.clientX, y: e.clientY };
16993
+ const buffered = rec?.buffered;
16994
+ if (rec && buffered) {
16479
16995
  const dx = e.clientX - buffered.clientX;
16480
16996
  const dy = e.clientY - buffered.clientY;
16481
16997
  if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
16482
- bufferedDown.delete(e.pointerId);
16998
+ rec.buffered = null;
16483
16999
  cancelLongPress(e.pointerId);
16484
17000
  dispatch2(buffered.ev);
16485
17001
  }
16486
17002
  }
16487
- const mtGestureId = `multitouch-${activePointers.size}`;
16488
- if (activePointers.size >= 2 && dispatcherNow().inFlight().has(mtGestureId)) {
16489
- const { centroid, spread } = computeMultiTouchGeometry(pointerPositions);
17003
+ const mtGestureId = `multitouch-${held.size}`;
17004
+ if (held.size >= 2 && dispatcherNow().inFlight().has(mtGestureId)) {
17005
+ const { centroid, spread } = heldGeometry();
16490
17006
  const mtEv = {
16491
17007
  kind: "multitouch",
16492
- fingers: activePointers.size,
17008
+ fingers: held.size,
16493
17009
  altKey: e.altKey,
16494
17010
  ctrlKey: e.ctrlKey,
16495
17011
  metaKey: e.metaKey,
@@ -16561,7 +17077,7 @@ function useGestureDispatcher(opts) {
16561
17077
  }
16562
17078
  function resolveHoverCursor() {
16563
17079
  if (!canvas || !lastHover) return;
16564
- if (activePointers.size > 0 || dispatcherNow().inFlight().size > 0) {
17080
+ if (held.size > 0 || dispatcherNow().inFlight().size > 0) {
16565
17081
  applyHoverCursor(dispatcherNow().inFlightCursor());
16566
17082
  return;
16567
17083
  }
@@ -16609,6 +17125,10 @@ function useGestureDispatcher(opts) {
16609
17125
  if (!disposed) refreshHoverCursor();
16610
17126
  }, 0);
16611
17127
  };
17128
+ const onHoverMove = (e) => {
17129
+ if (held.has(e.pointerId)) return;
17130
+ onPointerMove(e);
17131
+ };
16612
17132
  const onHoverPointerLeave = () => {
16613
17133
  lastHover = null;
16614
17134
  paintedCursorRef.current?.()?.clearPointer();
@@ -16617,20 +17137,21 @@ function useGestureDispatcher(opts) {
16617
17137
  const onPointerUp = (e) => {
16618
17138
  routeAt(e.pointerId, e.clientX, e.clientY);
16619
17139
  cancelLongPress(e.pointerId);
16620
- const prevSize = activePointers.size;
16621
- activePointers.delete(e.pointerId);
16622
- pointerPositions.delete(e.pointerId);
16623
- const staysUnderThreshold = bufferedDown.delete(e.pointerId);
16624
- if (prevSize >= 2 && activePointers.size < 2) {
17140
+ const prevSize = held.size;
17141
+ const rec = held.get(e.pointerId);
17142
+ held.delete(e.pointerId);
17143
+ const staysUnderThreshold = rec?.buffered != null;
17144
+ if (prevSize >= 2 && held.size < 2) {
16625
17145
  const mtGestureId = `multitouch-${prevSize}`;
16626
17146
  if (dispatcherNow().inFlight().has(mtGestureId)) {
16627
17147
  dispatcherNow().cancelAll("commit");
16628
17148
  }
16629
17149
  const start = multiTouchStart;
16630
17150
  if (start) {
16631
- const finalPositions = new Map(pointerPositions);
16632
- finalPositions.set(e.pointerId, { x: e.clientX, y: e.clientY });
16633
- const { centroid } = computeMultiTouchGeometry(finalPositions);
17151
+ const { centroid } = computeMultiTouchGeometry([
17152
+ ...[...held.values()].map((p) => p.pos),
17153
+ { x: e.clientX, y: e.clientY }
17154
+ ]);
16634
17155
  const dx = centroid.x - start.centroid.x;
16635
17156
  const dy = centroid.y - start.centroid.y;
16636
17157
  if (Math.hypot(dx, dy) <= TAP_THRESHOLD_PX) {
@@ -16647,8 +17168,7 @@ function useGestureDispatcher(opts) {
16647
17168
  multiTouchStart = null;
16648
17169
  }
16649
17170
  }
16650
- const downForClick = lastPointerDown.get(e.pointerId);
16651
- const movedDuringDrag = downForClick ? downForClick.clientX !== e.clientX || downForClick.clientY !== e.clientY : true;
17171
+ const movedDuringDrag = rec ? rec.down.clientX !== e.clientX || rec.down.clientY !== e.clientY : true;
16652
17172
  const hadDragInFlight = dispatcherNow().inFlight().has(pointerGestureId(e.pointerId)) && movedDuringDrag;
16653
17173
  const wUp = toWorld2(e.clientX, e.clientY);
16654
17174
  const ev = {
@@ -16664,8 +17184,7 @@ function useGestureDispatcher(opts) {
16664
17184
  shiftKey: e.shiftKey
16665
17185
  };
16666
17186
  dispatch2(ev);
16667
- const down = lastPointerDown.get(e.pointerId);
16668
- lastPointerDown.delete(e.pointerId);
17187
+ const down = rec?.down;
16669
17188
  if (!hadDragInFlight && staysUnderThreshold && down) {
16670
17189
  const wClick = toWorld2(e.clientX, e.clientY);
16671
17190
  const clickEv = {
@@ -16722,24 +17241,24 @@ function useGestureDispatcher(opts) {
16722
17241
  if (!disposed) refreshHoverCursor();
16723
17242
  }, 0);
16724
17243
  };
16725
- const onPointerCancel = (e) => {
16726
- routeAt(e.pointerId, e.clientX, e.clientY);
16727
- cancelLongPress(e.pointerId);
16728
- activePointers.delete(e.pointerId);
16729
- pointerPositions.delete(e.pointerId);
16730
- lastPointerDown.delete(e.pointerId);
16731
- bufferedDown.delete(e.pointerId);
17244
+ const onPointerCancel = (pointerId) => {
17245
+ if (disposed) return;
17246
+ const rec = held.get(pointerId);
17247
+ if (!rec) return;
17248
+ held.delete(pointerId);
17249
+ routeAt(pointerId, rec.pos.x, rec.pos.y);
17250
+ cancelLongPress(pointerId);
16732
17251
  multiTouchStart = null;
16733
17252
  const ev = {
16734
17253
  kind: "pointercancel",
16735
- pointerId: e.pointerId,
16736
- altKey: e.altKey,
16737
- ctrlKey: e.ctrlKey,
16738
- metaKey: e.metaKey,
16739
- shiftKey: e.shiftKey
17254
+ pointerId,
17255
+ altKey: rec.down.altKey,
17256
+ ctrlKey: rec.down.ctrlKey,
17257
+ metaKey: rec.down.metaKey,
17258
+ shiftKey: rec.down.shiftKey
16740
17259
  };
16741
17260
  dispatch2(ev);
16742
- routeRelease(e.pointerId);
17261
+ routeRelease(pointerId);
16743
17262
  };
16744
17263
  const onContextMenu = (e) => {
16745
17264
  e.preventDefault();
@@ -16827,9 +17346,7 @@ function useGestureDispatcher(opts) {
16827
17346
  canvas?.addEventListener("pointerleave", onHoverPointerLeave);
16828
17347
  canvas?.addEventListener("wheel", onWheel, { passive: false });
16829
17348
  canvas?.addEventListener("pointerdown", onPointerDown);
16830
- canvas?.addEventListener("pointermove", onPointerMove);
16831
- canvas?.addEventListener("pointerup", onPointerUp);
16832
- canvas?.addEventListener("pointercancel", onPointerCancel);
17349
+ canvas?.addEventListener("pointermove", onHoverMove);
16833
17350
  canvas?.addEventListener("contextmenu", onContextMenu);
16834
17351
  canvas?.addEventListener("dragenter", onDragOver);
16835
17352
  canvas?.addEventListener("dragover", onDragOver);
@@ -16847,9 +17364,7 @@ function useGestureDispatcher(opts) {
16847
17364
  canvas?.removeEventListener("pointerleave", onHoverPointerLeave);
16848
17365
  canvas?.removeEventListener("wheel", onWheel);
16849
17366
  canvas?.removeEventListener("pointerdown", onPointerDown);
16850
- canvas?.removeEventListener("pointermove", onPointerMove);
16851
- canvas?.removeEventListener("pointerup", onPointerUp);
16852
- canvas?.removeEventListener("pointercancel", onPointerCancel);
17367
+ canvas?.removeEventListener("pointermove", onHoverMove);
16853
17368
  canvas?.removeEventListener("contextmenu", onContextMenu);
16854
17369
  canvas?.removeEventListener("dragenter", onDragOver);
16855
17370
  canvas?.removeEventListener("dragover", onDragOver);
@@ -16860,10 +17375,138 @@ function useGestureDispatcher(opts) {
16860
17375
  clearHoverCursor();
16861
17376
  cancelAllLongPress();
16862
17377
  disposed = true;
17378
+ for (const p of held.values()) p.session.cancel();
17379
+ held.clear();
16863
17380
  for (const d of allDispatchers()) d.cancelAll("cancel");
16864
17381
  };
16865
17382
  }, [enabled, keyboard, canvasRef]);
16866
17383
  }
17384
+ var SPRING_DAMPING = 0.5;
17385
+ function useDecayLoop() {
17386
+ const stateRef = useRef(null);
17387
+ const frameLoop = useVisibleRaf(
17388
+ (now) => {
17389
+ tick(now);
17390
+ },
17391
+ {
17392
+ // Coasting does not continue while suspended: dropping `lastTime` makes
17393
+ // the resuming frame seed a fresh interval instead of applying an hour of
17394
+ // friction at once, which would end the decay before it was seen.
17395
+ onResume: useCallback(() => {
17396
+ if (stateRef.current) stateRef.current.lastTime = null;
17397
+ }, [])
17398
+ }
17399
+ );
17400
+ const cancel = useCallback(() => {
17401
+ frameLoop.cancel();
17402
+ stateRef.current = null;
17403
+ }, [frameLoop]);
17404
+ const tick = useCallback((now) => {
17405
+ const s = stateRef.current;
17406
+ if (!s) return;
17407
+ if (s.lastTime === null) {
17408
+ s.lastTime = now;
17409
+ frameLoop.request();
17410
+ return;
17411
+ }
17412
+ const dt = Math.min(now - s.lastTime, 64);
17413
+ s.lastTime = now;
17414
+ const f = Math.pow(s.friction, dt / 16.67);
17415
+ s.vx *= f;
17416
+ s.vy *= f;
17417
+ const speed = Math.sqrt(s.vx * s.vx + s.vy * s.vy);
17418
+ if (speed < s.minSpeed) {
17419
+ stateRef.current = null;
17420
+ frameLoop.cancel();
17421
+ s.onEnd?.();
17422
+ return;
17423
+ }
17424
+ let dx = s.vx * dt;
17425
+ let dy = s.vy * dt;
17426
+ if (s.viewBounds && s.boundary) {
17427
+ const { minX, maxX, minY, maxY } = s.viewBounds;
17428
+ const newX = s.posX + dx;
17429
+ const newY = s.posY + dy;
17430
+ if (s.boundary === "stop") {
17431
+ if (minX !== void 0 && newX < minX) {
17432
+ dx = minX - s.posX;
17433
+ s.vx = 0;
17434
+ } else if (maxX !== void 0 && newX > maxX) {
17435
+ dx = maxX - s.posX;
17436
+ s.vx = 0;
17437
+ }
17438
+ if (minY !== void 0 && newY < minY) {
17439
+ dy = minY - s.posY;
17440
+ s.vy = 0;
17441
+ } else if (maxY !== void 0 && newY > maxY) {
17442
+ dy = maxY - s.posY;
17443
+ s.vy = 0;
17444
+ }
17445
+ } else if (s.boundary === "spring") {
17446
+ if (minX !== void 0 && newX < minX) {
17447
+ dx = minX - s.posX;
17448
+ s.vx = Math.abs(s.vx) * SPRING_DAMPING;
17449
+ } else if (maxX !== void 0 && newX > maxX) {
17450
+ dx = maxX - s.posX;
17451
+ s.vx = -Math.abs(s.vx) * SPRING_DAMPING;
17452
+ }
17453
+ if (minY !== void 0 && newY < minY) {
17454
+ dy = minY - s.posY;
17455
+ s.vy = Math.abs(s.vy) * SPRING_DAMPING;
17456
+ } else if (maxY !== void 0 && newY > maxY) {
17457
+ dy = maxY - s.posY;
17458
+ s.vy = -Math.abs(s.vy) * SPRING_DAMPING;
17459
+ }
17460
+ } else {
17461
+ if (minX !== void 0 && newX < minX) {
17462
+ dx = minX - s.posX;
17463
+ s.vx = Math.abs(s.vx);
17464
+ } else if (maxX !== void 0 && newX > maxX) {
17465
+ dx = maxX - s.posX;
17466
+ s.vx = -Math.abs(s.vx);
17467
+ }
17468
+ if (minY !== void 0 && newY < minY) {
17469
+ dy = minY - s.posY;
17470
+ s.vy = Math.abs(s.vy);
17471
+ } else if (maxY !== void 0 && newY > maxY) {
17472
+ dy = maxY - s.posY;
17473
+ s.vy = -Math.abs(s.vy);
17474
+ }
17475
+ }
17476
+ s.posX += dx;
17477
+ s.posY += dy;
17478
+ }
17479
+ s.onTick(dx, dy);
17480
+ frameLoop.request();
17481
+ }, [frameLoop]);
17482
+ const start = useCallback((config) => {
17483
+ cancel();
17484
+ const { velocity, friction = 0.92, minSpeed = 0.01, viewBounds, boundary, initialPosition, onTick, onEnd } = config;
17485
+ const speed = Math.sqrt(velocity.vx ** 2 + velocity.vy ** 2);
17486
+ if (speed < minSpeed) {
17487
+ onEnd?.();
17488
+ return;
17489
+ }
17490
+ stateRef.current = {
17491
+ vx: velocity.vx,
17492
+ vy: velocity.vy,
17493
+ friction,
17494
+ minSpeed,
17495
+ lastTime: null,
17496
+ posX: initialPosition?.x ?? 0,
17497
+ posY: initialPosition?.y ?? 0,
17498
+ viewBounds,
17499
+ boundary,
17500
+ onTick,
17501
+ onEnd
17502
+ };
17503
+ frameLoop.request();
17504
+ }, [cancel, frameLoop]);
17505
+ useEffect(() => () => {
17506
+ cancel();
17507
+ }, [cancel]);
17508
+ return { start, cancel };
17509
+ }
16867
17510
  function dist(a, b) {
16868
17511
  return Math.hypot(b.x - a.x, b.y - a.y);
16869
17512
  }
@@ -20485,7 +21128,9 @@ function useRotateTool(adapter, options) {
20485
21128
  [overlay]
20486
21129
  );
20487
21130
  }
20488
- function useHandTool(_opts = {}) {
21131
+ function useHandTool(opts = {}) {
21132
+ const optsRef = useRef(opts);
21133
+ optsRef.current = opts;
20489
21134
  return useMemo(
20490
21135
  () => Object.assign(defineViewportTool({
20491
21136
  id: "hand",
@@ -20503,7 +21148,16 @@ function useHandTool(_opts = {}) {
20503
21148
  // viewportDragPanAction, which also owns the grabbing cursor
20504
21149
  // (`Action.activeCursor`) the tool used to declare as an `engaged`
20505
21150
  // phase entry.
20506
- bindings: [{ spec: { kind: "drag" }, actionId: "viewport.dragPan" }]
21151
+ bindings: [{
21152
+ spec: { kind: "drag" },
21153
+ actionId: "viewport.dragPan",
21154
+ opts: {
21155
+ params: () => ({
21156
+ axis: optsRef.current.axis ?? "both",
21157
+ inertia: optsRef.current.inertia
21158
+ })
21159
+ }
21160
+ }]
20507
21161
  }),
20508
21162
  []
20509
21163
  );
@@ -21764,6 +22418,20 @@ function projectBounds(b, view) {
21764
22418
  const [sx, sy] = worldToScreen(b.x, b.y, t);
21765
22419
  return { ...b, x: sx, y: sy, width: b.width * view.scale.x, height: b.height * view.scale.y };
21766
22420
  }
22421
+ function leavesOf(id, getChildren, isContainer) {
22422
+ if (!isContainer(id)) return [id];
22423
+ const out = [];
22424
+ const visit = (nid) => {
22425
+ const kids = getChildren(nid);
22426
+ if (kids.length === 0) {
22427
+ if (!isContainer(nid)) out.push(nid);
22428
+ return;
22429
+ }
22430
+ for (const k of kids) visit(k);
22431
+ };
22432
+ visit(id);
22433
+ return out;
22434
+ }
21767
22435
  function makeContainerAwareBoundsResolver(getPose, getBounds, getChildren, isContainer) {
21768
22436
  if (getChildren === void 0 || isContainer === void 0) {
21769
22437
  return (id) => {
@@ -21771,26 +22439,12 @@ function makeContainerAwareBoundsResolver(getPose, getBounds, getChildren, isCon
21771
22439
  return p === null ? null : getBounds(p);
21772
22440
  };
21773
22441
  }
21774
- const leavesOf = (id) => {
21775
- if (!isContainer(id)) return [id];
21776
- const out = [];
21777
- const visit = (nid) => {
21778
- const kids = getChildren(nid);
21779
- if (kids.length === 0) {
21780
- if (!isContainer(nid)) out.push(nid);
21781
- return;
21782
- }
21783
- for (const k of kids) visit(k);
21784
- };
21785
- visit(id);
21786
- return out;
21787
- };
21788
22442
  return (id) => {
21789
22443
  if (!isContainer(id)) {
21790
22444
  const p = getPose(id);
21791
22445
  return p === null ? null : getBounds(p);
21792
22446
  }
21793
- const leaves = leavesOf(id);
22447
+ const leaves = leavesOf(id, getChildren, isContainer);
21794
22448
  if (leaves.length === 0) return null;
21795
22449
  const leafBounds = [];
21796
22450
  for (const leafId of leaves) {
@@ -22923,7 +23577,7 @@ function CanvasInner(props, ref) {
22923
23577
  return {
22924
23578
  worldX,
22925
23579
  worldY,
22926
- modifiers: m ? { alt: m.alt, shift: m.shift, meta: m.meta, ctrl: m.ctrl, space: false } : { alt: false, shift: false, meta: false, ctrl: false, space: false },
23580
+ modifiers: m ? { alt: m.alt, shift: m.shift, meta: m.meta, ctrl: m.ctrl } : { alt: false, shift: false, meta: false, ctrl: false },
22927
23581
  selection: effectiveSelectionRefForCtx.current,
22928
23582
  adapter: effectiveAdapterRefForCtx.current,
22929
23583
  applyOps: (ops, label) => {
@@ -26567,10 +27221,10 @@ function boundsOf(coords) {
26567
27221
  }
26568
27222
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
26569
27223
  }
26570
- function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize, animation) {
26571
- const wiring = useRef({ currentViewRef, onViewChange, recenter, hostSize, animation });
26572
- wiring.current = { currentViewRef, onViewChange, recenter, hostSize, animation };
26573
- const shape = `${recenter ? "r" : ""}${hostSize ? "h" : ""}${animation ? "a" : ""}`;
27224
+ function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize, animation, decay) {
27225
+ const wiring = useRef({ currentViewRef, onViewChange, recenter, hostSize, animation, decay });
27226
+ wiring.current = { currentViewRef, onViewChange, recenter, hostSize, animation, decay };
27227
+ const shape = `${recenter ? "r" : ""}${hostSize ? "h" : ""}${animation ? "a" : ""}${decay ? "d" : ""}`;
26574
27228
  const shapeRef = useRef(null);
26575
27229
  const viewApiRef = useRef(null);
26576
27230
  if (viewApiRef.current === null || shapeRef.current !== shape) {
@@ -26589,6 +27243,10 @@ function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize, anim
26589
27243
  animate: (to, opts) => wiring.current.animation.animate(to, opts),
26590
27244
  stopAnimation: () => wiring.current.animation.stop(),
26591
27245
  animationTarget: () => wiring.current.animation.target()
27246
+ } : {},
27247
+ ...decay ? {
27248
+ decay: (config) => wiring.current.decay.start(config),
27249
+ stopDecay: () => wiring.current.decay.cancel()
26592
27250
  } : {}
26593
27251
  };
26594
27252
  }
@@ -27321,7 +27979,6 @@ function useActionsPropResolver(actions) {
27321
27979
  }
27322
27980
  if (!prop) return;
27323
27981
  const unregisters = [];
27324
- const restores = [];
27325
27982
  const warnedIds = /* @__PURE__ */ new Set();
27326
27983
  for (const [slotId, entry] of Object.entries(prop)) {
27327
27984
  if (entry === null) {
@@ -27349,60 +28006,30 @@ function useActionsPropResolver(actions) {
27349
28006
  }
27350
28007
  const { id: _drop, ...rest } = entry;
27351
28008
  const merged = { ...existing, ...rest };
27352
- restores.push(() => {
27353
- reg.register(existing);
27354
- });
27355
28009
  unregisters.push(reg.register(merged));
27356
28010
  }
27357
28011
  return () => {
27358
28012
  for (const u of unregisters) u();
27359
- for (const r of restores) r();
27360
28013
  };
27361
28014
  }, [reg, actions]);
27362
28015
  }
27363
- var viewportWheelPanAction = {
27364
- id: "viewport.wheelPan",
27365
- label: "Pan (wheel)",
27366
- group: "viewport",
27367
- /**
27368
- * Plain wheel → pan; Shift+wheel → horizontal pan (axis swap).
27369
- * Cmd+wheel is claimed by `viewportZoomAction`. The matcher's strict
27370
- * modifier check keeps the bindings exclusive: omitting `mods` forbids
27371
- * any modifier, and `mods: { shift: true }` forbids the others.
27372
- */
27373
- defaultBinding: [
27374
- { spec: { kind: "wheel" }, opts: {} },
27375
- { spec: { kind: "wheel", mods: { shift: true } }, opts: { params: { swapAxis: true } } }
27376
- ],
27377
- requires: ["view"],
27378
- invoker: {
27379
- timing: "immediate",
27380
- run(deps, params) {
27381
- const view = deps.view;
27382
- if (!view) return;
27383
- const deltaX = params?.deltaX ?? 0;
27384
- const deltaY = params?.deltaY ?? 0;
27385
- const current = view.get();
27386
- const swap = params?.swapAxis === true;
27387
- const dx = swap ? (deltaX !== 0 ? deltaX : deltaY) / current.scale.x : deltaX / current.scale.x;
27388
- const dy = swap ? 0 : deltaY / current.scale.y;
27389
- view.set({ ...current, x: current.x + dx, y: current.y + dy });
27390
- }
27391
- },
27392
- enabled: () => true
27393
- };
27394
28016
  function useViewportActions(args) {
27395
28017
  const { pan, zoom, pinchZoom } = args;
27396
28018
  const reg = useActionsRegistry();
27397
28019
  const regRef = useRef(reg);
27398
28020
  regRef.current = reg;
28021
+ const panKey = typeof pan === "object" ? JSON.stringify(pan) : String(pan);
27399
28022
  const zoomKey = typeof zoom === "object" ? JSON.stringify(zoom) : String(zoom);
27400
28023
  const pinchKey = typeof pinchZoom === "object" ? JSON.stringify(pinchZoom) : String(pinchZoom);
27401
28024
  useEffect(() => {
27402
28025
  const r = regRef.current;
27403
28026
  if (!r) return;
27404
28027
  const unregisters = [];
27405
- if (pan) unregisters.push(r.register(viewportWheelPanAction));
28028
+ if (pan) {
28029
+ unregisters.push(
28030
+ r.register(makeViewportWheelPanAction(typeof pan === "object" ? pan : {}))
28031
+ );
28032
+ }
27406
28033
  if (zoom) {
27407
28034
  const zoomAction = typeof zoom === "object" ? makeViewportZoomAction(zoom) : makeViewportZoomAction();
27408
28035
  unregisters.push(r.register(zoomAction));
@@ -27417,7 +28044,7 @@ function useViewportActions(args) {
27417
28044
  return () => {
27418
28045
  for (const u of unregisters) u();
27419
28046
  };
27420
- }, [pan, zoomKey, pinchKey]);
28047
+ }, [panKey, zoomKey, pinchKey]);
27421
28048
  }
27422
28049
  function createNodeRouting() {
27423
28050
  const entries = [];
@@ -27630,7 +28257,6 @@ function SceneCanvasInner(props, ref) {
27630
28257
  () => isSerialized(sceneInput) ? sceneFromJSON(sceneInput, {}) : null
27631
28258
  );
27632
28259
  const scene = bakedScene ?? sceneInput;
27633
- useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);
27634
28260
  const { view: viewProp, onViewChange: onViewChangeProp, defaultView, ...restProps } = rest;
27635
28261
  const internalCanvasRef = useRef(null);
27636
28262
  const canvasApiRef = useRef(null);
@@ -27661,6 +28287,9 @@ function SceneCanvasInner(props, ref) {
27661
28287
  useEffect(() => scene.overrides.subscribe(() => {
27662
28288
  canvasApiRef.current?.requestRedraw?.();
27663
28289
  }), [scene]);
28290
+ useEffect(() => scene.subscribe(() => {
28291
+ canvasApiRef.current?.requestRedraw?.();
28292
+ }), [scene]);
27664
28293
  const currentViewRef = useRef(
27665
28294
  viewProp ?? defaultView ?? { x: 0, y: 0, scale: { x: 1, y: 1 } }
27666
28295
  );
@@ -27841,13 +28470,13 @@ function SceneCanvasInner(props, ref) {
27841
28470
  const viewportRegistered = !!viewport;
27842
28471
  const inertiaEnabled = !!viewport?.inertia;
27843
28472
  const inertiaObj = typeof viewport?.inertia === "object" ? viewport.inertia : void 0;
27844
- const handToolInertia = inertiaEnabled && inertiaObj ? {
27845
- friction: inertiaObj.friction,
27846
- minSpeed: inertiaObj.minSpeed,
27847
- boundary: inertiaObj.boundary,
27848
- bounds: inertiaObj.bounds
28473
+ const handToolInertia = inertiaEnabled ? {
28474
+ friction: inertiaObj?.friction,
28475
+ minSpeed: inertiaObj?.minSpeed,
28476
+ boundary: inertiaObj?.boundary,
28477
+ bounds: inertiaObj?.bounds
27849
28478
  } : void 0;
27850
- const handTool = useHandTool(handToolInertia ? {} : {});
28479
+ const handTool = useHandTool(handToolInertia ? { inertia: handToolInertia } : {});
27851
28480
  const viewportAmbient = [];
27852
28481
  const baseRequestedTools = defaultTools ?? (toolBundle ? BUNDLE_TOOLS[toolBundle] : null) ?? (viewportRegistered ? ["select", "rotate", "hand"] : ["select", "rotate"]);
27853
28482
  const KNOWN_BUILTIN_IDS = /* @__PURE__ */ new Set([
@@ -28300,7 +28929,7 @@ function SceneCanvasInner(props, ref) {
28300
28929
  dispatcher,
28301
28930
  getActionRef,
28302
28931
  pickEvery: internalPickEvery,
28303
- viewportPanEnabled: viewport?.pan !== false,
28932
+ viewportPanEnabled: viewport?.pan ?? true,
28304
28933
  viewportZoom: resolvedViewportZoom,
28305
28934
  viewportPinchZoom: viewport?.pinchZoom ?? true,
28306
28935
  viewportRecenter: viewport?.recenter,
@@ -28591,6 +29220,7 @@ function StandardActionsRegistrar({
28591
29220
  getActionRef.current = null;
28592
29221
  };
28593
29222
  }, [registry22, getActionRef]);
29223
+ const decayLoop = useDecayLoop();
28594
29224
  const view = useViewDepSource(
28595
29225
  currentViewRef,
28596
29226
  onViewChange,
@@ -28599,7 +29229,8 @@ function StandardActionsRegistrar({
28599
29229
  const el = canvasRef.current;
28600
29230
  return el ? { width: el.clientWidth, height: el.clientHeight } : null;
28601
29231
  },
28602
- viewAnimation
29232
+ viewAnimation,
29233
+ decayLoop
28603
29234
  );
28604
29235
  const sceneAsHistory = scene;
28605
29236
  useStandardActions({ selection, scene, view, history: sceneAsHistory });
@@ -29032,8 +29663,7 @@ function MinimapCanvasInner(props) {
29032
29663
  }, [mainView, mainViewDims, indicatorStyle]);
29033
29664
  const extraCommands = useMemo(() => [indicatorCmd], [indicatorCmd]);
29034
29665
  const localCanvasRef = useRef(null);
29035
- const dragActiveRef = useRef(false);
29036
- const capturedPointerIdRef = useRef(null);
29666
+ const sessionRef = useRef(null);
29037
29667
  const mainViewRef = useRef(mainView);
29038
29668
  const mainViewDimsRef = useRef(mainViewDims);
29039
29669
  const onMainViewChangeRef = useRef(onMainViewChange);
@@ -29062,36 +29692,18 @@ function MinimapCanvasInner(props) {
29062
29692
  }, []);
29063
29693
  const handlePointerDown = useCallback((ev) => {
29064
29694
  const canvas = localCanvasRef.current;
29065
- if (!canvas) return;
29695
+ if (!canvas || sessionRef.current) return;
29066
29696
  if (ev.pointerType === "mouse" && ev.button !== 0) return;
29067
- try {
29068
- canvas.setPointerCapture(ev.pointerId);
29069
- } catch {
29070
- }
29071
- dragActiveRef.current = true;
29072
- capturedPointerIdRef.current = ev.pointerId;
29073
- recenterFromPointer(ev);
29074
- }, [recenterFromPointer]);
29075
- const handlePointerMove = useCallback((ev) => {
29076
- if (!dragActiveRef.current) return;
29077
- if (capturedPointerIdRef.current !== null && ev.pointerId !== capturedPointerIdRef.current) {
29078
- return;
29079
- }
29697
+ const done = () => {
29698
+ sessionRef.current = null;
29699
+ };
29700
+ sessionRef.current = openPointerSession(canvas, ev, {
29701
+ onMove: recenterFromPointer,
29702
+ onEnd: done,
29703
+ onCancel: done
29704
+ });
29080
29705
  recenterFromPointer(ev);
29081
29706
  }, [recenterFromPointer]);
29082
- const releaseCapture = useCallback((ev) => {
29083
- const canvas = localCanvasRef.current;
29084
- if (canvas) {
29085
- try {
29086
- if (canvas.hasPointerCapture?.(ev.pointerId)) {
29087
- canvas.releasePointerCapture(ev.pointerId);
29088
- }
29089
- } catch {
29090
- }
29091
- }
29092
- dragActiveRef.current = false;
29093
- capturedPointerIdRef.current = null;
29094
- }, []);
29095
29707
  const setCanvasRef = useCallback((el) => {
29096
29708
  localCanvasRef.current = el;
29097
29709
  if (typeof canvasRef === "function") {
@@ -29104,16 +29716,11 @@ function MinimapCanvasInner(props) {
29104
29716
  const canvas = localCanvasRef.current;
29105
29717
  if (!canvas) return;
29106
29718
  canvas.addEventListener("pointerdown", handlePointerDown);
29107
- canvas.addEventListener("pointermove", handlePointerMove);
29108
- canvas.addEventListener("pointerup", releaseCapture);
29109
- canvas.addEventListener("pointercancel", releaseCapture);
29110
29719
  return () => {
29111
29720
  canvas.removeEventListener("pointerdown", handlePointerDown);
29112
- canvas.removeEventListener("pointermove", handlePointerMove);
29113
- canvas.removeEventListener("pointerup", releaseCapture);
29114
- canvas.removeEventListener("pointercancel", releaseCapture);
29721
+ sessionRef.current?.cancel();
29115
29722
  };
29116
- }, [handlePointerDown, handlePointerMove, releaseCapture]);
29723
+ }, [handlePointerDown]);
29117
29724
  return /* @__PURE__ */ jsx(
29118
29725
  SceneViewCanvas,
29119
29726
  {
@@ -29385,6 +29992,6 @@ function oklchToOklab(L, C, h) {
29385
29992
  return [L, C * Math.cos(h), C * Math.sin(h)];
29386
29993
  }
29387
29994
 
29388
- export { ArrowIcon, EllipseIcon, FALLBACK_FIT_VIEW, LineIcon, MinimapCanvas, PATH_C, PATH_L, PATH_M, PATH_Q, PATH_Z, PathBuilder, PencilIcon, RectIcon, SCRIPT_METRICS, SceneCanvas, SceneViewCanvas, SelectIcon, TextIcon, UnknownIcon, WeaselProvider, asNodeId, boundsOfPath, buildSceneViewCommands, computeFitView, computeIndicatorCommand, createScene, eligibleContribution, ellipsePath, evaluateEnabled, getMarker, getPaintKind, isBuiltinToolPref, linePath, markerDrawCommands, oklabToSrgbU8, oklchToOklab, rectPath, renderSceneToCanvas, renderSceneToPixels, resolveStrokeWidth, rgbaToHex, sceneFromJSON, textCommand, tileGeometry, useActionsRegistry, useActiveToolContext, useOptionalDepRegistry, useVisibleRaf };
29389
- //# sourceMappingURL=chunk-7VW3S44V.js.map
29390
- //# sourceMappingURL=chunk-7VW3S44V.js.map
29995
+ export { ArrowIcon, EllipseIcon, FALLBACK_FIT_VIEW, LineIcon, MinimapCanvas, PATH_C, PATH_L, PATH_M, PATH_Q, PATH_Z, PathBuilder, PencilIcon, RectIcon, SCRIPT_METRICS, SceneCanvas, SceneViewCanvas, SelectIcon, TextIcon, UnknownIcon, WeaselProvider, asNodeId, boundsOfPath, buildSceneViewCommands, computeFitView, computeIndicatorCommand, createScene, eligibleContribution, ellipsePath, evaluateEnabled, getMarker, getPaintKind, isBuiltinToolPref, linePath, markerDrawCommands, oklabToSrgbU8, oklchToOklab, openPointerSession, rectPath, renderSceneToCanvas, renderSceneToPixels, resolveStrokeWidth, rgbaToHex, sceneFromJSON, startThresholdDrag, textCommand, tileGeometry, useActionsRegistry, useActiveToolContext, useOptionalDepRegistry, useVisibleRaf };
29996
+ //# sourceMappingURL=chunk-54ZWZ5FQ.js.map
29997
+ //# sourceMappingURL=chunk-54ZWZ5FQ.js.map