mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: centralize track placement into its own module
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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 insertDirection =
|
||||
!isPreferredTrackCompatible && strategy.verticalDragDirection
|
||||
? strategy.verticalDragDirection
|
||||
: strategy.hoverDirection;
|
||||
const { insertIndex, insertPosition } = resolvePreferredNewTrackPlacement({
|
||||
tracks,
|
||||
trackType,
|
||||
preferredIndex: strategy.trackIndex,
|
||||
direction: insertDirection,
|
||||
});
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user