@weasel-js/labkit 1.4.0-pre.1 → 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 (120) hide show
  1. package/dist/_dts/{CanvasStackContext-LnCfqNBA.d.ts → CanvasStackContext-BIRUVVGH.d.ts} +1 -1
  2. package/dist/_dts/PrefsForm.d-CgxUequc.d.ts +20 -0
  3. package/dist/_dts/{frac-C-2c72Ij.d.ts → frac-X7mWgd7y.d.ts} +10 -4
  4. package/dist/_dts/{index-BDVzvRzQ.d.ts → index-FEa1MudK.d.ts} +8 -2
  5. package/dist/_dts/{types-DYMaEvM5.d.ts → types-ChVJJHvk.d.ts} +3 -0
  6. package/dist/_dts/{types-D6s4b7if.d.ts → types-lg4TSCb2.d.ts} +3 -1
  7. package/dist/_dts/{useTrialState-gmMvZqPc.d.ts → useTrialState-Cdm13fvl.d.ts} +2 -1
  8. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +5041 -0
  9. package/dist/canvas/index.d.ts +16 -16
  10. package/dist/canvas/index.js +2 -2
  11. package/dist/chrome/index.d.ts +6 -6
  12. package/dist/chrome/index.js +5 -5
  13. package/dist/chunk-2YILQ7Y5.js +33 -0
  14. package/dist/chunk-2YILQ7Y5.js.map +1 -0
  15. package/dist/{chunk-TO2FUOKF.js → chunk-53AZ2O2N.js} +16 -13
  16. package/dist/chunk-53AZ2O2N.js.map +1 -0
  17. package/dist/{chunk-QB5SM4BM.js → chunk-7TRFMIWX.js} +55 -41
  18. package/dist/chunk-7TRFMIWX.js.map +1 -0
  19. package/dist/{chunk-6OEYTYML.js → chunk-BMQWLKOL.js} +15 -15
  20. package/dist/chunk-BMQWLKOL.js.map +1 -0
  21. package/dist/{chunk-M75ZU6ZZ.js → chunk-D7QIPGEB.js} +3 -3
  22. package/dist/{chunk-M75ZU6ZZ.js.map → chunk-D7QIPGEB.js.map} +1 -1
  23. package/dist/{chunk-L5QJOOLV.js → chunk-DG2IFMLA.js} +1309 -273
  24. package/dist/chunk-DG2IFMLA.js.map +1 -0
  25. package/dist/{chunk-LN6JDUGB.js → chunk-HXXTULDN.js} +2 -2
  26. package/dist/{chunk-LN6JDUGB.js.map → chunk-HXXTULDN.js.map} +1 -1
  27. package/dist/{chunk-RBGKL7NF.js → chunk-J6SBFZYT.js} +1807 -1814
  28. package/dist/chunk-J6SBFZYT.js.map +1 -0
  29. package/dist/{chunk-XJ6N32QP.js → chunk-LJSIUFYD.js} +14 -30
  30. package/dist/chunk-LJSIUFYD.js.map +1 -0
  31. package/dist/{chunk-AE5CNVRU.js → chunk-MIT3ISW4.js} +37 -31
  32. package/dist/chunk-MIT3ISW4.js.map +1 -0
  33. package/dist/{chunk-HHGVISVZ.js → chunk-MSR7EVS6.js} +7 -5
  34. package/dist/chunk-MSR7EVS6.js.map +1 -0
  35. package/dist/{chunk-ESIQNQ6K.js → chunk-OFCBEZMU.js} +12 -6
  36. package/dist/chunk-OFCBEZMU.js.map +1 -0
  37. package/dist/{chunk-HMFODCOX.js → chunk-PP7ZWAMV.js} +83 -75
  38. package/dist/chunk-PP7ZWAMV.js.map +1 -0
  39. package/dist/{chunk-E2UQVZ44.js → chunk-RLOXQZW3.js} +3 -3
  40. package/dist/{chunk-E2UQVZ44.js.map → chunk-RLOXQZW3.js.map} +1 -1
  41. package/dist/controls/index.d.ts +4 -3
  42. package/dist/controls/index.js +3 -3
  43. package/dist/dragdrop/index.d.ts +5 -5
  44. package/dist/dragdrop/index.js +2 -2
  45. package/dist/index.d.ts +57 -77
  46. package/dist/index.js +308 -161
  47. package/dist/index.js.map +1 -1
  48. package/dist/job/index.js +1 -1
  49. package/dist/layers/index.d.ts +6 -6
  50. package/dist/layers/index.js +3 -3
  51. package/dist/loupe/index.d.ts +7 -13
  52. package/dist/loupe/index.js +2 -2
  53. package/dist/passthrough/weasel-canvas.d.ts +3 -343
  54. package/dist/passthrough/weasel-canvas.js +1 -1
  55. package/dist/passthrough/weasel-ui.d.ts +141 -2866
  56. package/dist/passthrough/weasel-ui.js +2 -2
  57. package/dist/primitives/index.js +4 -4
  58. package/dist/state/index.d.ts +3 -3
  59. package/dist/state/index.js +4 -2
  60. package/dist/state/index.js.map +1 -1
  61. package/dist/styles.css +74 -5
  62. package/dist/surface/index.d.ts +17 -2
  63. package/dist/surface/index.js +3 -2
  64. package/dist/ui/layers/index.js +2 -2
  65. package/dist/undo/index.d.ts +6 -5
  66. package/package.json +8 -8
  67. package/src/annotations/AnnotationOverlay.tsx +10 -6
  68. package/src/annotations/Annotations.less +6 -0
  69. package/src/annotations/Annotations.overlay.test.tsx +113 -0
  70. package/src/annotations/MarkList.tsx +3 -2
  71. package/src/canvas/CanvasStack.tsx +8 -13
  72. package/src/canvas/useOrbit.test.ts +93 -1
  73. package/src/canvas/useOrbit.ts +41 -34
  74. package/src/canvas/usePanZoom.test.ts +102 -1
  75. package/src/canvas/usePanZoom.ts +54 -36
  76. package/src/config/builder.test.ts +5 -0
  77. package/src/config/builder.ts +6 -0
  78. package/src/config/types.ts +1 -0
  79. package/src/controls/ControlPanel.test.tsx +23 -0
  80. package/src/controls/ControlPanel.tsx +3 -0
  81. package/src/job/useJob.ts +1 -1
  82. package/src/lab/Lab.surface.test.tsx +8 -3
  83. package/src/lab/LabHeader.test.tsx +12 -0
  84. package/src/lab/LabHeader.tsx +16 -6
  85. package/src/lab/Workspace.tsx +1 -1
  86. package/src/layers/LayerList.test.tsx +72 -1
  87. package/src/layers/LayerList.tsx +40 -32
  88. package/src/passthrough/weasel-ui.test.ts +9 -0
  89. package/src/passthrough/weasel-ui.ts +4 -0
  90. package/src/primitives/FloatingPanel.test.tsx +82 -0
  91. package/src/primitives/FloatingPanel.tsx +59 -41
  92. package/src/primitives/ZoomControl.tsx +2 -0
  93. package/src/state/store.ts +10 -0
  94. package/src/state/types.ts +3 -0
  95. package/src/surface/AGENTS.md +11 -1
  96. package/src/surface/index.ts +7 -1
  97. package/src/surface/useSurfaceTile.ts +20 -2
  98. package/src/surface/useTiledSurface.ts +4 -1
  99. package/src/theme/Interstellar.stories.tsx +4 -3
  100. package/src/theme/base.less +39 -1
  101. package/src/trial/Trial.less +27 -5
  102. package/src/trial/TrialBody.test.tsx +67 -0
  103. package/src/trial/TrialBody.tsx +155 -0
  104. package/src/trial/TrialChrome.tsx +18 -10
  105. package/src/trial/index.ts +2 -0
  106. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +0 -564
  107. package/dist/_dts/PrefsForm-BkUJZx0A.d.ts +0 -204
  108. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +0 -21
  109. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +0 -87
  110. package/dist/_dts/types-C-gh9Ap-.d.ts +0 -695
  111. package/dist/chunk-6OEYTYML.js.map +0 -1
  112. package/dist/chunk-AE5CNVRU.js.map +0 -1
  113. package/dist/chunk-ESIQNQ6K.js.map +0 -1
  114. package/dist/chunk-HHGVISVZ.js.map +0 -1
  115. package/dist/chunk-HMFODCOX.js.map +0 -1
  116. package/dist/chunk-L5QJOOLV.js.map +0 -1
  117. package/dist/chunk-QB5SM4BM.js.map +0 -1
  118. package/dist/chunk-RBGKL7NF.js.map +0 -1
  119. package/dist/chunk-TO2FUOKF.js.map +0 -1
  120. package/dist/chunk-XJ6N32QP.js.map +0 -1
@@ -1,9 +1,9 @@
1
- import { createContext, createElement, forwardRef, useMemo, useRef, useCallback, useState, useImperativeHandle, useSyncExternalStore, useEffect, useLayoutEffect, useContext, useId, useReducer } from 'react';
1
+ import { createContext, createElement, forwardRef, useMemo, useRef, useCallback, useState, useEffect, useImperativeHandle, useSyncExternalStore, useLayoutEffect, useContext, useId, useReducer } from 'react';
2
2
  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-H6E6KPTU.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-H6E6KPTU.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"),
