@weasel-js/core 1.14.1 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/README.md +1 -1
  3. package/dist/affordances/pathAnchors.js +0 -1
  4. package/dist/affordances/pathAnchors.js.map +1 -1
  5. package/dist/barrel/paths.js +1 -1
  6. package/dist/barrel/renderer.js +2 -2
  7. package/dist/barrel/routing.js +6 -4
  8. package/dist/barrel/text.js +5 -3
  9. package/dist/canvas/Canvas.js +5 -1
  10. package/dist/canvas/Canvas.js.map +1 -1
  11. package/dist/canvas/DrawCanvas.js +5 -1
  12. package/dist/canvas/DrawCanvas.js.map +1 -1
  13. package/dist/canvas/SceneCanvas/SceneCanvasInput.js +4 -0
  14. package/dist/canvas/SceneCanvas/SceneCanvasInput.js.map +1 -1
  15. package/dist/canvas/SceneCanvas/builtinToolSets.js +6 -3
  16. package/dist/canvas/SceneCanvas/builtinToolSets.js.map +1 -1
  17. package/dist/canvas/SceneCanvas/defaultNodeProperties.js +1 -1
  18. package/dist/canvas/SceneCanvas/presets/paths.js +6 -6
  19. package/dist/canvas/SceneCanvas/presets/paths.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js +1 -1
  21. package/dist/canvas/SceneCanvas/shapeGeneratorProperties.js.map +1 -1
  22. package/dist/canvas/SceneCanvas/useBuiltinTools.js +2 -0
  23. package/dist/canvas/SceneCanvas/useBuiltinTools.js.map +1 -1
  24. package/dist/canvas/SceneCanvas/useContributionRoles.js +2 -0
  25. package/dist/canvas/SceneCanvas/useContributionRoles.js.map +1 -1
  26. package/dist/canvas/SceneCanvas/useSceneChrome.js +17 -5
  27. package/dist/canvas/SceneCanvas/useSceneChrome.js.map +1 -1
  28. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +3 -0
  29. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  30. package/dist/canvas/SceneCanvas.js +5 -4
  31. package/dist/canvas/SceneCanvas.js.map +1 -1
  32. package/dist/canvas/SceneViewCanvas.js +5 -1
  33. package/dist/canvas/SceneViewCanvas.js.map +1 -1
  34. package/dist/canvas/affordanceAt.js +0 -1
  35. package/dist/canvas/affordanceAt.js.map +1 -1
  36. package/dist/canvas/deps/chromeRules.js +37 -0
  37. package/dist/canvas/deps/chromeRules.js.map +1 -0
  38. package/dist/canvas/deps/editAnchors.js +0 -6
  39. package/dist/canvas/deps/editAnchors.js.map +1 -1
  40. package/dist/canvas/deps/hitTestArea.js +3 -1
  41. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  42. package/dist/canvas/deps/modeDefaultTool.js +23 -0
  43. package/dist/canvas/deps/modeDefaultTool.js.map +1 -0
  44. package/dist/canvas/deps/pathEditCommits.js +1 -1
  45. package/dist/canvas/deps/selectionSnapshot.js +23 -0
  46. package/dist/canvas/deps/selectionSnapshot.js.map +1 -0
  47. package/dist/canvas/deps/snap.js +1 -0
  48. package/dist/canvas/deps/snap.js.map +1 -1
  49. package/dist/canvas/pickReach.js +12 -8
  50. package/dist/canvas/pickReach.js.map +1 -1
  51. package/dist/canvas/pickWalk.js +7 -5
  52. package/dist/canvas/pickWalk.js.map +1 -1
  53. package/dist/canvas/scenePickSource.js +2 -2
  54. package/dist/canvas/scenePickSource.js.map +1 -1
  55. package/dist/canvas/useCanvasSurface.js +21 -3
  56. package/dist/canvas/useCanvasSurface.js.map +1 -1
  57. package/dist/canvas/useFrameLoop.js +6 -7
  58. package/dist/canvas/useFrameLoop.js.map +1 -1
  59. package/dist/canvas/useSurfaceSize.js +4 -3
  60. package/dist/canvas/useSurfaceSize.js.map +1 -1
  61. package/dist/core/geometry/regionHit.js +35 -16
  62. package/dist/core/geometry/regionHit.js.map +1 -1
  63. package/dist/core/paint/meshKindLazy.js +9 -10
  64. package/dist/core/paint/meshKindLazy.js.map +1 -1
  65. package/dist/core/scene/editMode.js +4 -1
  66. package/dist/core/scene/editMode.js.map +1 -1
  67. package/dist/core/scene/historyFacade.js +7 -50
  68. package/dist/core/scene/historyFacade.js.map +1 -1
  69. package/dist/core/scene/lockGuard.js +1 -0
  70. package/dist/core/scene/lockGuard.js.map +1 -1
  71. package/dist/core/scene/nodeGates.js +43 -23
  72. package/dist/core/scene/nodeGates.js.map +1 -1
  73. package/dist/core/scene/opTable.js +8 -8
  74. package/dist/core/scene/opTable.js.map +1 -1
  75. package/dist/core/scene/outerOps.js +2 -2
  76. package/dist/core/scene/outerOps.js.map +1 -1
  77. package/dist/core/scene/poseRemovals.js +1 -0
  78. package/dist/core/scene/poseRemovals.js.map +1 -1
  79. package/dist/core/scene/poseWrites.js +16 -15
  80. package/dist/core/scene/poseWrites.js.map +1 -1
  81. package/dist/core/scene/sceneJournals.js +78 -0
  82. package/dist/core/scene/sceneJournals.js.map +1 -0
  83. package/dist/core/scene/sceneRecording.js +3 -2
  84. package/dist/core/scene/sceneRecording.js.map +1 -1
  85. package/dist/core/scene/statedFields.js +10 -8
  86. package/dist/core/scene/statedFields.js.map +1 -1
  87. package/dist/core/scene/treeEdits.js +4 -9
  88. package/dist/core/scene/treeEdits.js.map +1 -1
  89. package/dist/core/viewport/useCanvasSize.js +47 -26
  90. package/dist/core/viewport/useCanvasSize.js.map +1 -1
  91. package/dist/{depSchema-C3BwxitB.d.ts → depSchema-ChQPzEkW.d.ts} +56 -25
  92. package/dist/domains/chrome-caps/resolve.js +1 -0
  93. package/dist/domains/chrome-caps/resolve.js.map +1 -1
  94. package/dist/domains/chrome-caps/useChromeCaps.js +4 -2
  95. package/dist/domains/chrome-caps/useChromeCaps.js.map +1 -1
  96. package/dist/domains/paths/pathEditState.js +0 -24
  97. package/dist/domains/paths/pathEditState.js.map +1 -1
  98. package/dist/domains/snap/probes2d.js +5 -0
  99. package/dist/domains/snap/probes2d.js.map +1 -1
  100. package/dist/domains/snap/resizeBox.js +43 -22
  101. package/dist/domains/snap/resizeBox.js.map +1 -1
  102. package/dist/domains/snap/sceneReads.js +11 -1
  103. package/dist/domains/snap/sceneReads.js.map +1 -1
  104. package/dist/domains/snap/sceneSession2d.js +4 -2
  105. package/dist/domains/snap/sceneSession2d.js.map +1 -1
  106. package/dist/domains/snap/session2d.js +14 -7
  107. package/dist/domains/snap/session2d.js.map +1 -1
  108. package/dist/domains/text/index.js +2 -2
  109. package/dist/icons.table.js +1 -1
  110. package/dist/icons.table.js.map +1 -1
  111. package/dist/icons.table.json +5 -0
  112. package/dist/index.d.ts +114 -62
  113. package/dist/index.js +12 -8
  114. package/dist/interactions/actions/booleans/action.js +4 -2
  115. package/dist/interactions/actions/booleans/action.js.map +1 -1
  116. package/dist/interactions/actions/defaults/anchorEditing.js +3 -6
  117. package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
  118. package/dist/interactions/actions/defaults/bendSegment.js +1 -2
  119. package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
  120. package/dist/interactions/actions/defaults/exitPathEdit.js +17 -1
  121. package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
  122. package/dist/interactions/actions/edit-anchors/action.js +3 -4
  123. package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
  124. package/dist/interactions/actions/insert/action.js +21 -2
  125. package/dist/interactions/actions/insert/action.js.map +1 -1
  126. package/dist/interactions/actions/insert/params.js +43 -0
  127. package/dist/interactions/actions/insert/params.js.map +1 -0
  128. package/dist/interactions/actions/notices.js +17 -0
  129. package/dist/interactions/actions/notices.js.map +1 -0
  130. package/dist/interactions/actions/outlines/createOutlines.js +8 -6
  131. package/dist/interactions/actions/outlines/createOutlines.js.map +1 -1
  132. package/dist/interactions/actions/outlines/createOutlinesAction.js +13 -8
  133. package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
  134. package/dist/interactions/actions/outlines/outlineStrokeAction.js +3 -2
  135. package/dist/interactions/actions/outlines/outlineStrokeAction.js.map +1 -1
  136. package/dist/interactions/actions/pathCommands/pathNodes.js +1 -1
  137. package/dist/interactions/actions/testUtils.js +0 -7
  138. package/dist/interactions/actions/testUtils.js.map +1 -1
  139. package/dist/interactions/actions/useNotices.js +13 -0
  140. package/dist/interactions/actions/useNotices.js.map +1 -0
  141. package/dist/test-seams.d.ts +5 -12
  142. package/dist/tools/builtin/anchors/anchorsTool.js +31 -0
  143. package/dist/tools/builtin/anchors/anchorsTool.js.map +1 -0
  144. package/dist/tools/builtin/polygon/usePolygonTool.js +1 -1
  145. package/dist/tools/builtin/star/useStarTool.js +1 -1
  146. package/dist/tools/index.js +4 -2
  147. package/dist/tools/toolOptions.js +47 -0
  148. package/dist/tools/toolOptions.js.map +1 -0
  149. package/dist/version.js +1 -1
  150. package/package.json +25 -25
  151. package/dist/interactions/actions/defaults/pathEditTools.js +0 -60
  152. package/dist/interactions/actions/defaults/pathEditTools.js.map +0 -1
@@ -30,7 +30,8 @@ function surfaceRuleCtx(surface, view) {
30
30
  allowedCapabilities: surface.mode?.allowedCapabilities ?? DEFAULT_ALLOWED_CAPABILITIES,
31
31
  selectionResizable: resizable ? view.selection.every((id) => resizable(id)) : void 0,
32
32
  editingAnchors: surface.editingAnchors,
33
- device: surface.device
33
+ device: surface.device,
34
+ ...surface.hiddenChrome ? { hiddenChrome: surface.hiddenChrome } : {}
34
35
  });
35
36
  }
