@weasel-js/labkit 1.4.0 → 1.4.1

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 (77) hide show
  1. package/dist/_dts/{CanvasStackContext-D6M0o8cQ.d.ts → CanvasStackContext-BIRUVVGH.d.ts} +1 -1
  2. package/dist/_dts/PrefsForm.d-CgxUequc.d.ts +20 -0
  3. package/dist/_dts/{frac-QS7XzvpG.d.ts → frac-X7mWgd7y.d.ts} +9 -3
  4. package/dist/_dts/{index-B2X21G1c.d.ts → index-FEa1MudK.d.ts} +1 -1
  5. package/dist/_dts/{types-C1Zw7s5P.d.ts → types-lg4TSCb2.d.ts} +2 -1
  6. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +5041 -0
  7. package/dist/canvas/index.d.ts +15 -15
  8. package/dist/canvas/index.js +2 -2
  9. package/dist/chrome/index.d.ts +5 -5
  10. package/dist/chrome/index.js +5 -5
  11. package/dist/{chunk-W3KSUXXR.js → chunk-53AZ2O2N.js} +4 -4
  12. package/dist/{chunk-W3KSUXXR.js.map → chunk-53AZ2O2N.js.map} +1 -1
  13. package/dist/{chunk-4AMN4MMB.js → chunk-7TRFMIWX.js} +53 -39
  14. package/dist/chunk-7TRFMIWX.js.map +1 -0
  15. package/dist/{chunk-UD7TEHZT.js → chunk-BMQWLKOL.js} +6 -6
  16. package/dist/{chunk-UD7TEHZT.js.map → chunk-BMQWLKOL.js.map} +1 -1
  17. package/dist/{chunk-YQGQUBKA.js → chunk-D7QIPGEB.js} +3 -3
  18. package/dist/{chunk-YQGQUBKA.js.map → chunk-D7QIPGEB.js.map} +1 -1
  19. package/dist/{chunk-7VW3S44V.js → chunk-DG2IFMLA.js} +802 -222
  20. package/dist/chunk-DG2IFMLA.js.map +1 -0
  21. package/dist/{chunk-LN6JDUGB.js → chunk-HXXTULDN.js} +2 -2
  22. package/dist/{chunk-LN6JDUGB.js.map → chunk-HXXTULDN.js.map} +1 -1
  23. package/dist/{chunk-L5WBGUVR.js → chunk-J6SBFZYT.js} +2131 -2174
  24. package/dist/chunk-J6SBFZYT.js.map +1 -0
  25. package/dist/{chunk-XCBCZEBO.js → chunk-MIT3ISW4.js} +37 -31
  26. package/dist/chunk-MIT3ISW4.js.map +1 -0
  27. package/dist/{chunk-QR5V3AFG.js → chunk-MSR7EVS6.js} +5 -5
  28. package/dist/{chunk-QR5V3AFG.js.map → chunk-MSR7EVS6.js.map} +1 -1
  29. package/dist/{chunk-B5VWJBWM.js → chunk-OFCBEZMU.js} +3 -3
  30. package/dist/{chunk-B5VWJBWM.js.map → chunk-OFCBEZMU.js.map} +1 -1
  31. package/dist/{chunk-VHFQKAUL.js → chunk-PP7ZWAMV.js} +83 -75
  32. package/dist/chunk-PP7ZWAMV.js.map +1 -0
  33. package/dist/{chunk-Y2HK44TG.js → chunk-RLOXQZW3.js} +3 -3
  34. package/dist/{chunk-Y2HK44TG.js.map → chunk-RLOXQZW3.js.map} +1 -1
  35. package/dist/controls/index.d.ts +4 -3
  36. package/dist/controls/index.js +3 -3
  37. package/dist/dragdrop/index.d.ts +4 -4
  38. package/dist/dragdrop/index.js +2 -2
  39. package/dist/index.d.ts +8 -71
  40. package/dist/index.js +21 -21
  41. package/dist/index.js.map +1 -1
  42. package/dist/job/index.js +1 -1
  43. package/dist/layers/index.d.ts +5 -5
  44. package/dist/layers/index.js +3 -3
  45. package/dist/loupe/index.d.ts +6 -12
  46. package/dist/loupe/index.js +2 -2
  47. package/dist/passthrough/weasel-canvas.d.ts +3 -343
  48. package/dist/passthrough/weasel-canvas.js +1 -1
  49. package/dist/passthrough/weasel-ui.d.ts +40 -3086
  50. package/dist/passthrough/weasel-ui.js +2 -2
  51. package/dist/primitives/index.js +4 -4
  52. package/dist/styles.css +10 -0
  53. package/dist/surface/index.js +2 -2
  54. package/dist/ui/layers/index.js +2 -2
  55. package/dist/undo/index.d.ts +5 -4
  56. package/package.json +8 -8
  57. package/src/canvas/CanvasStack.tsx +8 -13
  58. package/src/canvas/useOrbit.test.ts +93 -1
  59. package/src/canvas/useOrbit.ts +41 -34
  60. package/src/canvas/usePanZoom.test.ts +102 -1
  61. package/src/canvas/usePanZoom.ts +54 -36
  62. package/src/job/useJob.ts +1 -1
  63. package/src/lab/Workspace.tsx +1 -1
  64. package/src/layers/LayerList.test.tsx +72 -1
  65. package/src/layers/LayerList.tsx +40 -32
  66. package/src/primitives/FloatingPanel.test.tsx +82 -0
  67. package/src/primitives/FloatingPanel.tsx +59 -41
  68. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +0 -564
  69. package/dist/_dts/PrefsForm-BkUJZx0A.d.ts +0 -204
  70. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +0 -21
  71. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +0 -87
  72. package/dist/_dts/types-C-gh9Ap-.d.ts +0 -695
  73. package/dist/chunk-4AMN4MMB.js.map +0 -1
  74. package/dist/chunk-7VW3S44V.js.map +0 -1
  75. package/dist/chunk-L5WBGUVR.js.map +0 -1
  76. package/dist/chunk-VHFQKAUL.js.map +0 -1
  77. package/dist/chunk-XCBCZEBO.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"),
