@hyperframes/studio 0.7.46 → 0.7.48
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/{index-DO2MAFSG.js → index-CWHON2kh.js} +1 -1
- package/dist/assets/index-DX02Q1Yl.js +423 -0
- package/dist/assets/index-Dq7FEg0K.css +1 -0
- package/dist/assets/{index-BGVoOXdg.js → index-RTqYPUN7.js} +1 -1
- package/dist/index.d.ts +96 -29
- package/dist/index.html +2 -2
- package/dist/index.js +8443 -4305
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -32
- package/src/components/DesignPanelPromoteProvider.tsx +45 -0
- package/src/components/PanelTabButton.tsx +31 -0
- package/src/components/StudioPreviewArea.tsx +75 -3
- package/src/components/StudioRightPanel.tsx +111 -113
- package/src/components/editor/LayersPanel.tsx +2 -22
- package/src/components/editor/PromotableControl.tsx +105 -0
- package/src/components/editor/propertyPanelSections.tsx +42 -21
- package/src/components/panels/VariablesBindElement.tsx +266 -0
- package/src/components/panels/VariablesDeclarationForm.test.ts +135 -0
- package/src/components/panels/VariablesDeclarationForm.tsx +321 -0
- package/src/components/panels/VariablesOtherCompositions.tsx +159 -0
- package/src/components/panels/VariablesPanel.tsx +554 -0
- package/src/components/panels/VariablesRowAction.tsx +25 -0
- package/src/components/panels/VariablesValueControls.tsx +215 -0
- package/src/components/renders/useRenderQueue.ts +12 -0
- package/src/contexts/TimelineEditContext.tsx +7 -2
- package/src/contexts/VariablePromoteContext.tsx +138 -0
- package/src/contexts/variablePromoteHelpers.test.ts +124 -0
- package/src/contexts/variablePromoteHelpers.ts +53 -0
- package/src/contexts/variablePromoteIntegration.test.ts +80 -0
- package/src/hooks/previewVariablesStore.ts +34 -0
- package/src/hooks/timelineEditingHelpers.test.ts +104 -0
- package/src/hooks/timelineEditingHelpers.ts +386 -10
- package/src/hooks/useDomEditWiring.ts +9 -7
- package/src/hooks/useDomSelection.test.ts +36 -2
- package/src/hooks/useDomSelection.ts +56 -75
- package/src/hooks/useElementLifecycleOps.ts +57 -17
- package/src/hooks/usePreviewDocumentVersion.ts +27 -0
- package/src/hooks/useProjectCompositionVariables.ts +131 -0
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useStudioSdkSessions.ts +37 -0
- package/src/hooks/useTimelineEditing.test.tsx +963 -0
- package/src/hooks/useTimelineEditing.ts +122 -100
- package/src/hooks/useTimelineEditingTypes.ts +15 -0
- package/src/hooks/useTimelineGroupEditing.ts +370 -0
- package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +129 -0
- package/src/hooks/useTimelineSelectionPreviewSync.ts +130 -0
- package/src/hooks/useVariablesPersist.ts +61 -0
- package/src/player/components/BeatStrip.tsx +19 -3
- package/src/player/components/Player.tsx +7 -1
- package/src/player/components/Timeline.test.ts +38 -0
- package/src/player/components/Timeline.tsx +102 -80
- package/src/player/components/TimelineCanvas.tsx +340 -296
- package/src/player/components/TimelineClip.test.tsx +11 -0
- package/src/player/components/TimelineClipDiamonds.tsx +40 -9
- package/src/player/components/TimelineDragGhost.tsx +59 -0
- package/src/player/components/TimelineDropInsertionLine.tsx +32 -0
- package/src/player/components/TimelineLayerGroupHeader.tsx +97 -0
- package/src/player/components/TimelineLayerGutter.tsx +61 -0
- package/src/player/components/TimelineRuler.tsx +4 -2
- package/src/player/components/TimelineSelectionOverlays.tsx +55 -0
- package/src/player/components/timelineCallbacks.ts +20 -2
- package/src/player/components/timelineClipDragPreview.ts +52 -0
- package/src/player/components/timelineDragDrop.ts +19 -3
- package/src/player/components/timelineDropIndicator.test.ts +48 -0
- package/src/player/components/timelineDropIndicator.ts +33 -0
- package/src/player/components/timelineEditing.test.ts +251 -1
- package/src/player/components/timelineEditing.ts +178 -19
- package/src/player/components/timelineGroupEditing.ts +159 -0
- package/src/player/components/timelineLayerDrag.test.ts +200 -0
- package/src/player/components/timelineLayerDrag.ts +383 -0
- package/src/player/components/timelineLayout.test.ts +34 -0
- package/src/player/components/timelineLayout.ts +47 -0
- package/src/player/components/timelineMarqueeSelection.test.ts +87 -0
- package/src/player/components/timelineSnapTargets.test.ts +144 -0
- package/src/player/components/timelineSnapTargets.ts +164 -0
- package/src/player/components/timelineStacking.ts +62 -0
- package/src/player/components/timelineTrackOrder.test.ts +144 -0
- package/src/player/components/timelineTrackOrder.ts +173 -0
- package/src/player/components/useTimelineActiveClips.test.ts +20 -0
- package/src/player/components/useTimelineActiveClips.ts +15 -4
- package/src/player/components/useTimelineClipDrag.test.tsx +506 -0
- package/src/player/components/useTimelineClipDrag.ts +172 -126
- package/src/player/components/useTimelineClipGroupDrag.ts +417 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +73 -0
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +231 -0
- package/src/player/components/useTimelineMarqueeSelection.ts +276 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +4 -1
- package/src/player/hooks/useTimelinePlayer.ts +7 -0
- package/src/player/lib/layerOrdering.test.ts +77 -0
- package/src/player/lib/layerOrdering.ts +98 -0
- package/src/player/lib/playbackTypes.ts +3 -0
- package/src/player/lib/timelineDOM.test.ts +103 -0
- package/src/player/lib/timelineDOM.ts +80 -0
- package/src/player/lib/timelineElementHelpers.ts +1 -1
- package/src/player/store/playerStore.test.ts +92 -0
- package/src/player/store/playerStore.ts +69 -9
- package/src/utils/blockInstaller.ts +2 -14
- package/src/utils/editHistory.test.ts +61 -0
- package/src/utils/editHistory.ts +7 -1
- package/src/utils/rootDuration.test.ts +34 -0
- package/src/utils/rootDuration.ts +17 -0
- package/src/utils/sdkCutover.ts +45 -0
- package/src/utils/studioHelpers.test.ts +31 -0
- package/src/utils/studioHelpers.ts +29 -1
- package/src/utils/studioUrlState.test.ts +29 -0
- package/src/utils/studioUrlState.ts +16 -1
- package/dist/assets/index-BsLQ2qGn.js +0 -420
- package/dist/assets/index-DoVLXke0.css +0 -1
|
@@ -5,69 +5,29 @@ import {
|
|
|
5
5
|
resolveTimelineResize,
|
|
6
6
|
resolveTimelineAutoScroll,
|
|
7
7
|
type BlockedTimelineEditIntent,
|
|
8
|
+
type TimelineStackingReorderIntent,
|
|
8
9
|
} from "./timelineEditing";
|
|
9
10
|
import { usePlayerStore } from "../store/playerStore";
|
|
10
11
|
import type { TimelineElement } from "../store/playerStore";
|
|
11
12
|
import { TRACK_H } from "./timelineLayout";
|
|
12
13
|
import { isMusicTrack } from "../../utils/timelineInspector";
|
|
13
14
|
import { mergeUserBeats } from "../../utils/beatEditing";
|
|
15
|
+
import type { StackingTimelineLayer, TimelineLayerId } from "./timelineTrackOrder";
|
|
16
|
+
import type {
|
|
17
|
+
TimelineGroupMoveChange,
|
|
18
|
+
TimelineGroupResizeChange,
|
|
19
|
+
} from "../../hooks/useTimelineGroupEditing";
|
|
20
|
+
import {
|
|
21
|
+
buildTimelineSnapTargets,
|
|
22
|
+
snapEdgesToTargets,
|
|
23
|
+
snapResizeEdgeToTargets,
|
|
24
|
+
type TimelineSnapKind,
|
|
25
|
+
} from "./timelineSnapTargets";
|
|
26
|
+
import { resolveDragPreviewPlacement } from "./timelineClipDragPreview";
|
|
27
|
+
import { useTimelineClipGroupDrag } from "./useTimelineClipGroupDrag";
|
|
14
28
|
|
|
15
|
-
const BEAT_SNAP_PX = 8;
|
|
16
29
|
const EMPTY_BEAT_TIMES: number[] = [];
|
|
17
30
|
|
|
18
|
-
function snapToNearestBeat(time: number, beatTimes: number[], thresholdSecs: number): number {
|
|
19
|
-
let best = time;
|
|
20
|
-
let bestDist = thresholdSecs;
|
|
21
|
-
for (const bt of beatTimes) {
|
|
22
|
-
const d = Math.abs(bt - time);
|
|
23
|
-
if (d < bestDist) {
|
|
24
|
-
bestDist = d;
|
|
25
|
-
best = bt;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return best;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Snap a moved clip so whichever edge (start or end) is nearest a beat lands on
|
|
33
|
-
* it, keeping the duration fixed. Returns the (clamped) start plus the beat time
|
|
34
|
-
* it snapped to (for the grid-line highlight), or `beat: null` when no edge is
|
|
35
|
-
* within threshold.
|
|
36
|
-
*/
|
|
37
|
-
function snapMoveStartToBeat(
|
|
38
|
-
start: number,
|
|
39
|
-
duration: number,
|
|
40
|
-
beatTimes: number[],
|
|
41
|
-
pixelsPerSecond: number,
|
|
42
|
-
timelineDuration: number,
|
|
43
|
-
): { start: number; beat: number | null } {
|
|
44
|
-
if (beatTimes.length === 0) return { start, beat: null };
|
|
45
|
-
const snapSecs = BEAT_SNAP_PX / Math.max(pixelsPerSecond, 1);
|
|
46
|
-
const snappedStart = snapToNearestBeat(start, beatTimes, snapSecs);
|
|
47
|
-
const snappedEnd = snapToNearestBeat(start + duration, beatTimes, snapSecs);
|
|
48
|
-
const startMoved = snappedStart !== start;
|
|
49
|
-
const endMoved = snappedEnd !== start + duration;
|
|
50
|
-
|
|
51
|
-
let candidate = start;
|
|
52
|
-
let beat: number | null = null;
|
|
53
|
-
if (
|
|
54
|
-
startMoved &&
|
|
55
|
-
(!endMoved || Math.abs(snappedStart - start) <= Math.abs(snappedEnd - (start + duration)))
|
|
56
|
-
) {
|
|
57
|
-
candidate = snappedStart;
|
|
58
|
-
beat = snappedStart;
|
|
59
|
-
} else if (endMoved) {
|
|
60
|
-
candidate = snappedEnd - duration;
|
|
61
|
-
beat = snappedEnd;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const maxStart = Math.max(0, timelineDuration - duration);
|
|
65
|
-
const clamped = Math.max(0, Math.min(maxStart, Math.round(candidate * 1000) / 1000));
|
|
66
|
-
// If clamping pulled the clip off the snap target, drop the highlight.
|
|
67
|
-
if (beat != null && Math.abs(clamped - candidate) > 1e-6) beat = null;
|
|
68
|
-
return { start: clamped, beat };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
31
|
/* ── Shared state types ─────────────────────────────────────────── */
|
|
72
32
|
export interface DraggedClipState {
|
|
73
33
|
element: TimelineElement;
|
|
@@ -81,8 +41,14 @@ export interface DraggedClipState {
|
|
|
81
41
|
pointerOffsetY: number;
|
|
82
42
|
previewStart: number;
|
|
83
43
|
previewTrack: number;
|
|
44
|
+
previewLayerId: TimelineLayerId;
|
|
45
|
+
previewLayerIndex: number;
|
|
84
46
|
/** Beat time the clip will snap to on drop, for the grid-line highlight. */
|
|
85
47
|
snapBeatTime: number | null;
|
|
48
|
+
snapGuideTime: number | null;
|
|
49
|
+
snapGuideKind: TimelineSnapKind | null;
|
|
50
|
+
/** Sibling-scoped z-index reorder intent resolved from the vertical drag. */
|
|
51
|
+
previewStackingReorder: TimelineStackingReorderIntent | null;
|
|
86
52
|
started: boolean;
|
|
87
53
|
}
|
|
88
54
|
|
|
@@ -93,6 +59,8 @@ export interface ResizingClipState {
|
|
|
93
59
|
previewStart: number;
|
|
94
60
|
previewDuration: number;
|
|
95
61
|
previewPlaybackStart?: number;
|
|
62
|
+
snapGuideTime: number | null;
|
|
63
|
+
snapGuideKind: TimelineSnapKind | null;
|
|
96
64
|
started: boolean;
|
|
97
65
|
}
|
|
98
66
|
|
|
@@ -108,29 +76,41 @@ export interface BlockedClipState {
|
|
|
108
76
|
interface UseTimelineClipDragInput {
|
|
109
77
|
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
110
78
|
ppsRef: React.RefObject<number>;
|
|
111
|
-
|
|
112
|
-
|
|
79
|
+
trackOrderRef: React.RefObject<TimelineLayerId[]>;
|
|
80
|
+
timelineLayersRef: React.RefObject<StackingTimelineLayer[]>;
|
|
81
|
+
timelineElementsRef: React.RefObject<TimelineElement[]>;
|
|
113
82
|
onMoveElement?: (
|
|
114
83
|
element: TimelineElement,
|
|
115
|
-
updates: Pick<TimelineElement, "start" | "track"
|
|
84
|
+
updates: Pick<TimelineElement, "start" | "track"> & {
|
|
85
|
+
stackingReorder?: TimelineStackingReorderIntent | null;
|
|
86
|
+
},
|
|
116
87
|
) => Promise<void> | void;
|
|
117
88
|
onResizeElement?: (
|
|
118
89
|
element: TimelineElement,
|
|
119
90
|
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
|
120
91
|
) => Promise<void> | void;
|
|
92
|
+
onMoveElements?: (changes: TimelineGroupMoveChange[]) => Promise<void> | void;
|
|
93
|
+
onResizeElements?: (changes: TimelineGroupResizeChange[]) => Promise<void> | void;
|
|
94
|
+
onPreviewMoveElements?: (changes: TimelineGroupMoveChange[]) => void;
|
|
95
|
+
onPreviewResizeElements?: (changes: TimelineGroupResizeChange[]) => void;
|
|
121
96
|
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
122
97
|
setShowPopover: (show: boolean) => void;
|
|
123
|
-
/** Stable ref to the range selection setter
|
|
98
|
+
/** Stable ref to the range selection setter, wired after mount to break circular dependency. */
|
|
124
99
|
setRangeSelectionRef: React.RefObject<((sel: null) => void) | null>;
|
|
125
100
|
}
|
|
126
101
|
|
|
127
102
|
export function useTimelineClipDrag({
|
|
128
103
|
scrollRef,
|
|
129
104
|
ppsRef,
|
|
130
|
-
durationRef,
|
|
131
105
|
trackOrderRef,
|
|
106
|
+
timelineLayersRef,
|
|
107
|
+
timelineElementsRef,
|
|
132
108
|
onMoveElement,
|
|
133
109
|
onResizeElement,
|
|
110
|
+
onMoveElements,
|
|
111
|
+
onResizeElements,
|
|
112
|
+
onPreviewMoveElements,
|
|
113
|
+
onPreviewResizeElements,
|
|
134
114
|
onBlockedEditAttempt,
|
|
135
115
|
setShowPopover,
|
|
136
116
|
setRangeSelectionRef,
|
|
@@ -139,6 +119,8 @@ export function useTimelineClipDrag({
|
|
|
139
119
|
const rawBeatTimes = usePlayerStore((s) => s.beatAnalysis?.beatTimes ?? EMPTY_BEAT_TIMES);
|
|
140
120
|
const rawBeatStrengths = usePlayerStore((s) => s.beatAnalysis?.beatStrengths ?? EMPTY_BEAT_TIMES);
|
|
141
121
|
const beatEdits = usePlayerStore((s) => s.beatEdits);
|
|
122
|
+
const playhead = usePlayerStore((s) => s.currentTime);
|
|
123
|
+
const compositionDuration = usePlayerStore((s) => s.duration);
|
|
142
124
|
const musicStart = usePlayerStore((s) => s.elements.find(isMusicTrack)?.start ?? 0);
|
|
143
125
|
const musicPlaybackStart = usePlayerStore(
|
|
144
126
|
(s) => s.elements.find(isMusicTrack)?.playbackStart ?? 0,
|
|
@@ -166,6 +148,29 @@ export function useTimelineClipDrag({
|
|
|
166
148
|
|
|
167
149
|
const beatTimesRef = useRef<number[]>([]);
|
|
168
150
|
beatTimesRef.current = adjustedBeatTimes;
|
|
151
|
+
const playheadRef = useRef(0);
|
|
152
|
+
playheadRef.current = playhead;
|
|
153
|
+
const compositionDurationRef = useRef(0);
|
|
154
|
+
compositionDurationRef.current = compositionDuration;
|
|
155
|
+
|
|
156
|
+
const buildSnapTargets = useCallback(
|
|
157
|
+
(element: TimelineElement) => {
|
|
158
|
+
const draggedKey = element.key ?? element.id;
|
|
159
|
+
const selected = selectedElementIdsRef.current;
|
|
160
|
+
// In a group drag every selected clip moves together, so none of them may act
|
|
161
|
+
// as a snap target for the others; exclude the whole set, not just the grabbed clip.
|
|
162
|
+
const excludedKeys =
|
|
163
|
+
selected.size > 1 && selected.has(draggedKey) ? selected : new Set([draggedKey]);
|
|
164
|
+
return buildTimelineSnapTargets({
|
|
165
|
+
elements: timelineElementsRef.current,
|
|
166
|
+
excludedKeys,
|
|
167
|
+
playhead: playheadRef.current,
|
|
168
|
+
compDuration: compositionDurationRef.current,
|
|
169
|
+
beats: isMusicTrack(element) ? EMPTY_BEAT_TIMES : beatTimesRef.current,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
[timelineElementsRef],
|
|
173
|
+
);
|
|
169
174
|
|
|
170
175
|
const [draggedClip, setDraggedClip] = useState<DraggedClipState | null>(null);
|
|
171
176
|
const draggedClipRef = useRef<DraggedClipState | null>(null);
|
|
@@ -182,10 +187,35 @@ export function useTimelineClipDrag({
|
|
|
182
187
|
onMoveElementRef.current = onMoveElement;
|
|
183
188
|
const onResizeElementRef = useRef(onResizeElement);
|
|
184
189
|
onResizeElementRef.current = onResizeElement;
|
|
190
|
+
const onMoveElementsRef = useRef(onMoveElements);
|
|
191
|
+
onMoveElementsRef.current = onMoveElements;
|
|
192
|
+
const onResizeElementsRef = useRef(onResizeElements);
|
|
193
|
+
onResizeElementsRef.current = onResizeElements;
|
|
194
|
+
const onPreviewMoveElementsRef = useRef(onPreviewMoveElements);
|
|
195
|
+
onPreviewMoveElementsRef.current = onPreviewMoveElements;
|
|
196
|
+
const onPreviewResizeElementsRef = useRef(onPreviewResizeElements);
|
|
197
|
+
onPreviewResizeElementsRef.current = onPreviewResizeElements;
|
|
198
|
+
const selectedElementIdsRef = useRef(usePlayerStore.getState().selectedElementIds);
|
|
199
|
+
selectedElementIdsRef.current = usePlayerStore((s) => s.selectedElementIds);
|
|
185
200
|
|
|
186
201
|
const clipDragScrollRaf = useRef(0);
|
|
187
202
|
const clipDragPointerRef = useRef<{ clientX: number; clientY: number } | null>(null);
|
|
203
|
+
const {
|
|
204
|
+
previewGroupMove,
|
|
205
|
+
previewGroupResize,
|
|
206
|
+
commitGroupMove,
|
|
207
|
+
commitGroupResize,
|
|
208
|
+
clearGroupDragSessions,
|
|
209
|
+
} = useTimelineClipGroupDrag({
|
|
210
|
+
timelineElementsRef,
|
|
211
|
+
updateElement,
|
|
212
|
+
onMoveElementsRef,
|
|
213
|
+
onResizeElementsRef,
|
|
214
|
+
onPreviewMoveElementsRef,
|
|
215
|
+
onPreviewResizeElementsRef,
|
|
216
|
+
});
|
|
188
217
|
|
|
218
|
+
// fallow-ignore-next-line complexity
|
|
189
219
|
const updateDraggedClipPreview = useCallback(
|
|
190
220
|
(drag: DraggedClipState, clientX: number, clientY: number): DraggedClipState => {
|
|
191
221
|
const scroll = scrollRef.current;
|
|
@@ -202,33 +232,45 @@ export function useTimelineClipDrag({
|
|
|
202
232
|
currentScrollTop: scroll?.scrollTop ?? drag.originScrollTop,
|
|
203
233
|
pixelsPerSecond: ppsRef.current,
|
|
204
234
|
trackHeight: TRACK_H,
|
|
205
|
-
maxStart:
|
|
206
|
-
trackOrder:
|
|
235
|
+
maxStart: Number.POSITIVE_INFINITY,
|
|
236
|
+
trackOrder: timelineLayersRef.current.map((layer) => layer.placementTrack),
|
|
237
|
+
layerOrder: trackOrderRef.current,
|
|
238
|
+
timelineLayers: timelineLayersRef.current,
|
|
239
|
+
stackingElement: drag.element,
|
|
240
|
+
stackingElements: timelineElementsRef.current,
|
|
207
241
|
},
|
|
208
242
|
clientX,
|
|
209
243
|
clientY,
|
|
210
244
|
);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
);
|
|
245
|
+
const snap = snapEdgesToTargets(
|
|
246
|
+
nextMove.start,
|
|
247
|
+
drag.element.duration,
|
|
248
|
+
buildSnapTargets(drag.element),
|
|
249
|
+
ppsRef.current,
|
|
250
|
+
{ maxStart: Number.POSITIVE_INFINITY },
|
|
251
|
+
);
|
|
252
|
+
const groupMove = previewGroupMove(drag.element, selectedElementIdsRef.current, snap.start);
|
|
253
|
+
const placement = resolveDragPreviewPlacement(drag, nextMove, groupMove);
|
|
221
254
|
return {
|
|
222
255
|
...drag,
|
|
223
256
|
started: true,
|
|
224
257
|
pointerClientX: clientX,
|
|
225
258
|
pointerClientY: clientY,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
...placement,
|
|
260
|
+
snapBeatTime: snap.snapKind === "beat" ? snap.snapTime : null,
|
|
261
|
+
snapGuideTime: snap.snapTime,
|
|
262
|
+
snapGuideKind: snap.snapKind,
|
|
229
263
|
};
|
|
230
264
|
},
|
|
231
|
-
[
|
|
265
|
+
[
|
|
266
|
+
scrollRef,
|
|
267
|
+
ppsRef,
|
|
268
|
+
trackOrderRef,
|
|
269
|
+
timelineLayersRef,
|
|
270
|
+
timelineElementsRef,
|
|
271
|
+
buildSnapTargets,
|
|
272
|
+
previewGroupMove,
|
|
273
|
+
],
|
|
232
274
|
);
|
|
233
275
|
|
|
234
276
|
const stopClipDragAutoScroll = useCallback(() => {
|
|
@@ -287,6 +329,12 @@ export function useTimelineClipDrag({
|
|
|
287
329
|
|
|
288
330
|
const updateDraggedClipPreviewRef = useRef(updateDraggedClipPreview);
|
|
289
331
|
updateDraggedClipPreviewRef.current = updateDraggedClipPreview;
|
|
332
|
+
const commitGroupMoveRef = useRef(commitGroupMove);
|
|
333
|
+
commitGroupMoveRef.current = commitGroupMove;
|
|
334
|
+
const commitGroupResizeRef = useRef(commitGroupResize);
|
|
335
|
+
commitGroupResizeRef.current = commitGroupResize;
|
|
336
|
+
const clearGroupDragSessionsRef = useRef(clearGroupDragSessions);
|
|
337
|
+
clearGroupDragSessionsRef.current = clearGroupDragSessions;
|
|
290
338
|
const syncClipDragAutoScrollRef = useRef(syncClipDragAutoScroll);
|
|
291
339
|
syncClipDragAutoScrollRef.current = syncClipDragAutoScroll;
|
|
292
340
|
const stopClipDragAutoScrollRef = useRef(stopClipDragAutoScroll);
|
|
@@ -299,6 +347,7 @@ export function useTimelineClipDrag({
|
|
|
299
347
|
});
|
|
300
348
|
};
|
|
301
349
|
|
|
350
|
+
// fallow-ignore-next-line complexity
|
|
302
351
|
const handleWindowPointerMove = (e: PointerEvent) => {
|
|
303
352
|
const drag = draggedClipRef.current;
|
|
304
353
|
const resize = resizingClipRef.current;
|
|
@@ -322,7 +371,7 @@ export function useTimelineClipDrag({
|
|
|
322
371
|
const normalizedTag = resize.element.tag.toLowerCase();
|
|
323
372
|
const canSeedPlaybackStart = normalizedTag === "audio" || normalizedTag === "video";
|
|
324
373
|
const playbackRate = Math.max(resize.element.playbackRate ?? 1, 0.1);
|
|
325
|
-
const maxEnd =
|
|
374
|
+
const maxEnd = resize.element.start + sourceRemaining;
|
|
326
375
|
let nextResize = resolveTimelineResize(
|
|
327
376
|
{
|
|
328
377
|
start: resize.element.start,
|
|
@@ -341,54 +390,43 @@ export function useTimelineClipDrag({
|
|
|
341
390
|
e.clientX,
|
|
342
391
|
);
|
|
343
392
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// degenerate clip or run past the source/composition limit.
|
|
356
|
-
const snappedDuration = Math.round((snapped - nextResize.start) * 1000) / 1000;
|
|
357
|
-
if (snapped !== edgeTime && snapped <= maxEnd + 1e-6 && snappedDuration >= 0.05) {
|
|
358
|
-
nextResize = { ...nextResize, duration: snappedDuration };
|
|
359
|
-
}
|
|
360
|
-
} else {
|
|
361
|
-
const snapped = snapToNearestBeat(nextResize.start, beatTimes, snapSecs);
|
|
362
|
-
const delta = nextResize.start - snapped; // >0 when snapping left
|
|
363
|
-
// Leftward snap reveals more source; cap so playbackStart can't go < 0.
|
|
364
|
-
const maxLeftDelta =
|
|
365
|
-
nextResize.playbackStart != null
|
|
393
|
+
const snap = snapResizeEdgeToTargets(
|
|
394
|
+
resize.edge,
|
|
395
|
+
nextResize.start,
|
|
396
|
+
nextResize.duration,
|
|
397
|
+
buildSnapTargets(resize.element),
|
|
398
|
+
ppsRef.current,
|
|
399
|
+
{
|
|
400
|
+
minDuration: 0.05,
|
|
401
|
+
maxEnd,
|
|
402
|
+
maxLeftDelta:
|
|
403
|
+
resize.edge === "start" && nextResize.playbackStart != null
|
|
366
404
|
? nextResize.playbackStart / playbackRate
|
|
367
|
-
: Number.POSITIVE_INFINITY
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
playbackStart:
|
|
382
|
-
nextResize.playbackStart != null
|
|
383
|
-
? Math.round(
|
|
384
|
-
Math.max(0, nextResize.playbackStart - delta * playbackRate) * 1000,
|
|
385
|
-
) / 1000
|
|
386
|
-
: undefined,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
405
|
+
: Number.POSITIVE_INFINITY,
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
if (snap.snapTime != null) {
|
|
409
|
+
const unsnappedStart = nextResize.start;
|
|
410
|
+
nextResize = { ...nextResize, start: snap.start, duration: snap.duration };
|
|
411
|
+
if (resize.edge === "start" && nextResize.playbackStart != null) {
|
|
412
|
+
const delta = unsnappedStart - snap.start;
|
|
413
|
+
nextResize = {
|
|
414
|
+
...nextResize,
|
|
415
|
+
playbackStart:
|
|
416
|
+
Math.round(Math.max(0, nextResize.playbackStart - delta * playbackRate) * 1000) /
|
|
417
|
+
1000,
|
|
418
|
+
};
|
|
389
419
|
}
|
|
390
420
|
}
|
|
391
|
-
|
|
421
|
+
const groupResize = previewGroupResize(
|
|
422
|
+
resize.element,
|
|
423
|
+
selectedElementIdsRef.current,
|
|
424
|
+
resize.edge,
|
|
425
|
+
nextResize,
|
|
426
|
+
);
|
|
427
|
+
if (groupResize.active) {
|
|
428
|
+
nextResize = groupResize.updates;
|
|
429
|
+
}
|
|
392
430
|
setResizingClip((prev) =>
|
|
393
431
|
prev
|
|
394
432
|
? {
|
|
@@ -397,6 +435,8 @@ export function useTimelineClipDrag({
|
|
|
397
435
|
previewStart: nextResize.start,
|
|
398
436
|
previewDuration: nextResize.duration,
|
|
399
437
|
previewPlaybackStart: nextResize.playbackStart,
|
|
438
|
+
snapGuideTime: snap.snapTime,
|
|
439
|
+
snapGuideKind: snap.snapKind,
|
|
400
440
|
}
|
|
401
441
|
: prev,
|
|
402
442
|
);
|
|
@@ -434,6 +474,7 @@ export function useTimelineClipDrag({
|
|
|
434
474
|
syncClipDragAutoScrollRef.current(e.clientX, e.clientY);
|
|
435
475
|
};
|
|
436
476
|
|
|
477
|
+
// fallow-ignore-next-line complexity
|
|
437
478
|
const handleWindowPointerUp = () => {
|
|
438
479
|
stopClipDragAutoScrollRef.current();
|
|
439
480
|
|
|
@@ -446,6 +487,8 @@ export function useTimelineClipDrag({
|
|
|
446
487
|
suppressClickRef.current = true;
|
|
447
488
|
clearSuppressedClick();
|
|
448
489
|
|
|
490
|
+
if (commitGroupResizeRef.current(resize.element)) return;
|
|
491
|
+
|
|
449
492
|
const hasChanged =
|
|
450
493
|
resize.previewStart !== resize.element.start ||
|
|
451
494
|
resize.previewDuration !== resize.element.duration ||
|
|
@@ -492,24 +535,26 @@ export function useTimelineClipDrag({
|
|
|
492
535
|
suppressClickRef.current = true;
|
|
493
536
|
clearSuppressedClick();
|
|
494
537
|
|
|
495
|
-
|
|
496
|
-
|
|
538
|
+
if (commitGroupMoveRef.current(drag.element)) return;
|
|
539
|
+
|
|
540
|
+
const hasStackingReorder =
|
|
541
|
+
drag.previewStackingReorder != null && drag.previewStackingReorder.zIndexChanges.length > 0;
|
|
542
|
+
const hasChanged = drag.previewStart !== drag.element.start || hasStackingReorder;
|
|
497
543
|
if (!hasChanged) return;
|
|
498
544
|
|
|
499
545
|
updateElement(drag.element.key ?? drag.element.id, {
|
|
500
546
|
start: drag.previewStart,
|
|
501
|
-
track: drag.previewTrack,
|
|
502
547
|
});
|
|
503
548
|
|
|
504
549
|
Promise.resolve(
|
|
505
550
|
onMoveElementRef.current?.(drag.element, {
|
|
506
551
|
start: drag.previewStart,
|
|
507
|
-
track: drag.
|
|
552
|
+
track: drag.element.track,
|
|
553
|
+
stackingReorder: drag.previewStackingReorder,
|
|
508
554
|
}),
|
|
509
555
|
).catch((error) => {
|
|
510
556
|
updateElement(drag.element.key ?? drag.element.id, {
|
|
511
557
|
start: drag.element.start,
|
|
512
|
-
track: drag.element.track,
|
|
513
558
|
});
|
|
514
559
|
console.error("[Timeline] Failed to persist clip move", error);
|
|
515
560
|
});
|
|
@@ -519,6 +564,7 @@ export function useTimelineClipDrag({
|
|
|
519
564
|
window.addEventListener("pointerup", handleWindowPointerUp);
|
|
520
565
|
window.addEventListener("pointercancel", handleWindowPointerUp);
|
|
521
566
|
return () => {
|
|
567
|
+
clearGroupDragSessionsRef.current();
|
|
522
568
|
stopClipDragAutoScrollRef.current();
|
|
523
569
|
window.removeEventListener("pointermove", handleWindowPointerMove);
|
|
524
570
|
window.removeEventListener("pointerup", handleWindowPointerUp);
|