@weasel-js/core 1.15.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/dist/animation/poseTarget.js +3 -3
  3. package/dist/animation/poseTarget.js.map +1 -1
  4. package/dist/animation/reflow.js +7 -7
  5. package/dist/animation/reflow.js.map +1 -1
  6. package/dist/animation/rig/rigNode.js +2 -2
  7. package/dist/animation/rig/rigNode.js.map +1 -1
  8. package/dist/barrel/input.js +2 -1
  9. package/dist/barrel/paint.js +2 -2
  10. package/dist/barrel/viewport.js +3 -3
  11. package/dist/canvas/NodeShape.js +23 -10
  12. package/dist/canvas/NodeShape.js.map +1 -1
  13. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js +17 -64
  14. package/dist/canvas/SceneCanvas/PointerProviderIfRoot.js.map +1 -1
  15. package/dist/canvas/SceneCanvas/props.js.map +1 -1
  16. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js +1 -1
  17. package/dist/canvas/SceneCanvas/useSceneCanvasLayers.js.map +1 -1
  18. package/dist/canvas/SceneCanvas/useSceneRedrawSources.js +4 -1
  19. package/dist/canvas/SceneCanvas/useSceneRedrawSources.js.map +1 -1
  20. package/dist/canvas/SceneCanvas/useSceneSlotLayers.js +3 -3
  21. package/dist/canvas/SceneCanvas/useSceneSlotLayers.js.map +1 -1
  22. package/dist/canvas/SceneCanvas.js +1 -1
  23. package/dist/canvas/SceneCanvas.js.map +1 -1
  24. package/dist/canvas/SceneViewCanvas.js +4 -1
  25. package/dist/canvas/SceneViewCanvas.js.map +1 -1
  26. package/dist/canvas/accentColor.js +3 -3
  27. package/dist/canvas/accentColor.js.map +1 -1
  28. package/dist/canvas/deps/enteredGroup.js +5 -4
  29. package/dist/canvas/deps/enteredGroup.js.map +1 -1
  30. package/dist/canvas/deps/selectionSnapshot.js +7 -2
  31. package/dist/canvas/deps/selectionSnapshot.js.map +1 -1
  32. package/dist/canvas/nodeShapes/derivedPainter.js +57 -8
  33. package/dist/canvas/nodeShapes/derivedPainter.js.map +1 -1
  34. package/dist/canvas/nodeShapes/nodePaint.js +33 -2
  35. package/dist/canvas/nodeShapes/nodePaint.js.map +1 -1
  36. package/dist/canvas/nodeShapes/pathPainter.js +5 -4
  37. package/dist/canvas/nodeShapes/pathPainter.js.map +1 -1
  38. package/dist/canvas/nodeShapes/pickOutline.js +48 -0
  39. package/dist/canvas/nodeShapes/pickOutline.js.map +1 -0
  40. package/dist/canvas/nodeShapes/shapePainter.js +6 -4
  41. package/dist/canvas/nodeShapes/shapePainter.js.map +1 -1
  42. package/dist/canvas/paintOverrides.js +43 -0
  43. package/dist/canvas/paintOverrides.js.map +1 -0
  44. package/dist/canvas/renderSceneToPixels.js +1 -1
  45. package/dist/canvas/renderSceneToPixels.js.map +1 -1
  46. package/dist/canvas/sceneSlotWiring.js +3 -3
  47. package/dist/canvas/sceneSlotWiring.js.map +1 -1
  48. package/dist/canvas/sceneViewRender.js +11 -7
  49. package/dist/canvas/sceneViewRender.js.map +1 -1
  50. package/dist/canvas/slopsDebugLayer.js +8 -2
  51. package/dist/canvas/slopsDebugLayer.js.map +1 -1
  52. package/dist/canvas/trimOverrides.js +18 -0
  53. package/dist/canvas/trimOverrides.js.map +1 -0
  54. package/dist/canvas/useViewRegistrySurface.js +6 -3
  55. package/dist/canvas/useViewRegistrySurface.js.map +1 -1
  56. package/dist/canvas/viewRegistry.js +5 -5
  57. package/dist/canvas/viewRegistry.js.map +1 -1
  58. package/dist/core/ops/reorder/index.js +2 -6
  59. package/dist/core/ops/reorder/index.js.map +1 -1
  60. package/dist/core/pages/fitViewToPage.js +2 -7
  61. package/dist/core/pages/fitViewToPage.js.map +1 -1
  62. package/dist/core/pages/pages.js +3 -9
  63. package/dist/core/pages/pages.js.map +1 -1
  64. package/dist/core/paint/meshKindLazy.js +13 -6
  65. package/dist/core/paint/meshKindLazy.js.map +1 -1
  66. package/dist/core/scene/pathEffects/blend.js +2 -2
  67. package/dist/core/scene/pathEffects/blend.js.map +1 -1
  68. package/dist/core/scene/pathEffects/textAlong.js +6 -5
  69. package/dist/core/scene/pathEffects/textAlong.js.map +1 -1
  70. package/dist/core/scene/scene.js +12 -1
  71. package/dist/core/scene/scene.js.map +1 -1
  72. package/dist/core/viewport/clientToWorld.js +7 -1
  73. package/dist/core/viewport/clientToWorld.js.map +1 -1
  74. package/dist/core/viewport/zoomAt.js +6 -6
  75. package/dist/core/viewport/zoomAt.js.map +1 -1
  76. package/dist/devtools/root.js +11 -4
  77. package/dist/devtools/root.js.map +1 -1
  78. package/dist/devtools/transport.js +12 -16
  79. package/dist/devtools/transport.js.map +1 -1
  80. package/dist/devtools-protocol.d.ts +0 -11
  81. package/dist/domains/measure/measureHit.js +2 -8
  82. package/dist/domains/measure/measureHit.js.map +1 -1
  83. package/dist/domains/paths/anchorMarkers.js +5 -11
  84. package/dist/domains/paths/anchorMarkers.js.map +1 -1
  85. package/dist/domains/paths/pathEditingOverlayLayer.js +5 -9
  86. package/dist/domains/paths/pathEditingOverlayLayer.js.map +1 -1
  87. package/dist/domains/paths/pathLayer.js +20 -6
  88. package/dist/domains/paths/pathLayer.js.map +1 -1
  89. package/dist/domains/paths/penPreviewLayer.js +9 -12
  90. package/dist/domains/paths/penPreviewLayer.js.map +1 -1
  91. package/dist/domains/paths/shapeGenerator.js +1 -2
  92. package/dist/domains/paths/shapeGenerator.js.map +1 -1
  93. package/dist/domains/snap/angle2d.js +2 -1
  94. package/dist/domains/snap/angle2d.js.map +1 -1
  95. package/dist/domains/snap/outcomeStore.js +5 -8
  96. package/dist/domains/snap/outcomeStore.js.map +1 -1
  97. package/dist/domains/text/useSceneTextEdit.js +9 -4
  98. package/dist/domains/text/useSceneTextEdit.js.map +1 -1
  99. package/dist/domains/viewports/aim.js +3 -8
  100. package/dist/domains/viewports/aim.js.map +1 -1
  101. package/dist/domains/viewports/viewResolver.js +5 -3
  102. package/dist/domains/viewports/viewResolver.js.map +1 -1
  103. package/dist/index.d.ts +66 -50
  104. package/dist/index.js +5 -4
  105. package/dist/input/usePublishPointer.js +59 -0
  106. package/dist/input/usePublishPointer.js.map +1 -0
  107. package/dist/interactions/actions/defaults/containerScroll.js +9 -3
  108. package/dist/interactions/actions/defaults/containerScroll.js.map +1 -1
  109. package/dist/interactions/actions/defaults/enterGroup.js +3 -3
  110. package/dist/interactions/actions/defaults/enterGroup.js.map +1 -1
  111. package/dist/interactions/actions/defaults/pathEditMarks.js +2 -5
  112. package/dist/interactions/actions/defaults/pathEditMarks.js.map +1 -1
  113. package/dist/interactions/actions/edit-anchors/handleDrag.js +4 -7
  114. package/dist/interactions/actions/edit-anchors/handleDrag.js.map +1 -1
  115. package/dist/interactions/actions/move/behaviors/clampMove.js +42 -0
  116. package/dist/interactions/actions/move/behaviors/clampMove.js.map +1 -0
  117. package/dist/interactions/actions/move/layout/container.js +3 -6
  118. package/dist/interactions/actions/move/layout/container.js.map +1 -1
  119. package/dist/interactions/actions/move/layout/exits.js +2 -2
  120. package/dist/interactions/actions/move/layout/exits.js.map +1 -1
  121. package/dist/interactions/actions/transform-again/memory.js +2 -2
  122. package/dist/interactions/actions/transform-again/memory.js.map +1 -1
  123. package/dist/move.d.ts +18 -3
  124. package/dist/move.js +2 -1
  125. package/dist/version.js +1 -1
  126. package/package.json +25 -25
  127. package/dist/canvas/colorOverrides.js +0 -23
  128. package/dist/canvas/colorOverrides.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePublishPointer.js","names":[],"sources":["../../src/input/usePublishPointer.ts"],"sourcesContent":["import { useEffect, type RefObject } from 'react';\nimport { heldPointers } from '@weasel-js/gestures';\nimport { useLatest } from '@weasel-js/react';\nimport { usePointerContext, type PointerWorldPos } from '@weasel-js/routing/react';\n\n/** Where a client point is in the world and which view it is over, or `null`\n * when there is no answer yet (the surface has not mounted its camera). */\nexport type LocatePointer = (\n pointerId: number,\n clientX: number,\n clientY: number,\n) => NonNullable<PointerWorldPos> | null;\n\n/**\n * Publishes the pointer over `elementRef` into the pointer store in scope, as\n * `locate` places it. A pointer pressed on the element keeps publishing once\n * it leaves, so a drag that runs off the edge still reports where it is; a\n * pointer that leaves with nothing held clears the store, unless another view\n * has published since. No-op with no provider in scope.\n *\n * `locate` is read on each event, so it need not be stable.\n */\nexport function usePublishPointer(\n elementRef: RefObject<HTMLElement | null>,\n locate: LocatePointer,\n): void {\n const store = usePointerContext();\n const locateRef = useLatest(locate);\n useEffect(() => {\n const el = elementRef.current;\n if (!el || !store) return;\n let viewId: string | null = null;\n const publish = (e: PointerEvent): void => {\n const at = locateRef.current(e.pointerId, e.clientX, e.clientY);\n if (!at) return;\n viewId = at.viewId;\n store.set(at);\n };\n const held = heldPointers(el);\n const onLeave = (e: PointerEvent): void => {\n if (held.count() > 0) {\n publish(e);\n return;\n }\n const now = store.get();\n if (now && now.viewId === viewId) store.set(null);\n };\n // The element's own listener has the moves that still target it.\n const onDocMove = (e: PointerEvent): void => {\n if (held.count() === 0 || el.contains(e.target as Node | null)) return;\n publish(e);\n };\n el.addEventListener('pointermove', publish);\n el.addEventListener('pointerleave', onLeave);\n document.addEventListener('pointermove', onDocMove);\n return () => {\n el.removeEventListener('pointermove', publish);\n el.removeEventListener('pointerleave', onLeave);\n document.removeEventListener('pointermove', onDocMove);\n held.release();\n };\n }, [elementRef, store, locateRef]);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsBA,SAAgB,kBACd,YACA,QACM;CACN,MAAM,QAAQ,kBAAkB;CAChC,MAAM,YAAY,UAAU,MAAM;CAClC,gBAAgB;EACd,MAAM,KAAK,WAAW;EACtB,IAAI,CAAC,MAAM,CAAC,OAAO;EACnB,IAAI,SAAwB;EAC5B,MAAM,WAAW,MAA0B;GACzC,MAAM,KAAK,UAAU,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO;GAC9D,IAAI,CAAC,IAAI;GACT,SAAS,GAAG;GACZ,MAAM,IAAI,EAAE;EACd;EACA,MAAM,OAAO,aAAa,EAAE;EAC5B,MAAM,WAAW,MAA0B;GACzC,IAAI,KAAK,MAAM,IAAI,GAAG;IACpB,QAAQ,CAAC;IACT;GACF;GACA,MAAM,MAAM,MAAM,IAAI;GACtB,IAAI,OAAO,IAAI,WAAW,QAAQ,MAAM,IAAI,IAAI;EAClD;EAEA,MAAM,aAAa,MAA0B;GAC3C,IAAI,KAAK,MAAM,MAAM,KAAK,GAAG,SAAS,EAAE,MAAqB,GAAG;GAChE,QAAQ,CAAC;EACX;EACA,GAAG,iBAAiB,eAAe,OAAO;EAC1C,GAAG,iBAAiB,gBAAgB,OAAO;EAC3C,SAAS,iBAAiB,eAAe,SAAS;EAClD,aAAa;GACX,GAAG,oBAAoB,eAAe,OAAO;GAC7C,GAAG,oBAAoB,gBAAgB,OAAO;GAC9C,SAAS,oBAAoB,eAAe,SAAS;GACrD,KAAK,QAAQ;EACf;CACF,GAAG;EAAC;EAAY;EAAO;CAAS,CAAC;AACnC"}
@@ -5,9 +5,16 @@ import { nodeGates } from "../../../core/scene/nodeGates.js";
5
5
  import { scenePoseAdapter, sceneViewPoseAdapter } from "../../../core/scene/scenePoseAdapter.js";
6
6
  import { wheelPan } from "../../../core/viewport/wheelHandler.js";
7
7
  import { poseDescriptorOf } from "../poseDescriptorDep.js";
8
+ import { clamp } from "@weasel-js/geom";
8
9
 
9
10
  // src/interactions/actions/defaults/containerScroll.ts
10
11
  /**
12
+ * Wheel scrolling of a layout container whose content runs past its bounds,
13
+ * such as `tileGrid({ overflow: 'scroll' })`. The strategy's `contentExtent`
14
+ * says how far; the offset goes to `scene.scroll`, which moves what the view
15
+ * shows and leaves the document where it is.
16
+ */
17
+ /**
11
18
  * How far `id`'s content can scroll: the span its layout's `contentExtent`
12
19
  * reaches past its bounds, read through the document fold the layout pass
13
20
  * arranges it in. Measured in the container's own frame, the axes a scroll
@@ -88,7 +95,6 @@ function scrollablesAt(scene, at, descriptor) {
88
95
  function scrollableAt(scene, at, descriptor) {
89
96
  return scrollablesAt(scene, at, descriptor)[0] ?? null;
90
97
  }
91
- var clamp = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v));
92
98
  function sceneOf(deps) {
93
99
  return deps?.scene;
94
100
  }
@@ -153,8 +159,8 @@ var containerScrollAction = {
153
159
  y: 0
154
160
  };
155
161
  const to = {
156
- x: clamp(from.x + by.x, hit.range.x),
157
- y: clamp(from.y + by.y, hit.range.y)
162
+ x: clamp(from.x + by.x, ...hit.range.x),
163
+ y: clamp(from.y + by.y, ...hit.range.y)
158
164
  };
159
165
  if (to.x === from.x && to.y === from.y) continue;
160
166
  scene.scroll.set(hit.id, to);
@@ -1 +1 @@
1
- {"version":3,"file":"containerScroll.js","names":[],"sources":["../../../../src/interactions/actions/defaults/containerScroll.ts"],"sourcesContent":["/**\n * Wheel scrolling of a layout container whose content runs past its bounds,\n * such as `tileGrid({ overflow: 'scroll' })`. The strategy's `contentExtent`\n * says how far; the offset goes to `scene.scroll`, which moves what the view\n * shows and leaves the document where it is.\n */\nimport type { Action, ActionDeps } from '@weasel-js/routing';\nimport { poseDescriptorForNode, type PoseDescriptor } from 'core/geometry/poseDescriptor';\nimport { targetTransformOf, worldToLocal } from 'core/geometry/targetFrame';\nimport { composeWorldPose } from 'core/scene/composePose';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport { scenePoseAdapter, sceneViewPoseAdapter } from 'core/scene/scenePoseAdapter';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { wheelPan } from 'core/viewport/wheelHandler';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport type { ImmediateParams } from '@weasel-js/routing';\n\ntype AnyScene = Scene<unknown, string, unknown>;\n\n/** How far a container's content can scroll on each axis, in its own units:\n * `[min, max]`, zero included, so an unscrolled container is in range. */\nexport interface ScrollRange {\n x: readonly [number, number];\n y: readonly [number, number];\n}\n\n/**\n * How far `id`'s content can scroll: the span its layout's `contentExtent`\n * reaches past its bounds, read through the document fold the layout pass\n * arranges it in. Measured in the container's own frame, the axes a scroll\n * offset runs along, so a turned container scrolls by its own length rather\n * than its box's. `null` for a node whose layout reports no extent, one whose\n * pose is not a frame, and under a composition that cannot scroll.\n */\nexport function scrollRange(scene: AnyScene, id: NodeId, descriptor: PoseDescriptor<unknown>): ScrollRange | null {\n const layout = scene.layoutOf(id);\n const node = scene.get(id);\n const { compose, decompose, scroll } = scene.composition;\n if (!layout?.contentExtent || !node || !scroll) return null;\n const doc = scenePoseAdapter(scene);\n if (doc.definesFrame?.(id) === false) return null;\n const world = composeWorldPose(doc, id, compose);\n const b = poseDescriptorForNode(descriptor, node).getBounds(decompose(world, world));\n const children = scene.childrenOf(id).map((cid) => ({\n id: cid as string,\n pose: decompose(world, composeWorldPose(doc, cid, compose)),\n }));\n const e = layout.contentExtent({ id: id as string, bounds: b }, children);\n return {\n x: [Math.min(0, e.x - b.x), Math.max(0, e.x + e.width - (b.x + b.width))],\n y: [Math.min(0, e.y - b.y), Math.max(0, e.y + e.height - (b.y + b.height))],\n };\n}\n\nconst canScroll = (r: ScrollRange): boolean => r.x[0] < r.x[1] || r.y[0] < r.y[1];\n\ninterface BoxLike { x: number; y: number; width: number; height: number }\nconst isBox = (p: unknown): p is BoxLike => {\n const b = p as Partial<BoxLike> | null;\n return typeof b?.x === 'number' && typeof b.y === 'number' && typeof b.width === 'number' && typeof b.height === 'number';\n};\n\n/** Whether the world point `at` falls on a container shown at `pose`, tested\n * in its own unturned box, so a turned container takes only its own area. */\nfunction shownAt(pose: unknown, at: { x: number; y: number }, bounds: () => BoxLike): boolean {\n const b = isBox(pose) ? pose : bounds();\n const p = isBox(pose) ? worldToLocal(targetTransformOf(pose), at.x, at.y) : at;\n return p !== null && p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;\n}\n\ninterface Scrollable { id: NodeId; range: ScrollRange }\n\n/** The shown containers under the world point `at` whose content can scroll,\n * innermost first: the innermost one and each that holds it. */\nfunction scrollablesAt(scene: AnyScene, at: { x: number; y: number }, descriptor: PoseDescriptor<unknown>): Scrollable[] {\n const gates = nodeGates(scene);\n const view = sceneViewPoseAdapter(scene);\n const { compose } = scene.composition;\n const hits = new Map<NodeId, ScrollRange>();\n let innermost: NodeId | null = null;\n const visit = (ids: readonly NodeId[]): void => {\n for (const id of ids) {\n const node = scene.get(id);\n if (!node || gates.hidden(id)) continue;\n const range = scrollRange(scene, id, descriptor);\n if (range && canScroll(range)) {\n const pose = composeWorldPose(view, id, compose);\n if (shownAt(pose, at, () => poseDescriptorForNode(descriptor, node).getBounds(pose))) {\n hits.set(id, range);\n innermost = id;\n }\n }\n visit(scene.childrenOf(id));\n }\n };\n visit(scene.roots);\n const out: Scrollable[] = [];\n for (let id: NodeId | null = innermost; id !== null; id = scene.get(id)?.parent ?? null) {\n const range = hits.get(id);\n if (range) out.push({ id, range });\n }\n return out;\n}\n\n/** The innermost shown container under the world point `at` whose content\n * can scroll, with its range. */\nexport function scrollableAt(\n scene: AnyScene,\n at: { x: number; y: number },\n descriptor: PoseDescriptor<unknown>,\n): Scrollable | null {\n return scrollablesAt(scene, at, descriptor)[0] ?? null;\n}\n\nconst clamp = (v: number, [lo, hi]: readonly [number, number]): number => Math.min(hi, Math.max(lo, v));\n\nfunction sceneOf(deps: ActionDeps | undefined): AnyScene | undefined {\n return (deps as { scene?: AnyScene } | undefined)?.scene;\n}\n\n/**\n * Wheel over a container whose content runs past its bounds scrolls that\n * content along the container's own axes, clamped to the strategy's\n * `contentExtent`; Shift+wheel scrolls it across. A container already at the\n * end of its range in the wheel's direction hands the wheel to the scrollable\n * container holding it, and past the outermost one the action passes, so the\n * wheel pans the viewport. Anywhere else it declines.\n */\nexport const containerScrollAction: Action & { requires: string[] } = {\n id: 'container.scroll',\n label: 'Scroll container (wheel)',\n group: 'viewport',\n defaultBinding: [\n { spec: { kind: 'wheel' }, opts: { id: 'container.scroll.wheel', yields: true } },\n {\n spec: { kind: 'wheel', mods: { shift: true } },\n opts: { id: 'container.scroll.swapAxis', yields: true, params: { swapAxis: true } },\n },\n ],\n requires: ['scene', 'view', 'poseDescriptor'],\n enabled: (deps, at) => {\n const scene = sceneOf(deps);\n if (!scene || !at) return 'not-applicable';\n return scrollableAt(scene, at, poseDescriptorOf(deps?.poseDescriptor)) ? true : 'not-applicable';\n },\n invoker: {\n timing: 'immediate',\n run(deps, params) {\n const scene = sceneOf(deps);\n const view = deps.view?.get();\n const world = (params as ImmediateParams | undefined)?.world;\n if (!scene || !view || !world) return 'pass';\n const delta = { deltaX: (params?.deltaX as number | undefined) ?? 0, deltaY: (params?.deltaY as number | undefined) ?? 0 };\n // The pan's own conversion, from a view at the origin: how far the wheel moves in world units.\n const by = wheelPan({ x: 0, y: 0, scale: view.scale }, delta, { swapAxis: params?.swapAxis === true });\n for (const hit of scrollablesAt(scene, world, poseDescriptorOf(deps.poseDescriptor))) {\n const from = scene.scroll.get(hit.id) ?? { x: 0, y: 0 };\n const to = { x: clamp(from.x + by.x, hit.range.x), y: clamp(from.y + by.y, hit.range.y) };\n if (to.x === from.x && to.y === from.y) continue;\n scene.scroll.set(hit.id, to);\n return undefined;\n }\n return 'pass';\n },\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"containerScroll.js","names":[],"sources":["../../../../src/interactions/actions/defaults/containerScroll.ts"],"sourcesContent":["/**\n * Wheel scrolling of a layout container whose content runs past its bounds,\n * such as `tileGrid({ overflow: 'scroll' })`. The strategy's `contentExtent`\n * says how far; the offset goes to `scene.scroll`, which moves what the view\n * shows and leaves the document where it is.\n */\nimport { clamp } from '@weasel-js/geom';\nimport type { Action, ActionDeps } from '@weasel-js/routing';\nimport { poseDescriptorForNode, type PoseDescriptor } from 'core/geometry/poseDescriptor';\nimport { targetTransformOf, worldToLocal } from 'core/geometry/targetFrame';\nimport { composeWorldPose } from 'core/scene/composePose';\nimport { nodeGates } from 'core/scene/nodeGates';\nimport { scenePoseAdapter, sceneViewPoseAdapter } from 'core/scene/scenePoseAdapter';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { wheelPan } from 'core/viewport/wheelHandler';\nimport { poseDescriptorOf } from '../poseDescriptorDep';\nimport type { ImmediateParams } from '@weasel-js/routing';\n\ntype AnyScene = Scene<unknown, string, unknown>;\n\n/** How far a container's content can scroll on each axis, in its own units:\n * `[min, max]`, zero included, so an unscrolled container is in range. */\nexport interface ScrollRange {\n x: readonly [number, number];\n y: readonly [number, number];\n}\n\n/**\n * How far `id`'s content can scroll: the span its layout's `contentExtent`\n * reaches past its bounds, read through the document fold the layout pass\n * arranges it in. Measured in the container's own frame, the axes a scroll\n * offset runs along, so a turned container scrolls by its own length rather\n * than its box's. `null` for a node whose layout reports no extent, one whose\n * pose is not a frame, and under a composition that cannot scroll.\n */\nexport function scrollRange(scene: AnyScene, id: NodeId, descriptor: PoseDescriptor<unknown>): ScrollRange | null {\n const layout = scene.layoutOf(id);\n const node = scene.get(id);\n const { compose, decompose, scroll } = scene.composition;\n if (!layout?.contentExtent || !node || !scroll) return null;\n const doc = scenePoseAdapter(scene);\n if (doc.definesFrame?.(id) === false) return null;\n const world = composeWorldPose(doc, id, compose);\n const b = poseDescriptorForNode(descriptor, node).getBounds(decompose(world, world));\n const children = scene.childrenOf(id).map((cid) => ({\n id: cid as string,\n pose: decompose(world, composeWorldPose(doc, cid, compose)),\n }));\n const e = layout.contentExtent({ id: id as string, bounds: b }, children);\n return {\n x: [Math.min(0, e.x - b.x), Math.max(0, e.x + e.width - (b.x + b.width))],\n y: [Math.min(0, e.y - b.y), Math.max(0, e.y + e.height - (b.y + b.height))],\n };\n}\n\nconst canScroll = (r: ScrollRange): boolean => r.x[0] < r.x[1] || r.y[0] < r.y[1];\n\ninterface BoxLike { x: number; y: number; width: number; height: number }\nconst isBox = (p: unknown): p is BoxLike => {\n const b = p as Partial<BoxLike> | null;\n return typeof b?.x === 'number' && typeof b.y === 'number' && typeof b.width === 'number' && typeof b.height === 'number';\n};\n\n/** Whether the world point `at` falls on a container shown at `pose`, tested\n * in its own unturned box, so a turned container takes only its own area. */\nfunction shownAt(pose: unknown, at: { x: number; y: number }, bounds: () => BoxLike): boolean {\n const b = isBox(pose) ? pose : bounds();\n const p = isBox(pose) ? worldToLocal(targetTransformOf(pose), at.x, at.y) : at;\n return p !== null && p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;\n}\n\ninterface Scrollable { id: NodeId; range: ScrollRange }\n\n/** The shown containers under the world point `at` whose content can scroll,\n * innermost first: the innermost one and each that holds it. */\nfunction scrollablesAt(scene: AnyScene, at: { x: number; y: number }, descriptor: PoseDescriptor<unknown>): Scrollable[] {\n const gates = nodeGates(scene);\n const view = sceneViewPoseAdapter(scene);\n const { compose } = scene.composition;\n const hits = new Map<NodeId, ScrollRange>();\n let innermost: NodeId | null = null;\n const visit = (ids: readonly NodeId[]): void => {\n for (const id of ids) {\n const node = scene.get(id);\n if (!node || gates.hidden(id)) continue;\n const range = scrollRange(scene, id, descriptor);\n if (range && canScroll(range)) {\n const pose = composeWorldPose(view, id, compose);\n if (shownAt(pose, at, () => poseDescriptorForNode(descriptor, node).getBounds(pose))) {\n hits.set(id, range);\n innermost = id;\n }\n }\n visit(scene.childrenOf(id));\n }\n };\n visit(scene.roots);\n const out: Scrollable[] = [];\n for (let id: NodeId | null = innermost; id !== null; id = scene.get(id)?.parent ?? null) {\n const range = hits.get(id);\n if (range) out.push({ id, range });\n }\n return out;\n}\n\n/** The innermost shown container under the world point `at` whose content\n * can scroll, with its range. */\nexport function scrollableAt(\n scene: AnyScene,\n at: { x: number; y: number },\n descriptor: PoseDescriptor<unknown>,\n): Scrollable | null {\n return scrollablesAt(scene, at, descriptor)[0] ?? null;\n}\n\nfunction sceneOf(deps: ActionDeps | undefined): AnyScene | undefined {\n return (deps as { scene?: AnyScene } | undefined)?.scene;\n}\n\n/**\n * Wheel over a container whose content runs past its bounds scrolls that\n * content along the container's own axes, clamped to the strategy's\n * `contentExtent`; Shift+wheel scrolls it across. A container already at the\n * end of its range in the wheel's direction hands the wheel to the scrollable\n * container holding it, and past the outermost one the action passes, so the\n * wheel pans the viewport. Anywhere else it declines.\n */\nexport const containerScrollAction: Action & { requires: string[] } = {\n id: 'container.scroll',\n label: 'Scroll container (wheel)',\n group: 'viewport',\n defaultBinding: [\n { spec: { kind: 'wheel' }, opts: { id: 'container.scroll.wheel', yields: true } },\n {\n spec: { kind: 'wheel', mods: { shift: true } },\n opts: { id: 'container.scroll.swapAxis', yields: true, params: { swapAxis: true } },\n },\n ],\n requires: ['scene', 'view', 'poseDescriptor'],\n enabled: (deps, at) => {\n const scene = sceneOf(deps);\n if (!scene || !at) return 'not-applicable';\n return scrollableAt(scene, at, poseDescriptorOf(deps?.poseDescriptor)) ? true : 'not-applicable';\n },\n invoker: {\n timing: 'immediate',\n run(deps, params) {\n const scene = sceneOf(deps);\n const view = deps.view?.get();\n const world = (params as ImmediateParams | undefined)?.world;\n if (!scene || !view || !world) return 'pass';\n const delta = { deltaX: (params?.deltaX as number | undefined) ?? 0, deltaY: (params?.deltaY as number | undefined) ?? 0 };\n // The pan's own conversion, from a view at the origin: how far the wheel moves in world units.\n const by = wheelPan({ x: 0, y: 0, scale: view.scale }, delta, { swapAxis: params?.swapAxis === true });\n for (const hit of scrollablesAt(scene, world, poseDescriptorOf(deps.poseDescriptor))) {\n const from = scene.scroll.get(hit.id) ?? { x: 0, y: 0 };\n const to = { x: clamp(from.x + by.x, ...hit.range.x), y: clamp(from.y + by.y, ...hit.range.y) };\n if (to.x === from.x && to.y === from.y) continue;\n scene.scroll.set(hit.id, to);\n return undefined;\n }\n return 'pass';\n },\n },\n};\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { isEnterable } from "../../../canvas/deps/enteredGroup.js";
2
2
  import { pressTarget } from "../../../tools/builtin/pressTarget.js";
3
+ import { rectContainsPoint } from "@weasel-js/geom";
3
4
  import { ActionDisabledReason } from "@weasel-js/routing";
4
5
 
5
6
  // src/interactions/actions/defaults/enterGroup.ts
6
- var inside = (b, p) => p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;
7
7
  /**
8
8
  * Leaves every entered group whose box does not hold `p`, innermost first. A press that lands
9
9
  * inside the entered group's box stays in it, and so does one on a group with no box to read.
@@ -11,7 +11,7 @@ var inside = (b, p) => p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <
11
11
  function leaveGroupsOutside(entered, boxOf, p) {
12
12
  for (let id = entered.get(); id !== null; id = entered.get()) {
13
13
  const box = boxOf(id);
14
- if (box === null || inside(box, p)) return;
14
+ if (box === null || rectContainsPoint(box, p.x, p.y)) return;
15
15
  entered.leave();
16
16
  if (entered.get() === id) return;
17
17
  }
@@ -63,7 +63,7 @@ function createLeaveGroupAction(opts) {
63
63
  if (here === null || deps?.editAnchors?.editingId) return ActionDisabledReason.NotApplicable;
64
64
  if (!at) return true;
65
65
  const box = opts.boxOf(here);
66
- return box !== null && !inside(box, at) ? true : ActionDisabledReason.NotApplicable;
66
+ return box !== null && !rectContainsPoint(box, at.x, at.y) ? true : ActionDisabledReason.NotApplicable;
67
67
  },
68
68
  invoker: {
69
69
  timing: "immediate",
@@ -1 +1 @@
1
- {"version":3,"file":"enterGroup.js","names":[],"sources":["../../../../src/interactions/actions/defaults/enterGroup.ts"],"sourcesContent":["import type { Action, ActionDeps, WorldPoint } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport { isEnterable, type EnteredGroupApi } from 'canvas/deps/enteredGroup';\nimport type { NodeId } from 'core/scene/types';\nimport { pressTarget, type PressTargetTree } from '../../../tools/builtin/pressTarget';\nimport type { ImmediateParams } from '@weasel-js/routing';\n\ntype Box = { x: number; y: number; width: number; height: number };\n\n/** What `group.enter` reads: the node under a point and the tree above it. */\nexport interface EnterGroupOptions {\n /** The node under the point, before any group above it is considered. */\n pickHit: (world: WorldPoint) => string | null;\n tree: PressTargetTree;\n}\n\n/** What `group.leave` reads: where a group's box is. */\nexport interface LeaveGroupOptions {\n boxOf: (id: string) => Box | null;\n}\n\nconst inside = (b: Box, p: WorldPoint) =>\n p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;\n\n/**\n * Leaves every entered group whose box does not hold `p`, innermost first. A press that lands\n * inside the entered group's box stays in it, and so does one on a group with no box to read.\n */\nexport function leaveGroupsOutside(\n entered: EnteredGroupApi,\n boxOf: LeaveGroupOptions['boxOf'],\n p: WorldPoint,\n): void {\n for (let id = entered.get(); id !== null; id = entered.get()) {\n const box = boxOf(id);\n if (box === null || inside(box, p)) return;\n entered.leave();\n if (entered.get() === id) return;\n }\n}\n\n/**\n * `group.enter` — enters the group a press at the point would select. Bound to a double-click\n * on a body by `useSelectTool`. It declines where that is not a group, so the double-click\n * falls through to text editing, path editing and the mode triggers.\n */\nexport function createEnterGroupAction(opts: EnterGroupOptions): Action {\n const targetAt = (deps: ActionDeps | undefined, p: WorldPoint | undefined): string | null => {\n const entered = deps?.enteredGroup;\n if (!entered || !p) return null;\n const hit = opts.pickHit(p);\n if (hit === null) return null;\n const target = pressTarget(opts.tree, hit, entered.get());\n return isEnterable(opts.tree.getNode(target) as Parameters<typeof isEnterable>[0]) ? target : null;\n };\n return {\n id: 'group.enter',\n label: 'Enter group',\n eligible: { capability: 'creates-selection' },\n requires: ['enteredGroup'],\n enabled: (deps, at) => (targetAt(deps, at) !== null ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const world = (params as ImmediateParams | undefined)?.world;\n const target = world ? targetAt(deps, world) : null;\n if (target !== null) deps.enteredGroup?.enter(target as NodeId);\n },\n },\n };\n}\n\n/**\n * `group.leave` — Escape leaves the entered group for the one above it; a press outside the\n * entered group's box leaves every group that box does not hold. With nothing entered it\n * declines, so Escape clears the selection as before, and it declines while path anchors are\n * being edited, so that Escape exits the edit first.\n */\nexport function createLeaveGroupAction(opts: LeaveGroupOptions): Action {\n return {\n id: 'group.leave',\n label: 'Leave group',\n eligible: { capability: 'creates-selection' },\n requires: ['enteredGroup', 'editAnchors'],\n enabled: (deps, at) => {\n const here = deps?.enteredGroup?.get() ?? null;\n if (here === null || deps?.editAnchors?.editingId) return ActionDisabledReason.NotApplicable;\n if (!at) return true;\n const box = opts.boxOf(here);\n return box !== null && !inside(box, at) ? true : ActionDisabledReason.NotApplicable;\n },\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const entered = deps.enteredGroup;\n if (!entered) return;\n const world = (params as ImmediateParams | undefined)?.world;\n if (world) leaveGroupsOutside(entered, opts.boxOf, world);\n else entered.leave();\n },\n },\n };\n}\n"],"mappings":";;;;;AAqBA,IAAM,UAAU,GAAQ,MACtB,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;;;;;AAMrE,SAAgB,mBACd,SACA,OACA,GACM;CACN,KAAK,IAAI,KAAK,QAAQ,IAAI,GAAG,OAAO,MAAM,KAAK,QAAQ,IAAI,GAAG;EAC5D,MAAM,MAAM,MAAM,EAAE;EACpB,IAAI,QAAQ,QAAQ,OAAO,KAAK,CAAC,GAAG;EACpC,QAAQ,MAAM;EACd,IAAI,QAAQ,IAAI,MAAM,IAAI;CAC5B;AACF;;;;;;AAOA,SAAgB,uBAAuB,MAAiC;CACtE,MAAM,YAAY,MAA8B,MAA6C;EAC3F,MAAM,UAAU,MAAM;EACtB,IAAI,CAAC,WAAW,CAAC,GAAG,OAAO;EAC3B,MAAM,MAAM,KAAK,QAAQ,CAAC;EAC1B,IAAI,QAAQ,MAAM,OAAO;EACzB,MAAM,SAAS,YAAY,KAAK,MAAM,KAAK,QAAQ,IAAI,CAAC;EACxD,OAAO,YAAY,KAAK,KAAK,QAAQ,MAAM,CAAsC,IAAI,SAAS;CAChG;CACA,OAAO;EACL,IAAI;EACJ,OAAO;EACP,UAAU,EAAE,YAAY,oBAAoB;EAC5C,UAAU,CAAC,cAAc;EACzB,UAAU,MAAM,OAAQ,SAAS,MAAM,EAAE,MAAM,OAAO,OAAO,qBAAqB;EAClF,SAAS;GACP,QAAQ;GACR,MAAM,MAAM,WAAW;IACrB,MAAM,QAAS,QAAwC;IACvD,MAAM,SAAS,QAAQ,SAAS,MAAM,KAAK,IAAI;IAC/C,IAAI,WAAW,MAAM,KAAK,cAAc,MAAM,MAAgB;GAChE;EACF;CACF;AACF;;;;;;;AAQA,SAAgB,uBAAuB,MAAiC;CACtE,OAAO;EACL,IAAI;EACJ,OAAO;EACP,UAAU,EAAE,YAAY,oBAAoB;EAC5C,UAAU,CAAC,gBAAgB,aAAa;EACxC,UAAU,MAAM,OAAO;GACrB,MAAM,OAAO,MAAM,cAAc,IAAI,KAAK;GAC1C,IAAI,SAAS,QAAQ,MAAM,aAAa,WAAW,OAAO,qBAAqB;GAC/E,IAAI,CAAC,IAAI,OAAO;GAChB,MAAM,MAAM,KAAK,MAAM,IAAI;GAC3B,OAAO,QAAQ,QAAQ,CAAC,OAAO,KAAK,EAAE,IAAI,OAAO,qBAAqB;EACxE;EACA,SAAS;GACP,QAAQ;GACR,MAAM,MAAM,WAAW;IACrB,MAAM,UAAU,KAAK;IACrB,IAAI,CAAC,SAAS;IACd,MAAM,QAAS,QAAwC;IACvD,IAAI,OAAO,mBAAmB,SAAS,KAAK,OAAO,KAAK;SACnD,QAAQ,MAAM;GACrB;EACF;CACF;AACF"}
1
+ {"version":3,"file":"enterGroup.js","names":[],"sources":["../../../../src/interactions/actions/defaults/enterGroup.ts"],"sourcesContent":["import { rectContainsPoint } from '@weasel-js/geom';\nimport type { Action, ActionDeps, WorldPoint } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport { isEnterable, type EnteredGroupApi } from 'canvas/deps/enteredGroup';\nimport type { NodeId } from 'core/scene/types';\nimport { pressTarget, type PressTargetTree } from '../../../tools/builtin/pressTarget';\nimport type { ImmediateParams } from '@weasel-js/routing';\n\ntype Box = { x: number; y: number; width: number; height: number };\n\n/** What `group.enter` reads: the node under a point and the tree above it. */\nexport interface EnterGroupOptions {\n /** The node under the point, before any group above it is considered. */\n pickHit: (world: WorldPoint) => string | null;\n tree: PressTargetTree;\n}\n\n/** What `group.leave` reads: where a group's box is. */\nexport interface LeaveGroupOptions {\n boxOf: (id: string) => Box | null;\n}\n\n/**\n * Leaves every entered group whose box does not hold `p`, innermost first. A press that lands\n * inside the entered group's box stays in it, and so does one on a group with no box to read.\n */\nexport function leaveGroupsOutside(\n entered: EnteredGroupApi,\n boxOf: LeaveGroupOptions['boxOf'],\n p: WorldPoint,\n): void {\n for (let id = entered.get(); id !== null; id = entered.get()) {\n const box = boxOf(id);\n if (box === null || rectContainsPoint(box, p.x, p.y)) return;\n entered.leave();\n if (entered.get() === id) return;\n }\n}\n\n/**\n * `group.enter` — enters the group a press at the point would select. Bound to a double-click\n * on a body by `useSelectTool`. It declines where that is not a group, so the double-click\n * falls through to text editing, path editing and the mode triggers.\n */\nexport function createEnterGroupAction(opts: EnterGroupOptions): Action {\n const targetAt = (deps: ActionDeps | undefined, p: WorldPoint | undefined): string | null => {\n const entered = deps?.enteredGroup;\n if (!entered || !p) return null;\n const hit = opts.pickHit(p);\n if (hit === null) return null;\n const target = pressTarget(opts.tree, hit, entered.get());\n return isEnterable(opts.tree.getNode(target) as Parameters<typeof isEnterable>[0]) ? target : null;\n };\n return {\n id: 'group.enter',\n label: 'Enter group',\n eligible: { capability: 'creates-selection' },\n requires: ['enteredGroup'],\n enabled: (deps, at) => (targetAt(deps, at) !== null ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const world = (params as ImmediateParams | undefined)?.world;\n const target = world ? targetAt(deps, world) : null;\n if (target !== null) deps.enteredGroup?.enter(target as NodeId);\n },\n },\n };\n}\n\n/**\n * `group.leave` — Escape leaves the entered group for the one above it; a press outside the\n * entered group's box leaves every group that box does not hold. With nothing entered it\n * declines, so Escape clears the selection as before, and it declines while path anchors are\n * being edited, so that Escape exits the edit first.\n */\nexport function createLeaveGroupAction(opts: LeaveGroupOptions): Action {\n return {\n id: 'group.leave',\n label: 'Leave group',\n eligible: { capability: 'creates-selection' },\n requires: ['enteredGroup', 'editAnchors'],\n enabled: (deps, at) => {\n const here = deps?.enteredGroup?.get() ?? null;\n if (here === null || deps?.editAnchors?.editingId) return ActionDisabledReason.NotApplicable;\n if (!at) return true;\n const box = opts.boxOf(here);\n return box !== null && !rectContainsPoint(box, at.x, at.y) ? true : ActionDisabledReason.NotApplicable;\n },\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const entered = deps.enteredGroup;\n if (!entered) return;\n const world = (params as ImmediateParams | undefined)?.world;\n if (world) leaveGroupsOutside(entered, opts.boxOf, world);\n else entered.leave();\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;AA0BA,SAAgB,mBACd,SACA,OACA,GACM;CACN,KAAK,IAAI,KAAK,QAAQ,IAAI,GAAG,OAAO,MAAM,KAAK,QAAQ,IAAI,GAAG;EAC5D,MAAM,MAAM,MAAM,EAAE;EACpB,IAAI,QAAQ,QAAQ,kBAAkB,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG;EACtD,QAAQ,MAAM;EACd,IAAI,QAAQ,IAAI,MAAM,IAAI;CAC5B;AACF;;;;;;AAOA,SAAgB,uBAAuB,MAAiC;CACtE,MAAM,YAAY,MAA8B,MAA6C;EAC3F,MAAM,UAAU,MAAM;EACtB,IAAI,CAAC,WAAW,CAAC,GAAG,OAAO;EAC3B,MAAM,MAAM,KAAK,QAAQ,CAAC;EAC1B,IAAI,QAAQ,MAAM,OAAO;EACzB,MAAM,SAAS,YAAY,KAAK,MAAM,KAAK,QAAQ,IAAI,CAAC;EACxD,OAAO,YAAY,KAAK,KAAK,QAAQ,MAAM,CAAsC,IAAI,SAAS;CAChG;CACA,OAAO;EACL,IAAI;EACJ,OAAO;EACP,UAAU,EAAE,YAAY,oBAAoB;EAC5C,UAAU,CAAC,cAAc;EACzB,UAAU,MAAM,OAAQ,SAAS,MAAM,EAAE,MAAM,OAAO,OAAO,qBAAqB;EAClF,SAAS;GACP,QAAQ;GACR,MAAM,MAAM,WAAW;IACrB,MAAM,QAAS,QAAwC;IACvD,MAAM,SAAS,QAAQ,SAAS,MAAM,KAAK,IAAI;IAC/C,IAAI,WAAW,MAAM,KAAK,cAAc,MAAM,MAAgB;GAChE;EACF;CACF;AACF;;;;;;;AAQA,SAAgB,uBAAuB,MAAiC;CACtE,OAAO;EACL,IAAI;EACJ,OAAO;EACP,UAAU,EAAE,YAAY,oBAAoB;EAC5C,UAAU,CAAC,gBAAgB,aAAa;EACxC,UAAU,MAAM,OAAO;GACrB,MAAM,OAAO,MAAM,cAAc,IAAI,KAAK;GAC1C,IAAI,SAAS,QAAQ,MAAM,aAAa,WAAW,OAAO,qBAAqB;GAC/E,IAAI,CAAC,IAAI,OAAO;GAChB,MAAM,MAAM,KAAK,MAAM,IAAI;GAC3B,OAAO,QAAQ,QAAQ,CAAC,kBAAkB,KAAK,GAAG,GAAG,GAAG,CAAC,IAAI,OAAO,qBAAqB;EAC3F;EACA,SAAS;GACP,QAAQ;GACR,MAAM,MAAM,WAAW;IACrB,MAAM,UAAU,KAAK;IACrB,IAAI,CAAC,SAAS;IACd,MAAM,QAAS,QAAwC;IACvD,IAAI,OAAO,mBAAmB,SAAS,KAAK,OAAO,KAAK;SACnD,QAAQ,MAAM;GACrB;EACF;CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { pathToAnchors } from "@weasel-js/geom";
1
+ import { pathToAnchors, viewWorldToScreen } from "@weasel-js/geom";
2
2
 
3
3
  // src/interactions/actions/defaults/pathEditMarks.ts
4
4
  /** The open path as extensions read it, or null with none open. */
@@ -26,10 +26,7 @@ function markUnder(dep, deps, wx, wy) {
26
26
  y: 1
27
27
  }
28
28
  };
29
- const toScreen = (p) => ({
30
- x: (p.x - view.x) * view.scale.x,
31
- y: (p.y - view.y) * view.scale.y
32
- });
29
+ const toScreen = (p) => viewWorldToScreen(view, p);
33
30
  const screen = toScreen({
34
31
  x: wx,
35
32
  y: wy
@@ -1 +1 @@
1
- {"version":3,"file":"pathEditMarks.js","names":[],"sources":["../../../../src/interactions/actions/defaults/pathEditMarks.ts"],"sourcesContent":["/** Path edit's side of the marks its extensions draw: finding the one a click\n * lands on, and deleting the selected one. */\n\nimport type { ActionDeps } from '@weasel-js/routing';\nimport { pathToAnchors, type AnchorSet, type PolygonPath } from '@weasel-js/geom';\nimport type { PathEditMark, PathEditTarget } from 'domains/paths/pathEditExtension';\nimport type { EditAnchorsDep } from '../depSchema';\n\n/** The open path as extensions read it, or null with none open. */\nexport function editTargetOf(dep: EditAnchorsDep): PathEditTarget | null {\n const node = dep.editingId ? dep.getNodeShape(dep.editingId) : null;\n const path = node ? (dep.getEditablePath(dep.editingId) as PolygonPath | null | undefined) : null;\n if (!node || path?.kind !== 'polygon') return null;\n return { id: dep.editingId, node, path, anchors: pathToAnchors(path) as AnchorSet };\n}\n\n/** The extension mark under the world point `(wx, wy)`, or null. */\nexport function markUnder(dep: EditAnchorsDep, deps: ActionDeps | undefined, wx: number, wy: number): PathEditMark | null {\n const extensions = dep.extensions?.filter((e) => e.markAt) ?? [];\n const target = extensions.length > 0 ? editTargetOf(dep) : null;\n if (!target) return null;\n const view = deps?.view?.get() ?? { x: 0, y: 0, scale: { x: 1, y: 1 } };\n const toScreen = (p: { x: number; y: number }) => ({ x: (p.x - view.x) * view.scale.x, y: (p.y - view.y) * view.scale.y });\n const screen = toScreen({ x: wx, y: wy });\n for (const ext of [...extensions].reverse()) {\n const index = ext.markAt!(target, toScreen, screen);\n if (index !== -1) return { owner: ext.id, index };\n }\n return null;\n}\n\n/** Delete the selected extension mark. False when none is selected, or its\n * owner cannot delete it. */\nexport function deleteSelectedMark(dep: EditAnchorsDep): boolean {\n const mark = dep.selectedMark;\n const owner = mark ? dep.extensions?.find((e) => e.id === mark.owner) : undefined;\n const target = owner?.deleteMark && dep.store ? editTargetOf(dep) : null;\n if (!mark || !target) return false;\n owner!.deleteMark!(target, mark.index, dep.store!);\n dep.setSelectedMark?.(null);\n return true;\n}\n"],"mappings":";;;;AASA,SAAgB,aAAa,KAA4C;CACvE,MAAM,OAAO,IAAI,YAAY,IAAI,aAAa,IAAI,SAAS,IAAI;CAC/D,MAAM,OAAO,OAAQ,IAAI,gBAAgB,IAAI,SAAS,IAAuC;CAC7F,IAAI,CAAC,QAAQ,MAAM,SAAS,WAAW,OAAO;CAC9C,OAAO;EAAE,IAAI,IAAI;EAAW;EAAM;EAAM,SAAS,cAAc,IAAI;CAAe;AACpF;;AAGA,SAAgB,UAAU,KAAqB,MAA8B,IAAY,IAAiC;CACxH,MAAM,aAAa,IAAI,YAAY,QAAQ,MAAM,EAAE,MAAM,KAAK,CAAC;CAC/D,MAAM,SAAS,WAAW,SAAS,IAAI,aAAa,GAAG,IAAI;CAC3D,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,OAAO,MAAM,MAAM,IAAI,KAAK;EAAE,GAAG;EAAG,GAAG;EAAG,OAAO;GAAE,GAAG;GAAG,GAAG;EAAE;CAAE;CACtE,MAAM,YAAY,OAAiC;EAAE,IAAI,EAAE,IAAI,KAAK,KAAK,KAAK,MAAM;EAAG,IAAI,EAAE,IAAI,KAAK,KAAK,KAAK,MAAM;CAAE;CACxH,MAAM,SAAS,SAAS;EAAE,GAAG;EAAI,GAAG;CAAG,CAAC;CACxC,KAAK,MAAM,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,QAAQ,GAAG;EAC3C,MAAM,QAAQ,IAAI,OAAQ,QAAQ,UAAU,MAAM;EAClD,IAAI,UAAU,IAAI,OAAO;GAAE,OAAO,IAAI;GAAI;EAAM;CAClD;CACA,OAAO;AACT;;;AAIA,SAAgB,mBAAmB,KAA8B;CAC/D,MAAM,OAAO,IAAI;CACjB,MAAM,QAAQ,OAAO,IAAI,YAAY,MAAM,MAAM,EAAE,OAAO,KAAK,KAAK,IAAI;CACxE,MAAM,SAAS,OAAO,cAAc,IAAI,QAAQ,aAAa,GAAG,IAAI;CACpE,IAAI,CAAC,QAAQ,CAAC,QAAQ,OAAO;CAC7B,MAAO,WAAY,QAAQ,KAAK,OAAO,IAAI,KAAM;CACjD,IAAI,kBAAkB,IAAI;CAC1B,OAAO;AACT"}
1
+ {"version":3,"file":"pathEditMarks.js","names":[],"sources":["../../../../src/interactions/actions/defaults/pathEditMarks.ts"],"sourcesContent":["/** Path edit's side of the marks its extensions draw: finding the one a click\n * lands on, and deleting the selected one. */\n\nimport type { ActionDeps } from '@weasel-js/routing';\nimport { pathToAnchors, viewWorldToScreen, type AnchorSet, type PolygonPath } from '@weasel-js/geom';\nimport type { PathEditMark, PathEditTarget } from 'domains/paths/pathEditExtension';\nimport type { EditAnchorsDep } from '../depSchema';\n\n/** The open path as extensions read it, or null with none open. */\nexport function editTargetOf(dep: EditAnchorsDep): PathEditTarget | null {\n const node = dep.editingId ? dep.getNodeShape(dep.editingId) : null;\n const path = node ? (dep.getEditablePath(dep.editingId) as PolygonPath | null | undefined) : null;\n if (!node || path?.kind !== 'polygon') return null;\n return { id: dep.editingId, node, path, anchors: pathToAnchors(path) as AnchorSet };\n}\n\n/** The extension mark under the world point `(wx, wy)`, or null. */\nexport function markUnder(dep: EditAnchorsDep, deps: ActionDeps | undefined, wx: number, wy: number): PathEditMark | null {\n const extensions = dep.extensions?.filter((e) => e.markAt) ?? [];\n const target = extensions.length > 0 ? editTargetOf(dep) : null;\n if (!target) return null;\n const view = deps?.view?.get() ?? { x: 0, y: 0, scale: { x: 1, y: 1 } };\n const toScreen = (p: { x: number; y: number }) => viewWorldToScreen(view, p);\n const screen = toScreen({ x: wx, y: wy });\n for (const ext of [...extensions].reverse()) {\n const index = ext.markAt!(target, toScreen, screen);\n if (index !== -1) return { owner: ext.id, index };\n }\n return null;\n}\n\n/** Delete the selected extension mark. False when none is selected, or its\n * owner cannot delete it. */\nexport function deleteSelectedMark(dep: EditAnchorsDep): boolean {\n const mark = dep.selectedMark;\n const owner = mark ? dep.extensions?.find((e) => e.id === mark.owner) : undefined;\n const target = owner?.deleteMark && dep.store ? editTargetOf(dep) : null;\n if (!mark || !target) return false;\n owner!.deleteMark!(target, mark.index, dep.store!);\n dep.setSelectedMark?.(null);\n return true;\n}\n"],"mappings":";;;;AASA,SAAgB,aAAa,KAA4C;CACvE,MAAM,OAAO,IAAI,YAAY,IAAI,aAAa,IAAI,SAAS,IAAI;CAC/D,MAAM,OAAO,OAAQ,IAAI,gBAAgB,IAAI,SAAS,IAAuC;CAC7F,IAAI,CAAC,QAAQ,MAAM,SAAS,WAAW,OAAO;CAC9C,OAAO;EAAE,IAAI,IAAI;EAAW;EAAM;EAAM,SAAS,cAAc,IAAI;CAAe;AACpF;;AAGA,SAAgB,UAAU,KAAqB,MAA8B,IAAY,IAAiC;CACxH,MAAM,aAAa,IAAI,YAAY,QAAQ,MAAM,EAAE,MAAM,KAAK,CAAC;CAC/D,MAAM,SAAS,WAAW,SAAS,IAAI,aAAa,GAAG,IAAI;CAC3D,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,OAAO,MAAM,MAAM,IAAI,KAAK;EAAE,GAAG;EAAG,GAAG;EAAG,OAAO;GAAE,GAAG;GAAG,GAAG;EAAE;CAAE;CACtE,MAAM,YAAY,MAAgC,kBAAkB,MAAM,CAAC;CAC3E,MAAM,SAAS,SAAS;EAAE,GAAG;EAAI,GAAG;CAAG,CAAC;CACxC,KAAK,MAAM,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,QAAQ,GAAG;EAC3C,MAAM,QAAQ,IAAI,OAAQ,QAAQ,UAAU,MAAM;EAClD,IAAI,UAAU,IAAI,OAAO;GAAE,OAAO,IAAI;GAAI;EAAM;CAClD;CACA,OAAO;AACT;;;AAIA,SAAgB,mBAAmB,KAA8B;CAC/D,MAAM,OAAO,IAAI;CACjB,MAAM,QAAQ,OAAO,IAAI,YAAY,MAAM,MAAM,EAAE,OAAO,KAAK,KAAK,IAAI;CACxE,MAAM,SAAS,OAAO,cAAc,IAAI,QAAQ,aAAa,GAAG,IAAI;CACpE,IAAI,CAAC,QAAQ,CAAC,QAAQ,OAAO;CAC7B,MAAO,WAAY,QAAQ,KAAK,OAAO,IAAI,KAAM;CACjD,IAAI,kBAAkB,IAAI;CAC1B,OAAO;AACT"}
@@ -1,4 +1,4 @@
1
- import { handleAlongItsAngle, handleAtItsLength } from "@weasel-js/geom";
1
+ import { distance, handleAlongItsAngle, handleAtItsLength } from "@weasel-js/geom";
2
2
  import { roleReader } from "@weasel-js/routing";
3
3
 
4
4
  // src/interactions/actions/edit-anchors/handleDrag.ts
@@ -33,7 +33,7 @@ function locksInForce(held, ctx) {
33
33
  /** The handle dragged toward `pointer` under `locks`. */
34
34
  function lockHandle(press, pointer, locks) {
35
35
  const { anchor, handle } = press;
36
- const len = dist(anchor, handle);
36
+ const len = distance(anchor, handle);
37
37
  if (locks.angle && locks.length) return {
38
38
  to: { ...handle },
39
39
  guide: lineGuide(anchor, handle, len)
@@ -42,7 +42,7 @@ function lockHandle(press, pointer, locks) {
42
42
  const to = handleAlongItsAngle(anchor, handle, pointer);
43
43
  return {
44
44
  to,
45
- guide: lineGuide(anchor, handle, Math.max(len, dist(anchor, to)))
45
+ guide: lineGuide(anchor, handle, Math.max(len, distance(anchor, to)))
46
46
  };
47
47
  }
48
48
  if (locks.length) return {
@@ -56,7 +56,7 @@ function lockHandle(press, pointer, locks) {
56
56
  }
57
57
  /** The line through `anchor` toward `toward`, reaching past `reach` on both sides. */
58
58
  function lineGuide(anchor, toward, reach) {
59
- const d = dist(anchor, toward);
59
+ const d = distance(anchor, toward);
60
60
  if (d === 0) return {
61
61
  kind: "polyline",
62
62
  points: [anchor, anchor],
@@ -92,9 +92,6 @@ function circleGuide(center, r) {
92
92
  role: "lock"
93
93
  };
94
94
  }
95
- function dist(a, b) {
96
- return Math.hypot(b.x - a.x, b.y - a.y);
97
- }
98
95
 
99
96
  export { HANDLE_LOCK_ROLES, lockHandle, locksInForce };
100
97
  //# sourceMappingURL=handleDrag.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"handleDrag.js","names":[],"sources":["../../../../src/interactions/actions/edit-anchors/handleDrag.ts"],"sourcesContent":["/**\n * The locks on a control-handle drag — its angle, its length, or both — and\n * the guide that shows which one is holding.\n */\nimport { roleReader, type ModifierRoles, type ModifierState, type OngoingOverlay, type WorldPoint } from '@weasel-js/routing';\nimport { handleAlongItsAngle, handleAtItsLength, type Point } from '@weasel-js/geom';\nimport type { HandleLocks } from './params';\n\n/** Where a handle drag's anchor and handle sat at the press. */\nexport interface HandlePress {\n anchor: Point;\n handle: Point;\n}\n\n/** Where the handle goes this move, and the guide its lock draws. */\nexport interface LockedHandle {\n to: Point;\n /** The line or circle the handle is held to; null with no lock held. */\n guide: OngoingOverlay | null;\n}\n\nconst CIRCLE_SEGMENTS = 64;\n/** How far past the handle the angle guide reaches, as a share of its length. */\nconst LINE_OVERSHOOT = 1.25;\n\n/** The modifier roles a handle drag reads: Shift holds its angle, Cmd or\n * Ctrl its length. */\nexport const HANDLE_LOCK_ROLES = {\n 'handle.holdAngle': { label: 'Hold the handle angle', keys: ['shift'] },\n 'handle.holdLength': { label: 'Hold the handle length', keys: ['meta', 'ctrl'] },\n} as const satisfies ModifierRoles;\n\nconst lockHeld = roleReader(HANDLE_LOCK_ROLES);\n\n/** The locks in force: those `held` names, plus those the event's\n * {@link HANDLE_LOCK_ROLES} hold. */\nexport function locksInForce(\n held: HandleLocks,\n ctx: { roles?: Readonly<Record<string, boolean>>; modifiers: ModifierState },\n): HandleLocks {\n return {\n angle: held.angle || lockHeld(ctx, 'handle.holdAngle'),\n length: held.length || lockHeld(ctx, 'handle.holdLength'),\n };\n}\n\n/** The handle dragged toward `pointer` under `locks`. */\nexport function lockHandle(press: HandlePress, pointer: WorldPoint, locks: HandleLocks): LockedHandle {\n const { anchor, handle } = press;\n const len = dist(anchor, handle);\n if (locks.angle && locks.length) return { to: { ...handle }, guide: lineGuide(anchor, handle, len) };\n if (locks.angle) {\n const to = handleAlongItsAngle(anchor, handle, pointer);\n return { to, guide: lineGuide(anchor, handle, Math.max(len, dist(anchor, to))) };\n }\n if (locks.length) return { to: handleAtItsLength(anchor, handle, pointer), guide: circleGuide(anchor, len) };\n return { to: pointer, guide: null };\n}\n\n/** The line through `anchor` toward `toward`, reaching past `reach` on both sides. */\nfunction lineGuide(anchor: Point, toward: Point, reach: number): OngoingOverlay {\n const d = dist(anchor, toward);\n if (d === 0) return { kind: 'polyline', points: [anchor, anchor], role: 'lock' };\n const span = reach * LINE_OVERSHOOT;\n const ux = ((toward.x - anchor.x) / d) * span;\n const uy = ((toward.y - anchor.y) / d) * span;\n return {\n kind: 'polyline',\n points: [{ x: anchor.x - ux, y: anchor.y - uy }, { x: anchor.x + ux, y: anchor.y + uy }],\n role: 'lock',\n };\n}\n\nfunction circleGuide(center: Point, r: number): OngoingOverlay {\n const points: Point[] = [];\n for (let i = 0; i <= CIRCLE_SEGMENTS; i++) {\n const a = (i / CIRCLE_SEGMENTS) * Math.PI * 2;\n points.push({ x: center.x + Math.cos(a) * r, y: center.y + Math.sin(a) * r });\n }\n return { kind: 'polyline', points, role: 'lock' };\n}\n\nfunction dist(a: Point, b: Point): number {\n return Math.hypot(b.x - a.x, b.y - a.y);\n}\n"],"mappings":";;;;;;;;AAqBA,IAAM,kBAAkB;;AAExB,IAAM,iBAAiB;;;AAIvB,IAAa,oBAAoB;CAC/B,oBAAoB;EAAE,OAAO;EAAyB,MAAM,CAAC,OAAO;CAAE;CACtE,qBAAqB;EAAE,OAAO;EAA0B,MAAM,CAAC,QAAQ,MAAM;CAAE;AACjF;AAEA,IAAM,WAAW,WAAW,iBAAiB;;;AAI7C,SAAgB,aACd,MACA,KACa;CACb,OAAO;EACL,OAAO,KAAK,SAAS,SAAS,KAAK,kBAAkB;EACrD,QAAQ,KAAK,UAAU,SAAS,KAAK,mBAAmB;CAC1D;AACF;;AAGA,SAAgB,WAAW,OAAoB,SAAqB,OAAkC;CACpG,MAAM,EAAE,QAAQ,WAAW;CAC3B,MAAM,MAAM,KAAK,QAAQ,MAAM;CAC/B,IAAI,MAAM,SAAS,MAAM,QAAQ,OAAO;EAAE,IAAI,EAAE,GAAG,OAAO;EAAG,OAAO,UAAU,QAAQ,QAAQ,GAAG;CAAE;CACnG,IAAI,MAAM,OAAO;EACf,MAAM,KAAK,oBAAoB,QAAQ,QAAQ,OAAO;EACtD,OAAO;GAAE;GAAI,OAAO,UAAU,QAAQ,QAAQ,KAAK,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC,CAAC;EAAE;CACjF;CACA,IAAI,MAAM,QAAQ,OAAO;EAAE,IAAI,kBAAkB,QAAQ,QAAQ,OAAO;EAAG,OAAO,YAAY,QAAQ,GAAG;CAAE;CAC3G,OAAO;EAAE,IAAI;EAAS,OAAO;CAAK;AACpC;;AAGA,SAAS,UAAU,QAAe,QAAe,OAA+B;CAC9E,MAAM,IAAI,KAAK,QAAQ,MAAM;CAC7B,IAAI,MAAM,GAAG,OAAO;EAAE,MAAM;EAAY,QAAQ,CAAC,QAAQ,MAAM;EAAG,MAAM;CAAO;CAC/E,MAAM,OAAO,QAAQ;CACrB,MAAM,MAAO,OAAO,IAAI,OAAO,KAAK,IAAK;CACzC,MAAM,MAAO,OAAO,IAAI,OAAO,KAAK,IAAK;CACzC,OAAO;EACL,MAAM;EACN,QAAQ,CAAC;GAAE,GAAG,OAAO,IAAI;GAAI,GAAG,OAAO,IAAI;EAAG,GAAG;GAAE,GAAG,OAAO,IAAI;GAAI,GAAG,OAAO,IAAI;EAAG,CAAC;EACvF,MAAM;CACR;AACF;AAEA,SAAS,YAAY,QAAe,GAA2B;CAC7D,MAAM,SAAkB,CAAC;CACzB,KAAK,IAAI,IAAI,GAAG,KAAK,iBAAiB,KAAK;EACzC,MAAM,IAAK,IAAI,kBAAmB,KAAK,KAAK;EAC5C,OAAO,KAAK;GAAE,GAAG,OAAO,IAAI,KAAK,IAAI,CAAC,IAAI;GAAG,GAAG,OAAO,IAAI,KAAK,IAAI,CAAC,IAAI;EAAE,CAAC;CAC9E;CACA,OAAO;EAAE,MAAM;EAAY;EAAQ,MAAM;CAAO;AAClD;AAEA,SAAS,KAAK,GAAU,GAAkB;CACxC,OAAO,KAAK,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AACxC"}
1
+ {"version":3,"file":"handleDrag.js","names":[],"sources":["../../../../src/interactions/actions/edit-anchors/handleDrag.ts"],"sourcesContent":["/**\n * The locks on a control-handle drag — its angle, its length, or both — and\n * the guide that shows which one is holding.\n */\nimport { roleReader, type ModifierRoles, type ModifierState, type OngoingOverlay, type WorldPoint } from '@weasel-js/routing';\nimport { distance, handleAlongItsAngle, handleAtItsLength, type Point } from '@weasel-js/geom';\nimport type { HandleLocks } from './params';\n\n/** Where a handle drag's anchor and handle sat at the press. */\nexport interface HandlePress {\n anchor: Point;\n handle: Point;\n}\n\n/** Where the handle goes this move, and the guide its lock draws. */\nexport interface LockedHandle {\n to: Point;\n /** The line or circle the handle is held to; null with no lock held. */\n guide: OngoingOverlay | null;\n}\n\nconst CIRCLE_SEGMENTS = 64;\n/** How far past the handle the angle guide reaches, as a share of its length. */\nconst LINE_OVERSHOOT = 1.25;\n\n/** The modifier roles a handle drag reads: Shift holds its angle, Cmd or\n * Ctrl its length. */\nexport const HANDLE_LOCK_ROLES = {\n 'handle.holdAngle': { label: 'Hold the handle angle', keys: ['shift'] },\n 'handle.holdLength': { label: 'Hold the handle length', keys: ['meta', 'ctrl'] },\n} as const satisfies ModifierRoles;\n\nconst lockHeld = roleReader(HANDLE_LOCK_ROLES);\n\n/** The locks in force: those `held` names, plus those the event's\n * {@link HANDLE_LOCK_ROLES} hold. */\nexport function locksInForce(\n held: HandleLocks,\n ctx: { roles?: Readonly<Record<string, boolean>>; modifiers: ModifierState },\n): HandleLocks {\n return {\n angle: held.angle || lockHeld(ctx, 'handle.holdAngle'),\n length: held.length || lockHeld(ctx, 'handle.holdLength'),\n };\n}\n\n/** The handle dragged toward `pointer` under `locks`. */\nexport function lockHandle(press: HandlePress, pointer: WorldPoint, locks: HandleLocks): LockedHandle {\n const { anchor, handle } = press;\n const len = distance(anchor, handle);\n if (locks.angle && locks.length) return { to: { ...handle }, guide: lineGuide(anchor, handle, len) };\n if (locks.angle) {\n const to = handleAlongItsAngle(anchor, handle, pointer);\n return { to, guide: lineGuide(anchor, handle, Math.max(len, distance(anchor, to))) };\n }\n if (locks.length) return { to: handleAtItsLength(anchor, handle, pointer), guide: circleGuide(anchor, len) };\n return { to: pointer, guide: null };\n}\n\n/** The line through `anchor` toward `toward`, reaching past `reach` on both sides. */\nfunction lineGuide(anchor: Point, toward: Point, reach: number): OngoingOverlay {\n const d = distance(anchor, toward);\n if (d === 0) return { kind: 'polyline', points: [anchor, anchor], role: 'lock' };\n const span = reach * LINE_OVERSHOOT;\n const ux = ((toward.x - anchor.x) / d) * span;\n const uy = ((toward.y - anchor.y) / d) * span;\n return {\n kind: 'polyline',\n points: [{ x: anchor.x - ux, y: anchor.y - uy }, { x: anchor.x + ux, y: anchor.y + uy }],\n role: 'lock',\n };\n}\n\nfunction circleGuide(center: Point, r: number): OngoingOverlay {\n const points: Point[] = [];\n for (let i = 0; i <= CIRCLE_SEGMENTS; i++) {\n const a = (i / CIRCLE_SEGMENTS) * Math.PI * 2;\n points.push({ x: center.x + Math.cos(a) * r, y: center.y + Math.sin(a) * r });\n }\n return { kind: 'polyline', points, role: 'lock' };\n}\n"],"mappings":";;;;;;;;AAqBA,IAAM,kBAAkB;;AAExB,IAAM,iBAAiB;;;AAIvB,IAAa,oBAAoB;CAC/B,oBAAoB;EAAE,OAAO;EAAyB,MAAM,CAAC,OAAO;CAAE;CACtE,qBAAqB;EAAE,OAAO;EAA0B,MAAM,CAAC,QAAQ,MAAM;CAAE;AACjF;AAEA,IAAM,WAAW,WAAW,iBAAiB;;;AAI7C,SAAgB,aACd,MACA,KACa;CACb,OAAO;EACL,OAAO,KAAK,SAAS,SAAS,KAAK,kBAAkB;EACrD,QAAQ,KAAK,UAAU,SAAS,KAAK,mBAAmB;CAC1D;AACF;;AAGA,SAAgB,WAAW,OAAoB,SAAqB,OAAkC;CACpG,MAAM,EAAE,QAAQ,WAAW;CAC3B,MAAM,MAAM,SAAS,QAAQ,MAAM;CACnC,IAAI,MAAM,SAAS,MAAM,QAAQ,OAAO;EAAE,IAAI,EAAE,GAAG,OAAO;EAAG,OAAO,UAAU,QAAQ,QAAQ,GAAG;CAAE;CACnG,IAAI,MAAM,OAAO;EACf,MAAM,KAAK,oBAAoB,QAAQ,QAAQ,OAAO;EACtD,OAAO;GAAE;GAAI,OAAO,UAAU,QAAQ,QAAQ,KAAK,IAAI,KAAK,SAAS,QAAQ,EAAE,CAAC,CAAC;EAAE;CACrF;CACA,IAAI,MAAM,QAAQ,OAAO;EAAE,IAAI,kBAAkB,QAAQ,QAAQ,OAAO;EAAG,OAAO,YAAY,QAAQ,GAAG;CAAE;CAC3G,OAAO;EAAE,IAAI;EAAS,OAAO;CAAK;AACpC;;AAGA,SAAS,UAAU,QAAe,QAAe,OAA+B;CAC9E,MAAM,IAAI,SAAS,QAAQ,MAAM;CACjC,IAAI,MAAM,GAAG,OAAO;EAAE,MAAM;EAAY,QAAQ,CAAC,QAAQ,MAAM;EAAG,MAAM;CAAO;CAC/E,MAAM,OAAO,QAAQ;CACrB,MAAM,MAAO,OAAO,IAAI,OAAO,KAAK,IAAK;CACzC,MAAM,MAAO,OAAO,IAAI,OAAO,KAAK,IAAK;CACzC,OAAO;EACL,MAAM;EACN,QAAQ,CAAC;GAAE,GAAG,OAAO,IAAI;GAAI,GAAG,OAAO,IAAI;EAAG,GAAG;GAAE,GAAG,OAAO,IAAI;GAAI,GAAG,OAAO,IAAI;EAAG,CAAC;EACvF,MAAM;CACR;AACF;AAEA,SAAS,YAAY,QAAe,GAA2B;CAC7D,MAAM,SAAkB,CAAC;CACzB,KAAK,IAAI,IAAI,GAAG,KAAK,iBAAiB,KAAK;EACzC,MAAM,IAAK,IAAI,kBAAmB,KAAK,KAAK;EAC5C,OAAO,KAAK;GAAE,GAAG,OAAO,IAAI,KAAK,IAAI,CAAC,IAAI;GAAG,GAAG,OAAO,IAAI,KAAK,IAAI,CAAC,IAAI;EAAE,CAAC;CAC9E;CACA,OAAO;EAAE,MAAM;EAAY;EAAQ,MAAM;CAAO;AAClD"}
@@ -0,0 +1,42 @@
1
+ import { AUTO_POSE_DESCRIPTOR } from "../../../../core/geometry/autoPoseDescriptor.js";
2
+ import { unionRects } from "@weasel-js/geom";
3
+
4
+ // src/interactions/actions/move/behaviors/clampMove.ts
5
+ /** The shift along one axis that keeps `[lo, hi]` moved by `d` inside `[min, max]`; a span wider
6
+ * than the range is held to its start. */
7
+ function clampAxis(d, lo, hi, min, max) {
8
+ if (hi + d > max) d = max - hi;
9
+ if (lo + d < min) d = min - lo;
10
+ return d;
11
+ }
12
+ /**
13
+ * Move behavior that keeps the dragged nodes inside `bounds`: the drag's translation is
14
+ * clamped so the box around every dragged node's start pose, moved by it, stays in the rect.
15
+ * The clamp applies to the preview and the commit alike, so a node let go past an edge lands
16
+ * on it. Put it after behaviors that reshape the translation, so it has the last word.
17
+ */
18
+ function clampMove(opts) {
19
+ const d = opts.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
20
+ return { onMove(ctx, proposed) {
21
+ const b = typeof opts.bounds === "function" ? opts.bounds() : opts.bounds;
22
+ const starts = [];
23
+ for (const id of ctx.draggedIds) {
24
+ const pose = ctx.origin.get(id);
25
+ if (pose !== void 0) starts.push(d.getBounds(pose));
26
+ }
27
+ const u = unionRects(starts);
28
+ if (!u) return void 0;
29
+ const x0 = u.x, y0 = u.y, x1 = u.x + u.width, y1 = u.y + u.height;
30
+ const dx = clampAxis(proposed.dx, x0, x1, b.x, b.x + b.width);
31
+ const dy = clampAxis(proposed.dy, y0, y1, b.y, b.y + b.height);
32
+ if (dx === proposed.dx && dy === proposed.dy) return void 0;
33
+ return { transform: {
34
+ kind: "translate",
35
+ dx,
36
+ dy
37
+ } };
38
+ } };
39
+ }
40
+
41
+ export { clampMove };
42
+ //# sourceMappingURL=clampMove.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clampMove.js","names":[],"sources":["../../../../../src/interactions/actions/move/behaviors/clampMove.ts"],"sourcesContent":["import { unionRects, type FramedBox, type Rect } from '@weasel-js/geom';\nimport type { PoseDescriptor } from 'core/geometry/poseDescriptor';\nimport { AUTO_POSE_DESCRIPTOR } from 'core/geometry/autoPoseDescriptor';\nimport type { MoveBehavior } from '../../../gestures/types';\n\n/** What {@link clampMove} takes. */\nexport interface ClampMoveOptions<TPose> {\n /** The world rect the dragged nodes stay inside, or a function read each frame for one that changes. */\n bounds: FramedBox | (() => FramedBox);\n /** Reads each pose's box. Default `AUTO_POSE_DESCRIPTOR`. */\n poseDescriptor?: PoseDescriptor<TPose>;\n}\n\n/** The shift along one axis that keeps `[lo, hi]` moved by `d` inside `[min, max]`; a span wider\n * than the range is held to its start. */\nfunction clampAxis(d: number, lo: number, hi: number, min: number, max: number): number {\n if (hi + d > max) d = max - hi;\n if (lo + d < min) d = min - lo;\n return d;\n}\n\n/**\n * Move behavior that keeps the dragged nodes inside `bounds`: the drag's translation is\n * clamped so the box around every dragged node's start pose, moved by it, stays in the rect.\n * The clamp applies to the preview and the commit alike, so a node let go past an edge lands\n * on it. Put it after behaviors that reshape the translation, so it has the last word.\n */\nexport function clampMove<TPose>(opts: ClampMoveOptions<TPose>): MoveBehavior<TPose> {\n const d = (opts.poseDescriptor ?? AUTO_POSE_DESCRIPTOR) as PoseDescriptor<TPose>;\n return {\n onMove(ctx, proposed) {\n const b = typeof opts.bounds === 'function' ? opts.bounds() : opts.bounds;\n const starts: Rect[] = [];\n for (const id of ctx.draggedIds) {\n const pose = ctx.origin.get(id);\n if (pose !== undefined) starts.push(d.getBounds(pose));\n }\n const u = unionRects(starts);\n if (!u) return undefined;\n const x0 = u.x, y0 = u.y, x1 = u.x + u.width, y1 = u.y + u.height;\n const dx = clampAxis(proposed.dx, x0, x1, b.x, b.x + b.width);\n const dy = clampAxis(proposed.dy, y0, y1, b.y, b.y + b.height);\n if (dx === proposed.dx && dy === proposed.dy) return undefined;\n return { transform: { kind: 'translate', dx, dy } };\n },\n };\n}\n"],"mappings":";;;;;;AAeA,SAAS,UAAU,GAAW,IAAY,IAAY,KAAa,KAAqB;CACtF,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM;CAC5B,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM;CAC5B,OAAO;AACT;;;;;;;AAQA,SAAgB,UAAiB,MAAoD;CACnF,MAAM,IAAK,KAAK,kBAAkB;CAClC,OAAO,EACL,OAAO,KAAK,UAAU;EACpB,MAAM,IAAI,OAAO,KAAK,WAAW,aAAa,KAAK,OAAO,IAAI,KAAK;EACnE,MAAM,SAAiB,CAAC;EACxB,KAAK,MAAM,MAAM,IAAI,YAAY;GAC/B,MAAM,OAAO,IAAI,OAAO,IAAI,EAAE;GAC9B,IAAI,SAAS,QAAW,OAAO,KAAK,EAAE,UAAU,IAAI,CAAC;EACvD;EACA,MAAM,IAAI,WAAW,MAAM;EAC3B,IAAI,CAAC,GAAG,OAAO;EACf,MAAM,KAAK,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,OAAO,KAAK,EAAE,IAAI,EAAE;EAC3D,MAAM,KAAK,UAAU,SAAS,IAAI,IAAI,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK;EAC5D,MAAM,KAAK,UAAU,SAAS,IAAI,IAAI,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM;EAC7D,IAAI,OAAO,SAAS,MAAM,OAAO,SAAS,IAAI,OAAO;EACrD,OAAO,EAAE,WAAW;GAAE,MAAM;GAAa;GAAI;EAAG,EAAE;CACpD,EACF;AACF"}
@@ -6,12 +6,9 @@ import { planeOf } from "../../planeInput.js";
6
6
  import { carryDragged } from "./dragged.js";
7
7
  import { refusalOf } from "../../dropVerdict.js";
8
8
  import { boxOf, containerView, draggedInView } from "./view.js";
9
- import { pointInPath } from "@weasel-js/geom";
9
+ import { pointInPath, rectContainsPointHalfOpen } from "@weasel-js/geom";
10
10
 
11
11
  // src/interactions/actions/move/layout/container.ts
12
- function pointInBounds(b, at) {
13
- return at.x >= b.x && at.x < b.x + b.width && at.y >= b.y && at.y < b.y + b.height;
14
- }
15
12
  /** Whether a world point falls in a drop region, which is authored relative to
16
13
  * the top-left of the container's world bounds. */
17
14
  function regionContains(region, bounds, point) {
@@ -70,7 +67,7 @@ function pickContainer(session, dragged, center) {
70
67
  if (region) return regionContains(region, aabb, at);
71
68
  if (mode === "region") return false;
72
69
  if (layout.contains) return layout.contains(cPose, at);
73
- return pointInBounds(aabb, at);
70
+ return rectContainsPointHalfOpen(aabb, at.x, at.y);
74
71
  };
75
72
  const consider = (id, zPath) => {
76
73
  const layout = layoutDep.getLayout(id);
@@ -120,5 +117,5 @@ function pickContainer(session, dragged, center) {
120
117
  return chooseContainer(scene, mode, candidates);
121
118
  }
122
119
 
123
- export { pickContainer, pointInBounds };
120
+ export { pickContainer };
124
121
  //# sourceMappingURL=container.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"container.js","names":[],"sources":["../../../../../src/interactions/actions/move/layout/container.ts"],"sourcesContent":["import { pointInPath } from '@weasel-js/geom';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { poseDescriptorForNode } from 'core/geometry/poseDescriptor';\nimport { toPlane, type PlaneMap } from 'core/viewport/parallax';\nimport { childFrameAt, composeWorldPose } from 'core/scene/composePose';\nimport { sceneViewPoseAdapter } from 'core/scene/scenePoseAdapter';\nimport type { DropRegion, LayoutContainer, LayoutStrategy } from '../../../../layout/types';\nimport type { LayoutDep } from '../../depSchema';\nimport { planeOf } from '../../planeInput';\nimport type { MoveSession } from '../session';\nimport { carryDragged, type DraggedChild } from './dragged';\nimport { refusalOf } from '../../dropVerdict';\nimport type { StrategyView } from 'core/scene/strategyView';\nimport { boxOf, containerView, draggedInView } from './view';\n\nexport interface Bounds4 { x: number; y: number; width: number; height: number }\nexport function pointInBounds(b: Bounds4, at: { x: number; y: number }): boolean {\n return at.x >= b.x && at.x < b.x + b.width && at.y >= b.y && at.y < b.y + b.height;\n}\n\ntype Layout = LayoutStrategy<unknown>;\nexport interface Candidate {\n id: NodeId;\n /** The container's body in world: what the pointer is tested against. */\n bounds: Bounds4;\n /** Its content's frame, `bounds` moved by its scroll offset, in `view`'s\n * space: the `LayoutContainer.bounds` its strategy is handed, so the slots\n * it places agree with its children as they are shown. */\n content: Bounds4;\n /** The container as its strategy sees it: upright, in its own frame. */\n view: StrategyView<unknown>;\n layout: LayoutStrategy<unknown>;\n zPath: number[];\n depth: number;\n /** The edited plane into this container's. */\n plane: PlaneMap | null;\n /** The reason it refuses the drop, when it does: it still blocks the containers around it. */\n refused?: string;\n}\n\n/** Whether a world point falls in a drop region, which is authored relative to\n * the top-left of the container's world bounds. */\nfunction regionContains(\n region: DropRegion,\n bounds: { x: number; y: number },\n point: { x: number; y: number },\n): boolean {\n const local = { x: point.x - bounds.x, y: point.y - bounds.y };\n return typeof region === 'function'\n ? region(local)\n : pointInPath(region, local.x, local.y);\n}\n\nfunction chooseContainer(\n scene: Scene<unknown, string, unknown>,\n mode: NonNullable<LayoutDep['dropTarget']>,\n candidates: readonly Candidate[],\n): Candidate | null {\n let dest: Candidate | null = null;\n if (mode === 'topmost') {\n // Painted last wins, in the scene's own render order — which crosses\n // subtrees and layers, where a sibling-index path cannot.\n const byId = new Map<NodeId, Candidate>(candidates.map((c) => [c.id, c]));\n for (const id of scene.renderOrder()) dest = byId.get(id) ?? dest;\n } else {\n // Deepest wins; sibling-index path breaks ties (higher z = later index).\n for (const c of candidates) {\n if (dest === null) { dest = c; continue; }\n if (c.depth > dest.depth) { dest = c; continue; }\n if (c.depth < dest.depth) continue;\n let cAfter = false;\n for (let i = 0; i < c.zPath.length; i++) {\n if (c.zPath[i] > dest.zPath[i]) { cAfter = true; break; }\n if (c.zPath[i] < dest.zPath[i]) { cAfter = false; break; }\n }\n if (cAfter) dest = c;\n }\n }\n return dest;\n}\n\n/** The layout container that takes the drop at `center`, or null. */\nexport function pickContainer(\n session: MoveSession,\n dragged: readonly DraggedChild[],\n center: { x: number; y: number },\n): Candidate | null {\n const layoutDep = session.layout;\n if (!layoutDep) return null;\n const scene = session.scene;\n const pc = session.pc;\n const poseAdapter = sceneViewPoseAdapter(scene);\n const d = session.projection;\n const draggedIds = new Set<NodeId>(dragged.map((c) => c.id));\n const candidates: Candidate[] = [];\n const mode = layoutDep.dropTarget ?? 'innermost';\n const testInside = (\n id: NodeId, cPose: unknown, aabb: Bounds4, layout: Layout, at: { x: number; y: number },\n ): boolean => {\n const region = layout.dropRegion?.({ id: id as string, bounds: aabb }) ?? null;\n if (region) return regionContains(region, aabb, at);\n if (mode === 'region') return false;\n if (layout.contains) return layout.contains(cPose, at);\n return pointInBounds(aabb, at);\n };\n const consider = (id: NodeId, zPath: number[]): void => {\n const layout = layoutDep.getLayout(id as string);\n if (!layout) return;\n const node = scene.get(id);\n if (!node) return;\n const worldPose = composeWorldPose(poseAdapter, id as string, pc.compose);\n const g = poseDescriptorForNode(d, node);\n const worldAABB = g.getBounds(worldPose);\n const plane = planeOf(session.view, node.layer);\n const at = plane ? toPlane(plane, center) : center;\n if (!testInside(id, worldPose, worldAABB, layout, at)) return;\n const verdict = session.verdict(id);\n if (verdict === false) return;\n const refused = refusalOf(verdict);\n const view = containerView(pc, g, worldPose);\n const content = boxOf(g, view.toStrategy(childFrameAt(poseAdapter, id as string, worldPose)));\n if (refused === null && layout.acceptsDrop) {\n const arg: LayoutContainer = { id: id as string, bounds: content };\n for (const c of dragged) {\n if (!layout.acceptsDrop(arg, draggedInView(view, carryDragged(session, c, plane).arg))) return;\n }\n }\n candidates.push({\n id,\n bounds: worldAABB,\n content,\n view,\n layout,\n zPath,\n depth: zPath.length,\n plane,\n ...(refused !== null ? { refused } : {}),\n });\n };\n const walk = (parentId: NodeId | null, parentPath: number[]): void => {\n const childIds = parentId === null ? scene.roots : scene.childrenOf(parentId);\n for (let i = 0; i < childIds.length; i++) {\n // A dragged root's subtree goes with it: dropping into one of its own\n // descendants would reparent it under itself.\n if (draggedIds.has(childIds[i])) continue;\n const childPath = [...parentPath, i];\n consider(childIds[i], childPath);\n walk(childIds[i], childPath);\n }\n };\n walk(null, []);\n\n return chooseContainer(scene, mode, candidates);\n}\n"],"mappings":";;;;;;;;;;;AAgBA,SAAgB,cAAc,GAAY,IAAuC;CAC/E,OAAO,GAAG,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,EAAE,SAAS,GAAG,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,EAAE;AAC9E;;;AAwBA,SAAS,eACP,QACA,QACA,OACS;CACT,MAAM,QAAQ;EAAE,GAAG,MAAM,IAAI,OAAO;EAAG,GAAG,MAAM,IAAI,OAAO;CAAE;CAC7D,OAAO,OAAO,WAAW,aACrB,OAAO,KAAK,IACZ,YAAY,QAAQ,MAAM,GAAG,MAAM,CAAC;AAC1C;AAEA,SAAS,gBACP,OACA,MACA,YACkB;CAClB,IAAI,OAAyB;CAC7B,IAAI,SAAS,WAAW;EAGtB,MAAM,OAAO,IAAI,IAAuB,WAAW,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;EACxE,KAAK,MAAM,MAAM,MAAM,YAAY,GAAG,OAAO,KAAK,IAAI,EAAE,KAAK;CAC/D,OAEE,KAAK,MAAM,KAAK,YAAY;EAC1B,IAAI,SAAS,MAAM;GAAE,OAAO;GAAG;EAAU;EACzC,IAAI,EAAE,QAAQ,KAAK,OAAO;GAAE,OAAO;GAAG;EAAU;EAChD,IAAI,EAAE,QAAQ,KAAK,OAAO;EAC1B,IAAI,SAAS;EACb,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,MAAM,QAAQ,KAAK;GACvC,IAAI,EAAE,MAAM,KAAK,KAAK,MAAM,IAAI;IAAE,SAAS;IAAM;GAAO;GACxD,IAAI,EAAE,MAAM,KAAK,KAAK,MAAM,IAAI;IAAE,SAAS;IAAO;GAAO;EAC3D;EACA,IAAI,QAAQ,OAAO;CACrB;CAEF,OAAO;AACT;;AAGA,SAAgB,cACd,SACA,SACA,QACkB;CAClB,MAAM,YAAY,QAAQ;CAC1B,IAAI,CAAC,WAAW,OAAO;CACvB,MAAM,QAAQ,QAAQ;CACtB,MAAM,KAAK,QAAQ;CACnB,MAAM,cAAc,qBAAqB,KAAK;CAC9C,MAAM,IAAI,QAAQ;CAClB,MAAM,aAAa,IAAI,IAAY,QAAQ,KAAK,MAAM,EAAE,EAAE,CAAC;CAC3D,MAAM,aAA0B,CAAC;CACjC,MAAM,OAAO,UAAU,cAAc;CACrC,MAAM,cACJ,IAAY,OAAgB,MAAe,QAAgB,OAC/C;EACZ,MAAM,SAAS,OAAO,aAAa;GAAM;GAAc,QAAQ;EAAK,CAAC,KAAK;EAC1E,IAAI,QAAQ,OAAO,eAAe,QAAQ,MAAM,EAAE;EAClD,IAAI,SAAS,UAAU,OAAO;EAC9B,IAAI,OAAO,UAAU,OAAO,OAAO,SAAS,OAAO,EAAE;EACrD,OAAO,cAAc,MAAM,EAAE;CAC/B;CACA,MAAM,YAAY,IAAY,UAA0B;EACtD,MAAM,SAAS,UAAU,UAAU,EAAY;EAC/C,IAAI,CAAC,QAAQ;EACb,MAAM,OAAO,MAAM,IAAI,EAAE;EACzB,IAAI,CAAC,MAAM;EACX,MAAM,YAAY,iBAAiB,aAAa,IAAc,GAAG,OAAO;EACxE,MAAM,IAAI,sBAAsB,GAAG,IAAI;EACvC,MAAM,YAAY,EAAE,UAAU,SAAS;EACvC,MAAM,QAAQ,QAAQ,QAAQ,MAAM,KAAK,KAAK;EAC9C,MAAM,KAAK,QAAQ,QAAQ,OAAO,MAAM,IAAI;EAC5C,IAAI,CAAC,WAAW,IAAI,WAAW,WAAW,QAAQ,EAAE,GAAG;EACvD,MAAM,UAAU,QAAQ,QAAQ,EAAE;EAClC,IAAI,YAAY,OAAO;EACvB,MAAM,UAAU,UAAU,OAAO;EACjC,MAAM,OAAO,cAAc,IAAI,GAAG,SAAS;EAC3C,MAAM,UAAU,MAAM,GAAG,KAAK,WAAW,aAAa,aAAa,IAAc,SAAS,CAAC,CAAC;EAC5F,IAAI,YAAY,QAAQ,OAAO,aAAa;GAC1C,MAAM,MAAuB;IAAM;IAAc,QAAQ;GAAQ;GACjE,KAAK,MAAM,KAAK,SACd,IAAI,CAAC,OAAO,YAAY,KAAK,cAAc,MAAM,aAAa,SAAS,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG;EAE5F;EACA,WAAW,KAAK;GACd;GACA,QAAQ;GACR;GACA;GACA;GACA;GACA,OAAO,MAAM;GACb;GACA,GAAI,YAAY,OAAO,EAAE,QAAQ,IAAI,CAAC;EACxC,CAAC;CACH;CACA,MAAM,QAAQ,UAAyB,eAA+B;EACpE,MAAM,WAAW,aAAa,OAAO,MAAM,QAAQ,MAAM,WAAW,QAAQ;EAC5E,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GAGxC,IAAI,WAAW,IAAI,SAAS,EAAE,GAAG;GACjC,MAAM,YAAY,CAAC,GAAG,YAAY,CAAC;GACnC,SAAS,SAAS,IAAI,SAAS;GAC/B,KAAK,SAAS,IAAI,SAAS;EAC7B;CACF;CACA,KAAK,MAAM,CAAC,CAAC;CAEb,OAAO,gBAAgB,OAAO,MAAM,UAAU;AAChD"}
1
+ {"version":3,"file":"container.js","names":[],"sources":["../../../../../src/interactions/actions/move/layout/container.ts"],"sourcesContent":["import { pointInPath, rectContainsPointHalfOpen } from '@weasel-js/geom';\nimport type { NodeId, Scene } from 'core/scene/types';\nimport { poseDescriptorForNode } from 'core/geometry/poseDescriptor';\nimport { toPlane, type PlaneMap } from 'core/viewport/parallax';\nimport { childFrameAt, composeWorldPose } from 'core/scene/composePose';\nimport { sceneViewPoseAdapter } from 'core/scene/scenePoseAdapter';\nimport type { DropRegion, LayoutContainer, LayoutStrategy } from '../../../../layout/types';\nimport type { LayoutDep } from '../../depSchema';\nimport { planeOf } from '../../planeInput';\nimport type { MoveSession } from '../session';\nimport { carryDragged, type DraggedChild } from './dragged';\nimport { refusalOf } from '../../dropVerdict';\nimport type { StrategyView } from 'core/scene/strategyView';\nimport { boxOf, containerView, draggedInView } from './view';\n\nexport interface Bounds4 { x: number; y: number; width: number; height: number }\n\ntype Layout = LayoutStrategy<unknown>;\nexport interface Candidate {\n id: NodeId;\n /** The container's body in world: what the pointer is tested against. */\n bounds: Bounds4;\n /** Its content's frame, `bounds` moved by its scroll offset, in `view`'s\n * space: the `LayoutContainer.bounds` its strategy is handed, so the slots\n * it places agree with its children as they are shown. */\n content: Bounds4;\n /** The container as its strategy sees it: upright, in its own frame. */\n view: StrategyView<unknown>;\n layout: LayoutStrategy<unknown>;\n zPath: number[];\n depth: number;\n /** The edited plane into this container's. */\n plane: PlaneMap | null;\n /** The reason it refuses the drop, when it does: it still blocks the containers around it. */\n refused?: string;\n}\n\n/** Whether a world point falls in a drop region, which is authored relative to\n * the top-left of the container's world bounds. */\nfunction regionContains(\n region: DropRegion,\n bounds: { x: number; y: number },\n point: { x: number; y: number },\n): boolean {\n const local = { x: point.x - bounds.x, y: point.y - bounds.y };\n return typeof region === 'function'\n ? region(local)\n : pointInPath(region, local.x, local.y);\n}\n\nfunction chooseContainer(\n scene: Scene<unknown, string, unknown>,\n mode: NonNullable<LayoutDep['dropTarget']>,\n candidates: readonly Candidate[],\n): Candidate | null {\n let dest: Candidate | null = null;\n if (mode === 'topmost') {\n // Painted last wins, in the scene's own render order — which crosses\n // subtrees and layers, where a sibling-index path cannot.\n const byId = new Map<NodeId, Candidate>(candidates.map((c) => [c.id, c]));\n for (const id of scene.renderOrder()) dest = byId.get(id) ?? dest;\n } else {\n // Deepest wins; sibling-index path breaks ties (higher z = later index).\n for (const c of candidates) {\n if (dest === null) { dest = c; continue; }\n if (c.depth > dest.depth) { dest = c; continue; }\n if (c.depth < dest.depth) continue;\n let cAfter = false;\n for (let i = 0; i < c.zPath.length; i++) {\n if (c.zPath[i] > dest.zPath[i]) { cAfter = true; break; }\n if (c.zPath[i] < dest.zPath[i]) { cAfter = false; break; }\n }\n if (cAfter) dest = c;\n }\n }\n return dest;\n}\n\n/** The layout container that takes the drop at `center`, or null. */\nexport function pickContainer(\n session: MoveSession,\n dragged: readonly DraggedChild[],\n center: { x: number; y: number },\n): Candidate | null {\n const layoutDep = session.layout;\n if (!layoutDep) return null;\n const scene = session.scene;\n const pc = session.pc;\n const poseAdapter = sceneViewPoseAdapter(scene);\n const d = session.projection;\n const draggedIds = new Set<NodeId>(dragged.map((c) => c.id));\n const candidates: Candidate[] = [];\n const mode = layoutDep.dropTarget ?? 'innermost';\n const testInside = (\n id: NodeId, cPose: unknown, aabb: Bounds4, layout: Layout, at: { x: number; y: number },\n ): boolean => {\n const region = layout.dropRegion?.({ id: id as string, bounds: aabb }) ?? null;\n if (region) return regionContains(region, aabb, at);\n if (mode === 'region') return false;\n if (layout.contains) return layout.contains(cPose, at);\n return rectContainsPointHalfOpen(aabb, at.x, at.y);\n };\n const consider = (id: NodeId, zPath: number[]): void => {\n const layout = layoutDep.getLayout(id as string);\n if (!layout) return;\n const node = scene.get(id);\n if (!node) return;\n const worldPose = composeWorldPose(poseAdapter, id as string, pc.compose);\n const g = poseDescriptorForNode(d, node);\n const worldAABB = g.getBounds(worldPose);\n const plane = planeOf(session.view, node.layer);\n const at = plane ? toPlane(plane, center) : center;\n if (!testInside(id, worldPose, worldAABB, layout, at)) return;\n const verdict = session.verdict(id);\n if (verdict === false) return;\n const refused = refusalOf(verdict);\n const view = containerView(pc, g, worldPose);\n const content = boxOf(g, view.toStrategy(childFrameAt(poseAdapter, id as string, worldPose)));\n if (refused === null && layout.acceptsDrop) {\n const arg: LayoutContainer = { id: id as string, bounds: content };\n for (const c of dragged) {\n if (!layout.acceptsDrop(arg, draggedInView(view, carryDragged(session, c, plane).arg))) return;\n }\n }\n candidates.push({\n id,\n bounds: worldAABB,\n content,\n view,\n layout,\n zPath,\n depth: zPath.length,\n plane,\n ...(refused !== null ? { refused } : {}),\n });\n };\n const walk = (parentId: NodeId | null, parentPath: number[]): void => {\n const childIds = parentId === null ? scene.roots : scene.childrenOf(parentId);\n for (let i = 0; i < childIds.length; i++) {\n // A dragged root's subtree goes with it: dropping into one of its own\n // descendants would reparent it under itself.\n if (draggedIds.has(childIds[i])) continue;\n const childPath = [...parentPath, i];\n consider(childIds[i], childPath);\n walk(childIds[i], childPath);\n }\n };\n walk(null, []);\n\n return chooseContainer(scene, mode, candidates);\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { toPlane } from "../../../../core/viewport/parallax.js";
3
3
  import { composeWorldPose } from "../../../../core/scene/composeFrame.js";
4
4
  import { sceneViewPoseAdapter } from "../../../../core/scene/scenePoseAdapter.js";
5
5
  import { planeOf } from "../../planeInput.js";
6
- import { pointInBounds } from "./container.js";
6
+ import { rectContainsPointHalfOpen } from "@weasel-js/geom";
7
7
 
8
8
  // src/interactions/actions/move/layout/exits.ts
9
9
  /** Where each dragged root that leaves its layout container on release lands.
@@ -27,7 +27,7 @@ function resolveLayoutExits(session, dropCenter, ids) {
27
27
  const pose = composeWorldPose(poseAdapter, id, pc.compose);
28
28
  if (layout?.contains) return layout.contains(pose, at);
29
29
  const b = poseDescriptorForNode(d, node).getBounds(pose);
30
- return pointInBounds(b, at);
30
+ return rectContainsPointHalfOpen(b, at.x, at.y);
31
31
  };
32
32
  const landings = /* @__PURE__ */ new Map();
33
33
  const landingOn = (layer) => {
@@ -1 +1 @@
1
- {"version":3,"file":"exits.js","names":[],"sources":["../../../../../src/interactions/actions/move/layout/exits.ts"],"sourcesContent":["import type { NodeId } from 'core/scene/types';\nimport { poseDescriptorForNode } from 'core/geometry/poseDescriptor';\nimport { toPlane } from 'core/viewport/parallax';\nimport { composeWorldPose } from 'core/scene/composePose';\nimport { sceneViewPoseAdapter } from 'core/scene/scenePoseAdapter';\nimport type { LayoutStrategy } from '../../../../layout/types';\nimport { planeOf } from '../../planeInput';\nimport type { MoveSession } from '../session';\nimport { pointInBounds } from './container';\n\n/** Where each dragged root that leaves its layout container on release lands.\n * A root leaves when the drop center falls outside the body of the layout\n * container holding it, and lands under the innermost plain container beneath\n * the drop that admits it (`canDropInto`) — a layout container there has\n * already declined it — or at the top level of its layer. A root whose parent\n * has no layout is not this pass's to move; `reparentOnDrop` decides for those. */\nexport function resolveLayoutExits(\n session: MoveSession, dropCenter: { x: number; y: number } | null, ids: Iterable<NodeId>,\n): Map<NodeId, NodeId | null> {\n const out = new Map<NodeId, NodeId | null>();\n const layoutDep = session.layout;\n const center = dropCenter;\n if (!layoutDep || !center) return out;\n const { scene, pc, projection: d } = session;\n const poseAdapter = sceneViewPoseAdapter(scene);\n const excluded = new Set<NodeId>([...session.ids, ...session.cascadeIds]);\n const holds = (id: NodeId, layout: LayoutStrategy<unknown> | null): boolean => {\n const node = scene.get(id)!;\n const plane = planeOf(session.view, node.layer);\n const at = plane ? toPlane(plane, center) : center;\n const pose = composeWorldPose(poseAdapter, id as string, pc.compose);\n if (layout?.contains) return layout.contains(pose, at);\n const b = poseDescriptorForNode(d, node).getBounds(pose);\n return pointInBounds(b, at);\n };\n const landings = new Map<string, NodeId | null>();\n const landingOn = (layer: string): NodeId | null => {\n if (landings.has(layer)) return landings.get(layer)!;\n // Deepest wins; among equals, the later sibling (painted over the other).\n let best: NodeId | null = null;\n let bestDepth = -1;\n const walk = (parentId: NodeId | null, depth: number): void => {\n for (const id of parentId === null ? scene.roots : scene.childrenOf(parentId)) {\n if (excluded.has(id)) continue;\n const node = scene.get(id);\n if (!node) continue;\n if (node.layer === layer && depth >= bestDepth\n && layoutDep.getLayout(id as string) === null && holds(id, null) && session.admits(id)) {\n best = id;\n bestDepth = depth;\n }\n walk(id, depth + 1);\n }\n };\n walk(null, 0);\n landings.set(layer, best);\n return best;\n };\n for (const id of ids) {\n const node = scene.get(id);\n const parent = node?.parent ?? null;\n if (!node || parent === null) continue;\n const layout = layoutDep.getLayout(parent as string);\n if (!layout || holds(parent, layout)) continue;\n out.set(id, landingOn(node.layer));\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgB,mBACd,SAAsB,YAA6C,KACvC;CAC5B,MAAM,sBAAM,IAAI,IAA2B;CAC3C,MAAM,YAAY,QAAQ;CAC1B,MAAM,SAAS;CACf,IAAI,CAAC,aAAa,CAAC,QAAQ,OAAO;CAClC,MAAM,EAAE,OAAO,IAAI,YAAY,MAAM;CACrC,MAAM,cAAc,qBAAqB,KAAK;CAC9C,MAAM,2BAAW,IAAI,IAAY,CAAC,GAAG,QAAQ,KAAK,GAAG,QAAQ,UAAU,CAAC;CACxE,MAAM,SAAS,IAAY,WAAoD;EAC7E,MAAM,OAAO,MAAM,IAAI,EAAE;EACzB,MAAM,QAAQ,QAAQ,QAAQ,MAAM,KAAK,KAAK;EAC9C,MAAM,KAAK,QAAQ,QAAQ,OAAO,MAAM,IAAI;EAC5C,MAAM,OAAO,iBAAiB,aAAa,IAAc,GAAG,OAAO;EACnE,IAAI,QAAQ,UAAU,OAAO,OAAO,SAAS,MAAM,EAAE;EACrD,MAAM,IAAI,sBAAsB,GAAG,IAAI,CAAC,CAAC,UAAU,IAAI;EACvD,OAAO,cAAc,GAAG,EAAE;CAC5B;CACA,MAAM,2BAAW,IAAI,IAA2B;CAChD,MAAM,aAAa,UAAiC;EAClD,IAAI,SAAS,IAAI,KAAK,GAAG,OAAO,SAAS,IAAI,KAAK;EAElD,IAAI,OAAsB;EAC1B,IAAI,YAAY;EAChB,MAAM,QAAQ,UAAyB,UAAwB;GAC7D,KAAK,MAAM,MAAM,aAAa,OAAO,MAAM,QAAQ,MAAM,WAAW,QAAQ,GAAG;IAC7E,IAAI,SAAS,IAAI,EAAE,GAAG;IACtB,MAAM,OAAO,MAAM,IAAI,EAAE;IACzB,IAAI,CAAC,MAAM;IACX,IAAI,KAAK,UAAU,SAAS,SAAS,aAChC,UAAU,UAAU,EAAY,MAAM,QAAQ,MAAM,IAAI,IAAI,KAAK,QAAQ,OAAO,EAAE,GAAG;KACxF,OAAO;KACP,YAAY;IACd;IACA,KAAK,IAAI,QAAQ,CAAC;GACpB;EACF;EACA,KAAK,MAAM,CAAC;EACZ,SAAS,IAAI,OAAO,IAAI;EACxB,OAAO;CACT;CACA,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,IAAI,EAAE;EACzB,MAAM,SAAS,MAAM,UAAU;EAC/B,IAAI,CAAC,QAAQ,WAAW,MAAM;EAC9B,MAAM,SAAS,UAAU,UAAU,MAAgB;EACnD,IAAI,CAAC,UAAU,MAAM,QAAQ,MAAM,GAAG;EACtC,IAAI,IAAI,IAAI,UAAU,KAAK,KAAK,CAAC;CACnC;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"exits.js","names":[],"sources":["../../../../../src/interactions/actions/move/layout/exits.ts"],"sourcesContent":["import type { NodeId } from 'core/scene/types';\nimport { poseDescriptorForNode } from 'core/geometry/poseDescriptor';\nimport { toPlane } from 'core/viewport/parallax';\nimport { composeWorldPose } from 'core/scene/composePose';\nimport { sceneViewPoseAdapter } from 'core/scene/scenePoseAdapter';\nimport type { LayoutStrategy } from '../../../../layout/types';\nimport { planeOf } from '../../planeInput';\nimport type { MoveSession } from '../session';\nimport { rectContainsPointHalfOpen } from '@weasel-js/geom';\n\n/** Where each dragged root that leaves its layout container on release lands.\n * A root leaves when the drop center falls outside the body of the layout\n * container holding it, and lands under the innermost plain container beneath\n * the drop that admits it (`canDropInto`) — a layout container there has\n * already declined it — or at the top level of its layer. A root whose parent\n * has no layout is not this pass's to move; `reparentOnDrop` decides for those. */\nexport function resolveLayoutExits(\n session: MoveSession, dropCenter: { x: number; y: number } | null, ids: Iterable<NodeId>,\n): Map<NodeId, NodeId | null> {\n const out = new Map<NodeId, NodeId | null>();\n const layoutDep = session.layout;\n const center = dropCenter;\n if (!layoutDep || !center) return out;\n const { scene, pc, projection: d } = session;\n const poseAdapter = sceneViewPoseAdapter(scene);\n const excluded = new Set<NodeId>([...session.ids, ...session.cascadeIds]);\n const holds = (id: NodeId, layout: LayoutStrategy<unknown> | null): boolean => {\n const node = scene.get(id)!;\n const plane = planeOf(session.view, node.layer);\n const at = plane ? toPlane(plane, center) : center;\n const pose = composeWorldPose(poseAdapter, id as string, pc.compose);\n if (layout?.contains) return layout.contains(pose, at);\n const b = poseDescriptorForNode(d, node).getBounds(pose);\n return rectContainsPointHalfOpen(b, at.x, at.y);\n };\n const landings = new Map<string, NodeId | null>();\n const landingOn = (layer: string): NodeId | null => {\n if (landings.has(layer)) return landings.get(layer)!;\n // Deepest wins; among equals, the later sibling (painted over the other).\n let best: NodeId | null = null;\n let bestDepth = -1;\n const walk = (parentId: NodeId | null, depth: number): void => {\n for (const id of parentId === null ? scene.roots : scene.childrenOf(parentId)) {\n if (excluded.has(id)) continue;\n const node = scene.get(id);\n if (!node) continue;\n if (node.layer === layer && depth >= bestDepth\n && layoutDep.getLayout(id as string) === null && holds(id, null) && session.admits(id)) {\n best = id;\n bestDepth = depth;\n }\n walk(id, depth + 1);\n }\n };\n walk(null, 0);\n landings.set(layer, best);\n return best;\n };\n for (const id of ids) {\n const node = scene.get(id);\n const parent = node?.parent ?? null;\n if (!node || parent === null) continue;\n const layout = layoutDep.getLayout(parent as string);\n if (!layout || holds(parent, layout)) continue;\n out.set(id, landingOn(node.layer));\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgB,mBACd,SAAsB,YAA6C,KACvC;CAC5B,MAAM,sBAAM,IAAI,IAA2B;CAC3C,MAAM,YAAY,QAAQ;CAC1B,MAAM,SAAS;CACf,IAAI,CAAC,aAAa,CAAC,QAAQ,OAAO;CAClC,MAAM,EAAE,OAAO,IAAI,YAAY,MAAM;CACrC,MAAM,cAAc,qBAAqB,KAAK;CAC9C,MAAM,2BAAW,IAAI,IAAY,CAAC,GAAG,QAAQ,KAAK,GAAG,QAAQ,UAAU,CAAC;CACxE,MAAM,SAAS,IAAY,WAAoD;EAC7E,MAAM,OAAO,MAAM,IAAI,EAAE;EACzB,MAAM,QAAQ,QAAQ,QAAQ,MAAM,KAAK,KAAK;EAC9C,MAAM,KAAK,QAAQ,QAAQ,OAAO,MAAM,IAAI;EAC5C,MAAM,OAAO,iBAAiB,aAAa,IAAc,GAAG,OAAO;EACnE,IAAI,QAAQ,UAAU,OAAO,OAAO,SAAS,MAAM,EAAE;EACrD,MAAM,IAAI,sBAAsB,GAAG,IAAI,CAAC,CAAC,UAAU,IAAI;EACvD,OAAO,0BAA0B,GAAG,GAAG,GAAG,GAAG,CAAC;CAChD;CACA,MAAM,2BAAW,IAAI,IAA2B;CAChD,MAAM,aAAa,UAAiC;EAClD,IAAI,SAAS,IAAI,KAAK,GAAG,OAAO,SAAS,IAAI,KAAK;EAElD,IAAI,OAAsB;EAC1B,IAAI,YAAY;EAChB,MAAM,QAAQ,UAAyB,UAAwB;GAC7D,KAAK,MAAM,MAAM,aAAa,OAAO,MAAM,QAAQ,MAAM,WAAW,QAAQ,GAAG;IAC7E,IAAI,SAAS,IAAI,EAAE,GAAG;IACtB,MAAM,OAAO,MAAM,IAAI,EAAE;IACzB,IAAI,CAAC,MAAM;IACX,IAAI,KAAK,UAAU,SAAS,SAAS,aAChC,UAAU,UAAU,EAAY,MAAM,QAAQ,MAAM,IAAI,IAAI,KAAK,QAAQ,OAAO,EAAE,GAAG;KACxF,OAAO;KACP,YAAY;IACd;IACA,KAAK,IAAI,QAAQ,CAAC;GACpB;EACF;EACA,KAAK,MAAM,CAAC;EACZ,SAAS,IAAI,OAAO,IAAI;EACxB,OAAO;CACT;CACA,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,OAAO,MAAM,IAAI,EAAE;EACzB,MAAM,SAAS,MAAM,UAAU;EAC/B,IAAI,CAAC,QAAQ,WAAW,MAAM;EAC9B,MAAM,SAAS,UAAU,UAAU,MAAgB;EACnD,IAAI,CAAC,UAAU,MAAM,QAAQ,MAAM,GAAG;EACtC,IAAI,IAAI,IAAI,UAAU,KAAK,KAAK,CAAC;CACnC;CACA,OAAO;AACT"}
@@ -1,6 +1,6 @@
1
1
  // src/interactions/actions/transform-again/memory.ts
2
2
  var LAST = /* @__PURE__ */ new WeakMap();
3
- function sameIds(a, b) {
3
+ function sameMembers(a, b) {
4
4
  if (a.length !== b.length) return false;
5
5
  const set = new Set(a);
6
6
  return b.every((id) => set.has(id));
@@ -20,7 +20,7 @@ function lastTransform(scene) {
20
20
  */
21
21
  function recordTransform(scene, transform, subject) {
22
22
  const prev = LAST.get(scene);
23
- if (transform.kind === "translate" && prev?.transform.kind === "duplicate" && sameIds(prev.subject, subject)) {
23
+ if (transform.kind === "translate" && prev?.transform.kind === "duplicate" && sameMembers(prev.subject, subject)) {
24
24
  LAST.set(scene, {
25
25
  transform: {
26
26
  kind: "duplicate",
@@ -1 +1 @@
1
- {"version":3,"file":"memory.js","names":[],"sources":["../../../../src/interactions/actions/transform-again/memory.ts"],"sourcesContent":["import type { ResizeAnchor } from '@weasel-js/routing';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\n\n/**\n * A transform the selection underwent, kept relative so it can be applied to\n * whatever is selected next.\n *\n * - `translate`: a world offset.\n * - `rotate`: radians about the selection's center, or about each node's own\n * center (`pivot: 'each'`).\n * - `scale`: the box the selection was resized to, in units of the box it\n * started as (`{ x: 0, y: 0, width: 1, height: 1 }` is no change), and the\n * anchor the resize held fixed.\n * - `duplicate`: copy the selection, offset by the world offset `(dx, dy)`, and\n * select the copies.\n *\n * Offsets are world, so a repeat inside a turned group steps the way the drag\n * or nudge it repeats did on screen.\n */\nexport type RepeatableTransform =\n | { readonly kind: 'translate'; readonly dx: number; readonly dy: number }\n | { readonly kind: 'rotate'; readonly angle: number; readonly pivot: 'union' | 'each' }\n | { readonly kind: 'scale'; readonly to: Bounds; readonly anchor: ResizeAnchor }\n | { readonly kind: 'duplicate'; readonly dx: number; readonly dy: number };\n\ninterface Entry {\n readonly transform: RepeatableTransform;\n /** The ids selected once it landed. */\n readonly subject: readonly string[];\n}\n\nconst LAST = new WeakMap<object, Entry>();\n\nfunction sameIds(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n const set = new Set(a);\n return b.every((id) => set.has(id));\n}\n\n/**\n * The last transform recorded against `scene`, which `transformAgain` repeats.\n */\nexport function lastTransform(scene: object): RepeatableTransform | undefined {\n return LAST.get(scene)?.transform;\n}\n\n/**\n * Remember `transform` as the last one `scene` underwent, `subject` being the\n * ids selected once it landed. A translate of exactly the copies a\n * `duplicate` just made folds into it, so the copies' offset from their\n * sources is what repeats: duplicate, drag the copy, and each repeat steps\n * one more copy along.\n */\nexport function recordTransform(\n scene: object,\n transform: RepeatableTransform,\n subject: readonly string[],\n): void {\n const prev = LAST.get(scene);\n if (\n transform.kind === 'translate'\n && prev?.transform.kind === 'duplicate'\n && sameIds(prev.subject, subject)\n ) {\n LAST.set(scene, {\n transform: { kind: 'duplicate', dx: prev.transform.dx + transform.dx, dy: prev.transform.dy + transform.dy },\n subject: [...subject],\n });\n return;\n }\n LAST.set(scene, { transform, subject: [...subject] });\n}\n\n/** Drop what `scene` remembers, so `transformAgain` has nothing to repeat. */\nexport function forgetTransform(scene: object): void {\n LAST.delete(scene);\n}\n"],"mappings":";AA+BA,IAAM,uBAAO,IAAI,QAAuB;AAExC,SAAS,QAAQ,GAAsB,GAA+B;CACpE,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,MAAM,MAAM,IAAI,IAAI,CAAC;CACrB,OAAO,EAAE,OAAO,OAAO,IAAI,IAAI,EAAE,CAAC;AACpC;;;;AAKA,SAAgB,cAAc,OAAgD;CAC5E,OAAO,KAAK,IAAI,KAAK,CAAC,EAAE;AAC1B;;;;;;;;AASA,SAAgB,gBACd,OACA,WACA,SACM;CACN,MAAM,OAAO,KAAK,IAAI,KAAK;CAC3B,IACE,UAAU,SAAS,eAChB,MAAM,UAAU,SAAS,eACzB,QAAQ,KAAK,SAAS,OAAO,GAChC;EACA,KAAK,IAAI,OAAO;GACd,WAAW;IAAE,MAAM;IAAa,IAAI,KAAK,UAAU,KAAK,UAAU;IAAI,IAAI,KAAK,UAAU,KAAK,UAAU;GAAG;GAC3G,SAAS,CAAC,GAAG,OAAO;EACtB,CAAC;EACD;CACF;CACA,KAAK,IAAI,OAAO;EAAE;EAAW,SAAS,CAAC,GAAG,OAAO;CAAE,CAAC;AACtD;;AAGA,SAAgB,gBAAgB,OAAqB;CACnD,KAAK,OAAO,KAAK;AACnB"}
1
+ {"version":3,"file":"memory.js","names":[],"sources":["../../../../src/interactions/actions/transform-again/memory.ts"],"sourcesContent":["import type { ResizeAnchor } from '@weasel-js/routing';\nimport type { Bounds } from 'core/viewport/fitViewToBounds';\n\n/**\n * A transform the selection underwent, kept relative so it can be applied to\n * whatever is selected next.\n *\n * - `translate`: a world offset.\n * - `rotate`: radians about the selection's center, or about each node's own\n * center (`pivot: 'each'`).\n * - `scale`: the box the selection was resized to, in units of the box it\n * started as (`{ x: 0, y: 0, width: 1, height: 1 }` is no change), and the\n * anchor the resize held fixed.\n * - `duplicate`: copy the selection, offset by the world offset `(dx, dy)`, and\n * select the copies.\n *\n * Offsets are world, so a repeat inside a turned group steps the way the drag\n * or nudge it repeats did on screen.\n */\nexport type RepeatableTransform =\n | { readonly kind: 'translate'; readonly dx: number; readonly dy: number }\n | { readonly kind: 'rotate'; readonly angle: number; readonly pivot: 'union' | 'each' }\n | { readonly kind: 'scale'; readonly to: Bounds; readonly anchor: ResizeAnchor }\n | { readonly kind: 'duplicate'; readonly dx: number; readonly dy: number };\n\ninterface Entry {\n readonly transform: RepeatableTransform;\n /** The ids selected once it landed. */\n readonly subject: readonly string[];\n}\n\nconst LAST = new WeakMap<object, Entry>();\n\nfunction sameMembers(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n const set = new Set(a);\n return b.every((id) => set.has(id));\n}\n\n/**\n * The last transform recorded against `scene`, which `transformAgain` repeats.\n */\nexport function lastTransform(scene: object): RepeatableTransform | undefined {\n return LAST.get(scene)?.transform;\n}\n\n/**\n * Remember `transform` as the last one `scene` underwent, `subject` being the\n * ids selected once it landed. A translate of exactly the copies a\n * `duplicate` just made folds into it, so the copies' offset from their\n * sources is what repeats: duplicate, drag the copy, and each repeat steps\n * one more copy along.\n */\nexport function recordTransform(\n scene: object,\n transform: RepeatableTransform,\n subject: readonly string[],\n): void {\n const prev = LAST.get(scene);\n if (\n transform.kind === 'translate'\n && prev?.transform.kind === 'duplicate'\n && sameMembers(prev.subject, subject)\n ) {\n LAST.set(scene, {\n transform: { kind: 'duplicate', dx: prev.transform.dx + transform.dx, dy: prev.transform.dy + transform.dy },\n subject: [...subject],\n });\n return;\n }\n LAST.set(scene, { transform, subject: [...subject] });\n}\n\n/** Drop what `scene` remembers, so `transformAgain` has nothing to repeat. */\nexport function forgetTransform(scene: object): void {\n LAST.delete(scene);\n}\n"],"mappings":";AA+BA,IAAM,uBAAO,IAAI,QAAuB;AAExC,SAAS,YAAY,GAAsB,GAA+B;CACxE,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,MAAM,MAAM,IAAI,IAAI,CAAC;CACrB,OAAO,EAAE,OAAO,OAAO,IAAI,IAAI,EAAE,CAAC;AACpC;;;;AAKA,SAAgB,cAAc,OAAgD;CAC5E,OAAO,KAAK,IAAI,KAAK,CAAC,EAAE;AAC1B;;;;;;;;AASA,SAAgB,gBACd,OACA,WACA,SACM;CACN,MAAM,OAAO,KAAK,IAAI,KAAK;CAC3B,IACE,UAAU,SAAS,eAChB,MAAM,UAAU,SAAS,eACzB,YAAY,KAAK,SAAS,OAAO,GACpC;EACA,KAAK,IAAI,OAAO;GACd,WAAW;IAAE,MAAM;IAAa,IAAI,KAAK,UAAU,KAAK,UAAU;IAAI,IAAI,KAAK,UAAU,KAAK,UAAU;GAAG;GAC3G,SAAS,CAAC,GAAG,OAAO;EACtB,CAAC;EACD;CACF;CACA,KAAK,IAAI,OAAO;EAAE;EAAW,SAAS,CAAC,GAAG,OAAO;CAAE,CAAC;AACtD;;AAGA,SAAgB,gBAAgB,OAAqB;CACnD,KAAK,OAAO,KAAK;AACnB"}
package/dist/move.d.ts CHANGED
@@ -2,10 +2,10 @@ export { U as UseMoveOptions } from './options-DAjkhbcs.js';
2
2
  import { S as SnapTarget } from './types-VCtfchp-.js';
3
3
  import { M as MoveBehavior } from './clone-Bz4iqGfE.js';
4
4
  import { O as OriginProjection } from './originProjection-gY0jb4Wb.js';
5
+ import { FramedBox } from '@weasel-js/geom';
6
+ import { P as PoseDescriptor } from './poseDescriptor-CvSxVhCd.js';
5
7
  import './types-A_sNJYRd.js';
6
- import '@weasel-js/geom';
7
8
  import '@weasel-js/history';
8
- import './poseDescriptor-CvSxVhCd.js';
9
9
  import '@weasel-js/animator';
10
10
  import '@weasel-js/routing';
11
11
  import '@weasel-js/react';
@@ -51,4 +51,19 @@ declare function snapBackOrDelete<TPose>(args: {
51
51
  origin: OriginProjection<TPose>;
52
52
  }): MoveBehavior<TPose>;
53
53
 
54
- export { snapBackOrDelete, snapToContainer };
54
+ /** What {@link clampMove} takes. */
55
+ interface ClampMoveOptions<TPose> {
56
+ /** The world rect the dragged nodes stay inside, or a function read each frame for one that changes. */
57
+ bounds: FramedBox | (() => FramedBox);
58
+ /** Reads each pose's box. Default `AUTO_POSE_DESCRIPTOR`. */
59
+ poseDescriptor?: PoseDescriptor<TPose>;
60
+ }
61
+ /**
62
+ * Move behavior that keeps the dragged nodes inside `bounds`: the drag's translation is
63
+ * clamped so the box around every dragged node's start pose, moved by it, stays in the rect.
64
+ * The clamp applies to the preview and the commit alike, so a node let go past an edge lands
65
+ * on it. Put it after behaviors that reshape the translation, so it has the last word.
66
+ */
67
+ declare function clampMove<TPose>(opts: ClampMoveOptions<TPose>): MoveBehavior<TPose>;
68
+
69
+ export { type ClampMoveOptions, clampMove, snapBackOrDelete, snapToContainer };
package/dist/move.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { snapToContainer } from "./interactions/actions/move/behaviors/snapToContainer.js";
2
2
  import { snapBackOrDelete } from "./interactions/actions/move/behaviors/snapBackOrDelete.js";
3
+ import { clampMove } from "./interactions/actions/move/behaviors/clampMove.js";
3
4
 
4
- export { snapBackOrDelete, snapToContainer };
5
+ export { clampMove, snapBackOrDelete, snapToContainer };
package/dist/version.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * throwing, since nothing about version reporting should be able to break a
8
8
  * consumer's build.
9
9
  */
10
- var VERSION = "1.15.1";
10
+ var VERSION = "1.16.0";
11
11
 
12
12
  export { VERSION };
13
13
  //# sourceMappingURL=version.js.map