@weasel-js/core 1.8.1 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +9 -5
  3. package/dist/{builtins-CMEh6-Oc.d.ts → builtins-CQcvcLXh.d.ts} +98 -31
  4. package/dist/canvas/DrawCanvas.js +33 -33
  5. package/dist/canvas/DrawCanvas.js.map +1 -1
  6. package/dist/canvas/MinimapCanvas.js +29 -29
  7. package/dist/canvas/MinimapCanvas.js.map +1 -1
  8. package/dist/canvas/NodeShape.js +63 -54
  9. package/dist/canvas/NodeShape.js.map +1 -1
  10. package/dist/canvas/SceneCanvas/features.js +26 -5
  11. package/dist/canvas/SceneCanvas/features.js.map +1 -1
  12. package/dist/canvas/SceneCanvas/useSceneSelectTool.js +3 -2
  13. package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
  14. package/dist/canvas/SceneCanvas.js +526 -529
  15. package/dist/canvas/SceneCanvas.js.map +1 -1
  16. package/dist/canvas/SceneViewCanvas.js +35 -35
  17. package/dist/canvas/SceneViewCanvas.js.map +1 -1
  18. package/dist/canvas/defaultDrawOne.js +6 -1
  19. package/dist/canvas/defaultDrawOne.js.map +1 -1
  20. package/dist/canvas/pickWalk.js.map +1 -1
  21. package/dist/canvas/sceneAdapter.js.map +1 -1
  22. package/dist/canvas/useHitObserver.js +23 -0
  23. package/dist/canvas/useHitObserver.js.map +1 -0
  24. package/dist/canvas/useHostAnchor.js.map +1 -1
  25. package/dist/core/device/useDeviceProfile.js.map +1 -1
  26. package/dist/core/scene/derivedPath.js.map +1 -1
  27. package/dist/core/scene/effectivePose.js.map +1 -1
  28. package/dist/core/scene/kitRegistry.js.map +1 -1
  29. package/dist/core/scene/scene.js.map +1 -1
  30. package/dist/core/scene/types.js.map +1 -1
  31. package/dist/core/scene/useScene.js.map +1 -1
  32. package/dist/core/viewport/fitViewToBounds.js +3 -3
  33. package/dist/core/viewport/fitViewToBounds.js.map +1 -1
  34. package/dist/core/viewport/screenSlop.js +8 -2
  35. package/dist/core/viewport/screenSlop.js.map +1 -1
  36. package/dist/features/minimap/actions.js.map +1 -1
  37. package/dist/features/paths/markerCommands.js +95 -63
  38. package/dist/features/paths/markerCommands.js.map +1 -1
  39. package/dist/features/paths/tessellate/dash.js +52 -0
  40. package/dist/features/paths/tessellate/dash.js.map +1 -0
  41. package/dist/features/paths/tessellate/insets.js +32 -0
  42. package/dist/features/paths/tessellate/insets.js.map +1 -0
  43. package/dist/features/paths/tessellate/metric.js +72 -0
  44. package/dist/features/paths/tessellate/metric.js.map +1 -0
  45. package/dist/features/paths/tessellate/stroke.js +74 -134
  46. package/dist/features/paths/tessellate/stroke.js.map +1 -1
  47. package/dist/features/paths/tessellate/vertexWidths.js +32 -0
  48. package/dist/features/paths/tessellate/vertexWidths.js.map +1 -0
  49. package/dist/features/pointer/PointerContext.js +2 -2
  50. package/dist/features/pointer/PointerContext.js.map +1 -1
  51. package/dist/features/poseRun/usePoseRun.js.map +1 -1
  52. package/dist/features/text/useTextEdit.js +86 -84
  53. package/dist/features/text/useTextEdit.js.map +1 -1
  54. package/dist/index.d.ts +78 -39
  55. package/dist/index.js +148 -148
  56. package/dist/interactions/actions/defaults/align.js.map +1 -1
  57. package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
  58. package/dist/interactions/actions/defaults/areaSelect.js.map +1 -1
  59. package/dist/interactions/actions/defaults/booleans.js.map +1 -1
  60. package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
  61. package/dist/interactions/actions/defaults/clearSelection.js.map +1 -1
  62. package/dist/interactions/actions/defaults/clipboard.js.map +1 -1
  63. package/dist/interactions/actions/defaults/clone.js.map +1 -1
  64. package/dist/interactions/actions/defaults/createOutlines.js +1 -1
  65. package/dist/interactions/actions/defaults/createOutlines.js.map +1 -1
  66. package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
  67. package/dist/interactions/actions/defaults/delete.js.map +1 -1
  68. package/dist/interactions/actions/defaults/distribute.js.map +1 -1
  69. package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
  70. package/dist/interactions/actions/defaults/editAnchors.js.map +1 -1
  71. package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
  72. package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
  73. package/dist/interactions/actions/defaults/escape.js +1 -1
  74. package/dist/interactions/actions/defaults/escape.js.map +1 -1
  75. package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
  76. package/dist/interactions/actions/defaults/flip.js.map +1 -1
  77. package/dist/interactions/actions/defaults/group.js.map +1 -1
  78. package/dist/interactions/actions/defaults/ingest.js.map +1 -1
  79. package/dist/interactions/actions/defaults/insert.js.map +1 -1
  80. package/dist/interactions/actions/defaults/insertPathAnchor.js +1 -1
  81. package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
  82. package/dist/interactions/actions/defaults/lassoSelect.js.map +1 -1
  83. package/dist/interactions/actions/defaults/move.js.map +1 -1
  84. package/dist/interactions/actions/defaults/nudge.js.map +1 -1
  85. package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
  86. package/dist/interactions/actions/defaults/reorder.js.map +1 -1
  87. package/dist/interactions/actions/defaults/requiresSelection.js.map +1 -1
  88. package/dist/interactions/actions/defaults/resize.js.map +1 -1
  89. package/dist/interactions/actions/defaults/rotate.js.map +1 -1
  90. package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
  91. package/dist/interactions/actions/defaults/slice.js.map +1 -1
  92. package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
  93. package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
  94. package/dist/interactions/actions/defaults/undoRedo.js +1 -1
  95. package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
  96. package/dist/interactions/actions/defaults/viewportDragPan.js.map +1 -1
  97. package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
  98. package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
  99. package/dist/interactions/actions/planeInput.js.map +1 -1
  100. package/dist/interactions/gestures/shared/screenTolerance.js.map +1 -1
  101. package/dist/{registerProgram-ateHDGjw.d.ts → registerProgram-BMZRDgrh.d.ts} +4 -0
  102. package/dist/renderer/cache/markerCommandCache.js +46 -42
  103. package/dist/renderer/cache/markerCommandCache.js.map +1 -1
  104. package/dist/renderer/cache/outlineStrokeMeshCache.js +30 -25
  105. package/dist/renderer/cache/outlineStrokeMeshCache.js.map +1 -1
  106. package/dist/renderer/cache/strokeMeshCache.js +42 -31
  107. package/dist/renderer/cache/strokeMeshCache.js.map +1 -1
  108. package/dist/renderer/cullDrawCommands.js +32 -31
  109. package/dist/renderer/cullDrawCommands.js.map +1 -1
  110. package/dist/renderer/draw.js +315 -362
  111. package/dist/renderer/draw.js.map +1 -1
  112. package/dist/renderer/glyphOutlineMesh.js +67 -0
  113. package/dist/renderer/glyphOutlineMesh.js.map +1 -0
  114. package/dist/renderer/math/mat3.js +8 -3
  115. package/dist/renderer/math/mat3.js.map +1 -1
  116. package/dist/renderer/strokeLengths.js +50 -0
  117. package/dist/renderer/strokeLengths.js.map +1 -0
  118. package/dist/renderer.d.ts +2 -2
  119. package/dist/{test-seams-fYf6m5pu.d.ts → test-seams-CuIxe0xY.d.ts} +1 -1
  120. package/dist/test-seams.d.ts +2 -2
  121. package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
  122. package/dist/tools/builtin/pen/usePenTool.js +1 -1
  123. package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
  124. package/dist/tools/builtin/select/selectionContributions.js +43 -23
  125. package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
  126. package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
  127. package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
  128. package/dist/version.js +1 -1
  129. package/package.json +17 -17
