@weasel-js/core 1.11.1 → 1.12.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 (297) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/WeaselProvider.js +1 -1
  3. package/dist/affordances/pathAnchors.js +10 -6
  4. package/dist/affordances/pathAnchors.js.map +1 -1
  5. package/dist/barrel/canvas.js +3 -1
  6. package/dist/barrel/paint.js +4 -4
  7. package/dist/barrel/paths.js +8 -4
  8. package/dist/barrel/renderer.js +3 -2
  9. package/dist/barrel/routing.js +4 -4
  10. package/dist/barrel/scene.js +2 -1
  11. package/dist/barrel/text.js +2 -2
  12. package/dist/barrel/viewport.js +6 -2
  13. package/dist/canvas/Canvas.js +2 -1
  14. package/dist/canvas/Canvas.js.map +1 -1
  15. package/dist/canvas/CanvasView.js +23 -34
  16. package/dist/canvas/CanvasView.js.map +1 -1
  17. package/dist/canvas/ModalityHud.js +1 -1
  18. package/dist/canvas/NodeShape.js +4 -1
  19. package/dist/canvas/NodeShape.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/ActionsProviderIfRoot.js +1 -1
  21. package/dist/canvas/SceneCanvas/DepRegistryProviderIfRoot.js +1 -1
  22. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +1 -1
  23. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js +23 -0
  24. package/dist/canvas/SceneCanvas/SurfaceDeclarations.js.map +1 -0
  25. package/dist/canvas/SceneCanvas/canvasPassThrough.js +1 -0
  26. package/dist/canvas/SceneCanvas/canvasPassThrough.js.map +1 -1
  27. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  28. package/dist/canvas/SceneCanvas/scenePointQuery.js +7 -3
  29. package/dist/canvas/SceneCanvas/scenePointQuery.js.map +1 -1
  30. package/dist/canvas/SceneCanvas/useActionTunings.js +6 -1
  31. package/dist/canvas/SceneCanvas/useActionTunings.js.map +1 -1
  32. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js +1 -0
  33. package/dist/canvas/SceneCanvas/useCanvasDevtoolsRoot.js.map +1 -1
  34. package/dist/canvas/SceneCanvas/useCanvasTools.js +7 -2
  35. package/dist/canvas/SceneCanvas/useCanvasTools.js.map +1 -1
  36. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js +9 -3
  37. package/dist/canvas/SceneCanvas/useSceneCanvasHandle.js.map +1 -1
  38. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +10 -2
  39. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  40. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js +1 -0
  41. package/dist/canvas/SceneCanvas/useSceneCanvasTools.js.map +1 -1
  42. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js +19 -4
  43. package/dist/canvas/SceneCanvas/useSceneGestureDispatcher.js.map +1 -1
  44. package/dist/canvas/SceneCanvas/useSceneStandardActions.js +2 -1
  45. package/dist/canvas/SceneCanvas/useSceneStandardActions.js.map +1 -1
  46. package/dist/canvas/SceneCanvas.js +11 -10
  47. package/dist/canvas/SceneCanvas.js.map +1 -1
  48. package/dist/canvas/ViewMarker.js +35 -0
  49. package/dist/canvas/ViewMarker.js.map +1 -0
  50. package/dist/canvas/defaultDrawOne.js +2 -0
  51. package/dist/canvas/defaultDrawOne.js.map +1 -1
  52. package/dist/canvas/deps/booleans.js +2 -1
  53. package/dist/canvas/deps/booleans.js.map +1 -1
  54. package/dist/canvas/deps/hitTestArea.js +2 -2
  55. package/dist/canvas/deps/hitTestArea.js.map +1 -1
  56. package/dist/canvas/deps/insert.js +6 -2
  57. package/dist/canvas/deps/insert.js.map +1 -1
  58. package/dist/canvas/deps/pathEditCommits.js +3 -7
  59. package/dist/canvas/deps/pathEditCommits.js.map +1 -1
  60. package/dist/canvas/drawnBounds.js +11 -3
  61. package/dist/canvas/drawnBounds.js.map +1 -1
  62. package/dist/canvas/inheritedPaints.js +44 -0
  63. package/dist/canvas/inheritedPaints.js.map +1 -0
  64. package/dist/canvas/nodeShapes/derivedPainter.js +15 -16
  65. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  66. package/dist/canvas/nodeShapes/nodePaint.js +13 -34
  67. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  68. package/dist/canvas/nodeShapes/pathPainter.js +9 -8
  69. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  70. package/dist/canvas/nodeShapes/rectFallbackPainter.js +4 -5
  71. package/dist/canvas/nodeShapes/rectFallbackPainter.js.map +1 -1
  72. package/dist/canvas/nodeShapes/shapePainter.js +11 -10
  73. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  74. package/dist/canvas/nodeShapes/textPainter.js +13 -12
  75. package/dist/canvas/nodeShapes/textPainter.js.map +1 -1
  76. package/dist/canvas/pickReach.js +3 -3
  77. package/dist/canvas/pickReach.js.map +1 -1
  78. package/dist/canvas/pickWalk.js +3 -1
  79. package/dist/canvas/pickWalk.js.map +1 -1
  80. package/dist/canvas/renderSceneToPixels.js +1 -1
  81. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  82. package/dist/canvas/sceneSlotWiring.js +2 -1
  83. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  84. package/dist/canvas/sceneViewRender.js +2 -1
  85. package/dist/canvas/sceneViewRender.js.map +1 -1
  86. package/dist/canvas/surfaceContribution.js +12 -8
  87. package/dist/canvas/surfaceContribution.js.map +1 -1
  88. package/dist/canvas/useCanvasView.js +4 -2
  89. package/dist/canvas/useCanvasView.js.map +1 -1
  90. package/dist/canvas/useLayerStack.js +6 -2
  91. package/dist/canvas/useLayerStack.js.map +1 -1
  92. package/dist/canvas/useViewAim.js +121 -0
  93. package/dist/canvas/useViewAim.js.map +1 -0
  94. package/dist/canvas/useViewNesting.js +3 -2
  95. package/dist/canvas/useViewNesting.js.map +1 -1
  96. package/dist/canvas/useViewRegistrySurface.js +4 -2
  97. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  98. package/dist/canvas/viewMarkers.js +63 -0
  99. package/dist/canvas/viewMarkers.js.map +1 -0
  100. package/dist/canvas/viewRegistry.js +27 -1
  101. package/dist/canvas/viewRegistry.js.map +1 -1
  102. package/dist/core/pages/pageEdits.js +2 -3
  103. package/dist/core/pages/pageEdits.js.map +1 -1
  104. package/dist/core/paint/meshKindLazy.js +59 -2
  105. package/dist/core/paint/meshKindLazy.js.map +1 -1
  106. package/dist/core/scene/clipNothing.js +13 -0
  107. package/dist/core/scene/clipNothing.js.map +1 -0
  108. package/dist/core/scene/depMemo.js +11 -3
  109. package/dist/core/scene/depMemo.js.map +1 -1
  110. package/dist/core/scene/dependents.js +7 -1
  111. package/dist/core/scene/dependents.js.map +1 -1
  112. package/dist/core/scene/derivedParts.js +10 -11
  113. package/dist/core/scene/derivedParts.js.map +1 -1
  114. package/dist/core/scene/effectivePose.js +7 -2
  115. package/dist/core/scene/effectivePose.js.map +1 -1
  116. package/dist/core/scene/fontReads.js +37 -0
  117. package/dist/core/scene/fontReads.js.map +1 -0
  118. package/dist/core/scene/inheritedPaint.js +75 -0
  119. package/dist/core/scene/inheritedPaint.js.map +1 -0
  120. package/dist/core/scene/kitRegistry.js +5 -0
  121. package/dist/core/scene/kitRegistry.js.map +1 -1
  122. package/dist/core/scene/nodeMemo.js +15 -7
  123. package/dist/core/scene/nodeMemo.js.map +1 -1
  124. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  125. package/dist/core/scene/pathEffects/derive.js +7 -2
  126. package/dist/core/scene/pathEffects/derive.js.map +1 -1
  127. package/dist/core/scene/pathEffects/effects.js +3 -1
  128. package/dist/core/scene/pathEffects/effects.js.map +1 -1
  129. package/dist/core/scene/pathEffects/textAlong.js +95 -0
  130. package/dist/core/scene/pathEffects/textAlong.js.map +1 -0
  131. package/dist/{cornerHandles-BHuoMs1o.d.ts → cornerHandles-p8fDIAKt.d.ts} +2 -1
  132. package/dist/{depSchema-BpZNOv7q.d.ts → depSchema-mG19aEdD.d.ts} +61 -13
  133. package/dist/devtools/registry.js +4 -4
  134. package/dist/devtools/registry.js.map +1 -1
  135. package/dist/features/measure/measureContribution.js +4 -2
  136. package/dist/features/measure/measureContribution.js.map +1 -1
  137. package/dist/features/modes/modeTriggers.js +7 -3
  138. package/dist/features/modes/modeTriggers.js.map +1 -1
  139. package/dist/features/pathEffects/effectSources.js +60 -0
  140. package/dist/features/pathEffects/effectSources.js.map +1 -0
  141. package/dist/features/pathEffects/readSource.js +17 -1
  142. package/dist/features/pathEffects/readSource.js.map +1 -1
  143. package/dist/features/pathEffects/textItems.js +53 -0
  144. package/dist/features/pathEffects/textItems.js.map +1 -0
  145. package/dist/features/pathEffects/useEffectSourcesLayer.js +57 -0
  146. package/dist/features/pathEffects/useEffectSourcesLayer.js.map +1 -0
  147. package/dist/features/paths/pathEditState.js +5 -2
  148. package/dist/features/paths/pathEditState.js.map +1 -1
  149. package/dist/features/text/sceneTextDrafts.js +65 -0
  150. package/dist/features/text/sceneTextDrafts.js.map +1 -0
  151. package/dist/features/text/textHitAt.js +31 -2
  152. package/dist/features/text/textHitAt.js.map +1 -1
  153. package/dist/features/text/useSceneTextEdit.js +22 -18
  154. package/dist/features/text/useSceneTextEdit.js.map +1 -1
  155. package/dist/features/viewports/aim.js +105 -0
  156. package/dist/features/viewports/aim.js.map +1 -0
  157. package/dist/features/viewports/viewMarker.js +82 -0
  158. package/dist/features/viewports/viewMarker.js.map +1 -0
  159. package/dist/features/viewports/viewMarkerDrag.js +55 -0
  160. package/dist/features/viewports/viewMarkerDrag.js.map +1 -0
  161. package/dist/index.d.ts +500 -201
  162. package/dist/index.js +23 -12
  163. package/dist/interactions/actions/booleans/booleans.js +2 -1
  164. package/dist/interactions/actions/booleans/booleans.js.map +1 -1
  165. package/dist/interactions/actions/booleans/operands.js +6 -5
  166. package/dist/interactions/actions/booleans/operands.js.map +1 -1
  167. package/dist/interactions/actions/clipboard/action.js +27 -17
  168. package/dist/interactions/actions/clipboard/action.js.map +1 -1
  169. package/dist/interactions/actions/defaults/anchorEditing.js +44 -23
  170. package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
  171. package/dist/interactions/actions/defaults/anchorJoin.js +12 -6
  172. package/dist/interactions/actions/defaults/anchorJoin.js.map +1 -1
  173. package/dist/interactions/actions/defaults/bendSegment.js +7 -4
  174. package/dist/interactions/actions/defaults/bendSegment.js.map +1 -1
  175. package/dist/interactions/actions/defaults/cancelGesture.js +11 -8
  176. package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
  177. package/dist/interactions/actions/defaults/contextMenu.js +17 -8
  178. package/dist/interactions/actions/defaults/contextMenu.js.map +1 -1
  179. package/dist/interactions/actions/defaults/createPaintAction.js +1 -1
  180. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  181. package/dist/interactions/actions/defaults/delete.js +28 -14
  182. package/dist/interactions/actions/defaults/delete.js.map +1 -1
  183. package/dist/interactions/actions/defaults/duplicate.js +11 -8
  184. package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
  185. package/dist/interactions/actions/defaults/enterPathEdit.js +19 -9
  186. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  187. package/dist/interactions/actions/defaults/escape.js +11 -8
  188. package/dist/interactions/actions/defaults/escape.js.map +1 -1
  189. package/dist/interactions/actions/defaults/exitPathEdit.js +7 -4
  190. package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
  191. package/dist/interactions/actions/defaults/group.js +19 -13
  192. package/dist/interactions/actions/defaults/group.js.map +1 -1
  193. package/dist/interactions/actions/defaults/ingest.js +10 -4
  194. package/dist/interactions/actions/defaults/ingest.js.map +1 -1
  195. package/dist/interactions/actions/defaults/insertPathAnchor.js +7 -4
  196. package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
  197. package/dist/interactions/actions/defaults/nudge.js +8 -2
  198. package/dist/interactions/actions/defaults/nudge.js.map +1 -1
  199. package/dist/interactions/actions/defaults/pathEditTools.js +11 -8
  200. package/dist/interactions/actions/defaults/pathEditTools.js.map +1 -1
  201. package/dist/interactions/actions/defaults/pinchZoom.js +7 -4
  202. package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
  203. package/dist/interactions/actions/defaults/reorder.js +24 -6
  204. package/dist/interactions/actions/defaults/reorder.js.map +1 -1
  205. package/dist/interactions/actions/defaults/selectAll.js +8 -5
  206. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  207. package/dist/interactions/actions/defaults/setFill.js +7 -3
  208. package/dist/interactions/actions/defaults/setFill.js.map +1 -1
  209. package/dist/interactions/actions/defaults/setFillOpacity.js +1 -1
  210. package/dist/interactions/actions/defaults/setStroke.js +12 -11
  211. package/dist/interactions/actions/defaults/setStroke.js.map +1 -1
  212. package/dist/interactions/actions/defaults/setStrokeOpacity.js +1 -1
  213. package/dist/interactions/actions/defaults/toolActivate.js +7 -1
  214. package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
  215. package/dist/interactions/actions/defaults/toolResetToDefault.js +7 -4
  216. package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
  217. package/dist/interactions/actions/defaults/undoRedo.js +19 -13
  218. package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
  219. package/dist/interactions/actions/defaults/viewportWheelPan.js +16 -7
  220. package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
  221. package/dist/interactions/actions/defaults/viewportZoom.js +24 -6
  222. package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
  223. package/dist/interactions/actions/edit-anchors/action.js +7 -4
  224. package/dist/interactions/actions/edit-anchors/action.js.map +1 -1
  225. package/dist/interactions/actions/flip/action.js +8 -2
  226. package/dist/interactions/actions/flip/action.js.map +1 -1
  227. package/dist/interactions/actions/insert/repositionAction.js +2 -1
  228. package/dist/interactions/actions/insert/repositionAction.js.map +1 -1
  229. package/dist/interactions/actions/move/action.js +7 -4
  230. package/dist/interactions/actions/move/action.js.map +1 -1
  231. package/dist/interactions/actions/outlines/createOutlinesAction.js +8 -5
  232. package/dist/interactions/actions/outlines/createOutlinesAction.js.map +1 -1
  233. package/dist/interactions/actions/outlines/outlineStroke.js +5 -3
  234. package/dist/interactions/actions/outlines/outlineStroke.js.map +1 -1
  235. package/dist/interactions/actions/pathCommands/cutJoin.js +8 -5
  236. package/dist/interactions/actions/pathCommands/cutJoin.js.map +1 -1
  237. package/dist/interactions/actions/resize/cornerHandles.js +2 -1
  238. package/dist/interactions/actions/resize/cornerHandles.js.map +1 -1
  239. package/dist/interactions/actions/selectionSubtree.js +12 -7
  240. package/dist/interactions/actions/selectionSubtree.js.map +1 -1
  241. package/dist/interactions/actions/transform-again/action.js +8 -5
  242. package/dist/interactions/actions/transform-again/action.js.map +1 -1
  243. package/dist/json.d.ts +4 -0
  244. package/dist/json.js +3 -0
  245. package/dist/kitSettingsSchema.js +8 -1
  246. package/dist/kitSettingsSchema.js.map +1 -1
  247. package/dist/{math-CrJULaZI.d.ts → math-B56zxVs5.d.ts} +10 -1
  248. package/dist/math.d.ts +1 -1
  249. package/dist/math.js +2 -1
  250. package/dist/resize.d.ts +1 -1
  251. package/dist/test-seams.d.ts +2 -2
  252. package/dist/tools/builtin/ellipse/useEllipseTool.js +1 -0
  253. package/dist/tools/builtin/ellipse/useEllipseTool.js.map +1 -1
  254. package/dist/tools/builtin/eyedropper/useEyedropperTool.js +2 -1
  255. package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
  256. package/dist/tools/builtin/hand/dragPanContribution.js +4 -1
  257. package/dist/tools/builtin/hand/dragPanContribution.js.map +1 -1
  258. package/dist/tools/builtin/hand/useHandTool.js +7 -4
  259. package/dist/tools/builtin/hand/useHandTool.js.map +1 -1
  260. package/dist/tools/builtin/image/useImageTool.js +1 -0
  261. package/dist/tools/builtin/image/useImageTool.js.map +1 -1
  262. package/dist/tools/builtin/lasso/useLassoTool.js +1 -0
  263. package/dist/tools/builtin/lasso/useLassoTool.js.map +1 -1
  264. package/dist/tools/builtin/line/useLineTool.js +1 -0
  265. package/dist/tools/builtin/line/useLineTool.js.map +1 -1
  266. package/dist/tools/builtin/measure/useMeasureTool.js +12 -5
  267. package/dist/tools/builtin/measure/useMeasureTool.js.map +1 -1
  268. package/dist/tools/builtin/pen/usePenTool.js +14 -6
  269. package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
  270. package/dist/tools/builtin/pencil/usePencilTool.js +1 -0
  271. package/dist/tools/builtin/pencil/usePencilTool.js.map +1 -1
  272. package/dist/tools/builtin/polygon/usePolygonTool.js +13 -4
  273. package/dist/tools/builtin/polygon/usePolygonTool.js.map +1 -1
  274. package/dist/tools/builtin/rect/useRectTool.js +1 -0
  275. package/dist/tools/builtin/rect/useRectTool.js.map +1 -1
  276. package/dist/tools/builtin/select/adapterPointQuery.js +3 -2
  277. package/dist/tools/builtin/select/adapterPointQuery.js.map +1 -1
  278. package/dist/tools/builtin/select/pressDecision.js +32 -0
  279. package/dist/tools/builtin/select/pressDecision.js.map +1 -0
  280. package/dist/tools/builtin/select/selectionContributions.js +22 -13
  281. package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
  282. package/dist/tools/builtin/select/useSelectTool.js +13 -10
  283. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  284. package/dist/tools/builtin/slice/useSliceTool.js +22 -8
  285. package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
  286. package/dist/tools/builtin/star/useStarTool.js +13 -4
  287. package/dist/tools/builtin/star/useStarTool.js.map +1 -1
  288. package/dist/tools/builtin/sticky/stickyNode.js +4 -1
  289. package/dist/tools/builtin/sticky/stickyNode.js.map +1 -1
  290. package/dist/tools/builtin/sticky/useStickyTool.js +10 -5
  291. package/dist/tools/builtin/sticky/useStickyTool.js.map +1 -1
  292. package/dist/tools/builtin/text/useTextTool.js +3 -1
  293. package/dist/tools/builtin/text/useTextTool.js.map +1 -1
  294. package/dist/util/paint.js +1 -3
  295. package/dist/util/paint.js.map +1 -1
  296. package/dist/version.js +1 -1
  297. package/package.json +28 -24
