@oh-just-another/state 0.57.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 (269) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +60 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/actions/actionClipboard.d.ts +6 -0
  6. package/dist/actions/actionClipboard.d.ts.map +1 -0
  7. package/dist/actions/actionClipboard.js +26 -0
  8. package/dist/actions/actionClipboard.js.map +1 -0
  9. package/dist/actions/actionGrouping.d.ts +5 -0
  10. package/dist/actions/actionGrouping.d.ts.map +1 -0
  11. package/dist/actions/actionGrouping.js +26 -0
  12. package/dist/actions/actionGrouping.js.map +1 -0
  13. package/dist/actions/actionHistory.d.ts +5 -0
  14. package/dist/actions/actionHistory.d.ts.map +1 -0
  15. package/dist/actions/actionHistory.js +25 -0
  16. package/dist/actions/actionHistory.js.map +1 -0
  17. package/dist/actions/actionKeyboard.d.ts +3 -0
  18. package/dist/actions/actionKeyboard.d.ts.map +1 -0
  19. package/dist/actions/actionKeyboard.js +130 -0
  20. package/dist/actions/actionKeyboard.js.map +1 -0
  21. package/dist/actions/actionLayout.d.ts +3 -0
  22. package/dist/actions/actionLayout.d.ts.map +1 -0
  23. package/dist/actions/actionLayout.js +65 -0
  24. package/dist/actions/actionLayout.js.map +1 -0
  25. package/dist/actions/actionMode.d.ts +13 -0
  26. package/dist/actions/actionMode.d.ts.map +1 -0
  27. package/dist/actions/actionMode.js +116 -0
  28. package/dist/actions/actionMode.js.map +1 -0
  29. package/dist/actions/actionSelection.d.ts +9 -0
  30. package/dist/actions/actionSelection.d.ts.map +1 -0
  31. package/dist/actions/actionSelection.js +57 -0
  32. package/dist/actions/actionSelection.js.map +1 -0
  33. package/dist/actions/actionView.d.ts +7 -0
  34. package/dist/actions/actionView.d.ts.map +1 -0
  35. package/dist/actions/actionView.js +14 -0
  36. package/dist/actions/actionView.js.map +1 -0
  37. package/dist/actions/actionZOrder.d.ts +7 -0
  38. package/dist/actions/actionZOrder.d.ts.map +1 -0
  39. package/dist/actions/actionZOrder.js +42 -0
  40. package/dist/actions/actionZOrder.js.map +1 -0
  41. package/dist/actions/actionZoom.d.ts +8 -0
  42. package/dist/actions/actionZoom.d.ts.map +1 -0
  43. package/dist/actions/actionZoom.js +61 -0
  44. package/dist/actions/actionZoom.js.map +1 -0
  45. package/dist/actions/index.d.ts +12 -0
  46. package/dist/actions/index.d.ts.map +1 -0
  47. package/dist/actions/index.js +11 -0
  48. package/dist/actions/index.js.map +1 -0
  49. package/dist/actions/registry.d.ts +68 -0
  50. package/dist/actions/registry.d.ts.map +1 -0
  51. package/dist/actions/registry.js +300 -0
  52. package/dist/actions/registry.js.map +1 -0
  53. package/dist/actions/types.d.ts +103 -0
  54. package/dist/actions/types.d.ts.map +1 -0
  55. package/dist/actions/types.js +10 -0
  56. package/dist/actions/types.js.map +1 -0
  57. package/dist/animation-tick.d.ts +28 -0
  58. package/dist/animation-tick.d.ts.map +1 -0
  59. package/dist/animation-tick.js +50 -0
  60. package/dist/animation-tick.js.map +1 -0
  61. package/dist/auto-compact.d.ts +27 -0
  62. package/dist/auto-compact.d.ts.map +1 -0
  63. package/dist/auto-compact.js +46 -0
  64. package/dist/auto-compact.js.map +1 -0
  65. package/dist/auto-layout-scheduler.d.ts +50 -0
  66. package/dist/auto-layout-scheduler.d.ts.map +1 -0
  67. package/dist/auto-layout-scheduler.js +94 -0
  68. package/dist/auto-layout-scheduler.js.map +1 -0
  69. package/dist/built-in-handlers.d.ts +24 -0
  70. package/dist/built-in-handlers.d.ts.map +1 -0
  71. package/dist/built-in-handlers.js +210 -0
  72. package/dist/built-in-handlers.js.map +1 -0
  73. package/dist/clipboard.d.ts +51 -0
  74. package/dist/clipboard.d.ts.map +1 -0
  75. package/dist/clipboard.js +105 -0
  76. package/dist/clipboard.js.map +1 -0
  77. package/dist/constants.d.ts +544 -0
  78. package/dist/constants.d.ts.map +1 -0
  79. package/dist/constants.js +537 -0
  80. package/dist/constants.js.map +1 -0
  81. package/dist/dom-events.d.ts +13 -0
  82. package/dist/dom-events.d.ts.map +1 -0
  83. package/dist/dom-events.js +70 -0
  84. package/dist/dom-events.js.map +1 -0
  85. package/dist/editor/anchor-points.d.ts +22 -0
  86. package/dist/editor/anchor-points.d.ts.map +1 -0
  87. package/dist/editor/anchor-points.js +28 -0
  88. package/dist/editor/anchor-points.js.map +1 -0
  89. package/dist/editor/applies/create.d.ts +60 -0
  90. package/dist/editor/applies/create.d.ts.map +1 -0
  91. package/dist/editor/applies/create.js +107 -0
  92. package/dist/editor/applies/create.js.map +1 -0
  93. package/dist/editor/applies/edge.d.ts +32 -0
  94. package/dist/editor/applies/edge.d.ts.map +1 -0
  95. package/dist/editor/applies/edge.js +44 -0
  96. package/dist/editor/applies/edge.js.map +1 -0
  97. package/dist/editor/applies/link-move.d.ts +53 -0
  98. package/dist/editor/applies/link-move.d.ts.map +1 -0
  99. package/dist/editor/applies/link-move.js +160 -0
  100. package/dist/editor/applies/link-move.js.map +1 -0
  101. package/dist/editor/applies/move.d.ts +38 -0
  102. package/dist/editor/applies/move.d.ts.map +1 -0
  103. package/dist/editor/applies/move.js +83 -0
  104. package/dist/editor/applies/move.js.map +1 -0
  105. package/dist/editor/applies/resize.d.ts +68 -0
  106. package/dist/editor/applies/resize.d.ts.map +1 -0
  107. package/dist/editor/applies/resize.js +199 -0
  108. package/dist/editor/applies/resize.js.map +1 -0
  109. package/dist/editor/applies/selection.d.ts +34 -0
  110. package/dist/editor/applies/selection.d.ts.map +1 -0
  111. package/dist/editor/applies/selection.js +65 -0
  112. package/dist/editor/applies/selection.js.map +1 -0
  113. package/dist/editor/applies/snap-grid.d.ts +45 -0
  114. package/dist/editor/applies/snap-grid.d.ts.map +1 -0
  115. package/dist/editor/applies/snap-grid.js +90 -0
  116. package/dist/editor/applies/snap-grid.js.map +1 -0
  117. package/dist/editor/container-ops.d.ts +74 -0
  118. package/dist/editor/container-ops.d.ts.map +1 -0
  119. package/dist/editor/container-ops.js +262 -0
  120. package/dist/editor/container-ops.js.map +1 -0
  121. package/dist/editor/event-fanout.d.ts +48 -0
  122. package/dist/editor/event-fanout.d.ts.map +1 -0
  123. package/dist/editor/event-fanout.js +53 -0
  124. package/dist/editor/event-fanout.js.map +1 -0
  125. package/dist/editor/gesture-tx.d.ts +77 -0
  126. package/dist/editor/gesture-tx.d.ts.map +1 -0
  127. package/dist/editor/gesture-tx.js +93 -0
  128. package/dist/editor/gesture-tx.js.map +1 -0
  129. package/dist/editor/hit-test.d.ts +178 -0
  130. package/dist/editor/hit-test.d.ts.map +1 -0
  131. package/dist/editor/hit-test.js +460 -0
  132. package/dist/editor/hit-test.js.map +1 -0
  133. package/dist/editor/long-press.d.ts +34 -0
  134. package/dist/editor/long-press.d.ts.map +1 -0
  135. package/dist/editor/long-press.js +58 -0
  136. package/dist/editor/long-press.js.map +1 -0
  137. package/dist/editor/pinch.d.ts +37 -0
  138. package/dist/editor/pinch.d.ts.map +1 -0
  139. package/dist/editor/pinch.js +90 -0
  140. package/dist/editor/pinch.js.map +1 -0
  141. package/dist/editor/pointer-binding.d.ts +18 -0
  142. package/dist/editor/pointer-binding.d.ts.map +1 -0
  143. package/dist/editor/pointer-binding.js +1087 -0
  144. package/dist/editor/pointer-binding.js.map +1 -0
  145. package/dist/editor/public/annotations.d.ts +54 -0
  146. package/dist/editor/public/annotations.d.ts.map +1 -0
  147. package/dist/editor/public/annotations.js +106 -0
  148. package/dist/editor/public/annotations.js.map +1 -0
  149. package/dist/editor/public/arrange-group.d.ts +81 -0
  150. package/dist/editor/public/arrange-group.d.ts.map +1 -0
  151. package/dist/editor/public/arrange-group.js +193 -0
  152. package/dist/editor/public/arrange-group.js.map +1 -0
  153. package/dist/editor/public/brush.d.ts +33 -0
  154. package/dist/editor/public/brush.d.ts.map +1 -0
  155. package/dist/editor/public/brush.js +54 -0
  156. package/dist/editor/public/brush.js.map +1 -0
  157. package/dist/editor/public/clipboard.d.ts +31 -0
  158. package/dist/editor/public/clipboard.d.ts.map +1 -0
  159. package/dist/editor/public/clipboard.js +35 -0
  160. package/dist/editor/public/clipboard.js.map +1 -0
  161. package/dist/editor/public/image-insert.d.ts +30 -0
  162. package/dist/editor/public/image-insert.d.ts.map +1 -0
  163. package/dist/editor/public/image-insert.js +57 -0
  164. package/dist/editor/public/image-insert.js.map +1 -0
  165. package/dist/editor/public/layers.d.ts +56 -0
  166. package/dist/editor/public/layers.d.ts.map +1 -0
  167. package/dist/editor/public/layers.js +104 -0
  168. package/dist/editor/public/layers.js.map +1 -0
  169. package/dist/editor/public/link.d.ts +25 -0
  170. package/dist/editor/public/link.d.ts.map +1 -0
  171. package/dist/editor/public/link.js +74 -0
  172. package/dist/editor/public/link.js.map +1 -0
  173. package/dist/editor/public/placement.d.ts +60 -0
  174. package/dist/editor/public/placement.d.ts.map +1 -0
  175. package/dist/editor/public/placement.js +111 -0
  176. package/dist/editor/public/placement.js.map +1 -0
  177. package/dist/editor/public/selection-ops.d.ts +78 -0
  178. package/dist/editor/public/selection-ops.d.ts.map +1 -0
  179. package/dist/editor/public/selection-ops.js +226 -0
  180. package/dist/editor/public/selection-ops.js.map +1 -0
  181. package/dist/editor/public/text-edit.d.ts +17 -0
  182. package/dist/editor/public/text-edit.d.ts.map +1 -0
  183. package/dist/editor/public/text-edit.js +28 -0
  184. package/dist/editor/public/text-edit.js.map +1 -0
  185. package/dist/editor/public/z-order.d.ts +47 -0
  186. package/dist/editor/public/z-order.d.ts.map +1 -0
  187. package/dist/editor/public/z-order.js +151 -0
  188. package/dist/editor/public/z-order.js.map +1 -0
  189. package/dist/editor/public/zoom-pan.d.ts +44 -0
  190. package/dist/editor/public/zoom-pan.d.ts.map +1 -0
  191. package/dist/editor/public/zoom-pan.js +116 -0
  192. package/dist/editor/public/zoom-pan.js.map +1 -0
  193. package/dist/editor/render-orchestrator.d.ts +11 -0
  194. package/dist/editor/render-orchestrator.d.ts.map +1 -0
  195. package/dist/editor/render-orchestrator.js +473 -0
  196. package/dist/editor/render-orchestrator.js.map +1 -0
  197. package/dist/editor/resize-helpers.d.ts +30 -0
  198. package/dist/editor/resize-helpers.d.ts.map +1 -0
  199. package/dist/editor/resize-helpers.js +121 -0
  200. package/dist/editor/resize-helpers.js.map +1 -0
  201. package/dist/editor/shape-filters.d.ts +11 -0
  202. package/dist/editor/shape-filters.d.ts.map +1 -0
  203. package/dist/editor/shape-filters.js +19 -0
  204. package/dist/editor/shape-filters.js.map +1 -0
  205. package/dist/editor/shape-traits.d.ts +16 -0
  206. package/dist/editor/shape-traits.d.ts.map +1 -0
  207. package/dist/editor/shape-traits.js +29 -0
  208. package/dist/editor/shape-traits.js.map +1 -0
  209. package/dist/editor/viewport-helpers.d.ts +24 -0
  210. package/dist/editor/viewport-helpers.d.ts.map +1 -0
  211. package/dist/editor/viewport-helpers.js +63 -0
  212. package/dist/editor/viewport-helpers.js.map +1 -0
  213. package/dist/editor-events.d.ts +33 -0
  214. package/dist/editor-events.d.ts.map +1 -0
  215. package/dist/editor-events.js +2 -0
  216. package/dist/editor-events.js.map +1 -0
  217. package/dist/editor.d.ts +1841 -0
  218. package/dist/editor.d.ts.map +1 -0
  219. package/dist/editor.js +4803 -0
  220. package/dist/editor.js.map +1 -0
  221. package/dist/file-drop.d.ts +99 -0
  222. package/dist/file-drop.d.ts.map +1 -0
  223. package/dist/file-drop.js +198 -0
  224. package/dist/file-drop.js.map +1 -0
  225. package/dist/frame-helpers.d.ts +30 -0
  226. package/dist/frame-helpers.d.ts.map +1 -0
  227. package/dist/frame-helpers.js +105 -0
  228. package/dist/frame-helpers.js.map +1 -0
  229. package/dist/group-helpers.d.ts +39 -0
  230. package/dist/group-helpers.d.ts.map +1 -0
  231. package/dist/group-helpers.js +116 -0
  232. package/dist/group-helpers.js.map +1 -0
  233. package/dist/handle.d.ts +73 -0
  234. package/dist/handle.d.ts.map +1 -0
  235. package/dist/handle.js +184 -0
  236. package/dist/handle.js.map +1 -0
  237. package/dist/index.d.ts +26 -0
  238. package/dist/index.d.ts.map +1 -0
  239. package/dist/index.js +16 -0
  240. package/dist/index.js.map +1 -0
  241. package/dist/interactive.d.ts +21 -0
  242. package/dist/interactive.d.ts.map +1 -0
  243. package/dist/interactive.js +11 -0
  244. package/dist/interactive.js.map +1 -0
  245. package/dist/link-selection.d.ts +21 -0
  246. package/dist/link-selection.d.ts.map +1 -0
  247. package/dist/link-selection.js +37 -0
  248. package/dist/link-selection.js.map +1 -0
  249. package/dist/machine.d.ts +322 -0
  250. package/dist/machine.d.ts.map +1 -0
  251. package/dist/machine.js +568 -0
  252. package/dist/machine.js.map +1 -0
  253. package/dist/modes.d.ts +24 -0
  254. package/dist/modes.d.ts.map +1 -0
  255. package/dist/modes.js +2 -0
  256. package/dist/modes.js.map +1 -0
  257. package/dist/overlay.d.ts +262 -0
  258. package/dist/overlay.d.ts.map +1 -0
  259. package/dist/overlay.js +673 -0
  260. package/dist/overlay.js.map +1 -0
  261. package/dist/platform.d.ts +70 -0
  262. package/dist/platform.d.ts.map +1 -0
  263. package/dist/platform.js +115 -0
  264. package/dist/platform.js.map +1 -0
  265. package/dist/selection.d.ts +18 -0
  266. package/dist/selection.d.ts.map +1 -0
  267. package/dist/selection.js +31 -0
  268. package/dist/selection.js.map +1 -0
  269. package/package.json +59 -0