@@ -1 +1 @@
1
- {"version":3,"file":"pinchZoom.js","names":["makePinchZoomAction","clamp","opts","DEFAULT_MIN_ZOOM","DEFAULT_MAX_ZOOM","view","ctx","multiTouch","scratch","debug","startView","ax","ay","screenToWorld","viewToTransform","mt","moveCtx","currentSpread","factor","prev","centroid","current","zoomed","zoomAt","next","pinchZoomAction"],"sources":["../../../../src/interactions/actions/defaults/pinchZoom.ts"],"sourcesContent":["/**\n * `pinchZoomAction` — ongoing Action descriptor for two-finger pinch zoom\n * and pan. The id and label stay `pinchZoom` / `Pinch Zoom`.\n *\n * ## Status: REAL\n *\n * Implements pinch-zoom via the multi-touch pointer stream:\n * - `start`: captures startSpread from `ctx.multiTouch.pinch.startSpread`\n * (or falls back to `ctx.multiTouch.spread`). Records the initial centroid.\n * - `onMove`: computes zoom factor = currentSpread / startSpread, anchors\n * `zoomAt` on the previous centroid, then translates by the centroid\n * delta. Together those pin the world point under the gesture midpoint\n * as the midpoint moves, so the same gesture zooms and pans.\n * - `onEnd`: no-op — zoom is already applied each frame.\n *\n * ## Dispatcher extensions required\n *\n * - `InvocationCtx.multiTouch.pinch`: the dispatcher\n * populates it on every multitouch move-pump event. `startSpread` is\n * captured once when the handle opens; `currentSpread` updates each frame.\n * - `InputEvent.multitouch.centroid / spread`:\n * `useGestureDispatcher` synthesizes updated geometry on `pointermove`\n * when a multitouch handle is in flight.\n *\n * ## Graceful degradation\n *\n * If `ctx.multiTouch.pinch` is absent (e.g. old dispatcher without these\n * extensions), `start` returns an empty handle — no-op, no crash.\n *\n * @see zoomAt — fixed-point zoom primitive from `core/viewport/zoomAt`.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle } from '@weasel-js/routing';\nimport type { ViewApi } from '../depSchema';\nimport { zoomAt } from 'core/viewport/zoomAt';\nimport { viewToTransform } from 'core/viewport/view';\nimport { screenToWorld } from 'core/viewport/viewTransform';\nimport type { DebugSink } from '@weasel-js/routing';\nimport { DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM } from 'core/viewport/zoomBounds';\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface PinchScratch {\n view: ViewApi;\n startSpread: number;\n centroid: { x: number; y: number };\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Tuning for {@link makePinchZoomAction}.\n */\nexport interface PinchZoomOptions {\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}\n\n/**\n * @experimental\n * Build a `viewport.pinchZoom` Action descriptor with a configurable scale\n * clamp. The binding (two-finger multitouch) is fixed; only the clamp varies.\n *\n * Requires dep-schema entries: `view`.\n *\n * The invoker is `ongoing`. Zoom is applied per-frame via `view.set(zoomAt(...))`.\n */\n// No `eligible` field → pinch zoom is always eligible across all modes.\nexport function makePinchZoomAction(\n opts: PinchZoomOptions = {},\n): Action & { requires: string[] } {\n const clamp = { min: opts.min ?? DEFAULT_MIN_ZOOM, max: opts.max ?? DEFAULT_MAX_ZOOM };\n return {\n id: 'viewport.pinchZoom',\n label: 'Pinch Zoom',\n group: 'viewport',\n defaultBinding: { kind: 'multiTouch', fingers: 2 },\n requires: ['view', 'debug'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, _opts): OngoingHandle {\n const view = ctx.deps.view as ViewApi | undefined;\n if (!view) return {};\n\n const multiTouch = ctx.multiTouch;\n if (!multiTouch) return {};\n\n // On start, spread comes from the initial multitouch event.\n // The pinch field may not be set yet on start (it's populated on moves).\n const startSpread = multiTouch.spread > 0 ? multiTouch.spread : 1;\n\n const scratch: PinchScratch = { view, startSpread, centroid: multiTouch.centroid };\n const debug = ctx.deps.debug as DebugSink | undefined;\n const startView = view.get();\n const [ax, ay] = screenToWorld(multiTouch.centroid.x, multiTouch.centroid.y, viewToTransform(startView));\n\n return {\n kind: 'pinch',\n onMove(moveCtx: InvocationCtx): void {\n const mt = moveCtx.multiTouch;\n if (!mt?.pinch) return;\n const { currentSpread, centroid } = mt.pinch;\n if (currentSpread <= 0 || scratch.startSpread <= 0) return;\n const factor = currentSpread / scratch.startSpread;\n // Update startSpread each frame so the factor is incremental.\n // This avoids drift accumulation across many frames.\n scratch.startSpread = currentSpread;\n const prev = scratch.centroid;\n scratch.centroid = centroid;\n const current = scratch.view.get();\n // Anchor the zoom on where the fingers *were*, then translate by how\n // far they travelled — together those pin the world point under the\n // centroid as it moves. Anchoring on the new centroid instead drops\n // the translation half, so a two-finger drag zooms without panning.\n const zoomed = zoomAt(current, prev, factor, clamp);\n const next = {\n scale: zoomed.scale,\n x: zoomed.x - (centroid.x - prev.x) / zoomed.scale.x,\n y: zoomed.y - (centroid.y - prev.y) / zoomed.scale.y,\n };\n scratch.view.set(next);\n debug?.recordViewport('zoom', startView, next, { x: ax, y: ay });\n },\n // onEnd: nothing to do — zoom was applied each frame.\n };\n },\n },\n enabled: () => true,\n };\n}\n\n/**\n * @experimental\n * Default `viewport.pinchZoom` descriptor: two-finger pinch with the kit's\n * default 0.1–8 scale clamp. Equivalent to `makePinchZoomAction()`.\n */\nexport const pinchZoomAction: Action & { requires: string[] } = makePinchZoomAction();\n"],"mappings":";;;;;AA4EA,SAAgBA,EACd,IAAyB,CAAC,GACO;CACjC,IAAMC,IAAQ;EAAE,KAAKC,EAAK,OAAOC;EAAkB,KAAKD,EAAK,OAAOE;CAAiB;CACrF,OAAO;EACL,IAAI;EACJ,OAAO;EACP,OAAO;EACP,gBAAgB;GAAE,MAAM;GAAc,SAAS;EAAE;EACjD,UAAU,CAAC,QAAQ,OAAO;EAC1B,SAAS;GACP,QAAQ;GACR,MAAM,GAAoB,GAAsB;IAC9C,IAAMC,IAAOC,EAAI,KAAK;IACtB,IAAI,CAACD,GAAM,OAAO,CAAC;IAEnB,IAAME,IAAaD,EAAI;IACvB,IAAI,CAACC,GAAY,OAAO,CAAC;IAMzB,IAAMC,IAAwB;KAAE;KAAM,aAFlBD,EAAW,SAAS,IAAIA,EAAW,SAAS;KAEb,UAAUA,EAAW;IAAS,GAC3EE,IAAQH,EAAI,KAAK,OACjBI,IAAYL,EAAK,IAAI,GACrB,CAACM,GAAIC,KAAMC,EAAcN,EAAW,SAAS,GAAGA,EAAW,SAAS,GAAGO,EAAgBJ,CAAS,CAAC;IAEvG,OAAO;KACL,MAAM;KACN,OAAO,GAA8B;MACnC,IAAMK,IAAKC,EAAQ;MACnB,IAAI,CAACD,GAAI,OAAO;MAChB,IAAM,EAAE,kBAAe,gBAAaA,EAAG;MACvC,IAAIE,KAAiB,KAAKT,EAAQ,eAAe,GAAG;MACpD,IAAMU,IAASD,IAAgBT,EAAQ;MAGvC,EAAQ,cAAcS;MACtB,IAAME,IAAOX,EAAQ;MACrB,EAAQ,WAAWY;MACnB,IAAMC,IAAUb,EAAQ,KAAK,IAAI,GAK3Bc,IAASC,EAAOF,GAASF,GAAMD,GAAQjB,CAAK,GAC5CuB,IAAO;OACX,OAAOF,EAAO;OACd,GAAGA,EAAO,KAAKF,EAAS,IAAID,EAAK,KAAKG,EAAO,MAAM;OACnD,GAAGA,EAAO,KAAKF,EAAS,IAAID,EAAK,KAAKG,EAAO,MAAM;MACrD;MAEA,AADAd,EAAQ,KAAK,IAAIgB,CAAI,GACrBf,GAAO,eAAe,QAAQC,GAAWc,GAAM;OAAE,GAAGb;OAAI,GAAGC;MAAG,CAAC;KACjE;IAEF;GACF;EACF;EACA,eAAe;CACjB;AACF;AAOA,IAAaa,IAAmDzB,EAAoB"}
1
+ {"version":3,"file":"pinchZoom.js","names":["makePinchZoomAction","clamp","opts","DEFAULT_MIN_ZOOM","DEFAULT_MAX_ZOOM","view","ctx","multiTouch","scratch","debug","startView","ax","ay","screenToWorld","viewToTransform","mt","moveCtx","currentSpread","factor","prev","centroid","current","zoomed","zoomAt","next","pinchZoomAction"],"sources":["../../../../src/interactions/actions/defaults/pinchZoom.ts"],"sourcesContent":["/**\n * `pinchZoomAction` — ongoing Action descriptor for two-finger pinch zoom\n * and pan. The id and label stay `pinchZoom` / `Pinch Zoom`.\n *\n * ## Status: REAL\n *\n * Implements pinch-zoom via the multi-touch pointer stream:\n * - `start`: captures startSpread from `ctx.multiTouch.pinch.startSpread`\n * (or falls back to `ctx.multiTouch.spread`). Records the initial centroid.\n * - `onMove`: computes zoom factor = currentSpread / startSpread, anchors\n * `zoomAt` on the previous centroid, then translates by the centroid\n * delta. Together those pin the world point under the gesture midpoint\n * as the midpoint moves, so the same gesture zooms and pans.\n * - `onEnd`: no-op — zoom is already applied each frame.\n *\n * ## Dispatcher extensions required\n *\n * - `InvocationCtx.multiTouch.pinch`: the dispatcher\n * populates it on every multitouch move-pump event. `startSpread` is\n * captured once when the handle opens; `currentSpread` updates each frame.\n * - `InputEvent.multitouch.centroid / spread`:\n * `useGestureDispatcher` synthesizes updated geometry on `pointermove`\n * when a multitouch handle is in flight.\n *\n * ## Graceful degradation\n *\n * If `ctx.multiTouch.pinch` is absent (e.g. old dispatcher without these\n * extensions), `start` returns an empty handle — no-op, no crash.\n *\n * @see zoomAt — fixed-point zoom primitive from `core/viewport/zoomAt`.\n */\n\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle } from '@weasel-js/routing';\nimport type { ViewApi } from '../depSchema';\nimport { zoomAt } from 'core/viewport/zoomAt';\nimport { viewToTransform } from 'core/viewport/view';\nimport { screenToWorld } from 'core/viewport/viewTransform';\nimport { DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM } from 'core/viewport/zoomBounds';\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface PinchScratch {\n view: ViewApi;\n startSpread: number;\n centroid: { x: number; y: number };\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Tuning for {@link makePinchZoomAction}.\n */\nexport interface PinchZoomOptions {\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}\n\n/**\n * @experimental\n * Build a `viewport.pinchZoom` Action descriptor with a configurable scale\n * clamp. The binding (two-finger multitouch) is fixed; only the clamp varies.\n *\n * Requires dep-schema entries: `view`.\n *\n * The invoker is `ongoing`. Zoom is applied per-frame via `view.set(zoomAt(...))`.\n */\n// No `eligible` field → pinch zoom is always eligible across all modes.\nexport function makePinchZoomAction(\n opts: PinchZoomOptions = {},\n): Action & { requires: string[] } {\n const clamp = { min: opts.min ?? DEFAULT_MIN_ZOOM, max: opts.max ?? DEFAULT_MAX_ZOOM };\n return {\n id: 'viewport.pinchZoom',\n label: 'Pinch Zoom',\n group: 'viewport',\n defaultBinding: { kind: 'multiTouch', fingers: 2 },\n requires: ['view', 'debug'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, _opts): OngoingHandle {\n const view = ctx.deps.view;\n if (!view) return {};\n\n const multiTouch = ctx.multiTouch;\n if (!multiTouch) return {};\n\n // On start, spread comes from the initial multitouch event.\n // The pinch field may not be set yet on start (it's populated on moves).\n const startSpread = multiTouch.spread > 0 ? multiTouch.spread : 1;\n\n const scratch: PinchScratch = { view, startSpread, centroid: multiTouch.centroid };\n const debug = ctx.deps.debug;\n const startView = view.get();\n const [ax, ay] = screenToWorld(multiTouch.centroid.x, multiTouch.centroid.y, viewToTransform(startView));\n\n return {\n kind: 'pinch',\n onMove(moveCtx: InvocationCtx): void {\n const mt = moveCtx.multiTouch;\n if (!mt?.pinch) return;\n const { currentSpread, centroid } = mt.pinch;\n if (currentSpread <= 0 || scratch.startSpread <= 0) return;\n const factor = currentSpread / scratch.startSpread;\n // Update startSpread each frame so the factor is incremental.\n // This avoids drift accumulation across many frames.\n scratch.startSpread = currentSpread;\n const prev = scratch.centroid;\n scratch.centroid = centroid;\n const current = scratch.view.get();\n // Anchor the zoom on where the fingers *were*, then translate by how\n // far they travelled — together those pin the world point under the\n // centroid as it moves. Anchoring on the new centroid instead drops\n // the translation half, so a two-finger drag zooms without panning.\n const zoomed = zoomAt(current, prev, factor, clamp);\n const next = {\n scale: zoomed.scale,\n x: zoomed.x - (centroid.x - prev.x) / zoomed.scale.x,\n y: zoomed.y - (centroid.y - prev.y) / zoomed.scale.y,\n };\n scratch.view.set(next);\n debug?.recordViewport('zoom', startView, next, { x: ax, y: ay });\n },\n // onEnd: nothing to do — zoom was applied each frame.\n };\n },\n },\n enabled: () => true,\n };\n}\n\n/**\n * @experimental\n * Default `viewport.pinchZoom` descriptor: two-finger pinch with the kit's\n * default 0.1–8 scale clamp. Equivalent to `makePinchZoomAction()`.\n */\nexport const pinchZoomAction: Action & { requires: string[] } = makePinchZoomAction();\n"],"mappings":";;;;;AA2EA,SAAgBA,EACd,IAAyB,CAAC,GACO;CACjC,IAAMC,IAAQ;EAAE,KAAKC,EAAK,OAAOC;EAAkB,KAAKD,EAAK,OAAOE;CAAiB;CACrF,OAAO;EACL,IAAI;EACJ,OAAO;EACP,OAAO;EACP,gBAAgB;GAAE,MAAM;GAAc,SAAS;EAAE;EACjD,UAAU,CAAC,QAAQ,OAAO;EAC1B,SAAS;GACP,QAAQ;GACR,MAAM,GAAoB,GAAsB;IAC9C,IAAMC,IAAOC,EAAI,KAAK;IACtB,IAAI,CAACD,GAAM,OAAO,CAAC;IAEnB,IAAME,IAAaD,EAAI;IACvB,IAAI,CAACC,GAAY,OAAO,CAAC;IAMzB,IAAMC,IAAwB;KAAE;KAAM,aAFlBD,EAAW,SAAS,IAAIA,EAAW,SAAS;KAEb,UAAUA,EAAW;IAAS,GAC3EE,IAAQH,EAAI,KAAK,OACjBI,IAAYL,EAAK,IAAI,GACrB,CAACM,GAAIC,KAAMC,EAAcN,EAAW,SAAS,GAAGA,EAAW,SAAS,GAAGO,EAAgBJ,CAAS,CAAC;IAEvG,OAAO;KACL,MAAM;KACN,OAAO,GAA8B;MACnC,IAAMK,IAAKC,EAAQ;MACnB,IAAI,CAACD,GAAI,OAAO;MAChB,IAAM,EAAE,kBAAe,gBAAaA,EAAG;MACvC,IAAIE,KAAiB,KAAKT,EAAQ,eAAe,GAAG;MACpD,IAAMU,IAASD,IAAgBT,EAAQ;MAGvC,EAAQ,cAAcS;MACtB,IAAME,IAAOX,EAAQ;MACrB,EAAQ,WAAWY;MACnB,IAAMC,IAAUb,EAAQ,KAAK,IAAI,GAK3Bc,IAASC,EAAOF,GAASF,GAAMD,GAAQjB,CAAK,GAC5CuB,IAAO;OACX,OAAOF,EAAO;OACd,GAAGA,EAAO,KAAKF,EAAS,IAAID,EAAK,KAAKG,EAAO,MAAM;OACnD,GAAGA,EAAO,KAAKF,EAAS,IAAID,EAAK,KAAKG,EAAO,MAAM;MACrD;MAEA,AADAd,EAAQ,KAAK,IAAIgB,CAAI,GACrBf,GAAO,eAAe,QAAQC,GAAWc,GAAM;OAAE,GAAGb;OAAI,GAAGC;MAAG,CAAC;KACjE;IAEF;GACF;EACF;EACA,eAAe;CACjB;AACF;AAOA,IAAaa,IAAmDzB,EAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"reorder.js","names":["reorderSelection","ids","selection","op","createReorderOp","direction","distance","applyOps","scene","defaultCommitAdapter","reorderForwardAction","createElement","BringForwardIcon","BringToFrontIcon","params","deps","requiresSelection","reorderBackwardAction","SendBackwardIcon","SendToBackIcon"],"sources":["../../../../src/interactions/actions/defaults/reorder.ts"],"sourcesContent":["import { createReorderOp } from 'core/ops/reorder';\nimport { createElement } from 'react';\nimport { BringForwardIcon, BringToFrontIcon, SendBackwardIcon, SendToBackIcon } from './icons/editIcons';\nimport type { Scene } from 'core/scene/types';\nimport type { Op } from 'core/ops/types';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport type { Action } from '@weasel-js/routing';\nimport { requiresSelection } from './requiresSelection';\n\n// ---------------------------------------------------------------------------\n// Shared helper\n// ---------------------------------------------------------------------------\n\ntype ReorderDistance = 'adjacent' | 'extreme';\n\n/**\n * Execute a directional reorder on the current selection. Translates\n * `direction` + `distance` into the four-way `ReorderDirection` understood by\n * `createReorderOp`, then routes the single reorder op through the consumer\n * `applyOps` commit hook (consumer history) when present, falling back to\n * `scene.applyBatch` against the shared `defaultCommitAdapter`. Either way the\n * whole reorder lands as one undo entry labelled `'Reorder'`.\n */\nfunction reorderSelection(\n selection: SelectionApi,\n scene: Scene<unknown, string, unknown>,\n direction: 'forward' | 'backward',\n distance: ReorderDistance,\n applyOps: ((ops: Op[], label: string) => void) | undefined,\n): void {\n const ids = selection.get() as string[];\n if (ids.length === 0) return;\n\n const reorderDir =\n direction === 'forward'\n ? distance === 'extreme' ? 'front' : 'forward'\n : distance === 'extreme' ? 'back' : 'backward';\n\n // `createReorderOp` partitions `ids` by their current parent and rewrites\n // each affected parent's child order via the adapter's `getChildren` /\n // `setChildOrder` (and `getParent`) — all carried by `defaultCommitAdapter`.\n const op = createReorderOp({ ids, direction: reorderDir, label: 'Reorder' });\n\n if (applyOps) applyOps([op], 'Reorder');\n else scene.applyBatch([op], 'Reorder', defaultCommitAdapter(scene, selection.adapterMethods));\n}\n\n// ---------------------------------------------------------------------------\n// Descriptors\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Parametric descriptor for forward reorder (`reorder.forward`).\n *\n * Collapses the old `reorder.forward` (adjacent) and `reorder.front`\n * (extreme) actions into one descriptor with two keybindings. The distance\n * param (`'adjacent'` | `'extreme'`) is carried in `opts.params.distance`\n * and forwarded to `invoker.run` by the dispatcher.\n */\nexport const reorderForwardAction: Action & { requires: string[] } = {\n id: 'reorder.forward',\n label: 'Bring Forward',\n group: 'reorder',\n variants: [\n { key: 'adjacent', label: 'Bring Forward', icon: createElement(BringForwardIcon), params: { distance: 'adjacent' } },\n { key: 'extreme', label: 'Bring to Front', icon: createElement(BringToFrontIcon), params: { distance: 'extreme' } },\n ],\n defaultBinding: [\n {\n // Modifier matching is strict, so Shift must be absent here — listing\n // the shifted `'}'` on this binding would be dead weight, since you\n // can't type it without the Shift this spec forbids.\n spec: { kind: 'key', key: [']'], mods: { mod: true } },\n opts: { params: { distance: 'adjacent' } },\n },\n {\n // Cmd+Shift+] → bring-to-front, the convention every drawing app uses.\n // With Shift held the US-layout key reports as '}', so both characters\n // are listed. Some browsers bind Cmd+Shift+[ / ] to tab switching and\n // may consume the event before the page sees it — hence the Cmd+Alt\n // binding below, which nothing reserves.\n spec: { kind: 'key', key: [']', '}'], mods: { mod: true, shift: true } },\n opts: { params: { distance: 'extreme' } },\n },\n {\n // Cmd+Alt+] → bring-to-front. On macOS Option+] emits '‘' (U+2018);\n // whether Cmd suppresses that transform is browser-dependent, so match\n // both the bracket and the Option-produced char (US layout). matchKey is\n // character-based, so non-US layouts may need a rebind.\n spec: { kind: 'key', key: [']', '‘'], mods: { mod: true, alt: true } },\n opts: { params: { distance: 'extreme' } },\n },\n ],\n eligible: { capability: 'edits-page' },\n requires: ['selection', 'scene', 'applyOps'],\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const distance = (params?.distance as ReorderDistance | undefined) ?? 'adjacent';\n const selection = deps.selection as SelectionApi | undefined;\n const scene = deps.scene as Scene<unknown, string, unknown> | undefined;\n const applyOps = deps.applyOps as ((ops: Op[], label: string) => void) | undefined;\n if (!selection || !scene) return;\n reorderSelection(selection, scene, 'forward', distance, applyOps);\n },\n },\n enabled: requiresSelection,\n};\n\n/**\n * @experimental\n * Parametric descriptor for backward reorder (`reorder.backward`).\n *\n * Collapses the old `reorder.backward` (adjacent) and `reorder.back`\n * (extreme) actions into one descriptor with two keybindings.\n */\nexport const reorderBackwardAction: Action & { requires: string[] } = {\n id: 'reorder.backward',\n label: 'Send Backward',\n group: 'reorder',\n variants: [\n { key: 'adjacent', label: 'Send Backward', icon: createElement(SendBackwardIcon), params: { distance: 'adjacent' } },\n { key: 'extreme', label: 'Send to Back', icon: createElement(SendToBackIcon), params: { distance: 'extreme' } },\n ],\n defaultBinding: [\n {\n spec: { kind: 'key', key: ['['], mods: { mod: true } },\n opts: { params: { distance: 'adjacent' } },\n },\n {\n // Cmd+Shift+[ → send-to-back. See the bring-to-front notes above.\n spec: { kind: 'key', key: ['[', '{'], mods: { mod: true, shift: true } },\n opts: { params: { distance: 'extreme' } },\n },\n {\n // Cmd+Alt+[ → send-to-back, the binding no browser reserves. On macOS\n // Option+[ emits '“' (U+201C), included alongside the bracket.\n spec: { kind: 'key', key: ['[', '“'], mods: { mod: true, alt: true } },\n opts: { params: { distance: 'extreme' } },\n },\n ],\n eligible: { capability: 'edits-page' },\n requires: ['selection', 'scene', 'applyOps'],\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const distance = (params?.distance as ReorderDistance | undefined) ?? 'adjacent';\n const selection = deps.selection as SelectionApi | undefined;\n const scene = deps.scene as Scene<unknown, string, unknown> | undefined;\n const applyOps = deps.applyOps as ((ops: Op[], label: string) => void) | undefined;\n if (!selection || !scene) return;\n reorderSelection(selection, scene, 'backward', distance, applyOps);\n },\n },\n enabled: requiresSelection,\n};\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"reorder.js","names":["reorderSelection","ids","selection","op","createReorderOp","direction","distance","applyOps","scene","defaultCommitAdapter","reorderForwardAction","createElement","BringForwardIcon","BringToFrontIcon","params","deps","requiresSelection","reorderBackwardAction","SendBackwardIcon","SendToBackIcon"],"sources":["../../../../src/interactions/actions/defaults/reorder.ts"],"sourcesContent":["import { createReorderOp } from 'core/ops/reorder';\nimport { createElement } from 'react';\nimport { BringForwardIcon, BringToFrontIcon, SendBackwardIcon, SendToBackIcon } from './icons/editIcons';\nimport type { Scene } from 'core/scene/types';\nimport type { Op } from 'core/ops/types';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport type { Action } from '@weasel-js/routing';\nimport { requiresSelection } from './requiresSelection';\n\n// ---------------------------------------------------------------------------\n// Shared helper\n// ---------------------------------------------------------------------------\n\ntype ReorderDistance = 'adjacent' | 'extreme';\n\n/**\n * Execute a directional reorder on the current selection. Translates\n * `direction` + `distance` into the four-way `ReorderDirection` understood by\n * `createReorderOp`, then routes the single reorder op through the consumer\n * `applyOps` commit hook (consumer history) when present, falling back to\n * `scene.applyBatch` against the shared `defaultCommitAdapter`. Either way the\n * whole reorder lands as one undo entry labelled `'Reorder'`.\n */\nfunction reorderSelection(\n selection: SelectionApi,\n scene: Scene<unknown, string, unknown>,\n direction: 'forward' | 'backward',\n distance: ReorderDistance,\n applyOps: ((ops: Op[], label: string) => void) | undefined,\n): void {\n const ids = selection.get() as string[];\n if (ids.length === 0) return;\n\n const reorderDir =\n direction === 'forward'\n ? distance === 'extreme' ? 'front' : 'forward'\n : distance === 'extreme' ? 'back' : 'backward';\n\n // `createReorderOp` partitions `ids` by their current parent and rewrites\n // each affected parent's child order via the adapter's `getChildren` /\n // `setChildOrder` (and `getParent`) — all carried by `defaultCommitAdapter`.\n const op = createReorderOp({ ids, direction: reorderDir, label: 'Reorder' });\n\n if (applyOps) applyOps([op], 'Reorder');\n else scene.applyBatch([op], 'Reorder', defaultCommitAdapter(scene, selection.adapterMethods));\n}\n\n// ---------------------------------------------------------------------------\n// Descriptors\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Parametric descriptor for forward reorder (`reorder.forward`).\n *\n * Collapses the old `reorder.forward` (adjacent) and `reorder.front`\n * (extreme) actions into one descriptor with two keybindings. The distance\n * param (`'adjacent'` | `'extreme'`) is carried in `opts.params.distance`\n * and forwarded to `invoker.run` by the dispatcher.\n */\nexport const reorderForwardAction: Action & { requires: string[] } = {\n id: 'reorder.forward',\n label: 'Bring Forward',\n group: 'reorder',\n variants: [\n { key: 'adjacent', label: 'Bring Forward', icon: createElement(BringForwardIcon), params: { distance: 'adjacent' } },\n { key: 'extreme', label: 'Bring to Front', icon: createElement(BringToFrontIcon), params: { distance: 'extreme' } },\n ],\n defaultBinding: [\n {\n // Modifier matching is strict, so Shift must be absent here — listing\n // the shifted `'}'` on this binding would be dead weight, since you\n // can't type it without the Shift this spec forbids.\n spec: { kind: 'key', key: [']'], mods: { mod: true } },\n opts: { params: { distance: 'adjacent' } },\n },\n {\n // Cmd+Shift+] → bring-to-front, the convention every drawing app uses.\n // With Shift held the US-layout key reports as '}', so both characters\n // are listed. Some browsers bind Cmd+Shift+[ / ] to tab switching and\n // may consume the event before the page sees it — hence the Cmd+Alt\n // binding below, which nothing reserves.\n spec: { kind: 'key', key: [']', '}'], mods: { mod: true, shift: true } },\n opts: { params: { distance: 'extreme' } },\n },\n {\n // Cmd+Alt+] → bring-to-front. On macOS Option+] emits '‘' (U+2018);\n // whether Cmd suppresses that transform is browser-dependent, so match\n // both the bracket and the Option-produced char (US layout). matchKey is\n // character-based, so non-US layouts may need a rebind.\n spec: { kind: 'key', key: [']', '‘'], mods: { mod: true, alt: true } },\n opts: { params: { distance: 'extreme' } },\n },\n ],\n eligible: { capability: 'edits-page' },\n requires: ['selection', 'scene', 'applyOps'],\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const distance = (params?.distance as ReorderDistance | undefined) ?? 'adjacent';\n const selection = deps.selection;\n const scene = deps.scene;\n const applyOps = deps.applyOps;\n if (!selection || !scene) return;\n reorderSelection(selection, scene, 'forward', distance, applyOps);\n },\n },\n enabled: requiresSelection,\n};\n\n/**\n * @experimental\n * Parametric descriptor for backward reorder (`reorder.backward`).\n *\n * Collapses the old `reorder.backward` (adjacent) and `reorder.back`\n * (extreme) actions into one descriptor with two keybindings.\n */\nexport const reorderBackwardAction: Action & { requires: string[] } = {\n id: 'reorder.backward',\n label: 'Send Backward',\n group: 'reorder',\n variants: [\n { key: 'adjacent', label: 'Send Backward', icon: createElement(SendBackwardIcon), params: { distance: 'adjacent' } },\n { key: 'extreme', label: 'Send to Back', icon: createElement(SendToBackIcon), params: { distance: 'extreme' } },\n ],\n defaultBinding: [\n {\n spec: { kind: 'key', key: ['['], mods: { mod: true } },\n opts: { params: { distance: 'adjacent' } },\n },\n {\n // Cmd+Shift+[ → send-to-back. See the bring-to-front notes above.\n spec: { kind: 'key', key: ['[', '{'], mods: { mod: true, shift: true } },\n opts: { params: { distance: 'extreme' } },\n },\n {\n // Cmd+Alt+[ → send-to-back, the binding no browser reserves. On macOS\n // Option+[ emits '“' (U+201C), included alongside the bracket.\n spec: { kind: 'key', key: ['[', '“'], mods: { mod: true, alt: true } },\n opts: { params: { distance: 'extreme' } },\n },\n ],\n eligible: { capability: 'edits-page' },\n requires: ['selection', 'scene', 'applyOps'],\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const distance = (params?.distance as ReorderDistance | undefined) ?? 'adjacent';\n const selection = deps.selection;\n const scene = deps.scene;\n const applyOps = deps.applyOps;\n if (!selection || !scene) return;\n reorderSelection(selection, scene, 'backward', distance, applyOps);\n },\n },\n enabled: requiresSelection,\n};\n\n"],"mappings":";;;;;;AAwBA,SAASA,EACP,GACA,GACA,GACA,GACA,GACM;CACN,IAAMC,IAAMC,EAAU,IAAI;CAC1B,IAAID,EAAI,WAAW,GAAG;CAUtB,IAAME,IAAKC,EAAgB;EAAE;EAAK,WAPhCC,MAAc,YACVC,MAAa,YAAY,UAAU,YACnCA,MAAa,YAAY,SAAS;EAKiB,OAAO;CAAU,CAAC;CAE3E,AAAIC,IAAUA,EAAS,CAACJ,CAAE,GAAG,SAAS,IACjCK,EAAM,WAAW,CAACL,CAAE,GAAG,WAAWM,EAAqBD,GAAON,EAAU,cAAc,CAAC;AAC9F;AAeA,IAAaQ,IAAwD;CACnE,IAAI;CACJ,OAAO;CACP,OAAO;CACP,UAAU,CACR;EAAE,KAAK;EAAY,OAAO;EAAiB,MAAMC,EAAcC,CAAgB;EAAG,QAAQ,EAAE,UAAU,WAAW;CAAE,GACnH;EAAE,KAAK;EAAW,OAAO;EAAkB,MAAMD,EAAcE,CAAgB;EAAG,QAAQ,EAAE,UAAU,UAAU;CAAE,CACpH;CACA,gBAAgB;EACd;GAIE,MAAM;IAAE,MAAM;IAAO,KAAK,CAAC,GAAG;IAAG,MAAM,EAAE,KAAK,GAAK;GAAE;GACrD,MAAM,EAAE,QAAQ,EAAE,UAAU,WAAW,EAAE;EAC3C;EACA;GAME,MAAM;IAAE,MAAM;IAAO,KAAK,CAAC,KAAK,GAAG;IAAG,MAAM;KAAE,KAAK;KAAM,OAAO;IAAK;GAAE;GACvE,MAAM,EAAE,QAAQ,EAAE,UAAU,UAAU,EAAE;EAC1C;EACA;GAKE,MAAM;IAAE,MAAM;IAAO,KAAK,CAAC,KAAK,GAAG;IAAG,MAAM;KAAE,KAAK;KAAM,KAAK;IAAK;GAAE;GACrE,MAAM,EAAE,QAAQ,EAAE,UAAU,UAAU,EAAE;EAC1C;CACF;CACA,UAAU,EAAE,YAAY,aAAa;CACrC,UAAU;EAAC;EAAa;EAAS;CAAU;CAC3C,SAAS;EACP,QAAQ;EACR,MAAM,GAAM,MAAW;GACrB,IAAMP,IAAYQ,GAAQ,YAA4C,YAChEZ,IAAYa,EAAK,WACjBP,IAAQO,EAAK,OACbR,IAAWQ,EAAK;GACtB,AAAKb,KAAcM,KACnBR,EAAiBE,GAAWM,GAAO,WAAWF,GAAUC,CAAQ;EAClE;CACF;CACA,SAASS;AACX,GASaC,IAAyD;CACpE,IAAI;CACJ,OAAO;CACP,OAAO;CACP,UAAU,CACR;EAAE,KAAK;EAAY,OAAO;EAAiB,MAAMN,EAAcO,CAAgB;EAAG,QAAQ,EAAE,UAAU,WAAW;CAAE,GACnH;EAAE,KAAK;EAAW,OAAO;EAAgB,MAAMP,EAAcQ,CAAc;EAAG,QAAQ,EAAE,UAAU,UAAU;CAAE,CAChH;CACA,gBAAgB;EACd;GACE,MAAM;IAAE,MAAM;IAAO,KAAK,CAAC,GAAG;IAAG,MAAM,EAAE,KAAK,GAAK;GAAE;GACrD,MAAM,EAAE,QAAQ,EAAE,UAAU,WAAW,EAAE;EAC3C;EACA;GAEE,MAAM;IAAE,MAAM;IAAO,KAAK,CAAC,KAAK,GAAG;IAAG,MAAM;KAAE,KAAK;KAAM,OAAO;IAAK;GAAE;GACvE,MAAM,EAAE,QAAQ,EAAE,UAAU,UAAU,EAAE;EAC1C;EACA;GAGE,MAAM;IAAE,MAAM;IAAO,KAAK,CAAC,KAAK,GAAG;IAAG,MAAM;KAAE,KAAK;KAAM,KAAK;IAAK;GAAE;GACrE,MAAM,EAAE,QAAQ,EAAE,UAAU,UAAU,EAAE;EAC1C;CACF;CACA,UAAU,EAAE,YAAY,aAAa;CACrC,UAAU;EAAC;EAAa;EAAS;CAAU;CAC3C,SAAS;EACP,QAAQ;EACR,MAAM,GAAM,MAAW;GACrB,IAAMb,IAAYQ,GAAQ,YAA4C,YAChEZ,IAAYa,EAAK,WACjBP,IAAQO,EAAK,OACbR,IAAWQ,EAAK;GACtB,AAAKb,KAAcM,KACnBR,EAAiBE,GAAWM,GAAO,YAAYF,GAAUC,CAAQ;EACnE;CACF;CACA,SAASS;AACX"}
@@ -1 +1 @@
1
- {"version":3,"file":"requiresSelection.js","names":["requiresSelection","selection","deps","ActionDisabledReason"],"sources":["../../../../src/interactions/actions/defaults/requiresSelection.ts"],"sourcesContent":["import type { SelectionApi } from 'core/selection/useSelection';\nimport type { ActionDeps } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\n\n/**\n * Shared `enabled` predicate for selection-gated actions (reorder, duplicate,\n * flip, nudge, …).\n *\n * Returns `true` when the active selection is non-empty, otherwise\n * `SelectionRequired` so the palette/ActionBar can grey the entry out.\n *\n * The dispatcher evaluates this gate with the action's built deps, so a\n * deps-aware predicate is required for these actions to dispatch at all — a\n * constant `() => SelectionRequired` stub falls through forever. When called\n * without deps (e.g. a bare `enabled()` introspection) it conservatively\n * reports `SelectionRequired`; the invoker independently no-ops on an empty\n * selection, so dispatch correctness never relies on the gate alone.\n */\nexport function requiresSelection(deps?: ActionDeps): true | ActionDisabledReason {\n const selection = deps?.selection as SelectionApi | undefined;\n if (!selection || (selection.get() as unknown[]).length === 0) {\n return ActionDisabledReason.SelectionRequired;\n }\n return true;\n}\n"],"mappings":";;AAkBA,SAAgBA,EAAkB,GAAgD;CAChF,IAAMC,IAAYC,GAAM;CAIxB,OAHI,CAACD,KAAcA,EAAU,IAAI,CAAC,CAAe,WAAW,IACnDE,EAAqB,oBAEvB;AACT"}
1
+ {"version":3,"file":"requiresSelection.js","names":["requiresSelection","selection","deps","ActionDisabledReason"],"sources":["../../../../src/interactions/actions/defaults/requiresSelection.ts"],"sourcesContent":["import type { ActionDeps } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\n\n/**\n * Shared `enabled` predicate for selection-gated actions (reorder, duplicate,\n * flip, nudge, …).\n *\n * Returns `true` when the active selection is non-empty, otherwise\n * `SelectionRequired` so the palette/ActionBar can grey the entry out.\n *\n * The dispatcher evaluates this gate with the action's built deps, so a\n * deps-aware predicate is required for these actions to dispatch at all — a\n * constant `() => SelectionRequired` stub falls through forever. When called\n * without deps (e.g. a bare `enabled()` introspection) it conservatively\n * reports `SelectionRequired`; the invoker independently no-ops on an empty\n * selection, so dispatch correctness never relies on the gate alone.\n */\nexport function requiresSelection(deps?: ActionDeps): true | ActionDisabledReason {\n const selection = deps?.selection;\n if (!selection || (selection.get() as unknown[]).length === 0) {\n return ActionDisabledReason.SelectionRequired;\n }\n return true;\n}\n"],"mappings":";;AAiBA,SAAgBA,EAAkB,GAAgD;CAChF,IAAMC,IAAYC,GAAM;CAIxB,OAHI,CAACD,KAAcA,EAAU,IAAI,CAAC,CAAe,WAAW,IACnDE,EAAqB,oBAEvB;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"resize.js","names":["IDENTITY_EXPAND","ids","EMPTY_POINT_SNAP","resolveDeps","geometry","poseDescriptorOf","ctx","dep","DEFAULT_RESIZE_BEHAVIORS","buildPointSnapContext","cx","pose","cy","tl","rotatePoint","rotation","tr","br","bl","draggedCorner","fixedCorner","anchor","fixedPt","draggedPt","applyPointSnap","result","dx","dy","pin","Dx","Dy","cos","sin","localW","localH","newWidth","newHeight","newCx","newCy","pointSnapToCamera","cam","w","fromPlane","m","p","pointSnapFromPlane","r","toPlane","computeProposedBounds","cs","sn","dxLocal","dyLocal","nx","ob","ny","nw","nh","resizeAction","inPlane","selection","scene","applyOps","geometryProjection","expanded","expandIds","isGroupPath","writeIds","frame","scenePoseFrame","view","stored","layer","id","carryPose","world","planeOf","startPoses","startWorlds","leafBounds","node","invertPlane","originBounds","unionBounds","originPose","originRotation","fixedLocal","fixedCornerOf","fixedWorld","startWorld","readView","gestureViewReader","readPlane","gesturePlaneReader","gestureCtx","ctxAsRect","b","behaviors","scratch","readGestureLifecycle","policy","commitResize","target","defaultCommitAdapter","reduceBehaviorEnd","commitGestureOps","lifecycle","ops","next","from","committed","sb","db","boxToBox","dataOp","geometryDataOp","nb","createTransformOp","mctx","proposedBounds","behaviorCtx","computePose","sp","poseRotationOf","remapRotatedLeaf","proposedPose","newCenterX","newCenterY","newFixedLocal","newFixedWorld","correctionX","correctionY","translatePoseViaDescriptor","poseAsRect","psCtx","plane","beh","reason","runBehaviorCancel","dropPreviewOverrides","selectionLayer"],"sources":["../../../../src/interactions/actions/defaults/resize.ts"],"sourcesContent":["/**\n * `resizeAction` — ongoing Action descriptor for anchor-relative drag resize.\n *\n * ## Status: REAL\n *\n * Performs real anchor-relative resize math for rect-shaped poses. Reads\n * `ctx.drag.affordance.anchor` to determine which corner is fixed, captures\n * start poses, applies per-frame bounds remapping, and commits one batch on\n * end — labeled, transient and observed per the `resizePolicy` dep's\n * lifecycle fields.\n *\n * ## Behaviors / point-snap / expandIds — via the `resizePolicy` dep\n *\n * The `resizePolicy` dep carries the behavior options the legacy `useResize`\n * hook exposed (`behaviors`, `pointSnapBehaviors`, `expandIds`). When the dep\n * is registered, this invoker:\n *\n * - Calls `expandIds([id])` at start. When the result expands beyond the\n * starting id, takes the group path (union-AABB origin, per-leaf remap).\n * - Runs `behaviors[]` after the raw anchor-math bounds are computed.\n * Behaviors return `{ pose? }` and rewrite the proposed bounds before\n * they're projected back into pose space.\n * - Runs `pointSnap[]` after the bounds→pose projection. The first non-null\n * `PointSnapResult` wins; this invoker back-solves a pose so the chosen\n * frame's world point lands on the snap target.\n *\n * When the dep is absent the invoker falls back to identity defaults\n * (no behaviors, no snap, `ids => ids` expansion).\n *\n * ## Pose geometry — via the `poseDescriptor` dep\n *\n * Every pose read and rewrite goes through the `poseDescriptor` dep, which\n * `<SceneCanvas>` sources from its `poseDescriptor` prop. Unsourced it is\n * `AUTO_POSE_DESCRIPTOR`, which dispatches per call to `pathPoseDescriptor`\n * for Path-shaped poses and to `RECT_POSE_DESCRIPTOR` for everything else.\n *\n * @see useResize — the React hook this descriptor mirrors.\n * @see src/interactions/actions/resize/geometry.ts — `PoseDescriptor`.\n */\n\nimport { gesturePlaneReader, gestureViewReader } from '../../gestures/shared/screenTolerance';\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle } from '@weasel-js/routing';\nimport type { Scene, NodeId } from 'core/scene/types';\nimport { syncPreviewOverrides, dropPreviewOverrides } from '../previewOverrides';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport type {\n GestureContext,\n ModifierState,\n PointSnapBehavior,\n PointSnapContext,\n PointSnapResult,\n ResizeAnchor,\n BoundsConstraint,\n RotatedPose,\n} from '../../gestures/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { ResizePolicy } from '../depSchema';\nimport { DEFAULT_RESIZE_BEHAVIORS } from '../resize/behaviors';\nimport { remapRotatedLeaf, translatePoseViaDescriptor, type PoseDescriptor } from '../resize/geometry';\nimport { poseRotationOf } from 'core/geometry/poseRotation';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport { fixedCornerOf } from '../resize/cornerHandles';\nimport { rotatePoint } from '../rotate/geometry';\nimport type { Op } from 'core/ops/types';\nimport type { Mat3 } from '@weasel-js/geom';\nimport { boxToBox } from '@weasel-js/geom';\nimport { createTransformOp } from 'core/ops/transform';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport { geometryDataOp, type GeometryProjection } from '../geometryProjection';\nimport { unionBounds } from 'core/geometry/unionBounds';\nimport { scenePoseFrame, type PoseFrame } from '../poseFrame';\nimport { commitGestureOps, readGestureLifecycle, reduceBehaviorEnd, runBehaviorCancel, type GestureLifecycle } from '../gestureLifecycle';\nimport { carryPose, invertPlane, inPlane, planeOf, selectionLayer } from '../planeInput';\nimport { fromPlane, toPlane, type PlaneMap } from 'core/viewport/parallax';\n\n// ---------------------------------------------------------------------------\n// Defaults applied when `resizePolicy` dep is absent. Mirrors the\n// destructuring defaults in `useResize`.\n// ---------------------------------------------------------------------------\n\nconst IDENTITY_EXPAND = (ids: string[]) => ids;\nconst EMPTY_POINT_SNAP: PointSnapBehavior<Bounds>[] = [];\n\nfunction resolveDeps(ctx: InvocationCtx): {\n behaviors: BoundsConstraint<Bounds>[];\n pointSnap: PointSnapBehavior<Bounds>[];\n expandIds: (ids: string[]) => string[];\n geometry: PoseDescriptor<unknown>;\n policy: ResizePolicy<unknown> | undefined;\n} {\n const geometry = poseDescriptorOf(ctx.deps.poseDescriptor);\n const dep = ctx.deps.resizePolicy as ResizePolicy<unknown> | undefined;\n if (!dep) {\n return {\n // Standard kit behaviors (shift = aspect lock) apply even with no\n // policy wired; consumers opt out via an explicit `behaviors: []`.\n behaviors: DEFAULT_RESIZE_BEHAVIORS as BoundsConstraint<Bounds>[],\n pointSnap: EMPTY_POINT_SNAP,\n expandIds: IDENTITY_EXPAND,\n geometry,\n policy: undefined,\n };\n }\n return {\n behaviors: dep.constraints as unknown as BoundsConstraint<Bounds>[],\n pointSnap: dep.pointSnap as unknown as PointSnapBehavior<Bounds>[],\n expandIds: dep.expandIds,\n geometry,\n policy: dep,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Point-snap math (lifted from `useResize`).\n// ---------------------------------------------------------------------------\n\nfunction buildPointSnapContext<TPose extends Bounds>(\n pose: TPose,\n rotation: number,\n anchor: ResizeAnchor,\n modifiers: ModifierState,\n): PointSnapContext<TPose> {\n const cx = pose.x + pose.width / 2;\n const cy = pose.y + pose.height / 2;\n\n const tl = rotatePoint(pose.x, pose.y, cx, cy, rotation);\n const tr = rotatePoint(pose.x + pose.width, pose.y, cx, cy, rotation);\n const br = rotatePoint(pose.x + pose.width, pose.y + pose.height, cx, cy, rotation);\n const bl = rotatePoint(pose.x, pose.y + pose.height, cx, cy, rotation);\n\n let draggedCorner: { worldX: number; worldY: number } | null = null;\n let fixedCorner: { worldX: number; worldY: number } | null = null;\n\n if (anchor.x !== 'free' && anchor.y !== 'free') {\n const fixedPt =\n anchor.x === 'min' && anchor.y === 'min' ? tl :\n anchor.x === 'max' && anchor.y === 'min' ? tr :\n anchor.x === 'max' && anchor.y === 'max' ? br : bl;\n const draggedPt =\n anchor.x === 'min' && anchor.y === 'min' ? br :\n anchor.x === 'max' && anchor.y === 'min' ? bl :\n anchor.x === 'max' && anchor.y === 'max' ? tl : tr;\n fixedCorner = { worldX: fixedPt.x, worldY: fixedPt.y };\n draggedCorner = { worldX: draggedPt.x, worldY: draggedPt.y };\n }\n\n return {\n draggedCorner,\n fixedCorner,\n center: { worldX: cx, worldY: cy },\n origin: { worldX: tl.x, worldY: tl.y },\n rotation,\n anchor,\n proposed: pose,\n modifiers,\n };\n}\n\nfunction applyPointSnap<TPose extends Bounds>(\n pose: TPose,\n rotation: number,\n result: PointSnapResult,\n ctx: PointSnapContext<TPose>,\n): TPose {\n if (result.frame === 'center') {\n const dx = result.worldX - ctx.center.worldX;\n const dy = result.worldY - ctx.center.worldY;\n return { ...pose, x: pose.x + dx, y: pose.y + dy };\n }\n if (result.frame === 'origin') {\n const dx = result.worldX - ctx.origin.worldX;\n const dy = result.worldY - ctx.origin.worldY;\n return { ...pose, x: pose.x + dx, y: pose.y + dy };\n }\n const pin = result.frame === 'dragged-corner' ? ctx.fixedCorner : ctx.draggedCorner;\n if (!pin) return pose;\n const Dx = result.worldX - pin.worldX;\n const Dy = result.worldY - pin.worldY;\n const cos = Math.cos(rotation);\n const sin = Math.sin(rotation);\n const localW = Dx * cos + Dy * sin;\n const localH = -Dx * sin + Dy * cos;\n const newWidth = Math.abs(localW);\n const newHeight = Math.abs(localH);\n const newCx = (pin.worldX + result.worldX) / 2;\n const newCy = (pin.worldY + result.worldY) / 2;\n return { ...pose, x: newCx - newWidth / 2, y: newCy - newHeight / 2, width: newWidth, height: newHeight };\n}\n\n/** `ctx`'s points in the camera's world, for a snap rule written against it. */\nfunction pointSnapToCamera<TPose extends Bounds>(\n ctx: PointSnapContext<TPose>,\n m: PlaneMap,\n): PointSnapContext<TPose> {\n const cam = (p: { worldX: number; worldY: number }) => {\n const w = fromPlane(m, { x: p.worldX, y: p.worldY });\n return { worldX: w.x, worldY: w.y };\n };\n return {\n ...ctx,\n draggedCorner: ctx.draggedCorner && cam(ctx.draggedCorner),\n fixedCorner: ctx.fixedCorner && cam(ctx.fixedCorner),\n center: cam(ctx.center),\n origin: cam(ctx.origin),\n };\n}\n\n/** A snap rule's answer, back in the plane the resize works in. */\nfunction pointSnapFromPlane(\n r: PointSnapResult | null | undefined,\n m: PlaneMap,\n): PointSnapResult | null | undefined {\n if (!r) return r;\n const p = toPlane(m, { x: r.worldX, y: r.worldY });\n return { ...r, worldX: p.x, worldY: p.y };\n}\n\n// ---------------------------------------------------------------------------\n// Anchor math (rotated and unrotated).\n// ---------------------------------------------------------------------------\n\nfunction computeProposedBounds(\n ob: Bounds,\n anchor: ResizeAnchor,\n dx: number,\n dy: number,\n rotation: number,\n): Bounds {\n if (rotation !== 0) {\n const cs = Math.cos(-rotation);\n const sn = Math.sin(-rotation);\n const dxLocal = cs * dx - sn * dy;\n const dyLocal = sn * dx + cs * dy;\n dx = dxLocal;\n dy = dyLocal;\n }\n let nx = ob.x;\n let ny = ob.y;\n let nw = ob.width;\n let nh = ob.height;\n if (anchor.x === 'min') {\n nw = ob.width + dx;\n } else if (anchor.x === 'max') {\n nx = ob.x + dx;\n nw = ob.width - dx;\n }\n if (anchor.y === 'min') {\n nh = ob.height + dy;\n } else if (anchor.y === 'max') {\n ny = ob.y + dy;\n nh = ob.height - dy;\n }\n return { x: nx, y: ny, width: nw, height: nh };\n}\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface ResizeScratch {\n /** Ids the gesture writes to. For group expansion this is the leaf set;\n * for the single-id path it's the original selection. */\n writeIds: NodeId[];\n scene: Scene<unknown, string, unknown>;\n /** World reads and local writes over the scene's composition strategy. */\n frame: PoseFrame<unknown>;\n /** Start poses keyed by write id, as stored — the `from` of each op. */\n startPoses: Map<NodeId, unknown>;\n /** Start poses in world — the frame the anchor math and the drag share. */\n startWorlds: Map<NodeId, unknown>;\n /** Shared origin bounds (union for group/multi-select, own bounds for\n * single). Used as the `src` rect for `geometry.remapBounds`. */\n originBounds: Bounds;\n anchor: ResizeAnchor;\n startWorld: { x: number; y: number };\n /** Rotation captured at gesture start (from `geometry.getRotation?`). */\n originRotation: number;\n /** World-space position of the diagonally opposite corner at start. Used\n * for the rotated-resize fixed-corner pin correction. */\n fixedWorld: { x: number; y: number };\n /** In-flight preview poses buffered during drag. */\n previews: Map<NodeId, unknown>;\n overrideEntries: Map<NodeId, { pose: unknown }>;\n geometry: PoseDescriptor<unknown>;\n behaviors: BoundsConstraint<Bounds>[];\n pointSnap: PointSnapBehavior<Bounds>[];\n /** Gesture context handed to behaviors. Reused across move/end. */\n gestureCtx: GestureContext<unknown>;\n /** Optional consumer commit hook captured at gesture start. When present,\n * the final pose commit routes through it (consumer history) instead of\n * `scene.applyBatch`. Undefined → fall back to `scene.applyBatch`. */\n applyOps?: (ops: Op[], label: string) => void;\n /** Optional eager-sync seam captured at gesture start. When present, the\n * commit emits a `setData` op per leaf mapping its data-held geometry by\n * the box→box affine the pose underwent. Undefined → pose-only commit. */\n geometryProjection?: GeometryProjection;\n /** Label, history and callbacks from the `resizePolicy` dep. */\n lifecycle: GestureLifecycle;\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Static descriptor for the `resize` Action.\n *\n * Requires dep-schema entries: `selection`, `scene`.\n * Reads optional `resizePolicy` dep when present.\n * Requires `InvocationCtx.drag.affordance` with a `handle:*` kind.\n */\nexport const resizeAction: Action & { requires: string[] } = inPlane({\n id: 'resize',\n label: 'Resize',\n // No default binding: a bare `{ kind: 'drag' }` claimed every drag at ambient\n // scope, tying areaSelect's. The resize handles bind it\n // (`selectionTransformBindings`, the `transform` preset).\n eligible: { capability: 'transforms-selection' },\n requires: ['selection', 'scene', 'resizePolicy', 'poseDescriptor', 'applyOps', 'geometryProjection', 'poseComposition', 'view'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, _opts): OngoingHandle {\n const selection = ctx.deps.selection as SelectionApi | undefined;\n const scene = ctx.deps.scene as Scene<unknown, string, unknown> | undefined;\n const applyOps = ctx.deps.applyOps as ((ops: Op[], label: string) => void) | undefined;\n const geometryProjection = ctx.deps.geometryProjection as GeometryProjection | undefined;\n\n if (!selection || !scene) return {};\n\n const ids = selection.get() as NodeId[];\n if (ids.length === 0) return {};\n\n const affordance = ctx.drag?.affordance;\n const anchor = affordance?.anchor;\n if (!anchor) return {};\n\n const { behaviors, pointSnap, expandIds, geometry, policy } = resolveDeps(ctx);\n\n // Group expansion. When the starting selection is a single id but\n // `expandIds` returns a different set, we're in the group path:\n // every leaf is written, and origin bounds is the union AABB.\n // When `expandIds` returns the same list (identity default or no-op),\n // we write the selected ids directly.\n const expanded = expandIds(ids as unknown as string[]);\n if (expanded.length === 0) return {};\n\n const isGroupPath =\n ids.length === 1 && (expanded.length !== 1 || expanded[0] !== (ids[0] as string));\n const writeIds = (isGroupPath ? expanded : (ids as unknown as string[])) as NodeId[];\n\n // Capture start poses + per-leaf bounds. The drag delta is world, so\n // the anchor math runs on world poses — carried into the plane the edit\n // is measured in — and each result is carried back to its node's plane\n // and stored in the node's own parent frame.\n const frame = scenePoseFrame(scene, ctx.deps.poseComposition);\n const view = ctx.deps.view;\n /** A world result for `id`, as stored. */\n const stored = (id: NodeId, world: unknown): unknown => {\n const layer = scene.get(id)?.layer;\n return frame.local(id, carryPose(geometry, world, layer === undefined ? null : planeOf(view, layer)));\n };\n const startPoses = new Map<NodeId, unknown>();\n const startWorlds = new Map<NodeId, unknown>();\n const leafBounds: Bounds[] = [];\n for (const id of writeIds) {\n const node = scene.get(id);\n if (!node) continue;\n startPoses.set(id, node.pose);\n const world = carryPose(geometry, frame.world(id), invertPlane(planeOf(view, node.layer)));\n startWorlds.set(id, world);\n leafBounds.push(geometry.getBounds(world));\n }\n if (startPoses.size === 0) return {};\n\n // `leafBounds` is non-empty here (startPoses.size === 0 guarded above),\n // and the union branch only runs for length >= 2 → `!` is safe.\n const originBounds = leafBounds.length === 1\n ? leafBounds[0]\n : unionBounds(leafBounds)!;\n\n // Rotation captured only for the single-write-id path; group-resize\n // takes the AABB-frame (unrotated) path even if leaves have rotation.\n const originPose = !isGroupPath && writeIds.length === 1\n ? startWorlds.get(writeIds[0])\n : (originBounds as unknown);\n const originRotation = !isGroupPath\n ? (geometry.getRotation?.(originPose) ?? 0)\n : 0;\n\n const fixedLocal = fixedCornerOf(originBounds, anchor);\n const fixedWorld = originRotation === 0\n ? fixedLocal\n : rotatePoint(\n fixedLocal.x, fixedLocal.y,\n originBounds.x + originBounds.width / 2,\n originBounds.y + originBounds.height / 2,\n originRotation,\n );\n\n const startWorld = { x: ctx.drag?.start.x ?? 0, y: ctx.drag?.start.y ?? 0 };\n\n // Minimal GestureContext for behaviors. `draggedIds` matches the\n // legacy hook: the starting id (not the expanded leaf set).\n const readView = gestureViewReader(ctx.deps);\n const readPlane = gesturePlaneReader(ctx.deps);\n const gestureCtx: GestureContext<unknown> = {\n draggedIds: ids as unknown as string[],\n origin: new Map<string, unknown>([[ids[0] as string, originPose]]),\n current: new Map<string, unknown>([[ids[0] as string, originPose]]),\n snap: null,\n modifiers: { ...ctx.modifiers },\n pointer: { worldX: startWorld.x, worldY: startWorld.y, clientX: 0, clientY: 0 },\n view: readView(),\n plane: readPlane(),\n // `adapter` is unused by the kit's behaviors; cast to satisfy the type.\n adapter: undefined as unknown as GestureContext<unknown>['adapter'],\n scratch: {},\n };\n\n // Fire behavior onStart hooks (parity with `useResize`).\n const ctxAsRect = gestureCtx as unknown as GestureContext<Bounds>;\n for (const b of behaviors) b.onStart?.(ctxAsRect);\n\n const scratch: ResizeScratch = {\n writeIds,\n scene,\n frame,\n startPoses,\n startWorlds,\n originBounds,\n anchor,\n startWorld,\n originRotation,\n fixedWorld,\n previews: new Map<NodeId, unknown>(),\n overrideEntries: new Map<NodeId, { pose: unknown }>(),\n geometry,\n behaviors,\n pointSnap,\n gestureCtx,\n applyOps,\n geometryProjection,\n lifecycle: readGestureLifecycle(policy, 'Resize', behaviors),\n };\n const { lifecycle } = scratch;\n lifecycle.start(writeIds as string[]);\n\n // Returns whether anything reached the document.\n const commitResize = (): boolean => {\n const target = {\n scene,\n applyOps: scratch.applyOps,\n adapter: defaultCommitAdapter(scene, selection.adapterMethods),\n };\n const r = reduceBehaviorEnd(scratch.behaviors, scratch.gestureCtx as unknown as GestureContext<Bounds>);\n if (r === null) return false;\n if (r !== undefined) {\n commitGestureOps(target, lifecycle, r);\n return true;\n }\n\n if (scratch.previews.size === 0) return false;\n\n // One transform op per write id (from = pre-drag start pose, to =\n // final preview pose), committed as a single batch → one undo entry.\n const ops: Op[] = [];\n for (const id of scratch.writeIds) {\n const next = scratch.previews.get(id);\n if (next === undefined) continue;\n const from = scratch.startPoses.get(id);\n if (from === undefined) continue;\n let committed = next;\n // Eager-sync: map data-held geometry by the box→box affine the\n // pose underwent (start-pose bounds → final-pose bounds). Opt-in\n // via the geometryProjection seam; a no-op when the dep is absent.\n if (scratch.geometryProjection) {\n const node = scratch.scene.get(id);\n if (node) {\n const sb = scratch.geometry.getBounds(from);\n const db = scratch.geometry.getBounds(next);\n const m: Mat3 = boxToBox(\n sb.x, sb.y, sb.width, sb.height,\n db.x, db.y, db.width, db.height,\n );\n const dataOp = geometryDataOp(\n scratch.geometryProjection,\n { id: id as string, data: node.data, pose: from },\n m,\n lifecycle.label,\n );\n if (dataOp) {\n ops.push(dataOp);\n // The data absorbed the affine — including any mirror from a\n // drag past the opposite edge — so the pose must commit as a\n // normalized AABB. Leaving signed extents on the pose makes\n // the node unhittable (hit-testing assumes normalized boxes).\n // Without the seam the signed pose is kept: a bare-AABB pose\n // is the only carrier of the flip there.\n if (db.width < 0 || db.height < 0) {\n const nb = {\n ...db,\n x: db.width < 0 ? db.x + db.width : db.x,\n y: db.height < 0 ? db.y + db.height : db.y,\n width: Math.abs(db.width),\n height: Math.abs(db.height),\n };\n committed = scratch.geometry.remapBounds(next, db, nb) as typeof next;\n }\n }\n }\n }\n ops.push(createTransformOp<unknown>({\n id: id as string,\n from,\n to: committed,\n label: lifecycle.label,\n }));\n }\n if (ops.length === 0) return false;\n commitGestureOps(target, lifecycle, ops);\n return true;\n };\n\n return {\n kind: 'resize',\n onMove(mctx: InvocationCtx): void {\n if (!mctx.drag) return;\n const dx = mctx.drag.current.x - scratch.startWorld.x;\n const dy = mctx.drag.current.y - scratch.startWorld.y;\n\n // Refresh modifiers + pointer on the gesture ctx for behaviors.\n scratch.gestureCtx.modifiers = { ...mctx.modifiers };\n scratch.gestureCtx.pointer = {\n worldX: mctx.drag.current.x,\n worldY: mctx.drag.current.y,\n clientX: 0,\n clientY: 0,\n };\n scratch.gestureCtx.view = readView();\n scratch.gestureCtx.plane = readPlane();\n\n let proposedBounds = computeProposedBounds(\n scratch.originBounds,\n scratch.anchor,\n dx,\n dy,\n scratch.originRotation,\n );\n\n // Behaviors run in bounds-space.\n const behaviorCtx = scratch.gestureCtx as unknown as GestureContext<Bounds>;\n for (const b of scratch.behaviors) {\n const r = b.onMove?.(behaviorCtx, {\n pose: proposedBounds,\n anchor: scratch.anchor,\n });\n if (!r) continue;\n if (r.pose !== undefined) {\n proposedBounds = {\n x: r.pose.x, y: r.pose.y, width: r.pose.width, height: r.pose.height,\n };\n }\n }\n\n // Project bounds → pose. For the group path, each leaf is remapped\n // from its own startPose; for single, from the start pose directly.\n //\n // A rotated leaf in a group needs its own frame: `src`/`dst` are\n // world-frame here, unlike the single path where the drag delta was\n // already projected into the leaf's frame, so the naive affine\n // scales the wrong pair of extents. See `remapRotatedLeaf`.\n const computePose = (id: NodeId): unknown => {\n const sp = scratch.startWorlds.get(id);\n if (sp === undefined) return undefined;\n if (isGroupPath && poseRotationOf(sp) !== null) {\n return remapRotatedLeaf(\n sp as RotatedPose, scratch.originBounds, proposedBounds,\n );\n }\n return scratch.geometry.remapBounds(sp, scratch.originBounds, proposedBounds);\n };\n\n // Single-id path: apply rotation correction + point-snap, then\n // store. Group path: rotation correction + point-snap don't apply\n // (group resize is AABB-frame).\n scratch.previews.clear();\n\n if (!isGroupPath && scratch.writeIds.length === 1) {\n const id = scratch.writeIds[0];\n let proposedPose = computePose(id);\n if (proposedPose === undefined) { syncPreviewOverrides(scratch); return; }\n\n if (scratch.originRotation !== 0) {\n const newCenterX = proposedBounds.x + proposedBounds.width / 2;\n const newCenterY = proposedBounds.y + proposedBounds.height / 2;\n const newFixedLocal = fixedCornerOf(proposedBounds, scratch.anchor);\n const newFixedWorld = rotatePoint(\n newFixedLocal.x, newFixedLocal.y,\n newCenterX, newCenterY,\n scratch.originRotation,\n );\n const correctionX = scratch.fixedWorld.x - newFixedWorld.x;\n const correctionY = scratch.fixedWorld.y - newFixedWorld.y;\n proposedPose = translatePoseViaDescriptor(proposedPose, correctionX, correctionY, scratch.geometry);\n }\n\n if (scratch.pointSnap.length > 0) {\n const rotation = scratch.originRotation;\n const poseAsRect = proposedPose as Bounds;\n const psCtx = buildPointSnapContext(\n poseAsRect,\n rotation,\n scratch.anchor,\n scratch.gestureCtx.modifiers,\n );\n // Point snaps are the surface's rules over the camera's world, so\n // on a plane their points cross at this boundary.\n const plane = scratch.gestureCtx.plane ?? null;\n for (const beh of scratch.pointSnap) {\n const result = plane\n ? pointSnapFromPlane(beh.onMove(pointSnapToCamera(psCtx, plane)), plane)\n : beh.onMove(psCtx);\n if (result) {\n proposedPose = applyPointSnap(poseAsRect, rotation, result, psCtx) as unknown;\n break;\n }\n }\n }\n\n scratch.previews.set(id, stored(id, proposedPose));\n scratch.gestureCtx.current = new Map<string, unknown>([[id as string, proposedPose]]);\n } else {\n // Group / multi path: remap each leaf and store.\n for (const id of scratch.writeIds) {\n const next = computePose(id);\n if (next === undefined) continue;\n scratch.previews.set(id, stored(id, next));\n }\n }\n\n syncPreviewOverrides(scratch);\n },\n\n onEnd(_endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n let committed = false;\n try {\n if (reason === 'commit') committed = commitResize();\n else runBehaviorCancel(scratch.behaviors, scratch.gestureCtx as unknown as GestureContext<Bounds>);\n } finally {\n dropPreviewOverrides(scratch);\n scratch.previews.clear();\n lifecycle.end(committed);\n }\n },\n previewIds: () => scratch.previews.keys(),\n previewPose: (id: string) => scratch.previews.get(id as NodeId) ?? null,\n };\n },\n },\n /**\n * Always enabled at the descriptor level. The dispatcher's\n * specificity-fallthrough loop treats anything `!== true` as \"skip and\n * try the next match\" — returning `ActionDisabledReason.SelectionRequired`\n * here makes the resize binding silently lose to lower-specificity\n * matches (e.g. ambient `insertAction`), inserting phantom nodes on\n * every drag instead of resizing. The invoker self-guards on empty\n * selection (returns `{}`), so a static `true` here is correct.\n *\n * Fix: previously `() => ActionDisabledReason.SelectionRequired`\n * — that broke every dispatcher-routed resize. See git blame.\n */\n enabled: () => true,\n}, selectionLayer);\n"],"mappings":"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"}
1
+ {"version":3,"file":"resize.js","names":["IDENTITY_EXPAND","ids","EMPTY_POINT_SNAP","resolveDeps","geometry","poseDescriptorOf","ctx","dep","DEFAULT_RESIZE_BEHAVIORS","buildPointSnapContext","cx","pose","cy","tl","rotatePoint","rotation","tr","br","bl","draggedCorner","fixedCorner","anchor","fixedPt","draggedPt","applyPointSnap","result","dx","dy","pin","Dx","Dy","cos","sin","localW","localH","newWidth","newHeight","newCx","newCy","pointSnapToCamera","cam","w","fromPlane","m","p","pointSnapFromPlane","r","toPlane","computeProposedBounds","cs","sn","dxLocal","dyLocal","nx","ob","ny","nw","nh","resizeAction","inPlane","selection","scene","applyOps","geometryProjection","expanded","expandIds","isGroupPath","writeIds","frame","scenePoseFrame","view","stored","layer","id","carryPose","world","planeOf","startPoses","startWorlds","leafBounds","node","invertPlane","originBounds","unionBounds","originPose","originRotation","fixedLocal","fixedCornerOf","fixedWorld","startWorld","readView","gestureViewReader","readPlane","gesturePlaneReader","gestureCtx","ctxAsRect","b","behaviors","scratch","readGestureLifecycle","policy","commitResize","target","defaultCommitAdapter","reduceBehaviorEnd","commitGestureOps","lifecycle","ops","next","from","committed","sb","db","boxToBox","dataOp","geometryDataOp","nb","createTransformOp","mctx","proposedBounds","behaviorCtx","computePose","sp","poseRotationOf","remapRotatedLeaf","proposedPose","newCenterX","newCenterY","newFixedLocal","newFixedWorld","correctionX","correctionY","translatePoseViaDescriptor","poseAsRect","psCtx","plane","beh","reason","runBehaviorCancel","dropPreviewOverrides","selectionLayer"],"sources":["../../../../src/interactions/actions/defaults/resize.ts"],"sourcesContent":["/**\n * `resizeAction` — ongoing Action descriptor for anchor-relative drag resize.\n *\n * ## Status: REAL\n *\n * Performs real anchor-relative resize math for rect-shaped poses. Reads\n * `ctx.drag.affordance.anchor` to determine which corner is fixed, captures\n * start poses, applies per-frame bounds remapping, and commits one batch on\n * end — labeled, transient and observed per the `resizePolicy` dep's\n * lifecycle fields.\n *\n * ## Behaviors / point-snap / expandIds — via the `resizePolicy` dep\n *\n * The `resizePolicy` dep carries the behavior options the legacy `useResize`\n * hook exposed (`behaviors`, `pointSnapBehaviors`, `expandIds`). When the dep\n * is registered, this invoker:\n *\n * - Calls `expandIds([id])` at start. When the result expands beyond the\n * starting id, takes the group path (union-AABB origin, per-leaf remap).\n * - Runs `behaviors[]` after the raw anchor-math bounds are computed.\n * Behaviors return `{ pose? }` and rewrite the proposed bounds before\n * they're projected back into pose space.\n * - Runs `pointSnap[]` after the bounds→pose projection. The first non-null\n * `PointSnapResult` wins; this invoker back-solves a pose so the chosen\n * frame's world point lands on the snap target.\n *\n * When the dep is absent the invoker falls back to identity defaults\n * (no behaviors, no snap, `ids => ids` expansion).\n *\n * ## Pose geometry — via the `poseDescriptor` dep\n *\n * Every pose read and rewrite goes through the `poseDescriptor` dep, which\n * `<SceneCanvas>` sources from its `poseDescriptor` prop. Unsourced it is\n * `AUTO_POSE_DESCRIPTOR`, which dispatches per call to `pathPoseDescriptor`\n * for Path-shaped poses and to `RECT_POSE_DESCRIPTOR` for everything else.\n *\n * @see useResize — the React hook this descriptor mirrors.\n * @see src/interactions/actions/resize/geometry.ts — `PoseDescriptor`.\n */\n\nimport { gesturePlaneReader, gestureViewReader } from '../../gestures/shared/screenTolerance';\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle } from '@weasel-js/routing';\nimport type { Scene, NodeId } from 'core/scene/types';\nimport { syncPreviewOverrides, dropPreviewOverrides } from '../previewOverrides';\nimport type {\n GestureContext,\n ModifierState,\n PointSnapBehavior,\n PointSnapContext,\n PointSnapResult,\n ResizeAnchor,\n BoundsConstraint,\n RotatedPose,\n} from '../../gestures/types';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport type { ResizePolicy } from '../depSchema';\nimport { DEFAULT_RESIZE_BEHAVIORS } from '../resize/behaviors';\nimport { remapRotatedLeaf, translatePoseViaDescriptor, type PoseDescriptor } from '../resize/geometry';\nimport { poseRotationOf } from 'core/geometry/poseRotation';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport { fixedCornerOf } from '../resize/cornerHandles';\nimport { rotatePoint } from '../rotate/geometry';\nimport type { Op } from 'core/ops/types';\nimport type { Mat3 } from '@weasel-js/geom';\nimport { boxToBox } from '@weasel-js/geom';\nimport { createTransformOp } from 'core/ops/transform';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport { geometryDataOp, type GeometryProjection } from '../geometryProjection';\nimport { unionBounds } from 'core/geometry/unionBounds';\nimport { scenePoseFrame, type PoseFrame } from '../poseFrame';\nimport { commitGestureOps, readGestureLifecycle, reduceBehaviorEnd, runBehaviorCancel, type GestureLifecycle } from '../gestureLifecycle';\nimport { carryPose, invertPlane, inPlane, planeOf, selectionLayer } from '../planeInput';\nimport { fromPlane, toPlane, type PlaneMap } from 'core/viewport/parallax';\n\n// ---------------------------------------------------------------------------\n// Defaults applied when `resizePolicy` dep is absent. Mirrors the\n// destructuring defaults in `useResize`.\n// ---------------------------------------------------------------------------\n\nconst IDENTITY_EXPAND = (ids: string[]) => ids;\nconst EMPTY_POINT_SNAP: PointSnapBehavior<Bounds>[] = [];\n\nfunction resolveDeps(ctx: InvocationCtx): {\n behaviors: BoundsConstraint<Bounds>[];\n pointSnap: PointSnapBehavior<Bounds>[];\n expandIds: (ids: string[]) => string[];\n geometry: PoseDescriptor<unknown>;\n policy: ResizePolicy<unknown> | undefined;\n} {\n const geometry = poseDescriptorOf(ctx.deps.poseDescriptor);\n const dep = ctx.deps.resizePolicy;\n if (!dep) {\n return {\n // Standard kit behaviors (shift = aspect lock) apply even with no\n // policy wired; consumers opt out via an explicit `behaviors: []`.\n behaviors: DEFAULT_RESIZE_BEHAVIORS as BoundsConstraint<Bounds>[],\n pointSnap: EMPTY_POINT_SNAP,\n expandIds: IDENTITY_EXPAND,\n geometry,\n policy: undefined,\n };\n }\n return {\n behaviors: dep.constraints as unknown as BoundsConstraint<Bounds>[],\n pointSnap: dep.pointSnap as unknown as PointSnapBehavior<Bounds>[],\n expandIds: dep.expandIds,\n geometry,\n policy: dep,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Point-snap math (lifted from `useResize`).\n// ---------------------------------------------------------------------------\n\nfunction buildPointSnapContext<TPose extends Bounds>(\n pose: TPose,\n rotation: number,\n anchor: ResizeAnchor,\n modifiers: ModifierState,\n): PointSnapContext<TPose> {\n const cx = pose.x + pose.width / 2;\n const cy = pose.y + pose.height / 2;\n\n const tl = rotatePoint(pose.x, pose.y, cx, cy, rotation);\n const tr = rotatePoint(pose.x + pose.width, pose.y, cx, cy, rotation);\n const br = rotatePoint(pose.x + pose.width, pose.y + pose.height, cx, cy, rotation);\n const bl = rotatePoint(pose.x, pose.y + pose.height, cx, cy, rotation);\n\n let draggedCorner: { worldX: number; worldY: number } | null = null;\n let fixedCorner: { worldX: number; worldY: number } | null = null;\n\n if (anchor.x !== 'free' && anchor.y !== 'free') {\n const fixedPt =\n anchor.x === 'min' && anchor.y === 'min' ? tl :\n anchor.x === 'max' && anchor.y === 'min' ? tr :\n anchor.x === 'max' && anchor.y === 'max' ? br : bl;\n const draggedPt =\n anchor.x === 'min' && anchor.y === 'min' ? br :\n anchor.x === 'max' && anchor.y === 'min' ? bl :\n anchor.x === 'max' && anchor.y === 'max' ? tl : tr;\n fixedCorner = { worldX: fixedPt.x, worldY: fixedPt.y };\n draggedCorner = { worldX: draggedPt.x, worldY: draggedPt.y };\n }\n\n return {\n draggedCorner,\n fixedCorner,\n center: { worldX: cx, worldY: cy },\n origin: { worldX: tl.x, worldY: tl.y },\n rotation,\n anchor,\n proposed: pose,\n modifiers,\n };\n}\n\nfunction applyPointSnap<TPose extends Bounds>(\n pose: TPose,\n rotation: number,\n result: PointSnapResult,\n ctx: PointSnapContext<TPose>,\n): TPose {\n if (result.frame === 'center') {\n const dx = result.worldX - ctx.center.worldX;\n const dy = result.worldY - ctx.center.worldY;\n return { ...pose, x: pose.x + dx, y: pose.y + dy };\n }\n if (result.frame === 'origin') {\n const dx = result.worldX - ctx.origin.worldX;\n const dy = result.worldY - ctx.origin.worldY;\n return { ...pose, x: pose.x + dx, y: pose.y + dy };\n }\n const pin = result.frame === 'dragged-corner' ? ctx.fixedCorner : ctx.draggedCorner;\n if (!pin) return pose;\n const Dx = result.worldX - pin.worldX;\n const Dy = result.worldY - pin.worldY;\n const cos = Math.cos(rotation);\n const sin = Math.sin(rotation);\n const localW = Dx * cos + Dy * sin;\n const localH = -Dx * sin + Dy * cos;\n const newWidth = Math.abs(localW);\n const newHeight = Math.abs(localH);\n const newCx = (pin.worldX + result.worldX) / 2;\n const newCy = (pin.worldY + result.worldY) / 2;\n return { ...pose, x: newCx - newWidth / 2, y: newCy - newHeight / 2, width: newWidth, height: newHeight };\n}\n\n/** `ctx`'s points in the camera's world, for a snap rule written against it. */\nfunction pointSnapToCamera<TPose extends Bounds>(\n ctx: PointSnapContext<TPose>,\n m: PlaneMap,\n): PointSnapContext<TPose> {\n const cam = (p: { worldX: number; worldY: number }) => {\n const w = fromPlane(m, { x: p.worldX, y: p.worldY });\n return { worldX: w.x, worldY: w.y };\n };\n return {\n ...ctx,\n draggedCorner: ctx.draggedCorner && cam(ctx.draggedCorner),\n fixedCorner: ctx.fixedCorner && cam(ctx.fixedCorner),\n center: cam(ctx.center),\n origin: cam(ctx.origin),\n };\n}\n\n/** A snap rule's answer, back in the plane the resize works in. */\nfunction pointSnapFromPlane(\n r: PointSnapResult | null | undefined,\n m: PlaneMap,\n): PointSnapResult | null | undefined {\n if (!r) return r;\n const p = toPlane(m, { x: r.worldX, y: r.worldY });\n return { ...r, worldX: p.x, worldY: p.y };\n}\n\n// ---------------------------------------------------------------------------\n// Anchor math (rotated and unrotated).\n// ---------------------------------------------------------------------------\n\nfunction computeProposedBounds(\n ob: Bounds,\n anchor: ResizeAnchor,\n dx: number,\n dy: number,\n rotation: number,\n): Bounds {\n if (rotation !== 0) {\n const cs = Math.cos(-rotation);\n const sn = Math.sin(-rotation);\n const dxLocal = cs * dx - sn * dy;\n const dyLocal = sn * dx + cs * dy;\n dx = dxLocal;\n dy = dyLocal;\n }\n let nx = ob.x;\n let ny = ob.y;\n let nw = ob.width;\n let nh = ob.height;\n if (anchor.x === 'min') {\n nw = ob.width + dx;\n } else if (anchor.x === 'max') {\n nx = ob.x + dx;\n nw = ob.width - dx;\n }\n if (anchor.y === 'min') {\n nh = ob.height + dy;\n } else if (anchor.y === 'max') {\n ny = ob.y + dy;\n nh = ob.height - dy;\n }\n return { x: nx, y: ny, width: nw, height: nh };\n}\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface ResizeScratch {\n /** Ids the gesture writes to. For group expansion this is the leaf set;\n * for the single-id path it's the original selection. */\n writeIds: NodeId[];\n scene: Scene<unknown, string, unknown>;\n /** World reads and local writes over the scene's composition strategy. */\n frame: PoseFrame<unknown>;\n /** Start poses keyed by write id, as stored — the `from` of each op. */\n startPoses: Map<NodeId, unknown>;\n /** Start poses in world — the frame the anchor math and the drag share. */\n startWorlds: Map<NodeId, unknown>;\n /** Shared origin bounds (union for group/multi-select, own bounds for\n * single). Used as the `src` rect for `geometry.remapBounds`. */\n originBounds: Bounds;\n anchor: ResizeAnchor;\n startWorld: { x: number; y: number };\n /** Rotation captured at gesture start (from `geometry.getRotation?`). */\n originRotation: number;\n /** World-space position of the diagonally opposite corner at start. Used\n * for the rotated-resize fixed-corner pin correction. */\n fixedWorld: { x: number; y: number };\n /** In-flight preview poses buffered during drag. */\n previews: Map<NodeId, unknown>;\n overrideEntries: Map<NodeId, { pose: unknown }>;\n geometry: PoseDescriptor<unknown>;\n behaviors: BoundsConstraint<Bounds>[];\n pointSnap: PointSnapBehavior<Bounds>[];\n /** Gesture context handed to behaviors. Reused across move/end. */\n gestureCtx: GestureContext<unknown>;\n /** Optional consumer commit hook captured at gesture start. When present,\n * the final pose commit routes through it (consumer history) instead of\n * `scene.applyBatch`. Undefined → fall back to `scene.applyBatch`. */\n applyOps?: (ops: Op[], label: string) => void;\n /** Optional eager-sync seam captured at gesture start. When present, the\n * commit emits a `setData` op per leaf mapping its data-held geometry by\n * the box→box affine the pose underwent. Undefined → pose-only commit. */\n geometryProjection?: GeometryProjection;\n /** Label, history and callbacks from the `resizePolicy` dep. */\n lifecycle: GestureLifecycle;\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Static descriptor for the `resize` Action.\n *\n * Requires dep-schema entries: `selection`, `scene`.\n * Reads optional `resizePolicy` dep when present.\n * Requires `InvocationCtx.drag.affordance` with a `handle:*` kind.\n */\nexport const resizeAction: Action & { requires: string[] } = inPlane({\n id: 'resize',\n label: 'Resize',\n // No default binding: a bare `{ kind: 'drag' }` claimed every drag at ambient\n // scope, tying areaSelect's. The resize handles bind it\n // (`selectionResizeBindings`, the `resize` preset).\n eligible: { capability: 'transforms-selection' },\n requires: ['selection', 'scene', 'resizePolicy', 'poseDescriptor', 'applyOps', 'geometryProjection', 'poseComposition', 'view'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, _opts): OngoingHandle {\n const selection = ctx.deps.selection;\n const scene = ctx.deps.scene;\n const applyOps = ctx.deps.applyOps;\n const geometryProjection = ctx.deps.geometryProjection;\n\n if (!selection || !scene) return {};\n\n const ids = selection.get() as NodeId[];\n if (ids.length === 0) return {};\n\n const affordance = ctx.drag?.affordance;\n const anchor = affordance?.anchor;\n if (!anchor) return {};\n\n const { behaviors, pointSnap, expandIds, geometry, policy } = resolveDeps(ctx);\n\n // Group expansion. When the starting selection is a single id but\n // `expandIds` returns a different set, we're in the group path:\n // every leaf is written, and origin bounds is the union AABB.\n // When `expandIds` returns the same list (identity default or no-op),\n // we write the selected ids directly.\n const expanded = expandIds(ids as unknown as string[]);\n if (expanded.length === 0) return {};\n\n const isGroupPath =\n ids.length === 1 && (expanded.length !== 1 || expanded[0] !== (ids[0] as string));\n const writeIds = (isGroupPath ? expanded : (ids as unknown as string[])) as NodeId[];\n\n // Capture start poses + per-leaf bounds. The drag delta is world, so\n // the anchor math runs on world poses — carried into the plane the edit\n // is measured in — and each result is carried back to its node's plane\n // and stored in the node's own parent frame.\n const frame = scenePoseFrame(scene, ctx.deps.poseComposition);\n const view = ctx.deps.view;\n /** A world result for `id`, as stored. */\n const stored = (id: NodeId, world: unknown): unknown => {\n const layer = scene.get(id)?.layer;\n return frame.local(id, carryPose(geometry, world, layer === undefined ? null : planeOf(view, layer)));\n };\n const startPoses = new Map<NodeId, unknown>();\n const startWorlds = new Map<NodeId, unknown>();\n const leafBounds: Bounds[] = [];\n for (const id of writeIds) {\n const node = scene.get(id);\n if (!node) continue;\n startPoses.set(id, node.pose);\n const world = carryPose(geometry, frame.world(id), invertPlane(planeOf(view, node.layer)));\n startWorlds.set(id, world);\n leafBounds.push(geometry.getBounds(world));\n }\n if (startPoses.size === 0) return {};\n\n // `leafBounds` is non-empty here (startPoses.size === 0 guarded above),\n // and the union branch only runs for length >= 2 → `!` is safe.\n const originBounds = leafBounds.length === 1\n ? leafBounds[0]\n : unionBounds(leafBounds)!;\n\n // Rotation captured only for the single-write-id path; group-resize\n // takes the AABB-frame (unrotated) path even if leaves have rotation.\n const originPose = !isGroupPath && writeIds.length === 1\n ? startWorlds.get(writeIds[0])\n : (originBounds as unknown);\n const originRotation = !isGroupPath\n ? (geometry.getRotation?.(originPose) ?? 0)\n : 0;\n\n const fixedLocal = fixedCornerOf(originBounds, anchor);\n const fixedWorld = originRotation === 0\n ? fixedLocal\n : rotatePoint(\n fixedLocal.x, fixedLocal.y,\n originBounds.x + originBounds.width / 2,\n originBounds.y + originBounds.height / 2,\n originRotation,\n );\n\n const startWorld = { x: ctx.drag?.start.x ?? 0, y: ctx.drag?.start.y ?? 0 };\n\n // Minimal GestureContext for behaviors. `draggedIds` matches the\n // legacy hook: the starting id (not the expanded leaf set).\n const readView = gestureViewReader(ctx.deps);\n const readPlane = gesturePlaneReader(ctx.deps);\n const gestureCtx: GestureContext<unknown> = {\n draggedIds: ids as unknown as string[],\n origin: new Map<string, unknown>([[ids[0] as string, originPose]]),\n current: new Map<string, unknown>([[ids[0] as string, originPose]]),\n snap: null,\n modifiers: { ...ctx.modifiers },\n pointer: { worldX: startWorld.x, worldY: startWorld.y, clientX: 0, clientY: 0 },\n view: readView(),\n plane: readPlane(),\n // `adapter` is unused by the kit's behaviors; cast to satisfy the type.\n adapter: undefined as unknown as GestureContext<unknown>['adapter'],\n scratch: {},\n };\n\n // Fire behavior onStart hooks (parity with `useResize`).\n const ctxAsRect = gestureCtx as unknown as GestureContext<Bounds>;\n for (const b of behaviors) b.onStart?.(ctxAsRect);\n\n const scratch: ResizeScratch = {\n writeIds,\n scene,\n frame,\n startPoses,\n startWorlds,\n originBounds,\n anchor,\n startWorld,\n originRotation,\n fixedWorld,\n previews: new Map<NodeId, unknown>(),\n overrideEntries: new Map<NodeId, { pose: unknown }>(),\n geometry,\n behaviors,\n pointSnap,\n gestureCtx,\n applyOps,\n geometryProjection,\n lifecycle: readGestureLifecycle(policy, 'Resize', behaviors),\n };\n const { lifecycle } = scratch;\n lifecycle.start(writeIds as string[]);\n\n // Returns whether anything reached the document.\n const commitResize = (): boolean => {\n const target = {\n scene,\n applyOps: scratch.applyOps,\n adapter: defaultCommitAdapter(scene, selection.adapterMethods),\n };\n const r = reduceBehaviorEnd(scratch.behaviors, scratch.gestureCtx as unknown as GestureContext<Bounds>);\n if (r === null) return false;\n if (r !== undefined) {\n commitGestureOps(target, lifecycle, r);\n return true;\n }\n\n if (scratch.previews.size === 0) return false;\n\n // One transform op per write id (from = pre-drag start pose, to =\n // final preview pose), committed as a single batch → one undo entry.\n const ops: Op[] = [];\n for (const id of scratch.writeIds) {\n const next = scratch.previews.get(id);\n if (next === undefined) continue;\n const from = scratch.startPoses.get(id);\n if (from === undefined) continue;\n let committed = next;\n // Eager-sync: map data-held geometry by the box→box affine the\n // pose underwent (start-pose bounds → final-pose bounds). Opt-in\n // via the geometryProjection seam; a no-op when the dep is absent.\n if (scratch.geometryProjection) {\n const node = scratch.scene.get(id);\n if (node) {\n const sb = scratch.geometry.getBounds(from);\n const db = scratch.geometry.getBounds(next);\n const m: Mat3 = boxToBox(\n sb.x, sb.y, sb.width, sb.height,\n db.x, db.y, db.width, db.height,\n );\n const dataOp = geometryDataOp(\n scratch.geometryProjection,\n { id: id as string, data: node.data, pose: from },\n m,\n lifecycle.label,\n );\n if (dataOp) {\n ops.push(dataOp);\n // The data absorbed the affine — including any mirror from a\n // drag past the opposite edge — so the pose must commit as a\n // normalized AABB. Leaving signed extents on the pose makes\n // the node unhittable (hit-testing assumes normalized boxes).\n // Without the seam the signed pose is kept: a bare-AABB pose\n // is the only carrier of the flip there.\n if (db.width < 0 || db.height < 0) {\n const nb = {\n ...db,\n x: db.width < 0 ? db.x + db.width : db.x,\n y: db.height < 0 ? db.y + db.height : db.y,\n width: Math.abs(db.width),\n height: Math.abs(db.height),\n };\n committed = scratch.geometry.remapBounds(next, db, nb) as typeof next;\n }\n }\n }\n }\n ops.push(createTransformOp<unknown>({\n id: id as string,\n from,\n to: committed,\n label: lifecycle.label,\n }));\n }\n if (ops.length === 0) return false;\n commitGestureOps(target, lifecycle, ops);\n return true;\n };\n\n return {\n kind: 'resize',\n onMove(mctx: InvocationCtx): void {\n if (!mctx.drag) return;\n const dx = mctx.drag.current.x - scratch.startWorld.x;\n const dy = mctx.drag.current.y - scratch.startWorld.y;\n\n // Refresh modifiers + pointer on the gesture ctx for behaviors.\n scratch.gestureCtx.modifiers = { ...mctx.modifiers };\n scratch.gestureCtx.pointer = {\n worldX: mctx.drag.current.x,\n worldY: mctx.drag.current.y,\n clientX: 0,\n clientY: 0,\n };\n scratch.gestureCtx.view = readView();\n scratch.gestureCtx.plane = readPlane();\n\n let proposedBounds = computeProposedBounds(\n scratch.originBounds,\n scratch.anchor,\n dx,\n dy,\n scratch.originRotation,\n );\n\n // Behaviors run in bounds-space.\n const behaviorCtx = scratch.gestureCtx as unknown as GestureContext<Bounds>;\n for (const b of scratch.behaviors) {\n const r = b.onMove?.(behaviorCtx, {\n pose: proposedBounds,\n anchor: scratch.anchor,\n });\n if (!r) continue;\n if (r.pose !== undefined) {\n proposedBounds = {\n x: r.pose.x, y: r.pose.y, width: r.pose.width, height: r.pose.height,\n };\n }\n }\n\n // Project bounds → pose. For the group path, each leaf is remapped\n // from its own startPose; for single, from the start pose directly.\n //\n // A rotated leaf in a group needs its own frame: `src`/`dst` are\n // world-frame here, unlike the single path where the drag delta was\n // already projected into the leaf's frame, so the naive affine\n // scales the wrong pair of extents. See `remapRotatedLeaf`.\n const computePose = (id: NodeId): unknown => {\n const sp = scratch.startWorlds.get(id);\n if (sp === undefined) return undefined;\n if (isGroupPath && poseRotationOf(sp) !== null) {\n return remapRotatedLeaf(\n sp as RotatedPose, scratch.originBounds, proposedBounds,\n );\n }\n return scratch.geometry.remapBounds(sp, scratch.originBounds, proposedBounds);\n };\n\n // Single-id path: apply rotation correction + point-snap, then\n // store. Group path: rotation correction + point-snap don't apply\n // (group resize is AABB-frame).\n scratch.previews.clear();\n\n if (!isGroupPath && scratch.writeIds.length === 1) {\n const id = scratch.writeIds[0];\n let proposedPose = computePose(id);\n if (proposedPose === undefined) { syncPreviewOverrides(scratch); return; }\n\n if (scratch.originRotation !== 0) {\n const newCenterX = proposedBounds.x + proposedBounds.width / 2;\n const newCenterY = proposedBounds.y + proposedBounds.height / 2;\n const newFixedLocal = fixedCornerOf(proposedBounds, scratch.anchor);\n const newFixedWorld = rotatePoint(\n newFixedLocal.x, newFixedLocal.y,\n newCenterX, newCenterY,\n scratch.originRotation,\n );\n const correctionX = scratch.fixedWorld.x - newFixedWorld.x;\n const correctionY = scratch.fixedWorld.y - newFixedWorld.y;\n proposedPose = translatePoseViaDescriptor(proposedPose, correctionX, correctionY, scratch.geometry);\n }\n\n if (scratch.pointSnap.length > 0) {\n const rotation = scratch.originRotation;\n const poseAsRect = proposedPose as Bounds;\n const psCtx = buildPointSnapContext(\n poseAsRect,\n rotation,\n scratch.anchor,\n scratch.gestureCtx.modifiers,\n );\n // Point snaps are the surface's rules over the camera's world, so\n // on a plane their points cross at this boundary.\n const plane = scratch.gestureCtx.plane ?? null;\n for (const beh of scratch.pointSnap) {\n const result = plane\n ? pointSnapFromPlane(beh.onMove(pointSnapToCamera(psCtx, plane)), plane)\n : beh.onMove(psCtx);\n if (result) {\n proposedPose = applyPointSnap(poseAsRect, rotation, result, psCtx) as unknown;\n break;\n }\n }\n }\n\n scratch.previews.set(id, stored(id, proposedPose));\n scratch.gestureCtx.current = new Map<string, unknown>([[id as string, proposedPose]]);\n } else {\n // Group / multi path: remap each leaf and store.\n for (const id of scratch.writeIds) {\n const next = computePose(id);\n if (next === undefined) continue;\n scratch.previews.set(id, stored(id, next));\n }\n }\n\n syncPreviewOverrides(scratch);\n },\n\n onEnd(_endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n let committed = false;\n try {\n if (reason === 'commit') committed = commitResize();\n else runBehaviorCancel(scratch.behaviors, scratch.gestureCtx as unknown as GestureContext<Bounds>);\n } finally {\n dropPreviewOverrides(scratch);\n scratch.previews.clear();\n lifecycle.end(committed);\n }\n },\n previewIds: () => scratch.previews.keys(),\n previewPose: (id: string) => scratch.previews.get(id as NodeId) ?? null,\n };\n },\n },\n /**\n * Always enabled at the descriptor level. The dispatcher's\n * specificity-fallthrough loop treats anything `!== true` as \"skip and\n * try the next match\" — returning `ActionDisabledReason.SelectionRequired`\n * here makes the resize binding silently lose to lower-specificity\n * matches (e.g. ambient `insertAction`), inserting phantom nodes on\n * every drag instead of resizing. The invoker self-guards on empty\n * selection (returns `{}`), so a static `true` here is correct.\n *\n * Fix: previously `() => ActionDisabledReason.SelectionRequired`\n * — that broke every dispatcher-routed resize. See git blame.\n */\n enabled: () => true,\n}, selectionLayer);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"rotate.js","names":["applyRotationDelta","rotated","d","pose","originRotation","delta","useUnionPivot","cos","sin","ox","originCenter","unionCenter","oy","nx","ny","translatePoseViaDescriptor","rotateAction","inPlane","selection","ctx","scene","applyOps","params","resolveParams","opts","behaviors","ids","descriptor","poseDescriptorOf","frame","scenePoseFrame","originPoses","originWorlds","originCenters","originRotations","visual","id","node","g","poseDescriptorForNode","world","carryPose","invertPlane","planeOf","b","visualBoundsViaDescriptor","union","unionAABB","startPointerAngle","readView","gestureViewReader","view","readPlane","gesturePlaneReader","scratch","\"pivot\"","readGestureLifecycle","p","moveGestureAdapter","turnedWorlds","out","origin","syncGestureCurrent","turned","shapeDelta","gctx","moveCtx","primary","raw","proposed","r","rotation","commitRotate","target","defaultCommitAdapter","reduceBehaviorEnd","commitGestureOps","lifecycle","ops","next","from","createTransformOp","recomputePreviews","layer","own","committed","reason","runBehaviorCancel","dropPreviewOverrides","selectionLayer"],"sources":["../../../../src/interactions/actions/defaults/rotate.ts"],"sourcesContent":["/**\n * `rotateAction` — ongoing Action descriptor for pointer-driven rotation.\n *\n * ## Status: REAL (unrotated-pivot path)\n *\n * Implements the core rotation math from `useRotate`:\n * - `start`: captures origin poses + AABB centers; computes start pointer\n * angle around the union center of selected nodes.\n * - `onMove`: derives pointer angle delta from start, applies to each node's\n * origin rotation. In union-pivot mode (multi-selection) orbits each item's\n * center around the union center.\n * - `onEnd('commit')`: builds one `createTransformOp` per node (from =\n * pre-mutation origin pose, to = rotated pose) and commits them as a\n * single batch → one undo entry for the whole drag. Routes through the\n * optional `applyOps` dep (consumer history) when present, else\n * `scene.applyBatch` + `defaultCommitAdapter`.\n * - `onEnd('cancel')`: no scene writes (scene never mutated during drag).\n *\n * ## Constraints vs `useRotate`\n *\n * - Reads and writes poses through the `poseDescriptor` dep; a pose whose\n * descriptor has no `withRotation`, or reports `supportsRotation` false, is\n * left alone.\n * - Behaviors (`opts.behaviors`) see the first rotated node's turned pose; a\n * pose one returns from `onMove` sets the rotation for the whole selection.\n * - No overlay rendering — deferred to Phase 7 overlay surface.\n * - Shift-snap (15° quantum) is NOT wired in this phase — omitted deliberately\n * to keep the invoker self-contained (would need to read shift from onMove ctx).\n * TODO: thread shift from InvocationCtx.modifiers.shift into snap logic.\n */\n\nimport { gesturePlaneReader, gestureViewReader } from '../../gestures/shared/screenTolerance';\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle } from '@weasel-js/routing';\nimport { resolveParams } from '@weasel-js/routing';\nimport type { Scene, NodeId } from 'core/scene/types';\nimport { syncPreviewOverrides, dropPreviewOverrides } from '../previewOverrides';\nimport type { Op } from 'core/ops/types';\nimport { createTransformOp } from 'core/ops/transform';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport type { SelectionApi } from 'core/selection/useSelection';\nimport { unionAABB } from 'core/geometry/unionBounds';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport {\n poseDescriptorForNode,\n translatePoseViaDescriptor,\n visualBoundsViaDescriptor,\n type PoseDescriptor,\n} from '../resize/geometry';\nimport { scenePoseFrame, type PoseFrame } from '../poseFrame';\nimport { commitGestureOps, readGestureLifecycle, reduceBehaviorEnd, runBehaviorCancel, type GestureLifecycle } from '../gestureLifecycle';\nimport { moveGestureAdapter } from '../move/gestureAdapter';\nimport type { GestureContext, RotateBehavior, RotateProposed } from '../../gestures/types';\nimport { carryPose, invertPlane, inPlane, planeOf, selectionLayer } from '../planeInput';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Apply a rotation delta (radians) to a pose, optionally orbiting the item's\n * center around a shared union center. */\nfunction applyRotationDelta(\n d: PoseDescriptor<unknown>,\n pose: unknown,\n originRotation: number,\n delta: number,\n originCenter: { x: number; y: number },\n unionCenter: { x: number; y: number },\n useUnionPivot: boolean,\n): unknown {\n const rotated = d.withRotation!(pose, originRotation + delta);\n if (!useUnionPivot) return rotated;\n const cos = Math.cos(delta);\n const sin = Math.sin(delta);\n const ox = originCenter.x - unionCenter.x;\n const oy = originCenter.y - unionCenter.y;\n const nx = unionCenter.x + ox * cos - oy * sin;\n const ny = unionCenter.y + ox * sin + oy * cos;\n return translatePoseViaDescriptor(rotated, nx - originCenter.x, ny - originCenter.y, d);\n}\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface RotateScratch {\n ids: NodeId[];\n scene: Scene<unknown, string, unknown>;\n descriptor: PoseDescriptor<unknown>;\n /** World reads and local writes over the scene's composition strategy. */\n frame: PoseFrame<unknown>;\n /** Origin pose for each selected node as stored — the `from` of its op. */\n originPoses: Map<NodeId, unknown>;\n /** Origin pose for each selected node in world — what the gesture turns. */\n originWorlds: Map<NodeId, unknown>;\n /** AABB center for each selected node (from origin pose). */\n originCenters: Map<NodeId, { x: number; y: number }>;\n /** Per-node origin rotation (radians). */\n originRotations: Map<NodeId, number>;\n /** Union center of all selected nodes — the rotation pivot. */\n unionCenter: { x: number; y: number };\n /** Pointer angle around unionCenter at drag start. */\n startPointerAngle: number;\n /** Running delta — updated each onMove, applied once at commit. */\n currentDelta: number;\n /** Use union pivot (multi-select) vs each item's own center. */\n useUnionPivot: boolean;\n /** In-flight preview poses keyed by node id. */\n previews: Map<NodeId, unknown>;\n overrideEntries: Map<NodeId, { pose: unknown }>;\n /** Optional consumer commit hook captured at gesture start. When present,\n * ops-based commits route through it (consumer history) instead of\n * `scene.applyBatch`. Undefined → fall back to `scene.applyBatch`. */\n applyOps?: (ops: Op[], label: string) => void;\n lifecycle: GestureLifecycle;\n behaviors: RotateBehavior<unknown>[];\n /** Reused gesture context handed to behaviors across the drag. */\n gestureCtx: GestureContext<unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Static descriptor for the `rotate` Action.\n *\n * Requires dep-schema entries: `selection`, `scene`.\n *\n * Implements the unrotated-pivot rotation path from `useRotate`.\n *\n * @see useRotate — the React hook this descriptor mirrors for the rect case.\n */\nexport const rotateAction: Action & { requires: string[] } = inPlane({\n id: 'rotate',\n label: 'Rotate',\n // No default binding. It used to be a bare `{ kind: 'drag' }`, which made\n // any drag no active tool claimed rotate a non-empty selection. The rotation\n // handle binds it (`selectionTransformBindings`, the `transform` preset).\n eligible: { capability: 'transforms-selection' },\n requires: ['selection', 'scene', 'applyOps', 'poseDescriptor', 'poseComposition', 'view'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, opts): OngoingHandle {\n const selection = ctx.deps.selection as SelectionApi | undefined;\n const scene = ctx.deps.scene as Scene<unknown, string, unknown> | undefined;\n const applyOps = ctx.deps.applyOps as ((ops: Op[], label: string) => void) | undefined;\n const params = resolveParams(opts?.params);\n const behaviors = (opts?.behaviors ?? []) as RotateBehavior<unknown>[];\n\n if (!selection || !scene) return {};\n\n const ids = selection.get() as NodeId[];\n if (ids.length === 0) return {};\n\n const descriptor = poseDescriptorOf(ctx.deps.poseDescriptor);\n // The pivot the pointer orbits is a world point, so every pose the\n // gesture turns is a world pose. Each turned result is stored back in\n // the node's own parent frame at preview and commit alike.\n const frame = scenePoseFrame(scene, ctx.deps.poseComposition);\n const originPoses = new Map<NodeId, unknown>();\n const originWorlds = new Map<NodeId, unknown>();\n const originCenters = new Map<NodeId, { x: number; y: number }>();\n const originRotations = new Map<NodeId, number>();\n const visual: Bounds[] = [];\n\n for (const id of ids) {\n const node = scene.get(id);\n if (!node) continue;\n const g = poseDescriptorForNode(descriptor, node);\n if (!g.withRotation || g.supportsRotation?.(node.pose) === false) continue;\n originPoses.set(id, node.pose);\n // The pivot is a world point, so every measurement the gesture orbits\n // is taken from the world pose, not the stored one — carried into the\n // plane the edit is measured in, where the pointer's angle is read.\n const world = carryPose(g, frame.world(id), invertPlane(planeOf(ctx.deps.view, node.layer)));\n originWorlds.set(id, world);\n const b = g.getBounds(world);\n originCenters.set(id, { x: b.x + b.width / 2, y: b.y + b.height / 2 });\n originRotations.set(id, g.getRotation?.(world) ?? 0);\n visual.push(visualBoundsViaDescriptor(world, g));\n }\n\n if (originPoses.size === 0) return {};\n\n const union = unionAABB(visual)!;\n const unionCenter = { x: union.x + union.width / 2, y: union.y + union.height / 2 };\n const startPointerAngle = Math.atan2(\n ctx.world.y - unionCenter.y,\n ctx.world.x - unionCenter.x,\n );\n\n const readView = gestureViewReader(ctx.deps);\n const view = ctx.deps.view;\n\n const readPlane = gesturePlaneReader(ctx.deps);\n const scratch: RotateScratch = {\n ids,\n scene,\n descriptor,\n frame,\n originPoses,\n originWorlds,\n originCenters,\n originRotations,\n unionCenter,\n startPointerAngle,\n currentDelta: 0,\n useUnionPivot: ids.length > 1 && params?.['pivot'] !== 'each',\n previews: new Map<NodeId, unknown>(),\n overrideEntries: new Map<NodeId, { pose: unknown }>(),\n applyOps,\n lifecycle: readGestureLifecycle(params, 'Rotate', behaviors),\n behaviors,\n gestureCtx: {\n draggedIds: [...originPoses.keys()] as string[],\n origin: new Map([...originWorlds].map(([id, p]) => [id as string, p])),\n current: new Map([...originWorlds].map(([id, p]) => [id as string, p])),\n snap: null,\n modifiers: { ...ctx.modifiers },\n pointer: { worldX: ctx.world.x, worldY: ctx.world.y, clientX: 0, clientY: 0 },\n view: readView(),\n plane: readPlane(),\n adapter: moveGestureAdapter(scene) as unknown as GestureContext<unknown>['adapter'],\n scratch: {},\n },\n };\n const { lifecycle } = scratch;\n\n /** Each rotated node's world pose turned by `delta`. */\n const turnedWorlds = (delta: number): Map<NodeId, unknown> => {\n const out = new Map<NodeId, unknown>();\n for (const [id, origin] of scratch.originWorlds) {\n out.set(id, applyRotationDelta(\n scratch.descriptor,\n origin,\n scratch.originRotations.get(id) ?? 0,\n delta,\n scratch.originCenters.get(id) ?? { x: 0, y: 0 },\n scratch.unionCenter,\n scratch.useUnionPivot,\n ));\n }\n return out;\n };\n\n const syncGestureCurrent = (delta: number): Map<NodeId, unknown> => {\n const turned = turnedWorlds(delta);\n for (const [id, pose] of turned) scratch.gestureCtx.current.set(id as string, pose);\n return turned;\n };\n\n /** The pointer's delta as the behaviors reshape it. */\n const shapeDelta = (moveCtx: InvocationCtx, raw: number): number => {\n const gctx = scratch.gestureCtx;\n gctx.modifiers = { ...moveCtx.modifiers };\n gctx.pointer = { worldX: moveCtx.world.x, worldY: moveCtx.world.y, clientX: 0, clientY: 0 };\n gctx.view = readView();\n gctx.plane = readPlane();\n const primary = scratch.originWorlds.keys().next().value as NodeId;\n const originRotation = scratch.originRotations.get(primary) ?? 0;\n let delta = raw;\n let proposed: RotateProposed<unknown> = {\n pose: syncGestureCurrent(raw).get(primary),\n rotation: originRotation + raw,\n };\n for (const b of scratch.behaviors) {\n const r = b.onMove?.(gctx, proposed);\n if (!r || r.pose === undefined) continue;\n const rotation = scratch.descriptor.getRotation?.(r.pose) ?? 0;\n delta = rotation - originRotation;\n proposed = { pose: r.pose, rotation };\n }\n return delta;\n };\n\n // Returns whether anything reached the document.\n const commitRotate = (): boolean => {\n const target = {\n scene,\n applyOps: scratch.applyOps,\n adapter: defaultCommitAdapter(scene, selection.adapterMethods),\n };\n if (scratch.behaviors.length > 0) {\n syncGestureCurrent(scratch.currentDelta);\n const r = reduceBehaviorEnd(scratch.behaviors, scratch.gestureCtx);\n if (r === null) return false;\n if (r !== undefined) {\n commitGestureOps(target, lifecycle, r);\n return true;\n }\n }\n if (scratch.currentDelta === 0) return false;\n // One transform op per node: `from` is the origin pose captured at drag\n // start, `to` the rotated preview. A single batch, so one undo entry.\n const ops: Op[] = [];\n for (const id of scratch.ids) {\n const next = scratch.previews.get(id);\n if (next === undefined) continue;\n const from = scratch.originPoses.get(id);\n if (from === undefined) continue;\n ops.push(createTransformOp<unknown>({\n id: id as string,\n from,\n to: next,\n label: lifecycle.label,\n }));\n }\n if (ops.length === 0) return false;\n commitGestureOps(target, lifecycle, ops);\n return true;\n };\n\n const recomputePreviews = (delta: number) => {\n scratch.previews.clear();\n if (delta !== 0) {\n for (const [id, turned] of turnedWorlds(delta)) {\n const layer = scratch.scene.get(id)?.layer;\n const own = layer === undefined ? null : planeOf(view, layer);\n scratch.previews.set(id, scratch.frame.local(id, carryPose(scratch.descriptor, turned, own)));\n }\n }\n syncPreviewOverrides(scratch);\n };\n\n for (const b of behaviors) b.onStart?.(scratch.gestureCtx);\n lifecycle.start(scratch.gestureCtx.draggedIds);\n\n return {\n kind: 'rotate',\n onMove(moveCtx: InvocationCtx): void {\n const pointerAngle = Math.atan2(\n moveCtx.world.y - scratch.unionCenter.y,\n moveCtx.world.x - scratch.unionCenter.x,\n );\n const raw = pointerAngle - scratch.startPointerAngle;\n scratch.currentDelta = scratch.behaviors.length > 0 ? shapeDelta(moveCtx, raw) : raw;\n recomputePreviews(scratch.currentDelta);\n },\n onEnd(_endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n let committed = false;\n try {\n if (reason === 'commit') committed = commitRotate();\n else runBehaviorCancel(scratch.behaviors, scratch.gestureCtx);\n } finally {\n dropPreviewOverrides(scratch);\n scratch.previews.clear();\n lifecycle.end(committed);\n }\n },\n previewIds: () => scratch.previews.keys(),\n previewPose: (id: string) => scratch.previews.get(id as NodeId) ?? null,\n };\n },\n },\n enabled: () => true,\n}, selectionLayer);\n"],"mappings":"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"}
1
+ {"version":3,"file":"rotate.js","names":["applyRotationDelta","rotated","d","pose","originRotation","delta","useUnionPivot","cos","sin","ox","originCenter","unionCenter","oy","nx","ny","translatePoseViaDescriptor","rotateAction","inPlane","selection","ctx","scene","applyOps","params","resolveParams","opts","behaviors","ids","descriptor","poseDescriptorOf","frame","scenePoseFrame","originPoses","originWorlds","originCenters","originRotations","visual","id","node","g","poseDescriptorForNode","world","carryPose","invertPlane","planeOf","b","visualBoundsViaDescriptor","union","unionAABB","startPointerAngle","readView","gestureViewReader","view","readPlane","gesturePlaneReader","scratch","\"pivot\"","readGestureLifecycle","p","moveGestureAdapter","turnedWorlds","out","origin","syncGestureCurrent","turned","shapeDelta","gctx","moveCtx","primary","raw","proposed","r","rotation","commitRotate","target","defaultCommitAdapter","reduceBehaviorEnd","commitGestureOps","lifecycle","ops","next","from","createTransformOp","recomputePreviews","layer","own","committed","reason","runBehaviorCancel","dropPreviewOverrides","selectionLayer"],"sources":["../../../../src/interactions/actions/defaults/rotate.ts"],"sourcesContent":["/**\n * `rotateAction` — ongoing Action descriptor for pointer-driven rotation.\n *\n * ## Status: REAL (unrotated-pivot path)\n *\n * Implements the core rotation math from `useRotate`:\n * - `start`: captures origin poses + AABB centers; computes start pointer\n * angle around the union center of selected nodes.\n * - `onMove`: derives pointer angle delta from start, applies to each node's\n * origin rotation. In union-pivot mode (multi-selection) orbits each item's\n * center around the union center.\n * - `onEnd('commit')`: builds one `createTransformOp` per node (from =\n * pre-mutation origin pose, to = rotated pose) and commits them as a\n * single batch → one undo entry for the whole drag. Routes through the\n * optional `applyOps` dep (consumer history) when present, else\n * `scene.applyBatch` + `defaultCommitAdapter`.\n * - `onEnd('cancel')`: no scene writes (scene never mutated during drag).\n *\n * ## Constraints vs `useRotate`\n *\n * - Reads and writes poses through the `poseDescriptor` dep; a pose whose\n * descriptor has no `withRotation`, or reports `supportsRotation` false, is\n * left alone.\n * - Behaviors (`opts.behaviors`) see the first rotated node's turned pose; a\n * pose one returns from `onMove` sets the rotation for the whole selection.\n * - No overlay rendering — deferred to Phase 7 overlay surface.\n * - Shift-snap (15° quantum) is NOT wired in this phase — omitted deliberately\n * to keep the invoker self-contained (would need to read shift from onMove ctx).\n * TODO: thread shift from InvocationCtx.modifiers.shift into snap logic.\n */\n\nimport { gesturePlaneReader, gestureViewReader } from '../../gestures/shared/screenTolerance';\nimport type { Action } from '@weasel-js/routing';\nimport type { InvocationCtx, OngoingHandle } from '@weasel-js/routing';\nimport { resolveParams } from '@weasel-js/routing';\nimport type { Scene, NodeId } from 'core/scene/types';\nimport { syncPreviewOverrides, dropPreviewOverrides } from '../previewOverrides';\nimport type { Op } from 'core/ops/types';\nimport { createTransformOp } from 'core/ops/transform';\nimport { defaultCommitAdapter } from '../defaultCommitAdapter';\nimport { unionAABB } from 'core/geometry/unionBounds';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport {\n poseDescriptorForNode,\n translatePoseViaDescriptor,\n visualBoundsViaDescriptor,\n type PoseDescriptor,\n} from '../resize/geometry';\nimport { scenePoseFrame, type PoseFrame } from '../poseFrame';\nimport { commitGestureOps, readGestureLifecycle, reduceBehaviorEnd, runBehaviorCancel, type GestureLifecycle } from '../gestureLifecycle';\nimport { moveGestureAdapter } from '../move/gestureAdapter';\nimport type { GestureContext, RotateBehavior, RotateProposed } from '../../gestures/types';\nimport { carryPose, invertPlane, inPlane, planeOf, selectionLayer } from '../planeInput';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Apply a rotation delta (radians) to a pose, optionally orbiting the item's\n * center around a shared union center. */\nfunction applyRotationDelta(\n d: PoseDescriptor<unknown>,\n pose: unknown,\n originRotation: number,\n delta: number,\n originCenter: { x: number; y: number },\n unionCenter: { x: number; y: number },\n useUnionPivot: boolean,\n): unknown {\n const rotated = d.withRotation!(pose, originRotation + delta);\n if (!useUnionPivot) return rotated;\n const cos = Math.cos(delta);\n const sin = Math.sin(delta);\n const ox = originCenter.x - unionCenter.x;\n const oy = originCenter.y - unionCenter.y;\n const nx = unionCenter.x + ox * cos - oy * sin;\n const ny = unionCenter.y + ox * sin + oy * cos;\n return translatePoseViaDescriptor(rotated, nx - originCenter.x, ny - originCenter.y, d);\n}\n\n// ---------------------------------------------------------------------------\n// Internal scratch\n// ---------------------------------------------------------------------------\n\ninterface RotateScratch {\n ids: NodeId[];\n scene: Scene<unknown, string, unknown>;\n descriptor: PoseDescriptor<unknown>;\n /** World reads and local writes over the scene's composition strategy. */\n frame: PoseFrame<unknown>;\n /** Origin pose for each selected node as stored — the `from` of its op. */\n originPoses: Map<NodeId, unknown>;\n /** Origin pose for each selected node in world — what the gesture turns. */\n originWorlds: Map<NodeId, unknown>;\n /** AABB center for each selected node (from origin pose). */\n originCenters: Map<NodeId, { x: number; y: number }>;\n /** Per-node origin rotation (radians). */\n originRotations: Map<NodeId, number>;\n /** Union center of all selected nodes — the rotation pivot. */\n unionCenter: { x: number; y: number };\n /** Pointer angle around unionCenter at drag start. */\n startPointerAngle: number;\n /** Running delta — updated each onMove, applied once at commit. */\n currentDelta: number;\n /** Use union pivot (multi-select) vs each item's own center. */\n useUnionPivot: boolean;\n /** In-flight preview poses keyed by node id. */\n previews: Map<NodeId, unknown>;\n overrideEntries: Map<NodeId, { pose: unknown }>;\n /** Optional consumer commit hook captured at gesture start. When present,\n * ops-based commits route through it (consumer history) instead of\n * `scene.applyBatch`. Undefined → fall back to `scene.applyBatch`. */\n applyOps?: (ops: Op[], label: string) => void;\n lifecycle: GestureLifecycle;\n behaviors: RotateBehavior<unknown>[];\n /** Reused gesture context handed to behaviors across the drag. */\n gestureCtx: GestureContext<unknown>;\n}\n\n// ---------------------------------------------------------------------------\n// Descriptor\n// ---------------------------------------------------------------------------\n\n/**\n * @experimental\n * Static descriptor for the `rotate` Action.\n *\n * Requires dep-schema entries: `selection`, `scene`.\n *\n * Implements the unrotated-pivot rotation path from `useRotate`.\n *\n * @see useRotate — the React hook this descriptor mirrors for the rect case.\n */\nexport const rotateAction: Action & { requires: string[] } = inPlane({\n id: 'rotate',\n label: 'Rotate',\n // No default binding. It used to be a bare `{ kind: 'drag' }`, which made\n // any drag no active tool claimed rotate a non-empty selection. The rotation\n // handle binds it (`selectionRotateBindings`, the `rotate` preset).\n eligible: { capability: 'transforms-selection' },\n requires: ['selection', 'scene', 'applyOps', 'poseDescriptor', 'poseComposition', 'view'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, opts): OngoingHandle {\n const selection = ctx.deps.selection;\n const scene = ctx.deps.scene;\n const applyOps = ctx.deps.applyOps;\n const params = resolveParams(opts?.params);\n const behaviors = (opts?.behaviors ?? []) as RotateBehavior<unknown>[];\n\n if (!selection || !scene) return {};\n\n const ids = selection.get() as NodeId[];\n if (ids.length === 0) return {};\n\n const descriptor = poseDescriptorOf(ctx.deps.poseDescriptor);\n // The pivot the pointer orbits is a world point, so every pose the\n // gesture turns is a world pose. Each turned result is stored back in\n // the node's own parent frame at preview and commit alike.\n const frame = scenePoseFrame(scene, ctx.deps.poseComposition);\n const originPoses = new Map<NodeId, unknown>();\n const originWorlds = new Map<NodeId, unknown>();\n const originCenters = new Map<NodeId, { x: number; y: number }>();\n const originRotations = new Map<NodeId, number>();\n const visual: Bounds[] = [];\n\n for (const id of ids) {\n const node = scene.get(id);\n if (!node) continue;\n const g = poseDescriptorForNode(descriptor, node);\n if (!g.withRotation || g.supportsRotation?.(node.pose) === false) continue;\n originPoses.set(id, node.pose);\n // The pivot is a world point, so every measurement the gesture orbits\n // is taken from the world pose, not the stored one — carried into the\n // plane the edit is measured in, where the pointer's angle is read.\n const world = carryPose(g, frame.world(id), invertPlane(planeOf(ctx.deps.view, node.layer)));\n originWorlds.set(id, world);\n const b = g.getBounds(world);\n originCenters.set(id, { x: b.x + b.width / 2, y: b.y + b.height / 2 });\n originRotations.set(id, g.getRotation?.(world) ?? 0);\n visual.push(visualBoundsViaDescriptor(world, g));\n }\n\n if (originPoses.size === 0) return {};\n\n const union = unionAABB(visual)!;\n const unionCenter = { x: union.x + union.width / 2, y: union.y + union.height / 2 };\n const startPointerAngle = Math.atan2(\n ctx.world.y - unionCenter.y,\n ctx.world.x - unionCenter.x,\n );\n\n const readView = gestureViewReader(ctx.deps);\n const view = ctx.deps.view;\n\n const readPlane = gesturePlaneReader(ctx.deps);\n const scratch: RotateScratch = {\n ids,\n scene,\n descriptor,\n frame,\n originPoses,\n originWorlds,\n originCenters,\n originRotations,\n unionCenter,\n startPointerAngle,\n currentDelta: 0,\n useUnionPivot: ids.length > 1 && params?.['pivot'] !== 'each',\n previews: new Map<NodeId, unknown>(),\n overrideEntries: new Map<NodeId, { pose: unknown }>(),\n applyOps,\n lifecycle: readGestureLifecycle(params, 'Rotate', behaviors),\n behaviors,\n gestureCtx: {\n draggedIds: [...originPoses.keys()] as string[],\n origin: new Map([...originWorlds].map(([id, p]) => [id as string, p])),\n current: new Map([...originWorlds].map(([id, p]) => [id as string, p])),\n snap: null,\n modifiers: { ...ctx.modifiers },\n pointer: { worldX: ctx.world.x, worldY: ctx.world.y, clientX: 0, clientY: 0 },\n view: readView(),\n plane: readPlane(),\n adapter: moveGestureAdapter(scene) as unknown as GestureContext<unknown>['adapter'],\n scratch: {},\n },\n };\n const { lifecycle } = scratch;\n\n /** Each rotated node's world pose turned by `delta`. */\n const turnedWorlds = (delta: number): Map<NodeId, unknown> => {\n const out = new Map<NodeId, unknown>();\n for (const [id, origin] of scratch.originWorlds) {\n out.set(id, applyRotationDelta(\n scratch.descriptor,\n origin,\n scratch.originRotations.get(id) ?? 0,\n delta,\n scratch.originCenters.get(id) ?? { x: 0, y: 0 },\n scratch.unionCenter,\n scratch.useUnionPivot,\n ));\n }\n return out;\n };\n\n const syncGestureCurrent = (delta: number): Map<NodeId, unknown> => {\n const turned = turnedWorlds(delta);\n for (const [id, pose] of turned) scratch.gestureCtx.current.set(id as string, pose);\n return turned;\n };\n\n /** The pointer's delta as the behaviors reshape it. */\n const shapeDelta = (moveCtx: InvocationCtx, raw: number): number => {\n const gctx = scratch.gestureCtx;\n gctx.modifiers = { ...moveCtx.modifiers };\n gctx.pointer = { worldX: moveCtx.world.x, worldY: moveCtx.world.y, clientX: 0, clientY: 0 };\n gctx.view = readView();\n gctx.plane = readPlane();\n const primary = scratch.originWorlds.keys().next().value as NodeId;\n const originRotation = scratch.originRotations.get(primary) ?? 0;\n let delta = raw;\n let proposed: RotateProposed<unknown> = {\n pose: syncGestureCurrent(raw).get(primary),\n rotation: originRotation + raw,\n };\n for (const b of scratch.behaviors) {\n const r = b.onMove?.(gctx, proposed);\n if (!r || r.pose === undefined) continue;\n const rotation = scratch.descriptor.getRotation?.(r.pose) ?? 0;\n delta = rotation - originRotation;\n proposed = { pose: r.pose, rotation };\n }\n return delta;\n };\n\n // Returns whether anything reached the document.\n const commitRotate = (): boolean => {\n const target = {\n scene,\n applyOps: scratch.applyOps,\n adapter: defaultCommitAdapter(scene, selection.adapterMethods),\n };\n if (scratch.behaviors.length > 0) {\n syncGestureCurrent(scratch.currentDelta);\n const r = reduceBehaviorEnd(scratch.behaviors, scratch.gestureCtx);\n if (r === null) return false;\n if (r !== undefined) {\n commitGestureOps(target, lifecycle, r);\n return true;\n }\n }\n if (scratch.currentDelta === 0) return false;\n // One transform op per node: `from` is the origin pose captured at drag\n // start, `to` the rotated preview. A single batch, so one undo entry.\n const ops: Op[] = [];\n for (const id of scratch.ids) {\n const next = scratch.previews.get(id);\n if (next === undefined) continue;\n const from = scratch.originPoses.get(id);\n if (from === undefined) continue;\n ops.push(createTransformOp<unknown>({\n id: id as string,\n from,\n to: next,\n label: lifecycle.label,\n }));\n }\n if (ops.length === 0) return false;\n commitGestureOps(target, lifecycle, ops);\n return true;\n };\n\n const recomputePreviews = (delta: number) => {\n scratch.previews.clear();\n if (delta !== 0) {\n for (const [id, turned] of turnedWorlds(delta)) {\n const layer = scratch.scene.get(id)?.layer;\n const own = layer === undefined ? null : planeOf(view, layer);\n scratch.previews.set(id, scratch.frame.local(id, carryPose(scratch.descriptor, turned, own)));\n }\n }\n syncPreviewOverrides(scratch);\n };\n\n for (const b of behaviors) b.onStart?.(scratch.gestureCtx);\n lifecycle.start(scratch.gestureCtx.draggedIds);\n\n return {\n kind: 'rotate',\n onMove(moveCtx: InvocationCtx): void {\n const pointerAngle = Math.atan2(\n moveCtx.world.y - scratch.unionCenter.y,\n moveCtx.world.x - scratch.unionCenter.x,\n );\n const raw = pointerAngle - scratch.startPointerAngle;\n scratch.currentDelta = scratch.behaviors.length > 0 ? shapeDelta(moveCtx, raw) : raw;\n recomputePreviews(scratch.currentDelta);\n },\n onEnd(_endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n let committed = false;\n try {\n if (reason === 'commit') committed = commitRotate();\n else runBehaviorCancel(scratch.behaviors, scratch.gestureCtx);\n } finally {\n dropPreviewOverrides(scratch);\n scratch.previews.clear();\n lifecycle.end(committed);\n }\n },\n previewIds: () => scratch.previews.keys(),\n previewPose: (id: string) => scratch.previews.get(id as NodeId) ?? null,\n };\n },\n },\n enabled: () => true,\n}, selectionLayer);\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"selectAll.js","names":["selectAllAction","scene","deps","view","skipped","l","all","n"],"sources":["../../../../src/interactions/actions/defaults/selectAll.ts"],"sourcesContent":["import type { NodeId } from 'core/scene/types';\nimport type { Action } from '@weasel-js/routing';\n\n/** The slice of a scene this action reads. Structural, so a consumer store\n * that answers these is a valid `scene` dep without being a `Scene`. */\ninterface SelectAllScene {\n renderOrder?: () => Iterable<NodeId>;\n renderOrderNodes?: () => readonly { id: NodeId; layer: string }[];\n layers?: readonly { id: string; visible: boolean; locked?: boolean }[];\n /** Consulted when present; a store without it is judged by layer alone. */\n isLocked?: (id: NodeId) => boolean;\n}\n\n/**\n * @experimental\n * Static descriptor for the `selectAll` Action. Selects every scene node the\n * user can see and reach — nodes on a hidden or locked layer, or on a layer\n * the asking view does not paint, are left out, so Cmd+A then Delete cannot\n * take content that is off screen or locked.\n */\nexport const selectAllAction: Action & { requires: string[] } = {\n id: 'selectAll',\n label: 'Select All',\n defaultBinding: { kind: 'key', key: 'a', mods: { mod: true } },\n eligible: { capability: 'creates-selection' },\n requires: ['scene', 'selection', 'view'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const scene = deps.scene as SelectAllScene | undefined;\n const view = deps.view as { layerIsPainted?(layerId: string): boolean } | undefined;\n const skipped = new Set(\n (scene?.layers ?? []).filter((l) => !l.visible || l.locked === true).map((l) => l.id),\n );\n // Scene exposes `renderOrder()` (bottom→top iterable of NodeIds), not\n // the never-implemented `listAll()`. The node walk is the same sequence\n // and carries the layer each id sits on, which the id walk cannot.\n const filtered = skipped.size > 0 || view?.layerIsPainted !== undefined;\n const all: NodeId[] = !filtered || !scene?.renderOrderNodes\n ? (scene?.renderOrder ? [...scene.renderOrder()] : [])\n : scene.renderOrderNodes()\n .filter((n) => !skipped.has(n.layer)\n && view?.layerIsPainted?.(n.layer) !== false\n && scene.isLocked?.(n.id) !== true)\n .map((n) => n.id);\n if (all.length === 0) return;\n (deps.selection as { set(ids: NodeId[]): void } | undefined)?.set(all);\n },\n },\n};\n"],"mappings":";AAoBA,IAAaA,IAAmD;CAC9D,IAAI;CACJ,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAO,KAAK;EAAK,MAAM,EAAE,KAAK,GAAK;CAAE;CAC7D,UAAU,EAAE,YAAY,oBAAoB;CAC5C,UAAU;EAAC;EAAS;EAAa;CAAM;CACvC,SAAS;EACP,QAAQ;EACR,MAAM,MAAS;GACb,IAAMC,IAAQC,EAAK,OACbC,IAAOD,EAAK,MACZE,IAAU,IAAI,KACjBH,GAAO,UAAU,CAAC,EAAA,CAAG,QAAQ,MAAM,CAACI,EAAE,WAAWA,EAAE,WAAW,EAAI,CAAC,CAAC,KAAK,MAAMA,EAAE,EAAE,CACtF,GAKMC,IAAgB,EADLF,EAAQ,OAAO,KAAKD,GAAM,mBAAmB,KAAA,MAC3B,CAACF,GAAO,mBACtCA,GAAO,cAAc,CAAC,GAAGA,EAAM,YAAY,CAAC,IAAI,CAAC,IAClDA,EAAM,iBAAiB,CAAC,CACvB,QAAQ,MAAM,CAACG,EAAQ,IAAIG,EAAE,KAAK,KAC9BJ,GAAM,iBAAiBI,EAAE,KAAK,MAAM,MACpCN,EAAM,WAAWM,EAAE,EAAE,MAAM,EAAI,CAAC,CACpC,KAAK,MAAMA,EAAE,EAAE;GACpB,AAAID,EAAI,WAAW,KACnBJ,EAAM,WAAwD,IAAII,CAAG;EACvE;CACF;AACF"}
1
+ {"version":3,"file":"selectAll.js","names":["selectAllAction","scene","deps","view","skipped","l","all","n"],"sources":["../../../../src/interactions/actions/defaults/selectAll.ts"],"sourcesContent":["import type { NodeId } from 'core/scene/types';\nimport type { Action } from '@weasel-js/routing';\n\n/**\n * @experimental\n * Static descriptor for the `selectAll` Action. Selects every scene node the\n * user can see and reach — nodes on a hidden or locked layer, or on a layer\n * the asking view does not paint, are left out, so Cmd+A then Delete cannot\n * take content that is off screen or locked.\n */\nexport const selectAllAction: Action & { requires: string[] } = {\n id: 'selectAll',\n label: 'Select All',\n defaultBinding: { kind: 'key', key: 'a', mods: { mod: true } },\n eligible: { capability: 'creates-selection' },\n requires: ['scene', 'selection', 'view'],\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const scene = deps.scene;\n const view = deps.view;\n const skipped = new Set(\n (scene?.layers ?? []).filter((l) => !l.visible || l.locked === true).map((l) => l.id),\n );\n // Scene exposes `renderOrder()` (bottom→top iterable of NodeIds), not\n // the never-implemented `listAll()`. The node walk is the same sequence\n // and carries the layer each id sits on, which the id walk cannot.\n const filtered = skipped.size > 0 || view?.layerIsPainted !== undefined;\n const all: NodeId[] = !filtered || !scene?.renderOrderNodes\n ? (scene?.renderOrder ? [...scene.renderOrder()] : [])\n : scene.renderOrderNodes()\n .filter((n) => !skipped.has(n.layer)\n && view?.layerIsPainted?.(n.layer) !== false\n && scene.isLocked?.(n.id) !== true)\n .map((n) => n.id);\n if (all.length === 0) return;\n deps.selection?.set(all);\n },\n },\n};\n"],"mappings":";AAUA,IAAaA,IAAmD;CAC9D,IAAI;CACJ,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAO,KAAK;EAAK,MAAM,EAAE,KAAK,GAAK;CAAE;CAC7D,UAAU,EAAE,YAAY,oBAAoB;CAC5C,UAAU;EAAC;EAAS;EAAa;CAAM;CACvC,SAAS;EACP,QAAQ;EACR,MAAM,MAAS;GACb,IAAMC,IAAQC,EAAK,OACbC,IAAOD,EAAK,MACZE,IAAU,IAAI,KACjBH,GAAO,UAAU,CAAC,EAAA,CAAG,QAAQ,MAAM,CAACI,EAAE,WAAWA,EAAE,WAAW,EAAI,CAAC,CAAC,KAAK,MAAMA,EAAE,EAAE,CACtF,GAKMC,IAAgB,EADLF,EAAQ,OAAO,KAAKD,GAAM,mBAAmB,KAAA,MAC3B,CAACF,GAAO,mBACtCA,GAAO,cAAc,CAAC,GAAGA,EAAM,YAAY,CAAC,IAAI,CAAC,IAClDA,EAAM,iBAAiB,CAAC,CACvB,QAAQ,MAAM,CAACG,EAAQ,IAAIG,EAAE,KAAK,KAC9BJ,GAAM,iBAAiBI,EAAE,KAAK,MAAM,MACpCN,EAAM,WAAWM,EAAE,EAAE,MAAM,EAAI,CAAC,CACpC,KAAK,MAAMA,EAAE,EAAE;GACpB,AAAID,EAAI,WAAW,KACnBJ,EAAK,WAAW,IAAII,CAAG;EACzB;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"slice.js","names":["sliceAction","dep","ctx","\"slice\"","a","current","trail","open","freehand","resolveParams","opts","\"cut\"","cut","p","moveCtx","reason","endCtx","deps","ActionDisabledReason"],"sources":["../../../../src/interactions/actions/defaults/slice.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport type { SliceDep } from '../depSchema';\nimport { ActionDisabledReason, resolveParams } from '@weasel-js/routing';\nimport type { BindingOpts, InvocationCtx, OngoingHandle, OngoingOverlay, Point2 } from '@weasel-js/routing';\n\n/**\n * @experimental\n * Static descriptor for the `slice` Action.\n *\n * Ongoing drag invoker: tracks the cut while the gesture is in flight,\n * publishes it as a `'cut'` overlay, and on commit hands it to\n * `SliceDep.commit(cut)`. No-ops gracefully when the `slice` dep is absent.\n *\n * The cut is a straight line from drag start to the pointer by default. With\n * the binding param `cut: 'freehand'` it is the whole drag trail instead.\n */\nexport const sliceAction: Action & { requires: string[] } = {\n id: 'slice',\n label: 'Slice',\n group: 'edit',\n eligible: { capability: 'edits-page' },\n requires: ['slice'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle {\n const dep = ctx.deps['slice'] as SliceDep | undefined;\n const a: Point2 = ctx.drag?.start ?? ctx.world;\n let current: Point2 = ctx.drag?.current ?? ctx.world;\n let trail: ReadonlyArray<Point2> | undefined;\n let open = true;\n\n const freehand = () => resolveParams(opts?.params)?.['cut'] === 'freehand';\n const cut = (): Point2[] =>\n freehand() && trail && trail.length >= 2\n ? trail.map((p) => ({ x: p.x, y: p.y }))\n : [a, current];\n\n return {\n kind: 'slice',\n onMove(moveCtx: InvocationCtx): void {\n current = moveCtx.drag?.current ?? moveCtx.world;\n trail = moveCtx.drag?.points;\n },\n overlay(): OngoingOverlay | null {\n if (!open) return null;\n return { kind: 'polyline', points: cut(), role: 'cut' };\n },\n onEnd(endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n open = false;\n if (reason === 'cancel' || !dep) return;\n current = endCtx.drag?.current ?? endCtx.world;\n trail = endCtx.drag?.points ?? trail;\n dep.commit(cut());\n },\n };\n },\n },\n // Slice can only act when a `slice` dep is wired (its `onEnd` no-ops\n // otherwise), so reflect dep presence for any UI reading action-enabled\n // state. The dispatcher still self-guards via the empty-handle path.\n enabled: (deps) => (deps?.['slice'] ? true : ActionDisabledReason.NotApplicable),\n};\n"],"mappings":";;AAgBA,IAAaA,IAA+C;CAC1D,IAAI;CACJ,OAAO;CACP,OAAO;CACP,UAAU,EAAE,YAAY,aAAa;CACrC,UAAU,CAAC,OAAO;CAClB,SAAS;EACP,QAAQ;EACR,MAAM,GAAoB,GAAmC;GAC3D,IAAMC,IAAMC,EAAI,KAAKC,OACfC,IAAYF,EAAI,MAAM,SAASA,EAAI,OACrCG,IAAkBH,EAAI,MAAM,WAAWA,EAAI,OAC3CI,GACAC,IAAO,IAELC,UAAiBC,EAAcC,GAAM,MAAM,CAAC,EAAGC,QAAW,YAC1DC,UACJJ,EAAS,KAAKF,KAASA,EAAM,UAAU,IACnCA,EAAM,KAAK,OAAO;IAAE,GAAGO,EAAE;IAAG,GAAGA,EAAE;GAAE,EAAE,IACrC,CAACT,GAAGC,CAAO;GAEjB,OAAO;IACL,MAAM;IACN,OAAO,GAA8B;KAEnC,AADA,IAAUS,EAAQ,MAAM,WAAWA,EAAQ,OAC3C,IAAQA,EAAQ,MAAM;IACxB;IACA,UAAiC;KAE/B,OADKP,IACE;MAAE,MAAM;MAAY,QAAQK,EAAI;MAAG,MAAM;KAAM,IADpC;IAEpB;IACA,MAAM,GAAuB,GAAmC;KAE9D,AADA,IAAO,IACHG,MAAW,YAAad,MAC5B,IAAUe,EAAO,MAAM,WAAWA,EAAO,OACzC,IAAQA,EAAO,MAAM,UAAUV,GAC/BL,EAAI,OAAOW,EAAI,CAAC;IAClB;GACF;EACF;CACF;CAIA,UAAU,MAAUK,GAAOd,QAAW,KAAOe,EAAqB;AACpE"}
1
+ {"version":3,"file":"slice.js","names":["sliceAction","dep","ctx","a","current","trail","open","freehand","resolveParams","opts","\"cut\"","cut","p","moveCtx","reason","endCtx","deps","\"slice\"","ActionDisabledReason"],"sources":["../../../../src/interactions/actions/defaults/slice.ts"],"sourcesContent":["import type { Action } from '@weasel-js/routing';\nimport { ActionDisabledReason, resolveParams } from '@weasel-js/routing';\nimport type { BindingOpts, InvocationCtx, OngoingHandle, OngoingOverlay, Point2 } from '@weasel-js/routing';\n\n/**\n * @experimental\n * Static descriptor for the `slice` Action.\n *\n * Ongoing drag invoker: tracks the cut while the gesture is in flight,\n * publishes it as a `'cut'` overlay, and on commit hands it to\n * `SliceDep.commit(cut)`. No-ops gracefully when the `slice` dep is absent.\n *\n * The cut is a straight line from drag start to the pointer by default. With\n * the binding param `cut: 'freehand'` it is the whole drag trail instead.\n */\nexport const sliceAction: Action & { requires: string[] } = {\n id: 'slice',\n label: 'Slice',\n group: 'edit',\n eligible: { capability: 'edits-page' },\n requires: ['slice'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle {\n const dep = ctx.deps.slice;\n const a: Point2 = ctx.drag?.start ?? ctx.world;\n let current: Point2 = ctx.drag?.current ?? ctx.world;\n let trail: ReadonlyArray<Point2> | undefined;\n let open = true;\n\n const freehand = () => resolveParams(opts?.params)?.['cut'] === 'freehand';\n const cut = (): Point2[] =>\n freehand() && trail && trail.length >= 2\n ? trail.map((p) => ({ x: p.x, y: p.y }))\n : [a, current];\n\n return {\n kind: 'slice',\n onMove(moveCtx: InvocationCtx): void {\n current = moveCtx.drag?.current ?? moveCtx.world;\n trail = moveCtx.drag?.points;\n },\n overlay(): OngoingOverlay | null {\n if (!open) return null;\n return { kind: 'polyline', points: cut(), role: 'cut' };\n },\n onEnd(endCtx: InvocationCtx, reason: 'commit' | 'cancel'): void {\n open = false;\n if (reason === 'cancel' || !dep) return;\n current = endCtx.drag?.current ?? endCtx.world;\n trail = endCtx.drag?.points ?? trail;\n dep.commit(cut());\n },\n };\n },\n },\n // Slice can only act when a `slice` dep is wired (its `onEnd` no-ops\n // otherwise), so reflect dep presence for any UI reading action-enabled\n // state. The dispatcher still self-guards via the empty-handle path.\n enabled: (deps) => (deps?.['slice'] ? true : ActionDisabledReason.NotApplicable),\n};\n"],"mappings":";;AAeA,IAAaA,IAA+C;CAC1D,IAAI;CACJ,OAAO;CACP,OAAO;CACP,UAAU,EAAE,YAAY,aAAa;CACrC,UAAU,CAAC,OAAO;CAClB,SAAS;EACP,QAAQ;EACR,MAAM,GAAoB,GAAmC;GAC3D,IAAMC,IAAMC,EAAI,KAAK,OACfC,IAAYD,EAAI,MAAM,SAASA,EAAI,OACrCE,IAAkBF,EAAI,MAAM,WAAWA,EAAI,OAC3CG,GACAC,IAAO,IAELC,UAAiBC,EAAcC,GAAM,MAAM,CAAC,EAAGC,QAAW,YAC1DC,UACJJ,EAAS,KAAKF,KAASA,EAAM,UAAU,IACnCA,EAAM,KAAK,OAAO;IAAE,GAAGO,EAAE;IAAG,GAAGA,EAAE;GAAE,EAAE,IACrC,CAACT,GAAGC,CAAO;GAEjB,OAAO;IACL,MAAM;IACN,OAAO,GAA8B;KAEnC,AADA,IAAUS,EAAQ,MAAM,WAAWA,EAAQ,OAC3C,IAAQA,EAAQ,MAAM;IACxB;IACA,UAAiC;KAE/B,OADKP,IACE;MAAE,MAAM;MAAY,QAAQK,EAAI;MAAG,MAAM;KAAM,IADpC;IAEpB;IACA,MAAM,GAAuB,GAAmC;KAE9D,AADA,IAAO,IACHG,MAAW,YAAab,MAC5B,IAAUc,EAAO,MAAM,WAAWA,EAAO,OACzC,IAAQA,EAAO,MAAM,UAAUV,GAC/BJ,EAAI,OAAOU,EAAI,CAAC;IAClB;GACF;EACF;CACF;CAIA,UAAU,MAAUK,GAAOC,QAAW,KAAOC,EAAqB;AACpE"}
@@ -1 +1 @@
1
- {"version":3,"file":"toolActivate.js","names":["TOOL_ACTIVATE_ID","buildToolActivateBindings","specs","mods","keyOpts","makeToolActivateAction","bindings","activeTool","deps","toolId","params","isEligible"],"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';\nimport type { ActiveToolContextValue } from '@weasel-js/routing/react';\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. They used to be spread flat and the mismatch was hidden by an\n // `as never` cast, so the matcher — which treats an absent modifier as\n // \"must NOT be held\" — could never match a modifier-qualified shortcut\n // like Cmd+D. Nobody noticed while `useKeybindings`'s own document\n // listener was doing the real matching.\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; the invoker reads\n * `params.toolId` and calls `activeTool.setActive(toolId)`. Imperative\n * callers (palette, toolbar) reach the same effect via\n * `registry.trigger('tool.activate', { toolId })`.\n *\n * `isEligible` gates activation on the tool's `eligibility.capabilities` vs the active\n * mode — the same predicate `ToolPalette` uses to grey a button out. It has\n * to live in the invoker rather than in `Action.eligible` because eligibility\n * here is per-TOOL (read off `params.toolId`), while `Action.eligible` is a\n * static per-action descriptor. Omit it (or pass one that always returns\n * true) 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 as ActiveToolContextValue | undefined;\n const toolId = params?.toolId as string | undefined;\n if (!activeTool || !toolId) return;\n // Greyed out in the palette ⇒ not reachable by shortcut either. Before\n // this, `ToolDef.capabilities` described an intent the runtime never\n // enforced: in text-edit mode the palette greyed the pen button while\n // `P` still activated pen.\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":";AAQA,IAAaA,IAAmB;AAoBhC,SAAgBC,EACd,GACgB;CAChB,OAAOC,EAAM,KAAK,EAAE,WAAQ,iBAAc;EAOxC,IAAMC,IAA6C,CAAC;EAIpD,OAHIC,EAAQ,QAAQ,KAAA,MAAW,EAAK,MAAMA,EAAQ,MAC9CA,EAAQ,QAAQ,KAAA,MAAW,EAAK,MAAMA,EAAQ,MAC9CA,EAAQ,UAAU,KAAA,MAAW,EAAK,QAAQA,EAAQ,QAC/C;GACL,MAAM;IACJ,MAAM;IACN,KAAKA,EAAQ;IACb,GAAI,OAAO,KAAKD,CAAI,CAAC,CAAC,SAAS,IAAI,EAAE,QAAK,IAAI,CAAC;GACjD;GACA,MAAM,EAAE,QAAQ,EAAE,UAAO,EAAE;EAC7B;CACF,CAAC;AACH;AAcA,SAAgBE,EACd,GACA,GACQ;CAgBR,OAAO;EACL,IAAIL;EACJ,OAAO;EACP,gBAAgBM;EAChB,OAAO;EACP,SAAA;GAnBA,QAAQ;GACR,MAAM,GAAM,MAAW;IACrB,IAAMC,IAAaC,EAAK,YAClBC,IAASC,GAAQ;IACvB,AAAKH,KAAeE,MAKhB,MAAeE,EAAWF,CAAM,MACpCF,EAAW,UAAUE,CAAM;GAC7B;EAQA;EACA,UAAU,CAAC,YAAY;CACzB;AACF"}
1
+ {"version":3,"file":"toolActivate.js","names":["TOOL_ACTIVATE_ID","buildToolActivateBindings","specs","mods","keyOpts","makeToolActivateAction","bindings","activeTool","deps","toolId","params","isEligible"],"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. They used to be spread flat and the mismatch was hidden by an\n // `as never` cast, so the matcher — which treats an absent modifier as\n // \"must NOT be held\" — could never match a modifier-qualified shortcut\n // like Cmd+D. Nobody noticed while `useKeybindings`'s own document\n // listener was doing the real matching.\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; the invoker reads\n * `params.toolId` and calls `activeTool.setActive(toolId)`. Imperative\n * callers (palette, toolbar) reach the same effect via\n * `registry.trigger('tool.activate', { toolId })`.\n *\n * `isEligible` gates activation on the tool's `eligibility.capabilities` vs the active\n * mode — the same predicate `ToolPalette` uses to grey a button out. It has\n * to live in the invoker rather than in `Action.eligible` because eligibility\n * here is per-TOOL (read off `params.toolId`), while `Action.eligible` is a\n * static per-action descriptor. Omit it (or pass one that always returns\n * true) 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 // Greyed out in the palette ⇒ not reachable by shortcut either. Before\n // this, `ToolDef.capabilities` described an intent the runtime never\n // enforced: in text-edit mode the palette greyed the pen button while\n // `P` still activated pen.\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,IAAaA,IAAmB;AAoBhC,SAAgBC,EACd,GACgB;CAChB,OAAOC,EAAM,KAAK,EAAE,WAAQ,iBAAc;EAOxC,IAAMC,IAA6C,CAAC;EAIpD,OAHIC,EAAQ,QAAQ,KAAA,MAAW,EAAK,MAAMA,EAAQ,MAC9CA,EAAQ,QAAQ,KAAA,MAAW,EAAK,MAAMA,EAAQ,MAC9CA,EAAQ,UAAU,KAAA,MAAW,EAAK,QAAQA,EAAQ,QAC/C;GACL,MAAM;IACJ,MAAM;IACN,KAAKA,EAAQ;IACb,GAAI,OAAO,KAAKD,CAAI,CAAC,CAAC,SAAS,IAAI,EAAE,QAAK,IAAI,CAAC;GACjD;GACA,MAAM,EAAE,QAAQ,EAAE,UAAO,EAAE;EAC7B;CACF,CAAC;AACH;AAcA,SAAgBE,EACd,GACA,GACQ;CAgBR,OAAO;EACL,IAAIL;EACJ,OAAO;EACP,gBAAgBM;EAChB,OAAO;EACP,SAAA;GAnBA,QAAQ;GACR,MAAM,GAAM,MAAW;IACrB,IAAMC,IAAaC,EAAK,YAClBC,IAASC,GAAQ;IACvB,AAAKH,KAAeE,MAKhB,MAAeE,EAAWF,CAAM,MACpCF,EAAW,UAAUE,CAAM;GAC7B;EAQA;EACA,UAAU,CAAC,YAAY;CACzB;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"toolResetToDefault.js","names":["TOOL_RESET_TO_DEFAULT_ID","makeToolResetToDefaultAction","activeTool","deps","target","getTarget","ActionDisabledReason"],"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';\nimport type { ActiveToolContextValue } from '@weasel-js/routing/react';\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 * This used to live in a raw document `keydown` listener inside\n * `useKeybindings`, running alongside — and unaware of — the dispatcher's own\n * Escape bindings (`cancelGesture`, `exitPathEdit`, `escape`). Both fired on\n * the same keypress because they were separate listeners.\n *\n * As an Action it joins the dispatcher's Escape order, which is\n * first-match-wins. It registers at ambient scope, the lowest priority, so\n * one Escape press does exactly one 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 * That IS a behavior change from \"reset the tool *and* clear the selection on\n * the same press.\" That order is the coherent reading of Escape, and the old\n * pairing was an artifact of two uncoordinated listeners rather than a design.\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 as ActiveToolContextValue | undefined;\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":";;AAMA,IAAaA,IAA2B;AA0BxC,SAAgBC,EACd,GACQ;CAWR,OAAO;EACL,IAAID;EACJ,OAAO;EACP,gBAAgB;GAAE,MAAM;GAAO,KAAK;EAAS;EAC7C,SAAA;GAbA,QAAQ;GACR,MAAM,MAAS;IACb,IAAME,IAAaC,EAAK,YAClBC,IAASC,EAAU;IACzB,AAAKH,KAAeE,KACpBF,EAAW,UAAUE,CAAM;GAC7B;EAOA;EACA,UAAU,CAAC,YAAY;EAIvB,eAAgBC,EAAU,IAAI,KAAOC,EAAqB;CAC5D;AACF"}
1
+ {"version":3,"file":"toolResetToDefault.js","names":["TOOL_RESET_TO_DEFAULT_ID","makeToolResetToDefaultAction","activeTool","deps","target","getTarget","ActionDisabledReason"],"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 * This used to live in a raw document `keydown` listener inside\n * `useKeybindings`, running alongside — and unaware of — the dispatcher's own\n * Escape bindings (`cancelGesture`, `exitPathEdit`, `escape`). Both fired on\n * the same keypress because they were separate listeners.\n *\n * As an Action it joins the dispatcher's Escape order, which is\n * first-match-wins. It registers at ambient scope, the lowest priority, so\n * one Escape press does exactly one 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 * That IS a behavior change from \"reset the tool *and* clear the selection on\n * the same press.\" That order is the coherent reading of Escape, and the old\n * pairing was an artifact of two uncoordinated listeners rather than a design.\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,IAAaA,IAA2B;AA0BxC,SAAgBC,EACd,GACQ;CAWR,OAAO;EACL,IAAID;EACJ,OAAO;EACP,gBAAgB;GAAE,MAAM;GAAO,KAAK;EAAS;EAC7C,SAAA;GAbA,QAAQ;GACR,MAAM,MAAS;IACb,IAAME,IAAaC,EAAK,YAClBC,IAASC,EAAU;IACzB,AAAKH,KAAeE,KACpBF,EAAW,UAAUE,CAAM;GAC7B;EAOA;EACA,UAAU,CAAC,YAAY;EAIvB,eAAgBC,EAAU,IAAI,KAAOC,EAAqB;CAC5D;AACF"}
@@ -3,7 +3,7 @@ import { createElement as n } from "react";
3
3
  import { ActionDisabledReason as r } from "@weasel-js/routing";
4
4
  //#region src/interactions/actions/defaults/undoRedo.ts
5
5
  function i(e, t) {
6
- return (e?.history)?.[t]?.() ? !0 : r.NotApplicable;
6
+ return e?.history?.[t]?.() ? !0 : r.NotApplicable;
7
7
  }
8
8
  var a = {
9
9
  id: "undo",
@@ -1 +1 @@
1
- {"version":3,"file":"undoRedo.js","names":["historyCan","deps","which","ActionDisabledReason","undoAction","createElement","UndoIcon","redoAction","RedoIcon"],"sources":["../../../../src/interactions/actions/defaults/undoRedo.ts"],"sourcesContent":["import { createElement } from 'react';\nimport { RedoIcon, UndoIcon } from './icons/actionGlyphIcons';\nimport { ActionDisabledReason, type Action, type ActionDeps } from '@weasel-js/routing';\n\ntype HistoryReads = { canUndo?: () => boolean; canRedo?: () => boolean };\n\nfunction historyCan(deps: ActionDeps | undefined, which: 'canUndo' | 'canRedo'): true | ActionDisabledReason {\n const history = deps?.history as HistoryReads | undefined;\n return history?.[which]?.() ? true : ActionDisabledReason.NotApplicable;\n}\n\n/**\n * @experimental\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: createElement(UndoIcon),\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 as { undo?: () => boolean } | undefined)?.undo?.();\n },\n },\n enabled: (deps) => historyCan(deps, 'canUndo'),\n};\n\n/**\n * @experimental\n * Static descriptor for the `redo` Action.\n */\nexport const redoAction: Action & { requires: string[] } = {\n id: 'redo',\n label: 'Redo',\n icon: createElement(RedoIcon),\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 as { redo?: () => boolean } | undefined)?.redo?.();\n },\n },\n enabled: (deps) => historyCan(deps, 'canRedo'),\n};\n"],"mappings":";;;;AAMA,SAASA,EAAW,GAA8B,GAA2D;CAE3G,QADgBC,GAAM,QAAA,GACLC,EAAM,GAAG,IAAI,KAAOC,EAAqB;AAC5D;AAOA,IAAaC,IAA8C;CACzD,IAAI;CACJ,OAAO;CACP,MAAMC,EAAcC,CAAQ;CAC5B,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAO,KAAK;EAAK,MAAM,EAAE,KAAK,GAAK;CAAE;CAC7D,UAAU,CAAC,SAAS;CACpB,SAAS;EACP,QAAQ;EACR,MAAM,MAAS;GACb,EAAM,SAAkD,OAAO;EACjE;CACF;CACA,UAAU,MAASN,EAAWC,GAAM,SAAS;AAC/C,GAMaM,IAA8C;CACzD,IAAI;CACJ,OAAO;CACP,MAAMF,EAAcG,CAAQ;CAC5B,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAO,KAAK;EAAK,MAAM;GAAE,KAAK;GAAM,OAAO;EAAK;CAAE;CAC1E,UAAU,CAAC,SAAS;CACpB,SAAS;EACP,QAAQ;EACR,MAAM,MAAS;GACb,EAAM,SAAkD,OAAO;EACjE;CACF;CACA,UAAU,MAASR,EAAWC,GAAM,SAAS;AAC/C"}
1
+ {"version":3,"file":"undoRedo.js","names":["historyCan","deps","which","ActionDisabledReason","undoAction","createElement","UndoIcon","redoAction","RedoIcon"],"sources":["../../../../src/interactions/actions/defaults/undoRedo.ts"],"sourcesContent":["import { createElement } from 'react';\nimport { RedoIcon, UndoIcon } from './icons/actionGlyphIcons';\nimport { ActionDisabledReason, type Action, type ActionDeps } from '@weasel-js/routing';\n\nfunction historyCan(deps: ActionDeps | undefined, which: 'canUndo' | 'canRedo'): true | ActionDisabledReason {\n return deps?.history?.[which]?.() ? true : ActionDisabledReason.NotApplicable;\n}\n\n/**\n * @experimental\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: createElement(UndoIcon),\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 * @experimental\n * Static descriptor for the `redo` Action.\n */\nexport const redoAction: Action & { requires: string[] } = {\n id: 'redo',\n label: 'Redo',\n icon: createElement(RedoIcon),\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":";;;;AAIA,SAASA,EAAW,GAA8B,GAA2D;CAC3G,OAAOC,GAAM,UAAUC,EAAM,GAAG,IAAI,KAAOC,EAAqB;AAClE;AAOA,IAAaC,IAA8C;CACzD,IAAI;CACJ,OAAO;CACP,MAAMC,EAAcC,CAAQ;CAC5B,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAO,KAAK;EAAK,MAAM,EAAE,KAAK,GAAK;CAAE;CAC7D,UAAU,CAAC,SAAS;CACpB,SAAS;EACP,QAAQ;EACR,MAAM,MAAS;GACb,EAAK,SAAS,OAAO;EACvB;CACF;CACA,UAAU,MAASN,EAAWC,GAAM,SAAS;AAC/C,GAMaM,IAA8C;CACzD,IAAI;CACJ,OAAO;CACP,MAAMF,EAAcG,CAAQ;CAC5B,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAO,KAAK;EAAK,MAAM;GAAE,KAAK;GAAM,OAAO;EAAK;CAAE;CAC1E,UAAU,CAAC,SAAS;CACpB,SAAS;EACP,QAAQ;EACR,MAAM,MAAS;GACb,EAAK,SAAS,OAAO;EACvB;CACF;CACA,UAAU,MAASR,EAAWC,GAAM,SAAS;AAC/C"}