mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
codebase overhaul (#697)
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
import type { TimelineTrack, ElementType } from "@/types/timeline";
|
||||
import { TRACK_HEIGHTS, TRACK_GAP } from "@/constants/timeline-constants";
|
||||
import { wouldElementOverlap } from "./element-utils";
|
||||
import type { ComputeDropTargetParams, DropTarget } from "@/types/timeline";
|
||||
import { isMainTrack } from "./track-utils";
|
||||
|
||||
function getTrackAtY({
|
||||
mouseY,
|
||||
tracks,
|
||||
verticalDragDirection,
|
||||
}: {
|
||||
mouseY: number;
|
||||
tracks: TimelineTrack[];
|
||||
verticalDragDirection?: "up" | "down" | null;
|
||||
}): { trackIndex: number; relativeY: number } | null {
|
||||
let cumulativeHeight = 0;
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const trackHeight = TRACK_HEIGHTS[tracks[i].type];
|
||||
const trackTop = cumulativeHeight;
|
||||
const trackBottom = trackTop + trackHeight;
|
||||
|
||||
if (mouseY >= trackTop && mouseY < trackBottom) {
|
||||
return {
|
||||
trackIndex: i,
|
||||
relativeY: mouseY - trackTop,
|
||||
};
|
||||
}
|
||||
|
||||
if (i < tracks.length - 1 && verticalDragDirection) {
|
||||
const gapTop = trackBottom;
|
||||
const gapBottom = gapTop + TRACK_GAP;
|
||||
if (mouseY >= gapTop && mouseY < gapBottom) {
|
||||
const isDraggingUp = verticalDragDirection === "up";
|
||||
return {
|
||||
trackIndex: isDraggingUp ? i : i + 1,
|
||||
relativeY: isDraggingUp ? trackHeight - 1 : 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
cumulativeHeight += trackHeight + TRACK_GAP;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isCompatible({
|
||||
elementType,
|
||||
trackType,
|
||||
}: {
|
||||
elementType: ElementType;
|
||||
trackType: TimelineTrack["type"];
|
||||
}): boolean {
|
||||
if (elementType === "text") return trackType === "text";
|
||||
if (elementType === "audio") return trackType === "audio";
|
||||
if (elementType === "sticker") return trackType === "sticker";
|
||||
if (elementType === "video" || elementType === "image") {
|
||||
return trackType === "video";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getMainTrackIndex({ tracks }: { tracks: TimelineTrack[] }): number {
|
||||
return tracks.findIndex((track) => isMainTrack(track));
|
||||
}
|
||||
|
||||
function findInsertIndex({
|
||||
elementType,
|
||||
tracks,
|
||||
preferredIndex,
|
||||
insertAbove,
|
||||
}: {
|
||||
elementType: ElementType;
|
||||
tracks: TimelineTrack[];
|
||||
preferredIndex: number;
|
||||
insertAbove: boolean;
|
||||
}): { index: number; position: "above" | "below" } {
|
||||
const mainTrackIndex = getMainTrackIndex({ tracks });
|
||||
|
||||
if (elementType === "audio") {
|
||||
if (preferredIndex <= mainTrackIndex) {
|
||||
return { index: mainTrackIndex + 1, position: "below" };
|
||||
}
|
||||
return {
|
||||
index: insertAbove ? preferredIndex : preferredIndex + 1,
|
||||
position: insertAbove ? "above" : "below",
|
||||
};
|
||||
}
|
||||
|
||||
const overlayInsertIndex = insertAbove ? preferredIndex : preferredIndex + 1;
|
||||
|
||||
if (mainTrackIndex >= 0 && overlayInsertIndex > mainTrackIndex) {
|
||||
return { index: mainTrackIndex, position: "above" };
|
||||
}
|
||||
|
||||
return {
|
||||
index: overlayInsertIndex,
|
||||
position: insertAbove ? "above" : "below",
|
||||
};
|
||||
}
|
||||
|
||||
export function computeDropTarget({
|
||||
elementType,
|
||||
mouseX,
|
||||
mouseY,
|
||||
tracks,
|
||||
playheadTime,
|
||||
isExternalDrop,
|
||||
elementDuration,
|
||||
pixelsPerSecond,
|
||||
zoomLevel,
|
||||
verticalDragDirection,
|
||||
startTimeOverride,
|
||||
excludeElementId,
|
||||
}: ComputeDropTargetParams): DropTarget {
|
||||
const xPosition =
|
||||
typeof startTimeOverride === "number"
|
||||
? startTimeOverride
|
||||
: isExternalDrop
|
||||
? playheadTime
|
||||
: Math.max(0, mouseX / (pixelsPerSecond * zoomLevel));
|
||||
|
||||
const mainTrackIndex = getMainTrackIndex({ tracks });
|
||||
|
||||
if (tracks.length === 0) {
|
||||
if (elementType === "audio") {
|
||||
return {
|
||||
trackIndex: 0,
|
||||
isNewTrack: true,
|
||||
insertPosition: "below",
|
||||
xPosition,
|
||||
};
|
||||
}
|
||||
return { trackIndex: 0, isNewTrack: true, insertPosition: null, xPosition };
|
||||
}
|
||||
|
||||
const trackAtMouse = getTrackAtY({ mouseY, tracks, verticalDragDirection });
|
||||
|
||||
if (!trackAtMouse) {
|
||||
const isAboveAllTracks = mouseY < 0;
|
||||
|
||||
if (elementType === "audio") {
|
||||
return {
|
||||
trackIndex: tracks.length,
|
||||
isNewTrack: true,
|
||||
insertPosition: "below",
|
||||
xPosition,
|
||||
};
|
||||
}
|
||||
|
||||
if (isAboveAllTracks) {
|
||||
return {
|
||||
trackIndex: 0,
|
||||
isNewTrack: true,
|
||||
insertPosition: "above",
|
||||
xPosition,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
trackIndex: Math.max(0, mainTrackIndex),
|
||||
isNewTrack: true,
|
||||
insertPosition: "above",
|
||||
xPosition,
|
||||
};
|
||||
}
|
||||
|
||||
const { trackIndex, relativeY } = trackAtMouse;
|
||||
const track = tracks[trackIndex];
|
||||
const trackHeight = TRACK_HEIGHTS[track.type];
|
||||
const isInUpperHalf = relativeY < trackHeight / 2;
|
||||
|
||||
const isTrackCompatible = isCompatible({
|
||||
elementType,
|
||||
trackType: track.type,
|
||||
});
|
||||
|
||||
const endTime = xPosition + elementDuration;
|
||||
const hasOverlap = wouldElementOverlap({
|
||||
elements: track.elements,
|
||||
startTime: xPosition,
|
||||
endTime,
|
||||
excludeElementId,
|
||||
});
|
||||
|
||||
if (isTrackCompatible && !hasOverlap) {
|
||||
return {
|
||||
trackIndex,
|
||||
isNewTrack: false,
|
||||
insertPosition: null,
|
||||
xPosition,
|
||||
};
|
||||
}
|
||||
|
||||
let insertAbove = isInUpperHalf;
|
||||
if (!isTrackCompatible && verticalDragDirection) {
|
||||
insertAbove = verticalDragDirection === "up";
|
||||
}
|
||||
|
||||
const { index, position } = findInsertIndex({
|
||||
elementType,
|
||||
tracks,
|
||||
preferredIndex: trackIndex,
|
||||
insertAbove,
|
||||
});
|
||||
|
||||
return {
|
||||
trackIndex: index,
|
||||
isNewTrack: true,
|
||||
insertPosition: position,
|
||||
xPosition,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDropLineY({
|
||||
dropTarget,
|
||||
tracks,
|
||||
}: {
|
||||
dropTarget: DropTarget;
|
||||
tracks: TimelineTrack[];
|
||||
}): number {
|
||||
const safeTrackIndex = Math.min(
|
||||
Math.max(dropTarget.trackIndex, 0),
|
||||
tracks.length,
|
||||
);
|
||||
let y = 0;
|
||||
|
||||
for (let i = 0; i < safeTrackIndex; i++) {
|
||||
y += TRACK_HEIGHTS[tracks[i].type] + TRACK_GAP;
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
Reference in New Issue
Block a user