@@ -3845,31 +3855,66 @@ function eligibleContribution(reg, entry) {
3845
3855
  var CURSOR_INK = "#141418";
3846
3856
  var CURSOR_HALO = "#ffffff";
3847
3857
  var CURSOR_HALO_WIDTH = 2.6;
3858
+ var CURSOR_ANGLE_STEPS = 16;
3848
3859
  var CURSOR_MAX_CSS_PX = 128;
3849
- function renderHalo(p) {
3860
+ function halo(p, k) {
3850
3861
  switch (p.role) {
3851
3862
  case "ink":
3852
3863
  case "accent":
3853
- return `<path d="${p.d}" fill="${CURSOR_HALO}" stroke="${CURSOR_HALO}" stroke-width="${CURSOR_HALO_WIDTH}" stroke-linejoin="round"/>`;
3864
+ return { d: p.d, fill: CURSOR_HALO, stroke: { color: CURSOR_HALO, width: CURSOR_HALO_WIDTH * k } };
3854
3865
  case "stroke":
3855
- return `<path d="${p.d}" fill="none" stroke="${CURSOR_HALO}" stroke-width="${p.width + CURSOR_HALO_WIDTH}" stroke-linecap="round" stroke-linejoin="round"/>`;
3866
+ return { d: p.d, stroke: { color: CURSOR_HALO, width: (p.width + CURSOR_HALO_WIDTH) * k } };
3856
3867
  // A detail IS halo-coloured and sits on top of the ink; it has no halo.
3857
3868
  case "detail":
3858
- return "";
3869
+ return null;
3859
3870
  }
3860
3871
  }
3861
- function renderInk(p) {
3872
+ function ink(p, k) {
3862
3873
  switch (p.role) {
3863
3874
  case "ink":
3864
- return `<path d="${p.d}" fill="${CURSOR_INK}"/>`;
3875
+ return { d: p.d, fill: CURSOR_INK };
3865
3876
  case "stroke":
3866
- return `<path d="${p.d}" fill="none" stroke="${CURSOR_INK}" stroke-width="${p.width}" stroke-linecap="round" stroke-linejoin="round"/>`;
3877
+ return { d: p.d, stroke: { color: CURSOR_INK, width: p.width * k } };
3867
3878
  case "detail":
3868
- return `<path d="${p.d}" fill="none" stroke="${CURSOR_HALO}" stroke-width="${p.width}" stroke-linecap="round"/>`;
3879
+ return { d: p.d, stroke: { color: CURSOR_HALO, width: p.width * k } };
3869
3880
  case "accent":
3870
- return `<path d="${p.d}" fill="${p.fill}"/>`;
3881
+ return { d: p.d, fill: p.fill };
3871
3882
  }
3872
3883
  }
3884
+ function cursorPaintOps(glyph, opts = {}) {
3885
+ const k = opts.lineWidthScale ?? 1;
3886
+ const out = [];
3887
+ for (const p of glyph.paths) {
3888
+ const h = halo(p, k);
3889
+ if (h) out.push(h);
3890
+ }
3891
+ for (const p of glyph.paths) out.push(ink(p, k));
3892
+ return out;
3893
+ }
3894
+ function cursorPaintMatrix(glyph, placement) {
3895
+ const s = placement.size / glyph.box;
3896
+ const theta = placement.angle ?? 0;
3897
+ const a = s * Math.cos(theta);
3898
+ const b = s * Math.sin(theta);
3899
+ const [hx, hy] = glyph.hotspot;
3900
+ return [a, b, -b, a, placement.at.x - (a * hx - b * hy), placement.at.y - (b * hx + a * hy)];
3901
+ }
3902
+ function cursorWorldSize(glyph, worldRadius, scale2) {
3903
+ const r = glyph.radius ?? glyph.box / 2;
3904
+ return worldRadius * scale2 * glyph.box / r;
3905
+ }
3906
+ function chromeLineWidthScale(glyph, size) {
3907
+ return glyph.box / size;
3908
+ }
3909
+ function quantizeCursorAngle(angle) {
3910
+ const i = Math.round(angle / (Math.PI * 2) * CURSOR_ANGLE_STEPS);
3911
+ return (i % CURSOR_ANGLE_STEPS + CURSOR_ANGLE_STEPS) % CURSOR_ANGLE_STEPS;
3912
+ }
3913
+ function renderOp(op) {
3914
+ const fill = op.fill === void 0 ? "none" : op.fill;
3915
+ const stroke = op.stroke ? ` stroke="${op.stroke.color}" stroke-width="${op.stroke.width}" stroke-linecap="round" stroke-linejoin="round"` : "";
3916
+ return `<path d="${op.d}" fill="${fill}"${stroke}/>`;
3917
+ }
3873
3918
  function bakeCursor(glyph, opts = {}) {
3874
3919
  const size = opts.size ?? 24;
3875
3920
  if (size > CURSOR_MAX_CSS_PX) {
@@ -3877,10 +3922,18 @@ function bakeCursor(glyph, opts = {}) {
3877
3922
  `cursor size ${size} exceeds the ${CURSOR_MAX_CSS_PX}px cap: the browser would drop the image and silently fall back. Use the painted tier.`
3878
3923
  );
3879
3924
  }
3880
- const body = glyph.paths.map(renderHalo).join("") + glyph.paths.map(renderInk).join("");
3925
+ const paths = cursorPaintOps(glyph).map(renderOp).join("");
3926
+ const c = glyph.box / 2;
3927
+ const step = quantizeCursorAngle(opts.angle ?? 0);
3928
+ const theta = step / CURSOR_ANGLE_STEPS * Math.PI * 2;
3929
+ const body = step === 0 ? paths : `<g transform="rotate(${step * 360 / CURSOR_ANGLE_STEPS} ${c} ${c})">${paths}</g>`;
3881
3930
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${glyph.box} ${glyph.box}">${body}</svg>`;
3882
- const hx = Math.round(glyph.hotspot[0] / glyph.box * size);
3883
- const hy = Math.round(glyph.hotspot[1] / glyph.box * size);
3931
+ const dx = glyph.hotspot[0] - c;
3932
+ const dy = glyph.hotspot[1] - c;
3933
+ const cos = Math.cos(theta);
3934
+ const sin = Math.sin(theta);
3935
+ const hx = Math.round((c + cos * dx - sin * dy) / glyph.box * size);
3936
+ const hy = Math.round((c + sin * dx + cos * dy) / glyph.box * size);
3884
3937
  const uri = `data:image/svg+xml,${encodeURIComponent(svg)}`;
3885
3938
  return `url("${uri}") ${hx} ${hy}, ${opts.fallback ?? "default"}`;
3886
3939
  }
@@ -3937,23 +3990,263 @@ var GLYPHS = {
3937
3990
  "d": "M 18.2 2.6 A 3.2 3.2 0 1 0 18.2 9 A 3.2 3.2 0 1 0 18.2 2.6 Z"
3938
3991
  }
3939
3992
  ]
3993
+ },
3994
+ "brush": {
3995
+ "box": 24,
3996
+ "hotspot": [
3997
+ 12,
3998
+ 12
3999
+ ],
4000
+ "radius": 9.9,
4001
+ "paths": [
4002
+ {
4003
+ "role": "stroke",
4004
+ "d": "M 12 2.1 A 9.9 9.9 0 1 0 12 21.9 A 9.9 9.9 0 1 0 12 2.1 Z",
4005
+ "width": 1.6
4006
+ }
4007
+ ]
4008
+ },
4009
+ "crosshairRect": {
4010
+ "box": 24,
4011
+ "hotspot": [
4012
+ 9,
4013
+ 9
4014
+ ],
4015
+ "paths": [
4016
+ {
4017
+ "role": "stroke",
4018
+ "d": "M 1.6 9 L 6.4 9",
4019
+ "width": 1.6
4020
+ },
4021
+ {
4022
+ "role": "stroke",
4023
+ "d": "M 11.6 9 L 16.4 9",
4024
+ "width": 1.6
4025
+ },
4026
+ {
4027
+ "role": "stroke",
4028
+ "d": "M 9 1.6 L 9 6.4",
4029
+ "width": 1.6
4030
+ },
4031
+ {
4032
+ "role": "stroke",
4033
+ "d": "M 9 11.6 L 9 16.4",
4034
+ "width": 1.6
4035
+ },
4036
+ {
4037
+ "role": "ink",
4038
+ "d": "M 14.2 14.2 L 21 14.2 L 21 21 L 14.2 21 Z"
4039
+ }
4040
+ ]
4041
+ },
4042
+ "crosshairEllipse": {
4043
+ "box": 24,
4044
+ "hotspot": [
4045
+ 9,
4046
+ 9
4047
+ ],
4048
+ "paths": [
4049
+ {
4050
+ "role": "stroke",
4051
+ "d": "M 1.6 9 L 6.4 9",
4052
+ "width": 1.6
4053
+ },
4054
+ {
4055
+ "role": "stroke",
4056
+ "d": "M 11.6 9 L 16.4 9",
4057
+ "width": 1.6
4058
+ },
4059
+ {
4060
+ "role": "stroke",
4061
+ "d": "M 9 1.6 L 9 6.4",
4062
+ "width": 1.6
4063
+ },
4064
+ {
4065
+ "role": "stroke",
4066
+ "d": "M 9 11.6 L 9 16.4",
4067
+ "width": 1.6
4068
+ },
4069
+ {
4070
+ "role": "ink",
4071
+ "d": "M 17.6 13.9 A 3.7 3.7 0 1 0 17.6 21.3 A 3.7 3.7 0 1 0 17.6 13.9 Z"
4072
+ }
4073
+ ]
4074
+ },
4075
+ "crosshairLine": {
4076
+ "box": 24,
4077
+ "hotspot": [
4078
+ 9,
4079
+ 9
4080
+ ],
4081
+ "paths": [
4082
+ {
4083
+ "role": "stroke",
4084
+ "d": "M 1.6 9 L 6.4 9",
4085
+ "width": 1.6
4086
+ },
4087
+ {
4088
+ "role": "stroke",
4089
+ "d": "M 11.6 9 L 16.4 9",
4090
+ "width": 1.6
4091
+ },
4092
+ {
4093
+ "role": "stroke",
4094
+ "d": "M 9 1.6 L 9 6.4",
4095
+ "width": 1.6
4096
+ },
4097
+ {
4098
+ "role": "stroke",
4099
+ "d": "M 9 11.6 L 9 16.4",
4100
+ "width": 1.6
4101
+ },
4102
+ {
4103
+ "role": "stroke",
4104
+ "d": "M 14.2 21 L 21 14.2",
4105
+ "width": 2.2
4106
+ }
4107
+ ]
4108
+ },
4109
+ "crosshairStar": {
4110
+ "box": 24,
4111
+ "hotspot": [
4112
+ 9,
4113
+ 9
4114
+ ],
4115
+ "paths": [
4116
+ {
4117
+ "role": "stroke",
4118
+ "d": "M 1.6 9 L 6.4 9",
4119
+ "width": 1.6
4120
+ },
4121
+ {
4122
+ "role": "stroke",
4123
+ "d": "M 11.6 9 L 16.4 9",
4124
+ "width": 1.6
4125
+ },
4126
+ {
4127
+ "role": "stroke",
4128
+ "d": "M 9 1.6 L 9 6.4",
4129
+ "width": 1.6
4130
+ },
4131
+ {
4132
+ "role": "stroke",
4133
+ "d": "M 9 11.6 L 9 16.4",
4134
+ "width": 1.6
4135
+ },
4136
+ {
4137
+ "role": "ink",
4138
+ "d": "M 17.6 13.4 L 18.72 16.06 L 21.59 16.3 L 19.41 18.19 L 20.07 21 L 17.6 19.5 L 15.13 21 L 15.79 18.19 L 13.61 16.3 L 16.48 16.06 Z"
4139
+ }
4140
+ ]
4141
+ },
4142
+ "crosshairPolygon": {
4143
+ "box": 24,
4144
+ "hotspot": [
4145
+ 9,
4146
+ 9
4147
+ ],
4148
+ "paths": [
4149
+ {
4150
+ "role": "stroke",
4151
+ "d": "M 1.6 9 L 6.4 9",
4152
+ "width": 1.6
4153
+ },
4154
+ {
4155
+ "role": "stroke",
4156
+ "d": "M 11.6 9 L 16.4 9",
4157
+ "width": 1.6
4158
+ },
4159
+ {
4160
+ "role": "stroke",
4161
+ "d": "M 9 1.6 L 9 6.4",
4162
+ "width": 1.6
4163
+ },
4164
+ {
4165
+ "role": "stroke",
4166
+ "d": "M 9 11.6 L 9 16.4",
4167
+ "width": 1.6
4168
+ },
4169
+ {
4170
+ "role": "ink",
4171
+ "d": "M 17.6 13.5 L 21.5 16.33 L 20.01 20.92 L 15.19 20.92 L 13.7 16.33 Z"
4172
+ }
4173
+ ]
4174
+ },
4175
+ "resize": {
4176
+ "box": 24,
4177
+ "hotspot": [
4178
+ 12,
4179
+ 12
4180
+ ],
4181
+ "paths": [
4182
+ {
4183
+ "role": "ink",
4184
+ "d": "M 2.5 12 L 7 8.8 L 7 10.7 L 17 10.7 L 17 8.8 L 21.5 12 L 17 15.2 L 17 13.3 L 7 13.3 L 7 15.2 Z"
4185
+ }
4186
+ ]
4187
+ },
4188
+ "rotate": {
4189
+ "box": 24,
4190
+ "hotspot": [
4191
+ 12,
4192
+ 12
4193
+ ],
4194
+ "paths": [
4195
+ {
4196
+ "role": "stroke",
4197
+ "d": "M 5.8 9.74 A 6.6 6.6 0 0 1 18.2 9.74",
4198
+ "width": 2.8
4199
+ },
4200
+ {
4201
+ "role": "ink",
4202
+ "d": "M 4.22 14.07 L 2.98 8.72 L 8.62 10.77 Z"
4203
+ },
4204
+ {
4205
+ "role": "ink",
4206
+ "d": "M 19.78 14.07 L 15.38 10.77 L 21.02 8.72 Z"
4207
+ }
4208
+ ]
3940
4209
  }
3941
4210
  };
