@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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/actions/actionClipboard.d.ts +6 -0
- package/dist/actions/actionClipboard.d.ts.map +1 -0
- package/dist/actions/actionClipboard.js +26 -0
- package/dist/actions/actionClipboard.js.map +1 -0
- package/dist/actions/actionGrouping.d.ts +5 -0
- package/dist/actions/actionGrouping.d.ts.map +1 -0
- package/dist/actions/actionGrouping.js +26 -0
- package/dist/actions/actionGrouping.js.map +1 -0
- package/dist/actions/actionHistory.d.ts +5 -0
- package/dist/actions/actionHistory.d.ts.map +1 -0
- package/dist/actions/actionHistory.js +25 -0
- package/dist/actions/actionHistory.js.map +1 -0
- package/dist/actions/actionKeyboard.d.ts +3 -0
- package/dist/actions/actionKeyboard.d.ts.map +1 -0
- package/dist/actions/actionKeyboard.js +130 -0
- package/dist/actions/actionKeyboard.js.map +1 -0
- package/dist/actions/actionLayout.d.ts +3 -0
- package/dist/actions/actionLayout.d.ts.map +1 -0
- package/dist/actions/actionLayout.js +65 -0
- package/dist/actions/actionLayout.js.map +1 -0
- package/dist/actions/actionMode.d.ts +13 -0
- package/dist/actions/actionMode.d.ts.map +1 -0
- package/dist/actions/actionMode.js +116 -0
- package/dist/actions/actionMode.js.map +1 -0
- package/dist/actions/actionSelection.d.ts +9 -0
- package/dist/actions/actionSelection.d.ts.map +1 -0
- package/dist/actions/actionSelection.js +57 -0
- package/dist/actions/actionSelection.js.map +1 -0
- package/dist/actions/actionView.d.ts +7 -0
- package/dist/actions/actionView.d.ts.map +1 -0
- package/dist/actions/actionView.js +14 -0
- package/dist/actions/actionView.js.map +1 -0
- package/dist/actions/actionZOrder.d.ts +7 -0
- package/dist/actions/actionZOrder.d.ts.map +1 -0
- package/dist/actions/actionZOrder.js +42 -0
- package/dist/actions/actionZOrder.js.map +1 -0
- package/dist/actions/actionZoom.d.ts +8 -0
- package/dist/actions/actionZoom.d.ts.map +1 -0
- package/dist/actions/actionZoom.js +61 -0
- package/dist/actions/actionZoom.js.map +1 -0
- package/dist/actions/index.d.ts +12 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/registry.d.ts +68 -0
- package/dist/actions/registry.d.ts.map +1 -0
- package/dist/actions/registry.js +300 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/actions/types.d.ts +103 -0
- package/dist/actions/types.d.ts.map +1 -0
- package/dist/actions/types.js +10 -0
- package/dist/actions/types.js.map +1 -0
- package/dist/animation-tick.d.ts +28 -0
- package/dist/animation-tick.d.ts.map +1 -0
- package/dist/animation-tick.js +50 -0
- package/dist/animation-tick.js.map +1 -0
- package/dist/auto-compact.d.ts +27 -0
- package/dist/auto-compact.d.ts.map +1 -0
- package/dist/auto-compact.js +46 -0
- package/dist/auto-compact.js.map +1 -0
- package/dist/auto-layout-scheduler.d.ts +50 -0
- package/dist/auto-layout-scheduler.d.ts.map +1 -0
- package/dist/auto-layout-scheduler.js +94 -0
- package/dist/auto-layout-scheduler.js.map +1 -0
- package/dist/built-in-handlers.d.ts +24 -0
- package/dist/built-in-handlers.d.ts.map +1 -0
- package/dist/built-in-handlers.js +210 -0
- package/dist/built-in-handlers.js.map +1 -0
- package/dist/clipboard.d.ts +51 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +105 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/constants.d.ts +544 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +537 -0
- package/dist/constants.js.map +1 -0
- package/dist/dom-events.d.ts +13 -0
- package/dist/dom-events.d.ts.map +1 -0
- package/dist/dom-events.js +70 -0
- package/dist/dom-events.js.map +1 -0
- package/dist/editor/anchor-points.d.ts +22 -0
- package/dist/editor/anchor-points.d.ts.map +1 -0
- package/dist/editor/anchor-points.js +28 -0
- package/dist/editor/anchor-points.js.map +1 -0
- package/dist/editor/applies/create.d.ts +60 -0
- package/dist/editor/applies/create.d.ts.map +1 -0
- package/dist/editor/applies/create.js +107 -0
- package/dist/editor/applies/create.js.map +1 -0
- package/dist/editor/applies/edge.d.ts +32 -0
- package/dist/editor/applies/edge.d.ts.map +1 -0
- package/dist/editor/applies/edge.js +44 -0
- package/dist/editor/applies/edge.js.map +1 -0
- package/dist/editor/applies/link-move.d.ts +53 -0
- package/dist/editor/applies/link-move.d.ts.map +1 -0
- package/dist/editor/applies/link-move.js +160 -0
- package/dist/editor/applies/link-move.js.map +1 -0
- package/dist/editor/applies/move.d.ts +38 -0
- package/dist/editor/applies/move.d.ts.map +1 -0
- package/dist/editor/applies/move.js +83 -0
- package/dist/editor/applies/move.js.map +1 -0
- package/dist/editor/applies/resize.d.ts +68 -0
- package/dist/editor/applies/resize.d.ts.map +1 -0
- package/dist/editor/applies/resize.js +199 -0
- package/dist/editor/applies/resize.js.map +1 -0
- package/dist/editor/applies/selection.d.ts +34 -0
- package/dist/editor/applies/selection.d.ts.map +1 -0
- package/dist/editor/applies/selection.js +65 -0
- package/dist/editor/applies/selection.js.map +1 -0
- package/dist/editor/applies/snap-grid.d.ts +45 -0
- package/dist/editor/applies/snap-grid.d.ts.map +1 -0
- package/dist/editor/applies/snap-grid.js +90 -0
- package/dist/editor/applies/snap-grid.js.map +1 -0
- package/dist/editor/container-ops.d.ts +74 -0
- package/dist/editor/container-ops.d.ts.map +1 -0
- package/dist/editor/container-ops.js +262 -0
- package/dist/editor/container-ops.js.map +1 -0
- package/dist/editor/event-fanout.d.ts +48 -0
- package/dist/editor/event-fanout.d.ts.map +1 -0
- package/dist/editor/event-fanout.js +53 -0
- package/dist/editor/event-fanout.js.map +1 -0
- package/dist/editor/gesture-tx.d.ts +77 -0
- package/dist/editor/gesture-tx.d.ts.map +1 -0
- package/dist/editor/gesture-tx.js +93 -0
- package/dist/editor/gesture-tx.js.map +1 -0
- package/dist/editor/hit-test.d.ts +178 -0
- package/dist/editor/hit-test.d.ts.map +1 -0
- package/dist/editor/hit-test.js +460 -0
- package/dist/editor/hit-test.js.map +1 -0
- package/dist/editor/long-press.d.ts +34 -0
- package/dist/editor/long-press.d.ts.map +1 -0
- package/dist/editor/long-press.js +58 -0
- package/dist/editor/long-press.js.map +1 -0
- package/dist/editor/pinch.d.ts +37 -0
- package/dist/editor/pinch.d.ts.map +1 -0
- package/dist/editor/pinch.js +90 -0
- package/dist/editor/pinch.js.map +1 -0
- package/dist/editor/pointer-binding.d.ts +18 -0
- package/dist/editor/pointer-binding.d.ts.map +1 -0
- package/dist/editor/pointer-binding.js +1087 -0
- package/dist/editor/pointer-binding.js.map +1 -0
- package/dist/editor/public/annotations.d.ts +54 -0
- package/dist/editor/public/annotations.d.ts.map +1 -0
- package/dist/editor/public/annotations.js +106 -0
- package/dist/editor/public/annotations.js.map +1 -0
- package/dist/editor/public/arrange-group.d.ts +81 -0
- package/dist/editor/public/arrange-group.d.ts.map +1 -0
- package/dist/editor/public/arrange-group.js +193 -0
- package/dist/editor/public/arrange-group.js.map +1 -0
- package/dist/editor/public/brush.d.ts +33 -0
- package/dist/editor/public/brush.d.ts.map +1 -0
- package/dist/editor/public/brush.js +54 -0
- package/dist/editor/public/brush.js.map +1 -0
- package/dist/editor/public/clipboard.d.ts +31 -0
- package/dist/editor/public/clipboard.d.ts.map +1 -0
- package/dist/editor/public/clipboard.js +35 -0
- package/dist/editor/public/clipboard.js.map +1 -0
- package/dist/editor/public/image-insert.d.ts +30 -0
- package/dist/editor/public/image-insert.d.ts.map +1 -0
- package/dist/editor/public/image-insert.js +57 -0
- package/dist/editor/public/image-insert.js.map +1 -0
- package/dist/editor/public/layers.d.ts +56 -0
- package/dist/editor/public/layers.d.ts.map +1 -0
- package/dist/editor/public/layers.js +104 -0
- package/dist/editor/public/layers.js.map +1 -0
- package/dist/editor/public/link.d.ts +25 -0
- package/dist/editor/public/link.d.ts.map +1 -0
- package/dist/editor/public/link.js +74 -0
- package/dist/editor/public/link.js.map +1 -0
- package/dist/editor/public/placement.d.ts +60 -0
- package/dist/editor/public/placement.d.ts.map +1 -0
- package/dist/editor/public/placement.js +111 -0
- package/dist/editor/public/placement.js.map +1 -0
- package/dist/editor/public/selection-ops.d.ts +78 -0
- package/dist/editor/public/selection-ops.d.ts.map +1 -0
- package/dist/editor/public/selection-ops.js +226 -0
- package/dist/editor/public/selection-ops.js.map +1 -0
- package/dist/editor/public/text-edit.d.ts +17 -0
- package/dist/editor/public/text-edit.d.ts.map +1 -0
- package/dist/editor/public/text-edit.js +28 -0
- package/dist/editor/public/text-edit.js.map +1 -0
- package/dist/editor/public/z-order.d.ts +47 -0
- package/dist/editor/public/z-order.d.ts.map +1 -0
- package/dist/editor/public/z-order.js +151 -0
- package/dist/editor/public/z-order.js.map +1 -0
- package/dist/editor/public/zoom-pan.d.ts +44 -0
- package/dist/editor/public/zoom-pan.d.ts.map +1 -0
- package/dist/editor/public/zoom-pan.js +116 -0
- package/dist/editor/public/zoom-pan.js.map +1 -0
- package/dist/editor/render-orchestrator.d.ts +11 -0
- package/dist/editor/render-orchestrator.d.ts.map +1 -0
- package/dist/editor/render-orchestrator.js +473 -0
- package/dist/editor/render-orchestrator.js.map +1 -0
- package/dist/editor/resize-helpers.d.ts +30 -0
- package/dist/editor/resize-helpers.d.ts.map +1 -0
- package/dist/editor/resize-helpers.js +121 -0
- package/dist/editor/resize-helpers.js.map +1 -0
- package/dist/editor/shape-filters.d.ts +11 -0
- package/dist/editor/shape-filters.d.ts.map +1 -0
- package/dist/editor/shape-filters.js +19 -0
- package/dist/editor/shape-filters.js.map +1 -0
- package/dist/editor/shape-traits.d.ts +16 -0
- package/dist/editor/shape-traits.d.ts.map +1 -0
- package/dist/editor/shape-traits.js +29 -0
- package/dist/editor/shape-traits.js.map +1 -0
- package/dist/editor/viewport-helpers.d.ts +24 -0
- package/dist/editor/viewport-helpers.d.ts.map +1 -0
- package/dist/editor/viewport-helpers.js +63 -0
- package/dist/editor/viewport-helpers.js.map +1 -0
- package/dist/editor-events.d.ts +33 -0
- package/dist/editor-events.d.ts.map +1 -0
- package/dist/editor-events.js +2 -0
- package/dist/editor-events.js.map +1 -0
- package/dist/editor.d.ts +1841 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +4803 -0
- package/dist/editor.js.map +1 -0
- package/dist/file-drop.d.ts +99 -0
- package/dist/file-drop.d.ts.map +1 -0
- package/dist/file-drop.js +198 -0
- package/dist/file-drop.js.map +1 -0
- package/dist/frame-helpers.d.ts +30 -0
- package/dist/frame-helpers.d.ts.map +1 -0
- package/dist/frame-helpers.js +105 -0
- package/dist/frame-helpers.js.map +1 -0
- package/dist/group-helpers.d.ts +39 -0
- package/dist/group-helpers.d.ts.map +1 -0
- package/dist/group-helpers.js +116 -0
- package/dist/group-helpers.js.map +1 -0
- package/dist/handle.d.ts +73 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +184 -0
- package/dist/handle.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive.d.ts +21 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +11 -0
- package/dist/interactive.js.map +1 -0
- package/dist/link-selection.d.ts +21 -0
- package/dist/link-selection.d.ts.map +1 -0
- package/dist/link-selection.js +37 -0
- package/dist/link-selection.js.map +1 -0
- package/dist/machine.d.ts +322 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +568 -0
- package/dist/machine.js.map +1 -0
- package/dist/modes.d.ts +24 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +2 -0
- package/dist/modes.js.map +1 -0
- package/dist/overlay.d.ts +262 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +673 -0
- package/dist/overlay.js.map +1 -0
- package/dist/platform.d.ts +70 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +115 -0
- package/dist/platform.js.map +1 -0
- package/dist/selection.d.ts +18 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +31 -0
- package/dist/selection.js.map +1 -0
- 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
|