summaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/src/hooks/timeline/element/use-element-interaction.ts89
-rw-r--r--apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts104
2 files changed, 154 insertions, 39 deletions
diff --git a/apps/web/src/hooks/timeline/element/use-element-interaction.ts b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
index 98d21d9b..5458efe7 100644
--- a/apps/web/src/hooks/timeline/element/use-element-interaction.ts
+++ b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
@@ -191,6 +191,7 @@ export function useElementInteraction({
const [isPendingDrag, setIsPendingDrag] = useState(false);
const pendingDragRef = useRef<PendingDragState | null>(null);
const moveGroupRef = useRef<MoveGroup | null>(null);
+ const newTrackIdsRef = useRef<string[]>([]);
const groupMoveResultRef = useRef<GroupMoveResult | null>(null);
const lastMouseXRef = useRef(0);
const mouseDownLocationRef = useRef<{ x: number; y: number } | null>(null);
@@ -227,6 +228,7 @@ export function useElementInteraction({
const endDrag = useCallback(() => {
moveGroupRef.current = null;
+ newTrackIdsRef.current = [];
groupMoveResultRef.current = null;
setDragState(initialDragState);
setDragDropTarget(null);
@@ -262,7 +264,7 @@ export function useElementInteraction({
target: {
kind: "newTracks",
anchorInsertIndex: dropTarget.trackIndex,
- newTrackIds: group.members.map(() => generateUUID()),
+ newTrackIds: newTrackIdsRef.current,
},
});
}
@@ -297,7 +299,7 @@ export function useElementInteraction({
target: {
kind: "newTracks",
anchorInsertIndex: dropTarget.trackIndex,
- newTrackIds: group.members.map(() => generateUUID()),
+ newTrackIds: newTrackIdsRef.current,
},
});
},
@@ -386,18 +388,61 @@ export function useElementInteraction({
}
moveGroupRef.current = moveGroup;
- groupMoveResultRef.current = null;
+ newTrackIdsRef.current = moveGroup.members.map(() => generateUUID());
const dragTimeOffsets: Record<string, number> = {};
for (const member of moveGroup.members) {
dragTimeOffsets[member.elementId] = member.timeOffset;
}
+ const {
+ snappedTime: initialSnappedTime,
+ snapPoint: initialSnapPoint,
+ } = getDragSnapResult({
+ frameSnappedTime: snappedTime,
+ group: moveGroup,
+ });
+ const verticalDragDirection = getVerticalDragDirection({
+ startMouseY: pendingDragRef.current.startMouseY,
+ currentMouseY: clientY,
+ });
+ const anchorDropTarget = getDragDropTarget({
+ clientX,
+ clientY,
+ elementId: pendingDragRef.current.elementId,
+ trackId: pendingDragRef.current.trackId,
+ tracks: sceneTracks,
+ tracksContainerRef,
+ tracksScrollRef,
+ headerRef,
+ zoomLevel,
+ snappedTime: initialSnappedTime,
+ verticalDragDirection,
+ });
+ const nextGroupMoveResult =
+ anchorDropTarget != null
+ ? resolveGroupDragMove({
+ group: moveGroup,
+ snappedTime: initialSnappedTime,
+ dropTarget: anchorDropTarget,
+ })
+ : null;
+ groupMoveResultRef.current = nextGroupMoveResult;
+ setDragDropTarget(
+ anchorDropTarget &&
+ (anchorDropTarget.isNewTrack || !nextGroupMoveResult)
+ ? {
+ ...anchorDropTarget,
+ isNewTrack: true,
+ }
+ : null,
+ );
startDrag({
...pendingDragRef.current,
dragElementIds: moveGroup.members.map((member) => member.elementId),
dragTimeOffsets,
- initialCurrentTime: snappedTime,
+ initialCurrentTime: initialSnappedTime,
initialCurrentMouseY: clientY,
});
+ onSnapPointChange?.(initialSnapPoint);
startedDragThisEvent = true;
pendingDragRef.current = null;
setIsPendingDrag(false);
@@ -533,28 +578,6 @@ export function useElementInteraction({
}
}
- const dropTarget = getDragDropTarget({
- clientX,
- clientY,
- elementId: dragState.elementId,
- trackId: dragState.trackId,
- tracks: sceneTracks,
- tracksContainerRef,
- tracksScrollRef,
- headerRef,
- zoomLevel,
- snappedTime: dragState.currentTime,
- verticalDragDirection: getVerticalDragDirection({
- startMouseY: dragState.startMouseY,
- currentMouseY: clientY,
- }),
- });
- if (!dropTarget) {
- endDrag();
- onSnapPointChange?.(null);
- return;
- }
- const snappedTime = dragState.currentTime;
const moveGroup = moveGroupRef.current;
if (!moveGroup) {
endDrag();
@@ -562,11 +585,7 @@ export function useElementInteraction({
return;
}
- const groupMoveResult = resolveGroupDragMove({
- group: moveGroup,
- snappedTime,
- dropTarget,
- });
+ const groupMoveResult = groupMoveResultRef.current;
if (!groupMoveResult) {
endDrag();
onSnapPointChange?.(null);
@@ -604,18 +623,10 @@ export function useElementInteraction({
dragState.isDragging,
dragState.elementId,
dragState.startElementTime,
- dragState.startMouseY,
dragState.trackId,
- dragState.currentTime,
- zoomLevel,
endDrag,
onSnapPointChange,
editor.timeline,
- tracksContainerRef,
- tracksScrollRef,
- headerRef,
- resolveGroupDragMove,
- sceneTracks,
]);
useEffect(() => {
diff --git a/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts b/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts
index e6c86e41..9e89c48a 100644
--- a/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts
+++ b/apps/web/src/lib/timeline/group-move/__tests__/group-move.test.ts
@@ -11,6 +11,7 @@ import type {
import type { Transform } from "@/lib/rendering";
import { buildMoveGroup } from "@/lib/timeline/group-move/build-group";
import { resolveGroupMove } from "@/lib/timeline/group-move/resolve-move";
+import { snapGroupEdges } from "@/lib/timeline/group-move/snap";
function buildTransform(): Transform {
return {
@@ -284,4 +285,107 @@ describe("group move", () => {
},
]);
});
+
+ test("snapGroupEdges snaps the closest group edge and preserves offsets", () => {
+ const tracks = buildTracks({
+ overlay: [
+ buildVideoTrack({
+ id: "overlay-video",
+ elements: [
+ buildVideoElement({ id: "video-1", startTime: 5, duration: 2 }),
+ ],
+ }),
+ buildTextTrack({
+ id: "overlay-text",
+ elements: [
+ buildTextElement({ id: "text-1", startTime: 8, duration: 3 }),
+ ],
+ }),
+ ],
+ main: buildVideoTrack({
+ id: "main",
+ elements: [
+ buildVideoElement({ id: "video-snap", startTime: 10, duration: 4 }),
+ ],
+ }),
+ });
+ const group = buildMoveGroup({
+ anchorRef: { trackId: "overlay-video", elementId: "video-1" },
+ selectedElements: [
+ { trackId: "overlay-video", elementId: "video-1" },
+ { trackId: "overlay-text", elementId: "text-1" },
+ ],
+ tracks,
+ });
+ if (!group) {
+ throw new Error("Expected group");
+ }
+
+ const result = snapGroupEdges({
+ group,
+ anchorStartTime: 6,
+ tracks,
+ playheadTime: 100,
+ zoomLevel: 1,
+ });
+
+ expect(result).toEqual({
+ snappedAnchorStartTime: 7,
+ snapPoint: {
+ time: 10,
+ type: "element-start",
+ elementId: "video-snap",
+ trackId: "main",
+ },
+ });
+ });
+
+ test("snapGroupEdges returns the raw anchor time when nothing is within threshold", () => {
+ const tracks = buildTracks({
+ overlay: [
+ buildVideoTrack({
+ id: "overlay-video",
+ elements: [
+ buildVideoElement({ id: "video-1", startTime: 5, duration: 2 }),
+ ],
+ }),
+ buildTextTrack({
+ id: "overlay-text",
+ elements: [
+ buildTextElement({ id: "text-1", startTime: 8, duration: 3 }),
+ ],
+ }),
+ ],
+ main: buildVideoTrack({
+ id: "main",
+ elements: [
+ buildVideoElement({ id: "video-snap", startTime: 100, duration: 4 }),
+ ],
+ }),
+ });
+ const group = buildMoveGroup({
+ anchorRef: { trackId: "overlay-video", elementId: "video-1" },
+ selectedElements: [
+ { trackId: "overlay-video", elementId: "video-1" },
+ { trackId: "overlay-text", elementId: "text-1" },
+ ],
+ tracks,
+ });
+ if (!group) {
+ throw new Error("Expected group");
+ }
+
+ const result = snapGroupEdges({
+ group,
+ anchorStartTime: 6,
+ tracks,
+ playheadTime: 200,
+ zoomLevel: 1000,
+ });
+
+ expect(result).toEqual({
+ snappedAnchorStartTime: 6,
+ snapPoint: null,
+ });
+ });
});