@hyperframes/studio 0.5.0-alpha.14 → 0.5.0-alpha.15
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/assets/{hyperframes-player-vibA20NC.js → hyperframes-player-Cd8vYWxP.js} +2 -2
- package/dist/assets/index-DFLVGWTx.js +106 -0
- package/dist/assets/index-mXJ-UH9F.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +785 -377
- package/src/captions/generator.test.ts +19 -0
- package/src/captions/generator.ts +9 -2
- package/src/captions/hooks/useCaptionSync.ts +6 -1
- package/src/captions/parser.test.ts +14 -0
- package/src/captions/parser.ts +1 -0
- package/src/components/editor/DomEditOverlay.test.ts +241 -0
- package/src/components/editor/DomEditOverlay.tsx +970 -115
- package/src/components/editor/PropertyPanel.tsx +91 -83
- package/src/components/editor/domEditing.test.ts +161 -29
- package/src/components/editor/domEditing.ts +84 -113
- package/src/components/editor/manualEdits.test.ts +945 -0
- package/src/components/editor/manualEdits.ts +1397 -0
- package/src/components/editor/manualOffsetDrag.test.ts +140 -0
- package/src/components/editor/manualOffsetDrag.ts +307 -0
- package/src/components/renders/RenderQueue.tsx +10 -3
- package/src/hooks/usePersistentEditHistory.test.ts +1 -0
- package/src/hooks/usePersistentEditHistory.ts +3 -2
- package/src/player/components/CompositionThumbnail.test.ts +1 -1
- package/src/player/components/CompositionThumbnail.tsx +1 -1
- package/src/player/components/Player.tsx +54 -9
- package/src/player/hooks/useTimelinePlayer.test.ts +1 -0
- package/src/utils/clipboard.test.ts +1 -0
- package/src/utils/frameCapture.ts +3 -1
- package/src/utils/projectRouting.test.ts +87 -0
- package/src/utils/projectRouting.ts +27 -0
- package/dist/assets/index-JhhmFie-.js +0 -105
- package/dist/assets/index-KioPDrX6.css +0 -1
|
@@ -1,35 +1,88 @@
|
|
|
1
1
|
import { memo, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
2
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
3
|
import { type DomEditSelection, findElementForSelection } from "./domEditing";
|
|
4
|
+
import {
|
|
5
|
+
applyManualOffsetDragCommit,
|
|
6
|
+
applyManualOffsetDragDraft,
|
|
7
|
+
createManualOffsetDragMember,
|
|
8
|
+
endManualOffsetDragMembers,
|
|
9
|
+
restoreManualOffsetDragMembers,
|
|
10
|
+
type ManualOffsetDragMember,
|
|
11
|
+
} from "./manualOffsetDrag";
|
|
12
|
+
import {
|
|
13
|
+
applyStudioBoxSize,
|
|
14
|
+
applyStudioBoxSizeDraft,
|
|
15
|
+
applyStudioRotation,
|
|
16
|
+
applyStudioRotationDraft,
|
|
17
|
+
beginStudioManualEditGesture,
|
|
18
|
+
captureStudioBoxSize,
|
|
19
|
+
captureStudioPathOffset,
|
|
20
|
+
captureStudioRotation,
|
|
21
|
+
endStudioManualEditGesture,
|
|
22
|
+
isStudioManualEditGestureCurrent,
|
|
23
|
+
readStudioBoxSize,
|
|
24
|
+
readStudioRotation,
|
|
25
|
+
restoreStudioBoxSize,
|
|
26
|
+
restoreStudioPathOffset,
|
|
27
|
+
restoreStudioRotation,
|
|
28
|
+
type StudioBoxSizeSnapshot,
|
|
29
|
+
type StudioPathOffsetSnapshot,
|
|
30
|
+
type StudioRotationSnapshot,
|
|
31
|
+
} from "./manualEdits";
|
|
4
32
|
|
|
5
33
|
interface OverlayRect {
|
|
6
34
|
left: number;
|
|
7
35
|
top: number;
|
|
8
36
|
width: number;
|
|
9
37
|
height: number;
|
|
10
|
-
|
|
11
|
-
|
|
38
|
+
editScaleX: number;
|
|
39
|
+
editScaleY: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface GroupOverlayItem {
|
|
43
|
+
key: string;
|
|
44
|
+
selection: DomEditSelection;
|
|
45
|
+
element: HTMLElement;
|
|
46
|
+
rect: OverlayRect;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DomEditGroupPathOffsetCommit {
|
|
50
|
+
selection: DomEditSelection;
|
|
51
|
+
next: { x: number; y: number };
|
|
12
52
|
}
|
|
13
53
|
|
|
14
54
|
interface DomEditOverlayProps {
|
|
15
55
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
56
|
+
activeCompositionPath: string | null;
|
|
16
57
|
selection: DomEditSelection | null;
|
|
58
|
+
groupSelections?: DomEditSelection[];
|
|
59
|
+
hoverSelection: DomEditSelection | null;
|
|
17
60
|
allowCanvasMovement?: boolean;
|
|
18
61
|
onCanvasMouseDown: (
|
|
19
62
|
event: React.MouseEvent<HTMLDivElement>,
|
|
20
63
|
options?: { preferClipAncestor?: boolean },
|
|
21
64
|
) => void;
|
|
22
|
-
|
|
23
|
-
|
|
65
|
+
onCanvasPointerMove: (
|
|
66
|
+
event: React.PointerEvent<HTMLDivElement>,
|
|
67
|
+
options?: { preferClipAncestor?: boolean },
|
|
68
|
+
) => DomEditSelection | null;
|
|
69
|
+
onCanvasPointerLeave: () => void;
|
|
70
|
+
onSelectionChange: (
|
|
71
|
+
selection: DomEditSelection,
|
|
72
|
+
options?: { revealPanel?: boolean; additive?: boolean },
|
|
73
|
+
) => void;
|
|
24
74
|
onBlockedMove: (selection: DomEditSelection) => void;
|
|
25
|
-
|
|
75
|
+
onManualDragStart?: () => void;
|
|
76
|
+
onPathOffsetCommit: (
|
|
26
77
|
selection: DomEditSelection,
|
|
27
|
-
next: {
|
|
78
|
+
next: { x: number; y: number },
|
|
28
79
|
) => Promise<void> | void;
|
|
29
|
-
|
|
80
|
+
onGroupPathOffsetCommit: (updates: DomEditGroupPathOffsetCommit[]) => Promise<void> | void;
|
|
81
|
+
onBoxSizeCommit: (
|
|
30
82
|
selection: DomEditSelection,
|
|
31
83
|
next: { width: number; height: number },
|
|
32
84
|
) => Promise<void> | void;
|
|
85
|
+
onRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void> | void;
|
|
33
86
|
}
|
|
34
87
|
|
|
35
88
|
function toOverlayRect(
|
|
@@ -48,22 +101,82 @@ function toOverlayRect(
|
|
|
48
101
|
if (!rootWidth || !rootHeight) return null;
|
|
49
102
|
|
|
50
103
|
const elementRect = element.getBoundingClientRect();
|
|
51
|
-
const
|
|
52
|
-
const
|
|
104
|
+
const rootScaleX = iframeRect.width / rootWidth;
|
|
105
|
+
const rootScaleY = iframeRect.height / rootHeight;
|
|
106
|
+
const sourceBoundary = findSourceBoundary(element);
|
|
107
|
+
const sourceBoundaryRect = sourceBoundary?.getBoundingClientRect();
|
|
108
|
+
const editScale = resolveDomEditCoordinateScale({
|
|
109
|
+
rootScaleX,
|
|
110
|
+
rootScaleY,
|
|
111
|
+
sourceRectWidth: sourceBoundaryRect?.width,
|
|
112
|
+
sourceRectHeight: sourceBoundaryRect?.height,
|
|
113
|
+
sourceWidth: readPositiveDimension(sourceBoundary?.getAttribute("data-width") ?? null),
|
|
114
|
+
sourceHeight: readPositiveDimension(sourceBoundary?.getAttribute("data-height") ?? null),
|
|
115
|
+
});
|
|
53
116
|
|
|
54
117
|
return {
|
|
55
|
-
left: iframeRect.left - overlayRect.left + elementRect.left *
|
|
56
|
-
top: iframeRect.top - overlayRect.top + elementRect.top *
|
|
57
|
-
width: elementRect.width *
|
|
58
|
-
height: elementRect.height *
|
|
59
|
-
scaleX,
|
|
60
|
-
scaleY,
|
|
118
|
+
left: iframeRect.left - overlayRect.left + (elementRect.left - rootRect.left) * rootScaleX,
|
|
119
|
+
top: iframeRect.top - overlayRect.top + (elementRect.top - rootRect.top) * rootScaleY,
|
|
120
|
+
width: elementRect.width * rootScaleX,
|
|
121
|
+
height: elementRect.height * rootScaleY,
|
|
122
|
+
editScaleX: editScale.scaleX,
|
|
123
|
+
editScaleY: editScale.scaleY,
|
|
61
124
|
};
|
|
62
125
|
}
|
|
63
126
|
|
|
64
|
-
|
|
127
|
+
function readPositiveDimension(value: string | null): number | null {
|
|
128
|
+
if (!value) return null;
|
|
129
|
+
const parsed = Number.parseFloat(value);
|
|
130
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findSourceBoundary(element: HTMLElement): HTMLElement | null {
|
|
134
|
+
let current: HTMLElement | null = element;
|
|
135
|
+
while (current) {
|
|
136
|
+
if (
|
|
137
|
+
current.hasAttribute("data-composition-file") ||
|
|
138
|
+
current.hasAttribute("data-composition-src")
|
|
139
|
+
) {
|
|
140
|
+
return current;
|
|
141
|
+
}
|
|
142
|
+
current = current.parentElement;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resolveDomEditCoordinateScale(input: {
|
|
148
|
+
rootScaleX: number;
|
|
149
|
+
rootScaleY: number;
|
|
150
|
+
sourceRectWidth?: number;
|
|
151
|
+
sourceRectHeight?: number;
|
|
152
|
+
sourceWidth?: number | null;
|
|
153
|
+
sourceHeight?: number | null;
|
|
154
|
+
}): { scaleX: number; scaleY: number } {
|
|
155
|
+
const rootScaleX = input.rootScaleX > 0 ? input.rootScaleX : 1;
|
|
156
|
+
const rootScaleY = input.rootScaleY > 0 ? input.rootScaleY : 1;
|
|
157
|
+
const sourceScaleX =
|
|
158
|
+
input.sourceRectWidth && input.sourceRectWidth > 0 && input.sourceWidth && input.sourceWidth > 0
|
|
159
|
+
? (input.sourceRectWidth * rootScaleX) / input.sourceWidth
|
|
160
|
+
: rootScaleX;
|
|
161
|
+
const sourceScaleY =
|
|
162
|
+
input.sourceRectHeight &&
|
|
163
|
+
input.sourceRectHeight > 0 &&
|
|
164
|
+
input.sourceHeight &&
|
|
165
|
+
input.sourceHeight > 0
|
|
166
|
+
? (input.sourceRectHeight * rootScaleY) / input.sourceHeight
|
|
167
|
+
: rootScaleY;
|
|
168
|
+
return {
|
|
169
|
+
scaleX: sourceScaleX > 0 ? sourceScaleX : rootScaleX,
|
|
170
|
+
scaleY: sourceScaleY > 0 ? sourceScaleY : rootScaleY,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type GestureKind = "drag" | "resize" | "rotate";
|
|
65
175
|
const BLOCKED_MOVE_THRESHOLD_PX = 4;
|
|
176
|
+
const MIN_RESIZE_EDGE_PX = 20;
|
|
66
177
|
const OVERLAY_RECT_EPSILON_PX = 0.5;
|
|
178
|
+
const ROTATION_COMMIT_EPSILON_DEGREES = 0.05;
|
|
179
|
+
const ROTATION_SNAP_DEGREES = 15;
|
|
67
180
|
|
|
68
181
|
function rectsEqual(a: OverlayRect | null, b: OverlayRect | null): boolean {
|
|
69
182
|
if (a === b) return true;
|
|
@@ -73,8 +186,55 @@ function rectsEqual(a: OverlayRect | null, b: OverlayRect | null): boolean {
|
|
|
73
186
|
Math.abs(a.top - b.top) < OVERLAY_RECT_EPSILON_PX &&
|
|
74
187
|
Math.abs(a.width - b.width) < OVERLAY_RECT_EPSILON_PX &&
|
|
75
188
|
Math.abs(a.height - b.height) < OVERLAY_RECT_EPSILON_PX &&
|
|
76
|
-
Math.abs(a.
|
|
77
|
-
Math.abs(a.
|
|
189
|
+
Math.abs(a.editScaleX - b.editScaleX) < 0.001 &&
|
|
190
|
+
Math.abs(a.editScaleY - b.editScaleY) < 0.001
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function groupOverlayItemsEqual(a: GroupOverlayItem[], b: GroupOverlayItem[]): boolean {
|
|
195
|
+
if (a === b) return true;
|
|
196
|
+
if (a.length !== b.length) return false;
|
|
197
|
+
return a.every((item, index) => {
|
|
198
|
+
const other = b[index];
|
|
199
|
+
return Boolean(
|
|
200
|
+
other &&
|
|
201
|
+
item.key === other.key &&
|
|
202
|
+
item.element === other.element &&
|
|
203
|
+
item.selection === other.selection &&
|
|
204
|
+
rectsEqual(item.rect, other.rect),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function resolveDomEditGroupOverlayRect(rects: OverlayRect[]): OverlayRect | null {
|
|
210
|
+
const first = rects[0];
|
|
211
|
+
if (!first) return null;
|
|
212
|
+
|
|
213
|
+
let left = first.left;
|
|
214
|
+
let top = first.top;
|
|
215
|
+
let right = first.left + first.width;
|
|
216
|
+
let bottom = first.top + first.height;
|
|
217
|
+
|
|
218
|
+
for (const rect of rects.slice(1)) {
|
|
219
|
+
left = Math.min(left, rect.left);
|
|
220
|
+
top = Math.min(top, rect.top);
|
|
221
|
+
right = Math.max(right, rect.left + rect.width);
|
|
222
|
+
bottom = Math.max(bottom, rect.top + rect.height);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
left,
|
|
227
|
+
top,
|
|
228
|
+
width: right - left,
|
|
229
|
+
height: bottom - top,
|
|
230
|
+
editScaleX: 1,
|
|
231
|
+
editScaleY: 1,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function filterNestedDomEditGroupItems<T extends { element: HTMLElement }>(items: T[]): T[] {
|
|
236
|
+
return items.filter(
|
|
237
|
+
(item) => !items.some((other) => other !== item && other.element.contains(item.element)),
|
|
78
238
|
);
|
|
79
239
|
}
|
|
80
240
|
|
|
@@ -89,31 +249,120 @@ function selectionCacheKey(
|
|
|
89
249
|
].join("|");
|
|
90
250
|
}
|
|
91
251
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
) {
|
|
97
|
-
|
|
98
|
-
|
|
252
|
+
type FocusableDomEditOverlay = {
|
|
253
|
+
focus(options?: FocusOptions): void;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export function focusDomEditOverlayElement(element: FocusableDomEditOverlay | null): void {
|
|
257
|
+
element?.focus({ preventScroll: true });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function resolveDomEditResizeGesture(input: {
|
|
261
|
+
originWidth: number;
|
|
262
|
+
originHeight: number;
|
|
263
|
+
actualWidth: number;
|
|
264
|
+
actualHeight: number;
|
|
265
|
+
scaleX: number;
|
|
266
|
+
scaleY: number;
|
|
267
|
+
dx: number;
|
|
268
|
+
dy: number;
|
|
269
|
+
uniform: boolean;
|
|
270
|
+
}): { overlayWidth: number; overlayHeight: number; width: number; height: number } {
|
|
271
|
+
const scaleX = input.scaleX > 0 ? input.scaleX : 1;
|
|
272
|
+
const scaleY = input.scaleY > 0 ? input.scaleY : 1;
|
|
273
|
+
|
|
274
|
+
if (input.uniform) {
|
|
275
|
+
const deltaX = input.dx / scaleX;
|
|
276
|
+
const deltaY = input.dy / scaleY;
|
|
277
|
+
const delta = Math.abs(deltaX) >= Math.abs(deltaY) ? deltaX : deltaY;
|
|
278
|
+
const side = Math.max(1, Math.max(input.actualWidth, input.actualHeight) + delta);
|
|
279
|
+
return {
|
|
280
|
+
overlayWidth: Math.max(MIN_RESIZE_EDGE_PX, side * scaleX),
|
|
281
|
+
overlayHeight: Math.max(MIN_RESIZE_EDGE_PX, side * scaleY),
|
|
282
|
+
width: side,
|
|
283
|
+
height: side,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
overlayWidth: Math.max(MIN_RESIZE_EDGE_PX, input.originWidth + input.dx),
|
|
289
|
+
overlayHeight: Math.max(MIN_RESIZE_EDGE_PX, input.originHeight + input.dy),
|
|
290
|
+
width: Math.max(1, input.actualWidth + input.dx / scaleX),
|
|
291
|
+
height: Math.max(1, input.actualHeight + input.dy / scaleY),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function pointerAngleDegrees(centerX: number, centerY: number, x: number, y: number): number {
|
|
296
|
+
return (Math.atan2(y - centerY, x - centerX) * 180) / Math.PI;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeAngleDelta(delta: number): number {
|
|
300
|
+
return ((((delta + 180) % 360) + 360) % 360) - 180;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function roundAngle(angle: number): number {
|
|
304
|
+
return Math.round(angle * 10) / 10;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function resolveDomEditRotationGesture(input: {
|
|
308
|
+
centerX: number;
|
|
309
|
+
centerY: number;
|
|
310
|
+
startX: number;
|
|
311
|
+
startY: number;
|
|
312
|
+
currentX: number;
|
|
313
|
+
currentY: number;
|
|
314
|
+
actualAngle: number;
|
|
315
|
+
snap: boolean;
|
|
316
|
+
}): { angle: number } {
|
|
317
|
+
const startAngle = pointerAngleDegrees(input.centerX, input.centerY, input.startX, input.startY);
|
|
318
|
+
const currentAngle = pointerAngleDegrees(
|
|
319
|
+
input.centerX,
|
|
320
|
+
input.centerY,
|
|
321
|
+
input.currentX,
|
|
322
|
+
input.currentY,
|
|
323
|
+
);
|
|
324
|
+
const delta = normalizeAngleDelta(currentAngle - startAngle);
|
|
325
|
+
const angle = input.actualAngle + delta;
|
|
326
|
+
return {
|
|
327
|
+
angle: input.snap
|
|
328
|
+
? Math.round(angle / ROTATION_SNAP_DEGREES) * ROTATION_SNAP_DEGREES
|
|
329
|
+
: roundAngle(angle),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function hasDomEditRotationChanged(initialAngle: number, nextAngle: number): boolean {
|
|
334
|
+
return Math.abs(nextAngle - initialAngle) >= ROTATION_COMMIT_EPSILON_DEGREES;
|
|
99
335
|
}
|
|
100
336
|
|
|
101
337
|
interface GestureState {
|
|
102
338
|
kind: GestureKind;
|
|
339
|
+
mode: "path-offset" | "box-size" | "rotation";
|
|
340
|
+
selection: DomEditSelection;
|
|
103
341
|
startX: number;
|
|
104
342
|
startY: number;
|
|
105
|
-
|
|
106
|
-
|
|
343
|
+
centerX: number;
|
|
344
|
+
centerY: number;
|
|
345
|
+
initialPathOffset: StudioPathOffsetSnapshot;
|
|
346
|
+
initialRotation: StudioRotationSnapshot;
|
|
347
|
+
initialBoxSize: StudioBoxSizeSnapshot;
|
|
348
|
+
pathOffsetMember?: ManualOffsetDragMember;
|
|
107
349
|
originLeft: number;
|
|
108
350
|
originTop: number;
|
|
109
351
|
originWidth: number;
|
|
110
352
|
originHeight: number;
|
|
111
|
-
actualLeft: number;
|
|
112
|
-
actualTop: number;
|
|
113
353
|
actualWidth: number;
|
|
114
354
|
actualHeight: number;
|
|
115
|
-
|
|
116
|
-
|
|
355
|
+
actualRotation: number;
|
|
356
|
+
editScaleX: number;
|
|
357
|
+
editScaleY: number;
|
|
358
|
+
manualEditDragToken?: string;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface GroupGestureState {
|
|
362
|
+
startX: number;
|
|
363
|
+
startY: number;
|
|
364
|
+
originItems: GroupOverlayItem[];
|
|
365
|
+
members: ManualOffsetDragMember[];
|
|
117
366
|
}
|
|
118
367
|
|
|
119
368
|
interface BlockedMoveState {
|
|
@@ -123,36 +372,76 @@ interface BlockedMoveState {
|
|
|
123
372
|
notified: boolean;
|
|
124
373
|
}
|
|
125
374
|
|
|
375
|
+
type ResolvedElementRef = {
|
|
376
|
+
current: { key: string; element: HTMLElement } | null;
|
|
377
|
+
};
|
|
378
|
+
|
|
126
379
|
export const DomEditOverlay = memo(function DomEditOverlay({
|
|
127
380
|
iframeRef,
|
|
381
|
+
activeCompositionPath,
|
|
128
382
|
selection,
|
|
383
|
+
groupSelections = [],
|
|
384
|
+
hoverSelection,
|
|
129
385
|
allowCanvasMovement = true,
|
|
130
386
|
onCanvasMouseDown,
|
|
131
|
-
|
|
132
|
-
|
|
387
|
+
onCanvasPointerMove,
|
|
388
|
+
onCanvasPointerLeave,
|
|
389
|
+
onSelectionChange,
|
|
133
390
|
onBlockedMove,
|
|
134
|
-
|
|
135
|
-
|
|
391
|
+
onManualDragStart,
|
|
392
|
+
onPathOffsetCommit,
|
|
393
|
+
onGroupPathOffsetCommit,
|
|
394
|
+
onBoxSizeCommit,
|
|
395
|
+
onRotationCommit,
|
|
136
396
|
}: DomEditOverlayProps) {
|
|
137
397
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
|
138
398
|
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
139
399
|
const [overlayRect, setOverlayRect] = useState<OverlayRect | null>(null);
|
|
400
|
+
const [hoverRect, setHoverRect] = useState<OverlayRect | null>(null);
|
|
401
|
+
const [groupOverlayItems, setGroupOverlayItems] = useState<GroupOverlayItem[]>([]);
|
|
140
402
|
const gestureRef = useRef<GestureState | null>(null);
|
|
403
|
+
const groupGestureRef = useRef<GroupGestureState | null>(null);
|
|
141
404
|
const blockedMoveRef = useRef<BlockedMoveState | null>(null);
|
|
142
405
|
const suppressNextBoxClickRef = useRef(false);
|
|
406
|
+
const suppressNextBoxMouseDownRef = useRef(false);
|
|
407
|
+
const suppressNextOverlayMouseDownRef = useRef(false);
|
|
143
408
|
const rafPausedRef = useRef(false);
|
|
144
409
|
const resolvedElementRef = useRef<{ key: string; element: HTMLElement } | null>(null);
|
|
410
|
+
const resolvedHoverElementRef = useRef<{ key: string; element: HTMLElement } | null>(null);
|
|
411
|
+
const resolvedGroupElementRef = useRef<Map<string, HTMLElement>>(new Map());
|
|
145
412
|
|
|
146
413
|
const selectionRef = useRef(selection);
|
|
147
414
|
selectionRef.current = selection;
|
|
415
|
+
const activeCompositionPathRef = useRef(activeCompositionPath);
|
|
416
|
+
activeCompositionPathRef.current = activeCompositionPath;
|
|
417
|
+
const groupSelectionsRef = useRef(groupSelections);
|
|
418
|
+
groupSelectionsRef.current = groupSelections;
|
|
419
|
+
const hoverSelectionRef = useRef(hoverSelection);
|
|
420
|
+
hoverSelectionRef.current = hoverSelection;
|
|
148
421
|
const overlayRectRef = useRef(overlayRect);
|
|
149
422
|
overlayRectRef.current = overlayRect;
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
423
|
+
const hoverRectRef = useRef(hoverRect);
|
|
424
|
+
hoverRectRef.current = hoverRect;
|
|
425
|
+
const groupOverlayItemsRef = useRef(groupOverlayItems);
|
|
426
|
+
groupOverlayItemsRef.current = groupOverlayItems;
|
|
427
|
+
const onPathOffsetCommitRef = useRef(onPathOffsetCommit);
|
|
428
|
+
onPathOffsetCommitRef.current = onPathOffsetCommit;
|
|
429
|
+
const onGroupPathOffsetCommitRef = useRef(onGroupPathOffsetCommit);
|
|
430
|
+
onGroupPathOffsetCommitRef.current = onGroupPathOffsetCommit;
|
|
431
|
+
const onBoxSizeCommitRef = useRef(onBoxSizeCommit);
|
|
432
|
+
onBoxSizeCommitRef.current = onBoxSizeCommit;
|
|
433
|
+
const onRotationCommitRef = useRef(onRotationCommit);
|
|
434
|
+
onRotationCommitRef.current = onRotationCommit;
|
|
154
435
|
const onBlockedMoveRef = useRef(onBlockedMove);
|
|
155
436
|
onBlockedMoveRef.current = onBlockedMove;
|
|
437
|
+
const onManualDragStartRef = useRef(onManualDragStart);
|
|
438
|
+
onManualDragStartRef.current = onManualDragStart;
|
|
439
|
+
const onCanvasPointerMoveRef = useRef(onCanvasPointerMove);
|
|
440
|
+
onCanvasPointerMoveRef.current = onCanvasPointerMove;
|
|
441
|
+
const onCanvasPointerLeaveRef = useRef(onCanvasPointerLeave);
|
|
442
|
+
onCanvasPointerLeaveRef.current = onCanvasPointerLeave;
|
|
443
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
444
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
156
445
|
|
|
157
446
|
useMountEffect(() => {
|
|
158
447
|
let frame = 0;
|
|
@@ -166,9 +455,29 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
166
455
|
overlayRectRef.current = next;
|
|
167
456
|
setOverlayRect(next);
|
|
168
457
|
};
|
|
169
|
-
const
|
|
458
|
+
const clearHoverRect = () => {
|
|
459
|
+
if (!hoverRectRef.current) return;
|
|
460
|
+
hoverRectRef.current = null;
|
|
461
|
+
setHoverRect(null);
|
|
462
|
+
};
|
|
463
|
+
const setNextHoverRect = (next: OverlayRect | null) => {
|
|
464
|
+
if (rectsEqual(hoverRectRef.current, next)) return;
|
|
465
|
+
hoverRectRef.current = next;
|
|
466
|
+
setHoverRect(next);
|
|
467
|
+
};
|
|
468
|
+
const clearGroupOverlayItems = () => {
|
|
469
|
+
if (groupOverlayItemsRef.current.length === 0) return;
|
|
470
|
+
groupOverlayItemsRef.current = [];
|
|
471
|
+
setGroupOverlayItems([]);
|
|
472
|
+
};
|
|
473
|
+
const setNextGroupOverlayItems = (next: GroupOverlayItem[]) => {
|
|
474
|
+
if (groupOverlayItemsEqual(groupOverlayItemsRef.current, next)) return;
|
|
475
|
+
groupOverlayItemsRef.current = next;
|
|
476
|
+
setGroupOverlayItems(next);
|
|
477
|
+
};
|
|
478
|
+
const resolveElement = (doc: Document, sel: DomEditSelection, cacheRef: ResolvedElementRef) => {
|
|
170
479
|
const key = selectionCacheKey(sel);
|
|
171
|
-
const cached =
|
|
480
|
+
const cached = cacheRef.current;
|
|
172
481
|
if (
|
|
173
482
|
cached?.key === key &&
|
|
174
483
|
cached.element.isConnected &&
|
|
@@ -177,8 +486,21 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
177
486
|
return cached.element;
|
|
178
487
|
}
|
|
179
488
|
|
|
180
|
-
const next = findElementForSelection(doc, sel,
|
|
181
|
-
|
|
489
|
+
const next = findElementForSelection(doc, sel, activeCompositionPathRef.current);
|
|
490
|
+
cacheRef.current = next ? { key, element: next } : null;
|
|
491
|
+
return next;
|
|
492
|
+
};
|
|
493
|
+
const resolveGroupElement = (doc: Document, sel: DomEditSelection) => {
|
|
494
|
+
const key = selectionCacheKey(sel);
|
|
495
|
+
const cached = resolvedGroupElementRef.current.get(key);
|
|
496
|
+
if (cached?.isConnected && cached.ownerDocument === doc) return cached;
|
|
497
|
+
|
|
498
|
+
const next = findElementForSelection(doc, sel, activeCompositionPathRef.current);
|
|
499
|
+
if (next) {
|
|
500
|
+
resolvedGroupElementRef.current.set(key, next);
|
|
501
|
+
} else {
|
|
502
|
+
resolvedGroupElementRef.current.delete(key);
|
|
503
|
+
}
|
|
182
504
|
return next;
|
|
183
505
|
};
|
|
184
506
|
|
|
@@ -189,23 +511,80 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
189
511
|
const sel = selectionRef.current;
|
|
190
512
|
const iframe = iframeRef.current;
|
|
191
513
|
const overlayEl = overlayRef.current;
|
|
192
|
-
if (!
|
|
514
|
+
if (!iframe || !overlayEl) {
|
|
193
515
|
resolvedElementRef.current = null;
|
|
516
|
+
resolvedHoverElementRef.current = null;
|
|
517
|
+
resolvedGroupElementRef.current.clear();
|
|
194
518
|
clearOverlayRect();
|
|
519
|
+
clearHoverRect();
|
|
520
|
+
clearGroupOverlayItems();
|
|
195
521
|
return;
|
|
196
522
|
}
|
|
197
523
|
|
|
198
524
|
const doc = iframe.contentDocument;
|
|
199
|
-
if (!doc)
|
|
525
|
+
if (!doc) {
|
|
526
|
+
resolvedElementRef.current = null;
|
|
527
|
+
resolvedHoverElementRef.current = null;
|
|
528
|
+
resolvedGroupElementRef.current.clear();
|
|
529
|
+
clearOverlayRect();
|
|
530
|
+
clearHoverRect();
|
|
531
|
+
clearGroupOverlayItems();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
200
534
|
|
|
201
|
-
|
|
202
|
-
|
|
535
|
+
if (sel) {
|
|
536
|
+
const el = resolveElement(doc, sel, resolvedElementRef);
|
|
537
|
+
if (el) {
|
|
538
|
+
setNextOverlayRect(toOverlayRect(overlayEl, iframe, el));
|
|
539
|
+
} else {
|
|
540
|
+
clearOverlayRect();
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
resolvedElementRef.current = null;
|
|
203
544
|
clearOverlayRect();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const group = groupSelectionsRef.current;
|
|
548
|
+
if (group.length > 0) {
|
|
549
|
+
const nextGroupItems: GroupOverlayItem[] = [];
|
|
550
|
+
const liveGroupKeys = new Set<string>();
|
|
551
|
+
for (const groupSelection of group) {
|
|
552
|
+
const key = selectionCacheKey(groupSelection);
|
|
553
|
+
liveGroupKeys.add(key);
|
|
554
|
+
const el = resolveGroupElement(doc, groupSelection);
|
|
555
|
+
const rect = el ? toOverlayRect(overlayEl, iframe, el) : null;
|
|
556
|
+
if (el && rect)
|
|
557
|
+
nextGroupItems.push({ key, selection: groupSelection, element: el, rect });
|
|
558
|
+
}
|
|
559
|
+
for (const key of resolvedGroupElementRef.current.keys()) {
|
|
560
|
+
if (!liveGroupKeys.has(key)) resolvedGroupElementRef.current.delete(key);
|
|
561
|
+
}
|
|
562
|
+
setNextGroupOverlayItems(nextGroupItems);
|
|
563
|
+
} else {
|
|
564
|
+
resolvedGroupElementRef.current.clear();
|
|
565
|
+
clearGroupOverlayItems();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const hoverSel = hoverSelectionRef.current;
|
|
569
|
+
const hoverMatchesSelection = Boolean(
|
|
570
|
+
sel && hoverSel && selectionCacheKey(sel) === selectionCacheKey(hoverSel),
|
|
571
|
+
);
|
|
572
|
+
const hoverMatchesGroup = Boolean(
|
|
573
|
+
hoverSel && group.some((entry) => selectionCacheKey(entry) === selectionCacheKey(hoverSel)),
|
|
574
|
+
);
|
|
575
|
+
if (!hoverSel || hoverMatchesSelection || hoverMatchesGroup) {
|
|
576
|
+
resolvedHoverElementRef.current = null;
|
|
577
|
+
clearHoverRect();
|
|
204
578
|
return;
|
|
205
579
|
}
|
|
206
580
|
|
|
207
|
-
const
|
|
208
|
-
|
|
581
|
+
const hoverEl = resolveElement(doc, hoverSel, resolvedHoverElementRef);
|
|
582
|
+
if (!hoverEl) {
|
|
583
|
+
clearHoverRect();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
setNextHoverRect(toOverlayRect(overlayEl, iframe, hoverEl));
|
|
209
588
|
};
|
|
210
589
|
|
|
211
590
|
frame = requestAnimationFrame(update);
|
|
@@ -218,50 +597,188 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
218
597
|
selection.selectorIndex ?? 0
|
|
219
598
|
}`;
|
|
220
599
|
}, [selection]);
|
|
600
|
+
const groupBounds = useMemo(
|
|
601
|
+
() => resolveDomEditGroupOverlayRect(groupOverlayItems.map((item) => item.rect)),
|
|
602
|
+
[groupOverlayItems],
|
|
603
|
+
);
|
|
604
|
+
const hasGroupSelection = groupSelections.length > 1;
|
|
605
|
+
const groupCanMove =
|
|
606
|
+
hasGroupSelection &&
|
|
607
|
+
groupOverlayItems.length > 1 &&
|
|
608
|
+
groupOverlayItems.every((item) => item.selection.capabilities.canApplyManualOffset);
|
|
221
609
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
610
|
+
const setDraftOverlayRect = (next: OverlayRect) => {
|
|
611
|
+
if (rectsEqual(overlayRectRef.current, next)) return;
|
|
612
|
+
overlayRectRef.current = next;
|
|
613
|
+
setOverlayRect(next);
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const restoreGestureOverlayRect = (g: GestureState) => {
|
|
617
|
+
setDraftOverlayRect({
|
|
618
|
+
left: g.originLeft,
|
|
619
|
+
top: g.originTop,
|
|
620
|
+
width: g.originWidth,
|
|
621
|
+
height: g.originHeight,
|
|
622
|
+
editScaleX: g.editScaleX,
|
|
623
|
+
editScaleY: g.editScaleY,
|
|
624
|
+
});
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const setDraftGroupOverlayItems = (next: GroupOverlayItem[]) => {
|
|
628
|
+
if (groupOverlayItemsEqual(groupOverlayItemsRef.current, next)) return;
|
|
629
|
+
groupOverlayItemsRef.current = next;
|
|
630
|
+
setGroupOverlayItems(next);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const restoreGroupGestureOverlayItems = (g: GroupGestureState) => {
|
|
634
|
+
setDraftGroupOverlayItems(g.originItems);
|
|
635
|
+
};
|
|
227
636
|
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
637
|
+
const startGroupDrag = (e: React.PointerEvent<HTMLElement>) => {
|
|
638
|
+
const items = groupOverlayItemsRef.current;
|
|
639
|
+
if (items.length <= 1) return false;
|
|
640
|
+
|
|
641
|
+
const blockedSelection = items.find(
|
|
642
|
+
(item) => !item.selection.capabilities.canApplyManualOffset,
|
|
643
|
+
)?.selection;
|
|
644
|
+
if (blockedSelection) {
|
|
645
|
+
e.preventDefault();
|
|
646
|
+
e.stopPropagation();
|
|
647
|
+
onBlockedMoveRef.current(blockedSelection);
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
onManualDragStartRef.current?.();
|
|
652
|
+
|
|
653
|
+
const dragItems = filterNestedDomEditGroupItems(items);
|
|
654
|
+
|
|
655
|
+
const members: ManualOffsetDragMember[] = [];
|
|
656
|
+
for (const item of dragItems) {
|
|
657
|
+
const result = createManualOffsetDragMember({
|
|
658
|
+
key: item.key,
|
|
659
|
+
selection: item.selection,
|
|
660
|
+
element: item.element,
|
|
661
|
+
rect: item.rect,
|
|
662
|
+
});
|
|
663
|
+
if (!result.ok) {
|
|
664
|
+
restoreManualOffsetDragMembers(members);
|
|
665
|
+
e.preventDefault();
|
|
666
|
+
e.stopPropagation();
|
|
667
|
+
onBlockedMoveRef.current(result.selection);
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
members.push(result.member);
|
|
671
|
+
}
|
|
234
672
|
|
|
235
673
|
e.preventDefault();
|
|
236
674
|
e.stopPropagation();
|
|
237
|
-
|
|
675
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
676
|
+
|
|
677
|
+
rafPausedRef.current = true;
|
|
678
|
+
groupGestureRef.current = {
|
|
679
|
+
startX: e.clientX,
|
|
680
|
+
startY: e.clientY,
|
|
681
|
+
originItems: items,
|
|
682
|
+
members,
|
|
683
|
+
};
|
|
684
|
+
return true;
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const restoreGroupPathOffsets = (g: GroupGestureState) => {
|
|
688
|
+
restoreManualOffsetDragMembers(g.members);
|
|
689
|
+
restoreGroupGestureOverlayItems(g);
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
const startGesture = (
|
|
693
|
+
kind: GestureKind,
|
|
694
|
+
e: React.PointerEvent<HTMLElement>,
|
|
695
|
+
options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
|
|
696
|
+
) => {
|
|
697
|
+
const sel = options?.selection ?? selectionRef.current;
|
|
698
|
+
const rect = options?.rect ?? overlayRectRef.current;
|
|
699
|
+
const box = boxRef.current;
|
|
700
|
+
const overlayEl = overlayRef.current;
|
|
701
|
+
if (!sel || !rect) return false;
|
|
702
|
+
if (kind !== "drag" && !box) return false;
|
|
703
|
+
const mode: GestureState["mode"] =
|
|
704
|
+
kind === "rotate" ? "rotation" : kind === "drag" ? "path-offset" : "box-size";
|
|
705
|
+
if (kind === "drag" && !sel.capabilities.canApplyManualOffset) return false;
|
|
706
|
+
if (kind === "resize" && !sel.capabilities.canApplyManualSize) return false;
|
|
707
|
+
if (kind === "rotate" && !sel.capabilities.canApplyManualRotation) return false;
|
|
708
|
+
if (kind === "resize" && (!Number.isFinite(rect.width) || !Number.isFinite(rect.height))) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
const size = readStudioBoxSize(sel.element);
|
|
712
|
+
const rotation = readStudioRotation(sel.element);
|
|
713
|
+
const actualWidth = size.width > 0 ? size.width : rect.width / rect.editScaleX;
|
|
714
|
+
const actualHeight = size.height > 0 ? size.height : rect.height / rect.editScaleY;
|
|
715
|
+
let initialPathOffset = captureStudioPathOffset(sel.element);
|
|
716
|
+
let manualEditDragToken: string | undefined;
|
|
717
|
+
let pathOffsetMember: ManualOffsetDragMember | undefined;
|
|
718
|
+
if (kind === "drag") {
|
|
719
|
+
onManualDragStartRef.current?.();
|
|
720
|
+
const result = createManualOffsetDragMember({
|
|
721
|
+
key: selectionCacheKey(sel),
|
|
722
|
+
selection: sel,
|
|
723
|
+
element: sel.element,
|
|
724
|
+
rect,
|
|
725
|
+
});
|
|
726
|
+
if (!result.ok) {
|
|
727
|
+
onBlockedMoveRef.current(result.selection);
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
pathOffsetMember = result.member;
|
|
731
|
+
initialPathOffset = result.member.initialPathOffset;
|
|
732
|
+
manualEditDragToken = result.member.gestureToken;
|
|
733
|
+
} else {
|
|
734
|
+
manualEditDragToken = beginStudioManualEditGesture(sel.element);
|
|
735
|
+
}
|
|
736
|
+
const overlayBounds = overlayEl?.getBoundingClientRect();
|
|
737
|
+
const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
|
|
738
|
+
const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
|
|
739
|
+
|
|
740
|
+
e.preventDefault();
|
|
741
|
+
e.stopPropagation();
|
|
742
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
238
743
|
|
|
239
744
|
rafPausedRef.current = true;
|
|
240
745
|
|
|
241
746
|
gestureRef.current = {
|
|
242
747
|
kind,
|
|
748
|
+
mode,
|
|
749
|
+
selection: sel,
|
|
243
750
|
startX: e.clientX,
|
|
244
751
|
startY: e.clientY,
|
|
245
|
-
|
|
246
|
-
|
|
752
|
+
centerX,
|
|
753
|
+
centerY,
|
|
754
|
+
initialPathOffset,
|
|
755
|
+
initialRotation: captureStudioRotation(sel.element),
|
|
756
|
+
initialBoxSize: captureStudioBoxSize(sel.element),
|
|
757
|
+
pathOffsetMember,
|
|
247
758
|
originLeft: rect.left,
|
|
248
759
|
originTop: rect.top,
|
|
249
760
|
originWidth: rect.width,
|
|
250
761
|
originHeight: rect.height,
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
762
|
+
actualWidth,
|
|
763
|
+
actualHeight,
|
|
764
|
+
actualRotation: rotation.angle,
|
|
765
|
+
editScaleX: rect.editScaleX,
|
|
766
|
+
editScaleY: rect.editScaleY,
|
|
767
|
+
manualEditDragToken,
|
|
257
768
|
};
|
|
769
|
+
return true;
|
|
258
770
|
};
|
|
259
771
|
|
|
260
|
-
const onPointerMove = (e: React.PointerEvent) => {
|
|
772
|
+
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
261
773
|
const g = gestureRef.current;
|
|
262
|
-
const
|
|
774
|
+
const groupG = groupGestureRef.current;
|
|
775
|
+
const sel = g?.selection ?? selectionRef.current;
|
|
263
776
|
const box = boxRef.current;
|
|
264
777
|
const blockedMove = blockedMoveRef.current;
|
|
778
|
+
if (!blockedMove && !g && !groupG) {
|
|
779
|
+
onCanvasPointerMoveRef.current(e, { preferClipAncestor: false });
|
|
780
|
+
}
|
|
781
|
+
|
|
265
782
|
if (blockedMove && sel) {
|
|
266
783
|
const dx = e.clientX - blockedMove.startX;
|
|
267
784
|
const dy = e.clientY - blockedMove.startY;
|
|
@@ -273,33 +790,139 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
273
790
|
return;
|
|
274
791
|
}
|
|
275
792
|
|
|
276
|
-
if (
|
|
793
|
+
if (groupG) {
|
|
794
|
+
const dx = e.clientX - groupG.startX;
|
|
795
|
+
const dy = e.clientY - groupG.startY;
|
|
796
|
+
setDraftGroupOverlayItems(
|
|
797
|
+
groupG.originItems.map((item) => ({
|
|
798
|
+
...item,
|
|
799
|
+
rect: {
|
|
800
|
+
...item.rect,
|
|
801
|
+
left: item.rect.left + dx,
|
|
802
|
+
top: item.rect.top + dy,
|
|
803
|
+
},
|
|
804
|
+
})),
|
|
805
|
+
);
|
|
806
|
+
for (const member of groupG.members) {
|
|
807
|
+
applyManualOffsetDragDraft(member, dx, dy);
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (!g || !sel) return;
|
|
277
813
|
|
|
278
814
|
const dx = e.clientX - g.startX;
|
|
279
815
|
const dy = e.clientY - g.startY;
|
|
280
816
|
|
|
817
|
+
if (g.kind === "rotate") {
|
|
818
|
+
const nextRotation = resolveDomEditRotationGesture({
|
|
819
|
+
centerX: g.centerX,
|
|
820
|
+
centerY: g.centerY,
|
|
821
|
+
startX: g.startX,
|
|
822
|
+
startY: g.startY,
|
|
823
|
+
currentX: e.clientX,
|
|
824
|
+
currentY: e.clientY,
|
|
825
|
+
actualAngle: g.actualRotation,
|
|
826
|
+
snap: e.shiftKey,
|
|
827
|
+
});
|
|
828
|
+
applyStudioRotationDraft(sel.element, nextRotation);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
281
832
|
if (g.kind === "drag") {
|
|
282
833
|
const nextBoxLeft = g.originLeft + dx;
|
|
283
834
|
const nextBoxTop = g.originTop + dy;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
835
|
+
setDraftOverlayRect({
|
|
836
|
+
left: nextBoxLeft,
|
|
837
|
+
top: nextBoxTop,
|
|
838
|
+
width: g.originWidth,
|
|
839
|
+
height: g.originHeight,
|
|
840
|
+
editScaleX: g.editScaleX,
|
|
841
|
+
editScaleY: g.editScaleY,
|
|
842
|
+
});
|
|
843
|
+
if (box) {
|
|
844
|
+
box.style.left = `${nextBoxLeft}px`;
|
|
845
|
+
box.style.top = `${nextBoxTop}px`;
|
|
846
|
+
}
|
|
847
|
+
if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
|
|
288
848
|
} else {
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
849
|
+
if (!box) return;
|
|
850
|
+
const nextSize = resolveDomEditResizeGesture({
|
|
851
|
+
originWidth: g.originWidth,
|
|
852
|
+
originHeight: g.originHeight,
|
|
853
|
+
actualWidth: g.actualWidth,
|
|
854
|
+
actualHeight: g.actualHeight,
|
|
855
|
+
scaleX: g.editScaleX,
|
|
856
|
+
scaleY: g.editScaleY,
|
|
857
|
+
dx,
|
|
858
|
+
dy,
|
|
859
|
+
uniform: e.shiftKey,
|
|
860
|
+
});
|
|
861
|
+
setDraftOverlayRect({
|
|
862
|
+
left: g.originLeft,
|
|
863
|
+
top: g.originTop,
|
|
864
|
+
width: nextSize.overlayWidth,
|
|
865
|
+
height: nextSize.overlayHeight,
|
|
866
|
+
editScaleX: g.editScaleX,
|
|
867
|
+
editScaleY: g.editScaleY,
|
|
868
|
+
});
|
|
869
|
+
box.style.width = `${nextSize.overlayWidth}px`;
|
|
870
|
+
box.style.height = `${nextSize.overlayHeight}px`;
|
|
871
|
+
applyStudioBoxSizeDraft(sel.element, nextSize);
|
|
295
872
|
}
|
|
296
873
|
};
|
|
297
874
|
|
|
298
875
|
const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
299
876
|
const g = gestureRef.current;
|
|
300
|
-
const
|
|
877
|
+
const groupG = groupGestureRef.current;
|
|
878
|
+
const sel = g?.selection ?? selectionRef.current;
|
|
301
879
|
const box = boxRef.current;
|
|
302
880
|
blockedMoveRef.current = null;
|
|
881
|
+
|
|
882
|
+
if (groupG) {
|
|
883
|
+
groupGestureRef.current = null;
|
|
884
|
+
rafPausedRef.current = false;
|
|
885
|
+
|
|
886
|
+
const dx = e.clientX - groupG.startX;
|
|
887
|
+
const dy = e.clientY - groupG.startY;
|
|
888
|
+
const movedDistance = Math.hypot(e.clientX - groupG.startX, e.clientY - groupG.startY);
|
|
889
|
+
if (movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
|
|
890
|
+
restoreGroupPathOffsets(groupG);
|
|
891
|
+
suppressNextBoxClickRef.current = true;
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
setDraftGroupOverlayItems(
|
|
896
|
+
groupG.originItems.map((item) => ({
|
|
897
|
+
...item,
|
|
898
|
+
rect: {
|
|
899
|
+
...item.rect,
|
|
900
|
+
left: item.rect.left + dx,
|
|
901
|
+
top: item.rect.top + dy,
|
|
902
|
+
},
|
|
903
|
+
})),
|
|
904
|
+
);
|
|
905
|
+
const updates = groupG.members.map((member) => {
|
|
906
|
+
const finalOffset = applyManualOffsetDragCommit(member, dx, dy);
|
|
907
|
+
return { selection: member.selection, next: finalOffset };
|
|
908
|
+
});
|
|
909
|
+
void Promise.resolve(onGroupPathOffsetCommitRef.current(updates))
|
|
910
|
+
.catch(() => {
|
|
911
|
+
for (const member of groupG.members) {
|
|
912
|
+
if (
|
|
913
|
+
member.gestureToken &&
|
|
914
|
+
isStudioManualEditGestureCurrent(member.element, member.gestureToken)
|
|
915
|
+
) {
|
|
916
|
+
restoreStudioPathOffset(member.element, member.initialPathOffset);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
})
|
|
920
|
+
.finally(() => {
|
|
921
|
+
endManualOffsetDragMembers(groupG.members);
|
|
922
|
+
});
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
303
926
|
if (!g || !sel) {
|
|
304
927
|
gestureRef.current = null;
|
|
305
928
|
rafPausedRef.current = false;
|
|
@@ -311,12 +934,13 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
311
934
|
|
|
312
935
|
const movedDistance = Math.hypot(e.clientX - g.startX, e.clientY - g.startY);
|
|
313
936
|
if (g.kind === "drag" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
|
|
314
|
-
|
|
315
|
-
|
|
937
|
+
restoreStudioPathOffset(sel.element, g.initialPathOffset);
|
|
938
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
316
939
|
if (box) {
|
|
317
940
|
box.style.left = `${g.originLeft}px`;
|
|
318
941
|
box.style.top = `${g.originTop}px`;
|
|
319
942
|
}
|
|
943
|
+
restoreGestureOverlayRect(g);
|
|
320
944
|
suppressNextBoxClickRef.current = true;
|
|
321
945
|
onCanvasMouseDown(e as unknown as React.MouseEvent<HTMLDivElement>, {
|
|
322
946
|
preferClipAncestor: false,
|
|
@@ -324,26 +948,92 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
324
948
|
return;
|
|
325
949
|
}
|
|
326
950
|
|
|
327
|
-
if (g.kind === "
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
951
|
+
if (g.kind === "resize" && movedDistance < BLOCKED_MOVE_THRESHOLD_PX) {
|
|
952
|
+
restoreStudioBoxSize(sel.element, g.initialBoxSize);
|
|
953
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
954
|
+
if (box) {
|
|
955
|
+
box.style.width = `${g.originWidth}px`;
|
|
956
|
+
box.style.height = `${g.originHeight}px`;
|
|
957
|
+
}
|
|
958
|
+
restoreGestureOverlayRect(g);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (g.kind === "rotate") {
|
|
963
|
+
const finalRotation = resolveDomEditRotationGesture({
|
|
964
|
+
centerX: g.centerX,
|
|
965
|
+
centerY: g.centerY,
|
|
966
|
+
startX: g.startX,
|
|
967
|
+
startY: g.startY,
|
|
968
|
+
currentX: e.clientX,
|
|
969
|
+
currentY: e.clientY,
|
|
970
|
+
actualAngle: g.actualRotation,
|
|
971
|
+
snap: e.shiftKey,
|
|
972
|
+
});
|
|
973
|
+
if (!hasDomEditRotationChanged(g.actualRotation, finalRotation.angle)) {
|
|
974
|
+
restoreStudioRotation(sel.element, g.initialRotation);
|
|
975
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
applyStudioRotation(sel.element, finalRotation);
|
|
979
|
+
void Promise.resolve(onRotationCommitRef.current(sel, finalRotation))
|
|
980
|
+
.catch(() => {
|
|
981
|
+
if (
|
|
982
|
+
g.manualEditDragToken &&
|
|
983
|
+
isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)
|
|
984
|
+
) {
|
|
985
|
+
restoreStudioRotation(sel.element, g.initialRotation);
|
|
986
|
+
}
|
|
987
|
+
})
|
|
988
|
+
.finally(() => {
|
|
989
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
990
|
+
});
|
|
991
|
+
} else if (g.kind === "drag") {
|
|
992
|
+
const dx = e.clientX - g.startX;
|
|
993
|
+
const dy = e.clientY - g.startY;
|
|
994
|
+
if (!g.pathOffsetMember) return;
|
|
995
|
+
const finalOffset = applyManualOffsetDragCommit(g.pathOffsetMember, dx, dy);
|
|
996
|
+
const nextBoxLeft = g.originLeft + dx;
|
|
997
|
+
const nextBoxTop = g.originTop + dy;
|
|
998
|
+
setDraftOverlayRect({
|
|
999
|
+
left: nextBoxLeft,
|
|
1000
|
+
top: nextBoxTop,
|
|
1001
|
+
width: g.originWidth,
|
|
1002
|
+
height: g.originHeight,
|
|
1003
|
+
editScaleX: g.editScaleX,
|
|
1004
|
+
editScaleY: g.editScaleY,
|
|
1005
|
+
});
|
|
1006
|
+
if (box) {
|
|
1007
|
+
box.style.left = `${nextBoxLeft}px`;
|
|
1008
|
+
box.style.top = `${nextBoxTop}px`;
|
|
1009
|
+
}
|
|
1010
|
+
void Promise.resolve(onPathOffsetCommitRef.current(sel, finalOffset))
|
|
1011
|
+
.catch(() => {
|
|
1012
|
+
if (
|
|
1013
|
+
g.pathOffsetMember?.gestureToken &&
|
|
1014
|
+
isStudioManualEditGestureCurrent(sel.element, g.pathOffsetMember.gestureToken)
|
|
1015
|
+
) {
|
|
1016
|
+
restoreStudioPathOffset(sel.element, g.initialPathOffset);
|
|
1017
|
+
}
|
|
1018
|
+
})
|
|
1019
|
+
.finally(() => {
|
|
1020
|
+
if (g.pathOffsetMember) endManualOffsetDragMembers([g.pathOffsetMember]);
|
|
1021
|
+
});
|
|
336
1022
|
} else {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
void Promise.resolve(
|
|
340
|
-
() => {
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
1023
|
+
const finalSize = readStudioBoxSize(sel.element);
|
|
1024
|
+
applyStudioBoxSize(sel.element, finalSize);
|
|
1025
|
+
void Promise.resolve(onBoxSizeCommitRef.current(sel, finalSize))
|
|
1026
|
+
.catch(() => {
|
|
1027
|
+
if (
|
|
1028
|
+
g.manualEditDragToken &&
|
|
1029
|
+
isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken)
|
|
1030
|
+
) {
|
|
1031
|
+
restoreStudioBoxSize(sel.element, g.initialBoxSize);
|
|
1032
|
+
}
|
|
1033
|
+
})
|
|
1034
|
+
.finally(() => {
|
|
1035
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
1036
|
+
});
|
|
347
1037
|
}
|
|
348
1038
|
};
|
|
349
1039
|
|
|
@@ -352,22 +1042,78 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
352
1042
|
// box stops propagation for drag gestures, but clicks on the transparent overlay
|
|
353
1043
|
// area outside the box pass through to the iframe pick logic.
|
|
354
1044
|
const handleOverlayMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
1045
|
+
if (suppressNextOverlayMouseDownRef.current) {
|
|
1046
|
+
suppressNextOverlayMouseDownRef.current = false;
|
|
1047
|
+
suppressNextBoxMouseDownRef.current = false;
|
|
1048
|
+
suppressNextBoxClickRef.current = false;
|
|
1049
|
+
event.preventDefault();
|
|
1050
|
+
event.stopPropagation();
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
355
1053
|
const target = event.target as HTMLElement | null;
|
|
356
1054
|
if (target?.closest('[data-dom-edit-selection-box="true"]')) return;
|
|
357
1055
|
onCanvasMouseDown(event, { preferClipAncestor: false });
|
|
1056
|
+
if (event.shiftKey) {
|
|
1057
|
+
suppressNextBoxMouseDownRef.current = true;
|
|
1058
|
+
suppressNextBoxClickRef.current = true;
|
|
1059
|
+
}
|
|
358
1060
|
};
|
|
359
1061
|
|
|
360
|
-
const
|
|
1062
|
+
const handleOverlayPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
|
1063
|
+
if (!allowCanvasMovement || event.button !== 0) return;
|
|
1064
|
+
if (event.shiftKey) {
|
|
1065
|
+
const candidate =
|
|
1066
|
+
onCanvasPointerMoveRef.current(event, {
|
|
1067
|
+
preferClipAncestor: false,
|
|
1068
|
+
}) ?? hoverSelectionRef.current;
|
|
1069
|
+
if (!candidate) return;
|
|
1070
|
+
|
|
1071
|
+
event.preventDefault();
|
|
1072
|
+
event.stopPropagation();
|
|
1073
|
+
suppressNextOverlayMouseDownRef.current = true;
|
|
1074
|
+
suppressNextBoxMouseDownRef.current = true;
|
|
1075
|
+
suppressNextBoxClickRef.current = true;
|
|
1076
|
+
onSelectionChangeRef.current(candidate, { additive: true });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
361
1080
|
const target = event.target as HTMLElement | null;
|
|
362
1081
|
if (target?.closest('[data-dom-edit-selection-box="true"]')) return;
|
|
363
|
-
|
|
1082
|
+
|
|
1083
|
+
const hoverCandidate = onCanvasPointerMoveRef.current(event, {
|
|
1084
|
+
preferClipAncestor: false,
|
|
1085
|
+
});
|
|
1086
|
+
const candidate = hoverCandidate ?? hoverSelectionRef.current;
|
|
1087
|
+
if (!candidate?.capabilities.canApplyManualOffset) return;
|
|
1088
|
+
|
|
1089
|
+
const overlayEl = overlayRef.current;
|
|
1090
|
+
const iframe = iframeRef.current;
|
|
1091
|
+
const candidateRect =
|
|
1092
|
+
overlayEl && iframe ? toOverlayRect(overlayEl, iframe, candidate.element) : null;
|
|
1093
|
+
if (!candidateRect) return;
|
|
1094
|
+
|
|
1095
|
+
suppressNextOverlayMouseDownRef.current = true;
|
|
1096
|
+
selectionRef.current = candidate;
|
|
1097
|
+
overlayRectRef.current = candidateRect;
|
|
1098
|
+
hoverRectRef.current = null;
|
|
1099
|
+
setOverlayRect(candidateRect);
|
|
1100
|
+
setHoverRect(null);
|
|
1101
|
+
const didStartGesture = startGesture("drag", event, {
|
|
1102
|
+
selection: candidate,
|
|
1103
|
+
rect: candidateRect,
|
|
1104
|
+
});
|
|
1105
|
+
if (!didStartGesture) {
|
|
1106
|
+
suppressNextOverlayMouseDownRef.current = false;
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
onSelectionChangeRef.current(candidate);
|
|
364
1110
|
};
|
|
365
1111
|
|
|
366
1112
|
// Click on the selection box itself → re-pick the element under the pointer.
|
|
367
1113
|
// This lets you click a child element even when a parent is selected, because
|
|
368
1114
|
// the click coordinates are forwarded to the iframe's element picker.
|
|
369
1115
|
const handleBoxClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
370
|
-
if (gestureRef.current) return;
|
|
1116
|
+
if (gestureRef.current || groupGestureRef.current) return;
|
|
371
1117
|
if (suppressNextBoxClickRef.current) {
|
|
372
1118
|
suppressNextBoxClickRef.current = false;
|
|
373
1119
|
event.stopPropagation();
|
|
@@ -377,24 +1123,124 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
377
1123
|
};
|
|
378
1124
|
|
|
379
1125
|
const clearPointerState = () => {
|
|
1126
|
+
const groupG = groupGestureRef.current;
|
|
1127
|
+
if (groupG) restoreGroupPathOffsets(groupG);
|
|
1128
|
+
const g = gestureRef.current;
|
|
1129
|
+
const sel = g?.selection ?? selectionRef.current;
|
|
1130
|
+
if (g?.mode === "path-offset" && sel) {
|
|
1131
|
+
restoreStudioPathOffset(sel.element, g.initialPathOffset);
|
|
1132
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
1133
|
+
restoreGestureOverlayRect(g);
|
|
1134
|
+
}
|
|
1135
|
+
if (g?.mode === "box-size" && sel) {
|
|
1136
|
+
restoreStudioBoxSize(sel.element, g.initialBoxSize);
|
|
1137
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
1138
|
+
restoreGestureOverlayRect(g);
|
|
1139
|
+
}
|
|
1140
|
+
if (g?.mode === "rotation" && sel) {
|
|
1141
|
+
restoreStudioRotation(sel.element, g.initialRotation);
|
|
1142
|
+
endStudioManualEditGesture(sel.element, g.manualEditDragToken);
|
|
1143
|
+
}
|
|
380
1144
|
blockedMoveRef.current = null;
|
|
1145
|
+
groupGestureRef.current = null;
|
|
381
1146
|
gestureRef.current = null;
|
|
382
1147
|
rafPausedRef.current = false;
|
|
383
1148
|
};
|
|
384
1149
|
|
|
385
1150
|
return (
|
|
386
1151
|
<div
|
|
387
|
-
key={selectionKey}
|
|
388
1152
|
ref={overlayRef}
|
|
389
|
-
className="absolute inset-0 z-10 pointer-events-auto"
|
|
1153
|
+
className="absolute inset-0 z-10 pointer-events-auto outline-none"
|
|
1154
|
+
tabIndex={-1}
|
|
1155
|
+
aria-label="Composition canvas"
|
|
1156
|
+
onPointerDownCapture={(event) => focusDomEditOverlayElement(event.currentTarget)}
|
|
1157
|
+
onPointerDown={handleOverlayPointerDown}
|
|
390
1158
|
onMouseDown={handleOverlayMouseDown}
|
|
391
|
-
onDoubleClick={handleOverlayDoubleClick}
|
|
392
1159
|
onPointerMove={onPointerMove}
|
|
1160
|
+
onPointerLeave={() => onCanvasPointerLeaveRef.current()}
|
|
393
1161
|
onPointerUp={onPointerUp}
|
|
394
1162
|
onPointerCancel={clearPointerState}
|
|
395
1163
|
>
|
|
396
|
-
{
|
|
1164
|
+
{hoverSelection && hoverRect && (
|
|
1165
|
+
<div
|
|
1166
|
+
aria-hidden="true"
|
|
1167
|
+
data-dom-edit-hover-box="true"
|
|
1168
|
+
className="pointer-events-none absolute rounded-xl border border-studio-accent/80 bg-studio-accent/5 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
|
1169
|
+
style={{
|
|
1170
|
+
left: hoverRect.left,
|
|
1171
|
+
top: hoverRect.top,
|
|
1172
|
+
width: hoverRect.width,
|
|
1173
|
+
height: hoverRect.height,
|
|
1174
|
+
}}
|
|
1175
|
+
/>
|
|
1176
|
+
)}
|
|
1177
|
+
{hasGroupSelection && groupOverlayItems.length > 1 && groupBounds && (
|
|
1178
|
+
<>
|
|
1179
|
+
{groupOverlayItems.map((item) => (
|
|
1180
|
+
<div
|
|
1181
|
+
key={item.key}
|
|
1182
|
+
aria-hidden="true"
|
|
1183
|
+
className="pointer-events-none absolute rounded-xl border border-studio-accent/70 bg-studio-accent/[0.03]"
|
|
1184
|
+
style={{
|
|
1185
|
+
left: item.rect.left,
|
|
1186
|
+
top: item.rect.top,
|
|
1187
|
+
width: item.rect.width,
|
|
1188
|
+
height: item.rect.height,
|
|
1189
|
+
}}
|
|
1190
|
+
/>
|
|
1191
|
+
))}
|
|
1192
|
+
<div
|
|
1193
|
+
data-dom-edit-selection-box="true"
|
|
1194
|
+
className="pointer-events-auto absolute rounded-xl border border-studio-accent bg-studio-accent/5 shadow-[0_0_0_1px_rgba(60,230,172,0.3)]"
|
|
1195
|
+
style={{
|
|
1196
|
+
left: groupBounds.left,
|
|
1197
|
+
top: groupBounds.top,
|
|
1198
|
+
width: groupBounds.width,
|
|
1199
|
+
height: groupBounds.height,
|
|
1200
|
+
cursor: allowCanvasMovement && groupCanMove ? "move" : "default",
|
|
1201
|
+
}}
|
|
1202
|
+
onPointerDown={(e) => {
|
|
1203
|
+
if (!allowCanvasMovement) return;
|
|
1204
|
+
if (e.shiftKey) return;
|
|
1205
|
+
startGroupDrag(e);
|
|
1206
|
+
}}
|
|
1207
|
+
onMouseDown={(e) => {
|
|
1208
|
+
if (!suppressNextBoxMouseDownRef.current) return;
|
|
1209
|
+
suppressNextBoxMouseDownRef.current = false;
|
|
1210
|
+
e.preventDefault();
|
|
1211
|
+
e.stopPropagation();
|
|
1212
|
+
}}
|
|
1213
|
+
onClick={handleBoxClick}
|
|
1214
|
+
/>
|
|
1215
|
+
</>
|
|
1216
|
+
)}
|
|
1217
|
+
{!hasGroupSelection && selection && overlayRect && (
|
|
397
1218
|
<>
|
|
1219
|
+
{allowCanvasMovement && selection.capabilities.canApplyManualRotation && (
|
|
1220
|
+
<div
|
|
1221
|
+
className="pointer-events-none absolute"
|
|
1222
|
+
style={{
|
|
1223
|
+
left: overlayRect.left + overlayRect.width / 2,
|
|
1224
|
+
top: overlayRect.top - 34,
|
|
1225
|
+
width: 28,
|
|
1226
|
+
height: 34,
|
|
1227
|
+
transform: "translateX(-50%)",
|
|
1228
|
+
}}
|
|
1229
|
+
>
|
|
1230
|
+
<div className="absolute left-1/2 top-3 bottom-0 w-px -translate-x-1/2 bg-studio-accent/60" />
|
|
1231
|
+
<button
|
|
1232
|
+
type="button"
|
|
1233
|
+
className="pointer-events-auto absolute left-1/2 top-0 h-3 w-3 -translate-x-1/2 rounded-full border border-studio-accent bg-studio-accent p-0 shadow-[0_0_0_2px_rgba(60,230,172,0.18)]"
|
|
1234
|
+
style={{ cursor: "grab", touchAction: "none" }}
|
|
1235
|
+
title="Rotate"
|
|
1236
|
+
aria-label="Rotate selection"
|
|
1237
|
+
onPointerDown={(e) => {
|
|
1238
|
+
e.stopPropagation();
|
|
1239
|
+
startGesture("rotate", e);
|
|
1240
|
+
}}
|
|
1241
|
+
/>
|
|
1242
|
+
</div>
|
|
1243
|
+
)}
|
|
398
1244
|
<div
|
|
399
1245
|
key={selectionKey}
|
|
400
1246
|
ref={boxRef}
|
|
@@ -405,11 +1251,15 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
405
1251
|
top: overlayRect.top,
|
|
406
1252
|
width: overlayRect.width,
|
|
407
1253
|
height: overlayRect.height,
|
|
408
|
-
cursor:
|
|
1254
|
+
cursor:
|
|
1255
|
+
allowCanvasMovement && selection.capabilities.canApplyManualOffset
|
|
1256
|
+
? "move"
|
|
1257
|
+
: "default",
|
|
409
1258
|
}}
|
|
410
1259
|
onPointerDown={(e) => {
|
|
411
1260
|
if (!allowCanvasMovement) return;
|
|
412
|
-
if (
|
|
1261
|
+
if (e.shiftKey) return;
|
|
1262
|
+
if (selection.capabilities.canApplyManualOffset) {
|
|
413
1263
|
startGesture("drag", e);
|
|
414
1264
|
return;
|
|
415
1265
|
}
|
|
@@ -423,11 +1273,16 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
423
1273
|
notified: false,
|
|
424
1274
|
};
|
|
425
1275
|
}}
|
|
1276
|
+
onMouseDown={(e) => {
|
|
1277
|
+
if (!suppressNextBoxMouseDownRef.current) return;
|
|
1278
|
+
suppressNextBoxMouseDownRef.current = false;
|
|
1279
|
+
e.preventDefault();
|
|
1280
|
+
e.stopPropagation();
|
|
1281
|
+
}}
|
|
426
1282
|
onClick={handleBoxClick}
|
|
427
|
-
onDoubleClick={onSelectedDoubleClick}
|
|
428
1283
|
>
|
|
429
1284
|
{/* Resize handle — bottom-right corner */}
|
|
430
|
-
{allowCanvasMovement && selection.capabilities.
|
|
1285
|
+
{allowCanvasMovement && selection.capabilities.canApplyManualSize && (
|
|
431
1286
|
<div
|
|
432
1287
|
className="absolute -right-1.5 -bottom-1.5 w-3 h-3 rounded-sm bg-studio-accent border border-studio-accent/60"
|
|
433
1288
|
style={{ cursor: "se-resize", touchAction: "none" }}
|