review: fixes

This commit is contained in:
Maze Winther 2026-04-15 05:52:33 +02:00
parent ac43a5a73f
commit 8a3577d2e7
2 changed files with 154 additions and 39 deletions

View File

@ -191,6 +191,7 @@ export function useElementInteraction({
const [isPendingDrag, setIsPendingDrag] = useState(false); const [isPendingDrag, setIsPendingDrag] = useState(false);
const pendingDragRef = useRef<PendingDragState | null>(null); const pendingDragRef = useRef<PendingDragState | null>(null);
const moveGroupRef = useRef<MoveGroup | null>(null); const moveGroupRef = useRef<MoveGroup | null>(null);
const newTrackIdsRef = useRef<string[]>([]);
const groupMoveResultRef = useRef<GroupMoveResult | null>(null); const groupMoveResultRef = useRef<GroupMoveResult | null>(null);
const lastMouseXRef = useRef(0); const lastMouseXRef = useRef(0);
const mouseDownLocationRef = useRef<{ x: number; y: number } | null>(null); const mouseDownLocationRef = useRef<{ x: number; y: number } | null>(null);
@ -227,6 +228,7 @@ export function useElementInteraction({
const endDrag = useCallback(() => { const endDrag = useCallback(() => {
moveGroupRef.current = null; moveGroupRef.current = null;
newTrackIdsRef.current = [];
groupMoveResultRef.current = null; groupMoveResultRef.current = null;
setDragState(initialDragState); setDragState(initialDragState);
setDragDropTarget(null); setDragDropTarget(null);
@ -262,7 +264,7 @@ export function useElementInteraction({
target: { target: {
kind: "newTracks", kind: "newTracks",
anchorInsertIndex: dropTarget.trackIndex, anchorInsertIndex: dropTarget.trackIndex,
newTrackIds: group.members.map(() => generateUUID()), newTrackIds: newTrackIdsRef.current,
}, },
}); });
} }
@ -297,7 +299,7 @@ export function useElementInteraction({
target: { target: {
kind: "newTracks", kind: "newTracks",
anchorInsertIndex: dropTarget.trackIndex, anchorInsertIndex: dropTarget.trackIndex,
newTrackIds: group.members.map(() => generateUUID()), newTrackIds: newTrackIdsRef.current,
}, },
}); });
}, },
@ -386,18 +388,61 @@ export function useElementInteraction({
} }
moveGroupRef.current = moveGroup; moveGroupRef.current = moveGroup;
groupMoveResultRef.current = null; newTrackIdsRef.current = moveGroup.members.map(() => generateUUID());
const dragTimeOffsets: Record<string, number> = {}; const dragTimeOffsets: Record<string, number> = {};
for (const member of moveGroup.members) { for (const member of moveGroup.members) {
dragTimeOffsets[member.elementId] = member.timeOffset; 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({ startDrag({
...pendingDragRef.current, ...pendingDragRef.current,
dragElementIds: moveGroup.members.map((member) => member.elementId), dragElementIds: moveGroup.members.map((member) => member.elementId),
dragTimeOffsets, dragTimeOffsets,
initialCurrentTime: snappedTime, initialCurrentTime: initialSnappedTime,
initialCurrentMouseY: clientY, initialCurrentMouseY: clientY,
}); });
onSnapPointChange?.(initialSnapPoint);
startedDragThisEvent = true; startedDragThisEvent = true;
pendingDragRef.current = null; pendingDragRef.current = null;
setIsPendingDrag(false); 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; const moveGroup = moveGroupRef.current;
if (!moveGroup) { if (!moveGroup) {
endDrag(); endDrag();
@ -562,11 +585,7 @@ export function useElementInteraction({
return; return;
} }
const groupMoveResult = resolveGroupDragMove({ const groupMoveResult = groupMoveResultRef.current;
group: moveGroup,
snappedTime,
dropTarget,
});
if (!groupMoveResult) { if (!groupMoveResult) {
endDrag(); endDrag();
onSnapPointChange?.(null); onSnapPointChange?.(null);
@ -604,18 +623,10 @@ export function useElementInteraction({
dragState.isDragging, dragState.isDragging,
dragState.elementId, dragState.elementId,
dragState.startElementTime, dragState.startElementTime,
dragState.startMouseY,
dragState.trackId, dragState.trackId,
dragState.currentTime,
zoomLevel,
endDrag, endDrag,
onSnapPointChange, onSnapPointChange,
editor.timeline, editor.timeline,
tracksContainerRef,
tracksScrollRef,
headerRef,
resolveGroupDragMove,
sceneTracks,
]); ]);
useEffect(() => { useEffect(() => {

View File

@ -11,6 +11,7 @@ import type {
import type { Transform } from "@/lib/rendering"; import type { Transform } from "@/lib/rendering";
import { buildMoveGroup } from "@/lib/timeline/group-move/build-group"; import { buildMoveGroup } from "@/lib/timeline/group-move/build-group";
import { resolveGroupMove } from "@/lib/timeline/group-move/resolve-move"; import { resolveGroupMove } from "@/lib/timeline/group-move/resolve-move";
import { snapGroupEdges } from "@/lib/timeline/group-move/snap";
function buildTransform(): Transform { function buildTransform(): Transform {
return { 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,
});
});
}); });