@weasel-js/labkit 0.7.2 → 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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -3
  3. package/dist/_dts/{DrawCommand-CEvS5AdV.d.ts → DrawCommand-BwRCZYuo.d.ts} +106 -52
  4. package/dist/_dts/{useExperimentState-eSvpwV_P.d.ts → useExperimentState-CJn2hHzd.d.ts} +6 -4
  5. package/dist/{chunk-SSMHPMDV.js → chunk-53XSBIUK.js} +12 -11
  6. package/dist/chunk-53XSBIUK.js.map +1 -0
  7. package/dist/{chunk-BG6NLMOL.js → chunk-FP5LYDVX.js} +1229 -1227
  8. package/dist/chunk-FP5LYDVX.js.map +1 -0
  9. package/dist/{chunk-ERZQAU4N.js → chunk-HNXVLKBG.js} +3 -3
  10. package/dist/{chunk-ERZQAU4N.js.map → chunk-HNXVLKBG.js.map} +1 -1
  11. package/dist/{chunk-5XDFILHZ.js → chunk-XVBXYKFJ.js} +1675 -670
  12. package/dist/chunk-XVBXYKFJ.js.map +1 -0
  13. package/dist/fonts/inter-latin.woff2 +0 -0
  14. package/dist/fonts/oswald-latin-variable.woff2 +0 -0
  15. package/dist/index.d.ts +37 -13
  16. package/dist/index.js +776 -27
  17. package/dist/index.js.map +1 -1
  18. package/dist/passthrough/weasel-canvas.d.ts +1 -1
  19. package/dist/passthrough/weasel-canvas.js +1 -1
  20. package/dist/passthrough/weasel-ui.d.ts +332 -189
  21. package/dist/passthrough/weasel-ui.js +2 -2
  22. package/dist/state/index.d.ts +2 -2
  23. package/dist/state/index.js +2 -2
  24. package/dist/styles.css +229 -288
  25. package/dist/ui/layers/index.js +3 -3
  26. package/package.json +5 -6
  27. package/src/canvas/CanvasStack.less +1 -1
  28. package/src/controls/ControlPanel.less +12 -12
  29. package/src/dragdrop/DragGhost.less +2 -2
  30. package/src/dragdrop/Palette.less +6 -6
  31. package/src/index.ts +2 -0
  32. package/src/lab/Lab.test.tsx +20 -15
  33. package/src/lab/Lab.tsx +33 -32
  34. package/src/lab/LabContext.ts +3 -3
  35. package/src/lab/LabShell.less +14 -14
  36. package/src/lab/LabShell.test.tsx +7 -20
  37. package/src/lab/LabShell.tsx +22 -13
  38. package/src/lab/WorkspaceGrid.less +1 -1
  39. package/src/lab/WorkspaceGrid.stories.tsx +4 -4
  40. package/src/lab/index.ts +1 -1
  41. package/src/lab/useSystemMode.ts +30 -0
  42. package/src/layers/LayerList.less +9 -9
  43. package/src/primitives/FpsMeter.less +5 -5
  44. package/src/primitives/ScaleIndicator.less +7 -7
  45. package/src/primitives/Sidebar.less +12 -12
  46. package/src/primitives/StatusBar.less +10 -10
  47. package/src/primitives/Toolbar.less +16 -16
  48. package/src/state/store.test.ts +17 -9
  49. package/src/state/store.ts +13 -10
  50. package/src/state/types.ts +5 -2
  51. package/src/theme/Interstellar.stories.less +20 -20
  52. package/src/theme/Interstellar.stories.tsx +44 -59
  53. package/src/theme/base.less +29 -28
  54. package/src/theme/interstellar.test.ts +30 -0
  55. package/src/theme/interstellar.tokens.json +57 -0
  56. package/src/theme/interstellar.ts +9 -0
  57. package/src/ui/layers/LayerStack.less +21 -21
  58. package/src/ui/properties/CurveField.less +5 -5
  59. package/src/ui/properties/EffectCard.tsx +2 -2
  60. package/src/ui/properties/PropertyGroup.less +6 -6
  61. package/src/ui/properties/PropertyPanel.less +70 -70
  62. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +1 -1
  63. package/src/ui/properties/storyLayouts.tsx +1 -1
  64. package/src/workspace/Workspace.less +14 -14
  65. package/src/workspace/Workspace.stories.tsx +2 -2
  66. package/src/workspace/Workspace.test.tsx +2 -2
  67. package/dist/chunk-5XDFILHZ.js.map +0 -1
  68. package/dist/chunk-BG6NLMOL.js.map +0 -1
  69. package/dist/chunk-SSMHPMDV.js.map +0 -1
  70. package/dist/theme-interstellar.css +0 -25
  71. package/dist/theme-light.css +0 -13
  72. package/src/fonts/oswald-latin-variable.woff2 +0 -0
  73. package/src/theme/interstellar.less +0 -34
  74. package/src/theme/light.less +0 -14
  75. package/src/theme/tokens.less +0 -85
@@ -16,11 +16,12 @@ 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" } },
23
23
  { name: "contextMenu", hasTarget: true },
24
+ { name: "longPress", hasTarget: true },
24
25
  { name: "multiTouchTap", hasTarget: false, arg: { name: "fingers", values: ["2", "3", "4"] } }
25
26
  ];
