mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
275 lines
5.7 KiB
TypeScript
275 lines
5.7 KiB
TypeScript
import type { TrackType, TimelineTrack } from "@/lib/timeline";
|
|
import {
|
|
getDefaultInsertIndexForTrack,
|
|
getHighestInsertIndexForTrack,
|
|
resolvePreferredNewTrackPlacement,
|
|
} from "./insert-index";
|
|
import { getTrackTypeForElementType } from "./compatibility";
|
|
import { enforceMainTrackStart } from "./main-track";
|
|
import { canPlaceTimeSpansOnTrack } from "./overlap";
|
|
import type {
|
|
PlacementResult,
|
|
PlacementStrategy,
|
|
PlacementSubject,
|
|
PlacementTimeSpan,
|
|
} from "./types";
|
|
|
|
type ResolveTrackPlacementParams = PlacementSubject & {
|
|
tracks: TimelineTrack[];
|
|
timeSpans: PlacementTimeSpan[];
|
|
strategy: PlacementStrategy;
|
|
};
|
|
|
|
function buildExistingTrackResult({
|
|
track,
|
|
trackIndex,
|
|
tracks,
|
|
timeSpans,
|
|
}: {
|
|
track: TimelineTrack;
|
|
trackIndex: number;
|
|
tracks: TimelineTrack[];
|
|
timeSpans: PlacementTimeSpan[];
|
|
}): PlacementResult {
|
|
const firstSpan = timeSpans[0];
|
|
const requestedStartTime = firstSpan?.startTime ?? 0;
|
|
const adjustedStartTime = enforceMainTrackStart({
|
|
tracks,
|
|
targetTrackId: track.id,
|
|
requestedStartTime,
|
|
excludeElementId: firstSpan?.excludeElementId,
|
|
});
|
|
return {
|
|
kind: "existingTrack",
|
|
trackId: track.id,
|
|
trackIndex,
|
|
trackType: track.type,
|
|
...(adjustedStartTime !== requestedStartTime ? { adjustedStartTime } : {}),
|
|
};
|
|
}
|
|
|
|
function buildNewTrackResult({
|
|
trackType,
|
|
insertIndex,
|
|
insertPosition,
|
|
}: {
|
|
trackType: TrackType;
|
|
insertIndex: number;
|
|
insertPosition: "above" | "below" | null;
|
|
}): PlacementResult {
|
|
return {
|
|
kind: "newTrack",
|
|
trackType,
|
|
insertIndex,
|
|
insertPosition,
|
|
};
|
|
}
|
|
|
|
function findFirstAvailableTrackIndex({
|
|
tracks,
|
|
trackType,
|
|
timeSpans,
|
|
}: {
|
|
tracks: TimelineTrack[];
|
|
trackType: TrackType;
|
|
timeSpans: PlacementTimeSpan[];
|
|
}): number {
|
|
return tracks.findIndex((track) => {
|
|
return (
|
|
track.type === trackType &&
|
|
canPlaceTimeSpansOnTrack({
|
|
track,
|
|
timeSpans,
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
function resolveAlwaysNewTrack({
|
|
tracks,
|
|
trackType,
|
|
position,
|
|
}: {
|
|
tracks: TimelineTrack[];
|
|
trackType: TrackType;
|
|
position: "highest" | "default";
|
|
}): PlacementResult {
|
|
const insertIndex =
|
|
position === "highest"
|
|
? getHighestInsertIndexForTrack({
|
|
tracks,
|
|
trackType,
|
|
})
|
|
: getDefaultInsertIndexForTrack({
|
|
tracks,
|
|
trackType,
|
|
});
|
|
|
|
return buildNewTrackResult({
|
|
trackType,
|
|
insertIndex,
|
|
insertPosition: null,
|
|
});
|
|
}
|
|
|
|
function getInsertDirection({
|
|
hoverDirection,
|
|
verticalDragDirection,
|
|
}: {
|
|
hoverDirection: "above" | "below";
|
|
verticalDragDirection?: "up" | "down" | null;
|
|
}): "above" | "below" {
|
|
if (verticalDragDirection === "up") {
|
|
return "above";
|
|
}
|
|
|
|
if (verticalDragDirection === "down") {
|
|
return "below";
|
|
}
|
|
|
|
return hoverDirection;
|
|
}
|
|
|
|
export function resolveTrackPlacement({
|
|
tracks,
|
|
...placement
|
|
}: ResolveTrackPlacementParams): PlacementResult | null {
|
|
const trackType =
|
|
"trackType" in placement
|
|
? placement.trackType
|
|
: getTrackTypeForElementType({
|
|
elementType: placement.elementType,
|
|
});
|
|
const { timeSpans, strategy } = placement;
|
|
|
|
if (strategy.type === "explicit") {
|
|
const trackIndex = tracks.findIndex(
|
|
(track) => track.id === strategy.trackId,
|
|
);
|
|
if (trackIndex < 0) {
|
|
return null;
|
|
}
|
|
|
|
const track = tracks[trackIndex];
|
|
if (track.type !== trackType) {
|
|
return null;
|
|
}
|
|
|
|
return buildExistingTrackResult({ track, trackIndex, tracks, timeSpans });
|
|
}
|
|
|
|
if (strategy.type === "firstAvailable") {
|
|
const existingTrackIndex = findFirstAvailableTrackIndex({
|
|
tracks,
|
|
trackType,
|
|
timeSpans,
|
|
});
|
|
if (existingTrackIndex >= 0) {
|
|
return buildExistingTrackResult({
|
|
track: tracks[existingTrackIndex],
|
|
trackIndex: existingTrackIndex,
|
|
tracks,
|
|
timeSpans,
|
|
});
|
|
}
|
|
|
|
return resolveAlwaysNewTrack({
|
|
tracks,
|
|
trackType,
|
|
position: "default",
|
|
});
|
|
}
|
|
|
|
if (strategy.type === "preferIndex") {
|
|
const preferredTrack = tracks[strategy.trackIndex];
|
|
const isPreferredTrackCompatible =
|
|
!!preferredTrack && preferredTrack.type === trackType;
|
|
const canUseExistingTrack =
|
|
!strategy.createNewTrackOnly &&
|
|
isPreferredTrackCompatible &&
|
|
canPlaceTimeSpansOnTrack({
|
|
track: preferredTrack,
|
|
timeSpans,
|
|
});
|
|
if (canUseExistingTrack) {
|
|
return buildExistingTrackResult({
|
|
track: preferredTrack,
|
|
trackIndex: strategy.trackIndex,
|
|
tracks,
|
|
timeSpans,
|
|
});
|
|
}
|
|
|
|
const { insertIndex, insertPosition } = resolvePreferredNewTrackPlacement({
|
|
tracks,
|
|
trackType,
|
|
preferredIndex: strategy.trackIndex,
|
|
direction: getInsertDirection({
|
|
hoverDirection: strategy.hoverDirection,
|
|
verticalDragDirection: !isPreferredTrackCompatible
|
|
? strategy.verticalDragDirection
|
|
: null,
|
|
}),
|
|
});
|
|
return buildNewTrackResult({
|
|
trackType,
|
|
insertIndex,
|
|
insertPosition,
|
|
});
|
|
}
|
|
|
|
if (strategy.type === "aboveSource") {
|
|
const aboveTrackIndex = strategy.sourceTrackIndex - 1;
|
|
const aboveTrack = tracks[aboveTrackIndex];
|
|
if (
|
|
aboveTrack &&
|
|
aboveTrack.type === trackType &&
|
|
canPlaceTimeSpansOnTrack({
|
|
track: aboveTrack,
|
|
timeSpans,
|
|
})
|
|
) {
|
|
return buildExistingTrackResult({
|
|
track: aboveTrack,
|
|
trackIndex: aboveTrackIndex,
|
|
tracks,
|
|
timeSpans,
|
|
});
|
|
}
|
|
|
|
const firstAvailableTrackIndex = findFirstAvailableTrackIndex({
|
|
tracks,
|
|
trackType,
|
|
timeSpans,
|
|
});
|
|
if (firstAvailableTrackIndex >= 0) {
|
|
return buildExistingTrackResult({
|
|
track: tracks[firstAvailableTrackIndex],
|
|
trackIndex: firstAvailableTrackIndex,
|
|
tracks,
|
|
timeSpans,
|
|
});
|
|
}
|
|
|
|
const insertIndex =
|
|
strategy.sourceTrackIndex >= 0
|
|
? strategy.sourceTrackIndex
|
|
: getHighestInsertIndexForTrack({
|
|
tracks,
|
|
trackType,
|
|
});
|
|
|
|
return buildNewTrackResult({
|
|
trackType,
|
|
insertIndex,
|
|
insertPosition: null,
|
|
});
|
|
}
|
|
|
|
return resolveAlwaysNewTrack({
|
|
tracks,
|
|
trackType,
|
|
position: strategy.position,
|
|
});
|
|
}
|