refactor: snapping system
This commit is contained in:
parent
ab64c48c92
commit
4d77e3f2bb
|
|
@ -23,7 +23,7 @@ import { TimelinePlayhead } from "./timeline-playhead";
|
|||
import { SelectionBox } from "../../selection-box";
|
||||
import { useSelectionBox } from "@/hooks/timeline/use-selection-box";
|
||||
import { SnapIndicator } from "./snap-indicator";
|
||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
||||
import type { SnapPoint } from "@/lib/timeline/snap-utils";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
import {
|
||||
TIMELINE_CONSTANTS,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useSnapIndicatorPosition } from "@/hooks/timeline/use-snap-indicator-position";
|
||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
||||
import type { SnapPoint } from "@/lib/timeline/snap-utils";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
|
||||
interface SnapIndicatorProps {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useEditor } from "@/hooks/use-editor";
|
|||
import { useAssetsPanelStore } from "@/stores/assets-panel-store";
|
||||
import AudioWaveform from "./audio-waveform";
|
||||
import { useTimelineElementResize } from "@/hooks/timeline/element/use-element-resize";
|
||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
||||
import type { SnapPoint } from "@/lib/timeline/snap-utils";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import {
|
||||
getTrackClasses,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useElementSelection } from "@/hooks/timeline/element/use-element-select
|
|||
import { TimelineElement } from "./timeline-element";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
import type { TimelineElement as TimelineElementType } from "@/types/timeline";
|
||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
||||
import type { SnapPoint } from "@/lib/timeline/snap-utils";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
||||
import type { ElementDragState } from "@/types/timeline";
|
||||
|
|
|
|||
|
|
@ -17,14 +17,16 @@ import { snapTimeToFrame } from "@/lib/time";
|
|||
import { computeDropTarget } from "@/lib/timeline/drop-utils";
|
||||
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
|
||||
import { generateUUID } from "@/utils/id";
|
||||
import { useTimelineSnapping } from "@/hooks/timeline/use-timeline-snapping";
|
||||
import {
|
||||
snapElementEdge,
|
||||
type SnapPoint,
|
||||
} from "@/lib/timeline/snap-utils";
|
||||
import type {
|
||||
DropTarget,
|
||||
ElementDragState,
|
||||
TimelineElement,
|
||||
TimelineTrack,
|
||||
} from "@/types/timeline";
|
||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
||||
|
||||
interface UseElementInteractionProps {
|
||||
zoomLevel: number;
|
||||
|
|
@ -162,7 +164,6 @@ export function useElementInteraction({
|
|||
const editor = useEditor();
|
||||
const isShiftHeldRef = useShiftKey();
|
||||
const tracks = editor.timeline.getTracks();
|
||||
const { snapElementEdge } = useTimelineSnapping();
|
||||
const {
|
||||
isElementSelected,
|
||||
selectElement,
|
||||
|
|
@ -255,14 +256,7 @@ export function useElementInteraction({
|
|||
snapPoint: snapResult.snapPoint,
|
||||
};
|
||||
},
|
||||
[
|
||||
snappingEnabled,
|
||||
editor.playback,
|
||||
snapElementEdge,
|
||||
tracks,
|
||||
zoomLevel,
|
||||
isShiftHeldRef,
|
||||
],
|
||||
[snappingEnabled, editor.playback, tracks, zoomLevel, isShiftHeldRef],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import type { TimelineElement, TimelineTrack } from "@/types/timeline";
|
|||
import { useEditor } from "@/hooks/use-editor";
|
||||
import { useShiftKey } from "@/hooks/use-shift-key";
|
||||
import {
|
||||
useTimelineSnapping,
|
||||
findSnapPoints,
|
||||
snapToNearestPoint,
|
||||
type SnapPoint,
|
||||
} from "@/hooks/timeline/use-timeline-snapping";
|
||||
} from "@/lib/timeline/snap-utils";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
|
||||
export interface ResizeState {
|
||||
|
|
@ -39,7 +40,7 @@ export function useTimelineElementResize({
|
|||
const activeProject = editor.project.getActive();
|
||||
const isShiftHeldRef = useShiftKey();
|
||||
const snappingEnabled = useTimelineStore((state) => state.snappingEnabled);
|
||||
const { findSnapPoints, snapToNearestPoint } = useTimelineSnapping();
|
||||
|
||||
|
||||
const [resizing, setResizing] = useState<ResizeState | null>(null);
|
||||
const [currentTrimStart, setCurrentTrimStart] = useState(element.trimStart);
|
||||
|
|
@ -269,8 +270,6 @@ export function useTimelineElementResize({
|
|||
activeProject.settings.fps,
|
||||
snappingEnabled,
|
||||
editor,
|
||||
findSnapPoints,
|
||||
snapToNearestPoint,
|
||||
element.id,
|
||||
onSnapPointChange,
|
||||
canExtendElementDuration,
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@ import { useShiftKey } from "@/hooks/use-shift-key";
|
|||
import { DRAG_THRESHOLD_PX } from "@/constants/timeline-constants";
|
||||
import { snapTimeToFrame } from "@/lib/time";
|
||||
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
|
||||
import { useTimelineSnapping } from "@/hooks/timeline/use-timeline-snapping";
|
||||
import {
|
||||
findSnapPoints,
|
||||
snapToNearestPoint,
|
||||
type SnapPoint,
|
||||
} from "@/lib/timeline/snap-utils";
|
||||
import type { Bookmark } from "@/types/timeline";
|
||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
||||
|
||||
export interface BookmarkDragState {
|
||||
isDragging: boolean;
|
||||
|
|
@ -47,8 +50,6 @@ export function useBookmarkDrag({
|
|||
const playheadTime = editor.playback.getCurrentTime();
|
||||
const duration = editor.timeline.getTotalDuration();
|
||||
|
||||
const { findSnapPoints, snapToNearestPoint } = useTimelineSnapping();
|
||||
|
||||
const [dragState, setDragState] = useState<BookmarkDragState>({
|
||||
isDragging: false,
|
||||
bookmarkTime: null,
|
||||
|
|
@ -112,16 +113,7 @@ export function useBookmarkDrag({
|
|||
snapPoint: result.snapPoint,
|
||||
};
|
||||
},
|
||||
[
|
||||
snappingEnabled,
|
||||
findSnapPoints,
|
||||
snapToNearestPoint,
|
||||
tracks,
|
||||
playheadTime,
|
||||
bookmarks,
|
||||
zoomLevel,
|
||||
isShiftHeldRef,
|
||||
],
|
||||
[snappingEnabled, tracks, playheadTime, bookmarks, zoomLevel, isShiftHeldRef],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|||
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
||||
import { useEditor } from "../use-editor";
|
||||
import { useShiftKey } from "@/hooks/use-shift-key";
|
||||
import { useTimelineSnapping } from "@/hooks/timeline/use-timeline-snapping";
|
||||
import {
|
||||
findSnapPoints,
|
||||
snapToNearestPoint,
|
||||
} from "@/lib/timeline/snap-utils";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
|
||||
interface UseTimelinePlayheadProps {
|
||||
|
|
@ -28,10 +31,6 @@ export function useTimelinePlayhead({
|
|||
const isPlaying = editor.playback.getIsPlaying();
|
||||
const isScrubbing = editor.playback.getIsScrubbing();
|
||||
const isShiftHeldRef = useShiftKey();
|
||||
const { snapToNearestPoint, findSnapPoints } = useTimelineSnapping({
|
||||
enableElementSnapping: true,
|
||||
enablePlayheadSnapping: false,
|
||||
});
|
||||
|
||||
const seek = useCallback(
|
||||
({ time }: { time: number }) => editor.playback.seek({ time }),
|
||||
|
|
@ -87,6 +86,7 @@ export function useTimelinePlayhead({
|
|||
tracks,
|
||||
playheadTime: frameTime,
|
||||
bookmarks,
|
||||
enablePlayheadSnapping: false,
|
||||
});
|
||||
const snapResult = snapToNearestPoint({
|
||||
targetTime: frameTime,
|
||||
|
|
@ -110,8 +110,6 @@ export function useTimelinePlayhead({
|
|||
isShiftHeldRef,
|
||||
editor.scenes,
|
||||
editor.timeline,
|
||||
findSnapPoints,
|
||||
snapToNearestPoint,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
import { useCallback } from "react";
|
||||
import type { Bookmark, TimelineTrack } from "@/types/timeline";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { BOOKMARK_TIME_EPSILON } from "@/lib/timeline/bookmarks";
|
||||
|
||||
export interface SnapPoint {
|
||||
time: number;
|
||||
type: "element-start" | "element-end" | "playhead" | "bookmark";
|
||||
elementId?: string;
|
||||
trackId?: string;
|
||||
}
|
||||
|
||||
export interface SnapResult {
|
||||
snappedTime: number;
|
||||
snapPoint: SnapPoint | null;
|
||||
snapDistance: number;
|
||||
}
|
||||
|
||||
export interface UseTimelineSnappingOptions {
|
||||
snapThreshold?: number;
|
||||
enableElementSnapping?: boolean;
|
||||
enablePlayheadSnapping?: boolean;
|
||||
enableBookmarkSnapping?: boolean;
|
||||
}
|
||||
|
||||
export function useTimelineSnapping({
|
||||
snapThreshold = 10,
|
||||
enableElementSnapping = true,
|
||||
enablePlayheadSnapping = true,
|
||||
enableBookmarkSnapping = true,
|
||||
}: UseTimelineSnappingOptions = {}) {
|
||||
const findSnapPoints = useCallback(
|
||||
({
|
||||
tracks,
|
||||
playheadTime,
|
||||
excludeElementId,
|
||||
bookmarks = [],
|
||||
excludeBookmarkTime,
|
||||
}: {
|
||||
tracks: Array<TimelineTrack>;
|
||||
playheadTime: number;
|
||||
excludeElementId?: string;
|
||||
bookmarks?: Array<Bookmark>;
|
||||
excludeBookmarkTime?: number;
|
||||
}): SnapPoint[] => {
|
||||
const snapPoints: SnapPoint[] = [];
|
||||
|
||||
if (enableElementSnapping) {
|
||||
for (const track of tracks) {
|
||||
for (const element of track.elements) {
|
||||
if (element.id === excludeElementId) continue;
|
||||
|
||||
const elementStart = element.startTime;
|
||||
const elementEnd = element.startTime + element.duration;
|
||||
|
||||
snapPoints.push(
|
||||
{
|
||||
time: elementStart,
|
||||
type: "element-start",
|
||||
elementId: element.id,
|
||||
trackId: track.id,
|
||||
},
|
||||
{
|
||||
time: elementEnd,
|
||||
type: "element-end",
|
||||
elementId: element.id,
|
||||
trackId: track.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enablePlayheadSnapping) {
|
||||
snapPoints.push({
|
||||
time: playheadTime,
|
||||
type: "playhead",
|
||||
});
|
||||
}
|
||||
|
||||
if (enableBookmarkSnapping) {
|
||||
for (const bookmark of bookmarks) {
|
||||
if (
|
||||
excludeBookmarkTime != null &&
|
||||
Math.abs(bookmark.time - excludeBookmarkTime) <
|
||||
BOOKMARK_TIME_EPSILON
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
snapPoints.push({
|
||||
time: bookmark.time,
|
||||
type: "bookmark",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return snapPoints;
|
||||
},
|
||||
[enableElementSnapping, enablePlayheadSnapping, enableBookmarkSnapping],
|
||||
);
|
||||
|
||||
const snapToNearestPoint = useCallback(
|
||||
({
|
||||
targetTime,
|
||||
snapPoints,
|
||||
zoomLevel,
|
||||
}: {
|
||||
targetTime: number;
|
||||
snapPoints: Array<SnapPoint>;
|
||||
zoomLevel: number;
|
||||
}): SnapResult => {
|
||||
const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||
const thresholdInSeconds = snapThreshold / pixelsPerSecond;
|
||||
|
||||
let closestSnapPoint: SnapPoint | null = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
for (const snapPoint of snapPoints) {
|
||||
const distance = Math.abs(targetTime - snapPoint.time);
|
||||
if (distance < thresholdInSeconds && distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestSnapPoint = snapPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
snappedTime: closestSnapPoint ? closestSnapPoint.time : targetTime,
|
||||
snapPoint: closestSnapPoint,
|
||||
snapDistance: closestDistance,
|
||||
};
|
||||
},
|
||||
[snapThreshold],
|
||||
);
|
||||
|
||||
const snapElementEdge = useCallback(
|
||||
({
|
||||
targetTime,
|
||||
elementDuration,
|
||||
tracks,
|
||||
playheadTime,
|
||||
zoomLevel,
|
||||
excludeElementId,
|
||||
snapToStart = true,
|
||||
bookmarks = [],
|
||||
}: {
|
||||
targetTime: number;
|
||||
elementDuration: number;
|
||||
tracks: Array<TimelineTrack>;
|
||||
playheadTime: number;
|
||||
zoomLevel: number;
|
||||
excludeElementId?: string;
|
||||
snapToStart?: boolean;
|
||||
bookmarks?: Array<Bookmark>;
|
||||
}): SnapResult => {
|
||||
const snapPoints = findSnapPoints({
|
||||
tracks,
|
||||
playheadTime,
|
||||
excludeElementId,
|
||||
bookmarks,
|
||||
});
|
||||
|
||||
const effectiveTargetTime = snapToStart
|
||||
? targetTime
|
||||
: targetTime + elementDuration;
|
||||
const snapResult = snapToNearestPoint({
|
||||
targetTime: effectiveTargetTime,
|
||||
snapPoints,
|
||||
zoomLevel,
|
||||
});
|
||||
|
||||
if (!snapToStart && snapResult.snapPoint) {
|
||||
snapResult.snappedTime = snapResult.snappedTime - elementDuration;
|
||||
}
|
||||
|
||||
return snapResult;
|
||||
},
|
||||
[findSnapPoints, snapToNearestPoint],
|
||||
);
|
||||
|
||||
return {
|
||||
snapElementEdge,
|
||||
findSnapPoints,
|
||||
snapToNearestPoint,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import type { Bookmark, TimelineTrack } from "@/types/timeline";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
import { BOOKMARK_TIME_EPSILON } from "@/lib/timeline/bookmarks";
|
||||
|
||||
export interface SnapPoint {
|
||||
time: number;
|
||||
type: "element-start" | "element-end" | "playhead" | "bookmark";
|
||||
elementId?: string;
|
||||
trackId?: string;
|
||||
}
|
||||
|
||||
export interface SnapResult {
|
||||
snappedTime: number;
|
||||
snapPoint: SnapPoint | null;
|
||||
snapDistance: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SNAP_THRESHOLD_PX = 10;
|
||||
|
||||
export function findSnapPoints({
|
||||
tracks,
|
||||
playheadTime,
|
||||
excludeElementId,
|
||||
bookmarks = [],
|
||||
excludeBookmarkTime,
|
||||
enableElementSnapping = true,
|
||||
enablePlayheadSnapping = true,
|
||||
enableBookmarkSnapping = true,
|
||||
}: {
|
||||
tracks: Array<TimelineTrack>;
|
||||
playheadTime: number;
|
||||
excludeElementId?: string;
|
||||
bookmarks?: Array<Bookmark>;
|
||||
excludeBookmarkTime?: number;
|
||||
enableElementSnapping?: boolean;
|
||||
enablePlayheadSnapping?: boolean;
|
||||
enableBookmarkSnapping?: boolean;
|
||||
}): SnapPoint[] {
|
||||
const snapPoints: SnapPoint[] = [];
|
||||
|
||||
if (enableElementSnapping) {
|
||||
for (const track of tracks) {
|
||||
for (const element of track.elements) {
|
||||
if (element.id === excludeElementId) continue;
|
||||
snapPoints.push(
|
||||
{
|
||||
time: element.startTime,
|
||||
type: "element-start",
|
||||
elementId: element.id,
|
||||
trackId: track.id,
|
||||
},
|
||||
{
|
||||
time: element.startTime + element.duration,
|
||||
type: "element-end",
|
||||
elementId: element.id,
|
||||
trackId: track.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enablePlayheadSnapping) {
|
||||
snapPoints.push({ time: playheadTime, type: "playhead" });
|
||||
}
|
||||
|
||||
if (enableBookmarkSnapping) {
|
||||
for (const bookmark of bookmarks) {
|
||||
if (
|
||||
excludeBookmarkTime != null &&
|
||||
Math.abs(bookmark.time - excludeBookmarkTime) < BOOKMARK_TIME_EPSILON
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
snapPoints.push({ time: bookmark.time, type: "bookmark" });
|
||||
}
|
||||
}
|
||||
|
||||
return snapPoints;
|
||||
}
|
||||
|
||||
export function snapToNearestPoint({
|
||||
targetTime,
|
||||
snapPoints,
|
||||
zoomLevel,
|
||||
snapThreshold = DEFAULT_SNAP_THRESHOLD_PX,
|
||||
}: {
|
||||
targetTime: number;
|
||||
snapPoints: Array<SnapPoint>;
|
||||
zoomLevel: number;
|
||||
snapThreshold?: number;
|
||||
}): SnapResult {
|
||||
const pixelsPerSecond = TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
|
||||
const thresholdInSeconds = snapThreshold / pixelsPerSecond;
|
||||
|
||||
let closestSnapPoint: SnapPoint | null = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
for (const snapPoint of snapPoints) {
|
||||
const distance = Math.abs(targetTime - snapPoint.time);
|
||||
if (distance < thresholdInSeconds && distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestSnapPoint = snapPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
snappedTime: closestSnapPoint ? closestSnapPoint.time : targetTime,
|
||||
snapPoint: closestSnapPoint,
|
||||
snapDistance: closestDistance,
|
||||
};
|
||||
}
|
||||
|
||||
export function snapElementEdge({
|
||||
targetTime,
|
||||
elementDuration,
|
||||
tracks,
|
||||
playheadTime,
|
||||
zoomLevel,
|
||||
excludeElementId,
|
||||
snapToStart = true,
|
||||
bookmarks = [],
|
||||
}: {
|
||||
targetTime: number;
|
||||
elementDuration: number;
|
||||
tracks: Array<TimelineTrack>;
|
||||
playheadTime: number;
|
||||
zoomLevel: number;
|
||||
excludeElementId?: string;
|
||||
snapToStart?: boolean;
|
||||
bookmarks?: Array<Bookmark>;
|
||||
}): SnapResult {
|
||||
const snapPoints = findSnapPoints({
|
||||
tracks,
|
||||
playheadTime,
|
||||
excludeElementId,
|
||||
bookmarks,
|
||||
});
|
||||
|
||||
const effectiveTargetTime = snapToStart
|
||||
? targetTime
|
||||
: targetTime + elementDuration;
|
||||
|
||||
const snapResult = snapToNearestPoint({
|
||||
targetTime: effectiveTargetTime,
|
||||
snapPoints,
|
||||
zoomLevel,
|
||||
});
|
||||
|
||||
if (!snapToStart && snapResult.snapPoint) {
|
||||
snapResult.snappedTime = snapResult.snappedTime - elementDuration;
|
||||
}
|
||||
|
||||
return snapResult;
|
||||
}
|
||||
Loading…
Reference in New Issue