@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,1087 @@
|
|
|
1
|
+
import { findContainerAt, getAnchorWorld, getDropZoneWorld, getElement, getElementWorldBounds, getLink, getLinkPath, getLinkWaypointMidpoints, isImage, updateAnnotation, } from "@oh-just-another/scene";
|
|
2
|
+
import { boundsFromPoints, interpretPressEnd, DRAG_THRESHOLD } from "../machine.js";
|
|
3
|
+
import { fromPointerEvent } from "../dom-events.js";
|
|
4
|
+
import * as Selection from "../selection.js";
|
|
5
|
+
import * as LinkSelection from "../link-selection.js";
|
|
6
|
+
import { getInteractiveHitTester } from "../interactive.js";
|
|
7
|
+
import { anchorOverlayPoints } from "./anchor-points.js";
|
|
8
|
+
import { snapshotMovingLinks } from "./applies/link-move.js";
|
|
9
|
+
import { ANCHOR_DOT_ACTIVE_RADIUS, LINK_ENDPOINT_HANDLE_RADIUS, LINK_START_ANCHOR_OUTSET, LONG_PRESS_MAX_MOVEMENT_PX, MAX_ZOOM, MIN_ZOOM, WHEEL_PAN_FACTOR, WHEEL_ZOOM_MAX_STEP, WHEEL_ZOOM_SPEED, } from "../constants.js";
|
|
10
|
+
/** Inclusive integer range `[a..b]`; empty when `b < a`. */
|
|
11
|
+
const range = (a, b) => {
|
|
12
|
+
const out = [];
|
|
13
|
+
for (let i = a; i <= b; i++)
|
|
14
|
+
out.push(i);
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
const distanceTo = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
18
|
+
const clampZoom = (z) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z));
|
|
19
|
+
/** Index-access helper: throws on out-of-range instead of returning `undefined`. */
|
|
20
|
+
const req = (v) => {
|
|
21
|
+
if (v === undefined)
|
|
22
|
+
throw new Error("packages/state: index out of range");
|
|
23
|
+
return v;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Pointer + wheel event binding. Owns the branchy dispatch — pan /
|
|
27
|
+
* pinch / brush / annotation / interactive-hit / machine flow —
|
|
28
|
+
* kept in one file so reading any single handler does not require
|
|
29
|
+
* jumping across module boundaries.
|
|
30
|
+
*
|
|
31
|
+
* Returns an unsubscribe function that removes every listener it
|
|
32
|
+
* installed.
|
|
33
|
+
*
|
|
34
|
+
* Typed against the full `Editor` class (type-only import — erased at
|
|
35
|
+
* runtime, so no import cycle and dependency-cruiser ignores it). This
|
|
36
|
+
* handler reaches deep into Editor's surface, so the members it touches
|
|
37
|
+
* are declared `public` (with the `_`-prefix convention marking them
|
|
38
|
+
* internal). editor.ts is the only call site.
|
|
39
|
+
*/
|
|
40
|
+
export const bindPointerEvents = (editor) => {
|
|
41
|
+
const onDown = (ev) => {
|
|
42
|
+
ev.preventDefault();
|
|
43
|
+
editor.host.setPointerCapture(ev.pointerId);
|
|
44
|
+
const data = fromPointerEvent(ev, editor.host);
|
|
45
|
+
// Fresh press — forget any additive promotion from the last gesture.
|
|
46
|
+
editor.additivePressAdded = null;
|
|
47
|
+
// Pan gesture detection — must come BEFORE the normal flow so a
|
|
48
|
+
// right-click or Space+left-click never starts a select/draw
|
|
49
|
+
// gesture. Two triggers:
|
|
50
|
+
// • Right mouse button (button === 2).
|
|
51
|
+
// • Left mouse button + Space currently held.
|
|
52
|
+
// Middle-click (button === 1) is covered under the same trigger.
|
|
53
|
+
const isRightClick = ev.button === 2 || ev.button === 1;
|
|
54
|
+
const isSpaceLeftDrag = ev.button === 0 && editor.spaceHeld;
|
|
55
|
+
const isHandModeLeftDrag = ev.button === 0 && editor.mode === "hand";
|
|
56
|
+
if (isRightClick || isSpaceLeftDrag || isHandModeLeftDrag) {
|
|
57
|
+
// Suppress the next native contextmenu — we'll either pan
|
|
58
|
+
// (if user drags) or manually fire the long-press callback
|
|
59
|
+
// at pointerup (if it was a click-style right-click).
|
|
60
|
+
if (isRightClick)
|
|
61
|
+
editor.suppressNextContextMenu = true;
|
|
62
|
+
editor.beginPanGesture(ev.pointerId, ev.button, data.point);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Track every active pointer so we can detect a 2-finger pinch.
|
|
66
|
+
// On the *second* concurrent pointer, cancel whatever single-pointer
|
|
67
|
+
// gesture the machine started (it'd otherwise interpret the second
|
|
68
|
+
// touch as a one-finger drag) and enter pinch mode.
|
|
69
|
+
editor.activePointers.set(ev.pointerId, data.point);
|
|
70
|
+
if (editor.activePointers.size === 2) {
|
|
71
|
+
// First touch already kicked off a POINTER_DOWN — undo it so the
|
|
72
|
+
// shape under finger #1 doesn't get dragged when finger #2 lands.
|
|
73
|
+
editor.actor.send({ type: "POINTER_CANCEL" });
|
|
74
|
+
editor.cancelGesture();
|
|
75
|
+
editor.cancelLongPress();
|
|
76
|
+
// A one-finger pan may already be in flight (first finger dragged on
|
|
77
|
+
// empty canvas) — drop it so it doesn't fight the pinch. No menu fires
|
|
78
|
+
// (touch pan is button 0), so clear directly rather than endPanGesture.
|
|
79
|
+
editor.panGesture = null;
|
|
80
|
+
editor.touchPanCandidate = null;
|
|
81
|
+
editor.beginPinch();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (editor.activePointers.size > 2) {
|
|
85
|
+
// 3-finger and more: stay in pinch mode but ignore additional
|
|
86
|
+
// contacts — the gesture math uses the first two pointers only.
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Schedule a long-press fire — cancelled by movement or release.
|
|
90
|
+
editor.startLongPress(data.point);
|
|
91
|
+
const worldPoint = editor.screenToWorld(data.point);
|
|
92
|
+
// Active in-canvas text edit owns the pointer. A press inside the
|
|
93
|
+
// edited shape repositions the caret (and starts a drag-select);
|
|
94
|
+
// a press outside commits the edit, then falls through so the same
|
|
95
|
+
// click does its normal thing (select / create elsewhere).
|
|
96
|
+
if (editor.editingTextElement !== null) {
|
|
97
|
+
if (editor.editedElementContainsPoint(worldPoint)) {
|
|
98
|
+
editor.cancelLongPress();
|
|
99
|
+
editor.setTextCaretFromPoint(worldPoint);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
editor.commitTextEdit();
|
|
103
|
+
// fall through to normal press handling for this click
|
|
104
|
+
}
|
|
105
|
+
// Brush mode owns the gesture end-to-end — no machine, no
|
|
106
|
+
// interactive testers, no auto-select. Start a stroke at the
|
|
107
|
+
// press point with the device's pressure; onMove extends; onUp
|
|
108
|
+
// commits as a single BrushElement patch.
|
|
109
|
+
if (editor.mode === "brush") {
|
|
110
|
+
editor.beginBrushStroke(worldPoint, ev.pressure);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Text tool — a click places a new empty text shape (or, when it
|
|
114
|
+
// lands on an existing text shape, edits that one) and opens the
|
|
115
|
+
// inline editor straight away. Click-based, no rubber-band; we
|
|
116
|
+
// intercept before the machine flow like brush. Cancel the
|
|
117
|
+
// long-press so its context menu can't pop over the editor.
|
|
118
|
+
if (editor.mode === "draw-text") {
|
|
119
|
+
editor.cancelLongPress();
|
|
120
|
+
const hit = editor.hitTest(worldPoint);
|
|
121
|
+
const existing = hit.kind === "element" ? getElement(editor._scene, hit.id) : null;
|
|
122
|
+
if (existing?.type === "text") {
|
|
123
|
+
editor._selection = Selection.single(existing.id);
|
|
124
|
+
editor.beginTextEdit(existing.id);
|
|
125
|
+
editor.notify();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
editor.createTextAt(worldPoint);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Annotation pin drag — when the press lands on a pin, take over
|
|
133
|
+
// the gesture entirely (skip machine, skip interactive testers).
|
|
134
|
+
// Pin position updates per pointermove; commits on pointerup.
|
|
135
|
+
const annHit = editor.hitAnnotation(worldPoint);
|
|
136
|
+
if (annHit) {
|
|
137
|
+
const ann = editor._scene.annotations.get(annHit);
|
|
138
|
+
if (ann) {
|
|
139
|
+
editor.annotationDrag = {
|
|
140
|
+
id: annHit,
|
|
141
|
+
originPosition: { ...ann.position },
|
|
142
|
+
originWorldPoint: worldPoint,
|
|
143
|
+
moved: false,
|
|
144
|
+
};
|
|
145
|
+
editor.setSelectedAnnotation(annHit);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Interactive sub-element check: when the press lands on a shape whose
|
|
150
|
+
// type has a registered hit-tester (rich templates, etc.) and the
|
|
151
|
+
// tester finds an interactive node, fire its emit and skip the normal
|
|
152
|
+
// press flow entirely. This is what makes a click on a template Button
|
|
153
|
+
// behave differently from a click on the template body.
|
|
154
|
+
const topElement = editor.acceleratedElementAt(worldPoint);
|
|
155
|
+
if (topElement) {
|
|
156
|
+
const tester = getInteractiveHitTester(topElement.type);
|
|
157
|
+
if (tester) {
|
|
158
|
+
const local = {
|
|
159
|
+
x: worldPoint.x - topElement.position.x,
|
|
160
|
+
y: worldPoint.y - topElement.position.y,
|
|
161
|
+
};
|
|
162
|
+
const emit = tester(topElement, local);
|
|
163
|
+
if (emit) {
|
|
164
|
+
editor.applyEmit(emit);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Elbow segment drag: a press on an interior segment's midpoint handle
|
|
170
|
+
// moves the whole segment perpendicular. Checked before the hit-test so
|
|
171
|
+
// it isn't read as deselect.
|
|
172
|
+
const segDragLink = editor.selectedLink;
|
|
173
|
+
if (editor.mode === "select" && segDragLink) {
|
|
174
|
+
const edge = getLink(editor._scene, segDragLink);
|
|
175
|
+
if (edge && (edge.routing ?? "straight") === "orthogonal") {
|
|
176
|
+
const path = getLinkPath(editor._scene, edge);
|
|
177
|
+
if (path && path.length >= 2) {
|
|
178
|
+
const zoom = editor._scene.viewport.zoom || 1;
|
|
179
|
+
const r = LINK_ENDPOINT_HANDLE_RADIUS / zoom;
|
|
180
|
+
const r2 = r * r;
|
|
181
|
+
// Draggable segments: the single segment of a straight elbow (grab
|
|
182
|
+
// to bend it → insert), or interior segments k in 1..len-3 of a
|
|
183
|
+
// routed elbow (terminal stubs touch from/to and aren't slid).
|
|
184
|
+
const segs = path.length === 2 ? [0] : range(1, path.length - 3);
|
|
185
|
+
for (const k of segs) {
|
|
186
|
+
const a = req(path[k]);
|
|
187
|
+
const b = req(path[k + 1]);
|
|
188
|
+
const mx = (a.x + b.x) / 2;
|
|
189
|
+
const my = (a.y + b.y) / 2;
|
|
190
|
+
const dx = mx - worldPoint.x;
|
|
191
|
+
const dy = my - worldPoint.y;
|
|
192
|
+
if (dx * dx + dy * dy <= r2) {
|
|
193
|
+
const axis = Math.abs(a.y - b.y) < 1e-6 ? "h" : "v";
|
|
194
|
+
const at = axis === "h" ? (a.x + b.x) / 2 : (a.y + b.y) / 2;
|
|
195
|
+
const pos = axis === "h" ? a.y : a.x; // pinned perpendicular coord
|
|
196
|
+
// Double-click a segment handle → drop its pin (back to auto route).
|
|
197
|
+
if (editor.isHandleDoubleClick(worldPoint)) {
|
|
198
|
+
editor.resetSegmentPin(segDragLink, axis, pos, at);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
editor.beginSegmentDrag(segDragLink, axis, at);
|
|
202
|
+
}
|
|
203
|
+
editor.cancelLongPress();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Bend-point (waypoint) drag on the selected link. A press on an
|
|
211
|
+
// existing waypoint handle moves it; a press on a segment-midpoint
|
|
212
|
+
// handle inserts a new waypoint there (on first move). Checked before
|
|
213
|
+
// the normal hit-test so it isn't read as deselect / new gesture.
|
|
214
|
+
const wpDragLink = editor.selectedLink;
|
|
215
|
+
if (editor.mode === "select" && wpDragLink) {
|
|
216
|
+
const edge = getLink(editor._scene, wpDragLink);
|
|
217
|
+
// Elbow links use segment-drag (separate mechanic), not free waypoints.
|
|
218
|
+
const path = edge && (edge.routing ?? "straight") !== "orthogonal"
|
|
219
|
+
? getLinkPath(editor._scene, edge)
|
|
220
|
+
: null;
|
|
221
|
+
if (edge && path && path.length >= 2) {
|
|
222
|
+
const zoom = editor._scene.viewport.zoom || 1;
|
|
223
|
+
const r = LINK_ENDPOINT_HANDLE_RADIUS / zoom;
|
|
224
|
+
const r2 = r * r;
|
|
225
|
+
const waypoints = [...(edge.waypoints ?? [])];
|
|
226
|
+
const within = (p) => {
|
|
227
|
+
const dx = p.x - worldPoint.x;
|
|
228
|
+
const dy = p.y - worldPoint.y;
|
|
229
|
+
return dx * dx + dy * dy <= r2;
|
|
230
|
+
};
|
|
231
|
+
// Existing waypoints take priority over the midpoint "add" handles.
|
|
232
|
+
let grabbed = false;
|
|
233
|
+
for (let i = 0; i < waypoints.length; i++) {
|
|
234
|
+
if (within(req(waypoints[i]))) {
|
|
235
|
+
// Double-click a waypoint handle → delete the bend point.
|
|
236
|
+
if (editor.isHandleDoubleClick(worldPoint)) {
|
|
237
|
+
editor.deleteWaypoint(wpDragLink, i);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
editor.beginWaypointDrag(wpDragLink, i, false);
|
|
241
|
+
}
|
|
242
|
+
grabbed = true;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!grabbed) {
|
|
247
|
+
// "Add waypoint" handles sit on the drawn arc (bezier) / chord
|
|
248
|
+
// (straight) — same geometry the overlay renders, so the click target
|
|
249
|
+
// matches the visible handle.
|
|
250
|
+
const mids = getLinkWaypointMidpoints(editor._scene, edge) ?? [];
|
|
251
|
+
for (let i = 0; i < mids.length; i++) {
|
|
252
|
+
if (within(req(mids[i]))) {
|
|
253
|
+
editor.beginWaypointDrag(wpDragLink, i, true);
|
|
254
|
+
grabbed = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (grabbed) {
|
|
260
|
+
editor.cancelLongPress();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Link-start anchor drag: a press on one of the single selected
|
|
266
|
+
// element's link-start dots begins an edge FROM that anchor without
|
|
267
|
+
// switching to the draw-edge tool. Checked before the normal hit-test
|
|
268
|
+
// / auto-select so the press isn't read as a body drag or lasso. The
|
|
269
|
+
// dots are the same world points the overlay draws (shared
|
|
270
|
+
// `anchorOverlayPoints`), so they're grabbable exactly where they
|
|
271
|
+
// appear. Modifier-clicks fall through to normal (additive) select.
|
|
272
|
+
if (editor.mode === "select" &&
|
|
273
|
+
!data.modifiers.shift &&
|
|
274
|
+
!data.modifiers.meta &&
|
|
275
|
+
!data.modifiers.ctrl) {
|
|
276
|
+
// Begin a link FROM a start dot of the single SELECTED element
|
|
277
|
+
// (connection dots only on the selected shape, so only its dots
|
|
278
|
+
// start a link). The dots sit OUTSIDE the shape; they're grabbable
|
|
279
|
+
// exactly where the overlay draws them (shared `anchorOverlayPoints`).
|
|
280
|
+
const tryAnchorDrag = (shapeId) => {
|
|
281
|
+
const shape = getElement(editor._scene, shapeId);
|
|
282
|
+
if (!shape)
|
|
283
|
+
return false;
|
|
284
|
+
const zoom = editor._scene.viewport.zoom || 1;
|
|
285
|
+
const { names, worldPoints } = anchorOverlayPoints(shape, LINK_START_ANCHOR_OUTSET / zoom);
|
|
286
|
+
const grab = (ANCHOR_DOT_ACTIVE_RADIUS + editor.anchorStartHitSlop) / zoom;
|
|
287
|
+
let bestName = null;
|
|
288
|
+
let bestD2 = grab * grab;
|
|
289
|
+
for (let i = 0; i < worldPoints.length; i++) {
|
|
290
|
+
const p = req(worldPoints[i]);
|
|
291
|
+
const dx = p.x - worldPoint.x;
|
|
292
|
+
const dy = p.y - worldPoint.y;
|
|
293
|
+
const d2 = dx * dx + dy * dy;
|
|
294
|
+
if (d2 <= bestD2) {
|
|
295
|
+
bestD2 = d2;
|
|
296
|
+
bestName = req(names[i]);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (bestName === null)
|
|
300
|
+
return false;
|
|
301
|
+
editor.cancelLongPress();
|
|
302
|
+
editor.linkDragFromAnchor = {
|
|
303
|
+
fromElement: shapeId,
|
|
304
|
+
fromWorld: getAnchorWorld(shape, { kind: "named", name: bestName }),
|
|
305
|
+
anchorName: bestName,
|
|
306
|
+
origin: worldPoint,
|
|
307
|
+
moved: false,
|
|
308
|
+
};
|
|
309
|
+
return true;
|
|
310
|
+
};
|
|
311
|
+
const selId = editor._selection.size === 1 ? [...editor._selection][0] : null;
|
|
312
|
+
if (selId && tryAnchorDrag(selId))
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
let target = editor.hitTest(worldPoint);
|
|
316
|
+
// Auto-select on press for shapes / edges that the user is about
|
|
317
|
+
// to act on (drag, resize handles): you can't manipulate an
|
|
318
|
+
// element that isn't selected, so pressing on an unselected one
|
|
319
|
+
// promotes it to the selection BEFORE the drag starts. Shift/Cmd
|
|
320
|
+
// extends instead of replacing. We don't promote inside the
|
|
321
|
+
// group-handle / edge-endpoint paths because those already act on
|
|
322
|
+
// the existing selection.
|
|
323
|
+
// Cmd/Ctrl-click on a shape that carries a (safe) link is a
|
|
324
|
+
// link-open gesture, NOT additive selection — don't mutate the
|
|
325
|
+
// selection on press; `onUp` opens the URL on a tap.
|
|
326
|
+
const linkModifier = data.modifiers.meta || data.modifiers.ctrl;
|
|
327
|
+
const isLinkOpen = linkModifier && target.kind === "element" && editor.elementLink(target.id) !== null;
|
|
328
|
+
if (!isLinkOpen && target.kind === "element" && !editor._selection.has(target.id)) {
|
|
329
|
+
const additive = data.modifiers.shift || data.modifiers.meta || data.modifiers.ctrl;
|
|
330
|
+
editor._selection = additive
|
|
331
|
+
? Selection.add(editor._selection, target.id)
|
|
332
|
+
: Selection.single(target.id);
|
|
333
|
+
// Remember an additive promotion so a tap's up-handler doesn't
|
|
334
|
+
// SELECT_TOGGLE it back off.
|
|
335
|
+
if (additive)
|
|
336
|
+
editor.additivePressAdded = target.id;
|
|
337
|
+
// Plain (non-additive) press on an element replaces the whole
|
|
338
|
+
// selection, so drop any selected links; an additive press keeps them.
|
|
339
|
+
if (!additive && editor._selectedLinks.size > 0) {
|
|
340
|
+
editor._selectedLinks = LinkSelection.EMPTY;
|
|
341
|
+
}
|
|
342
|
+
// Notify happens at the end of the gesture path; selecting now
|
|
343
|
+
// ensures the live `_selection` reflects what subsequent
|
|
344
|
+
// MOVE_SHAPE emits will operate on.
|
|
345
|
+
editor.notify();
|
|
346
|
+
}
|
|
347
|
+
// ⌥-drag duplicate: holding Alt and pressing a selected element clones
|
|
348
|
+
// the whole selection IN PLACE (incl. frame contents / group
|
|
349
|
+
// descendants); the drag below then moves the clones, leaving the
|
|
350
|
+
// originals. Runs after auto-select so the press target is in the
|
|
351
|
+
// selection. `target` is re-pointed to the clone so the snapshot drags it.
|
|
352
|
+
// (An ⌥-press without a drag leaves a stacked clone — undoable.)
|
|
353
|
+
if (data.modifiers.alt &&
|
|
354
|
+
editor.mode === "select" &&
|
|
355
|
+
target.kind === "element" &&
|
|
356
|
+
editor._selection.has(target.id)) {
|
|
357
|
+
const clone = editor.duplicateSelectedInPlace(target.id);
|
|
358
|
+
if (clone !== null)
|
|
359
|
+
target = { kind: "element", id: clone, bounds: target.bounds };
|
|
360
|
+
}
|
|
361
|
+
// Track the dragged shape id for container drop / drag-out logic
|
|
362
|
+
// on pointerup. Cleared in onUp / cancel.
|
|
363
|
+
editor.dragElementId = target.kind === "element" ? target.id : null;
|
|
364
|
+
editor.containerHover = null;
|
|
365
|
+
// Snapshot positions for the upcoming drag. Two paths populate the
|
|
366
|
+
// group-move snapshot:
|
|
367
|
+
// 1. Press lands on an already-selected shape → drag the whole
|
|
368
|
+
// selection (with descendants of any selected group).
|
|
369
|
+
// 2. Press lands on a group shape (whether selected or not) →
|
|
370
|
+
// drag that group and its descendants. Without this, a click-
|
|
371
|
+
// drag on an unselected group would only move the wrapper
|
|
372
|
+
// (zero-bounds, invisible) and leave its children behind —
|
|
373
|
+
// looking exactly like the group had been ungrouped.
|
|
374
|
+
if (target.kind === "element") {
|
|
375
|
+
const pressedElement = getElement(editor._scene, target.id);
|
|
376
|
+
const pressedIsGroup = pressedElement?.type === "group";
|
|
377
|
+
const pressedIsFrame = pressedElement?.type === "frame";
|
|
378
|
+
const inSelection = editor._selection.has(target.id);
|
|
379
|
+
if (inSelection || pressedIsGroup || pressedIsFrame) {
|
|
380
|
+
const ids = new Set();
|
|
381
|
+
if (inSelection) {
|
|
382
|
+
// Locked / layer-locked elements stay put (still selectable).
|
|
383
|
+
for (const id of editor.expandSelectionWithDescendants()) {
|
|
384
|
+
const s = getElement(editor._scene, id);
|
|
385
|
+
if (s && editor.isElementManipulable(s))
|
|
386
|
+
ids.add(id);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (pressedIsGroup) {
|
|
390
|
+
const visit = (parentId) => {
|
|
391
|
+
if (ids.has(parentId))
|
|
392
|
+
return;
|
|
393
|
+
ids.add(parentId);
|
|
394
|
+
for (const child of editor._scene.elements.values()) {
|
|
395
|
+
if (child.parentId === parentId)
|
|
396
|
+
visit(child.id);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
visit(target.id);
|
|
400
|
+
}
|
|
401
|
+
if (pressedIsFrame) {
|
|
402
|
+
// Frame drag pulls every shape with matching frameId along.
|
|
403
|
+
// Frames are flat associations — no recursive descent needed.
|
|
404
|
+
ids.add(target.id);
|
|
405
|
+
for (const s of editor._scene.elements.values()) {
|
|
406
|
+
if (s.frameId === target.id)
|
|
407
|
+
ids.add(s.id);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Links that ride along with this drag: selected links (moved
|
|
411
|
+
// whole) + connectors bound on both ends to the dragged elements
|
|
412
|
+
// (geometry follows). Snapshotted at press so frames don't compound.
|
|
413
|
+
const movingLinks = snapshotMovingLinks(editor._scene, ids, editor._selectedLinks);
|
|
414
|
+
// Start a group drag for a real multi-selection OR whenever links
|
|
415
|
+
// need to ride along (even with a single dragged element).
|
|
416
|
+
if (ids.size > 1 || movingLinks.size > 0) {
|
|
417
|
+
const snap = new Map();
|
|
418
|
+
for (const id of ids) {
|
|
419
|
+
const s = getElement(editor._scene, id);
|
|
420
|
+
if (s)
|
|
421
|
+
snap.set(id, s.position);
|
|
422
|
+
}
|
|
423
|
+
editor.groupMoveOrigin = snap;
|
|
424
|
+
editor.groupLinkMoveOrigin = movingLinks.size > 0 ? movingLinks : null;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
editor.groupMoveOrigin = null;
|
|
428
|
+
editor.groupLinkMoveOrigin = null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
editor.groupMoveOrigin = null;
|
|
433
|
+
editor.groupLinkMoveOrigin = null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
editor.groupMoveOrigin = null;
|
|
438
|
+
}
|
|
439
|
+
// Snapshot each member's world bounds + position + scale when the
|
|
440
|
+
// press lands on a group-handle so the per-frame resize math has
|
|
441
|
+
// a stable baseline to scale against.
|
|
442
|
+
//
|
|
443
|
+
// For single-group selection the selection itself is just the
|
|
444
|
+
// group wrapper (zero intrinsic bounds), so the snapshot would
|
|
445
|
+
// be useless. Expand to include every descendant — those are
|
|
446
|
+
// the leaves applyGroupResize actually scales. Same expansion
|
|
447
|
+
// is harmless for plain multi-selection (no descendants).
|
|
448
|
+
if (target.kind === "group-handle") {
|
|
449
|
+
const elements = new Map();
|
|
450
|
+
for (const id of editor.expandSelectionWithDescendants()) {
|
|
451
|
+
const s = getElement(editor._scene, id);
|
|
452
|
+
if (!s)
|
|
453
|
+
continue;
|
|
454
|
+
elements.set(id, {
|
|
455
|
+
position: s.position,
|
|
456
|
+
bounds: getElementWorldBounds(s),
|
|
457
|
+
scale: s.scale,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
// Links that scale with the box: selected links + connectors bound on
|
|
461
|
+
// both ends to the resized elements (same set as the move snapshot).
|
|
462
|
+
const links = snapshotMovingLinks(editor._scene, new Set(elements.keys()), editor._selectedLinks);
|
|
463
|
+
editor.groupResizeOrigin = { combined: target.bounds, elements, links };
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
editor.groupResizeOrigin = null;
|
|
467
|
+
}
|
|
468
|
+
// Arm one-finger pan: a TOUCH press on empty canvas in select mode.
|
|
469
|
+
// A tap still deselects via the machine below; onMove promotes this to
|
|
470
|
+
// a pan once the finger drags past slop (instead of a marquee lasso).
|
|
471
|
+
editor.touchPanCandidate =
|
|
472
|
+
data.kind === "touch" && editor.mode === "select" && target.kind === "empty"
|
|
473
|
+
? data.point
|
|
474
|
+
: null;
|
|
475
|
+
editor.actor.send({
|
|
476
|
+
type: "POINTER_DOWN",
|
|
477
|
+
point: worldPoint,
|
|
478
|
+
target,
|
|
479
|
+
modifiers: data.modifiers,
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
const onMove = (ev) => {
|
|
483
|
+
const data = fromPointerEvent(ev, editor.host);
|
|
484
|
+
// Pan gesture in flight — translate cursor delta to a screen
|
|
485
|
+
// pan and short-circuit. Doesn't touch the machine.
|
|
486
|
+
if (editor.panGesture?.pointerId === ev.pointerId) {
|
|
487
|
+
const dx = data.point.x - editor.panGesture.lastPoint.x;
|
|
488
|
+
const dy = data.point.y - editor.panGesture.lastPoint.y;
|
|
489
|
+
editor.panGesture.lastPoint = data.point;
|
|
490
|
+
// Mark as moved once total displacement crosses the slop
|
|
491
|
+
// threshold — used at pointerup to decide context-menu vs
|
|
492
|
+
// drag for right-click gestures.
|
|
493
|
+
if (!editor.panGesture.moved &&
|
|
494
|
+
distanceTo(editor.panGesture.startPoint, data.point) > LONG_PRESS_MAX_MOVEMENT_PX) {
|
|
495
|
+
editor.panGesture.moved = true;
|
|
496
|
+
}
|
|
497
|
+
// Natural-grab direction: cursor right → world moves right
|
|
498
|
+
// (shapes follow the finger). `viewportPanBy` already
|
|
499
|
+
// subtracts deltaScreen from pan, so we pass the raw cursor
|
|
500
|
+
// delta — no extra inversion.
|
|
501
|
+
editor.panBy({ x: dx, y: dy });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// Update tracked pointer position. In pinch mode, recompute the
|
|
505
|
+
// gesture and short-circuit before sending to the machine.
|
|
506
|
+
if (editor.activePointers.has(ev.pointerId)) {
|
|
507
|
+
editor.activePointers.set(ev.pointerId, data.point);
|
|
508
|
+
}
|
|
509
|
+
if (editor.pinch.isActive()) {
|
|
510
|
+
editor.applyPinch();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// One-finger pan: an armed touch press on empty canvas that has now
|
|
514
|
+
// dragged past slop becomes a pan (not a marquee lasso). beginPanGesture
|
|
515
|
+
// POINTER_CANCELs the nascent machine gesture (clears the lasso preview).
|
|
516
|
+
if (editor.touchPanCandidate !== null &&
|
|
517
|
+
editor.activePointers.size === 1 &&
|
|
518
|
+
distanceTo(editor.touchPanCandidate, data.point) > LONG_PRESS_MAX_MOVEMENT_PX) {
|
|
519
|
+
const origin = editor.touchPanCandidate;
|
|
520
|
+
editor.touchPanCandidate = null;
|
|
521
|
+
editor.beginPanGesture(ev.pointerId, ev.button, origin);
|
|
522
|
+
const pan = editor.panGesture;
|
|
523
|
+
if (pan) {
|
|
524
|
+
pan.moved = true;
|
|
525
|
+
editor.panBy({ x: data.point.x - origin.x, y: data.point.y - origin.y });
|
|
526
|
+
pan.lastPoint = data.point;
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Cancel long-press timer if the finger has moved beyond slop.
|
|
531
|
+
editor.longPress.cancelIfMovedBeyond(data.point, LONG_PRESS_MAX_MOVEMENT_PX);
|
|
532
|
+
const worldPoint = editor.screenToWorld(data.point);
|
|
533
|
+
// Track cursor for paste-at-cursor and other commands that want a
|
|
534
|
+
// sensible drop target.
|
|
535
|
+
editor.lastPointerWorld = worldPoint;
|
|
536
|
+
// Context cursor: recompute every move (before the gesture branches below
|
|
537
|
+
// early-return) so it reflects hover targets AND active gestures.
|
|
538
|
+
editor.refreshCursor(worldPoint);
|
|
539
|
+
// Host-managed elbow segment drag of the selected link.
|
|
540
|
+
if (editor.isDraggingSegment) {
|
|
541
|
+
editor.updateSegmentDrag(worldPoint);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Host-managed waypoint (bend-point) drag of the selected link.
|
|
545
|
+
if (editor.isDraggingWaypoint) {
|
|
546
|
+
editor.updateWaypointDrag(worldPoint);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Link drag started from a start-anchor (select mode, no tool switch).
|
|
550
|
+
// Host-managed end-to-end — the machine never saw a POINTER_DOWN for
|
|
551
|
+
// it. Mirror the draw-edge tool: live link preview from the anchor +
|
|
552
|
+
// snap-target highlight on the shape under the cursor. Gated on the
|
|
553
|
+
// same drag threshold so a click that barely moves is not a draw.
|
|
554
|
+
if (editor.linkDragFromAnchor) {
|
|
555
|
+
const drag = editor.linkDragFromAnchor;
|
|
556
|
+
if (!drag.moved) {
|
|
557
|
+
const dx = worldPoint.x - drag.origin.x;
|
|
558
|
+
const dy = worldPoint.y - drag.origin.y;
|
|
559
|
+
if (dx * dx + dy * dy < DRAG_THRESHOLD * DRAG_THRESHOLD)
|
|
560
|
+
return;
|
|
561
|
+
drag.moved = true;
|
|
562
|
+
}
|
|
563
|
+
editor.applyLinkPreview(drag.fromElement, drag.fromWorld, worldPoint);
|
|
564
|
+
editor.updateHoveredLinkTarget(worldPoint);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Drag-select inside the edited text shape.
|
|
568
|
+
if (editor.editingTextElement !== null && editor.isTextDragging) {
|
|
569
|
+
editor.extendTextSelectionToPoint(worldPoint);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// Brush stroke in progress — append a vertex and skip everything
|
|
573
|
+
// else (machine, container hover, hovered-edge target).
|
|
574
|
+
if (editor.brushStroke) {
|
|
575
|
+
editor.extendBrushStroke(worldPoint, ev.pressure);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Container drop preview: while dragging a single shape, find the
|
|
579
|
+
// topmost container under cursor (excluding the dragged shape and
|
|
580
|
+
// its descendants) and stash the drop-zone for the overlay.
|
|
581
|
+
if (editor.dragElementId) {
|
|
582
|
+
const dragged = editor.dragElementId;
|
|
583
|
+
const exclude = new Set([dragged]);
|
|
584
|
+
// Don't drop a container onto itself or into one of its own
|
|
585
|
+
// descendants (would create a cycle).
|
|
586
|
+
for (const s of editor._scene.elements.values()) {
|
|
587
|
+
let cursor = s.parentId;
|
|
588
|
+
for (let i = 0; cursor && i < 64; i++) {
|
|
589
|
+
if (cursor === dragged) {
|
|
590
|
+
exclude.add(s.id);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
cursor = editor._scene.elements.get(cursor)?.parentId;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const container = findContainerAt(editor._scene, worldPoint, exclude);
|
|
597
|
+
if (container) {
|
|
598
|
+
const zone = getDropZoneWorld(container);
|
|
599
|
+
if (zone) {
|
|
600
|
+
const next = { id: container.id, dropZone: zone };
|
|
601
|
+
if (editor.containerHover?.id !== next.id ||
|
|
602
|
+
editor.containerHover.dropZone !== next.dropZone) {
|
|
603
|
+
editor.containerHover = next;
|
|
604
|
+
editor.notify();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else if (editor.containerHover !== null) {
|
|
609
|
+
editor.containerHover = null;
|
|
610
|
+
editor.notify();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Annotation drag — update annotation position from delta. No
|
|
614
|
+
// patches per-move; commit on pointerup so undo is one step.
|
|
615
|
+
if (editor.annotationDrag) {
|
|
616
|
+
const drag = editor.annotationDrag;
|
|
617
|
+
const dx = worldPoint.x - drag.originWorldPoint.x;
|
|
618
|
+
const dy = worldPoint.y - drag.originWorldPoint.y;
|
|
619
|
+
if (dx !== 0 || dy !== 0)
|
|
620
|
+
drag.moved = true;
|
|
621
|
+
const ann = editor._scene.annotations.get(drag.id);
|
|
622
|
+
if (ann) {
|
|
623
|
+
// Mutate via apply to keep render in sync; final commit on
|
|
624
|
+
// up rewrites the patch from origin to final.
|
|
625
|
+
const newPos = { x: drag.originPosition.x + dx, y: drag.originPosition.y + dy };
|
|
626
|
+
const next = { ...ann, position: newPos };
|
|
627
|
+
const annotations = new Map(editor._scene.annotations);
|
|
628
|
+
annotations.set(drag.id, next);
|
|
629
|
+
editor._scene = { ...editor._scene, annotations };
|
|
630
|
+
editor.notify();
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// Fan out to anyone listening for the local cursor — `@collab`
|
|
635
|
+
// broadcasts it via awareness. Fires on every move; subscribers
|
|
636
|
+
// throttle if they care.
|
|
637
|
+
for (const fn of editor.cursorListeners)
|
|
638
|
+
fn(worldPoint);
|
|
639
|
+
const ctx = editor.actor.getSnapshot().context;
|
|
640
|
+
if (ctx.pressOrigin &&
|
|
641
|
+
ctx.mode !== "select" &&
|
|
642
|
+
ctx.mode !== "draw-edge" &&
|
|
643
|
+
editor.isDrawingPhase(ctx)) {
|
|
644
|
+
// Update rubber-band preview live for rect / ellipse drawing.
|
|
645
|
+
editor.drawingPreview = boundsFromPoints(ctx.pressOrigin, worldPoint);
|
|
646
|
+
}
|
|
647
|
+
// Port-overlay tracking in draw-edge mode — both when idle (showing
|
|
648
|
+
// where you can start an edge) and during the gesture (showing the
|
|
649
|
+
// snap target as the pointer crosses shapes).
|
|
650
|
+
if (ctx.mode === "draw-edge") {
|
|
651
|
+
editor.updateHoveredLinkTarget(worldPoint);
|
|
652
|
+
}
|
|
653
|
+
else if (editor.hoveredLinkTarget !== null) {
|
|
654
|
+
editor.hoveredLinkTarget = null;
|
|
655
|
+
editor.notify();
|
|
656
|
+
}
|
|
657
|
+
// Hover-to-play: while idle (no active press) and directly over an
|
|
658
|
+
// animated image, signal hover so a paused GIF can resume. (No
|
|
659
|
+
// link-attach anchor reveal on idle hover — those appear only while
|
|
660
|
+
// a link is actually being drawn; see the draw-edge / drag-from-
|
|
661
|
+
// anchor paths.)
|
|
662
|
+
if (!ctx.pressOrigin) {
|
|
663
|
+
const hov = editor.hitTest(worldPoint);
|
|
664
|
+
const directHs = hov.kind === "element" ? editor._scene.elements.get(hov.id) : undefined;
|
|
665
|
+
editor.hoverAnimatedElement(directHs && isImage(directHs) && directHs.animationKind ? directHs.id : null);
|
|
666
|
+
// Track the idle cursor so the SINGLE selected element's link-start dot
|
|
667
|
+
// grows by proximity (`worldPoint`). Only the selected element's dots
|
|
668
|
+
// react; connecting from an unselected shape on hover is not offered.
|
|
669
|
+
editor.setHoverCursorWorld(editor.mode === "select" ? worldPoint : null);
|
|
670
|
+
}
|
|
671
|
+
editor.actor.send({ type: "POINTER_MOVE", point: worldPoint });
|
|
672
|
+
};
|
|
673
|
+
const onUp = (ev) => {
|
|
674
|
+
if (editor.host.hasPointerCapture(ev.pointerId)) {
|
|
675
|
+
editor.host.releasePointerCapture(ev.pointerId);
|
|
676
|
+
}
|
|
677
|
+
editor.activePointers.delete(ev.pointerId);
|
|
678
|
+
// Tap or drag finished — disarm the one-finger-pan candidate (a tap
|
|
679
|
+
// never promoted to a pan, so it falls through to select/deselect).
|
|
680
|
+
editor.touchPanCandidate = null;
|
|
681
|
+
// Pan gesture ends — clean up cursor and state, skip the rest.
|
|
682
|
+
if (editor.panGesture?.pointerId === ev.pointerId) {
|
|
683
|
+
editor.endPanGesture();
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
// Exit pinch when the second-to-last finger lifts — the surviving
|
|
687
|
+
// touch (if any) does NOT resume as a single-finger drag, because
|
|
688
|
+
// we already cancelled the machine on pinch entry.
|
|
689
|
+
if (editor.pinch.isActive()) {
|
|
690
|
+
if (editor.activePointers.size < 2)
|
|
691
|
+
editor.pinch.end();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
// Long-press loses its chance the moment the user releases.
|
|
695
|
+
editor.cancelLongPress();
|
|
696
|
+
// Commit a host-managed elbow segment drag (one undo step).
|
|
697
|
+
if (editor.isDraggingSegment) {
|
|
698
|
+
editor.endSegmentDrag();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
// Commit a host-managed waypoint drag (one undo step; collapses if
|
|
702
|
+
// dropped onto the line).
|
|
703
|
+
if (editor.isDraggingWaypoint) {
|
|
704
|
+
editor.endWaypointDrag();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
// Commit a link drag that began from a start-anchor. If it moved past
|
|
708
|
+
// the threshold, create the edge (landing on the shape under the
|
|
709
|
+
// cursor, if any). If it did NOT move it was a plain click that merely
|
|
710
|
+
// landed in the anchor grab halo — fall back to normal click semantics
|
|
711
|
+
// so the gesture still selects / deselects. Either way clear the
|
|
712
|
+
// preview/hover.
|
|
713
|
+
//
|
|
714
|
+
// A click *exactly on a dot* (narrow radius) spawns a new element + link
|
|
715
|
+
// in that dot's direction; a click in the wider grab halo hit-tests as
|
|
716
|
+
// empty canvas → deselect.
|
|
717
|
+
if (editor.linkDragFromAnchor) {
|
|
718
|
+
const drag = editor.linkDragFromAnchor;
|
|
719
|
+
editor.linkDragFromAnchor = null;
|
|
720
|
+
const upData = fromPointerEvent(ev, editor.host);
|
|
721
|
+
const upWorld = editor.screenToWorld(upData.point);
|
|
722
|
+
if (drag.moved) {
|
|
723
|
+
const upHit = editor.hitTest(upWorld);
|
|
724
|
+
const toElement = upHit.kind === "element" ? upHit.id : null;
|
|
725
|
+
editor.applyEmit({
|
|
726
|
+
type: "CREATE_EDGE",
|
|
727
|
+
fromElement: drag.fromElement,
|
|
728
|
+
toElement,
|
|
729
|
+
fromPoint: drag.fromWorld,
|
|
730
|
+
toPoint: upWorld,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
// A click, not a draw. If it landed exactly ON the dot (narrow
|
|
735
|
+
// radius), spawn a new element + link in that dot's direction.
|
|
736
|
+
// Otherwise it was a click in the wider grab halo → normal
|
|
737
|
+
// select / deselect by hit-test.
|
|
738
|
+
const selShape = getElement(editor._scene, drag.fromElement);
|
|
739
|
+
const zoom = editor._scene.viewport.zoom || 1;
|
|
740
|
+
let onDot = false;
|
|
741
|
+
if (selShape) {
|
|
742
|
+
const { names, worldPoints } = anchorOverlayPoints(selShape, LINK_START_ANCHOR_OUTSET / zoom);
|
|
743
|
+
const idx = names.indexOf(drag.anchorName);
|
|
744
|
+
if (idx >= 0) {
|
|
745
|
+
const dp = req(worldPoints[idx]);
|
|
746
|
+
const r = editor.anchorClickRadius / zoom;
|
|
747
|
+
const dx = dp.x - drag.origin.x;
|
|
748
|
+
const dy = dp.y - drag.origin.y;
|
|
749
|
+
onDot = dx * dx + dy * dy <= r * r;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (onDot) {
|
|
753
|
+
editor.createLinkedElementFromAnchor(drag.fromElement, drag.anchorName);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
const upHit = editor.hitTest(upWorld);
|
|
757
|
+
if (upHit.kind === "empty")
|
|
758
|
+
editor.applyEmit({ type: "SELECT_CLEAR" });
|
|
759
|
+
else if (upHit.kind === "element")
|
|
760
|
+
editor.applyEmit({ type: "SELECT_REPLACE", id: upHit.id });
|
|
761
|
+
else if (upHit.kind === "link")
|
|
762
|
+
editor.applyEmit({ type: "SELECT_EDGE_REPLACE", id: upHit.id });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
editor.edgePreview = null;
|
|
766
|
+
editor.hoveredLinkTarget = null;
|
|
767
|
+
editor.notify();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
// End an in-canvas text drag-select.
|
|
771
|
+
if (editor.editingTextElement !== null && editor.isTextDragging) {
|
|
772
|
+
editor.endTextDragSelect();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// Commit brush stroke if one is in progress.
|
|
776
|
+
if (editor.brushStroke) {
|
|
777
|
+
editor.commitBrushStroke();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
// Annotation drag commit — issue a single patch that goes from
|
|
781
|
+
// origin position to final position so history has one undo step.
|
|
782
|
+
if (editor.annotationDrag) {
|
|
783
|
+
const drag = editor.annotationDrag;
|
|
784
|
+
editor.annotationDrag = null;
|
|
785
|
+
if (drag.moved) {
|
|
786
|
+
const final = editor._scene.annotations.get(drag.id);
|
|
787
|
+
if (final) {
|
|
788
|
+
// Reset to origin, then issue patch with proper before/after.
|
|
789
|
+
const origin = { ...final, position: drag.originPosition };
|
|
790
|
+
const annotations = new Map(editor._scene.annotations);
|
|
791
|
+
annotations.set(drag.id, origin);
|
|
792
|
+
editor._scene = { ...editor._scene, annotations };
|
|
793
|
+
const r = updateAnnotation(editor._scene, drag.id, () => final);
|
|
794
|
+
editor._scene = r.scene;
|
|
795
|
+
editor._history.push(r.patch);
|
|
796
|
+
editor.notify();
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const data = fromPointerEvent(ev, editor.host);
|
|
802
|
+
const worldPoint = editor.screenToWorld(data.point);
|
|
803
|
+
// Cmd/Ctrl-click on a linked element (a tap, not a drag) opens its
|
|
804
|
+
// href — onDown already skipped the additive-select promote for this
|
|
805
|
+
// case, so selection is untouched.
|
|
806
|
+
{
|
|
807
|
+
const origin = editor.actor.getSnapshot().context.pressOrigin;
|
|
808
|
+
const linkMod = data.modifiers.meta || data.modifiers.ctrl;
|
|
809
|
+
if (origin && linkMod) {
|
|
810
|
+
const zoom = editor._scene.viewport.zoom || 1;
|
|
811
|
+
const movedPx = Math.hypot(worldPoint.x - origin.x, worldPoint.y - origin.y) * zoom;
|
|
812
|
+
if (movedPx < LONG_PRESS_MAX_MOVEMENT_PX) {
|
|
813
|
+
const hit = editor.hitTest(worldPoint);
|
|
814
|
+
if (hit.kind === "element") {
|
|
815
|
+
const href = editor.elementLink(hit.id);
|
|
816
|
+
if (href) {
|
|
817
|
+
editor.openLink(href);
|
|
818
|
+
editor.actor.send({ type: "POINTER_UP", point: worldPoint });
|
|
819
|
+
editor.commitGesture();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// First, fire any click-style effect derived from the press context.
|
|
827
|
+
const ctxBeforeUp = editor.actor.getSnapshot().context;
|
|
828
|
+
let clickEffect = interpretPressEnd(ctxBeforeUp, worldPoint);
|
|
829
|
+
// A shift/meta TAP on a shape the press already added additively must
|
|
830
|
+
// NOT toggle it back off — the press handled the add, this would undo
|
|
831
|
+
// it (net zero). Drop the redundant toggle. (shift-tap on a shape that
|
|
832
|
+
// was already selected → additivePressAdded is null → toggle still
|
|
833
|
+
// fires and removes it, as expected.)
|
|
834
|
+
if (clickEffect?.type === "SELECT_TOGGLE" && editor.additivePressAdded === clickEffect.id) {
|
|
835
|
+
clickEffect = null;
|
|
836
|
+
}
|
|
837
|
+
editor.additivePressAdded = null;
|
|
838
|
+
// Group isolation routing:
|
|
839
|
+
// - Double-click on a grouped shape → enter the topmost group
|
|
840
|
+
// ancestor; select the inner shape directly (skipping the
|
|
841
|
+
// promote-to-group logic that would otherwise re-select the
|
|
842
|
+
// group root).
|
|
843
|
+
// - Inside isolation, a click that lands outside the entered
|
|
844
|
+
// group's descendants (empty space OR another shape) exits
|
|
845
|
+
// isolation and lets the normal selection happen.
|
|
846
|
+
// Both branches override what `interpretPressEnd` produced.
|
|
847
|
+
const handledByIsolation = editor.routeIsolationClick(clickEffect, worldPoint);
|
|
848
|
+
if (!handledByIsolation && clickEffect) {
|
|
849
|
+
editor.applyEmit(clickEffect);
|
|
850
|
+
}
|
|
851
|
+
editor.drawingPreview = null;
|
|
852
|
+
// Provide the up-side hit-test when the gesture cares about where
|
|
853
|
+
// it lands: drawing a new edge, or re-binding an existing edge
|
|
854
|
+
// endpoint. The hit-test sees the *current* selection (edge or
|
|
855
|
+
// shape) and so resolves correctly to either kind.
|
|
856
|
+
// For drawing a NEW edge, the normal hit-test resolves the drop (no link is
|
|
857
|
+
// selected, so it falls through to the element). For re-binding an EXISTING
|
|
858
|
+
// endpoint, the dragged end now lives at the cursor and `hitTest` would
|
|
859
|
+
// return that endpoint handle instead of the element under it — use the
|
|
860
|
+
// element-only attach hit-test so the drop binds to the shape / anchor.
|
|
861
|
+
let upTarget;
|
|
862
|
+
if (ctxBeforeUp.mode === "draw-edge") {
|
|
863
|
+
upTarget = editor.hitTest(worldPoint);
|
|
864
|
+
}
|
|
865
|
+
else if (ctxBeforeUp.pressTarget?.kind === "edge-endpoint") {
|
|
866
|
+
upTarget = editor.linkAttachTargetAt(worldPoint);
|
|
867
|
+
}
|
|
868
|
+
editor.actor.send(upTarget !== undefined
|
|
869
|
+
? { type: "POINTER_UP", point: worldPoint, target: upTarget }
|
|
870
|
+
: { type: "POINTER_UP", point: worldPoint });
|
|
871
|
+
// Container reparent / drag-out — must run before commitGesture
|
|
872
|
+
// so the parentId / autoGrow patches land in the same undo step.
|
|
873
|
+
editor.applyContainerDrop(worldPoint);
|
|
874
|
+
// Frame membership re-evaluation (drop into / out of a frame, or a
|
|
875
|
+
// frame resized over shapes) — also before commit so it's one undo.
|
|
876
|
+
editor.reconcileFrameMembership();
|
|
877
|
+
editor.commitGesture();
|
|
878
|
+
// A tap (not a drag) on an animated image toggles its GIF playback —
|
|
879
|
+
// the way to resume a heavy GIF that auto-stopped or a GIF held paused
|
|
880
|
+
// under prefers-reduced-motion. Gated on near-zero displacement so it
|
|
881
|
+
// never fires at the end of a drag.
|
|
882
|
+
const origin = ctxBeforeUp.pressOrigin;
|
|
883
|
+
if (origin) {
|
|
884
|
+
const zoom = editor._scene.viewport.zoom || 1;
|
|
885
|
+
const movedPx = Math.hypot(worldPoint.x - origin.x, worldPoint.y - origin.y) * zoom;
|
|
886
|
+
if (movedPx < LONG_PRESS_MAX_MOVEMENT_PX) {
|
|
887
|
+
const hit = editor.hitTest(worldPoint);
|
|
888
|
+
if (hit.kind === "element") {
|
|
889
|
+
const s = editor._scene.elements.get(hit.id);
|
|
890
|
+
if (s && isImage(s) && s.animationKind)
|
|
891
|
+
editor.togglePlayback(s.id);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// The gesture is over — recompute the cursor for the now-idle hover state.
|
|
896
|
+
editor.refreshCursor(worldPoint);
|
|
897
|
+
};
|
|
898
|
+
const onCancel = (ev) => {
|
|
899
|
+
editor.activePointers.delete(ev.pointerId);
|
|
900
|
+
editor.touchPanCandidate = null;
|
|
901
|
+
if (editor.panGesture?.pointerId === ev.pointerId) {
|
|
902
|
+
editor.endPanGesture();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (editor.pinch.isActive()) {
|
|
906
|
+
if (editor.activePointers.size < 2)
|
|
907
|
+
editor.pinch.end();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
editor.cancelLongPress();
|
|
911
|
+
// Abort a link-from-anchor drag — drop the preview/hover, create
|
|
912
|
+
// nothing.
|
|
913
|
+
if (editor.linkDragFromAnchor) {
|
|
914
|
+
editor.linkDragFromAnchor = null;
|
|
915
|
+
editor.edgePreview = null;
|
|
916
|
+
editor.hoveredLinkTarget = null;
|
|
917
|
+
editor.notify();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (editor.brushStroke) {
|
|
921
|
+
editor.cancelBrushStroke();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
// Annotation drag — revert to origin on cancel.
|
|
925
|
+
if (editor.annotationDrag) {
|
|
926
|
+
const drag = editor.annotationDrag;
|
|
927
|
+
editor.annotationDrag = null;
|
|
928
|
+
const ann = editor._scene.annotations.get(drag.id);
|
|
929
|
+
if (ann) {
|
|
930
|
+
const annotations = new Map(editor._scene.annotations);
|
|
931
|
+
annotations.set(drag.id, { ...ann, position: drag.originPosition });
|
|
932
|
+
editor._scene = { ...editor._scene, annotations };
|
|
933
|
+
editor.notify();
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
editor.drawingPreview = null;
|
|
938
|
+
editor.actor.send({ type: "POINTER_CANCEL" });
|
|
939
|
+
editor.cancelGesture();
|
|
940
|
+
};
|
|
941
|
+
editor.host.addEventListener("pointerdown", onDown);
|
|
942
|
+
editor.host.addEventListener("pointermove", onMove);
|
|
943
|
+
editor.host.addEventListener("pointerup", onUp);
|
|
944
|
+
editor.host.addEventListener("pointercancel", onCancel);
|
|
945
|
+
// Right-click handling: the contextmenu DOM event fires once per
|
|
946
|
+
// right mouse press, AFTER pointerup on most browsers. We use
|
|
947
|
+
// `suppressNextContextMenu` (set on right-click pointerdown) to:
|
|
948
|
+
// • preventDefault the native browser menu;
|
|
949
|
+
// • stopPropagation so window-level listeners (like
|
|
950
|
+
// `@react-ui/ContextMenu` default) don't re-open a menu when
|
|
951
|
+
// the user was actually panning.
|
|
952
|
+
// The "menu on click without drag" path lives in `endPanGesture`:
|
|
953
|
+
// it fires `longPressListeners` directly, which is what
|
|
954
|
+
// ContextMenu also subscribes to. So a clean right-click still
|
|
955
|
+
// produces a menu — through our event channel, not the native
|
|
956
|
+
// contextmenu DOM event.
|
|
957
|
+
const onContextMenu = (ev) => {
|
|
958
|
+
if (!editor.suppressNextContextMenu)
|
|
959
|
+
return;
|
|
960
|
+
editor.suppressNextContextMenu = false;
|
|
961
|
+
ev.preventDefault();
|
|
962
|
+
ev.stopPropagation();
|
|
963
|
+
};
|
|
964
|
+
// Capture phase so we beat the window-level listener that
|
|
965
|
+
// ContextMenu attaches in its useEffect.
|
|
966
|
+
editor.host.addEventListener("contextmenu", onContextMenu, true);
|
|
967
|
+
// Window-level Space tracking so Space anywhere on the page
|
|
968
|
+
// arms the next mouse drag as a pan. Skip when focus is in a
|
|
969
|
+
// text input — Space should still type a space there.
|
|
970
|
+
const isEditableTarget = (target) => {
|
|
971
|
+
if (!(target instanceof HTMLElement))
|
|
972
|
+
return false;
|
|
973
|
+
const tag = target.tagName;
|
|
974
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable;
|
|
975
|
+
};
|
|
976
|
+
const onKeyDown = (ev) => {
|
|
977
|
+
if (ev.code !== "Space" && ev.key !== " ")
|
|
978
|
+
return;
|
|
979
|
+
if (isEditableTarget(ev.target))
|
|
980
|
+
return;
|
|
981
|
+
if (editor.spaceHeld)
|
|
982
|
+
return;
|
|
983
|
+
editor.spaceHeld = true;
|
|
984
|
+
// Visual affordance: "grab" cursor signals the user can drag-pan.
|
|
985
|
+
editor.refreshCursor();
|
|
986
|
+
// Prevent page scroll on Space — common in browsers when no
|
|
987
|
+
// input is focused. We're holding it as a modifier, not as text.
|
|
988
|
+
ev.preventDefault();
|
|
989
|
+
};
|
|
990
|
+
const onKeyUp = (ev) => {
|
|
991
|
+
if (ev.code !== "Space" && ev.key !== " ")
|
|
992
|
+
return;
|
|
993
|
+
if (!editor.spaceHeld)
|
|
994
|
+
return;
|
|
995
|
+
editor.spaceHeld = false;
|
|
996
|
+
// Recompute (→ "grabbing" if a pan is still in flight, else idle hover).
|
|
997
|
+
editor.refreshCursor();
|
|
998
|
+
};
|
|
999
|
+
// window guard so node-env tests can still construct the editor.
|
|
1000
|
+
if (typeof window !== "undefined") {
|
|
1001
|
+
window.addEventListener("keydown", onKeyDown);
|
|
1002
|
+
window.addEventListener("keyup", onKeyUp);
|
|
1003
|
+
}
|
|
1004
|
+
// Wheel routing: mouse wheel → zoom, trackpad → pan / pinch.
|
|
1005
|
+
// Browsers fire identical `wheel` events for both devices and no
|
|
1006
|
+
// per-event signal reliably distinguishes them.
|
|
1007
|
+
//
|
|
1008
|
+
// Per-event classification:
|
|
1009
|
+
// • Cmd / Ctrl + wheel (also browser-synthesized for trackpad
|
|
1010
|
+
// pinch) → ZOOM around cursor.
|
|
1011
|
+
// • Shift + plain wheel → horizontal pan from vertical delta.
|
|
1012
|
+
// • Any deltaX ≠ 0 → trackpad 2D swipe → PAN both axes.
|
|
1013
|
+
// • Plain deltaY only → ZOOM (mouse wheel; rare pure-vertical
|
|
1014
|
+
// trackpad swipes also land here).
|
|
1015
|
+
//
|
|
1016
|
+
// Pan direction: `panBy` subtracts deltaScreen from `viewport.pan`,
|
|
1017
|
+
// so we negate the wheel delta — positive deltaX (page scrolls
|
|
1018
|
+
// right) → camera right → content shifts LEFT, matching native
|
|
1019
|
+
// browser scroll feel.
|
|
1020
|
+
const onWheel = (ev) => {
|
|
1021
|
+
ev.preventDefault();
|
|
1022
|
+
const rect = editor.host.getBoundingClientRect();
|
|
1023
|
+
const screenPoint = { x: ev.clientX - rect.left, y: ev.clientY - rect.top };
|
|
1024
|
+
const applyZoom = () => {
|
|
1025
|
+
if (ev.deltaY === 0)
|
|
1026
|
+
return;
|
|
1027
|
+
// Clamp the raw wheel `deltaY` to MAX_STEP so the harsh ratchet
|
|
1028
|
+
// of a mouse wheel turns into a calm ~10 % step per notch, while
|
|
1029
|
+
// trackpad pinch events — which arrive with `|deltaY|` of 2–5 —
|
|
1030
|
+
// bypass the clamp and stay granular for smooth multi-frame zooms.
|
|
1031
|
+
const clampedDelta = Math.abs(ev.deltaY) > WHEEL_ZOOM_MAX_STEP
|
|
1032
|
+
? WHEEL_ZOOM_MAX_STEP * Math.sign(ev.deltaY)
|
|
1033
|
+
: ev.deltaY;
|
|
1034
|
+
const factor = 1 - (clampedDelta * WHEEL_ZOOM_SPEED) / 100;
|
|
1035
|
+
if (factor <= 0)
|
|
1036
|
+
return;
|
|
1037
|
+
const currentZoom = editor._scene.viewport.zoom;
|
|
1038
|
+
const nextZoom = clampZoom(currentZoom * factor);
|
|
1039
|
+
if (nextZoom === currentZoom)
|
|
1040
|
+
return;
|
|
1041
|
+
const anchor = editor.screenToWorld(screenPoint);
|
|
1042
|
+
editor.zoomAt(nextZoom / currentZoom, anchor);
|
|
1043
|
+
};
|
|
1044
|
+
const applyPan = () => {
|
|
1045
|
+
let dx = ev.deltaX;
|
|
1046
|
+
let dy = ev.deltaY;
|
|
1047
|
+
if (ev.shiftKey && dx === 0) {
|
|
1048
|
+
dx = dy;
|
|
1049
|
+
dy = 0;
|
|
1050
|
+
}
|
|
1051
|
+
editor.panBy({ x: -dx * WHEEL_PAN_FACTOR, y: -dy * WHEEL_PAN_FACTOR });
|
|
1052
|
+
};
|
|
1053
|
+
// Modifier-driven zoom (Cmd/Ctrl+wheel + trackpad pinch via
|
|
1054
|
+
// browser-synthesized ctrlKey).
|
|
1055
|
+
if (ev.ctrlKey || ev.metaKey) {
|
|
1056
|
+
applyZoom();
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
// Trackpad 2-finger swipe with any horizontal component →
|
|
1060
|
+
// pan both axes. Mouse wheels never set deltaX, so this
|
|
1061
|
+
// branch never misroutes mouse input.
|
|
1062
|
+
if (ev.deltaX !== 0) {
|
|
1063
|
+
applyPan();
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
// Plain vertical wheel — always ZOOM. Pure-vertical trackpad swipes
|
|
1067
|
+
// also hit this; users who want vertical-only trackpad pan use
|
|
1068
|
+
// Space+drag or right-drag.
|
|
1069
|
+
applyZoom();
|
|
1070
|
+
};
|
|
1071
|
+
// `passive: false` because we preventDefault. Browsers default wheel
|
|
1072
|
+
// listeners to passive — must opt out explicitly.
|
|
1073
|
+
editor.host.addEventListener("wheel", onWheel, { passive: false });
|
|
1074
|
+
return () => {
|
|
1075
|
+
editor.host.removeEventListener("pointerdown", onDown);
|
|
1076
|
+
editor.host.removeEventListener("pointermove", onMove);
|
|
1077
|
+
editor.host.removeEventListener("pointerup", onUp);
|
|
1078
|
+
editor.host.removeEventListener("pointercancel", onCancel);
|
|
1079
|
+
editor.host.removeEventListener("contextmenu", onContextMenu, true);
|
|
1080
|
+
editor.host.removeEventListener("wheel", onWheel);
|
|
1081
|
+
if (typeof window !== "undefined") {
|
|
1082
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
1083
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
};
|
|
1087
|
+
//# sourceMappingURL=pointer-binding.js.map
|