mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: snapping system
This commit is contained in:
@@ -23,7 +23,7 @@ import { TimelinePlayhead } from "./timeline-playhead";
|
|||||||
import { SelectionBox } from "../../selection-box";
|
import { SelectionBox } from "../../selection-box";
|
||||||
import { useSelectionBox } from "@/hooks/timeline/use-selection-box";
|
import { useSelectionBox } from "@/hooks/timeline/use-selection-box";
|
||||||
import { SnapIndicator } from "./snap-indicator";
|
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 type { TimelineTrack } from "@/types/timeline";
|
||||||
import {
|
import {
|
||||||
TIMELINE_CONSTANTS,
|
TIMELINE_CONSTANTS,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSnapIndicatorPosition } from "@/hooks/timeline/use-snap-indicator-position";
|
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";
|
import type { TimelineTrack } from "@/types/timeline";
|
||||||
|
|
||||||
interface SnapIndicatorProps {
|
interface SnapIndicatorProps {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEditor } from "@/hooks/use-editor";
|
|||||||
import { useAssetsPanelStore } from "@/stores/assets-panel-store";
|
import { useAssetsPanelStore } from "@/stores/assets-panel-store";
|
||||||
import AudioWaveform from "./audio-waveform";
|
import AudioWaveform from "./audio-waveform";
|
||||||
import { useTimelineElementResize } from "@/hooks/timeline/element/use-element-resize";
|
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 { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import {
|
import {
|
||||||
getTrackClasses,
|
getTrackClasses,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useElementSelection } from "@/hooks/timeline/element/use-element-select
|
|||||||
import { TimelineElement } from "./timeline-element";
|
import { TimelineElement } from "./timeline-element";
|
||||||
import type { TimelineTrack } from "@/types/timeline";
|
import type { TimelineTrack } from "@/types/timeline";
|
||||||
import type { TimelineElement as TimelineElementType } 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 { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
||||||
import type { ElementDragState } from "@/types/timeline";
|
import type { ElementDragState } from "@/types/timeline";
|
||||||
|
|||||||
@@ -17,14 +17,16 @@ import { snapTimeToFrame } from "@/lib/time";
|
|||||||
import { computeDropTarget } from "@/lib/timeline/drop-utils";
|
import { computeDropTarget } from "@/lib/timeline/drop-utils";
|
||||||
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
|
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
|
||||||
import { generateUUID } from "@/utils/id";
|
import { generateUUID } from "@/utils/id";
|
||||||
import { useTimelineSnapping } from "@/hooks/timeline/use-timeline-snapping";
|
import {
|
||||||
|
snapElementEdge,
|
||||||
|
type SnapPoint,
|
||||||
|
} from "@/lib/timeline/snap-utils";
|
||||||
import type {
|
import type {
|
||||||
DropTarget,
|
DropTarget,
|
||||||
ElementDragState,
|
ElementDragState,
|
||||||
TimelineElement,
|
TimelineElement,
|
||||||
TimelineTrack,
|
TimelineTrack,
|
||||||
} from "@/types/timeline";
|
} from "@/types/timeline";
|
||||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
|
||||||
|
|
||||||
interface UseElementInteractionProps {
|
interface UseElementInteractionProps {
|
||||||
zoomLevel: number;
|
zoomLevel: number;
|
||||||
@@ -162,7 +164,6 @@ export function useElementInteraction({
|
|||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const isShiftHeldRef = useShiftKey();
|
const isShiftHeldRef = useShiftKey();
|
||||||
const tracks = editor.timeline.getTracks();
|
const tracks = editor.timeline.getTracks();
|
||||||
const { snapElementEdge } = useTimelineSnapping();
|
|
||||||
const {
|
const {
|
||||||
isElementSelected,
|
isElementSelected,
|
||||||
selectElement,
|
selectElement,
|
||||||
@@ -255,14 +256,7 @@ export function useElementInteraction({
|
|||||||
snapPoint: snapResult.snapPoint,
|
snapPoint: snapResult.snapPoint,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[snappingEnabled, editor.playback, tracks, zoomLevel, isShiftHeldRef],
|
||||||
snappingEnabled,
|
|
||||||
editor.playback,
|
|
||||||
snapElementEdge,
|
|
||||||
tracks,
|
|
||||||
zoomLevel,
|
|
||||||
isShiftHeldRef,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import type { TimelineElement, TimelineTrack } from "@/types/timeline";
|
|||||||
import { useEditor } from "@/hooks/use-editor";
|
import { useEditor } from "@/hooks/use-editor";
|
||||||
import { useShiftKey } from "@/hooks/use-shift-key";
|
import { useShiftKey } from "@/hooks/use-shift-key";
|
||||||
import {
|
import {
|
||||||
useTimelineSnapping,
|
findSnapPoints,
|
||||||
|
snapToNearestPoint,
|
||||||
type SnapPoint,
|
type SnapPoint,
|
||||||
} from "@/hooks/timeline/use-timeline-snapping";
|
} from "@/lib/timeline/snap-utils";
|
||||||
import { useTimelineStore } from "@/stores/timeline-store";
|
import { useTimelineStore } from "@/stores/timeline-store";
|
||||||
|
|
||||||
export interface ResizeState {
|
export interface ResizeState {
|
||||||
@@ -39,7 +40,7 @@ export function useTimelineElementResize({
|
|||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
const isShiftHeldRef = useShiftKey();
|
const isShiftHeldRef = useShiftKey();
|
||||||
const snappingEnabled = useTimelineStore((state) => state.snappingEnabled);
|
const snappingEnabled = useTimelineStore((state) => state.snappingEnabled);
|
||||||
const { findSnapPoints, snapToNearestPoint } = useTimelineSnapping();
|
|
||||||
|
|
||||||
const [resizing, setResizing] = useState<ResizeState | null>(null);
|
const [resizing, setResizing] = useState<ResizeState | null>(null);
|
||||||
const [currentTrimStart, setCurrentTrimStart] = useState(element.trimStart);
|
const [currentTrimStart, setCurrentTrimStart] = useState(element.trimStart);
|
||||||
@@ -269,8 +270,6 @@ export function useTimelineElementResize({
|
|||||||
activeProject.settings.fps,
|
activeProject.settings.fps,
|
||||||
snappingEnabled,
|
snappingEnabled,
|
||||||
editor,
|
editor,
|
||||||
findSnapPoints,
|
|
||||||
snapToNearestPoint,
|
|
||||||
element.id,
|
element.id,
|
||||||
onSnapPointChange,
|
onSnapPointChange,
|
||||||
canExtendElementDuration,
|
canExtendElementDuration,
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ import { useShiftKey } from "@/hooks/use-shift-key";
|
|||||||
import { DRAG_THRESHOLD_PX } from "@/constants/timeline-constants";
|
import { DRAG_THRESHOLD_PX } from "@/constants/timeline-constants";
|
||||||
import { snapTimeToFrame } from "@/lib/time";
|
import { snapTimeToFrame } from "@/lib/time";
|
||||||
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";
|
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 { Bookmark } from "@/types/timeline";
|
||||||
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
|
|
||||||
|
|
||||||
export interface BookmarkDragState {
|
export interface BookmarkDragState {
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
@@ -47,8 +50,6 @@ export function useBookmarkDrag({
|
|||||||
const playheadTime = editor.playback.getCurrentTime();
|
const playheadTime = editor.playback.getCurrentTime();
|
||||||
const duration = editor.timeline.getTotalDuration();
|
const duration = editor.timeline.getTotalDuration();
|
||||||
|
|
||||||
const { findSnapPoints, snapToNearestPoint } = useTimelineSnapping();
|
|
||||||
|
|
||||||
const [dragState, setDragState] = useState<BookmarkDragState>({
|
const [dragState, setDragState] = useState<BookmarkDragState>({
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
bookmarkTime: null,
|
bookmarkTime: null,
|
||||||
@@ -112,16 +113,7 @@ export function useBookmarkDrag({
|
|||||||
snapPoint: result.snapPoint,
|
snapPoint: result.snapPoint,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[snappingEnabled, tracks, playheadTime, bookmarks, zoomLevel, isShiftHeldRef],
|
||||||
snappingEnabled,
|
|
||||||
findSnapPoints,
|
|
||||||
snapToNearestPoint,
|
|
||||||
tracks,
|
|
||||||
playheadTime,
|
|
||||||
bookmarks,
|
|
||||||
zoomLevel,
|
|
||||||
isShiftHeldRef,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { useState, useEffect, useCallback, useRef } from "react";
|
|||||||
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
|
||||||
import { useEditor } from "../use-editor";
|
import { useEditor } from "../use-editor";
|
||||||
import { useShiftKey } from "@/hooks/use-shift-key";
|
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";
|
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||||
|
|
||||||
interface UseTimelinePlayheadProps {
|
interface UseTimelinePlayheadProps {
|
||||||
@@ -28,10 +31,6 @@ export function useTimelinePlayhead({
|
|||||||
const isPlaying = editor.playback.getIsPlaying();
|
const isPlaying = editor.playback.getIsPlaying();
|
||||||
const isScrubbing = editor.playback.getIsScrubbing();
|
const isScrubbing = editor.playback.getIsScrubbing();
|
||||||
const isShiftHeldRef = useShiftKey();
|
const isShiftHeldRef = useShiftKey();
|
||||||
const { snapToNearestPoint, findSnapPoints } = useTimelineSnapping({
|
|
||||||
enableElementSnapping: true,
|
|
||||||
enablePlayheadSnapping: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const seek = useCallback(
|
const seek = useCallback(
|
||||||
({ time }: { time: number }) => editor.playback.seek({ time }),
|
({ time }: { time: number }) => editor.playback.seek({ time }),
|
||||||
@@ -87,6 +86,7 @@ export function useTimelinePlayhead({
|
|||||||
tracks,
|
tracks,
|
||||||
playheadTime: frameTime,
|
playheadTime: frameTime,
|
||||||
bookmarks,
|
bookmarks,
|
||||||
|
enablePlayheadSnapping: false,
|
||||||
});
|
});
|
||||||
const snapResult = snapToNearestPoint({
|
const snapResult = snapToNearestPoint({
|
||||||
targetTime: frameTime,
|
targetTime: frameTime,
|
||||||
@@ -110,8 +110,6 @@ export function useTimelinePlayhead({
|
|||||||
isShiftHeldRef,
|
isShiftHeldRef,
|
||||||
editor.scenes,
|
editor.scenes,
|
||||||
editor.timeline,
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user