@weasel-js/labkit 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +11 -3
  2. package/dist/_dts/{DrawCommand-DACv9lhe.d.ts → DrawCommand-BwRCZYuo.d.ts} +72 -6
  3. package/dist/_dts/{useExperimentState-eSvpwV_P.d.ts → useExperimentState-CJn2hHzd.d.ts} +6 -4
  4. package/dist/{chunk-SSMHPMDV.js → chunk-53XSBIUK.js} +12 -11
  5. package/dist/chunk-53XSBIUK.js.map +1 -0
  6. package/dist/{chunk-FBZCQXI2.js → chunk-FP5LYDVX.js} +1229 -1227
  7. package/dist/chunk-FP5LYDVX.js.map +1 -0
  8. package/dist/{chunk-OHGEOTCV.js → chunk-HNXVLKBG.js} +3 -3
  9. package/dist/{chunk-OHGEOTCV.js.map → chunk-HNXVLKBG.js.map} +1 -1
  10. package/dist/{chunk-6V7ROR7G.js → chunk-XVBXYKFJ.js} +1168 -557
  11. package/dist/chunk-XVBXYKFJ.js.map +1 -0
  12. package/dist/index.d.ts +37 -13
  13. package/dist/index.js +776 -27
  14. package/dist/index.js.map +1 -1
  15. package/dist/passthrough/weasel-canvas.d.ts +1 -1
  16. package/dist/passthrough/weasel-canvas.js +1 -1
  17. package/dist/passthrough/weasel-ui.d.ts +97 -69
  18. package/dist/passthrough/weasel-ui.js +2 -2
  19. package/dist/state/index.d.ts +2 -2
  20. package/dist/state/index.js +2 -2
  21. package/dist/styles.css +228 -287
  22. package/dist/ui/layers/index.js +3 -3
  23. package/package.json +4 -6
  24. package/src/canvas/CanvasStack.less +1 -1
  25. package/src/controls/ControlPanel.less +12 -12
  26. package/src/dragdrop/DragGhost.less +2 -2
  27. package/src/dragdrop/Palette.less +6 -6
  28. package/src/index.ts +2 -0
  29. package/src/lab/Lab.test.tsx +20 -15
  30. package/src/lab/Lab.tsx +33 -32
  31. package/src/lab/LabContext.ts +3 -3
  32. package/src/lab/LabShell.less +14 -14
  33. package/src/lab/LabShell.test.tsx +7 -20
  34. package/src/lab/LabShell.tsx +22 -13
  35. package/src/lab/WorkspaceGrid.less +1 -1
  36. package/src/lab/WorkspaceGrid.stories.tsx +4 -4
  37. package/src/lab/index.ts +1 -1
  38. package/src/lab/useSystemMode.ts +30 -0
  39. package/src/layers/LayerList.less +9 -9
  40. package/src/primitives/FpsMeter.less +5 -5
  41. package/src/primitives/ScaleIndicator.less +7 -7
  42. package/src/primitives/Sidebar.less +12 -12
  43. package/src/primitives/StatusBar.less +10 -10
  44. package/src/primitives/Toolbar.less +16 -16
  45. package/src/state/store.test.ts +17 -9
  46. package/src/state/store.ts +13 -10
  47. package/src/state/types.ts +5 -2
  48. package/src/theme/Interstellar.stories.less +20 -20
  49. package/src/theme/Interstellar.stories.tsx +44 -59
  50. package/src/theme/base.less +26 -26
  51. package/src/theme/interstellar.test.ts +30 -0
  52. package/src/theme/interstellar.tokens.json +57 -0
  53. package/src/theme/interstellar.ts +9 -0
  54. package/src/ui/layers/LayerStack.less +21 -21
  55. package/src/ui/properties/CurveField.less +5 -5
  56. package/src/ui/properties/EffectCard.tsx +2 -2
  57. package/src/ui/properties/PropertyGroup.less +6 -6
  58. package/src/ui/properties/PropertyPanel.less +70 -70
  59. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +1 -1
  60. package/src/ui/properties/storyLayouts.tsx +1 -1
  61. package/src/workspace/Workspace.less +14 -14
  62. package/src/workspace/Workspace.stories.tsx +2 -2
  63. package/src/workspace/Workspace.test.tsx +2 -2
  64. package/dist/chunk-6V7ROR7G.js.map +0 -1
  65. package/dist/chunk-FBZCQXI2.js.map +0 -1
  66. package/dist/chunk-SSMHPMDV.js.map +0 -1
  67. package/dist/theme-interstellar.css +0 -25
  68. package/dist/theme-light.css +0 -13
  69. package/src/theme/interstellar.less +0 -34
  70. package/src/theme/light.less +0 -14
  71. package/src/theme/tokens.less +0 -85
@@ -16,7 +16,7 @@ var GESTURE_DESCRIPTORS = [
16
16
  { name: "pointerDown", hasTarget: true },
17
17
  { name: "dblTap", hasTarget: true },
18
18
  { name: "drag", hasTarget: true },
19
- { name: "wheel", hasTarget: false, arg: { name: "direction", values: ["up", "down", "*"], default: "*" } },
19
+ { name: "wheel", hasTarget: true, arg: { name: "direction", values: ["up", "down", "*"], default: "*" } },
20
20
  { name: "keyDown", hasTarget: false, arg: { name: "key", values: "free" } },
21
21
  { name: "keyUp", hasTarget: false, arg: { name: "key", values: "free" } },
22
22
  { name: "keyHeld", hasTarget: false, arg: { name: "key", values: "free" } },
@@ -388,12 +388,12 @@ function checkKey(req, held) {
388
388
  if (req === "forbidden") return !held;
389
389
  return true;
390
390
  }
391
- function matchKey(eventKey, specKey) {
391
+ function matchKey(eventKey, specKey2) {
392
392
  const lower = eventKey.toLowerCase();
393
- if (Array.isArray(specKey)) {
394
- return specKey.some((k) => k.toLowerCase() === lower);
393
+ if (Array.isArray(specKey2)) {
394
+ return specKey2.some((k) => k.toLowerCase() === lower);
395
395
  }
396
- return specKey.toLowerCase() === lower;
396
+ return specKey2.toLowerCase() === lower;
397
397
  }
398
398
  function affordanceKindOf(target) {
399
399
  if (typeof target !== "object" || target === null) return void 0;
@@ -488,7 +488,7 @@ function matchSpec(e, spec, isMac, phaseCtx) {
488
488
  const direction = spec.direction ?? "*";
489
489
  if (direction === "up" && !(e.deltaY < 0)) return false;
490
490
  if (direction === "down" && !(e.deltaY > 0)) return false;
491
- return true;
491
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
492
492
  }
493
493
  case "click": {
494
494
  if (e.kind !== "click") return false;
@@ -498,12 +498,12 @@ function matchSpec(e, spec, isMac, phaseCtx) {
498
498
  case "doubleClick": {
499
499
  if (e.kind !== "doubleclick") return false;
500
500
  if (!matchModifiers(e, spec.mods, isMac)) return false;
501
- return matchTarget(e.target, spec.target, e.bodyTarget, e.bodyKind);
501
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
502
502
  }
503
503
  case "contextMenu": {
504
504
  if (e.kind !== "contextmenu") return false;
505
505
  if (!matchModifiers(e, spec.mods, isMac)) return false;
506
- return matchTarget(e.target, spec.target, e.bodyTarget, e.bodyKind);
506
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
507
507
  }
508
508
  case "longPress": {
509
509
  if (e.kind !== "longpress") return false;
@@ -547,8 +547,6 @@ function matchSpec(e, spec, isMac, phaseCtx) {
547
547
  }
548
548
  }
549
549
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
550
-
551
- // ../core/dist/chunk-6DAUIMTO.js
552
550
  var routing_exports = {};
553
551
  __export(routing_exports, {
554
552
  GESTURE_DESCRIPTORS: () => GESTURE_DESCRIPTORS,
@@ -597,7 +595,11 @@ function defineTool(def) {
597
595
  };
598
596
  return {
599
597
  id: def.id,
600
- capabilities: def.capabilities,
598
+ eligibility: {
599
+ focus: true,
600
+ capabilities: def.capabilities,
601
+ ...def.hotkey ? { offhand: def.hotkey } : {}
602
+ },
601
603
  actions: def.actions,
602
604
  def,
603
605
  presentation: def.presentation,
@@ -683,6 +685,255 @@ function argOf(spec, fallback) {
683
685
  if ("direction" in spec) return spec.direction ?? fallback;
684
686
  return fallback;
685
687
  }
688
+ var DepRegistryContext = createContext(null);
689
+ function DepRegistryProvider({ children }) {
690
+ const sourcesRef = useRef(/* @__PURE__ */ new Map());
691
+ const registry4 = useMemo(() => ({
692
+ register: (name, source) => {
693
+ sourcesRef.current.set(name, source);
694
+ return () => {
695
+ sourcesRef.current.delete(name);
696
+ };
697
+ },
698
+ get: (name) => sourcesRef.current.get(name)?.()
699
+ }), []);
700
+ return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry4, children });
701
+ }
702
+ function useDepRegistry() {
703
+ const r = useContext(DepRegistryContext);
704
+ if (r === null) {
705
+ throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
706
+ }
707
+ return r;
708
+ }
709
+ function useOptionalDepRegistry() {
710
+ return useContext(DepRegistryContext);
711
+ }
712
+ function useDepSource(name, source) {
713
+ const registry4 = useDepRegistry();
714
+ const sourceRef = useRef(source);
715
+ sourceRef.current = source;
716
+ useEffect(() => {
717
+ return registry4.register(name, () => sourceRef.current());
718
+ }, [name, registry4]);
719
+ }
720
+ var DEV = (() => {
721
+ try {
722
+ return Boolean(import.meta.env?.DEV);
723
+ } catch {
724
+ return false;
725
+ }
726
+ })();
727
+ function buildDepsFromRequires(action, depRegistry) {
728
+ const requires = action.requires ?? [];
729
+ const entries = requires.map((name) => [name, depRegistry.get(name)]);
730
+ const deps = Object.fromEntries(entries);
731
+ if (DEV) {
732
+ const declared = new Set(requires);
733
+ return new Proxy(deps, {
734
+ get(target, prop, receiver) {
735
+ if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
736
+ const message = `[weasel:dispatcher] action "${action.id}" read deps.${prop} but did not declare it in \`requires\`. Add \`requires: [...'${prop}']\` to the descriptor or the dep will be undefined at runtime.`;
737
+ console.warn(message);
738
+ throw new Error(message);
739
+ }
740
+ return Reflect.get(target, prop, receiver);
741
+ }
742
+ });
743
+ }
744
+ return deps;
745
+ }
746
+ function actionBindings(action) {
747
+ const gs = action.defaultBinding;
748
+ if (!gs) return [];
749
+ const raw = Array.isArray(gs) ? gs : [gs];
750
+ return raw.map((entry) => {
751
+ const isBoundObj = !("kind" in entry);
752
+ const spec = isBoundObj ? entry.spec : entry;
753
+ const opts = isBoundObj ? entry.opts : void 0;
754
+ return { spec, actionId: action.id, ...opts !== void 0 ? { opts } : {} };
755
+ });
756
+ }
757
+ var ActionDisabledReason = {
758
+ SelectionRequired: "selection-required",
759
+ NotApplicable: "not-applicable",
760
+ /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
761
+ PredicateThrew: "predicate-threw"
762
+ };
763
+ var enabledSlowWarned = /* @__PURE__ */ new Set();
764
+ var enabledThrewWarned = /* @__PURE__ */ new Set();
765
+ var ENABLED_BUDGET_MS = 4;
766
+ function evaluateEnabled(action, deps) {
767
+ if (!action.enabled) return { enabled: true };
768
+ const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
769
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
770
+ const start = isDev ? now() : 0;
771
+ try {
772
+ const result = action.enabled(deps);
773
+ if (isDev) {
774
+ const elapsed = now() - start;
775
+ if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
776
+ enabledSlowWarned.add(action.id);
777
+ console.warn(
778
+ `weasel actions: enabled() for action "${action.id}" took ${elapsed.toFixed(2)}ms (budget ${ENABLED_BUDGET_MS}ms). The predicate must be pure and fast \u2014 see Action.enabled JSDoc.`
779
+ );
780
+ }
781
+ }
782
+ if (result === true) return { enabled: true };
783
+ return { enabled: false, reason: result };
784
+ } catch (e) {
785
+ if (isDev && !enabledThrewWarned.has(action.id)) {
786
+ enabledThrewWarned.add(action.id);
787
+ console.warn(
788
+ `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
789
+ e
790
+ );
791
+ }
792
+ return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
793
+ }
794
+ }
795
+ function validateActionId(id) {
796
+ if (id.length === 0) {
797
+ throw new Error(`weasel: action id may not be empty`);
798
+ }
799
+ if (RESERVED_ID_PREFIXES.has(id[0])) {
800
+ throw new Error(
801
+ `weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
802
+ );
803
+ }
804
+ if (RESERVED_ID_NAMES.has(id)) {
805
+ throw new Error(
806
+ `weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
807
+ );
808
+ }
809
+ }
810
+ function validateActionDefaultBinding(action) {
811
+ const gs = action.defaultBinding;
812
+ if (!gs) return;
813
+ const entries = Array.isArray(gs) ? gs : [gs];
814
+ for (const entry of entries) {
815
+ const spec = "kind" in entry ? entry : entry.spec;
816
+ const phase = spec.phase;
817
+ if (phase === void 0) continue;
818
+ const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
819
+ for (const atom of atoms) {
820
+ if (atom.channel === "&") {
821
+ throw new Error(
822
+ `weasel: action "${action.id}" defaultBinding uses '&' channel which has no owning tool to resolve to. Use a named tool channel (e.g. '[rect:engaged]') or '*' instead, or move this binding to the tool's own Tool.bindings.`
823
+ );
824
+ }
825
+ }
826
+ }
827
+ }
828
+ var ActionsContext = createContext(null);
829
+ function ActionsProvider({ children }) {
830
+ const actionsRef = useRef(/* @__PURE__ */ new Map());
831
+ const versionRef = useRef(0);
832
+ const cachedRef = useRef([]);
833
+ const cachedVerRef = useRef(-1);
834
+ const listenersRef = useRef(/* @__PURE__ */ new Set());
835
+ const depReg = useOptionalDepRegistry();
836
+ const depRegRef = useRef(depReg);
837
+ depRegRef.current = depReg;
838
+ const wiredDepRegRef = useRef(null);
839
+ const dispatcherRef = useRef(null);
840
+ const registry4 = useMemo(() => {
841
+ const snapshot = () => {
842
+ const v = versionRef.current;
843
+ if (cachedVerRef.current === v) return cachedRef.current;
844
+ const out = Object.freeze(Array.from(actionsRef.current.values()));
845
+ cachedRef.current = out;
846
+ cachedVerRef.current = v;
847
+ return out;
848
+ };
849
+ const notify2 = () => {
850
+ for (const l of listenersRef.current) {
851
+ try {
852
+ l();
853
+ } catch (err) {
854
+ console.error("weasel ActionsRegistry: subscriber threw", err);
855
+ }
856
+ }
857
+ };
858
+ return {
859
+ register: (action) => {
860
+ validateActionId(action.id);
861
+ validateActionDefaultBinding(action);
862
+ actionsRef.current.set(action.id, action);
863
+ versionRef.current++;
864
+ notify2();
865
+ return () => {
866
+ const cur = actionsRef.current.get(action.id);
867
+ if (cur === action) {
868
+ actionsRef.current.delete(action.id);
869
+ versionRef.current++;
870
+ notify2();
871
+ }
872
+ };
873
+ },
874
+ unregister: (id) => {
875
+ if (actionsRef.current.delete(id)) {
876
+ versionRef.current++;
877
+ notify2();
878
+ }
879
+ },
880
+ list: () => snapshot(),
881
+ trigger: (id, params) => {
882
+ const a = actionsRef.current.get(id);
883
+ if (!a) return false;
884
+ try {
885
+ if (a.invoker && a.invoker.timing === "immediate") {
886
+ const r = wiredDepRegRef.current ?? depRegRef.current;
887
+ const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
888
+ selection: r.get("selection"),
889
+ scene: r.get("scene"),
890
+ history: r.get("history"),
891
+ view: r.get("view"),
892
+ pointer: r.get("pointer"),
893
+ activeTool: r.get("activeTool"),
894
+ booleansAdapter: r.get("booleansAdapter")
895
+ };
896
+ a.invoker.run(deps, params);
897
+ }
898
+ } catch (err) {
899
+ console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
900
+ }
901
+ return true;
902
+ },
903
+ subscribe: (listener) => {
904
+ listenersRef.current.add(listener);
905
+ return () => {
906
+ listenersRef.current.delete(listener);
907
+ };
908
+ },
909
+ setDispatcher: (d) => {
910
+ dispatcherRef.current = d;
911
+ },
912
+ setDepRegistry: (r) => {
913
+ wiredDepRegRef.current = r;
914
+ },
915
+ begin: (id, params) => {
916
+ const disp = dispatcherRef.current;
917
+ if (!disp) return null;
918
+ const r = wiredDepRegRef.current ?? depRegRef.current;
919
+ const deps = r ? {
920
+ selection: r.get("selection"),
921
+ scene: r.get("scene"),
922
+ history: r.get("history"),
923
+ view: r.get("view"),
924
+ pointer: r.get("pointer"),
925
+ activeTool: r.get("activeTool"),
926
+ booleansAdapter: r.get("booleansAdapter")
927
+ } : {};
928
+ return disp.beginUiOngoing(id, deps, params);
929
+ }
930
+ };
931
+ }, []);
932
+ return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry4, children });
933
+ }
934
+ function useActionsRegistry() {
935
+ return useContext(ActionsContext);
936
+ }
686
937
  function findConflicts(tools) {
687
938
  const entries = buildRouteRegistry(tools);
688
939
  const groups = /* @__PURE__ */ new Map();
@@ -724,9 +975,15 @@ function predicateId(pred) {
724
975
  }
725
976
  return id;
726
977
  }
978
+ function actionAsTool(action) {
979
+ return { id: action.id, eligibility: {}, bindings: actionBindings(action) };
980
+ }
727
981
  function findScopedConflicts(scopes) {
728
982
  const registry4 = Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry);
729
- const ambient = scopes.ambient ?? [];
983
+ const actions = scopes.actions ?? [];
984
+ const ambientActions = actions.filter((a) => a.scope !== "hotkey").map(actionAsTool);
985
+ const hotkeyActions = actions.filter((a) => a.scope === "hotkey").map(actionAsTool);
986
+ const ambient = [...scopes.ambient ?? [], ...ambientActions];
730
987
  const out = [];
731
988
  const seen = /* @__PURE__ */ new Set();
732
989
  const add = (conflicts) => {
@@ -737,12 +994,13 @@ function findScopedConflicts(scopes) {
737
994
  out.push(c);
738
995
  }
739
996
  };
740
- for (const tool of [...registry4, ...ambient]) add(findConflicts([tool]));
997
+ for (const tool of [...registry4, ...scopes.ambient ?? []]) add(findConflicts([tool]));
741
998
  if (ambient.length > 1) add(findConflicts(ambient));
742
- const hotkeyTools = registry4.filter(
743
- (t) => t.def?.hotkey !== void 0
744
- );
745
- if (hotkeyTools.length > 1) add(findConflicts(hotkeyTools));
999
+ const hotkey = [
1000
+ ...registry4.filter((t) => t.def?.hotkey !== void 0),
1001
+ ...hotkeyActions
1002
+ ];
1003
+ if (hotkey.length > 1) add(findConflicts(hotkey));
746
1004
  return out;
747
1005
  }
