@weasel-js/labkit 0.8.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +11 -3
  2. package/dist/_dts/{DrawCommand-DACv9lhe.d.ts → DrawCommand-CitWPxMY.d.ts} +80 -7
  3. package/dist/_dts/{useExperimentState-eSvpwV_P.d.ts → useExperimentState-CJn2hHzd.d.ts} +6 -4
  4. package/dist/{chunk-SSMHPMDV.js → chunk-53XSBIUK.js} +12 -11
  5. package/dist/chunk-53XSBIUK.js.map +1 -0
  6. package/dist/{chunk-OHGEOTCV.js → chunk-EXBV7A6X.js} +3 -3
  7. package/dist/{chunk-OHGEOTCV.js.map → chunk-EXBV7A6X.js.map} +1 -1
  8. package/dist/{chunk-6V7ROR7G.js → chunk-ISEK5LXT.js} +1931 -904
  9. package/dist/chunk-ISEK5LXT.js.map +1 -0
  10. package/dist/chunk-NRD3TDNQ.js +5946 -0
  11. package/dist/chunk-NRD3TDNQ.js.map +1 -0
  12. package/dist/index.d.ts +37 -13
  13. package/dist/index.js +776 -27
  14. package/dist/index.js.map +1 -1
  15. package/dist/passthrough/weasel-canvas.d.ts +29 -6
  16. package/dist/passthrough/weasel-canvas.js +1 -1
  17. package/dist/passthrough/weasel-ui.d.ts +169 -79
  18. package/dist/passthrough/weasel-ui.js +2 -2
  19. package/dist/state/index.d.ts +2 -2
  20. package/dist/state/index.js +2 -2
  21. package/dist/styles.css +228 -287
  22. package/dist/ui/layers/index.js +3 -3
  23. package/package.json +4 -6
  24. package/src/canvas/CanvasStack.less +1 -1
  25. package/src/controls/ControlPanel.less +12 -12
  26. package/src/dragdrop/DragGhost.less +2 -2
  27. package/src/dragdrop/Palette.less +6 -6
  28. package/src/index.ts +2 -0
  29. package/src/lab/Lab.test.tsx +20 -15
  30. package/src/lab/Lab.tsx +33 -32
  31. package/src/lab/LabContext.ts +3 -3
  32. package/src/lab/LabShell.less +14 -14
  33. package/src/lab/LabShell.test.tsx +7 -20
  34. package/src/lab/LabShell.tsx +22 -13
  35. package/src/lab/WorkspaceGrid.less +1 -1
  36. package/src/lab/WorkspaceGrid.stories.tsx +4 -4
  37. package/src/lab/index.ts +1 -1
  38. package/src/lab/useSystemMode.ts +30 -0
  39. package/src/layers/LayerList.less +9 -9
  40. package/src/passthrough/weasel-ui.ts +4 -0
  41. package/src/primitives/FpsMeter.less +5 -5
  42. package/src/primitives/ScaleIndicator.less +7 -7
  43. package/src/primitives/Sidebar.less +12 -12
  44. package/src/primitives/StatusBar.less +10 -10
  45. package/src/primitives/Toolbar.less +16 -16
  46. package/src/state/store.test.ts +17 -9
  47. package/src/state/store.ts +13 -10
  48. package/src/state/types.ts +5 -2
  49. package/src/theme/Interstellar.stories.less +20 -20
  50. package/src/theme/Interstellar.stories.tsx +44 -59
  51. package/src/theme/base.less +26 -26
  52. package/src/theme/interstellar.test.ts +30 -0
  53. package/src/theme/interstellar.tokens.json +57 -0
  54. package/src/theme/interstellar.ts +9 -0
  55. package/src/ui/layers/LayerStack.less +21 -21
  56. package/src/ui/properties/CurveField.less +5 -5
  57. package/src/ui/properties/EffectCard.tsx +2 -2
  58. package/src/ui/properties/PropertyGroup.less +6 -6
  59. package/src/ui/properties/PropertyPanel.less +70 -70
  60. package/src/ui/properties/PropertyPanel.stories.tsx +0 -1
  61. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +1 -1
  62. package/src/ui/properties/storyLayouts.tsx +1 -1
  63. package/src/workspace/Workspace.less +14 -14
  64. package/src/workspace/Workspace.stories.tsx +2 -2
  65. package/src/workspace/Workspace.test.tsx +2 -2
  66. package/dist/chunk-6V7ROR7G.js.map +0 -1
  67. package/dist/chunk-FBZCQXI2.js +0 -6037
  68. package/dist/chunk-FBZCQXI2.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/theme/interstellar.less +0 -34
  73. package/src/theme/light.less +0 -14
  74. package/src/theme/tokens.less +0 -85
