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