3942
4211
  var cache2 = /* @__PURE__ */ new Map();
3943
4212
  function cursorFor(name, opts = {}) {
3944
4213
  const size = opts.size ?? 24;
3945
4214
  const fallback = opts.fallback ?? "default";
3946
- const key = `${name}|${size}|${fallback}`;
4215
+ const step = quantizeCursorAngle(opts.angle ?? 0);
4216
+ const key = `${name}|${size}|${step}|${fallback}`;
3947
4217
  const hit = cache2.get(key);
3948
4218
  if (hit !== void 0) return hit;
3949
4219
  const glyph = GLYPHS[name];
3950
4220
  if (glyph === void 0) {
3951
4221
  throw new Error(`unknown cursor glyph: ${String(name)}`);
3952
4222
  }
3953
- const css = bakeCursor(glyph, { size, fallback });
4223
+ const css = bakeCursor(glyph, { size, fallback, angle: opts.angle ?? 0 });
3954
4224
  cache2.set(key, css);
3955
4225
  return css;
3956
4226
  }
4227
+ function resolveCursorTier(spec) {
4228
+ if (typeof spec === "string") return { kind: "css", css: spec };
4229
+ const { glyph, size, worldRadius, angle = 0, fallback } = spec;
4230
+ if (worldRadius !== void 0) {
4231
+ return { kind: "painted", glyph, angle, worldRadius };
4232
+ }
4233
+ if (size !== void 0 && size > CURSOR_MAX_CSS_PX) {
4234
+ return { kind: "painted", glyph, angle, size };
4235
+ }
4236
+ return {
4237
+ kind: "css",
4238
+ css: cursorFor(glyph, {
4239
+ ...size !== void 0 ? { size } : {},
4240
+ ...fallback !== void 0 ? { fallback } : {},
4241
+ angle
4242
+ })
4243
+ };
4244
+ }
4245
+ function resolveCursor(spec) {
4246
+ if (spec === void 0) return void 0;
4247
+ const r = resolveCursorTier(spec);
4248
+ return r.kind === "css" ? r.css : "none";
4249
+ }
3957
4250
 
3958
4251
  // ../history/dist/index.js
3959
4252
  var RESUMERS = /* @__PURE__ */ new WeakMap();
@@ -4377,7 +4670,7 @@ function serialToEntry(se, custom, logger) {
4377
4670
  };
4378
4671
  }
4379
4672
 
4380
- // ../core/dist/chunk-FUQWXDG3.js
4673
+ // ../core/dist/chunk-NU7D64RL.js
4381
4674
  function worldToScreen(worldX, worldY, view) {
4382
4675
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
4383
4676
  }
@@ -4550,6 +4843,44 @@ void main() {
4550
4843
  }
4551
4844
  `
4552
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
+ );
4553
4884
  var IMAGE_FILL_UNIFORMS = [
4554
4885
  "u_proj",
4555
4886
  "u_model",
@@ -4560,6 +4891,15 @@ var IMAGE_FILL_UNIFORMS = [
4560
4891
  "u_colorBias"
4561
4892
  ];
4562
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"];
4563
4903
  var GRAD_VERT_SRC = (
4564
4904
  /* glsl */
4565
4905
  `#version 300 es
@@ -5680,6 +6020,7 @@ seedBuiltins();
5680
6020
  function getPaintKind(kind) {
5681
6021
  return KINDS.get(kind ?? "solid");
5682
6022
  }
6023
+ var SPRITE_STRIDE = 9;
5683
6024
  function tessellate(path, opts = {}) {
5684
6025
  if (path.kind === "rect") return tessellateRect(path);
5685
6026
  return tessellatePolygon(path, opts);
@@ -7362,6 +7703,208 @@ var SolidBatch = class {
7362
7703
  gl.deleteVertexArray(set.vao);
7363
7704
  }
7364
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
+ };
7365
7908
  var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
7366
7909
  var FRAME_PROJ = /* @__PURE__ */ new WeakMap();
7367
7910
  function uploadedFor(ctx, prog) {
@@ -7455,13 +7998,14 @@ function dispatch(ctx, cmd) {
7455
7998
  case "path":
7456
7999
  return drawPath(ctx, cmd);
7457
8000
  case "text":
7458
- flushSolids(ctx);
8001
+ flushBatches(ctx);
7459
8002
  return drawText(ctx, cmd);
7460
8003
  case "image":
7461
- flushSolids(ctx);
7462
8004
  return drawImage(ctx, cmd);
8005
+ case "sprites":
8006
+ return drawSprites(ctx, cmd);
7463
8007
  case "shader":
7464
- flushSolids(ctx);
8008
+ flushBatches(ctx);
7465
8009
  return drawShader(ctx, cmd);
7466
8010
  }
7467
8011
  }
@@ -7638,6 +8182,7 @@ function canBatchMesh(mesh) {
7638
8182
  return !mesh.requiresStencil && mesh.vertices.length >> 1 <= MAX_BATCHED_MESH_VERTICES;
7639
8183
  }