@@ -16,7 +16,7 @@ var GESTURE_DESCRIPTORS = [
16
16
  { name: "pointerDown", hasTarget: true },
17
17
  { name: "dblTap", hasTarget: true },
18
18
  { name: "drag", hasTarget: true },
19
- { name: "wheel", hasTarget: false, arg: { name: "direction", values: ["up", "down", "*"], default: "*" } },
19
+ { name: "wheel", hasTarget: true, arg: { name: "direction", values: ["up", "down", "*"], default: "*" } },
20
20
  { name: "keyDown", hasTarget: false, arg: { name: "key", values: "free" } },
21
21
  { name: "keyUp", hasTarget: false, arg: { name: "key", values: "free" } },
22
22
  { name: "keyHeld", hasTarget: false, arg: { name: "key", values: "free" } },
@@ -388,44 +388,65 @@ function checkKey(req, held) {
388
388
  if (req === "forbidden") return !held;
389
389
  return true;
390
390
  }
391
- function matchKey(eventKey, specKey) {
391
+ function matchKey(eventKey, specKey2) {
392
392
  const lower = eventKey.toLowerCase();
393
- if (Array.isArray(specKey)) {
394
- return specKey.some((k) => k.toLowerCase() === lower);
393
+ if (Array.isArray(specKey2)) {
394
+ return specKey2.some((k) => k.toLowerCase() === lower);
395
395
  }
396
- return specKey.toLowerCase() === lower;
396
+ return specKey2.toLowerCase() === lower;
397
397
  }
398
398
  function affordanceKindOf(target) {
399
399
  if (typeof target !== "object" || target === null) return void 0;
400
400
  const k = target.kind;
401
401
  return typeof k === "string" ? k : void 0;
402
402
  }
403
- function matchTarget(target, specTarget, bodyTarget, bodyKind) {
404
- if (specTarget === void 0) return true;
405
- if (typeof specTarget === "object" && specTarget !== null && "kindOf" in specTarget && typeof specTarget.kindOf === "function") {
406
- return specTarget.kindOf(target, bodyTarget);
403
+ function hasPrefix(s, prefix) {
404
+ return s.startsWith(prefix);
405
+ }
406
+ function parseTargetSpec(spec) {
407
+ if (typeof spec === "object") {
408
+ return spec !== null && typeof spec.kindOf === "function" ? { form: "predicate", kindOf: spec.kindOf } : null;
409
+ }
410
+ if (typeof spec !== "string") return null;
411
+ if (spec === "empty" || spec === "selected-body" || spec === "unselected-body") {
412
+ return { form: "body", body: spec };
407
413
  }
408
- if (typeof specTarget !== "string") return false;
409
- if (specTarget === "empty" || specTarget === "selected-body" || specTarget === "unselected-body") {
410
- if (bodyTarget === void 0) return false;
411
- return bodyTarget === specTarget;
414
+ if (hasPrefix(spec, "affordance:")) {
415
+ return { form: "affordance", kind: spec.slice("affordance:".length) };
412
416
  }
413
- if (specTarget.startsWith("affordance:")) {
414
- const wanted = specTarget.slice("affordance:".length);
415
- return affordanceKindOf(target) === wanted;
417
+ if (hasPrefix(spec, "kind:")) {
418
+ const rest = spec.slice("kind:".length);
419
+ const requireSelected = rest.endsWith(":selected");
420
+ return {
421
+ form: "kind",
422
+ kind: requireSelected ? rest.slice(0, -":selected".length) : rest,
423
+ requireSelected
424
+ };
416
425
  }
417
- if (specTarget.startsWith("kind:")) {
418
- if (bodyKind === void 0) return false;
419
- let wanted = specTarget.slice("kind:".length);
420
- let requireSelected = false;
421
- if (wanted.endsWith(":selected")) {
422
- wanted = wanted.slice(0, -":selected".length);
423
- requireSelected = true;
426
+ return null;
427
+ }
428
+ function matchTarget(target, specTarget, bodyTarget, bodyKind) {
429
+ if (specTarget === void 0) return true;
430
+ const form = parseTargetSpec(specTarget);
431
+ if (form === null) return false;
432
+ switch (form.form) {
433
+ // Receives the raw target (affordance hit for drag, e.target otherwise)
434
+ // plus the bodyTarget classification.
435
+ case "predicate":
436
+ return form.kindOf(target, bodyTarget);
437
+ // Resolved from the `classifyTarget` result packed into the event's
438
+ // `bodyTarget`. Unwired classifyTarget → absent → no match.
439
+ case "body":
440
+ return bodyTarget !== void 0 && bodyTarget === form.body;
441
+ case "affordance":
442
+ return affordanceKindOf(target) === form.kind;
443
+ case "kind":
444
+ if (bodyKind === void 0 || bodyKind !== form.kind) return false;
445
+ return form.requireSelected ? bodyTarget === "selected-body" : true;
446
+ default: {
447
+ return false;
424
448
  }
425
- if (bodyKind !== wanted) return false;
426
- return requireSelected ? bodyTarget === "selected-body" : true;
427
449
  }
428
- return false;
429
450
  }
430
451
  function normalizePhase(spec) {
431
452
  if (typeof spec === "string") {
@@ -488,7 +509,7 @@ function matchSpec(e, spec, isMac, phaseCtx) {
488
509
  const direction = spec.direction ?? "*";
489
510
  if (direction === "up" && !(e.deltaY < 0)) return false;
490
511
  if (direction === "down" && !(e.deltaY > 0)) return false;
491
- return true;
512
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
492
513
  }
493
514
  case "click": {
494
515
  if (e.kind !== "click") return false;
@@ -498,12 +519,12 @@ function matchSpec(e, spec, isMac, phaseCtx) {
498
519
  case "doubleClick": {
499
520
  if (e.kind !== "doubleclick") return false;
500
521
  if (!matchModifiers(e, spec.mods, isMac)) return false;
501
- return matchTarget(e.target, spec.target, e.bodyTarget, e.bodyKind);
522
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
502
523
  }
503
524
  case "contextMenu": {
504
525
  if (e.kind !== "contextmenu") return false;
505
526
  if (!matchModifiers(e, spec.mods, isMac)) return false;
506
- return matchTarget(e.target, spec.target, e.bodyTarget, e.bodyKind);
527
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
507
528
  }
508
529
  case "longPress": {
509
530
  if (e.kind !== "longpress") return false;
@@ -547,8 +568,6 @@ function matchSpec(e, spec, isMac, phaseCtx) {
547
568
  }
548
569
  }
549
570
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
550
-
551
- // ../core/dist/chunk-6DAUIMTO.js
552
571
  var routing_exports = {};
553
572
  __export(routing_exports, {
554
573
  GESTURE_DESCRIPTORS: () => GESTURE_DESCRIPTORS,
@@ -597,7 +616,11 @@ function defineTool(def) {
597
616
  };
598
617
  return {
599
618
  id: def.id,
600
- capabilities: def.capabilities,
619
+ eligibility: {
620
+ focus: true,
621
+ capabilities: def.capabilities,
622
+ ...def.hotkey ? { offhand: def.hotkey } : {}
623
+ },
601
624
  actions: def.actions,
602
625
  def,
603
626
  presentation: def.presentation,
@@ -683,6 +706,255 @@ function argOf(spec, fallback) {
683
706
  if ("direction" in spec) return spec.direction ?? fallback;
684
707
  return fallback;
685
708
  }
709
+ var DepRegistryContext = createContext(null);
710
+ function DepRegistryProvider({ children }) {
711
+ const sourcesRef = useRef(/* @__PURE__ */ new Map());
712
+ const registry4 = useMemo(() => ({
713
+ register: (name, source) => {
714
+ sourcesRef.current.set(name, source);
715
+ return () => {
716
+ sourcesRef.current.delete(name);
717
+ };
718
+ },
719
+ get: (name) => sourcesRef.current.get(name)?.()
720
+ }), []);
721
+ return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry4, children });
722
+ }
723
+ function useDepRegistry() {
724
+ const r = useContext(DepRegistryContext);
725
+ if (r === null) {
726
+ throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
727
+ }
728
+ return r;
729
+ }
730
+ function useOptionalDepRegistry() {
731
+ return useContext(DepRegistryContext);
732
+ }
733
+ function useDepSource(name, source) {
734
+ const registry4 = useDepRegistry();
735
+ const sourceRef = useRef(source);
736
+ sourceRef.current = source;
737
+ useEffect(() => {
738
+ return registry4.register(name, () => sourceRef.current());
739
+ }, [name, registry4]);
740
+ }
741
+ var DEV = (() => {
742
+ try {
743
+ return Boolean(import.meta.env?.DEV);
744
+ } catch {
745
+ return false;
746
+ }
747
+ })();
748
+ function buildDepsFromRequires(action, depRegistry) {
749
+ const requires = action.requires ?? [];
750
+ const entries = requires.map((name) => [name, depRegistry.get(name)]);
751
+ const deps = Object.fromEntries(entries);
752
+ if (DEV) {
753
+ const declared = new Set(requires);
754
+ return new Proxy(deps, {
755
+ get(target, prop, receiver) {
756
+ if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
757
+ 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.`;
758
+ console.warn(message);
759
+ throw new Error(message);
760
+ }
761
+ return Reflect.get(target, prop, receiver);
762
+ }
763
+ });
764
+ }
765
+ return deps;
766
+ }
767
+ function actionBindings(action) {
768
+ const gs = action.defaultBinding;
769
+ if (!gs) return [];
770
+ const raw = Array.isArray(gs) ? gs : [gs];
771
+ return raw.map((entry) => {
772
+ const isBoundObj = !("kind" in entry);
773
+ const spec = isBoundObj ? entry.spec : entry;
774
+ const opts = isBoundObj ? entry.opts : void 0;
775
+ return { spec, actionId: action.id, ...opts !== void 0 ? { opts } : {} };
776
+ });
777
+ }
778
+ var ActionDisabledReason = {
779
+ SelectionRequired: "selection-required",
780
+ NotApplicable: "not-applicable",
781
+ /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
782
+ PredicateThrew: "predicate-threw"
783
+ };
784
+ var enabledSlowWarned = /* @__PURE__ */ new Set();
785
+ var enabledThrewWarned = /* @__PURE__ */ new Set();
786
+ var ENABLED_BUDGET_MS = 4;
787
+ function evaluateEnabled(action, deps) {
788
+ if (!action.enabled) return { enabled: true };
789
+ const isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
790
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
791
+ const start = isDev ? now() : 0;
792
+ try {
793
+ const result = action.enabled(deps);
794
+ if (isDev) {
795
+ const elapsed = now() - start;
796
+ if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
797
+ enabledSlowWarned.add(action.id);
798
+ console.warn(
799
+ `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.`
800
+ );
801
+ }
802
+ }
803
+ if (result === true) return { enabled: true };
804
+ return { enabled: false, reason: result };
805
+ } catch (e) {
806
+ if (isDev && !enabledThrewWarned.has(action.id)) {
807
+ enabledThrewWarned.add(action.id);
808
+ console.warn(
809
+ `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
810
+ e
811
+ );
812
+ }
813
+ return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
814
+ }
815
+ }
816
+ function validateActionId(id) {
817
+ if (id.length === 0) {
818
+ throw new Error(`weasel: action id may not be empty`);
819
+ }
820
+ if (RESERVED_ID_PREFIXES.has(id[0])) {
821
+ throw new Error(
822
+ `weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
823
+ );
824
+ }
825
+ if (RESERVED_ID_NAMES.has(id)) {
826
+ throw new Error(
827
+ `weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
828
+ );
829
+ }
830
+ }
831
+ function validateActionDefaultBinding(action) {
832
+ const gs = action.defaultBinding;
833
+ if (!gs) return;
834
+ const entries = Array.isArray(gs) ? gs : [gs];
835
+ for (const entry of entries) {
836
+ const spec = "kind" in entry ? entry : entry.spec;
837
+ const phase = spec.phase;
838
+ if (phase === void 0) continue;
839
+ const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
840
+ for (const atom of atoms) {
841
+ if (atom.channel === "&") {
842
+ throw new Error(
843
+ `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.`
844
+ );
845
+ }
846
+ }
847
+ }
848
+ }
849
+ var ActionsContext = createContext(null);
850
+ function ActionsProvider({ children }) {
851
+ const actionsRef = useRef(/* @__PURE__ */ new Map());
852
+ const versionRef = useRef(0);
853
+ const cachedRef = useRef([]);
854
+ const cachedVerRef = useRef(-1);
855
+ const listenersRef = useRef(/* @__PURE__ */ new Set());
856
+ const depReg = useOptionalDepRegistry();
857
+ const depRegRef = useRef(depReg);
858
+ depRegRef.current = depReg;
859
+ const wiredDepRegRef = useRef(null);
860
+ const dispatcherRef = useRef(null);
861
+ const registry4 = useMemo(() => {
862
+ const snapshot = () => {
863
+ const v = versionRef.current;
864
+ if (cachedVerRef.current === v) return cachedRef.current;
865
+ const out = Object.freeze(Array.from(actionsRef.current.values()));
866
+ cachedRef.current = out;
867
+ cachedVerRef.current = v;
868
+ return out;
869
+ };
870
+ const notify2 = () => {
871
+ for (const l of listenersRef.current) {
872
+ try {
873
+ l();
874
+ } catch (err) {
875
+ console.error("weasel ActionsRegistry: subscriber threw", err);
876
+ }
877
+ }
878
+ };
879
+ return {
880
+ register: (action) => {
881
+ validateActionId(action.id);
882
+ validateActionDefaultBinding(action);
883
+ actionsRef.current.set(action.id, action);
884
+ versionRef.current++;
885
+ notify2();
886
+ return () => {
887
+ const cur = actionsRef.current.get(action.id);
888
+ if (cur === action) {
889
+ actionsRef.current.delete(action.id);
890
+ versionRef.current++;
891
+ notify2();
892
+ }
893
+ };
894
+ },
895
+ unregister: (id) => {
896
+ if (actionsRef.current.delete(id)) {
897
+ versionRef.current++;
898
+ notify2();
899
+ }
900
+ },
901
+ list: () => snapshot(),
902
+ trigger: (id, params) => {
903
+ const a = actionsRef.current.get(id);
904
+ if (!a) return false;
905
+ try {
906
+ if (a.invoker && a.invoker.timing === "immediate") {
907
+ const r = wiredDepRegRef.current ?? depRegRef.current;
908
+ const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
909
+ selection: r.get("selection"),
910
+ scene: r.get("scene"),
911
+ history: r.get("history"),
912
+ view: r.get("view"),
913
+ pointer: r.get("pointer"),
914
+ activeTool: r.get("activeTool"),
915
+ booleansAdapter: r.get("booleansAdapter")
916
+ };
917
+ a.invoker.run(deps, params);
918
+ }
919
+ } catch (err) {
920
+ console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
921
+ }
922
+ return true;
923
+ },
924
+ subscribe: (listener) => {
925
+ listenersRef.current.add(listener);
926
+ return () => {
927
+ listenersRef.current.delete(listener);
928
+ };
929
+ },
930
+ setDispatcher: (d) => {
931
+ dispatcherRef.current = d;
932
+ },
933
+ setDepRegistry: (r) => {
934
+ wiredDepRegRef.current = r;
935
+ },
936
+ begin: (id, params) => {
937
+ const disp = dispatcherRef.current;
938
+ if (!disp) return null;
939
+ const r = wiredDepRegRef.current ?? depRegRef.current;
940
+ const deps = r ? {
941
+ selection: r.get("selection"),
942
+ scene: r.get("scene"),
943
+ history: r.get("history"),
944
+ view: r.get("view"),
945
+ pointer: r.get("pointer"),
946
+ activeTool: r.get("activeTool"),
947
+ booleansAdapter: r.get("booleansAdapter")
948
+ } : {};
949
+ return disp.beginUiOngoing(id, deps, params);
950
+ }
951
+ };
952
+ }, []);
953
+ return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry4, children });
954
+ }
955
+ function useActionsRegistry() {
956
+ return useContext(ActionsContext);
957
+ }
686
958
  function findConflicts(tools) {
687
959
  const entries = buildRouteRegistry(tools);
688
960
  const groups = /* @__PURE__ */ new Map();
@@ -724,9 +996,15 @@ function predicateId(pred) {
724
996
  }
725
997
  return id;
726
998
  }
999
+ function actionAsTool(action) {
1000
+ return { id: action.id, eligibility: {}, bindings: actionBindings(action) };
1001
+ }
727
1002
  function findScopedConflicts(scopes) {
728
1003
  const registry4 = Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry);
729
- const ambient = scopes.ambient ?? [];
1004
+ const actions = scopes.actions ?? [];
1005
+ const ambientActions = actions.filter((a) => a.scope !== "hotkey").map(actionAsTool);
1006
+ const hotkeyActions = actions.filter((a) => a.scope === "hotkey").map(actionAsTool);
1007
+ const ambient = [...scopes.ambient ?? [], ...ambientActions];
730
1008
  const out = [];
731
1009
  const seen = /* @__PURE__ */ new Set();
732
1010
  const add = (conflicts) => {
@@ -737,12 +1015,13 @@ function findScopedConflicts(scopes) {
737
1015
  out.push(c);
738
1016
  }
739
1017
  };
740
- for (const tool of [...registry4, ...ambient]) add(findConflicts([tool]));
1018
+ for (const tool of [...registry4, ...scopes.ambient ?? []]) add(findConflicts([tool]));
741
1019
  if (ambient.length > 1) add(findConflicts(ambient));
742
- const hotkeyTools = registry4.filter(
743
- (t) => t.def?.hotkey !== void 0
744
- );
745
- if (hotkeyTools.length > 1) add(findConflicts(hotkeyTools));
1020
+ const hotkey = [
1021
+ ...registry4.filter((t) => t.def?.hotkey !== void 0),
1022
+ ...hotkeyActions
1023
+ ];
1024
+ if (hotkey.length > 1) add(findConflicts(hotkey));
746
1025
  return out;
747
1026
  }
748
1027
  function formatConflict(conflict) {
@@ -882,7 +1161,7 @@ function transformCoords(coords, m) {
882
1161
  return out;
883
1162
  }
884
1163
 
885
- // ../core/dist/chunk-GLZLSPGJ.js
1164
+ // ../core/dist/chunk-6SHZHXXR.js
886
1165
  function resolveUnit(v, unitSystem) {
887
1166
  if (typeof v === "number") return v;
888
1167
  if (!unitSystem) {
@@ -1453,10 +1732,32 @@ var RECT_POSE_DESCRIPTOR = {
1453
1732
  translate: RECT_POSE_DESCRIPTOR.translate,
1454
1733
  intersectsRect: RECT_POSE_DESCRIPTOR.intersectsRect,
1455
1734
  lerp: RECT_POSE_DESCRIPTOR.lerp});
1456
- var pathPoseDescriptor = {
1457
- getBounds: (path) => boundsOfPath(path),
1458
- remapBounds: (path, src, dst) => {
1459
- const sx = src.width === 0 ? 0 : dst.width / src.width;
1735
+ function remapRotatedLeaf(pose, src, dst) {
1736
+ const sx = src.width === 0 ? 1 : dst.width / src.width;
1737
+ const sy = src.height === 0 ? 1 : dst.height / src.height;
1738
+ const theta = pose.rotation ?? 0;
1739
+ const cos = Math.cos(theta);
1740
+ const sin = Math.sin(theta);
1741
+ const ux = sx * cos, uy = sy * sin;
1742
+ const vx = -sx * sin, vy = sy * cos;
1743
+ const width = pose.width * Math.hypot(ux, uy);
1744
+ const height = pose.height * Math.hypot(vx, vy);
1745
+ const rotation = Math.atan2(uy, ux);
1746
+ const cxNew = dst.x + (pose.x + pose.width / 2 - src.x) * sx;
1747
+ const cyNew = dst.y + (pose.y + pose.height / 2 - src.y) * sy;
1748
+ return {
1749
+ ...pose,
1750
+ x: cxNew - width / 2,
1751
+ y: cyNew - height / 2,
1752
+ width,
1753
+ height,
1754
+ rotation
1755
+ };
1756
+ }
1757
+ var pathPoseDescriptor = {
1758
+ getBounds: (path) => boundsOfPath(path),
1759
+ remapBounds: (path, src, dst) => {
1760
+ const sx = src.width === 0 ? 0 : dst.width / src.width;
1460
1761
  const sy = src.height === 0 ? 0 : dst.height / src.height;
1461
1762
  if (path.kind === "rect") {
1462
1763
  return {
@@ -1549,14 +1850,29 @@ var RECT_ORIGIN_PROJECTION = {
1549
1850
  getOrigin: (p) => ({ x: p.x, y: p.y }),
1550
1851
  translate: (p, dx, dy) => ({ ...p, x: p.x + dx, y: p.y + dy })
1551
1852
  };
1552
- function meanScale(s) {
1553
- return Math.sqrt(s.x * s.y);
1853
+ var MIN_SCALE = 1e-9;
1854
+ function pxExtent(px, scale2) {
1855
+ return {
1856
+ x: px / Math.max(MIN_SCALE, Math.abs(scale2.x)),
1857
+ y: px / Math.max(MIN_SCALE, Math.abs(scale2.y))
1858
+ };
1859
+ }
1860
+ function scaleDelta(dx, dy, scale2) {
1861
+ return { x: dx * scale2.x, y: dy * scale2.y };
1862
+ }
1863
+ function withinPxBox(dx, dy, px, scale2) {
1864
+ const s = scaleDelta(dx, dy, scale2);
1865
+ return Math.abs(s.x) <= px && Math.abs(s.y) <= px;
1866
+ }
1867
+ function withinPxRadius(dx, dy, px, scale2) {
1868
+ const s = scaleDelta(dx, dy, scale2);
1869
+ return s.x * s.x + s.y * s.y <= px * px;
1554
1870
  }
1555
1871
 
1556
1872
  // ../core/dist/chunk-GVCNT7UH.js
1557
1873
  var FACTORIES = /* @__PURE__ */ new Map();
1558
- function registerOpFactory(name, build) {
1559
- FACTORIES.set(name, build);
1874
+ function registerOpFactory(name, build2) {
1875
+ FACTORIES.set(name, build2);
1560
1876
  }
1561
1877
  function rebuildOp(name, args) {
1562
1878
  const f = FACTORIES.get(name);
@@ -1593,7 +1909,7 @@ function createDeleteOp(args) {
1593
1909
  }
1594
1910
  registerOpFactory("delete", (args) => createDeleteOp(args));
1595
1911
 
1596
- // ../core/dist/chunk-BPRHGTLQ.js
1912
+ // ../core/dist/chunk-UB6A6L77.js
1597
1913
  function createTransformOp(args) {
1598
1914
  const { id, from, to, label, coalesceKey = `transform:${id}` } = args;
1599
1915
  return {
@@ -1674,7 +1990,7 @@ function snap(strategy, opts = {}) {
1674
1990
  };
1675
1991
  }
1676
1992
 
1677
- // ../core/dist/chunk-CMBE45MT.js
1993
+ // ../core/dist/chunk-Z3KNFTTS.js
1678
1994
  function lockAspectWithModifier(opts = {}) {
1679
1995
  const { key = "shift" } = opts;
1680
1996
  return {
@@ -1870,11 +2186,163 @@ function materializePaste(adapter, snapshot, opts) {
1870
2186
  return { ops, newIds, created };
1871
2187
  }
1872
2188
 
1873
- // ../core/dist/chunk-Y52N27PF.js
2189
+ // ../core/dist/chunk-IO5Z75X4.js
2190
+ var counter = 0;
1874
2191
  var registry = /* @__PURE__ */ new Map();
1875
2192
  function getTexture(id) {
1876
2193
  return registry.get(id) ?? null;
1877
2194
  }
2195
+ function registerTexture(image) {
2196
+ const id = `tex_${++counter}`;
2197
+ registry.set(id, { source: image });
2198
+ return { id };
2199
+ }
2200
+ function tileSize(spec) {
2201
+ if (spec.size !== void 0) return spec.size;
2202
+ switch (spec.tile) {
2203
+ case "hatch":
2204
+ return 5;
2205
+ case "crosshatch":
2206
+ return 6;
2207
+ case "dots":
2208
+ return 6;
2209
+ case "chunks":
2210
+ return 48;
2211
+ default:
2212
+ return 8;
2213
+ }
2214
+ }
2215
+ function mulberry32(seed) {
2216
+ let s = seed | 0;
2217
+ return () => {
2218
+ s = s + 1831565813 | 0;
2219
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
2220
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2221
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2222
+ };
2223
+ }
2224
+ function tileGeometry(spec) {
2225
+ const s = tileSize(spec);
2226
+ const { color } = spec;
2227
+ switch (spec.tile) {
2228
+ case "hatch": {
2229
+ const width = spec.lineWidth ?? 1;
2230
+ return { size: s, shapes: forwardDiagonals(s, width, color) };
2231
+ }
2232
+ case "crosshatch": {
2233
+ const width = spec.lineWidth ?? 0.8;
2234
+ return {
2235
+ size: s,
2236
+ shapes: [...forwardDiagonals(s, width, color), ...backwardDiagonals(s, width, color)]
2237
+ };
2238
+ }
2239
+ case "dots": {
2240
+ const r = spec.radius ?? 1;
2241
+ return { size: s, shapes: [{ kind: "circle", cx: s / 2, cy: s / 2, r, color }] };
2242
+ }
2243
+ case "chunks": {
2244
+ const density = spec.density ?? 0.35;
2245
+ const chunkSize = spec.chunkSize ?? 3;
2246
+ const rand = mulberry32(spec.seed ?? 42);
2247
+ const count = Math.round(s * s * density / (chunkSize * chunkSize));
2248
+ const shapes = [];
2249
+ if (spec.bg !== void 0) {
2250
+ shapes.push({ kind: "rect", x: 0, y: 0, width: s, height: s, color: spec.bg });
2251
+ }
2252
+ for (let i = 0; i < count; i++) {
2253
+ const cx = rand() * s;
2254
+ const cy = rand() * s;
2255
+ const w = chunkSize * (0.5 + rand());
2256
+ const h = chunkSize * (0.5 + rand());
2257
+ const rotation = rand() * Math.PI;
2258
+ shapes.push({ kind: "ellipse", cx, cy, rx: w / 2, ry: h / 2, rotation, color });
2259
+ }
2260
+ return { size: s, shapes };
2261
+ }
2262
+ default:
2263
+ return { size: s, shapes: [] };
2264
+ }
2265
+ }
2266
+ function forwardDiagonals(s, width, color) {
2267
+ return [
2268
+ { kind: "line", x1: -1, y1: s + 1, x2: s + 1, y2: -1, width, color },
2269
+ { kind: "line", x1: -1, y1: 1, x2: 1, y2: -1, width, color },
2270
+ { kind: "line", x1: s - 1, y1: s + 1, x2: s + 1, y2: s - 1, width, color }
2271
+ ];
2272
+ }
2273
+ function backwardDiagonals(s, width, color) {
2274
+ return [
2275
+ { kind: "line", x1: -1, y1: -1, x2: s + 1, y2: s + 1, width, color },
2276
+ { kind: "line", x1: s - 1, y1: -1, x2: s + 1, y2: 1, width, color },
2277
+ { kind: "line", x1: -1, y1: s - 1, x2: 1, y2: s + 1, width, color }
2278
+ ];
2279
+ }
2280
+ function drawTileShapes(oc, geometry) {
2281
+ for (const shape of geometry.shapes) {
2282
+ if (shape.kind === "line") {
2283
+ oc.strokeStyle = shape.color;
2284
+ oc.lineWidth = shape.width;
2285
+ oc.beginPath();
2286
+ oc.moveTo(shape.x1, shape.y1);
2287
+ oc.lineTo(shape.x2, shape.y2);
2288
+ oc.stroke();
2289
+ continue;
2290
+ }
2291
+ oc.fillStyle = shape.color;
2292
+ if (shape.kind === "rect") {
2293
+ oc.fillRect(shape.x, shape.y, shape.width, shape.height);
2294
+ continue;
2295
+ }
2296
+ oc.beginPath();
2297
+ if (shape.kind === "circle") {
2298
+ oc.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);
2299
+ } else {
2300
+ oc.ellipse(shape.cx, shape.cy, shape.rx, shape.ry, shape.rotation, 0, Math.PI * 2);
2301
+ }
2302
+ oc.fill();
2303
+ }
2304
+ }
2305
+ function createTilePattern(opts) {
2306
+ let off;
2307
+ try {
2308
+ off = new OffscreenCanvas(opts.size, opts.size);
2309
+ } catch {
2310
+ if (typeof document === "undefined") return null;
2311
+ const c = document.createElement("canvas");
2312
+ c.width = opts.size;
2313
+ c.height = opts.size;
2314
+ off = c;
2315
+ }
2316
+ const oc = off.getContext("2d");
2317
+ if (!oc) return null;
2318
+ opts.draw(oc, opts.size);
2319
+ let bitmap;
2320
+ if (typeof off.transferToImageBitmap === "function") {
2321
+ bitmap = off.transferToImageBitmap();
2322
+ } else {
2323
+ return null;
2324
+ }
2325
+ return registerTexture(bitmap);
2326
+ }
2327
+ function build(spec) {
2328
+ const geometry = tileGeometry(spec);
2329
+ return createTilePattern({
2330
+ size: tileSize(spec),
2331
+ draw: (oc) => drawTileShapes(oc, geometry)
2332
+ });
2333
+ }
2334
+ function hatch(params) {
2335
+ return build({ tile: "hatch", ...params });
2336
+ }
2337
+ function crosshatch(params) {
2338
+ return build({ tile: "crosshatch", ...params });
2339
+ }
2340
+ function dots(params) {
2341
+ return build({ tile: "dots", ...params });
2342
+ }
2343
+ function chunks(params) {
2344
+ return build({ tile: "chunks", ...params });
2345
+ }
1878
2346
 
1879
2347
  // ../font/dist/index.js
1880
2348
  var subscribers = /* @__PURE__ */ new Set();
@@ -2012,8 +2480,8 @@ function alphaToSdf(alpha, width, height, radius, cutoff) {
2012
2480
  edt2d(gridInner, width, height, f, d, v, z);
2013
2481
  const out = new Uint8Array(n);
2014
2482
  for (let i = 0; i < n; i++) {
2015
- const dist4 = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]);
2016
- const byte = Math.round(255 - 255 * (dist4 / radius + cutoff));
2483
+ const dist3 = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]);
2484
+ const byte = Math.round(255 - 255 * (dist3 / radius + cutoff));
2017
2485
  out[i] = byte < 0 ? 0 : byte > 255 ? 255 : byte;
2018
2486
  }
2019
2487
  return out;
@@ -2219,17 +2687,17 @@ function dynamicPageTextureId(page) {
2219
2687
  return `weasel-dyn-sdf-page-${page}`;
2220
2688
  }
2221
2689
  var uploadedVersions = /* @__PURE__ */ new WeakMap();
2222
- function syncDynamicPageTexture(cache6, pageIndex) {
2690
+ function syncDynamicPageTexture(cache7, pageIndex) {
2223
2691
  const page = pages[pageIndex];
2224
2692
  if (!page) return false;
2225
2693
  const id = dynamicPageTextureId(pageIndex);
2226
- let seen = uploadedVersions.get(cache6);
2694
+ let seen = uploadedVersions.get(cache7);
2227
2695
  if (!seen) {
2228
2696
  seen = /* @__PURE__ */ new Map();
2229
- uploadedVersions.set(cache6, seen);
2697
+ uploadedVersions.set(cache7, seen);
2230
2698
  }
2231
- if (!cache6.has(id)) {
2232
- cache6.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
2699
+ if (!cache7.has(id)) {
2700
+ cache7.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
2233
2701
  seen.set(pageIndex, page.version);
2234
2702
  return true;
2235
2703
  }
@@ -2242,7 +2710,7 @@ function syncDynamicPageTexture(cache6, pageIndex) {
2242
2710
  const src = (patch.y + row) * PAGE_SIZE + patch.x;
2243
2711
  tight.set(page.data.subarray(src, src + patch.w), row * patch.w);
2244
2712
  }
2245
- cache6.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
2713
+ cache7.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
2246
2714
  }
2247
2715
  seen.set(pageIndex, page.version);
2248
2716
  return true;
@@ -2639,8 +3107,8 @@ function eligibleForMode(mode, toolTags) {
2639
3107
  }
2640
3108
  return false;
2641
3109
  }
2642
- function eligibleTool(reg, tool) {
2643
- return eligibleForMode(reg.current(), tool.capabilities ?? []);
3110
+ function eligibleContribution(reg, entry) {
3111
+ return eligibleForMode(reg.current(), entry.eligibility?.capabilities ?? []);
2644
3112
  }
2645
3113
 
2646
3114
  // ../history/dist/index.js
@@ -3009,7 +3477,7 @@ function serialToEntry(se, custom, logger) {
3009
3477
  };
3010
3478
  }
3011
3479
 
3012
- // ../core/dist/chunk-DLV5F4NY.js
3480
+ // ../core/dist/chunk-FVNWK7U3.js
3013
3481
  function worldToScreen(worldX, worldY, view) {
3014
3482
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
3015
3483
  }
@@ -3260,6 +3728,58 @@ var GRAD_FILL_UNIFORMS = [
3260
3728
  "u_gradAngle"
3261
3729
  ];
3262
3730
  var GRAD_FILL_ATTRIBUTES = ["a_position"];
3731
+ var PATTERN_VERT_SRC = (
3732
+ /* glsl */
3733
+ `#version 300 es
3734
+ in vec2 a_position;
3735
+ uniform mat3 u_proj;
3736
+ uniform mat3 u_model;
3737
+ uniform mat3 u_worldInv;
3738
+ out vec2 v_world;
3739
+ void main() {
3740
+ vec3 screen = u_model * vec3(a_position, 1.0);
3741
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
3742
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
3743
+ vec3 world = u_worldInv * vec3(screen.xy, 1.0);
3744
+ v_world = world.xy;
3745
+ }
3746
+ `
3747
+ );
3748
+ var PATTERN_FRAG_SRC = (
3749
+ /* glsl */
3750
+ `#version 300 es
3751
+ precision highp float;
3752
+ in vec2 v_world;
3753
+ uniform sampler2D u_sampler;
3754
+ uniform vec2 u_tileOrigin;
3755
+ uniform vec2 u_tileSize;
3756
+ uniform float u_opacity;
3757
+ uniform float u_alpha;
3758
+ uniform mat4 u_colorMatrix;
3759
+ uniform vec4 u_colorBias;
3760
+ out vec4 outColor;
3761
+ void main() {
3762
+ vec2 uv = (v_world - u_tileOrigin) / max(u_tileSize, vec2(0.0001));
3763
+ vec4 texel = texture(u_sampler, uv);
3764
+ vec4 mapped = clamp(u_colorMatrix * texel + u_colorBias, 0.0, 1.0);
3765
+ float a = mapped.a * u_opacity * u_alpha;
3766
+ outColor = vec4(mapped.rgb * a, a);
3767
+ }
3768
+ `
3769
+ );
3770
+ var PATTERN_FILL_UNIFORMS = [
3771
+ "u_proj",
3772
+ "u_model",
3773
+ "u_worldInv",
3774
+ "u_sampler",
3775
+ "u_tileOrigin",
3776
+ "u_tileSize",
3777
+ "u_opacity",
3778
+ "u_alpha",
3779
+ "u_colorMatrix",
3780
+ "u_colorBias"
3781
+ ];
3782
+ var PATTERN_FILL_ATTRIBUTES = ["a_position"];
3263
3783
  var GLMeshCache = class {
3264
3784
  constructor(gl, aPositionLoc) {
3265
3785
  this.gl = gl;
@@ -3382,17 +3902,20 @@ var GLTextureCache = class {
3382
3902
  has(id) {
3383
3903
  return this.map.has(id);
3384
3904
  }
3385
- upload(id, source) {
3905
+ /** Uploads once per id; `wrap` therefore applies on first upload only.
3906
+ * An id is either a pattern tile or a shader texture, never both. */
3907
+ upload(id, source, wrap = "clamp") {
3386
3908
  if (this.map.has(id)) return id;
3387
3909
  const gl = this.gl;
3388
3910
  const tex = gl.createTexture();
3389
3911
  if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
3912
+ const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
3390
3913
  gl.bindTexture(gl.TEXTURE_2D, tex);
3391
3914
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
3392
3915
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3393
3916
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3394
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3395
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3917
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
3918
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
3396
3919
  gl.bindTexture(gl.TEXTURE_2D, null);
3397
3920
  this.map.set(id, tex);
3398
3921
  return id;
@@ -3987,6 +4510,18 @@ function scale(m, sx, sy) {
3987
4510
  const s = create(sx, 0, 0, sy, 0, 0);
3988
4511
  return multiply2(m, s);
3989
4512
  }
4513
+ function invert(m) {
4514
+ const a = m[0], b = m[1];
4515
+ const c = m[3], d = m[4];
4516
+ const tx = m[6], ty = m[7];
4517
+ const det = a * d - b * c;
4518
+ if (det === 0 || !Number.isFinite(det)) return identity();
4519
+ const ia = d / det;
4520
+ const ib = -b / det;
4521
+ const ic = -c / det;
4522
+ const id = a / det;
4523
+ return create(ia, ib, ic, id, -(ia * tx + ic * ty), -(ib * tx + id * ty));
4524
+ }
3990
4525
  function apply(m, x, y) {
3991
4526
  const a = m[0], b = m[1];
3992
4527
  const c = m[3], d = m[4];
@@ -4014,6 +4549,7 @@ var mat3 = {
4014
4549
  multiply: multiply2,
4015
4550
  translate: translate2,
4016
4551
  scale,
4552
+ invert,
4017
4553
  apply,
4018
4554
  screenToClip
4019
4555
  };
@@ -5033,7 +5569,7 @@ function warnMissingGlyphOnce(family, cp) {
5033
5569
  `weasel layoutRuns: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
5034
5570
  );
5035
5571
  }
5036
- function layoutRuns(runs, opts, origin) {
5572
+ function layoutRuns(runs, opts) {
5037
5573
  const ctx = { groups: /* @__PURE__ */ new Map() };
5038
5574
  const entries = [];
5039
5575
  let prevCp;
@@ -5203,7 +5739,7 @@ function layoutRuns(runs, opts, origin) {
5203
5739
  }
5204
5740
  }
5205
5741
  const lineBoxes = [];
5206
- let penY = origin.y;
5742
+ let penY = 0;
5207
5743
  let maxLineWidth = 0;
5208
5744
  const finiteWidth = Number.isFinite(opts.maxWidth) ? opts.maxWidth : 0;
5209
5745
  for (const line of lines) {
@@ -5215,7 +5751,7 @@ function layoutRuns(runs, opts, origin) {
5215
5751
  const slack = finiteWidth - line.width;
5216
5752
  return opts.align === "center" ? slack / 2 : slack;
5217
5753
  })();
5218
- const lineX0 = origin.x + alignShift;
5754
+ const lineX0 = alignShift;
5219
5755
  const baselineSource = line.entries[0] ?? line.blank;
5220
5756
  const lineBaselineY = baselineSource ? penY + baselineSource.font.common.base * (baselineSource.fontSize / baselineSource.font.info.size) : penY;
5221
5757
  let penX = lineX0;
@@ -5289,7 +5825,7 @@ function layoutRuns(runs, opts, origin) {
5289
5825
  groups: [...ctx.groups.values()],
5290
5826
  decorations,
5291
5827
  lines: lineBoxes,
5292
- bounds: { width: maxLineWidth, height: penY - origin.y }
5828
+ bounds: { width: maxLineWidth, height: penY }
5293
5829
  };
5294
5830
  }
5295
5831
  var LAYOUT_CACHE_VARIANT_LIMIT = 8;
@@ -5302,10 +5838,10 @@ function outlineBucket(runs, min) {
5302
5838
  for (const size of sizes) if (size >= min) n++;
5303
5839
  return n;
5304
5840
  }
5305
- function variantKey2(runs, opts, origin) {
5306
- return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}|${origin.x}|${origin.y}`;
5841
+ function variantKey2(runs, opts) {
5842
+ return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}`;
5307
5843
  }
5308
- function cachedLayoutRuns(runs, opts, origin) {
5844
+ function cachedLayoutRuns(runs, opts) {
5309
5845
  const fonts = glyphGeneration();
5310
5846
  let entry = cache2.get(runs);
5311
5847
  if (entry === void 0) {
@@ -5315,10 +5851,10 @@ function cachedLayoutRuns(runs, opts, origin) {
5315
5851
  entry.generation = fonts;
5316
5852
  entry.byVariant.clear();
5317
5853
  }
5318
- const key = variantKey2(runs, opts, origin);
5854
+ const key = variantKey2(runs, opts);
5319
5855
  const hit = entry.byVariant.get(key);
5320
5856
  if (hit !== void 0) return hit;
5321
- const laid = layoutRuns(runs, opts, origin);
5857
+ const laid = layoutRuns(runs, opts);
5322
5858
  if (entry.byVariant.size >= LAYOUT_CACHE_VARIANT_LIMIT) entry.byVariant.clear();
5323
5859
  entry.byVariant.set(key, laid);
5324
5860
  return laid;
@@ -5626,25 +6162,264 @@ function outlineStrokeMesh(glyphKey, d, emWidth, stroke) {
5626
6162
  cache4.set(key, mesh);
5627
6163
  return mesh.indices.length === 0 ? null : mesh;
5628
6164
  }
5629
- function setColorMatrixUniforms(ctx, prog) {
5630
- const gl = ctx.gl;
5631
- const cm = ctx.state.colorMatrix;
5632
- const m4 = new Float32Array(16);
5633
- for (let row = 0; row < 4; row++) {
5634
- for (let col = 0; col < 4; col++) {
5635
- m4[col * 4 + row] = cm[row * 5 + col];
6165
+ var MAX_VERTICES_PER_BATCH = 32768;
6166
+ var FLOATS_PER_VERTEX = 6;
6167
+ var INITIAL_VERTICES = 256;
6168
+ var INITIAL_INDICES = 384;
6169
+ var SolidBatch = class {
6170
+ gl;
6171
+ vao;
6172
+ vbo;
6173
+ ibo;
6174
+ verts = new Float32Array(INITIAL_VERTICES * FLOATS_PER_VERTEX);
6175
+ idx = new Uint32Array(INITIAL_INDICES);
6176
+ nVerts = 0;
6177
+ nIdx = 0;
6178
+ /** What the GPU buffers are currently sized for. */
6179
+ vboVerts = 0;
6180
+ iboIdx = 0;
6181
+ constructor(gl, prog) {
6182
+ const aPos = prog.attribute("a_position");
6183
+ const aColor = prog.attribute("a_vertexColor");
6184
+ if (aPos === void 0 || aColor === void 0) {
6185
+ throw new Error("SolidBatch: vertex-color program is missing a_position / a_vertexColor");
6186
+ }
6187
+ const vao = gl.createVertexArray();
6188
+ const vbo = gl.createBuffer();
6189
+ const ibo = gl.createBuffer();
6190
+ if (!vao || !vbo || !ibo) throw new Error("SolidBatch: failed to create GL objects");
6191
+ this.gl = gl;
6192
+ this.vao = vao;
6193
+ this.vbo = vbo;
6194
+ this.ibo = ibo;
6195
+ const stride = FLOATS_PER_VERTEX * 4;
6196
+ gl.bindVertexArray(vao);
6197
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
6198
+ gl.enableVertexAttribArray(aPos);
6199
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, stride, 0);
6200
+ gl.enableVertexAttribArray(aColor);
6201
+ gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 8);
6202
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
6203
+ gl.bindVertexArray(null);
6204
+ this.growGpu(INITIAL_VERTICES, INITIAL_INDICES);
6205
+ }
6206
+ get length() {
6207
+ return this.nIdx;
6208
+ }
6209
+ /** Whether staging `vertices` more would put the run past the per-flush cap. */
6210
+ wouldOverflow(vertices) {
6211
+ return this.nVerts + vertices > MAX_VERTICES_PER_BATCH;
6212
+ }
6213
+ /**
6214
+ * Append one rect's four corners through `m`, all carrying `rgba` (straight
6215
+ * alpha). An affine maps a rect to a parallelogram, so two triangles still
6216
+ * cover it and the batch draws at `u_model` identity.
6217
+ */
6218
+ pushRect(x, y, w, h, m, r, g, b, a) {
6219
+ this.reserve(4, 6);
6220
+ const v = this.verts;
6221
+ let i = this.nVerts * FLOATS_PER_VERTEX;
6222
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
6223
+ const x1 = x + w;
6224
+ const y1 = y + h;
6225
+ const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
6226
+ const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
6227
+ const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
6228
+ const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
6229
+ v[i++] = ax;
6230
+ v[i++] = ay;
6231
+ v[i++] = r;
6232
+ v[i++] = g;
6233
+ v[i++] = b;
6234
+ v[i++] = a;
6235
+ v[i++] = bx;
6236
+ v[i++] = by;
6237
+ v[i++] = r;
6238
+ v[i++] = g;
6239
+ v[i++] = b;
6240
+ v[i++] = a;
6241
+ v[i++] = cx;
6242
+ v[i++] = cy;
6243
+ v[i++] = r;
6244
+ v[i++] = g;
6245
+ v[i++] = b;
6246
+ v[i++] = a;
6247
+ v[i++] = dx;
6248
+ v[i++] = dy;
6249
+ v[i++] = r;
6250
+ v[i++] = g;
6251
+ v[i++] = b;
6252
+ v[i++] = a;
6253
+ const base = this.nVerts;
6254
+ let j = this.nIdx;
6255
+ this.idx[j++] = base;
6256
+ this.idx[j++] = base + 1;
6257
+ this.idx[j++] = base + 2;
6258
+ this.idx[j++] = base;
6259
+ this.idx[j++] = base + 2;
6260
+ this.idx[j++] = base + 3;
6261
+ this.nVerts += 4;
6262
+ this.nIdx += 6;
6263
+ }
6264
+ /**
6265
+ * Append a tessellated mesh through `m`, all vertices carrying `rgba`. The
6266
+ * mesh's own indices are rebased onto the staged vertices, which is why the
6267
+ * index buffer is uploaded per flush rather than written once.
6268
+ */
6269
+ pushMesh(mesh, m, r, g, b, a) {
6270
+ const src = mesh.vertices;
6271
+ const srcIdx = mesh.indices;
6272
+ const n = src.length >> 1;
6273
+ this.reserve(n, srcIdx.length);
6274
+ const v = this.verts;
6275
+ let i = this.nVerts * FLOATS_PER_VERTEX;
6276
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
6277
+ for (let k = 0; k < n; k++) {
6278
+ const x = src[k * 2];
6279
+ const y = src[k * 2 + 1];
6280
+ v[i++] = ma * x + mc * y + mtx;
6281
+ v[i++] = mb * x + md * y + mty;
6282
+ v[i++] = r;
6283
+ v[i++] = g;
6284
+ v[i++] = b;
6285
+ v[i++] = a;
6286
+ }
6287
+ const base = this.nVerts;
6288
+ const out = this.idx;
6289
+ let j = this.nIdx;
6290
+ for (let k = 0; k < srcIdx.length; k++) out[j++] = base + srcIdx[k];
6291
+ this.nVerts += n;
6292
+ this.nIdx += srcIdx.length;
6293
+ }
6294
+ /** Upload the staged geometry and bind the batch VAO. Returns the index
6295
+ * count for the caller's `drawElements`. */
6296
+ uploadAndBind() {
6297
+ const gl = this.gl;
6298
+ if (this.nVerts > this.vboVerts || this.nIdx > this.iboIdx) {
6299
+ this.growGpu(this.nVerts, this.nIdx);
6300
+ }
6301
+ gl.bindVertexArray(this.vao);
6302
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
6303
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nVerts * FLOATS_PER_VERTEX);
6304
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, this.idx, 0, this.nIdx);
6305
+ return this.nIdx;
6306
+ }
6307
+ reset() {
6308
+ this.nVerts = 0;
6309
+ this.nIdx = 0;
6310
+ }
6311
+ dispose() {
6312
+ const gl = this.gl;
6313
+ gl.deleteBuffer(this.vbo);
6314
+ gl.deleteBuffer(this.ibo);
6315
+ gl.deleteVertexArray(this.vao);
6316
+ }
6317
+ /** Grow the CPU arrays so `vertices` / `indices` more fit. */
6318
+ reserve(vertices, indices) {
6319
+ const needVerts = (this.nVerts + vertices) * FLOATS_PER_VERTEX;
6320
+ if (needVerts > this.verts.length) {
6321
+ let len = this.verts.length;
6322
+ while (len < needVerts) len *= 2;
6323
+ const grown = new Float32Array(len);
6324
+ grown.set(this.verts);
6325
+ this.verts = grown;
6326
+ }
6327
+ const needIdx = this.nIdx + indices;
6328
+ if (needIdx > this.idx.length) {
6329
+ let len = this.idx.length;
6330
+ while (len < needIdx) len *= 2;
6331
+ const grown = new Uint32Array(len);
6332
+ grown.set(this.idx);
6333
+ this.idx = grown;
6334
+ }
6335
+ }
6336
+ /** Size both GPU buffers for at least what is staged, doubling to amortize. */
6337
+ growGpu(vertices, indices) {
6338
+ const gl = this.gl;
6339
+ if (vertices > this.vboVerts) {
6340
+ let capacity = Math.max(this.vboVerts, INITIAL_VERTICES);
6341
+ while (capacity < vertices) capacity *= 2;
6342
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
6343
+ gl.bufferData(gl.ARRAY_BUFFER, capacity * FLOATS_PER_VERTEX * 4, gl.DYNAMIC_DRAW);
6344
+ this.vboVerts = capacity;
6345
+ }
6346
+ if (indices > this.iboIdx) {
6347
+ let capacity = Math.max(this.iboIdx, INITIAL_INDICES);
6348
+ while (capacity < indices) capacity *= 2;
6349
+ gl.bindVertexArray(this.vao);
6350
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo);
6351
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, capacity * 4, gl.DYNAMIC_DRAW);
6352
+ gl.bindVertexArray(null);
6353
+ this.iboIdx = capacity;
5636
6354
  }
5637
6355
  }
6356
+ };
6357
+ var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
6358
+ var FRAME_PROJ = /* @__PURE__ */ new WeakMap();
6359
+ function uploadedFor(ctx, prog) {
6360
+ let byProgram = FRAME_UPLOADS.get(ctx);
6361
+ if (!byProgram) {
6362
+ byProgram = /* @__PURE__ */ new WeakMap();
6363
+ FRAME_UPLOADS.set(ctx, byProgram);
6364
+ }
6365
+ let uploaded = byProgram.get(prog);
6366
+ if (!uploaded) {
6367
+ uploaded = {};
6368
+ byProgram.set(prog, uploaded);
6369
+ }
6370
+ return uploaded;
6371
+ }
6372
+ function sameValues(prev, next) {
6373
+ if (prev === void 0 || prev.length !== next.length) return false;
6374
+ for (let i = 0; i < prev.length; i++) if (prev[i] !== next[i]) return false;
6375
+ return true;
6376
+ }
6377
+ var BATCH_MODEL = mat3.identity();
6378
+ function projFor(ctx) {
6379
+ let proj = FRAME_PROJ.get(ctx);
6380
+ if (!proj) {
6381
+ proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
6382
+ FRAME_PROJ.set(ctx, proj);
6383
+ }
6384
+ return proj;
6385
+ }
6386
+ var COLOR_MATRIX_SCRATCH = new Float32Array(16);
6387
+ function setColorMatrixUniforms(ctx, prog, cm = ctx.state.colorMatrix) {
6388
+ const gl = ctx.gl;
6389
+ const uploaded = uploadedFor(ctx, prog);
5638
6390
  const mLoc = prog.uniform("u_colorMatrix");
6391
+ if (mLoc !== void 0) {
6392
+ for (let row = 0; row < 4; row++) {
6393
+ for (let col = 0; col < 4; col++) {
6394
+ COLOR_MATRIX_SCRATCH[col * 4 + row] = cm[row * 5 + col];
6395
+ }
6396
+ }
6397
+ if (!sameValues(uploaded.colorMatrix, COLOR_MATRIX_SCRATCH)) {
6398
+ gl.uniformMatrix4fv(mLoc, false, COLOR_MATRIX_SCRATCH);
6399
+ uploaded.colorMatrix = Float32Array.from(COLOR_MATRIX_SCRATCH);
6400
+ }
6401
+ }
5639
6402
  const bLoc = prog.uniform("u_colorBias");
5640
- if (mLoc !== void 0) gl.uniformMatrix4fv(mLoc, false, m4);
5641
- if (bLoc !== void 0) gl.uniform4f(bLoc, cm[4], cm[9], cm[14], cm[19]);
6403
+ if (bLoc !== void 0) {
6404
+ const bias = [cm[4], cm[9], cm[14], cm[19]];
6405
+ if (!sameValues(uploaded.colorBias, bias)) {
6406
+ gl.uniform4f(bLoc, bias[0], bias[1], bias[2], bias[3]);
6407
+ uploaded.colorBias = Float32Array.from(bias);
6408
+ }
6409
+ }
5642
6410
  }
5643
- function fillMeshHandle(ctx, path) {
6411
+ function fillMesh(ctx, path) {
5644
6412
  if (ctx.flattenTolerance !== void 0) {
5645
- return ctx.meshCache.uploadTransient(tessellate(path, { flattenTolerance: ctx.flattenTolerance }));
6413
+ return tessellate(path, { flattenTolerance: ctx.flattenTolerance });
5646
6414
  }
5647
- return ctx.meshCache.handleFor(getMesh(path));
6415
+ return getMesh(path);
6416
+ }
6417
+ function meshHandle(ctx, mesh) {
6418
+ if (ctx.flattenTolerance !== void 0) return ctx.meshCache.uploadTransient(mesh);
6419
+ return ctx.meshCache.handleFor(mesh);
6420
+ }
6421
+ function fillMeshHandle(ctx, path) {
6422
+ return meshHandle(ctx, fillMesh(ctx, path));
5648
6423
  }
5649
6424
  function dispatch(ctx, cmd) {
5650
6425
  switch (cmd.kind) {
@@ -5653,10 +6428,13 @@ function dispatch(ctx, cmd) {
5653
6428
  case "path":
5654
6429
  return drawPath(ctx, cmd);
5655
6430
  case "text":
6431
+ flushSolids(ctx);
5656
6432
  return drawText(ctx, cmd);
5657
6433
  case "image":
6434
+ flushSolids(ctx);
5658
6435
  return drawImage(ctx, cmd);
5659
6436
  case "shader":
6437
+ flushSolids(ctx);
5660
6438
  return drawShader(ctx, cmd);
5661
6439
  }
5662
6440
  }
@@ -5785,11 +6563,13 @@ function drawGroup(ctx, cmd) {
5785
6563
  "weasel: clip nesting depth exceeded (max 7). You can't nest more than 7 levels of clipped containers in a single draw tree. Flatten the hierarchy or compose poses outside the scene graph."
5786
6564
  );
5787
6565
  }
6566
+ flushSolids(ctx);
5788
6567
  pushClip(ctx, cmd.clip, newDepth);
5789
6568
  ctx.clipDepth = newDepth;
5790
6569
  }
5791
6570
  for (const child of cmd.children) dispatch(ctx, child);
5792
6571
  if (cmd.clip) {
6572
+ flushSolids(ctx);
5793
6573
  popClip(ctx, cmd.clip, ctx.clipDepth - 1);
5794
6574
  ctx.clipDepth -= 1;
5795
6575
  }
@@ -5798,38 +6578,98 @@ function drawGroup(ctx, cmd) {
5798
6578
  function drawPath(ctx, cmd) {
5799
6579
  if (!cmd.fill && !cmd.stroke) return;
5800
6580
  if (cmd.fill) {
5801
- const isSolidRectFast = cmd.path.kind === "rect" && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid") && (!cmd.vertexColors || cmd.vertexColors.length === 0) && ctx.rectVao !== null && ctx.rectVbo !== null;
5802
- if (isSolidRectFast && cmd.path.kind === "rect") {
5803
- drawRectFast(ctx, cmd.path, cmd.fill);
6581
+ const batchablePaint = (cmd.fill.fill === void 0 || cmd.fill.fill === "solid") && (!cmd.vertexColors || cmd.vertexColors.length === 0);
6582
+ const solid = cmd.fill;
6583
+ if (batchablePaint && cmd.path.kind === "rect") {
6584
+ pushRect(ctx, cmd.path, solid);
5804
6585
  } else {
5805
- const handle = fillMeshHandle(ctx, cmd.path);
5806
- if (cmd.vertexColors && cmd.vertexColors.length > 0 && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid")) {
5807
- drawPathFillVColor(ctx, cmd, cmd.fill, handle);
5808
- } else if (handle.requiresStencil) {
5809
- drawPathFillStencil(ctx, cmd.fill, handle);
6586
+ const mesh = fillMesh(ctx, cmd.path);
6587
+ if (batchablePaint && canBatchMesh(mesh)) {
6588
+ pushMesh(ctx, mesh, solid);
5810
6589
  } else {
5811
- drawPathFillByKind(ctx, cmd.fill, handle);
6590
+ flushSolids(ctx);
6591
+ const handle = meshHandle(ctx, mesh);
6592
+ if (cmd.vertexColors && cmd.vertexColors.length > 0 && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid")) {
6593
+ drawPathFillVColor(ctx, cmd, solid, handle);
6594
+ } else if (handle.requiresStencil) {
6595
+ drawPathFillStencil(ctx, cmd.fill, handle);
6596
+ } else {
6597
+ drawPathFillByKind(ctx, cmd.fill, handle);
6598
+ }
5812
6599
  }
5813
6600
  }
5814
6601
  }
5815
- if (cmd.stroke) {
5816
- drawPathStroke(ctx, cmd);
6602
+ if (cmd.stroke) drawPathStroke(ctx, cmd);
6603
+ }
6604
+ function isIdentityColorMatrix(cm) {
6605
+ return cm === IDENTITY_COLOR_MATRIX || sameValues(IDENTITY_COLOR_MATRIX, cm);
6606
+ }
6607
+ function stagedStateIsLive(ctx, staged) {
6608
+ if (staged.clipDepth !== ctx.clipDepth) return false;
6609
+ if (!staged.foldsAlpha && staged.alpha !== ctx.state.alpha) return false;
6610
+ const colorMatrix = ctx.state.colorMatrix;
6611
+ return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
6612
+ }
6613
+ var MAX_BATCHED_MESH_VERTICES = 256;
6614
+ function canBatchMesh(mesh) {
6615
+ return !mesh.requiresStencil && mesh.vertices.length >> 1 <= MAX_BATCHED_MESH_VERTICES;
6616
+ }
6617
+ function stageSolid(ctx, vertices) {
6618
+ if (ctx.solidState !== void 0 && !stagedStateIsLive(ctx, ctx.solidState)) flushSolids(ctx);
6619
+ if (ctx.solidBatch.wouldOverflow(vertices)) flushSolids(ctx);
6620
+ if (ctx.solidState === void 0) {
6621
+ const colorMatrix = ctx.state.colorMatrix;
6622
+ const foldsAlpha = isIdentityColorMatrix(colorMatrix);
6623
+ ctx.solidState = {
6624
+ alpha: foldsAlpha ? 1 : ctx.state.alpha,
6625
+ colorMatrix,
6626
+ clipDepth: ctx.clipDepth,
6627
+ foldsAlpha
6628
+ };
5817
6629
  }
6630
+ return ctx.solidState;
6631
+ }
6632
+ function stagedColor(ctx, staged, paint) {
6633
+ const [r, g, b, a] = resolveColor(paint.color);
6634
+ return [r, g, b, a * (paint.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1)];
6635
+ }
6636
+ function pushRect(ctx, rect, fill) {
6637
+ const staged = stageSolid(ctx, 4);
6638
+ const [r, g, b, a] = stagedColor(ctx, staged, fill);
6639
+ ctx.solidBatch.pushRect(
6640
+ rect.x,
6641
+ rect.y,
6642
+ rect.width,
6643
+ rect.height,
6644
+ ctx.state.transform,
6645
+ r,
6646
+ g,
6647
+ b,
6648
+ a
6649
+ );
6650
+ }
6651
+ function pushMesh(ctx, mesh, paint) {
6652
+ const staged = stageSolid(ctx, mesh.vertices.length >> 1);
6653
+ const [r, g, b, a] = stagedColor(ctx, staged, paint);
6654
+ ctx.solidBatch.pushMesh(mesh, ctx.state.transform, r, g, b, a);
5818
6655
  }
5819
- function drawRectFast(ctx, rect, fill) {
6656
+ function flushSolids(ctx) {
6657
+ const batch = ctx.solidBatch;
6658
+ const staged = ctx.solidState;
6659
+ if (batch.length === 0 || staged === void 0) return;
5820
6660
  const gl = ctx.gl;
5821
- const { x, y, width: w, height: h } = rect;
5822
- const corners = new Float32Array([x, y, x + w, y, x + w, y + h, x, y + h]);
5823
- gl.useProgram(ctx.pathFill.handle);
5824
- gl.bindVertexArray(ctx.rectVao);
5825
- gl.bindBuffer(gl.ARRAY_BUFFER, ctx.rectVbo);
5826
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, corners);
5827
- setProjAndModel(ctx, ctx.pathFill);
5828
- setSolidPaintUniforms(ctx, ctx.pathFill, fill.color, fill.opacity);
5829
- setColorMatrixUniforms(ctx, ctx.pathFill);
5830
- applyClipTest(ctx);
5831
- gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, 0);
6661
+ const prog = ctx.pathFillVColor;
6662
+ gl.useProgram(prog.handle);
6663
+ const indexCount = batch.uploadAndBind();
6664
+ setProjAndModel(ctx, prog, BATCH_MODEL);
6665
+ gl.uniform4f(prog.uniform("u_color"), 1, 1, 1, 1);
6666
+ gl.uniform1f(prog.uniform("u_alpha"), staged.alpha);
6667
+ setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
6668
+ applyClipTest(ctx, staged.clipDepth);
6669
+ gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
5832
6670
  gl.bindVertexArray(null);
6671
+ batch.reset();
6672
+ ctx.solidState = void 0;
5833
6673
  }
5834
6674
  function expandAnchorColors(perAnchor, handle) {
5835
6675
  const aA = handle.anchorA;
@@ -5874,11 +6714,18 @@ function drawPathFillVColor(ctx, cmd, fill, handle) {
5874
6714
  gl.bindVertexArray(null);
5875
6715
  gl.deleteBuffer(colorVbo);
5876
6716
  }
5877
- function setProjAndModel(ctx, prog) {
6717
+ function setProjAndModel(ctx, prog, model = ctx.state.transform) {
5878
6718
  const gl = ctx.gl;
5879
- const proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
5880
- gl.uniformMatrix3fv(prog.uniform("u_proj"), false, proj);
5881
- gl.uniformMatrix3fv(prog.uniform("u_model"), false, ctx.state.transform);
6719
+ const uploaded = uploadedFor(ctx, prog);
6720
+ const proj = projFor(ctx);
6721
+ if (!sameValues(uploaded.proj, proj)) {
6722
+ gl.uniformMatrix3fv(prog.uniform("u_proj"), false, proj);
6723
+ uploaded.proj = Float32Array.from(proj);
6724
+ }
6725
+ if (!sameValues(uploaded.model, model)) {
6726
+ gl.uniformMatrix3fv(prog.uniform("u_model"), false, model);
6727
+ uploaded.model = Float32Array.from(model);
6728
+ }
5882
6729
  }
5883
6730
  function setSolidPaintUniforms(ctx, prog, color, opacity) {
5884
6731
  const gl = ctx.gl;
@@ -5916,28 +6763,42 @@ function drawPathFillPattern(ctx, fill, handle) {
5916
6763
  if (isDev3) console.warn(`weasel: pattern TextureHandle "${tex.id}" not registered`);
5917
6764
  return;
5918
6765
  }
5919
- ctx.textureCache.upload(tex.id, entry.source);
6766
+ ctx.textureCache.upload(tex.id, entry.source, "repeat");
5920
6767
  const gl = ctx.gl;
5921
- gl.useProgram(ctx.imageFill.handle);
6768
+ gl.useProgram(ctx.patternFill.handle);
5922
6769
  gl.bindVertexArray(handle.vao);
5923
- setProjAndModel(ctx, ctx.imageFill);
5924
- setColorMatrixUniforms(ctx, ctx.imageFill);
6770
+ setProjAndModel(ctx, ctx.patternFill);
6771
+ setColorMatrixUniforms(ctx, ctx.patternFill);
6772
+ gl.uniformMatrix3fv(ctx.patternFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
6773
+ const [tw, th] = textureSize(entry.source);
6774
+ const origin = fill.origin ?? { x: 0, y: 0 };
6775
+ gl.uniform2f(ctx.patternFill.uniform("u_tileOrigin"), origin.x, origin.y);
6776
+ gl.uniform2f(ctx.patternFill.uniform("u_tileSize"), tw, th);
5925
6777
  ctx.textureCache.bind(tex.id, 0);
5926
- gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
5927
- gl.uniform1f(ctx.imageFill.uniform("u_opacity"), fill.opacity ?? 1);
5928
- gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
6778
+ gl.uniform1i(ctx.patternFill.uniform("u_sampler"), 0);
6779
+ gl.uniform1f(ctx.patternFill.uniform("u_opacity"), fill.opacity ?? 1);
6780
+ gl.uniform1f(ctx.patternFill.uniform("u_alpha"), ctx.state.alpha);
5929
6781
  applyClipTest(ctx);
5930
6782
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
5931
6783
  gl.bindVertexArray(null);
5932
6784
  }
6785
+ function textureSize(source) {
6786
+ const w = "naturalWidth" in source ? source.naturalWidth : source.width;
6787
+ const h = "naturalHeight" in source ? source.naturalHeight : source.height;
6788
+ return [w || 1, h || 1];
6789
+ }
6790
+ function gradientSpaceInverse(ctx, units) {
6791
+ if (units === "local") return mat3.invert(ctx.state.transform);
6792
+ if (units === "world" && ctx.viewMatrix) return mat3.invert(ctx.viewMatrix);
6793
+ return mat3.identity();
6794
+ }
5933
6795
  function drawPathFillGradient(ctx, fill, handle) {
5934
6796
  const gl = ctx.gl;
5935
6797
  const key = ctx.gradRampCache.upload(fill.stops);
5936
6798
  gl.useProgram(ctx.gradFill.handle);
5937
6799
  gl.bindVertexArray(handle.vao);
5938
6800
  setProjAndModel(ctx, ctx.gradFill);
5939
- const identity3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
5940
- gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, identity3);
6801
+ gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
5941
6802
  ctx.gradRampCache.bind(key, 0);
5942
6803
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
5943
6804
  gl.uniform1f(ctx.gradFill.uniform("u_alpha"), ctx.state.alpha);
@@ -5971,9 +6832,8 @@ function drawPathFillGradient(ctx, fill, handle) {
5971
6832
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
5972
6833
  gl.bindVertexArray(null);
5973
6834
  }
5974
- function applyClipTest(ctx) {
6835
+ function applyClipTest(ctx, depth = ctx.clipDepth) {
5975
6836
  const gl = ctx.gl;
5976
- const depth = ctx.clipDepth;
5977
6837
  if (depth === 0) {
5978
6838
  gl.disable(gl.STENCIL_TEST);
5979
6839
  return;
@@ -6057,6 +6917,7 @@ function drawPathStroke(ctx, cmd) {
6057
6917
  }
6058
6918
  const align = stroke.align ?? "center";
6059
6919
  if (cmd.path.kind === "polygon" && align !== "center") {
6920
+ flushSolids(ctx);
6060
6921
  drawPathStrokeStenciled(ctx, cmd, align);
6061
6922
  return;
6062
6923
  }
@@ -6067,6 +6928,12 @@ function drawPathStrokeUnclipped(ctx, cmd) {
6067
6928
  const solid = stroke.paint;
6068
6929
  const mesh = tessellateStroke(cmd.path, stroke, { flattenTolerance: ctx.flattenTolerance });
6069
6930
  if (mesh.indices.length === 0) return;
6931
+ const hasVColors = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
6932
+ if (!hasVColors && canBatchMesh(mesh)) {
6933
+ pushMesh(ctx, mesh, solid);
6934
+ return;
6935
+ }
6936
+ flushSolids(ctx);
6070
6937
  const handle = ctx.meshCache.uploadTransient(mesh);
6071
6938
  const gl = ctx.gl;
6072
6939
  if (stroke.vertexColors && stroke.vertexColors.length > 0) {
@@ -6164,20 +7031,17 @@ function drawText(ctx, cmd) {
6164
7031
  const maxWidth = cmd.maxWidth ?? Infinity;
6165
7032
  const minScreen = ctx.textOutlineMinScreenSize ?? OUTLINE_MIN_SCREEN_PX;
6166
7033
  const outlineMinSize = Number.isFinite(minScreen) ? minScreen / modelScale(ctx.state.transform) : void 0;
6167
- const laid = cachedLayoutRuns(
6168
- cmd.runs,
6169
- { maxWidth, lineHeight, align, outlineMinSize },
6170
- { x: cmd.x, y: cmd.y }
6171
- );
7034
+ const laid = cachedLayoutRuns(cmd.runs, { maxWidth, lineHeight, align, outlineMinSize });
6172
7035
  if (laid.groups.length === 0 && laid.decorations.length === 0) return;
6173
- const dy = verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
7036
+ const dx = cmd.x;
7037
+ const dy = cmd.y + verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
6174
7038
  const gl = ctx.gl;
6175
7039
  applyClipTest(ctx);
6176
7040
  const preparedPrograms = /* @__PURE__ */ new Set();
6177
7041
  let currentProg = null;
6178
7042
  for (const group of laid.groups) {
6179
7043
  if (group.source === "outline") {
6180
- drawTextOutlineGroup(ctx, group, dy);
7044
+ drawTextOutlineGroup(ctx, group, dx, dy);
6181
7045
  currentProg = null;
6182
7046
  continue;
6183
7047
  }
@@ -6192,31 +7056,31 @@ function drawText(ctx, cmd) {
6192
7056
  setColorMatrixUniforms(ctx, prog);
6193
7057
  gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
6194
7058
  }
6195
- drawTextGroup(ctx, group, prog, dy);
7059
+ drawTextGroup(ctx, group, prog, dx, dy);
6196
7060
  }
6197
- drawTextDecorations(ctx, laid.decorations, dy);
7061
+ drawTextDecorations(ctx, laid.decorations, dx, dy);
6198
7062
  }
6199
7063
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
6200
- function drawTextOutlineGroup(ctx, group, dy) {
6201
- const mesh = outlineGroupMesh(group, dy);
7064
+ function drawTextOutlineGroup(ctx, group, dx, dy) {
7065
+ const mesh = outlineGroupMesh(group, dx, dy);
6202
7066
  if (mesh) drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
6203
7067
  if (!group.stroke) return;
6204
- const strokeMesh = outlineGroupStrokeMesh(group, dy);
7068
+ const strokeMesh = outlineGroupStrokeMesh(group, dx, dy);
6205
7069
  if (strokeMesh) {
6206
7070
  drawPathFillByKind(ctx, group.stroke.paint, ctx.meshCache.uploadTransient(strokeMesh));
6207
7071
  }
6208
7072
  }
6209
- function outlineGroupMesh(group, dy) {
6210
- return mergeGlyphMeshes(group, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
7073
+ function outlineGroupMesh(group, dx, dy) {
7074
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
6211
7075
  }
6212
- function outlineGroupStrokeMesh(group, dy) {
7076
+ function outlineGroupStrokeMesh(group, dx, dy) {
6213
7077
  const stroke = group.stroke;
6214
7078
  if (!stroke) return null;
6215
7079
  const width = stroke.width ?? 1;
6216
7080
  if (!(width > 0)) return null;
6217
- return mergeGlyphMeshes(group, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
7081
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
6218
7082
  }
6219
- function mergeGlyphMeshes(group, dy, meshFor) {
7083
+ function mergeGlyphMeshes(group, dx, dy, meshFor) {
6220
7084
  const parts = [];
6221
7085
  let vertexFloats = 0;
6222
7086
  let indexCount = 0;
@@ -6239,7 +7103,7 @@ function mergeGlyphMeshes(group, dy, meshFor) {
6239
7103
  for (let k = 0; k < mesh.vertices.length; k += 2) {
6240
7104
  const ex = mesh.vertices[k];
6241
7105
  const ey = mesh.vertices[k + 1];
6242
- vertices[vi++] = x + (ex - ey * shear) * scale2;
7106
+ vertices[vi++] = x + dx + (ex - ey * shear) * scale2;
6243
7107
  vertices[vi++] = baselineY + dy + ey * scale2;
6244
7108
  }
6245
7109
  for (let k = 0; k < mesh.indices.length; k++) indices[ii++] = base + mesh.indices[k];
@@ -6247,7 +7111,7 @@ function mergeGlyphMeshes(group, dy, meshFor) {
6247
7111
  }
6248
7112
  return { vertices, indices };
6249
7113
  }
6250
- function drawTextDecorations(ctx, decorations, dy) {
7114
+ function drawTextDecorations(ctx, decorations, dx, dy) {
6251
7115
  if (decorations.length === 0) return;
6252
7116
  const batches = /* @__PURE__ */ new Map();
6253
7117
  for (const d of decorations) {
@@ -6270,14 +7134,15 @@ function drawTextDecorations(ctx, decorations, dy) {
6270
7134
  const vertices = new Float32Array(rects.length * 4 * 2);
6271
7135
  let vi = 0;
6272
7136
  for (const d of rects) {
7137
+ const dx0 = d.x0 + dx, dx1 = d.x1 + dx;
6273
7138
  const dy0 = d.y0 + dy, dy1 = d.y1 + dy;
6274
- vertices[vi++] = d.x0;
7139
+ vertices[vi++] = dx0;
6275
7140
  vertices[vi++] = dy0;
6276
- vertices[vi++] = d.x1;
7141
+ vertices[vi++] = dx1;
6277
7142
  vertices[vi++] = dy0;
6278
- vertices[vi++] = d.x0;
7143
+ vertices[vi++] = dx0;
6279
7144
  vertices[vi++] = dy1;
6280
- vertices[vi++] = d.x1;
7145
+ vertices[vi++] = dx1;
6281
7146
  vertices[vi++] = dy1;
6282
7147
  }
6283
7148
  const indices = new Uint32Array(rects.length * 6);
@@ -6316,7 +7181,7 @@ function drawTextDecorations(ctx, decorations, dy) {
6316
7181
  gl.deleteBuffer(ibo);
6317
7182
  }
6318
7183
  }
6319
- function drawTextGroup(ctx, group, prog, dy) {
7184
+ function drawTextGroup(ctx, group, prog, dx, dy) {
6320
7185
  if (group.source === "canvas") {
6321
7186
  if (!syncDynamicPageTexture(ctx.textureCache, group.page)) return;
6322
7187
  } else {
@@ -6327,23 +7192,24 @@ function drawTextGroup(ctx, group, prog, dy) {
6327
7192
  const vertices = new Float32Array(group.quads.length * 4 * 5);
6328
7193
  let vi = 0;
6329
7194
  for (const q of group.quads) {
7195
+ const x0 = q.x0 + dx, x1 = q.x1 + dx;
6330
7196
  const y0 = q.y0 + dy, y1 = q.y1 + dy, by = q.baselineY + dy;
6331
- vertices[vi++] = q.x0;
7197
+ vertices[vi++] = x0;
6332
7198
  vertices[vi++] = y0;
6333
7199
  vertices[vi++] = q.u0;
6334
7200
  vertices[vi++] = q.v0;
6335
7201
  vertices[vi++] = by;
6336
- vertices[vi++] = q.x1;
7202
+ vertices[vi++] = x1;
6337
7203
  vertices[vi++] = y0;
6338
7204
  vertices[vi++] = q.u1;
6339
7205
  vertices[vi++] = q.v0;
6340
7206
  vertices[vi++] = by;
6341
- vertices[vi++] = q.x0;
7207
+ vertices[vi++] = x0;
6342
7208
  vertices[vi++] = y1;
6343
7209
  vertices[vi++] = q.u0;
6344
7210
  vertices[vi++] = q.v1;
6345
7211
  vertices[vi++] = by;
6346
- vertices[vi++] = q.x1;
7212
+ vertices[vi++] = x1;
6347
7213
  vertices[vi++] = y1;
6348
7214
  vertices[vi++] = q.u1;
6349
7215
  vertices[vi++] = q.v1;
@@ -6457,6 +7323,11 @@ function drawImage(ctx, cmd) {
6457
7323
  setProjAndModel(ctx, ctx.imageFill);
6458
7324
  setColorMatrixUniforms(ctx, ctx.imageFill);
6459
7325
  ctx.imageCache.bind(cmd.image, 0);
7326
+ gl.texParameteri(
7327
+ gl.TEXTURE_2D,
7328
+ gl.TEXTURE_MAG_FILTER,
7329
+ cmd.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
7330
+ );
6460
7331
  gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
6461
7332
  gl.uniform1f(ctx.imageFill.uniform("u_opacity"), cmd.opacity ?? 1);
6462
7333
  gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
@@ -6543,6 +7414,7 @@ var WeaselRenderer = class {
6543
7414
  textSdfR8;
6544
7415
  imageFill;
6545
7416
  gradFill;
7417
+ patternFill;
6546
7418
  meshCache;
6547
7419
  textureCache;
6548
7420
  imageCache;
@@ -6550,10 +7422,7 @@ var WeaselRenderer = class {
6550
7422
  programRegistry = /* @__PURE__ */ new Map();
6551
7423
  quadVbo = null;
6552
7424
  quadIbo = null;
6553
- /** Shared rect-fill geometry: 4 verts × 2 floats (dynamic), 6-index static IBO. */
6554
- rectVao = null;
6555
- rectVbo = null;
6556
- rectIbo = null;
7425
+ solidBatch;
6557
7426
  groupState = new GroupState();
6558
7427
  widthCss;
6559
7428
  heightCss;
@@ -6618,6 +7487,9 @@ var WeaselRenderer = class {
6618
7487
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
6619
7488
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
6620
7489
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
7490
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
7491
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
7492
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
6621
7493
  const aPos = this.pathFill.attribute("a_position");
6622
7494
  if (aPos === void 0) throw new Error("WeaselRenderer: a_position not found in path-fill shader");
6623
7495
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -6625,31 +7497,7 @@ var WeaselRenderer = class {
6625
7497
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
6626
7498
  this.gradRampCache = new GradientRampCache(this.gl);
6627
7499
  this.uploadQuadGeometry();
6628
- this.uploadRectGeometry(aPos);
6629
- }
6630
- /** Allocate the shared rect VAO + dynamic VBO (4 verts × 2 floats) + static
6631
- * IBO (6 indices). drawRectFast bufferSubData's the 4 corner coords each
6632
- * draw, avoiding per-rect buffer allocation in animated demos. */
6633
- uploadRectGeometry(aPositionLoc) {
6634
- const gl = this.gl;
6635
- this.rectVao = gl.createVertexArray();
6636
- this.rectVbo = gl.createBuffer();
6637
- this.rectIbo = gl.createBuffer();
6638
- if (!this.rectVao || !this.rectVbo || !this.rectIbo) {
6639
- throw new Error("WeaselRenderer: failed to create rect-fast geometry");
6640
- }
6641
- gl.bindVertexArray(this.rectVao);
6642
- gl.bindBuffer(gl.ARRAY_BUFFER, this.rectVbo);
6643
- gl.bufferData(gl.ARRAY_BUFFER, 8 * 4, gl.DYNAMIC_DRAW);
6644
- gl.enableVertexAttribArray(aPositionLoc);
6645
- gl.vertexAttribPointer(aPositionLoc, 2, gl.FLOAT, false, 0, 0);
6646
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.rectIbo);
6647
- gl.bufferData(
6648
- gl.ELEMENT_ARRAY_BUFFER,
6649
- new Uint32Array([0, 1, 2, 0, 2, 3]),
6650
- gl.STATIC_DRAW
6651
- );
6652
- gl.bindVertexArray(null);
7500
+ this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
6653
7501
  }
6654
7502
  uploadQuadGeometry() {
6655
7503
  const gl = this.gl;
@@ -6726,6 +7574,9 @@ var WeaselRenderer = class {
6726
7574
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
6727
7575
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
6728
7576
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
7577
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
7578
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
7579
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
6729
7580
  const aPos = this.pathFill.attribute("a_position");
6730
7581
  if (aPos === void 0) throw new Error("a_position missing after restore");
6731
7582
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -6733,7 +7584,7 @@ var WeaselRenderer = class {
6733
7584
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
6734
7585
  this.gradRampCache = new GradientRampCache(this.gl);
6735
7586
  this.uploadQuadGeometry();
6736
- this.uploadRectGeometry(aPos);
7587
+ this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
6737
7588
  for (const id of this.programRegistry.keys()) {
6738
7589
  const src = getProgramSource(id);
6739
7590
  if (!src) continue;
@@ -6793,11 +7644,17 @@ var WeaselRenderer = class {
6793
7644
  this.gradRampCache.free();
6794
7645
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
6795
7646
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
6796
- if (this.rectVbo) gl.deleteBuffer(this.rectVbo);
6797
- if (this.rectIbo) gl.deleteBuffer(this.rectIbo);
6798
- if (this.rectVao) gl.deleteVertexArray(this.rectVao);
7647
+ this.solidBatch.dispose();
6799
7648
  }
6800
- render(commands) {
7649
+ /**
7650
+ * Draw one frame.
7651
+ *
7652
+ * `viewMatrix` is the frame's world→screen transform. It is only consulted
7653
+ * by `units: 'world'` gradients, which fall back to screen space without
7654
+ * it — every other command carries its own transform in the stream, so
7655
+ * callers with no view concept can keep calling `render(commands)`.
7656
+ */
7657
+ render(commands, viewMatrix) {
6801
7658
  if (this.contextLost || this.disposed) return;
6802
7659
  const gl = this.gl;
6803
7660
  this.meshCache.drainPendingDeletes();
@@ -6812,6 +7669,7 @@ var WeaselRenderer = class {
6812
7669
  textSdfR8: this.textSdfR8,
6813
7670
  imageFill: this.imageFill,
6814
7671
  gradFill: this.gradFill,
7672
+ patternFill: this.patternFill,
6815
7673
  meshCache: this.meshCache,
6816
7674
  textureCache: this.textureCache,
6817
7675
  imageCache: this.imageCache,
@@ -6819,16 +7677,17 @@ var WeaselRenderer = class {
6819
7677
  programRegistry: this.programRegistry,
6820
7678
  quadVbo: this.quadVbo,
6821
7679
  quadIbo: this.quadIbo,
6822
- rectVao: this.rectVao,
6823
- rectVbo: this.rectVbo,
7680
+ solidBatch: this.solidBatch,
6824
7681
  state: this.groupState,
6825
7682
  widthCss: this.widthCss,
6826
7683
  heightCss: this.heightCss,
6827
7684
  clipDepth: 0,
6828
7685
  flattenTolerance: this.flattenTolerance,
6829
- textOutlineMinScreenSize: this.textOutlineMinScreenSize
7686
+ textOutlineMinScreenSize: this.textOutlineMinScreenSize,
7687
+ viewMatrix
6830
7688
  };
6831
7689
  for (const cmd of commands) dispatch(ctx, cmd);
7690
+ flushSolids(ctx);
6832
7691
  this.meshCache.freeTransient();
6833
7692
  }
6834
7693
  resize(dims) {
@@ -6858,6 +7717,10 @@ var WeaselRenderer = class {
6858
7717
  return this.pathFillVColor;
6859
7718
  }
6860
7719
  /** @internal */
7720
+ _solidBatch() {
7721
+ return this.solidBatch;
7722
+ }
7723
+ /** @internal */
6861
7724
  _textSdf() {
6862
7725
  return this.textSdf;
6863
7726
  }
@@ -6874,6 +7737,10 @@ var WeaselRenderer = class {
6874
7737
  return this.gradFill;
6875
7738
  }
6876
7739
  /** @internal */
7740
+ _patternFill() {
7741
+ return this.patternFill;
7742
+ }
7743
+ /** @internal */
6877
7744
  _meshCache() {
6878
7745
  return this.meshCache;
6879
7746
  }
@@ -6934,6 +7801,9 @@ function alignedStrokeRect(rect, align, width = 1) {
6934
7801
  height: rect.height + 2 * d
6935
7802
  };
6936
7803
  }
7804
+ function meanScale(s) {
7805
+ return Math.sqrt(s.x * s.y);
7806
+ }
6937
7807
  var DEFAULT_LINE = {
6938
7808
  paint: { fill: "solid", color: "rgba(255, 255, 255, 0.08)" },
6939
7809
  width: 1
@@ -7074,374 +7944,136 @@ var DEFAULT_DEVICE_PROFILE = resolveDeviceProfile({
7074
7944
  coarsePointer: false,
7075
7945
  canHover: true,
7076
7946
  dpr: 1
7077
- });
7078
- var COARSE_QUERY = "(pointer: coarse)";
7079
- var HOVER_QUERY = "(hover: hover)";
7080
- function hasMatchMedia() {
7081
- return typeof window !== "undefined" && typeof window.matchMedia === "function";
7082
- }
7083
- function detectOnce() {
7084
- const dpr = typeof window !== "undefined" && window.devicePixelRatio || 1;
7085
- if (!hasMatchMedia()) {
7086
- return {
7087
- coarsePointer: DEFAULT_DEVICE_PROFILE.coarsePointer,
7088
- canHover: DEFAULT_DEVICE_PROFILE.canHover,
7089
- dpr
7090
- };
7091
- }
7092
- return {
7093
- coarsePointer: window.matchMedia(COARSE_QUERY).matches,
7094
- canHover: window.matchMedia(HOVER_QUERY).matches,
7095
- dpr
7096
- };
7097
- }
7098
- function useDetectedFacts() {
7099
- const [facts, setFacts] = useState(detectOnce);
7100
- useEffect(() => {
7101
- if (!hasMatchMedia()) return;
7102
- const coarseMq = window.matchMedia(COARSE_QUERY);
7103
- const hoverMq = window.matchMedia(HOVER_QUERY);
7104
- const onCoarse = (e) => setFacts((f) => ({ ...f, coarsePointer: e.matches }));
7105
- const onHover = (e) => setFacts((f) => ({ ...f, canHover: e.matches }));
7106
- coarseMq.addEventListener("change", onCoarse);
7107
- hoverMq.addEventListener("change", onHover);
7108
- let disposed = false;
7109
- let densityMq = null;
7110
- const onDensity = () => {
7111
- if (disposed) return;
7112
- const next = window.devicePixelRatio || 1;
7113
- setFacts((f) => f.dpr === next ? f : { ...f, dpr: next });
7114
- arm();
7115
- };
7116
- function arm() {
7117
- if (disposed) return;
7118
- densityMq?.removeEventListener("change", onDensity);
7119
- densityMq = window.matchMedia(
7120
- `(resolution: ${window.devicePixelRatio || 1}dppx)`
7121
- );
7122
- densityMq.addEventListener("change", onDensity);
7123
- }
7124
- arm();
7125
- setFacts({
7126
- coarsePointer: coarseMq.matches,
7127
- canHover: hoverMq.matches,
7128
- dpr: window.devicePixelRatio || 1
7129
- });
7130
- return () => {
7131
- disposed = true;
7132
- coarseMq.removeEventListener("change", onCoarse);
7133
- hoverMq.removeEventListener("change", onHover);
7134
- densityMq?.removeEventListener("change", onDensity);
7135
- };
7136
- }, []);
7137
- return facts;
7138
- }
7139
- var DeviceProfileContext = createContext(null);
7140
- function useDeviceProfile(overrides) {
7141
- const provided = useContext(DeviceProfileContext);
7142
- const detected = useDetectedFacts();
7143
- const base = provided ?? detected;
7144
- const { coarsePointer: coarsePointer2, canHover: canHover2, dpr, targetScale } = overrides ?? {};
7145
- return useMemo(
7146
- () => resolveDeviceProfile(base, {
7147
- ...coarsePointer2 !== void 0 ? { coarsePointer: coarsePointer2 } : {},
7148
- ...canHover2 !== void 0 ? { canHover: canHover2 } : {},
7149
- ...dpr !== void 0 ? { dpr } : {},
7150
- ...targetScale !== void 0 ? { targetScale } : {}
7151
- }),
7152
- [base.coarsePointer, base.canHover, base.dpr, coarsePointer2, canHover2, dpr, targetScale]
7153
- );
7154
- }
7155
- function DeviceProfileProvider({ value, children }) {
7156
- const profile = useDeviceProfile(value);
7157
- return createElement(DeviceProfileContext.Provider, { value: profile }, children);
7158
- }
7159
- var HANDLE_BASE_PX = 8;
7160
- var ANCHOR_HIT_BASE_PX = 8;
7161
- var ROTATION_HANDLE_BASE_PX = 24;
7162
- function zoomAt(view, anchor, factor, opts) {
7163
- const fx = typeof factor === "number" ? factor : factor.x;
7164
- const fy = typeof factor === "number" ? factor : factor.y;
7165
- const minX = typeof opts?.min === "number" ? opts.min : opts?.min?.x ?? 0.1;
7166
- const minY = typeof opts?.min === "number" ? opts.min : opts?.min?.y ?? 0.1;
7167
- const maxX = typeof opts?.max === "number" ? opts.max : opts?.max?.x ?? 8;
7168
- const maxY = typeof opts?.max === "number" ? opts.max : opts?.max?.y ?? 8;
7169
- const nextX = Math.min(maxX, Math.max(minX, view.scale.x * fx));
7170
- const nextY = Math.min(maxY, Math.max(minY, view.scale.y * fy));
7171
- const worldX = anchor.x / view.scale.x + view.x;
7172
- const worldY = anchor.y / view.scale.y + view.y;
7173
- return {
7174
- scale: { x: nextX, y: nextY },
7175
- x: worldX - anchor.x / nextX,
7176
- y: worldY - anchor.y / nextY
7177
- };
7178
- }
7179
- function clampView(view, bounds, canvas) {
7180
- const visW = canvas.width / view.scale.x;
7181
- const visH = canvas.height / view.scale.y;
7182
- let x;
7183
- if (visW >= bounds.width) {
7184
- x = bounds.x + (bounds.width - visW) / 2;
7185
- } else {
7186
- const minX = bounds.x;
7187
- const maxX = bounds.x + bounds.width - visW;
7188
- x = view.x < minX ? minX : view.x > maxX ? maxX : view.x;
7189
- }
7190
- let y;
7191
- if (visH >= bounds.height) {
7192
- y = bounds.y + (bounds.height - visH) / 2;
7193
- } else {
7194
- const minY = bounds.y;
7195
- const maxY = bounds.y + bounds.height - visH;
7196
- y = view.y < minY ? minY : view.y > maxY ? maxY : view.y;
7197
- }
7198
- return x === view.x && y === view.y ? view : { x, y, scale: view.scale };
7199
- }
7200
- function isEditableTarget2(target) {
7201
- if (!target || !(target instanceof Element)) return false;
7202
- const tag = target.tagName;
7203
- if (tag === "INPUT" || tag === "TEXTAREA") return true;
7204
- if (target.isContentEditable) return true;
7205
- const ce = target.getAttribute("contenteditable");
7206
- return ce === "true" || ce === "";
7207
- }
7208
- var DepRegistryContext = createContext(null);
7209
- function DepRegistryProvider({ children }) {
7210
- const sourcesRef = useRef(/* @__PURE__ */ new Map());
7211
- const registry22 = useMemo(() => ({
7212
- register: (name, source) => {
7213
- sourcesRef.current.set(name, source);
7214
- return () => {
7215
- sourcesRef.current.delete(name);
7216
- };
7217
- },
7218
- get: (name) => sourcesRef.current.get(name)?.()
7219
- }), []);
7220
- return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry22, children });
7221
- }
7222
- function useDepRegistry() {
7223
- const r = useContext(DepRegistryContext);
7224
- if (r === null) {
7225
- throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
7226
- }
7227
- return r;
7228
- }
7229
- function useOptionalDepRegistry() {
7230
- return useContext(DepRegistryContext);
7231
- }
7232
- function useDepSource(name, source) {
7233
- const registry22 = useDepRegistry();
7234
- const sourceRef = useRef(source);
7235
- sourceRef.current = source;
7236
- useEffect(() => {
7237
- return registry22.register(name, () => sourceRef.current());
7238
- }, [name, registry22]);
7239
- }
7240
- var DEV = (() => {
7241
- try {
7242
- return Boolean(import.meta.env?.DEV);
7243
- } catch {
7244
- return false;
7245
- }
7246
- })();
7247
- function buildDepsFromRequires(action, depRegistry) {
7248
- const requires = action.requires ?? [];
7249
- const entries = requires.map((name) => [name, depRegistry.get(name)]);
7250
- const deps = Object.fromEntries(entries);
7251
- if (DEV) {
7252
- const declared = new Set(requires);
7253
- return new Proxy(deps, {
7254
- get(target, prop, receiver) {
7255
- if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
7256
- const message = `[weasel:dispatcher] action "${action.id}" read deps.${prop} but did not declare it in \`requires\`. Add \`requires: [...'${prop}']\` to the descriptor or the dep will be undefined at runtime.`;
7257
- console.warn(message);
7258
- throw new Error(message);
7259
- }
7260
- return Reflect.get(target, prop, receiver);
7261
- }
7262
- });
7263
- }
7264
- return deps;
7265
- }
7266
- var ActionDisabledReason = {
7267
- SelectionRequired: "selection-required",
7268
- NotApplicable: "not-applicable",
7269
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
7270
- PredicateThrew: "predicate-threw"
7271
- };
7272
- var enabledSlowWarned = /* @__PURE__ */ new Set();
7273
- var enabledThrewWarned = /* @__PURE__ */ new Set();
7274
- var ENABLED_BUDGET_MS = 4;
7275
- function evaluateEnabled(action, deps) {
7276
- if (!action.enabled) return { enabled: true };
7277
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
7278
- const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
7279
- const start = isDev3 ? now() : 0;
7280
- try {
7281
- const result = action.enabled(deps);
7282
- if (isDev3) {
7283
- const elapsed = now() - start;
7284
- if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
7285
- enabledSlowWarned.add(action.id);
7286
- console.warn(
7287
- `weasel actions: enabled() for action "${action.id}" took ${elapsed.toFixed(2)}ms (budget ${ENABLED_BUDGET_MS}ms). The predicate must be pure and fast \u2014 see Action.enabled JSDoc.`
7288
- );
7289
- }
7290
- }
7291
- if (result === true) return { enabled: true };
7292
- return { enabled: false, reason: result };
7293
- } catch (e) {
7294
- if (isDev3 && !enabledThrewWarned.has(action.id)) {
7295
- enabledThrewWarned.add(action.id);
7296
- console.warn(
7297
- `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
7298
- e
7299
- );
7300
- }
7301
- return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
7302
- }
7303
- }
7304
- function validateActionId(id) {
7305
- if (id.length === 0) {
7306
- throw new Error(`weasel: action id may not be empty`);
7307
- }
7308
- if (RESERVED_ID_PREFIXES.has(id[0])) {
7309
- throw new Error(
7310
- `weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
7311
- );
7312
- }
7313
- if (RESERVED_ID_NAMES.has(id)) {
7314
- throw new Error(
7315
- `weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
7316
- );
7317
- }
7318
- }
7319
- function validateActionDefaultBinding(action) {
7320
- const gs = action.defaultBinding;
7321
- if (!gs) return;
7322
- const entries = Array.isArray(gs) ? gs : [gs];
7323
- for (const entry of entries) {
7324
- const spec = "kind" in entry ? entry : entry.spec;
7325
- const phase = spec.phase;
7326
- if (phase === void 0) continue;
7327
- const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
7328
- for (const atom of atoms) {
7329
- if (atom.channel === "&") {
7330
- throw new Error(
7331
- `weasel: action "${action.id}" defaultBinding uses '&' channel which has no owning tool to resolve to. Use a named tool channel (e.g. '[rect:engaged]') or '*' instead, or move this binding to the tool's own Tool.bindings.`
7332
- );
7333
- }
7334
- }
7335
- }
7336
- }
7337
- var ActionsContext = createContext(null);
7338
- function ActionsProvider({ children }) {
7339
- const actionsRef = useRef(/* @__PURE__ */ new Map());
7340
- const versionRef = useRef(0);
7341
- const cachedRef = useRef([]);
7342
- const cachedVerRef = useRef(-1);
7343
- const listenersRef = useRef(/* @__PURE__ */ new Set());
7344
- const depReg = useOptionalDepRegistry();
7345
- const depRegRef = useRef(depReg);
7346
- depRegRef.current = depReg;
7347
- const wiredDepRegRef = useRef(null);
7348
- const dispatcherRef = useRef(null);
7349
- const registry22 = useMemo(() => {
7350
- const snapshot = () => {
7351
- const v = versionRef.current;
7352
- if (cachedVerRef.current === v) return cachedRef.current;
7353
- const out = Object.freeze(Array.from(actionsRef.current.values()));
7354
- cachedRef.current = out;
7355
- cachedVerRef.current = v;
7356
- return out;
7357
- };
7358
- const notify2 = () => {
7359
- for (const l of listenersRef.current) {
7360
- try {
7361
- l();
7362
- } catch (err) {
7363
- console.error("weasel ActionsRegistry: subscriber threw", err);
7364
- }
7365
- }
7366
- };
7367
- return {
7368
- register: (action) => {
7369
- validateActionId(action.id);
7370
- validateActionDefaultBinding(action);
7371
- actionsRef.current.set(action.id, action);
7372
- versionRef.current++;
7373
- notify2();
7374
- return () => {
7375
- const cur = actionsRef.current.get(action.id);
7376
- if (cur === action) {
7377
- actionsRef.current.delete(action.id);
7378
- versionRef.current++;
7379
- notify2();
7380
- }
7381
- };
7382
- },
7383
- unregister: (id) => {
7384
- if (actionsRef.current.delete(id)) {
7385
- versionRef.current++;
7386
- notify2();
7387
- }
7388
- },
7389
- list: () => snapshot(),
7390
- trigger: (id, params) => {
7391
- const a = actionsRef.current.get(id);
7392
- if (!a) return false;
7393
- try {
7394
- if (a.invoker && a.invoker.timing === "immediate") {
7395
- const r = wiredDepRegRef.current ?? depRegRef.current;
7396
- const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
7397
- selection: r.get("selection"),
7398
- scene: r.get("scene"),
7399
- history: r.get("history"),
7400
- view: r.get("view"),
7401
- pointer: r.get("pointer"),
7402
- activeTool: r.get("activeTool"),
7403
- booleansAdapter: r.get("booleansAdapter")
7404
- };
7405
- a.invoker.run(deps, params);
7406
- }
7407
- } catch (err) {
7408
- console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
7409
- }
7410
- return true;
7411
- },
7412
- subscribe: (listener) => {
7413
- listenersRef.current.add(listener);
7414
- return () => {
7415
- listenersRef.current.delete(listener);
7416
- };
7417
- },
7418
- setDispatcher: (d) => {
7419
- dispatcherRef.current = d;
7420
- },
7421
- setDepRegistry: (r) => {
7422
- wiredDepRegRef.current = r;
7423
- },
7424
- begin: (id, params) => {
7425
- const disp = dispatcherRef.current;
7426
- if (!disp) return null;
7427
- const r = wiredDepRegRef.current ?? depRegRef.current;
7428
- const deps = r ? {
7429
- selection: r.get("selection"),
7430
- scene: r.get("scene"),
7431
- history: r.get("history"),
7432
- view: r.get("view"),
7433
- pointer: r.get("pointer"),
7434
- activeTool: r.get("activeTool"),
7435
- booleansAdapter: r.get("booleansAdapter")
7436
- } : {};
7437
- return disp.beginUiOngoing(id, deps, params);
7438
- }
7947
+ });
7948
+ var COARSE_QUERY = "(pointer: coarse)";
7949
+ var HOVER_QUERY = "(hover: hover)";
7950
+ function hasMatchMedia() {
7951
+ return typeof window !== "undefined" && typeof window.matchMedia === "function";
7952
+ }
7953
+ function detectOnce() {
7954
+ const dpr = typeof window !== "undefined" && window.devicePixelRatio || 1;
7955
+ if (!hasMatchMedia()) {
7956
+ return {
7957
+ coarsePointer: DEFAULT_DEVICE_PROFILE.coarsePointer,
7958
+ canHover: DEFAULT_DEVICE_PROFILE.canHover,
7959
+ dpr
7960
+ };
7961
+ }
7962
+ return {
7963
+ coarsePointer: window.matchMedia(COARSE_QUERY).matches,
7964
+ canHover: window.matchMedia(HOVER_QUERY).matches,
7965
+ dpr
7966
+ };
7967
+ }
7968
+ function useDetectedFacts() {
7969
+ const [facts, setFacts] = useState(detectOnce);
7970
+ useEffect(() => {
7971
+ if (!hasMatchMedia()) return;
7972
+ const coarseMq = window.matchMedia(COARSE_QUERY);
7973
+ const hoverMq = window.matchMedia(HOVER_QUERY);
7974
+ const onCoarse = (e) => setFacts((f) => ({ ...f, coarsePointer: e.matches }));
7975
+ const onHover = (e) => setFacts((f) => ({ ...f, canHover: e.matches }));
7976
+ coarseMq.addEventListener("change", onCoarse);
7977
+ hoverMq.addEventListener("change", onHover);
7978
+ let disposed = false;
7979
+ let densityMq = null;
7980
+ const onDensity = () => {
7981
+ if (disposed) return;
7982
+ const next = window.devicePixelRatio || 1;
7983
+ setFacts((f) => f.dpr === next ? f : { ...f, dpr: next });
7984
+ arm();
7985
+ };
7986
+ function arm() {
7987
+ if (disposed) return;
7988
+ densityMq?.removeEventListener("change", onDensity);
7989
+ densityMq = window.matchMedia(
7990
+ `(resolution: ${window.devicePixelRatio || 1}dppx)`
7991
+ );
7992
+ densityMq.addEventListener("change", onDensity);
7993
+ }
7994
+ arm();
7995
+ setFacts({
7996
+ coarsePointer: coarseMq.matches,
7997
+ canHover: hoverMq.matches,
7998
+ dpr: window.devicePixelRatio || 1
7999
+ });
8000
+ return () => {
8001
+ disposed = true;
8002
+ coarseMq.removeEventListener("change", onCoarse);
8003
+ hoverMq.removeEventListener("change", onHover);
8004
+ densityMq?.removeEventListener("change", onDensity);
7439
8005
  };
7440
8006
  }, []);
7441
- return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry22, children });
8007
+ return facts;
7442
8008
  }
7443
- function useActionsRegistry() {
7444
- return useContext(ActionsContext);
8009
+ var DeviceProfileContext = createContext(null);
8010
+ function useDeviceProfile(overrides) {
8011
+ const provided = useContext(DeviceProfileContext);
8012
+ const detected = useDetectedFacts();
8013
+ const base = provided ?? detected;
8014
+ const { coarsePointer: coarsePointer2, canHover: canHover2, dpr, targetScale } = overrides ?? {};
8015
+ return useMemo(
8016
+ () => resolveDeviceProfile(base, {
8017
+ ...coarsePointer2 !== void 0 ? { coarsePointer: coarsePointer2 } : {},
8018
+ ...canHover2 !== void 0 ? { canHover: canHover2 } : {},
8019
+ ...dpr !== void 0 ? { dpr } : {},
8020
+ ...targetScale !== void 0 ? { targetScale } : {}
8021
+ }),
8022
+ [base.coarsePointer, base.canHover, base.dpr, coarsePointer2, canHover2, dpr, targetScale]
8023
+ );
8024
+ }
8025
+ function DeviceProfileProvider({ value, children }) {
8026
+ const profile = useDeviceProfile(value);
8027
+ return createElement(DeviceProfileContext.Provider, { value: profile }, children);
8028
+ }
8029
+ var HANDLE_BASE_PX = 8;
8030
+ var ANCHOR_HIT_BASE_PX = 8;
8031
+ var ROTATION_HANDLE_BASE_PX = 24;
8032
+ function zoomAt(view, anchor, factor, opts) {
8033
+ const fx = typeof factor === "number" ? factor : factor.x;
8034
+ const fy = typeof factor === "number" ? factor : factor.y;
8035
+ const minX = typeof opts?.min === "number" ? opts.min : opts?.min?.x ?? 0.1;
8036
+ const minY = typeof opts?.min === "number" ? opts.min : opts?.min?.y ?? 0.1;
8037
+ const maxX = typeof opts?.max === "number" ? opts.max : opts?.max?.x ?? 8;
8038
+ const maxY = typeof opts?.max === "number" ? opts.max : opts?.max?.y ?? 8;
8039
+ const nextX = Math.min(maxX, Math.max(minX, view.scale.x * fx));
8040
+ const nextY = Math.min(maxY, Math.max(minY, view.scale.y * fy));
8041
+ const worldX = anchor.x / view.scale.x + view.x;
8042
+ const worldY = anchor.y / view.scale.y + view.y;
8043
+ return {
8044
+ scale: { x: nextX, y: nextY },
8045
+ x: worldX - anchor.x / nextX,
8046
+ y: worldY - anchor.y / nextY
8047
+ };
8048
+ }
8049
+ function clampView(view, bounds, canvas) {
8050
+ const visW = canvas.width / view.scale.x;
8051
+ const visH = canvas.height / view.scale.y;
8052
+ let x;
8053
+ if (visW >= bounds.width) {
8054
+ x = bounds.x + (bounds.width - visW) / 2;
8055
+ } else {
8056
+ const minX = bounds.x;
8057
+ const maxX = bounds.x + bounds.width - visW;
8058
+ x = view.x < minX ? minX : view.x > maxX ? maxX : view.x;
8059
+ }
8060
+ let y;
8061
+ if (visH >= bounds.height) {
8062
+ y = bounds.y + (bounds.height - visH) / 2;
8063
+ } else {
8064
+ const minY = bounds.y;
8065
+ const maxY = bounds.y + bounds.height - visH;
8066
+ y = view.y < minY ? minY : view.y > maxY ? maxY : view.y;
8067
+ }
8068
+ return x === view.x && y === view.y ? view : { x, y, scale: view.scale };
8069
+ }
8070
+ function isEditableTarget2(target) {
8071
+ if (!target || !(target instanceof Element)) return false;
8072
+ const tag = target.tagName;
8073
+ if (tag === "INPUT" || tag === "TEXTAREA") return true;
8074
+ if (target.isContentEditable) return true;
8075
+ const ce = target.getAttribute("contenteditable");
8076
+ return ce === "true" || ce === "";
7445
8077
  }
7446
8078
  function resolveParams(params) {
7447
8079
  if (params === void 0) return void 0;
@@ -7474,12 +8106,7 @@ function moveGestureAdapter(scene) {
7474
8106
  return {
7475
8107
  getNode: (id) => scene.get(asNodeId(id)),
7476
8108
  getNodes: () => {
7477
- const out = [];
7478
- for (const id of scene.renderOrder()) {
7479
- const n = scene.get(id);
7480
- if (n) out.push(n);
7481
- }
7482
- return out;
8109
+ return [...scene.renderOrderNodes()];
7483
8110
  },
7484
8111
  getPose: (id) => scene.get(asNodeId(id)).pose,
7485
8112
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -7977,6 +8604,60 @@ var moveAction = {
7977
8604
  */
7978
8605
  enabled: () => true
7979
8606
  };
8607
+ function poseRotationOf(pose) {
8608
+ const p = pose;
8609
+ if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
8610
+ return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
8611
+ }
8612
+ return null;
8613
+ }
8614
+ function rotatePathAround(path, cx, cy, rotation) {
8615
+ if (path.kind === "rect") {
8616
+ return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
8617
+ }
8618
+ return rotatePolygon(path, cx, cy, rotation);
8619
+ }
8620
+ function rotatePolygon(path, cx, cy, rotation) {
8621
+ const cos = Math.cos(rotation);
8622
+ const sin = Math.sin(rotation);
8623
+ const { commands, coords } = path;
8624
+ const next = new Float32Array(coords.length);
8625
+ let ci = 0;
8626
+ for (let i = 0; i < commands.length; i++) {
8627
+ const len = COORD_COUNT3[commands[i]];
8628
+ for (let k = 0; k < len; k += 2) {
8629
+ const dx = coords[ci + k] - cx;
8630
+ const dy = coords[ci + k + 1] - cy;
8631
+ next[ci + k] = cx + dx * cos - dy * sin;
8632
+ next[ci + k + 1] = cy + dx * sin + dy * cos;
8633
+ }
8634
+ ci += len;
8635
+ }
8636
+ return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
8637
+ }
8638
+ function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
8639
+ const cos = Math.cos(rotation);
8640
+ const sin = Math.sin(rotation);
8641
+ const rotate2 = (px, py) => {
8642
+ const dx = px - cx;
8643
+ const dy = py - cy;
8644
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
8645
+ };
8646
+ const [x0, y0] = rotate2(x, y);
8647
+ const [x1, y1] = rotate2(x + w, y);
8648
+ const [x2, y2] = rotate2(x + w, y + h);
8649
+ const [x3, y3] = rotate2(x, y + h);
8650
+ const commands = new Uint8Array([PATH_M2, PATH_L2, PATH_L2, PATH_L2, PATH_Z2]);
8651
+ const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
8652
+ return { kind: "polygon", commands, coords, fillRule: "nonzero" };
8653
+ }
8654
+ var COORD_COUNT3 = {
8655
+ [PATH_M2]: 2,
8656
+ [PATH_L2]: 2,
8657
+ [PATH_C2]: 6,
8658
+ [PATH_Q2]: 4,
8659
+ [PATH_Z2]: 0
8660
+ };
7980
8661
  function aabbCenter(bounds) {
7981
8662
  return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
7982
8663
  }
@@ -7991,12 +8672,7 @@ function defaultCommitAdapter(scene) {
7991
8672
  return {
7992
8673
  getNode: (id) => scene.get(asNodeId(id)),
7993
8674
  getNodes: () => {
7994
- const out = [];
7995
- for (const id of scene.renderOrder()) {
7996
- const n = scene.get(id);
7997
- if (n) out.push(n);
7998
- }
7999
- return out;
8675
+ return [...scene.renderOrderNodes()];
8000
8676
  },
8001
8677
  getPose: (id) => scene.get(asNodeId(id)).pose,
8002
8678
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -8269,6 +8945,13 @@ var resizeAction = {
8269
8945
  const computePose = (id) => {
8270
8946
  const sp = scratch.startPoses.get(id);
8271
8947
  if (sp === void 0) return void 0;
8948
+ if (isGroupPath && poseRotationOf(sp) !== null) {
8949
+ return remapRotatedLeaf(
8950
+ sp,
8951
+ scratch.originBounds,
8952
+ proposedBounds
8953
+ );
8954
+ }
8272
8955
  return scratch.geometry.remapBounds(sp, scratch.originBounds, proposedBounds);
8273
8956
  };
8274
8957
  scratch.previews.clear();
@@ -8889,9 +9572,9 @@ var clearSelectionAction = {
8889
9572
  return true;
8890
9573
  }
8891
9574
  };
8892
- var counter = 0;
9575
+ var counter2 = 0;
8893
9576
  function freshNodeId() {
8894
- return asNodeId(`n${(counter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
9577
+ return asNodeId(`n${(counter2++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
8895
9578
  }
8896
9579
  function translatePose(pose, dx, dy) {
8897
9580
  const p = pose;
@@ -9108,7 +9791,13 @@ function makeViewportZoomAction(opts = {}) {
9108
9791
  };
9109
9792
  }
9110
9793
  makeViewportZoomAction();
9111
- var isBody = (_target, bodyTarget) => bodyTarget === "selected-body" || bodyTarget === "unselected-body";
9794
+ function bodyPredicate(fn) {
9795
+ return Object.assign(fn, { readsAffordance: false });
9796
+ }
9797
+ var isBody = bodyPredicate((_target, bodyTarget) => bodyTarget === "selected-body" || bodyTarget === "unselected-body");
9798
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "selected-body");
9799
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "unselected-body");
9800
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "empty");
9112
9801
  function kindOf(target) {
9113
9802
  if (typeof target !== "object" || target === null) return void 0;
9114
9803
  const k = target.kind;
@@ -9147,17 +9836,17 @@ function splitCubicAtT(p0, p1, p2, p3, t) {
9147
9836
  function fitCubicThroughDeletion(prev, next) {
9148
9837
  const dx = next.x - prev.x;
9149
9838
  const dy = next.y - prev.y;
9150
- const dist4 = Math.sqrt(dx * dx + dy * dy);
9839
+ const dist3 = Math.sqrt(dx * dx + dy * dy);
9151
9840
  const c1DirX = prev.outHandle ? prev.outHandle.x - prev.x : dx;
9152
9841
  const c1DirY = prev.outHandle ? prev.outHandle.y - prev.y : dy;
9153
- const c1 = controlOnSide(prev, c1DirX, c1DirY, dist4, 1 / 3);
9842
+ const c1 = controlOnSide(prev, c1DirX, c1DirY, dist3, 1 / 3);
9154
9843
  const c2DirX = next.inHandle ? next.inHandle.x - next.x : -dx;
9155
9844
  const c2DirY = next.inHandle ? next.inHandle.y - next.y : -dy;
9156
- const c2 = controlOnSide(next, c2DirX, c2DirY, dist4, 1 / 3);
9845
+ const c2 = controlOnSide(next, c2DirX, c2DirY, dist3, 1 / 3);
9157
9846
  return { c1, c2 };
9158
9847
  }
9159
- function controlOnSide(anchor, dirX, dirY, dist4, fraction) {
9160
- const targetDist = dist4 * fraction;
9848
+ function controlOnSide(anchor, dirX, dirY, dist3, fraction) {
9849
+ const targetDist = dist3 * fraction;
9161
9850
  const len = Math.hypot(dirX, dirY) || 1;
9162
9851
  const ux = dirX / len, uy = dirY / len;
9163
9852
  return { x: anchor.x + ux * targetDist, y: anchor.y + uy * targetDist };
@@ -9414,60 +10103,6 @@ function anchorsInRect(set, rect) {
9414
10103
  }
9415
10104
  return hits;
9416
10105
  }
9417
- function poseRotationOf(pose) {
9418
- const p = pose;
9419
- if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
9420
- return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
9421
- }
9422
- return null;
9423
- }
9424
- function rotatePathAround(path, cx, cy, rotation) {
9425
- if (path.kind === "rect") {
9426
- return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
9427
- }
9428
- return rotatePolygon(path, cx, cy, rotation);
9429
- }
9430
- function rotatePolygon(path, cx, cy, rotation) {
9431
- const cos = Math.cos(rotation);
9432
- const sin = Math.sin(rotation);
9433
- const { commands, coords } = path;
9434
- const next = new Float32Array(coords.length);
9435
- let ci = 0;
9436
- for (let i = 0; i < commands.length; i++) {
9437
- const len = COORD_COUNT3[commands[i]];
9438
- for (let k = 0; k < len; k += 2) {
9439
- const dx = coords[ci + k] - cx;
9440
- const dy = coords[ci + k + 1] - cy;
9441
- next[ci + k] = cx + dx * cos - dy * sin;
9442
- next[ci + k + 1] = cy + dx * sin + dy * cos;
9443
- }
9444
- ci += len;
9445
- }
9446
- return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
9447
- }
9448
- function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
9449
- const cos = Math.cos(rotation);
9450
- const sin = Math.sin(rotation);
9451
- const rotate2 = (px, py) => {
9452
- const dx = px - cx;
9453
- const dy = py - cy;
9454
- return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
9455
- };
9456
- const [x0, y0] = rotate2(x, y);
9457
- const [x1, y1] = rotate2(x + w, y);
9458
- const [x2, y2] = rotate2(x + w, y + h);
9459
- const [x3, y3] = rotate2(x, y + h);
9460
- const commands = new Uint8Array([PATH_M2, PATH_L2, PATH_L2, PATH_L2, PATH_Z2]);
9461
- const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
9462
- return { kind: "polygon", commands, coords, fillRule: "nonzero" };
9463
- }
9464
- var COORD_COUNT3 = {
9465
- [PATH_M2]: 2,
9466
- [PATH_L2]: 2,
9467
- [PATH_C2]: 6,
9468
- [PATH_Q2]: 4,
9469
- [PATH_Z2]: 0
9470
- };
9471
10106
  function pathInPoseFrame(path, pose) {
9472
10107
  if (path.kind === "rect") {
9473
10108
  if (path.x === pose.x && path.y === pose.y && path.width === pose.width && path.height === pose.height) return path;
@@ -11020,7 +11655,7 @@ var LABEL = {
11020
11655
  divide: "Divide",
11021
11656
  crop: "Crop"
11022
11657
  };
11023
- function isEmpty(p) {
11658
+ function isEmpty2(p) {
11024
11659
  return p.commands.length === 0;
11025
11660
  }
11026
11661
  function applyBooleanOp(adapter, op) {
@@ -11058,7 +11693,7 @@ function applyBooleanOp(adapter, op) {
11058
11693
  break;
11059
11694
  }
11060
11695
  }
11061
- results = results.filter((p) => !isEmpty(p));
11696
+ results = results.filter((p) => !isEmpty2(p));
11062
11697
  if (results.length === 0) return { kind: "noop", reason: "empty-result" };
11063
11698
  const topmostId = entries[entries.length - 1].id;
11064
11699
  const topAnchor = adapter.getZOrder?.(topmostId);
@@ -11594,14 +12229,24 @@ var DEFAULT_PALETTE = [
11594
12229
  "#7ab8d4",
11595
12230
  "#d47a7a"
11596
12231
  ];
12232
+ function resolveFill(scratch, prev) {
12233
+ if (scratch.currentPaint) return scratch.currentPaint;
12234
+ const prevColor = typeof prev?.fill === "string" ? prev.fill : DEFAULT_FILL_COLOR;
12235
+ return mergeAlphaFromPrev(scratch.currentColor, prevColor);
12236
+ }
11597
12237
  function refreshPreviews(scratch) {
11598
12238
  scratch.previews.clear();
11599
12239
  for (const id of scratch.ids) {
11600
12240
  const prev = scratch.startData.get(id);
11601
- const next = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
11602
- scratch.previews.set(id, { ...prev ?? {}, fill: next });
12241
+ scratch.previews.set(id, { ...prev ?? {}, fill: resolveFill(scratch, prev) });
11603
12242
  }
11604
12243
  }
12244
+ function readParams(params) {
12245
+ return {
12246
+ color: params?.color,
12247
+ paint: params?.paint
12248
+ };
12249
+ }
11605
12250
  var setFillAction = {
11606
12251
  id: "setFill",
11607
12252
  label: "Set fill",
@@ -11615,10 +12260,11 @@ var setFillAction = {
11615
12260
  const applyOps = ctx.deps.applyOps;
11616
12261
  if (!selection || !scene) return {};
11617
12262
  const ids = selection.get();
11618
- if (ids.length === 0) return {};
11619
- const ctxColor = ctx.params?.color;
11620
- const optsColor = opts?.params?.color;
11621
- const initialColor = ctxColor ?? optsColor ?? DEFAULT_STROKE_COLOR;
12263
+ if (ids.length === 0) return {};
12264
+ const fromCtx = readParams(ctx.params);
12265
+ const fromOpts = readParams(opts?.params);
12266
+ const initialPaint = fromCtx.paint ?? fromOpts.paint;
12267
+ const initialColor = fromCtx.color ?? fromOpts.color ?? DEFAULT_STROKE_COLOR;
11622
12268
  const startData = /* @__PURE__ */ new Map();
11623
12269
  for (const id of ids) {
11624
12270
  const node = scene.get(id);
@@ -11629,15 +12275,20 @@ var setFillAction = {
11629
12275
  scene,
11630
12276
  startData,
11631
12277
  currentColor: initialColor,
12278
+ currentPaint: initialPaint,
11632
12279
  previews: /* @__PURE__ */ new Map(),
11633
12280
  applyOps
11634
12281
  };
11635
12282
  refreshPreviews(scratch);
11636
12283
  return {
11637
12284
  onMove(moveCtx) {
11638
- const next = moveCtx.params?.color;
11639
- if (next === void 0) return;
11640
- scratch.currentColor = next;
12285
+ const { color, paint } = readParams(moveCtx.params);
12286
+ if (color === void 0 && paint === void 0) return;
12287
+ if (paint !== void 0) scratch.currentPaint = paint;
12288
+ if (color !== void 0) {
12289
+ scratch.currentColor = color;
12290
+ if (paint === void 0) scratch.currentPaint = void 0;
12291
+ }
11641
12292
  refreshPreviews(scratch);
11642
12293
  },
11643
12294
  onEnd(_endCtx, reason) {
@@ -11647,8 +12298,7 @@ var setFillAction = {
11647
12298
  }
11648
12299
  const ops = [];
11649
12300
  for (const id of scratch.ids) {
11650
- const prev = scratch.startData.get(id);
11651
- const merged = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
12301
+ const merged = resolveFill(scratch, scratch.startData.get(id));
11652
12302
  const nodeNow = scratch.scene.get(id);
11653
12303
  if (!nodeNow) continue;
11654
12304
  const from = { ...nodeNow.data };
@@ -12503,13 +13153,82 @@ function modsCount(mods) {
12503
13153
  }
12504
13154
  function targetRank(target) {
12505
13155
  if (target === void 0) return 0;
12506
- if (typeof target !== "string") return 1;
12507
- if (target.startsWith("kind:")) return target.endsWith(":selected") ? 3 : 2;
12508
- if (target.startsWith("affordance:")) return 2;
12509
- return 1;
13156
+ const form = parseTargetSpec(target);
13157
+ if (form === null) return 1;
13158
+ switch (form.form) {
13159
+ case "kind":
13160
+ return form.requireSelected ? 3 : 2;
13161
+ case "affordance":
13162
+ return 2;
13163
+ case "body":
13164
+ return 1;
13165
+ case "predicate":
13166
+ return 1;
13167
+ default: {
13168
+ return 1;
13169
+ }
13170
+ }
13171
+ }
13172
+ function targetConsultsAffordance(specTarget) {
13173
+ if (specTarget === void 0) return false;
13174
+ const form = parseTargetSpec(specTarget);
13175
+ if (form === null) return false;
13176
+ switch (form.form) {
13177
+ case "predicate":
13178
+ return form.kindOf.readsAffordance !== false;
13179
+ case "affordance":
13180
+ return true;
13181
+ case "body":
13182
+ return false;
13183
+ case "kind":
13184
+ return false;
13185
+ default: {
13186
+ return false;
13187
+ }
13188
+ }
13189
+ }
13190
+ function specTargetOf(spec) {
13191
+ return "target" in spec ? spec.target : void 0;
13192
+ }
13193
+ function claimOf(e) {
13194
+ return "affordance" in e ? e.affordance : void 0;
13195
+ }
13196
+ function claimGestureOf(e) {
13197
+ switch (e.kind) {
13198
+ case "pointerdown":
13199
+ case "click":
13200
+ return "pointer";
13201
+ case "doubleclick":
13202
+ return "doubleClick";
13203
+ case "contextmenu":
13204
+ return "contextMenu";
13205
+ case "longpress":
13206
+ return "longPress";
13207
+ case "wheel":
13208
+ return "wheel";
13209
+ default:
13210
+ return null;
13211
+ }
13212
+ }
13213
+ function isExclusiveClaim(e) {
13214
+ const claim = claimOf(e);
13215
+ if (claim?.strength !== "exclusive") return false;
13216
+ const gesture = claimGestureOf(e);
13217
+ if (gesture === null) return false;
13218
+ return claim.claimedKinds === void 0 || claim.claimedKinds.includes(gesture);
13219
+ }
13220
+ var warnedDeadClaims = /* @__PURE__ */ new Set();
13221
+ function reportDeadClaim(owner, warn) {
13222
+ if (process.env.NODE_ENV === "production") return;
13223
+ const key = String(owner);
13224
+ if (warnedDeadClaims.has(key)) return;
13225
+ warnedDeadClaims.add(key);
13226
+ warn(
13227
+ `[weasel] exclusive claim by "${key}" matched no binding: no \`affordance:\` or \`kindOf\` target resolved against it, so the press was dropped.`
13228
+ );
12510
13229
  }
12511
13230
  function specificity(spec) {
12512
- const t = targetRank("target" in spec ? spec.target : void 0);
13231
+ const t = targetRank(specTargetOf(spec));
12513
13232
  const mods = "mods" in spec ? spec.mods : void 0;
12514
13233
  const m = modsCount(mods);
12515
13234
  const p = "phase" in spec && spec.phase !== void 0 ? 1 : 0;
@@ -12524,12 +13243,14 @@ function compareSpecificity(a, b) {
12524
13243
  }
12525
13244
  return 0;
12526
13245
  }
12527
- function matchSorted(e, bindings, isMac, engagedChannels) {
13246
+ function matchSorted(e, bindings, isMac, engagedChannels, warn = (m) => console.warn(m)) {
12528
13247
  const out = [];
12529
13248
  const engaged = engagedChannels ?? EMPTY_ENGAGED2;
13249
+ const exclusive = isExclusiveClaim(e);
13250
+ const pool = exclusive ? bindings.filter((sb) => targetConsultsAffordance(specTargetOf(sb.binding.spec))) : bindings;
12530
13251
  for (const scope of SCOPE_PRIORITY) {
12531
13252
  const scopeMatches = [];
12532
- for (const sb of bindings) {
13253
+ for (const sb of pool) {
12533
13254
  if (sb.scope !== scope) continue;
12534
13255
  const phaseCtx = { selfChannel: sb.ownerToolId, engagedChannels: engaged };
12535
13256
  if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
@@ -12539,9 +13260,32 @@ function matchSorted(e, bindings, isMac, engagedChannels) {
12539
13260
  scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
12540
13261
  out.push(...scopeMatches);
12541
13262
  }
13263
+ if (exclusive && out.length === 0 && bindings.length > 0) {
13264
+ reportDeadClaim(claimOf(e)?.owner, warn);
13265
+ }
12542
13266
  return out;
12543
13267
  }
12544
13268
  var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
13269
+ function liveScope(id, eligibility, state) {
13270
+ const tags = eligibility.capabilities;
13271
+ if (tags && tags.length > 0 && state.allows && !state.allows(tags)) return null;
13272
+ if (state.engagedIds?.has(id)) return "hotkey";
13273
+ if (eligibility.offhand && state.heldTriggers.has(eligibility.offhand)) return "hotkey";
13274
+ if (eligibility.focus && state.focusedId === id) return "active";
13275
+ if (eligibility.always || eligibility.claimed) return "ambient";
13276
+ return null;
13277
+ }
13278
+ function scopeBindings(entries, state) {
13279
+ const out = [];
13280
+ for (const entry of entries) {
13281
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
13282
+ if (scope === null) continue;
13283
+ for (const binding of entry.bindings ?? []) {
13284
+ out.push({ binding, scope, ownerToolId: entry.id });
13285
+ }
13286
+ }
13287
+ return out;
13288
+ }
12545
13289
  var ALWAYS = { all: [] };
12546
13290
  var NEVER = { any: [] };
12547
13291
  function isAllRule(r) {
@@ -12836,6 +13580,7 @@ function filterEligible(matches, actionLookup, ruleCtx) {
12836
13580
  return isEligible(action, ruleCtx);
12837
13581
  });
12838
13582
  }
13583
+ var EMPTY_TRIGGERS = /* @__PURE__ */ new Set();
12839
13584
  var EMPTY_ENGAGED22 = /* @__PURE__ */ new Set();
12840
13585
  function modifiersOf(e) {
12841
13586
  return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
@@ -12943,34 +13688,23 @@ function createDispatcher(opts) {
12943
13688
  return `ongoing-${event.kind}`;
12944
13689
  }
12945
13690
  function assembleScopedBindings(ctx) {
12946
- const result = [];
12947
- for (const toolId of ctx.hotkeyStack) {
12948
- const tool = ctx.toolsById.get(toolId);
12949
- for (const binding of tool?.bindings ?? []) {
12950
- result.push({ binding, scope: "hotkey", ownerToolId: toolId });
12951
- }
12952
- }
12953
- const activeTool = ctx.toolsById.get(ctx.activeToolId);
12954
- for (const binding of activeTool?.bindings ?? []) {
12955
- result.push({ binding, scope: "active", ownerToolId: ctx.activeToolId });
12956
- }
12957
- for (const toolId of ctx.ambientToolIds ?? []) {
12958
- const tool = ctx.toolsById.get(toolId);
12959
- for (const binding of tool?.bindings ?? []) {
12960
- result.push({ binding, scope: "ambient", ownerToolId: toolId });
12961
- }
12962
- }
13691
+ const ordered = [];
13692
+ for (const id of ctx.hotkeyStack) {
13693
+ const tool = ctx.toolsById.get(id);
13694
+ if (tool && !ordered.includes(tool)) ordered.push(tool);
13695
+ }
13696
+ for (const tool of ctx.toolsById.values()) {
13697
+ if (!ordered.includes(tool)) ordered.push(tool);
13698
+ }
13699
+ const result = scopeBindings(ordered, {
13700
+ focusedId: ctx.activeToolId,
13701
+ heldTriggers: EMPTY_TRIGGERS,
13702
+ engagedIds: new Set(ctx.hotkeyStack)
13703
+ });
12963
13704
  for (const action of ctx.actions.list()) {
12964
- const gs = action.defaultBinding;
12965
- if (!gs) continue;
12966
- const raw = Array.isArray(gs) ? gs : [gs];
12967
- for (const entry of raw) {
12968
- const isBoundObj = !("kind" in entry);
12969
- const spec = isBoundObj ? entry.spec : entry;
12970
- const opts2 = isBoundObj ? entry.opts : void 0;
12971
- const defaultBinding = { spec, actionId: action.id, ...opts2 !== void 0 ? { opts: opts2 } : {} };
12972
- const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
12973
- result.push({ binding: defaultBinding, scope: targetScope, ownerToolId: null });
13705
+ const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
13706
+ for (const binding of actionBindings(action)) {
13707
+ result.push({ binding, scope: targetScope, ownerToolId: null });
12974
13708
  }
12975
13709
  }
12976
13710
  return result;
@@ -13125,15 +13859,26 @@ function createDispatcher(opts) {
13125
13859
  deltaY: event.deltaY,
13126
13860
  clientX: event.clientX,
13127
13861
  clientY: event.clientY,
13862
+ affordance: event.affordance,
13128
13863
  ...resolved
13129
13864
  };
13130
13865
  } else if (event.kind === "click" || event.kind === "doubleclick") {
13131
13866
  params = {
13132
13867
  worldX: event.worldX,
13133
13868
  worldY: event.worldY,
13869
+ affordance: event.affordance,
13134
13870
  // Press point as well as release point — an action that places
13135
13871
  // geometry at the click wants the former. See `ClickEvent`.
13136
- ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY, affordance: event.affordance } : {},
13872
+ ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
13873
+ mods: modifiersOf(event),
13874
+ ...resolved
13875
+ };
13876
+ } else if (event.kind === "contextmenu" || event.kind === "longpress") {
13877
+ params = {
13878
+ worldX: event.kind === "contextmenu" ? event.worldX : event.x,
13879
+ worldY: event.kind === "contextmenu" ? event.worldY : event.y,
13880
+ affordance: event.affordance,
13881
+ bodyTarget: event.bodyTarget,
13137
13882
  mods: modifiersOf(event),
13138
13883
  ...resolved
13139
13884
  };
@@ -13501,7 +14246,7 @@ function computeMultiTouchGeometry(positions) {
13501
14246
  return { centroid, spread };
13502
14247
  }
13503
14248
  function useGestureDispatcher(opts) {
13504
- const { canvasRef, actions, toolsById, ambientToolIds, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick } = opts;
14249
+ const { canvasRef, actions, toolsById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick } = opts;
13505
14250
  const onDoubleClickRef = useRef(onDoubleClick);
13506
14251
  onDoubleClickRef.current = onDoubleClick;
13507
14252
  const activeTool = useActiveToolContext();
@@ -13516,7 +14261,6 @@ function useGestureDispatcher(opts) {
13516
14261
  activeToolId: activeTool.active,
13517
14262
  hotkeyStack: activeTool.hotkeyStack,
13518
14263
  toolsById,
13519
- ambientToolIds,
13520
14264
  isMac: IS_MAC,
13521
14265
  getRuleCtx
13522
14266
  });
@@ -13526,7 +14270,6 @@ function useGestureDispatcher(opts) {
13526
14270
  activeToolId: activeTool.active,
13527
14271
  hotkeyStack: activeTool.hotkeyStack,
13528
14272
  toolsById,
13529
- ambientToolIds,
13530
14273
  isMac: IS_MAC,
13531
14274
  getRuleCtx
13532
14275
  };
@@ -13604,6 +14347,7 @@ function useGestureDispatcher(opts) {
13604
14347
  ctrlKey: down.ctrlKey,
13605
14348
  metaKey: down.metaKey,
13606
14349
  shiftKey: down.shiftKey,
14350
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
13607
14351
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
13608
14352
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
13609
14353
  };
@@ -13613,11 +14357,15 @@ function useGestureDispatcher(opts) {
13613
14357
  y: down.worldY,
13614
14358
  clientX: down.clientX,
13615
14359
  clientY: down.clientY,
13616
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
13617
14360
  ...shared
13618
14361
  });
13619
14362
  if (result === "unhandled") {
13620
- dispatch2({ kind: "contextmenu", ...shared });
14363
+ dispatch2({
14364
+ kind: "contextmenu",
14365
+ worldX: down.worldX,
14366
+ worldY: down.worldY,
14367
+ ...shared
14368
+ });
13621
14369
  }
13622
14370
  };
13623
14371
  const DOUBLE_CLICK_MAX_MS = 600;
@@ -13672,6 +14420,9 @@ function useGestureDispatcher(opts) {
13672
14420
  const onWheel = (e) => {
13673
14421
  const rect = canvas?.getBoundingClientRect();
13674
14422
  const [localX, localY] = rect ? clientToCanvasRect(rect, e.clientX, e.clientY) : [e.clientX, e.clientY];
14423
+ const screenPoint = { x: e.clientX, y: e.clientY };
14424
+ const affordance = affordanceAtRef.current?.(screenPoint) ?? void 0;
14425
+ const body = classifyTargetRef.current?.(screenPoint);
13675
14426
  const ev = {
13676
14427
  kind: "wheel",
13677
14428
  altKey: e.altKey,
@@ -13681,7 +14432,10 @@ function useGestureDispatcher(opts) {
13681
14432
  deltaX: e.deltaX,
13682
14433
  deltaY: e.deltaY,
13683
14434
  clientX: localX,
13684
- clientY: localY
14435
+ clientY: localY,
14436
+ ...affordance !== void 0 ? { affordance } : {},
14437
+ ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
14438
+ ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
13685
14439
  };
13686
14440
  const result = dispatch2(ev);
13687
14441
  if (result === "handled") {
@@ -13975,6 +14729,7 @@ function useGestureDispatcher(opts) {
13975
14729
  shiftKey: e.shiftKey,
13976
14730
  worldX: wClick.x,
13977
14731
  worldY: wClick.y,
14732
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
13978
14733
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
13979
14734
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
13980
14735
  };
@@ -14016,8 +14771,10 @@ function useGestureDispatcher(opts) {
14016
14771
  };
14017
14772
  const onContextMenu = (e) => {
14018
14773
  e.preventDefault();
14019
- const worldPoint = { x: e.clientX, y: e.clientY };
14020
- const menuBody = classifyTargetRef.current?.(worldPoint);
14774
+ const screenPoint = { x: e.clientX, y: e.clientY };
14775
+ const affordance = affordanceAtRef.current?.(screenPoint) ?? void 0;
14776
+ const menuBody = classifyTargetRef.current?.(screenPoint);
14777
+ const w = toWorld(e.clientX, e.clientY);
14021
14778
  const ev = {
14022
14779
  kind: "contextmenu",
14023
14780
  target: e.target,
@@ -14025,6 +14782,9 @@ function useGestureDispatcher(opts) {
14025
14782
  ctrlKey: e.ctrlKey,
14026
14783
  metaKey: e.metaKey,
14027
14784
  shiftKey: e.shiftKey,
14785
+ worldX: w.x,
14786
+ worldY: w.y,
14787
+ ...affordance !== void 0 ? { affordance } : {},
14028
14788
  ...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
14029
14789
  ...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
14030
14790
  };
@@ -14323,47 +15083,203 @@ function usePinchGesture(canvasRef, onPinch, enabled = true) {
14323
15083
  };
14324
15084
  }, [canvasRef, enabled]);
14325
15085
  }
14326
- function useTools(opts) {
14327
- if (!(opts.active in opts.registry)) {
14328
- throw new Error(`useTools: active "${opts.active}" not in registry`);
15086
+ var TOOL_OFFHAND_ID = "tool.offhand";
15087
+ function offhandKeyFor(trigger) {
15088
+ switch (trigger) {
15089
+ case "space":
15090
+ return " ";
15091
+ case "alt":
15092
+ return "Alt";
15093
+ case "ctrl":
15094
+ return "Control";
15095
+ case "meta":
15096
+ return "Meta";
15097
+ case "shift":
15098
+ return "Shift";
14329
15099
  }
15100
+ }
15101
+ function buildToolOffhandBindings(specs) {
15102
+ return specs.map(({ toolId, key }) => ({
15103
+ spec: { kind: "key-held", key },
15104
+ opts: { params: { toolId } }
15105
+ }));
15106
+ }
15107
+ function makeToolOffhandAction(bindings) {
15108
+ return {
15109
+ id: TOOL_OFFHAND_ID,
15110
+ label: "Engage tool (offhand)",
15111
+ defaultBinding: bindings,
15112
+ scope: "hotkey",
15113
+ invoker: {
15114
+ timing: "ongoing",
15115
+ start: (ctx, opts) => {
15116
+ const activeTool = ctx.deps.activeTool;
15117
+ const toolId = resolveParams(opts?.params)?.toolId;
15118
+ if (!activeTool || !toolId) return {};
15119
+ activeTool.pushHotkey(toolId);
15120
+ return {
15121
+ onEnd: () => {
15122
+ activeTool.popHotkey();
15123
+ }
15124
+ };
15125
+ }
15126
+ },
15127
+ requires: ["activeTool"]
15128
+ };
15129
+ }
15130
+ var NO_TRIGGERS = /* @__PURE__ */ new Set();
15131
+ function useContributions(opts) {
14330
15132
  const ctx = useActiveToolContext();
15133
+ const actionsRegistry = useActionsRegistry();
14331
15134
  const hasInitializedRef = useRef(false);
14332
15135
  const isFirstRender = !hasInitializedRef.current;
14333
15136
  const needsFirstMountSyncRef = useRef(false);
14334
15137
  if (isFirstRender) {
14335
15138
  hasInitializedRef.current = true;
14336
- needsFirstMountSyncRef.current = ctx.active === "select" && opts.active !== "select";
15139
+ needsFirstMountSyncRef.current = ctx.active === "select" && opts.focused !== "select";
14337
15140
  }
14338
15141
  useEffect(() => {
14339
15142
  if (!needsFirstMountSyncRef.current) return;
14340
15143
  needsFirstMountSyncRef.current = false;
14341
- ctx.setActive(opts.active);
15144
+ ctx.setActive(opts.focused);
15145
+ }, []);
15146
+ const focused2 = isFirstRender && ctx.active === "select" && opts.focused !== "select" ? opts.focused : ctx.active;
15147
+ const entriesRef = useRef(opts.entries);
15148
+ entriesRef.current = opts.entries;
15149
+ const focusedRef = useRef(focused2);
15150
+ focusedRef.current = focused2;
15151
+ const engagedRef = useRef(ctx.hotkeyStack);
15152
+ engagedRef.current = ctx.hotkeyStack;
15153
+ const setFocused = useCallback((id) => {
15154
+ ctx.setActive(id);
15155
+ }, [ctx]);
15156
+ const snapshot = useCallback(() => {
15157
+ const engaged = engagedRef.current;
15158
+ const engagedIds = new Set(engaged);
15159
+ const ordered = [];
15160
+ for (const id of engaged) {
15161
+ const entry = entriesRef.current.find((e) => e.id === id);
15162
+ if (entry && !ordered.includes(entry)) ordered.push(entry);
15163
+ }
15164
+ for (const entry of entriesRef.current) {
15165
+ if (!ordered.includes(entry)) ordered.push(entry);
15166
+ }
15167
+ return {
15168
+ ordered,
15169
+ state: { focusedId: focusedRef.current, heldTriggers: NO_TRIGGERS, engagedIds }
15170
+ };
14342
15171
  }, []);
14343
- const active = isFirstRender && ctx.active === "select" && opts.active !== "select" ? opts.active : ctx.active;
14344
- const hotkeyStack = ctx.hotkeyStack;
14345
- const hotkeyEngaged = hotkeyStack.at(-1) ?? null;
14346
- const registryRef = useRef(opts.registry);
14347
- registryRef.current = opts.registry;
14348
- const ambientRef = useRef(opts.ambient ?? []);
14349
- ambientRef.current = opts.ambient ?? [];
14350
- const activeRef = useRef(active);
14351
- activeRef.current = active;
15172
+ const scopedBindings = useCallback(() => {
15173
+ const { ordered, state } = snapshot();
15174
+ return scopeBindings(ordered, state);
15175
+ }, [snapshot]);
15176
+ const overlays = useCallback(() => {
15177
+ const { ordered, state } = snapshot();
15178
+ const active = [];
15179
+ const hotkey = [];
15180
+ const ambient = [];
15181
+ for (const entry of ordered) {
15182
+ if (!entry.overlay) continue;
15183
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
15184
+ if (scope === "active") active.push(entry.overlay);
15185
+ else if (scope === "hotkey") hotkey.push(entry.overlay);
15186
+ else if (scope === "ambient") ambient.push(entry.overlay);
15187
+ }
15188
+ return [...active, ...hotkey, ...ambient];
15189
+ }, [snapshot]);
15190
+ const offhandSig = opts.entries.map((e) => e.eligibility?.offhand ? `${e.id}:${e.eligibility.offhand}` : "").filter(Boolean).join(",");
15191
+ useEffect(() => {
15192
+ if (!actionsRegistry || offhandSig === "") return;
15193
+ const specs = offhandSig.split(",").map((entry) => {
15194
+ const [toolId, trigger] = entry.split(":");
15195
+ return { toolId, key: offhandKeyFor(trigger) };
15196
+ });
15197
+ return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
15198
+ }, [actionsRegistry, offhandSig]);
15199
+ const lastConflictSigRef = useRef(null);
15200
+ const entrySig = opts.entries.map((e) => e.id).join(",");
15201
+ useEffect(() => {
15202
+ if (process.env.NODE_ENV === "production") return;
15203
+ if (lastConflictSigRef.current === entrySig) return;
15204
+ lastConflictSigRef.current = entrySig;
15205
+ const registry22 = [];
15206
+ const ambient = [];
15207
+ for (const entry of entriesRef.current) {
15208
+ (entry.eligibility?.focus ? registry22 : ambient).push(entry);
15209
+ }
15210
+ reportRouteConflicts({
15211
+ registry: registry22,
15212
+ ambient,
15213
+ actions: actionsRegistry?.list() ?? []
15214
+ });
15215
+ }, [entrySig, actionsRegistry]);
15216
+ return useMemo(
15217
+ () => ({
15218
+ entries: entriesRef.current,
15219
+ focused: focused2,
15220
+ setFocused,
15221
+ scopedBindings,
15222
+ overlays
15223
+ }),
15224
+ [focused2, setFocused, scopedBindings, overlays]
15225
+ );
15226
+ }
15227
+ function declareSlot(tool, slot) {
15228
+ const hotkey = tool.def?.hotkey;
15229
+ const eligibility = {
15230
+ ...tool.eligibility,
15231
+ [slot]: true,
15232
+ ...hotkey ? { offhand: hotkey } : {}
15233
+ };
15234
+ return sameEligibility(tool.eligibility, eligibility) ? tool : { ...tool, eligibility };
15235
+ }
15236
+ function sameEligibility(a, b) {
15237
+ if (!a) return false;
15238
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
15239
+ for (const key of keys) if (a[key] !== b[key]) return false;
15240
+ return true;
15241
+ }
15242
+ function useTools(opts) {
15243
+ if (!(opts.active in opts.registry)) {
15244
+ throw new Error(`useTools: active "${opts.active}" not in registry`);
15245
+ }
15246
+ const ctx = useActiveToolContext();
15247
+ const slotted = useMemo(() => {
15248
+ const registry22 = {};
15249
+ for (const [id, tool] of Object.entries(opts.registry)) {
15250
+ registry22[id] = declareSlot(tool, "focus");
15251
+ }
15252
+ const ambient = (opts.ambient ?? []).map((t) => declareSlot(t, "always"));
15253
+ const byId2 = /* @__PURE__ */ new Map();
15254
+ for (const tool of [...Object.values(registry22), ...ambient]) {
15255
+ const prior = byId2.get(tool.id);
15256
+ if (prior) byId2.set(tool.id, { ...tool, eligibility: { ...prior.eligibility, ...tool.eligibility } });
15257
+ else byId2.set(tool.id, tool);
15258
+ }
15259
+ return { registry: registry22, ambient, entries: [...byId2.values()] };
15260
+ }, [opts.registry, opts.ambient]);
15261
+ const contributions = useContributions({ entries: slotted.entries, focused: opts.active });
15262
+ const hotkeyEngaged = ctx.hotkeyStack.at(-1) ?? null;
15263
+ const slottedRef = useRef(slotted);
15264
+ slottedRef.current = slotted;
15265
+ const activeRef = useRef(contributions.focused);
15266
+ activeRef.current = contributions.focused;
14352
15267
  const hotkeyRef = useRef(hotkeyEngaged);
14353
15268
  hotkeyRef.current = hotkeyEngaged;
15269
+ const setFocused = contributions.setFocused;
14354
15270
  const setActive = useCallback(
14355
15271
  (id) => {
14356
- if (!(id in registryRef.current)) {
15272
+ if (!(id in slottedRef.current.registry)) {
14357
15273
  throw new Error(`setActive: "${id}" not in registry`);
14358
15274
  }
14359
15275
  dlog("tools", "active:", activeRef.current, "\u2192", id);
14360
- ctx.setActive(id);
15276
+ setFocused(id);
14361
15277
  },
14362
- [ctx]
15278
+ [setFocused]
14363
15279
  );
14364
15280
  const engageHotkey = useCallback(
14365
15281
  (id) => {
14366
- if (!(id in registryRef.current)) {
15282
+ if (!(id in slottedRef.current.registry)) {
14367
15283
  throw new Error(`engageHotkey: "${id}" not in registry`);
14368
15284
  }
14369
15285
  dlog("tools", "hotkey engaged:", id);
@@ -14375,32 +15291,12 @@ function useTools(opts) {
14375
15291
  if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
14376
15292
  ctx.popHotkey();
14377
15293
  }, [ctx]);
14378
- const lastConflictSigRef = useRef(null);
14379
- const toolSetSig = Object.keys(opts.registry).join(",") + "|" + (opts.ambient ?? []).map((t) => t.id).join(",");
14380
- useEffect(() => {
14381
- if (process.env.NODE_ENV === "production") return;
14382
- if (lastConflictSigRef.current === toolSetSig) return;
14383
- lastConflictSigRef.current = toolSetSig;
14384
- reportRouteConflicts({
14385
- registry: registryRef.current,
14386
- ambient: ambientRef.current
14387
- });
14388
- }, [toolSetSig]);
14389
15294
  const has = useCallback(
14390
- (id) => id in registryRef.current || ambientRef.current.some((t) => t.id === id),
15295
+ (id) => id in slottedRef.current.registry || slottedRef.current.ambient.some((t) => t.id === id),
14391
15296
  []
14392
15297
  );
14393
- const getActiveOverlays = useCallback(() => {
14394
- const out = [];
14395
- const activeTool = registryRef.current[activeRef.current];
14396
- if (activeTool?.overlay) out.push(activeTool.overlay);
14397
- const mod = hotkeyRef.current ? registryRef.current[hotkeyRef.current] : null;
14398
- if (mod?.overlay) out.push(mod.overlay);
14399
- for (const t of ambientRef.current) {
14400
- if (t.overlay) out.push(t.overlay);
14401
- }
14402
- return out;
14403
- }, []);
15298
+ const active = contributions.focused;
15299
+ const getActiveOverlays = contributions.overlays;
14404
15300
  return useMemo(
14405
15301
  () => ({
14406
15302
  active,
@@ -14408,44 +15304,14 @@ function useTools(opts) {
14408
15304
  hotkeyEngaged,
14409
15305
  engageHotkey,
14410
15306
  disengageHotkey,
14411
- ambient: ambientRef.current,
14412
- registry: registryRef.current,
15307
+ ambient: slottedRef.current.ambient,
15308
+ registry: slottedRef.current.registry,
14413
15309
  has,
14414
15310
  getActiveOverlays
14415
15311
  }),
14416
15312
  [active, setActive, hotkeyEngaged, engageHotkey, disengageHotkey, has, getActiveOverlays]
14417
15313
  );
14418
15314
  }
14419
- var TOOL_OFFHAND_ID = "tool.offhand";
14420
- function buildToolOffhandBindings(specs) {
14421
- return specs.map(({ toolId, key }) => ({
14422
- spec: { kind: "key-held", key },
14423
- opts: { params: { toolId } }
14424
- }));
14425
- }
14426
- function makeToolOffhandAction(bindings) {
14427
- return {
14428
- id: TOOL_OFFHAND_ID,
14429
- label: "Engage tool (offhand)",
14430
- defaultBinding: bindings,
14431
- scope: "hotkey",
14432
- invoker: {
14433
- timing: "ongoing",
14434
- start: (ctx, opts) => {
14435
- const activeTool = ctx.deps.activeTool;
14436
- const toolId = resolveParams(opts?.params)?.toolId;
14437
- if (!activeTool || !toolId) return {};
14438
- activeTool.pushHotkey(toolId);
14439
- return {
14440
- onEnd: () => {
14441
- activeTool.popHotkey();
14442
- }
14443
- };
14444
- }
14445
- },
14446
- requires: ["activeTool"]
14447
- };
14448
- }
14449
15315
  var TOOL_ACTIVATE_ID = "tool.activate";
14450
15316
  function buildToolActivateBindings(specs) {
14451
15317
  return specs.map(({ toolId, keyOpts }) => {
@@ -14517,9 +15383,6 @@ var BUILTIN_SELECT_KEYS = {
14517
15383
  hand: { key: "H" },
14518
15384
  pen: { key: "P" }
14519
15385
  };
14520
- var BUILTIN_OFFHAND_ACTIONS = {
14521
- hand: " "
14522
- };
14523
15386
  function useKeybindings(tools, options = {}) {
14524
15387
  const toolsRef = useRef(tools);
14525
15388
  toolsRef.current = tools;
@@ -14527,19 +15390,6 @@ function useKeybindings(tools, options = {}) {
14527
15390
  optionsRef.current = options;
14528
15391
  const initialActiveRef = useRef(tools.active);
14529
15392
  const registry22 = useActionsRegistry();
14530
- useEffect(() => {
14531
- if (optionsRef.current.disable) return;
14532
- if (!registry22) return;
14533
- const specs = [];
14534
- for (const [toolId, key] of Object.entries(BUILTIN_OFFHAND_ACTIONS)) {
14535
- if (toolsRef.current.has(toolId)) {
14536
- specs.push({ toolId, key });
14537
- }
14538
- }
14539
- if (specs.length === 0) return;
14540
- const bindings = buildToolOffhandBindings(specs);
14541
- return registry22.register(makeToolOffhandAction(bindings));
14542
- }, [registry22, tools]);
14543
15393
  useEffect(() => {
14544
15394
  if (optionsRef.current.disable) return;
14545
15395
  if (!registry22) return;
@@ -14912,21 +15762,18 @@ function textLineBoxes(pose, opts = {}) {
14912
15762
  const style = resolveTextStyle(pose.style);
14913
15763
  const source = pose.runs && pose.runs.length > 0 ? pose.runs : pose.text;
14914
15764
  const runs = resolveRuns(toRuns(source), style);
14915
- const laid = layoutRuns(
14916
- runs,
14917
- {
14918
- maxWidth: opts.maxWidth ?? pose.width,
14919
- lineHeight: style.lineHeight,
14920
- align: style.align
14921
- },
14922
- { x: pose.x, y: pose.y }
14923
- );
14924
- const dy = verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
15765
+ const laid = layoutRuns(runs, {
15766
+ maxWidth: opts.maxWidth ?? pose.width,
15767
+ lineHeight: style.lineHeight,
15768
+ align: style.align
15769
+ });
15770
+ const dx = pose.x;
15771
+ const dy = pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
14925
15772
  const out = [];
14926
15773
  for (const line of laid.lines) {
14927
15774
  if (!opts.includeEmpty && line.x1 <= line.x0) continue;
14928
15775
  out.push({
14929
- x: line.x0 - padding,
15776
+ x: line.x0 + dx - padding,
14930
15777
  y: line.y0 + dy - padding,
14931
15778
  width: line.x1 - line.x0 + padding * 2,
14932
15779
  height: line.y1 - line.y0 + padding * 2
@@ -14934,7 +15781,82 @@ function textLineBoxes(pose, opts = {}) {
14934
15781
  }
14935
15782
  return out;
14936
15783
  }
15784
+ function fillInPoseFrame(fill, box) {
15785
+ if (isBoundsPattern(fill)) {
15786
+ const o = fill.origin ?? { x: 0, y: 0 };
15787
+ return { ...fill, origin: { x: box.x + o.x, y: box.y + o.y }, units: "local" };
15788
+ }
15789
+ if (!isBoundsGradient(fill)) return fill;
15790
+ const toBox = (p) => ({
15791
+ x: box.x + p.x * box.width,
15792
+ y: box.y + p.y * box.height
15793
+ });
15794
+ if (fill.fill === "linear-gradient") {
15795
+ return { ...fill, from: toBox(fill.from), to: toBox(fill.to), units: "local" };
15796
+ }
15797
+ if (fill.fill === "radial-gradient") {
15798
+ const scale2 = Math.hypot(box.width, box.height) / Math.SQRT2;
15799
+ return { ...fill, center: toBox(fill.center), radius: fill.radius * scale2, units: "local" };
15800
+ }
15801
+ return { ...fill, center: toBox(fill.center), units: "local" };
15802
+ }
15803
+ function isBoundsPattern(fill) {
15804
+ return fill.fill === "pattern" && fill.units === "bounds";
15805
+ }
15806
+ function isBoundsGradient(fill) {
15807
+ if (fill === void 0) return false;
15808
+ const kind = fill.fill;
15809
+ if (kind !== "linear-gradient" && kind !== "radial-gradient" && kind !== "conic-gradient") {
15810
+ return false;
15811
+ }
15812
+ return fill.units === "bounds";
15813
+ }
14937
15814
  var cache5 = /* @__PURE__ */ new Map();
15815
+ function specKey(spec) {
15816
+ const keys = Object.keys(spec).sort();
15817
+ const bag = spec;
15818
+ return keys.map((k) => `${k}=${String(bag[k])}`).join("&");
15819
+ }
15820
+ function resolvePatternSpec(spec) {
15821
+ const key = specKey(spec);
15822
+ const hit = cache5.get(key);
15823
+ if (hit !== void 0) return hit;
15824
+ const built = buildTile(spec);
15825
+ cache5.set(key, built);
15826
+ return built;
15827
+ }
15828
+ function buildTile(spec) {
15829
+ const { color, size } = spec;
15830
+ switch (spec.tile) {
15831
+ case "hatch":
15832
+ return hatch({ color, size, lineWidth: spec.lineWidth });
15833
+ case "crosshatch":
15834
+ return crosshatch({ color, size, lineWidth: spec.lineWidth });
15835
+ case "dots":
15836
+ return dots({ color, size, radius: spec.radius });
15837
+ case "chunks":
15838
+ return chunks({
15839
+ color,
15840
+ size,
15841
+ bg: spec.bg,
15842
+ density: spec.density,
15843
+ chunkSize: spec.chunkSize,
15844
+ seed: spec.seed
15845
+ });
15846
+ default:
15847
+ return null;
15848
+ }
15849
+ }
15850
+ function isPatternSpec(payload) {
15851
+ return "tile" in payload;
15852
+ }
15853
+ function resolveFillPattern(fill) {
15854
+ if (fill.fill !== "pattern") return fill;
15855
+ if (!isPatternSpec(fill.pattern)) return fill;
15856
+ const handle = resolvePatternSpec(fill.pattern);
15857
+ return handle ? { ...fill, pattern: handle } : null;
15858
+ }
15859
+ var cache6 = /* @__PURE__ */ new Map();
14938
15860
  var subscribers2 = /* @__PURE__ */ new Set();
14939
15861
  var defaultLoader = async (src) => {
14940
15862
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -14952,28 +15874,28 @@ function notify() {
14952
15874
  }
14953
15875
  function startLoad(src) {
14954
15876
  const entry = { status: "loading" };
14955
- cache5.set(src, entry);
15877
+ cache6.set(src, entry);
14956
15878
  loader(src).then(
14957
15879
  (bitmap) => {
14958
- if (cache5.get(src) !== entry) return;
14959
- cache5.set(src, { status: "ready", bitmap });
15880
+ if (cache6.get(src) !== entry) return;
15881
+ cache6.set(src, { status: "ready", bitmap });
14960
15882
  notify();
14961
15883
  },
14962
15884
  () => {
14963
- if (cache5.get(src) !== entry) return;
14964
- cache5.set(src, { status: "error" });
15885
+ if (cache6.get(src) !== entry) return;
15886
+ cache6.set(src, { status: "error" });
14965
15887
  notify();
14966
15888
  }
14967
15889
  );
14968
15890
  }
14969
15891
  function getImageBitmap(src) {
14970
- const entry = cache5.get(src);
15892
+ const entry = cache6.get(src);
14971
15893
  if (entry) return entry.bitmap;
14972
15894
  startLoad(src);
14973
15895
  return void 0;
14974
15896
  }
14975
15897
  function imageStatus(src) {
14976
- return cache5.get(src)?.status ?? "idle";
15898
+ return cache6.get(src)?.status ?? "idle";
14977
15899
  }
14978
15900
  function subscribeImageReady(cb) {
14979
15901
  subscribers2.add(cb);
@@ -15151,6 +16073,17 @@ function rectsToPath(rects) {
15151
16073
  }
15152
16074
  return { kind: "polygon", commands, coords, fillRule: "nonzero" };
15153
16075
  }
16076
+ function isFillStyle(v) {
16077
+ return typeof v === "object" && v !== null;
16078
+ }
16079
+ function resolveNodeFill(fill, color, hasStroke, fallback) {
16080
+ if (isFillStyle(fill)) return fill;
16081
+ const c = fill ?? color;
16082
+ if (c === "none") return null;
16083
+ if (c !== void 0) return { color: c };
16084
+ if (hasStroke && fallback === null) return null;
16085
+ return fallback === null ? null : { color: fallback };
16086
+ }
15154
16087
  var PATH_PAINTER = {
15155
16088
  id: "kit:path",
15156
16089
  matches: (node) => {
@@ -15164,12 +16097,12 @@ var PATH_PAINTER = {
15164
16097
  const d = node.data;
15165
16098
  const projected = pathInPoseFrame(d.path, pose);
15166
16099
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
15167
- const fillColor = d.fill ?? d.color;
15168
- const hasFill = fillColor !== "none" && (fillColor !== void 0 || !hasStroke);
16100
+ const declared = resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888");
16101
+ const fill = declared && resolveFillPattern(fillInPoseFrame(declared, pose));
15169
16102
  const cmd = {
15170
16103
  kind: "path",
15171
16104
  path: projected,
15172
- ...hasFill ? { fill: { color: fillColor ?? "#888" } } : {},
16105
+ ...fill ? { fill } : {},
15173
16106
  ...hasStroke ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
15174
16107
  };
15175
16108
  return [cmd];
@@ -15185,9 +16118,8 @@ var PATH_PAINTER = {
15185
16118
  ink: (node) => {
15186
16119
  const d = node.data;
15187
16120
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
15188
- const fillColor = d.fill ?? d.color;
15189
16121
  return {
15190
- filled: fillColor !== "none" && (fillColor !== void 0 || !hasStroke),
16122
+ filled: resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888") !== null,
15191
16123
  strokeWidth: hasStroke ? d.strokeWidth ?? 1 : 0
15192
16124
  };
15193
16125
  }
@@ -15204,10 +16136,12 @@ var SHAPE_PAINTER = {
15204
16136
  paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
15205
16137
  const d = node.data;
15206
16138
  const path = pathForShape(d, pose);
16139
+ const declaredFill = resolveNodeFill(d.fill, d.color, false, "#888");
16140
+ const shapeFill = declaredFill && resolveFillPattern(fillInPoseFrame(declaredFill, pose));
15207
16141
  return [{
15208
16142
  kind: "path",
15209
16143
  path,
15210
- fill: { color: d.fill ?? d.color ?? "#888" },
16144
+ ...shapeFill ? { fill: shapeFill } : {},
15211
16145
  ...d.stroke && (d.strokeWidth ?? 0) > 0 ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
15212
16146
  }];
15213
16147
  }),
@@ -15603,11 +16537,8 @@ function useSelectTool(adapter, options) {
15603
16537
  ...moveOpts
15604
16538
  },
15605
16539
  { spec: { kind: "drag", target: "empty" }, actionId: "areaSelect" },
15606
- // Click on empty canvas clear. Same no-affordance requirement as
15607
- // the press binding above, and for the same two reasons: clicking a
15608
- // widget that happens to float over empty canvas isn't a click on
15609
- // empty canvas, and declining in the SPEC lets the chrome's owner
15610
- // have the click instead of it being consumed here.
16540
+ // resize/rotate bind only drag (no click), so a click on either handle
16541
+ // (body genuinely empty) would hit target:'empty' and wrongly clear selection.
15611
16542
  {
15612
16543
  spec: {
15613
16544
  kind: "click",
@@ -15691,17 +16622,17 @@ function worldToLocal(xf, wx, wy) {
15691
16622
  function annulusSemiAxes(shape, view) {
15692
16623
  const bandPx = shape.minBandPx ?? 0;
15693
16624
  if (bandPx === 0) return { rx: shape.rx, ry: shape.ry };
15694
- const band = bandPx / meanScale(view.scale);
16625
+ const band = pxExtent(bandPx, view.scale);
15695
16626
  return {
15696
- rx: Math.max(shape.rx, shape.innerWidth / 2 + band),
15697
- ry: Math.max(shape.ry, shape.innerHeight / 2 + band)
16627
+ rx: Math.max(shape.rx, shape.innerWidth / 2 + band.x),
16628
+ ry: Math.max(shape.ry, shape.innerHeight / 2 + band.y)
15698
16629
  };
15699
16630
  }
15700
16631
  function hitRegion(region, wx, wy, xf, view) {
15701
16632
  const local = worldToLocal(xf, wx, wy);
15702
16633
  if (region.shape.kind === "point") {
15703
- const radiusWorld = region.shape.hitRadiusPx / meanScale(view.scale);
15704
- return Math.abs(local.x - region.shape.x) <= radiusWorld && Math.abs(local.y - region.shape.y) <= radiusWorld;
16634
+ const anchor = localToWorld(xf, region.shape.x, region.shape.y);
16635
+ return withinPxBox(anchor.x - wx, anchor.y - wy, region.shape.hitRadiusPx, view.scale);
15705
16636
  }
15706
16637
  if (region.shape.kind === "annulus") {
15707
16638
  const s = region.shape;
@@ -15765,14 +16696,13 @@ function asIsVisible(data) {
15765
16696
  function recordRegionHitbox(debug, affordanceId, region, xf, view) {
15766
16697
  if (region.shape.kind === "point") {
15767
16698
  const w2 = localToWorld(xf, region.shape.x, region.shape.y);
15768
- const r2 = region.shape.hitRadiusPx / meanScale(view.scale);
16699
+ const e = pxExtent(region.shape.hitRadiusPx, view.scale);
15769
16700
  debug.recordHitbox(affordanceId, "handle", {
15770
16701
  kind: "rect",
15771
- x: w2.x - r2,
15772
- y: w2.y - r2,
15773
- width: r2 * 2,
15774
- height: r2 * 2,
15775
- ...xf.identity ? {} : { rotation: Math.atan2(xf.sin, xf.cos) }
16702
+ x: w2.x - e.x,
16703
+ y: w2.y - e.y,
16704
+ width: e.x * 2,
16705
+ height: e.y * 2
15776
16706
  });
15777
16707
  return;
15778
16708
  }
@@ -15831,7 +16761,7 @@ function paintRegion(region, xf, view, state, viewT, out) {
15831
16761
  if (paint.kind === "annulus") {
15832
16762
  if (region.shape.kind !== "annulus") return;
15833
16763
  const s = region.shape;
15834
- const insetLocal = (paint.insetPx ?? 0) / meanScale(view.scale);
16764
+ const insetLocal = pxExtent(paint.insetPx ?? 0, view.scale);
15835
16765
  const cmd = annulusCommand(s, annulusSemiAxes(s, view), insetLocal, xf, viewT, paint.fill, paint.stroke);
15836
16766
  if (cmd) out.push(cmd);
15837
16767
  return;
@@ -15848,13 +16778,13 @@ function paintRegion(region, xf, view, state, viewT, out) {
15848
16778
  }
15849
16779
  }
15850
16780
  function annulusCommand(shape, semiAxes, insetLocal, xf, viewT, fill, stroke) {
15851
- const rx = semiAxes.rx - insetLocal;
15852
- const ry = semiAxes.ry - insetLocal;
16781
+ const rx = semiAxes.rx - insetLocal.x;
16782
+ const ry = semiAxes.ry - insetLocal.y;
15853
16783
  if (rx <= 0 || ry <= 0) return null;
15854
16784
  const cx = shape.cx;
15855
16785
  const cy = shape.cy;
15856
- const rectHW = shape.innerWidth / 2 + insetLocal;
15857
- const rectHH = shape.innerHeight / 2 + insetLocal;
16786
+ const rectHW = shape.innerWidth / 2 + insetLocal.x;
16787
+ const rectHH = shape.innerHeight / 2 + insetLocal.y;
15858
16788
  const haveTopBottom = rectHH < ry;
15859
16789
  const haveLeftRight = rectHW < rx;
15860
16790
  if (!haveTopBottom && !haveLeftRight) return null;
@@ -16101,6 +17031,7 @@ function useHandTool(opts = {}) {
16101
17031
  () => Object.assign(defineViewportTool({
16102
17032
  id: "hand",
16103
17033
  capabilities: ["navigation"],
17034
+ hotkey: "space",
16104
17035
  hookName: "useHandTool",
16105
17036
  presentation: {
16106
17037
  label: "Hand",
@@ -16221,10 +17152,6 @@ function mirrorHandle(anchor, out) {
16221
17152
  if (!out || anchor.altBroken) return void 0;
16222
17153
  return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
16223
17154
  }
16224
- function dist2(ax, ay, bx, by) {
16225
- const dx = ax - bx, dy = ay - by;
16226
- return Math.hypot(dx, dy);
16227
- }
16228
17155
  function usePenTool(options) {
16229
17156
  const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, snapPoint: snapPoint2 } = options;
16230
17157
  const scratchRef = useRef(null);
@@ -16248,11 +17175,14 @@ function usePenTool(options) {
16248
17175
  const fn = optsRef.current.snapPoint;
16249
17176
  return fn ? fn({ x, y }) : { x, y };
16250
17177
  }, []);
16251
- const closeRadius = useCallback((deps) => {
16252
- const view = deps.view?.get();
16253
- const scale2 = view?.scale ?? { x: 1, y: 1 };
16254
- return optsRef.current.closeHitRadius / meanScale(scale2);
16255
- }, []);
17178
+ const withinCloseRadius = useCallback(
17179
+ (deps, ax, ay, wx, wy) => {
17180
+ const view = deps.view?.get();
17181
+ const scale2 = view?.scale ?? { x: 1, y: 1 };
17182
+ return withinPxRadius(ax - wx, ay - wy, optsRef.current.closeHitRadius, scale2);
17183
+ },
17184
+ []
17185
+ );
16256
17186
  const actions = useMemo(() => {
16257
17187
  const s = () => scratchRef.current;
16258
17188
  return [
@@ -16268,10 +17198,9 @@ function usePenTool(options) {
16268
17198
  if (p?.pressX === void 0 || p.pressY === void 0) return;
16269
17199
  const { x: wx, y: wy } = snap2(p.pressX, p.pressY);
16270
17200
  const scratch = s();
16271
- const radius = closeRadius(deps);
16272
17201
  if (scratch.current && scratch.current.anchors.length >= 3) {
16273
17202
  const first = scratch.current.anchors[0];
16274
- if (dist2(first.x, first.y, wx, wy) <= radius) {
17203
+ if (withinCloseRadius(deps, first.x, first.y, wx, wy)) {
16275
17204
  scratch.current.closed = true;
16276
17205
  scratch.finishedSubpaths.push(scratch.current);
16277
17206
  scratch.current = null;
@@ -16390,7 +17319,7 @@ function usePenTool(options) {
16390
17319
  }
16391
17320
  }
16392
17321
  ];
16393
- }, [commit, snap2, closeRadius]);
17322
+ }, [commit, snap2, withinCloseRadius]);
16394
17323
  return useMemo(() => {
16395
17324
  return defineTool({
16396
17325
  id: "pen",
@@ -17559,6 +18488,13 @@ function wrapWithPoseRotation(cmds, pose) {
17559
18488
  children: cmds
17560
18489
  }];
17561
18490
  }
18491
+ function wrapNodeOutput(cmds, pose, alpha) {
18492
+ const rotated = wrapWithPoseRotation(cmds, pose);
18493
+ if (alpha !== 1 && rotated.length > 0) {
18494
+ return [{ kind: "group", alpha, children: rotated }];
18495
+ }
18496
+ return rotated;
18497
+ }
17562
18498
  function CursorCoordsHud({ canvasRef, viewRef, offset }) {
17563
18499
  const [state, setState] = useState({
17564
18500
  client: { x: 0, y: 0 },
@@ -17806,12 +18742,7 @@ function buildSceneLayer(cfg, adapter, debugSink, boundsOfFn, hideIds, slot) {
17806
18742
  const oy = pose.y ?? (b ? b.y : 0);
17807
18743
  debugSink.recordOrigin(obj.id, { x: ox, y: oy });
17808
18744
  }
17809
- const rotated = wrapWithPoseRotation(cmds, pose);
17810
- const alpha = cfg.alphaFor ? cfg.alphaFor(obj.id) : 1;
17811
- if (alpha !== 1 && rotated.length > 0) {
17812
- return [{ kind: "group", alpha, children: rotated }];
17813
- }
17814
- return rotated;
18745
+ return wrapNodeOutput(cmds, pose, cfg.alphaFor ? cfg.alphaFor(obj.id) : 1);
17815
18746
  };
17816
18747
  const hierarchicalAdapter = cfg.toPose ? {
17817
18748
  ...a,
@@ -17835,13 +18766,8 @@ function buildSceneLayer(cfg, adapter, debugSink, boundsOfFn, hideIds, slot) {
17835
18766
  const pose = toPose(obj);
17836
18767
  if (drawOne) {
17837
18768
  const cmds = drawOne(obj, pose, view);
17838
- const rotated = wrapWithPoseRotation(cmds, pose);
17839
- const alpha = cfg.alphaFor ? cfg.alphaFor(obj.id) : 1;
17840
- if (alpha !== 1 && rotated.length > 0) {
17841
- children.push({ kind: "group", alpha, children: rotated });
17842
- } else {
17843
- for (const cmd of rotated) children.push(cmd);
17844
- }
18769
+ const wrapped = wrapNodeOutput(cmds, pose, cfg.alphaFor ? cfg.alphaFor(obj.id) : 1);
18770
+ for (const cmd of wrapped) children.push(cmd);
17845
18771
  }
17846
18772
  if (debugSink) {
17847
18773
  const b = boundsOfFn ? boundsOfFn(obj.id) : null;
@@ -17956,8 +18882,8 @@ function CanvasInner(props, ref) {
17956
18882
  const isVisible = getIsVisibleRef.current?.() ?? alwaysVisible;
17957
18883
  for (const layer of layers2) {
17958
18884
  if (!layer.hitTest) continue;
17959
- const binding = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
17960
- if (binding) return { layerId: layer.id, binding };
18885
+ const hit = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
18886
+ if (hit) return { layerId: layer.id, hit };
17961
18887
  }
17962
18888
  return null;
17963
18889
  }, []);
@@ -18393,7 +19319,7 @@ function CanvasInner(props, ref) {
18393
19319
  effectiveView,
18394
19320
  { width, height }
18395
19321
  );
18396
- renderer.render(commands);
19322
+ renderer.render(commands, viewToMat3(effectiveView));
18397
19323
  }, [layersWithDebug, width, height, effectiveView, debugSink, redrawNonce, dprProp]);
18398
19324
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
18399
19325
  useEffect(() => {
@@ -18555,6 +19481,7 @@ function createScene(options) {
18555
19481
  return i;
18556
19482
  }
18557
19483
  function rebuildLayerIndex() {
19484
+ invalidateOrder();
18558
19485
  state.layerIndex.clear();
18559
19486
  for (let i = 0; i < state.layers.length; i++) {
18560
19487
  state.layerIndex.set(state.layers[i].id, i);
@@ -18580,6 +19507,7 @@ function createScene(options) {
18580
19507
  const idx = sibs.indexOf(id);
18581
19508
  if (idx < 0) throw new Error(`Scene: node "${id}" not found in its parent's children`);
18582
19509
  sibs.splice(idx, 1);
19510
+ invalidateOrder();
18583
19511
  return { parent: node.parent, index: idx };
18584
19512
  }
18585
19513
  function attach(id, parent, index) {
@@ -18588,6 +19516,7 @@ function createScene(options) {
18588
19516
  sibs.splice(i, 0, id);
18589
19517
  const node = requireNode(id);
18590
19518
  node.parent = parent;
19519
+ invalidateOrder();
18591
19520
  }
18592
19521
  function descendants(id, out) {
18593
19522
  const n = state.nodes.get(id);
@@ -18667,9 +19596,11 @@ function createScene(options) {
18667
19596
  apply: (p) => {
18668
19597
  requireLayerIndex(p.to);
18669
19598
  requireNode(p.id).layer = p.to;
19599
+ invalidateOrder();
18670
19600
  },
18671
19601
  revert: (p) => {
18672
19602
  requireNode(p.id).layer = p.from;
19603
+ invalidateOrder();
18673
19604
  }
18674
19605
  });
18675
19606
  registerKitOp("kit:move", {
@@ -18808,21 +19739,98 @@ function createScene(options) {
18808
19739
  registered.set(k, h);
18809
19740
  }
18810
19741
  }
18811
- function* renderOrderInternal() {
18812
- for (const layer of state.layers) {
18813
- const stack = [...state.roots].reverse();
18814
- while (stack.length > 0) {
18815
- const id = stack.pop();
18816
- const node = state.nodes.get(id);
18817
- if (!node) continue;
18818
- if (node.layer === layer.id) yield id;
18819
- if (node.kind === "container") {
18820
- for (let i = node.children.length - 1; i >= 0; i--) {
18821
- stack.push(node.children[i]);
18822
- }
19742
+ function renderOrderInternal() {
19743
+ return state.layers.length === 1 ? renderOrderFlat() : renderOrderBucketed();
19744
+ }
19745
+ function renderOrderNodesInternal() {
19746
+ return state.layers.length === 1 ? renderOrderNodesFlat() : renderOrderNodesBucketed();
19747
+ }
19748
+ let structureGeneration = 0;
19749
+ let orderCache = { gen: 0 };
19750
+ function invalidateOrder() {
19751
+ structureGeneration++;
19752
+ }
19753
+ function currentOrderCache() {
19754
+ if (orderCache.gen !== structureGeneration) {
19755
+ orderCache = { gen: structureGeneration };
19756
+ }
19757
+ return orderCache;
19758
+ }
19759
+ function renderOrderFlat() {
19760
+ const only = state.layers[0].id;
19761
+ const out = [];
19762
+ const stack = [...state.roots].reverse();
19763
+ while (stack.length > 0) {
19764
+ const id = stack.pop();
19765
+ const node = state.nodes.get(id);
19766
+ if (!node) continue;
19767
+ if (node.layer === only) out.push(id);
19768
+ if (node.kind === "container") {
19769
+ for (let i = node.children.length - 1; i >= 0; i--) {
19770
+ stack.push(node.children[i]);
19771
+ }
19772
+ }
19773
+ }
19774
+ return out;
19775
+ }
19776
+ function renderOrderNodesFlat() {
19777
+ const only = state.layers[0].id;
19778
+ const out = [];
19779
+ const stack = [...state.roots].reverse();
19780
+ while (stack.length > 0) {
19781
+ const id = stack.pop();
19782
+ const node = state.nodes.get(id);
19783
+ if (!node) continue;
19784
+ if (node.layer === only) out.push(node);
19785
+ if (node.kind === "container") {
19786
+ for (let i = node.children.length - 1; i >= 0; i--) {
19787
+ stack.push(node.children[i]);
19788
+ }
19789
+ }
19790
+ }
19791
+ return out;
19792
+ }
19793
+ function renderOrderBucketed() {
19794
+ const buckets = state.layers.map(() => []);
19795
+ const stack = [...state.roots].reverse();
19796
+ while (stack.length > 0) {
19797
+ const id = stack.pop();
19798
+ const node = state.nodes.get(id);
19799
+ if (!node) continue;
19800
+ const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
19801
+ if (bucket) bucket.push(id);
19802
+ if (node.kind === "container") {
19803
+ for (let i = node.children.length - 1; i >= 0; i--) {
19804
+ stack.push(node.children[i]);
19805
+ }
19806
+ }
19807
+ }
19808
+ const out = [];
19809
+ for (const bucket of buckets) {
19810
+ for (const id of bucket) out.push(id);
19811
+ }
19812
+ return out;
19813
+ }
19814
+ function renderOrderNodesBucketed() {
19815
+ const buckets = state.layers.map(() => []);
19816
+ const stack = [...state.roots].reverse();
19817
+ while (stack.length > 0) {
19818
+ const id = stack.pop();
19819
+ const node = state.nodes.get(id);
19820
+ if (!node) continue;
19821
+ const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
19822
+ if (bucket) bucket.push(node);
19823
+ if (node.kind === "container") {
19824
+ for (let i = node.children.length - 1; i >= 0; i--) {
19825
+ stack.push(node.children[i]);
18823
19826
  }
18824
19827
  }
18825
19828
  }
19829
+ const out = [];
19830
+ for (const bucket of buckets) {
19831
+ for (const node of bucket) out.push(node);
19832
+ }
19833
+ return out;
18826
19834
  }
18827
19835
  const scene = {
18828
19836
  get nodes() {
@@ -18851,7 +19859,12 @@ function createScene(options) {
18851
19859
  return out;
18852
19860
  },
18853
19861
  renderOrder() {
18854
- return renderOrderInternal();
19862
+ const c = currentOrderCache();
19863
+ return c.ids ??= renderOrderInternal();
19864
+ },
19865
+ renderOrderNodes() {
19866
+ const c = currentOrderCache();
19867
+ return c.nodes ??= renderOrderNodesInternal();
18855
19868
  },
18856
19869
  add(spec) {
18857
19870
  const id = spec.id ?? generateId();
@@ -19159,9 +20172,8 @@ function createScene(options) {
19159
20172
  },
19160
20173
  toJSON() {
19161
20174
  const nodes = [];
19162
- for (const id of renderOrderInternal()) {
19163
- const n = state.nodes.get(id);
19164
- if (!n) continue;
20175
+ for (const n of renderOrderNodesInternal()) {
20176
+ const id = n.id;
19165
20177
  const out = {
19166
20178
  id,
19167
20179
  kind: n.kind,
@@ -19195,6 +20207,7 @@ function createScene(options) {
19195
20207
  state.roots.length = 0;
19196
20208
  state.layers.length = 0;
19197
20209
  state.layerIndex.clear();
20210
+ invalidateOrder();
19198
20211
  for (let i = 0; i < json.systemLayers.length; i++) {
19199
20212
  const spec = json.systemLayers[i];
19200
20213
  if (state.layerIndex.has(spec.id)) {
@@ -19463,9 +20476,8 @@ function sceneToAdapter(scene, options = {}) {
19463
20476
  getNodes() {
19464
20477
  const visible = visibleLayers();
19465
20478
  const out = [];
19466
- for (const id of scene.renderOrder()) {
19467
- const n = scene.get(id);
19468
- if (n && visible.has(n.layer)) out.push(n);
20479
+ for (const n of scene.renderOrderNodes()) {
20480
+ if (visible.has(n.layer)) out.push(n);
19469
20481
  }
19470
20482
  return out;
19471
20483
  },
@@ -19796,9 +20808,7 @@ function useSceneSelectTool(args) {
19796
20808
  // path descriptor for a tighter test.
19797
20809
  hitTestArea: (rect) => {
19798
20810
  const hits = [];
19799
- for (const nid of scene.renderOrder()) {
19800
- const n = scene.get(nid);
19801
- if (!n) continue;
20811
+ for (const n of scene.renderOrderNodes()) {
19802
20812
  if (isPathLike2(n.pose) && pathPoseDescriptor.intersectsRect) {
19803
20813
  if (pathPoseDescriptor.intersectsRect(n.pose, rect)) hits.push(n.id);
19804
20814
  continue;
@@ -19836,9 +20846,8 @@ function useSceneSelectTool(args) {
19836
20846
  }
19837
20847
  const tolerance = pickTolerancePx / meanScale(getView?.()?.scale ?? { x: 1, y: 1 });
19838
20848
  const out = [];
19839
- for (const id of scene.renderOrder()) {
19840
- const n = scene.get(id);
19841
- if (!n || !poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
20849
+ for (const n of scene.renderOrderNodes()) {
20850
+ if (!poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
19842
20851
  if (shapePicking && !shapeCoversPoint(n, n.pose, wx, wy, { tolerance })) continue;
19843
20852
  out.push(n.id);
19844
20853
  }
@@ -20774,6 +21783,8 @@ function toAffordanceHit(hit) {
20774
21783
  const scratch = hit.binding.initialScratch ?? {};
20775
21784
  return {
20776
21785
  kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,
21786
+ owner: hit.affordanceId,
21787
+ strength: "shared",
20777
21788
  ...scratch.targetId !== void 0 ? { targetIds: [scratch.targetId] } : {},
20778
21789
  ...scratch.anchor !== void 0 ? { anchor: scratch.anchor } : {},
20779
21790
  ...scratch.fixedPoint !== void 0 ? { fixedPoint: scratch.fixedPoint } : {},
@@ -20953,23 +21964,25 @@ function hitTestAreaPolygon(scene, area, areaBounds) {
20953
21964
  const ab = areaBounds ?? boundsOf(area);
20954
21965
  if (!ab) return [];
20955
21966
  const hits = [];
20956
- for (const id of scene.renderOrder()) {
20957
- const node = scene.get(id);
20958
- if (!node || node.kind === "container") continue;
21967
+ const order = scene.renderOrderNodes();
21968
+ for (let i = 0; i < order.length; i++) {
21969
+ const node = order[i];
21970
+ if (node.kind === "container") continue;
20959
21971
  const pose = node.pose;
20960
- const b = aabbOfPose(pose);
21972
+ const silhouette = pose !== null && typeof pose === "object" && pose.kind === "polygon";
21973
+ const b = silhouette ? nodeMemo(node, "aabb", pose, () => aabbOfPose(pose)) : aabbOfPose(pose);
20961
21974
  if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) {
20962
21975
  continue;
20963
21976
  }
20964
21977
  if (b.x >= ab.x + ab.width || b.x + b.width <= ab.x || b.y >= ab.y + ab.height || b.y + b.height <= ab.y) {
20965
21978
  continue;
20966
21979
  }
20967
- if (!isPathLike2(pose) || pose.kind === "rect") {
20968
- hits.push(id);
21980
+ if (!silhouette) {
21981
+ hits.push(node.id);
20969
21982
  continue;
20970
21983
  }
20971
21984
  if (silhouetteOverlapsArea(pose.coords, area)) {
20972
- hits.push(id);
21985
+ hits.push(node.id);
20973
21986
  }
20974
21987
  }
20975
21988
  return hits;
@@ -21294,7 +22307,7 @@ function chordLengthParam(samples) {
21294
22307
  ts[0] = 0;
21295
22308
  let total = 0;
21296
22309
  for (let i = 1; i < n; i++) {
21297
- total += dist3(samples[i - 1], samples[i]);
22310
+ total += dist2(samples[i - 1], samples[i]);
21298
22311
  ts[i] = total;
21299
22312
  }
21300
22313
  if (total === 0) return null;
@@ -21327,13 +22340,13 @@ function solveLS(samples, ts, a, e, tStart, tEnd) {
21327
22340
  let alpha1;
21328
22341
  let alpha2;
21329
22342
  if (Math.abs(det) < 1e-12) {
21330
- const chord = dist3(a, e);
22343
+ const chord = dist2(a, e);
21331
22344
  alpha1 = alpha2 = chord / 3;
21332
22345
  } else {
21333
22346
  alpha1 = (c22 * x1 - c12 * x2) / det;
21334
22347
  alpha2 = (c11 * x2 - c12 * x1) / det;
21335
22348
  if (alpha1 < 1e-6 || alpha2 < 1e-6) {
21336
- const chord = dist3(a, e);
22349
+ const chord = dist2(a, e);
21337
22350
  alpha1 = alpha2 = chord / 3;
21338
22351
  }
21339
22352
  }
@@ -21369,7 +22382,7 @@ function newtonRefineT(p, t, p0, p1, p2, p3) {
21369
22382
  function sub(a, b) {
21370
22383
  return { x: a.x - b.x, y: a.y - b.y };
21371
22384
  }
21372
- function dist3(a, b) {
22385
+ function dist2(a, b) {
21373
22386
  return Math.hypot(a.x - b.x, a.y - b.y);
21374
22387
  }
21375
22388
  function unit(v) {
@@ -22329,7 +23342,7 @@ function SceneCanvasInner(props, ref) {
22329
23342
  const getMode = getActiveModeRef.current;
22330
23343
  if (!getMode) return true;
22331
23344
  const registry22 = toolsForEligibilityRef.current?.registry;
22332
- const caps = registry22?.[toolId]?.capabilities ?? [];
23345
+ const caps = registry22?.[toolId]?.eligibility.capabilities ?? [];
22333
23346
  if (caps.length === 0) return false;
22334
23347
  const allowed = getMode().allowedCapabilities;
22335
23348
  for (const c of caps) if (allowed.has(c)) return true;
@@ -22803,10 +23816,6 @@ function GestureDispatcherMounter({
22803
23816
  for (const tool of tools.ambient) m.set(tool.id, tool);
22804
23817
  return m;
22805
23818
  }, [tools.registry, tools.ambient]);
22806
- const ambientToolIds = useMemo(
22807
- () => tools.ambient.map((t) => t.id),
22808
- [tools.ambient]
22809
- );
22810
23819
  const boundsOfRef = useRef(boundsOf2);
22811
23820
  boundsOfRef.current = boundsOf2;
22812
23821
  const pickEveryRef = useRef(pickEvery);
@@ -22918,9 +23927,14 @@ function GestureDispatcherMounter({
22918
23927
  const worldPoint = clientToWorld2(screenPoint.x, screenPoint.y);
22919
23928
  const extra = canvasApiRef?.current?.hitTestExtras?.(worldPoint.x, worldPoint.y);
22920
23929
  if (extra) {
23930
+ const claim = extra.hit;
22921
23931
  return {
22922
23932
  kind: `layer:${extra.layerId}`,
22923
- ...extra.binding.initialScratch !== void 0 ? { payload: extra.binding.initialScratch } : {}
23933
+ owner: extra.layerId,
23934
+ strength: claim.strength ?? "shared",
23935
+ ...claim.claimedKinds !== void 0 ? { claimedKinds: claim.claimedKinds } : {},
23936
+ ...claim.cursor !== void 0 ? { cursor: claim.cursor } : {},
23937
+ ...claim.initialScratch !== void 0 ? { payload: claim.initialScratch } : {}
22924
23938
  };
22925
23939
  }
22926
23940
  return affordanceAt ? affordanceAt(worldPoint) : null;
@@ -22940,7 +23954,6 @@ function GestureDispatcherMounter({
22940
23954
  canvasRef,
22941
23955
  actions: registry22,
22942
23956
  toolsById,
22943
- ambientToolIds,
22944
23957
  enabled,
22945
23958
  keyboard,
22946
23959
  affordanceAt: wrappedAffordanceAt,
@@ -23124,14 +24137,25 @@ inferredNodeRouting.map((e) => ({
23124
24137
  schema: shapeSchema({ text: e.name === "text" })
23125
24138
  }));
23126
24139
  var RENDERER_CACHE = /* @__PURE__ */ new WeakMap();
23127
- function buildSceneViewCommands(scene, view, drawOne, extraCommands) {
23128
- const children = [];
23129
- for (const id of scene.renderOrder()) {
23130
- const node = scene.get(id);
23131
- if (!node) continue;
23132
- const cmds = drawOne(node, node.pose, view);
23133
- for (const cmd of cmds) children.push(cmd);
23134
- }
24140
+ function sceneAsHierarchy(scene) {
24141
+ return {
24142
+ getLayers: () => scene.layers.map((l) => ({ id: l.id, visible: l.visible })),
24143
+ getNode: (id) => scene.get(id),
24144
+ getChildren: (parentId) => {
24145
+ if (parentId === null) return scene.roots;
24146
+ const node = scene.get(parentId);
24147
+ return node && node.kind === "container" ? node.children : [];
24148
+ },
24149
+ getPose: (id) => scene.get(id).pose
24150
+ };
24151
+ }
24152
+ function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor) {
24153
+ const wrappedDrawOne = (node, pose, v) => wrapNodeOutput(drawOne(node, pose, v), pose, alphaFor ? alphaFor(node.id) : 1);
24154
+ const children = buildSceneTree(
24155
+ sceneAsHierarchy(scene),
24156
+ wrappedDrawOne,
24157
+ view
24158
+ );
23135
24159
  if (extraCommands && extraCommands.length > 0) {
23136
24160
  for (const cmd of extraCommands) children.push(cmd);
23137
24161
  }
@@ -23142,7 +24166,7 @@ function buildSceneViewCommands(scene, view, drawOne, extraCommands) {
23142
24166
  }];
23143
24167
  }
23144
24168
  function renderSceneToCanvas(args) {
23145
- const { canvas, scene, view, width, height, drawOne, extraCommands } = args;
24169
+ const { canvas, scene, view, width, height, drawOne, extraCommands, alphaFor } = args;
23146
24170
  const dpr = args.dpr ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
23147
24171
  let entry = RENDERER_CACHE.get(canvas);
23148
24172
  if (!entry) {
@@ -23170,11 +24194,11 @@ function renderSceneToCanvas(args) {
23170
24194
  entry.height = height;
23171
24195
  entry.dpr = dpr;
23172
24196
  }
23173
- const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands);
23174
- entry.renderer.render(commands);
24197
+ const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor);
24198
+ entry.renderer.render(commands, viewToMat3(view));
23175
24199
  }
23176
24200
  function SceneViewCanvasInner(props) {
23177
- const { scene, view, width, height, drawOne, extraCommands, className, canvasRef } = props;
24201
+ const { scene, view, width, height, drawOne, extraCommands, alphaFor, className, canvasRef } = props;
23178
24202
  useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);
23179
24203
  const localRef = useRef(null);
23180
24204
  const setCanvasRef = (el) => {
@@ -23195,7 +24219,8 @@ function SceneViewCanvasInner(props) {
23195
24219
  width,
23196
24220
  height,
23197
24221
  drawOne,
23198
- extraCommands
24222
+ extraCommands,
24223
+ alphaFor
23199
24224
  });
23200
24225
  });
23201
24226
  return /* @__PURE__ */ jsx(
@@ -23292,6 +24317,7 @@ function MinimapCanvasInner(props) {
23292
24317
  width,
23293
24318
  height,
23294
24319
  drawOne,
24320
+ alphaFor,
23295
24321
  fit = "scene",
23296
24322
  poseBounds,
23297
24323
  indicatorStyle,
@@ -23403,6 +24429,7 @@ function MinimapCanvasInner(props) {
23403
24429
  height,
23404
24430
  drawOne,
23405
24431
  extraCommands,
24432
+ alphaFor,
23406
24433
  className,
23407
24434
  canvasRef: setCanvasRef
23408
24435
  }
@@ -23545,6 +24572,6 @@ function oklchToOklab(L, C, h) {
23545
24572
  return [L, C * Math.cos(h), C * Math.sin(h)];
23546
24573
  }
23547
24574
 
23548
- export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleTool, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
23549
- //# sourceMappingURL=chunk-6V7ROR7G.js.map
23550
- //# sourceMappingURL=chunk-6V7ROR7G.js.map
24575
+ export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleContribution, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
24576
+ //# sourceMappingURL=chunk-ISEK5LXT.js.map
24577
+ //# sourceMappingURL=chunk-ISEK5LXT.js.map