36
37
  /** The chrome-caps answers for a surface. The object keeps its identity for
@@ -47,7 +48,8 @@ function useChromeCaps(source) {
47
48
  mode: now.getMode?.(),
48
49
  resizable: now.resizable,
49
50
  editingAnchors: now.editingAnchors(),
50
- device: now.device
51
+ device: now.device,
52
+ hiddenChrome: now.getHiddenChrome?.()
51
53
  }, inputs);
52
54
  };
53
55
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"useChromeCaps.js","names":[],"sources":["../../../src/domains/chrome-caps/useChromeCaps.ts"],"sourcesContent":["/**\n * The rule context a surface's chrome-visibility rules and action eligibility\n * are evaluated against, and the hook that keeps one for `<SceneCanvas>`.\n */\nimport { useMemo } from 'react';\nimport type { ActiveMode } from '@weasel-js/modes';\nimport { useLatest } from '@weasel-js/react';\nimport {\n buildRuleCtx,\n DEFAULT_ALLOWED_CAPABILITIES,\n type DeviceProfile,\n type ModifierState,\n type RuleCtx,\n} from '@weasel-js/routing';\nimport type { NodeId } from '../../core/scene/types';\nimport { viewZoom, type View } from '../../core/viewport/view';\nimport { resolveVisibility } from './resolve';\nimport type { VisibilityRules } from './types';\n\n/**\n * The part of a rule context that belongs to one view. Everything else a\n * `RuleCtx` carries — mode, capabilities, device, focus — is the surface's and\n * the same for every view drawn on it.\n */\nexport interface ViewRuleInputs {\n selection: readonly NodeId[];\n view: View;\n /** What that view's dispatcher currently has in flight. */\n action: { kind: string | null; id: string | null };\n}\n\n/** The part of a rule context that belongs to the surface. */\nexport interface SurfaceRuleInputs {\n focused: boolean;\n /** The modifier keys held. Absent means none, so `modifierHeld` never matches. */\n modifiers?: ModifierState | undefined;\n hover: NodeId | null;\n /** Absent on a surface with no mode registry, which behaves as normal mode. */\n mode: ActiveMode | undefined;\n /** Whether one node may be resized. Absent means every node may. */\n resizable: ((id: string) => boolean) | undefined;\n editingAnchors: boolean;\n device: DeviceProfile;\n}\n\nconst NO_MODIFIERS = { alt: false, ctrl: false, meta: false, shift: false };\n\n/** One view's rule context on a surface. */\nexport function surfaceRuleCtx(surface: SurfaceRuleInputs, view: ViewRuleInputs): RuleCtx {\n const { resizable } = surface;\n return buildRuleCtx({\n focused: surface.focused,\n selection: view.selection,\n multiActive: view.selection.length > 1,\n modifiers: surface.modifiers ?? NO_MODIFIERS,\n action: view.action,\n hover: surface.hover,\n zoom: viewZoom(view.view),\n mode: surface.mode?.id ?? 'normal',\n // An empty set would make every `capability:` rule false and hide the\n // chrome those rules gate.\n allowedCapabilities: surface.mode?.allowedCapabilities ?? DEFAULT_ALLOWED_CAPABILITIES,\n // Left undefined without a predicate, which `resizable:` reads as resizable.\n selectionResizable: resizable ? view.selection.every((id) => resizable(id)) : undefined,\n editingAnchors: surface.editingAnchors,\n device: surface.device,\n });\n}\n\n/** Where a surface's rule context comes from. Read at the moment a rule is\n * evaluated, never at render. */\nexport interface ChromeCapsSource {\n /** The surface's rule table, over the kit defaults. */\n rules: VisibilityRules | undefined;\n /** The active mode. Absent when no mode registry is wired. */\n getMode: (() => ActiveMode) | undefined;\n /** Absent means the surface always counts as focused. */\n getFocused: (() => boolean) | undefined;\n getHover: () => NodeId | null;\n /** Absent means no modifier key ever counts as held. */\n getModifiers?: (() => ModifierState) | undefined;\n resizable: ((id: string) => boolean) | undefined;\n editingAnchors: () => boolean;\n device: DeviceProfile;\n}\n\n/**\n * A surface's chrome-caps answers for one view. The rules are surface-wide on\n * purpose — one consumer table, one set of kit defaults — but a rule keyed on\n * selection, camera or the in-flight action answers differently per view, so\n * the caller supplies that part and both the paint gate and the dispatcher's\n * eligibility filter read the same context.\n */\nexport interface ChromeCaps {\n /** `undefined` when the surface gates nothing: no mode registry is wired. */\n ruleCtx(inputs: ViewRuleInputs): RuleCtx | undefined;\n isVisible(inputs: ViewRuleInputs): (id: string) => boolean;\n}\n\n/** The chrome-caps answers for a surface. The object keeps its identity for\n * the life of the component and reads `source` as of the last commit. */\nexport function useChromeCaps(source: ChromeCapsSource): ChromeCaps {\n const live = useLatest(source);\n return useMemo(() => {\n const build = (inputs: ViewRuleInputs): RuleCtx => {\n const now = live.current;\n return surfaceRuleCtx({\n focused: now.getFocused ? now.getFocused() : true,\n modifiers: now.getModifiers?.(),\n hover: now.getHover(),\n mode: now.getMode?.(),\n resizable: now.resizable,\n editingAnchors: now.editingAnchors(),\n device: now.device,\n }, inputs);\n };\n return {\n ruleCtx: (inputs) => (live.current.getMode ? build(inputs) : undefined),\n isVisible: (inputs) => resolveVisibility(live.current.rules, build(inputs)),\n };\n }, [live]);\n}\n"],"mappings":";;;;;;;;;;;AA6CA,IAAM,eAAe;CAAE,KAAK;CAAO,MAAM;CAAO,MAAM;CAAO,OAAO;AAAM;;AAG1E,SAAgB,eAAe,SAA4B,MAA+B;CACxF,MAAM,EAAE,cAAc;CACtB,OAAO,aAAa;EAClB,SAAS,QAAQ;EACjB,WAAW,KAAK;EAChB,aAAa,KAAK,UAAU,SAAS;EACrC,WAAW,QAAQ,aAAa;EAChC,QAAQ,KAAK;EACb,OAAO,QAAQ;EACf,MAAM,SAAS,KAAK,IAAI;EACxB,MAAM,QAAQ,MAAM,MAAM;EAG1B,qBAAqB,QAAQ,MAAM,uBAAuB;EAE1D,oBAAoB,YAAY,KAAK,UAAU,OAAO,OAAO,UAAU,EAAE,CAAC,IAAI;EAC9E,gBAAgB,QAAQ;EACxB,QAAQ,QAAQ;CAClB,CAAC;AACH;;;AAkCA,SAAgB,cAAc,QAAsC;CAClE,MAAM,OAAO,UAAU,MAAM;CAC7B,OAAO,cAAc;EACnB,MAAM,SAAS,WAAoC;GACjD,MAAM,MAAM,KAAK;GACjB,OAAO,eAAe;IACpB,SAAS,IAAI,aAAa,IAAI,WAAW,IAAI;IAC7C,WAAW,IAAI,eAAe;IAC9B,OAAO,IAAI,SAAS;IACpB,MAAM,IAAI,UAAU;IACpB,WAAW,IAAI;IACf,gBAAgB,IAAI,eAAe;IACnC,QAAQ,IAAI;GACd,GAAG,MAAM;EACX;EACA,OAAO;GACL,UAAU,WAAY,KAAK,QAAQ,UAAU,MAAM,MAAM,IAAI;GAC7D,YAAY,WAAW,kBAAkB,KAAK,QAAQ,OAAO,MAAM,MAAM,CAAC;EAC5E;CACF,GAAG,CAAC,IAAI,CAAC;AACX"}
1
+ {"version":3,"file":"useChromeCaps.js","names":[],"sources":["../../../src/domains/chrome-caps/useChromeCaps.ts"],"sourcesContent":["/**\n * The rule context a surface's chrome-visibility rules and action eligibility\n * are evaluated against, and the hook that keeps one for `<SceneCanvas>`.\n */\nimport { useMemo } from 'react';\nimport type { ActiveMode } from '@weasel-js/modes';\nimport { useLatest } from '@weasel-js/react';\nimport {\n buildRuleCtx,\n DEFAULT_ALLOWED_CAPABILITIES,\n type DeviceProfile,\n type ModifierState,\n type RuleCtx,\n} from '@weasel-js/routing';\nimport type { NodeId } from '../../core/scene/types';\nimport { viewZoom, type View } from '../../core/viewport/view';\nimport { resolveVisibility } from './resolve';\nimport type { VisibilityRules } from './types';\n\n/**\n * The part of a rule context that belongs to one view. Everything else a\n * `RuleCtx` carries — mode, capabilities, device, focus — is the surface's and\n * the same for every view drawn on it.\n */\nexport interface ViewRuleInputs {\n selection: readonly NodeId[];\n view: View;\n /** What that view's dispatcher currently has in flight. */\n action: { kind: string | null; id: string | null };\n}\n\n/** The part of a rule context that belongs to the surface. */\nexport interface SurfaceRuleInputs {\n focused: boolean;\n /** The modifier keys held. Absent means none, so `modifierHeld` never matches. */\n modifiers?: ModifierState | undefined;\n hover: NodeId | null;\n /** Absent on a surface with no mode registry, which behaves as normal mode. */\n mode: ActiveMode | undefined;\n /** Whether one node may be resized. Absent means every node may. */\n resizable: ((id: string) => boolean) | undefined;\n editingAnchors: boolean;\n device: DeviceProfile;\n /** Chrome the active tool hides. Absent hides nothing. */\n hiddenChrome?: ReadonlySet<string> | undefined;\n}\n\nconst NO_MODIFIERS = { alt: false, ctrl: false, meta: false, shift: false };\n\n/** One view's rule context on a surface. */\nexport function surfaceRuleCtx(surface: SurfaceRuleInputs, view: ViewRuleInputs): RuleCtx {\n const { resizable } = surface;\n return buildRuleCtx({\n focused: surface.focused,\n selection: view.selection,\n multiActive: view.selection.length > 1,\n modifiers: surface.modifiers ?? NO_MODIFIERS,\n action: view.action,\n hover: surface.hover,\n zoom: viewZoom(view.view),\n mode: surface.mode?.id ?? 'normal',\n // An empty set would make every `capability:` rule false and hide the\n // chrome those rules gate.\n allowedCapabilities: surface.mode?.allowedCapabilities ?? DEFAULT_ALLOWED_CAPABILITIES,\n // Left undefined without a predicate, which `resizable:` reads as resizable.\n selectionResizable: resizable ? view.selection.every((id) => resizable(id)) : undefined,\n editingAnchors: surface.editingAnchors,\n device: surface.device,\n ...(surface.hiddenChrome ? { hiddenChrome: surface.hiddenChrome } : {}),\n });\n}\n\n/** Where a surface's rule context comes from. Read at the moment a rule is\n * evaluated, never at render. */\nexport interface ChromeCapsSource {\n /** The surface's rule table, over the kit defaults. */\n rules: VisibilityRules | undefined;\n /** The active mode. Absent when no mode registry is wired. */\n getMode: (() => ActiveMode) | undefined;\n /** Absent means the surface always counts as focused. */\n getFocused: (() => boolean) | undefined;\n getHover: () => NodeId | null;\n /** Absent means no modifier key ever counts as held. */\n getModifiers?: (() => ModifierState) | undefined;\n resizable: ((id: string) => boolean) | undefined;\n editingAnchors: () => boolean;\n device: DeviceProfile;\n /** The chrome the active tool hides (`ToolDef.hides`). Absent hides nothing. */\n getHiddenChrome?: (() => ReadonlySet<string> | undefined) | undefined;\n}\n\n/**\n * A surface's chrome-caps answers for one view. The rules are surface-wide on\n * purpose — one consumer table, one set of kit defaults — but a rule keyed on\n * selection, camera or the in-flight action answers differently per view, so\n * the caller supplies that part and both the paint gate and the dispatcher's\n * eligibility filter read the same context.\n */\nexport interface ChromeCaps {\n /** `undefined` when the surface gates nothing: no mode registry is wired. */\n ruleCtx(inputs: ViewRuleInputs): RuleCtx | undefined;\n isVisible(inputs: ViewRuleInputs): (id: string) => boolean;\n}\n\n/** The chrome-caps answers for a surface. The object keeps its identity for\n * the life of the component and reads `source` as of the last commit. */\nexport function useChromeCaps(source: ChromeCapsSource): ChromeCaps {\n const live = useLatest(source);\n return useMemo(() => {\n const build = (inputs: ViewRuleInputs): RuleCtx => {\n const now = live.current;\n return surfaceRuleCtx({\n focused: now.getFocused ? now.getFocused() : true,\n modifiers: now.getModifiers?.(),\n hover: now.getHover(),\n mode: now.getMode?.(),\n resizable: now.resizable,\n editingAnchors: now.editingAnchors(),\n device: now.device,\n hiddenChrome: now.getHiddenChrome?.(),\n }, inputs);\n };\n return {\n ruleCtx: (inputs) => (live.current.getMode ? build(inputs) : undefined),\n isVisible: (inputs) => resolveVisibility(live.current.rules, build(inputs)),\n };\n }, [live]);\n}\n"],"mappings":";;;;;;;;;;;AA+CA,IAAM,eAAe;CAAE,KAAK;CAAO,MAAM;CAAO,MAAM;CAAO,OAAO;AAAM;;AAG1E,SAAgB,eAAe,SAA4B,MAA+B;CACxF,MAAM,EAAE,cAAc;CACtB,OAAO,aAAa;EAClB,SAAS,QAAQ;EACjB,WAAW,KAAK;EAChB,aAAa,KAAK,UAAU,SAAS;EACrC,WAAW,QAAQ,aAAa;EAChC,QAAQ,KAAK;EACb,OAAO,QAAQ;EACf,MAAM,SAAS,KAAK,IAAI;EACxB,MAAM,QAAQ,MAAM,MAAM;EAG1B,qBAAqB,QAAQ,MAAM,uBAAuB;EAE1D,oBAAoB,YAAY,KAAK,UAAU,OAAO,OAAO,UAAU,EAAE,CAAC,IAAI;EAC9E,gBAAgB,QAAQ;EACxB,QAAQ,QAAQ;EAChB,GAAI,QAAQ,eAAe,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;CACvE,CAAC;AACH;;;AAoCA,SAAgB,cAAc,QAAsC;CAClE,MAAM,OAAO,UAAU,MAAM;CAC7B,OAAO,cAAc;EACnB,MAAM,SAAS,WAAoC;GACjD,MAAM,MAAM,KAAK;GACjB,OAAO,eAAe;IACpB,SAAS,IAAI,aAAa,IAAI,WAAW,IAAI;IAC7C,WAAW,IAAI,eAAe;IAC9B,OAAO,IAAI,SAAS;IACpB,MAAM,IAAI,UAAU;IACpB,WAAW,IAAI;IACf,gBAAgB,IAAI,eAAe;IACnC,QAAQ,IAAI;IACZ,cAAc,IAAI,kBAAkB;GACtC,GAAG,MAAM;EACX;EACA,OAAO;GACL,UAAU,WAAY,KAAK,QAAQ,UAAU,MAAM,MAAM,IAAI;GAC7D,YAAY,WAAW,kBAAkB,KAAK,QAAQ,OAAO,MAAM,MAAM,CAAC;EAC5E;CACF,GAAG,CAAC,IAAI,CAAC;AACX"}
@@ -22,7 +22,6 @@ function createPathEditState(source = {}) {
22
22
  let selectedAnchors = NO_ANCHORS;
23
23
  let marquee = null;
24
24
  let handleLocks = null;
25
- let ownTool = null;
26
25
  let idSet = NO_IDS;
27
26
  let selectedMark = null;
28
27
  let extensions = source.extensions ?? NO_EXTENSIONS;
@@ -47,7 +46,6 @@ function createPathEditState(source = {}) {
47
46
  },
48
47
  setEditingId(id) {
49
48
  const host = source.host?.();
50
- if (next(id) !== getEditingId()) ownTool = null;
51
49
  if (host) {
52
50
  host.setEditingId(id);
53
51
  return;
@@ -94,28 +92,6 @@ function createPathEditState(source = {}) {
94
92
  };
95
93
  notify();
96
94
  },
97
- getTool() {
98
- const on = getEditingId();
99
- const host = source.host?.();
100
- if (host?.tool) return on ? host.tool() : null;
101
- if (ownTool && ownTool.on !== on) ownTool = null;
102
- return ownTool?.id ?? null;
103
- },
104
- setTool(id) {
105
- const host = source.host?.();
106
- if (host?.tool && host.setTool) {
107
- host.setTool(id);
108
- return;
109
- }
110
- const on = getEditingId();
111
- const tool = id && on ? {
112
- id,
113
- on
114
- } : null;
115
- if (tool?.id === ownTool?.id && tool?.on === ownTool?.on) return;
116
- ownTool = tool;
117
- notify();
118
- },
119
95
  subscribe: changed.subscribe
120
96
  };
121
97
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pathEditState.js","names":[],"sources":["../../../src/domains/paths/pathEditState.ts"],"sourcesContent":["/**\n * Which path is being anchor-edited, which of its anchors are selected, and\n * the anchor marquee in flight: one object every reader shares — the\n * `editAnchors` dep, the anchor overlay, the selection overlay's suppressed\n * set, the chrome-caps rule context, and the select tool's shift-click lock.\n */\nimport { createEmitter } from '@weasel-js/registry';\nimport type { PathEditHost } from 'canvas/SceneCanvas/pathEditHost';\nimport type { HandleLocks } from 'interactions/actions/edit-anchors/params';\nimport type { PathEditExtension, PathEditMark } from './pathEditExtension';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { effectSourceFor } from 'domains/pathEffects/effectSources';\n\n/** A world-coord rect. */\nexport interface AnchorMarquee {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * What decides whether a stored edit target is still one: its node exists and\n * is selected, and the mode permits anchor editing. Each is read at the\n * moment of the question.\n */\nexport interface PathEditSource {\n /** Whether `id` names a node that exists and is selected. Absent means\n * every id does. */\n isSelectedNode?(id: string): boolean;\n /** A host that owns the id; the state then keeps none of its own. */\n host?(): PathEditHost | undefined;\n /**\n * Whether anchor editing is permitted right now — whether the active mode\n * allows the `edits-anchors` capability.\n *\n * **Omit this when no mode registry is wired.** Absent means \"no modality,\n * so nothing revokes edit mode\"; a predicate built from an empty capability\n * set would revoke edit mode instantly.\n */\n isAllowed?(): boolean;\n /** The extensions it starts with; `setExtensions` replaces them. */\n extensions?: readonly PathEditExtension[];\n}\n\nexport interface PathEditState {\n /**\n * The node whose anchors are editable now, or `''`: the stored id, or the\n * host's, while `PathEditSource` still lets it stand. Masked rather than\n * cleared: a host's mode machine can leave path-edit mode without running\n * `exitPathEdit`, and nothing re-renders when it does, so there is no\n * moment to write the id back.\n */\n getEditingId(): string;\n /** `getEditingId()` as the set the selection overlay skips. */\n getEditingIds(): ReadonlySet<string>;\n setEditingId(id: string | null): void;\n /** Selected anchors (flat indices) of the path being edited. */\n getSelectedAnchors(): ReadonlySet<number>;\n setSelectedAnchors(next: ReadonlySet<number>): void;\n /** The in-flight anchor-marquee rect, in the edited path's world. */\n getMarquee(): AnchorMarquee | null;\n setMarquee(rect: AnchorMarquee | null): void;\n /** The extension mark selected in place of anchors, or null. */\n getSelectedMark(): PathEditMark | null;\n setSelectedMark(mark: PathEditMark | null): void;\n /** The canvas's path-edit extensions, in order. */\n getExtensions(): readonly PathEditExtension[];\n /** Replace the extensions; the overlay repaints when they change. */\n setExtensions(next: readonly PathEditExtension[]): void;\n /** The handle locks the `handleLock.*` toggles set; null until one is\n * pressed. Not tied to the edited path, so they outlast a change of it. */\n getHandleLocks(): HandleLocks | null;\n setHandleLocks(next: HandleLocks): void;\n /**\n * The path-edit tool in use, or null for plain anchor editing. Path edit has\n * a set of named tools, one active at a time, each changing what a plain\n * drag does: `'simplifyBrush'` brushes the path simpler. A tool belongs to\n * the open path; with none open, or a different one, there is none.\n */\n getTool(): string | null;\n /** Switch tool, or pass null to go back to plain anchor editing. Ignored\n * with no path open. */\n setTool(id: string | null): void;\n /** Notified when a write changes what the anchor overlay paints. A host\n * that owns the id announces its own changes. */\n subscribe(listener: () => void): () => void;\n}\n\nconst NO_IDS: ReadonlySet<string> = new Set();\nconst NO_EXTENSIONS: readonly PathEditExtension[] = [];\nconst NO_ANCHORS: ReadonlySet<number> = new Set();\n\n/** Whether `id` names a node `scene` holds and `selection` includes, or the path a selected effect hides and is edited\n * through. */\nexport function isSelectedNode(\n scene: Pick<Scene<unknown, string, unknown>, 'get'>,\n selection: { readonly current: readonly string[] },\n id: string,\n): boolean {\n if (scene.get(id as NodeId) == null) return false;\n return selection.current.includes(id)\n || selection.current.some((sel) => effectSourceFor(scene, sel as NodeId, 'path') === id);\n}\n\nexport function createPathEditState(source: PathEditSource = {}): PathEditState {\n let ownId = '';\n let selectedAnchors = NO_ANCHORS;\n let marquee: AnchorMarquee | null = null;\n let handleLocks: HandleLocks | null = null;\n let ownTool: { id: string; on: string } | null = null;\n let idSet = NO_IDS;\n let selectedMark: PathEditMark | null = null;\n let extensions = source.extensions ?? NO_EXTENSIONS;\n const changed = createEmitter();\n const notify = changed.emit;\n\n const next = (id: string | null): string => id ?? '';\n const getEditingId = (): string => {\n const host = source.host?.();\n const id = host ? host.editingId() ?? '' : ownId;\n if (!id) return '';\n if (source.isAllowed && !source.isAllowed()) return '';\n if (source.isSelectedNode && !source.isSelectedNode(id)) return '';\n return id;\n };\n\n return {\n getEditingId,\n getEditingIds() {\n const id = getEditingId();\n if (!id) return NO_IDS;\n if (!idSet.has(id)) idSet = new Set([id]);\n return idSet;\n },\n setEditingId(id) {\n const host = source.host?.();\n if (next(id) !== getEditingId()) ownTool = null;\n if (host) { host.setEditingId(id); return; }\n if (next(id) === ownId) return;\n ownId = next(id);\n notify();\n },\n getSelectedAnchors: () => {\n const host = source.host?.();\n return host?.selectedAnchors && host.setSelectedAnchors ? host.selectedAnchors() : selectedAnchors;\n },\n setSelectedAnchors(next) {\n const host = source.host?.();\n if (host?.selectedAnchors && host.setSelectedAnchors) { host.setSelectedAnchors(next); return; }\n selectedAnchors = next;\n notify();\n },\n getMarquee: () => marquee,\n setMarquee(rect) {\n marquee = rect;\n notify();\n },\n getSelectedMark: () => selectedMark,\n setSelectedMark(mark) {\n if (mark?.owner === selectedMark?.owner && mark?.index === selectedMark?.index) return;\n selectedMark = mark;\n notify();\n },\n getExtensions: () => extensions,\n setExtensions(next) {\n if (next === extensions) return;\n extensions = next;\n notify();\n },\n getHandleLocks: () => handleLocks,\n setHandleLocks(next) {\n handleLocks = { angle: next.angle, length: next.length };\n notify();\n },\n getTool() {\n const on = getEditingId();\n const host = source.host?.();\n if (host?.tool) return on ? host.tool() : null;\n // Dropped on the read that finds its path gone: a host can close the\n // path without telling this state.\n if (ownTool && ownTool.on !== on) ownTool = null;\n return ownTool?.id ?? null;\n },\n setTool(id) {\n const host = source.host?.();\n if (host?.tool && host.setTool) { host.setTool(id); return; }\n const on = getEditingId();\n const tool = id && on ? { id, on } : null;\n if (tool?.id === ownTool?.id && tool?.on === ownTool?.on) return;\n ownTool = tool;\n notify();\n },\n subscribe: changed.subscribe,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pathEditState.js","names":[],"sources":["../../../src/domains/paths/pathEditState.ts"],"sourcesContent":["/**\n * Which path is being anchor-edited, which of its anchors are selected, and\n * the anchor marquee in flight: one object every reader shares — the\n * `editAnchors` dep, the anchor overlay, the selection overlay's suppressed\n * set, the chrome-caps rule context, and the select tool's shift-click lock.\n */\nimport { createEmitter } from '@weasel-js/registry';\nimport type { PathEditHost } from 'canvas/SceneCanvas/pathEditHost';\nimport type { HandleLocks } from 'interactions/actions/edit-anchors/params';\nimport type { PathEditExtension, PathEditMark } from './pathEditExtension';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { effectSourceFor } from 'domains/pathEffects/effectSources';\n\n/** A world-coord rect. */\nexport interface AnchorMarquee {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * What decides whether a stored edit target is still one: its node exists and\n * is selected, and the mode permits anchor editing. Each is read at the\n * moment of the question.\n */\nexport interface PathEditSource {\n /** Whether `id` names a node that exists and is selected. Absent means\n * every id does. */\n isSelectedNode?(id: string): boolean;\n /** A host that owns the id; the state then keeps none of its own. */\n host?(): PathEditHost | undefined;\n /**\n * Whether anchor editing is permitted right now — whether the active mode\n * allows the `edits-anchors` capability.\n *\n * **Omit this when no mode registry is wired.** Absent means \"no modality,\n * so nothing revokes edit mode\"; a predicate built from an empty capability\n * set would revoke edit mode instantly.\n */\n isAllowed?(): boolean;\n /** The extensions it starts with; `setExtensions` replaces them. */\n extensions?: readonly PathEditExtension[];\n}\n\nexport interface PathEditState {\n /**\n * The node whose anchors are editable now, or `''`: the stored id, or the\n * host's, while `PathEditSource` still lets it stand. Masked rather than\n * cleared: a host's mode machine can leave path-edit mode without running\n * `exitPathEdit`, and nothing re-renders when it does, so there is no\n * moment to write the id back.\n */\n getEditingId(): string;\n /** `getEditingId()` as the set the selection overlay skips. */\n getEditingIds(): ReadonlySet<string>;\n setEditingId(id: string | null): void;\n /** Selected anchors (flat indices) of the path being edited. */\n getSelectedAnchors(): ReadonlySet<number>;\n setSelectedAnchors(next: ReadonlySet<number>): void;\n /** The in-flight anchor-marquee rect, in the edited path's world. */\n getMarquee(): AnchorMarquee | null;\n setMarquee(rect: AnchorMarquee | null): void;\n /** The extension mark selected in place of anchors, or null. */\n getSelectedMark(): PathEditMark | null;\n setSelectedMark(mark: PathEditMark | null): void;\n /** The canvas's path-edit extensions, in order. */\n getExtensions(): readonly PathEditExtension[];\n /** Replace the extensions; the overlay repaints when they change. */\n setExtensions(next: readonly PathEditExtension[]): void;\n /** The handle locks the `handleLock.*` toggles set; null until one is\n * pressed. Not tied to the edited path, so they outlast a change of it. */\n getHandleLocks(): HandleLocks | null;\n setHandleLocks(next: HandleLocks): void;\n /** Notified when a write changes what the anchor overlay paints. A host\n * that owns the id announces its own changes. */\n subscribe(listener: () => void): () => void;\n}\n\nconst NO_IDS: ReadonlySet<string> = new Set();\nconst NO_EXTENSIONS: readonly PathEditExtension[] = [];\nconst NO_ANCHORS: ReadonlySet<number> = new Set();\n\n/** Whether `id` names a node `scene` holds and `selection` includes, or the path a selected effect hides and is edited\n * through. */\nexport function isSelectedNode(\n scene: Pick<Scene<unknown, string, unknown>, 'get'>,\n selection: { readonly current: readonly string[] },\n id: string,\n): boolean {\n if (scene.get(id as NodeId) == null) return false;\n return selection.current.includes(id)\n || selection.current.some((sel) => effectSourceFor(scene, sel as NodeId, 'path') === id);\n}\n\nexport function createPathEditState(source: PathEditSource = {}): PathEditState {\n let ownId = '';\n let selectedAnchors = NO_ANCHORS;\n let marquee: AnchorMarquee | null = null;\n let handleLocks: HandleLocks | null = null;\n let idSet = NO_IDS;\n let selectedMark: PathEditMark | null = null;\n let extensions = source.extensions ?? NO_EXTENSIONS;\n const changed = createEmitter();\n const notify = changed.emit;\n\n const next = (id: string | null): string => id ?? '';\n const getEditingId = (): string => {\n const host = source.host?.();\n const id = host ? host.editingId() ?? '' : ownId;\n if (!id) return '';\n if (source.isAllowed && !source.isAllowed()) return '';\n if (source.isSelectedNode && !source.isSelectedNode(id)) return '';\n return id;\n };\n\n return {\n getEditingId,\n getEditingIds() {\n const id = getEditingId();\n if (!id) return NO_IDS;\n if (!idSet.has(id)) idSet = new Set([id]);\n return idSet;\n },\n setEditingId(id) {\n const host = source.host?.();\n if (host) { host.setEditingId(id); return; }\n if (next(id) === ownId) return;\n ownId = next(id);\n notify();\n },\n getSelectedAnchors: () => {\n const host = source.host?.();\n return host?.selectedAnchors && host.setSelectedAnchors ? host.selectedAnchors() : selectedAnchors;\n },\n setSelectedAnchors(next) {\n const host = source.host?.();\n if (host?.selectedAnchors && host.setSelectedAnchors) { host.setSelectedAnchors(next); return; }\n selectedAnchors = next;\n notify();\n },\n getMarquee: () => marquee,\n setMarquee(rect) {\n marquee = rect;\n notify();\n },\n getSelectedMark: () => selectedMark,\n setSelectedMark(mark) {\n if (mark?.owner === selectedMark?.owner && mark?.index === selectedMark?.index) return;\n selectedMark = mark;\n notify();\n },\n getExtensions: () => extensions,\n setExtensions(next) {\n if (next === extensions) return;\n extensions = next;\n notify();\n },\n getHandleLocks: () => handleLocks,\n setHandleLocks(next) {\n handleLocks = { angle: next.angle, length: next.length };\n notify();\n },\n subscribe: changed.subscribe,\n };\n}\n"],"mappings":";;;;;;;;;;AA+EA,IAAM,yBAA8B,IAAI,IAAI;AAC5C,IAAM,gBAA8C,CAAC;AACrD,IAAM,6BAAkC,IAAI,IAAI;;;AAIhD,SAAgB,eACd,OACA,WACA,IACS;CACT,IAAI,MAAM,IAAI,EAAY,KAAK,MAAM,OAAO;CAC5C,OAAO,UAAU,QAAQ,SAAS,EAAE,KAC/B,UAAU,QAAQ,MAAM,QAAQ,gBAAgB,OAAO,KAAe,MAAM,MAAM,EAAE;AAC3F;AAEA,SAAgB,oBAAoB,SAAyB,CAAC,GAAkB;CAC9E,IAAI,QAAQ;CACZ,IAAI,kBAAkB;CACtB,IAAI,UAAgC;CACpC,IAAI,cAAkC;CACtC,IAAI,QAAQ;CACZ,IAAI,eAAoC;CACxC,IAAI,aAAa,OAAO,cAAc;CACtC,MAAM,UAAU,cAAc;CAC9B,MAAM,SAAS,QAAQ;CAEvB,MAAM,QAAQ,OAA8B,MAAM;CAClD,MAAM,qBAA6B;EACjC,MAAM,OAAO,OAAO,OAAO;EAC3B,MAAM,KAAK,OAAO,KAAK,UAAU,KAAK,KAAK;EAC3C,IAAI,CAAC,IAAI,OAAO;EAChB,IAAI,OAAO,aAAa,CAAC,OAAO,UAAU,GAAG,OAAO;EACpD,IAAI,OAAO,kBAAkB,CAAC,OAAO,eAAe,EAAE,GAAG,OAAO;EAChE,OAAO;CACT;CAEA,OAAO;EACL;EACA,gBAAgB;GACd,MAAM,KAAK,aAAa;GACxB,IAAI,CAAC,IAAI,OAAO;GAChB,IAAI,CAAC,MAAM,IAAI,EAAE,GAAG,wBAAQ,IAAI,IAAI,CAAC,EAAE,CAAC;GACxC,OAAO;EACT;EACA,aAAa,IAAI;GACf,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,MAAM;IAAE,KAAK,aAAa,EAAE;IAAG;GAAQ;GAC3C,IAAI,KAAK,EAAE,MAAM,OAAO;GACxB,QAAQ,KAAK,EAAE;GACf,OAAO;EACT;EACA,0BAA0B;GACxB,MAAM,OAAO,OAAO,OAAO;GAC3B,OAAO,MAAM,mBAAmB,KAAK,qBAAqB,KAAK,gBAAgB,IAAI;EACrF;EACA,mBAAmB,MAAM;GACvB,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,MAAM,mBAAmB,KAAK,oBAAoB;IAAE,KAAK,mBAAmB,IAAI;IAAG;GAAQ;GAC/F,kBAAkB;GAClB,OAAO;EACT;EACA,kBAAkB;EAClB,WAAW,MAAM;GACf,UAAU;GACV,OAAO;EACT;EACA,uBAAuB;EACvB,gBAAgB,MAAM;GACpB,IAAI,MAAM,UAAU,cAAc,SAAS,MAAM,UAAU,cAAc,OAAO;GAChF,eAAe;GACf,OAAO;EACT;EACA,qBAAqB;EACrB,cAAc,MAAM;GAClB,IAAI,SAAS,YAAY;GACzB,aAAa;GACb,OAAO;EACT;EACA,sBAAsB;EACtB,eAAe,MAAM;GACnB,cAAc;IAAE,OAAO,KAAK;IAAO,QAAQ,KAAK;GAAO;GACvD,OAAO;EACT;EACA,WAAW,QAAQ;CACrB;AACF"}
@@ -60,6 +60,11 @@ function moveProbes(press, ids, reads, box) {
60
60
  role: "origin"
61
61
  });
62
62
  }
63
+ if (reads.anchors) for (const id of ids) reads.anchors(id).forEach((at, i) => out.push({
64
+ id: `anchor:${id}:${i}`,
65
+ at,
66
+ role: "anchor"
67
+ }));
63
68
  if (box) out.push({
64
69
  id: "box.min",
65
70
  at: {
@@ -1 +1 @@
1
- {"version":3,"file":"probes2d.js","names":[],"sources":["../../../src/domains/snap/probes2d.ts"],"sourcesContent":["import { unionBounds } from 'core/geometry/unionBounds';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { SnapBegin } from 'interactions/actions/snapDep';\nimport type { SnapProbeAt } from './types';\nimport { resizeProbes, resizeSubject, type ResizeSubject } from './resizeBox';\nimport type { Point } from '@weasel-js/geom';\n\n/** How a 2D session reads its subject at press. */\nexport interface ProbeReads {\n /** `id`'s world pose now, or undefined when it is gone. */\n pose(id: string): unknown;\n /** The axis-aligned box `id` is drawn with at `pose`, stroke included. */\n drawnBox(id: string, pose: unknown): Bounds;\n /** The point of `pose` a grid rounds: its `OriginProjection` origin. */\n origin(pose: unknown): Point;\n /** The point of the subject's own geometry nearest `press`, or null. */\n geometryAt?(press: Point, ids: readonly string[]): Point | null;\n}\n\n/** The union drawn box of the subjects, or null when none is left. */\nexport function subjectBox(ids: readonly string[], reads: ProbeReads): Bounds | null {\n const boxes: Bounds[] = [];\n for (const id of ids) {\n const pose = reads.pose(id);\n if (pose !== undefined) boxes.push(reads.drawnBox(id, pose));\n }\n return unionBounds(boxes);\n}\n\n/** What a gesture drags, read once at press. */\nexport interface Subject2d {\n /** A move's subjects' union drawn box. */\n box: Bounds | null;\n /** A resize's box and frame: the one it names, else the subjects' drawn\n * box, axis-aligned. */\n resize: ResizeSubject | null;\n}\n\n/** The subject of `req`, null where the gesture has none or nothing is left. */\nexport function subjectOf(req: SnapBegin, reads: ProbeReads): Subject2d {\n if (req.gesture === 'move') return { box: subjectBox(req.ids ?? [], reads), resize: null };\n if (req.gesture !== 'resize' || !req.handle) return { box: null, resize: null };\n if (req.box) return { box: null, resize: resizeSubject(req.box, req.handle) };\n const bounds = subjectBox(req.ids ?? [], reads);\n return { box: null, resize: bounds ? resizeSubject({ bounds }, req.handle) : null };\n}\n\n/** The probes a gesture snaps, where they are at press. */\nexport function buildProbes(req: SnapBegin, reads: ProbeReads, subject = subjectOf(req, reads)): SnapProbeAt[] {\n const ids = req.ids ?? [];\n switch (req.gesture) {\n case 'point':\n return [{ id: 'pointer', at: req.press, role: 'pointer' }];\n case 'resize':\n return subject.resize ? resizeProbes(subject.resize) : [{ id: 'pointer', at: req.press, role: 'pointer' }];\n case 'move':\n return moveProbes(req.press, ids, reads, subject.box);\n case 'rotate':\n case 'handle':\n return [];\n }\n}\n\nfunction moveProbes(press: Point, ids: readonly string[], reads: ProbeReads, box: Bounds | null): SnapProbeAt[] {\n const out: SnapProbeAt[] = [];\n for (const id of ids) {\n const pose = reads.pose(id);\n if (pose !== undefined) out.push({ id: `origin:${id}`, at: reads.origin(pose), role: 'origin' });\n }\n if (box) {\n out.push(\n { id: 'box.min', at: { x: box.x, y: box.y }, role: 'box' },\n { id: 'box.center', at: { x: box.x + box.width / 2, y: box.y + box.height / 2 }, role: 'box' },\n { id: 'box.max', at: { x: box.x + box.width, y: box.y + box.height }, role: 'box' },\n );\n }\n const geometry = reads.geometryAt?.(press, ids) ?? press;\n out.push({ id: 'geometry', at: { x: geometry.x, y: geometry.y }, role: 'geometry' });\n return out;\n}\n"],"mappings":";;;;;AAoBA,SAAgB,WAAW,KAAwB,OAAkC;CACnF,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,KAAK,EAAE;EAC1B,IAAI,SAAS,QAAW,MAAM,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC;CAC7D;CACA,OAAO,YAAY,KAAK;AAC1B;;AAYA,SAAgB,UAAU,KAAgB,OAA8B;CACtE,IAAI,IAAI,YAAY,QAAQ,OAAO;EAAE,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK;EAAG,QAAQ;CAAK;CACzF,IAAI,IAAI,YAAY,YAAY,CAAC,IAAI,QAAQ,OAAO;EAAE,KAAK;EAAM,QAAQ;CAAK;CAC9E,IAAI,IAAI,KAAK,OAAO;EAAE,KAAK;EAAM,QAAQ,cAAc,IAAI,KAAK,IAAI,MAAM;CAAE;CAC5E,MAAM,SAAS,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK;CAC9C,OAAO;EAAE,KAAK;EAAM,QAAQ,SAAS,cAAc,EAAE,OAAO,GAAG,IAAI,MAAM,IAAI;CAAK;AACpF;;AAGA,SAAgB,YAAY,KAAgB,OAAmB,UAAU,UAAU,KAAK,KAAK,GAAkB;CAC7G,MAAM,MAAM,IAAI,OAAO,CAAC;CACxB,QAAQ,IAAI,SAAZ;EACE,KAAK,SACH,OAAO,CAAC;GAAE,IAAI;GAAW,IAAI,IAAI;GAAO,MAAM;EAAU,CAAC;EAC3D,KAAK,UACH,OAAO,QAAQ,SAAS,aAAa,QAAQ,MAAM,IAAI,CAAC;GAAE,IAAI;GAAW,IAAI,IAAI;GAAO,MAAM;EAAU,CAAC;EAC3G,KAAK,QACH,OAAO,WAAW,IAAI,OAAO,KAAK,OAAO,QAAQ,GAAG;EACtD,KAAK;EACL,KAAK,UACH,OAAO,CAAC;CACZ;AACF;AAEA,SAAS,WAAW,OAAc,KAAwB,OAAmB,KAAmC;CAC9G,MAAM,MAAqB,CAAC;CAC5B,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,KAAK,EAAE;EAC1B,IAAI,SAAS,QAAW,IAAI,KAAK;GAAE,IAAI,UAAU;GAAM,IAAI,MAAM,OAAO,IAAI;GAAG,MAAM;EAAS,CAAC;CACjG;CACA,IAAI,KACF,IAAI,KACF;EAAE,IAAI;EAAW,IAAI;GAAE,GAAG,IAAI;GAAG,GAAG,IAAI;EAAE;EAAG,MAAM;CAAM,GACzD;EAAE,IAAI;EAAc,IAAI;GAAE,GAAG,IAAI,IAAI,IAAI,QAAQ;GAAG,GAAG,IAAI,IAAI,IAAI,SAAS;EAAE;EAAG,MAAM;CAAM,GAC7F;EAAE,IAAI;EAAW,IAAI;GAAE,GAAG,IAAI,IAAI,IAAI;GAAO,GAAG,IAAI,IAAI,IAAI;EAAO;EAAG,MAAM;CAAM,CACpF;CAEF,MAAM,WAAW,MAAM,aAAa,OAAO,GAAG,KAAK;CACnD,IAAI,KAAK;EAAE,IAAI;EAAY,IAAI;GAAE,GAAG,SAAS;GAAG,GAAG,SAAS;EAAE;EAAG,MAAM;CAAW,CAAC;CACnF,OAAO;AACT"}
1
+ {"version":3,"file":"probes2d.js","names":[],"sources":["../../../src/domains/snap/probes2d.ts"],"sourcesContent":["import { unionBounds } from 'core/geometry/unionBounds';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { SnapBegin } from 'interactions/actions/snapDep';\nimport type { SnapProbeAt } from './types';\nimport { resizeProbes, resizeSubject, type ResizeSubject } from './resizeBox';\nimport type { Point } from '@weasel-js/geom';\n\n/** How a 2D session reads its subject at press. */\nexport interface ProbeReads {\n /** `id`'s world pose now, or undefined when it is gone. */\n pose(id: string): unknown;\n /** The axis-aligned box `id` is drawn with at `pose`, stroke included. */\n drawnBox(id: string, pose: unknown): Bounds;\n /** The point of `pose` the `origin` probe sits at: its `OriginProjection` origin. */\n origin(pose: unknown): Point;\n /** The point of the subject's own geometry nearest `press`, or null. */\n geometryAt?(press: Point, ids: readonly string[]): Point | null;\n /** `id`'s declared snap anchors, in world. */\n anchors?(id: string): readonly Point[];\n}\n\n/** The union drawn box of the subjects, or null when none is left. */\nexport function subjectBox(ids: readonly string[], reads: ProbeReads): Bounds | null {\n const boxes: Bounds[] = [];\n for (const id of ids) {\n const pose = reads.pose(id);\n if (pose !== undefined) boxes.push(reads.drawnBox(id, pose));\n }\n return unionBounds(boxes);\n}\n\n/** What a gesture drags, read once at press. */\nexport interface Subject2d {\n /** A move's subjects' union drawn box. */\n box: Bounds | null;\n /** A resize's box and frame: the one it names, else the subjects' drawn\n * box, axis-aligned. */\n resize: ResizeSubject | null;\n}\n\n/** The subject of `req`, null where the gesture has none or nothing is left. */\nexport function subjectOf(req: SnapBegin, reads: ProbeReads): Subject2d {\n if (req.gesture === 'move') return { box: subjectBox(req.ids ?? [], reads), resize: null };\n if (req.gesture !== 'resize' || !req.handle) return { box: null, resize: null };\n if (req.box) return { box: null, resize: resizeSubject(req.box, req.handle) };\n const bounds = subjectBox(req.ids ?? [], reads);\n return { box: null, resize: bounds ? resizeSubject({ bounds }, req.handle) : null };\n}\n\n/** The probes a gesture snaps, where they are at press. */\nexport function buildProbes(req: SnapBegin, reads: ProbeReads, subject = subjectOf(req, reads)): SnapProbeAt[] {\n const ids = req.ids ?? [];\n switch (req.gesture) {\n case 'point':\n return [{ id: 'pointer', at: req.press, role: 'pointer' }];\n case 'resize':\n return subject.resize ? resizeProbes(subject.resize) : [{ id: 'pointer', at: req.press, role: 'pointer' }];\n case 'move':\n return moveProbes(req.press, ids, reads, subject.box);\n case 'rotate':\n case 'handle':\n return [];\n }\n}\n\nfunction moveProbes(press: Point, ids: readonly string[], reads: ProbeReads, box: Bounds | null): SnapProbeAt[] {\n const out: SnapProbeAt[] = [];\n for (const id of ids) {\n const pose = reads.pose(id);\n if (pose !== undefined) out.push({ id: `origin:${id}`, at: reads.origin(pose), role: 'origin' });\n }\n if (reads.anchors) {\n for (const id of ids) {\n reads.anchors(id).forEach((at, i) => out.push({ id: `anchor:${id}:${i}`, at, role: 'anchor' }));\n }\n }\n if (box) {\n out.push(\n { id: 'box.min', at: { x: box.x, y: box.y }, role: 'box' },\n { id: 'box.center', at: { x: box.x + box.width / 2, y: box.y + box.height / 2 }, role: 'box' },\n { id: 'box.max', at: { x: box.x + box.width, y: box.y + box.height }, role: 'box' },\n );\n }\n const geometry = reads.geometryAt?.(press, ids) ?? press;\n out.push({ id: 'geometry', at: { x: geometry.x, y: geometry.y }, role: 'geometry' });\n return out;\n}\n"],"mappings":";;;;;AAsBA,SAAgB,WAAW,KAAwB,OAAkC;CACnF,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,KAAK,EAAE;EAC1B,IAAI,SAAS,QAAW,MAAM,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC;CAC7D;CACA,OAAO,YAAY,KAAK;AAC1B;;AAYA,SAAgB,UAAU,KAAgB,OAA8B;CACtE,IAAI,IAAI,YAAY,QAAQ,OAAO;EAAE,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK;EAAG,QAAQ;CAAK;CACzF,IAAI,IAAI,YAAY,YAAY,CAAC,IAAI,QAAQ,OAAO;EAAE,KAAK;EAAM,QAAQ;CAAK;CAC9E,IAAI,IAAI,KAAK,OAAO;EAAE,KAAK;EAAM,QAAQ,cAAc,IAAI,KAAK,IAAI,MAAM;CAAE;CAC5E,MAAM,SAAS,WAAW,IAAI,OAAO,CAAC,GAAG,KAAK;CAC9C,OAAO;EAAE,KAAK;EAAM,QAAQ,SAAS,cAAc,EAAE,OAAO,GAAG,IAAI,MAAM,IAAI;CAAK;AACpF;;AAGA,SAAgB,YAAY,KAAgB,OAAmB,UAAU,UAAU,KAAK,KAAK,GAAkB;CAC7G,MAAM,MAAM,IAAI,OAAO,CAAC;CACxB,QAAQ,IAAI,SAAZ;EACE,KAAK,SACH,OAAO,CAAC;GAAE,IAAI;GAAW,IAAI,IAAI;GAAO,MAAM;EAAU,CAAC;EAC3D,KAAK,UACH,OAAO,QAAQ,SAAS,aAAa,QAAQ,MAAM,IAAI,CAAC;GAAE,IAAI;GAAW,IAAI,IAAI;GAAO,MAAM;EAAU,CAAC;EAC3G,KAAK,QACH,OAAO,WAAW,IAAI,OAAO,KAAK,OAAO,QAAQ,GAAG;EACtD,KAAK;EACL,KAAK,UACH,OAAO,CAAC;CACZ;AACF;AAEA,SAAS,WAAW,OAAc,KAAwB,OAAmB,KAAmC;CAC9G,MAAM,MAAqB,CAAC;CAC5B,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,KAAK,EAAE;EAC1B,IAAI,SAAS,QAAW,IAAI,KAAK;GAAE,IAAI,UAAU;GAAM,IAAI,MAAM,OAAO,IAAI;GAAG,MAAM;EAAS,CAAC;CACjG;CACA,IAAI,MAAM,SACR,KAAK,MAAM,MAAM,KACf,MAAM,QAAQ,EAAE,CAAC,CAAC,SAAS,IAAI,MAAM,IAAI,KAAK;EAAE,IAAI,UAAU,GAAG,GAAG;EAAK;EAAI,MAAM;CAAS,CAAC,CAAC;CAGlG,IAAI,KACF,IAAI,KACF;EAAE,IAAI;EAAW,IAAI;GAAE,GAAG,IAAI;GAAG,GAAG,IAAI;EAAE;EAAG,MAAM;CAAM,GACzD;EAAE,IAAI;EAAc,IAAI;GAAE,GAAG,IAAI,IAAI,IAAI,QAAQ;GAAG,GAAG,IAAI,IAAI,IAAI,SAAS;EAAE;EAAG,MAAM;CAAM,GAC7F;EAAE,IAAI;EAAW,IAAI;GAAE,GAAG,IAAI,IAAI,IAAI;GAAO,GAAG,IAAI,IAAI,IAAI;EAAO;EAAG,MAAM;CAAM,CACpF;CAEF,MAAM,WAAW,MAAM,aAAa,OAAO,GAAG,KAAK;CACnD,IAAI,KAAK;EAAE,IAAI;EAAY,IAAI;GAAE,GAAG,SAAS;GAAG,GAAG,SAAS;EAAE;EAAG,MAAM;CAAW,CAAC;CACnF,OAAO;AACT"}
@@ -80,10 +80,10 @@ function movedBox(s, delta) {
80
80
  };
81
81
  return movedEdges(s.bounds, s.handle, local.x, local.y);
82
82
  }
83
- /** The box's axis-aligned extent once the handle has moved by `delta`. */
84
- function resizedBox(s, delta) {
83
+ /** The box's corners in ctx coordinates once the handle has moved by `delta`. */
84
+ function cornersAt(s, delta) {
85
85
  const b = movedBox(s, delta);
86
- const corners = [
86
+ return [
87
87
  s.at({
88
88
  x: b.x,
89
89
  y: b.y
@@ -101,6 +101,10 @@ function resizedBox(s, delta) {
101
101
  y: b.y + b.height
102
102
  })
103
103
  ];
104
+ }
105
+ /** The box's axis-aligned extent once the handle has moved by `delta`. */
106
+ function resizedBox(s, delta) {
107
+ const corners = cornersAt(s, delta);
104
108
  const xs = corners.map((c) => c.x);
105
109
  const ys = corners.map((c) => c.y);
106
110
  const x = Math.min(...xs);
@@ -112,26 +116,43 @@ function resizedBox(s, delta) {
112
116
  height: Math.max(...ys) - y
113
117
  };
114
118
  }
115
- /** The sides of {@link resizedBox} a drag of `delta` moves: the side across
116
- * from the pinned one, or the pinned side's once the drag has flipped the
117
- * box past it. A frame that turns or flips the box moves no side of its
118
- * extent alone. */
119
- function resizeEdges(s, delta) {
120
- if (!(Math.abs(s.ex.y) < EPS && Math.abs(s.ey.x) < EPS && s.ex.x > 0 && s.ey.y > 0)) return {
121
- x: null,
122
- y: null
123
- };
124
- const b = movedBox(s, delta);
125
- const moved = (pin, size) => {
126
- if (pin === "free") return null;
127
- return size >= 0 ? pin === "min" ? "max" : "min" : pin;
128
- };
129
- return {
130
- x: moved(s.handle.x, b.width),
131
- y: moved(s.handle.y, b.height)
132
- };
119
+ var near = (a, b, scale) => Math.abs(a - b) <= 1e-9 * Math.max(1, scale);
120
+ /** The sides of {@link resizedBox} a drag at `delta` moves, and how fast. A
121
+ * side moves with the corners at its extreme; where two of them tie and move
122
+ * apart, as a turned box's do at a kink of its extent, it is left out. */
123
+ function resizeSides(s, delta) {
124
+ const now = cornersAt(s, delta);
125
+ const stepX = cornersAt(s, {
126
+ x: delta.x + 1,
127
+ y: delta.y
128
+ });
129
+ const stepY = cornersAt(s, {
130
+ x: delta.x,
131
+ y: delta.y + 1
132
+ });
133
+ const out = [];
134
+ for (const axis of ["x", "y"]) {
135
+ const vs = now.map((c) => c[axis]);
136
+ const span = Math.max(...vs) - Math.min(...vs);
137
+ for (const side of ["min", "max"]) {
138
+ const edge = side === "min" ? Math.min(...vs) : Math.max(...vs);
139
+ const rates = now.flatMap((c, i) => near(c[axis], edge, span) ? [{
140
+ x: stepX[i][axis] - c[axis],
141
+ y: stepY[i][axis] - c[axis]
142
+ }] : []);
143
+ const rate = rates[0];
144
+ const size = Math.hypot(rate.x, rate.y);
145
+ if (size < EPS || rates.some((r) => !near(r.x, rate.x, size) || !near(r.y, rate.y, size))) continue;
146
+ out.push({
147
+ axis,
148
+ side,
149
+ rate
150
+ });
151
+ }
152
+ }
153
+ return out;
133
154
  }
134
155
 
135
156
  //#endregion
136
- export { EPS, diagonal, edgeAxis, resizeEdges, resizeProbes, resizeSubject, resizedBox };
157
+ export { EPS, diagonal, edgeAxis, resizeProbes, resizeSides, resizeSubject, resizedBox };
137
158
  //# sourceMappingURL=resizeBox.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resizeBox.js","names":[],"sources":["../../../src/domains/snap/resizeBox.ts"],"sourcesContent":["import type { ResizeAnchor } from '@weasel-js/routing';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { fixedCornerOf, movedEdges, movingPoint } from 'interactions/actions/resize/cornerHandles';\nimport type { SnapResizeBox } from 'interactions/actions/snapDep';\nimport type { SnapEdge, SnapEdges, SnapProbeAt } from './types';\nimport type { Point } from '@weasel-js/geom';\n\nexport const EPS = 1e-9;\n\n/** A resize's box with its frame read out, in ctx coordinates. */\nexport interface ResizeSubject {\n /** In the box's own frame. */\n bounds: Bounds;\n handle: ResizeAnchor;\n at(p: Point): Point;\n /** Where the frame's unit axes point. */\n ex: Point;\n ey: Point;\n}\n\nexport function resizeSubject(box: SnapResizeBox, handle: ResizeAnchor): ResizeSubject {\n const at = box.toWorld ?? ((p: Point) => p);\n const o = at({ x: 0, y: 0 });\n const ax = at({ x: 1, y: 0 });\n const ay = at({ x: 0, y: 1 });\n return { bounds: box.bounds, handle, at, ex: { x: ax.x - o.x, y: ax.y - o.y }, ey: { x: ay.x - o.x, y: ay.y - o.y } };\n}\n\n/** The dragged corner or edge midpoint as `pointer`, and as `box` the dragged\n * corner or both ends of the dragged edge: the points a drag δ moves by δ. */\nexport function resizeProbes(s: ResizeSubject): SnapProbeAt[] {\n const { bounds: b, handle } = s;\n const out: SnapProbeAt[] = [{ id: 'pointer', at: s.at(movingPoint(b, handle)), role: 'pointer' }];\n const sides = (pin: ResizeAnchor['x'], lo: number, size: number): number[] =>\n pin === 'min' ? [lo + size] : pin === 'max' ? [lo] : [lo, lo + size];\n const xs = sides(handle.x, b.x, b.width);\n const ys = sides(handle.y, b.y, b.height);\n if (xs.length === 2 && ys.length === 2) return out;\n for (const x of xs) for (const y of ys) out.push({ id: `box.${out.length - 1}`, at: s.at({ x, y }), role: 'box' });\n return out;\n}\n\n/** The direction an edge handle's edge moves along; null for a corner. */\nexport function edgeAxis(s: ResizeSubject): Point | null {\n const { x, y } = s.handle;\n if ((x === 'free') === (y === 'free')) return null;\n return y === 'free' ? s.ex : s.ey;\n}\n\n/** From the fixed corner to the dragged one. */\nexport function diagonal(s: ResizeSubject): Point {\n const f = s.at(fixedCornerOf(s.bounds, s.handle));\n const m = s.at(movingPoint(s.bounds, s.handle));\n return { x: m.x - f.x, y: m.y - f.y };\n}\n\n/** The box in its own frame once the handle has moved by `delta`. */\nfunction movedBox(s: ResizeSubject, delta: Point): Bounds {\n const det = s.ex.x * s.ey.y - s.ey.x * s.ex.y;\n const local = Math.abs(det) < EPS\n ? { x: 0, y: 0 }\n : { x: (s.ey.y * delta.x - s.ey.x * delta.y) / det, y: (s.ex.x * delta.y - s.ex.y * delta.x) / det };\n return movedEdges(s.bounds, s.handle, local.x, local.y);\n}\n\n/** The box's axis-aligned extent once the handle has moved by `delta`. */\nexport function resizedBox(s: ResizeSubject, delta: Point): Bounds {\n const b = movedBox(s, delta);\n const corners = [\n s.at({ x: b.x, y: b.y }), s.at({ x: b.x + b.width, y: b.y }),\n s.at({ x: b.x, y: b.y + b.height }), s.at({ x: b.x + b.width, y: b.y + b.height }),\n ];\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n const x = Math.min(...xs);\n const y = Math.min(...ys);\n return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };\n}\n\n/** The sides of {@link resizedBox} a drag of `delta` moves: the side across\n * from the pinned one, or the pinned side's once the drag has flipped the\n * box past it. A frame that turns or flips the box moves no side of its\n * extent alone. */\nexport function resizeEdges(s: ResizeSubject, delta: Point): SnapEdges {\n const square = Math.abs(s.ex.y) < EPS && Math.abs(s.ey.x) < EPS && s.ex.x > 0 && s.ey.y > 0;\n if (!square) return { x: null, y: null };\n const b = movedBox(s, delta);\n const moved = (pin: ResizeAnchor['x'], size: number): SnapEdge => {\n if (pin === 'free') return null;\n const far = pin === 'min' ? 'max' : 'min';\n return size >= 0 ? far : pin;\n };\n return { x: moved(s.handle.x, b.width), y: moved(s.handle.y, b.height) };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"resizeBox.js","names":[],"sources":["../../../src/domains/snap/resizeBox.ts"],"sourcesContent":["import type { ResizeAnchor } from '@weasel-js/routing';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { fixedCornerOf, movedEdges, movingPoint } from 'interactions/actions/resize/cornerHandles';\nimport type { SnapResizeBox } from 'interactions/actions/snapDep';\nimport type { SnapProbeAt, SnapSide } from './types';\nimport type { Point } from '@weasel-js/geom';\n\nexport const EPS = 1e-9;\n\n/** A resize's box with its frame read out, in ctx coordinates. */\nexport interface ResizeSubject {\n /** In the box's own frame. */\n bounds: Bounds;\n handle: ResizeAnchor;\n at(p: Point): Point;\n /** Where the frame's unit axes point. */\n ex: Point;\n ey: Point;\n}\n\nexport function resizeSubject(box: SnapResizeBox, handle: ResizeAnchor): ResizeSubject {\n const at = box.toWorld ?? ((p: Point) => p);\n const o = at({ x: 0, y: 0 });\n const ax = at({ x: 1, y: 0 });\n const ay = at({ x: 0, y: 1 });\n return { bounds: box.bounds, handle, at, ex: { x: ax.x - o.x, y: ax.y - o.y }, ey: { x: ay.x - o.x, y: ay.y - o.y } };\n}\n\n/** The dragged corner or edge midpoint as `pointer`, and as `box` the dragged\n * corner or both ends of the dragged edge: the points a drag δ moves by δ. */\nexport function resizeProbes(s: ResizeSubject): SnapProbeAt[] {\n const { bounds: b, handle } = s;\n const out: SnapProbeAt[] = [{ id: 'pointer', at: s.at(movingPoint(b, handle)), role: 'pointer' }];\n const sides = (pin: ResizeAnchor['x'], lo: number, size: number): number[] =>\n pin === 'min' ? [lo + size] : pin === 'max' ? [lo] : [lo, lo + size];\n const xs = sides(handle.x, b.x, b.width);\n const ys = sides(handle.y, b.y, b.height);\n if (xs.length === 2 && ys.length === 2) return out;\n for (const x of xs) for (const y of ys) out.push({ id: `box.${out.length - 1}`, at: s.at({ x, y }), role: 'box' });\n return out;\n}\n\n/** The direction an edge handle's edge moves along; null for a corner. */\nexport function edgeAxis(s: ResizeSubject): Point | null {\n const { x, y } = s.handle;\n if ((x === 'free') === (y === 'free')) return null;\n return y === 'free' ? s.ex : s.ey;\n}\n\n/** From the fixed corner to the dragged one. */\nexport function diagonal(s: ResizeSubject): Point {\n const f = s.at(fixedCornerOf(s.bounds, s.handle));\n const m = s.at(movingPoint(s.bounds, s.handle));\n return { x: m.x - f.x, y: m.y - f.y };\n}\n\n/** The box in its own frame once the handle has moved by `delta`. */\nfunction movedBox(s: ResizeSubject, delta: Point): Bounds {\n const det = s.ex.x * s.ey.y - s.ey.x * s.ex.y;\n const local = Math.abs(det) < EPS\n ? { x: 0, y: 0 }\n : { x: (s.ey.y * delta.x - s.ey.x * delta.y) / det, y: (s.ex.x * delta.y - s.ex.y * delta.x) / det };\n return movedEdges(s.bounds, s.handle, local.x, local.y);\n}\n\n/** The box's corners in ctx coordinates once the handle has moved by `delta`. */\nfunction cornersAt(s: ResizeSubject, delta: Point): Point[] {\n const b = movedBox(s, delta);\n return [\n s.at({ x: b.x, y: b.y }), s.at({ x: b.x + b.width, y: b.y }),\n s.at({ x: b.x, y: b.y + b.height }), s.at({ x: b.x + b.width, y: b.y + b.height }),\n ];\n}\n\n/** The box's axis-aligned extent once the handle has moved by `delta`. */\nexport function resizedBox(s: ResizeSubject, delta: Point): Bounds {\n const corners = cornersAt(s, delta);\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n const x = Math.min(...xs);\n const y = Math.min(...ys);\n return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };\n}\n\nconst near = (a: number, b: number, scale: number): boolean => Math.abs(a - b) <= 1e-9 * Math.max(1, scale);\n\n/** The sides of {@link resizedBox} a drag at `delta` moves, and how fast. A\n * side moves with the corners at its extreme; where two of them tie and move\n * apart, as a turned box's do at a kink of its extent, it is left out. */\nexport function resizeSides(s: ResizeSubject, delta: Point): SnapSide[] {\n const now = cornersAt(s, delta);\n // The corners are affine in the drag, so a unit step reads their rate exactly.\n const stepX = cornersAt(s, { x: delta.x + 1, y: delta.y });\n const stepY = cornersAt(s, { x: delta.x, y: delta.y + 1 });\n const out: SnapSide[] = [];\n for (const axis of ['x', 'y'] as const) {\n const vs = now.map((c) => c[axis]);\n const span = Math.max(...vs) - Math.min(...vs);\n for (const side of ['min', 'max'] as const) {\n const edge = side === 'min' ? Math.min(...vs) : Math.max(...vs);\n const rates = now.flatMap((c, i) => (near(c[axis], edge, span)\n ? [{ x: stepX[i]![axis] - c[axis], y: stepY[i]![axis] - c[axis] }]\n : []));\n const rate = rates[0]!;\n const size = Math.hypot(rate.x, rate.y);\n if (size < EPS || rates.some((r) => !near(r.x, rate.x, size) || !near(r.y, rate.y, size))) continue;\n out.push({ axis, side, rate });\n }\n }\n return out;\n}\n"],"mappings":"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"}
@@ -9,11 +9,21 @@ import { applyToPoint, invert } from "@weasel-js/geom";
9
9
  function sceneProbeReads(args) {
10
10
  const { scene } = args;
11
11
  const frame = scenePoseFrame(scene);
12
+ const anchors = args.anchors;
12
13
  return {
13
14
  pose: (id) => scene.get(id) === void 0 ? void 0 : frame.world(id),
14
15
  drawnBox: (id, pose) => args.boxes.aabb(id, pose),
15
16
  origin: (pose) => (args.originProjection ?? AUTO_ORIGIN_PROJECTION).getOrigin(pose),
16
- ...args.geometryAt ? { geometryAt: args.geometryAt } : {}
17
+ ...args.geometryAt ? { geometryAt: args.geometryAt } : {},
18
+ ...anchors ? { anchors: worldAnchors(scene, anchors) } : {}
19
+ };
20
+ }
21
+ function worldAnchors(scene, anchors) {
22
+ const frames = sceneSnapFrames(scene);
23
+ return (id) => {
24
+ const local = anchors(id);
25
+ const frame = local?.length ? frames(id) : void 0;
26
+ return local && frame ? local.map((p) => frame.toWorld(p)) : [];
17
27
  };
18
28
  }
19
29
  /** Each node's world rotation, read through its own descriptor the way a
@@ -1 +1 @@
1
- {"version":3,"file":"sceneReads.js","names":[],"sources":["../../../src/domains/snap/sceneReads.ts"],"sourcesContent":["import { applyToPoint, invert, type Point } from '@weasel-js/geom';\nimport type { WorldPoint } from '@weasel-js/routing';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport type { DrawnBoxes } from 'interactions/actions/drawnBoxes';\nimport { nodeDescriptors, poseDescriptorOf } from 'interactions/actions/poseDescriptorDep';\nimport { sceneBoxFrames, scenePoseFrame } from 'interactions/actions/poseFrame';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport { AUTO_ORIGIN_PROJECTION, type OriginProjection } from 'interactions/gestures/shared/originProjection';\nimport type { ProbeReads } from './probes2d';\nimport type { SnapFrame } from './types';\n\n/** {@link ProbeReads} over a canvas's scene, read when a session begins.\n * `boxes` is the canvas's `drawnBoxes`, which alignment measures with too. */\nexport function sceneProbeReads(args: {\n scene: Scene<unknown, string, unknown>;\n boxes: DrawnBoxes;\n geometryAt?: (press: WorldPoint, ids: readonly string[]) => WorldPoint | null;\n /** How a pose's origin is read. Default `AUTO_ORIGIN_PROJECTION`. */\n originProjection?: OriginProjection<unknown>;\n}): ProbeReads {\n const { scene } = args;\n const frame = scenePoseFrame(scene);\n return {\n pose: (id) => (scene.get(id as NodeId) === undefined ? undefined : frame.world(id as NodeId)),\n drawnBox: (id, pose) => args.boxes.aabb(id, pose),\n origin: (pose) => (args.originProjection ?? AUTO_ORIGIN_PROJECTION).getOrigin(pose),\n ...(args.geometryAt ? { geometryAt: args.geometryAt } : {}),\n };\n}\n\n/** Each node's world rotation, read through its own descriptor the way a\n * rotate reads its subject; undefined for a node that cannot turn. */\nexport function sceneRotations(\n scene: Scene<unknown, string, unknown>,\n descriptor?: PoseDescriptor<unknown>,\n): (id: string) => number | undefined {\n const frame = scenePoseFrame(scene);\n const descriptorOf = nodeDescriptors(scene, poseDescriptorOf(descriptor));\n return (id) => {\n const node = scene.get(id as NodeId);\n if (!node) return undefined;\n const d = descriptorOf(id);\n if (!d.withRotation || !d.getRotation || d.supportsRotation?.(node.pose) === false) return undefined;\n return d.getRotation(frame.world(id as NodeId));\n };\n}\n\n/** Each node's box frame, as `SnapCollect.frameOf` hands it to a source. */\nexport function sceneSnapFrames(scene: Scene<unknown, string, unknown>): (id: string) => SnapFrame | undefined {\n const boxes = sceneBoxFrames(scene);\n return (id) => {\n const m = boxes(id);\n if (!m) return undefined;\n const back = invert(m);\n const through = (t: typeof m, p: Point): Point => {\n const [x, y] = applyToPoint(t, p.x, p.y);\n return { x, y };\n };\n return { toWorld: (p) => through(m, p), toLocal: (p) => (back ? through(back, p) : null) };\n };\n}\n\n/** `ids` and every node under them: what a gesture moving `ids` carries. */\nexport function withSubtrees(scene: Scene<unknown, string, unknown>, ids: readonly string[]): string[] {\n const out = new Set<string>();\n const walk = (id: string): void => {\n if (out.has(id)) return;\n out.add(id);\n if (scene.get(id as NodeId) !== undefined) for (const c of scene.childrenOf(id as NodeId)) walk(c);\n };\n for (const id of ids) walk(id);\n return [...out];\n}\n"],"mappings":";;;;;;;;AAaA,SAAgB,gBAAgB,MAMjB;CACb,MAAM,EAAE,UAAU;CAClB,MAAM,QAAQ,eAAe,KAAK;CAClC,OAAO;EACL,OAAO,OAAQ,MAAM,IAAI,EAAY,MAAM,SAAY,SAAY,MAAM,MAAM,EAAY;EAC3F,WAAW,IAAI,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI;EAChD,SAAS,UAAU,KAAK,oBAAoB,uBAAA,CAAwB,UAAU,IAAI;EAClF,GAAI,KAAK,aAAa,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;CAC3D;AACF;;;AAIA,SAAgB,eACd,OACA,YACoC;CACpC,MAAM,QAAQ,eAAe,KAAK;CAClC,MAAM,eAAe,gBAAgB,OAAO,iBAAiB,UAAU,CAAC;CACxE,QAAQ,OAAO;EACb,MAAM,OAAO,MAAM,IAAI,EAAY;EACnC,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,IAAI,aAAa,EAAE;EACzB,IAAI,CAAC,EAAE,gBAAgB,CAAC,EAAE,eAAe,EAAE,mBAAmB,KAAK,IAAI,MAAM,OAAO,OAAO;EAC3F,OAAO,EAAE,YAAY,MAAM,MAAM,EAAY,CAAC;CAChD;AACF;;AAGA,SAAgB,gBAAgB,OAA+E;CAC7G,MAAM,QAAQ,eAAe,KAAK;CAClC,QAAQ,OAAO;EACb,MAAM,IAAI,MAAM,EAAE;EAClB,IAAI,CAAC,GAAG,OAAO;EACf,MAAM,OAAO,OAAO,CAAC;EACrB,MAAM,WAAW,GAAa,MAAoB;GAChD,MAAM,CAAC,GAAG,KAAK,aAAa,GAAG,EAAE,GAAG,EAAE,CAAC;GACvC,OAAO;IAAE;IAAG;GAAE;EAChB;EACA,OAAO;GAAE,UAAU,MAAM,QAAQ,GAAG,CAAC;GAAG,UAAU,MAAO,OAAO,QAAQ,MAAM,CAAC,IAAI;EAAM;CAC3F;AACF;;AAGA,SAAgB,aAAa,OAAwC,KAAkC;CACrG,MAAM,sBAAM,IAAI,IAAY;CAC5B,MAAM,QAAQ,OAAqB;EACjC,IAAI,IAAI,IAAI,EAAE,GAAG;EACjB,IAAI,IAAI,EAAE;EACV,IAAI,MAAM,IAAI,EAAY,MAAM,QAAW,KAAK,MAAM,KAAK,MAAM,WAAW,EAAY,GAAG,KAAK,CAAC;CACnG;CACA,KAAK,MAAM,MAAM,KAAK,KAAK,EAAE;CAC7B,OAAO,CAAC,GAAG,GAAG;AAChB"}
1
+ {"version":3,"file":"sceneReads.js","names":[],"sources":["../../../src/domains/snap/sceneReads.ts"],"sourcesContent":["import { applyToPoint, invert, type Point } from '@weasel-js/geom';\nimport type { WorldPoint } from '@weasel-js/routing';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport type { DrawnBoxes } from 'interactions/actions/drawnBoxes';\nimport { nodeDescriptors, poseDescriptorOf } from 'interactions/actions/poseDescriptorDep';\nimport { sceneBoxFrames, scenePoseFrame } from 'interactions/actions/poseFrame';\nimport type { PoseDescriptor } from 'interactions/actions/resize/geometry';\nimport { AUTO_ORIGIN_PROJECTION, type OriginProjection } from 'interactions/gestures/shared/originProjection';\nimport type { ProbeReads } from './probes2d';\nimport type { SnapAnchors, SnapFrame } from './types';\n\n/** {@link ProbeReads} over a canvas's scene, read when a session begins.\n * `boxes` is the canvas's `drawnBoxes`, which alignment measures with too. */\nexport function sceneProbeReads(args: {\n scene: Scene<unknown, string, unknown>;\n boxes: DrawnBoxes;\n geometryAt?: (press: WorldPoint, ids: readonly string[]) => WorldPoint | null;\n /** How a pose's origin is read. Default `AUTO_ORIGIN_PROJECTION`. */\n originProjection?: OriginProjection<unknown>;\n /** Each node's snap anchors, in its box frame. */\n anchors?: SnapAnchors;\n}): ProbeReads {\n const { scene } = args;\n const frame = scenePoseFrame(scene);\n const anchors = args.anchors;\n return {\n pose: (id) => (scene.get(id as NodeId) === undefined ? undefined : frame.world(id as NodeId)),\n drawnBox: (id, pose) => args.boxes.aabb(id, pose),\n origin: (pose) => (args.originProjection ?? AUTO_ORIGIN_PROJECTION).getOrigin(pose),\n ...(args.geometryAt ? { geometryAt: args.geometryAt } : {}),\n ...(anchors ? { anchors: worldAnchors(scene, anchors) } : {}),\n };\n}\n\nfunction worldAnchors(scene: Scene<unknown, string, unknown>, anchors: SnapAnchors): (id: string) => readonly Point[] {\n const frames = sceneSnapFrames(scene);\n return (id) => {\n const local = anchors(id);\n const frame = local?.length ? frames(id) : undefined;\n return local && frame ? local.map((p) => frame.toWorld(p)) : [];\n };\n}\n\n/** Each node's world rotation, read through its own descriptor the way a\n * rotate reads its subject; undefined for a node that cannot turn. */\nexport function sceneRotations(\n scene: Scene<unknown, string, unknown>,\n descriptor?: PoseDescriptor<unknown>,\n): (id: string) => number | undefined {\n const frame = scenePoseFrame(scene);\n const descriptorOf = nodeDescriptors(scene, poseDescriptorOf(descriptor));\n return (id) => {\n const node = scene.get(id as NodeId);\n if (!node) return undefined;\n const d = descriptorOf(id);\n if (!d.withRotation || !d.getRotation || d.supportsRotation?.(node.pose) === false) return undefined;\n return d.getRotation(frame.world(id as NodeId));\n };\n}\n\n/** Each node's box frame, as `SnapCollect.frameOf` hands it to a source. */\nexport function sceneSnapFrames(scene: Scene<unknown, string, unknown>): (id: string) => SnapFrame | undefined {\n const boxes = sceneBoxFrames(scene);\n return (id) => {\n const m = boxes(id);\n if (!m) return undefined;\n const back = invert(m);\n const through = (t: typeof m, p: Point): Point => {\n const [x, y] = applyToPoint(t, p.x, p.y);\n return { x, y };\n };\n return { toWorld: (p) => through(m, p), toLocal: (p) => (back ? through(back, p) : null) };\n };\n}\n\n/** `ids` and every node under them: what a gesture moving `ids` carries. */\nexport function withSubtrees(scene: Scene<unknown, string, unknown>, ids: readonly string[]): string[] {\n const out = new Set<string>();\n const walk = (id: string): void => {\n if (out.has(id)) return;\n out.add(id);\n if (scene.get(id as NodeId) !== undefined) for (const c of scene.childrenOf(id as NodeId)) walk(c);\n };\n for (const id of ids) walk(id);\n return [...out];\n}\n"],"mappings":";;;;;;;;AAaA,SAAgB,gBAAgB,MAQjB;CACb,MAAM,EAAE,UAAU;CAClB,MAAM,QAAQ,eAAe,KAAK;CAClC,MAAM,UAAU,KAAK;CACrB,OAAO;EACL,OAAO,OAAQ,MAAM,IAAI,EAAY,MAAM,SAAY,SAAY,MAAM,MAAM,EAAY;EAC3F,WAAW,IAAI,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI;EAChD,SAAS,UAAU,KAAK,oBAAoB,uBAAA,CAAwB,UAAU,IAAI;EAClF,GAAI,KAAK,aAAa,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;EACzD,GAAI,UAAU,EAAE,SAAS,aAAa,OAAO,OAAO,EAAE,IAAI,CAAC;CAC7D;AACF;AAEA,SAAS,aAAa,OAAwC,SAAwD;CACpH,MAAM,SAAS,gBAAgB,KAAK;CACpC,QAAQ,OAAO;EACb,MAAM,QAAQ,QAAQ,EAAE;EACxB,MAAM,QAAQ,OAAO,SAAS,OAAO,EAAE,IAAI;EAC3C,OAAO,SAAS,QAAQ,MAAM,KAAK,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC;CAChE;AACF;;;AAIA,SAAgB,eACd,OACA,YACoC;CACpC,MAAM,QAAQ,eAAe,KAAK;CAClC,MAAM,eAAe,gBAAgB,OAAO,iBAAiB,UAAU,CAAC;CACxE,QAAQ,OAAO;EACb,MAAM,OAAO,MAAM,IAAI,EAAY;EACnC,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,IAAI,aAAa,EAAE;EACzB,IAAI,CAAC,EAAE,gBAAgB,CAAC,EAAE,eAAe,EAAE,mBAAmB,KAAK,IAAI,MAAM,OAAO,OAAO;EAC3F,OAAO,EAAE,YAAY,MAAM,MAAM,EAAY,CAAC;CAChD;AACF;;AAGA,SAAgB,gBAAgB,OAA+E;CAC7G,MAAM,QAAQ,eAAe,KAAK;CAClC,QAAQ,OAAO;EACb,MAAM,IAAI,MAAM,EAAE;EAClB,IAAI,CAAC,GAAG,OAAO;EACf,MAAM,OAAO,OAAO,CAAC;EACrB,MAAM,WAAW,GAAa,MAAoB;GAChD,MAAM,CAAC,GAAG,KAAK,aAAa,GAAG,EAAE,GAAG,EAAE,CAAC;GACvC,OAAO;IAAE;IAAG;GAAE;EAChB;EACA,OAAO;GAAE,UAAU,MAAM,QAAQ,GAAG,CAAC;GAAG,UAAU,MAAO,OAAO,QAAQ,MAAM,CAAC,IAAI;EAAM;CAC3F;AACF;;AAGA,SAAgB,aAAa,OAAwC,KAAkC;CACrG,MAAM,sBAAM,IAAI,IAAY;CAC5B,MAAM,QAAQ,OAAqB;EACjC,IAAI,IAAI,IAAI,EAAE,GAAG;EACjB,IAAI,IAAI,EAAE;EACV,IAAI,MAAM,IAAI,EAAY,MAAM,QAAW,KAAK,MAAM,KAAK,MAAM,WAAW,EAAY,GAAG,KAAK,CAAC;CACnG;CACA,KAAK,MAAM,MAAM,KAAK,KAAK,EAAE;CAC7B,OAAO,CAAC,GAAG,GAAG;AAChB"}
@@ -11,7 +11,8 @@ function sceneSession2d(i) {
11
11
  scene: i.scene,
12
12
  boxes: i.boxes,
13
13
  ...i.geometryAt ? { geometryAt: i.geometryAt } : {},
14
- ...i.originProjection ? { originProjection: i.originProjection } : {}
14
+ ...i.originProjection ? { originProjection: i.originProjection } : {},
15
+ ...i.anchors ? { anchors: i.anchors } : {}
15
16
  });
16
17
  const subject = subjectOf(i.req, reads);
17
18
  return createSession2d({
@@ -73,7 +74,8 @@ function createSnapSession2d(opts) {
73
74
  reachPx: opts.reachPx ?? DEFAULT_SNAP_PX,
74
75
  store: opts.store ?? createSnapOutcomeStore(),
75
76
  ...opts.originProjection ? { originProjection: opts.originProjection } : {},
76
- ...opts.poseDescriptor ? { poseDescriptor: opts.poseDescriptor } : {}
77
+ ...opts.poseDescriptor ? { poseDescriptor: opts.poseDescriptor } : {},
78
+ ...opts.anchors ? { anchors: opts.anchors } : {}
77
79
  });
78
80
  }
79
81
 
@@ -1 +1 @@
1
- {"version":3,"file":"sceneSession2d.js","names":[],"sources":["../../../src/domains/snap/sceneSession2d.ts"],"sourcesContent":["import type { ResizeAnchor, WorldPoint } from '@weasel-js/routing';\nimport type { View } from '@weasel-js/geom';\nimport type { DebugSink } from '@weasel-js/routing';\nimport type { PoseDescriptor } from 'core/geometry/poseDescriptor';\nimport type { Scene } from 'core/scene/types';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { OriginProjection } from 'interactions/gestures/shared/originProjection';\nimport { drawnBoxes, type DrawnBoxes } from 'interactions/actions/drawnBoxes';\nimport type { SnapBegin, SnapConstrain, SnapGeometry, SnapResizeBox, SnapSession } from 'interactions/actions/snapDep';\nimport { createSnapOutcomeStore, type SnapOutcomeStore } from './outcomeStore';\nimport { buildProbes, subjectOf } from './probes2d';\nimport { sceneProbeReads, sceneRotations, sceneSnapFrames, withSubtrees } from './sceneReads';\nimport { createSession2d, DEFAULT_SNAP_PX } from './session2d';\nimport type { SnapGesture, SnapSource } from './types';\n\ntype AnyScene = Scene<unknown, string, unknown>;\n\n/** What a session over a scene runs on: its subject's probes and boxes come\n * from `scene`, measured through `boxes`. */\nexport interface SceneSessionInputs {\n req: SnapBegin;\n scene: AnyScene;\n boxes: DrawnBoxes;\n sources: readonly SnapSource[];\n view(): View | null;\n reachPx: number;\n store: SnapOutcomeStore;\n geometryAt?: (press: WorldPoint, ids: readonly string[]) => WorldPoint | null;\n /** How a pose's origin is read. Default `AUTO_ORIGIN_PROJECTION`. */\n originProjection?: OriginProjection<unknown>;\n /** How a node's rotation is read. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<unknown>;\n /** Where each frame's candidates are recorded. */\n debug?: DebugSink;\n}\n\n/** A 2D session whose probes are the subject's, read from the scene at press. */\nexport function sceneSession2d(i: SceneSessionInputs): SnapSession {\n const reads = sceneProbeReads({\n scene: i.scene, boxes: i.boxes,\n ...(i.geometryAt ? { geometryAt: i.geometryAt } : {}),\n ...(i.originProjection ? { originProjection: i.originProjection } : {}),\n });\n const subject = subjectOf(i.req, reads);\n return createSession2d({\n req: i.req,\n sources: i.sources,\n probes: buildProbes(i.req, reads, subject),\n box: subject.box,\n resize: subject.resize,\n drawnBox: (id) => {\n const pose = reads.pose(id);\n return pose === undefined ? undefined : reads.drawnBox(id, pose);\n },\n ...(i.req.ids ? { exclude: withSubtrees(i.scene, i.req.ids) } : {}),\n view: i.view,\n reachPx: i.reachPx,\n scene: i.scene,\n rotationOf: sceneRotations(i.scene, i.poseDescriptor),\n frameOf: sceneSnapFrames(i.scene),\n store: i.store,\n ...(i.debug ? { debug: i.debug } : {}),\n });\n}\n\n/** Options for {@link createSnapSession2d}. */\nexport interface SnapSession2dOptions {\n scene: AnyScene;\n /** The camera the reach is measured through. */\n view: View;\n sources: readonly SnapSource[];\n gesture: SnapGesture;\n /** World coords. */\n press: WorldPoint;\n /** The subject, whose geometry offers no targets. */\n ids?: readonly string[];\n /** The container a subject not yet in the scene lands in. */\n parent?: string;\n /** Point gestures: the previous anchor, a line's start. */\n origin?: WorldPoint;\n constrain?: SnapConstrain;\n /** Resize: the sides the drag pins. */\n handle?: ResizeAnchor;\n /** Rotate: the point the angle turns about. */\n pivot?: WorldPoint;\n /** Rotate: the subject's angle at press. */\n theta0?: number;\n /** Angle gestures: the reach as an angle, radians. */\n reachRad?: number;\n /** Resize: the box `handle` belongs to. Default: the subjects' drawn box. */\n box?: SnapResizeBox;\n also?: SnapGeometry;\n /** The reach of a source that sets none, in screen px. Default `DEFAULT_SNAP_PX`. */\n reachPx?: number;\n /** Where the session writes its marks. Default: a store of its own. */\n store?: SnapOutcomeStore;\n /** How poses are read. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<unknown>;\n /** Whether drawn boxes include the stroke. Default `true`. */\n boundsIncludeStroke?: boolean;\n /** How a pose's origin, the point a grid or a guide snaps, is read.\n * Default `AUTO_ORIGIN_PROJECTION`: a rect's `{x, y}`, a path's bounds. */\n originProjection?: OriginProjection<unknown>;\n /** How `view`'s world maps into the parallax plane the gesture's points\n * are in. Default: they are in `view`'s own world. */\n plane?: PlaneMap | null;\n}\n\n/**\n * One gesture's snapping over a scene, for a host without `<SceneCanvas>`:\n * the session a canvas's `snap` dep begins, built from the same probes and\n * drawn boxes. Call `at` with each world point the gesture reaches, and\n * `end` when it lets go.\n */\nexport function createSnapSession2d(opts: SnapSession2dOptions): SnapSession {\n const view = opts.view;\n const req: SnapBegin = {\n gesture: opts.gesture,\n press: opts.press,\n ...(opts.ids ? { ids: opts.ids } : {}),\n ...(opts.parent !== undefined ? { parent: opts.parent } : {}),\n ...(opts.origin ? { origin: opts.origin } : {}),\n ...(opts.constrain ? { constrain: opts.constrain } : {}),\n ...(opts.handle ? { handle: opts.handle } : {}),\n ...(opts.pivot ? { pivot: opts.pivot } : {}),\n ...(opts.theta0 !== undefined ? { theta0: opts.theta0 } : {}),\n ...(opts.reachRad !== undefined ? { reachRad: opts.reachRad } : {}),\n ...(opts.box ? { box: opts.box } : {}),\n ...(opts.also ? { also: opts.also } : {}),\n ...(opts.plane ? { plane: () => opts.plane ?? null } : {}),\n };\n return sceneSession2d({\n req,\n scene: opts.scene,\n boxes: drawnBoxes({\n scene: opts.scene,\n view: { get: () => view, set: () => {} },\n ...(opts.poseDescriptor ? { poseDescriptor: opts.poseDescriptor } : {}),\n ...(opts.boundsIncludeStroke !== undefined ? { boundsIncludeStroke: opts.boundsIncludeStroke } : {}),\n }),\n sources: opts.sources,\n view: () => view,\n reachPx: opts.reachPx ?? DEFAULT_SNAP_PX,\n store: opts.store ?? createSnapOutcomeStore(),\n ...(opts.originProjection ? { originProjection: opts.originProjection } : {}),\n ...(opts.poseDescriptor ? { poseDescriptor: opts.poseDescriptor } : {}),\n });\n}\n"],"mappings":";;;;;;;;AAqCA,SAAgB,eAAe,GAAoC;CACjE,MAAM,QAAQ,gBAAgB;EAC5B,OAAO,EAAE;EAAO,OAAO,EAAE;EACzB,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;EACnD,GAAI,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,IAAI,CAAC;CACvE,CAAC;CACD,MAAM,UAAU,UAAU,EAAE,KAAK,KAAK;CACtC,OAAO,gBAAgB;EACrB,KAAK,EAAE;EACP,SAAS,EAAE;EACX,QAAQ,YAAY,EAAE,KAAK,OAAO,OAAO;EACzC,KAAK,QAAQ;EACb,QAAQ,QAAQ;EAChB,WAAW,OAAO;GAChB,MAAM,OAAO,MAAM,KAAK,EAAE;GAC1B,OAAO,SAAS,SAAY,SAAY,MAAM,SAAS,IAAI,IAAI;EACjE;EACA,GAAI,EAAE,IAAI,MAAM,EAAE,SAAS,aAAa,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,IAAI,CAAC;EACjE,MAAM,EAAE;EACR,SAAS,EAAE;EACX,OAAO,EAAE;EACT,YAAY,eAAe,EAAE,OAAO,EAAE,cAAc;EACpD,SAAS,gBAAgB,EAAE,KAAK;EAChC,OAAO,EAAE;EACT,GAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;CACtC,CAAC;AACH;;;;;;;AAmDA,SAAgB,oBAAoB,MAAyC;CAC3E,MAAM,OAAO,KAAK;CAgBlB,OAAO,eAAe;EACpB;GAfA,SAAS,KAAK;GACd,OAAO,KAAK;GACZ,GAAI,KAAK,MAAM,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;GACpC,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;GAC3D,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;GAC7C,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;GACtD,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;GAC7C,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;GAC1C,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;GAC3D,GAAI,KAAK,aAAa,SAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;GACjE,GAAI,KAAK,MAAM,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;GACpC,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;GACvC,GAAI,KAAK,QAAQ,EAAE,aAAa,KAAK,SAAS,KAAK,IAAI,CAAC;EAGxD;EACA,OAAO,KAAK;EACZ,OAAO,WAAW;GAChB,OAAO,KAAK;GACZ,MAAM;IAAE,WAAW;IAAM,WAAW,CAAC;GAAE;GACvC,GAAI,KAAK,iBAAiB,EAAE,gBAAgB,KAAK,eAAe,IAAI,CAAC;GACrE,GAAI,KAAK,wBAAwB,SAAY,EAAE,qBAAqB,KAAK,oBAAoB,IAAI,CAAC;EACpG,CAAC;EACD,SAAS,KAAK;EACd,YAAY;EACZ,SAAS,KAAK,WAAW;EACzB,OAAO,KAAK,SAAS,uBAAuB;EAC5C,GAAI,KAAK,mBAAmB,EAAE,kBAAkB,KAAK,iBAAiB,IAAI,CAAC;EAC3E,GAAI,KAAK,iBAAiB,EAAE,gBAAgB,KAAK,eAAe,IAAI,CAAC;CACvE,CAAC;AACH"}
1
+ {"version":3,"file":"sceneSession2d.js","names":[],"sources":["../../../src/domains/snap/sceneSession2d.ts"],"sourcesContent":["import type { ResizeAnchor, WorldPoint } from '@weasel-js/routing';\nimport type { View } from '@weasel-js/geom';\nimport type { DebugSink } from '@weasel-js/routing';\nimport type { PoseDescriptor } from 'core/geometry/poseDescriptor';\nimport type { Scene } from 'core/scene/types';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { OriginProjection } from 'interactions/gestures/shared/originProjection';\nimport { drawnBoxes, type DrawnBoxes } from 'interactions/actions/drawnBoxes';\nimport type { SnapBegin, SnapConstrain, SnapGeometry, SnapResizeBox, SnapSession } from 'interactions/actions/snapDep';\nimport { createSnapOutcomeStore, type SnapOutcomeStore } from './outcomeStore';\nimport { buildProbes, subjectOf } from './probes2d';\nimport { sceneProbeReads, sceneRotations, sceneSnapFrames, withSubtrees } from './sceneReads';\nimport { createSession2d, DEFAULT_SNAP_PX } from './session2d';\nimport type { SnapAnchors, SnapGesture, SnapSource } from './types';\n\ntype AnyScene = Scene<unknown, string, unknown>;\n\n/** What a session over a scene runs on: its subject's probes and boxes come\n * from `scene`, measured through `boxes`. */\nexport interface SceneSessionInputs {\n req: SnapBegin;\n scene: AnyScene;\n boxes: DrawnBoxes;\n sources: readonly SnapSource[];\n view(): View | null;\n reachPx: number;\n store: SnapOutcomeStore;\n geometryAt?: (press: WorldPoint, ids: readonly string[]) => WorldPoint | null;\n /** How a pose's origin is read. Default `AUTO_ORIGIN_PROJECTION`. */\n originProjection?: OriginProjection<unknown>;\n /** How a node's rotation is read. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<unknown>;\n /** Each node's snap anchors, in its box frame: `SceneCanvasSnap.anchors`. */\n anchors?: SnapAnchors;\n /** Where each frame's candidates are recorded. */\n debug?: DebugSink;\n}\n\n/** A 2D session whose probes are the subject's, read from the scene at press. */\nexport function sceneSession2d(i: SceneSessionInputs): SnapSession {\n const reads = sceneProbeReads({\n scene: i.scene, boxes: i.boxes,\n ...(i.geometryAt ? { geometryAt: i.geometryAt } : {}),\n ...(i.originProjection ? { originProjection: i.originProjection } : {}),\n ...(i.anchors ? { anchors: i.anchors } : {}),\n });\n const subject = subjectOf(i.req, reads);\n return createSession2d({\n req: i.req,\n sources: i.sources,\n probes: buildProbes(i.req, reads, subject),\n box: subject.box,\n resize: subject.resize,\n drawnBox: (id) => {\n const pose = reads.pose(id);\n return pose === undefined ? undefined : reads.drawnBox(id, pose);\n },\n ...(i.req.ids ? { exclude: withSubtrees(i.scene, i.req.ids) } : {}),\n view: i.view,\n reachPx: i.reachPx,\n scene: i.scene,\n rotationOf: sceneRotations(i.scene, i.poseDescriptor),\n frameOf: sceneSnapFrames(i.scene),\n store: i.store,\n ...(i.debug ? { debug: i.debug } : {}),\n });\n}\n\n/** Options for {@link createSnapSession2d}. */\nexport interface SnapSession2dOptions {\n scene: AnyScene;\n /** The camera the reach is measured through. */\n view: View;\n sources: readonly SnapSource[];\n gesture: SnapGesture;\n /** World coords. */\n press: WorldPoint;\n /** The subject, whose geometry offers no targets. */\n ids?: readonly string[];\n /** The container a subject not yet in the scene lands in. */\n parent?: string;\n /** Point gestures: the previous anchor, a line's start. */\n origin?: WorldPoint;\n constrain?: SnapConstrain;\n /** Resize: the sides the drag pins. */\n handle?: ResizeAnchor;\n /** Rotate: the point the angle turns about. */\n pivot?: WorldPoint;\n /** Rotate: the subject's angle at press. */\n theta0?: number;\n /** Angle gestures: the reach as an angle, radians. */\n reachRad?: number;\n /** Resize: the box `handle` belongs to. Default: the subjects' drawn box. */\n box?: SnapResizeBox;\n also?: SnapGeometry;\n /** The reach of a source that sets none, in screen px. Default `DEFAULT_SNAP_PX`. */\n reachPx?: number;\n /** Where the session writes its marks. Default: a store of its own. */\n store?: SnapOutcomeStore;\n /** How poses are read. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<unknown>;\n /** Whether drawn boxes include the stroke. Default `true`. */\n boundsIncludeStroke?: boolean;\n /** How a pose's origin, the point a grid or a guide snaps, is read.\n * Default `AUTO_ORIGIN_PROJECTION`: a rect's `{x, y}`, a path's bounds. */\n originProjection?: OriginProjection<unknown>;\n /** Points of each node a move snaps, in its own box frame, as\n * `SceneCanvasSnap.anchors`. */\n anchors?: SnapAnchors;\n /** How `view`'s world maps into the parallax plane the gesture's points\n * are in. Default: they are in `view`'s own world. */\n plane?: PlaneMap | null;\n}\n\n/**\n * One gesture's snapping over a scene, for a host without `<SceneCanvas>`:\n * the session a canvas's `snap` dep begins, built from the same probes and\n * drawn boxes. Call `at` with each world point the gesture reaches, and\n * `end` when it lets go.\n */\nexport function createSnapSession2d(opts: SnapSession2dOptions): SnapSession {\n const view = opts.view;\n const req: SnapBegin = {\n gesture: opts.gesture,\n press: opts.press,\n ...(opts.ids ? { ids: opts.ids } : {}),\n ...(opts.parent !== undefined ? { parent: opts.parent } : {}),\n ...(opts.origin ? { origin: opts.origin } : {}),\n ...(opts.constrain ? { constrain: opts.constrain } : {}),\n ...(opts.handle ? { handle: opts.handle } : {}),\n ...(opts.pivot ? { pivot: opts.pivot } : {}),\n ...(opts.theta0 !== undefined ? { theta0: opts.theta0 } : {}),\n ...(opts.reachRad !== undefined ? { reachRad: opts.reachRad } : {}),\n ...(opts.box ? { box: opts.box } : {}),\n ...(opts.also ? { also: opts.also } : {}),\n ...(opts.plane ? { plane: () => opts.plane ?? null } : {}),\n };\n return sceneSession2d({\n req,\n scene: opts.scene,\n boxes: drawnBoxes({\n scene: opts.scene,\n view: { get: () => view, set: () => {} },\n ...(opts.poseDescriptor ? { poseDescriptor: opts.poseDescriptor } : {}),\n ...(opts.boundsIncludeStroke !== undefined ? { boundsIncludeStroke: opts.boundsIncludeStroke } : {}),\n }),\n sources: opts.sources,\n view: () => view,\n reachPx: opts.reachPx ?? DEFAULT_SNAP_PX,\n store: opts.store ?? createSnapOutcomeStore(),\n ...(opts.originProjection ? { originProjection: opts.originProjection } : {}),\n ...(opts.poseDescriptor ? { poseDescriptor: opts.poseDescriptor } : {}),\n ...(opts.anchors ? { anchors: opts.anchors } : {}),\n });\n}\n"],"mappings":";;;;;;;;AAuCA,SAAgB,eAAe,GAAoC;CACjE,MAAM,QAAQ,gBAAgB;EAC5B,OAAO,EAAE;EAAO,OAAO,EAAE;EACzB,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;EACnD,GAAI,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,IAAI,CAAC;EACrE,GAAI,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,IAAI,CAAC;CAC5C,CAAC;CACD,MAAM,UAAU,UAAU,EAAE,KAAK,KAAK;CACtC,OAAO,gBAAgB;EACrB,KAAK,EAAE;EACP,SAAS,EAAE;EACX,QAAQ,YAAY,EAAE,KAAK,OAAO,OAAO;EACzC,KAAK,QAAQ;EACb,QAAQ,QAAQ;EAChB,WAAW,OAAO;GAChB,MAAM,OAAO,MAAM,KAAK,EAAE;GAC1B,OAAO,SAAS,SAAY,SAAY,MAAM,SAAS,IAAI,IAAI;EACjE;EACA,GAAI,EAAE,IAAI,MAAM,EAAE,SAAS,aAAa,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,IAAI,CAAC;EACjE,MAAM,EAAE;EACR,SAAS,EAAE;EACX,OAAO,EAAE;EACT,YAAY,eAAe,EAAE,OAAO,EAAE,cAAc;EACpD,SAAS,gBAAgB,EAAE,KAAK;EAChC,OAAO,EAAE;EACT,GAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;CACtC,CAAC;AACH;;;;;;;AAsDA,SAAgB,oBAAoB,MAAyC;CAC3E,MAAM,OAAO,KAAK;CAgBlB,OAAO,eAAe;EACpB;GAfA,SAAS,KAAK;GACd,OAAO,KAAK;GACZ,GAAI,KAAK,MAAM,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;GACpC,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;GAC3D,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;GAC7C,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;GACtD,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;GAC7C,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;GAC1C,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;GAC3D,GAAI,KAAK,aAAa,SAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;GACjE,GAAI,KAAK,MAAM,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;GACpC,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;GACvC,GAAI,KAAK,QAAQ,EAAE,aAAa,KAAK,SAAS,KAAK,IAAI,CAAC;EAGxD;EACA,OAAO,KAAK;EACZ,OAAO,WAAW;GAChB,OAAO,KAAK;GACZ,MAAM;IAAE,WAAW;IAAM,WAAW,CAAC;GAAE;GACvC,GAAI,KAAK,iBAAiB,EAAE,gBAAgB,KAAK,eAAe,IAAI,CAAC;GACrE,GAAI,KAAK,wBAAwB,SAAY,EAAE,qBAAqB,KAAK,oBAAoB,IAAI,CAAC;EACpG,CAAC;EACD,SAAS,KAAK;EACd,YAAY;EACZ,SAAS,KAAK,WAAW;EACzB,OAAO,KAAK,SAAS,uBAAuB;EAC5C,GAAI,KAAK,mBAAmB,EAAE,kBAAkB,KAAK,iBAAiB,IAAI,CAAC;EAC3E,GAAI,KAAK,iBAAiB,EAAE,gBAAgB,KAAK,eAAe,IAAI,CAAC;EACrE,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;CAClD,CAAC;AACH"}
@@ -1,4 +1,4 @@
1
- import { resizeEdges, resizeSubject, resizedBox } from "./resizeBox.js";
1
+ import { resizeSides, resizeSubject, resizedBox } from "./resizeBox.js";
2
2
  import { markFromPlane } from "./marks.js";
3
3
  import { screenOf } from "./screen.js";
4
4
  import { angleAt } from "./angle2d.js";
@@ -113,17 +113,24 @@ function collectAll(sources, probesAtPress, fixed, { delta0, view, plane }, reac
113
113
  ...fixed.origin ? { origin: fixed.origin } : {}
114
114
  });
115
115
  }
116
- /** The subject's box where `delta0` puts it, and the sides that moved. */
116
+ /** The subject's box where `delta0` puts it, the sides that move, and where
117
+ * any other move would put it. */
117
118
  function subjectAt({ box, resize }, delta0) {
118
119
  if (resize) return {
119
120
  box: resizedBox(resize, delta0),
120
- edges: resizeEdges(resize, delta0)
121
+ sides: resizeSides(resize, delta0),
122
+ boxAt: (d) => resizedBox(resize, d)
121
123
  };
122
- return box ? { box: {
124
+ if (!box) return {};
125
+ const boxAt = (d) => ({
123
126
  ...box,
124
- x: box.x + delta0.x,
125
- y: box.y + delta0.y
126
- } } : {};
127
+ x: box.x + d.x,
128
+ y: box.y + d.y
129
+ });
130
+ return {
131
+ box: boxAt(delta0),
132
+ boxAt
133
+ };
127
134
  }
128
135
  /** A session no source answers: it only holds the point to the gesture's
129
136
  * input subspace, as when a host provides no `snap` dep. */
@@ -1 +1 @@
1
- {"version":3,"file":"session2d.js","names":[],"sources":["../../../src/domains/snap/session2d.ts"],"sourcesContent":["import { closestOnFlat, resolveSnap, type SnapCandidate } from '@weasel-js/geom/nd';\nimport type { View, Point } from '@weasel-js/geom';\nimport type { DebugSink } from '@weasel-js/routing';\nimport type { Scene } from 'core/scene/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { SnapBegin, SnapOutcome, SnapSession } from 'interactions/actions/snapDep';\nimport { angleAt } from './angle2d';\nimport { inputFlat } from './inputSubspace';\nimport { gatherSnapCandidates, resolvedSnapMarks, snapSourcesFor, type SnapCandidateMark } from './gather';\nimport { recordSnapCandidates } from './debugCandidates';\nimport { markFromPlane } from './marks';\nimport { createSnapOutcomeStore, type SnapOutcomeStore } from './outcomeStore';\nimport { resizedBox, resizeEdges, resizeSubject, type ResizeSubject } from './resizeBox';\nimport { screenOf } from './screen';\nimport type { SnapCollect, SnapFrame, SnapMark, SnapProbeAt, SnapSource } from './types';\n\n/** The canvas's reach when neither a source nor the kit settings name one. */\nexport const DEFAULT_SNAP_PX = 8;\n\n/** What a 2D session runs on, captured at press. */\nexport interface Session2dInputs {\n req: SnapBegin;\n /** In source order: earlier wins a full tie. */\n sources: readonly SnapSource[];\n /** At press. */\n probes: readonly SnapProbeAt[];\n /** A move's subject: its drawn box at press. */\n box?: Bounds | null;\n /** A resize's box and frame. */\n resize?: ResizeSubject | null;\n /** `id`'s drawn box now, for sources measuring the scene the way the\n * subject's box was. */\n drawnBox?: (id: string) => Bounds | undefined;\n view(): View | null;\n /** The reach of a source that sets none, in screen px. */\n reachPx: number;\n scene?: Scene<unknown, string, unknown>;\n /** `id`'s world rotation, for angle targets; undefined for a node that\n * cannot turn. */\n rotationOf?: (id: string) => number | undefined;\n /** `id`'s box frame, for sources that work in a container's axes. */\n frameOf?: (id: string) => SnapFrame | undefined;\n store: SnapOutcomeStore;\n /** Nodes that offer no targets. Default `req.ids`. */\n exclude?: readonly string[];\n /** Where each frame's candidates are recorded, cleared at `end`. */\n debug?: DebugSink;\n}\n\n/** A snap session over a 2D canvas: ctx points are world points. */\nexport function createSession2d(inputs: Session2dInputs): SnapSession {\n const { req, store } = inputs;\n const sources = snapSourcesFor(inputs.sources, req.gesture);\n const fixed = collectAtPress(inputs);\n return {\n at(p, mods) {\n const delta0 = { x: p.x - req.press.x, y: p.y - req.press.y };\n const input = inputFlat(req, mods, delta0, inputs.resize);\n if (mods.suspend) {\n store.set([]);\n inputs.debug?.clearSnap();\n return outcome(req.press, closestOnFlat(input, [delta0.x, delta0.y]), false, []);\n }\n const view = inputs.view();\n const plane = req.plane?.() ?? null;\n const subject = subjectAt(inputs, delta0);\n const candidates = collectAll(sources, inputs.probes, { ...fixed, ...subject }, { delta0, view, plane }, inputs.reachPx);\n const res = resolveSnap({\n probes: inputs.probes.map((q) => ({ id: q.id, at: [q.at.x, q.at.y], role: q.role })),\n input,\n delta0: [delta0.x, delta0.y],\n candidates,\n toScreen: screenOf(view, plane),\n });\n if (inputs.debug) recordSnapCandidates(inputs.debug, candidates, res.satisfied, inputs.probes, delta0, plane);\n const delta = { x: res.delta[0], y: res.delta[1] };\n const marks = resolvedSnapMarks(res.satisfied, delta);\n store.set(plane ? marks.map((m) => markFromPlane(m, plane)) : marks);\n return outcome(req.press, res.delta, res.members.length > 0, marks);\n },\n angle: (theta, mods) => angleAt(inputs, sources, theta, mods),\n end() {\n store.set([]);\n inputs.debug?.clearSnap();\n },\n };\n}\n\nfunction outcome(press: Point, delta: readonly number[], held: boolean, marks: readonly SnapMark[]): SnapOutcome {\n return { point: { x: press.x + delta[0], y: press.y + delta[1] }, held, marks };\n}\n\n/** What a source is told that holds from press to release. */\ntype FixedCollect = Pick<SnapCollect, 'gesture' | 'ids' | 'parent' | 'exclude' | 'drawnBox' | 'origin' | 'also' | 'scene' | 'frameOf'>;\n\n/** What changes from one frame to the next. */\ninterface Frame {\n delta0: Point;\n view: View | null;\n plane: PlaneMap | null;\n}\n\nfunction collectAtPress(inputs: Session2dInputs): FixedCollect {\n const { req } = inputs;\n return {\n gesture: req.gesture,\n ...(req.ids ? { ids: req.ids } : {}),\n ...(req.parent !== undefined ? { parent: req.parent } : {}),\n exclude: inputs.exclude ?? req.ids ?? [],\n ...(inputs.drawnBox ? { drawnBox: inputs.drawnBox } : {}),\n ...(req.origin ? { origin: req.origin } : {}),\n ...(req.also ? { also: req.also } : {}),\n ...(inputs.scene ? { scene: inputs.scene } : {}),\n ...(inputs.frameOf ? { frameOf: inputs.frameOf } : {}),\n };\n}\n\nfunction collectAll(\n sources: readonly SnapSource[], probesAtPress: readonly SnapProbeAt[],\n fixed: FixedCollect & Pick<SnapCollect, 'box' | 'edges'>, { delta0, view, plane }: Frame, reachPx: number,\n): SnapCandidate<SnapCandidateMark<Point>>[] {\n const camera = view ? Math.min(Math.abs(view.scale.x), Math.abs(view.scale.y)) : 1;\n // A camera unit is `plane.scale` plane units.\n const scale = plane ? camera / Math.max(Math.abs(plane.scale.x), Math.abs(plane.scale.y)) : camera;\n const probes = probesAtPress.map((q) => ({ ...q, at: { x: q.at.x + delta0.x, y: q.at.y + delta0.y } }));\n return gatherSnapCandidates(sources, probes, (_source, offered, sourceReach) => ({\n ...fixed, probes: offered, delta0,\n ...(plane ? { plane } : {}),\n reachWorld: (px) => px / scale,\n reachPx: sourceReach,\n }), { toVec: (p) => [p.x, p.y], reachPx, ...(fixed.origin ? { origin: fixed.origin } : {}) });\n}\n\n/** The subject's box where `delta0` puts it, and the sides that moved. */\nfunction subjectAt(\n { box, resize }: Pick<Session2dInputs, 'box' | 'resize'>, delta0: Point,\n): Pick<SnapCollect, 'box' | 'edges'> {\n if (resize) return { box: resizedBox(resize, delta0), edges: resizeEdges(resize, delta0) };\n return box ? { box: { ...box, x: box.x + delta0.x, y: box.y + delta0.y } } : {};\n}\n\n/** A session no source answers: it only holds the point to the gesture's\n * input subspace, as when a host provides no `snap` dep. */\nexport function inputSession(req: SnapBegin): SnapSession {\n return createSession2d({\n req,\n sources: [],\n probes: [],\n resize: req.gesture === 'resize' && req.handle && req.box ? resizeSubject(req.box, req.handle) : null,\n view: () => null,\n reachPx: 0,\n store: createSnapOutcomeStore(),\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session2d.js","names":[],"sources":["../../../src/domains/snap/session2d.ts"],"sourcesContent":["import { closestOnFlat, resolveSnap, type SnapCandidate } from '@weasel-js/geom/nd';\nimport type { View, Point } from '@weasel-js/geom';\nimport type { DebugSink } from '@weasel-js/routing';\nimport type { Scene } from 'core/scene/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { PlaneMap } from 'core/viewport/parallax';\nimport type { SnapBegin, SnapOutcome, SnapSession } from 'interactions/actions/snapDep';\nimport { angleAt } from './angle2d';\nimport { inputFlat } from './inputSubspace';\nimport { gatherSnapCandidates, resolvedSnapMarks, snapSourcesFor, type SnapCandidateMark } from './gather';\nimport { recordSnapCandidates } from './debugCandidates';\nimport { markFromPlane } from './marks';\nimport { createSnapOutcomeStore, type SnapOutcomeStore } from './outcomeStore';\nimport { resizedBox, resizeSides, resizeSubject, type ResizeSubject } from './resizeBox';\nimport { screenOf } from './screen';\nimport type { SnapCollect, SnapFrame, SnapMark, SnapProbeAt, SnapSource } from './types';\n\n/** The canvas's reach when neither a source nor the kit settings name one. */\nexport const DEFAULT_SNAP_PX = 8;\n\n/** What a 2D session runs on, captured at press. */\nexport interface Session2dInputs {\n req: SnapBegin;\n /** In source order: earlier wins a full tie. */\n sources: readonly SnapSource[];\n /** At press. */\n probes: readonly SnapProbeAt[];\n /** A move's subject: its drawn box at press. */\n box?: Bounds | null;\n /** A resize's box and frame. */\n resize?: ResizeSubject | null;\n /** `id`'s drawn box now, for sources measuring the scene the way the\n * subject's box was. */\n drawnBox?: (id: string) => Bounds | undefined;\n view(): View | null;\n /** The reach of a source that sets none, in screen px. */\n reachPx: number;\n scene?: Scene<unknown, string, unknown>;\n /** `id`'s world rotation, for angle targets; undefined for a node that\n * cannot turn. */\n rotationOf?: (id: string) => number | undefined;\n /** `id`'s box frame, for sources that work in a container's axes. */\n frameOf?: (id: string) => SnapFrame | undefined;\n store: SnapOutcomeStore;\n /** Nodes that offer no targets. Default `req.ids`. */\n exclude?: readonly string[];\n /** Where each frame's candidates are recorded, cleared at `end`. */\n debug?: DebugSink;\n}\n\n/** A snap session over a 2D canvas: ctx points are world points. */\nexport function createSession2d(inputs: Session2dInputs): SnapSession {\n const { req, store } = inputs;\n const sources = snapSourcesFor(inputs.sources, req.gesture);\n const fixed = collectAtPress(inputs);\n return {\n at(p, mods) {\n const delta0 = { x: p.x - req.press.x, y: p.y - req.press.y };\n const input = inputFlat(req, mods, delta0, inputs.resize);\n if (mods.suspend) {\n store.set([]);\n inputs.debug?.clearSnap();\n return outcome(req.press, closestOnFlat(input, [delta0.x, delta0.y]), false, []);\n }\n const view = inputs.view();\n const plane = req.plane?.() ?? null;\n const subject = subjectAt(inputs, delta0);\n const candidates = collectAll(sources, inputs.probes, { ...fixed, ...subject }, { delta0, view, plane }, inputs.reachPx);\n const res = resolveSnap({\n probes: inputs.probes.map((q) => ({ id: q.id, at: [q.at.x, q.at.y], role: q.role })),\n input,\n delta0: [delta0.x, delta0.y],\n candidates,\n toScreen: screenOf(view, plane),\n });\n if (inputs.debug) recordSnapCandidates(inputs.debug, candidates, res.satisfied, inputs.probes, delta0, plane);\n const delta = { x: res.delta[0], y: res.delta[1] };\n const marks = resolvedSnapMarks(res.satisfied, delta);\n store.set(plane ? marks.map((m) => markFromPlane(m, plane)) : marks);\n return outcome(req.press, res.delta, res.members.length > 0, marks);\n },\n angle: (theta, mods) => angleAt(inputs, sources, theta, mods),\n end() {\n store.set([]);\n inputs.debug?.clearSnap();\n },\n };\n}\n\nfunction outcome(press: Point, delta: readonly number[], held: boolean, marks: readonly SnapMark[]): SnapOutcome {\n return { point: { x: press.x + delta[0], y: press.y + delta[1] }, held, marks };\n}\n\n/** What a source is told that holds from press to release. */\ntype FixedCollect = Pick<SnapCollect, 'gesture' | 'ids' | 'parent' | 'exclude' | 'drawnBox' | 'origin' | 'also' | 'scene' | 'frameOf'>;\n\n/** What changes from one frame to the next. */\ninterface Frame {\n delta0: Point;\n view: View | null;\n plane: PlaneMap | null;\n}\n\nfunction collectAtPress(inputs: Session2dInputs): FixedCollect {\n const { req } = inputs;\n return {\n gesture: req.gesture,\n ...(req.ids ? { ids: req.ids } : {}),\n ...(req.parent !== undefined ? { parent: req.parent } : {}),\n exclude: inputs.exclude ?? req.ids ?? [],\n ...(inputs.drawnBox ? { drawnBox: inputs.drawnBox } : {}),\n ...(req.origin ? { origin: req.origin } : {}),\n ...(req.also ? { also: req.also } : {}),\n ...(inputs.scene ? { scene: inputs.scene } : {}),\n ...(inputs.frameOf ? { frameOf: inputs.frameOf } : {}),\n };\n}\n\nfunction collectAll(\n sources: readonly SnapSource[], probesAtPress: readonly SnapProbeAt[],\n fixed: FixedCollect & Pick<SnapCollect, 'box' | 'sides' | 'boxAt'>, { delta0, view, plane }: Frame, reachPx: number,\n): SnapCandidate<SnapCandidateMark<Point>>[] {\n const camera = view ? Math.min(Math.abs(view.scale.x), Math.abs(view.scale.y)) : 1;\n // A camera unit is `plane.scale` plane units.\n const scale = plane ? camera / Math.max(Math.abs(plane.scale.x), Math.abs(plane.scale.y)) : camera;\n const probes = probesAtPress.map((q) => ({ ...q, at: { x: q.at.x + delta0.x, y: q.at.y + delta0.y } }));\n return gatherSnapCandidates(sources, probes, (_source, offered, sourceReach) => ({\n ...fixed, probes: offered, delta0,\n ...(plane ? { plane } : {}),\n reachWorld: (px) => px / scale,\n reachPx: sourceReach,\n }), { toVec: (p) => [p.x, p.y], reachPx, ...(fixed.origin ? { origin: fixed.origin } : {}) });\n}\n\n/** The subject's box where `delta0` puts it, the sides that move, and where\n * any other move would put it. */\nfunction subjectAt(\n { box, resize }: Pick<Session2dInputs, 'box' | 'resize'>, delta0: Point,\n): Pick<SnapCollect, 'box' | 'sides' | 'boxAt'> {\n if (resize) {\n return { box: resizedBox(resize, delta0), sides: resizeSides(resize, delta0), boxAt: (d) => resizedBox(resize, d) };\n }\n if (!box) return {};\n const boxAt = (d: Point): Bounds => ({ ...box, x: box.x + d.x, y: box.y + d.y });\n return { box: boxAt(delta0), boxAt };\n}\n\n/** A session no source answers: it only holds the point to the gesture's\n * input subspace, as when a host provides no `snap` dep. */\nexport function inputSession(req: SnapBegin): SnapSession {\n return createSession2d({\n req,\n sources: [],\n probes: [],\n resize: req.gesture === 'resize' && req.handle && req.box ? resizeSubject(req.box, req.handle) : null,\n view: () => null,\n reachPx: 0,\n store: createSnapOutcomeStore(),\n });\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { useSceneTextEdit } from "./useSceneTextEdit.js";
2
2
  import { createTextLayer } from "./textLayer.js";
3
- import { textCommand as textCommand$1, textCommandFromPose as textCommandFromPose$1, textCommandFromRuns as textCommandFromRuns$1 } from "@weasel-js/renderer";
3
+ import { textCommand as textCommand$1, textCommandFromPose as textCommandFromPose$1, textCommandFromRuns as textCommandFromRuns$1, textCommandFromRunsInto } from "@weasel-js/renderer";
4
4
  import { TextOutlinesError as TextOutlinesError$1, loadTextOutlines as loadTextOutlines$1, textToPath as textToPath$1, textToPathsByPaint as textToPathsByPaint$1 } from "@weasel-js/text";
5
5
  import { TEXT_EDIT_FONT_HOLD, charOffsetToDomPosition, domPositionToCharOffset, domToRuns, runsToDom, useTextEdit as useTextEdit$1 } from "@weasel-js/text-dom";
6
6
 
7
- export { TEXT_EDIT_FONT_HOLD, TextOutlinesError$1 as TextOutlinesError, charOffsetToDomPosition, createTextLayer, domPositionToCharOffset, domToRuns, loadTextOutlines$1 as loadTextOutlines, runsToDom, textCommand$1 as textCommand, textCommandFromPose$1 as textCommandFromPose, textCommandFromRuns$1 as textCommandFromRuns, textToPath$1 as textToPath, textToPathsByPaint$1 as textToPathsByPaint, useSceneTextEdit, useTextEdit$1 as useTextEdit };
7
+ export { TEXT_EDIT_FONT_HOLD, TextOutlinesError$1 as TextOutlinesError, charOffsetToDomPosition, createTextLayer, domPositionToCharOffset, domToRuns, loadTextOutlines$1 as loadTextOutlines, runsToDom, textCommand$1 as textCommand, textCommandFromPose$1 as textCommandFromPose, textCommandFromRuns$1 as textCommandFromRuns, textCommandFromRunsInto, textToPath$1 as textToPath, textToPathsByPaint$1 as textToPathsByPaint, useSceneTextEdit, useTextEdit$1 as useTextEdit };