748
1006
  function formatConflict(conflict) {
@@ -882,7 +1140,7 @@ function transformCoords(coords, m) {
882
1140
  return out;
883
1141
  }
884
1142
 
885
- // ../core/dist/chunk-GLZLSPGJ.js
1143
+ // ../core/dist/chunk-6SHZHXXR.js
886
1144
  function resolveUnit(v, unitSystem) {
887
1145
  if (typeof v === "number") return v;
888
1146
  if (!unitSystem) {
@@ -1453,6 +1711,28 @@ var RECT_POSE_DESCRIPTOR = {
1453
1711
  translate: RECT_POSE_DESCRIPTOR.translate,
1454
1712
  intersectsRect: RECT_POSE_DESCRIPTOR.intersectsRect,
1455
1713
  lerp: RECT_POSE_DESCRIPTOR.lerp});
1714
+ function remapRotatedLeaf(pose, src, dst) {
1715
+ const sx = src.width === 0 ? 1 : dst.width / src.width;
1716
+ const sy = src.height === 0 ? 1 : dst.height / src.height;
1717
+ const theta = pose.rotation ?? 0;
1718
+ const cos = Math.cos(theta);
1719
+ const sin = Math.sin(theta);
1720
+ const ux = sx * cos, uy = sy * sin;
1721
+ const vx = -sx * sin, vy = sy * cos;
1722
+ const width = pose.width * Math.hypot(ux, uy);
1723
+ const height = pose.height * Math.hypot(vx, vy);
1724
+ const rotation = Math.atan2(uy, ux);
1725
+ const cxNew = dst.x + (pose.x + pose.width / 2 - src.x) * sx;
1726
+ const cyNew = dst.y + (pose.y + pose.height / 2 - src.y) * sy;
1727
+ return {
1728
+ ...pose,
1729
+ x: cxNew - width / 2,
1730
+ y: cyNew - height / 2,
1731
+ width,
1732
+ height,
1733
+ rotation
1734
+ };
1735
+ }
1456
1736
  var pathPoseDescriptor = {
1457
1737
  getBounds: (path) => boundsOfPath(path),
1458
1738
  remapBounds: (path, src, dst) => {
@@ -1549,14 +1829,29 @@ var RECT_ORIGIN_PROJECTION = {
1549
1829
  getOrigin: (p) => ({ x: p.x, y: p.y }),
1550
1830
  translate: (p, dx, dy) => ({ ...p, x: p.x + dx, y: p.y + dy })
1551
1831
  };
1552
- function meanScale(s) {
1553
- return Math.sqrt(s.x * s.y);
1832
+ var MIN_SCALE = 1e-9;
1833
+ function pxExtent(px, scale2) {
1834
+ return {
1835
+ x: px / Math.max(MIN_SCALE, Math.abs(scale2.x)),
1836
+ y: px / Math.max(MIN_SCALE, Math.abs(scale2.y))
1837
+ };
1838
+ }
1839
+ function scaleDelta(dx, dy, scale2) {
1840
+ return { x: dx * scale2.x, y: dy * scale2.y };
1841
+ }
1842
+ function withinPxBox(dx, dy, px, scale2) {
1843
+ const s = scaleDelta(dx, dy, scale2);
1844
+ return Math.abs(s.x) <= px && Math.abs(s.y) <= px;
1845
+ }
1846
+ function withinPxRadius(dx, dy, px, scale2) {
1847
+ const s = scaleDelta(dx, dy, scale2);
1848
+ return s.x * s.x + s.y * s.y <= px * px;
1554
1849
  }
1555
1850
 
1556
1851
  // ../core/dist/chunk-GVCNT7UH.js
1557
1852
  var FACTORIES = /* @__PURE__ */ new Map();
1558
- function registerOpFactory(name, build) {
1559
- FACTORIES.set(name, build);
1853
+ function registerOpFactory(name, build2) {
1854
+ FACTORIES.set(name, build2);
1560
1855
  }
1561
1856
  function rebuildOp(name, args) {
1562
1857
  const f = FACTORIES.get(name);
@@ -1593,7 +1888,7 @@ function createDeleteOp(args) {
1593
1888
  }
1594
1889
  registerOpFactory("delete", (args) => createDeleteOp(args));
1595
1890
 
1596
- // ../core/dist/chunk-BPRHGTLQ.js
1891
+ // ../core/dist/chunk-UB6A6L77.js
1597
1892
  function createTransformOp(args) {
1598
1893
  const { id, from, to, label, coalesceKey = `transform:${id}` } = args;
1599
1894
  return {
@@ -1674,7 +1969,7 @@ function snap(strategy, opts = {}) {
1674
1969
  };
1675
1970
  }
1676
1971
 
1677
- // ../core/dist/chunk-CMBE45MT.js
1972
+ // ../core/dist/chunk-Z3KNFTTS.js
1678
1973
  function lockAspectWithModifier(opts = {}) {
1679
1974
  const { key = "shift" } = opts;
1680
1975
  return {
@@ -1870,15 +2165,167 @@ function materializePaste(adapter, snapshot, opts) {
1870
2165
  return { ops, newIds, created };
1871
2166
  }
1872
2167
 
1873
- // ../core/dist/chunk-Y52N27PF.js
2168
+ // ../core/dist/chunk-IO5Z75X4.js
2169
+ var counter = 0;
1874
2170
  var registry = /* @__PURE__ */ new Map();
1875
2171
  function getTexture(id) {
1876
2172
  return registry.get(id) ?? null;
1877
2173
  }
1878
-
1879
- // ../font/dist/index.js
1880
- var subscribers = /* @__PURE__ */ new Set();
1881
- var generation = 0;
2174
+ function registerTexture(image) {
2175
+ const id = `tex_${++counter}`;
2176
+ registry.set(id, { source: image });
2177
+ return { id };
2178
+ }
2179
+ function tileSize(spec) {
2180
+ if (spec.size !== void 0) return spec.size;
2181
+ switch (spec.tile) {
2182
+ case "hatch":
2183
+ return 5;
2184
+ case "crosshatch":
2185
+ return 6;
2186
+ case "dots":
2187
+ return 6;
2188
+ case "chunks":
2189
+ return 48;
2190
+ default:
2191
+ return 8;
2192
+ }
2193
+ }
2194
+ function mulberry32(seed) {
2195
+ let s = seed | 0;
2196
+ return () => {
2197
+ s = s + 1831565813 | 0;
2198
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
2199
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2200
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2201
+ };
2202
+ }
2203
+ function tileGeometry(spec) {
2204
+ const s = tileSize(spec);
2205
+ const { color } = spec;
2206
+ switch (spec.tile) {
2207
+ case "hatch": {
2208
+ const width = spec.lineWidth ?? 1;
2209
+ return { size: s, shapes: forwardDiagonals(s, width, color) };
2210
+ }
2211
+ case "crosshatch": {
2212
+ const width = spec.lineWidth ?? 0.8;
2213
+ return {
2214
+ size: s,
2215
+ shapes: [...forwardDiagonals(s, width, color), ...backwardDiagonals(s, width, color)]
2216
+ };
2217
+ }
2218
+ case "dots": {
2219
+ const r = spec.radius ?? 1;
2220
+ return { size: s, shapes: [{ kind: "circle", cx: s / 2, cy: s / 2, r, color }] };
2221
+ }
2222
+ case "chunks": {
2223
+ const density = spec.density ?? 0.35;
2224
+ const chunkSize = spec.chunkSize ?? 3;
2225
+ const rand = mulberry32(spec.seed ?? 42);
2226
+ const count = Math.round(s * s * density / (chunkSize * chunkSize));
2227
+ const shapes = [];
2228
+ if (spec.bg !== void 0) {
2229
+ shapes.push({ kind: "rect", x: 0, y: 0, width: s, height: s, color: spec.bg });
2230
+ }
2231
+ for (let i = 0; i < count; i++) {
2232
+ const cx = rand() * s;
2233
+ const cy = rand() * s;
2234
+ const w = chunkSize * (0.5 + rand());
2235
+ const h = chunkSize * (0.5 + rand());
2236
+ const rotation = rand() * Math.PI;
2237
+ shapes.push({ kind: "ellipse", cx, cy, rx: w / 2, ry: h / 2, rotation, color });
2238
+ }
2239
+ return { size: s, shapes };
2240
+ }
2241
+ default:
2242
+ return { size: s, shapes: [] };
2243
+ }
2244
+ }
2245
+ function forwardDiagonals(s, width, color) {
2246
+ return [
2247
+ { kind: "line", x1: -1, y1: s + 1, x2: s + 1, y2: -1, width, color },
2248
+ { kind: "line", x1: -1, y1: 1, x2: 1, y2: -1, width, color },
2249
+ { kind: "line", x1: s - 1, y1: s + 1, x2: s + 1, y2: s - 1, width, color }
2250
+ ];
2251
+ }
2252
+ function backwardDiagonals(s, width, color) {
2253
+ return [
2254
+ { kind: "line", x1: -1, y1: -1, x2: s + 1, y2: s + 1, width, color },
2255
+ { kind: "line", x1: s - 1, y1: -1, x2: s + 1, y2: 1, width, color },
2256
+ { kind: "line", x1: -1, y1: s - 1, x2: 1, y2: s + 1, width, color }
2257
+ ];
2258
+ }
2259
+ function drawTileShapes(oc, geometry) {
2260
+ for (const shape of geometry.shapes) {
2261
+ if (shape.kind === "line") {
2262
+ oc.strokeStyle = shape.color;
2263
+ oc.lineWidth = shape.width;
2264
+ oc.beginPath();
2265
+ oc.moveTo(shape.x1, shape.y1);
2266
+ oc.lineTo(shape.x2, shape.y2);
2267
+ oc.stroke();
2268
+ continue;
2269
+ }
2270
+ oc.fillStyle = shape.color;
2271
+ if (shape.kind === "rect") {
2272
+ oc.fillRect(shape.x, shape.y, shape.width, shape.height);
2273
+ continue;
2274
+ }
2275
+ oc.beginPath();
2276
+ if (shape.kind === "circle") {
2277
+ oc.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);
2278
+ } else {
2279
+ oc.ellipse(shape.cx, shape.cy, shape.rx, shape.ry, shape.rotation, 0, Math.PI * 2);
2280
+ }
2281
+ oc.fill();
2282
+ }
2283
+ }
2284
+ function createTilePattern(opts) {
2285
+ let off;
2286
+ try {
2287
+ off = new OffscreenCanvas(opts.size, opts.size);
2288
+ } catch {
2289
+ if (typeof document === "undefined") return null;
2290
+ const c = document.createElement("canvas");
2291
+ c.width = opts.size;
2292
+ c.height = opts.size;
2293
+ off = c;
2294
+ }
2295
+ const oc = off.getContext("2d");
2296
+ if (!oc) return null;
2297
+ opts.draw(oc, opts.size);
2298
+ let bitmap;
2299
+ if (typeof off.transferToImageBitmap === "function") {
2300
+ bitmap = off.transferToImageBitmap();
2301
+ } else {
2302
+ return null;
2303
+ }
2304
+ return registerTexture(bitmap);
2305
+ }
2306
+ function build(spec) {
2307
+ const geometry = tileGeometry(spec);
2308
+ return createTilePattern({
2309
+ size: tileSize(spec),
2310
+ draw: (oc) => drawTileShapes(oc, geometry)
2311
+ });
2312
+ }
2313
+ function hatch(params) {
2314
+ return build({ tile: "hatch", ...params });
2315
+ }
2316
+ function crosshatch(params) {
2317
+ return build({ tile: "crosshatch", ...params });
2318
+ }
2319
+ function dots(params) {
2320
+ return build({ tile: "dots", ...params });
2321
+ }
2322
+ function chunks(params) {
2323
+ return build({ tile: "chunks", ...params });
2324
+ }
2325
+
2326
+ // ../font/dist/index.js
2327
+ var subscribers = /* @__PURE__ */ new Set();
2328
+ var generation = 0;
1882
2329
  function glyphGeneration() {
1883
2330
  return generation;
1884
2331
  }
@@ -2012,8 +2459,8 @@ function alphaToSdf(alpha, width, height, radius, cutoff) {
2012
2459
  edt2d(gridInner, width, height, f, d, v, z);
2013
2460
  const out = new Uint8Array(n);
2014
2461
  for (let i = 0; i < n; i++) {
2015
- const dist4 = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]);
2016
- const byte = Math.round(255 - 255 * (dist4 / radius + cutoff));
2462
+ const dist3 = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]);
2463
+ const byte = Math.round(255 - 255 * (dist3 / radius + cutoff));
2017
2464
  out[i] = byte < 0 ? 0 : byte > 255 ? 255 : byte;
2018
2465
  }
2019
2466
  return out;
@@ -2219,17 +2666,17 @@ function dynamicPageTextureId(page) {
2219
2666
  return `weasel-dyn-sdf-page-${page}`;
2220
2667
  }
2221
2668
  var uploadedVersions = /* @__PURE__ */ new WeakMap();
2222
- function syncDynamicPageTexture(cache6, pageIndex) {
2669
+ function syncDynamicPageTexture(cache7, pageIndex) {
2223
2670
  const page = pages[pageIndex];
2224
2671
  if (!page) return false;
2225
2672
  const id = dynamicPageTextureId(pageIndex);
2226
- let seen = uploadedVersions.get(cache6);
2673
+ let seen = uploadedVersions.get(cache7);
2227
2674
  if (!seen) {
2228
2675
  seen = /* @__PURE__ */ new Map();
2229
- uploadedVersions.set(cache6, seen);
2676
+ uploadedVersions.set(cache7, seen);
2230
2677
  }
2231
- if (!cache6.has(id)) {
2232
- cache6.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
2678
+ if (!cache7.has(id)) {
2679
+ cache7.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
2233
2680
  seen.set(pageIndex, page.version);
2234
2681
  return true;
2235
2682
  }
@@ -2242,7 +2689,7 @@ function syncDynamicPageTexture(cache6, pageIndex) {
2242
2689
  const src = (patch.y + row) * PAGE_SIZE + patch.x;
2243
2690
  tight.set(page.data.subarray(src, src + patch.w), row * patch.w);
2244
2691
  }
2245
- cache6.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
2692
+ cache7.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
2246
2693
  }
2247
2694
  seen.set(pageIndex, page.version);
2248
2695
  return true;
@@ -2639,8 +3086,8 @@ function eligibleForMode(mode, toolTags) {
2639
3086
  }
2640
3087
  return false;
2641
3088
  }
2642
- function eligibleTool(reg, tool) {
2643
- return eligibleForMode(reg.current(), tool.capabilities ?? []);
3089
+ function eligibleContribution(reg, entry) {
3090
+ return eligibleForMode(reg.current(), entry.eligibility?.capabilities ?? []);
2644
3091
  }
2645
3092
 
2646
3093
  // ../history/dist/index.js
@@ -3009,7 +3456,7 @@ function serialToEntry(se, custom, logger) {
3009
3456
  };
3010
3457
  }
3011
3458
 
3012
- // ../core/dist/chunk-DLV5F4NY.js
3459
+ // ../core/dist/chunk-D2C6K6FO.js
3013
3460
  function worldToScreen(worldX, worldY, view) {
3014
3461
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
3015
3462
  }
@@ -3260,6 +3707,58 @@ var GRAD_FILL_UNIFORMS = [
3260
3707
  "u_gradAngle"
3261
3708
  ];
3262
3709
  var GRAD_FILL_ATTRIBUTES = ["a_position"];
3710
+ var PATTERN_VERT_SRC = (
3711
+ /* glsl */
3712
+ `#version 300 es
3713
+ in vec2 a_position;
3714
+ uniform mat3 u_proj;
3715
+ uniform mat3 u_model;
3716
+ uniform mat3 u_worldInv;
3717
+ out vec2 v_world;
3718
+ void main() {
3719
+ vec3 screen = u_model * vec3(a_position, 1.0);
3720
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
3721
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
3722
+ vec3 world = u_worldInv * vec3(screen.xy, 1.0);
3723
+ v_world = world.xy;
3724
+ }
3725
+ `
3726
+ );
3727
+ var PATTERN_FRAG_SRC = (
3728
+ /* glsl */
3729
+ `#version 300 es
3730
+ precision highp float;
3731
+ in vec2 v_world;
3732
+ uniform sampler2D u_sampler;
3733
+ uniform vec2 u_tileOrigin;
3734
+ uniform vec2 u_tileSize;
3735
+ uniform float u_opacity;
3736
+ uniform float u_alpha;
3737
+ uniform mat4 u_colorMatrix;
3738
+ uniform vec4 u_colorBias;
3739
+ out vec4 outColor;
3740
+ void main() {
3741
+ vec2 uv = (v_world - u_tileOrigin) / max(u_tileSize, vec2(0.0001));
3742
+ vec4 texel = texture(u_sampler, uv);
3743
+ vec4 mapped = clamp(u_colorMatrix * texel + u_colorBias, 0.0, 1.0);
3744
+ float a = mapped.a * u_opacity * u_alpha;
3745
+ outColor = vec4(mapped.rgb * a, a);
3746
+ }
3747
+ `
3748
+ );
3749
+ var PATTERN_FILL_UNIFORMS = [
3750
+ "u_proj",
3751
+ "u_model",
3752
+ "u_worldInv",
3753
+ "u_sampler",
3754
+ "u_tileOrigin",
3755
+ "u_tileSize",
3756
+ "u_opacity",
3757
+ "u_alpha",
3758
+ "u_colorMatrix",
3759
+ "u_colorBias"
3760
+ ];
3761
+ var PATTERN_FILL_ATTRIBUTES = ["a_position"];
3263
3762
  var GLMeshCache = class {
3264
3763
  constructor(gl, aPositionLoc) {
3265
3764
  this.gl = gl;
@@ -3382,17 +3881,20 @@ var GLTextureCache = class {
3382
3881
  has(id) {
3383
3882
  return this.map.has(id);
3384
3883
  }
3385
- upload(id, source) {
3884
+ /** Uploads once per id; `wrap` therefore applies on first upload only.
3885
+ * An id is either a pattern tile or a shader texture, never both. */
3886
+ upload(id, source, wrap = "clamp") {
3386
3887
  if (this.map.has(id)) return id;
3387
3888
  const gl = this.gl;
3388
3889
  const tex = gl.createTexture();
3389
3890
  if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
3891
+ const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
3390
3892
  gl.bindTexture(gl.TEXTURE_2D, tex);
3391
3893
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
3392
3894
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3393
3895
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3394
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3395
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3896
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
3897
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
3396
3898
  gl.bindTexture(gl.TEXTURE_2D, null);
3397
3899
  this.map.set(id, tex);
3398
3900
  return id;
@@ -3987,6 +4489,18 @@ function scale(m, sx, sy) {
3987
4489
  const s = create(sx, 0, 0, sy, 0, 0);
3988
4490
  return multiply2(m, s);
3989
4491
  }
4492
+ function invert(m) {
4493
+ const a = m[0], b = m[1];
4494
+ const c = m[3], d = m[4];
4495
+ const tx = m[6], ty = m[7];
4496
+ const det = a * d - b * c;
4497
+ if (det === 0 || !Number.isFinite(det)) return identity();
4498
+ const ia = d / det;
4499
+ const ib = -b / det;
4500
+ const ic = -c / det;
4501
+ const id = a / det;
4502
+ return create(ia, ib, ic, id, -(ia * tx + ic * ty), -(ib * tx + id * ty));
4503
+ }
3990
4504
  function apply(m, x, y) {
3991
4505
  const a = m[0], b = m[1];
3992
4506
  const c = m[3], d = m[4];
@@ -4014,6 +4528,7 @@ var mat3 = {
4014
4528
  multiply: multiply2,
4015
4529
  translate: translate2,
4016
4530
  scale,
4531
+ invert,
4017
4532
  apply,
4018
4533
  screenToClip
4019
4534
  };
@@ -5916,28 +6431,42 @@ function drawPathFillPattern(ctx, fill, handle) {
5916
6431
  if (isDev3) console.warn(`weasel: pattern TextureHandle "${tex.id}" not registered`);
5917
6432
  return;
5918
6433
  }
5919
- ctx.textureCache.upload(tex.id, entry.source);
6434
+ ctx.textureCache.upload(tex.id, entry.source, "repeat");
5920
6435
  const gl = ctx.gl;
5921
- gl.useProgram(ctx.imageFill.handle);
6436
+ gl.useProgram(ctx.patternFill.handle);
5922
6437
  gl.bindVertexArray(handle.vao);
5923
- setProjAndModel(ctx, ctx.imageFill);
5924
- setColorMatrixUniforms(ctx, ctx.imageFill);
6438
+ setProjAndModel(ctx, ctx.patternFill);
6439
+ setColorMatrixUniforms(ctx, ctx.patternFill);
6440
+ gl.uniformMatrix3fv(ctx.patternFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
6441
+ const [tw, th] = textureSize(entry.source);
6442
+ const origin = fill.origin ?? { x: 0, y: 0 };
6443
+ gl.uniform2f(ctx.patternFill.uniform("u_tileOrigin"), origin.x, origin.y);
6444
+ gl.uniform2f(ctx.patternFill.uniform("u_tileSize"), tw, th);
5925
6445
  ctx.textureCache.bind(tex.id, 0);
5926
- gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
5927
- gl.uniform1f(ctx.imageFill.uniform("u_opacity"), fill.opacity ?? 1);
5928
- gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
6446
+ gl.uniform1i(ctx.patternFill.uniform("u_sampler"), 0);
6447
+ gl.uniform1f(ctx.patternFill.uniform("u_opacity"), fill.opacity ?? 1);
6448
+ gl.uniform1f(ctx.patternFill.uniform("u_alpha"), ctx.state.alpha);
5929
6449
  applyClipTest(ctx);
5930
6450
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
5931
6451
  gl.bindVertexArray(null);
5932
6452
  }
6453
+ function textureSize(source) {
6454
+ const w = "naturalWidth" in source ? source.naturalWidth : source.width;
6455
+ const h = "naturalHeight" in source ? source.naturalHeight : source.height;
6456
+ return [w || 1, h || 1];
6457
+ }
6458
+ function gradientSpaceInverse(ctx, units) {
6459
+ if (units === "local") return mat3.invert(ctx.state.transform);
6460
+ if (units === "world" && ctx.viewMatrix) return mat3.invert(ctx.viewMatrix);
6461
+ return mat3.identity();
6462
+ }
5933
6463
  function drawPathFillGradient(ctx, fill, handle) {
5934
6464
  const gl = ctx.gl;
5935
6465
  const key = ctx.gradRampCache.upload(fill.stops);
5936
6466
  gl.useProgram(ctx.gradFill.handle);
5937
6467
  gl.bindVertexArray(handle.vao);
5938
6468
  setProjAndModel(ctx, ctx.gradFill);
5939
- const identity3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
5940
- gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, identity3);
6469
+ gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
5941
6470
  ctx.gradRampCache.bind(key, 0);
5942
6471
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
5943
6472
  gl.uniform1f(ctx.gradFill.uniform("u_alpha"), ctx.state.alpha);
@@ -6457,6 +6986,11 @@ function drawImage(ctx, cmd) {
6457
6986
  setProjAndModel(ctx, ctx.imageFill);
6458
6987
  setColorMatrixUniforms(ctx, ctx.imageFill);
6459
6988
  ctx.imageCache.bind(cmd.image, 0);
6989
+ gl.texParameteri(
6990
+ gl.TEXTURE_2D,
6991
+ gl.TEXTURE_MAG_FILTER,
6992
+ cmd.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
6993
+ );
6460
6994
  gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
6461
6995
  gl.uniform1f(ctx.imageFill.uniform("u_opacity"), cmd.opacity ?? 1);
6462
6996
  gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
@@ -6543,6 +7077,7 @@ var WeaselRenderer = class {
6543
7077
  textSdfR8;
6544
7078
  imageFill;
6545
7079
  gradFill;
7080
+ patternFill;
6546
7081
  meshCache;
6547
7082
  textureCache;
6548
7083
  imageCache;
@@ -6618,6 +7153,9 @@ var WeaselRenderer = class {
6618
7153
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
6619
7154
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
6620
7155
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
7156
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
7157
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
7158
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
6621
7159
  const aPos = this.pathFill.attribute("a_position");
6622
7160
  if (aPos === void 0) throw new Error("WeaselRenderer: a_position not found in path-fill shader");
6623
7161
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -6726,6 +7264,9 @@ var WeaselRenderer = class {
6726
7264
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
6727
7265
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
6728
7266
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
7267
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
7268
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
7269
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
6729
7270
  const aPos = this.pathFill.attribute("a_position");
6730
7271
  if (aPos === void 0) throw new Error("a_position missing after restore");
6731
7272
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -6797,7 +7338,15 @@ var WeaselRenderer = class {
6797
7338
  if (this.rectIbo) gl.deleteBuffer(this.rectIbo);
6798
7339
  if (this.rectVao) gl.deleteVertexArray(this.rectVao);
6799
7340
  }
6800
- render(commands) {
7341
+ /**
7342
+ * Draw one frame.
7343
+ *
7344
+ * `viewMatrix` is the frame's world→screen transform. It is only consulted
7345
+ * by `units: 'world'` gradients, which fall back to screen space without
7346
+ * it — every other command carries its own transform in the stream, so
7347
+ * callers with no view concept can keep calling `render(commands)`.
7348
+ */
7349
+ render(commands, viewMatrix) {
6801
7350
  if (this.contextLost || this.disposed) return;
6802
7351
  const gl = this.gl;
6803
7352
  this.meshCache.drainPendingDeletes();
@@ -6812,6 +7361,7 @@ var WeaselRenderer = class {
6812
7361
  textSdfR8: this.textSdfR8,
6813
7362
  imageFill: this.imageFill,
6814
7363
  gradFill: this.gradFill,
7364
+ patternFill: this.patternFill,
6815
7365
  meshCache: this.meshCache,
6816
7366
  textureCache: this.textureCache,
6817
7367
  imageCache: this.imageCache,
@@ -6826,7 +7376,8 @@ var WeaselRenderer = class {
6826
7376
  heightCss: this.heightCss,
6827
7377
  clipDepth: 0,
6828
7378
  flattenTolerance: this.flattenTolerance,
6829
- textOutlineMinScreenSize: this.textOutlineMinScreenSize
7379
+ textOutlineMinScreenSize: this.textOutlineMinScreenSize,
7380
+ viewMatrix
6830
7381
  };
6831
7382
  for (const cmd of commands) dispatch(ctx, cmd);
6832
7383
  this.meshCache.freeTransient();
@@ -6874,6 +7425,10 @@ var WeaselRenderer = class {
6874
7425
  return this.gradFill;
6875
7426
  }
6876
7427
  /** @internal */
7428
+ _patternFill() {
7429
+ return this.patternFill;
7430
+ }
7431
+ /** @internal */
6877
7432
  _meshCache() {
6878
7433
  return this.meshCache;
6879
7434
  }
@@ -6934,6 +7489,9 @@ function alignedStrokeRect(rect, align, width = 1) {
6934
7489
  height: rect.height + 2 * d
6935
7490
  };
6936
7491
  }
7492
+ function meanScale(s) {
7493
+ return Math.sqrt(s.x * s.y);
7494
+ }
6937
7495
  var DEFAULT_LINE = {
6938
7496
  paint: { fill: "solid", color: "rgba(255, 255, 255, 0.08)" },
6939
7497
  width: 1
@@ -7205,244 +7763,6 @@ function isEditableTarget2(target) {
7205
7763
  const ce = target.getAttribute("contenteditable");
7206
7764
  return ce === "true" || ce === "";
7207
7765
  }
7208
- var DepRegistryContext = createContext(null);
7209
- function DepRegistryProvider({ children }) {
7210
- const sourcesRef = useRef(/* @__PURE__ */ new Map());
7211
- const registry22 = useMemo(() => ({
7212
- register: (name, source) => {
7213
- sourcesRef.current.set(name, source);
7214
- return () => {
7215
- sourcesRef.current.delete(name);
7216
- };
7217
- },
7218
- get: (name) => sourcesRef.current.get(name)?.()
7219
- }), []);
7220
- return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry22, children });
7221
- }
7222
- function useDepRegistry() {
7223
- const r = useContext(DepRegistryContext);
7224
- if (r === null) {
7225
- throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
7226
- }
7227
- return r;
7228
- }
7229
- function useOptionalDepRegistry() {
7230
- return useContext(DepRegistryContext);
7231
- }
7232
- function useDepSource(name, source) {
7233
- const registry22 = useDepRegistry();
7234
- const sourceRef = useRef(source);
7235
- sourceRef.current = source;
7236
- useEffect(() => {
7237
- return registry22.register(name, () => sourceRef.current());
7238
- }, [name, registry22]);
7239
- }
7240
- var DEV = (() => {
7241
- try {
7242
- return Boolean(import.meta.env?.DEV);
7243
- } catch {
7244
- return false;
7245
- }
7246
- })();
7247
- function buildDepsFromRequires(action, depRegistry) {
7248
- const requires = action.requires ?? [];
7249
- const entries = requires.map((name) => [name, depRegistry.get(name)]);
7250
- const deps = Object.fromEntries(entries);
7251
- if (DEV) {
7252
- const declared = new Set(requires);
7253
- return new Proxy(deps, {
7254
- get(target, prop, receiver) {
7255
- if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
7256
- const message = `[weasel:dispatcher] action "${action.id}" read deps.${prop} but did not declare it in \`requires\`. Add \`requires: [...'${prop}']\` to the descriptor or the dep will be undefined at runtime.`;
7257
- console.warn(message);
7258
- throw new Error(message);
7259
- }
7260
- return Reflect.get(target, prop, receiver);
7261
- }
7262
- });
7263
- }
7264
- return deps;
7265
- }
7266
- var ActionDisabledReason = {
7267
- SelectionRequired: "selection-required",
7268
- NotApplicable: "not-applicable",
7269
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
7270
- PredicateThrew: "predicate-threw"
7271
- };
7272
- var enabledSlowWarned = /* @__PURE__ */ new Set();
7273
- var enabledThrewWarned = /* @__PURE__ */ new Set();
7274
- var ENABLED_BUDGET_MS = 4;
7275
- function evaluateEnabled(action, deps) {
7276
- if (!action.enabled) return { enabled: true };
7277
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
7278
- const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
7279
- const start = isDev3 ? now() : 0;
7280
- try {
7281
- const result = action.enabled(deps);
7282
- if (isDev3) {
7283
- const elapsed = now() - start;
7284
- if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
7285
- enabledSlowWarned.add(action.id);
7286
- console.warn(
7287
- `weasel actions: enabled() for action "${action.id}" took ${elapsed.toFixed(2)}ms (budget ${ENABLED_BUDGET_MS}ms). The predicate must be pure and fast \u2014 see Action.enabled JSDoc.`
7288
- );
7289
- }
7290
- }
7291
- if (result === true) return { enabled: true };
7292
- return { enabled: false, reason: result };
7293
- } catch (e) {
7294
- if (isDev3 && !enabledThrewWarned.has(action.id)) {
7295
- enabledThrewWarned.add(action.id);
7296
- console.warn(
7297
- `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
7298
- e
7299
- );
7300
- }
7301
- return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
7302
- }
7303
- }
7304
- function validateActionId(id) {
7305
- if (id.length === 0) {
7306
- throw new Error(`weasel: action id may not be empty`);
7307
- }
7308
- if (RESERVED_ID_PREFIXES.has(id[0])) {
7309
- throw new Error(
7310
- `weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
7311
- );
7312
- }
7313
- if (RESERVED_ID_NAMES.has(id)) {
7314
- throw new Error(
7315
- `weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
7316
- );
7317
- }
7318
- }
7319
- function validateActionDefaultBinding(action) {
7320
- const gs = action.defaultBinding;
7321
- if (!gs) return;
7322
- const entries = Array.isArray(gs) ? gs : [gs];
7323
- for (const entry of entries) {
7324
- const spec = "kind" in entry ? entry : entry.spec;
7325
- const phase = spec.phase;
7326
- if (phase === void 0) continue;
7327
- const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
7328
- for (const atom of atoms) {
7329
- if (atom.channel === "&") {
7330
- throw new Error(
7331
- `weasel: action "${action.id}" defaultBinding uses '&' channel which has no owning tool to resolve to. Use a named tool channel (e.g. '[rect:engaged]') or '*' instead, or move this binding to the tool's own Tool.bindings.`
7332
- );
7333
- }
7334
- }
7335
- }
7336
- }
7337
- var ActionsContext = createContext(null);
7338
- function ActionsProvider({ children }) {
7339
- const actionsRef = useRef(/* @__PURE__ */ new Map());
7340
- const versionRef = useRef(0);
7341
- const cachedRef = useRef([]);
7342
- const cachedVerRef = useRef(-1);
7343
- const listenersRef = useRef(/* @__PURE__ */ new Set());
7344
- const depReg = useOptionalDepRegistry();
7345
- const depRegRef = useRef(depReg);
7346
- depRegRef.current = depReg;
7347
- const wiredDepRegRef = useRef(null);
7348
- const dispatcherRef = useRef(null);
7349
- const registry22 = useMemo(() => {
7350
- const snapshot = () => {
7351
- const v = versionRef.current;
7352
- if (cachedVerRef.current === v) return cachedRef.current;
7353
- const out = Object.freeze(Array.from(actionsRef.current.values()));
7354
- cachedRef.current = out;
7355
- cachedVerRef.current = v;
7356
- return out;
7357
- };
7358
- const notify2 = () => {
7359
- for (const l of listenersRef.current) {
7360
- try {
7361
- l();
7362
- } catch (err) {
7363
- console.error("weasel ActionsRegistry: subscriber threw", err);
7364
- }
7365
- }
7366
- };
7367
- return {
7368
- register: (action) => {
7369
- validateActionId(action.id);
7370
- validateActionDefaultBinding(action);
7371
- actionsRef.current.set(action.id, action);
7372
- versionRef.current++;
7373
- notify2();
7374
- return () => {
7375
- const cur = actionsRef.current.get(action.id);
7376
- if (cur === action) {
7377
- actionsRef.current.delete(action.id);
7378
- versionRef.current++;
7379
- notify2();
7380
- }
7381
- };
7382
- },
7383
- unregister: (id) => {
7384
- if (actionsRef.current.delete(id)) {
7385
- versionRef.current++;
7386
- notify2();
7387
- }
7388
- },
7389
- list: () => snapshot(),
7390
- trigger: (id, params) => {
7391
- const a = actionsRef.current.get(id);
7392
- if (!a) return false;
7393
- try {
7394
- if (a.invoker && a.invoker.timing === "immediate") {
7395
- const r = wiredDepRegRef.current ?? depRegRef.current;
7396
- const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
7397
- selection: r.get("selection"),
7398
- scene: r.get("scene"),
7399
- history: r.get("history"),
7400
- view: r.get("view"),
7401
- pointer: r.get("pointer"),
7402
- activeTool: r.get("activeTool"),
7403
- booleansAdapter: r.get("booleansAdapter")
7404
- };
7405
- a.invoker.run(deps, params);
7406
- }
7407
- } catch (err) {
7408
- console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
7409
- }
7410
- return true;
7411
- },
7412
- subscribe: (listener) => {
7413
- listenersRef.current.add(listener);
7414
- return () => {
7415
- listenersRef.current.delete(listener);
7416
- };
7417
- },
7418
- setDispatcher: (d) => {
7419
- dispatcherRef.current = d;
7420
- },
7421
- setDepRegistry: (r) => {
7422
- wiredDepRegRef.current = r;
7423
- },
7424
- begin: (id, params) => {
7425
- const disp = dispatcherRef.current;
7426
- if (!disp) return null;
7427
- const r = wiredDepRegRef.current ?? depRegRef.current;
7428
- const deps = r ? {
7429
- selection: r.get("selection"),
7430
- scene: r.get("scene"),
7431
- history: r.get("history"),
7432
- view: r.get("view"),
7433
- pointer: r.get("pointer"),
7434
- activeTool: r.get("activeTool"),
7435
- booleansAdapter: r.get("booleansAdapter")
7436
- } : {};
7437
- return disp.beginUiOngoing(id, deps, params);
7438
- }
7439
- };
7440
- }, []);
7441
- return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry22, children });
7442
- }
7443
- function useActionsRegistry() {
7444
- return useContext(ActionsContext);
7445
- }
7446
7766
  function resolveParams(params) {
7447
7767
  if (params === void 0) return void 0;
7448
7768
  return typeof params === "function" ? params() : params;
@@ -7977,6 +8297,60 @@ var moveAction = {
7977
8297
  */
7978
8298
  enabled: () => true
7979
8299
  };
8300
+ function poseRotationOf(pose) {
8301
+ const p = pose;
8302
+ if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
8303
+ return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
8304
+ }
8305
+ return null;
8306
+ }
8307
+ function rotatePathAround(path, cx, cy, rotation) {
8308
+ if (path.kind === "rect") {
8309
+ return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
8310
+ }
8311
+ return rotatePolygon(path, cx, cy, rotation);
8312
+ }
8313
+ function rotatePolygon(path, cx, cy, rotation) {
8314
+ const cos = Math.cos(rotation);
8315
+ const sin = Math.sin(rotation);
8316
+ const { commands, coords } = path;
8317
+ const next = new Float32Array(coords.length);
8318
+ let ci = 0;
8319
+ for (let i = 0; i < commands.length; i++) {
8320
+ const len = COORD_COUNT3[commands[i]];
8321
+ for (let k = 0; k < len; k += 2) {
8322
+ const dx = coords[ci + k] - cx;
8323
+ const dy = coords[ci + k + 1] - cy;
8324
+ next[ci + k] = cx + dx * cos - dy * sin;
8325
+ next[ci + k + 1] = cy + dx * sin + dy * cos;
8326
+ }
8327
+ ci += len;
8328
+ }
8329
+ return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
8330
+ }
8331
+ function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
8332
+ const cos = Math.cos(rotation);
8333
+ const sin = Math.sin(rotation);
8334
+ const rotate2 = (px, py) => {
8335
+ const dx = px - cx;
8336
+ const dy = py - cy;
8337
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
8338
+ };
8339
+ const [x0, y0] = rotate2(x, y);
8340
+ const [x1, y1] = rotate2(x + w, y);
8341
+ const [x2, y2] = rotate2(x + w, y + h);
8342
+ const [x3, y3] = rotate2(x, y + h);
8343
+ const commands = new Uint8Array([PATH_M2, PATH_L2, PATH_L2, PATH_L2, PATH_Z2]);
8344
+ const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
8345
+ return { kind: "polygon", commands, coords, fillRule: "nonzero" };
8346
+ }
8347
+ var COORD_COUNT3 = {
8348
+ [PATH_M2]: 2,
8349
+ [PATH_L2]: 2,
8350
+ [PATH_C2]: 6,
8351
+ [PATH_Q2]: 4,
8352
+ [PATH_Z2]: 0
8353
+ };
7980
8354
  function aabbCenter(bounds) {
7981
8355
  return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
7982
8356
  }
@@ -8269,6 +8643,13 @@ var resizeAction = {
8269
8643
  const computePose = (id) => {
8270
8644
  const sp = scratch.startPoses.get(id);
8271
8645
  if (sp === void 0) return void 0;
8646
+ if (isGroupPath && poseRotationOf(sp) !== null) {
8647
+ return remapRotatedLeaf(
8648
+ sp,
8649
+ scratch.originBounds,
8650
+ proposedBounds
8651
+ );
8652
+ }
8272
8653
  return scratch.geometry.remapBounds(sp, scratch.originBounds, proposedBounds);
8273
8654
  };
8274
8655
  scratch.previews.clear();
@@ -8889,9 +9270,9 @@ var clearSelectionAction = {
8889
9270
  return true;
8890
9271
  }
8891
9272
  };
8892
- var counter = 0;
9273
+ var counter2 = 0;
8893
9274
  function freshNodeId() {
8894
- return asNodeId(`n${(counter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
9275
+ return asNodeId(`n${(counter2++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
8895
9276
  }
8896
9277
  function translatePose(pose, dx, dy) {
8897
9278
  const p = pose;
@@ -9108,7 +9489,13 @@ function makeViewportZoomAction(opts = {}) {
9108
9489
  };
9109
9490
  }
9110
9491
  makeViewportZoomAction();
9111
- var isBody = (_target, bodyTarget) => bodyTarget === "selected-body" || bodyTarget === "unselected-body";
9492
+ function bodyPredicate(fn) {
9493
+ return Object.assign(fn, { readsAffordance: false });
9494
+ }
9495
+ var isBody = bodyPredicate((_target, bodyTarget) => bodyTarget === "selected-body" || bodyTarget === "unselected-body");
9496
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "selected-body");
9497
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "unselected-body");
9498
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "empty");
9112
9499
  function kindOf(target) {
9113
9500
  if (typeof target !== "object" || target === null) return void 0;
9114
9501
  const k = target.kind;
@@ -9147,17 +9534,17 @@ function splitCubicAtT(p0, p1, p2, p3, t) {
9147
9534
  function fitCubicThroughDeletion(prev, next) {
9148
9535
  const dx = next.x - prev.x;
9149
9536
  const dy = next.y - prev.y;
9150
- const dist4 = Math.sqrt(dx * dx + dy * dy);
9537
+ const dist3 = Math.sqrt(dx * dx + dy * dy);
9151
9538
  const c1DirX = prev.outHandle ? prev.outHandle.x - prev.x : dx;
9152
9539
  const c1DirY = prev.outHandle ? prev.outHandle.y - prev.y : dy;
9153
- const c1 = controlOnSide(prev, c1DirX, c1DirY, dist4, 1 / 3);
9540
+ const c1 = controlOnSide(prev, c1DirX, c1DirY, dist3, 1 / 3);
9154
9541
  const c2DirX = next.inHandle ? next.inHandle.x - next.x : -dx;
9155
9542
  const c2DirY = next.inHandle ? next.inHandle.y - next.y : -dy;
9156
- const c2 = controlOnSide(next, c2DirX, c2DirY, dist4, 1 / 3);
9543
+ const c2 = controlOnSide(next, c2DirX, c2DirY, dist3, 1 / 3);
9157
9544
  return { c1, c2 };
9158
9545
  }
9159
- function controlOnSide(anchor, dirX, dirY, dist4, fraction) {
9160
- const targetDist = dist4 * fraction;
9546
+ function controlOnSide(anchor, dirX, dirY, dist3, fraction) {
9547
+ const targetDist = dist3 * fraction;
9161
9548
  const len = Math.hypot(dirX, dirY) || 1;
9162
9549
  const ux = dirX / len, uy = dirY / len;
9163
9550
  return { x: anchor.x + ux * targetDist, y: anchor.y + uy * targetDist };
@@ -9414,60 +9801,6 @@ function anchorsInRect(set, rect) {
9414
9801
  }
9415
9802
  return hits;
9416
9803
  }
9417
- function poseRotationOf(pose) {
9418
- const p = pose;
9419
- if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
9420
- return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
9421
- }
9422
- return null;
9423
- }
9424
- function rotatePathAround(path, cx, cy, rotation) {
9425
- if (path.kind === "rect") {
9426
- return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
9427
- }
9428
- return rotatePolygon(path, cx, cy, rotation);
9429
- }
9430
- function rotatePolygon(path, cx, cy, rotation) {
9431
- const cos = Math.cos(rotation);
9432
- const sin = Math.sin(rotation);
9433
- const { commands, coords } = path;
9434
- const next = new Float32Array(coords.length);
9435
- let ci = 0;
9436
- for (let i = 0; i < commands.length; i++) {
9437
- const len = COORD_COUNT3[commands[i]];
9438
- for (let k = 0; k < len; k += 2) {
9439
- const dx = coords[ci + k] - cx;
9440
- const dy = coords[ci + k + 1] - cy;
9441
- next[ci + k] = cx + dx * cos - dy * sin;
9442
- next[ci + k + 1] = cy + dx * sin + dy * cos;
9443
- }
9444
- ci += len;
9445
- }
9446
- return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
9447
- }
9448
- function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
9449
- const cos = Math.cos(rotation);
9450
- const sin = Math.sin(rotation);
9451
- const rotate2 = (px, py) => {
9452
- const dx = px - cx;
9453
- const dy = py - cy;
9454
- return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
9455
- };
9456
- const [x0, y0] = rotate2(x, y);
9457
- const [x1, y1] = rotate2(x + w, y);
9458
- const [x2, y2] = rotate2(x + w, y + h);
9459
- const [x3, y3] = rotate2(x, y + h);
9460
- const commands = new Uint8Array([PATH_M2, PATH_L2, PATH_L2, PATH_L2, PATH_Z2]);
9461
- const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
9462
- return { kind: "polygon", commands, coords, fillRule: "nonzero" };
9463
- }
9464
- var COORD_COUNT3 = {
9465
- [PATH_M2]: 2,
9466
- [PATH_L2]: 2,
9467
- [PATH_C2]: 6,
9468
- [PATH_Q2]: 4,
9469
- [PATH_Z2]: 0
9470
- };
9471
9804
  function pathInPoseFrame(path, pose) {
9472
9805
  if (path.kind === "rect") {
9473
9806
  if (path.x === pose.x && path.y === pose.y && path.width === pose.width && path.height === pose.height) return path;
@@ -11020,7 +11353,7 @@ var LABEL = {
11020
11353
  divide: "Divide",
11021
11354
  crop: "Crop"
11022
11355
  };
11023
- function isEmpty(p) {
11356
+ function isEmpty2(p) {
11024
11357
  return p.commands.length === 0;
11025
11358
  }
11026
11359
  function applyBooleanOp(adapter, op) {
@@ -11058,7 +11391,7 @@ function applyBooleanOp(adapter, op) {
11058
11391
  break;
11059
11392
  }
11060
11393
  }
11061
- results = results.filter((p) => !isEmpty(p));
11394
+ results = results.filter((p) => !isEmpty2(p));
11062
11395
  if (results.length === 0) return { kind: "noop", reason: "empty-result" };
11063
11396
  const topmostId = entries[entries.length - 1].id;
11064
11397
  const topAnchor = adapter.getZOrder?.(topmostId);
@@ -11594,14 +11927,24 @@ var DEFAULT_PALETTE = [
11594
11927
  "#7ab8d4",
11595
11928
  "#d47a7a"
11596
11929
  ];
11930
+ function resolveFill(scratch, prev) {
11931
+ if (scratch.currentPaint) return scratch.currentPaint;
11932
+ const prevColor = typeof prev?.fill === "string" ? prev.fill : DEFAULT_FILL_COLOR;
11933
+ return mergeAlphaFromPrev(scratch.currentColor, prevColor);
11934
+ }
11597
11935
  function refreshPreviews(scratch) {
11598
11936
  scratch.previews.clear();
11599
11937
  for (const id of scratch.ids) {
11600
11938
  const prev = scratch.startData.get(id);
11601
- const next = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
11602
- scratch.previews.set(id, { ...prev ?? {}, fill: next });
11939
+ scratch.previews.set(id, { ...prev ?? {}, fill: resolveFill(scratch, prev) });
11603
11940
  }
11604
11941
  }
11942
+ function readParams(params) {
11943
+ return {
11944
+ color: params?.color,
11945
+ paint: params?.paint
11946
+ };
11947
+ }
11605
11948
  var setFillAction = {
11606
11949
  id: "setFill",
11607
11950
  label: "Set fill",
@@ -11616,9 +11959,10 @@ var setFillAction = {
11616
11959
  if (!selection || !scene) return {};
11617
11960
  const ids = selection.get();
11618
11961
  if (ids.length === 0) return {};
11619
- const ctxColor = ctx.params?.color;
11620
- const optsColor = opts?.params?.color;
11621
- const initialColor = ctxColor ?? optsColor ?? DEFAULT_STROKE_COLOR;
11962
+ const fromCtx = readParams(ctx.params);
11963
+ const fromOpts = readParams(opts?.params);
11964
+ const initialPaint = fromCtx.paint ?? fromOpts.paint;
11965
+ const initialColor = fromCtx.color ?? fromOpts.color ?? DEFAULT_STROKE_COLOR;
11622
11966
  const startData = /* @__PURE__ */ new Map();
11623
11967
  for (const id of ids) {
11624
11968
  const node = scene.get(id);
@@ -11629,15 +11973,20 @@ var setFillAction = {
11629
11973
  scene,
11630
11974
  startData,
11631
11975
  currentColor: initialColor,
11976
+ currentPaint: initialPaint,
11632
11977
  previews: /* @__PURE__ */ new Map(),
11633
11978
  applyOps
11634
11979
  };
11635
11980
  refreshPreviews(scratch);
11636
11981
  return {
11637
11982
  onMove(moveCtx) {
11638
- const next = moveCtx.params?.color;
11639
- if (next === void 0) return;
11640
- scratch.currentColor = next;
11983
+ const { color, paint } = readParams(moveCtx.params);
11984
+ if (color === void 0 && paint === void 0) return;
11985
+ if (paint !== void 0) scratch.currentPaint = paint;
11986
+ if (color !== void 0) {
11987
+ scratch.currentColor = color;
11988
+ if (paint === void 0) scratch.currentPaint = void 0;
11989
+ }
11641
11990
  refreshPreviews(scratch);
11642
11991
  },
11643
11992
  onEnd(_endCtx, reason) {
@@ -11647,8 +11996,7 @@ var setFillAction = {
11647
11996
  }
11648
11997
  const ops = [];
11649
11998
  for (const id of scratch.ids) {
11650
- const prev = scratch.startData.get(id);
11651
- const merged = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
11999
+ const merged = resolveFill(scratch, scratch.startData.get(id));
11652
12000
  const nodeNow = scratch.scene.get(id);
11653
12001
  if (!nodeNow) continue;
11654
12002
  const from = { ...nodeNow.data };
@@ -12508,6 +12856,54 @@ function targetRank(target) {
12508
12856
  if (target.startsWith("affordance:")) return 2;
12509
12857
  return 1;
12510
12858
  }
12859
+ function targetConsultsAffordance(specTarget) {
12860
+ if (specTarget === void 0) return false;
12861
+ if (typeof specTarget === "object" && specTarget !== null && "kindOf" in specTarget) {
12862
+ const kindOf2 = specTarget.kindOf;
12863
+ return kindOf2?.readsAffordance !== false;
12864
+ }
12865
+ return typeof specTarget === "string" && specTarget.startsWith("affordance:");
12866
+ }
12867
+ function specTargetOf(spec) {
12868
+ return "target" in spec ? spec.target : void 0;
12869
+ }
12870
+ function claimOf(e) {
12871
+ return "affordance" in e ? e.affordance : void 0;
12872
+ }
12873
+ function claimGestureOf(e) {
12874
+ switch (e.kind) {
12875
+ case "pointerdown":
12876
+ case "click":
12877
+ return "pointer";
12878
+ case "doubleclick":
12879
+ return "doubleClick";
12880
+ case "contextmenu":
12881
+ return "contextMenu";
12882
+ case "longpress":
12883
+ return "longPress";
12884
+ case "wheel":
12885
+ return "wheel";
12886
+ default:
12887
+ return null;
12888
+ }
12889
+ }
12890
+ function isExclusiveClaim(e) {
12891
+ const claim = claimOf(e);
12892
+ if (claim?.strength !== "exclusive") return false;
12893
+ const gesture = claimGestureOf(e);
12894
+ if (gesture === null) return false;
12895
+ return claim.claimedKinds === void 0 || claim.claimedKinds.includes(gesture);
12896
+ }
12897
+ var warnedDeadClaims = /* @__PURE__ */ new Set();
12898
+ function reportDeadClaim(owner, warn) {
12899
+ if (process.env.NODE_ENV === "production") return;
12900
+ const key = String(owner);
12901
+ if (warnedDeadClaims.has(key)) return;
12902
+ warnedDeadClaims.add(key);
12903
+ warn(
12904
+ `[weasel] exclusive claim by "${key}" matched no binding: no \`affordance:\` or \`kindOf\` target resolved against it, so the press was dropped.`
12905
+ );
12906
+ }
12511
12907
  function specificity(spec) {
12512
12908
  const t = targetRank("target" in spec ? spec.target : void 0);
12513
12909
  const mods = "mods" in spec ? spec.mods : void 0;
@@ -12524,12 +12920,14 @@ function compareSpecificity(a, b) {
12524
12920
  }
12525
12921
  return 0;
12526
12922
  }
12527
- function matchSorted(e, bindings, isMac, engagedChannels) {
12923
+ function matchSorted(e, bindings, isMac, engagedChannels, warn = (m) => console.warn(m)) {
12528
12924
  const out = [];
12529
12925
  const engaged = engagedChannels ?? EMPTY_ENGAGED2;
12926
+ const exclusive = isExclusiveClaim(e);
12927
+ const pool = exclusive ? bindings.filter((sb) => targetConsultsAffordance(specTargetOf(sb.binding.spec))) : bindings;
12530
12928
  for (const scope of SCOPE_PRIORITY) {
12531
12929
  const scopeMatches = [];
12532
- for (const sb of bindings) {
12930
+ for (const sb of pool) {
12533
12931
  if (sb.scope !== scope) continue;
12534
12932
  const phaseCtx = { selfChannel: sb.ownerToolId, engagedChannels: engaged };
12535
12933
  if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
@@ -12539,9 +12937,32 @@ function matchSorted(e, bindings, isMac, engagedChannels) {
12539
12937
  scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
12540
12938
  out.push(...scopeMatches);
12541
12939
  }
12940
+ if (exclusive && out.length === 0 && bindings.length > 0) {
12941
+ reportDeadClaim(claimOf(e)?.owner, warn);
12942
+ }
12542
12943
  return out;
12543
12944
  }
12544
12945
  var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
12946
+ function liveScope(id, eligibility, state) {
12947
+ const tags = eligibility.capabilities;
12948
+ if (tags && tags.length > 0 && state.allows && !state.allows(tags)) return null;
12949
+ if (state.engagedIds?.has(id)) return "hotkey";
12950
+ if (eligibility.offhand && state.heldTriggers.has(eligibility.offhand)) return "hotkey";
12951
+ if (eligibility.focus && state.focusedId === id) return "active";
12952
+ if (eligibility.always || eligibility.claimed) return "ambient";
12953
+ return null;
12954
+ }
12955
+ function scopeBindings(entries, state) {
12956
+ const out = [];
12957
+ for (const entry of entries) {
12958
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
12959
+ if (scope === null) continue;
12960
+ for (const binding of entry.bindings ?? []) {
12961
+ out.push({ binding, scope, ownerToolId: entry.id });
12962
+ }
12963
+ }
12964
+ return out;
12965
+ }
12545
12966
  var ALWAYS = { all: [] };
12546
12967
  var NEVER = { any: [] };
12547
12968
  function isAllRule(r) {
@@ -12836,6 +13257,7 @@ function filterEligible(matches, actionLookup, ruleCtx) {
12836
13257
  return isEligible(action, ruleCtx);
12837
13258
  });
12838
13259
  }
13260
+ var EMPTY_TRIGGERS = /* @__PURE__ */ new Set();
12839
13261
  var EMPTY_ENGAGED22 = /* @__PURE__ */ new Set();
12840
13262
  function modifiersOf(e) {
12841
13263
  return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
@@ -12943,34 +13365,23 @@ function createDispatcher(opts) {
12943
13365
  return `ongoing-${event.kind}`;
12944
13366
  }
12945
13367
  function assembleScopedBindings(ctx) {
12946
- const result = [];
12947
- for (const toolId of ctx.hotkeyStack) {
12948
- const tool = ctx.toolsById.get(toolId);
12949
- for (const binding of tool?.bindings ?? []) {
12950
- result.push({ binding, scope: "hotkey", ownerToolId: toolId });
12951
- }
12952
- }
12953
- const activeTool = ctx.toolsById.get(ctx.activeToolId);
12954
- for (const binding of activeTool?.bindings ?? []) {
12955
- result.push({ binding, scope: "active", ownerToolId: ctx.activeToolId });
12956
- }
12957
- for (const toolId of ctx.ambientToolIds ?? []) {
12958
- const tool = ctx.toolsById.get(toolId);
12959
- for (const binding of tool?.bindings ?? []) {
12960
- result.push({ binding, scope: "ambient", ownerToolId: toolId });
12961
- }
12962
- }
13368
+ const ordered = [];
13369
+ for (const id of ctx.hotkeyStack) {
13370
+ const tool = ctx.toolsById.get(id);
13371
+ if (tool && !ordered.includes(tool)) ordered.push(tool);
13372
+ }
13373
+ for (const tool of ctx.toolsById.values()) {
13374
+ if (!ordered.includes(tool)) ordered.push(tool);
13375
+ }
13376
+ const result = scopeBindings(ordered, {
13377
+ focusedId: ctx.activeToolId,
13378
+ heldTriggers: EMPTY_TRIGGERS,
13379
+ engagedIds: new Set(ctx.hotkeyStack)
13380
+ });
12963
13381
  for (const action of ctx.actions.list()) {
12964
- const gs = action.defaultBinding;
12965
- if (!gs) continue;
12966
- const raw = Array.isArray(gs) ? gs : [gs];
12967
- for (const entry of raw) {
12968
- const isBoundObj = !("kind" in entry);
12969
- const spec = isBoundObj ? entry.spec : entry;
12970
- const opts2 = isBoundObj ? entry.opts : void 0;
12971
- const defaultBinding = { spec, actionId: action.id, ...opts2 !== void 0 ? { opts: opts2 } : {} };
12972
- const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
12973
- result.push({ binding: defaultBinding, scope: targetScope, ownerToolId: null });
13382
+ const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
13383
+ for (const binding of actionBindings(action)) {
13384
+ result.push({ binding, scope: targetScope, ownerToolId: null });
12974
13385
  }
12975
13386
  }
12976
13387
  return result;
@@ -13125,15 +13536,26 @@ function createDispatcher(opts) {
13125
13536
  deltaY: event.deltaY,
13126
13537
  clientX: event.clientX,
13127
13538
  clientY: event.clientY,
13539
+ affordance: event.affordance,
13128
13540
  ...resolved
13129
13541
  };
13130
13542
  } else if (event.kind === "click" || event.kind === "doubleclick") {
13131
13543
  params = {
13132
13544
  worldX: event.worldX,
13133
13545
  worldY: event.worldY,
13546
+ affordance: event.affordance,
13134
13547
  // Press point as well as release point — an action that places
13135
13548
  // geometry at the click wants the former. See `ClickEvent`.
13136
- ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY, affordance: event.affordance } : {},
13549
+ ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
13550
+ mods: modifiersOf(event),
13551
+ ...resolved
13552
+ };
13553
+ } else if (event.kind === "contextmenu" || event.kind === "longpress") {
13554
+ params = {
13555
+ worldX: event.kind === "contextmenu" ? event.worldX : event.x,
13556
+ worldY: event.kind === "contextmenu" ? event.worldY : event.y,
13557
+ affordance: event.affordance,
13558
+ bodyTarget: event.bodyTarget,
13137
13559
  mods: modifiersOf(event),
13138
13560
  ...resolved
13139
13561
  };
@@ -13501,7 +13923,7 @@ function computeMultiTouchGeometry(positions) {
13501
13923
  return { centroid, spread };
13502
13924
  }
13503
13925
  function useGestureDispatcher(opts) {
13504
- const { canvasRef, actions, toolsById, ambientToolIds, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick } = opts;
13926
+ const { canvasRef, actions, toolsById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick } = opts;
13505
13927
  const onDoubleClickRef = useRef(onDoubleClick);
13506
13928
  onDoubleClickRef.current = onDoubleClick;
13507
13929
  const activeTool = useActiveToolContext();
@@ -13516,7 +13938,6 @@ function useGestureDispatcher(opts) {
13516
13938
  activeToolId: activeTool.active,
13517
13939
  hotkeyStack: activeTool.hotkeyStack,
13518
13940
  toolsById,
13519
- ambientToolIds,
13520
13941
  isMac: IS_MAC,
13521
13942
  getRuleCtx
13522
13943
  });
@@ -13526,7 +13947,6 @@ function useGestureDispatcher(opts) {
13526
13947
  activeToolId: activeTool.active,
13527
13948
  hotkeyStack: activeTool.hotkeyStack,
13528
13949
  toolsById,
13529
- ambientToolIds,
13530
13950
  isMac: IS_MAC,
13531
13951
  getRuleCtx
13532
13952
  };
@@ -13604,6 +14024,7 @@ function useGestureDispatcher(opts) {
13604
14024
  ctrlKey: down.ctrlKey,
13605
14025
  metaKey: down.metaKey,
13606
14026
  shiftKey: down.shiftKey,
14027
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
13607
14028
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
13608
14029
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
13609
14030
  };
@@ -13613,11 +14034,15 @@ function useGestureDispatcher(opts) {
13613
14034
  y: down.worldY,
13614
14035
  clientX: down.clientX,
13615
14036
  clientY: down.clientY,
13616
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
13617
14037
  ...shared
13618
14038
  });
13619
14039
  if (result === "unhandled") {
13620
- dispatch2({ kind: "contextmenu", ...shared });
14040
+ dispatch2({
14041
+ kind: "contextmenu",
14042
+ worldX: down.worldX,
14043
+ worldY: down.worldY,
14044
+ ...shared
14045
+ });
13621
14046
  }
13622
14047
  };
13623
14048
  const DOUBLE_CLICK_MAX_MS = 600;
@@ -13672,6 +14097,9 @@ function useGestureDispatcher(opts) {
13672
14097
  const onWheel = (e) => {
13673
14098
  const rect = canvas?.getBoundingClientRect();
13674
14099
  const [localX, localY] = rect ? clientToCanvasRect(rect, e.clientX, e.clientY) : [e.clientX, e.clientY];
14100
+ const screenPoint = { x: e.clientX, y: e.clientY };
14101
+ const affordance = affordanceAtRef.current?.(screenPoint) ?? void 0;
14102
+ const body = classifyTargetRef.current?.(screenPoint);
13675
14103
  const ev = {
13676
14104
  kind: "wheel",
13677
14105
  altKey: e.altKey,
@@ -13681,7 +14109,10 @@ function useGestureDispatcher(opts) {
13681
14109
  deltaX: e.deltaX,
13682
14110
  deltaY: e.deltaY,
13683
14111
  clientX: localX,
13684
- clientY: localY
14112
+ clientY: localY,
14113
+ ...affordance !== void 0 ? { affordance } : {},
14114
+ ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
14115
+ ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
13685
14116
  };
13686
14117
  const result = dispatch2(ev);
13687
14118
  if (result === "handled") {
@@ -13975,6 +14406,7 @@ function useGestureDispatcher(opts) {
13975
14406
  shiftKey: e.shiftKey,
13976
14407
  worldX: wClick.x,
13977
14408
  worldY: wClick.y,
14409
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
13978
14410
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
13979
14411
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
13980
14412
  };
@@ -14016,8 +14448,10 @@ function useGestureDispatcher(opts) {
14016
14448
  };
14017
14449
  const onContextMenu = (e) => {
14018
14450
  e.preventDefault();
14019
- const worldPoint = { x: e.clientX, y: e.clientY };
14020
- const menuBody = classifyTargetRef.current?.(worldPoint);
14451
+ const screenPoint = { x: e.clientX, y: e.clientY };
14452
+ const affordance = affordanceAtRef.current?.(screenPoint) ?? void 0;
14453
+ const menuBody = classifyTargetRef.current?.(screenPoint);
14454
+ const w = toWorld(e.clientX, e.clientY);
14021
14455
  const ev = {
14022
14456
  kind: "contextmenu",
14023
14457
  target: e.target,
@@ -14025,6 +14459,9 @@ function useGestureDispatcher(opts) {
14025
14459
  ctrlKey: e.ctrlKey,
14026
14460
  metaKey: e.metaKey,
14027
14461
  shiftKey: e.shiftKey,
14462
+ worldX: w.x,
14463
+ worldY: w.y,
14464
+ ...affordance !== void 0 ? { affordance } : {},
14028
14465
  ...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
14029
14466
  ...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
14030
14467
  };
@@ -14323,47 +14760,203 @@ function usePinchGesture(canvasRef, onPinch, enabled = true) {
14323
14760
  };
14324
14761
  }, [canvasRef, enabled]);
14325
14762
  }
14326
- function useTools(opts) {
14327
- if (!(opts.active in opts.registry)) {
14328
- throw new Error(`useTools: active "${opts.active}" not in registry`);
14763
+ var TOOL_OFFHAND_ID = "tool.offhand";
14764
+ function offhandKeyFor(trigger) {
14765
+ switch (trigger) {
14766
+ case "space":
14767
+ return " ";
14768
+ case "alt":
14769
+ return "Alt";
14770
+ case "ctrl":
14771
+ return "Control";
14772
+ case "meta":
14773
+ return "Meta";
14774
+ case "shift":
14775
+ return "Shift";
14329
14776
  }
14777
+ }
14778
+ function buildToolOffhandBindings(specs) {
14779
+ return specs.map(({ toolId, key }) => ({
14780
+ spec: { kind: "key-held", key },
14781
+ opts: { params: { toolId } }
14782
+ }));
14783
+ }
14784
+ function makeToolOffhandAction(bindings) {
14785
+ return {
14786
+ id: TOOL_OFFHAND_ID,
14787
+ label: "Engage tool (offhand)",
14788
+ defaultBinding: bindings,
14789
+ scope: "hotkey",
14790
+ invoker: {
14791
+ timing: "ongoing",
14792
+ start: (ctx, opts) => {
14793
+ const activeTool = ctx.deps.activeTool;
14794
+ const toolId = resolveParams(opts?.params)?.toolId;
14795
+ if (!activeTool || !toolId) return {};
14796
+ activeTool.pushHotkey(toolId);
14797
+ return {
14798
+ onEnd: () => {
14799
+ activeTool.popHotkey();
14800
+ }
14801
+ };
14802
+ }
14803
+ },
14804
+ requires: ["activeTool"]
14805
+ };
14806
+ }
14807
+ var NO_TRIGGERS = /* @__PURE__ */ new Set();
14808
+ function useContributions(opts) {
14330
14809
  const ctx = useActiveToolContext();
14810
+ const actionsRegistry = useActionsRegistry();
14331
14811
  const hasInitializedRef = useRef(false);
14332
14812
  const isFirstRender = !hasInitializedRef.current;
14333
14813
  const needsFirstMountSyncRef = useRef(false);
14334
14814
  if (isFirstRender) {
14335
14815
  hasInitializedRef.current = true;
14336
- needsFirstMountSyncRef.current = ctx.active === "select" && opts.active !== "select";
14816
+ needsFirstMountSyncRef.current = ctx.active === "select" && opts.focused !== "select";
14337
14817
  }
14338
14818
  useEffect(() => {
14339
14819
  if (!needsFirstMountSyncRef.current) return;
14340
14820
  needsFirstMountSyncRef.current = false;
14341
- ctx.setActive(opts.active);
14821
+ ctx.setActive(opts.focused);
14822
+ }, []);
14823
+ const focused2 = isFirstRender && ctx.active === "select" && opts.focused !== "select" ? opts.focused : ctx.active;
14824
+ const entriesRef = useRef(opts.entries);
14825
+ entriesRef.current = opts.entries;
14826
+ const focusedRef = useRef(focused2);
14827
+ focusedRef.current = focused2;
14828
+ const engagedRef = useRef(ctx.hotkeyStack);
14829
+ engagedRef.current = ctx.hotkeyStack;
14830
+ const setFocused = useCallback((id) => {
14831
+ ctx.setActive(id);
14832
+ }, [ctx]);
14833
+ const snapshot = useCallback(() => {
14834
+ const engaged = engagedRef.current;
14835
+ const engagedIds = new Set(engaged);
14836
+ const ordered = [];
14837
+ for (const id of engaged) {
14838
+ const entry = entriesRef.current.find((e) => e.id === id);
14839
+ if (entry && !ordered.includes(entry)) ordered.push(entry);
14840
+ }
14841
+ for (const entry of entriesRef.current) {
14842
+ if (!ordered.includes(entry)) ordered.push(entry);
14843
+ }
14844
+ return {
14845
+ ordered,
14846
+ state: { focusedId: focusedRef.current, heldTriggers: NO_TRIGGERS, engagedIds }
14847
+ };
14342
14848
  }, []);
14343
- const active = isFirstRender && ctx.active === "select" && opts.active !== "select" ? opts.active : ctx.active;
14344
- const hotkeyStack = ctx.hotkeyStack;
14345
- const hotkeyEngaged = hotkeyStack.at(-1) ?? null;
14346
- const registryRef = useRef(opts.registry);
14347
- registryRef.current = opts.registry;
14348
- const ambientRef = useRef(opts.ambient ?? []);
14349
- ambientRef.current = opts.ambient ?? [];
14350
- const activeRef = useRef(active);
14351
- activeRef.current = active;
14849
+ const scopedBindings = useCallback(() => {
14850
+ const { ordered, state } = snapshot();
14851
+ return scopeBindings(ordered, state);
14852
+ }, [snapshot]);
14853
+ const overlays = useCallback(() => {
14854
+ const { ordered, state } = snapshot();
14855
+ const active = [];
14856
+ const hotkey = [];
14857
+ const ambient = [];
14858
+ for (const entry of ordered) {
14859
+ if (!entry.overlay) continue;
14860
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
14861
+ if (scope === "active") active.push(entry.overlay);
14862
+ else if (scope === "hotkey") hotkey.push(entry.overlay);
14863
+ else if (scope === "ambient") ambient.push(entry.overlay);
14864
+ }
14865
+ return [...active, ...hotkey, ...ambient];
14866
+ }, [snapshot]);
14867
+ const offhandSig = opts.entries.map((e) => e.eligibility?.offhand ? `${e.id}:${e.eligibility.offhand}` : "").filter(Boolean).join(",");
14868
+ useEffect(() => {
14869
+ if (!actionsRegistry || offhandSig === "") return;
14870
+ const specs = offhandSig.split(",").map((entry) => {
14871
+ const [toolId, trigger] = entry.split(":");
14872
+ return { toolId, key: offhandKeyFor(trigger) };
14873
+ });
14874
+ return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
14875
+ }, [actionsRegistry, offhandSig]);
14876
+ const lastConflictSigRef = useRef(null);
14877
+ const entrySig = opts.entries.map((e) => e.id).join(",");
14878
+ useEffect(() => {
14879
+ if (process.env.NODE_ENV === "production") return;
14880
+ if (lastConflictSigRef.current === entrySig) return;
14881
+ lastConflictSigRef.current = entrySig;
14882
+ const registry22 = [];
14883
+ const ambient = [];
14884
+ for (const entry of entriesRef.current) {
14885
+ (entry.eligibility?.focus ? registry22 : ambient).push(entry);
14886
+ }
14887
+ reportRouteConflicts({
14888
+ registry: registry22,
14889
+ ambient,
14890
+ actions: actionsRegistry?.list() ?? []
14891
+ });
14892
+ }, [entrySig, actionsRegistry]);
14893
+ return useMemo(
14894
+ () => ({
14895
+ entries: entriesRef.current,
14896
+ focused: focused2,
14897
+ setFocused,
14898
+ scopedBindings,
14899
+ overlays
14900
+ }),
14901
+ [focused2, setFocused, scopedBindings, overlays]
14902
+ );
14903
+ }
14904
+ function declareSlot(tool, slot) {
14905
+ const hotkey = tool.def?.hotkey;
14906
+ const eligibility = {
14907
+ ...tool.eligibility,
14908
+ [slot]: true,
14909
+ ...hotkey ? { offhand: hotkey } : {}
14910
+ };
14911
+ return sameEligibility(tool.eligibility, eligibility) ? tool : { ...tool, eligibility };
14912
+ }
14913
+ function sameEligibility(a, b) {
14914
+ if (!a) return false;
14915
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
14916
+ for (const key of keys) if (a[key] !== b[key]) return false;
14917
+ return true;
14918
+ }
14919
+ function useTools(opts) {
14920
+ if (!(opts.active in opts.registry)) {
14921
+ throw new Error(`useTools: active "${opts.active}" not in registry`);
14922
+ }
14923
+ const ctx = useActiveToolContext();
14924
+ const slotted = useMemo(() => {
14925
+ const registry22 = {};
14926
+ for (const [id, tool] of Object.entries(opts.registry)) {
14927
+ registry22[id] = declareSlot(tool, "focus");
14928
+ }
14929
+ const ambient = (opts.ambient ?? []).map((t) => declareSlot(t, "always"));
14930
+ const byId2 = /* @__PURE__ */ new Map();
14931
+ for (const tool of [...Object.values(registry22), ...ambient]) {
14932
+ const prior = byId2.get(tool.id);
14933
+ if (prior) byId2.set(tool.id, { ...tool, eligibility: { ...prior.eligibility, ...tool.eligibility } });
14934
+ else byId2.set(tool.id, tool);
14935
+ }
14936
+ return { registry: registry22, ambient, entries: [...byId2.values()] };
14937
+ }, [opts.registry, opts.ambient]);
14938
+ const contributions = useContributions({ entries: slotted.entries, focused: opts.active });
14939
+ const hotkeyEngaged = ctx.hotkeyStack.at(-1) ?? null;
14940
+ const slottedRef = useRef(slotted);
14941
+ slottedRef.current = slotted;
14942
+ const activeRef = useRef(contributions.focused);
14943
+ activeRef.current = contributions.focused;
14352
14944
  const hotkeyRef = useRef(hotkeyEngaged);
14353
14945
  hotkeyRef.current = hotkeyEngaged;
14946
+ const setFocused = contributions.setFocused;
14354
14947
  const setActive = useCallback(
14355
14948
  (id) => {
14356
- if (!(id in registryRef.current)) {
14949
+ if (!(id in slottedRef.current.registry)) {
14357
14950
  throw new Error(`setActive: "${id}" not in registry`);
14358
14951
  }
14359
14952
  dlog("tools", "active:", activeRef.current, "\u2192", id);
14360
- ctx.setActive(id);
14953
+ setFocused(id);
14361
14954
  },
14362
- [ctx]
14955
+ [setFocused]
14363
14956
  );
14364
14957
  const engageHotkey = useCallback(
14365
14958
  (id) => {
14366
- if (!(id in registryRef.current)) {
14959
+ if (!(id in slottedRef.current.registry)) {
14367
14960
  throw new Error(`engageHotkey: "${id}" not in registry`);
14368
14961
  }
14369
14962
  dlog("tools", "hotkey engaged:", id);
@@ -14375,32 +14968,12 @@ function useTools(opts) {
14375
14968
  if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
14376
14969
  ctx.popHotkey();
14377
14970
  }, [ctx]);
14378
- const lastConflictSigRef = useRef(null);
14379
- const toolSetSig = Object.keys(opts.registry).join(",") + "|" + (opts.ambient ?? []).map((t) => t.id).join(",");
14380
- useEffect(() => {
14381
- if (process.env.NODE_ENV === "production") return;
14382
- if (lastConflictSigRef.current === toolSetSig) return;
14383
- lastConflictSigRef.current = toolSetSig;
14384
- reportRouteConflicts({
14385
- registry: registryRef.current,
14386
- ambient: ambientRef.current
14387
- });
14388
- }, [toolSetSig]);
14389
14971
  const has = useCallback(
14390
- (id) => id in registryRef.current || ambientRef.current.some((t) => t.id === id),
14972
+ (id) => id in slottedRef.current.registry || slottedRef.current.ambient.some((t) => t.id === id),
14391
14973
  []
14392
14974
  );
14393
- const getActiveOverlays = useCallback(() => {
14394
- const out = [];
14395
- const activeTool = registryRef.current[activeRef.current];
14396
- if (activeTool?.overlay) out.push(activeTool.overlay);
14397
- const mod = hotkeyRef.current ? registryRef.current[hotkeyRef.current] : null;
14398
- if (mod?.overlay) out.push(mod.overlay);
14399
- for (const t of ambientRef.current) {
14400
- if (t.overlay) out.push(t.overlay);
14401
- }
14402
- return out;
14403
- }, []);
14975
+ const active = contributions.focused;
14976
+ const getActiveOverlays = contributions.overlays;
14404
14977
  return useMemo(
14405
14978
  () => ({
14406
14979
  active,
@@ -14408,44 +14981,14 @@ function useTools(opts) {
14408
14981
  hotkeyEngaged,
14409
14982
  engageHotkey,
14410
14983
  disengageHotkey,
14411
- ambient: ambientRef.current,
14412
- registry: registryRef.current,
14984
+ ambient: slottedRef.current.ambient,
14985
+ registry: slottedRef.current.registry,
14413
14986
  has,
14414
14987
  getActiveOverlays
14415
14988
  }),
14416
14989
  [active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays]
14417
14990
  );
14418
14991
  }
14419
- var TOOL_OFFHAND_ID = "tool.offhand";
14420
- function buildToolOffhandBindings(specs) {
14421
- return specs.map(({ toolId, key }) => ({
14422
- spec: { kind: "key-held", key },
14423
- opts: { params: { toolId } }
14424
- }));
14425
- }
14426
- function makeToolOffhandAction(bindings) {
14427
- return {
14428
- id: TOOL_OFFHAND_ID,
14429
- label: "Engage tool (offhand)",
14430
- defaultBinding: bindings,
14431
- scope: "hotkey",
14432
- invoker: {
14433
- timing: "ongoing",
14434
- start: (ctx, opts) => {
14435
- const activeTool = ctx.deps.activeTool;
14436
- const toolId = resolveParams(opts?.params)?.toolId;
14437
- if (!activeTool || !toolId) return {};
14438
- activeTool.pushHotkey(toolId);
14439
- return {
14440
- onEnd: () => {
14441
- activeTool.popHotkey();
14442
- }
14443
- };
14444
- }
14445
- },
14446
- requires: ["activeTool"]
14447
- };
14448
- }
14449
14992
  var TOOL_ACTIVATE_ID = "tool.activate";
14450
14993
  function buildToolActivateBindings(specs) {
14451
14994
  return specs.map(({ toolId, keyOpts }) => {
@@ -14517,9 +15060,6 @@ var BUILTIN_SELECT_KEYS = {
14517
15060
  hand: { key: "H" },
14518
15061
  pen: { key: "P" }
14519
15062
  };
14520
- var BUILTIN_OFFHAND_ACTIONS = {
14521
- hand: " "
14522
- };
14523
15063
  function useKeybindings(tools, options = {}) {
14524
15064
  const toolsRef = useRef(tools);
14525
15065
  toolsRef.current = tools;
@@ -14527,19 +15067,6 @@ function useKeybindings(tools, options = {}) {
14527
15067
  optionsRef.current = options;
14528
15068
  const initialActiveRef = useRef(tools.active);
14529
15069
  const registry22 = useActionsRegistry();
14530
- useEffect(() => {
14531
- if (optionsRef.current.disable) return;
14532
- if (!registry22) return;
14533
- const specs = [];
14534
- for (const [toolId, key] of Object.entries(BUILTIN_OFFHAND_ACTIONS)) {
14535
- if (toolsRef.current.has(toolId)) {
14536
- specs.push({ toolId, key });
14537
- }
14538
- }
14539
- if (specs.length === 0) return;
14540
- const bindings = buildToolOffhandBindings(specs);
14541
- return registry22.register(makeToolOffhandAction(bindings));
14542
- }, [registry22, tools]);
14543
15070
  useEffect(() => {
14544
15071
  if (optionsRef.current.disable) return;
14545
15072
  if (!registry22) return;
@@ -14934,7 +15461,82 @@ function textLineBoxes(pose, opts = {}) {
14934
15461
  }
14935
15462
  return out;
14936
15463
  }
15464
+ function fillInPoseFrame(fill, box) {
15465
+ if (isBoundsPattern(fill)) {
15466
+ const o = fill.origin ?? { x: 0, y: 0 };
15467
+ return { ...fill, origin: { x: box.x + o.x, y: box.y + o.y }, units: "local" };
15468
+ }
15469
+ if (!isBoundsGradient(fill)) return fill;
15470
+ const toBox = (p) => ({
15471
+ x: box.x + p.x * box.width,
15472
+ y: box.y + p.y * box.height
15473
+ });
15474
+ if (fill.fill === "linear-gradient") {
15475
+ return { ...fill, from: toBox(fill.from), to: toBox(fill.to), units: "local" };
15476
+ }
15477
+ if (fill.fill === "radial-gradient") {
15478
+ const scale2 = Math.hypot(box.width, box.height) / Math.SQRT2;
15479
+ return { ...fill, center: toBox(fill.center), radius: fill.radius * scale2, units: "local" };
15480
+ }
15481
+ return { ...fill, center: toBox(fill.center), units: "local" };
15482
+ }
15483
+ function isBoundsPattern(fill) {
15484
+ return fill.fill === "pattern" && fill.units === "bounds";
15485
+ }
15486
+ function isBoundsGradient(fill) {
15487
+ if (fill === void 0) return false;
15488
+ const kind = fill.fill;
15489
+ if (kind !== "linear-gradient" && kind !== "radial-gradient" && kind !== "conic-gradient") {
15490
+ return false;
15491
+ }
15492
+ return fill.units === "bounds";
15493
+ }
14937
15494
  var cache5 = /* @__PURE__ */ new Map();
15495
+ function specKey(spec) {
15496
+ const keys = Object.keys(spec).sort();
15497
+ const bag = spec;
15498
+ return keys.map((k) => `${k}=${String(bag[k])}`).join("&");
15499
+ }
15500
+ function resolvePatternSpec(spec) {
15501
+ const key = specKey(spec);
15502
+ const hit = cache5.get(key);
15503
+ if (hit !== void 0) return hit;
15504
+ const built = buildTile(spec);
15505
+ cache5.set(key, built);
15506
+ return built;
15507
+ }
15508
+ function buildTile(spec) {
15509
+ const { color, size } = spec;
15510
+ switch (spec.tile) {
15511
+ case "hatch":
15512
+ return hatch({ color, size, lineWidth: spec.lineWidth });
15513
+ case "crosshatch":
15514
+ return crosshatch({ color, size, lineWidth: spec.lineWidth });
15515
+ case "dots":
15516
+ return dots({ color, size, radius: spec.radius });
15517
+ case "chunks":
15518
+ return chunks({
15519
+ color,
15520
+ size,
15521
+ bg: spec.bg,
15522
+ density: spec.density,
15523
+ chunkSize: spec.chunkSize,
15524
+ seed: spec.seed
15525
+ });
15526
+ default:
15527
+ return null;
15528
+ }
15529
+ }
15530
+ function isPatternSpec(payload) {
15531
+ return "tile" in payload;
15532
+ }
15533
+ function resolveFillPattern(fill) {
15534
+ if (fill.fill !== "pattern") return fill;
15535
+ if (!isPatternSpec(fill.pattern)) return fill;
15536
+ const handle = resolvePatternSpec(fill.pattern);
15537
+ return handle ? { ...fill, pattern: handle } : null;
15538
+ }
15539
+ var cache6 = /* @__PURE__ */ new Map();
14938
15540
  var subscribers2 = /* @__PURE__ */ new Set();
14939
15541
  var defaultLoader = async (src) => {
14940
15542
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -14952,28 +15554,28 @@ function notify() {
14952
15554
  }
14953
15555
  function startLoad(src) {
14954
15556
  const entry = { status: "loading" };
14955
- cache5.set(src, entry);
15557
+ cache6.set(src, entry);
14956
15558
  loader(src).then(
14957
15559
  (bitmap) => {
14958
- if (cache5.get(src) !== entry) return;
14959
- cache5.set(src, { status: "ready", bitmap });
15560
+ if (cache6.get(src) !== entry) return;
15561
+ cache6.set(src, { status: "ready", bitmap });
14960
15562
  notify();
14961
15563
  },
14962
15564
  () => {
14963
- if (cache5.get(src) !== entry) return;
14964
- cache5.set(src, { status: "error" });
15565
+ if (cache6.get(src) !== entry) return;
15566
+ cache6.set(src, { status: "error" });
14965
15567
  notify();
14966
15568
  }
14967
15569
  );
14968
15570
  }
14969
15571
  function getImageBitmap(src) {
14970
- const entry = cache5.get(src);
15572
+ const entry = cache6.get(src);
14971
15573
  if (entry) return entry.bitmap;
14972
15574
  startLoad(src);
14973
15575
  return void 0;
14974
15576
  }
14975
15577
  function imageStatus(src) {
14976
- return cache5.get(src)?.status ?? "idle";
15578
+ return cache6.get(src)?.status ?? "idle";
14977
15579
  }
14978
15580
  function subscribeImageReady(cb) {
14979
15581
  subscribers2.add(cb);
@@ -15151,6 +15753,17 @@ function rectsToPath(rects) {
15151
15753
  }
15152
15754
  return { kind: "polygon", commands, coords, fillRule: "nonzero" };
15153
15755
  }
15756
+ function isFillStyle(v) {
15757
+ return typeof v === "object" && v !== null;
15758
+ }
15759
+ function resolveNodeFill(fill, color, hasStroke, fallback) {
15760
+ if (isFillStyle(fill)) return fill;
15761
+ const c = fill ?? color;
15762
+ if (c === "none") return null;
15763
+ if (c !== void 0) return { color: c };
15764
+ if (hasStroke && fallback === null) return null;
15765
+ return fallback === null ? null : { color: fallback };
15766
+ }
15154
15767
  var PATH_PAINTER = {
15155
15768
  id: "kit:path",
15156
15769
  matches: (node) => {
@@ -15164,12 +15777,12 @@ var PATH_PAINTER = {
15164
15777
  const d = node.data;
15165
15778
  const projected = pathInPoseFrame(d.path, pose);
15166
15779
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
15167
- const fillColor = d.fill ?? d.color;
15168
- const hasFill = fillColor !== "none" && (fillColor !== void 0 || !hasStroke);
15780
+ const declared = resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888");
15781
+ const fill = declared && resolveFillPattern(fillInPoseFrame(declared, pose));
15169
15782
  const cmd = {
15170
15783
  kind: "path",
15171
15784
  path: projected,
15172
- ...hasFill ? { fill: { color: fillColor ?? "#888" } } : {},
15785
+ ...fill ? { fill } : {},
15173
15786
  ...hasStroke ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
15174
15787
  };
15175
15788
  return [cmd];
@@ -15185,9 +15798,8 @@ var PATH_PAINTER = {
15185
15798
  ink: (node) => {
15186
15799
  const d = node.data;
15187
15800
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
15188
- const fillColor = d.fill ?? d.color;
15189
15801
  return {
15190
- filled: fillColor !== "none" && (fillColor !== void 0 || !hasStroke),
15802
+ filled: resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888") !== null,
15191
15803
  strokeWidth: hasStroke ? d.strokeWidth ?? 1 : 0
15192
15804
  };
15193
15805
  }
@@ -15204,10 +15816,12 @@ var SHAPE_PAINTER = {
15204
15816
  paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
15205
15817
  const d = node.data;
15206
15818
  const path = pathForShape(d, pose);
15819
+ const declaredFill = resolveNodeFill(d.fill, d.color, false, "#888");
15820
+ const shapeFill = declaredFill && resolveFillPattern(fillInPoseFrame(declaredFill, pose));
15207
15821
  return [{
15208
15822
  kind: "path",
15209
15823
  path,
15210
- fill: { color: d.fill ?? d.color ?? "#888" },
15824
+ ...shapeFill ? { fill: shapeFill } : {},
15211
15825
  ...d.stroke && (d.strokeWidth ?? 0) > 0 ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
15212
15826
  }];
15213
15827
  }),
@@ -15603,11 +16217,8 @@ function useSelectTool(adapter, options) {
15603
16217
  ...moveOpts
15604
16218
  },
15605
16219
  { spec: { kind: "drag", target: "empty" }, actionId: "areaSelect" },
15606
- // Click on empty canvas clear. Same no-affordance requirement as
15607
- // the press binding above, and for the same two reasons: clicking a
15608
- // widget that happens to float over empty canvas isn't a click on
15609
- // empty canvas, and declining in the SPEC lets the chrome's owner
15610
- // have the click instead of it being consumed here.
16220
+ // resize/rotate bind only drag (no click), so a click on either handle
16221
+ // (body genuinely empty) would hit target:'empty' and wrongly clear selection.
15611
16222
  {
15612
16223
  spec: {
15613
16224
  kind: "click",
@@ -15691,17 +16302,17 @@ function worldToLocal(xf, wx, wy) {
15691
16302
  function annulusSemiAxes(shape, view) {
15692
16303
  const bandPx = shape.minBandPx ?? 0;
15693
16304
  if (bandPx === 0) return { rx: shape.rx, ry: shape.ry };
15694
- const band = bandPx / meanScale(view.scale);
16305
+ const band = pxExtent(bandPx, view.scale);
15695
16306
  return {
15696
- rx: Math.max(shape.rx, shape.innerWidth / 2 + band),
15697
- ry: Math.max(shape.ry, shape.innerHeight / 2 + band)
16307
+ rx: Math.max(shape.rx, shape.innerWidth / 2 + band.x),
16308
+ ry: Math.max(shape.ry, shape.innerHeight / 2 + band.y)
15698
16309
  };
15699
16310
  }
15700
16311
  function hitRegion(region, wx, wy, xf, view) {
15701
16312
  const local = worldToLocal(xf, wx, wy);
15702
16313
  if (region.shape.kind === "point") {
15703
- const radiusWorld = region.shape.hitRadiusPx / meanScale(view.scale);
15704
- return Math.abs(local.x - region.shape.x) <= radiusWorld && Math.abs(local.y - region.shape.y) <= radiusWorld;
16314
+ const anchor = localToWorld(xf, region.shape.x, region.shape.y);
16315
+ return withinPxBox(anchor.x - wx, anchor.y - wy, region.shape.hitRadiusPx, view.scale);
15705
16316
  }
15706
16317
  if (region.shape.kind === "annulus") {
15707
16318
  const s = region.shape;
@@ -15765,14 +16376,13 @@ function asIsVisible(data) {
15765
16376
  function recordRegionHitbox(debug, affordanceId, region, xf, view) {
15766
16377
  if (region.shape.kind === "point") {
15767
16378
  const w2 = localToWorld(xf, region.shape.x, region.shape.y);
15768
- const r2 = region.shape.hitRadiusPx / meanScale(view.scale);
16379
+ const e = pxExtent(region.shape.hitRadiusPx, view.scale);
15769
16380
  debug.recordHitbox(affordanceId, "handle", {
15770
16381
  kind: "rect",
15771
- x: w2.x - r2,
15772
- y: w2.y - r2,
15773
- width: r2 * 2,
15774
- height: r2 * 2,
15775
- ...xf.identity ? {} : { rotation: Math.atan2(xf.sin, xf.cos) }
16382
+ x: w2.x - e.x,
16383
+ y: w2.y - e.y,
16384
+ width: e.x * 2,
16385
+ height: e.y * 2
15776
16386
  });
15777
16387
  return;
15778
16388
  }
@@ -15831,7 +16441,7 @@ function paintRegion(region, xf, view, state, viewT, out) {
15831
16441
  if (paint.kind === "annulus") {
15832
16442
  if (region.shape.kind !== "annulus") return;
15833
16443
  const s = region.shape;
15834
- const insetLocal = (paint.insetPx ?? 0) / meanScale(view.scale);
16444
+ const insetLocal = pxExtent(paint.insetPx ?? 0, view.scale);
15835
16445
  const cmd = annulusCommand(s, annulusSemiAxes(s, view), insetLocal, xf, viewT, paint.fill, paint.stroke);
15836
16446
  if (cmd) out.push(cmd);
15837
16447
  return;
@@ -15848,13 +16458,13 @@ function paintRegion(region, xf, view, state, viewT, out) {
15848
16458
  }
15849
16459
  }
15850
16460
  function annulusCommand(shape, semiAxes, insetLocal, xf, viewT, fill, stroke) {
15851
- const rx = semiAxes.rx - insetLocal;
15852
- const ry = semiAxes.ry - insetLocal;
16461
+ const rx = semiAxes.rx - insetLocal.x;
16462
+ const ry = semiAxes.ry - insetLocal.y;
15853
16463
  if (rx <= 0 || ry <= 0) return null;
15854
16464
  const cx = shape.cx;
15855
16465
  const cy = shape.cy;
15856
- const rectHW = shape.innerWidth / 2 + insetLocal;
15857
- const rectHH = shape.innerHeight / 2 + insetLocal;
16466
+ const rectHW = shape.innerWidth / 2 + insetLocal.x;
16467
+ const rectHH = shape.innerHeight / 2 + insetLocal.y;
15858
16468
  const haveTopBottom = rectHH < ry;
15859
16469
  const haveLeftRight = rectHW < rx;
15860
16470
  if (!haveTopBottom && !haveLeftRight) return null;
@@ -16101,6 +16711,7 @@ function useHandTool(opts = {}) {
16101
16711
  () => Object.assign(defineViewportTool({
16102
16712
  id: "hand",
16103
16713
  capabilities: ["navigation"],
16714
+ hotkey: "space",
16104
16715
  hookName: "useHandTool",
16105
16716
  presentation: {
16106
16717
  label: "Hand",
@@ -16221,10 +16832,6 @@ function mirrorHandle(anchor, out) {
16221
16832
  if (!out || anchor.altBroken) return void 0;
16222
16833
  return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
16223
16834
  }
16224
- function dist2(ax, ay, bx, by) {
16225
- const dx = ax - bx, dy = ay - by;
16226
- return Math.hypot(dx, dy);
16227
- }
16228
16835
  function usePenTool(options) {
16229
16836
  const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, snapPoint: snapPoint2 } = options;
16230
16837
  const scratchRef = useRef(null);
@@ -16248,11 +16855,14 @@ function usePenTool(options) {
16248
16855
  const fn = optsRef.current.snapPoint;
16249
16856
  return fn ? fn({ x, y }) : { x, y };
16250
16857
  }, []);
16251
- const closeRadius = useCallback((deps) => {
16252
- const view = deps.view?.get();
16253
- const scale2 = view?.scale ?? { x: 1, y: 1 };
16254
- return optsRef.current.closeHitRadius / meanScale(scale2);
16255
- }, []);
16858
+ const withinCloseRadius = useCallback(
16859
+ (deps, ax, ay, wx, wy) => {
16860
+ const view = deps.view?.get();
16861
+ const scale2 = view?.scale ?? { x: 1, y: 1 };
16862
+ return withinPxRadius(ax - wx, ay - wy, optsRef.current.closeHitRadius, scale2);
16863
+ },
16864
+ []
16865
+ );
16256
16866
  const actions = useMemo(() => {
16257
16867
  const s = () => scratchRef.current;
16258
16868
  return [
@@ -16268,10 +16878,9 @@ function usePenTool(options) {
16268
16878
  if (p?.pressX === void 0 || p.pressY === void 0) return;
16269
16879
  const { x: wx, y: wy } = snap2(p.pressX, p.pressY);
16270
16880
  const scratch = s();
16271
- const radius = closeRadius(deps);
16272
16881
  if (scratch.current && scratch.current.anchors.length >= 3) {
16273
16882
  const first = scratch.current.anchors[0];
16274
- if (dist2(first.x, first.y, wx, wy) <= radius) {
16883
+ if (withinCloseRadius(deps, first.x, first.y, wx, wy)) {
16275
16884
  scratch.current.closed = true;
16276
16885
  scratch.finishedSubpaths.push(scratch.current);
16277
16886
  scratch.current = null;
@@ -16390,7 +16999,7 @@ function usePenTool(options) {
16390
16999
  }
16391
17000
  }
16392
17001
  ];
16393
- }, [commit, snap2, closeRadius]);
17002
+ }, [commit, snap2, withinCloseRadius]);
16394
17003
  return useMemo(() => {
16395
17004
  return defineTool({
16396
17005
  id: "pen",
@@ -17956,8 +18565,8 @@ function CanvasInner(props, ref) {
17956
18565
  const isVisible = getIsVisibleRef.current?.() ?? alwaysVisible;
17957
18566
  for (const layer of layers2) {
17958
18567
  if (!layer.hitTest) continue;
17959
- const binding = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
17960
- if (binding) return { layerId: layer.id, binding };
18568
+ const hit = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
18569
+ if (hit) return { layerId: layer.id, hit };
17961
18570
  }
17962
18571
  return null;
17963
18572
  }, []);
@@ -18393,7 +19002,7 @@ function CanvasInner(props, ref) {
18393
19002
  effectiveView,
18394
19003
  { width, height }
18395
19004
  );
18396
- renderer.render(commands);
19005
+ renderer.render(commands, viewToMat3(effectiveView));
18397
19006
  }, [layersWithDebug, width, height, effectiveView, debugSink, redrawNonce, dprProp]);
18398
19007
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
18399
19008
  useEffect(() => {
@@ -20774,6 +21383,8 @@ function toAffordanceHit(hit) {
20774
21383
  const scratch = hit.binding.initialScratch ?? {};
20775
21384
  return {
20776
21385
  kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,
21386
+ owner: hit.affordanceId,
21387
+ strength: "shared",
20777
21388
  ...scratch.targetId !== void 0 ? { targetIds: [scratch.targetId] } : {},
20778
21389
  ...scratch.anchor !== void 0 ? { anchor: scratch.anchor } : {},
20779
21390
  ...scratch.fixedPoint !== void 0 ? { fixedPoint: scratch.fixedPoint } : {},
@@ -21294,7 +21905,7 @@ function chordLengthParam(samples) {
21294
21905
  ts[0] = 0;
21295
21906
  let total = 0;
21296
21907
  for (let i = 1; i < n; i++) {
21297
- total += dist3(samples[i - 1], samples[i]);
21908
+ total += dist2(samples[i - 1], samples[i]);
21298
21909
  ts[i] = total;
21299
21910
  }
21300
21911
  if (total === 0) return null;
@@ -21327,13 +21938,13 @@ function solveLS(samples, ts, a, e, tStart, tEnd) {
21327
21938
  let alpha1;
21328
21939
  let alpha2;
21329
21940
  if (Math.abs(det) < 1e-12) {
21330
- const chord = dist3(a, e);
21941
+ const chord = dist2(a, e);
21331
21942
  alpha1 = alpha2 = chord / 3;
21332
21943
  } else {
21333
21944
  alpha1 = (c22 * x1 - c12 * x2) / det;
21334
21945
  alpha2 = (c11 * x2 - c12 * x1) / det;
21335
21946
  if (alpha1 < 1e-6 || alpha2 < 1e-6) {
21336
- const chord = dist3(a, e);
21947
+ const chord = dist2(a, e);
21337
21948
  alpha1 = alpha2 = chord / 3;
21338
21949
  }
21339
21950
  }
@@ -21369,7 +21980,7 @@ function newtonRefineT(p, t, p0, p1, p2, p3) {
21369
21980
  function sub(a, b) {
21370
21981
  return { x: a.x - b.x, y: a.y - b.y };
21371
21982
  }
21372
- function dist3(a, b) {
21983
+ function dist2(a, b) {
21373
21984
  return Math.hypot(a.x - b.x, a.y - b.y);
21374
21985
  }
21375
21986
  function unit(v) {
@@ -22329,7 +22940,7 @@ function SceneCanvasInner(props, ref) {
22329
22940
  const getMode = getActiveModeRef.current;
22330
22941
  if (!getMode) return true;
22331
22942
  const registry22 = toolsForEligibilityRef.current?.registry;
22332
- const caps = registry22?.[toolId]?.capabilities ?? [];
22943
+ const caps = registry22?.[toolId]?.eligibility.capabilities ?? [];
22333
22944
  if (caps.length === 0) return false;
22334
22945
  const allowed = getMode().allowedCapabilities;
22335
22946
  for (const c of caps) if (allowed.has(c)) return true;
@@ -22803,10 +23414,6 @@ function GestureDispatcherMounter({
22803
23414
  for (const tool of tools.ambient) m.set(tool.id, tool);
22804
23415
  return m;
22805
23416
  }, [tools.registry, tools.ambient]);
22806
- const ambientToolIds = useMemo(
22807
- () => tools.ambient.map((t) => t.id),
22808
- [tools.ambient]
22809
- );
22810
23417
  const boundsOfRef = useRef(boundsOf2);
22811
23418
  boundsOfRef.current = boundsOf2;
22812
23419
  const pickEveryRef = useRef(pickEvery);
@@ -22918,9 +23525,14 @@ function GestureDispatcherMounter({
22918
23525
  const worldPoint = clientToWorld2(screenPoint.x, screenPoint.y);
22919
23526
  const extra = canvasApiRef?.current?.hitTestExtras?.(worldPoint.x, worldPoint.y);
22920
23527
  if (extra) {
23528
+ const claim = extra.hit;
22921
23529
  return {
22922
23530
  kind: `layer:${extra.layerId}`,
22923
- ...extra.binding.initialScratch !== void 0 ? { payload: extra.binding.initialScratch } : {}
23531
+ owner: extra.layerId,
23532
+ strength: claim.strength ?? "shared",
23533
+ ...claim.claimedKinds !== void 0 ? { claimedKinds: claim.claimedKinds } : {},
23534
+ ...claim.cursor !== void 0 ? { cursor: claim.cursor } : {},
23535
+ ...claim.initialScratch !== void 0 ? { payload: claim.initialScratch } : {}
22924
23536
  };
22925
23537
  }
22926
23538
  return affordanceAt ? affordanceAt(worldPoint) : null;
@@ -22940,7 +23552,6 @@ function GestureDispatcherMounter({
22940
23552
  canvasRef,
22941
23553
  actions: registry22,
22942
23554
  toolsById,
22943
- ambientToolIds,
22944
23555
  enabled,
22945
23556
  keyboard,
22946
23557
  affordanceAt: wrappedAffordanceAt,
@@ -23171,7 +23782,7 @@ function renderSceneToCanvas(args) {
23171
23782
  entry.dpr = dpr;
23172
23783
  }
23173
23784
  const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands);
23174
- entry.renderer.render(commands);
23785
+ entry.renderer.render(commands, viewToMat3(view));
23175
23786
  }
23176
23787
  function SceneViewCanvasInner(props) {
23177
23788
  const { scene, view, width, height, drawOne, extraCommands, className, canvasRef } = props;
@@ -23545,6 +24156,6 @@ function oklchToOklab(L, C, h) {
23545
24156
  return [L, C * Math.cos(h), C * Math.sin(h)];
23546
24157
  }
23547
24158
 
23548
- export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleTool, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
23549
- //# sourceMappingURL=chunk-6V7ROR7G.js.map
23550
- //# sourceMappingURL=chunk-6V7ROR7G.js.map
24159
+ export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleContribution, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
24160
+ //# sourceMappingURL=chunk-XVBXYKFJ.js.map
24161
+ //# sourceMappingURL=chunk-XVBXYKFJ.js.map