7640
8184
  function stageSolid(ctx, vertices) {
8185
+ flushImages(ctx);
7641
8186
  if (ctx.solidState !== void 0 && !stagedStateIsLive(ctx, ctx.solidState)) flushSolids(ctx);
7642
8187
  if (ctx.solidBatch.wouldOverflow(vertices)) flushSolids(ctx);
7643
8188
  if (ctx.solidState === void 0) {
@@ -7681,7 +8226,7 @@ function tryStageSolid(ctx, mesh, paint) {
7681
8226
  pushMesh(ctx, mesh, paint);
7682
8227
  return true;
7683
8228
  }
7684
- flushSolids(ctx);
8229
+ flushBatches(ctx);
7685
8230
  return false;
7686
8231
  }
7687
8232
  function flushSolids(ctx) {
@@ -7702,6 +8247,56 @@ function flushSolids(ctx) {
7702
8247
  batch.reset();
7703
8248
  ctx.solidState = void 0;
7704
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
+ }
7705
8300
  function expandAnchorColors(perAnchor, handle) {
7706
8301
  const aA = handle.anchorA;
7707
8302
  const aB = handle.anchorB;
@@ -7905,7 +8500,7 @@ function rasterizePathToStencil(ctx, path) {
7905
8500
  gl.bindVertexArray(null);
7906
8501
  }
7907
8502
  function pushClip(ctx, path, newDepth) {
7908
- flushSolids(ctx);
8503
+ flushBatches(ctx);
7909
8504
  const gl = ctx.gl;
7910
8505
  const ancestors = ancestorMask(newDepth - 1);
7911
8506
  const newBit = 1 << newDepth;
@@ -7920,7 +8515,7 @@ function pushClip(ctx, path, newDepth) {
7920
8515
  gl.stencilMask(255);
7921
8516
  }
7922
8517
  function popClip(ctx, path, oldDepth) {
7923
- flushSolids(ctx);
8518
+ flushBatches(ctx);
7924
8519
  const gl = ctx.gl;
7925
8520
  gl.enable(gl.STENCIL_TEST);
7926
8521
  const oldBit = 1 << oldDepth + 1;
@@ -7967,7 +8562,7 @@ function drawPathStroke(ctx, rawCmd) {
7967
8562
  const stroke = cmd.stroke;
7968
8563
  const align = stroke.align ?? "center";
7969
8564
  if (cmd.path.kind === "polygon" && align !== "center") {
7970
- flushSolids(ctx);
8565
+ flushBatches(ctx);
7971
8566
  drawPathStrokeStenciled(ctx, cmd, align);
7972
8567
  return;
7973
8568
  }
@@ -8315,45 +8910,6 @@ function drawTextGroup(ctx, group, prog, dx, dy) {
8315
8910
  gl.drawElements(gl.TRIANGLES, group.quads.length * 6, gl.UNSIGNED_INT, 0);
8316
8911
  gl.bindVertexArray(null);
8317
8912
  }
8318
- var IMAGE_QUAD_RING_SIZE = 64;
8319
- var IMAGE_QUAD_RINGS = /* @__PURE__ */ new WeakMap();
8320
- var IMAGE_QUAD_INDICES = new Uint32Array([0, 1, 2, 1, 3, 2]);
8321
- var IMAGE_QUAD_STRIDE = 16;
8322
- function imageQuadRing(gl, prog) {
8323
- const existing = IMAGE_QUAD_RINGS.get(prog);
8324
- if (existing) return existing;
8325
- const aPosLoc = prog.attribute("a_position");
8326
- const aUvLoc = prog.attribute("a_uv");
8327
- const ibo = gl.createBuffer();
8328
- if (!ibo) throw new Error("drawImage: createBuffer (IBO) returned null");
8329
- const vaos = [];
8330
- const vbos = [];
8331
- for (let slot = 0; slot < IMAGE_QUAD_RING_SIZE; slot++) {
8332
- const vao = gl.createVertexArray();
8333
- const vbo = gl.createBuffer();
8334
- if (!vao) throw new Error("drawImage: createVertexArray returned null");
8335
- if (!vbo) throw new Error("drawImage: createBuffer returned null");
8336
- gl.bindVertexArray(vao);
8337
- gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
8338
- gl.bufferData(gl.ARRAY_BUFFER, 4 * IMAGE_QUAD_STRIDE, gl.DYNAMIC_DRAW);
8339
- if (aPosLoc !== void 0) {
8340
- gl.enableVertexAttribArray(aPosLoc);
8341
- gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, IMAGE_QUAD_STRIDE, 0);
8342
- }
8343
- if (aUvLoc !== void 0) {
8344
- gl.enableVertexAttribArray(aUvLoc);
8345
- gl.vertexAttribPointer(aUvLoc, 2, gl.FLOAT, false, IMAGE_QUAD_STRIDE, 8);
8346
- }
8347
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
8348
- if (slot === 0) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, IMAGE_QUAD_INDICES, gl.STATIC_DRAW);
8349
- gl.bindVertexArray(null);
8350
- vaos.push(vao);
8351
- vbos.push(vbo);
8352
- }
8353
- const ring = { vaos, vbos, ibo, next: 0 };
8354
- IMAGE_QUAD_RINGS.set(prog, ring);
8355
- return ring;
8356
- }
8357
8913
  var TEXT_QUAD_RING_SIZE = 64;
8358
8914
  var TEXT_QUAD_RINGS = /* @__PURE__ */ new WeakMap();
8359
8915
  function grownTo(need) {
@@ -8431,22 +8987,9 @@ function disposeTextQuads(gl, prog) {
8431
8987
  gl.deleteBuffer(ring.ibo);
8432
8988
  TEXT_QUAD_RINGS.delete(prog);
8433
8989
  }
8434
- function disposeImageQuads(gl, prog) {
8435
- const ring = IMAGE_QUAD_RINGS.get(prog);
8436
- if (!ring) return;
8437
- for (const vao of ring.vaos) gl.deleteVertexArray(vao);
8438
- for (const vbo of ring.vbos) gl.deleteBuffer(vbo);
8439
- gl.deleteBuffer(ring.ibo);
8440
- IMAGE_QUAD_RINGS.delete(prog);
8441
- }
8442
- var IMAGE_QUAD_VERTICES = new Float32Array(16);
8443
8990
  function drawImage(ctx, cmd) {
8444
8991
  ctx.imageCache.upload(cmd.image, cmd.image);
8445
- const gl = ctx.gl;
8446
- const prog = ctx.imageFill;
8447
- gl.useProgram(prog.handle);
8448
- const x0 = cmd.x, y0 = cmd.y;
8449
- const x1 = cmd.x + cmd.w, y1 = cmd.y + cmd.h;
8992
+ stageImage(ctx, cmd.image, cmd.sampling ?? "linear");
8450
8993
  const src = cmd.source;
8451
8994
  let u0 = 0, v0 = 0, u1 = 1, v1 = 1;
8452
8995
  if (src) {
@@ -8466,43 +9009,50 @@ function drawImage(ctx, cmd) {
8466
9009
  v0 = v1;
8467
9010
  v1 = t;
8468
9011
  }
8469
- const v = IMAGE_QUAD_VERTICES;
8470
- v[0] = x0;
8471
- v[1] = y0;
8472
- v[2] = u0;
8473
- v[3] = v0;
8474
- v[4] = x1;
8475
- v[5] = y0;
8476
- v[6] = u1;
8477
- v[7] = v0;
8478
- v[8] = x0;
8479
- v[9] = y1;
8480
- v[10] = u0;
8481
- v[11] = v1;
8482
- v[12] = x1;
8483
- v[13] = y1;
8484
- v[14] = u1;
8485
- v[15] = v1;
8486
- const ring = imageQuadRing(gl, prog);
8487
- const slot = ring.next;
8488
- ring.next = (slot + 1) % IMAGE_QUAD_RING_SIZE;
8489
- gl.bindVertexArray(ring.vaos[slot]);
8490
- gl.bindBuffer(gl.ARRAY_BUFFER, ring.vbos[slot]);
8491
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, v);
8492
- setProjAndModel(ctx, prog);
8493
- setColorMatrixUniforms(ctx, prog);
8494
- ctx.imageCache.bind(cmd.image, 0);
8495
- gl.texParameteri(
8496
- gl.TEXTURE_2D,
8497
- gl.TEXTURE_MAG_FILTER,
8498
- 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
8499
9023
  );
8500
- gl.uniform1i(prog.uniform("u_sampler"), 0);
8501
- gl.uniform1f(prog.uniform("u_opacity"), cmd.opacity ?? 1);
8502
- setAlphaUniform(ctx, prog, ctx.state.alpha);
8503
- applyClipTest(ctx);
8504
- gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, 0);
8505
- 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
+ }
8506
9056
  }
8507
9057
  var CUSTOM_VERT_SRC = (
8508
9058
  /* glsl */
@@ -8591,6 +9141,7 @@ var WeaselRenderer = class {
8591
9141
  textSdf;
8592
9142
  textSdfR8;
8593
9143
  imageFill;
9144
+ imageFillVOpacity;
8594
9145
  gradFill;
8595
9146
  patternFill;
8596
9147
  meshCache;
@@ -8601,6 +9152,7 @@ var WeaselRenderer = class {
8601
9152
  quadVbo = null;
8602
9153
  quadIbo = null;
8603
9154
  solidBatch;
9155
+ imageBatch;
8604
9156
  groupState = new GroupState();
8605
9157
  widthCss;
8606
9158
  heightCss;
@@ -8665,6 +9217,13 @@ var WeaselRenderer = class {
8665
9217
  this.imageFill = new ShaderProgram(this.gl, IMAGE_VERT_SRC, IMAGE_FRAG_SRC);
8666
9218
  this.imageFill.lookupUniforms(IMAGE_FILL_UNIFORMS);
8667
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);
8668
9227
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
8669
9228
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
8670
9229
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
@@ -8679,6 +9238,7 @@ var WeaselRenderer = class {
8679
9238
  this.gradRampCache = new GradientRampCache(this.gl);
8680
9239
  this.uploadQuadGeometry();
8681
9240
  this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
9241
+ this.imageBatch = new ImageBatch(this.gl, this.imageFillVOpacity);
8682
9242
  }
8683
9243
  uploadQuadGeometry() {
8684
9244
  const gl = this.gl;
@@ -8802,6 +9362,13 @@ var WeaselRenderer = class {
8802
9362
  this.imageFill = new ShaderProgram(this.gl, IMAGE_VERT_SRC, IMAGE_FRAG_SRC);
8803
9363
  this.imageFill.lookupUniforms(IMAGE_FILL_UNIFORMS);
8804
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);
8805
9372
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
8806
9373
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
8807
9374
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
@@ -8816,6 +9383,7 @@ var WeaselRenderer = class {
8816
9383
  this.gradRampCache = new GradientRampCache(this.gl);
8817
9384
  this.uploadQuadGeometry();
8818
9385
  this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
9386
+ this.imageBatch = new ImageBatch(this.gl, this.imageFillVOpacity);
8819
9387
  for (const id of this.programRegistry.keys()) {
8820
9388
  const src = getProgramSource(id);
8821
9389
  if (!src) continue;
@@ -8864,7 +9432,7 @@ var WeaselRenderer = class {
8864
9432
  }
8865
9433
  this.meshCache.freeTransient();
8866
9434
  this.meshCache.drainPendingDeletes();
8867
- 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]) {
8868
9436
  gl.deleteProgram(prog.handle);
8869
9437
  }
8870
9438
  for (const prog of this.programRegistry.values()) {
@@ -8875,9 +9443,9 @@ var WeaselRenderer = class {
8875
9443
  this.gradRampCache.free();
8876
9444
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
8877
9445
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
8878
- disposeImageQuads(gl, this.imageFill);
8879
9446
  for (const prog of [this.textSdf, this.textSdfR8, this.pathFill]) disposeTextQuads(gl, prog);
8880
9447
  this.solidBatch.dispose();
9448
+ this.imageBatch.dispose();
8881
9449
  }
8882
9450
  /**
8883
9451
  * Draw one frame.
@@ -8906,6 +9474,7 @@ var WeaselRenderer = class {
8906
9474
  textSdf: this.textSdf,
8907
9475
  textSdfR8: this.textSdfR8,
8908
9476
  imageFill: this.imageFill,
9477
+ imageFillVOpacity: this.imageFillVOpacity,
8909
9478
  gradFill: this.gradFill,
8910
9479
  patternFill: this.patternFill,
8911
9480
  meshCache: this.meshCache,
@@ -8917,6 +9486,7 @@ var WeaselRenderer = class {
8917
9486
  quadVbo: this.quadVbo,
8918
9487
  quadIbo: this.quadIbo,
8919
9488
  solidBatch: this.solidBatch,
9489
+ imageBatch: this.imageBatch,
8920
9490
  state: this.groupState,
8921
9491
  widthCss: this.widthCss,
8922
9492
  heightCss: this.heightCss,
@@ -8926,7 +9496,7 @@ var WeaselRenderer = class {
8926
9496
  viewMatrix: viewMatrix2
8927
9497
  };
8928
9498
  for (const cmd of commands) dispatch(ctx, cmd);
8929
- flushSolids(ctx);
9499
+ flushBatches(ctx);
8930
9500
  this.meshCache.freeTransient();
8931
9501
  }
8932
9502
  resize(dims) {
@@ -8960,6 +9530,10 @@ var WeaselRenderer = class {
8960
9530
  return this.solidBatch;
8961
9531
  }
8962
9532
  /** @internal */
9533
+ _imageBatch() {
9534
+ return this.imageBatch;
9535
+ }
9536
+ /** @internal */
8963
9537
  _textSdf() {
8964
9538
  return this.textSdf;
8965
9539
  }
@@ -8972,6 +9546,10 @@ var WeaselRenderer = class {
8972
9546
  return this.imageFill;
8973
9547
  }
8974
9548
  /** @internal */
9549
+ _imageFillVOpacity() {
9550
+ return this.imageFillVOpacity;
9551
+ }
9552
+ /** @internal */
8975
9553
  _gradFill() {
8976
9554
  return this.gradFill;
8977
9555
  }
@@ -9567,6 +10145,82 @@ function clientToCanvas(canvas, clientX, clientY) {
9567
10145
  function clientToCanvasRect(rect, clientX, clientY) {
9568
10146
  return [clientX - rect.left, clientY - rect.top];
9569
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
+ }
9570
10224
  var BASE_PX = {
9571
10225
  handle: 8,
9572
10226
  anchor: 8,
@@ -9882,8 +10536,8 @@ function runLayoutPass(scratch, moveCtx) {
9882
10536
  for (const [cid, pose] of srcLayout.childPoses(srcContainer, srcChildren)) {
9883
10537
  const cur = composeWorldPose(poseAdapter, cid, pc.compose);
9884
10538
  const next = pose;
9885
- const same = cur.x === next.x && cur.y === next.y && cur.width === next.width && cur.height === next.height;
9886
- if (same) continue;
10539
+ const same2 = cur.x === next.x && cur.y === next.y && cur.width === next.width && cur.height === next.height;
10540
+ if (same2) continue;
9887
10541
  sourceReflow.set(cid, pose);
9888
10542
  const parent = scene.get(asNodeId(cid))?.parent ?? null;
9889
10543
  scratch.previews.set(asNodeId(cid), rebaseLocalPose(poseAdapter, pose, parent, pc.compose, pc.decompose));
@@ -11237,6 +11891,11 @@ var cloneAction = {
11237
11891
  id: "clone",
11238
11892
  label: "Clone",
11239
11893
  defaultBinding: { kind: "drag" },
11894
+ // The platform's duplicate cursor, and the whole reason this field is
11895
+ // modifier-aware: the select tool binds clone behind `mods: { alt: true }`,
11896
+ // so the pump predicts it — and shows this — only while Alt is down.
11897
+ cursor: "copy",
11898
+ activeCursor: "copy",
11240
11899
  eligible: { capability: ["edits-page", "creates-selection"] },
11241
11900
  requires: ["selection", "scene", "applyOps"],
11242
11901
  invoker: {
@@ -11321,6 +11980,28 @@ var cloneAction = {
11321
11980
  },
11322
11981
  enabled: () => true
11323
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
+ }
11324
12005
  var viewportDragPanAction = {
11325
12006
  id: "viewport.dragPan",
11326
12007
  label: "Drag to pan viewport",
@@ -11338,17 +12019,37 @@ var viewportDragPanAction = {
11338
12019
  requires: ["view"],
11339
12020
  invoker: {
11340
12021
  timing: "ongoing",
11341
- start(ctx) {
12022
+ start(ctx, opts) {
11342
12023
  const view = ctx.deps.view;
11343
12024
  if (!view) return {};
11344
12025
  const startView = view.get();
11345
- 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
+ };
11346
12038
  return {
11347
12039
  kind: "pan",
11348
12040
  onMove(moveCtx) {
11349
12041
  if (!moveCtx.drag) return;
11350
12042
  const screen = moveCtx.drag.screenDelta ?? moveCtx.drag.delta;
11351
- 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
+ }
11352
12053
  const sv = scratch.startView;
11353
12054
  scratch.view.set({
11354
12055
  ...sv,
@@ -11356,7 +12057,24 @@ var viewportDragPanAction = {
11356
12057
  y: sv.y - screenDy / sv.scale.y
11357
12058
  });
11358
12059
  },
11359
- 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
+ });
11360
12078
  }
11361
12079
  };
11362
12080
  }
@@ -11457,6 +12175,52 @@ function makeViewportZoomAction(opts = {}) {
11457
12175
  };
11458
12176
  }
11459
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
+ }
11460
12224
  function bodyPredicate(fn) {
11461
12225
  return Object.assign(fn, { readsAffordance: false });
11462
12226
  }
@@ -15627,7 +16391,7 @@ function createDispatcher(opts) {
15627
16391
  }
15628
16392
  function inFlightCursor() {
15629
16393
  for (const action of inFlightActions.values()) {
15630
- const cursor = action.activeCursor ?? action.cursor;
16394
+ const cursor = resolveCursor(action.activeCursor ?? action.cursor);
15631
16395
  if (cursor) return cursor;
15632
16396
  }
15633
16397
  return null;
@@ -15849,7 +16613,7 @@ function computeMultiTouchGeometry(positions) {
15849
16613
  return { centroid, spread };
15850
16614
  }
15851
16615
  function useGestureDispatcher(opts) {
15852
- const { canvasRef, actions, toolsById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick, views } = opts;
16616
+ const { canvasRef, actions, toolsById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, paintedCursor, getRuleCtx, onDoubleClick, views } = opts;
15853
16617
  const onDoubleClickRef = useRef(onDoubleClick);
15854
16618
  onDoubleClickRef.current = onDoubleClick;
15855
16619
  const activeTool = useActiveToolContext();
@@ -15898,6 +16662,8 @@ function useGestureDispatcher(opts) {
15898
16662
  ];
15899
16663
  const requestRedrawRef = useRef(requestRedraw);
15900
16664
  requestRedrawRef.current = requestRedraw;
16665
+ const paintedCursorRef = useRef(paintedCursor);
16666
+ paintedCursorRef.current = paintedCursor;
15901
16667
  const lastClickRef = useRef(null);
15902
16668
  const prevActiveRef = useRef(activeTool.active);
15903
16669
  useEffect(() => {
@@ -15958,6 +16724,7 @@ function useGestureDispatcher(opts) {
15958
16724
  };
15959
16725
  const heldKeys = /* @__PURE__ */ new Set();
15960
16726
  const activePointers = /* @__PURE__ */ new Set();
16727
+ const buttonsReportedFor = /* @__PURE__ */ new Set();
15961
16728
  const pointerPositions = /* @__PURE__ */ new Map();
15962
16729
  let multiTouchStart = null;
15963
16730
  const TAP_THRESHOLD_PX = 8;
@@ -16100,8 +16867,10 @@ function useGestureDispatcher(opts) {
16100
16867
  };
16101
16868
  const onPointerDown = (e) => {
16102
16869
  if ((e.button ?? 0) !== 0) return;
16870
+ if (activePointers.has(e.pointerId)) onPointerUp(e);
16103
16871
  routeDown(e.pointerId, e.clientX, e.clientY);
16104
16872
  activePointers.add(e.pointerId);
16873
+ if (reportsButtons(e)) buttonsReportedFor.add(e.pointerId);
16105
16874
  pointerPositions.set(e.pointerId, { x: e.clientX, y: e.clientY });
16106
16875
  try {
16107
16876
  canvas?.setPointerCapture?.(e.pointerId);
@@ -16180,6 +16949,10 @@ function useGestureDispatcher(opts) {
16180
16949
  }
16181
16950
  };
16182
16951
  const onPointerMove = (e) => {
16952
+ if (activePointers.has(e.pointerId) && buttonsReportedFor.has(e.pointerId) && isMissedRelease(e)) {
16953
+ onPointerUp(e);
16954
+ return;
16955
+ }
16183
16956
  routeAt(e.pointerId, e.clientX, e.clientY);
16184
16957
  if (activePointers.has(e.pointerId)) {
16185
16958
  pointerPositions.set(e.pointerId, { x: e.clientX, y: e.clientY });
@@ -16237,20 +17010,39 @@ function useGestureDispatcher(opts) {
16237
17010
  let lastHover = null;
16238
17011
  let cursorOverridden = false;
16239
17012
  const clearHoverCursor = () => {
17013
+ paintedCursorRef.current?.()?.setOverride(null);
16240
17014
  if (cursorOverridden && canvas) {
16241
17015
  canvas.style.cursor = "";
16242
17016
  cursorOverridden = false;
16243
17017
  }
16244
17018
  };
16245
- const applyHoverCursor = (cursor) => {
16246
- if (cursor && canvas) {
16247
- canvas.style.cursor = cursor;
16248
- cursorOverridden = true;
16249
- } else {
17019
+ const applyHoverCursor = (spec) => {
17020
+ const tier = spec === null ? null : resolveCursorTier(spec);
17021
+ if (!tier || !canvas) {
17022
+ clearHoverCursor();
17023
+ return;
17024
+ }
17025
+ const painted = tier.kind === "painted" ? tier : null;
17026
+ paintedCursorRef.current?.()?.setOverride(painted);
17027
+ const cursor = tier.kind === "css" ? tier.css : "none";
17028
+ if (!cursor) {
16250
17029
  clearHoverCursor();
17030
+ return;
16251
17031
  }
17032
+ canvas.style.cursor = cursor;
17033
+ cursorOverridden = true;
16252
17034
  };
17035
+ function pushPaintedPointer() {
17036
+ const painted = paintedCursorRef.current?.();
17037
+ if (!painted?.active() || !canvas || !lastHover) return;
17038
+ const r = canvas.getBoundingClientRect();
17039
+ painted.setPointer(lastHover.clientX - r.left, lastHover.clientY - r.top);
17040
+ }
16253
17041
  function refreshHoverCursor() {
17042
+ resolveHoverCursor();
17043
+ pushPaintedPointer();
17044
+ }
17045
+ function resolveHoverCursor() {
16254
17046
  if (!canvas || !lastHover) return;
16255
17047
  if (activePointers.size > 0 || dispatcherNow().inFlight().size > 0) {
16256
17048
  applyHoverCursor(dispatcherNow().inFlightCursor());
@@ -16302,6 +17094,7 @@ function useGestureDispatcher(opts) {
16302
17094
  };
16303
17095
  const onHoverPointerLeave = () => {
16304
17096
  lastHover = null;
17097
+ paintedCursorRef.current?.()?.clearPointer();
16305
17098
  clearHoverCursor();
16306
17099
  };
16307
17100
  const onPointerUp = (e) => {
@@ -16309,6 +17102,7 @@ function useGestureDispatcher(opts) {
16309
17102
  cancelLongPress(e.pointerId);
16310
17103
  const prevSize = activePointers.size;
16311
17104
  activePointers.delete(e.pointerId);
17105
+ buttonsReportedFor.delete(e.pointerId);
16312
17106
  pointerPositions.delete(e.pointerId);
16313
17107
  const staysUnderThreshold = bufferedDown.delete(e.pointerId);
16314
17108
  if (prevSize >= 2 && activePointers.size < 2) {
@@ -16416,6 +17210,7 @@ function useGestureDispatcher(opts) {
16416
17210
  routeAt(e.pointerId, e.clientX, e.clientY);
16417
17211
  cancelLongPress(e.pointerId);
16418
17212
  activePointers.delete(e.pointerId);
17213
+ buttonsReportedFor.delete(e.pointerId);
16419
17214
  pointerPositions.delete(e.pointerId);
16420
17215
  lastPointerDown.delete(e.pointerId);
16421
17216
  bufferedDown.delete(e.pointerId);
@@ -16431,6 +17226,11 @@ function useGestureDispatcher(opts) {
16431
17226
  dispatch2(ev);
16432
17227
  routeRelease(e.pointerId);
16433
17228
  };
17229
+ const onLostCapture = (e) => {
17230
+ const pe = e;
17231
+ if (!activePointers.has(pe.pointerId)) return;
17232
+ onPointerCancel(pe);
17233
+ };
16434
17234
  const onContextMenu = (e) => {
16435
17235
  e.preventDefault();
16436
17236
  routeAt(null, e.clientX, e.clientY);
@@ -16520,6 +17320,7 @@ function useGestureDispatcher(opts) {
16520
17320
  canvas?.addEventListener("pointermove", onPointerMove);
16521
17321
  canvas?.addEventListener("pointerup", onPointerUp);
16522
17322
  canvas?.addEventListener("pointercancel", onPointerCancel);
17323
+ canvas?.addEventListener(LOST_CAPTURE_EVENT, onLostCapture);
16523
17324
  canvas?.addEventListener("contextmenu", onContextMenu);
16524
17325
  canvas?.addEventListener("dragenter", onDragOver);
16525
17326
  canvas?.addEventListener("dragover", onDragOver);
@@ -16540,6 +17341,7 @@ function useGestureDispatcher(opts) {
16540
17341
  canvas?.removeEventListener("pointermove", onPointerMove);
16541
17342
  canvas?.removeEventListener("pointerup", onPointerUp);
16542
17343
  canvas?.removeEventListener("pointercancel", onPointerCancel);
17344
+ canvas?.removeEventListener(LOST_CAPTURE_EVENT, onLostCapture);
16543
17345
  canvas?.removeEventListener("contextmenu", onContextMenu);
16544
17346
  canvas?.removeEventListener("dragenter", onDragOver);
16545
17347
  canvas?.removeEventListener("dragover", onDragOver);
@@ -16554,6 +17356,132 @@ function useGestureDispatcher(opts) {
16554
17356
  };
16555
17357
  }, [enabled, keyboard, canvasRef]);
16556
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
+ }
16557
17485
  function dist(a, b) {
16558
17486
  return Math.hypot(b.x - a.x, b.y - a.y);
16559
17487
  }
@@ -17811,7 +18739,7 @@ function validateId(id, kind) {
17811
18739
  }
17812
18740
  function defineTool(def) {
17813
18741
  validateId(def.id, "tool");
17814
- const resolveCursor = (ctx) => {
18742
+ const resolveCursor3 = (ctx) => {
17815
18743
  if (def.cursor == null) return "";
17816
18744
  return typeof def.cursor === "function" ? def.cursor(ctx) : def.cursor;
17817
18745
  };
@@ -17829,7 +18757,7 @@ function defineTool(def) {
17829
18757
  onActivate: def.onActivate,
17830
18758
  onDeactivate: def.onDeactivate,
17831
18759
  initScratch: def.initScratch ?? (() => null),
17832
- cursor: resolveCursor,
18760
+ cursor: resolveCursor3,
17833
18761
  bindings: def.bindings,
17834
18762
  overlay: def.overlay,
17835
18763
  overlayPosition: def.overlayPosition
@@ -18829,10 +19757,10 @@ function findShapeInk(node, pose, ctx) {
18829
19757
  function shapeCoversPoint(node, pose, x, y, opts = {}) {
18830
19758
  const sil = findShapeSilhouette(node, pose, { derivedPath: opts.derivedPath });
18831
19759
  if (sil === null) return true;
18832
- const ink = findShapeInk(node, pose, { scale: opts.scale }) ?? DEFAULT_INK;
19760
+ const ink2 = findShapeInk(node, pose, { scale: opts.scale }) ?? DEFAULT_INK;
18833
19761
  const inside = pathContainsPoint(sil, x, y);
18834
- if (ink.filled && inside) return true;
18835
- const reach = (inside ? ink.inset : ink.outset) + (opts.tolerance ?? 0);
19762
+ if (ink2.filled && inside) return true;
19763
+ const reach = (inside ? ink2.inset : ink2.outset) + (opts.tolerance ?? 0);
18836
19764
  return reach > 0 && strokeHitTest(sil, x, y, reach);
18837
19765
  }
18838
19766
  var TEXT_PAINTER = {
@@ -19731,6 +20659,9 @@ function transformOf(state, targetId) {
19731
20659
  identity: false
19732
20660
  };
19733
20661
  }
20662
+ function angleOf(xf) {
20663
+ return xf.identity ? 0 : Math.atan2(xf.sin, xf.cos);
20664
+ }
19734
20665
  function localToWorld(xf, lx, ly) {
19735
20666
  if (xf.identity) return { x: lx, y: ly };
19736
20667
  const dx = lx - xf.cx;
@@ -20030,11 +20961,7 @@ var DEFAULT_PAINT = {
20030
20961
  insetPx: 10
20031
20962
  };
20032
20963
  function createRotationAffordance(opts = {}) {
20033
- const {
20034
- bandPx = 24,
20035
- paint = DEFAULT_PAINT,
20036
- cursor = "grab"
20037
- } = opts;
20964
+ const { bandPx = 24, paint = DEFAULT_PAINT, cursor } = opts;
20038
20965
  const annulusPaint = (() => {
20039
20966
  if (!paint) return null;
20040
20967
  if ("paint" in paint) {
@@ -20072,7 +20999,14 @@ function createRotationAffordance(opts = {}) {
20072
20999
  },
20073
21000
  ...annulusPaint ? { paint: annulusPaint } : {},
20074
21001
  hitKind: "rotate-handle",
20075
- cursor,
21002
+ // Turned with the target, so the arc follows the selection it rotates.
21003
+ // Resolved per region rather than defaulted in the option list because
21004
+ // the angle is only known once a target is picked.
21005
+ cursor: cursor ?? {
21006
+ glyph: "rotate",
21007
+ angle: angleOf(transformOf(state, target.id)),
21008
+ fallback: "grab"
21009
+ },
20076
21010
  bind: () => ({
20077
21011
  initialScratch: {
20078
21012
  targetId: target.id,
@@ -20169,7 +21103,9 @@ function useRotateTool(adapter, options) {
20169
21103
  [overlay]
20170
21104
  );
20171
21105
  }
20172
- function useHandTool(_opts = {}) {
21106
+ function useHandTool(opts = {}) {
21107
+ const optsRef = useRef(opts);
21108
+ optsRef.current = opts;
20173
21109
  return useMemo(
20174
21110
  () => Object.assign(defineViewportTool({
20175
21111
  id: "hand",
@@ -20187,7 +21123,16 @@ function useHandTool(_opts = {}) {
20187
21123
  // viewportDragPanAction, which also owns the grabbing cursor
20188
21124
  // (`Action.activeCursor`) the tool used to declare as an `engaged`
20189
21125
  // phase entry.
20190
- 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
+ }]
20191
21136
  }),
20192
21137
  []
20193
21138
  );
@@ -20551,7 +21496,7 @@ function useRectTool() {
20551
21496
  id: "rect",
20552
21497
  capabilities: ["creates-shapes"],
20553
21498
  hookName: "useRectTool",
20554
- cursor: "crosshair",
21499
+ cursor: cursorFor("crosshairRect", { fallback: "crosshair" }),
20555
21500
  presentation: {
20556
21501
  label: "Rectangle",
20557
21502
  icon: createElement(RectIcon),
@@ -20577,7 +21522,7 @@ function useEllipseTool() {
20577
21522
  id: "ellipse",
20578
21523
  capabilities: ["creates-shapes"],
20579
21524
  hookName: "useEllipseTool",
20580
- cursor: "crosshair",
21525
+ cursor: cursorFor("crosshairEllipse", { fallback: "crosshair" }),
20581
21526
  presentation: {
20582
21527
  label: "Ellipse",
20583
21528
  group: "shape",
@@ -20600,7 +21545,7 @@ function useLineTool() {
20600
21545
  id: "line",
20601
21546
  capabilities: ["creates-shapes"],
20602
21547
  hookName: "useLineTool",
20603
- cursor: "crosshair",
21548
+ cursor: cursorFor("crosshairLine", { fallback: "crosshair" }),
20604
21549
  presentation: {
20605
21550
  label: "Line",
20606
21551
  group: "shape",
@@ -20711,7 +21656,7 @@ function usePolygonTool(options = {}) {
20711
21656
  id: "polygon",
20712
21657
  capabilities: ["creates-shapes"],
20713
21658
  hookName: "usePolygonTool",
20714
- cursor: "crosshair",
21659
+ cursor: cursorFor("crosshairPolygon", { fallback: "crosshair" }),
20715
21660
  // Registered by <ToolActionsMounter>, inside the ActionsProvider —
20716
21661
  // see `ToolDef.actions` for why the hook can't register it itself.
20717
21662
  actions: [adjustSidesAction],
@@ -20811,7 +21756,7 @@ function useStarTool(options = {}) {
20811
21756
  id: "star",
20812
21757
  capabilities: ["creates-shapes"],
20813
21758
  hookName: "useStarTool",
20814
- cursor: "crosshair",
21759
+ cursor: cursorFor("crosshairStar", { fallback: "crosshair" }),
20815
21760
  // Registered by <ToolActionsMounter>, inside the ActionsProvider —
20816
21761
  // see `ToolDef.actions` for why the hook can't register it itself.
20817
21762
  actions: [adjustPointsAction],
@@ -21448,6 +22393,20 @@ function projectBounds(b, view) {
21448
22393
  const [sx, sy] = worldToScreen(b.x, b.y, t);
21449
22394
  return { ...b, x: sx, y: sy, width: b.width * view.scale.x, height: b.height * view.scale.y };
21450
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
+ }
21451
22410
  function makeContainerAwareBoundsResolver(getPose, getBounds, getChildren, isContainer) {
21452
22411
  if (getChildren === void 0 || isContainer === void 0) {
21453
22412
  return (id) => {
@@ -21455,26 +22414,12 @@ function makeContainerAwareBoundsResolver(getPose, getBounds, getChildren, isCon
21455
22414
  return p === null ? null : getBounds(p);
21456
22415
  };
21457
22416
  }
21458
- const leavesOf = (id) => {
21459
- if (!isContainer(id)) return [id];
21460
- const out = [];
21461
- const visit = (nid) => {
21462
- const kids = getChildren(nid);
21463
- if (kids.length === 0) {
21464
- if (!isContainer(nid)) out.push(nid);
21465
- return;
21466
- }
21467
- for (const k of kids) visit(k);
21468
- };
21469
- visit(id);
21470
- return out;
21471
- };
21472
22417
  return (id) => {
21473
22418
  if (!isContainer(id)) {
21474
22419
  const p = getPose(id);
21475
22420
  return p === null ? null : getBounds(p);
21476
22421
  }
21477
- const leaves = leavesOf(id);
22422
+ const leaves = leavesOf(id, getChildren, isContainer);
21478
22423
  if (leaves.length === 0) return null;
21479
22424
  const leafBounds = [];
21480
22425
  for (const leafId of leaves) {
@@ -21848,6 +22793,111 @@ function buildSceneTree(adapter, drawOne, view, forLayer, derivedPathOf) {
21848
22793
  }
21849
22794
  return out;
21850
22795
  }
22796
+ function same(a, b) {
22797
+ if (a === b) return true;
22798
+ if (!a || !b) return false;
22799
+ return a.glyph === b.glyph && a.angle === b.angle && a.size === b.size && a.worldRadius === b.worldRadius;
22800
+ }
22801
+ function createPaintedCursorState() {
22802
+ let base = null;
22803
+ let override = null;
22804
+ let at = null;
22805
+ const subs = /* @__PURE__ */ new Set();
22806
+ const notify2 = () => {
22807
+ for (const fn of subs) fn();
22808
+ };
22809
+ return {
22810
+ setBase(cursor) {
22811
+ if (same(base, cursor)) return;
22812
+ base = cursor;
22813
+ notify2();
22814
+ },
22815
+ setOverride(cursor) {
22816
+ if (same(override, cursor)) return;
22817
+ override = cursor;
22818
+ notify2();
22819
+ },
22820
+ setPointer(x, y) {
22821
+ if (at && at.x === x && at.y === y) return;
22822
+ at = { x, y };
22823
+ if (override ?? base) notify2();
22824
+ },
22825
+ clearPointer() {
22826
+ if (at === null) return;
22827
+ at = null;
22828
+ notify2();
22829
+ },
22830
+ active() {
22831
+ return (override ?? base) !== null;
22832
+ },
22833
+ current() {
22834
+ const cursor = override ?? base;
22835
+ if (!cursor || !at) return null;
22836
+ return { cursor, at };
22837
+ },
22838
+ subscribe(fn) {
22839
+ subs.add(fn);
22840
+ return () => {
22841
+ subs.delete(fn);
22842
+ };
22843
+ }
22844
+ };
22845
+ }
22846
+ var PAINTED_CURSOR_LAYER_ID = "painted-cursor";
22847
+ var pathCache = /* @__PURE__ */ new Map();
22848
+ function pathFor(d) {
22849
+ let p = pathCache.get(d);
22850
+ if (p === void 0) {
22851
+ p = pathFromD(d);
22852
+ pathCache.set(d, p);
22853
+ }
22854
+ return p;
22855
+ }
22856
+ function commandFor2(op) {
22857
+ return {
22858
+ kind: "path",
22859
+ path: pathFor(op.d),
22860
+ ...op.fill !== void 0 ? { fill: { color: op.fill } } : {},
22861
+ ...op.stroke !== void 0 ? {
22862
+ stroke: {
22863
+ paint: { color: op.stroke.color },
22864
+ width: op.stroke.width,
22865
+ cap: "round",
22866
+ join: "round"
22867
+ }
22868
+ } : {}
22869
+ };
22870
+ }
22871
+ function createPaintedCursorLayer(state) {
22872
+ return {
22873
+ id: PAINTED_CURSOR_LAYER_ID,
22874
+ label: "Painted cursor",
22875
+ space: "screen",
22876
+ draw: (_data, view) => {
22877
+ const frame = state.current();
22878
+ if (!frame) return [];
22879
+ const { cursor, at } = frame;
22880
+ const glyph = GLYPHS[cursor.glyph];
22881
+ if (!glyph) return [];
22882
+ const scale2 = (view.scale.x + view.scale.y) / 2;
22883
+ const worldSized = cursor.worldRadius !== void 0;
22884
+ const size = worldSized ? cursorWorldSize(glyph, cursor.worldRadius, scale2) : cursor.size ?? 24;
22885
+ if (!(size > 0) || !Number.isFinite(size)) return [];
22886
+ const ops = cursorPaintOps(
22887
+ glyph,
22888
+ worldSized ? { lineWidthScale: chromeLineWidthScale(glyph, size) } : {}
22889
+ );
22890
+ const [a, b, c, d, e, f] = cursorPaintMatrix(glyph, {
22891
+ size,
22892
+ angle: cursor.angle,
22893
+ at
22894
+ });
22895
+ const transform = new Float32Array([a, b, 0, c, d, 0, e, f, 1]);
22896
+ const children = ops.map(commandFor2);
22897
+ return [{ kind: "group", transform, children }];
22898
+ }
22899
+ };
22900
+ }
21851
22901
  function mat3ToRenderMatrix(m) {
21852
22902
  return new Float32Array([m[0], m[1], 0, m[2], m[3], 0, m[4], m[5], 1]);
21853
22903
  }
@@ -22217,11 +23267,11 @@ function resolveToolsCursor(tools, ctxBase) {
22217
23267
  const id = tools.hotkeyEngaged ?? tools.active;
22218
23268
  const tool = tools.registry[id];
22219
23269
  if (!tool?.cursor) return void 0;
22220
- if (typeof tool.cursor === "string") return tool.cursor;
23270
+ if (typeof tool.cursor !== "function") return resolveCursorTier(tool.cursor);
22221
23271
  if (!ctxBase) return void 0;
22222
23272
  try {
22223
23273
  const base = ctxBase();
22224
- return tool.cursor({ ...base, scratch: null });
23274
+ return resolveCursorTier(tool.cursor({ ...base, scratch: null }));
22225
23275
  } catch {
22226
23276
  return void 0;
22227
23277
  }
@@ -22386,6 +23436,14 @@ function CanvasInner(props, ref) {
22386
23436
  viewSubsRef.current.delete(fn);
22387
23437
  };
22388
23438
  }, []);
23439
+ const paintedCursor = useMemo(() => createPaintedCursorState(), []);
23440
+ const paintedCursorLayer = useMemo(
23441
+ () => createPaintedCursorLayer(paintedCursor),
23442
+ [paintedCursor]
23443
+ );
23444
+ useEffect(() => paintedCursor.subscribe(() => {
23445
+ requestRedraw();
23446
+ }), [paintedCursor, requestRedraw]);
22389
23447
  useImperativeHandle(ref, () => ({
22390
23448
  // Named rather than read off `canvasRef` so the handle rebuilds when a
22391
23449
  // detached surface's input element arrives, which is a render later.
@@ -22398,7 +23456,8 @@ function CanvasInner(props, ref) {
22398
23456
  getView,
22399
23457
  setView,
22400
23458
  subscribeView,
22401
- getPaintedVersion
23459
+ getPaintedVersion,
23460
+ paintedCursor
22402
23461
  }), [
22403
23462
  canvasRef,
22404
23463
  ownCanvasRef,
@@ -22412,7 +23471,8 @@ function CanvasInner(props, ref) {
22412
23471
  getView,
22413
23472
  setView,
22414
23473
  subscribeView,
22415
- getPaintedVersion
23474
+ getPaintedVersion,
23475
+ paintedCursor
22416
23476
  ]);
22417
23477
  const glRendererRef = useRef(null);
22418
23478
  const layerCacheRef = useRef(/* @__PURE__ */ new Map());
@@ -22492,7 +23552,7 @@ function CanvasInner(props, ref) {
22492
23552
  return {
22493
23553
  worldX,
22494
23554
  worldY,
22495
- 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 },
22496
23556
  selection: effectiveSelectionRefForCtx.current,
22497
23557
  adapter: effectiveAdapterRefForCtx.current,
22498
23558
  applyOps: (ops, label) => {
@@ -22676,8 +23736,16 @@ function CanvasInner(props, ref) {
22676
23736
  const layersWithDebug = useMemo(() => {
22677
23737
  const withViews = viewRegistry ? [...layers, ...viewRegistry.list().map((r) => r.layer)] : layers;
22678
23738
  const base = debugSink && resolvedDebugConfig ? [...withViews, createDebugOverlayLayer({ sink: debugSink, config: resolvedDebugConfig })] : withViews;
22679
- return [...base, ...extrasRef.current];
22680
- }, [layers, debugSink, resolvedDebugConfig, extrasVersion, viewRegistry, viewRegistryVersion]);
23739
+ return [...base, ...extrasRef.current, paintedCursorLayer];
23740
+ }, [
23741
+ layers,
23742
+ debugSink,
23743
+ resolvedDebugConfig,
23744
+ extrasVersion,
23745
+ viewRegistry,
23746
+ viewRegistryVersion,
23747
+ paintedCursorLayer
23748
+ ]);
22681
23749
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
22682
23750
  const paintInputsRef = useRef({
22683
23751
  layers: layersWithDebug,
@@ -22799,8 +23867,14 @@ function CanvasInner(props, ref) {
22799
23867
  if (!renderer) return;
22800
23868
  registerShadersOnRenderer(renderer, shaders);
22801
23869
  }, [shaderIdKey]);
22802
- const toolsCursor = tools ? resolveToolsCursor(tools, toolsCtxBase) : void 0;
23870
+ const toolsCursorTier = tools ? resolveToolsCursor(tools, toolsCtxBase) : void 0;
23871
+ const toolsCursor = toolsCursorTier === void 0 ? void 0 : toolsCursorTier.kind === "css" ? toolsCursorTier.css : "none";
22803
23872
  const effectiveStyle = toolsCursor ? { ...style, cursor: toolsCursor } : style;
23873
+ const toolsPainted = toolsCursorTier?.kind === "painted" ? toolsCursorTier : null;
23874
+ const toolsPaintedKey = toolsPainted ? `${toolsPainted.glyph}|${toolsPainted.angle}|${toolsPainted.size}|${toolsPainted.worldRadius}` : "";
23875
+ useEffect(() => {
23876
+ paintedCursor.setBase(toolsPainted);
23877
+ }, [paintedCursor, toolsPaintedKey]);
22804
23878
  const layersForMoveRef = useRef(layersWithDebug);
22805
23879
  layersForMoveRef.current = layersWithDebug;
22806
23880
  useEffect(() => {
@@ -24120,10 +25194,16 @@ function createCornerResizeAffordance(opts = {}) {
24120
25194
  hitKind: c.kind,
24121
25195
  // Diagonal by fixed-corner parity: a matched-axis anchor (min-min /
24122
25196
  // max-max fixed) means the dragged corner sits on the ↘ diagonal;
24123
- // mixed axes sit on the ↗ diagonal. Not rotation-aware — a rotated
24124
- // target keeps the unrotated hint, the same policy as every
24125
- // mainstream editor short of Figma.
24126
- cursor: c.anchor.x === c.anchor.y ? "nwse-resize" : "nesw-resize",
25197
+ // mixed axes sit on the ↗ diagonal. The glyph is horizontal at angle
25198
+ // 0, so the diagonal is a half-quarter-turn either way, plus the
25199
+ // target's own rotation. The keyword is the fallback rather than the
25200
+ // answer: CSS has four of them, which cannot express a target
25201
+ // rotated to anything but a multiple of 45°.
25202
+ cursor: {
25203
+ glyph: "resize",
25204
+ angle: (c.anchor.x === c.anchor.y ? Math.PI / 4 : -Math.PI / 4) + angleOf(xf),
25205
+ fallback: c.anchor.x === c.anchor.y ? "nwse-resize" : "nesw-resize"
25206
+ },
24127
25207
  bind: () => ({
24128
25208
  initialScratch: {
24129
25209
  anchor: c.anchor,
@@ -26116,10 +27196,10 @@ function boundsOf(coords) {
26116
27196
  }
26117
27197
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
26118
27198
  }
26119
- function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize, animation) {
26120
- const wiring = useRef({ currentViewRef, onViewChange, recenter, hostSize, animation });
26121
- wiring.current = { currentViewRef, onViewChange, recenter, hostSize, animation };
26122
- 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" : ""}`;
26123
27203
  const shapeRef = useRef(null);
26124
27204
  const viewApiRef = useRef(null);
26125
27205
  if (viewApiRef.current === null || shapeRef.current !== shape) {
@@ -26138,6 +27218,10 @@ function useViewDepSource(currentViewRef, onViewChange, recenter, hostSize, anim
26138
27218
  animate: (to, opts) => wiring.current.animation.animate(to, opts),
26139
27219
  stopAnimation: () => wiring.current.animation.stop(),
26140
27220
  animationTarget: () => wiring.current.animation.target()
27221
+ } : {},
27222
+ ...decay ? {
27223
+ decay: (config) => wiring.current.decay.start(config),
27224
+ stopDecay: () => wiring.current.decay.cancel()
26141
27225
  } : {}
26142
27226
  };
26143
27227
  }
@@ -26870,7 +27954,6 @@ function useActionsPropResolver(actions) {
26870
27954
  }
26871
27955
  if (!prop) return;
26872
27956
  const unregisters = [];
26873
- const restores = [];
26874
27957
  const warnedIds = /* @__PURE__ */ new Set();
26875
27958
  for (const [slotId, entry] of Object.entries(prop)) {
26876
27959
  if (entry === null) {
@@ -26898,60 +27981,30 @@ function useActionsPropResolver(actions) {
26898
27981
  }
26899
27982
  const { id: _drop, ...rest } = entry;
26900
27983
  const merged = { ...existing, ...rest };
26901
- restores.push(() => {
26902
- reg.register(existing);
26903
- });
26904
27984
  unregisters.push(reg.register(merged));
26905
27985
  }
26906
27986
  return () => {
26907
27987
  for (const u of unregisters) u();
26908
- for (const r of restores) r();
26909
27988
  };
26910
27989
  }, [reg, actions]);
26911
27990
  }
26912
- var viewportWheelPanAction = {
26913
- id: "viewport.wheelPan",
26914
- label: "Pan (wheel)",
26915
- group: "viewport",
26916
- /**
26917
- * Plain wheel → pan; Shift+wheel → horizontal pan (axis swap).
26918
- * Cmd+wheel is claimed by `viewportZoomAction`. The matcher's strict
26919
- * modifier check keeps the bindings exclusive: omitting `mods` forbids
26920
- * any modifier, and `mods: { shift: true }` forbids the others.
26921
- */
26922
- defaultBinding: [
26923
- { spec: { kind: "wheel" }, opts: {} },
26924
- { spec: { kind: "wheel", mods: { shift: true } }, opts: { params: { swapAxis: true } } }
26925
- ],
26926
- requires: ["view"],
26927
- invoker: {
26928
- timing: "immediate",
26929
- run(deps, params) {
26930
- const view = deps.view;
26931
- if (!view) return;
26932
- const deltaX = params?.deltaX ?? 0;
26933
- const deltaY = params?.deltaY ?? 0;
26934
- const current = view.get();
26935
- const swap = params?.swapAxis === true;
26936
- const dx = swap ? (deltaX !== 0 ? deltaX : deltaY) / current.scale.x : deltaX / current.scale.x;
26937
- const dy = swap ? 0 : deltaY / current.scale.y;
26938
- view.set({ ...current, x: current.x + dx, y: current.y + dy });
26939
- }
26940
- },
26941
- enabled: () => true
26942
- };
26943
27991
  function useViewportActions(args) {
26944
27992
  const { pan, zoom, pinchZoom } = args;
26945
27993
  const reg = useActionsRegistry();
26946
27994
  const regRef = useRef(reg);
26947
27995
  regRef.current = reg;
27996
+ const panKey = typeof pan === "object" ? JSON.stringify(pan) : String(pan);
26948
27997
  const zoomKey = typeof zoom === "object" ? JSON.stringify(zoom) : String(zoom);
26949
27998
  const pinchKey = typeof pinchZoom === "object" ? JSON.stringify(pinchZoom) : String(pinchZoom);
26950
27999
  useEffect(() => {
26951
28000
  const r = regRef.current;
26952
28001
  if (!r) return;
26953
28002
  const unregisters = [];
26954
- if (pan) unregisters.push(r.register(viewportWheelPanAction));
28003
+ if (pan) {
28004
+ unregisters.push(
28005
+ r.register(makeViewportWheelPanAction(typeof pan === "object" ? pan : {}))
28006
+ );
28007
+ }
26955
28008
  if (zoom) {
26956
28009
  const zoomAction = typeof zoom === "object" ? makeViewportZoomAction(zoom) : makeViewportZoomAction();
26957
28010
  unregisters.push(r.register(zoomAction));
@@ -26966,7 +28019,7 @@ function useViewportActions(args) {
26966
28019
  return () => {
26967
28020
  for (const u of unregisters) u();
26968
28021
  };
26969
- }, [pan, zoomKey, pinchKey]);
28022
+ }, [panKey, zoomKey, pinchKey]);
26970
28023
  }
26971
28024
  function createNodeRouting() {
26972
28025
  const entries = [];
@@ -27390,13 +28443,13 @@ function SceneCanvasInner(props, ref) {
27390
28443
  const viewportRegistered = !!viewport;
27391
28444
  const inertiaEnabled = !!viewport?.inertia;
27392
28445
  const inertiaObj = typeof viewport?.inertia === "object" ? viewport.inertia : void 0;
27393
- const handToolInertia = inertiaEnabled && inertiaObj ? {
27394
- friction: inertiaObj.friction,
27395
- minSpeed: inertiaObj.minSpeed,
27396
- boundary: inertiaObj.boundary,
27397
- bounds: inertiaObj.bounds
28446
+ const handToolInertia = inertiaEnabled ? {
28447
+ friction: inertiaObj?.friction,
28448
+ minSpeed: inertiaObj?.minSpeed,
28449
+ boundary: inertiaObj?.boundary,
28450
+ bounds: inertiaObj?.bounds
27398
28451
  } : void 0;
27399
- const handTool = useHandTool(handToolInertia ? {} : {});
28452
+ const handTool = useHandTool(handToolInertia ? { inertia: handToolInertia } : {});
27400
28453
  const viewportAmbient = [];
27401
28454
  const baseRequestedTools = defaultTools ?? (toolBundle ? BUNDLE_TOOLS[toolBundle] : null) ?? (viewportRegistered ? ["select", "rotate", "hand"] : ["select", "rotate"]);
27402
28455
  const KNOWN_BUILTIN_IDS = /* @__PURE__ */ new Set([
@@ -27849,7 +28902,7 @@ function SceneCanvasInner(props, ref) {
27849
28902
  dispatcher,
27850
28903
  getActionRef,
27851
28904
  pickEvery: internalPickEvery,
27852
- viewportPanEnabled: viewport?.pan !== false,
28905
+ viewportPanEnabled: viewport?.pan ?? true,
27853
28906
  viewportZoom: resolvedViewportZoom,
27854
28907
  viewportPinchZoom: viewport?.pinchZoom ?? true,
27855
28908
  viewportRecenter: viewport?.recenter,
@@ -28060,6 +29113,10 @@ function GestureDispatcherMounter({
28060
29113
  const requestRedraw = useCallback(() => {
28061
29114
  canvasApiRef?.current?.requestRedraw?.();
28062
29115
  }, [canvasApiRef]);
29116
+ const paintedCursor = useCallback(
29117
+ () => canvasApiRef?.current?.paintedCursor,
29118
+ [canvasApiRef]
29119
+ );
28063
29120
  const views = useMemo(() => {
28064
29121
  if (!viewRegistry) return void 0;
28065
29122
  return {
@@ -28078,6 +29135,7 @@ function GestureDispatcherMounter({
28078
29135
  dispatcher,
28079
29136
  clientToWorld: clientToWorld2,
28080
29137
  requestRedraw,
29138
+ paintedCursor,
28081
29139
  getRuleCtx,
28082
29140
  onDoubleClick,
28083
29141
  ...views ? { views } : {}
@@ -28135,6 +29193,7 @@ function StandardActionsRegistrar({
28135
29193
  getActionRef.current = null;
28136
29194
  };
28137
29195
  }, [registry22, getActionRef]);
29196
+ const decayLoop = useDecayLoop();
28138
29197
  const view = useViewDepSource(
28139
29198
  currentViewRef,
28140
29199
  onViewChange,
@@ -28143,7 +29202,8 @@ function StandardActionsRegistrar({
28143
29202
  const el = canvasRef.current;
28144
29203
  return el ? { width: el.clientWidth, height: el.clientHeight } : null;
28145
29204
  },
28146
- viewAnimation
29205
+ viewAnimation,
29206
+ decayLoop
28147
29207
  );
28148
29208
  const sceneAsHistory = scene;
28149
29209
  useStandardActions({ selection, scene, view, history: sceneAsHistory });
@@ -28576,8 +29636,7 @@ function MinimapCanvasInner(props) {
28576
29636
  }, [mainView, mainViewDims, indicatorStyle]);
28577
29637
  const extraCommands = useMemo(() => [indicatorCmd], [indicatorCmd]);
28578
29638
  const localCanvasRef = useRef(null);
28579
- const dragActiveRef = useRef(false);
28580
- const capturedPointerIdRef = useRef(null);
29639
+ const sessionRef = useRef(null);
28581
29640
  const mainViewRef = useRef(mainView);
28582
29641
  const mainViewDimsRef = useRef(mainViewDims);
28583
29642
  const onMainViewChangeRef = useRef(onMainViewChange);
@@ -28606,36 +29665,18 @@ function MinimapCanvasInner(props) {
28606
29665
  }, []);
28607
29666
  const handlePointerDown = useCallback((ev) => {
28608
29667
  const canvas = localCanvasRef.current;
28609
- if (!canvas) return;
29668
+ if (!canvas || sessionRef.current) return;
28610
29669
  if (ev.pointerType === "mouse" && ev.button !== 0) return;
28611
- try {
28612
- canvas.setPointerCapture(ev.pointerId);
28613
- } catch {
28614
- }
28615
- dragActiveRef.current = true;
28616
- capturedPointerIdRef.current = ev.pointerId;
28617
- recenterFromPointer(ev);
28618
- }, [recenterFromPointer]);
28619
- const handlePointerMove = useCallback((ev) => {
28620
- if (!dragActiveRef.current) return;
28621
- if (capturedPointerIdRef.current !== null && ev.pointerId !== capturedPointerIdRef.current) {
28622
- return;
28623
- }
29670
+ const done = () => {
29671
+ sessionRef.current = null;
29672
+ };
29673
+ sessionRef.current = openPointerSession(canvas, ev, {
29674
+ onMove: recenterFromPointer,
29675
+ onEnd: done,
29676
+ onCancel: done
29677
+ });
28624
29678
  recenterFromPointer(ev);
28625
29679
  }, [recenterFromPointer]);
28626
- const releaseCapture = useCallback((ev) => {
28627
- const canvas = localCanvasRef.current;
28628
- if (canvas) {
28629
- try {
28630
- if (canvas.hasPointerCapture?.(ev.pointerId)) {
28631
- canvas.releasePointerCapture(ev.pointerId);
28632
- }
28633
- } catch {
28634
- }
28635
- }
28636
- dragActiveRef.current = false;
28637
- capturedPointerIdRef.current = null;
28638
- }, []);
28639
29680
  const setCanvasRef = useCallback((el) => {
28640
29681
  localCanvasRef.current = el;
28641
29682
  if (typeof canvasRef === "function") {
@@ -28648,16 +29689,11 @@ function MinimapCanvasInner(props) {
28648
29689
  const canvas = localCanvasRef.current;
28649
29690
  if (!canvas) return;
28650
29691
  canvas.addEventListener("pointerdown", handlePointerDown);
28651
- canvas.addEventListener("pointermove", handlePointerMove);
28652
- canvas.addEventListener("pointerup", releaseCapture);
28653
- canvas.addEventListener("pointercancel", releaseCapture);
28654
29692
  return () => {
28655
29693
  canvas.removeEventListener("pointerdown", handlePointerDown);
28656
- canvas.removeEventListener("pointermove", handlePointerMove);
28657
- canvas.removeEventListener("pointerup", releaseCapture);
28658
- canvas.removeEventListener("pointercancel", releaseCapture);
29694
+ sessionRef.current?.cancel();
28659
29695
  };
28660
- }, [handlePointerDown, handlePointerMove, releaseCapture]);
29696
+ }, [handlePointerDown]);
28661
29697
  return /* @__PURE__ */ jsx(
28662
29698
  SceneViewCanvas,
28663
29699
  {
@@ -28929,6 +29965,6 @@ function oklchToOklab(L, C, h) {
28929
29965
  return [L, C * Math.cos(h), C * Math.sin(h)];
28930
29966
  }
28931
29967
 
28932
- 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 };
28933
- //# sourceMappingURL=chunk-L5QJOOLV.js.map
28934
- //# sourceMappingURL=chunk-L5QJOOLV.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