@@ -1,10 +1,14 @@
1
- import { DEFAULT_FILL_COLOR, paintWithColor } from "../../../util/paint.js";
2
1
  import { createPaintAction } from "./createPaintAction.js";
2
+ import { DEFAULT_FILL_COLOR, paintWithColor } from "../../../util/paint.js";
3
3
 
4
4
  //#region src/interactions/actions/defaults/setFill.ts
5
- /** Repaint a node's fill, keeping whatever else the fill carries. */
5
+ /** Repaint a node's fill, keeping whatever else the fill carries. None is
6
+ * that record without its `paint`. */
6
7
  function repaint(prev, state) {
7
- if (state.paint === null) return null;
8
+ if (state.paint === null) {
9
+ const { paint: _dropped, ...rest } = prev ?? {};
10
+ return rest;
11
+ }
8
12
  return {
9
13
  ...prev,
10
14
  paint: state.paint ?? paintWithColor(prev?.paint, state.color)
@@ -1 +1 @@
1
- {"version":3,"file":"setFill.js","names":[],"sources":["../../../../src/interactions/actions/defaults/setFill.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport type { Fill, Paint } from '@weasel-js/paint';\nimport { createPaintAction } from './createPaintAction';\nimport { paintWithColor, DEFAULT_FILL_COLOR } from '../../../util/paint';\n\n/** `setFill`'s gesture state. A `paint` shadows the color until a later color\n * supersedes it. */\ninterface SetFillState {\n /** May be 6- or 8-char hex. Ignored while `paint` is set. */\n color: string;\n /** `null` is an explicit no-fill, distinct from `undefined`, which means\n * this gesture carries no paint and the color applies. */\n paint?: Paint | null;\n}\n\n/** Repaint a node's fill, keeping whatever else the fill carries. */\nfunction repaint(prev: Fill | null | undefined, state: SetFillState): Fill | null {\n // `??` would read an explicit no-fill as \"no paint given\" and repaint the\n // node with the state's color, so absence has to be tested for.\n if (state.paint === null) return null;\n return { ...prev, paint: state.paint ?? paintWithColor(prev?.paint, state.color) };\n}\n\n/**\n * Static descriptor for the `setFill` Action.\n *\n * Takes either `color` (a hex string) or `paint` (a whole `Paint`, for\n * gradients and patterns). `paint` wins when both are present.\n *\n * Alpha semantics: alpha lives on the paint's `opacity`. A 6-char (no-alpha)\n * color adopts the alpha of the node's existing paint; an 8-char color states\n * its own. A `paint` is written verbatim.\n */\nexport const setFillAction: Action & { requires: string[] } = createPaintAction<\n SetFillState,\n Fill,\n 'fill'\n>({\n id: 'setFill',\n label: 'Set fill',\n dataKey: 'fill',\n initialState: (pick) => ({\n color: pick<string>('color') ?? DEFAULT_FILL_COLOR,\n paint: pick<Paint | null>('paint'),\n }),\n readParams: (prev, params) => {\n const color = params?.color as string | undefined;\n const paint = params?.paint as Paint | null | undefined;\n if (color === undefined && paint === undefined) return null;\n if (color === undefined) return { ...prev, paint };\n // An explicit color supersedes a paint from an earlier tick; otherwise a\n // picker drag after a gradient would do nothing.\n return { color, paint };\n },\n merge: repaint,\n});\n"],"mappings":";;;;;AAgBA,SAAS,QAAQ,MAA+B,OAAkC;CAGhF,IAAI,MAAM,UAAU,MAAM,OAAO;CACjC,OAAO;EAAE,GAAG;EAAM,OAAO,MAAM,SAAS,eAAe,MAAM,OAAO,MAAM,KAAK;CAAE;AACnF;;;;;;;;;;;AAYA,IAAa,gBAAiD,kBAI5D;CACA,IAAI;CACJ,OAAO;CACP,SAAS;CACT,eAAe,UAAU;EACvB,OAAO,KAAa,OAAO,KAAK;EAChC,OAAO,KAAmB,OAAO;CACnC;CACA,aAAa,MAAM,WAAW;EAC5B,MAAM,QAAQ,QAAQ;EACtB,MAAM,QAAQ,QAAQ;EACtB,IAAI,UAAU,UAAa,UAAU,QAAW,OAAO;EACvD,IAAI,UAAU,QAAW,OAAO;GAAE,GAAG;GAAM;EAAM;EAGjD,OAAO;GAAE;GAAO;EAAM;CACxB;CACA,OAAO;AACT,CAAC"}
1
+ {"version":3,"file":"setFill.js","names":[],"sources":["../../../../src/interactions/actions/defaults/setFill.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport type { Fill, Paint } from '@weasel-js/paint';\nimport { createPaintAction } from './createPaintAction';\nimport { paintWithColor, DEFAULT_FILL_COLOR } from '../../../util/paint';\n\n/** `setFill`'s gesture state. A `paint` shadows the color until a later color\n * supersedes it. */\ninterface SetFillState {\n /** May be 6- or 8-char hex. Ignored while `paint` is set. */\n color: string;\n /** `null` is None, distinct from `undefined`, which means this gesture\n * carries no paint and the color applies. */\n paint?: Paint | null;\n}\n\n/** Repaint a node's fill, keeping whatever else the fill carries. None is\n * that record without its `paint`. */\nfunction repaint(prev: Fill | null | undefined, state: SetFillState): Fill {\n if (state.paint === null) {\n const { paint: _dropped, ...rest } = prev ?? {};\n return rest;\n }\n return { ...prev, paint: state.paint ?? paintWithColor(prev?.paint, state.color) };\n}\n\n/**\n * Static descriptor for the `setFill` Action.\n *\n * Takes either `color` (a hex string) or `paint` (a whole `Paint`, for\n * gradients and patterns). `paint` wins when both are present.\n *\n * Alpha semantics: alpha lives on the paint's `opacity`. A 6-char (no-alpha)\n * color adopts the alpha of the node's existing paint; an 8-char color states\n * its own. A `paint` is written verbatim.\n */\nexport const setFillAction: Action & { requires: string[] } = createPaintAction<\n SetFillState,\n Fill,\n 'fill'\n>({\n id: 'setFill',\n label: 'Set fill',\n dataKey: 'fill',\n initialState: (pick) => ({\n color: pick<string>('color') ?? DEFAULT_FILL_COLOR,\n paint: pick<Paint | null>('paint'),\n }),\n readParams: (prev, params) => {\n const color = params?.color as string | undefined;\n const paint = params?.paint as Paint | null | undefined;\n if (color === undefined && paint === undefined) return null;\n if (color === undefined) return { ...prev, paint };\n // An explicit color supersedes a paint from an earlier tick; otherwise a\n // picker drag after a gradient would do nothing.\n return { color, paint };\n },\n merge: repaint,\n});\n"],"mappings":";;;;;;AAiBA,SAAS,QAAQ,MAA+B,OAA2B;CACzE,IAAI,MAAM,UAAU,MAAM;EACxB,MAAM,EAAE,OAAO,UAAU,GAAG,SAAS,QAAQ,CAAC;EAC9C,OAAO;CACT;CACA,OAAO;EAAE,GAAG;EAAM,OAAO,MAAM,SAAS,eAAe,MAAM,OAAO,MAAM,KAAK;CAAE;AACnF;;;;;;;;;;;AAYA,IAAa,gBAAiD,kBAI5D;CACA,IAAI;CACJ,OAAO;CACP,SAAS;CACT,eAAe,UAAU;EACvB,OAAO,KAAa,OAAO,KAAK;EAChC,OAAO,KAAmB,OAAO;CACnC;CACA,aAAa,MAAM,WAAW;EAC5B,MAAM,QAAQ,QAAQ;EACtB,MAAM,QAAQ,QAAQ;EACtB,IAAI,UAAU,UAAa,UAAU,QAAW,OAAO;EACvD,IAAI,UAAU,QAAW,OAAO;GAAE,GAAG;GAAM;EAAM;EAGjD,OAAO;GAAE;GAAO;EAAM;CACxB;CACA,OAAO;AACT,CAAC"}
@@ -1,5 +1,5 @@
1
- import { DEFAULT_FILL_COLOR, paintWithAlpha, solid } from "../../../util/paint.js";
2
1
  import { createPaintAction } from "./createPaintAction.js";
2
+ import { DEFAULT_FILL_COLOR, paintWithAlpha, solid } from "../../../util/paint.js";
3
3
 
4
4
  //#region src/interactions/actions/defaults/setFillOpacity.ts
5
5
  /**
@@ -1,20 +1,21 @@
1
- import { DEFAULT_STROKE_COLOR, paintWithColor, strokeOf, strokeWith } from "../../../util/paint.js";
2
1
  import { createPaintAction } from "./createPaintAction.js";
2
+ import { DEFAULT_STROKE_COLOR, paintWithColor } from "../../../util/paint.js";
3
3
 
4
4
  //#region src/interactions/actions/defaults/setStroke.ts
5
5
  /** Repaint a node's stroke, keeping its width, cap, join, dash and align, so
6
- * picking a paint does not discard the rest of the stroke. A node with no
7
- * stroke yet gets a hairline one. */
6
+ * picking a paint does not discard the rest of the stroke, and None keeps
7
+ * them for the next paint. A node with no stroke yet gets a hairline one. */
8
8
  function repaint(prev, state) {
9
- if (state.paint === null) return null;
10
- if (state.paint) return prev ? {
11
- ...prev,
12
- paint: state.paint
13
- } : strokeWith(state.paint);
14
- if (!prev) return strokeOf(state.color);
9
+ if (state.paint === null) {
10
+ const { paint: _dropped, ...rest } = prev ?? {};
11
+ return rest;
12
+ }
15
13
  return {
16
- ...prev,
17
- paint: paintWithColor(prev.paint, state.color)
14
+ ...prev?.paint == null && prev?.width === void 0 ? {
15
+ ...prev,
16
+ width: 1
17
+ } : prev,
18
+ paint: state.paint ?? paintWithColor(prev?.paint, state.color)
18
19
  };
19
20
  }
20
21
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"setStroke.js","names":[],"sources":["../../../../src/interactions/actions/defaults/setStroke.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport type { Paint, Stroke } from '@weasel-js/paint';\nimport { createPaintAction } from './createPaintAction';\nimport { paintWithColor, strokeOf, strokeWith, DEFAULT_STROKE_COLOR } from '../../../util/paint';\n\n/** `setStroke`'s gesture state — `setFill`'s, against a stroke. */\ninterface SetStrokeState {\n /** May be 6- or 8-char hex. Ignored while `paint` is set. */\n color: string;\n /** `null` is an explicit no-stroke, distinct from `undefined`, which means\n * this gesture carries no paint and the color applies. */\n paint?: Paint | null;\n}\n\n/** Repaint a node's stroke, keeping its width, cap, join, dash and align, so\n * picking a paint does not discard the rest of the stroke. A node with no\n * stroke yet gets a hairline one. */\nfunction repaint(prev: Stroke | null | undefined, state: SetStrokeState): Stroke | null {\n // A stroke whose paint is none paints nothing, so the whole stroke goes\n // rather than being kept as a widthful thing that draws no ink.\n if (state.paint === null) return null;\n if (state.paint) return prev ? { ...prev, paint: state.paint } : strokeWith(state.paint);\n if (!prev) return strokeOf(state.color);\n return { ...prev, paint: paintWithColor(prev.paint, state.color) };\n}\n\n/**\n * Static descriptor for the `setStroke` Action.\n *\n * Takes either `color` (a hex string) or `paint` (a whole `Paint`, for\n * gradients and patterns). `paint` wins when both are present. A stroke's\n * width, cap, join, dash and align survive either.\n *\n * Alpha semantics: alpha lives on the stroke paint's `opacity`. A 6-char\n * (no-alpha) color adopts the alpha of the node's existing stroke paint; an\n * 8-char color states its own. A `paint` is written verbatim.\n */\nexport const setStrokeAction: Action & { requires: string[] } = createPaintAction<\n SetStrokeState,\n Stroke,\n 'stroke'\n>({\n id: 'setStroke',\n label: 'Set stroke',\n dataKey: 'stroke',\n initialState: (pick) => ({\n color: pick<string>('color') ?? DEFAULT_STROKE_COLOR,\n paint: pick<Paint | null>('paint'),\n }),\n readParams: (prev, params) => {\n const color = params?.color as string | undefined;\n const paint = params?.paint as Paint | null | undefined;\n if (color === undefined && paint === undefined) return null;\n if (color === undefined) return { ...prev, paint };\n // An explicit color supersedes a paint from an earlier tick; otherwise a\n // picker drag after a gradient would do nothing.\n return { color, paint };\n },\n merge: repaint,\n});\n"],"mappings":";;;;;;;AAiBA,SAAS,QAAQ,MAAiC,OAAsC;CAGtF,IAAI,MAAM,UAAU,MAAM,OAAO;CACjC,IAAI,MAAM,OAAO,OAAO,OAAO;EAAE,GAAG;EAAM,OAAO,MAAM;CAAM,IAAI,WAAW,MAAM,KAAK;CACvF,IAAI,CAAC,MAAM,OAAO,SAAS,MAAM,KAAK;CACtC,OAAO;EAAE,GAAG;EAAM,OAAO,eAAe,KAAK,OAAO,MAAM,KAAK;CAAE;AACnE;;;;;;;;;;;;AAaA,IAAa,kBAAmD,kBAI9D;CACA,IAAI;CACJ,OAAO;CACP,SAAS;CACT,eAAe,UAAU;EACvB,OAAO,KAAa,OAAO,KAAK;EAChC,OAAO,KAAmB,OAAO;CACnC;CACA,aAAa,MAAM,WAAW;EAC5B,MAAM,QAAQ,QAAQ;EACtB,MAAM,QAAQ,QAAQ;EACtB,IAAI,UAAU,UAAa,UAAU,QAAW,OAAO;EACvD,IAAI,UAAU,QAAW,OAAO;GAAE,GAAG;GAAM;EAAM;EAGjD,OAAO;GAAE;GAAO;EAAM;CACxB;CACA,OAAO;AACT,CAAC"}
1
+ {"version":3,"file":"setStroke.js","names":[],"sources":["../../../../src/interactions/actions/defaults/setStroke.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport type { Paint, Stroke } from '@weasel-js/paint';\nimport { createPaintAction } from './createPaintAction';\nimport { paintWithColor, DEFAULT_STROKE_COLOR } from '../../../util/paint';\n\n/** `setStroke`'s gesture state — `setFill`'s, against a stroke. */\ninterface SetStrokeState {\n /** May be 6- or 8-char hex. Ignored while `paint` is set. */\n color: string;\n /** `null` is None, distinct from `undefined`, which means this gesture\n * carries no paint and the color applies. */\n paint?: Paint | null;\n}\n\n/** Repaint a node's stroke, keeping its width, cap, join, dash and align, so\n * picking a paint does not discard the rest of the stroke, and None keeps\n * them for the next paint. A node with no stroke yet gets a hairline one. */\nfunction repaint(prev: Stroke | null | undefined, state: SetStrokeState): Stroke {\n if (state.paint === null) {\n const { paint: _dropped, ...rest } = prev ?? {};\n return rest;\n }\n const base = prev?.paint == null && prev?.width === undefined ? { ...prev, width: 1 } : prev;\n return { ...base, paint: state.paint ?? paintWithColor(prev?.paint, state.color) };\n}\n\n/**\n * Static descriptor for the `setStroke` Action.\n *\n * Takes either `color` (a hex string) or `paint` (a whole `Paint`, for\n * gradients and patterns). `paint` wins when both are present. A stroke's\n * width, cap, join, dash and align survive either.\n *\n * Alpha semantics: alpha lives on the stroke paint's `opacity`. A 6-char\n * (no-alpha) color adopts the alpha of the node's existing stroke paint; an\n * 8-char color states its own. A `paint` is written verbatim.\n */\nexport const setStrokeAction: Action & { requires: string[] } = createPaintAction<\n SetStrokeState,\n Stroke,\n 'stroke'\n>({\n id: 'setStroke',\n label: 'Set stroke',\n dataKey: 'stroke',\n initialState: (pick) => ({\n color: pick<string>('color') ?? DEFAULT_STROKE_COLOR,\n paint: pick<Paint | null>('paint'),\n }),\n readParams: (prev, params) => {\n const color = params?.color as string | undefined;\n const paint = params?.paint as Paint | null | undefined;\n if (color === undefined && paint === undefined) return null;\n if (color === undefined) return { ...prev, paint };\n // An explicit color supersedes a paint from an earlier tick; otherwise a\n // picker drag after a gradient would do nothing.\n return { color, paint };\n },\n merge: repaint,\n});\n"],"mappings":";;;;;;;AAiBA,SAAS,QAAQ,MAAiC,OAA+B;CAC/E,IAAI,MAAM,UAAU,MAAM;EACxB,MAAM,EAAE,OAAO,UAAU,GAAG,SAAS,QAAQ,CAAC;EAC9C,OAAO;CACT;CAEA,OAAO;EAAE,GADI,MAAM,SAAS,QAAQ,MAAM,UAAU,SAAY;GAAE,GAAG;GAAM,OAAO;EAAE,IAAI;EACtE,OAAO,MAAM,SAAS,eAAe,MAAM,OAAO,MAAM,KAAK;CAAE;AACnF;;;;;;;;;;;;AAaA,IAAa,kBAAmD,kBAI9D;CACA,IAAI;CACJ,OAAO;CACP,SAAS;CACT,eAAe,UAAU;EACvB,OAAO,KAAa,OAAO,KAAK;EAChC,OAAO,KAAmB,OAAO;CACnC;CACA,aAAa,MAAM,WAAW;EAC5B,MAAM,QAAQ,QAAQ;EACtB,MAAM,QAAQ,QAAQ;EACtB,IAAI,UAAU,UAAa,UAAU,QAAW,OAAO;EACvD,IAAI,UAAU,QAAW,OAAO;GAAE,GAAG;GAAM;EAAM;EAGjD,OAAO;GAAE;GAAO;EAAM;CACxB;CACA,OAAO;AACT,CAAC"}
@@ -1,5 +1,5 @@
1
- import { DEFAULT_STROKE_COLOR, paintWithAlpha, solid, strokeOf } from "../../../util/paint.js";
2
1
  import { createPaintAction } from "./createPaintAction.js";
2
+ import { DEFAULT_STROKE_COLOR, paintWithAlpha, solid, strokeOf } from "../../../util/paint.js";
3
3
 
4
4
  //#region src/interactions/actions/defaults/setStrokeOpacity.ts
5
5
  /**
@@ -7,7 +7,10 @@ var TOOL_ACTIVATE_ID = "tool.activate";
7
7
  * `defaultBinding`. Each entry carries `opts.params.toolId` so the
8
8
  * invoker's `run` can pick out which tool to activate. */
9
9
  function buildToolActivateBindings(specs) {
10
+ const seen = /* @__PURE__ */ new Set();
10
11
  return specs.map(({ toolId, keyOpts }) => {
12
+ const id = seen.has(toolId) ? `${TOOL_ACTIVATE_ID}.${toolId}.${keyOpts.key}` : `${TOOL_ACTIVATE_ID}.${toolId}`;
13
+ seen.add(toolId);
11
14
  const mods = {};
12
15
  if (keyOpts.mod !== void 0) mods.mod = keyOpts.mod;
13
16
  if (keyOpts.alt !== void 0) mods.alt = keyOpts.alt;
@@ -18,7 +21,10 @@ function buildToolActivateBindings(specs) {
18
21
  key: keyOpts.key,
19
22
  ...Object.keys(mods).length > 0 ? { mods } : {}
20
23
  },
21
- opts: { params: { toolId } }
24
+ opts: {
25
+ id,
26
+ params: { toolId }
27
+ }
22
28
  };
23
29
  });
24
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"toolActivate.js","names":[],"sources":["../../../../src/interactions/actions/defaults/toolActivate.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport type { BoundGesture } from '@weasel-js/routing';\nimport type { ImmediateInvoker } from '@weasel-js/routing';\n\n/** Canonical id of the consolidated tool-activation action. One descriptor\n * serves every tool; the matched binding (or imperative caller) supplies\n * `params.toolId` to select which tool to activate. */\nexport const TOOL_ACTIVATE_ID = 'tool.activate';\n\n/** Per-tool key spec consumed by `makeToolActivateAction`. `mod/alt/shift`\n * sit at the top level, mirroring the `KeyBinding` shape tools declare. */\nexport interface ToolActivateKeyOpts {\n key: string;\n mod?: boolean;\n alt?: boolean;\n shift?: boolean | 'optional';\n}\n\n/** Single entry pairing a tool id with the key spec that activates it. */\nexport interface ToolActivateBindingSpec {\n toolId: string;\n keyOpts: ToolActivateKeyOpts;\n}\n\n/** Build the per-tool `BoundGesture` list for the consolidated action's\n * `defaultBinding`. Each entry carries `opts.params.toolId` so the\n * invoker's `run` can pick out which tool to activate. */\nexport function buildToolActivateBindings(\n specs: readonly ToolActivateBindingSpec[],\n): BoundGesture[] {\n return specs.map(({ toolId, keyOpts }) => {\n // Modifiers belong under `mods` (a `ModSpec`), not at the top level of\n // the spec: the matcher treats an absent modifier as \"must NOT be held\".\n const mods: Record<string, boolean | 'optional'> = {};\n if (keyOpts.mod !== undefined) mods.mod = keyOpts.mod;\n if (keyOpts.alt !== undefined) mods.alt = keyOpts.alt;\n if (keyOpts.shift !== undefined) mods.shift = keyOpts.shift;\n return {\n spec: {\n kind: 'key',\n key: keyOpts.key,\n ...(Object.keys(mods).length > 0 ? { mods } : {}),\n },\n opts: { params: { toolId } },\n } as BoundGesture;\n });\n}\n\n/** Build the consolidated `tool.activate` action. `bindings` enumerates one\n * `{ spec, opts: { params: { toolId } } }` per tool. Imperative callers reach\n * the same effect via `registry.trigger('tool.activate', { toolId })`.\n *\n * `isEligible` gates activation per tool, which the static per-action\n * `Action.eligible` cannot. Omit it for consumers with no mode registry. */\nexport function makeToolActivateAction(\n bindings: BoundGesture[],\n isEligible?: (toolId: string) => boolean,\n): Action {\n const invoker: ImmediateInvoker = {\n timing: 'immediate',\n run: (deps, params) => {\n const activeTool = deps.activeTool;\n const toolId = params?.toolId as string | undefined;\n if (!activeTool || !toolId) return;\n // Grayed out in the palette ⇒ not reachable by shortcut either.\n if (isEligible && !isEligible(toolId)) return;\n activeTool.setActive(toolId);\n },\n };\n\n return {\n id: TOOL_ACTIVATE_ID,\n label: 'Activate tool',\n defaultBinding: bindings,\n scope: 'hotkey',\n invoker,\n requires: ['activeTool'],\n } as Action & { requires: string[] };\n}\n"],"mappings":";;;;AAOA,IAAa,mBAAmB;;;;AAoBhC,SAAgB,0BACd,OACgB;CAChB,OAAO,MAAM,KAAK,EAAE,QAAQ,cAAc;EAGxC,MAAM,OAA6C,CAAC;EACpD,IAAI,QAAQ,QAAQ,QAAW,KAAK,MAAM,QAAQ;EAClD,IAAI,QAAQ,QAAQ,QAAW,KAAK,MAAM,QAAQ;EAClD,IAAI,QAAQ,UAAU,QAAW,KAAK,QAAQ,QAAQ;EACtD,OAAO;GACL,MAAM;IACJ,MAAM;IACN,KAAK,QAAQ;IACb,GAAI,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,IAAI,EAAE,KAAK,IAAI,CAAC;GACjD;GACA,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE;EAC7B;CACF,CAAC;AACH;;;;;;;AAQA,SAAgB,uBACd,UACA,YACQ;CAaR,OAAO;EACL,IAAI;EACJ,OAAO;EACP,gBAAgB;EAChB,OAAO;EACP;GAhBA,QAAQ;GACR,MAAM,MAAM,WAAW;IACrB,MAAM,aAAa,KAAK;IACxB,MAAM,SAAS,QAAQ;IACvB,IAAI,CAAC,cAAc,CAAC,QAAQ;IAE5B,IAAI,cAAc,CAAC,WAAW,MAAM,GAAG;IACvC,WAAW,UAAU,MAAM;GAC7B;EAQA;EACA,UAAU,CAAC,YAAY;CACzB;AACF"}
1
+ {"version":3,"file":"toolActivate.js","names":[],"sources":["../../../../src/interactions/actions/defaults/toolActivate.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport type { BoundGesture } from '@weasel-js/routing';\nimport type { ImmediateInvoker } from '@weasel-js/routing';\n\n/** Canonical id of the consolidated tool-activation action. One descriptor\n * serves every tool; the matched binding (or imperative caller) supplies\n * `params.toolId` to select which tool to activate. */\nexport const TOOL_ACTIVATE_ID = 'tool.activate';\n\n/** Per-tool key spec consumed by `makeToolActivateAction`. `mod/alt/shift`\n * sit at the top level, mirroring the `KeyBinding` shape tools declare. */\nexport interface ToolActivateKeyOpts {\n key: string;\n mod?: boolean;\n alt?: boolean;\n shift?: boolean | 'optional';\n}\n\n/** Single entry pairing a tool id with the key spec that activates it. */\nexport interface ToolActivateBindingSpec {\n toolId: string;\n keyOpts: ToolActivateKeyOpts;\n}\n\n/** Build the per-tool `BoundGesture` list for the consolidated action's\n * `defaultBinding`. Each entry carries `opts.params.toolId` so the\n * invoker's `run` can pick out which tool to activate. */\nexport function buildToolActivateBindings(\n specs: readonly ToolActivateBindingSpec[],\n): BoundGesture[] {\n const seen = new Set<string>();\n return specs.map(({ toolId, keyOpts }) => {\n const id = seen.has(toolId) ? `${TOOL_ACTIVATE_ID}.${toolId}.${keyOpts.key}` : `${TOOL_ACTIVATE_ID}.${toolId}`;\n seen.add(toolId);\n // Modifiers belong under `mods` (a `ModSpec`), not at the top level of\n // the spec: the matcher treats an absent modifier as \"must NOT be held\".\n const mods: Record<string, boolean | 'optional'> = {};\n if (keyOpts.mod !== undefined) mods.mod = keyOpts.mod;\n if (keyOpts.alt !== undefined) mods.alt = keyOpts.alt;\n if (keyOpts.shift !== undefined) mods.shift = keyOpts.shift;\n return {\n spec: {\n kind: 'key',\n key: keyOpts.key,\n ...(Object.keys(mods).length > 0 ? { mods } : {}),\n },\n opts: { id, params: { toolId } },\n } as BoundGesture;\n });\n}\n\n/** Build the consolidated `tool.activate` action. `bindings` enumerates one\n * `{ spec, opts: { params: { toolId } } }` per tool. Imperative callers reach\n * the same effect via `registry.trigger('tool.activate', { toolId })`.\n *\n * `isEligible` gates activation per tool, which the static per-action\n * `Action.eligible` cannot. Omit it for consumers with no mode registry. */\nexport function makeToolActivateAction(\n bindings: BoundGesture[],\n isEligible?: (toolId: string) => boolean,\n): Action {\n const invoker: ImmediateInvoker = {\n timing: 'immediate',\n run: (deps, params) => {\n const activeTool = deps.activeTool;\n const toolId = params?.toolId as string | undefined;\n if (!activeTool || !toolId) return;\n // Grayed out in the palette ⇒ not reachable by shortcut either.\n if (isEligible && !isEligible(toolId)) return;\n activeTool.setActive(toolId);\n },\n };\n\n return {\n id: TOOL_ACTIVATE_ID,\n label: 'Activate tool',\n defaultBinding: bindings,\n scope: 'hotkey',\n invoker,\n requires: ['activeTool'],\n } as Action & { requires: string[] };\n}\n"],"mappings":";;;;AAOA,IAAa,mBAAmB;;;;AAoBhC,SAAgB,0BACd,OACgB;CAChB,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO,MAAM,KAAK,EAAE,QAAQ,cAAc;EACxC,MAAM,KAAK,KAAK,IAAI,MAAM,IAAI,GAAG,iBAAiB,GAAG,OAAO,GAAG,QAAQ,QAAQ,GAAG,iBAAiB,GAAG;EACtG,KAAK,IAAI,MAAM;EAGf,MAAM,OAA6C,CAAC;EACpD,IAAI,QAAQ,QAAQ,QAAW,KAAK,MAAM,QAAQ;EAClD,IAAI,QAAQ,QAAQ,QAAW,KAAK,MAAM,QAAQ;EAClD,IAAI,QAAQ,UAAU,QAAW,KAAK,QAAQ,QAAQ;EACtD,OAAO;GACL,MAAM;IACJ,MAAM;IACN,KAAK,QAAQ;IACb,GAAI,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,IAAI,EAAE,KAAK,IAAI,CAAC;GACjD;GACA,MAAM;IAAE;IAAI,QAAQ,EAAE,OAAO;GAAE;EACjC;CACF,CAAC;AACH;;;;;;;AAQA,SAAgB,uBACd,UACA,YACQ;CAaR,OAAO;EACL,IAAI;EACJ,OAAO;EACP,gBAAgB;EAChB,OAAO;EACP;GAhBA,QAAQ;GACR,MAAM,MAAM,WAAW;IACrB,MAAM,aAAa,KAAK;IACxB,MAAM,SAAS,QAAQ;IACvB,IAAI,CAAC,cAAc,CAAC,QAAQ;IAE5B,IAAI,cAAc,CAAC,WAAW,MAAM,GAAG;IACvC,WAAW,UAAU,MAAM;GAC7B;EAQA;EACA,UAAU,CAAC,YAAY;CACzB;AACF"}
@@ -22,10 +22,13 @@ function makeToolResetToDefaultAction(getTarget) {
22
22
  return {
23
23
  id: TOOL_RESET_TO_DEFAULT_ID,
24
24
  label: "Return to default tool",
25
- defaultBinding: {
26
- kind: "key",
27
- key: "Escape"
28
- },
25
+ defaultBinding: [{
26
+ spec: {
27
+ kind: "key",
28
+ key: "Escape"
29
+ },
30
+ opts: { id: "tool.resetToDefault.key" }
31
+ }],
29
32
  invoker: {
30
33
  timing: "immediate",
31
34
  run: (deps) => {
@@ -1 +1 @@
1
- {"version":3,"file":"toolResetToDefault.js","names":[],"sources":["../../../../src/interactions/actions/defaults/toolResetToDefault.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport type { ImmediateInvoker } from '@weasel-js/routing';\n\n/** Canonical id of the \"Escape returns to the default tool\" action. */\nexport const TOOL_RESET_TO_DEFAULT_ID = 'tool.resetToDefault';\n\n/**\n * Build the Escape-returns-to-default-tool action.\n *\n * It joins the dispatcher's Escape order, which is first-match-wins, at\n * ambient scope, the lowest priority, so one Escape press does exactly one\n * thing, in this order:\n *\n * 1. cancel an in-flight gesture\n * 2. exit path-anchor edit mode\n * 3. clear the selection\n * 4. return to the default tool ← this action\n *\n * `getTarget` returns the tool id to return to, or `null` when the behavior\n * is disabled (consumer passed `defaultTool: null`) or already satisfied.\n */\nexport function makeToolResetToDefaultAction(\n getTarget: () => string | null,\n): Action {\n const invoker: ImmediateInvoker = {\n timing: 'immediate',\n run: (deps) => {\n const activeTool = deps.activeTool;\n const target = getTarget();\n if (!activeTool || !target) return;\n activeTool.setActive(target);\n },\n };\n\n return {\n id: TOOL_RESET_TO_DEFAULT_ID,\n label: 'Return to default tool',\n defaultBinding: { kind: 'key', key: 'Escape' },\n invoker,\n requires: ['activeTool'],\n // Report disabled (rather than silently no-op'ing) when there's nothing\n // to return to, so the dispatcher falls through and the keypress isn't\n // swallowed.\n enabled: () => (getTarget() ? true : ActionDisabledReason.NotApplicable),\n } as Action & { requires: string[] };\n}\n"],"mappings":";;;;AAKA,IAAa,2BAA2B;;;;;;;;;;;;;;;;AAiBxC,SAAgB,6BACd,WACQ;CAWR,OAAO;EACL,IAAI;EACJ,OAAO;EACP,gBAAgB;GAAE,MAAM;GAAO,KAAK;EAAS;EAC7C;GAbA,QAAQ;GACR,MAAM,SAAS;IACb,MAAM,aAAa,KAAK;IACxB,MAAM,SAAS,UAAU;IACzB,IAAI,CAAC,cAAc,CAAC,QAAQ;IAC5B,WAAW,UAAU,MAAM;GAC7B;EAOA;EACA,UAAU,CAAC,YAAY;EAIvB,eAAgB,UAAU,IAAI,OAAO,qBAAqB;CAC5D;AACF"}
1
+ {"version":3,"file":"toolResetToDefault.js","names":[],"sources":["../../../../src/interactions/actions/defaults/toolResetToDefault.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport type { ImmediateInvoker } from '@weasel-js/routing';\n\n/** Canonical id of the \"Escape returns to the default tool\" action. */\nexport const TOOL_RESET_TO_DEFAULT_ID = 'tool.resetToDefault';\n\n/**\n * Build the Escape-returns-to-default-tool action.\n *\n * It joins the dispatcher's Escape order, which is first-match-wins, at\n * ambient scope, the lowest priority, so one Escape press does exactly one\n * thing, in this order:\n *\n * 1. cancel an in-flight gesture\n * 2. exit path-anchor edit mode\n * 3. clear the selection\n * 4. return to the default tool ← this action\n *\n * `getTarget` returns the tool id to return to, or `null` when the behavior\n * is disabled (consumer passed `defaultTool: null`) or already satisfied.\n */\nexport function makeToolResetToDefaultAction(\n getTarget: () => string | null,\n): Action {\n const invoker: ImmediateInvoker = {\n timing: 'immediate',\n run: (deps) => {\n const activeTool = deps.activeTool;\n const target = getTarget();\n if (!activeTool || !target) return;\n activeTool.setActive(target);\n },\n };\n\n return {\n id: TOOL_RESET_TO_DEFAULT_ID,\n label: 'Return to default tool',\n defaultBinding: [{ spec: { kind: 'key', key: 'Escape' }, opts: { id: 'tool.resetToDefault.key' } }],\n invoker,\n requires: ['activeTool'],\n // Report disabled (rather than silently no-op'ing) when there's nothing\n // to return to, so the dispatcher falls through and the keypress isn't\n // swallowed.\n enabled: () => (getTarget() ? true : ActionDisabledReason.NotApplicable),\n } as Action & { requires: string[] };\n}\n"],"mappings":";;;;AAKA,IAAa,2BAA2B;;;;;;;;;;;;;;;;AAiBxC,SAAgB,6BACd,WACQ;CAWR,OAAO;EACL,IAAI;EACJ,OAAO;EACP,gBAAgB,CAAC;GAAE,MAAM;IAAE,MAAM;IAAO,KAAK;GAAS;GAAG,MAAM,EAAE,IAAI,0BAA0B;EAAE,CAAC;EAClG;GAbA,QAAQ;GACR,MAAM,SAAS;IACb,MAAM,aAAa,KAAK;IACxB,MAAM,SAAS,UAAU;IACzB,IAAI,CAAC,cAAc,CAAC,QAAQ;IAC5B,WAAW,UAAU,MAAM;GAC7B;EAOA;EACA,UAAU,CAAC,YAAY;EAIvB,eAAgB,UAAU,IAAI,OAAO,qBAAqB;CAC5D;AACF"}
@@ -12,11 +12,14 @@ var undoAction = {
12
12
  label: "Undo",
13
13
  icon: /* @icon */ "core:undo",
14
14
  group: "history",
15
- defaultBinding: {
16
- kind: "key",
17
- key: "z",
18
- mods: { mod: true }
19
- },
15
+ defaultBinding: [{
16
+ spec: {
17
+ kind: "key",
18
+ key: "z",
19
+ mods: { mod: true }
20
+ },
21
+ opts: { id: "undo.key" }
22
+ }],
20
23
  requires: ["history"],
21
24
  invoker: {
22
25
  timing: "immediate",
@@ -34,14 +37,17 @@ var redoAction = {
34
37
  label: "Redo",
35
38
  icon: /* @icon */ "core:redo",
36
39
  group: "history",
37
- defaultBinding: {
38
- kind: "key",
39
- key: "z",
40
- mods: {
41
- mod: true,
42
- shift: true
43
- }
44
- },
40
+ defaultBinding: [{
41
+ spec: {
42
+ kind: "key",
43
+ key: "z",
44
+ mods: {
45
+ mod: true,
46
+ shift: true
47
+ }
48
+ },
49
+ opts: { id: "redo.key" }
50
+ }],
45
51
  requires: ["history"],
46
52
  invoker: {
47
53
  timing: "immediate",
@@ -1 +1 @@
1
- {"version":3,"file":"undoRedo.js","names":[],"sources":["../../../../src/interactions/actions/defaults/undoRedo.ts"],"sourcesContent":["import { ActionDisabledReason, type Action, type ActionDeps } from '@weasel-js/routing';\nimport { icon } from '../../../icons.gen';\n\nfunction historyCan(deps: ActionDeps | undefined, which: 'canUndo' | 'canRedo'): true | ActionDisabledReason {\n return deps?.history?.[which]?.() ? true : ActionDisabledReason.NotApplicable;\n}\n\n/**\n * Static descriptor for the `undo` Action.\n */\n// No `eligible` field → undo/redo are always eligible across all modes.\nexport const undoAction: Action & { requires: string[] } = {\n id: 'undo',\n label: 'Undo',\n icon: icon('undo'),\n group: 'history',\n defaultBinding: { kind: 'key', key: 'z', mods: { mod: true } },\n requires: ['history'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n deps.history?.undo?.();\n },\n },\n enabled: (deps) => historyCan(deps, 'canUndo'),\n};\n\n/**\n * Static descriptor for the `redo` Action.\n */\nexport const redoAction: Action & { requires: string[] } = {\n id: 'redo',\n label: 'Redo',\n icon: icon('redo'),\n group: 'history',\n defaultBinding: { kind: 'key', key: 'z', mods: { mod: true, shift: true } },\n requires: ['history'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n deps.history?.redo?.();\n },\n },\n enabled: (deps) => historyCan(deps, 'canRedo'),\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"undoRedo.js","names":[],"sources":["../../../../src/interactions/actions/defaults/undoRedo.ts"],"sourcesContent":["import { ActionDisabledReason, type Action, type ActionDeps } from '@weasel-js/routing';\nimport { icon } from '../../../icons.gen';\n\nfunction historyCan(deps: ActionDeps | undefined, which: 'canUndo' | 'canRedo'): true | ActionDisabledReason {\n return deps?.history?.[which]?.() ? true : ActionDisabledReason.NotApplicable;\n}\n\n/**\n * Static descriptor for the `undo` Action.\n */\n// No `eligible` field → undo/redo are always eligible across all modes.\nexport const undoAction: Action & { requires: string[] } = {\n id: 'undo',\n label: 'Undo',\n icon: icon('undo'),\n group: 'history',\n defaultBinding: [{ spec: { kind: 'key', key: 'z', mods: { mod: true } }, opts: { id: 'undo.key' } }],\n requires: ['history'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n deps.history?.undo?.();\n },\n },\n enabled: (deps) => historyCan(deps, 'canUndo'),\n};\n\n/**\n * Static descriptor for the `redo` Action.\n */\nexport const redoAction: Action & { requires: string[] } = {\n id: 'redo',\n label: 'Redo',\n icon: icon('redo'),\n group: 'history',\n defaultBinding: [{ spec: { kind: 'key', key: 'z', mods: { mod: true, shift: true } }, opts: { id: 'redo.key' } }],\n requires: ['history'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n deps.history?.redo?.();\n },\n },\n enabled: (deps) => historyCan(deps, 'canRedo'),\n};\n"],"mappings":"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"}
@@ -18,13 +18,16 @@ var viewportWheelPanAction = {
18
18
  */
19
19
  defaultBinding: [{
20
20
  spec: { kind: "wheel" },
21
- opts: {}
21
+ opts: { id: "viewport.wheelPan.wheel" }
22
22
  }, {
23
23
  spec: {
24
24
  kind: "wheel",
25
25
  mods: { shift: true }
26
26
  },
27
- opts: { params: { swapAxis: true } }
27
+ opts: {
28
+ id: "viewport.wheelPan.swapAxis",
29
+ params: { swapAxis: true }
30
+ }
28
31
  }],
29
32
  requires: ["view", "debug"],
30
33
  invoker: {
@@ -57,16 +60,22 @@ function makeViewportWheelPanAction(opts = {}) {
57
60
  ...viewportWheelPanAction,
58
61
  defaultBinding: [{
59
62
  spec: { kind: "wheel" },
60
- opts: { params: { axis } }
63
+ opts: {
64
+ id: "viewport.wheelPan.wheel",
65
+ params: { axis }
66
+ }
61
67
  }, {
62
68
  spec: {
63
69
  kind: "wheel",
64
70
  mods: { shift: true }
65
71
  },
66
- opts: { params: {
67
- swapAxis: true,
68
- axis
69
- } }
72
+ opts: {
73
+ id: "viewport.wheelPan.swapAxis",
74
+ params: {
75
+ swapAxis: true,
76
+ axis
77
+ }
78
+ }
70
79
  }]
71
80
  };
72
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"viewportWheelPan.js","names":[],"sources":["../../../../src/interactions/actions/defaults/viewportWheelPan.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport { wheelPan } from 'core/viewport/wheelHandler';\n\n/**\n * Wheel and trackpad pan. Shift+wheel pans horizontally, by routing deltaY\n * into the x axis (`params.swapAxis`). `params.axis` locks the pan to one\n * axis, and outranks `swapAxis`: a shift-wheel routed into a barred axis\n * moves nothing.\n */\nexport const viewportWheelPanAction: Action & { requires: string[] } = {\n id: 'viewport.wheelPan',\n label: 'Pan (wheel)',\n group: 'viewport',\n /**\n * The matcher's strict modifier check keeps the bindings exclusive, and\n * leaves Cmd+wheel to `viewportZoomAction`: omitting `mods` forbids any\n * modifier, and `mods: { shift: true }` forbids the others.\n */\n defaultBinding: [\n { spec: { kind: 'wheel' }, opts: {} },\n { spec: { kind: 'wheel', mods: { shift: true } }, opts: { params: { swapAxis: true } } },\n ],\n requires: ['view', 'debug'],\n invoker: {\n timing: 'immediate',\n run(deps, params) {\n const view = deps.view;\n if (!view) return;\n const axis = (params?.axis as 'both' | 'x' | 'y' | undefined) ?? 'both';\n const deltaX = (params?.deltaX as number | undefined) ?? 0;\n const deltaY = (params?.deltaY as number | undefined) ?? 0;\n const from = view.get();\n const next = wheelPan(from, { deltaX, deltaY }, { axis, swapAxis: params?.swapAxis === true });\n view.set(next);\n deps.debug?.recordViewport('pan', from, next);\n },\n },\n enabled: () => true,\n};\n\n/** Tuning for {@link makeViewportWheelPanAction}. */\nexport interface WheelPanOptions {\n /** Lock the pan to one axis. Default `'both'`. */\n axis?: 'both' | 'x' | 'y';\n}\n\n/** Builds a `viewport.wheelPan` descriptor with `opts` baked into both of its\n * default bindings. `viewportWheelPanAction` is the unconfigured one. */\nexport function makeViewportWheelPanAction(\n opts: WheelPanOptions = {},\n): Action & { requires: string[] } {\n const axis = opts.axis ?? 'both';\n return {\n ...viewportWheelPanAction,\n defaultBinding: [\n { spec: { kind: 'wheel' }, opts: { params: { axis } } },\n { spec: { kind: 'wheel', mods: { shift: true } }, opts: { params: { swapAxis: true, axis } } },\n ],\n };\n}\n"],"mappings":";;;;;;;;;AASA,IAAa,yBAA0D;CACrE,IAAI;CACJ,OAAO;CACP,OAAO;;;;;;CAMP,gBAAgB,CACd;EAAE,MAAM,EAAE,MAAM,QAAQ;EAAG,MAAM,CAAC;CAAE,GACpC;EAAE,MAAM;GAAE,MAAM;GAAS,MAAM,EAAE,OAAO,KAAK;EAAE;EAAG,MAAM,EAAE,QAAQ,EAAE,UAAU,KAAK,EAAE;CAAE,CACzF;CACA,UAAU,CAAC,QAAQ,OAAO;CAC1B,SAAS;EACP,QAAQ;EACR,IAAI,MAAM,QAAQ;GAChB,MAAM,OAAO,KAAK;GAClB,IAAI,CAAC,MAAM;GACX,MAAM,OAAQ,QAAQ,QAA2C;GACjE,MAAM,SAAU,QAAQ,UAAiC;GACzD,MAAM,SAAU,QAAQ,UAAiC;GACzD,MAAM,OAAO,KAAK,IAAI;GACtB,MAAM,OAAO,SAAS,MAAM;IAAE;IAAQ;GAAO,GAAG;IAAE;IAAM,UAAU,QAAQ,aAAa;GAAK,CAAC;GAC7F,KAAK,IAAI,IAAI;GACb,KAAK,OAAO,eAAe,OAAO,MAAM,IAAI;EAC9C;CACF;CACA,eAAe;AACjB;;;AAUA,SAAgB,2BACd,OAAwB,CAAC,GACQ;CACjC,MAAM,OAAO,KAAK,QAAQ;CAC1B,OAAO;EACL,GAAG;EACH,gBAAgB,CACd;GAAE,MAAM,EAAE,MAAM,QAAQ;GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;EAAE,GACtD;GAAE,MAAM;IAAE,MAAM;IAAS,MAAM,EAAE,OAAO,KAAK;GAAE;GAAG,MAAM,EAAE,QAAQ;IAAE,UAAU;IAAM;GAAK,EAAE;EAAE,CAC/F;CACF;AACF"}
1
+ {"version":3,"file":"viewportWheelPan.js","names":[],"sources":["../../../../src/interactions/actions/defaults/viewportWheelPan.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport { wheelPan } from 'core/viewport/wheelHandler';\n\n/**\n * Wheel and trackpad pan. Shift+wheel pans horizontally, by routing deltaY\n * into the x axis (`params.swapAxis`). `params.axis` locks the pan to one\n * axis, and outranks `swapAxis`: a shift-wheel routed into a barred axis\n * moves nothing.\n */\nexport const viewportWheelPanAction: Action & { requires: string[] } = {\n id: 'viewport.wheelPan',\n label: 'Pan (wheel)',\n group: 'viewport',\n /**\n * The matcher's strict modifier check keeps the bindings exclusive, and\n * leaves Cmd+wheel to `viewportZoomAction`: omitting `mods` forbids any\n * modifier, and `mods: { shift: true }` forbids the others.\n */\n defaultBinding: [\n { spec: { kind: 'wheel' }, opts: { id: 'viewport.wheelPan.wheel' } },\n { spec: { kind: 'wheel', mods: { shift: true } }, opts: { id: 'viewport.wheelPan.swapAxis', params: { swapAxis: true } } },\n ],\n requires: ['view', 'debug'],\n invoker: {\n timing: 'immediate',\n run(deps, params) {\n const view = deps.view;\n if (!view) return;\n const axis = (params?.axis as 'both' | 'x' | 'y' | undefined) ?? 'both';\n const deltaX = (params?.deltaX as number | undefined) ?? 0;\n const deltaY = (params?.deltaY as number | undefined) ?? 0;\n const from = view.get();\n const next = wheelPan(from, { deltaX, deltaY }, { axis, swapAxis: params?.swapAxis === true });\n view.set(next);\n deps.debug?.recordViewport('pan', from, next);\n },\n },\n enabled: () => true,\n};\n\n/** Tuning for {@link makeViewportWheelPanAction}. */\nexport interface WheelPanOptions {\n /** Lock the pan to one axis. Default `'both'`. */\n axis?: 'both' | 'x' | 'y';\n}\n\n/** Builds a `viewport.wheelPan` descriptor with `opts` baked into both of its\n * default bindings. `viewportWheelPanAction` is the unconfigured one. */\nexport function makeViewportWheelPanAction(\n opts: WheelPanOptions = {},\n): Action & { requires: string[] } {\n const axis = opts.axis ?? 'both';\n return {\n ...viewportWheelPanAction,\n defaultBinding: [\n { spec: { kind: 'wheel' }, opts: { id: 'viewport.wheelPan.wheel', params: { axis } } },\n { spec: { kind: 'wheel', mods: { shift: true } }, opts: { id: 'viewport.wheelPan.swapAxis', params: { swapAxis: true, axis } } },\n ],\n };\n}\n"],"mappings":";;;;;;;;;AASA,IAAa,yBAA0D;CACrE,IAAI;CACJ,OAAO;CACP,OAAO;;;;;;CAMP,gBAAgB,CACd;EAAE,MAAM,EAAE,MAAM,QAAQ;EAAG,MAAM,EAAE,IAAI,0BAA0B;CAAE,GACnE;EAAE,MAAM;GAAE,MAAM;GAAS,MAAM,EAAE,OAAO,KAAK;EAAE;EAAG,MAAM;GAAE,IAAI;GAA8B,QAAQ,EAAE,UAAU,KAAK;EAAE;CAAE,CAC3H;CACA,UAAU,CAAC,QAAQ,OAAO;CAC1B,SAAS;EACP,QAAQ;EACR,IAAI,MAAM,QAAQ;GAChB,MAAM,OAAO,KAAK;GAClB,IAAI,CAAC,MAAM;GACX,MAAM,OAAQ,QAAQ,QAA2C;GACjE,MAAM,SAAU,QAAQ,UAAiC;GACzD,MAAM,SAAU,QAAQ,UAAiC;GACzD,MAAM,OAAO,KAAK,IAAI;GACtB,MAAM,OAAO,SAAS,MAAM;IAAE;IAAQ;GAAO,GAAG;IAAE;IAAM,UAAU,QAAQ,aAAa;GAAK,CAAC;GAC7F,KAAK,IAAI,IAAI;GACb,KAAK,OAAO,eAAe,OAAO,MAAM,IAAI;EAC9C;CACF;CACA,eAAe;AACjB;;;AAUA,SAAgB,2BACd,OAAwB,CAAC,GACQ;CACjC,MAAM,OAAO,KAAK,QAAQ;CAC1B,OAAO;EACL,GAAG;EACH,gBAAgB,CACd;GAAE,MAAM,EAAE,MAAM,QAAQ;GAAG,MAAM;IAAE,IAAI;IAA2B,QAAQ,EAAE,KAAK;GAAE;EAAE,GACrF;GAAE,MAAM;IAAE,MAAM;IAAS,MAAM,EAAE,OAAO,KAAK;GAAE;GAAG,MAAM;IAAE,IAAI;IAA8B,QAAQ;KAAE,UAAU;KAAM;IAAK;GAAE;EAAE,CACjI;CACF;AACF"}
@@ -27,18 +27,27 @@ function zoomBindings(wheel) {
27
27
  kind: "wheel",
28
28
  mods: { mod: true }
29
29
  },
30
- opts: { params: { kind: "wheel" } }
30
+ opts: {
31
+ id: "viewport.zoom.wheel",
32
+ params: { kind: "wheel" }
33
+ }
31
34
  },
32
35
  {
33
36
  spec: {
34
37
  kind: "wheel",
35
38
  mods: { ctrl: true }
36
39
  },
37
- opts: { params: { kind: "wheel" } }
40
+ opts: {
41
+ id: "viewport.zoom.trackpad",
42
+ params: { kind: "wheel" }
43
+ }
38
44
  },
39
45
  {
40
46
  spec: { kind: "pinch" },
41
- opts: { params: { kind: "pinch" } }
47
+ opts: {
48
+ id: "viewport.zoom.pinch",
49
+ params: { kind: "pinch" }
50
+ }
42
51
  },
43
52
  {
44
53
  spec: {
@@ -49,7 +58,10 @@ function zoomBindings(wheel) {
49
58
  shift: "optional"
50
59
  }
51
60
  },
52
- opts: { params: { kind: "in" } }
61
+ opts: {
62
+ id: "viewport.zoom.in",
63
+ params: { kind: "in" }
64
+ }
53
65
  },
54
66
  {
55
67
  spec: {
@@ -57,7 +69,10 @@ function zoomBindings(wheel) {
57
69
  key: "-",
58
70
  mods: { mod: true }
59
71
  },
60
- opts: { params: { kind: "out" } }
72
+ opts: {
73
+ id: "viewport.zoom.out",
74
+ params: { kind: "out" }
75
+ }
61
76
  },
62
77
  {
63
78
  spec: {
@@ -65,7 +80,10 @@ function zoomBindings(wheel) {
65
80
  key: "0",
66
81
  mods: { mod: true }
67
82
  },
68
- opts: { params: { kind: "reset" } }
83
+ opts: {
84
+ id: "viewport.zoom.reset",
85
+ params: { kind: "reset" }
86
+ }
69
87
  }
70
88
  ];
71
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"viewportZoom.js","names":[],"sources":["../../../../src/interactions/actions/defaults/viewportZoom.ts"],"sourcesContent":["import type { Action, BoundGesture } from '@weasel-js/routing';\nimport type { ViewApi } from '../depSchema';\nimport { axisZoomFactor, zoomAt, type ZoomAxis } from 'core/viewport/zoomAt';\nimport { wheelZoomFactor } from 'core/viewport/wheelHandler';\nimport { DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM } from 'core/viewport/zoomBounds';\nimport { viewToTransform, type View } from 'core/viewport/view';\nimport { screenToWorld } from 'core/viewport/viewTransform';\nimport type { ViewAnimationOptions } from 'core/viewport/useViewAnimation';\n\n// Multiplicative step for keyboard zoom (matches useKeyboardZoomTool default).\nconst KEY_STEP = 1.25;\n\n/** Keyboard-zoom anchor: the host center when the view dep can measure it,\n * else the canvas top-left origin. */\nfunction keyAnchor(view: ViewApi): { x: number; y: number } {\n const size = view.hostSize?.();\n return size ? { x: size.width / 2, y: size.height / 2 } : { x: 0, y: 0 };\n}\n\n/**\n * Tuning for the animated form of the discrete zoom steps.\n */\nexport interface ViewportZoomAnimateOptions extends ViewAnimationOptions {\n /** Duration for the Cmd+0 reset branch. Defaults to `ms`. */\n resetMs?: number;\n}\n\n/**\n * Tuning for {@link makeViewportZoomAction}.\n */\nexport interface ViewportZoomOptions {\n /**\n * Which wheel gesture triggers zoom.\n * - `'mod'` (default): Cmd/Ctrl+wheel — coexists with plain-wheel pan\n * (`viewport.wheelPan`).\n * - `'plain'`: bare wheel, no modifier. Pair with `viewport.pan: false`,\n * since plain wheel otherwise drives pan and the two would compete.\n */\n wheel?: 'plain' | 'mod';\n /** Lower clamp on the resulting view scale, forwarded to `zoomAt`. Default 0.1. */\n min?: number;\n /** Upper clamp on the resulting view scale, forwarded to `zoomAt`. Default 8. */\n max?: number;\n /** Hold wheel, trackpad-pinch, and key zoom to one axis; the other keeps its\n * scale and its translation. Default `'both'`. Cmd+0 still resets both. */\n axis?: ZoomAxis;\n /**\n * Glide the discrete steps (Cmd+=, Cmd+-, Cmd+0) instead of jumping.\n * `true` uses the kit defaults; an object tunes them. Wheel and pinch never\n * animate — their input already samples every frame. Requires a `view` dep\n * that implements `animate`; without one this is ignored.\n */\n animate?: boolean | ViewportZoomAnimateOptions;\n}\n\n/** The gestures `viewport.zoom` answers: the configurable wheel trigger, then\n * the fixed pinch and key bindings. */\nfunction zoomBindings(wheel: ViewportZoomOptions['wheel']): BoundGesture[] {\n // 'plain': bare wheel (omitting `mods` forbids any modifier — see the\n // matcher's strict modifier check). 'mod': Cmd/Ctrl+wheel.\n const wheelSpec =\n wheel === 'plain'\n ? { kind: 'wheel' as const }\n : { kind: 'wheel' as const, mods: { mod: true } };\n return [\n {\n spec: wheelSpec,\n opts: { params: { kind: 'wheel' } },\n },\n // Trackpad pinch, which the browser delivers as ctrl+wheel. Distinct from\n // the `mod` binding on mac (`mod` is metaKey there), so without its own\n // binding nothing claims it and the page zooms instead. Elsewhere it\n // duplicates `mod`, and `matchBest` picks a single winner.\n {\n spec: { kind: 'wheel' as const, mods: { ctrl: true } },\n opts: { params: { kind: 'wheel' } },\n },\n // Safari's trackpad pinch, reported as a scale rather than a wheel. Safari\n // may send the same pinch as ctrl+wheel too; the dispatcher swallows that\n // copy while a claimed gesture is live.\n {\n spec: { kind: 'pinch' as const },\n opts: { params: { kind: 'pinch' } },\n },\n // Cmd+= → zoom in (also accepts Cmd+Shift+= which is Cmd++ on many keyboards)\n {\n spec: { kind: 'key', key: '=', mods: { mod: true, shift: 'optional' } },\n opts: { params: { kind: 'in' } },\n },\n {\n spec: { kind: 'key', key: '-', mods: { mod: true } },\n opts: { params: { kind: 'out' } },\n },\n {\n spec: { kind: 'key', key: '0', mods: { mod: true } },\n opts: { params: { kind: 'reset' } },\n },\n ];\n}\n\n/**\n * Build a `viewport.zoom` Action descriptor with a configurable wheel trigger\n * and scale clamp. The keyboard bindings (Cmd+=/-/0) and the two trackpad\n * pinch bindings are fixed. A wheel or pinch sample zooms about its focal\n * point, which `zoomAt` expects in canvas-local pixels.\n */\nexport function makeViewportZoomAction(\n opts: ViewportZoomOptions = {},\n): Action & { requires: string[] } {\n const clamp = { min: opts.min ?? DEFAULT_MIN_ZOOM, max: opts.max ?? DEFAULT_MAX_ZOOM };\n const axis = opts.axis ?? 'both';\n\n const rawAnimate: ViewportZoomAnimateOptions | null =\n opts.animate === true ? {} : (opts.animate || null);\n const { resetMs, ...animateRest } = rawAnimate ?? {};\n const tweenOpts: ViewAnimationOptions | null = rawAnimate ? animateRest : null;\n\n return {\n id: 'viewport.zoom',\n label: 'Zoom',\n group: 'viewport',\n variants: [\n { key: 'in', label: 'Zoom In', params: { kind: 'in' } },\n { key: 'out', label: 'Zoom Out', params: { kind: 'out' } },\n { key: 'reset', label: 'Reset Zoom', params: { kind: 'reset' } },\n ],\n defaultBinding: zoomBindings(opts.wheel),\n requires: ['view', 'debug'],\n invoker: {\n timing: 'immediate',\n run(deps, params) {\n const view = deps.view;\n if (!view) return;\n const debug = deps.debug;\n const current = view.get();\n // Report a zoom about `anchor` (canvas-local px) from `from` to `to`.\n const record = (from: View, to: View, anchor: { x: number; y: number }) => {\n if (!debug) return;\n const [wx, wy] = screenToWorld(anchor.x, anchor.y, viewToTransform(from));\n debug.recordViewport('zoom', from, to, { x: wx, y: wy });\n };\n const kind = params?.kind as string | undefined;\n\n const canAnimate = tweenOpts !== null && typeof view.animate === 'function';\n const stepTo = (target: View, ms?: number) => {\n if (!canAnimate) { view.set(target); return; }\n view.animate!(target, { ...tweenOpts!, ms: ms ?? tweenOpts!.ms });\n };\n // Successive presses compound off where the camera is heading, not off\n // whichever frame the tween happens to be on.\n const stepFrom = (): View => (canAnimate && view.animationTarget?.()) || current;\n\n const focal = () => ({\n x: (params?.clientX as number | undefined) ?? 0,\n y: (params?.clientY as number | undefined) ?? 0,\n });\n const scaleBy = (factor: number) => {\n if (!(factor > 0) || !Number.isFinite(factor)) return;\n const next = zoomAt(current, focal(), axisZoomFactor(factor, axis), clamp);\n view.set(next);\n record(current, next, focal());\n };\n\n switch (kind) {\n case 'wheel':\n scaleBy(wheelZoomFactor((params?.deltaY as number | undefined) ?? 0));\n break;\n case 'pinch':\n scaleBy((params?.scale as number | undefined) ?? Number.NaN);\n break;\n case 'in':\n case 'out': {\n const from = stepFrom();\n const step = axisZoomFactor(kind === 'in' ? KEY_STEP : 1 / KEY_STEP, axis);\n const target = zoomAt(from, keyAnchor(view), step, clamp);\n stepTo(target);\n record(from, target, keyAnchor(view));\n break;\n }\n case 'reset': {\n // A recenter that returns its target can be animated; one that\n // returns nothing dispatched the view itself and is already done.\n const target = view.recenter\n ? view.recenter()\n : { x: 0, y: 0, scale: { x: 1, y: 1 } };\n if (target) stepTo(target, resetMs);\n break;\n }\n default:\n // Unknown kind — no-op. Legacy bridge calls with params=undefined;\n // default to zoom-in as a sensible fallback.\n if (params === undefined) {\n view.set(zoomAt(current, { x: 0, y: 0 }, axisZoomFactor(KEY_STEP, axis), clamp));\n }\n break;\n }\n },\n },\n enabled: () => true,\n };\n}\n\n/**\n * Default `viewport.zoom` descriptor: Cmd/Ctrl+wheel zoom with the kit's\n * default 0.1–8 scale clamp. Equivalent to `makeViewportZoomAction()`.\n */\nexport const viewportZoomAction: Action & { requires: string[] } = makeViewportZoomAction();\n"],"mappings":"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"}
1
+ {"version":3,"file":"viewportZoom.js","names":[],"sources":["../../../../src/interactions/actions/defaults/viewportZoom.ts"],"sourcesContent":["import type { Action, BoundGesture } from '@weasel-js/routing';\nimport type { ViewApi } from '../depSchema';\nimport { axisZoomFactor, zoomAt, type ZoomAxis } from 'core/viewport/zoomAt';\nimport { wheelZoomFactor } from 'core/viewport/wheelHandler';\nimport { DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM } from 'core/viewport/zoomBounds';\nimport { viewToTransform, type View } from 'core/viewport/view';\nimport { screenToWorld } from 'core/viewport/viewTransform';\nimport type { ViewAnimationOptions } from 'core/viewport/useViewAnimation';\n\n// Multiplicative step for keyboard zoom (matches useKeyboardZoomTool default).\nconst KEY_STEP = 1.25;\n\n/** Keyboard-zoom anchor: the host center when the view dep can measure it,\n * else the canvas top-left origin. */\nfunction keyAnchor(view: ViewApi): { x: number; y: number } {\n const size = view.hostSize?.();\n return size ? { x: size.width / 2, y: size.height / 2 } : { x: 0, y: 0 };\n}\n\n/**\n * Tuning for the animated form of the discrete zoom steps.\n */\nexport interface ViewportZoomAnimateOptions extends ViewAnimationOptions {\n /** Duration for the Cmd+0 reset branch. Defaults to `ms`. */\n resetMs?: number;\n}\n\n/**\n * Tuning for {@link makeViewportZoomAction}.\n */\nexport interface ViewportZoomOptions {\n /**\n * Which wheel gesture triggers zoom.\n * - `'mod'` (default): Cmd/Ctrl+wheel — coexists with plain-wheel pan\n * (`viewport.wheelPan`).\n * - `'plain'`: bare wheel, no modifier. Pair with `viewport.pan: false`,\n * since plain wheel otherwise drives pan and the two would compete.\n */\n wheel?: 'plain' | 'mod';\n /** Lower clamp on the resulting view scale, forwarded to `zoomAt`. Default 0.1. */\n min?: number;\n /** Upper clamp on the resulting view scale, forwarded to `zoomAt`. Default 8. */\n max?: number;\n /** Hold wheel, trackpad-pinch, and key zoom to one axis; the other keeps its\n * scale and its translation. Default `'both'`. Cmd+0 still resets both. */\n axis?: ZoomAxis;\n /**\n * Glide the discrete steps (Cmd+=, Cmd+-, Cmd+0) instead of jumping.\n * `true` uses the kit defaults; an object tunes them. Wheel and pinch never\n * animate — their input already samples every frame. Requires a `view` dep\n * that implements `animate`; without one this is ignored.\n */\n animate?: boolean | ViewportZoomAnimateOptions;\n}\n\n/** The gestures `viewport.zoom` answers: the configurable wheel trigger, then\n * the fixed pinch and key bindings. */\nfunction zoomBindings(wheel: ViewportZoomOptions['wheel']): BoundGesture[] {\n // 'plain': bare wheel (omitting `mods` forbids any modifier — see the\n // matcher's strict modifier check). 'mod': Cmd/Ctrl+wheel.\n const wheelSpec =\n wheel === 'plain'\n ? { kind: 'wheel' as const }\n : { kind: 'wheel' as const, mods: { mod: true } };\n return [\n {\n spec: wheelSpec,\n opts: { id: 'viewport.zoom.wheel', params: { kind: 'wheel' } },\n },\n // Trackpad pinch, which the browser delivers as ctrl+wheel. Distinct from\n // the `mod` binding on mac (`mod` is metaKey there), so without its own\n // binding nothing claims it and the page zooms instead. Elsewhere it\n // duplicates `mod`, and `matchBest` picks a single winner.\n {\n spec: { kind: 'wheel' as const, mods: { ctrl: true } },\n opts: { id: 'viewport.zoom.trackpad', params: { kind: 'wheel' } },\n },\n // Safari's trackpad pinch, reported as a scale rather than a wheel. Safari\n // may send the same pinch as ctrl+wheel too; the dispatcher swallows that\n // copy while a claimed gesture is live.\n {\n spec: { kind: 'pinch' as const },\n opts: { id: 'viewport.zoom.pinch', params: { kind: 'pinch' } },\n },\n // Cmd+= → zoom in (also accepts Cmd+Shift+= which is Cmd++ on many keyboards)\n {\n spec: { kind: 'key', key: '=', mods: { mod: true, shift: 'optional' } },\n opts: { id: 'viewport.zoom.in', params: { kind: 'in' } },\n },\n {\n spec: { kind: 'key', key: '-', mods: { mod: true } },\n opts: { id: 'viewport.zoom.out', params: { kind: 'out' } },\n },\n {\n spec: { kind: 'key', key: '0', mods: { mod: true } },\n opts: { id: 'viewport.zoom.reset', params: { kind: 'reset' } },\n },\n ];\n}\n\n/**\n * Build a `viewport.zoom` Action descriptor with a configurable wheel trigger\n * and scale clamp. The keyboard bindings (Cmd+=/-/0) and the two trackpad\n * pinch bindings are fixed. A wheel or pinch sample zooms about its focal\n * point, which `zoomAt` expects in canvas-local pixels.\n */\nexport function makeViewportZoomAction(\n opts: ViewportZoomOptions = {},\n): Action & { requires: string[] } {\n const clamp = { min: opts.min ?? DEFAULT_MIN_ZOOM, max: opts.max ?? DEFAULT_MAX_ZOOM };\n const axis = opts.axis ?? 'both';\n\n const rawAnimate: ViewportZoomAnimateOptions | null =\n opts.animate === true ? {} : (opts.animate || null);\n const { resetMs, ...animateRest } = rawAnimate ?? {};\n const tweenOpts: ViewAnimationOptions | null = rawAnimate ? animateRest : null;\n\n return {\n id: 'viewport.zoom',\n label: 'Zoom',\n group: 'viewport',\n variants: [\n { key: 'in', label: 'Zoom In', params: { kind: 'in' } },\n { key: 'out', label: 'Zoom Out', params: { kind: 'out' } },\n { key: 'reset', label: 'Reset Zoom', params: { kind: 'reset' } },\n ],\n defaultBinding: zoomBindings(opts.wheel),\n requires: ['view', 'debug'],\n invoker: {\n timing: 'immediate',\n run(deps, params) {\n const view = deps.view;\n if (!view) return;\n const debug = deps.debug;\n const current = view.get();\n // Report a zoom about `anchor` (canvas-local px) from `from` to `to`.\n const record = (from: View, to: View, anchor: { x: number; y: number }) => {\n if (!debug) return;\n const [wx, wy] = screenToWorld(anchor.x, anchor.y, viewToTransform(from));\n debug.recordViewport('zoom', from, to, { x: wx, y: wy });\n };\n const kind = params?.kind as string | undefined;\n\n const canAnimate = tweenOpts !== null && typeof view.animate === 'function';\n const stepTo = (target: View, ms?: number) => {\n if (!canAnimate) { view.set(target); return; }\n view.animate!(target, { ...tweenOpts!, ms: ms ?? tweenOpts!.ms });\n };\n // Successive presses compound off where the camera is heading, not off\n // whichever frame the tween happens to be on.\n const stepFrom = (): View => (canAnimate && view.animationTarget?.()) || current;\n\n const focal = () => ({\n x: (params?.clientX as number | undefined) ?? 0,\n y: (params?.clientY as number | undefined) ?? 0,\n });\n const scaleBy = (factor: number) => {\n if (!(factor > 0) || !Number.isFinite(factor)) return;\n const next = zoomAt(current, focal(), axisZoomFactor(factor, axis), clamp);\n view.set(next);\n record(current, next, focal());\n };\n\n switch (kind) {\n case 'wheel':\n scaleBy(wheelZoomFactor((params?.deltaY as number | undefined) ?? 0));\n break;\n case 'pinch':\n scaleBy((params?.scale as number | undefined) ?? Number.NaN);\n break;\n case 'in':\n case 'out': {\n const from = stepFrom();\n const step = axisZoomFactor(kind === 'in' ? KEY_STEP : 1 / KEY_STEP, axis);\n const target = zoomAt(from, keyAnchor(view), step, clamp);\n stepTo(target);\n record(from, target, keyAnchor(view));\n break;\n }\n case 'reset': {\n // A recenter that returns its target can be animated; one that\n // returns nothing dispatched the view itself and is already done.\n const target = view.recenter\n ? view.recenter()\n : { x: 0, y: 0, scale: { x: 1, y: 1 } };\n if (target) stepTo(target, resetMs);\n break;\n }\n default:\n // Unknown kind — no-op. Legacy bridge calls with params=undefined;\n // default to zoom-in as a sensible fallback.\n if (params === undefined) {\n view.set(zoomAt(current, { x: 0, y: 0 }, axisZoomFactor(KEY_STEP, axis), clamp));\n }\n break;\n }\n },\n },\n enabled: () => true,\n };\n}\n\n/**\n * Default `viewport.zoom` descriptor: Cmd/Ctrl+wheel zoom with the kit's\n * default 0.1–8 scale clamp. Equivalent to `makeViewportZoomAction()`.\n */\nexport const viewportZoomAction: Action & { requires: string[] } = makeViewportZoomAction();\n"],"mappings":";;;;;;;AAUA,IAAM,WAAW;;;AAIjB,SAAS,UAAU,MAAyC;CAC1D,MAAM,OAAO,KAAK,WAAW;CAC7B,OAAO,OAAO;EAAE,GAAG,KAAK,QAAQ;EAAG,GAAG,KAAK,SAAS;CAAE,IAAI;EAAE,GAAG;EAAG,GAAG;CAAE;AACzE;;;AAwCA,SAAS,aAAa,OAAqD;CAOzE,OAAO;EACL;GACE,MALF,UAAU,UACN,EAAE,MAAM,QAAiB,IACzB;IAAE,MAAM;IAAkB,MAAM,EAAE,KAAK,KAAK;GAAE;GAIhD,MAAM;IAAE,IAAI;IAAuB,QAAQ,EAAE,MAAM,QAAQ;GAAE;EAC/D;EAKA;GACE,MAAM;IAAE,MAAM;IAAkB,MAAM,EAAE,MAAM,KAAK;GAAE;GACrD,MAAM;IAAE,IAAI;IAA0B,QAAQ,EAAE,MAAM,QAAQ;GAAE;EAClE;EAIA;GACE,MAAM,EAAE,MAAM,QAAiB;GAC/B,MAAM;IAAE,IAAI;IAAuB,QAAQ,EAAE,MAAM,QAAQ;GAAE;EAC/D;EAEA;GACE,MAAM;IAAE,MAAM;IAAO,KAAK;IAAK,MAAM;KAAE,KAAK;KAAM,OAAO;IAAW;GAAE;GACtE,MAAM;IAAE,IAAI;IAAoB,QAAQ,EAAE,MAAM,KAAK;GAAE;EACzD;EACA;GACE,MAAM;IAAE,MAAM;IAAO,KAAK;IAAK,MAAM,EAAE,KAAK,KAAK;GAAE;GACnD,MAAM;IAAE,IAAI;IAAqB,QAAQ,EAAE,MAAM,MAAM;GAAE;EAC3D;EACA;GACE,MAAM;IAAE,MAAM;IAAO,KAAK;IAAK,MAAM,EAAE,KAAK,KAAK;GAAE;GACnD,MAAM;IAAE,IAAI;IAAuB,QAAQ,EAAE,MAAM,QAAQ;GAAE;EAC/D;CACF;AACF;;;;;;;AAQA,SAAgB,uBACd,OAA4B,CAAC,GACI;CACjC,MAAM,QAAQ;EAAE,KAAK,KAAK,OAAO;EAAkB,KAAK,KAAK,OAAO;CAAiB;CACrF,MAAM,OAAO,KAAK,QAAQ;CAE1B,MAAM,aACJ,KAAK,YAAY,OAAO,CAAC,IAAK,KAAK,WAAW;CAChD,MAAM,EAAE,SAAS,GAAG,gBAAgB,cAAc,CAAC;CACnD,MAAM,YAAyC,aAAa,cAAc;CAE1E,OAAO;EACL,IAAI;EACJ,OAAO;EACP,OAAO;EACP,UAAU;GACR;IAAE,KAAK;IAAM,OAAO;IAAW,QAAQ,EAAE,MAAM,KAAK;GAAE;GACtD;IAAE,KAAK;IAAO,OAAO;IAAY,QAAQ,EAAE,MAAM,MAAM;GAAE;GACzD;IAAE,KAAK;IAAS,OAAO;IAAc,QAAQ,EAAE,MAAM,QAAQ;GAAE;EACjE;EACA,gBAAgB,aAAa,KAAK,KAAK;EACvC,UAAU,CAAC,QAAQ,OAAO;EAC1B,SAAS;GACP,QAAQ;GACR,IAAI,MAAM,QAAQ;IAChB,MAAM,OAAO,KAAK;IAClB,IAAI,CAAC,MAAM;IACX,MAAM,QAAQ,KAAK;IACnB,MAAM,UAAU,KAAK,IAAI;IAEzB,MAAM,UAAU,MAAY,IAAU,WAAqC;KACzE,IAAI,CAAC,OAAO;KACZ,MAAM,CAAC,IAAI,MAAM,cAAc,OAAO,GAAG,OAAO,GAAG,gBAAgB,IAAI,CAAC;KACxE,MAAM,eAAe,QAAQ,MAAM,IAAI;MAAE,GAAG;MAAI,GAAG;KAAG,CAAC;IACzD;IACA,MAAM,OAAO,QAAQ;IAErB,MAAM,aAAa,cAAc,QAAQ,OAAO,KAAK,YAAY;IACjE,MAAM,UAAU,QAAc,OAAgB;KAC5C,IAAI,CAAC,YAAY;MAAE,KAAK,IAAI,MAAM;MAAG;KAAQ;KAC7C,KAAK,QAAS,QAAQ;MAAE,GAAG;MAAY,IAAI,MAAM,UAAW;KAAG,CAAC;IAClE;IAGA,MAAM,iBAAwB,cAAc,KAAK,kBAAkB,KAAM;IAEzE,MAAM,eAAe;KACnB,GAAI,QAAQ,WAAkC;KAC9C,GAAI,QAAQ,WAAkC;IAChD;IACA,MAAM,WAAW,WAAmB;KAClC,IAAI,EAAE,SAAS,MAAM,CAAC,OAAO,SAAS,MAAM,GAAG;KAC/C,MAAM,OAAO,OAAO,SAAS,MAAM,GAAG,eAAe,QAAQ,IAAI,GAAG,KAAK;KACzE,KAAK,IAAI,IAAI;KACb,OAAO,SAAS,MAAM,MAAM,CAAC;IAC/B;IAEA,QAAQ,MAAR;KACE,KAAK;MACH,QAAQ,gBAAiB,QAAQ,UAAiC,CAAC,CAAC;MACpE;KACF,KAAK;MACH,QAAS,QAAQ,SAAgC,GAAU;MAC3D;KACF,KAAK;KACL,KAAK,OAAO;MACV,MAAM,OAAO,SAAS;MACtB,MAAM,OAAO,eAAe,SAAS,OAAO,WAAW,IAAI,UAAU,IAAI;MACzE,MAAM,SAAS,OAAO,MAAM,UAAU,IAAI,GAAG,MAAM,KAAK;MACxD,OAAO,MAAM;MACb,OAAO,MAAM,QAAQ,UAAU,IAAI,CAAC;MACpC;KACF;KACA,KAAK,SAAS;MAGZ,MAAM,SAAS,KAAK,WAChB,KAAK,SAAS,IACd;OAAE,GAAG;OAAG,GAAG;OAAG,OAAO;QAAE,GAAG;QAAG,GAAG;OAAE;MAAE;MACxC,IAAI,QAAQ,OAAO,QAAQ,OAAO;MAClC;KACF;KACA,SAGE,IAAI,WAAW,QACb,KAAK,IAAI,OAAO,SAAS;MAAE,GAAG;MAAG,GAAG;KAAE,GAAG,eAAe,UAAU,IAAI,GAAG,KAAK,CAAC;IAGrF;GACF;EACF;EACA,eAAe;CACjB;AACF;;;;;AAMA,IAAa,qBAAsD,uBAAuB"}
@@ -160,10 +160,13 @@ var editAnchorsAction = inPlane({
160
160
  id: "editAnchors",
161
161
  label: "Edit Anchors",
162
162
  tags: pathOperationTags("drag"),
163
- defaultBinding: {
164
- kind: "drag",
165
- target: { kindOf: isAnchorOrControl }
166
- },
163
+ defaultBinding: [{
164
+ spec: {
165
+ kind: "drag",
166
+ target: { kindOf: isAnchorOrControl }
167
+ },
168
+ opts: { id: "editAnchors.drag" }
169
+ }],
167
170
  eligible: { capability: "edits-anchors" },
168
171
  modifierRoles: EDIT_ANCHORS_ROLES,
169
172
  requires: [
@@ -1 +1 @@
1
- {"version":3,"file":"action.js","names":[],"sources":["../../../../src/interactions/actions/edit-anchors/action.ts"],"sourcesContent":["/**\n * `editAnchorsAction` — ongoing Action descriptor for editing polygon anchors.\n *\n * - **Press** selects. Bare press replaces the anchor selection; Shift\n * toggles the pressed anchor in or out of it. Pressing an anchor that\n * is already part of a multi-anchor selection leaves the selection\n * alone, so you can grab a set and move it.\n * - **Drag** then moves whatever the press left selected — one anchor,\n * or every selected anchor together. The pressed anchor snaps through\n * the `snap` dep's `point` session, to what the drag leaves still of the\n * path and to the canvas's targets; Shift holds it to the axes and\n * diagonals through its press, and Ctrl suspends the snap.\n * - **Dragging a control handle** moves just that handle, mirroring the\n * opposite one while the anchor is smooth. Alt breaks the mirror; Shift\n * holds the handle to the angle it had at the press, changing only its\n * length; Mod (Cmd or Ctrl) holds its length, changing only its angle;\n * both freeze it. The `editAnchors` dep's `handleLocks`, or else the\n * `handleLock` param ({@link EditAnchorsParams}), holds a lock with no\n * modifier. The lock in force draws its guide as a `polyline` overlay\n * of role `lock`: the line, or the circle round the anchor.\n * - **Path-edit extensions shape the drag** through their `drag`: anchors\n * that follow an anchor drag by a share of it, where a dragged handle\n * goes after the locks, the wheel mid-drag, and overlays of their own.\n * - **Every frame is finished** by the dep's `finishEdit`, so what a\n * path-edit extension keeps true holds while the drag runs.\n * - **Under a `pathSymmetry` dep**, on a path that is its own image under\n * some of its transforms, every image of what is dragged follows it.\n *\n * Selection lives on the `editAnchors` dep (flat anchor indices) rather\n * than in this handle's scratch, because the keyboard actions\n * (`nudgeAnchors`, `deleteAnchors`) and the overlay all need to read it\n * between gestures.\n *\n * Operational when the consumer wires the `editAnchors` dep, and\n * `buildAffordanceAt` with a `getAnchorState` thunk so the dispatcher\n * classifies anchor/control-handle hits as `anchor:N`, `controlIn:N`, or\n * `controlOut:N` affordances on pointerdown. On any other affordance `start`\n * returns `{}` so other bindings can handle the drag.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { BindingOpts, InvocationCtx, OngoingHandle, OngoingOverlay } from '@weasel-js/routing';\nimport type { EditAnchorsDep, SnapDep } from '../depSchema';\nimport type { SnapBegin, SnapMods, SnapSession } from '../snapDep';\nimport { inputSession } from 'features/snap/session2d';\nimport { SNAP_ROLES, snapMods } from 'features/snap/roles';\nimport { ActionDisabledReason, isAnchorOrControl, resolveParams, roleReader, type ModifierRoles } from '@weasel-js/routing';\nimport {\n anchorAt,\n editAnchorSetTracked,\n moveHandleSymmetric,\n moveHandleTo,\n pathAwayFromAnchors,\n translateAnchorsSymmetric,\n type AnchorSet,\n type AnchorSymmetry,\n type PenAnchor,\n type PointOrigin,\n} from '@weasel-js/geom';\nimport { pathToAnchors, type PolygonPath } from '@weasel-js/geom';\nimport { inPlane, editingLayer } from '../planeInput';\nimport { HANDLE_LOCK_ROLES, lockHandle, locksInForce, type HandlePress } from './handleDrag';\nimport { locksOf, type EditAnchorsParams } from './params';\nimport { finishPathEditFrame, pathEditPreview } from './preview';\nimport {\n claimWheel, dragWeights, endExtensionDrags, extensionOverlays, placeHandle, startExtensionDrags, translateWeighted,\n} from './extensionDrags';\nimport type { PathEditDrag } from 'features/paths/pathEditExtension';\nimport { pathEditToolActive } from '../defaults/pathEditTools';\nimport { editedPathSymmetries } from './symmetry';\nimport { pathOperationTags } from '../pathGestureTags';\n\n/**\n * Parse an anchor affordance kind — `'anchor:N'` | `'controlIn:N'` |\n * `'controlOut:N'`, where N is the flat anchor index — or `null` when the\n * affordance is not an anchor kind.\n */\nfunction parseAnchorAffordance(\n kind: string,\n): { anchorIndex: number; part: 'anchor' | 'controlIn' | 'controlOut' } | null {\n const m = kind.match(/^(anchor|controlIn|controlOut):(\\d+)$/);\n if (!m) return null;\n return {\n anchorIndex: parseInt(m[2], 10),\n part: m[1] as 'anchor' | 'controlIn' | 'controlOut',\n };\n}\n\n/**\n * Resolve what a press on `anchorIndex` should leave selected.\n *\n * Shift toggles. A bare press on an anchor that's already part of the\n * selection keeps the selection intact (so the drag moves the whole set);\n * a bare press anywhere else collapses to just that anchor.\n */\nexport function selectionAfterAnchorPress(\n current: ReadonlySet<number>,\n anchorIndex: number,\n additive: boolean,\n): Set<number> {\n if (additive) {\n const next = new Set(current);\n if (next.has(anchorIndex)) next.delete(anchorIndex);\n else next.add(anchorIndex);\n return next;\n }\n if (current.has(anchorIndex) && current.size > 1) return new Set(current);\n return new Set([anchorIndex]);\n}\n\ninterface EditAnchorsScratch {\n dep: EditAnchorsDep;\n id: string;\n anchorIndex: number;\n /** `'anchor'` drags on-curve points (and their handles) by a delta.\n * `'controlIn'` / `'controlOut'` drag one control point to an absolute\n * world coord. */\n part: 'anchor' | 'controlIn' | 'controlOut';\n /** Anchors this drag moves — the pressed one, or the whole anchor\n * selection when the press landed inside it. Flat indices. */\n dragging: readonly number[];\n /** World position of the pressed anchor at drag start; the drag delta is\n * measured from here rather than accumulated per move, so the geometry\n * is always derived from `originPose` in one step. */\n anchorOrigin: { x: number; y: number };\n /** The anchor and the dragged control handle at the press, for a handle drag. */\n handlePress: HandlePress | null;\n /** The guide the lock in force drew at the last move. */\n guide: OngoingOverlay | null;\n /** The path-edit extensions' shares of this drag. */\n drags: readonly PathEditDrag[];\n /** The last move, to redo when an extension claims the wheel. */\n lastMove: InvocationCtx | null;\n originPose: PolygonPath;\n currentPose: PolygonPath;\n /** The transforms `originPose` is its own image under, read at the press. */\n symmetries: readonly AnchorSymmetry[];\n /** Where each point of `currentPose` came from in `originPose`. */\n origins?: readonly (PointOrigin | null)[];\n /** Other nodes' world paths the finished edit moves with it. */\n others: readonly { id: string; path: PolygonPath }[];\n}\n\ntype Point = { x: number; y: number };\n\n/**\n * An anchor drag's snap: a `point` session, begun on the first move, for\n * where the pressed anchor goes. The edited path offers no targets but what\n * the drag leaves still of it, and Shift holds the anchor to the axes and\n * diagonals through its press.\n */\nfunction anchorDragSnap(\n snap: SnapDep | undefined,\n press: { id: string; origin: Point; originPose: PolygonPath; dragging: readonly number[] },\n): { at(world: Point, mods: SnapMods): Point; end(): void } {\n let session: SnapSession | null = null;\n return {\n at(world, mods) {\n if (!session) {\n const { path, anchors } = pathAwayFromAnchors(press.originPose, press.dragging);\n const req: SnapBegin = {\n gesture: 'point', press: press.origin, ids: [press.id],\n also: { paths: [path], anchors }, constrain: 'axis',\n };\n session = snap?.begin(req) ?? inputSession(req);\n }\n return session.at(world, mods).point;\n },\n end() {\n session?.end();\n session = null;\n },\n };\n}\n\nconst paramsOf = (opts: BindingOpts | undefined): EditAnchorsParams =>\n (resolveParams(opts?.params) ?? {}) as EditAnchorsParams;\n\n/** Take `next` as the frame to show, finished by the dep. A frame it cannot\n * finish is dropped, so the path stays at the last one it could. */\nfunction track(scratch: EditAnchorsScratch, next: ReturnType<typeof editAnchorSetTracked>): void {\n if (!next) return;\n const { dep, id } = scratch;\n const done = finishPathEditFrame(dep, id, next.path, next.origins);\n if (!done) return;\n scratch.currentPose = done.path as PolygonPath;\n scratch.others = done.others as EditAnchorsScratch['others'];\n scratch.origins = next.origins;\n}\n\n/** The anchor and the handle a press on `part` grabbed; null for an anchor press. */\nfunction handlePressOf(\n origin: PenAnchor,\n part: EditAnchorsScratch['part'],\n): HandlePress | null {\n const handle = part === 'controlIn' ? origin.inHandle : part === 'controlOut' ? origin.outHandle : undefined;\n return handle ? { anchor: { x: origin.x, y: origin.y }, handle: { ...handle } } : null;\n}\n\n/** Move the dragged anchors to follow `to`, and the anchors the extensions\n * weight by their shares. A weighted drag moves no symmetric images. */\nfunction moveAnchors(scratch: EditAnchorsScratch, to: Point): void {\n if (scratch.dragging.length === 0) return;\n const dx = to.x - scratch.anchorOrigin.x;\n const dy = to.y - scratch.anchorOrigin.y;\n const weights = dragWeights(scratch.drags, scratch.dragging);\n track(scratch, editAnchorSetTracked(scratch.originPose, (set) => {\n if (weights) translateWeighted(set, weights, dx, dy);\n else translateAnchorsSymmetric(set, scratch.dragging, dx, dy, scratch.symmetries);\n }));\n}\n\n/** The modifiers `editAnchors` reads while it runs: Shift on the press adds\n * to the anchor selection, Alt breaks a smooth anchor's mirror, the handle\n * locks, and Shift holds an anchor drag to the axes and diagonals. An anchor\n * drag also reads `snap.suspend` at its default key, Ctrl, which is not\n * declared here because on a handle Ctrl holds the length. */\nexport const EDIT_ANCHORS_ROLES = {\n 'selection.add': { label: 'Add to the anchor selection', keys: ['shift'] },\n 'mirror.break': { label: \"Break the handles' mirror\", keys: ['alt'] },\n ...HANDLE_LOCK_ROLES,\n 'snap.constrain': SNAP_ROLES['snap.constrain'],\n} as const satisfies ModifierRoles;\n\nconst roleHeld = roleReader(EDIT_ANCHORS_ROLES);\n\n/** Move the dragged handle toward the pointer, under the locks in force, then\n * wherever the extensions place it. */\nfunction moveHandle(scratch: EditAnchorsScratch, ctx: InvocationCtx, params: EditAnchorsParams): void {\n const side = scratch.part === 'controlIn' ? 'in' : 'out';\n // Modifiers and held locks are read live, so either can change mid-drag.\n const held = (ctx.deps.editAnchors ?? scratch.dep).handleLocks ?? locksOf(params.handleLock);\n const locks = locksInForce(held, ctx);\n const locked = scratch.handlePress ? lockHandle(scratch.handlePress, ctx.world, locks) : { to: ctx.world, guide: null };\n scratch.guide = locked.guide;\n const to = placeHandle(scratch.drags, locked.to, { locks });\n const alt = roleHeld(ctx, 'mirror.break');\n const move = scratch.symmetries.length > 0\n ? (set: AnchorSet) => moveHandleSymmetric(set, scratch.anchorIndex, side, to.x, to.y, alt, scratch.symmetries)\n : (set: AnchorSet) => moveHandleTo(set, scratch.anchorIndex, side, to.x, to.y, alt);\n track(scratch, editAnchorSetTracked(scratch.originPose, move));\n}\n\n/**\n * What a press drags, the selection updated first, before any move: a handle\n * press drags its anchor's handle; an anchor press selects as\n * {@link selectionAfterAnchorPress} says and drags what it left selected —\n * nothing when Shift took the pressed anchor out of it.\n */\nfunction draggedByPress(\n dep: EditAnchorsDep,\n press: { anchorIndex: number; part: EditAnchorsScratch['part'] },\n shift: boolean,\n): readonly number[] {\n if (press.part !== 'anchor') return [press.anchorIndex];\n const next = selectionAfterAnchorPress(dep.selectedAnchors, press.anchorIndex, shift);\n dep.setSelectedAnchors(next);\n return next.has(press.anchorIndex) ? [...next] : [];\n}\n\n/** Static descriptor for the `editAnchors` Action. */\nexport const editAnchorsAction: Action & { requires: string[] } = inPlane({\n id: 'editAnchors',\n label: 'Edit Anchors',\n tags: pathOperationTags('drag'),\n defaultBinding: {\n kind: 'drag',\n // Only matches drags whose pointerdown hit an anchor or control handle.\n // Ties moveAction's `selected-body` on specificity and registers after\n // it, so move declines these presses in `start`.\n target: { kindOf: isAnchorOrControl },\n },\n eligible: { capability: 'edits-anchors' },\n modifierRoles: EDIT_ANCHORS_ROLES,\n requires: ['selection', 'editAnchors', 'pathSymmetry', 'snap'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, opts): OngoingHandle {\n const dep = ctx.deps.editAnchors;\n if (!dep) return {};\n\n const affordance = ctx.drag?.affordance;\n if (!affordance) {\n // No affordance hit → this drag doesn't target an anchor handle.\n // Let other bindings handle it (the handle returns {}, dispatcher moves on).\n return {};\n }\n\n const anchorInfo = parseAnchorAffordance(affordance.kind);\n if (!anchorInfo) {\n // Affordance is a non-anchor kind (e.g. resize handle, rotate handle).\n return {};\n }\n\n const { editingId } = dep;\n // World-coord polygon. For pose-as-polygon nodes this equals\n // node.pose; for data.path nodes it's pathInWorld-projected (translate +\n // rotation) so the anchor world coords match what the user clicked.\n const worldPath = dep.getEditablePath(editingId) as PolygonPath | undefined;\n if (!worldPath || worldPath.kind !== 'polygon') return {};\n\n // Decode once to validate the affordance index against the actual\n // geometry and to read the press origin. A stale affordance (the\n // path changed between pointerdown and now) fails here rather than\n // silently editing whichever anchor now holds that index.\n const decoded = pathToAnchors(worldPath) as AnchorSet;\n const origin = anchorAt(decoded, anchorInfo.anchorIndex);\n if (!origin) return {};\n if (anchorInfo.part === 'controlIn' && !origin.inHandle) return {};\n if (anchorInfo.part === 'controlOut' && !origin.outHandle) return {};\n\n let active = false;\n const preview = pathEditPreview(\n dep, editingId, () => (active ? { path: scratch.currentPose } : null), () => (active ? scratch.others : []),\n );\n if (!preview) return {};\n\n const dragging = draggedByPress(dep, anchorInfo, roleHeld(ctx, 'selection.add'));\n\n const scratch: EditAnchorsScratch = {\n dep,\n id: editingId,\n anchorIndex: anchorInfo.anchorIndex,\n part: anchorInfo.part,\n dragging,\n anchorOrigin: { x: origin.x, y: origin.y },\n handlePress: handlePressOf(origin, anchorInfo.part),\n guide: null,\n drags: startExtensionDrags(dep.extensions, {\n id: editingId,\n node: dep.getNodeShape(editingId),\n path: worldPath,\n anchors: decoded,\n part: anchorInfo.part,\n anchorIndex: anchorInfo.anchorIndex,\n dragging: anchorInfo.part === 'anchor' ? dragging : [],\n scale: ctx.deps.view?.get().scale,\n ...(ctx.deps.snap ? { snap: ctx.deps.snap } : {}),\n }),\n lastMove: null,\n originPose: worldPath,\n currentPose: worldPath,\n symmetries: editedPathSymmetries(ctx.deps.pathSymmetry, editingId, decoded),\n others: [],\n };\n const snapAnchor = anchorDragSnap(ctx.deps.snap, {\n id: editingId, origin: scratch.anchorOrigin, originPose: worldPath, dragging,\n });\n\n const move = (moveCtx: InvocationCtx): void => {\n scratch.lastMove = moveCtx;\n if (scratch.part !== 'anchor') return moveHandle(scratch, moveCtx, paramsOf(opts));\n moveAnchors(scratch, snapAnchor.at(moveCtx.world, snapMods(moveCtx)));\n };\n\n return {\n kind: 'edit-anchors',\n onMove(moveCtx: InvocationCtx): void {\n move(moveCtx);\n active = true;\n },\n onWheel(wheelCtx: InvocationCtx): boolean {\n if (!wheelCtx.wheel || !claimWheel(scratch.drags, wheelCtx.wheel)) return false;\n if (scratch.lastMove) move(scratch.lastMove);\n return true;\n },\n overlay(): readonly OngoingOverlay[] | null {\n if (!active) return null;\n const extra = scratch.drags.length > 0\n ? extensionOverlays(scratch.drags, pathToAnchors(scratch.currentPose) as AnchorSet)\n : [];\n const all = scratch.guide ? [scratch.guide, ...extra] : extra;\n return all.length > 0 ? all : null;\n },\n onEnd(_endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n active = false;\n try {\n if (reason === 'cancel' || scratch.originPose === scratch.currentPose) return;\n scratch.dep.applyEdit(scratch.id, scratch.currentPose, 'Edit anchors', scratch.origins);\n } finally {\n snapAnchor.end();\n endExtensionDrags(scratch.drags);\n }\n },\n previewIds: preview.previewIds,\n previewPose: preview.previewPose,\n previewData: preview.previewData,\n };\n },\n },\n // A path-edit tool has the drag.\n enabled: (deps) => (pathEditToolActive(deps) ? ActionDisabledReason.NotApplicable : true),\n}, editingLayer);\n"],"mappings":"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"}
1
+ {"version":3,"file":"action.js","names":[],"sources":["../../../../src/interactions/actions/edit-anchors/action.ts"],"sourcesContent":["/**\n * `editAnchorsAction` — ongoing Action descriptor for editing polygon anchors.\n *\n * - **Press** selects. Bare press replaces the anchor selection; Shift\n * toggles the pressed anchor in or out of it. Pressing an anchor that\n * is already part of a multi-anchor selection leaves the selection\n * alone, so you can grab a set and move it.\n * - **Drag** then moves whatever the press left selected — one anchor,\n * or every selected anchor together. The pressed anchor snaps through\n * the `snap` dep's `point` session, to what the drag leaves still of the\n * path and to the canvas's targets; Shift holds it to the axes and\n * diagonals through its press, and Ctrl suspends the snap.\n * - **Dragging a control handle** moves just that handle, mirroring the\n * opposite one while the anchor is smooth. Alt breaks the mirror; Shift\n * holds the handle to the angle it had at the press, changing only its\n * length; Mod (Cmd or Ctrl) holds its length, changing only its angle;\n * both freeze it. The `editAnchors` dep's `handleLocks`, or else the\n * `handleLock` param ({@link EditAnchorsParams}), holds a lock with no\n * modifier. The lock in force draws its guide as a `polyline` overlay\n * of role `lock`: the line, or the circle round the anchor.\n * - **Path-edit extensions shape the drag** through their `drag`: anchors\n * that follow an anchor drag by a share of it, where a dragged handle\n * goes after the locks, the wheel mid-drag, and overlays of their own.\n * - **Every frame is finished** by the dep's `finishEdit`, so what a\n * path-edit extension keeps true holds while the drag runs.\n * - **Under a `pathSymmetry` dep**, on a path that is its own image under\n * some of its transforms, every image of what is dragged follows it.\n *\n * Selection lives on the `editAnchors` dep (flat anchor indices) rather\n * than in this handle's scratch, because the keyboard actions\n * (`nudgeAnchors`, `deleteAnchors`) and the overlay all need to read it\n * between gestures.\n *\n * Operational when the consumer wires the `editAnchors` dep, and\n * `buildAffordanceAt` with a `getAnchorState` thunk so the dispatcher\n * classifies anchor/control-handle hits as `anchor:N`, `controlIn:N`, or\n * `controlOut:N` affordances on pointerdown. On any other affordance `start`\n * returns `{}` so other bindings can handle the drag.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { BindingOpts, InvocationCtx, OngoingHandle, OngoingOverlay } from '@weasel-js/routing';\nimport type { EditAnchorsDep, SnapDep } from '../depSchema';\nimport type { SnapBegin, SnapMods, SnapSession } from '../snapDep';\nimport { inputSession } from 'features/snap/session2d';\nimport { SNAP_ROLES, snapMods } from 'features/snap/roles';\nimport { ActionDisabledReason, isAnchorOrControl, resolveParams, roleReader, type ModifierRoles } from '@weasel-js/routing';\nimport {\n anchorAt,\n editAnchorSetTracked,\n moveHandleSymmetric,\n moveHandleTo,\n pathAwayFromAnchors,\n translateAnchorsSymmetric,\n type AnchorSet,\n type AnchorSymmetry,\n type PenAnchor,\n type PointOrigin,\n} from '@weasel-js/geom';\nimport { pathToAnchors, type PolygonPath } from '@weasel-js/geom';\nimport { inPlane, editingLayer } from '../planeInput';\nimport { HANDLE_LOCK_ROLES, lockHandle, locksInForce, type HandlePress } from './handleDrag';\nimport { locksOf, type EditAnchorsParams } from './params';\nimport { finishPathEditFrame, pathEditPreview } from './preview';\nimport {\n claimWheel, dragWeights, endExtensionDrags, extensionOverlays, placeHandle, startExtensionDrags, translateWeighted,\n} from './extensionDrags';\nimport type { PathEditDrag } from 'features/paths/pathEditExtension';\nimport { pathEditToolActive } from '../defaults/pathEditTools';\nimport { editedPathSymmetries } from './symmetry';\nimport { pathOperationTags } from '../pathGestureTags';\n\n/**\n * Parse an anchor affordance kind — `'anchor:N'` | `'controlIn:N'` |\n * `'controlOut:N'`, where N is the flat anchor index — or `null` when the\n * affordance is not an anchor kind.\n */\nfunction parseAnchorAffordance(\n kind: string,\n): { anchorIndex: number; part: 'anchor' | 'controlIn' | 'controlOut' } | null {\n const m = kind.match(/^(anchor|controlIn|controlOut):(\\d+)$/);\n if (!m) return null;\n return {\n anchorIndex: parseInt(m[2], 10),\n part: m[1] as 'anchor' | 'controlIn' | 'controlOut',\n };\n}\n\n/**\n * Resolve what a press on `anchorIndex` should leave selected.\n *\n * Shift toggles. A bare press on an anchor that's already part of the\n * selection keeps the selection intact (so the drag moves the whole set);\n * a bare press anywhere else collapses to just that anchor.\n */\nexport function selectionAfterAnchorPress(\n current: ReadonlySet<number>,\n anchorIndex: number,\n additive: boolean,\n): Set<number> {\n if (additive) {\n const next = new Set(current);\n if (next.has(anchorIndex)) next.delete(anchorIndex);\n else next.add(anchorIndex);\n return next;\n }\n if (current.has(anchorIndex) && current.size > 1) return new Set(current);\n return new Set([anchorIndex]);\n}\n\ninterface EditAnchorsScratch {\n dep: EditAnchorsDep;\n id: string;\n anchorIndex: number;\n /** `'anchor'` drags on-curve points (and their handles) by a delta.\n * `'controlIn'` / `'controlOut'` drag one control point to an absolute\n * world coord. */\n part: 'anchor' | 'controlIn' | 'controlOut';\n /** Anchors this drag moves — the pressed one, or the whole anchor\n * selection when the press landed inside it. Flat indices. */\n dragging: readonly number[];\n /** World position of the pressed anchor at drag start; the drag delta is\n * measured from here rather than accumulated per move, so the geometry\n * is always derived from `originPose` in one step. */\n anchorOrigin: { x: number; y: number };\n /** The anchor and the dragged control handle at the press, for a handle drag. */\n handlePress: HandlePress | null;\n /** The guide the lock in force drew at the last move. */\n guide: OngoingOverlay | null;\n /** The path-edit extensions' shares of this drag. */\n drags: readonly PathEditDrag[];\n /** The last move, to redo when an extension claims the wheel. */\n lastMove: InvocationCtx | null;\n originPose: PolygonPath;\n currentPose: PolygonPath;\n /** The transforms `originPose` is its own image under, read at the press. */\n symmetries: readonly AnchorSymmetry[];\n /** Where each point of `currentPose` came from in `originPose`. */\n origins?: readonly (PointOrigin | null)[];\n /** Other nodes' world paths the finished edit moves with it. */\n others: readonly { id: string; path: PolygonPath }[];\n}\n\ntype Point = { x: number; y: number };\n\n/**\n * An anchor drag's snap: a `point` session, begun on the first move, for\n * where the pressed anchor goes. The edited path offers no targets but what\n * the drag leaves still of it, and Shift holds the anchor to the axes and\n * diagonals through its press.\n */\nfunction anchorDragSnap(\n snap: SnapDep | undefined,\n press: { id: string; origin: Point; originPose: PolygonPath; dragging: readonly number[] },\n): { at(world: Point, mods: SnapMods): Point; end(): void } {\n let session: SnapSession | null = null;\n return {\n at(world, mods) {\n if (!session) {\n const { path, anchors } = pathAwayFromAnchors(press.originPose, press.dragging);\n const req: SnapBegin = {\n gesture: 'point', press: press.origin, ids: [press.id],\n also: { paths: [path], anchors }, constrain: 'axis',\n };\n session = snap?.begin(req) ?? inputSession(req);\n }\n return session.at(world, mods).point;\n },\n end() {\n session?.end();\n session = null;\n },\n };\n}\n\nconst paramsOf = (opts: BindingOpts | undefined): EditAnchorsParams =>\n (resolveParams(opts?.params) ?? {}) as EditAnchorsParams;\n\n/** Take `next` as the frame to show, finished by the dep. A frame it cannot\n * finish is dropped, so the path stays at the last one it could. */\nfunction track(scratch: EditAnchorsScratch, next: ReturnType<typeof editAnchorSetTracked>): void {\n if (!next) return;\n const { dep, id } = scratch;\n const done = finishPathEditFrame(dep, id, next.path, next.origins);\n if (!done) return;\n scratch.currentPose = done.path as PolygonPath;\n scratch.others = done.others as EditAnchorsScratch['others'];\n scratch.origins = next.origins;\n}\n\n/** The anchor and the handle a press on `part` grabbed; null for an anchor press. */\nfunction handlePressOf(\n origin: PenAnchor,\n part: EditAnchorsScratch['part'],\n): HandlePress | null {\n const handle = part === 'controlIn' ? origin.inHandle : part === 'controlOut' ? origin.outHandle : undefined;\n return handle ? { anchor: { x: origin.x, y: origin.y }, handle: { ...handle } } : null;\n}\n\n/** Move the dragged anchors to follow `to`, and the anchors the extensions\n * weight by their shares. A weighted drag moves no symmetric images. */\nfunction moveAnchors(scratch: EditAnchorsScratch, to: Point): void {\n if (scratch.dragging.length === 0) return;\n const dx = to.x - scratch.anchorOrigin.x;\n const dy = to.y - scratch.anchorOrigin.y;\n const weights = dragWeights(scratch.drags, scratch.dragging);\n track(scratch, editAnchorSetTracked(scratch.originPose, (set) => {\n if (weights) translateWeighted(set, weights, dx, dy);\n else translateAnchorsSymmetric(set, scratch.dragging, dx, dy, scratch.symmetries);\n }));\n}\n\n/** The modifiers `editAnchors` reads while it runs: Shift on the press adds\n * to the anchor selection, Alt breaks a smooth anchor's mirror, the handle\n * locks, and Shift holds an anchor drag to the axes and diagonals. An anchor\n * drag also reads `snap.suspend` at its default key, Ctrl, which is not\n * declared here because on a handle Ctrl holds the length. */\nexport const EDIT_ANCHORS_ROLES = {\n 'selection.add': { label: 'Add to the anchor selection', keys: ['shift'] },\n 'mirror.break': { label: \"Break the handles' mirror\", keys: ['alt'] },\n ...HANDLE_LOCK_ROLES,\n 'snap.constrain': SNAP_ROLES['snap.constrain'],\n} as const satisfies ModifierRoles;\n\nconst roleHeld = roleReader(EDIT_ANCHORS_ROLES);\n\n/** Move the dragged handle toward the pointer, under the locks in force, then\n * wherever the extensions place it. */\nfunction moveHandle(scratch: EditAnchorsScratch, ctx: InvocationCtx, params: EditAnchorsParams): void {\n const side = scratch.part === 'controlIn' ? 'in' : 'out';\n // Modifiers and held locks are read live, so either can change mid-drag.\n const held = (ctx.deps.editAnchors ?? scratch.dep).handleLocks ?? locksOf(params.handleLock);\n const locks = locksInForce(held, ctx);\n const locked = scratch.handlePress ? lockHandle(scratch.handlePress, ctx.world, locks) : { to: ctx.world, guide: null };\n scratch.guide = locked.guide;\n const to = placeHandle(scratch.drags, locked.to, { locks });\n const alt = roleHeld(ctx, 'mirror.break');\n const move = scratch.symmetries.length > 0\n ? (set: AnchorSet) => moveHandleSymmetric(set, scratch.anchorIndex, side, to.x, to.y, alt, scratch.symmetries)\n : (set: AnchorSet) => moveHandleTo(set, scratch.anchorIndex, side, to.x, to.y, alt);\n track(scratch, editAnchorSetTracked(scratch.originPose, move));\n}\n\n/**\n * What a press drags, the selection updated first, before any move: a handle\n * press drags its anchor's handle; an anchor press selects as\n * {@link selectionAfterAnchorPress} says and drags what it left selected —\n * nothing when Shift took the pressed anchor out of it.\n */\nfunction draggedByPress(\n dep: EditAnchorsDep,\n press: { anchorIndex: number; part: EditAnchorsScratch['part'] },\n shift: boolean,\n): readonly number[] {\n if (press.part !== 'anchor') return [press.anchorIndex];\n const next = selectionAfterAnchorPress(dep.selectedAnchors, press.anchorIndex, shift);\n dep.setSelectedAnchors(next);\n return next.has(press.anchorIndex) ? [...next] : [];\n}\n\n/** Static descriptor for the `editAnchors` Action. */\nexport const editAnchorsAction: Action & { requires: string[] } = inPlane({\n id: 'editAnchors',\n label: 'Edit Anchors',\n tags: pathOperationTags('drag'),\n defaultBinding: [{\n spec: {\n kind: 'drag',\n // Only matches drags whose pointerdown hit an anchor or control handle.\n // Ties moveAction's `selected-body` on specificity and registers after\n // it, so move declines these presses in `start`.\n target: { kindOf: isAnchorOrControl },\n },\n opts: { id: 'editAnchors.drag' },\n }],\n eligible: { capability: 'edits-anchors' },\n modifierRoles: EDIT_ANCHORS_ROLES,\n requires: ['selection', 'editAnchors', 'pathSymmetry', 'snap'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, opts): OngoingHandle {\n const dep = ctx.deps.editAnchors;\n if (!dep) return {};\n\n const affordance = ctx.drag?.affordance;\n if (!affordance) {\n // No affordance hit → this drag doesn't target an anchor handle.\n // Let other bindings handle it (the handle returns {}, dispatcher moves on).\n return {};\n }\n\n const anchorInfo = parseAnchorAffordance(affordance.kind);\n if (!anchorInfo) {\n // Affordance is a non-anchor kind (e.g. resize handle, rotate handle).\n return {};\n }\n\n const { editingId } = dep;\n // World-coord polygon. For pose-as-polygon nodes this equals\n // node.pose; for data.path nodes it's pathInWorld-projected (translate +\n // rotation) so the anchor world coords match what the user clicked.\n const worldPath = dep.getEditablePath(editingId) as PolygonPath | undefined;\n if (!worldPath || worldPath.kind !== 'polygon') return {};\n\n // Decode once to validate the affordance index against the actual\n // geometry and to read the press origin. A stale affordance (the\n // path changed between pointerdown and now) fails here rather than\n // silently editing whichever anchor now holds that index.\n const decoded = pathToAnchors(worldPath) as AnchorSet;\n const origin = anchorAt(decoded, anchorInfo.anchorIndex);\n if (!origin) return {};\n if (anchorInfo.part === 'controlIn' && !origin.inHandle) return {};\n if (anchorInfo.part === 'controlOut' && !origin.outHandle) return {};\n\n let active = false;\n const preview = pathEditPreview(\n dep, editingId, () => (active ? { path: scratch.currentPose } : null), () => (active ? scratch.others : []),\n );\n if (!preview) return {};\n\n const dragging = draggedByPress(dep, anchorInfo, roleHeld(ctx, 'selection.add'));\n\n const scratch: EditAnchorsScratch = {\n dep,\n id: editingId,\n anchorIndex: anchorInfo.anchorIndex,\n part: anchorInfo.part,\n dragging,\n anchorOrigin: { x: origin.x, y: origin.y },\n handlePress: handlePressOf(origin, anchorInfo.part),\n guide: null,\n drags: startExtensionDrags(dep.extensions, {\n id: editingId,\n node: dep.getNodeShape(editingId),\n path: worldPath,\n anchors: decoded,\n part: anchorInfo.part,\n anchorIndex: anchorInfo.anchorIndex,\n dragging: anchorInfo.part === 'anchor' ? dragging : [],\n scale: ctx.deps.view?.get().scale,\n ...(ctx.deps.snap ? { snap: ctx.deps.snap } : {}),\n }),\n lastMove: null,\n originPose: worldPath,\n currentPose: worldPath,\n symmetries: editedPathSymmetries(ctx.deps.pathSymmetry, editingId, decoded),\n others: [],\n };\n const snapAnchor = anchorDragSnap(ctx.deps.snap, {\n id: editingId, origin: scratch.anchorOrigin, originPose: worldPath, dragging,\n });\n\n const move = (moveCtx: InvocationCtx): void => {\n scratch.lastMove = moveCtx;\n if (scratch.part !== 'anchor') return moveHandle(scratch, moveCtx, paramsOf(opts));\n moveAnchors(scratch, snapAnchor.at(moveCtx.world, snapMods(moveCtx)));\n };\n\n return {\n kind: 'edit-anchors',\n onMove(moveCtx: InvocationCtx): void {\n move(moveCtx);\n active = true;\n },\n onWheel(wheelCtx: InvocationCtx): boolean {\n if (!wheelCtx.wheel || !claimWheel(scratch.drags, wheelCtx.wheel)) return false;\n if (scratch.lastMove) move(scratch.lastMove);\n return true;\n },\n overlay(): readonly OngoingOverlay[] | null {\n if (!active) return null;\n const extra = scratch.drags.length > 0\n ? extensionOverlays(scratch.drags, pathToAnchors(scratch.currentPose) as AnchorSet)\n : [];\n const all = scratch.guide ? [scratch.guide, ...extra] : extra;\n return all.length > 0 ? all : null;\n },\n onEnd(_endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n active = false;\n try {\n if (reason === 'cancel' || scratch.originPose === scratch.currentPose) return;\n scratch.dep.applyEdit(scratch.id, scratch.currentPose, 'Edit anchors', scratch.origins);\n } finally {\n snapAnchor.end();\n endExtensionDrags(scratch.drags);\n }\n },\n previewIds: preview.previewIds,\n previewPose: preview.previewPose,\n previewData: preview.previewData,\n };\n },\n },\n // A path-edit tool has the drag.\n enabled: (deps) => (pathEditToolActive(deps) ? ActionDisabledReason.NotApplicable : true),\n}, editingLayer);\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA6EA,SAAS,sBACP,MAC6E;CAC7E,MAAM,IAAI,KAAK,MAAM,uCAAuC;CAC5D,IAAI,CAAC,GAAG,OAAO;CACf,OAAO;EACL,aAAa,SAAS,EAAE,IAAI,EAAE;EAC9B,MAAM,EAAE;CACV;AACF;;;;;;;;AASA,SAAgB,0BACd,SACA,aACA,UACa;CACb,IAAI,UAAU;EACZ,MAAM,OAAO,IAAI,IAAI,OAAO;EAC5B,IAAI,KAAK,IAAI,WAAW,GAAG,KAAK,OAAO,WAAW;OAC7C,KAAK,IAAI,WAAW;EACzB,OAAO;CACT;CACA,IAAI,QAAQ,IAAI,WAAW,KAAK,QAAQ,OAAO,GAAG,OAAO,IAAI,IAAI,OAAO;CACxE,uBAAO,IAAI,IAAI,CAAC,WAAW,CAAC;AAC9B;;;;;;;AA2CA,SAAS,eACP,MACA,OAC0D;CAC1D,IAAI,UAA8B;CAClC,OAAO;EACL,GAAG,OAAO,MAAM;GACd,IAAI,CAAC,SAAS;IACZ,MAAM,EAAE,MAAM,YAAY,oBAAoB,MAAM,YAAY,MAAM,QAAQ;IAC9E,MAAM,MAAiB;KACrB,SAAS;KAAS,OAAO,MAAM;KAAQ,KAAK,CAAC,MAAM,EAAE;KACrD,MAAM;MAAE,OAAO,CAAC,IAAI;MAAG;KAAQ;KAAG,WAAW;IAC/C;IACA,UAAU,MAAM,MAAM,GAAG,KAAK,aAAa,GAAG;GAChD;GACA,OAAO,QAAQ,GAAG,OAAO,IAAI,CAAC,CAAC;EACjC;EACA,MAAM;GACJ,SAAS,IAAI;GACb,UAAU;EACZ;CACF;AACF;AAEA,IAAM,YAAY,SACf,cAAc,MAAM,MAAM,KAAK,CAAC;;;AAInC,SAAS,MAAM,SAA6B,MAAqD;CAC/F,IAAI,CAAC,MAAM;CACX,MAAM,EAAE,KAAK,OAAO;CACpB,MAAM,OAAO,oBAAoB,KAAK,IAAI,KAAK,MAAM,KAAK,OAAO;CACjE,IAAI,CAAC,MAAM;CACX,QAAQ,cAAc,KAAK;CAC3B,QAAQ,SAAS,KAAK;CACtB,QAAQ,UAAU,KAAK;AACzB;;AAGA,SAAS,cACP,QACA,MACoB;CACpB,MAAM,SAAS,SAAS,cAAc,OAAO,WAAW,SAAS,eAAe,OAAO,YAAY;CACnG,OAAO,SAAS;EAAE,QAAQ;GAAE,GAAG,OAAO;GAAG,GAAG,OAAO;EAAE;EAAG,QAAQ,EAAE,GAAG,OAAO;CAAE,IAAI;AACpF;;;AAIA,SAAS,YAAY,SAA6B,IAAiB;CACjE,IAAI,QAAQ,SAAS,WAAW,GAAG;CACnC,MAAM,KAAK,GAAG,IAAI,QAAQ,aAAa;CACvC,MAAM,KAAK,GAAG,IAAI,QAAQ,aAAa;CACvC,MAAM,UAAU,YAAY,QAAQ,OAAO,QAAQ,QAAQ;CAC3D,MAAM,SAAS,qBAAqB,QAAQ,aAAa,QAAQ;EAC/D,IAAI,SAAS,kBAAkB,KAAK,SAAS,IAAI,EAAE;OAC9C,0BAA0B,KAAK,QAAQ,UAAU,IAAI,IAAI,QAAQ,UAAU;CAClF,CAAC,CAAC;AACJ;;;;;;AAOA,IAAa,qBAAqB;CAChC,iBAAiB;EAAE,OAAO;EAA+B,MAAM,CAAC,OAAO;CAAE;CACzE,gBAAgB;EAAE,OAAO;EAA6B,MAAM,CAAC,KAAK;CAAE;CACpE,GAAG;CACH,kBAAkB,WAAW;AAC/B;AAEA,IAAM,WAAW,WAAW,kBAAkB;;;AAI9C,SAAS,WAAW,SAA6B,KAAoB,QAAiC;CACpG,MAAM,OAAO,QAAQ,SAAS,cAAc,OAAO;CAEnD,MAAM,QAAQ,IAAI,KAAK,eAAe,QAAQ,IAAA,CAAK,eAAe,QAAQ,OAAO,UAAU;CAC3F,MAAM,QAAQ,aAAa,MAAM,GAAG;CACpC,MAAM,SAAS,QAAQ,cAAc,WAAW,QAAQ,aAAa,IAAI,OAAO,KAAK,IAAI;EAAE,IAAI,IAAI;EAAO,OAAO;CAAK;CACtH,QAAQ,QAAQ,OAAO;CACvB,MAAM,KAAK,YAAY,QAAQ,OAAO,OAAO,IAAI,EAAE,MAAM,CAAC;CAC1D,MAAM,MAAM,SAAS,KAAK,cAAc;CACxC,MAAM,OAAO,QAAQ,WAAW,SAAS,KACpC,QAAmB,oBAAoB,KAAK,QAAQ,aAAa,MAAM,GAAG,GAAG,GAAG,GAAG,KAAK,QAAQ,UAAU,KAC1G,QAAmB,aAAa,KAAK,QAAQ,aAAa,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG;CACpF,MAAM,SAAS,qBAAqB,QAAQ,YAAY,IAAI,CAAC;AAC/D;;;;;;;AAQA,SAAS,eACP,KACA,OACA,OACmB;CACnB,IAAI,MAAM,SAAS,UAAU,OAAO,CAAC,MAAM,WAAW;CACtD,MAAM,OAAO,0BAA0B,IAAI,iBAAiB,MAAM,aAAa,KAAK;CACpF,IAAI,mBAAmB,IAAI;CAC3B,OAAO,KAAK,IAAI,MAAM,WAAW,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC;AACpD;;AAGA,IAAa,oBAAqD,QAAQ;CACxE,IAAI;CACJ,OAAO;CACP,MAAM,kBAAkB,MAAM;CAC9B,gBAAgB,CAAC;EACf,MAAM;GACJ,MAAM;GAIN,QAAQ,EAAE,QAAQ,kBAAkB;EACtC;EACA,MAAM,EAAE,IAAI,mBAAmB;CACjC,CAAC;CACD,UAAU,EAAE,YAAY,gBAAgB;CACxC,eAAe;CACf,UAAU;EAAC;EAAa;EAAe;EAAgB;CAAM;CAC7D,SAAS;EACP,QAAQ;EACR,MAAM,KAAoB,MAAqB;GAC7C,MAAM,MAAM,IAAI,KAAK;GACrB,IAAI,CAAC,KAAK,OAAO,CAAC;GAElB,MAAM,aAAa,IAAI,MAAM;GAC7B,IAAI,CAAC,YAGH,OAAO,CAAC;GAGV,MAAM,aAAa,sBAAsB,WAAW,IAAI;GACxD,IAAI,CAAC,YAEH,OAAO,CAAC;GAGV,MAAM,EAAE,cAAc;GAItB,MAAM,YAAY,IAAI,gBAAgB,SAAS;GAC/C,IAAI,CAAC,aAAa,UAAU,SAAS,WAAW,OAAO,CAAC;GAMxD,MAAM,UAAU,cAAc,SAAS;GACvC,MAAM,SAAS,SAAS,SAAS,WAAW,WAAW;GACvD,IAAI,CAAC,QAAQ,OAAO,CAAC;GACrB,IAAI,WAAW,SAAS,eAAe,CAAC,OAAO,UAAU,OAAO,CAAC;GACjE,IAAI,WAAW,SAAS,gBAAgB,CAAC,OAAO,WAAW,OAAO,CAAC;GAEnE,IAAI,SAAS;GACb,MAAM,UAAU,gBACd,KAAK,iBAAkB,SAAS,EAAE,MAAM,QAAQ,YAAY,IAAI,YAAc,SAAS,QAAQ,SAAS,CAAC,CAC3G;GACA,IAAI,CAAC,SAAS,OAAO,CAAC;GAEtB,MAAM,WAAW,eAAe,KAAK,YAAY,SAAS,KAAK,eAAe,CAAC;GAE/E,MAAM,UAA8B;IAClC;IACA,IAAI;IACJ,aAAa,WAAW;IACxB,MAAM,WAAW;IACjB;IACA,cAAc;KAAE,GAAG,OAAO;KAAG,GAAG,OAAO;IAAE;IACzC,aAAa,cAAc,QAAQ,WAAW,IAAI;IAClD,OAAO;IACP,OAAO,oBAAoB,IAAI,YAAY;KACzC,IAAI;KACJ,MAAM,IAAI,aAAa,SAAS;KAChC,MAAM;KACN,SAAS;KACT,MAAM,WAAW;KACjB,aAAa,WAAW;KACxB,UAAU,WAAW,SAAS,WAAW,WAAW,CAAC;KACrD,OAAO,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC;KAC5B,GAAI,IAAI,KAAK,OAAO,EAAE,MAAM,IAAI,KAAK,KAAK,IAAI,CAAC;IACjD,CAAC;IACD,UAAU;IACV,YAAY;IACZ,aAAa;IACb,YAAY,qBAAqB,IAAI,KAAK,cAAc,WAAW,OAAO;IAC1E,QAAQ,CAAC;GACX;GACA,MAAM,aAAa,eAAe,IAAI,KAAK,MAAM;IAC/C,IAAI;IAAW,QAAQ,QAAQ;IAAc,YAAY;IAAW;GACtE,CAAC;GAED,MAAM,QAAQ,YAAiC;IAC7C,QAAQ,WAAW;IACnB,IAAI,QAAQ,SAAS,UAAU,OAAO,WAAW,SAAS,SAAS,SAAS,IAAI,CAAC;IACjF,YAAY,SAAS,WAAW,GAAG,QAAQ,OAAO,SAAS,OAAO,CAAC,CAAC;GACtE;GAEA,OAAO;IACL,MAAM;IACN,OAAO,SAA8B;KACnC,KAAK,OAAO;KACZ,SAAS;IACX;IACA,QAAQ,UAAkC;KACxC,IAAI,CAAC,SAAS,SAAS,CAAC,WAAW,QAAQ,OAAO,SAAS,KAAK,GAAG,OAAO;KAC1E,IAAI,QAAQ,UAAU,KAAK,QAAQ,QAAQ;KAC3C,OAAO;IACT;IACA,UAA4C;KAC1C,IAAI,CAAC,QAAQ,OAAO;KACpB,MAAM,QAAQ,QAAQ,MAAM,SAAS,IACjC,kBAAkB,QAAQ,OAAO,cAAc,QAAQ,WAAW,CAAc,IAChF,CAAC;KACL,MAAM,MAAM,QAAQ,QAAQ,CAAC,QAAQ,OAAO,GAAG,KAAK,IAAI;KACxD,OAAO,IAAI,SAAS,IAAI,MAAM;IAChC;IACA,MAAM,SAAwB,QAAmC;KAC/D,SAAS;KACT,IAAI;MACF,IAAI,WAAW,YAAY,QAAQ,eAAe,QAAQ,aAAa;MACvE,QAAQ,IAAI,UAAU,QAAQ,IAAI,QAAQ,aAAa,gBAAgB,QAAQ,OAAO;KACxF,UAAU;MACR,WAAW,IAAI;MACf,kBAAkB,QAAQ,KAAK;KACjC;IACF;IACA,YAAY,QAAQ;IACpB,aAAa,QAAQ;IACrB,aAAa,QAAQ;GACvB;EACF;CACF;CAEA,UAAU,SAAU,mBAAmB,IAAI,IAAI,qBAAqB,gBAAgB;AACtF,GAAG,YAAY"}