@@ -0,0 +1,178 @@
1
+ import { type Scene, type Element } from "@oh-just-another/scene";
2
+ import type { AnnotationId, Bounds, LayerId, LinkId, ElementId, Transform, Vec2 } from "@oh-just-another/types";
3
+ import type { RenderTarget } from "@oh-just-another/renderer-core";
4
+ import type { PressTarget } from "../machine.js";
5
+ import type * as Selection from "../selection.js";
6
+ /**
7
+ * Bundle of everything `pickPressTarget` needs from the host
8
+ * editor. Kept narrow so this module doesn't import `Editor` and
9
+ * stays acycle-free. Callbacks are passed in instead of methods on
10
+ * a ref class because there are only a handful of them and each is
11
+ * a small closure over editor state at call time.
12
+ */
13
+ export interface HitTestContext {
14
+ readonly scene: Scene;
15
+ readonly selection: Selection.Selection;
16
+ readonly selectedLink: LinkId | null;
17
+ /** Count of selected links — group handles trigger on total objects > 1. */
18
+ readonly selectedLinkCount: number;
19
+ readonly enteredGroup: ElementId | null;
20
+ readonly handleHitSlop: number;
21
+ readonly edgeHandleHitSlop: number;
22
+ readonly edgeHitThreshold: number;
23
+ readonly hitAnnotation: (worldPoint: Vec2) => AnnotationId | null;
24
+ readonly selectionIsAspectLocked: () => boolean;
25
+ readonly combinedSelectionBounds: () => Bounds | null;
26
+ readonly acceleratedElementAt: (worldPoint: Vec2) => Element | undefined;
27
+ readonly isElementInteractable: (shape: Element) => boolean;
28
+ readonly isLayerLocked: (layerId: LayerId) => boolean;
29
+ readonly promoteToGroupRoot: (shape: Element) => Element;
30
+ }
31
+ /**
32
+ * Resolve the press target under the cursor — annotation pin →
33
+ * group resize handles → single-shape resize handles → edge
34
+ * endpoint handles → topmost shape → edge body → empty.
35
+ *
36
+ * Order matters: annotation pins sit visually above everything, so
37
+ * a click on one always wins. Resize handles win over the body of
38
+ * the shape they belong to so a click on the corner is treated as
39
+ * resize, not move. Link body comes after shapes because shapes
40
+ * have richer hit targets.
41
+ *
42
+ * Pure — every dependency on Editor goes through `ctx`. Editor's
43
+ * wrapper just forwards `worldPoint`.
44
+ */
45
+ export declare const pickPressTarget: (worldPoint: Vec2, ctx: HitTestContext) => PressTarget;
46
+ /**
47
+ * Minimal slice of the selected link's handle geometry the debug viz
48
+ * needs — endpoints plus the bend / segment handle world positions the
49
+ * orchestrator already computed for the real overlay. Mirrors
50
+ * `LinkSelection` in `overlay.ts` without importing it (would close the
51
+ * overlay ↔ hit-test runtime cycle).
52
+ */
53
+ export interface HitZoneEdge {
54
+ readonly from: Vec2;
55
+ readonly to: Vec2;
56
+ /** Existing bend points (world) — grabbable at `LINK_ENDPOINT_HANDLE_RADIUS`. */
57
+ readonly waypoints?: readonly Vec2[];
58
+ /** Segment / "add waypoint" midpoints (world) — same grab radius. */
59
+ readonly midpoints?: readonly Vec2[];
60
+ }
61
+ /**
62
+ * Link-attach drop-zones to paint while a link endpoint is being placed
63
+ * (drag from a start dot / draw-edge / endpoint rebind). These are the
64
+ * snap-engine catchments `snapLinkEndpoint` resolves against, so the user
65
+ * can see where a drop lands ON a point vs ON an edge:
66
+ * - `anchors` — world landing points of each element's connection anchors
67
+ * (drop within `thresholdWorld` → fixed anchor);
68
+ * - `outlineLoops` — world outline loops of each element (drop within
69
+ * `thresholdWorld` of the perimeter → fixed outline point).
70
+ * `thresholdWorld` is the snap threshold in WORLD units (constant on screen
71
+ * only after × zoom), matching the engine which compares world distances.
72
+ */
73
+ export interface HitZoneAttach {
74
+ readonly anchors: readonly Vec2[];
75
+ readonly outlineLoops: readonly (readonly Vec2[])[];
76
+ readonly thresholdWorld: number;
77
+ }
78
+ /**
79
+ * Element drop-zones to paint while an ELEMENT is being dragged — the two
80
+ * (separate) membership systems an element can be dropped into:
81
+ * - `frames` — full world bounds of every frame; an element joins the
82
+ * top-most frame whose bounds contain the dragged element's CENTROID
83
+ * (`reconcileFrameMembership`).
84
+ * - `containers` — resolved drop-zone world bounds of every template /
85
+ * auto-layout / static container; an element reparents to the container
86
+ * whose drop-zone contains the cursor (`findContainerAt`).
87
+ * All bounds are WORLD-space; the draw projects them to screen.
88
+ */
89
+ export interface HitZoneContainers {
90
+ readonly frames: readonly Bounds[];
91
+ readonly containers: readonly Bounds[];
92
+ }
93
+ /**
94
+ * Which hit-zone categories the debug overlay should paint. Each flag maps to
95
+ * one block in `drawHitZones`.
96
+ */
97
+ export interface HitZoneVisibility {
98
+ /** Resize-handle slop squares on resizable shapes. */
99
+ readonly resizeHandles: boolean;
100
+ /** Body bands for every link (the `findLinkAt` catchment). */
101
+ readonly linkBodies: boolean;
102
+ /** The selected link's endpoint / waypoint / segment handle circles. */
103
+ readonly selectedEdgeHandles: boolean;
104
+ /** The single selected element's link-start anchor dots (grab + click). */
105
+ readonly anchorDots: boolean;
106
+ /** Link-attach drop-zones (anchor catchments + edge bands + floating body). */
107
+ readonly attachDropZones: boolean;
108
+ /** Element drop-zones (frames + containers) while dragging an element. */
109
+ readonly containerDropZones: boolean;
110
+ /** Group-bounds resize-handle slop (multi-select / aspect-locked group). */
111
+ readonly groupHandles: boolean;
112
+ /** Annotation-pin grab radius. */
113
+ readonly annotationPins: boolean;
114
+ }
115
+ /**
116
+ * SINGLE source of truth for which hit-zone categories are actionable in the
117
+ * current interaction state — so the debug overlay highlights ONLY what the
118
+ * user can actually do right now (and the real `pickPressTarget` /
119
+ * pointer-binding would accept).
120
+ *
121
+ * Three mutually-exclusive regimes:
122
+ * - placing a link endpoint (drag from a start dot / draw-edge / endpoint
123
+ * rebind) → only the link-attach drop-zones are live;
124
+ * - dragging an element → only the element drop-zones (frames + containers)
125
+ * are live;
126
+ * - at rest → resize / link-body / selected-edge / start-dot zones.
127
+ * Every press in a drag regime is consumed by that gesture, so the other
128
+ * categories are inert and hidden.
129
+ */
130
+ export declare const hitZoneVisibility: (input: {
131
+ readonly linkDragActive: boolean;
132
+ readonly elementDragActive: boolean;
133
+ }) => HitZoneVisibility;
134
+ /**
135
+ * Inputs for `drawHitZones`. `visibility` (from `hitZoneVisibility`) gates
136
+ * which blocks paint; the geometry fields are consumed only by their
137
+ * corresponding block.
138
+ */
139
+ export interface DrawHitZonesOptions {
140
+ readonly scene: Scene;
141
+ readonly w2s: Transform;
142
+ readonly zoom: number;
143
+ readonly selection: Selection.Selection;
144
+ readonly visibility: HitZoneVisibility;
145
+ readonly edgeSelection?: HitZoneEdge;
146
+ readonly attach?: HitZoneAttach;
147
+ readonly containers?: HitZoneContainers;
148
+ /** Combined multi-selection / group bounds (world) — for group-handle slop. */
149
+ readonly groupBounds?: Bounds;
150
+ /** Restrict group-handle slop to the four corners (aspect-locked). */
151
+ readonly groupAspectLocked?: boolean;
152
+ }
153
+ /**
154
+ * Paint the mouse hit-zones gated by `visibility` so the tuned slop /
155
+ * threshold values can be eyeballed in the browser AND only the currently
156
+ * actionable targets show. Covers, in screen space (the hit-test works in
157
+ * screen px):
158
+ * - resize-handle slop squares for resizable shapes (matches the handle
159
+ * hit-test in `pickPressTarget`);
160
+ * - a body band for every link (the `LINK_HIT_THRESHOLD` band `findLinkAt`
161
+ * uses);
162
+ * - the SELECTED link's endpoint + waypoint + segment handle circles
163
+ * (`LINK_ENDPOINT_HANDLE_RADIUS` — these handles are only hit-testable on
164
+ * the selected link, so they're drawn only for it);
165
+ * - the SINGLE selected element's link-start anchor dots, each with TWO
166
+ * nested zones: the wider filled grab halo
167
+ * (`ANCHOR_DOT_ACTIVE_RADIUS + ANCHOR_START_HIT_SLOP`, begins a link
168
+ * drag) and the narrow dashed click radius (`ANCHOR_DOT_CLICK_RADIUS`,
169
+ * click-to-create-element).
170
+ *
171
+ * `edgeSelection` carries the sole selected link's handle positions the
172
+ * orchestrator already computed for the real overlay, so the drawn handle
173
+ * zones land exactly where the chrome does. Isolated in its own
174
+ * save/restore so the translucent paint state never leaks into the real
175
+ * selection chrome.
176
+ */
177
+ export declare const drawHitZones: (target: RenderTarget, opts: DrawHitZonesOptions) => void;
178
+ //# sourceMappingURL=hit-test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hit-test.d.ts","sourceRoot":"","sources":["../../src/editor/hit-test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,460 @@
1
+ import { findLinkAt, getAnnotationWorldPosition, getLink, getLinkPath, getElementWorldBounds, } from "@oh-just-another/scene";
2
+ import { matrix } from "@oh-just-another/math";
3
+ import { ALL_HANDLES, CORNER_HANDLES, HANDLE_HIT_SLOP, handlePosition, hitHandle, } from "../handle.js";
4
+ import { isResizable, resizeHandlesFor } from "./shape-traits.js";
5
+ import { anchorOverlayPoints } from "./anchor-points.js";
6
+ import { ANCHOR_DOT_ACTIVE_RADIUS, ANCHOR_DOT_CLICK_RADIUS, ANCHOR_START_HIT_SLOP, ANNOTATION_PIN_HIT_SLOP, DEBUG_HIT_ZONE_FILL_OPACITY, DEBUG_HIT_ZONE_STROKE_OPACITY, DEBUG_ZONE_ANCHOR_START, DEBUG_ZONE_ANNOTATION, DEBUG_ZONE_ATTACH_BODY, DEBUG_ZONE_ATTACH_EDGE, DEBUG_ZONE_ATTACH_POINT, DEBUG_ZONE_CONTAINER, DEBUG_ZONE_FRAME, DEBUG_ZONE_LINK_BODY, DEBUG_ZONE_LINK_HANDLE, DEBUG_ZONE_RESIZE, LINK_ENDPOINT_HANDLE_RADIUS, LINK_HIT_THRESHOLD, LINK_START_ANCHOR_OUTSET, } from "../constants.js";
7
+ import { getElement } from "@oh-just-another/scene";
8
+ /** Local hypot helper for the hit-test hot path (cheaper than a matrix op). */
9
+ const distanceTo = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
10
+ /** Index-access helper: throws on out-of-range instead of returning `undefined`. */
11
+ const req = (v) => {
12
+ if (v === undefined)
13
+ throw new Error("packages/state: index out of range");
14
+ return v;
15
+ };
16
+ /**
17
+ * Resolve the press target under the cursor — annotation pin →
18
+ * group resize handles → single-shape resize handles → edge
19
+ * endpoint handles → topmost shape → edge body → empty.
20
+ *
21
+ * Order matters: annotation pins sit visually above everything, so
22
+ * a click on one always wins. Resize handles win over the body of
23
+ * the shape they belong to so a click on the corner is treated as
24
+ * resize, not move. Link body comes after shapes because shapes
25
+ * have richer hit targets.
26
+ *
27
+ * Pure — every dependency on Editor goes through `ctx`. Editor's
28
+ * wrapper just forwards `worldPoint`.
29
+ */
30
+ export const pickPressTarget = (worldPoint, ctx) => {
31
+ const zoom = ctx.scene.viewport.zoom;
32
+ // 0. Annotation pin first — pins sit visually above everything,
33
+ // so a pointer-down that lands on a pin drives the pin drag
34
+ // gesture regardless of what's underneath.
35
+ const annId = ctx.hitAnnotation(worldPoint);
36
+ if (annId !== null) {
37
+ const ann = ctx.scene.annotations.get(annId);
38
+ if (ann) {
39
+ return {
40
+ kind: "annotation",
41
+ id: annId,
42
+ origin: getAnnotationWorldPosition(ctx.scene, ann),
43
+ };
44
+ }
45
+ }
46
+ // 1a. Group resize handles win when several shapes are selected,
47
+ // OR when a single group-typed shape is selected (which has
48
+ // no intrinsic bounds — children's union AABB serves as the
49
+ // resize frame). Aspect-locked groups restrict the hit set to
50
+ // the four corner handles.
51
+ const useGroupHandles = ctx.selection.size + ctx.selectedLinkCount > 1 || ctx.selectionIsAspectLocked();
52
+ if (useGroupHandles) {
53
+ const combined = ctx.combinedSelectionBounds();
54
+ if (combined) {
55
+ const aspectLocked = ctx.selectionIsAspectLocked();
56
+ const handleSet = aspectLocked ? CORNER_HANDLES : ALL_HANDLES;
57
+ const handle = hitHandle(worldPoint, combined, zoom, ctx.handleHitSlop, handleSet);
58
+ if (handle) {
59
+ return { kind: "group-handle", handle, bounds: combined };
60
+ }
61
+ }
62
+ }
63
+ // 1b. Resize handles on a single selected shape — only when exactly
64
+ // one shape is selected. Multi-selection drops per-shape handles
65
+ // in favour of the group bbox handles above; otherwise users
66
+ // could resize one child outside the combined frame, which is
67
+ // inconsistent with the group outline.
68
+ if (ctx.selection.size === 1) {
69
+ for (const id of ctx.selection) {
70
+ const shape = getElement(ctx.scene, id);
71
+ if (!shape || !isResizable(shape))
72
+ continue;
73
+ if (shape.locked === true)
74
+ continue; // locked: no resize handles
75
+ const bounds = getElementWorldBounds(shape);
76
+ const handle = hitHandle(worldPoint, bounds, zoom, ctx.handleHitSlop, resizeHandlesFor(shape));
77
+ if (handle) {
78
+ return { kind: "handle", elementId: id, handle, bounds };
79
+ }
80
+ }
81
+ }
82
+ // 2. Endpoint handles on a selected edge — only when an edge is
83
+ // selected. Threshold in screen pixels, converted to world.
84
+ if (ctx.selectedLink) {
85
+ const edge = getLink(ctx.scene, ctx.selectedLink);
86
+ if (edge) {
87
+ const path = getLinkPath(ctx.scene, edge);
88
+ if (path && path.length >= 2) {
89
+ const handleR = ctx.edgeHandleHitSlop / zoom;
90
+ const fromPoint = req(path[0]);
91
+ const toPoint = req(path[path.length - 1]);
92
+ if (distanceTo(worldPoint, fromPoint) <= handleR) {
93
+ return { kind: "edge-endpoint", linkId: edge.id, side: "from" };
94
+ }
95
+ if (distanceTo(worldPoint, toPoint) <= handleR) {
96
+ return { kind: "edge-endpoint", linkId: edge.id, side: "to" };
97
+ }
98
+ }
99
+ }
100
+ }
101
+ // 3. Topmost shape under cursor. Skip shapes whose layer is locked
102
+ // OR whose own / ancestor `locked` flag is set (group lock
103
+ // propagation). When the hit shape is a child of a group,
104
+ // promote to the group root unless the user has "entered" that
105
+ // group via double-click.
106
+ const shape = ctx.acceleratedElementAt(worldPoint);
107
+ if (shape && ctx.isElementInteractable(shape)) {
108
+ const target = ctx.promoteToGroupRoot(shape);
109
+ return { kind: "element", id: target.id, bounds: getElementWorldBounds(target) };
110
+ }
111
+ // 4. Link body under cursor.
112
+ const edge = findLinkAt(ctx.scene, worldPoint, ctx.edgeHitThreshold / zoom);
113
+ if (edge && !ctx.isLayerLocked(edge.layerId)) {
114
+ return { kind: "link", id: edge.id };
115
+ }
116
+ return { kind: "empty" };
117
+ };
118
+ // ---------------------------------------------------------------------------
119
+ // Debug hit-zone visualisation
120
+ // ---------------------------------------------------------------------------
121
+ // Lives next to `pickPressTarget` (the hit-test it visualises) so the drawn
122
+ // zones and the real hit-targets share the same geometry — handle slop,
123
+ // link threshold, endpoint radius — and can't drift. Painted by the
124
+ // selection overlay (host debug panel → Display → "Show hit-zones"); all
125
+ // sizes are screen-pixel because the hit-test works in screen space.
126
+ /** Translucent debug rect in `color`: filled zone + faint outline. */
127
+ const fillZoneRect = (target, x, y, w, h, color) => {
128
+ target.setStroke(null);
129
+ target.setFill(color);
130
+ target.setOpacity(DEBUG_HIT_ZONE_FILL_OPACITY);
131
+ target.beginPath();
132
+ target.rect(x, y, w, h);
133
+ target.fill();
134
+ target.setFill(null);
135
+ target.setStroke(color);
136
+ target.setStrokeWidth(1);
137
+ target.setOpacity(DEBUG_HIT_ZONE_STROKE_OPACITY);
138
+ target.beginPath();
139
+ target.rect(x, y, w, h);
140
+ target.stroke();
141
+ };
142
+ /** Translucent debug circle in `color`: filled zone + faint outline. */
143
+ const fillZoneCircle = (target, cx, cy, r, color) => {
144
+ target.setStroke(null);
145
+ target.setFill(color);
146
+ target.setOpacity(DEBUG_HIT_ZONE_FILL_OPACITY);
147
+ target.beginPath();
148
+ target.ellipse(cx, cy, r, r);
149
+ target.fill();
150
+ target.setFill(null);
151
+ target.setStroke(color);
152
+ target.setStrokeWidth(1);
153
+ target.setOpacity(DEBUG_HIT_ZONE_STROKE_OPACITY);
154
+ target.beginPath();
155
+ target.ellipse(cx, cy, r, r);
156
+ target.stroke();
157
+ };
158
+ /**
159
+ * Outline-only debug circle in `color` — no fill. Used to draw a SECONDARY
160
+ * zone nested inside a filled one (e.g. the narrow click radius inside the
161
+ * wider grab halo of an anchor dot) so the two read as distinct without the
162
+ * inner fill muddying the outer one. Dashed so it can't be mistaken for the
163
+ * solid grab-halo outline.
164
+ */
165
+ const strokeZoneCircle = (target, cx, cy, r, color) => {
166
+ target.setFill(null);
167
+ target.setStroke(color);
168
+ target.setStrokeWidth(1);
169
+ target.setDashArray([2, 2]);
170
+ target.setOpacity(DEBUG_HIT_ZONE_STROKE_OPACITY);
171
+ target.beginPath();
172
+ target.ellipse(cx, cy, r, r);
173
+ target.stroke();
174
+ target.setDashArray(null);
175
+ };
176
+ /**
177
+ * Translucent debug polygon (filled closed loop) in `color` — used for area
178
+ * zones like a floating-attach body or a container drop-zone. `pts` are
179
+ * SCREEN-space points.
180
+ */
181
+ const fillZoneLoop = (target, pts, color) => {
182
+ if (pts.length < 3)
183
+ return;
184
+ target.setStroke(null);
185
+ target.setFill(color);
186
+ target.setOpacity(DEBUG_HIT_ZONE_FILL_OPACITY);
187
+ target.beginPath();
188
+ const first = req(pts[0]);
189
+ target.moveTo(first.x, first.y);
190
+ for (let i = 1; i < pts.length; i++) {
191
+ const p = req(pts[i]);
192
+ target.lineTo(p.x, p.y);
193
+ }
194
+ target.closePath();
195
+ target.fill();
196
+ target.setFill(null);
197
+ target.setStroke(color);
198
+ target.setStrokeWidth(1);
199
+ target.setOpacity(DEBUG_HIT_ZONE_STROKE_OPACITY);
200
+ target.beginPath();
201
+ target.moveTo(first.x, first.y);
202
+ for (let i = 1; i < pts.length; i++) {
203
+ const p = req(pts[i]);
204
+ target.lineTo(p.x, p.y);
205
+ }
206
+ target.closePath();
207
+ target.stroke();
208
+ };
209
+ /**
210
+ * Resize hit-zones for a world AABB, matching `hitHandle` exactly: CORNER
211
+ * handles are square grabs at the outset dot positions; EDGE handles
212
+ * (`n/s/e/w`) are slop BANDS along the whole side (edge resize = drag the box
213
+ * side, not a midpoint square). Drawn in screen space.
214
+ */
215
+ const drawResizeZones = (target, w2s, wb, zoom, handleSet, color) => {
216
+ const tl = matrix.applyToPoint(w2s, { x: wb.x, y: wb.y });
217
+ const br = matrix.applyToPoint(w2s, { x: wb.x + wb.width, y: wb.y + wb.height });
218
+ const x = Math.min(tl.x, br.x);
219
+ const y = Math.min(tl.y, br.y);
220
+ const w = Math.abs(br.x - tl.x);
221
+ const h = Math.abs(br.y - tl.y);
222
+ const s = HANDLE_HIT_SLOP;
223
+ for (const id of handleSet) {
224
+ switch (id) {
225
+ case "nw":
226
+ case "ne":
227
+ case "se":
228
+ case "sw": {
229
+ const c = matrix.applyToPoint(w2s, handlePosition(id, wb, zoom));
230
+ fillZoneRect(target, c.x - s, c.y - s, s * 2, s * 2, color);
231
+ break;
232
+ }
233
+ case "n":
234
+ fillZoneRect(target, x - s, y - s, w + s * 2, s * 2, color);
235
+ break;
236
+ case "s":
237
+ fillZoneRect(target, x - s, y + h - s, w + s * 2, s * 2, color);
238
+ break;
239
+ case "w":
240
+ fillZoneRect(target, x - s, y - s, s * 2, h + s * 2, color);
241
+ break;
242
+ case "e":
243
+ fillZoneRect(target, x + w - s, y - s, s * 2, h + s * 2, color);
244
+ break;
245
+ }
246
+ }
247
+ };
248
+ /**
249
+ * SINGLE source of truth for which hit-zone categories are actionable in the
250
+ * current interaction state — so the debug overlay highlights ONLY what the
251
+ * user can actually do right now (and the real `pickPressTarget` /
252
+ * pointer-binding would accept).
253
+ *
254
+ * Three mutually-exclusive regimes:
255
+ * - placing a link endpoint (drag from a start dot / draw-edge / endpoint
256
+ * rebind) → only the link-attach drop-zones are live;
257
+ * - dragging an element → only the element drop-zones (frames + containers)
258
+ * are live;
259
+ * - at rest → resize / link-body / selected-edge / start-dot zones.
260
+ * Every press in a drag regime is consumed by that gesture, so the other
261
+ * categories are inert and hidden.
262
+ */
263
+ export const hitZoneVisibility = (input) => {
264
+ const none = {
265
+ resizeHandles: false,
266
+ linkBodies: false,
267
+ selectedEdgeHandles: false,
268
+ anchorDots: false,
269
+ attachDropZones: false,
270
+ containerDropZones: false,
271
+ groupHandles: false,
272
+ annotationPins: false,
273
+ };
274
+ // Link placement wins if somehow both are set (it can't normally).
275
+ if (input.linkDragActive)
276
+ return { ...none, attachDropZones: true };
277
+ if (input.elementDragActive)
278
+ return { ...none, containerDropZones: true };
279
+ return {
280
+ ...none,
281
+ resizeHandles: true,
282
+ linkBodies: true,
283
+ selectedEdgeHandles: true,
284
+ anchorDots: true,
285
+ groupHandles: true,
286
+ annotationPins: true,
287
+ };
288
+ };
289
+ /**
290
+ * Paint the mouse hit-zones gated by `visibility` so the tuned slop /
291
+ * threshold values can be eyeballed in the browser AND only the currently
292
+ * actionable targets show. Covers, in screen space (the hit-test works in
293
+ * screen px):
294
+ * - resize-handle slop squares for resizable shapes (matches the handle
295
+ * hit-test in `pickPressTarget`);
296
+ * - a body band for every link (the `LINK_HIT_THRESHOLD` band `findLinkAt`
297
+ * uses);
298
+ * - the SELECTED link's endpoint + waypoint + segment handle circles
299
+ * (`LINK_ENDPOINT_HANDLE_RADIUS` — these handles are only hit-testable on
300
+ * the selected link, so they're drawn only for it);
301
+ * - the SINGLE selected element's link-start anchor dots, each with TWO
302
+ * nested zones: the wider filled grab halo
303
+ * (`ANCHOR_DOT_ACTIVE_RADIUS + ANCHOR_START_HIT_SLOP`, begins a link
304
+ * drag) and the narrow dashed click radius (`ANCHOR_DOT_CLICK_RADIUS`,
305
+ * click-to-create-element).
306
+ *
307
+ * `edgeSelection` carries the sole selected link's handle positions the
308
+ * orchestrator already computed for the real overlay, so the drawn handle
309
+ * zones land exactly where the chrome does. Isolated in its own
310
+ * save/restore so the translucent paint state never leaks into the real
311
+ * selection chrome.
312
+ */
313
+ export const drawHitZones = (target, opts) => {
314
+ const { scene, w2s, zoom, selection, visibility, edgeSelection, attach, containers } = opts;
315
+ const { groupBounds, groupAspectLocked } = opts;
316
+ target.save();
317
+ // Resize-handle zones — ONLY for the single selected resizable, unlocked
318
+ // shape. Mirrors `pickPressTarget` step 1b, which offers per-shape handles
319
+ // only when exactly one shape is selected (multi-select / groups use the
320
+ // group-bounds handles below). Nothing is resizable with no selection.
321
+ if (visibility.resizeHandles && selection.size === 1) {
322
+ const id = req([...selection][0]);
323
+ const shape = getElement(scene, id);
324
+ if (shape && isResizable(shape) && shape.locked !== true) {
325
+ drawResizeZones(target, w2s, getElementWorldBounds(shape), zoom, resizeHandlesFor(shape), DEBUG_ZONE_RESIZE);
326
+ }
327
+ }
328
+ // Link body bands (polyline stroked at 2× the hit threshold) — the
329
+ // `findLinkAt` target, hit-testable for EVERY link.
330
+ if (visibility.linkBodies) {
331
+ for (const edge of scene.links.values()) {
332
+ const path = getLinkPath(scene, edge);
333
+ if (!path || path.length < 2)
334
+ continue;
335
+ target.setFill(null);
336
+ target.setStroke(DEBUG_ZONE_LINK_BODY);
337
+ target.setStrokeWidth(LINK_HIT_THRESHOLD * 2);
338
+ target.setOpacity(DEBUG_HIT_ZONE_FILL_OPACITY);
339
+ target.setLineCap("round");
340
+ target.setLineJoin("round");
341
+ target.beginPath();
342
+ const start = matrix.applyToPoint(w2s, req(path[0]));
343
+ target.moveTo(start.x, start.y);
344
+ for (let i = 1; i < path.length; i++) {
345
+ const p = matrix.applyToPoint(w2s, req(path[i]));
346
+ target.lineTo(p.x, p.y);
347
+ }
348
+ target.stroke();
349
+ }
350
+ }
351
+ // Selected link's handle zones — endpoint, waypoint and segment-midpoint
352
+ // grab circles. These handles only exist on the selected link (see
353
+ // `pickPressTarget` step 2 + the waypoint / segment drag in
354
+ // `pointer-binding`), so they're drawn only for it, at the same
355
+ // `LINK_ENDPOINT_HANDLE_RADIUS` the press uses.
356
+ if (visibility.selectedEdgeHandles && edgeSelection) {
357
+ const from = matrix.applyToPoint(w2s, edgeSelection.from);
358
+ const to = matrix.applyToPoint(w2s, edgeSelection.to);
359
+ fillZoneCircle(target, from.x, from.y, LINK_ENDPOINT_HANDLE_RADIUS, DEBUG_ZONE_LINK_HANDLE);
360
+ fillZoneCircle(target, to.x, to.y, LINK_ENDPOINT_HANDLE_RADIUS, DEBUG_ZONE_LINK_HANDLE);
361
+ for (const w of edgeSelection.waypoints ?? []) {
362
+ const p = matrix.applyToPoint(w2s, w);
363
+ fillZoneCircle(target, p.x, p.y, LINK_ENDPOINT_HANDLE_RADIUS, DEBUG_ZONE_LINK_HANDLE);
364
+ }
365
+ for (const m of edgeSelection.midpoints ?? []) {
366
+ const p = matrix.applyToPoint(w2s, m);
367
+ fillZoneCircle(target, p.x, p.y, LINK_ENDPOINT_HANDLE_RADIUS, DEBUG_ZONE_LINK_HANDLE);
368
+ }
369
+ }
370
+ // Single selected element's link-start anchor dots — wider filled grab
371
+ // halo (begins a link drag) + narrow dashed click radius (click-to-create).
372
+ // Only on a sole-element selection, matching the anchor-drag hit-test.
373
+ if (visibility.anchorDots && selection.size === 1) {
374
+ const id = req([...selection][0]);
375
+ const shape = getElement(scene, id);
376
+ if (shape) {
377
+ const grab = ANCHOR_DOT_ACTIVE_RADIUS + ANCHOR_START_HIT_SLOP;
378
+ const { worldPoints } = anchorOverlayPoints(shape, LINK_START_ANCHOR_OUTSET / zoom);
379
+ for (const p of worldPoints) {
380
+ const c = matrix.applyToPoint(w2s, p);
381
+ fillZoneCircle(target, c.x, c.y, grab, DEBUG_ZONE_ANCHOR_START);
382
+ strokeZoneCircle(target, c.x, c.y, ANCHOR_DOT_CLICK_RADIUS, DEBUG_ZONE_ANCHOR_START);
383
+ }
384
+ }
385
+ }
386
+ // Link-attach drop-zones — only while a link endpoint is being placed.
387
+ // Snap catchment is in WORLD units, so screen radius / band width = world
388
+ // threshold × zoom. Order (under → over): floating body fill (L4), edge band
389
+ // (L3), anchor catchment circles (L1/L2) so points sit on top.
390
+ if (visibility.attachDropZones && attach) {
391
+ const bandWorld = attach.thresholdWorld * zoom;
392
+ // L4: drop on the body interior → floating attach. Fill each candidate's
393
+ // outline loop so the user sees "drop anywhere here floats to this shape".
394
+ for (const loop of attach.outlineLoops) {
395
+ fillZoneLoop(target, loop.map((p) => matrix.applyToPoint(w2s, p)), DEBUG_ZONE_ATTACH_BODY);
396
+ }
397
+ // L3: outline band (fixed outline point along the perimeter).
398
+ target.setFill(null);
399
+ target.setStroke(DEBUG_ZONE_ATTACH_EDGE);
400
+ target.setStrokeWidth(bandWorld * 2);
401
+ target.setOpacity(DEBUG_HIT_ZONE_FILL_OPACITY);
402
+ target.setLineCap("round");
403
+ target.setLineJoin("round");
404
+ for (const loop of attach.outlineLoops) {
405
+ if (loop.length < 2)
406
+ continue;
407
+ target.beginPath();
408
+ const first = matrix.applyToPoint(w2s, req(loop[0]));
409
+ target.moveTo(first.x, first.y);
410
+ for (let i = 1; i < loop.length; i++) {
411
+ const p = matrix.applyToPoint(w2s, req(loop[i]));
412
+ target.lineTo(p.x, p.y);
413
+ }
414
+ target.closePath();
415
+ target.stroke();
416
+ }
417
+ // L1/L2: named / edge anchor catchment circles.
418
+ for (const a of attach.anchors) {
419
+ const c = matrix.applyToPoint(w2s, a);
420
+ fillZoneCircle(target, c.x, c.y, bandWorld, DEBUG_ZONE_ATTACH_POINT);
421
+ }
422
+ }
423
+ // Element drop-zones — only while an element is being dragged. Frames
424
+ // (centroid-in-bounds) and containers (cursor-in-drop-zone). Bounds
425
+ // are world-space; project the four corners to a screen loop.
426
+ if (visibility.containerDropZones && containers) {
427
+ const drawBounds = (b, color) => {
428
+ const corners = [
429
+ { x: b.x, y: b.y },
430
+ { x: b.x + b.width, y: b.y },
431
+ { x: b.x + b.width, y: b.y + b.height },
432
+ { x: b.x, y: b.y + b.height },
433
+ ].map((p) => matrix.applyToPoint(w2s, p));
434
+ fillZoneLoop(target, corners, color);
435
+ };
436
+ for (const b of containers.frames)
437
+ drawBounds(b, DEBUG_ZONE_FRAME);
438
+ for (const b of containers.containers)
439
+ drawBounds(b, DEBUG_ZONE_CONTAINER);
440
+ }
441
+ // Group-bounds resize-handle slop — multi-selection / aspect-locked group.
442
+ // Handle set matches the hit-test: corners only when aspect-locked, else all
443
+ // 8. Same `HANDLE_HIT_SLOP` squares as single-shape handles, resize colour.
444
+ if (visibility.groupHandles && groupBounds) {
445
+ const handleSet = groupAspectLocked ? CORNER_HANDLES : ALL_HANDLES;
446
+ drawResizeZones(target, w2s, groupBounds, zoom, handleSet, DEBUG_ZONE_RESIZE);
447
+ }
448
+ // Annotation-pin grab circles — radius `ANNOTATION_PIN_HIT_SLOP` (screen px),
449
+ // matching `hitAnnotation`. Pins sit visually above everything, so their hit
450
+ // wins first in `pickPressTarget`.
451
+ if (visibility.annotationPins) {
452
+ for (const ann of scene.annotations.values()) {
453
+ const c = matrix.applyToPoint(w2s, getAnnotationWorldPosition(scene, ann));
454
+ fillZoneCircle(target, c.x, c.y, ANNOTATION_PIN_HIT_SLOP, DEBUG_ZONE_ANNOTATION);
455
+ }
456
+ }
457
+ target.setOpacity(1);
458
+ target.restore();
459
+ };
460
+ //# sourceMappingURL=hit-test.js.map