26
27
  var BY_NAME = new Map(
@@ -318,6 +319,8 @@ function actionClause(parsed, required) {
318
319
  return `the user taps with ${parsed.arg ?? "multiple"} fingers`;
319
320
  case "contextMenu":
320
321
  return `the user ${modPrefix}opens the context menu${target}`;
322
+ case "longPress":
323
+ return `the user ${modPrefix}presses and holds${target}`;
321
324
  case "click":
322
325
  return `the user ${modPrefix}clicks${target}`;
323
326
  case "pointerDown":
@@ -385,12 +388,12 @@ function checkKey(req, held) {
385
388
  if (req === "forbidden") return !held;
386
389
  return true;
387
390
  }
388
- function matchKey(eventKey, specKey) {
391
+ function matchKey(eventKey, specKey2) {
389
392
  const lower = eventKey.toLowerCase();
390
- if (Array.isArray(specKey)) {
391
- return specKey.some((k) => k.toLowerCase() === lower);
393
+ if (Array.isArray(specKey2)) {
394
+ return specKey2.some((k) => k.toLowerCase() === lower);
392
395
  }
393
- return specKey.toLowerCase() === lower;
396
+ return specKey2.toLowerCase() === lower;
394
397
  }
395
398
  function affordanceKindOf(target) {
396
399
  if (typeof target !== "object" || target === null) return void 0;
@@ -485,7 +488,7 @@ function matchSpec(e, spec, isMac, phaseCtx) {
485
488
  const direction = spec.direction ?? "*";
486
489
  if (direction === "up" && !(e.deltaY < 0)) return false;
487
490
  if (direction === "down" && !(e.deltaY > 0)) return false;
488
- return true;
491
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
489
492
  }
490
493
  case "click": {
491
494
  if (e.kind !== "click") return false;
@@ -495,12 +498,17 @@ function matchSpec(e, spec, isMac, phaseCtx) {
495
498
  case "doubleClick": {
496
499
  if (e.kind !== "doubleclick") return false;
497
500
  if (!matchModifiers(e, spec.mods, isMac)) return false;
498
- return matchTarget(e.target, spec.target, e.bodyTarget, e.bodyKind);
501
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
499
502
  }
500
503
  case "contextMenu": {
501
504
  if (e.kind !== "contextmenu") return false;
502
505
  if (!matchModifiers(e, spec.mods, isMac)) return false;
503
- return matchTarget(e.target, spec.target, e.bodyTarget, e.bodyKind);
506
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
507
+ }
508
+ case "longPress": {
509
+ if (e.kind !== "longpress") return false;
510
+ if (!matchModifiers(e, spec.mods, isMac)) return false;
511
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
504
512
  }
505
513
  case "drag": {
506
514
  if (e.kind !== "pointerdown") return false;
@@ -539,8 +547,6 @@ function matchSpec(e, spec, isMac, phaseCtx) {
539
547
  }
540
548
  }
541
549
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
542
-
543
- // ../core/dist/chunk-SYM6RAM4.js
544
550
  var routing_exports = {};
545
551
  __export(routing_exports, {
546
552
  GESTURE_DESCRIPTORS: () => GESTURE_DESCRIPTORS,
@@ -589,7 +595,11 @@ function defineTool(def) {
589
595
  };
590
596
  return {
591
597
  id: def.id,
592
- capabilities: def.capabilities,
598
+ eligibility: {
599
+ focus: true,
600
+ capabilities: def.capabilities,
601
+ ...def.hotkey ? { offhand: def.hotkey } : {}
602
+ },
593
603
  actions: def.actions,
594
604
  def,
595
605
  presentation: def.presentation,
@@ -613,6 +623,7 @@ var SPEC_KIND_TO_GESTURE = {
613
623
  click: "click",
614
624
  doubleClick: "dblTap",
615
625
  contextMenu: "contextMenu",
626
+ longPress: "longPress",
616
627
  drag: "drag",
617
628
  pointerDown: "pointerDown",
618
629
  multiTouch: void 0,
@@ -674,6 +685,255 @@ function argOf(spec, fallback) {
674
685
  if ("direction" in spec) return spec.direction ?? fallback;
675
686
  return fallback;
676
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
+ }
677
937
  function findConflicts(tools) {
678
938
  const entries = buildRouteRegistry(tools);
679
939
  const groups = /* @__PURE__ */ new Map();
@@ -715,9 +975,15 @@ function predicateId(pred) {
715
975
  }
716
976
  return id;
717
977
  }
978
+ function actionAsTool(action) {
979
+ return { id: action.id, eligibility: {}, bindings: actionBindings(action) };
980
+ }
718
981
  function findScopedConflicts(scopes) {
719
982
  const registry4 = Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry);
720
- 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];
721
987
  const out = [];
722
988
  const seen = /* @__PURE__ */ new Set();
723
989
  const add = (conflicts) => {
@@ -728,12 +994,13 @@ function findScopedConflicts(scopes) {
728
994
  out.push(c);
729
995
  }
730
996
  };
731
- for (const tool of [...registry4, ...ambient]) add(findConflicts([tool]));
997
+ for (const tool of [...registry4, ...scopes.ambient ?? []]) add(findConflicts([tool]));
732
998
  if (ambient.length > 1) add(findConflicts(ambient));
733
- const hotkeyTools = registry4.filter(
734
- (t) => t.def?.hotkey !== void 0
735
- );
736
- 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));
737
1004
  return out;
738
1005
  }
739
1006
  function formatConflict(conflict) {
@@ -873,7 +1140,7 @@ function transformCoords(coords, m) {
873
1140
  return out;
874
1141
  }
875
1142
 
876
- // ../core/dist/chunk-CQNKCG34.js
1143
+ // ../core/dist/chunk-6SHZHXXR.js
877
1144
  function resolveUnit(v, unitSystem) {
878
1145
  if (typeof v === "number") return v;
879
1146
  if (!unitSystem) {
@@ -1444,6 +1711,28 @@ var RECT_POSE_DESCRIPTOR = {
1444
1711
  translate: RECT_POSE_DESCRIPTOR.translate,
1445
1712
  intersectsRect: RECT_POSE_DESCRIPTOR.intersectsRect,
1446
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
+ }
1447
1736
  var pathPoseDescriptor = {
1448
1737
  getBounds: (path) => boundsOfPath(path),
1449
1738
  remapBounds: (path, src, dst) => {
@@ -1540,14 +1829,29 @@ var RECT_ORIGIN_PROJECTION = {
1540
1829
  getOrigin: (p) => ({ x: p.x, y: p.y }),
1541
1830
  translate: (p, dx, dy) => ({ ...p, x: p.x + dx, y: p.y + dy })
1542
1831
  };
1543
- function meanScale(s) {
1544
- 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;
1545
1849
  }
1546
1850
 
1547
1851
  // ../core/dist/chunk-GVCNT7UH.js
1548
1852
  var FACTORIES = /* @__PURE__ */ new Map();
1549
- function registerOpFactory(name, build) {
1550
- FACTORIES.set(name, build);
1853
+ function registerOpFactory(name, build2) {
1854
+ FACTORIES.set(name, build2);
1551
1855
  }
1552
1856
  function rebuildOp(name, args) {
1553
1857
  const f = FACTORIES.get(name);
@@ -1584,7 +1888,7 @@ function createDeleteOp(args) {
1584
1888
  }
1585
1889
  registerOpFactory("delete", (args) => createDeleteOp(args));
1586
1890
 
1587
- // ../core/dist/chunk-VOVKONXA.js
1891
+ // ../core/dist/chunk-UB6A6L77.js
1588
1892
  function createTransformOp(args) {
1589
1893
  const { id, from, to, label, coalesceKey = `transform:${id}` } = args;
1590
1894
  return {
@@ -1665,7 +1969,7 @@ function snap(strategy, opts = {}) {
1665
1969
  };
1666
1970
  }
1667
1971
 
1668
- // ../core/dist/chunk-BGGZ4CVF.js
1972
+ // ../core/dist/chunk-Z3KNFTTS.js
1669
1973
  function lockAspectWithModifier(opts = {}) {
1670
1974
  const { key = "shift" } = opts;
1671
1975
  return {
@@ -1861,13 +2165,165 @@ function materializePaste(adapter, snapshot, opts) {
1861
2165
  return { ops, newIds, created };
1862
2166
  }
1863
2167
 
1864
- // ../core/dist/chunk-Y52N27PF.js
2168
+ // ../core/dist/chunk-IO5Z75X4.js
2169
+ var counter = 0;
1865
2170
  var registry = /* @__PURE__ */ new Map();
1866
2171
  function getTexture(id) {
1867
2172
  return registry.get(id) ?? null;
1868
2173
  }
1869
-
1870
- // ../font/dist/index.js
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
1871
2327
  var subscribers = /* @__PURE__ */ new Set();
1872
2328
  var generation = 0;
1873
2329
  function glyphGeneration() {
@@ -2003,8 +2459,8 @@ function alphaToSdf(alpha, width, height, radius, cutoff) {
2003
2459
  edt2d(gridInner, width, height, f, d, v, z);
2004
2460
  const out = new Uint8Array(n);
2005
2461
  for (let i = 0; i < n; i++) {
2006
- const dist4 = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]);
2007
- 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));
2008
2464
  out[i] = byte < 0 ? 0 : byte > 255 ? 255 : byte;
2009
2465
  }
2010
2466
  return out;
@@ -2210,17 +2666,17 @@ function dynamicPageTextureId(page) {
2210
2666
  return `weasel-dyn-sdf-page-${page}`;
2211
2667
  }
2212
2668
  var uploadedVersions = /* @__PURE__ */ new WeakMap();
2213
- function syncDynamicPageTexture(cache5, pageIndex) {
2669
+ function syncDynamicPageTexture(cache7, pageIndex) {
2214
2670
  const page = pages[pageIndex];
2215
2671
  if (!page) return false;
2216
2672
  const id = dynamicPageTextureId(pageIndex);
2217
- let seen = uploadedVersions.get(cache5);
2673
+ let seen = uploadedVersions.get(cache7);
2218
2674
  if (!seen) {
2219
2675
  seen = /* @__PURE__ */ new Map();
2220
- uploadedVersions.set(cache5, seen);
2676
+ uploadedVersions.set(cache7, seen);
2221
2677
  }
2222
- if (!cache5.has(id)) {
2223
- cache5.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
2678
+ if (!cache7.has(id)) {
2679
+ cache7.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
2224
2680
  seen.set(pageIndex, page.version);
2225
2681
  return true;
2226
2682
  }
@@ -2233,7 +2689,7 @@ function syncDynamicPageTexture(cache5, pageIndex) {
2233
2689
  const src = (patch.y + row) * PAGE_SIZE + patch.x;
2234
2690
  tight.set(page.data.subarray(src, src + patch.w), row * patch.w);
2235
2691
  }
2236
- cache5.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
2692
+ cache7.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
2237
2693
  }
2238
2694
  seen.set(pageIndex, page.version);
2239
2695
  return true;
@@ -2441,6 +2897,23 @@ var slots = /* @__PURE__ */ new Map();
2441
2897
  function slotKey(family, weight, style) {
2442
2898
  return `${family}|${weight}|${style}`;
2443
2899
  }
2900
+ function closeContours(d) {
2901
+ let out = "";
2902
+ let start = 0;
2903
+ for (let i = 1; i < d.length; i++) {
2904
+ const c = d[i];
2905
+ if (c !== "M" && c !== "m") continue;
2906
+ out += closeOne(d.slice(start, i));
2907
+ start = i;
2908
+ }
2909
+ return out + closeOne(d.slice(start));
2910
+ }
2911
+ function closeOne(contour) {
2912
+ const trimmed = contour.trimEnd();
2913
+ if (trimmed.length === 0) return "";
2914
+ const last = trimmed[trimmed.length - 1];
2915
+ return last === "Z" || last === "z" ? trimmed : `${trimmed}Z`;
2916
+ }
2444
2917
  function glyphOutline(family, weight, style, cp) {
2445
2918
  const slot = slots.get(slotKey(family, weight, style));
2446
2919
  if (!slot) return null;
@@ -2453,7 +2926,8 @@ function glyphOutline(family, weight, style, cp) {
2453
2926
  if (cached2 !== void 0) return cached2;
2454
2927
  let d = null;
2455
2928
  try {
2456
- d = slot.face.glyphD(cp);
2929
+ const raw = slot.face.glyphD(cp);
2930
+ d = raw === null ? null : closeContours(raw);
2457
2931
  } catch (err) {
2458
2932
  warnOnce(
2459
2933
  `glyph|${slotKey(family, weight, style)}`,
@@ -2612,8 +3086,8 @@ function eligibleForMode(mode, toolTags) {
2612
3086
  }
2613
3087
  return false;
2614
3088
  }
2615
- function eligibleTool(reg, tool) {
2616
- return eligibleForMode(reg.current(), tool.capabilities ?? []);
3089
+ function eligibleContribution(reg, entry) {
3090
+ return eligibleForMode(reg.current(), entry.eligibility?.capabilities ?? []);
2617
3091
  }
2618
3092
 
2619
3093
  // ../history/dist/index.js
@@ -2982,7 +3456,7 @@ function serialToEntry(se, custom, logger) {
2982
3456
  };
2983
3457
  }
2984
3458
 
2985
- // ../core/dist/chunk-5F3PIERN.js
3459
+ // ../core/dist/chunk-D2C6K6FO.js
2986
3460
  function worldToScreen(worldX, worldY, view) {
2987
3461
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
2988
3462
  }
@@ -3233,6 +3707,58 @@ var GRAD_FILL_UNIFORMS = [
3233
3707
  "u_gradAngle"
3234
3708
  ];
3235
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"];
3236
3762
  var GLMeshCache = class {
3237
3763
  constructor(gl, aPositionLoc) {
3238
3764
  this.gl = gl;
@@ -3355,17 +3881,20 @@ var GLTextureCache = class {
3355
3881
  has(id) {
3356
3882
  return this.map.has(id);
3357
3883
  }
3358
- 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") {
3359
3887
  if (this.map.has(id)) return id;
3360
3888
  const gl = this.gl;
3361
3889
  const tex = gl.createTexture();
3362
3890
  if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
3891
+ const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
3363
3892
  gl.bindTexture(gl.TEXTURE_2D, tex);
3364
3893
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
3365
3894
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3366
3895
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3367
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3368
- 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);
3369
3898
  gl.bindTexture(gl.TEXTURE_2D, null);
3370
3899
  this.map.set(id, tex);
3371
3900
  return id;
@@ -3960,6 +4489,18 @@ function scale(m, sx, sy) {
3960
4489
  const s = create(sx, 0, 0, sy, 0, 0);
3961
4490
  return multiply2(m, s);
3962
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
+ }
3963
4504
  function apply(m, x, y) {
3964
4505
  const a = m[0], b = m[1];
3965
4506
  const c = m[3], d = m[4];
@@ -3987,6 +4528,7 @@ var mat3 = {
3987
4528
  multiply: multiply2,
3988
4529
  translate: translate2,
3989
4530
  scale,
4531
+ invert,
3990
4532
  apply,
3991
4533
  screenToClip
3992
4534
  };
@@ -4355,6 +4897,16 @@ function extractPolygon(p, opts) {
4355
4897
  out.push(next);
4356
4898
  return next;
4357
4899
  };
4900
+ const dropDuplicateClosingPoint = () => {
4901
+ if (!pts || !aA || !aB || !aT) return;
4902
+ const n = pts.length / 2;
4903
+ if (n < 2) return;
4904
+ if (pts[0] !== pts[(n - 1) * 2] || pts[1] !== pts[(n - 1) * 2 + 1]) return;
4905
+ pts.length -= 2;
4906
+ aA.length -= 1;
4907
+ aB.length -= 1;
4908
+ aT.length -= 1;
4909
+ };
4358
4910
  const commit = (target) => {
4359
4911
  if (!pts || !aA || !aB || !aT) return;
4360
4912
  target.anchorA = new Uint32Array(aA);
@@ -4438,7 +4990,10 @@ function extractPolygon(p, opts) {
4438
4990
  break;
4439
4991
  }
4440
4992
  case PATH_Z2: {
4441
- if (current) current.closed = true;
4993
+ if (current) {
4994
+ current.closed = true;
4995
+ dropDuplicateClosingPoint();
4996
+ }
4442
4997
  break;
4443
4998
  }
4444
4999
  default:
@@ -4919,8 +5474,20 @@ function sameFill(a, b) {
4919
5474
  }
4920
5475
  return false;
4921
5476
  }
4922
- function groupKey(family, weight, style, synthetic, fill, source, page) {
4923
- return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${source}|${page}`;
5477
+ function strokeKey(s) {
5478
+ if (!s) return "-";
5479
+ return [
5480
+ fillKey(s.paint),
5481
+ s.width ?? 1,
5482
+ s.join ?? "miter",
5483
+ s.cap ?? "butt",
5484
+ s.miterLimit ?? "",
5485
+ s.align ?? "center",
5486
+ (s.dash ?? []).join(",")
5487
+ ].join(":");
5488
+ }
5489
+ function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
5490
+ return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
4924
5491
  }
4925
5492
  function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
4926
5493
  const atlasFamily = resolved.resolved.family;
@@ -4933,6 +5500,7 @@ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
4933
5500
  resolvedStyle,
4934
5501
  resolved.synthetic,
4935
5502
  run.fill,
5503
+ source === "outline" ? run.stroke : void 0,
4936
5504
  source,
4937
5505
  page
4938
5506
  );
@@ -4946,6 +5514,9 @@ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
4946
5514
  source,
4947
5515
  page,
4948
5516
  fill: run.fill,
5517
+ // Only the outline tier can paint it, and carrying it on an SDF group
5518
+ // would be a promise the renderer cannot keep.
5519
+ ...source === "outline" && run.stroke !== void 0 ? { stroke: run.stroke } : {},
4949
5520
  quads: [],
4950
5521
  glyphs: []
4951
5522
  };
@@ -5542,6 +6113,34 @@ function outlineMesh(key, d) {
5542
6113
  cache3.set(key, mesh);
5543
6114
  return mesh;
5544
6115
  }
6116
+ var OUTLINE_STROKE_MESH_CACHE_LIMIT = 2048;
6117
+ var WIDTH_QUANTUM = 1 / 1024;
6118
+ var cache4 = /* @__PURE__ */ new Map();
6119
+ function quantizeEmWidth(emWidth) {
6120
+ return Math.round(emWidth / WIDTH_QUANTUM) * WIDTH_QUANTUM;
6121
+ }
6122
+ function outlineStrokeMesh(glyphKey, d, emWidth, stroke) {
6123
+ if (!(emWidth > 0)) return null;
6124
+ const key = [
6125
+ glyphKey,
6126
+ emWidth,
6127
+ stroke.join ?? "miter",
6128
+ stroke.cap ?? "butt",
6129
+ stroke.miterLimit ?? "",
6130
+ stroke.align ?? "center",
6131
+ (stroke.dash ?? []).join(",")
6132
+ ].join("|");
6133
+ const cached2 = cache4.get(key);
6134
+ if (cached2 !== void 0) return cached2.indices.length === 0 ? null : cached2;
6135
+ if (cache4.size >= OUTLINE_STROKE_MESH_CACHE_LIMIT) cache4 = /* @__PURE__ */ new Map();
6136
+ const mesh = tessellateStroke(
6137
+ pathFromD(d),
6138
+ { ...stroke, width: emWidth },
6139
+ { flattenTolerance: OUTLINE_FLATTEN_TOLERANCE }
6140
+ );
6141
+ cache4.set(key, mesh);
6142
+ return mesh.indices.length === 0 ? null : mesh;
6143
+ }
5545
6144
  function setColorMatrixUniforms(ctx, prog) {
5546
6145
  const gl = ctx.gl;
5547
6146
  const cm = ctx.state.colorMatrix;
@@ -5832,28 +6431,42 @@ function drawPathFillPattern(ctx, fill, handle) {
5832
6431
  if (isDev3) console.warn(`weasel: pattern TextureHandle "${tex.id}" not registered`);
5833
6432
  return;
5834
6433
  }
5835
- ctx.textureCache.upload(tex.id, entry.source);
6434
+ ctx.textureCache.upload(tex.id, entry.source, "repeat");
5836
6435
  const gl = ctx.gl;
5837
- gl.useProgram(ctx.imageFill.handle);
6436
+ gl.useProgram(ctx.patternFill.handle);
5838
6437
  gl.bindVertexArray(handle.vao);
5839
- setProjAndModel(ctx, ctx.imageFill);
5840
- 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);
5841
6445
  ctx.textureCache.bind(tex.id, 0);
5842
- gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
5843
- gl.uniform1f(ctx.imageFill.uniform("u_opacity"), fill.opacity ?? 1);
5844
- 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);
5845
6449
  applyClipTest(ctx);
5846
6450
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
5847
6451
  gl.bindVertexArray(null);
5848
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
+ }
5849
6463
  function drawPathFillGradient(ctx, fill, handle) {
5850
6464
  const gl = ctx.gl;
5851
6465
  const key = ctx.gradRampCache.upload(fill.stops);
5852
6466
  gl.useProgram(ctx.gradFill.handle);
5853
6467
  gl.bindVertexArray(handle.vao);
5854
6468
  setProjAndModel(ctx, ctx.gradFill);
5855
- const identity3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
5856
- gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, identity3);
6469
+ gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
5857
6470
  ctx.gradRampCache.bind(key, 0);
5858
6471
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
5859
6472
  gl.uniform1f(ctx.gradFill.uniform("u_alpha"), ctx.state.alpha);
@@ -6115,16 +6728,30 @@ function drawText(ctx, cmd) {
6115
6728
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
6116
6729
  function drawTextOutlineGroup(ctx, group, dy) {
6117
6730
  const mesh = outlineGroupMesh(group, dy);
6118
- if (!mesh) return;
6119
- drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
6731
+ if (mesh) drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
6732
+ if (!group.stroke) return;
6733
+ const strokeMesh = outlineGroupStrokeMesh(group, dy);
6734
+ if (strokeMesh) {
6735
+ drawPathFillByKind(ctx, group.stroke.paint, ctx.meshCache.uploadTransient(strokeMesh));
6736
+ }
6120
6737
  }
6121
6738
  function outlineGroupMesh(group, dy) {
6739
+ return mergeGlyphMeshes(group, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
6740
+ }
6741
+ function outlineGroupStrokeMesh(group, dy) {
6742
+ const stroke = group.stroke;
6743
+ if (!stroke) return null;
6744
+ const width = stroke.width ?? 1;
6745
+ if (!(width > 0)) return null;
6746
+ return mergeGlyphMeshes(group, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
6747
+ }
6748
+ function mergeGlyphMeshes(group, dy, meshFor) {
6122
6749
  const parts = [];
6123
6750
  let vertexFloats = 0;
6124
6751
  let indexCount = 0;
6125
6752
  for (const glyph of group.glyphs) {
6126
- const mesh = outlineMesh(glyph.key, glyph.d);
6127
- if (mesh.indices.length === 0) continue;
6753
+ const mesh = meshFor(glyph);
6754
+ if (!mesh || mesh.indices.length === 0) continue;
6128
6755
  parts.push({ mesh, glyph });
6129
6756
  vertexFloats += mesh.vertices.length;
6130
6757
  indexCount += mesh.indices.length;
@@ -6359,6 +6986,11 @@ function drawImage(ctx, cmd) {
6359
6986
  setProjAndModel(ctx, ctx.imageFill);
6360
6987
  setColorMatrixUniforms(ctx, ctx.imageFill);
6361
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
+ );
6362
6994
  gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
6363
6995
  gl.uniform1f(ctx.imageFill.uniform("u_opacity"), cmd.opacity ?? 1);
6364
6996
  gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
@@ -6445,6 +7077,7 @@ var WeaselRenderer = class {
6445
7077
  textSdfR8;
6446
7078
  imageFill;
6447
7079
  gradFill;
7080
+ patternFill;
6448
7081
  meshCache;
6449
7082
  textureCache;
6450
7083
  imageCache;
@@ -6520,6 +7153,9 @@ var WeaselRenderer = class {
6520
7153
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
6521
7154
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
6522
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);
6523
7159
  const aPos = this.pathFill.attribute("a_position");
6524
7160
  if (aPos === void 0) throw new Error("WeaselRenderer: a_position not found in path-fill shader");
6525
7161
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -6628,6 +7264,9 @@ var WeaselRenderer = class {
6628
7264
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
6629
7265
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
6630
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);
6631
7270
  const aPos = this.pathFill.attribute("a_position");
6632
7271
  if (aPos === void 0) throw new Error("a_position missing after restore");
6633
7272
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -6699,7 +7338,15 @@ var WeaselRenderer = class {
6699
7338
  if (this.rectIbo) gl.deleteBuffer(this.rectIbo);
6700
7339
  if (this.rectVao) gl.deleteVertexArray(this.rectVao);
6701
7340
  }
6702
- 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) {
6703
7350
  if (this.contextLost || this.disposed) return;
6704
7351
  const gl = this.gl;
6705
7352
  this.meshCache.drainPendingDeletes();
@@ -6714,6 +7361,7 @@ var WeaselRenderer = class {
6714
7361
  textSdfR8: this.textSdfR8,
6715
7362
  imageFill: this.imageFill,
6716
7363
  gradFill: this.gradFill,
7364
+ patternFill: this.patternFill,
6717
7365
  meshCache: this.meshCache,
6718
7366
  textureCache: this.textureCache,
6719
7367
  imageCache: this.imageCache,
@@ -6728,7 +7376,8 @@ var WeaselRenderer = class {
6728
7376
  heightCss: this.heightCss,
6729
7377
  clipDepth: 0,
6730
7378
  flattenTolerance: this.flattenTolerance,
6731
- textOutlineMinScreenSize: this.textOutlineMinScreenSize
7379
+ textOutlineMinScreenSize: this.textOutlineMinScreenSize,
7380
+ viewMatrix
6732
7381
  };
6733
7382
  for (const cmd of commands) dispatch(ctx, cmd);
6734
7383
  this.meshCache.freeTransient();
@@ -6776,6 +7425,10 @@ var WeaselRenderer = class {
6776
7425
  return this.gradFill;
6777
7426
  }
6778
7427
  /** @internal */
7428
+ _patternFill() {
7429
+ return this.patternFill;
7430
+ }
7431
+ /** @internal */
6779
7432
  _meshCache() {
6780
7433
  return this.meshCache;
6781
7434
  }
@@ -6836,6 +7489,9 @@ function alignedStrokeRect(rect, align, width = 1) {
6836
7489
  height: rect.height + 2 * d
6837
7490
  };
6838
7491
  }
7492
+ function meanScale(s) {
7493
+ return Math.sqrt(s.x * s.y);
7494
+ }
6839
7495
  var DEFAULT_LINE = {
6840
7496
  paint: { fill: "solid", color: "rgba(255, 255, 255, 0.08)" },
6841
7497
  width: 1
@@ -6964,289 +7620,148 @@ function clientToCanvas(canvas, clientX, clientY) {
6964
7620
  function clientToCanvasRect(rect, clientX, clientY) {
6965
7621
  return [clientX - rect.left, clientY - rect.top];
6966
7622
  }
6967
- function zoomAt(view, anchor, factor, opts) {
6968
- const fx = typeof factor === "number" ? factor : factor.x;
6969
- const fy = typeof factor === "number" ? factor : factor.y;
6970
- const minX = typeof opts?.min === "number" ? opts.min : opts?.min?.x ?? 0.1;
6971
- const minY = typeof opts?.min === "number" ? opts.min : opts?.min?.y ?? 0.1;
6972
- const maxX = typeof opts?.max === "number" ? opts.max : opts?.max?.x ?? 8;
6973
- const maxY = typeof opts?.max === "number" ? opts.max : opts?.max?.y ?? 8;
6974
- const nextX = Math.min(maxX, Math.max(minX, view.scale.x * fx));
6975
- const nextY = Math.min(maxY, Math.max(minY, view.scale.y * fy));
6976
- const worldX = anchor.x / view.scale.x + view.x;
6977
- const worldY = anchor.y / view.scale.y + view.y;
7623
+ var COARSE_TARGET_SCALE = 1.75;
7624
+ function resolveDeviceProfile(detected, overrides) {
7625
+ const coarsePointer2 = overrides?.coarsePointer ?? detected.coarsePointer;
7626
+ const canHover2 = overrides?.canHover ?? detected.canHover;
7627
+ const dpr = overrides?.dpr ?? detected.dpr;
7628
+ const targetScale = overrides?.targetScale ?? (coarsePointer2 ? COARSE_TARGET_SCALE : 1);
7629
+ return { coarsePointer: coarsePointer2, canHover: canHover2, dpr, targetScale };
7630
+ }
7631
+ var DEFAULT_DEVICE_PROFILE = resolveDeviceProfile({
7632
+ coarsePointer: false,
7633
+ canHover: true,
7634
+ dpr: 1
7635
+ });
7636
+ var COARSE_QUERY = "(pointer: coarse)";
7637
+ var HOVER_QUERY = "(hover: hover)";
7638
+ function hasMatchMedia() {
7639
+ return typeof window !== "undefined" && typeof window.matchMedia === "function";
7640
+ }
7641
+ function detectOnce() {
7642
+ const dpr = typeof window !== "undefined" && window.devicePixelRatio || 1;
7643
+ if (!hasMatchMedia()) {
7644
+ return {
7645
+ coarsePointer: DEFAULT_DEVICE_PROFILE.coarsePointer,
7646
+ canHover: DEFAULT_DEVICE_PROFILE.canHover,
7647
+ dpr
7648
+ };
7649
+ }
6978
7650
  return {
6979
- scale: { x: nextX, y: nextY },
6980
- x: worldX - anchor.x / nextX,
6981
- y: worldY - anchor.y / nextY
7651
+ coarsePointer: window.matchMedia(COARSE_QUERY).matches,
7652
+ canHover: window.matchMedia(HOVER_QUERY).matches,
7653
+ dpr
6982
7654
  };
6983
7655
  }
6984
- function clampView(view, bounds, canvas) {
6985
- const visW = canvas.width / view.scale.x;
6986
- const visH = canvas.height / view.scale.y;
6987
- let x;
6988
- if (visW >= bounds.width) {
6989
- x = bounds.x + (bounds.width - visW) / 2;
6990
- } else {
6991
- const minX = bounds.x;
6992
- const maxX = bounds.x + bounds.width - visW;
6993
- x = view.x < minX ? minX : view.x > maxX ? maxX : view.x;
6994
- }
6995
- let y;
6996
- if (visH >= bounds.height) {
6997
- y = bounds.y + (bounds.height - visH) / 2;
6998
- } else {
6999
- const minY = bounds.y;
7000
- const maxY = bounds.y + bounds.height - visH;
7001
- y = view.y < minY ? minY : view.y > maxY ? maxY : view.y;
7002
- }
7003
- return x === view.x && y === view.y ? view : { x, y, scale: view.scale };
7004
- }
7005
- function isEditableTarget2(target) {
7006
- if (!target || !(target instanceof Element)) return false;
7007
- const tag = target.tagName;
7008
- if (tag === "INPUT" || tag === "TEXTAREA") return true;
7009
- if (target.isContentEditable) return true;
7010
- const ce = target.getAttribute("contenteditable");
7011
- return ce === "true" || ce === "";
7012
- }
7013
- var DepRegistryContext = createContext(null);
7014
- function DepRegistryProvider({ children }) {
7015
- const sourcesRef = useRef(/* @__PURE__ */ new Map());
7016
- const registry22 = useMemo(() => ({
7017
- register: (name, source) => {
7018
- sourcesRef.current.set(name, source);
7019
- return () => {
7020
- sourcesRef.current.delete(name);
7021
- };
7022
- },
7023
- get: (name) => sourcesRef.current.get(name)?.()
7024
- }), []);
7025
- return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry22, children });
7026
- }
7027
- function useDepRegistry() {
7028
- const r = useContext(DepRegistryContext);
7029
- if (r === null) {
7030
- throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
7031
- }
7032
- return r;
7033
- }
7034
- function useOptionalDepRegistry() {
7035
- return useContext(DepRegistryContext);
7036
- }
7037
- function useDepSource(name, source) {
7038
- const registry22 = useDepRegistry();
7039
- const sourceRef = useRef(source);
7040
- sourceRef.current = source;
7656
+ function useDetectedFacts() {
7657
+ const [facts, setFacts] = useState(detectOnce);
7041
7658
  useEffect(() => {
7042
- return registry22.register(name, () => sourceRef.current());
7043
- }, [name, registry22]);
7044
- }
7045
- var DEV = (() => {
7046
- try {
7047
- return Boolean(import.meta.env?.DEV);
7048
- } catch {
7049
- return false;
7050
- }
7051
- })();
7052
- function buildDepsFromRequires(action, depRegistry) {
7053
- const requires = action.requires ?? [];
7054
- const entries = requires.map((name) => [name, depRegistry.get(name)]);
7055
- const deps = Object.fromEntries(entries);
7056
- if (DEV) {
7057
- const declared = new Set(requires);
7058
- return new Proxy(deps, {
7059
- get(target, prop, receiver) {
7060
- if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
7061
- 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.`;
7062
- console.warn(message);
7063
- throw new Error(message);
7064
- }
7065
- return Reflect.get(target, prop, receiver);
7066
- }
7067
- });
7068
- }
7069
- return deps;
7070
- }
7071
- var ActionDisabledReason = {
7072
- SelectionRequired: "selection-required",
7073
- NotApplicable: "not-applicable",
7074
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
7075
- PredicateThrew: "predicate-threw"
7076
- };
7077
- var enabledSlowWarned = /* @__PURE__ */ new Set();
7078
- var enabledThrewWarned = /* @__PURE__ */ new Set();
7079
- var ENABLED_BUDGET_MS = 4;
7080
- function evaluateEnabled(action, deps) {
7081
- if (!action.enabled) return { enabled: true };
7082
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
7083
- const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
7084
- const start = isDev3 ? now() : 0;
7085
- try {
7086
- const result = action.enabled(deps);
7087
- if (isDev3) {
7088
- const elapsed = now() - start;
7089
- if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
7090
- enabledSlowWarned.add(action.id);
7091
- console.warn(
7092
- `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.`
7093
- );
7094
- }
7095
- }
7096
- if (result === true) return { enabled: true };
7097
- return { enabled: false, reason: result };
7098
- } catch (e) {
7099
- if (isDev3 && !enabledThrewWarned.has(action.id)) {
7100
- enabledThrewWarned.add(action.id);
7101
- console.warn(
7102
- `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
7103
- e
7659
+ if (!hasMatchMedia()) return;
7660
+ const coarseMq = window.matchMedia(COARSE_QUERY);
7661
+ const hoverMq = window.matchMedia(HOVER_QUERY);
7662
+ const onCoarse = (e) => setFacts((f) => ({ ...f, coarsePointer: e.matches }));
7663
+ const onHover = (e) => setFacts((f) => ({ ...f, canHover: e.matches }));
7664
+ coarseMq.addEventListener("change", onCoarse);
7665
+ hoverMq.addEventListener("change", onHover);
7666
+ let disposed = false;
7667
+ let densityMq = null;
7668
+ const onDensity = () => {
7669
+ if (disposed) return;
7670
+ const next = window.devicePixelRatio || 1;
7671
+ setFacts((f) => f.dpr === next ? f : { ...f, dpr: next });
7672
+ arm();
7673
+ };
7674
+ function arm() {
7675
+ if (disposed) return;
7676
+ densityMq?.removeEventListener("change", onDensity);
7677
+ densityMq = window.matchMedia(
7678
+ `(resolution: ${window.devicePixelRatio || 1}dppx)`
7104
7679
  );
7680
+ densityMq.addEventListener("change", onDensity);
7105
7681
  }
7106
- return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
7107
- }
7108
- }
7109
- function validateActionId(id) {
7110
- if (id.length === 0) {
7111
- throw new Error(`weasel: action id may not be empty`);
7112
- }
7113
- if (RESERVED_ID_PREFIXES.has(id[0])) {
7114
- throw new Error(
7115
- `weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
7116
- );
7117
- }
7118
- if (RESERVED_ID_NAMES.has(id)) {
7119
- throw new Error(
7120
- `weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
7121
- );
7122
- }
7123
- }
7124
- function validateActionDefaultBinding(action) {
7125
- const gs = action.defaultBinding;
7126
- if (!gs) return;
7127
- const entries = Array.isArray(gs) ? gs : [gs];
7128
- for (const entry of entries) {
7129
- const spec = "kind" in entry ? entry : entry.spec;
7130
- const phase = spec.phase;
7131
- if (phase === void 0) continue;
7132
- const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
7133
- for (const atom of atoms) {
7134
- if (atom.channel === "&") {
7135
- throw new Error(
7136
- `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.`
7137
- );
7138
- }
7139
- }
7140
- }
7141
- }
7142
- var ActionsContext = createContext(null);
7143
- function ActionsProvider({ children }) {
7144
- const actionsRef = useRef(/* @__PURE__ */ new Map());
7145
- const versionRef = useRef(0);
7146
- const cachedRef = useRef([]);
7147
- const cachedVerRef = useRef(-1);
7148
- const listenersRef = useRef(/* @__PURE__ */ new Set());
7149
- const depReg = useOptionalDepRegistry();
7150
- const depRegRef = useRef(depReg);
7151
- depRegRef.current = depReg;
7152
- const wiredDepRegRef = useRef(null);
7153
- const dispatcherRef = useRef(null);
7154
- const registry22 = useMemo(() => {
7155
- const snapshot = () => {
7156
- const v = versionRef.current;
7157
- if (cachedVerRef.current === v) return cachedRef.current;
7158
- const out = Object.freeze(Array.from(actionsRef.current.values()));
7159
- cachedRef.current = out;
7160
- cachedVerRef.current = v;
7161
- return out;
7162
- };
7163
- const notify2 = () => {
7164
- for (const l of listenersRef.current) {
7165
- try {
7166
- l();
7167
- } catch (err) {
7168
- console.error("weasel ActionsRegistry: subscriber threw", err);
7169
- }
7170
- }
7171
- };
7172
- return {
7173
- register: (action) => {
7174
- validateActionId(action.id);
7175
- validateActionDefaultBinding(action);
7176
- actionsRef.current.set(action.id, action);
7177
- versionRef.current++;
7178
- notify2();
7179
- return () => {
7180
- const cur = actionsRef.current.get(action.id);
7181
- if (cur === action) {
7182
- actionsRef.current.delete(action.id);
7183
- versionRef.current++;
7184
- notify2();
7185
- }
7186
- };
7187
- },
7188
- unregister: (id) => {
7189
- if (actionsRef.current.delete(id)) {
7190
- versionRef.current++;
7191
- notify2();
7192
- }
7193
- },
7194
- list: () => snapshot(),
7195
- trigger: (id, params) => {
7196
- const a = actionsRef.current.get(id);
7197
- if (!a) return false;
7198
- try {
7199
- if (a.invoker && a.invoker.timing === "immediate") {
7200
- const r = wiredDepRegRef.current ?? depRegRef.current;
7201
- const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
7202
- selection: r.get("selection"),
7203
- scene: r.get("scene"),
7204
- history: r.get("history"),
7205
- view: r.get("view"),
7206
- pointer: r.get("pointer"),
7207
- activeTool: r.get("activeTool"),
7208
- booleansAdapter: r.get("booleansAdapter")
7209
- };
7210
- a.invoker.run(deps, params);
7211
- }
7212
- } catch (err) {
7213
- console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
7214
- }
7215
- return true;
7216
- },
7217
- subscribe: (listener) => {
7218
- listenersRef.current.add(listener);
7219
- return () => {
7220
- listenersRef.current.delete(listener);
7221
- };
7222
- },
7223
- setDispatcher: (d) => {
7224
- dispatcherRef.current = d;
7225
- },
7226
- setDepRegistry: (r) => {
7227
- wiredDepRegRef.current = r;
7228
- },
7229
- begin: (id, params) => {
7230
- const disp = dispatcherRef.current;
7231
- if (!disp) return null;
7232
- const r = wiredDepRegRef.current ?? depRegRef.current;
7233
- const deps = r ? {
7234
- selection: r.get("selection"),
7235
- scene: r.get("scene"),
7236
- history: r.get("history"),
7237
- view: r.get("view"),
7238
- pointer: r.get("pointer"),
7239
- activeTool: r.get("activeTool"),
7240
- booleansAdapter: r.get("booleansAdapter")
7241
- } : {};
7242
- return disp.beginUiOngoing(id, deps, params);
7243
- }
7682
+ arm();
7683
+ setFacts({
7684
+ coarsePointer: coarseMq.matches,
7685
+ canHover: hoverMq.matches,
7686
+ dpr: window.devicePixelRatio || 1
7687
+ });
7688
+ return () => {
7689
+ disposed = true;
7690
+ coarseMq.removeEventListener("change", onCoarse);
7691
+ hoverMq.removeEventListener("change", onHover);
7692
+ densityMq?.removeEventListener("change", onDensity);
7244
7693
  };
7245
7694
  }, []);
7246
- return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry22, children });
7695
+ return facts;
7696
+ }
7697
+ var DeviceProfileContext = createContext(null);
7698
+ function useDeviceProfile(overrides) {
7699
+ const provided = useContext(DeviceProfileContext);
7700
+ const detected = useDetectedFacts();
7701
+ const base = provided ?? detected;
7702
+ const { coarsePointer: coarsePointer2, canHover: canHover2, dpr, targetScale } = overrides ?? {};
7703
+ return useMemo(
7704
+ () => resolveDeviceProfile(base, {
7705
+ ...coarsePointer2 !== void 0 ? { coarsePointer: coarsePointer2 } : {},
7706
+ ...canHover2 !== void 0 ? { canHover: canHover2 } : {},
7707
+ ...dpr !== void 0 ? { dpr } : {},
7708
+ ...targetScale !== void 0 ? { targetScale } : {}
7709
+ }),
7710
+ [base.coarsePointer, base.canHover, base.dpr, coarsePointer2, canHover2, dpr, targetScale]
7711
+ );
7712
+ }
7713
+ function DeviceProfileProvider({ value, children }) {
7714
+ const profile = useDeviceProfile(value);
7715
+ return createElement(DeviceProfileContext.Provider, { value: profile }, children);
7716
+ }
7717
+ var HANDLE_BASE_PX = 8;
7718
+ var ANCHOR_HIT_BASE_PX = 8;
7719
+ var ROTATION_HANDLE_BASE_PX = 24;
7720
+ function zoomAt(view, anchor, factor, opts) {
7721
+ const fx = typeof factor === "number" ? factor : factor.x;
7722
+ const fy = typeof factor === "number" ? factor : factor.y;
7723
+ const minX = typeof opts?.min === "number" ? opts.min : opts?.min?.x ?? 0.1;
7724
+ const minY = typeof opts?.min === "number" ? opts.min : opts?.min?.y ?? 0.1;
7725
+ const maxX = typeof opts?.max === "number" ? opts.max : opts?.max?.x ?? 8;
7726
+ const maxY = typeof opts?.max === "number" ? opts.max : opts?.max?.y ?? 8;
7727
+ const nextX = Math.min(maxX, Math.max(minX, view.scale.x * fx));
7728
+ const nextY = Math.min(maxY, Math.max(minY, view.scale.y * fy));
7729
+ const worldX = anchor.x / view.scale.x + view.x;
7730
+ const worldY = anchor.y / view.scale.y + view.y;
7731
+ return {
7732
+ scale: { x: nextX, y: nextY },
7733
+ x: worldX - anchor.x / nextX,
7734
+ y: worldY - anchor.y / nextY
7735
+ };
7247
7736
  }
7248
- function useActionsRegistry() {
7249
- return useContext(ActionsContext);
7737
+ function clampView(view, bounds, canvas) {
7738
+ const visW = canvas.width / view.scale.x;
7739
+ const visH = canvas.height / view.scale.y;
7740
+ let x;
7741
+ if (visW >= bounds.width) {
7742
+ x = bounds.x + (bounds.width - visW) / 2;
7743
+ } else {
7744
+ const minX = bounds.x;
7745
+ const maxX = bounds.x + bounds.width - visW;
7746
+ x = view.x < minX ? minX : view.x > maxX ? maxX : view.x;
7747
+ }
7748
+ let y;
7749
+ if (visH >= bounds.height) {
7750
+ y = bounds.y + (bounds.height - visH) / 2;
7751
+ } else {
7752
+ const minY = bounds.y;
7753
+ const maxY = bounds.y + bounds.height - visH;
7754
+ y = view.y < minY ? minY : view.y > maxY ? maxY : view.y;
7755
+ }
7756
+ return x === view.x && y === view.y ? view : { x, y, scale: view.scale };
7757
+ }
7758
+ function isEditableTarget2(target) {
7759
+ if (!target || !(target instanceof Element)) return false;
7760
+ const tag = target.tagName;
7761
+ if (tag === "INPUT" || tag === "TEXTAREA") return true;
7762
+ if (target.isContentEditable) return true;
7763
+ const ce = target.getAttribute("contenteditable");
7764
+ return ce === "true" || ce === "";
7250
7765
  }
7251
7766
  function resolveParams(params) {
7252
7767
  if (params === void 0) return void 0;
@@ -7782,6 +8297,60 @@ var moveAction = {
7782
8297
  */
7783
8298
  enabled: () => true
7784
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
+ };
7785
8354
  function aabbCenter(bounds) {
7786
8355
  return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
7787
8356
  }
@@ -8074,6 +8643,13 @@ var resizeAction = {
8074
8643
  const computePose = (id) => {
8075
8644
  const sp = scratch.startPoses.get(id);
8076
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
+ }
8077
8653
  return scratch.geometry.remapBounds(sp, scratch.originBounds, proposedBounds);
8078
8654
  };
8079
8655
  scratch.previews.clear();
@@ -8694,6 +9270,10 @@ var clearSelectionAction = {
8694
9270
  return true;
8695
9271
  }
8696
9272
  };
9273
+ var counter2 = 0;
9274
+ function freshNodeId() {
9275
+ return asNodeId(`n${(counter2++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
9276
+ }
8697
9277
  function translatePose(pose, dx, dy) {
8698
9278
  const p = pose;
8699
9279
  return {
@@ -8702,10 +9282,6 @@ function translatePose(pose, dx, dy) {
8702
9282
  y: (p["y"] ?? 0) + dy
8703
9283
  };
8704
9284
  }
8705
- var cloneIdCounter = 0;
8706
- function freshCloneId() {
8707
- return asNodeId(`n${(cloneIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
8708
- }
8709
9285
  var cloneAction = {
8710
9286
  id: "clone",
8711
9287
  label: "Clone",
@@ -8772,7 +9348,7 @@ var cloneAction = {
8772
9348
  if (origin === void 0 || !originNode) continue;
8773
9349
  const newPose = translatePose(origin, dx, dy);
8774
9350
  const node = {
8775
- id: freshCloneId(),
9351
+ id: freshNodeId(),
8776
9352
  kind: originNode.kind,
8777
9353
  layer: originNode.layer,
8778
9354
  pose: newPose,
@@ -8913,7 +9489,13 @@ function makeViewportZoomAction(opts = {}) {
8913
9489
  };
8914
9490
  }
8915
9491
  makeViewportZoomAction();
8916
- 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");
8917
9499
  function kindOf(target) {
8918
9500
  if (typeof target !== "object" || target === null) return void 0;
8919
9501
  const k = target.kind;
@@ -8952,17 +9534,17 @@ function splitCubicAtT(p0, p1, p2, p3, t) {
8952
9534
  function fitCubicThroughDeletion(prev, next) {
8953
9535
  const dx = next.x - prev.x;
8954
9536
  const dy = next.y - prev.y;
8955
- const dist4 = Math.sqrt(dx * dx + dy * dy);
9537
+ const dist3 = Math.sqrt(dx * dx + dy * dy);
8956
9538
  const c1DirX = prev.outHandle ? prev.outHandle.x - prev.x : dx;
8957
9539
  const c1DirY = prev.outHandle ? prev.outHandle.y - prev.y : dy;
8958
- const c1 = controlOnSide(prev, c1DirX, c1DirY, dist4, 1 / 3);
9540
+ const c1 = controlOnSide(prev, c1DirX, c1DirY, dist3, 1 / 3);
8959
9541
  const c2DirX = next.inHandle ? next.inHandle.x - next.x : -dx;
8960
9542
  const c2DirY = next.inHandle ? next.inHandle.y - next.y : -dy;
8961
- const c2 = controlOnSide(next, c2DirX, c2DirY, dist4, 1 / 3);
9543
+ const c2 = controlOnSide(next, c2DirX, c2DirY, dist3, 1 / 3);
8962
9544
  return { c1, c2 };
8963
9545
  }
8964
- function controlOnSide(anchor, dirX, dirY, dist4, fraction) {
8965
- const targetDist = dist4 * fraction;
9546
+ function controlOnSide(anchor, dirX, dirY, dist3, fraction) {
9547
+ const targetDist = dist3 * fraction;
8966
9548
  const len = Math.hypot(dirX, dirY) || 1;
8967
9549
  const ux = dirX / len, uy = dirY / len;
8968
9550
  return { x: anchor.x + ux * targetDist, y: anchor.y + uy * targetDist };
@@ -9219,60 +9801,6 @@ function anchorsInRect(set, rect) {
9219
9801
  }
9220
9802
  return hits;
9221
9803
  }
9222
- function poseRotationOf(pose) {
9223
- const p = pose;
9224
- if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
9225
- return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
9226
- }
9227
- return null;
9228
- }
9229
- function rotatePathAround(path, cx, cy, rotation) {
9230
- if (path.kind === "rect") {
9231
- return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
9232
- }
9233
- return rotatePolygon(path, cx, cy, rotation);
9234
- }
9235
- function rotatePolygon(path, cx, cy, rotation) {
9236
- const cos = Math.cos(rotation);
9237
- const sin = Math.sin(rotation);
9238
- const { commands, coords } = path;
9239
- const next = new Float32Array(coords.length);
9240
- let ci = 0;
9241
- for (let i = 0; i < commands.length; i++) {
9242
- const len = COORD_COUNT3[commands[i]];
9243
- for (let k = 0; k < len; k += 2) {
9244
- const dx = coords[ci + k] - cx;
9245
- const dy = coords[ci + k + 1] - cy;
9246
- next[ci + k] = cx + dx * cos - dy * sin;
9247
- next[ci + k + 1] = cy + dx * sin + dy * cos;
9248
- }
9249
- ci += len;
9250
- }
9251
- return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
9252
- }
9253
- function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
9254
- const cos = Math.cos(rotation);
9255
- const sin = Math.sin(rotation);
9256
- const rotate2 = (px, py) => {
9257
- const dx = px - cx;
9258
- const dy = py - cy;
9259
- return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
9260
- };
9261
- const [x0, y0] = rotate2(x, y);
9262
- const [x1, y1] = rotate2(x + w, y);
9263
- const [x2, y2] = rotate2(x + w, y + h);
9264
- const [x3, y3] = rotate2(x, y + h);
9265
- const commands = new Uint8Array([PATH_M2, PATH_L2, PATH_L2, PATH_L2, PATH_Z2]);
9266
- const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
9267
- return { kind: "polygon", commands, coords, fillRule: "nonzero" };
9268
- }
9269
- var COORD_COUNT3 = {
9270
- [PATH_M2]: 2,
9271
- [PATH_L2]: 2,
9272
- [PATH_C2]: 6,
9273
- [PATH_Q2]: 4,
9274
- [PATH_Z2]: 0
9275
- };
9276
9804
  function pathInPoseFrame(path, pose) {
9277
9805
  if (path.kind === "rect") {
9278
9806
  if (path.x === pose.x && path.y === pose.y && path.width === pose.width && path.height === pose.height) return path;
@@ -9555,6 +10083,119 @@ var pinchZoomAction = {
9555
10083
  },
9556
10084
  enabled: () => true
9557
10085
  };
10086
+ function hostIndex(scene, id) {
10087
+ const node = scene.get(id);
10088
+ const siblings = node?.parent == null ? scene.roots : scene.childrenOf(node.parent);
10089
+ return siblings.indexOf(id);
10090
+ }
10091
+ function hasSelectedAncestor(scene, id, set) {
10092
+ let parent = scene.get(id)?.parent ?? null;
10093
+ while (parent != null) {
10094
+ if (set.has(parent)) return true;
10095
+ parent = scene.get(parent)?.parent ?? null;
10096
+ }
10097
+ return false;
10098
+ }
10099
+ function buildDeleteOps(scene, ids, label) {
10100
+ const set = new Set(ids);
10101
+ const ops = [];
10102
+ for (const id of ids) {
10103
+ const node = scene.get(id);
10104
+ if (!node) continue;
10105
+ if (hasSelectedAncestor(scene, id, set)) continue;
10106
+ ops.push(createDeleteOp({
10107
+ node,
10108
+ index: hostIndex(scene, id),
10109
+ label
10110
+ }));
10111
+ }
10112
+ return ops;
10113
+ }
10114
+ var deleteAction = {
10115
+ id: "delete",
10116
+ label: "Delete",
10117
+ // Suppressed while any tool is mid-gesture — accidentally hitting
10118
+ // Delete during a drag shouldn't wipe the selection out from under
10119
+ // the in-flight handle.
10120
+ defaultBinding: {
10121
+ kind: "key",
10122
+ key: ["Delete", "Backspace"],
10123
+ phase: [{ channel: "*", phase: "initial" }]
10124
+ },
10125
+ eligible: { capability: "edits-page" },
10126
+ requires: ["scene", "selection", "applyOps"],
10127
+ invoker: {
10128
+ timing: "immediate",
10129
+ run: (deps) => {
10130
+ const selection = deps.selection;
10131
+ const scene = deps.scene;
10132
+ const applyOps = deps.applyOps;
10133
+ if (!selection || !scene) return;
10134
+ const ids = selection.get();
10135
+ if (ids.length === 0) return;
10136
+ const ops = buildDeleteOps(scene, ids, "Delete");
10137
+ if (ops.length > 0) {
10138
+ if (applyOps) applyOps(ops, "Delete");
10139
+ else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene));
10140
+ }
10141
+ selection.set([]);
10142
+ }
10143
+ },
10144
+ enabled: () => true
10145
+ };
10146
+ function requiresSelection(deps) {
10147
+ const selection = deps?.selection;
10148
+ if (!selection || selection.get().length === 0) {
10149
+ return ActionDisabledReason.SelectionRequired;
10150
+ }
10151
+ return true;
10152
+ }
10153
+ var clipboardCopyAction = {
10154
+ id: "clipboard.copy",
10155
+ label: "Copy",
10156
+ defaultBinding: { kind: "key", key: "c", mods: { mod: true } },
10157
+ // Same gate as `duplicate`: a mode that can neither edit the page nor own a
10158
+ // selection has nothing to copy. Notably excludes `text-edit`, where Cmd+C
10159
+ // must stay the browser's own copy of the caret range.
10160
+ eligible: { capability: ["edits-page", "creates-selection"] },
10161
+ // `selection` is read by the `enabled` gate, not the invoker — the
10162
+ // dispatcher builds deps for both, and an undeclared read is undefined.
10163
+ requires: ["clipboard", "selection"],
10164
+ invoker: {
10165
+ timing: "immediate",
10166
+ run: (deps) => {
10167
+ deps.clipboard?.copy();
10168
+ }
10169
+ },
10170
+ enabled: requiresSelection
10171
+ };
10172
+ var clipboardCutAction = {
10173
+ id: "clipboard.cut",
10174
+ label: "Cut",
10175
+ defaultBinding: { kind: "key", key: "x", mods: { mod: true } },
10176
+ eligible: { capability: "edits-page" },
10177
+ requires: ["clipboard", "scene", "selection", "applyOps"],
10178
+ invoker: {
10179
+ timing: "immediate",
10180
+ run: (deps) => {
10181
+ const clipboard = deps.clipboard;
10182
+ const selection = deps.selection;
10183
+ const scene = deps.scene;
10184
+ const applyOps = deps.applyOps;
10185
+ if (!clipboard || !selection || !scene) return;
10186
+ const ids = selection.get();
10187
+ if (ids.length === 0) return;
10188
+ clipboard.copy();
10189
+ const ops = buildDeleteOps(scene, ids, "Cut");
10190
+ if (ops.length > 0) {
10191
+ if (applyOps) applyOps(ops, "Cut");
10192
+ else scene.applyBatch(ops, "Cut", defaultCommitAdapter(scene));
10193
+ }
10194
+ selection.set([]);
10195
+ }
10196
+ },
10197
+ enabled: requiresSelection
10198
+ };
9558
10199
  var enterTextEditAction = {
9559
10200
  id: "enterTextEdit",
9560
10201
  label: "Enter text edit",
@@ -9677,26 +10318,47 @@ var selectAllAction = {
9677
10318
  if (all.length === 0) return;
9678
10319
  deps.selection?.set(all);
9679
10320
  }
9680
- }
9681
- };
9682
- function hostIndex(scene, id) {
9683
- const node = scene.get(id);
9684
- const siblings = node?.parent == null ? scene.roots : scene.childrenOf(node.parent);
9685
- return siblings.indexOf(id);
9686
- }
9687
- var deleteAction = {
9688
- id: "delete",
9689
- label: "Delete",
9690
- // Suppressed while any tool is mid-gesture — accidentally hitting
9691
- // Delete during a drag shouldn't wipe the selection out from under
9692
- // the in-flight handle.
9693
- defaultBinding: {
9694
- kind: "key",
9695
- key: ["Delete", "Backspace"],
9696
- phase: [{ channel: "*", phase: "initial" }]
9697
- },
9698
- eligible: { capability: "edits-page" },
9699
- requires: ["scene", "selection", "applyOps"],
10321
+ }
10322
+ };
10323
+ var DUPLICATE_OFFSET = 12;
10324
+ function translatePose2(pose, dx, dy) {
10325
+ const p = pose;
10326
+ return { ...p, x: (p["x"] ?? 0) + dx, y: (p["y"] ?? 0) + dy };
10327
+ }
10328
+ function hasSelectedAncestor2(scene, id, set) {
10329
+ let parent = scene.get(id)?.parent ?? null;
10330
+ while (parent != null) {
10331
+ if (set.has(parent)) return true;
10332
+ parent = scene.get(parent)?.parent ?? null;
10333
+ }
10334
+ return false;
10335
+ }
10336
+ function copySubtree(scene, id, parent, offset, offsetDescendants, ops) {
10337
+ const node = scene.get(id);
10338
+ if (!node) return null;
10339
+ const newId = freshNodeId();
10340
+ const copy = {
10341
+ ...node,
10342
+ id: newId,
10343
+ pose: translatePose2(node.pose, offset.dx, offset.dy),
10344
+ parent
10345
+ };
10346
+ ops.push(createInsertOp({ node: copy, label: "Duplicate" }));
10347
+ const childOffset = offsetDescendants ? offset : { dx: 0, dy: 0 };
10348
+ for (const child of scene.childrenOf(id)) {
10349
+ copySubtree(scene, child, newId, childOffset, offsetDescendants, ops);
10350
+ }
10351
+ return newId;
10352
+ }
10353
+ var duplicateAction = {
10354
+ id: "duplicate",
10355
+ label: "Duplicate",
10356
+ defaultBinding: { kind: "key", key: "d", mods: { mod: true } },
10357
+ eligible: { capability: ["edits-page", "creates-selection"] },
10358
+ // `selection` is read by the `enabled` gate as well as the invoker. An
10359
+ // undeclared read throws inside the dev-build deps Proxy before the gate can
10360
+ // answer, which is how this action spent a while doing nothing at all.
10361
+ requires: ["scene", "selection", "applyOps", "poseComposition"],
9700
10362
  invoker: {
9701
10363
  timing: "immediate",
9702
10364
  run: (deps) => {
@@ -9706,40 +10368,22 @@ var deleteAction = {
9706
10368
  if (!selection || !scene) return;
9707
10369
  const ids = selection.get();
9708
10370
  if (ids.length === 0) return;
10371
+ const set = new Set(ids);
10372
+ const offset = { dx: DUPLICATE_OFFSET, dy: DUPLICATE_OFFSET };
9709
10373
  const ops = [];
10374
+ const newIds = [];
9710
10375
  for (const id of ids) {
9711
- const node = scene.get(id);
10376
+ const nid = asNodeId(id);
10377
+ const node = scene.get(nid);
9712
10378
  if (!node) continue;
9713
- ops.push(createDeleteOp({
9714
- node,
9715
- index: hostIndex(scene, id),
9716
- label: "Delete"
9717
- }));
9718
- }
9719
- if (ops.length > 0) {
9720
- if (applyOps) applyOps(ops, "Delete");
9721
- else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene));
10379
+ if (hasSelectedAncestor2(scene, nid, set)) continue;
10380
+ const newId = copySubtree(scene, nid, node.parent ?? null, offset, deps.poseComposition === void 0, ops);
10381
+ if (newId) newIds.push(newId);
9722
10382
  }
9723
- selection.set([]);
9724
- }
9725
- },
9726
- enabled: () => true
9727
- };
9728
- function requiresSelection(deps) {
9729
- const selection = deps?.selection;
9730
- if (!selection || selection.get().length === 0) {
9731
- return ActionDisabledReason.SelectionRequired;
9732
- }
9733
- return true;
9734
- }
9735
- var duplicateAction = {
9736
- id: "duplicate",
9737
- label: "Duplicate",
9738
- defaultBinding: { kind: "key", key: "d", mods: { mod: true } },
9739
- eligible: { capability: ["edits-page", "creates-selection"] },
9740
- invoker: {
9741
- timing: "immediate",
9742
- run: (_deps, params) => {
10383
+ if (ops.length === 0) return;
10384
+ if (applyOps) applyOps(ops, "Duplicate");
10385
+ else scene.applyBatch(ops, "Duplicate", defaultCommitAdapter(scene));
10386
+ selection.set(newIds);
9743
10387
  }
9744
10388
  },
9745
10389
  enabled: requiresSelection
@@ -10709,7 +11353,7 @@ var LABEL = {
10709
11353
  divide: "Divide",
10710
11354
  crop: "Crop"
10711
11355
  };
10712
- function isEmpty(p) {
11356
+ function isEmpty2(p) {
10713
11357
  return p.commands.length === 0;
10714
11358
  }
10715
11359
  function applyBooleanOp(adapter, op) {
@@ -10747,7 +11391,7 @@ function applyBooleanOp(adapter, op) {
10747
11391
  break;
10748
11392
  }
10749
11393
  }
10750
- results = results.filter((p) => !isEmpty(p));
11394
+ results = results.filter((p) => !isEmpty2(p));
10751
11395
  if (results.length === 0) return { kind: "noop", reason: "empty-result" };
10752
11396
  const topmostId = entries[entries.length - 1].id;
10753
11397
  const topAnchor = adapter.getZOrder?.(topmostId);
@@ -11283,14 +11927,24 @@ var DEFAULT_PALETTE = [
11283
11927
  "#7ab8d4",
11284
11928
  "#d47a7a"
11285
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
+ }
11286
11935
  function refreshPreviews(scratch) {
11287
11936
  scratch.previews.clear();
11288
11937
  for (const id of scratch.ids) {
11289
11938
  const prev = scratch.startData.get(id);
11290
- const next = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
11291
- scratch.previews.set(id, { ...prev ?? {}, fill: next });
11939
+ scratch.previews.set(id, { ...prev ?? {}, fill: resolveFill(scratch, prev) });
11292
11940
  }
11293
11941
  }
11942
+ function readParams(params) {
11943
+ return {
11944
+ color: params?.color,
11945
+ paint: params?.paint
11946
+ };
11947
+ }
11294
11948
  var setFillAction = {
11295
11949
  id: "setFill",
11296
11950
  label: "Set fill",
@@ -11305,9 +11959,10 @@ var setFillAction = {
11305
11959
  if (!selection || !scene) return {};
11306
11960
  const ids = selection.get();
11307
11961
  if (ids.length === 0) return {};
11308
- const ctxColor = ctx.params?.color;
11309
- const optsColor = opts?.params?.color;
11310
- 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;
11311
11966
  const startData = /* @__PURE__ */ new Map();
11312
11967
  for (const id of ids) {
11313
11968
  const node = scene.get(id);
@@ -11318,15 +11973,20 @@ var setFillAction = {
11318
11973
  scene,
11319
11974
  startData,
11320
11975
  currentColor: initialColor,
11976
+ currentPaint: initialPaint,
11321
11977
  previews: /* @__PURE__ */ new Map(),
11322
11978
  applyOps
11323
11979
  };
11324
11980
  refreshPreviews(scratch);
11325
11981
  return {
11326
11982
  onMove(moveCtx) {
11327
- const next = moveCtx.params?.color;
11328
- if (next === void 0) return;
11329
- 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
+ }
11330
11990
  refreshPreviews(scratch);
11331
11991
  },
11332
11992
  onEnd(_endCtx, reason) {
@@ -11336,8 +11996,7 @@ var setFillAction = {
11336
11996
  }
11337
11997
  const ops = [];
11338
11998
  for (const id of scratch.ids) {
11339
- const prev = scratch.startData.get(id);
11340
- const merged = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
11999
+ const merged = resolveFill(scratch, scratch.startData.get(id));
11341
12000
  const nodeNow = scratch.scene.get(id);
11342
12001
  if (!nodeNow) continue;
11343
12002
  const from = { ...nodeNow.data };
@@ -11719,7 +12378,8 @@ function resolveTextStyle(style) {
11719
12378
  selectionColor: style.selectionColor ?? null,
11720
12379
  letterSpacing: style.letterSpacing ?? DEFAULT_TEXT_STYLE.letterSpacing,
11721
12380
  underline: style.underline ?? DEFAULT_TEXT_STYLE.underline,
11722
- strikethrough: style.strikethrough ?? DEFAULT_TEXT_STYLE.strikethrough
12381
+ strikethrough: style.strikethrough ?? DEFAULT_TEXT_STYLE.strikethrough,
12382
+ ...style.stroke !== void 0 ? { stroke: style.stroke } : {}
11723
12383
  };
11724
12384
  }
11725
12385
  function numericWeight(w) {
@@ -11740,6 +12400,9 @@ function resolveRuns(runs, style) {
11740
12400
  fontWeight: run.bold ? 700 : baseWeight,
11741
12401
  fontStyle: run.italic ? "italic" : style.fontStyle,
11742
12402
  fill: run.fill ?? style.fill,
12403
+ // Unlike the decorations below, a run's stroke *replaces* the node's
12404
+ // rather than adding to it — there is only one outline to paint.
12405
+ ...(run.stroke ?? style.stroke) !== void 0 ? { stroke: run.stroke ?? style.stroke } : {},
11743
12406
  letterSpacing: run.letterSpacing ?? style.letterSpacing,
11744
12407
  // `||`, not `??`: run-level decorations are additive over the node style.
11745
12408
  underline: run.underline || style.underline,
@@ -12116,7 +12779,9 @@ var KIT_STANDARD_DESCRIPTORS = [
12116
12779
  setStrokeAction,
12117
12780
  setFillOpacityAction,
12118
12781
  setStrokeOpacityAction,
12119
- ingestAction
12782
+ ingestAction,
12783
+ clipboardCopyAction,
12784
+ clipboardCutAction
12120
12785
  ];
12121
12786
  function useStandardActions(opts) {
12122
12787
  const optsRef = useRef(opts);
@@ -12191,6 +12856,54 @@ function targetRank(target) {
12191
12856
  if (target.startsWith("affordance:")) return 2;
12192
12857
  return 1;
12193
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
+ }
12194
12907
  function specificity(spec) {
12195
12908
  const t = targetRank("target" in spec ? spec.target : void 0);
12196
12909
  const mods = "mods" in spec ? spec.mods : void 0;
@@ -12207,12 +12920,14 @@ function compareSpecificity(a, b) {
12207
12920
  }
12208
12921
  return 0;
12209
12922
  }
12210
- function matchSorted(e, bindings, isMac, engagedChannels) {
12923
+ function matchSorted(e, bindings, isMac, engagedChannels, warn = (m) => console.warn(m)) {
12211
12924
  const out = [];
12212
12925
  const engaged = engagedChannels ?? EMPTY_ENGAGED2;
12926
+ const exclusive = isExclusiveClaim(e);
12927
+ const pool = exclusive ? bindings.filter((sb) => targetConsultsAffordance(specTargetOf(sb.binding.spec))) : bindings;
12213
12928
  for (const scope of SCOPE_PRIORITY) {
12214
12929
  const scopeMatches = [];
12215
- for (const sb of bindings) {
12930
+ for (const sb of pool) {
12216
12931
  if (sb.scope !== scope) continue;
12217
12932
  const phaseCtx = { selfChannel: sb.ownerToolId, engagedChannels: engaged };
12218
12933
  if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
@@ -12222,9 +12937,32 @@ function matchSorted(e, bindings, isMac, engagedChannels) {
12222
12937
  scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
12223
12938
  out.push(...scopeMatches);
12224
12939
  }
12940
+ if (exclusive && out.length === 0 && bindings.length > 0) {
12941
+ reportDeadClaim(claimOf(e)?.owner, warn);
12942
+ }
12225
12943
  return out;
12226
12944
  }
12227
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
+ }
12228
12966
  var ALWAYS = { all: [] };
12229
12967
  var NEVER = { any: [] };
12230
12968
  function isAllRule(r) {
@@ -12281,6 +13019,14 @@ function evaluateSelector(s, ctx) {
12281
13019
  const resizable2 = ctx.selectionResizable ?? true;
12282
13020
  if (resizable2 !== s.resizable) return false;
12283
13021
  }
13022
+ if (s.coarsePointer !== void 0) {
13023
+ const device = ctx.device ?? DEFAULT_DEVICE_PROFILE;
13024
+ if (device.coarsePointer !== s.coarsePointer) return false;
13025
+ }
13026
+ if (s.canHover !== void 0) {
13027
+ const device = ctx.device ?? DEFAULT_DEVICE_PROFILE;
13028
+ if (device.canHover !== s.canHover) return false;
13029
+ }
12284
13030
  return true;
12285
13031
  }
12286
13032
  var MAX_RULE_DEPTH = 10;
@@ -12350,6 +13096,8 @@ cond({ selection: { empty: true } });
12350
13096
  cond({ when: (c) => c.multiActive });
12351
13097
  cond({ hovering: true });
12352
13098
  cond({ hoveringSelected: true });
13099
+ cond({ coarsePointer: true });
13100
+ cond({ canHover: true });
12353
13101
  var DEFAULT_ALLOWED_CAPABILITIES = /* @__PURE__ */ new Set([
12354
13102
  ...NORMAL.allows,
12355
13103
  ...IMPLICIT_TAGS
@@ -12509,6 +13257,7 @@ function filterEligible(matches, actionLookup, ruleCtx) {
12509
13257
  return isEligible(action, ruleCtx);
12510
13258
  });
12511
13259
  }
13260
+ var EMPTY_TRIGGERS = /* @__PURE__ */ new Set();
12512
13261
  var EMPTY_ENGAGED22 = /* @__PURE__ */ new Set();
12513
13262
  function modifiersOf(e) {
12514
13263
  return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
@@ -12616,34 +13365,23 @@ function createDispatcher(opts) {
12616
13365
  return `ongoing-${event.kind}`;
12617
13366
  }
12618
13367
  function assembleScopedBindings(ctx) {
12619
- const result = [];
12620
- for (const toolId of ctx.hotkeyStack) {
12621
- const tool = ctx.toolsById.get(toolId);
12622
- for (const binding of tool?.bindings ?? []) {
12623
- result.push({ binding, scope: "hotkey", ownerToolId: toolId });
12624
- }
12625
- }
12626
- const activeTool = ctx.toolsById.get(ctx.activeToolId);
12627
- for (const binding of activeTool?.bindings ?? []) {
12628
- result.push({ binding, scope: "active", ownerToolId: ctx.activeToolId });
12629
- }
12630
- for (const toolId of ctx.ambientToolIds ?? []) {
12631
- const tool = ctx.toolsById.get(toolId);
12632
- for (const binding of tool?.bindings ?? []) {
12633
- result.push({ binding, scope: "ambient", ownerToolId: toolId });
12634
- }
12635
- }
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
+ });
12636
13381
  for (const action of ctx.actions.list()) {
12637
- const gs = action.defaultBinding;
12638
- if (!gs) continue;
12639
- const raw = Array.isArray(gs) ? gs : [gs];
12640
- for (const entry of raw) {
12641
- const isBoundObj = !("kind" in entry);
12642
- const spec = isBoundObj ? entry.spec : entry;
12643
- const opts2 = isBoundObj ? entry.opts : void 0;
12644
- const defaultBinding = { spec, actionId: action.id, ...opts2 !== void 0 ? { opts: opts2 } : {} };
12645
- const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
12646
- 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 });
12647
13385
  }
12648
13386
  }
12649
13387
  return result;
@@ -12798,15 +13536,26 @@ function createDispatcher(opts) {
12798
13536
  deltaY: event.deltaY,
12799
13537
  clientX: event.clientX,
12800
13538
  clientY: event.clientY,
13539
+ affordance: event.affordance,
12801
13540
  ...resolved
12802
13541
  };
12803
13542
  } else if (event.kind === "click" || event.kind === "doubleclick") {
12804
13543
  params = {
12805
13544
  worldX: event.worldX,
12806
13545
  worldY: event.worldY,
13546
+ affordance: event.affordance,
12807
13547
  // Press point as well as release point — an action that places
12808
13548
  // geometry at the click wants the former. See `ClickEvent`.
12809
- ...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,
12810
13559
  mods: modifiersOf(event),
12811
13560
  ...resolved
12812
13561
  };
@@ -13174,7 +13923,7 @@ function computeMultiTouchGeometry(positions) {
13174
13923
  return { centroid, spread };
13175
13924
  }
13176
13925
  function useGestureDispatcher(opts) {
13177
- 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;
13178
13927
  const onDoubleClickRef = useRef(onDoubleClick);
13179
13928
  onDoubleClickRef.current = onDoubleClick;
13180
13929
  const activeTool = useActiveToolContext();
@@ -13189,7 +13938,6 @@ function useGestureDispatcher(opts) {
13189
13938
  activeToolId: activeTool.active,
13190
13939
  hotkeyStack: activeTool.hotkeyStack,
13191
13940
  toolsById,
13192
- ambientToolIds,
13193
13941
  isMac: IS_MAC,
13194
13942
  getRuleCtx
13195
13943
  });
@@ -13199,7 +13947,6 @@ function useGestureDispatcher(opts) {
13199
13947
  activeToolId: activeTool.active,
13200
13948
  hotkeyStack: activeTool.hotkeyStack,
13201
13949
  toolsById,
13202
- ambientToolIds,
13203
13950
  isMac: IS_MAC,
13204
13951
  getRuleCtx
13205
13952
  };
@@ -13255,6 +14002,49 @@ function useGestureDispatcher(opts) {
13255
14002
  const DRAG_THRESHOLD_PX = 4;
13256
14003
  const bufferedDown = /* @__PURE__ */ new Map();
13257
14004
  const lastPointerDown = /* @__PURE__ */ new Map();
14005
+ const LONG_PRESS_MS = 500;
14006
+ const longPressTimers = /* @__PURE__ */ new Map();
14007
+ const cancelLongPress = (pointerId) => {
14008
+ const t = longPressTimers.get(pointerId);
14009
+ if (t !== void 0) {
14010
+ clearTimeout(t);
14011
+ longPressTimers.delete(pointerId);
14012
+ }
14013
+ };
14014
+ const cancelAllLongPress = () => {
14015
+ for (const t of longPressTimers.values()) clearTimeout(t);
14016
+ longPressTimers.clear();
14017
+ };
14018
+ const fireLongPress = (pointerId) => {
14019
+ const down = lastPointerDown.get(pointerId);
14020
+ if (!down) return;
14021
+ const shared = {
14022
+ target: down.target,
14023
+ altKey: down.altKey,
14024
+ ctrlKey: down.ctrlKey,
14025
+ metaKey: down.metaKey,
14026
+ shiftKey: down.shiftKey,
14027
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
14028
+ ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
14029
+ ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
14030
+ };
14031
+ const result = dispatch2({
14032
+ kind: "longpress",
14033
+ x: down.worldX,
14034
+ y: down.worldY,
14035
+ clientX: down.clientX,
14036
+ clientY: down.clientY,
14037
+ ...shared
14038
+ });
14039
+ if (result === "unhandled") {
14040
+ dispatch2({
14041
+ kind: "contextmenu",
14042
+ worldX: down.worldX,
14043
+ worldY: down.worldY,
14044
+ ...shared
14045
+ });
14046
+ }
14047
+ };
13258
14048
  const DOUBLE_CLICK_MAX_MS = 600;
13259
14049
  const DOUBLE_CLICK_MAX_PX = 8;
13260
14050
  const onKeyDown = (e) => {
@@ -13307,6 +14097,9 @@ function useGestureDispatcher(opts) {
13307
14097
  const onWheel = (e) => {
13308
14098
  const rect = canvas?.getBoundingClientRect();
13309
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);
13310
14103
  const ev = {
13311
14104
  kind: "wheel",
13312
14105
  altKey: e.altKey,
@@ -13316,7 +14109,10 @@ function useGestureDispatcher(opts) {
13316
14109
  deltaX: e.deltaX,
13317
14110
  deltaY: e.deltaY,
13318
14111
  clientX: localX,
13319
- 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 } : {}
13320
14116
  };
13321
14117
  const result = dispatch2(ev);
13322
14118
  if (result === "handled") {
@@ -13358,6 +14154,7 @@ function useGestureDispatcher(opts) {
13358
14154
  bufferedDown.set(e.pointerId, { ev, clientX: e.clientX, clientY: e.clientY });
13359
14155
  dispatch2({ ...ev, stage: "press" });
13360
14156
  lastPointerDown.set(e.pointerId, {
14157
+ target: e.target,
13361
14158
  clientX: e.clientX,
13362
14159
  clientY: e.clientY,
13363
14160
  worldX: w.x,
@@ -13370,7 +14167,19 @@ function useGestureDispatcher(opts) {
13370
14167
  metaKey: e.metaKey,
13371
14168
  shiftKey: e.shiftKey
13372
14169
  });
14170
+ if ((e.pointerType === "touch" || e.pointerType === "pen") && activePointers.size === 1) {
14171
+ cancelLongPress(e.pointerId);
14172
+ longPressTimers.set(
14173
+ e.pointerId,
14174
+ setTimeout(() => {
14175
+ longPressTimers.delete(e.pointerId);
14176
+ if (disposed) return;
14177
+ fireLongPress(e.pointerId);
14178
+ }, LONG_PRESS_MS)
14179
+ );
14180
+ }
13373
14181
  if (activePointers.size >= 2) {
14182
+ cancelAllLongPress();
13374
14183
  for (const id of activePointers) bufferedDown.delete(id);
13375
14184
  const { centroid, spread } = computeMultiTouchGeometry(pointerPositions);
13376
14185
  if (!multiTouchStart) {
@@ -13401,6 +14210,7 @@ function useGestureDispatcher(opts) {
13401
14210
  const dy = e.clientY - buffered.clientY;
13402
14211
  if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
13403
14212
  bufferedDown.delete(e.pointerId);
14213
+ cancelLongPress(e.pointerId);
13404
14214
  dispatch2(buffered.ev);
13405
14215
  }
13406
14216
  }
@@ -13514,6 +14324,7 @@ function useGestureDispatcher(opts) {
13514
14324
  clearHoverCursor();
13515
14325
  };
13516
14326
  const onPointerUp = (e) => {
14327
+ cancelLongPress(e.pointerId);
13517
14328
  const prevSize = activePointers.size;
13518
14329
  activePointers.delete(e.pointerId);
13519
14330
  pointerPositions.delete(e.pointerId);
@@ -13595,6 +14406,7 @@ function useGestureDispatcher(opts) {
13595
14406
  shiftKey: e.shiftKey,
13596
14407
  worldX: wClick.x,
13597
14408
  worldY: wClick.y,
14409
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
13598
14410
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
13599
14411
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
13600
14412
  };
@@ -13618,6 +14430,7 @@ function useGestureDispatcher(opts) {
13618
14430
  }, 0);
13619
14431
  };
13620
14432
  const onPointerCancel = (e) => {
14433
+ cancelLongPress(e.pointerId);
13621
14434
  activePointers.delete(e.pointerId);
13622
14435
  pointerPositions.delete(e.pointerId);
13623
14436
  lastPointerDown.delete(e.pointerId);
@@ -13635,8 +14448,10 @@ function useGestureDispatcher(opts) {
13635
14448
  };
13636
14449
  const onContextMenu = (e) => {
13637
14450
  e.preventDefault();
13638
- const worldPoint = { x: e.clientX, y: e.clientY };
13639
- 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);
13640
14455
  const ev = {
13641
14456
  kind: "contextmenu",
13642
14457
  target: e.target,
@@ -13644,6 +14459,9 @@ function useGestureDispatcher(opts) {
13644
14459
  ctrlKey: e.ctrlKey,
13645
14460
  metaKey: e.metaKey,
13646
14461
  shiftKey: e.shiftKey,
14462
+ worldX: w.x,
14463
+ worldY: w.y,
14464
+ ...affordance !== void 0 ? { affordance } : {},
13647
14465
  ...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
13648
14466
  ...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
13649
14467
  };
@@ -13740,6 +14558,7 @@ function useGestureDispatcher(opts) {
13740
14558
  window.removeEventListener("paste", onPaste);
13741
14559
  canvas?.classList.remove(DROPOVER_CLASS);
13742
14560
  clearHoverCursor();
14561
+ cancelAllLongPress();
13743
14562
  disposed = true;
13744
14563
  dispatcher.cancelAll("cancel");
13745
14564
  };
@@ -13941,47 +14760,203 @@ function usePinchGesture(canvasRef, onPinch, enabled = true) {
13941
14760
  };
13942
14761
  }, [canvasRef, enabled]);
13943
14762
  }
13944
- function useTools(opts) {
13945
- if (!(opts.active in opts.registry)) {
13946
- 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";
13947
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) {
13948
14809
  const ctx = useActiveToolContext();
14810
+ const actionsRegistry = useActionsRegistry();
13949
14811
  const hasInitializedRef = useRef(false);
13950
14812
  const isFirstRender = !hasInitializedRef.current;
13951
14813
  const needsFirstMountSyncRef = useRef(false);
13952
14814
  if (isFirstRender) {
13953
14815
  hasInitializedRef.current = true;
13954
- needsFirstMountSyncRef.current = ctx.active === "select" && opts.active !== "select";
14816
+ needsFirstMountSyncRef.current = ctx.active === "select" && opts.focused !== "select";
13955
14817
  }
13956
14818
  useEffect(() => {
13957
14819
  if (!needsFirstMountSyncRef.current) return;
13958
14820
  needsFirstMountSyncRef.current = false;
13959
- 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
+ };
13960
14848
  }, []);
13961
- const active = isFirstRender && ctx.active === "select" && opts.active !== "select" ? opts.active : ctx.active;
13962
- const hotkeyStack = ctx.hotkeyStack;
13963
- const hotkeyEngaged = hotkeyStack.at(-1) ?? null;
13964
- const registryRef = useRef(opts.registry);
13965
- registryRef.current = opts.registry;
13966
- const ambientRef = useRef(opts.ambient ?? []);
13967
- ambientRef.current = opts.ambient ?? [];
13968
- const activeRef = useRef(active);
13969
- 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;
13970
14944
  const hotkeyRef = useRef(hotkeyEngaged);
13971
14945
  hotkeyRef.current = hotkeyEngaged;
14946
+ const setFocused = contributions.setFocused;
13972
14947
  const setActive = useCallback(
13973
14948
  (id) => {
13974
- if (!(id in registryRef.current)) {
14949
+ if (!(id in slottedRef.current.registry)) {
13975
14950
  throw new Error(`setActive: "${id}" not in registry`);
13976
14951
  }
13977
14952
  dlog("tools", "active:", activeRef.current, "\u2192", id);
13978
- ctx.setActive(id);
14953
+ setFocused(id);
13979
14954
  },
13980
- [ctx]
14955
+ [setFocused]
13981
14956
  );
13982
14957
  const engageHotkey = useCallback(
13983
14958
  (id) => {
13984
- if (!(id in registryRef.current)) {
14959
+ if (!(id in slottedRef.current.registry)) {
13985
14960
  throw new Error(`engageHotkey: "${id}" not in registry`);
13986
14961
  }
13987
14962
  dlog("tools", "hotkey engaged:", id);
@@ -13993,32 +14968,12 @@ function useTools(opts) {
13993
14968
  if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
13994
14969
  ctx.popHotkey();
13995
14970
  }, [ctx]);
13996
- const lastConflictSigRef = useRef(null);
13997
- const toolSetSig = Object.keys(opts.registry).join(",") + "|" + (opts.ambient ?? []).map((t) => t.id).join(",");
13998
- useEffect(() => {
13999
- if (process.env.NODE_ENV === "production") return;
14000
- if (lastConflictSigRef.current === toolSetSig) return;
14001
- lastConflictSigRef.current = toolSetSig;
14002
- reportRouteConflicts({
14003
- registry: registryRef.current,
14004
- ambient: ambientRef.current
14005
- });
14006
- }, [toolSetSig]);
14007
14971
  const has = useCallback(
14008
- (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),
14009
14973
  []
14010
14974
  );
14011
- const getActiveOverlays = useCallback(() => {
14012
- const out = [];
14013
- const activeTool = registryRef.current[activeRef.current];
14014
- if (activeTool?.overlay) out.push(activeTool.overlay);
14015
- const mod = hotkeyRef.current ? registryRef.current[hotkeyRef.current] : null;
14016
- if (mod?.overlay) out.push(mod.overlay);
14017
- for (const t of ambientRef.current) {
14018
- if (t.overlay) out.push(t.overlay);
14019
- }
14020
- return out;
14021
- }, []);
14975
+ const active = contributions.focused;
14976
+ const getActiveOverlays = contributions.overlays;
14022
14977
  return useMemo(
14023
14978
  () => ({
14024
14979
  active,
@@ -14026,44 +14981,14 @@ function useTools(opts) {
14026
14981
  hotkeyEngaged,
14027
14982
  engageHotkey,
14028
14983
  disengageHotkey,
14029
- ambient: ambientRef.current,
14030
- registry: registryRef.current,
14984
+ ambient: slottedRef.current.ambient,
14985
+ registry: slottedRef.current.registry,
14031
14986
  has,
14032
14987
  getActiveOverlays
14033
14988
  }),
14034
14989
  [active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays]
14035
14990
  );
14036
14991
  }
14037
- var TOOL_OFFHAND_ID = "tool.offhand";
14038
- function buildToolOffhandBindings(specs) {
14039
- return specs.map(({ toolId, key }) => ({
14040
- spec: { kind: "key-held", key },
14041
- opts: { params: { toolId } }
14042
- }));
14043
- }
14044
- function makeToolOffhandAction(bindings) {
14045
- return {
14046
- id: TOOL_OFFHAND_ID,
14047
- label: "Engage tool (offhand)",
14048
- defaultBinding: bindings,
14049
- scope: "hotkey",
14050
- invoker: {
14051
- timing: "ongoing",
14052
- start: (ctx, opts) => {
14053
- const activeTool = ctx.deps.activeTool;
14054
- const toolId = resolveParams(opts?.params)?.toolId;
14055
- if (!activeTool || !toolId) return {};
14056
- activeTool.pushHotkey(toolId);
14057
- return {
14058
- onEnd: () => {
14059
- activeTool.popHotkey();
14060
- }
14061
- };
14062
- }
14063
- },
14064
- requires: ["activeTool"]
14065
- };
14066
- }
14067
14992
  var TOOL_ACTIVATE_ID = "tool.activate";
14068
14993
  function buildToolActivateBindings(specs) {
14069
14994
  return specs.map(({ toolId, keyOpts }) => {
@@ -14135,9 +15060,6 @@ var BUILTIN_SELECT_KEYS = {
14135
15060
  hand: { key: "H" },
14136
15061
  pen: { key: "P" }
14137
15062
  };
14138
- var BUILTIN_OFFHAND_ACTIONS = {
14139
- hand: " "
14140
- };
14141
15063
  function useKeybindings(tools, options = {}) {
14142
15064
  const toolsRef = useRef(tools);
14143
15065
  toolsRef.current = tools;
@@ -14145,19 +15067,6 @@ function useKeybindings(tools, options = {}) {
14145
15067
  optionsRef.current = options;
14146
15068
  const initialActiveRef = useRef(tools.active);
14147
15069
  const registry22 = useActionsRegistry();
14148
- useEffect(() => {
14149
- if (optionsRef.current.disable) return;
14150
- if (!registry22) return;
14151
- const specs = [];
14152
- for (const [toolId, key] of Object.entries(BUILTIN_OFFHAND_ACTIONS)) {
14153
- if (toolsRef.current.has(toolId)) {
14154
- specs.push({ toolId, key });
14155
- }
14156
- }
14157
- if (specs.length === 0) return;
14158
- const bindings = buildToolOffhandBindings(specs);
14159
- return registry22.register(makeToolOffhandAction(bindings));
14160
- }, [registry22, tools]);
14161
15070
  useEffect(() => {
14162
15071
  if (optionsRef.current.disable) return;
14163
15072
  if (!registry22) return;
@@ -14552,7 +15461,82 @@ function textLineBoxes(pose, opts = {}) {
14552
15461
  }
14553
15462
  return out;
14554
15463
  }
14555
- var cache4 = /* @__PURE__ */ new Map();
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
+ }
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();
14556
15540
  var subscribers2 = /* @__PURE__ */ new Set();
14557
15541
  var defaultLoader = async (src) => {
14558
15542
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -14570,28 +15554,28 @@ function notify() {
14570
15554
  }
14571
15555
  function startLoad(src) {
14572
15556
  const entry = { status: "loading" };
14573
- cache4.set(src, entry);
15557
+ cache6.set(src, entry);
14574
15558
  loader(src).then(
14575
15559
  (bitmap) => {
14576
- if (cache4.get(src) !== entry) return;
14577
- cache4.set(src, { status: "ready", bitmap });
15560
+ if (cache6.get(src) !== entry) return;
15561
+ cache6.set(src, { status: "ready", bitmap });
14578
15562
  notify();
14579
15563
  },
14580
15564
  () => {
14581
- if (cache4.get(src) !== entry) return;
14582
- cache4.set(src, { status: "error" });
15565
+ if (cache6.get(src) !== entry) return;
15566
+ cache6.set(src, { status: "error" });
14583
15567
  notify();
14584
15568
  }
14585
15569
  );
14586
15570
  }
14587
15571
  function getImageBitmap(src) {
14588
- const entry = cache4.get(src);
15572
+ const entry = cache6.get(src);
14589
15573
  if (entry) return entry.bitmap;
14590
15574
  startLoad(src);
14591
15575
  return void 0;
14592
15576
  }
14593
15577
  function imageStatus(src) {
14594
- return cache4.get(src)?.status ?? "idle";
15578
+ return cache6.get(src)?.status ?? "idle";
14595
15579
  }
14596
15580
  function subscribeImageReady(cb) {
14597
15581
  subscribers2.add(cb);
@@ -14685,6 +15669,13 @@ function shapeCoversPoint(node, pose, x, y, opts = {}) {
14685
15669
  const reach = ink.strokeWidth / 2 + (opts.tolerance ?? 0);
14686
15670
  return reach > 0 && strokeHitTest(sil, x, y, reach);
14687
15671
  }
15672
+ function withLeafStroke(style, stroke, strokeWidth) {
15673
+ if (style?.stroke !== void 0) return style;
15674
+ if (!stroke || stroke === "none") return style;
15675
+ const width = strokeWidth ?? 1;
15676
+ if (width <= 0) return style;
15677
+ return { ...style, stroke: { paint: { color: stroke }, width } };
15678
+ }
14688
15679
  var TEXT_PAINTER = {
14689
15680
  id: "kit:text",
14690
15681
  matches: (node) => {
@@ -14703,7 +15694,8 @@ var TEXT_PAINTER = {
14703
15694
  const d = node.data;
14704
15695
  const p = pose;
14705
15696
  const y = p.y;
14706
- return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, d.style, void 0, p.height)] : [textCommand(p.x, y, d.text, d.style, void 0, p.height)];
15697
+ const style = withLeafStroke(d.style, d.stroke, d.strokeWidth);
15698
+ return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, style, void 0, p.height)] : [textCommand(p.x, y, d.text, style, void 0, p.height)];
14707
15699
  }),
14708
15700
  // The pose is a *wrap box*, not a bounding box — "Away" in a 300-unit box
14709
15701
  // leaves most of it empty, and a pose-rect silhouette claims all of it. The
@@ -14761,6 +15753,17 @@ function rectsToPath(rects) {
14761
15753
  }
14762
15754
  return { kind: "polygon", commands, coords, fillRule: "nonzero" };
14763
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
+ }
14764
15767
  var PATH_PAINTER = {
14765
15768
  id: "kit:path",
14766
15769
  matches: (node) => {
@@ -14774,12 +15777,12 @@ var PATH_PAINTER = {
14774
15777
  const d = node.data;
14775
15778
  const projected = pathInPoseFrame(d.path, pose);
14776
15779
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
14777
- const fillColor = d.fill ?? d.color;
14778
- 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));
14779
15782
  const cmd = {
14780
15783
  kind: "path",
14781
15784
  path: projected,
14782
- ...hasFill ? { fill: { color: fillColor ?? "#888" } } : {},
15785
+ ...fill ? { fill } : {},
14783
15786
  ...hasStroke ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
14784
15787
  };
14785
15788
  return [cmd];
@@ -14795,9 +15798,8 @@ var PATH_PAINTER = {
14795
15798
  ink: (node) => {
14796
15799
  const d = node.data;
14797
15800
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
14798
- const fillColor = d.fill ?? d.color;
14799
15801
  return {
14800
- filled: fillColor !== "none" && (fillColor !== void 0 || !hasStroke),
15802
+ filled: resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888") !== null,
14801
15803
  strokeWidth: hasStroke ? d.strokeWidth ?? 1 : 0
14802
15804
  };
14803
15805
  }
@@ -14814,10 +15816,12 @@ var SHAPE_PAINTER = {
14814
15816
  paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
14815
15817
  const d = node.data;
14816
15818
  const path = pathForShape(d, pose);
15819
+ const declaredFill = resolveNodeFill(d.fill, d.color, false, "#888");
15820
+ const shapeFill = declaredFill && resolveFillPattern(fillInPoseFrame(declaredFill, pose));
14817
15821
  return [{
14818
15822
  kind: "path",
14819
15823
  path,
14820
- fill: { color: d.fill ?? d.color ?? "#888" },
15824
+ ...shapeFill ? { fill: shapeFill } : {},
14821
15825
  ...d.stroke && (d.strokeWidth ?? 0) > 0 ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
14822
15826
  }];
14823
15827
  }),
@@ -15213,11 +16217,8 @@ function useSelectTool(adapter, options) {
15213
16217
  ...moveOpts
15214
16218
  },
15215
16219
  { spec: { kind: "drag", target: "empty" }, actionId: "areaSelect" },
15216
- // Click on empty canvas clear. Same no-affordance requirement as
15217
- // the press binding above, and for the same two reasons: clicking a
15218
- // widget that happens to float over empty canvas isn't a click on
15219
- // empty canvas, and declining in the SPEC lets the chrome's owner
15220
- // 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.
15221
16222
  {
15222
16223
  spec: {
15223
16224
  kind: "click",
@@ -15301,17 +16302,17 @@ function worldToLocal(xf, wx, wy) {
15301
16302
  function annulusSemiAxes(shape, view) {
15302
16303
  const bandPx = shape.minBandPx ?? 0;
15303
16304
  if (bandPx === 0) return { rx: shape.rx, ry: shape.ry };
15304
- const band = bandPx / meanScale(view.scale);
16305
+ const band = pxExtent(bandPx, view.scale);
15305
16306
  return {
15306
- rx: Math.max(shape.rx, shape.innerWidth / 2 + band),
15307
- 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)
15308
16309
  };
15309
16310
  }
15310
16311
  function hitRegion(region, wx, wy, xf, view) {
15311
16312
  const local = worldToLocal(xf, wx, wy);
15312
16313
  if (region.shape.kind === "point") {
15313
- const radiusWorld = region.shape.hitRadiusPx / meanScale(view.scale);
15314
- 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);
15315
16316
  }
15316
16317
  if (region.shape.kind === "annulus") {
15317
16318
  const s = region.shape;
@@ -15375,14 +16376,13 @@ function asIsVisible(data) {
15375
16376
  function recordRegionHitbox(debug, affordanceId, region, xf, view) {
15376
16377
  if (region.shape.kind === "point") {
15377
16378
  const w2 = localToWorld(xf, region.shape.x, region.shape.y);
15378
- const r2 = region.shape.hitRadiusPx / meanScale(view.scale);
16379
+ const e = pxExtent(region.shape.hitRadiusPx, view.scale);
15379
16380
  debug.recordHitbox(affordanceId, "handle", {
15380
16381
  kind: "rect",
15381
- x: w2.x - r2,
15382
- y: w2.y - r2,
15383
- width: r2 * 2,
15384
- height: r2 * 2,
15385
- ...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
15386
16386
  });
15387
16387
  return;
15388
16388
  }
@@ -15441,7 +16441,7 @@ function paintRegion(region, xf, view, state, viewT, out) {
15441
16441
  if (paint.kind === "annulus") {
15442
16442
  if (region.shape.kind !== "annulus") return;
15443
16443
  const s = region.shape;
15444
- const insetLocal = (paint.insetPx ?? 0) / meanScale(view.scale);
16444
+ const insetLocal = pxExtent(paint.insetPx ?? 0, view.scale);
15445
16445
  const cmd = annulusCommand(s, annulusSemiAxes(s, view), insetLocal, xf, viewT, paint.fill, paint.stroke);
15446
16446
  if (cmd) out.push(cmd);
15447
16447
  return;
@@ -15458,13 +16458,13 @@ function paintRegion(region, xf, view, state, viewT, out) {
15458
16458
  }
15459
16459
  }
15460
16460
  function annulusCommand(shape, semiAxes, insetLocal, xf, viewT, fill, stroke) {
15461
- const rx = semiAxes.rx - insetLocal;
15462
- const ry = semiAxes.ry - insetLocal;
16461
+ const rx = semiAxes.rx - insetLocal.x;
16462
+ const ry = semiAxes.ry - insetLocal.y;
15463
16463
  if (rx <= 0 || ry <= 0) return null;
15464
16464
  const cx = shape.cx;
15465
16465
  const cy = shape.cy;
15466
- const rectHW = shape.innerWidth / 2 + insetLocal;
15467
- const rectHH = shape.innerHeight / 2 + insetLocal;
16466
+ const rectHW = shape.innerWidth / 2 + insetLocal.x;
16467
+ const rectHH = shape.innerHeight / 2 + insetLocal.y;
15468
16468
  const haveTopBottom = rectHH < ry;
15469
16469
  const haveLeftRight = rectHW < rx;
15470
16470
  if (!haveTopBottom && !haveLeftRight) return null;
@@ -15711,6 +16711,7 @@ function useHandTool(opts = {}) {
15711
16711
  () => Object.assign(defineViewportTool({
15712
16712
  id: "hand",
15713
16713
  capabilities: ["navigation"],
16714
+ hotkey: "space",
15714
16715
  hookName: "useHandTool",
15715
16716
  presentation: {
15716
16717
  label: "Hand",
@@ -15831,10 +16832,6 @@ function mirrorHandle(anchor, out) {
15831
16832
  if (!out || anchor.altBroken) return void 0;
15832
16833
  return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
15833
16834
  }
15834
- function dist2(ax, ay, bx, by) {
15835
- const dx = ax - bx, dy = ay - by;
15836
- return Math.hypot(dx, dy);
15837
- }
15838
16835
  function usePenTool(options) {
15839
16836
  const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, snapPoint: snapPoint2 } = options;
15840
16837
  const scratchRef = useRef(null);
@@ -15858,11 +16855,14 @@ function usePenTool(options) {
15858
16855
  const fn = optsRef.current.snapPoint;
15859
16856
  return fn ? fn({ x, y }) : { x, y };
15860
16857
  }, []);
15861
- const closeRadius = useCallback((deps) => {
15862
- const view = deps.view?.get();
15863
- const scale2 = view?.scale ?? { x: 1, y: 1 };
15864
- return optsRef.current.closeHitRadius / meanScale(scale2);
15865
- }, []);
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
+ );
15866
16866
  const actions = useMemo(() => {
15867
16867
  const s = () => scratchRef.current;
15868
16868
  return [
@@ -15878,10 +16878,9 @@ function usePenTool(options) {
15878
16878
  if (p?.pressX === void 0 || p.pressY === void 0) return;
15879
16879
  const { x: wx, y: wy } = snap2(p.pressX, p.pressY);
15880
16880
  const scratch = s();
15881
- const radius = closeRadius(deps);
15882
16881
  if (scratch.current && scratch.current.anchors.length >= 3) {
15883
16882
  const first = scratch.current.anchors[0];
15884
- if (dist2(first.x, first.y, wx, wy) <= radius) {
16883
+ if (withinCloseRadius(deps, first.x, first.y, wx, wy)) {
15885
16884
  scratch.current.closed = true;
15886
16885
  scratch.finishedSubpaths.push(scratch.current);
15887
16886
  scratch.current = null;
@@ -16000,7 +16999,7 @@ function usePenTool(options) {
16000
16999
  }
16001
17000
  }
16002
17001
  ];
16003
- }, [commit, snap2, closeRadius]);
17002
+ }, [commit, snap2, withinCloseRadius]);
16004
17003
  return useMemo(() => {
16005
17004
  return defineTool({
16006
17005
  id: "pen",
@@ -16753,7 +17752,7 @@ function buildChromeState(args) {
16753
17752
  }
16754
17753
  };
16755
17754
  }
16756
- var DEFAULT_ROTATION_HANDLE_DISTANCE = 24;
17755
+ var DEFAULT_ROTATION_HANDLE_DISTANCE = ROTATION_HANDLE_BASE_PX;
16757
17756
  function rotationHandle(pose, distance = DEFAULT_ROTATION_HANDLE_DISTANCE) {
16758
17757
  const rotation = pose.rotation ?? 0;
16759
17758
  const c = aabbCenter(pose);
@@ -16813,7 +17812,7 @@ var DEFAULT_HANDLE_OUTLINE = {
16813
17812
  paint: { fill: "solid", color: "#1a130d" },
16814
17813
  width: 1
16815
17814
  };
16816
- var DEFAULT_HANDLE_SIZE = 8;
17815
+ var DEFAULT_HANDLE_SIZE = HANDLE_BASE_PX;
16817
17816
  function defaultHandlesOf(b) {
16818
17817
  return [
16819
17818
  { x: b.x, y: b.y },
@@ -17566,8 +18565,8 @@ function CanvasInner(props, ref) {
17566
18565
  const isVisible = getIsVisibleRef.current?.() ?? alwaysVisible;
17567
18566
  for (const layer of layers2) {
17568
18567
  if (!layer.hitTest) continue;
17569
- const binding = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
17570
- 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 };
17571
18570
  }
17572
18571
  return null;
17573
18572
  }, []);
@@ -18003,7 +19002,7 @@ function CanvasInner(props, ref) {
18003
19002
  effectiveView,
18004
19003
  { width, height }
18005
19004
  );
18006
- renderer.render(commands);
19005
+ renderer.render(commands, viewToMat3(effectiveView));
18007
19006
  }, [layersWithDebug, width, height, effectiveView, debugSink, redrawNonce, dprProp]);
18008
19007
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
18009
19008
  useEffect(() => {
@@ -20212,8 +21211,8 @@ var DEFAULT_FILL3 = "#d4c4a8";
20212
21211
  var DEFAULT_STROKE = "#1a130d";
20213
21212
  function createCornerResizeAffordance(opts = {}) {
20214
21213
  const {
20215
- handleHitRadius = 8,
20216
- handleSize = 8,
21214
+ handleHitRadius = HANDLE_BASE_PX,
21215
+ handleSize = HANDLE_BASE_PX,
20217
21216
  fill = DEFAULT_FILL3,
20218
21217
  stroke = DEFAULT_STROKE
20219
21218
  } = opts;
@@ -20351,8 +21350,8 @@ function createPathAnchorAffordances(getAnchorState, opts = {}) {
20351
21350
  };
20352
21351
  return [anchors, controls];
20353
21352
  }
20354
- var HANDLE_HIT_RADIUS = 8;
20355
- var ANCHOR_HIT_RADIUS = 8;
21353
+ var HANDLE_HIT_RADIUS = HANDLE_BASE_PX;
21354
+ var ANCHOR_HIT_RADIUS = ANCHOR_HIT_BASE_PX;
20356
21355
  function buildAffordanceAt(opts) {
20357
21356
  const {
20358
21357
  getChromeState,
@@ -20384,6 +21383,8 @@ function toAffordanceHit(hit) {
20384
21383
  const scratch = hit.binding.initialScratch ?? {};
20385
21384
  return {
20386
21385
  kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,
21386
+ owner: hit.affordanceId,
21387
+ strength: "shared",
20387
21388
  ...scratch.targetId !== void 0 ? { targetIds: [scratch.targetId] } : {},
20388
21389
  ...scratch.anchor !== void 0 ? { anchor: scratch.anchor } : {},
20389
21390
  ...scratch.fixedPoint !== void 0 ? { fixedPoint: scratch.fixedPoint } : {},
@@ -20904,7 +21905,7 @@ function chordLengthParam(samples) {
20904
21905
  ts[0] = 0;
20905
21906
  let total = 0;
20906
21907
  for (let i = 1; i < n; i++) {
20907
- total += dist3(samples[i - 1], samples[i]);
21908
+ total += dist2(samples[i - 1], samples[i]);
20908
21909
  ts[i] = total;
20909
21910
  }
20910
21911
  if (total === 0) return null;
@@ -20937,13 +21938,13 @@ function solveLS(samples, ts, a, e, tStart, tEnd) {
20937
21938
  let alpha1;
20938
21939
  let alpha2;
20939
21940
  if (Math.abs(det) < 1e-12) {
20940
- const chord = dist3(a, e);
21941
+ const chord = dist2(a, e);
20941
21942
  alpha1 = alpha2 = chord / 3;
20942
21943
  } else {
20943
21944
  alpha1 = (c22 * x1 - c12 * x2) / det;
20944
21945
  alpha2 = (c11 * x2 - c12 * x1) / det;
20945
21946
  if (alpha1 < 1e-6 || alpha2 < 1e-6) {
20946
- const chord = dist3(a, e);
21947
+ const chord = dist2(a, e);
20947
21948
  alpha1 = alpha2 = chord / 3;
20948
21949
  }
20949
21950
  }
@@ -20979,7 +21980,7 @@ function newtonRefineT(p, t, p0, p1, p2, p3) {
20979
21980
  function sub(a, b) {
20980
21981
  return { x: a.x - b.x, y: a.y - b.y };
20981
21982
  }
20982
- function dist3(a, b) {
21983
+ function dist2(a, b) {
20983
21984
  return Math.hypot(a.x - b.x, a.y - b.y);
20984
21985
  }
20985
21986
  function unit(v) {
@@ -21567,7 +22568,6 @@ function makeGetNodeAtPoint(pickEvery, nodeResolver) {
21567
22568
  return { id, kind, pose, data };
21568
22569
  };
21569
22570
  }
21570
- var DEFAULT_HANDLE_SIZE2 = 8;
21571
22571
  var IS_DEV = (() => {
21572
22572
  try {
21573
22573
  return Boolean(import.meta.env?.DEV);
@@ -21586,10 +22586,10 @@ function recordCoordTrace(entry) {
21586
22586
  coordTrace.push(entry);
21587
22587
  if (coordTrace.length > COORD_TRACE_LIMIT) coordTrace.shift();
21588
22588
  }
21589
- function mergeLayersWithDefaults(user) {
22589
+ function mergeLayersWithDefaults(user, targetScale = 1) {
21590
22590
  const defaults = {
21591
22591
  scene: { drawOne: defaultDrawOne },
21592
- selectionOverlay: { handles: { size: DEFAULT_HANDLE_SIZE2 } }
22592
+ selectionOverlay: { handles: { size: HANDLE_BASE_PX * targetScale } }
21593
22593
  };
21594
22594
  if (!user) return defaults;
21595
22595
  const result = { ...user };
@@ -21677,8 +22677,10 @@ function SceneCanvasInner(props, ref) {
21677
22677
  chromeVisibility,
21678
22678
  getActiveMode,
21679
22679
  getFocused: getFocusedProp,
22680
+ device,
21680
22681
  ...rest
21681
22682
  } = props;
22683
+ const deviceProfile = useDeviceProfile(device);
21682
22684
  const isSerialized = (s) => typeof s === "object" && s != null && s.version === 1 && Array.isArray(s.nodes);
21683
22685
  const [bakedScene] = useState(
21684
22686
  () => isSerialized(sceneInput) ? sceneFromJSON(sceneInput, {}) : null
@@ -21796,7 +22798,7 @@ function SceneCanvasInner(props, ref) {
21796
22798
  }
21797
22799
  }), []);
21798
22800
  const selectToolWithDefaults = useMemo(() => ({
21799
- handleHitRadius: DEFAULT_HANDLE_SIZE2,
22801
+ handleHitRadius: HANDLE_BASE_PX * deviceProfile.targetScale,
21800
22802
  // Shift-click belongs to the anchor selection while a path is being
21801
22803
  // anchor-edited; see `UseSelectToolOptions.extendClickLocked`.
21802
22804
  extendClickLocked: () => effectivePathEditingId() !== "",
@@ -21806,7 +22808,7 @@ function SceneCanvasInner(props, ref) {
21806
22808
  // pipeline (audit 3.4). The classifier is now `select.pick`, which
21807
22809
  // declares that capability itself — one rule, evaluated in one place.
21808
22810
  ...selectToolOpts
21809
- }), [selectToolOpts]);
22811
+ }), [selectToolOpts, deviceProfile.targetScale]);
21810
22812
  const selectionRef = useRef(selection);
21811
22813
  selectionRef.current = selection;
21812
22814
  const kindClassifier = useMemo(() => {
@@ -21938,7 +22940,7 @@ function SceneCanvasInner(props, ref) {
21938
22940
  const getMode = getActiveModeRef.current;
21939
22941
  if (!getMode) return true;
21940
22942
  const registry22 = toolsForEligibilityRef.current?.registry;
21941
- const caps = registry22?.[toolId]?.capabilities ?? [];
22943
+ const caps = registry22?.[toolId]?.eligibility.capabilities ?? [];
21942
22944
  if (caps.length === 0) return false;
21943
22945
  const allowed = getMode().allowedCapabilities;
21944
22946
  for (const c of caps) if (allowed.has(c)) return true;
@@ -21964,8 +22966,8 @@ function SceneCanvasInner(props, ref) {
21964
22966
  };
21965
22967
  }, [Boolean(onDoubleClick)]);
21966
22968
  const mergedLayers = useMemo(
21967
- () => mergeLayersWithDefaults(layers),
21968
- [layers]
22969
+ () => mergeLayersWithDefaults(layers, deviceProfile.targetScale),
22970
+ [layers, deviceProfile.targetScale]
21969
22971
  );
21970
22972
  const resolvedActions = useMemo(() => {
21971
22973
  if (selectToolOpts?.rotate !== false) return actions;
@@ -22024,6 +23026,8 @@ function SceneCanvasInner(props, ref) {
22024
23026
  chromeVisibilityRef.current = effectiveChromeVisibility;
22025
23027
  const getActiveModeRef = useRef(getActiveMode);
22026
23028
  getActiveModeRef.current = getActiveMode;
23029
+ const deviceProfileRef = useRef(deviceProfile);
23030
+ deviceProfileRef.current = deviceProfile;
22027
23031
  const resizablePredRef = useRef(selectToolOpts?.resize?.resizable);
22028
23032
  resizablePredRef.current = selectToolOpts?.resize?.resizable;
22029
23033
  const buildCurrentRuleCtx = useCallback(() => {
@@ -22045,7 +23049,8 @@ function SceneCanvasInner(props, ref) {
22045
23049
  mode: modeInfo.id,
22046
23050
  allowedCapabilities: modeInfo.allowedCapabilities,
22047
23051
  selectionResizable,
22048
- editingAnchors: effectivePathEditingId() !== ""
23052
+ editingAnchors: effectivePathEditingId() !== "",
23053
+ device: deviceProfileRef.current
22049
23054
  };
22050
23055
  }, [dispatcher, getHover]);
22051
23056
  const anchorEditingAllowed = useMemo(
@@ -22300,7 +23305,7 @@ function SceneCanvasInner(props, ref) {
22300
23305
  testHookRef.current?._markReady();
22301
23306
  }
22302
23307
  }, []);
22303
- return /* @__PURE__ */ jsx(DepRegistryProviderIfRoot, { children: /* @__PURE__ */ jsx(PointerProviderIfRoot, { children: /* @__PURE__ */ jsxs(ActionsProviderIfRoot, { children: [
23308
+ return /* @__PURE__ */ jsx(DeviceProfileProvider, { value: device, children: /* @__PURE__ */ jsx(DepRegistryProviderIfRoot, { children: /* @__PURE__ */ jsx(PointerProviderIfRoot, { children: /* @__PURE__ */ jsxs(ActionsProviderIfRoot, { children: [
22304
23309
  canvas,
22305
23310
  /* @__PURE__ */ jsx(PointerPublisher, { canvasRef: internalCanvasRef, viewRef: currentViewRef }),
22306
23311
  /* @__PURE__ */ jsx(
@@ -22363,7 +23368,7 @@ function SceneCanvasInner(props, ref) {
22363
23368
  }
22364
23369
  ),
22365
23370
  children
22366
- ] }) }) });
23371
+ ] }) }) }) });
22367
23372
  }
22368
23373
  function ToolKeybindingsMounter({
22369
23374
  internalTools,
@@ -22409,10 +23414,6 @@ function GestureDispatcherMounter({
22409
23414
  for (const tool of tools.ambient) m.set(tool.id, tool);
22410
23415
  return m;
22411
23416
  }, [tools.registry, tools.ambient]);
22412
- const ambientToolIds = useMemo(
22413
- () => tools.ambient.map((t) => t.id),
22414
- [tools.ambient]
22415
- );
22416
23417
  const boundsOfRef = useRef(boundsOf2);
22417
23418
  boundsOfRef.current = boundsOf2;
22418
23419
  const pickEveryRef = useRef(pickEvery);
@@ -22524,9 +23525,14 @@ function GestureDispatcherMounter({
22524
23525
  const worldPoint = clientToWorld2(screenPoint.x, screenPoint.y);
22525
23526
  const extra = canvasApiRef?.current?.hitTestExtras?.(worldPoint.x, worldPoint.y);
22526
23527
  if (extra) {
23528
+ const claim = extra.hit;
22527
23529
  return {
22528
23530
  kind: `layer:${extra.layerId}`,
22529
- ...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 } : {}
22530
23536
  };
22531
23537
  }
22532
23538
  return affordanceAt ? affordanceAt(worldPoint) : null;
@@ -22546,7 +23552,6 @@ function GestureDispatcherMounter({
22546
23552
  canvasRef,
22547
23553
  actions: registry22,
22548
23554
  toolsById,
22549
- ambientToolIds,
22550
23555
  enabled,
22551
23556
  keyboard,
22552
23557
  affordanceAt: wrappedAffordanceAt,
@@ -22777,7 +23782,7 @@ function renderSceneToCanvas(args) {
22777
23782
  entry.dpr = dpr;
22778
23783
  }
22779
23784
  const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands);
22780
- entry.renderer.render(commands);
23785
+ entry.renderer.render(commands, viewToMat3(view));
22781
23786
  }
22782
23787
  function SceneViewCanvasInner(props) {
22783
23788
  const { scene, view, width, height, drawOne, extraCommands, className, canvasRef } = props;
@@ -23151,6 +24156,6 @@ function oklchToOklab(L, C, h) {
23151
24156
  return [L, C * Math.cos(h), C * Math.sin(h)];
23152
24157
  }
23153
24158
 
23154
- export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleTool, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
23155
- //# sourceMappingURL=chunk-5XDFILHZ.js.map
23156
- //# sourceMappingURL=chunk-5XDFILHZ.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