@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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { type DomEditSelection, findElementForSelection } from "./domEditing";
|
|
2
|
+
|
|
3
|
+
export interface OverlayRect {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
editScaleX: number;
|
|
9
|
+
editScaleY: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GroupOverlayItem {
|
|
13
|
+
key: string;
|
|
14
|
+
selection: DomEditSelection;
|
|
15
|
+
element: HTMLElement;
|
|
16
|
+
rect: OverlayRect;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ResolvedElementRef = {
|
|
20
|
+
current: { key: string; element: HTMLElement } | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function isElementVisibleForOverlay(el: HTMLElement): boolean {
|
|
24
|
+
const win = el.ownerDocument.defaultView;
|
|
25
|
+
if (!win) return true;
|
|
26
|
+
let current: HTMLElement | null = el;
|
|
27
|
+
while (current) {
|
|
28
|
+
const computed = win.getComputedStyle(current);
|
|
29
|
+
if (computed.display === "none" || computed.visibility === "hidden") return false;
|
|
30
|
+
const opacity = Number.parseFloat(computed.opacity);
|
|
31
|
+
if (Number.isFinite(opacity) && opacity <= 0.01) return false;
|
|
32
|
+
current = current.parentElement;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readPositiveDimension(value: string | null): number | null {
|
|
38
|
+
if (!value) return null;
|
|
39
|
+
const parsed = Number.parseFloat(value);
|
|
40
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findSourceBoundary(element: HTMLElement): HTMLElement | null {
|
|
44
|
+
let current: HTMLElement | null = element;
|
|
45
|
+
while (current) {
|
|
46
|
+
if (
|
|
47
|
+
current.hasAttribute("data-composition-file") ||
|
|
48
|
+
current.hasAttribute("data-composition-src")
|
|
49
|
+
) {
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
current = current.parentElement;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveDomEditCoordinateScale(input: {
|
|
58
|
+
rootScaleX: number;
|
|
59
|
+
rootScaleY: number;
|
|
60
|
+
sourceRectWidth?: number;
|
|
61
|
+
sourceRectHeight?: number;
|
|
62
|
+
sourceWidth?: number | null;
|
|
63
|
+
sourceHeight?: number | null;
|
|
64
|
+
}): { scaleX: number; scaleY: number } {
|
|
65
|
+
const rootScaleX = input.rootScaleX > 0 ? input.rootScaleX : 1;
|
|
66
|
+
const rootScaleY = input.rootScaleY > 0 ? input.rootScaleY : 1;
|
|
67
|
+
const sourceScaleX =
|
|
68
|
+
input.sourceRectWidth && input.sourceRectWidth > 0 && input.sourceWidth && input.sourceWidth > 0
|
|
69
|
+
? (input.sourceRectWidth * rootScaleX) / input.sourceWidth
|
|
70
|
+
: rootScaleX;
|
|
71
|
+
const sourceScaleY =
|
|
72
|
+
input.sourceRectHeight &&
|
|
73
|
+
input.sourceRectHeight > 0 &&
|
|
74
|
+
input.sourceHeight &&
|
|
75
|
+
input.sourceHeight > 0
|
|
76
|
+
? (input.sourceRectHeight * rootScaleY) / input.sourceHeight
|
|
77
|
+
: rootScaleY;
|
|
78
|
+
return {
|
|
79
|
+
scaleX: sourceScaleX > 0 ? sourceScaleX : rootScaleX,
|
|
80
|
+
scaleY: sourceScaleY > 0 ? sourceScaleY : rootScaleY,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function toOverlayRect(
|
|
85
|
+
overlayEl: HTMLDivElement,
|
|
86
|
+
iframe: HTMLIFrameElement,
|
|
87
|
+
element: HTMLElement,
|
|
88
|
+
): OverlayRect | null {
|
|
89
|
+
const iframeRect = iframe.getBoundingClientRect();
|
|
90
|
+
const overlayRect = overlayEl.getBoundingClientRect();
|
|
91
|
+
const doc = iframe.contentDocument;
|
|
92
|
+
const root =
|
|
93
|
+
doc?.querySelector<HTMLElement>("[data-composition-id]") ?? doc?.documentElement ?? null;
|
|
94
|
+
const rootRect = root?.getBoundingClientRect();
|
|
95
|
+
const rootWidth = rootRect?.width;
|
|
96
|
+
const rootHeight = rootRect?.height;
|
|
97
|
+
if (!rootWidth || !rootHeight) return null;
|
|
98
|
+
|
|
99
|
+
const elementRect = element.getBoundingClientRect();
|
|
100
|
+
const rootScaleX = iframeRect.width / rootWidth;
|
|
101
|
+
const rootScaleY = iframeRect.height / rootHeight;
|
|
102
|
+
const sourceBoundary = findSourceBoundary(element);
|
|
103
|
+
const sourceBoundaryRect = sourceBoundary?.getBoundingClientRect();
|
|
104
|
+
const editScale = resolveDomEditCoordinateScale({
|
|
105
|
+
rootScaleX,
|
|
106
|
+
rootScaleY,
|
|
107
|
+
sourceRectWidth: sourceBoundaryRect?.width,
|
|
108
|
+
sourceRectHeight: sourceBoundaryRect?.height,
|
|
109
|
+
sourceWidth: readPositiveDimension(sourceBoundary?.getAttribute("data-width") ?? null),
|
|
110
|
+
sourceHeight: readPositiveDimension(sourceBoundary?.getAttribute("data-height") ?? null),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
left: iframeRect.left - overlayRect.left + (elementRect.left - rootRect.left) * rootScaleX,
|
|
115
|
+
top: iframeRect.top - overlayRect.top + (elementRect.top - rootRect.top) * rootScaleY,
|
|
116
|
+
width: elementRect.width * rootScaleX,
|
|
117
|
+
height: elementRect.height * rootScaleY,
|
|
118
|
+
editScaleX: editScale.scaleX,
|
|
119
|
+
editScaleY: editScale.scaleY,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const OVERLAY_RECT_EPSILON_PX = 0.5;
|
|
124
|
+
|
|
125
|
+
export function rectsEqual(a: OverlayRect | null, b: OverlayRect | null): boolean {
|
|
126
|
+
if (a === b) return true;
|
|
127
|
+
if (!a || !b) return false;
|
|
128
|
+
return (
|
|
129
|
+
Math.abs(a.left - b.left) < OVERLAY_RECT_EPSILON_PX &&
|
|
130
|
+
Math.abs(a.top - b.top) < OVERLAY_RECT_EPSILON_PX &&
|
|
131
|
+
Math.abs(a.width - b.width) < OVERLAY_RECT_EPSILON_PX &&
|
|
132
|
+
Math.abs(a.height - b.height) < OVERLAY_RECT_EPSILON_PX &&
|
|
133
|
+
Math.abs(a.editScaleX - b.editScaleX) < 0.001 &&
|
|
134
|
+
Math.abs(a.editScaleY - b.editScaleY) < 0.001
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function groupOverlayItemsEqual(a: GroupOverlayItem[], b: GroupOverlayItem[]): boolean {
|
|
139
|
+
if (a === b) return true;
|
|
140
|
+
if (a.length !== b.length) return false;
|
|
141
|
+
return a.every((item, index) => {
|
|
142
|
+
const other = b[index];
|
|
143
|
+
return Boolean(
|
|
144
|
+
other &&
|
|
145
|
+
item.key === other.key &&
|
|
146
|
+
item.element === other.element &&
|
|
147
|
+
item.selection === other.selection &&
|
|
148
|
+
rectsEqual(item.rect, other.rect),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function resolveDomEditGroupOverlayRect(rects: OverlayRect[]): OverlayRect | null {
|
|
154
|
+
const first = rects[0];
|
|
155
|
+
if (!first) return null;
|
|
156
|
+
|
|
157
|
+
let left = first.left;
|
|
158
|
+
let top = first.top;
|
|
159
|
+
let right = first.left + first.width;
|
|
160
|
+
let bottom = first.top + first.height;
|
|
161
|
+
|
|
162
|
+
for (const rect of rects.slice(1)) {
|
|
163
|
+
left = Math.min(left, rect.left);
|
|
164
|
+
top = Math.min(top, rect.top);
|
|
165
|
+
right = Math.max(right, rect.left + rect.width);
|
|
166
|
+
bottom = Math.max(bottom, rect.top + rect.height);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
left,
|
|
171
|
+
top,
|
|
172
|
+
width: right - left,
|
|
173
|
+
height: bottom - top,
|
|
174
|
+
editScaleX: 1,
|
|
175
|
+
editScaleY: 1,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function filterNestedDomEditGroupItems<T extends { element: HTMLElement }>(items: T[]): T[] {
|
|
180
|
+
return items.filter(
|
|
181
|
+
(item) => !items.some((other) => other !== item && other.element.contains(item.element)),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function selectionCacheKey(
|
|
186
|
+
selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
|
|
187
|
+
): string {
|
|
188
|
+
return [
|
|
189
|
+
selection.sourceFile ?? "",
|
|
190
|
+
selection.id ?? "",
|
|
191
|
+
selection.selector ?? "",
|
|
192
|
+
selection.selectorIndex ?? "",
|
|
193
|
+
].join("|");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resolveElementForOverlay(
|
|
197
|
+
doc: Document,
|
|
198
|
+
sel: DomEditSelection,
|
|
199
|
+
activeCompositionPath: string | null,
|
|
200
|
+
cacheRef: ResolvedElementRef,
|
|
201
|
+
): HTMLElement | null {
|
|
202
|
+
const key = selectionCacheKey(sel);
|
|
203
|
+
const cached = cacheRef.current;
|
|
204
|
+
if (cached?.key === key && cached.element.isConnected && cached.element.ownerDocument === doc) {
|
|
205
|
+
return cached.element;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const next = findElementForSelection(doc, sel, activeCompositionPath);
|
|
209
|
+
cacheRef.current = next ? { key, element: next } : null;
|
|
210
|
+
return next;
|
|
211
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { DomEditSelection } from "./domEditing";
|
|
2
|
+
import type {
|
|
3
|
+
StudioBoxSizeSnapshot,
|
|
4
|
+
StudioPathOffsetSnapshot,
|
|
5
|
+
StudioRotationSnapshot,
|
|
6
|
+
} from "./manualEdits";
|
|
7
|
+
import type { ManualOffsetDragMember } from "./manualOffsetDrag";
|
|
8
|
+
import type { GroupOverlayItem } from "./domEditOverlayGeometry";
|
|
9
|
+
|
|
10
|
+
export type GestureKind = "drag" | "resize" | "rotate";
|
|
11
|
+
|
|
12
|
+
export const BLOCKED_MOVE_THRESHOLD_PX = 4;
|
|
13
|
+
export const MIN_RESIZE_EDGE_PX = 20;
|
|
14
|
+
const ROTATION_COMMIT_EPSILON_DEGREES = 0.05;
|
|
15
|
+
const ROTATION_SNAP_DEGREES = 15;
|
|
16
|
+
|
|
17
|
+
export interface GestureState {
|
|
18
|
+
kind: GestureKind;
|
|
19
|
+
mode: "path-offset" | "box-size" | "rotation";
|
|
20
|
+
selection: DomEditSelection;
|
|
21
|
+
startX: number;
|
|
22
|
+
startY: number;
|
|
23
|
+
centerX: number;
|
|
24
|
+
centerY: number;
|
|
25
|
+
initialPathOffset: StudioPathOffsetSnapshot;
|
|
26
|
+
initialRotation: StudioRotationSnapshot;
|
|
27
|
+
initialBoxSize: StudioBoxSizeSnapshot;
|
|
28
|
+
pathOffsetMember?: ManualOffsetDragMember;
|
|
29
|
+
originLeft: number;
|
|
30
|
+
originTop: number;
|
|
31
|
+
originWidth: number;
|
|
32
|
+
originHeight: number;
|
|
33
|
+
actualWidth: number;
|
|
34
|
+
actualHeight: number;
|
|
35
|
+
actualRotation: number;
|
|
36
|
+
editScaleX: number;
|
|
37
|
+
editScaleY: number;
|
|
38
|
+
manualEditDragToken?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface GroupGestureState {
|
|
42
|
+
startX: number;
|
|
43
|
+
startY: number;
|
|
44
|
+
originItems: GroupOverlayItem[];
|
|
45
|
+
members: ManualOffsetDragMember[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BlockedMoveState {
|
|
49
|
+
pointerId: number;
|
|
50
|
+
startX: number;
|
|
51
|
+
startY: number;
|
|
52
|
+
notified: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type FocusableDomEditOverlay = {
|
|
56
|
+
focus(options?: FocusOptions): void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function focusDomEditOverlayElement(element: FocusableDomEditOverlay | null): void {
|
|
60
|
+
element?.focus({ preventScroll: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveDomEditResizeGesture(input: {
|
|
64
|
+
originWidth: number;
|
|
65
|
+
originHeight: number;
|
|
66
|
+
actualWidth: number;
|
|
67
|
+
actualHeight: number;
|
|
68
|
+
scaleX: number;
|
|
69
|
+
scaleY: number;
|
|
70
|
+
dx: number;
|
|
71
|
+
dy: number;
|
|
72
|
+
uniform: boolean;
|
|
73
|
+
}): { overlayWidth: number; overlayHeight: number; width: number; height: number } {
|
|
74
|
+
const scaleX = input.scaleX > 0 ? input.scaleX : 1;
|
|
75
|
+
const scaleY = input.scaleY > 0 ? input.scaleY : 1;
|
|
76
|
+
|
|
77
|
+
if (input.uniform) {
|
|
78
|
+
const deltaX = input.dx / scaleX;
|
|
79
|
+
const deltaY = input.dy / scaleY;
|
|
80
|
+
const delta = Math.abs(deltaX) >= Math.abs(deltaY) ? deltaX : deltaY;
|
|
81
|
+
const side = Math.max(1, Math.max(input.actualWidth, input.actualHeight) + delta);
|
|
82
|
+
return {
|
|
83
|
+
overlayWidth: Math.max(MIN_RESIZE_EDGE_PX, side * scaleX),
|
|
84
|
+
overlayHeight: Math.max(MIN_RESIZE_EDGE_PX, side * scaleY),
|
|
85
|
+
width: side,
|
|
86
|
+
height: side,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
overlayWidth: Math.max(MIN_RESIZE_EDGE_PX, input.originWidth + input.dx),
|
|
92
|
+
overlayHeight: Math.max(MIN_RESIZE_EDGE_PX, input.originHeight + input.dy),
|
|
93
|
+
width: Math.max(1, input.actualWidth + input.dx / scaleX),
|
|
94
|
+
height: Math.max(1, input.actualHeight + input.dy / scaleY),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pointerAngleDegrees(centerX: number, centerY: number, x: number, y: number): number {
|
|
99
|
+
return (Math.atan2(y - centerY, x - centerX) * 180) / Math.PI;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeAngleDelta(delta: number): number {
|
|
103
|
+
return ((((delta + 180) % 360) + 360) % 360) - 180;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function roundAngle(angle: number): number {
|
|
107
|
+
return Math.round(angle * 10) / 10;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveDomEditRotationGesture(input: {
|
|
111
|
+
centerX: number;
|
|
112
|
+
centerY: number;
|
|
113
|
+
startX: number;
|
|
114
|
+
startY: number;
|
|
115
|
+
currentX: number;
|
|
116
|
+
currentY: number;
|
|
117
|
+
actualAngle: number;
|
|
118
|
+
snap: boolean;
|
|
119
|
+
}): { angle: number } {
|
|
120
|
+
const startAngle = pointerAngleDegrees(input.centerX, input.centerY, input.startX, input.startY);
|
|
121
|
+
const currentAngle = pointerAngleDegrees(
|
|
122
|
+
input.centerX,
|
|
123
|
+
input.centerY,
|
|
124
|
+
input.currentX,
|
|
125
|
+
input.currentY,
|
|
126
|
+
);
|
|
127
|
+
const delta = normalizeAngleDelta(currentAngle - startAngle);
|
|
128
|
+
const angle = input.actualAngle + delta;
|
|
129
|
+
return {
|
|
130
|
+
angle: input.snap
|
|
131
|
+
? Math.round(angle / ROTATION_SNAP_DEGREES) * ROTATION_SNAP_DEGREES
|
|
132
|
+
: roundAngle(angle),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function hasDomEditRotationChanged(initialAngle: number, nextAngle: number): boolean {
|
|
137
|
+
return Math.abs(nextAngle - initialAngle) >= ROTATION_COMMIT_EPSILON_DEGREES;
|
|
138
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gesture-begin functions: startGroupDrag and startGesture.
|
|
3
|
+
* These are pure "start a new gesture" operations — no draft rect updates.
|
|
4
|
+
*/
|
|
5
|
+
import { type DomEditSelection } from "./domEditing";
|
|
6
|
+
import {
|
|
7
|
+
createManualOffsetDragMember,
|
|
8
|
+
restoreManualOffsetDragMembers,
|
|
9
|
+
type ManualOffsetDragMember,
|
|
10
|
+
} from "./manualOffsetDrag";
|
|
11
|
+
import {
|
|
12
|
+
beginStudioManualEditGesture,
|
|
13
|
+
captureStudioBoxSize,
|
|
14
|
+
captureStudioPathOffset,
|
|
15
|
+
captureStudioRotation,
|
|
16
|
+
readStudioBoxSize,
|
|
17
|
+
readStudioRotation,
|
|
18
|
+
} from "./manualEdits";
|
|
19
|
+
import {
|
|
20
|
+
type OverlayRect,
|
|
21
|
+
filterNestedDomEditGroupItems,
|
|
22
|
+
selectionCacheKey,
|
|
23
|
+
} from "./domEditOverlayGeometry";
|
|
24
|
+
import { type GestureKind, type GestureState } from "./domEditOverlayGestures";
|
|
25
|
+
import type { UseDomEditOverlayGesturesOptions } from "./useDomEditOverlayGestures";
|
|
26
|
+
|
|
27
|
+
export function startGroupDrag(
|
|
28
|
+
e: React.PointerEvent<HTMLElement>,
|
|
29
|
+
opts: UseDomEditOverlayGesturesOptions,
|
|
30
|
+
): boolean {
|
|
31
|
+
const items = opts.groupOverlayItemsRef.current;
|
|
32
|
+
if (items.length <= 1) return false;
|
|
33
|
+
|
|
34
|
+
const blockedSelection = items.find(
|
|
35
|
+
(item) => !item.selection.capabilities.canApplyManualOffset,
|
|
36
|
+
)?.selection;
|
|
37
|
+
if (blockedSelection) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
opts.onBlockedMoveRef.current(blockedSelection);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
opts.onManualDragStartRef.current?.();
|
|
45
|
+
const dragItems = filterNestedDomEditGroupItems(items);
|
|
46
|
+
const members: ManualOffsetDragMember[] = [];
|
|
47
|
+
for (const item of dragItems) {
|
|
48
|
+
const result = createManualOffsetDragMember({
|
|
49
|
+
key: item.key,
|
|
50
|
+
selection: item.selection,
|
|
51
|
+
element: item.element,
|
|
52
|
+
rect: item.rect,
|
|
53
|
+
});
|
|
54
|
+
if (!result.ok) {
|
|
55
|
+
restoreManualOffsetDragMembers(members);
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
opts.onBlockedMoveRef.current(result.selection);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
members.push(result.member);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
67
|
+
opts.rafPausedRef.current = true;
|
|
68
|
+
opts.groupGestureRef.current = {
|
|
69
|
+
startX: e.clientX,
|
|
70
|
+
startY: e.clientY,
|
|
71
|
+
originItems: items,
|
|
72
|
+
members,
|
|
73
|
+
};
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function startGesture(
|
|
78
|
+
kind: GestureKind,
|
|
79
|
+
e: React.PointerEvent<HTMLElement>,
|
|
80
|
+
opts: UseDomEditOverlayGesturesOptions,
|
|
81
|
+
options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
|
|
82
|
+
): boolean {
|
|
83
|
+
const sel = options?.selection ?? opts.selectionRef.current;
|
|
84
|
+
const rect = options?.rect ?? opts.overlayRectRef.current;
|
|
85
|
+
const box = opts.boxRef.current;
|
|
86
|
+
const overlayEl = opts.overlayRef.current;
|
|
87
|
+
if (!sel || !rect) return false;
|
|
88
|
+
if (kind !== "drag" && !box) return false;
|
|
89
|
+
const mode: GestureState["mode"] =
|
|
90
|
+
kind === "rotate" ? "rotation" : kind === "drag" ? "path-offset" : "box-size";
|
|
91
|
+
if (kind === "drag" && !sel.capabilities.canApplyManualOffset) return false;
|
|
92
|
+
if (kind === "resize" && !sel.capabilities.canApplyManualSize) return false;
|
|
93
|
+
if (kind === "rotate" && !sel.capabilities.canApplyManualRotation) return false;
|
|
94
|
+
if (kind === "resize" && (!Number.isFinite(rect.width) || !Number.isFinite(rect.height)))
|
|
95
|
+
return false;
|
|
96
|
+
|
|
97
|
+
const size = readStudioBoxSize(sel.element);
|
|
98
|
+
const rotation = readStudioRotation(sel.element);
|
|
99
|
+
const actualWidth = size.width > 0 ? size.width : rect.width / rect.editScaleX;
|
|
100
|
+
const actualHeight = size.height > 0 ? size.height : rect.height / rect.editScaleY;
|
|
101
|
+
let initialPathOffset = captureStudioPathOffset(sel.element);
|
|
102
|
+
let manualEditDragToken: string | undefined;
|
|
103
|
+
let pathOffsetMember: ManualOffsetDragMember | undefined;
|
|
104
|
+
|
|
105
|
+
if (kind === "drag") {
|
|
106
|
+
opts.onManualDragStartRef.current?.();
|
|
107
|
+
const result = createManualOffsetDragMember({
|
|
108
|
+
key: selectionCacheKey(sel),
|
|
109
|
+
selection: sel,
|
|
110
|
+
element: sel.element,
|
|
111
|
+
rect,
|
|
112
|
+
});
|
|
113
|
+
if (!result.ok) {
|
|
114
|
+
opts.onBlockedMoveRef.current(result.selection);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
pathOffsetMember = result.member;
|
|
118
|
+
initialPathOffset = result.member.initialPathOffset;
|
|
119
|
+
manualEditDragToken = result.member.gestureToken;
|
|
120
|
+
} else {
|
|
121
|
+
manualEditDragToken = beginStudioManualEditGesture(sel.element);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const overlayBounds = overlayEl?.getBoundingClientRect();
|
|
125
|
+
const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
|
|
126
|
+
const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
130
|
+
opts.rafPausedRef.current = true;
|
|
131
|
+
opts.gestureRef.current = {
|
|
132
|
+
kind,
|
|
133
|
+
mode,
|
|
134
|
+
selection: sel,
|
|
135
|
+
startX: e.clientX,
|
|
136
|
+
startY: e.clientY,
|
|
137
|
+
centerX,
|
|
138
|
+
centerY,
|
|
139
|
+
initialPathOffset,
|
|
140
|
+
initialRotation: captureStudioRotation(sel.element),
|
|
141
|
+
initialBoxSize: captureStudioBoxSize(sel.element),
|
|
142
|
+
pathOffsetMember,
|
|
143
|
+
originLeft: rect.left,
|
|
144
|
+
originTop: rect.top,
|
|
145
|
+
originWidth: rect.width,
|
|
146
|
+
originHeight: rect.height,
|
|
147
|
+
actualWidth,
|
|
148
|
+
actualHeight,
|
|
149
|
+
actualRotation: rotation.angle,
|
|
150
|
+
editScaleX: rect.editScaleX,
|
|
151
|
+
editScaleY: rect.editScaleY,
|
|
152
|
+
manualEditDragToken,
|
|
153
|
+
};
|
|
154
|
+
return true;
|
|
155
|
+
}
|