@edv4h/usketch-plugin-tool-select 1.0.0 → 2.0.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/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +111 -649
- package/dist/plugin.js.map +1 -1
- package/dist/resize-utils.d.ts +1 -132
- package/dist/resize-utils.d.ts.map +1 -1
- package/dist/resize-utils.js +5 -387
- package/dist/resize-utils.js.map +1 -1
- package/package.json +5 -4
package/dist/plugin.js
CHANGED
|
@@ -1,99 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { safeRotation } from "@edv4h/usketch-shared";
|
|
3
|
+
import { createDeleteWithChildrenCommand } from "@edv4h/usketch-store";
|
|
4
|
+
import { findHandleAtScreenPoint, findMultiHandleAtScreenPoint, findRotationHandleAtScreenPoint, findShapeAtPoint, getCursorForHandle, getMultiSelectionBounds, getRotatedCursorForHandle, startDragSession, startMarqueeSession, startResizeSession, startRotateSession, trackHover, } from "@edv4h/usketch-tool-helpers";
|
|
4
5
|
import { clearMovingSelectionListeners, setMovingSelection } from "./drag-state.js";
|
|
5
6
|
import { clearDropTargetListeners, setDropTargetId } from "./drop-target-state.js";
|
|
6
7
|
import { clearEditingGroupListeners, getEditingGroupId, setEditingGroupId, } from "./group-edit-state.js";
|
|
7
8
|
import { clearHoveredShapeListeners, setHoveredShapeId } from "./hover-state.js";
|
|
8
9
|
import { clearMarqueeListeners, setMarquee, setMarqueeMode } from "./marquee-state.js";
|
|
9
|
-
import { applyFlip, computeMultiResizeUpdates, computeRawBounds, computeRelativeProps, findHandleAtScreenPoint, findMultiHandleAtScreenPoint, findRotationHandleAtScreenPoint, fixAnchorDrift, getAnchorEdges, getCursorForHandle, getMultiSelectionBounds, getRotatedCursorForHandle, } from "./resize-utils.js";
|
|
10
10
|
import { SelectionOverlay } from "./selection-overlay.js";
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const editingGroupId = getEditingGroupId();
|
|
15
|
-
// Iterate in reverse insertion order (top-most shape first)
|
|
16
|
-
// but prioritize non-container shapes over containers (island/frame)
|
|
17
|
-
const CONTAINER_TYPES = new Set(["island", "frame"]);
|
|
18
|
-
const entries = [...shapes.entries()].reverse();
|
|
19
|
-
// First pass: find non-container shapes
|
|
20
|
-
let containerHit = null;
|
|
21
|
-
for (const [id, data] of entries) {
|
|
22
|
-
const def = ctx.shapes.get(data.type);
|
|
23
|
-
if (!def?.hitTest(data, point))
|
|
24
|
-
continue;
|
|
25
|
-
// If inside a group editing session, only hit children of that group
|
|
26
|
-
if (editingGroupId) {
|
|
27
|
-
if (data.parentId === editingGroupId)
|
|
28
|
-
return id;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
// If this shape has a parent, check the parent type:
|
|
32
|
-
// - Frame/Island children are directly selectable (containers)
|
|
33
|
-
// - Group children resolve to the top-level group ancestor
|
|
34
|
-
if (typeof data.parentId === "string") {
|
|
35
|
-
const parent = ctx.store.getShape(data.parentId);
|
|
36
|
-
if (parent?.type === "frame" || parent?.type === "island") {
|
|
37
|
-
return id;
|
|
38
|
-
}
|
|
39
|
-
const ancestor = getTopLevelAncestor(ctx.store, id);
|
|
40
|
-
if (ancestor)
|
|
41
|
-
return ancestor.id;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
// Defer containers — prefer their children first
|
|
45
|
-
if (CONTAINER_TYPES.has(data.type)) {
|
|
46
|
-
if (!containerHit)
|
|
47
|
-
containerHit = id;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
return id;
|
|
51
|
-
}
|
|
52
|
-
// No non-container shape found, return the container itself
|
|
53
|
-
return containerHit;
|
|
54
|
-
}
|
|
55
|
-
function findShapesInRect(ctx, rect, mode) {
|
|
56
|
-
const test = mode === "contain" ? boxContains : boxesIntersect;
|
|
57
|
-
const shapes = ctx.store.getShapes();
|
|
58
|
-
const editingGroupId = getEditingGroupId();
|
|
59
|
-
const ids = new Set();
|
|
60
|
-
for (const [id, data] of shapes) {
|
|
61
|
-
const def = ctx.shapes.get(data.type);
|
|
62
|
-
const bounds = def
|
|
63
|
-
? def.getBounds(data)
|
|
64
|
-
: { x: data.x, y: data.y, width: data.width, height: data.height };
|
|
65
|
-
if (!test(rect, bounds))
|
|
66
|
-
continue;
|
|
67
|
-
// If in group edit mode, only select children of that group
|
|
68
|
-
if (editingGroupId) {
|
|
69
|
-
if (data.parentId === editingGroupId)
|
|
70
|
-
ids.add(id);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
// Frame children are directly selectable; group children resolve to ancestor
|
|
74
|
-
if (typeof data.parentId === "string") {
|
|
75
|
-
const parent = ctx.store.getShape(data.parentId);
|
|
76
|
-
if (parent?.type === "frame") {
|
|
77
|
-
ids.add(id);
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
const ancestor = getTopLevelAncestor(ctx.store, id);
|
|
81
|
-
if (ancestor)
|
|
82
|
-
ids.add(ancestor.id);
|
|
83
|
-
}
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
ids.add(id);
|
|
87
|
-
}
|
|
88
|
-
return [...ids];
|
|
89
|
-
}
|
|
90
|
-
function boxesIntersect(a, b) {
|
|
91
|
-
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
|
92
|
-
}
|
|
93
|
-
/** Returns true if `a` fully contains `b` */
|
|
94
|
-
function boxContains(a, b) {
|
|
95
|
-
return (a.x <= b.x && a.y <= b.y && a.x + a.width >= b.x + b.width && a.y + a.height >= b.y + b.height);
|
|
96
|
-
}
|
|
11
|
+
// Hit-test helpers used to live here; they were extracted to
|
|
12
|
+
// `@edv4h/usketch-tool-helpers` (Issue #576) so other tools can reuse
|
|
13
|
+
// the same shape-vs-container precedence rules.
|
|
97
14
|
// ── Icon ──
|
|
98
15
|
function SelectIcon() {
|
|
99
16
|
return (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", children: _jsx("path", { d: "M6 2L6 16L10 12L14 16L16 14L12 10L16 6Z", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round" }) }));
|
|
@@ -147,7 +64,7 @@ export const selectToolPlugin = {
|
|
|
147
64
|
function onPointerDown(toolCtx, event) {
|
|
148
65
|
disableTextSelection();
|
|
149
66
|
const viewport = toolCtx.store.getViewport();
|
|
150
|
-
// 0.
|
|
67
|
+
// 0. Rotation handle (outside bbox, no conflict with resize)
|
|
151
68
|
const rotationHit = findRotationHandleAtScreenPoint(event.screenPoint, toolCtx.shapes, toolCtx.store, viewport);
|
|
152
69
|
if (rotationHit) {
|
|
153
70
|
const shape = toolCtx.store.getShape(rotationHit);
|
|
@@ -155,27 +72,21 @@ export const selectToolPlugin = {
|
|
|
155
72
|
const cx = shape.x + shape.width / 2;
|
|
156
73
|
const cy = shape.y + shape.height / 2;
|
|
157
74
|
const startAngle = Math.atan2(event.worldPoint.y - cy, event.worldPoint.x - cx) * (180 / Math.PI);
|
|
158
|
-
// Collect child snapshots for container rotation
|
|
159
|
-
const childSnapshots = new Map();
|
|
160
|
-
if (shape.type === "frame" || shape.type === "group" || shape.type === "island") {
|
|
161
|
-
const children = getChildShapes(toolCtx.store, rotationHit);
|
|
162
|
-
for (const child of children) {
|
|
163
|
-
childSnapshots.set(child.id, { ...child });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
75
|
setOverrideCursor("grabbing");
|
|
167
76
|
dragState = {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
77
|
+
kind: "rotate",
|
|
78
|
+
session: startRotateSession({
|
|
79
|
+
ctx: toolCtx,
|
|
80
|
+
shapeId: rotationHit,
|
|
81
|
+
center: { x: cx, y: cy },
|
|
82
|
+
startAngle,
|
|
83
|
+
startRotation: safeRotation(shape.rotation),
|
|
84
|
+
}),
|
|
174
85
|
};
|
|
175
86
|
return;
|
|
176
87
|
}
|
|
177
88
|
}
|
|
178
|
-
// 1.
|
|
89
|
+
// 1. Single-shape resize handle
|
|
179
90
|
const handleHit = findHandleAtScreenPoint(event.screenPoint, toolCtx.shapes, toolCtx.store, viewport);
|
|
180
91
|
if (handleHit) {
|
|
181
92
|
const shape = toolCtx.store.getShape(handleHit.shapeId);
|
|
@@ -185,16 +96,20 @@ export const selectToolPlugin = {
|
|
|
185
96
|
? getRotatedCursorForHandle(handleHit.handle, shapeRotation)
|
|
186
97
|
: getCursorForHandle(handleHit.handle));
|
|
187
98
|
dragState = {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
99
|
+
kind: "resize",
|
|
100
|
+
rotation: shapeRotation,
|
|
101
|
+
session: startResizeSession({
|
|
102
|
+
kind: "single",
|
|
103
|
+
ctx: toolCtx,
|
|
104
|
+
shapeId: handleHit.shapeId,
|
|
105
|
+
handle: handleHit.handle,
|
|
106
|
+
startPoint: { x: event.worldPoint.x, y: event.worldPoint.y },
|
|
107
|
+
}),
|
|
193
108
|
};
|
|
194
109
|
return;
|
|
195
110
|
}
|
|
196
111
|
}
|
|
197
|
-
// 1b.
|
|
112
|
+
// 1b. Multi-selection resize handle
|
|
198
113
|
const selection = toolCtx.store.getSelection();
|
|
199
114
|
if (selection.size > 1) {
|
|
200
115
|
const groupBounds = getMultiSelectionBounds(toolCtx.store, toolCtx.shapes, selection);
|
|
@@ -202,35 +117,16 @@ export const selectToolPlugin = {
|
|
|
202
117
|
const multiHandle = findMultiHandleAtScreenPoint(event.screenPoint, groupBounds, viewport);
|
|
203
118
|
if (multiHandle) {
|
|
204
119
|
setOverrideCursor(getCursorForHandle(multiHandle));
|
|
205
|
-
const startShapeData = new Map();
|
|
206
|
-
const originalFullShapes = new Map();
|
|
207
|
-
for (const id of selection) {
|
|
208
|
-
const shape = toolCtx.store.getShape(id);
|
|
209
|
-
if (shape) {
|
|
210
|
-
originalFullShapes.set(id, { ...shape });
|
|
211
|
-
const def = toolCtx.shapes.get(shape.type);
|
|
212
|
-
const minSize = def?.minSize ?? { width: 1, height: 1 };
|
|
213
|
-
const rel = computeRelativeProps({ x: shape.x, y: shape.y, width: shape.width, height: shape.height }, groupBounds);
|
|
214
|
-
startShapeData.set(id, {
|
|
215
|
-
x: shape.x,
|
|
216
|
-
y: shape.y,
|
|
217
|
-
width: shape.width,
|
|
218
|
-
height: shape.height,
|
|
219
|
-
minWidth: minSize.width,
|
|
220
|
-
minHeight: minSize.height,
|
|
221
|
-
...rel,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
120
|
dragState = {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
121
|
+
kind: "multi-resize",
|
|
122
|
+
session: startResizeSession({
|
|
123
|
+
kind: "multi",
|
|
124
|
+
ctx: toolCtx,
|
|
125
|
+
selection,
|
|
126
|
+
handle: multiHandle,
|
|
127
|
+
startPoint: { x: event.worldPoint.x, y: event.worldPoint.y },
|
|
128
|
+
groupBounds,
|
|
129
|
+
}),
|
|
234
130
|
};
|
|
235
131
|
return;
|
|
236
132
|
}
|
|
@@ -238,11 +134,12 @@ export const selectToolPlugin = {
|
|
|
238
134
|
}
|
|
239
135
|
// 2. Double-click detection for group enter/exit
|
|
240
136
|
const now = Date.now();
|
|
241
|
-
const rawHitId = findShapeAtPoint(toolCtx, event.worldPoint
|
|
137
|
+
const rawHitId = findShapeAtPoint(toolCtx, event.worldPoint, {
|
|
138
|
+
editingGroupId: getEditingGroupId(),
|
|
139
|
+
});
|
|
242
140
|
if (rawHitId && now - lastClickTime < 400 && lastClickId === rawHitId) {
|
|
243
141
|
const shape = toolCtx.store.getShape(rawHitId);
|
|
244
142
|
if (shape?.type === "group" || shape?.type === "frame") {
|
|
245
|
-
// Enter group editing mode
|
|
246
143
|
setEditingGroupId(rawHitId);
|
|
247
144
|
toolCtx.store.clearSelection();
|
|
248
145
|
lastClickTime = 0;
|
|
@@ -263,7 +160,6 @@ export const selectToolPlugin = {
|
|
|
263
160
|
const hitId = rawHitId;
|
|
264
161
|
if (hitId) {
|
|
265
162
|
if (event.shiftKey) {
|
|
266
|
-
// Toggle shape in selection
|
|
267
163
|
if (selection.has(hitId)) {
|
|
268
164
|
toolCtx.store.removeFromSelection(hitId);
|
|
269
165
|
}
|
|
@@ -271,45 +167,24 @@ export const selectToolPlugin = {
|
|
|
271
167
|
toolCtx.store.addToSelection(hitId);
|
|
272
168
|
}
|
|
273
169
|
}
|
|
274
|
-
else {
|
|
275
|
-
|
|
276
|
-
if (!selection.has(hitId)) {
|
|
277
|
-
toolCtx.store.setSelection([hitId]);
|
|
278
|
-
}
|
|
170
|
+
else if (!selection.has(hitId)) {
|
|
171
|
+
toolCtx.store.setSelection([hitId]);
|
|
279
172
|
}
|
|
280
|
-
// Start drag-move for all selected shapes (including children of groups/frames)
|
|
281
173
|
const currentSelection = toolCtx.store.getSelection();
|
|
282
|
-
const startShapeSnapshots = new Map();
|
|
283
|
-
for (const id of currentSelection) {
|
|
284
|
-
const shape = toolCtx.store.getShape(id);
|
|
285
|
-
if (shape) {
|
|
286
|
-
startShapeSnapshots.set(id, { ...shape });
|
|
287
|
-
// Include all descendants of groups/frames/islands so they move together
|
|
288
|
-
if (shape.type === "group" || shape.type === "frame" || shape.type === "island") {
|
|
289
|
-
const queue = [id];
|
|
290
|
-
while (queue.length > 0) {
|
|
291
|
-
const parentId = queue.pop();
|
|
292
|
-
const children = getChildShapes(toolCtx.store, parentId);
|
|
293
|
-
for (const child of children) {
|
|
294
|
-
if (!startShapeSnapshots.has(child.id)) {
|
|
295
|
-
startShapeSnapshots.set(child.id, { ...child });
|
|
296
|
-
// Recurse into nested containers
|
|
297
|
-
if (child.type === "group" ||
|
|
298
|
-
child.type === "frame" ||
|
|
299
|
-
child.type === "island") {
|
|
300
|
-
queue.push(child.id);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
174
|
setMovingSelection(true);
|
|
309
|
-
|
|
310
|
-
|
|
175
|
+
const session = startDragSession({
|
|
176
|
+
ctx: toolCtx,
|
|
311
177
|
startPoint: { x: event.worldPoint.x, y: event.worldPoint.y },
|
|
312
|
-
|
|
178
|
+
shapeIds: currentSelection,
|
|
179
|
+
});
|
|
180
|
+
// Drop-target hit test must exclude every shape the session
|
|
181
|
+
// touches — including descendants of dragged containers — or
|
|
182
|
+
// the container's own children would falsely register as drop
|
|
183
|
+
// targets.
|
|
184
|
+
dragState = {
|
|
185
|
+
kind: "move",
|
|
186
|
+
session,
|
|
187
|
+
movingIds: session.movingShapeIds,
|
|
313
188
|
};
|
|
314
189
|
}
|
|
315
190
|
else {
|
|
@@ -318,338 +193,55 @@ export const selectToolPlugin = {
|
|
|
318
193
|
toolCtx.store.clearSelection();
|
|
319
194
|
}
|
|
320
195
|
dragState = {
|
|
321
|
-
|
|
322
|
-
startWorldPoint: { x: event.worldPoint.x, y: event.worldPoint.y },
|
|
196
|
+
kind: "marquee",
|
|
323
197
|
shiftKey: event.shiftKey,
|
|
198
|
+
session: startMarqueeSession({
|
|
199
|
+
ctx: toolCtx,
|
|
200
|
+
startWorldPoint: { x: event.worldPoint.x, y: event.worldPoint.y },
|
|
201
|
+
editingGroupId: getEditingGroupId(),
|
|
202
|
+
}),
|
|
324
203
|
};
|
|
325
204
|
}
|
|
326
205
|
}
|
|
327
206
|
function onPointerMove(toolCtx, event) {
|
|
328
207
|
if (!dragState) {
|
|
329
|
-
// Hover
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const handleHit = findHandleAtScreenPoint(event.screenPoint, toolCtx.shapes, toolCtx.store, viewport);
|
|
338
|
-
if (handleHit) {
|
|
339
|
-
const hoverShape = toolCtx.store.getShape(handleHit.shapeId);
|
|
340
|
-
const hoverRotation = safeRotation(hoverShape?.rotation);
|
|
341
|
-
setOverrideCursor(hoverRotation
|
|
342
|
-
? getRotatedCursorForHandle(handleHit.handle, hoverRotation)
|
|
343
|
-
: getCursorForHandle(handleHit.handle));
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
// Check multi-selection handles
|
|
347
|
-
const hoverSelection = toolCtx.store.getSelection();
|
|
348
|
-
if (hoverSelection.size > 1) {
|
|
349
|
-
const groupBounds = getMultiSelectionBounds(toolCtx.store, toolCtx.shapes, hoverSelection);
|
|
350
|
-
if (groupBounds) {
|
|
351
|
-
const multiHandle = findMultiHandleAtScreenPoint(event.screenPoint, groupBounds, viewport);
|
|
352
|
-
if (multiHandle) {
|
|
353
|
-
setOverrideCursor(getCursorForHandle(multiHandle));
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
setOverrideCursor("");
|
|
359
|
-
// Hover indicator: detect shape under cursor
|
|
360
|
-
const hoverHitId = findShapeAtPoint(toolCtx, event.worldPoint);
|
|
361
|
-
setHoveredShapeId(hoverHitId);
|
|
208
|
+
// Hover precedence (rotation handle → resize handle → multi-handle
|
|
209
|
+
// → shape body) is handled by the helper. The cursor is mirrored
|
|
210
|
+
// onto our `<style>` injection so it overrides any per-shape cursor.
|
|
211
|
+
const result = trackHover(toolCtx, event, {
|
|
212
|
+
editingGroupId: getEditingGroupId(),
|
|
213
|
+
});
|
|
214
|
+
setOverrideCursor(result.cursor);
|
|
215
|
+
setHoveredShapeId(result.handleHit || result.rotationHit ? null : result.hoveredShapeId);
|
|
362
216
|
return;
|
|
363
217
|
}
|
|
364
|
-
if (dragState.
|
|
365
|
-
|
|
366
|
-
(180 / Math.PI);
|
|
367
|
-
// Wrap angle diff to [-180, 180] to avoid ±360° jumps at atan2 boundary
|
|
368
|
-
let angleDiff = currentAngle - dragState.startAngle;
|
|
369
|
-
angleDiff = ((angleDiff + 540) % 360) - 180;
|
|
370
|
-
let newRotation = dragState.startRotation + angleDiff;
|
|
371
|
-
if (event.shiftKey) {
|
|
372
|
-
newRotation = snapAngle(newRotation, 15);
|
|
373
|
-
}
|
|
374
|
-
newRotation = normalizeAngle(newRotation);
|
|
375
|
-
toolCtx.store.updateShape(dragState.shapeId, { rotation: newRotation });
|
|
376
|
-
// Rotate child shapes around the container center
|
|
377
|
-
const deltaRad = ((newRotation - dragState.startRotation) * Math.PI) / 180;
|
|
378
|
-
const { center } = dragState;
|
|
379
|
-
for (const [childId, snap] of dragState.childSnapshots) {
|
|
380
|
-
const childCx = snap.x + snap.width / 2;
|
|
381
|
-
const childCy = snap.y + snap.height / 2;
|
|
382
|
-
const rx = childCx - center.x;
|
|
383
|
-
const ry = childCy - center.y;
|
|
384
|
-
const cos = Math.cos(deltaRad);
|
|
385
|
-
const sin = Math.sin(deltaRad);
|
|
386
|
-
const newCx = center.x + rx * cos - ry * sin;
|
|
387
|
-
const newCy = center.y + rx * sin + ry * cos;
|
|
388
|
-
const childRotation = normalizeAngle(safeRotation(snap.rotation) + newRotation - dragState.startRotation);
|
|
389
|
-
toolCtx.store.updateShape(childId, {
|
|
390
|
-
x: newCx - snap.width / 2,
|
|
391
|
-
y: newCy - snap.height / 2,
|
|
392
|
-
rotation: childRotation,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
218
|
+
if (dragState.kind === "rotate") {
|
|
219
|
+
dragState.session.update(event);
|
|
395
220
|
return;
|
|
396
221
|
}
|
|
397
|
-
if (dragState.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
// Compute world-space marquee (used for both hit testing and rendering)
|
|
402
|
-
const wx = Math.min(dragState.startWorldPoint.x, event.worldPoint.x);
|
|
403
|
-
const wy = Math.min(dragState.startWorldPoint.y, event.worldPoint.y);
|
|
404
|
-
const ww = Math.abs(event.worldPoint.x - dragState.startWorldPoint.x);
|
|
405
|
-
const wh = Math.abs(event.worldPoint.y - dragState.startWorldPoint.y);
|
|
406
|
-
const worldRect = { x: wx, y: wy, width: ww, height: wh };
|
|
407
|
-
const hitIds = findShapesInRect(toolCtx, worldRect, mode);
|
|
408
|
-
setMarquee(worldRect, hitIds);
|
|
222
|
+
if (dragState.kind === "marquee") {
|
|
223
|
+
const u = dragState.session.update(event);
|
|
224
|
+
setMarqueeMode(u.mode);
|
|
225
|
+
setMarquee(u.rect, [...u.hitIds]);
|
|
409
226
|
return;
|
|
410
227
|
}
|
|
411
|
-
if (dragState.
|
|
412
|
-
const
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
x: event.worldPoint.x - dragState.startPoint.x,
|
|
418
|
-
y: event.worldPoint.y - dragState.startPoint.y,
|
|
419
|
-
};
|
|
420
|
-
// Transform delta to local (un-rotated) space for rotated shapes
|
|
421
|
-
const delta = rotation ? deltaToLocal(worldDelta, rotation) : worldDelta;
|
|
422
|
-
// Flip detection: use unclamped (raw) bounds so minSize doesn't
|
|
423
|
-
// prevent the pointer from crossing the anchor edge.
|
|
424
|
-
const rawBounds = computeRawBounds(dragState.startData, dragState.handle, delta);
|
|
425
|
-
// Un-rotate the world point for flip detection in local space
|
|
426
|
-
let worldPointForFlip = event.worldPoint;
|
|
427
|
-
if (rotation) {
|
|
428
|
-
// Use rawBounds center (not startData) since resize changes x/y/width/height
|
|
429
|
-
const center = {
|
|
430
|
-
x: rawBounds.x + rawBounds.width / 2,
|
|
431
|
-
y: rawBounds.y + rawBounds.height / 2,
|
|
432
|
-
};
|
|
433
|
-
worldPointForFlip = unrotatePoint(event.worldPoint, center, (rotation * Math.PI) / 180);
|
|
434
|
-
}
|
|
435
|
-
const flip = applyFlip(dragState.handle, rawBounds, worldPointForFlip);
|
|
436
|
-
if (flip.flipped) {
|
|
437
|
-
const currentShape = toolCtx.store.getShape(dragState.shapeId);
|
|
438
|
-
if (currentShape) {
|
|
439
|
-
const anchor = getAnchorEdges(dragState.handle, rawBounds);
|
|
440
|
-
// Reset to zero-size shape at anchor position.
|
|
441
|
-
// This ensures the flipped handle's anchor is at the
|
|
442
|
-
// pointer's crossing point, preventing immediate re-flip.
|
|
443
|
-
const flippedData = { ...currentShape };
|
|
444
|
-
if (flip.flippedX && anchor.x !== undefined) {
|
|
445
|
-
flippedData.x = anchor.x;
|
|
446
|
-
flippedData.width = 0;
|
|
447
|
-
}
|
|
448
|
-
if (flip.flippedY && anchor.y !== undefined) {
|
|
449
|
-
flippedData.y = anchor.y;
|
|
450
|
-
flippedData.height = 0;
|
|
451
|
-
}
|
|
452
|
-
dragState = {
|
|
453
|
-
...dragState,
|
|
454
|
-
handle: flip.handle,
|
|
455
|
-
startData: flippedData,
|
|
456
|
-
startPoint: { x: event.worldPoint.x, y: event.worldPoint.y },
|
|
457
|
-
};
|
|
458
|
-
// Update store — def.resize will expand from zero on next frame
|
|
459
|
-
toolCtx.store.updateShape(dragState.shapeId, {
|
|
460
|
-
x: flippedData.x,
|
|
461
|
-
y: flippedData.y,
|
|
462
|
-
});
|
|
463
|
-
setOverrideCursor(rotation
|
|
464
|
-
? getRotatedCursorForHandle(flip.handle, rotation)
|
|
465
|
-
: getCursorForHandle(flip.handle));
|
|
466
|
-
}
|
|
467
|
-
// Skip resize this frame — next frame will use the new
|
|
468
|
-
// startData/startPoint with the flipped handle.
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
const resized = def.resize(dragState.startData, dragState.handle, delta);
|
|
472
|
-
// Fix anchor drift from minSize clamping
|
|
473
|
-
const fixed = fixAnchorDrift(dragState.handle, dragState.startData, resized);
|
|
474
|
-
resized.x = fixed.x;
|
|
475
|
-
resized.y = fixed.y;
|
|
476
|
-
const updates = {};
|
|
477
|
-
for (const key of Object.keys(resized)) {
|
|
478
|
-
if (key === "id" || key === "type" || key === "style")
|
|
479
|
-
continue;
|
|
480
|
-
const resizedValue = resized[key];
|
|
481
|
-
const startValue = dragState.startData[key];
|
|
482
|
-
if (resizedValue !== startValue) {
|
|
483
|
-
updates[key] = resizedValue;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
if (Object.keys(updates).length > 0) {
|
|
487
|
-
toolCtx.store.updateShape(dragState.shapeId, updates);
|
|
228
|
+
if (dragState.kind === "resize" || dragState.kind === "multi-resize") {
|
|
229
|
+
const u = dragState.session.update(event);
|
|
230
|
+
if (u.flippedHandle) {
|
|
231
|
+
setOverrideCursor(dragState.kind === "resize" && dragState.rotation
|
|
232
|
+
? getRotatedCursorForHandle(u.flippedHandle, dragState.rotation)
|
|
233
|
+
: getCursorForHandle(u.flippedHandle));
|
|
488
234
|
}
|
|
489
235
|
return;
|
|
490
236
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
y: event.worldPoint.y - dragState.startPoint.y,
|
|
495
|
-
};
|
|
496
|
-
// Flip detection on the group bounding box
|
|
497
|
-
const rawGroupBounds = computeRawBounds(dragState.startGroupBounds, dragState.handle, delta);
|
|
498
|
-
const flip = applyFlip(dragState.handle, rawGroupBounds, event.worldPoint);
|
|
499
|
-
if (flip.flipped) {
|
|
500
|
-
const anchor = getAnchorEdges(dragState.handle, rawGroupBounds);
|
|
501
|
-
// Reset group bounds to zero-size at anchor position (same approach as single resize)
|
|
502
|
-
const flippedGroupBounds = { ...dragState.startGroupBounds };
|
|
503
|
-
if (flip.flippedX && anchor.x !== undefined) {
|
|
504
|
-
flippedGroupBounds.x = anchor.x;
|
|
505
|
-
flippedGroupBounds.width = 0;
|
|
506
|
-
}
|
|
507
|
-
if (flip.flippedY && anchor.y !== undefined) {
|
|
508
|
-
flippedGroupBounds.y = anchor.y;
|
|
509
|
-
flippedGroupBounds.height = 0;
|
|
510
|
-
}
|
|
511
|
-
// Mirror relative ratios and reset only the flipped axis
|
|
512
|
-
const flippedShapeData = new Map();
|
|
513
|
-
for (const [id, data] of dragState.startShapeData) {
|
|
514
|
-
const newRel = { ...data };
|
|
515
|
-
if (flip.flippedX) {
|
|
516
|
-
newRel.relX = 1 - data.relX - data.relWidth;
|
|
517
|
-
newRel.x = flippedGroupBounds.x;
|
|
518
|
-
newRel.width = 0;
|
|
519
|
-
}
|
|
520
|
-
if (flip.flippedY) {
|
|
521
|
-
newRel.relY = 1 - data.relY - data.relHeight;
|
|
522
|
-
newRel.y = flippedGroupBounds.y;
|
|
523
|
-
newRel.height = 0;
|
|
524
|
-
}
|
|
525
|
-
flippedShapeData.set(id, newRel);
|
|
526
|
-
}
|
|
527
|
-
// Temporarily disable snap during flip-reset to prevent
|
|
528
|
-
// snap from altering the zero-size anchor positioning
|
|
529
|
-
toolCtx.events.emit("snap:configure", { enabled: false });
|
|
530
|
-
for (const [id, data] of flippedShapeData) {
|
|
531
|
-
const update = {};
|
|
532
|
-
if (flip.flippedX) {
|
|
533
|
-
update.x = data.x;
|
|
534
|
-
update.width = 0;
|
|
535
|
-
}
|
|
536
|
-
if (flip.flippedY) {
|
|
537
|
-
update.y = data.y;
|
|
538
|
-
update.height = 0;
|
|
539
|
-
}
|
|
540
|
-
toolCtx.store.updateShape(id, update);
|
|
541
|
-
}
|
|
542
|
-
toolCtx.events.emit("snap:configure", { enabled: true });
|
|
543
|
-
// Snapshot current shapes as new base for applyBounds
|
|
544
|
-
const flippedApplyBoundsBase = new Map();
|
|
545
|
-
for (const id of flippedShapeData.keys()) {
|
|
546
|
-
const current = toolCtx.store.getShape(id);
|
|
547
|
-
if (current)
|
|
548
|
-
flippedApplyBoundsBase.set(id, { ...current });
|
|
549
|
-
}
|
|
550
|
-
dragState = {
|
|
551
|
-
...dragState,
|
|
552
|
-
handle: flip.handle,
|
|
553
|
-
startPoint: { x: event.worldPoint.x, y: event.worldPoint.y },
|
|
554
|
-
startGroupBounds: flippedGroupBounds,
|
|
555
|
-
startShapeData: flippedShapeData,
|
|
556
|
-
applyBoundsBase: flippedApplyBoundsBase,
|
|
557
|
-
// originalShapeData/originalFullShapes preserved for undo
|
|
558
|
-
};
|
|
559
|
-
setOverrideCursor(getCursorForHandle(flip.handle));
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
const multiUpdates = computeMultiResizeUpdates(dragState.handle, dragState.startGroupBounds, delta, dragState.startShapeData);
|
|
563
|
-
for (const [id, upd] of multiUpdates) {
|
|
564
|
-
const baseShape = dragState.applyBoundsBase.get(id);
|
|
565
|
-
const def = baseShape ? toolCtx.shapes.get(baseShape.type) : undefined;
|
|
566
|
-
if (def?.applyBounds && baseShape) {
|
|
567
|
-
// Pass {x,y,width,height} first so snap plugin can adjust
|
|
568
|
-
toolCtx.store.updateShape(id, upd);
|
|
569
|
-
// Read back snapped bounds and derive geometry
|
|
570
|
-
const snapped = toolCtx.store.getShape(id);
|
|
571
|
-
if (snapped) {
|
|
572
|
-
const snappedBounds = {
|
|
573
|
-
x: snapped.x,
|
|
574
|
-
y: snapped.y,
|
|
575
|
-
width: snapped.width,
|
|
576
|
-
height: snapped.height,
|
|
577
|
-
};
|
|
578
|
-
const geom = def.applyBounds(baseShape, snappedBounds);
|
|
579
|
-
const { x: _x, y: _y, width: _w, height: _h, ...rest } = geom;
|
|
580
|
-
if (Object.keys(rest).length > 0) {
|
|
581
|
-
toolCtx.store.updateShape(id, rest);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
else {
|
|
586
|
-
toolCtx.store.updateShape(id, upd);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
// mode === "move"
|
|
592
|
-
const dx = event.worldPoint.x - dragState.startPoint.x;
|
|
593
|
-
const dy = event.worldPoint.y - dragState.startPoint.y;
|
|
594
|
-
const movingIds = new Set(dragState.startShapeSnapshots.keys());
|
|
595
|
-
const currentSelection = toolCtx.store.getSelection();
|
|
596
|
-
// Phase 1: Update selected (top-level) shapes first so snap adjusts them.
|
|
597
|
-
// Track the snap delta so children can follow the same offset.
|
|
598
|
-
let snapDx = 0;
|
|
599
|
-
let snapDy = 0;
|
|
600
|
-
for (const [id, snapshot] of dragState.startShapeSnapshots) {
|
|
601
|
-
if (!currentSelection.has(id))
|
|
602
|
-
continue;
|
|
603
|
-
toolCtx.store.updateShape(id, {
|
|
604
|
-
x: snapshot.x + dx,
|
|
605
|
-
y: snapshot.y + dy,
|
|
606
|
-
});
|
|
607
|
-
const snapped = toolCtx.store.getShape(id);
|
|
608
|
-
if (snapped) {
|
|
609
|
-
snapDx = snapped.x - (snapshot.x + dx);
|
|
610
|
-
snapDy = snapped.y - (snapshot.y + dy);
|
|
611
|
-
}
|
|
612
|
-
const def = toolCtx.shapes.get(snapshot.type);
|
|
613
|
-
if (def?.move) {
|
|
614
|
-
if (snapped) {
|
|
615
|
-
const snappedDx = snapped.x - snapshot.x;
|
|
616
|
-
const snappedDy = snapped.y - snapshot.y;
|
|
617
|
-
const geom = def.move(snapshot, snappedDx, snappedDy);
|
|
618
|
-
const { x: _x, y: _y, ...rest } = geom;
|
|
619
|
-
if (Object.keys(rest).length > 0) {
|
|
620
|
-
toolCtx.store.updateShape(id, rest);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
// Phase 2: Update child shapes with the same snap delta as their parent.
|
|
626
|
-
for (const [id, snapshot] of dragState.startShapeSnapshots) {
|
|
627
|
-
if (currentSelection.has(id))
|
|
628
|
-
continue;
|
|
629
|
-
toolCtx.store.updateShape(id, {
|
|
630
|
-
x: snapshot.x + dx + snapDx,
|
|
631
|
-
y: snapshot.y + dy + snapDy,
|
|
632
|
-
});
|
|
633
|
-
const def = toolCtx.shapes.get(snapshot.type);
|
|
634
|
-
if (def?.move) {
|
|
635
|
-
const snapped = toolCtx.store.getShape(id);
|
|
636
|
-
if (snapped) {
|
|
637
|
-
const snappedDx = snapped.x - snapshot.x;
|
|
638
|
-
const snappedDy = snapped.y - snapshot.y;
|
|
639
|
-
const geom = def.move(snapshot, snappedDx, snappedDy);
|
|
640
|
-
const { x: _x, y: _y, ...rest } = geom;
|
|
641
|
-
if (Object.keys(rest).length > 0) {
|
|
642
|
-
toolCtx.store.updateShape(id, rest);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
// Drop target: find frame/group under the cursor (excluding dragged shapes)
|
|
237
|
+
// kind === "move"
|
|
238
|
+
dragState.session.update(event);
|
|
239
|
+
// Drop target: find frame/group under the cursor (excluding dragged shapes).
|
|
648
240
|
const allShapes = toolCtx.store.getShapes();
|
|
649
241
|
let dropTarget = null;
|
|
650
242
|
const entries = [...allShapes.entries()].reverse();
|
|
651
243
|
for (const [id, shape] of entries) {
|
|
652
|
-
if (movingIds.has(id))
|
|
244
|
+
if (dragState.movingIds.has(id))
|
|
653
245
|
continue;
|
|
654
246
|
if (shape.type !== "frame" && shape.type !== "group")
|
|
655
247
|
continue;
|
|
@@ -672,180 +264,47 @@ export const selectToolPlugin = {
|
|
|
672
264
|
updateDropTarget(null);
|
|
673
265
|
if (!dragState)
|
|
674
266
|
return;
|
|
675
|
-
if (dragState.
|
|
267
|
+
if (dragState.kind === "rotate") {
|
|
676
268
|
setOverrideCursor("");
|
|
677
|
-
const
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
const parentTo = { rotation: safeRotation(currentShape.rotation) };
|
|
681
|
-
const hasMoved = parentFrom.rotation !== parentTo.rotation;
|
|
682
|
-
if (hasMoved) {
|
|
683
|
-
// Build before/after snapshots for parent + children
|
|
684
|
-
const beforeSnapshots = new Map();
|
|
685
|
-
const afterSnapshots = new Map();
|
|
686
|
-
const shapeId = dragState.shapeId;
|
|
687
|
-
beforeSnapshots.set(shapeId, { rotation: parentFrom.rotation });
|
|
688
|
-
afterSnapshots.set(shapeId, { rotation: parentTo.rotation });
|
|
689
|
-
for (const [childId, snap] of dragState.childSnapshots) {
|
|
690
|
-
const current = toolCtx.store.getShape(childId);
|
|
691
|
-
if (current) {
|
|
692
|
-
beforeSnapshots.set(childId, {
|
|
693
|
-
x: snap.x,
|
|
694
|
-
y: snap.y,
|
|
695
|
-
rotation: safeRotation(snap.rotation),
|
|
696
|
-
});
|
|
697
|
-
afterSnapshots.set(childId, {
|
|
698
|
-
x: current.x,
|
|
699
|
-
y: current.y,
|
|
700
|
-
rotation: safeRotation(current.rotation),
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
queueMicrotask(() => {
|
|
705
|
-
// Revert to before state
|
|
706
|
-
for (const [id, props] of beforeSnapshots) {
|
|
707
|
-
toolCtx.store.updateShape(id, props);
|
|
708
|
-
}
|
|
709
|
-
// Execute undoable command
|
|
710
|
-
toolCtx.commands.execute({
|
|
711
|
-
execute: () => {
|
|
712
|
-
for (const [id, props] of afterSnapshots) {
|
|
713
|
-
toolCtx.store.updateShape(id, props);
|
|
714
|
-
}
|
|
715
|
-
},
|
|
716
|
-
undo: () => {
|
|
717
|
-
for (const [id, props] of beforeSnapshots) {
|
|
718
|
-
toolCtx.store.updateShape(id, props);
|
|
719
|
-
}
|
|
720
|
-
},
|
|
721
|
-
});
|
|
722
|
-
});
|
|
723
|
-
}
|
|
269
|
+
const result = dragState.session.commit();
|
|
270
|
+
if (result) {
|
|
271
|
+
queueMicrotask(() => toolCtx.commands.execute(result.command));
|
|
724
272
|
}
|
|
725
273
|
dragState = null;
|
|
726
274
|
return;
|
|
727
275
|
}
|
|
728
|
-
if (dragState.
|
|
276
|
+
if (dragState.kind === "marquee") {
|
|
729
277
|
setMarquee(null);
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const mx = Math.min(sx, wx1);
|
|
736
|
-
const my = Math.min(sy, wy1);
|
|
737
|
-
const mw = Math.abs(wx1 - sx);
|
|
738
|
-
const mh = Math.abs(wy1 - sy);
|
|
739
|
-
// Skip tiny marquees (accidental clicks)
|
|
740
|
-
if (mw < 2 && mh < 2) {
|
|
741
|
-
dragState = null;
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
const mode = _event.altKey ? "contain" : "intersect";
|
|
745
|
-
const marqueeBox = { x: mx, y: my, width: mw, height: mh };
|
|
746
|
-
const hitIds = findShapesInRect(toolCtx, marqueeBox, mode);
|
|
747
|
-
if (dragState.shiftKey) {
|
|
748
|
-
for (const id of hitIds) {
|
|
749
|
-
toolCtx.store.addToSelection(id);
|
|
278
|
+
const result = dragState.session.commit();
|
|
279
|
+
if (result) {
|
|
280
|
+
if (dragState.shiftKey) {
|
|
281
|
+
for (const id of result.selection)
|
|
282
|
+
toolCtx.store.addToSelection(id);
|
|
750
283
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
toolCtx.store.setSelection(hitIds);
|
|
754
|
-
}
|
|
755
|
-
dragState = null;
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
if (dragState.mode === "resize") {
|
|
759
|
-
setOverrideCursor("");
|
|
760
|
-
const currentShape = toolCtx.store.getShape(dragState.shapeId);
|
|
761
|
-
if (currentShape) {
|
|
762
|
-
// Build from/to diffs for undo
|
|
763
|
-
const from = {};
|
|
764
|
-
const to = {};
|
|
765
|
-
for (const key of Object.keys(currentShape)) {
|
|
766
|
-
if (key === "id" || key === "type" || key === "style")
|
|
767
|
-
continue;
|
|
768
|
-
const currentValue = currentShape[key];
|
|
769
|
-
const startValue = dragState.startData[key];
|
|
770
|
-
if (currentValue !== startValue) {
|
|
771
|
-
from[key] = startValue;
|
|
772
|
-
to[key] = currentValue;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
if (Object.keys(to).length > 0) {
|
|
776
|
-
// Defer reset+execute to after canvas:pointerup disables snap
|
|
777
|
-
const shapeId = dragState.shapeId;
|
|
778
|
-
queueMicrotask(() => {
|
|
779
|
-
toolCtx.store.updateShape(shapeId, from);
|
|
780
|
-
toolCtx.commands.execute(createUpdateShapeCommand(toolCtx.store, shapeId, from, to));
|
|
781
|
-
});
|
|
284
|
+
else {
|
|
285
|
+
toolCtx.store.setSelection([...result.selection]);
|
|
782
286
|
}
|
|
783
287
|
}
|
|
784
288
|
dragState = null;
|
|
785
289
|
return;
|
|
786
290
|
}
|
|
787
|
-
if (dragState.
|
|
291
|
+
if (dragState.kind === "resize" || dragState.kind === "multi-resize") {
|
|
788
292
|
setOverrideCursor("");
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (!currentShape)
|
|
793
|
-
continue;
|
|
794
|
-
const from = {};
|
|
795
|
-
const to = {};
|
|
796
|
-
for (const key of Object.keys(currentShape)) {
|
|
797
|
-
if (key === "id" || key === "type" || key === "style")
|
|
798
|
-
continue;
|
|
799
|
-
const currentValue = currentShape[key];
|
|
800
|
-
const origValue = origFullShape[key];
|
|
801
|
-
if (currentValue !== origValue) {
|
|
802
|
-
from[key] = origValue;
|
|
803
|
-
to[key] = currentValue;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
if (Object.keys(to).length > 0) {
|
|
807
|
-
batchUpdates.push({ id, from, to });
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
if (batchUpdates.length > 0) {
|
|
811
|
-
// Defer reset+execute to after canvas:pointerup disables snap
|
|
812
|
-
const storeRef = toolCtx.store;
|
|
813
|
-
queueMicrotask(() => {
|
|
814
|
-
for (const { id, from } of batchUpdates) {
|
|
815
|
-
storeRef.updateShape(id, from);
|
|
816
|
-
}
|
|
817
|
-
toolCtx.commands.execute(createBatchUpdateShapesCommand(storeRef, batchUpdates));
|
|
818
|
-
});
|
|
293
|
+
const result = dragState.session.commit();
|
|
294
|
+
if (result) {
|
|
295
|
+
queueMicrotask(() => toolCtx.commands.execute(result.command));
|
|
819
296
|
}
|
|
820
297
|
dragState = null;
|
|
821
298
|
return;
|
|
822
299
|
}
|
|
823
|
-
//
|
|
300
|
+
// kind === "move"
|
|
824
301
|
setMovingSelection(false);
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
let hasMoved = false;
|
|
829
|
-
for (const [id, before] of beforeSnapshots) {
|
|
830
|
-
const current = toolCtx.store.getShape(id);
|
|
831
|
-
if (current) {
|
|
832
|
-
afterSnapshots.set(id, { ...current });
|
|
833
|
-
if (Math.abs(current.x - before.x) > 0.5 || Math.abs(current.y - before.y) > 0.5) {
|
|
834
|
-
hasMoved = true;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
if (hasMoved) {
|
|
839
|
-
// Collect the user-selected shapes (not their implicitly-moved children)
|
|
840
|
-
const userSelectedIds = [...toolCtx.store.getSelection()];
|
|
841
|
-
// Defer reset+execute to after canvas:pointerup disables snap,
|
|
842
|
-
// then notify plugins for auto-parenting
|
|
302
|
+
const userSelectedIds = [...toolCtx.store.getSelection()];
|
|
303
|
+
const result = dragState.session.commit();
|
|
304
|
+
if (result) {
|
|
843
305
|
queueMicrotask(() => {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
847
|
-
toolCtx.commands.execute(createMoveShapesCommand(toolCtx.store, beforeSnapshots, afterSnapshots));
|
|
848
|
-
// Notify after the move command is committed so reparent won't be undone
|
|
306
|
+
toolCtx.commands.execute(result.command);
|
|
307
|
+
// Notify after the move command is committed so reparent won't be undone.
|
|
849
308
|
toolCtx.events.emit("shapes:move-end", { shapeIds: userSelectedIds });
|
|
850
309
|
});
|
|
851
310
|
}
|
|
@@ -871,9 +330,12 @@ export const selectToolPlugin = {
|
|
|
871
330
|
onPointerUp,
|
|
872
331
|
onDeactivate,
|
|
873
332
|
});
|
|
874
|
-
// ── Selection
|
|
875
|
-
|
|
876
|
-
|
|
333
|
+
// ── Selection foreground (default) ──
|
|
334
|
+
// Registered at priority 0 so any app option or third-party plugin
|
|
335
|
+
// (recommended: 50) replaces it. See guides/selection-foreground.
|
|
336
|
+
const unregisterSelectionForeground = ctx.ui.registerSelectionForeground({
|
|
337
|
+
id: "tool-select-default",
|
|
338
|
+
priority: 0,
|
|
877
339
|
order: 80,
|
|
878
340
|
fixed: true,
|
|
879
341
|
render: (renderCtx) => (_jsx(SelectionOverlay, { store: ctx.store, shapes: ctx.shapes, viewport: renderCtx.viewport })),
|
|
@@ -895,7 +357,7 @@ export const selectToolPlugin = {
|
|
|
895
357
|
clearHoveredShapeListeners();
|
|
896
358
|
clearDropTargetListeners();
|
|
897
359
|
styleEl.remove();
|
|
898
|
-
|
|
360
|
+
unregisterSelectionForeground();
|
|
899
361
|
};
|
|
900
362
|
},
|
|
901
363
|
};
|