mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
279 lines
6.8 KiB
TypeScript
279 lines
6.8 KiB
TypeScript
import {
|
|
useState,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
type RefObject,
|
|
} from "react";
|
|
import { useEditor } from "@/hooks/use-editor";
|
|
import { useShiftKey } from "@/hooks/use-shift-key";
|
|
import { TIMELINE_DRAG_THRESHOLD_PX } from "@/components/editor/panels/timeline/interaction";
|
|
import { roundToFrame } from "opencut-wasm";
|
|
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
|
|
import {
|
|
findSnapPoints,
|
|
snapToNearestPoint,
|
|
type SnapPoint,
|
|
} from "@/lib/timeline/snap-utils";
|
|
import type { Bookmark } from "@/lib/timeline";
|
|
|
|
export interface BookmarkDragState {
|
|
isDragging: boolean;
|
|
bookmarkTime: number | null;
|
|
currentTime: number;
|
|
}
|
|
|
|
interface PendingBookmarkDrag {
|
|
bookmarkTime: number;
|
|
startMouseX: number;
|
|
startMouseY: number;
|
|
}
|
|
|
|
interface UseBookmarkDragProps {
|
|
zoomLevel: number;
|
|
scrollRef: RefObject<HTMLElement | null>;
|
|
snappingEnabled: boolean;
|
|
onSnapPointChange?: (snapPoint: SnapPoint | null) => void;
|
|
}
|
|
|
|
export function useBookmarkDrag({
|
|
zoomLevel,
|
|
scrollRef,
|
|
snappingEnabled,
|
|
onSnapPointChange,
|
|
}: UseBookmarkDragProps) {
|
|
const editor = useEditor();
|
|
const isShiftHeldRef = useShiftKey();
|
|
const tracks = editor.scenes.getActiveScene().tracks;
|
|
const activeScene = editor.scenes.getActiveScene();
|
|
const bookmarks = activeScene?.bookmarks ?? [];
|
|
const playheadTime = editor.playback.getCurrentTime();
|
|
const duration = editor.timeline.getTotalDuration();
|
|
|
|
const [dragState, setDragState] = useState<BookmarkDragState>({
|
|
isDragging: false,
|
|
bookmarkTime: null,
|
|
currentTime: 0,
|
|
});
|
|
const [isPendingDrag, setIsPendingDrag] = useState(false);
|
|
const pendingDragRef = useRef<PendingBookmarkDrag | null>(null);
|
|
const lastMouseXRef = useRef(0);
|
|
|
|
const startDrag = useCallback(
|
|
({
|
|
bookmarkTime,
|
|
initialCurrentTime,
|
|
}: {
|
|
bookmarkTime: number;
|
|
initialCurrentTime: number;
|
|
}) => {
|
|
setDragState({
|
|
isDragging: true,
|
|
bookmarkTime,
|
|
currentTime: initialCurrentTime,
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const endDrag = useCallback(() => {
|
|
setDragState({
|
|
isDragging: false,
|
|
bookmarkTime: null,
|
|
currentTime: 0,
|
|
});
|
|
}, []);
|
|
|
|
const getSnapResult = useCallback(
|
|
({
|
|
rawTime,
|
|
excludeBookmarkTime,
|
|
}: {
|
|
rawTime: number;
|
|
excludeBookmarkTime: number;
|
|
}): { snappedTime: number; snapPoint: SnapPoint | null } => {
|
|
const shouldSnap = snappingEnabled && !isShiftHeldRef.current;
|
|
if (!shouldSnap) {
|
|
return { snappedTime: rawTime, snapPoint: null };
|
|
}
|
|
|
|
const snapPoints = findSnapPoints({
|
|
tracks,
|
|
playheadTime,
|
|
bookmarks,
|
|
excludeBookmarkTime,
|
|
});
|
|
const result = snapToNearestPoint({
|
|
targetTime: rawTime,
|
|
snapPoints,
|
|
zoomLevel,
|
|
});
|
|
return {
|
|
snappedTime: result.snappedTime,
|
|
snapPoint: result.snapPoint,
|
|
};
|
|
},
|
|
[snappingEnabled, tracks, playheadTime, bookmarks, zoomLevel, isShiftHeldRef],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!dragState.isDragging && !isPendingDrag) return;
|
|
|
|
const handleMouseMove = (event: MouseEvent) => {
|
|
lastMouseXRef.current = event.clientX;
|
|
|
|
const scrollContainer = scrollRef.current;
|
|
if (!scrollContainer) return;
|
|
|
|
if (isPendingDrag && pendingDragRef.current) {
|
|
const { startMouseX, startMouseY, bookmarkTime } =
|
|
pendingDragRef.current;
|
|
const deltaX = Math.abs(event.clientX - startMouseX);
|
|
const deltaY = Math.abs(event.clientY - startMouseY);
|
|
|
|
if (
|
|
deltaX <= TIMELINE_DRAG_THRESHOLD_PX &&
|
|
deltaY <= TIMELINE_DRAG_THRESHOLD_PX
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const activeProject = editor.project.getActive();
|
|
if (!activeProject) return;
|
|
|
|
const scrollLeft = scrollContainer.scrollLeft;
|
|
const mouseTime = getMouseTimeFromClientX({
|
|
clientX: event.clientX,
|
|
containerRect: scrollContainer.getBoundingClientRect(),
|
|
zoomLevel,
|
|
scrollLeft,
|
|
});
|
|
const frameSnappedTime = roundToFrame({ time: Math.max(0, Math.min(mouseTime, duration)), rate: activeProject.settings.fps }) ?? Math.max(0, Math.min(mouseTime, duration));
|
|
const { snappedTime: initialTime } = getSnapResult({
|
|
rawTime: frameSnappedTime,
|
|
excludeBookmarkTime: bookmarkTime,
|
|
});
|
|
|
|
startDrag({
|
|
bookmarkTime,
|
|
initialCurrentTime: initialTime,
|
|
});
|
|
pendingDragRef.current = null;
|
|
setIsPendingDrag(false);
|
|
return;
|
|
}
|
|
|
|
if (!dragState.isDragging || dragState.bookmarkTime === null) return;
|
|
|
|
const activeProject = editor.project.getActive();
|
|
if (!activeProject) return;
|
|
|
|
const scrollLeft = scrollContainer.scrollLeft;
|
|
const mouseTime = getMouseTimeFromClientX({
|
|
clientX: event.clientX,
|
|
containerRect: scrollContainer.getBoundingClientRect(),
|
|
zoomLevel,
|
|
scrollLeft,
|
|
});
|
|
const clampedTime = Math.max(0, Math.min(mouseTime, duration));
|
|
const frameSnappedTime = roundToFrame({ time: clampedTime, rate: activeProject.settings.fps }) ?? clampedTime;
|
|
const snapResult = getSnapResult({
|
|
rawTime: frameSnappedTime,
|
|
excludeBookmarkTime: dragState.bookmarkTime,
|
|
});
|
|
|
|
setDragState((previousDragState) => ({
|
|
...previousDragState,
|
|
currentTime: snapResult.snappedTime,
|
|
}));
|
|
onSnapPointChange?.(snapResult.snapPoint);
|
|
};
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
return () => document.removeEventListener("mousemove", handleMouseMove);
|
|
}, [
|
|
dragState.isDragging,
|
|
dragState.bookmarkTime,
|
|
zoomLevel,
|
|
duration,
|
|
editor.project,
|
|
scrollRef,
|
|
isPendingDrag,
|
|
startDrag,
|
|
getSnapResult,
|
|
onSnapPointChange,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!dragState.isDragging) return;
|
|
|
|
const handleMouseUp = () => {
|
|
if (dragState.bookmarkTime === null) {
|
|
endDrag();
|
|
onSnapPointChange?.(null);
|
|
return;
|
|
}
|
|
|
|
const clampedTime = Math.max(
|
|
0,
|
|
Math.min(dragState.currentTime, duration),
|
|
);
|
|
|
|
editor.scenes.moveBookmark({
|
|
fromTime: dragState.bookmarkTime,
|
|
toTime: clampedTime,
|
|
});
|
|
|
|
endDrag();
|
|
onSnapPointChange?.(null);
|
|
};
|
|
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
return () => document.removeEventListener("mouseup", handleMouseUp);
|
|
}, [
|
|
dragState.isDragging,
|
|
dragState.bookmarkTime,
|
|
dragState.currentTime,
|
|
duration,
|
|
endDrag,
|
|
onSnapPointChange,
|
|
editor.scenes,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!isPendingDrag) return;
|
|
|
|
const handleMouseUp = () => {
|
|
pendingDragRef.current = null;
|
|
setIsPendingDrag(false);
|
|
onSnapPointChange?.(null);
|
|
};
|
|
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
return () => document.removeEventListener("mouseup", handleMouseUp);
|
|
}, [isPendingDrag, onSnapPointChange]);
|
|
|
|
const handleBookmarkMouseDown = useCallback(
|
|
({ event, bookmark }: { event: React.MouseEvent; bookmark: Bookmark }) => {
|
|
if (event.button !== 0) return;
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
pendingDragRef.current = {
|
|
bookmarkTime: bookmark.time,
|
|
startMouseX: event.clientX,
|
|
startMouseY: event.clientY,
|
|
};
|
|
setIsPendingDrag(true);
|
|
},
|
|
[],
|
|
);
|
|
|
|
return {
|
|
dragState,
|
|
handleBookmarkMouseDown,
|
|
lastMouseXRef,
|
|
};
|
|
}
|