@@ -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-NU7D64RL.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,82 @@ 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) abort("lostcapture");
10204
+ }
10205
+ if (opts.capture !== false) {
10206
+ try {
10207
+ origin.setPointerCapture?.(pointerId);
10208
+ } catch {
10209
+ }
10210
+ }
10211
+ doc.addEventListener("pointermove", onMove, true);
10212
+ doc.addEventListener("pointerup", onUp, true);
10213
+ doc.addEventListener("pointercancel", onPointerCancel, true);
10214
+ doc.addEventListener("pointerdown", onRepress, true);
10215
+ origin.addEventListener(LOST_CAPTURE_EVENT, onLostCapture);
10216
+ return {
10217
+ pointerId,
10218
+ get active() {
10219
+ return active;
10220
+ },
10221
+ cancel: () => abort("aborted")
10222
+ };
10223
+ }
9853
10224
  var BASE_PX = {
9854
10225
  handle: 8,
9855
10226
  anchor: 8,
@@ -11609,6 +11980,28 @@ var cloneAction = {
11609
11980
  },
11610
11981
  enabled: () => true
11611
11982
  };
11983
+ function createVelocityTracker() {
11984
+ let samples = [];
11985
+ return {
11986
+ record(dx, dy, t) {
11987
+ samples.push({ dx, dy, t });
11988
+ const cutoff = t - 100;
11989
+ samples = samples.filter((s) => s.t >= cutoff);
11990
+ },
11991
+ getVelocity() {
11992
+ const s = samples;
11993
+ if (s.length < 2) return { vx: 0, vy: 0 };
11994
+ const dt = s[s.length - 1].t - s[0].t;
11995
+ if (dt === 0) return { vx: 0, vy: 0 };
11996
+ const totalDx = s.slice(1).reduce((acc, p) => acc + p.dx, 0);
11997
+ const totalDy = s.slice(1).reduce((acc, p) => acc + p.dy, 0);
11998
+ return { vx: totalDx / dt, vy: totalDy / dt };
11999
+ },
12000
+ reset() {
12001
+ samples = [];
12002
+ }
12003
+ };
12004
+ }
11612
12005
  var viewportDragPanAction = {
11613
12006
  id: "viewport.dragPan",
11614
12007
  label: "Drag to pan viewport",
@@ -11626,17 +12019,37 @@ var viewportDragPanAction = {
11626
12019
  requires: ["view"],
11627
12020
  invoker: {
11628
12021
  timing: "ongoing",
11629
- start(ctx) {
12022
+ start(ctx, opts) {
11630
12023
  const view = ctx.deps.view;
11631
12024
  if (!view) return {};
11632
12025
  const startView = view.get();
11633
- const scratch = { startView, view };
12026
+ const params = resolveParams(opts?.params) ?? {};
12027
+ const inertia = params.inertia === false ? void 0 : params.inertia;
12028
+ const scratch = {
12029
+ startView,
12030
+ view,
12031
+ axis: params.axis ?? "both",
12032
+ inertia,
12033
+ // Only track when a coast could actually happen, so the common
12034
+ // no-inertia drag does no per-move work.
12035
+ tracker: inertia && view.decay ? createVelocityTracker() : void 0,
12036
+ lastScreen: { x: 0, y: 0 }
12037
+ };
11634
12038
  return {
11635
12039
  kind: "pan",
11636
12040
  onMove(moveCtx) {
11637
12041
  if (!moveCtx.drag) return;
11638
12042
  const screen = moveCtx.drag.screenDelta ?? moveCtx.drag.delta;
11639
- const { x: screenDx, y: screenDy } = screen;
12043
+ const screenDx = scratch.axis === "y" ? 0 : screen.x;
12044
+ const screenDy = scratch.axis === "x" ? 0 : screen.y;
12045
+ if (scratch.tracker) {
12046
+ scratch.tracker.record(
12047
+ screenDx - scratch.lastScreen.x,
12048
+ screenDy - scratch.lastScreen.y,
12049
+ performance.now()
12050
+ );
12051
+ scratch.lastScreen = { x: screenDx, y: screenDy };
12052
+ }
11640
12053
  const sv = scratch.startView;
11641
12054
  scratch.view.set({
11642
12055
  ...sv,
@@ -11644,7 +12057,24 @@ var viewportDragPanAction = {
11644
12057
  y: sv.y - screenDy / sv.scale.y
11645
12058
  });
11646
12059
  },
11647
- onEnd() {
12060
+ onEnd(_endCtx, reason) {
12061
+ const { tracker, inertia: inertia2, view: v } = scratch;
12062
+ if (reason === "cancel" || !tracker || !inertia2 || !v.decay) return;
12063
+ const { vx, vy } = tracker.getVelocity();
12064
+ const scale2 = scratch.startView.scale;
12065
+ v.decay({
12066
+ // Screen px/ms → world units/ms, matching the onMove conversion.
12067
+ velocity: { vx: -vx / scale2.x, vy: -vy / scale2.y },
12068
+ friction: inertia2.friction,
12069
+ minSpeed: inertia2.minSpeed,
12070
+ boundary: inertia2.boundary,
12071
+ viewBounds: inertia2.bounds,
12072
+ initialPosition: { x: v.get().x, y: v.get().y },
12073
+ onTick: (dx, dy) => {
12074
+ const cur = v.get();
12075
+ v.set({ ...cur, x: cur.x + dx, y: cur.y + dy });
12076
+ }
12077
+ });
11648
12078
  }
11649
12079
  };
11650
12080
  }
@@ -11745,6 +12175,52 @@ function makeViewportZoomAction(opts = {}) {
11745
12175
  };
11746
12176
  }
11747
12177
  makeViewportZoomAction();
12178
+ var viewportWheelPanAction = {
12179
+ id: "viewport.wheelPan",
12180
+ label: "Pan (wheel)",
12181
+ group: "viewport",
12182
+ /**
12183
+ * Plain wheel → pan; Shift+wheel → horizontal pan (axis swap).
12184
+ * Cmd+wheel is claimed by `viewportZoomAction`. The matcher's strict
12185
+ * modifier check keeps the bindings exclusive: omitting `mods` forbids
12186
+ * any modifier, and `mods: { shift: true }` forbids the others.
12187
+ */
12188
+ defaultBinding: [
12189
+ { spec: { kind: "wheel" }, opts: {} },
12190
+ { spec: { kind: "wheel", mods: { shift: true } }, opts: { params: { swapAxis: true } } }
12191
+ ],
12192
+ requires: ["view"],
12193
+ invoker: {
12194
+ timing: "immediate",
12195
+ run(deps, params) {
12196
+ const view = deps.view;
12197
+ if (!view) return;
12198
+ const axis = params?.axis ?? "both";
12199
+ const deltaX = params?.deltaX ?? 0;
12200
+ const deltaY = params?.deltaY ?? 0;
12201
+ const current = view.get();
12202
+ const swap = params?.swapAxis === true;
12203
+ const dx = swap ? (deltaX !== 0 ? deltaX : deltaY) / current.scale.x : deltaX / current.scale.x;
12204
+ const dy = swap ? 0 : deltaY / current.scale.y;
12205
+ view.set({
12206
+ ...current,
12207
+ x: current.x + (axis === "y" ? 0 : dx),
12208
+ y: current.y + (axis === "x" ? 0 : dy)
12209
+ });
12210
+ }
12211
+ },
12212
+ enabled: () => true
12213
+ };
12214
+ function makeViewportWheelPanAction(opts = {}) {
12215
+ const axis = opts.axis ?? "both";
12216
+ return {
12217
+ ...viewportWheelPanAction,
12218
+ defaultBinding: [
12219
+ { spec: { kind: "wheel" }, opts: { params: { axis } } },
12220
+ { spec: { kind: "wheel", mods: { shift: true } }, opts: { params: { swapAxis: true, axis } } }
12221
+ ]
12222
+ };
12223
+ }
11748
12224
  function bodyPredicate(fn) {
11749
12225
  return Object.assign(fn, { readsAffordance: false });
11750
12226
  }
@@ -16248,6 +16724,7 @@ function useGestureDispatcher(opts) {
16248
16724
  };
16249
16725
  const heldKeys = /* @__PURE__ */ new Set();
16250
16726
  const activePointers = /* @__PURE__ */ new Set();
16727
+ const buttonsReportedFor = /* @__PURE__ */ new Set();
16251
16728
  const pointerPositions = /* @__PURE__ */ new Map();
16252
16729
  let multiTouchStart = null;
16253
16730
  const TAP_THRESHOLD_PX = 8;
@@ -16390,8 +16867,10 @@ function useGestureDispatcher(opts) {
16390
16867
  };
16391
16868
  const onPointerDown = (e) => {
16392
16869
  if ((e.button ?? 0) !== 0) return;
16870
+ if (activePointers.has(e.pointerId)) onPointerUp(e);
16393
16871
  routeDown(e.pointerId, e.clientX, e.clientY);
16394
16872
  activePointers.add(e.pointerId);
16873
+ if (reportsButtons(e)) buttonsReportedFor.add(e.pointerId);
16395
16874
  pointerPositions.set(e.pointerId, { x: e.clientX, y: e.clientY });
16396
16875
  try {
16397
16876
  canvas?.setPointerCapture?.(e.pointerId);
@@ -16470,6 +16949,10 @@ function useGestureDispatcher(opts) {
16470
16949
  }
16471
16950
  };
16472
16951
  const onPointerMove = (e) => {
16952
+ if (activePointers.has(e.pointerId) && buttonsReportedFor.has(e.pointerId) && isMissedRelease(e)) {
16953
+ onPointerUp(e);
16954
+ return;
16955
+ }
16473
16956
  routeAt(e.pointerId, e.clientX, e.clientY);
16474
16957
  if (activePointers.has(e.pointerId)) {
16475
16958
  pointerPositions.set(e.pointerId, { x: e.clientX, y: e.clientY });
@@ -16619,6 +17102,7 @@ function useGestureDispatcher(opts) {
16619
17102
  cancelLongPress(e.pointerId);
16620
17103
  const prevSize = activePointers.size;
16621
17104
  activePointers.delete(e.pointerId);
17105
+ buttonsReportedFor.delete(e.pointerId);
16622
17106
  pointerPositions.delete(e.pointerId);
16623
17107
  const staysUnderThreshold = bufferedDown.delete(e.pointerId);
16624
17108
  if (prevSize >= 2 && activePointers.size < 2) {
@@ -16726,6 +17210,7 @@ function useGestureDispatcher(opts) {
16726
17210
  routeAt(e.pointerId, e.clientX, e.clientY);
16727
17211
  cancelLongPress(e.pointerId);
16728
17212
  activePointers.delete(e.pointerId);
17213
+ buttonsReportedFor.delete(e.pointerId);
16729
17214
  pointerPositions.delete(e.pointerId);
16730
17215
  lastPointerDown.delete(e.pointerId);
16731
17216
  bufferedDown.delete(e.pointerId);
@@ -16741,6 +17226,11 @@ function useGestureDispatcher(opts) {
16741
17226
  dispatch2(ev);
16742
17227
  routeRelease(e.pointerId);
16743
17228
  };
17229
+ const onLostCapture = (e) => {
17230
+ const pe = e;
17231
+ if (!activePointers.has(pe.pointerId)) return;
17232
+ onPointerCancel(pe);
17233
+ };
16744
17234
  const onContextMenu = (e) => {
16745
17235
  e.preventDefault();
16746
17236
  routeAt(null, e.clientX, e.clientY);
@@ -16830,6 +17320,7 @@ function useGestureDispatcher(opts) {
16830
17320
  canvas?.addEventListener("pointermove", onPointerMove);
16831
17321
  canvas?.addEventListener("pointerup", onPointerUp);
16832
17322
  canvas?.addEventListener("pointercancel", onPointerCancel);
17323
+ canvas?.addEventListener(LOST_CAPTURE_EVENT, onLostCapture);
16833
17324
  canvas?.addEventListener("contextmenu", onContextMenu);
16834
17325
  canvas?.addEventListener("dragenter", onDragOver);
16835
17326
  canvas?.addEventListener("dragover", onDragOver);
@@ -16850,6 +17341,7 @@ function useGestureDispatcher(opts) {
16850
17341
  canvas?.removeEventListener("pointermove", onPointerMove);
16851
17342
  canvas?.removeEventListener("pointerup", onPointerUp);
16852
17343
  canvas?.removeEventListener("pointercancel", onPointerCancel);
17344
+ canvas?.removeEventListener(LOST_CAPTURE_EVENT, onLostCapture);
16853
17345
  canvas?.removeEventListener("contextmenu", onContextMenu);
16854
17346
  canvas?.removeEventListener("dragenter", onDragOver);
16855
17347
  canvas?.removeEventListener("dragover", onDragOver);
@@ -16864,6 +17356,132 @@ function useGestureDispatcher(opts) {
16864
17356
  };
16865
17357
  }, [enabled, keyboard, canvasRef]);
16866
17358
  }
17359
+ var SPRING_DAMPING = 0.5;
17360
+ function useDecayLoop() {
17361
+ const stateRef = useRef(null);
17362
+ const frameLoop = useVisibleRaf(
17363
+ (now) => {
17364
+ tick(now);
17365
+ },
17366
+ {
17367
+ // Coasting does not continue while suspended: dropping `lastTime` makes
17368
+ // the resuming frame seed a fresh interval instead of applying an hour of
17369
+ // friction at once, which would end the decay before it was seen.
17370
+ onResume: useCallback(() => {
17371
+ if (stateRef.current) stateRef.current.lastTime = null;
17372
+ }, [])
17373
+ }
17374
+ );
17375
+ const cancel = useCallback(() => {
17376
+ frameLoop.cancel();
17377
+ stateRef.current = null;
17378
+ }, [frameLoop]);
17379
+ const tick = useCallback((now) => {
17380
+ const s = stateRef.current;
17381
+ if (!s) return;
17382
+ if (s.lastTime === null) {
17383
+ s.lastTime = now;
17384
+ frameLoop.request();
17385
+ return;
17386
+ }
17387
+ const dt = Math.min(now - s.lastTime, 64);
17388
+ s.lastTime = now;
17389
+ const f = Math.pow(s.friction, dt / 16.67);
17390
+ s.vx *= f;
17391
+ s.vy *= f;
17392
+ const speed = Math.sqrt(s.vx * s.vx + s.vy * s.vy);
17393
+ if (speed < s.minSpeed) {
17394
+ stateRef.current = null;
17395
+ frameLoop.cancel();
17396
+ s.onEnd?.();
17397
+ return;
17398
+ }
17399
+ let dx = s.vx * dt;
17400
+ let dy = s.vy * dt;
17401
+ if (s.viewBounds && s.boundary) {
17402
+ const { minX, maxX, minY, maxY } = s.viewBounds;
17403
+ const newX = s.posX + dx;
17404
+ const newY = s.posY + dy;
17405
+ if (s.boundary === "stop") {
17406
+ if (minX !== void 0 && newX < minX) {
17407
+ dx = minX - s.posX;
17408
+ s.vx = 0;
17409
+ } else if (maxX !== void 0 && newX > maxX) {
17410
+ dx = maxX - s.posX;
17411
+ s.vx = 0;
17412
+ }
17413
+ if (minY !== void 0 && newY < minY) {
17414
+ dy = minY - s.posY;
17415
+ s.vy = 0;
17416
+ } else if (maxY !== void 0 && newY > maxY) {
17417
+ dy = maxY - s.posY;
17418
+ s.vy = 0;
17419
+ }
17420
+ } else if (s.boundary === "spring") {
17421
+ if (minX !== void 0 && newX < minX) {
17422
+ dx = minX - s.posX;
17423
+ s.vx = Math.abs(s.vx) * SPRING_DAMPING;
17424
+ } else if (maxX !== void 0 && newX > maxX) {
17425
+ dx = maxX - s.posX;
17426
+ s.vx = -Math.abs(s.vx) * SPRING_DAMPING;
17427
+ }
17428
+ if (minY !== void 0 && newY < minY) {
17429
+ dy = minY - s.posY;
17430
+ s.vy = Math.abs(s.vy) * SPRING_DAMPING;
17431
+ } else if (maxY !== void 0 && newY > maxY) {
17432
+ dy = maxY - s.posY;
17433
+ s.vy = -Math.abs(s.vy) * SPRING_DAMPING;
17434
+ }
17435
+ } else {
17436
+ if (minX !== void 0 && newX < minX) {
17437
+ dx = minX - s.posX;
17438
+ s.vx = Math.abs(s.vx);
17439
+ } else if (maxX !== void 0 && newX > maxX) {
17440
+ dx = maxX - s.posX;
17441
+ s.vx = -Math.abs(s.vx);
17442
+ }
17443
+ if (minY !== void 0 && newY < minY) {
17444
+ dy = minY - s.posY;
17445
+ s.vy = Math.abs(s.vy);
17446
+ } else if (maxY !== void 0 && newY > maxY) {
17447
+ dy = maxY - s.posY;
17448
+ s.vy = -Math.abs(s.vy);
17449
+ }
17450
+ }
17451
+ s.posX += dx;
17452
+ s.posY += dy;
17453
+ }
17454
+ s.onTick(dx, dy);
17455
+ frameLoop.request();
17456
+ }, [frameLoop]);
17457
+ const start = useCallback((config) => {
17458
+ cancel();
17459
+ const { velocity, friction = 0.92, minSpeed = 0.01, viewBounds, boundary, initialPosition, onTick, onEnd } = config;
17460
+ const speed = Math.sqrt(velocity.vx ** 2 + velocity.vy ** 2);
17461
+ if (speed < minSpeed) {
17462
+ onEnd?.();
17463
+ return;
17464
+ }
17465
+ stateRef.current = {
17466
+ vx: velocity.vx,
17467
+ vy: velocity.vy,
17468
+ friction,
17469
+ minSpeed,
17470
+ lastTime: null,
17471
+ posX: initialPosition?.x ?? 0,
17472
+ posY: initialPosition?.y ?? 0,
17473
+ viewBounds,
17474
+ boundary,
17475
+ onTick,
17476
+ onEnd
17477
+ };
17478
+ frameLoop.request();
17479
+ }, [cancel, frameLoop]);
17480
+ useEffect(() => () => {
17481
+ cancel();
17482
+ }, [cancel]);
17483
+ return { start, cancel };
17484
+ }
16867
17485
  function dist(a, b) {
16868
17486
  return Math.hypot(b.x - a.x, b.y - a.y);
16869
17487
  }
@@ -20485,7 +21103,9 @@ function useRotateTool(adapter, options) {
20485
21103
  [overlay]
20486
21104
  );
20487
21105
  }
20488
- function useHandTool(_opts = {}) {
21106
+ function useHandTool(opts = {}) {
21107
+ const optsRef = useRef(opts);
21108
+ optsRef.current = opts;
20489
21109
  return useMemo(
20490
21110
  () => Object.assign(defineViewportTool({
20491
21111
  id: "hand",
@@ -20503,7 +21123,16 @@ function useHandTool(_opts = {}) {
20503
21123
  // viewportDragPanAction, which also owns the grabbing cursor
20504
21124
  // (`Action.activeCursor`) the tool used to declare as an `engaged`
20505
21125
  // phase entry.
20506
- bindings: [{ spec: { kind: "drag" }, actionId: "viewport.dragPan" }]
21126
+ bindings: [{
21127
+ spec: { kind: "drag" },
21128
+ actionId: "viewport.dragPan",
21129
+ opts: {
21130
+ params: () => ({
21131
+ axis: optsRef.current.axis ?? "both",
21132
+ inertia: optsRef.current.inertia
21133
+ })
21134
+ }
21135
+ }]
20507
21136
  }),
20508
21137
  []
20509
21138
  );
@@ -21764,6 +22393,20 @@ function projectBounds(b, view) {
21764
22393
  const [sx, sy] = worldToScreen(b.x, b.y, t);
21765
22394
  return { ...b, x: sx, y: sy, width: b.width * view.scale.x, height: b.height * view.scale.y };
21766
22395
  }
22396
+ function leavesOf(id, getChildren, isContainer) {
22397
+ if (!isContainer(id)) return [id];
22398
+ const out = [];
22399
+ const visit = (nid) => {
22400
+ const kids = getChildren(nid);
22401
+ if (kids.length === 0) {
22402
+ if (!isContainer(nid)) out.push(nid);
22403
+ return;
22404
+ }
22405
+ for (const k of kids) visit(k);
22406
+ };
22407
+ visit(id);
22408
+ return out;
22409
+ }
21767
22410
  function makeContainerAwareBoundsResolver(getPose, getBounds, getChildren, isContainer) {
21768
22411
  if (getChildren === void 0 || isContainer === void 0) {
21769
22412
  return (id) => {
@@ -21771,26 +22414,12 @@ function makeContainerAwareBoundsResolver(getPose, getBounds, getChildren, isCon
21771
22414
  return p === null ? null : getBounds(p);
21772
22415
  };
21773
22416
  }
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
22417
  return (id) => {
21789
22418
  if (!isContainer(id)) {
21790
22419
  const p = getPose(id);
21791
22420
  return p === null ? null : getBounds(p);
21792
22421
  }
21793
- const leaves = leavesOf(id);
22422
+ const leaves = leavesOf(id, getChildren, isContainer);
21794
22423
  if (leaves.length === 0) return null;
21795
22424
  const leafBounds = [];
21796
22425
  for (const leafId of leaves) {
@@ -22923,7 +23552,7 @@ function CanvasInner(props, ref) {
22923
23552
  return {
22924
23553
  worldX,
22925
23554
  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 },
23555
+ modifiers: m ? { alt: m.alt, shift: m.shift, meta: m.meta, ctrl: m.ctrl } : { alt: false, shift: false, meta: false, ctrl: false },
22927
23556
  selection: effectiveSelectionRefForCtx.current,
22928
23557
  adapter: effectiveAdapterRefForCtx.current,
22929
23558
  applyOps: (ops, label) => {
@@ -26567,10 +27196,10 @@ function boundsOf(coords) {
26567
27196
  }
26568
27197
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
26569
27198
  }
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" : ""}`;
27199
+ function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize, animation, decay) {
27200
+ const wiring = useRef({ currentViewRef, onViewChange, recenter, hostSize, animation, decay });
27201
+ wiring.current = { currentViewRef, onViewChange, recenter, hostSize, animation, decay };
27202
+ const shape = `${recenter ? "r" : ""}${hostSize ? "h" : ""}${animation ? "a" : ""}${decay ? "d" : ""}`;
26574
27203
  const shapeRef = useRef(null);
26575
27204
  const viewApiRef = useRef(null);
26576
27205
  if (viewApiRef.current === null || shapeRef.current !== shape) {
@@ -26589,6 +27218,10 @@ function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize, anim
26589
27218
  animate: (to, opts) => wiring.current.animation.animate(to, opts),
26590
27219
  stopAnimation: () => wiring.current.animation.stop(),
26591
27220
  animationTarget: () => wiring.current.animation.target()
27221
+ } : {},
27222
+ ...decay ? {
27223
+ decay: (config) => wiring.current.decay.start(config),
27224
+ stopDecay: () => wiring.current.decay.cancel()
26592
27225
  } : {}
26593
27226
  };
26594
27227
  }
@@ -27321,7 +27954,6 @@ function useActionsPropResolver(actions) {
27321
27954
  }
27322
27955
  if (!prop) return;
27323
27956
  const unregisters = [];
27324
- const restores = [];
27325
27957
  const warnedIds = /* @__PURE__ */ new Set();
27326
27958
  for (const [slotId, entry] of Object.entries(prop)) {
27327
27959
  if (entry === null) {
@@ -27349,60 +27981,30 @@ function useActionsPropResolver(actions) {
27349
27981
  }
27350
27982
  const { id: _drop, ...rest } = entry;
27351
27983
  const merged = { ...existing, ...rest };
27352
- restores.push(() => {
27353
- reg.register(existing);
27354
- });
27355
27984
  unregisters.push(reg.register(merged));
27356
27985
  }
27357
27986
  return () => {
27358
27987
  for (const u of unregisters) u();
27359
- for (const r of restores) r();
27360
27988
  };
27361
27989
  }, [reg, actions]);
27362
27990
  }
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
27991
  function useViewportActions(args) {
27395
27992
  const { pan, zoom, pinchZoom } = args;
27396
27993
  const reg = useActionsRegistry();
27397
27994
  const regRef = useRef(reg);
27398
27995
  regRef.current = reg;
27996
+ const panKey = typeof pan === "object" ? JSON.stringify(pan) : String(pan);
27399
27997
  const zoomKey = typeof zoom === "object" ? JSON.stringify(zoom) : String(zoom);
27400
27998
  const pinchKey = typeof pinchZoom === "object" ? JSON.stringify(pinchZoom) : String(pinchZoom);
27401
27999
  useEffect(() => {
27402
28000
  const r = regRef.current;
27403
28001
  if (!r) return;
27404
28002
  const unregisters = [];
27405
- if (pan) unregisters.push(r.register(viewportWheelPanAction));
28003
+ if (pan) {
28004
+ unregisters.push(
28005
+ r.register(makeViewportWheelPanAction(typeof pan === "object" ? pan : {}))
28006
+ );
28007
+ }
27406
28008
  if (zoom) {
27407
28009
  const zoomAction = typeof zoom === "object" ? makeViewportZoomAction(zoom) : makeViewportZoomAction();
27408
28010
  unregisters.push(r.register(zoomAction));
@@ -27417,7 +28019,7 @@ function useViewportActions(args) {
27417
28019
  return () => {
27418
28020
  for (const u of unregisters) u();
27419
28021
  };
27420
- }, [pan, zoomKey, pinchKey]);
28022
+ }, [panKey, zoomKey, pinchKey]);
27421
28023
  }
27422
28024
  function createNodeRouting() {
27423
28025
  const entries = [];
@@ -27841,13 +28443,13 @@ function SceneCanvasInner(props, ref) {
27841
28443
  const viewportRegistered = !!viewport;
27842
28444
  const inertiaEnabled = !!viewport?.inertia;
27843
28445
  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
28446
+ const handToolInertia = inertiaEnabled ? {
28447
+ friction: inertiaObj?.friction,
28448
+ minSpeed: inertiaObj?.minSpeed,
28449
+ boundary: inertiaObj?.boundary,
28450
+ bounds: inertiaObj?.bounds
27849
28451
  } : void 0;
27850
- const handTool = useHandTool(handToolInertia ? {} : {});
28452
+ const handTool = useHandTool(handToolInertia ? { inertia: handToolInertia } : {});
27851
28453
  const viewportAmbient = [];
27852
28454
  const baseRequestedTools = defaultTools ?? (toolBundle ? BUNDLE_TOOLS[toolBundle] : null) ?? (viewportRegistered ? ["select", "rotate", "hand"] : ["select", "rotate"]);
27853
28455
  const KNOWN_BUILTIN_IDS = /* @__PURE__ */ new Set([
@@ -28300,7 +28902,7 @@ function SceneCanvasInner(props, ref) {
28300
28902
  dispatcher,
28301
28903
  getActionRef,
28302
28904
  pickEvery: internalPickEvery,
28303
- viewportPanEnabled: viewport?.pan !== false,
28905
+ viewportPanEnabled: viewport?.pan ?? true,
28304
28906
  viewportZoom: resolvedViewportZoom,
28305
28907
  viewportPinchZoom: viewport?.pinchZoom ?? true,
28306
28908
  viewportRecenter: viewport?.recenter,
@@ -28591,6 +29193,7 @@ function StandardActionsRegistrar({
28591
29193
  getActionRef.current = null;
28592
29194
  };
28593
29195
  }, [registry22, getActionRef]);
29196
+ const decayLoop = useDecayLoop();
28594
29197
  const view = useViewDepSource(
28595
29198
  currentViewRef,
28596
29199
  onViewChange,
@@ -28599,7 +29202,8 @@ function StandardActionsRegistrar({
28599
29202
  const el = canvasRef.current;
28600
29203
  return el ? { width: el.clientWidth, height: el.clientHeight } : null;
28601
29204
  },
28602
- viewAnimation
29205
+ viewAnimation,
29206
+ decayLoop
28603
29207
  );
28604
29208
  const sceneAsHistory = scene;
28605
29209
  useStandardActions({ selection, scene, view, history: sceneAsHistory });
@@ -29032,8 +29636,7 @@ function MinimapCanvasInner(props) {
29032
29636
  }, [mainView, mainViewDims, indicatorStyle]);
29033
29637
  const extraCommands = useMemo(() => [indicatorCmd], [indicatorCmd]);
29034
29638
  const localCanvasRef = useRef(null);
29035
- const dragActiveRef = useRef(false);
29036
- const capturedPointerIdRef = useRef(null);
29639
+ const sessionRef = useRef(null);
29037
29640
  const mainViewRef = useRef(mainView);
29038
29641
  const mainViewDimsRef = useRef(mainViewDims);
29039
29642
  const onMainViewChangeRef = useRef(onMainViewChange);
@@ -29062,36 +29665,18 @@ function MinimapCanvasInner(props) {
29062
29665
  }, []);
29063
29666
  const handlePointerDown = useCallback((ev) => {
29064
29667
  const canvas = localCanvasRef.current;
29065
- if (!canvas) return;
29668
+ if (!canvas || sessionRef.current) return;
29066
29669
  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
- }
29670
+ const done = () => {
29671
+ sessionRef.current = null;
29672
+ };
29673
+ sessionRef.current = openPointerSession(canvas, ev, {
29674
+ onMove: recenterFromPointer,
29675
+ onEnd: done,
29676
+ onCancel: done
29677
+ });
29080
29678
  recenterFromPointer(ev);
29081
29679
  }, [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
29680
  const setCanvasRef = useCallback((el) => {
29096
29681
  localCanvasRef.current = el;
29097
29682
  if (typeof canvasRef === "function") {
@@ -29104,16 +29689,11 @@ function MinimapCanvasInner(props) {
29104
29689
  const canvas = localCanvasRef.current;
29105
29690
  if (!canvas) return;
29106
29691
  canvas.addEventListener("pointerdown", handlePointerDown);
29107
- canvas.addEventListener("pointermove", handlePointerMove);
29108
- canvas.addEventListener("pointerup", releaseCapture);
29109
- canvas.addEventListener("pointercancel", releaseCapture);
29110
29692
  return () => {
29111
29693
  canvas.removeEventListener("pointerdown", handlePointerDown);
29112
- canvas.removeEventListener("pointermove", handlePointerMove);
29113
- canvas.removeEventListener("pointerup", releaseCapture);
29114
- canvas.removeEventListener("pointercancel", releaseCapture);
29694
+ sessionRef.current?.cancel();
29115
29695
  };
29116
- }, [handlePointerDown, handlePointerMove, releaseCapture]);
29696
+ }, [handlePointerDown]);
29117
29697
  return /* @__PURE__ */ jsx(
29118
29698
  SceneViewCanvas,
29119
29699
  {
@@ -29385,6 +29965,6 @@ function oklchToOklab(L, C, h) {
29385
29965
  return [L, C * Math.cos(h), C * Math.sin(h)];
29386
29966
  }
29387
29967
 
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
29968
+ 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, textCommand, tileGeometry, useActionsRegistry, useActiveToolContext, useOptionalDepRegistry, useVisibleRaf };
29969
+ //# sourceMappingURL=chunk-DG2IFMLA.js.map
29970
+ //# sourceMappingURL=chunk-DG2IFMLA.js.map