summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/placement/resolve.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib/timeline/placement/resolve.ts')
-rw-r--r--apps/web/src/lib/timeline/placement/resolve.ts255
1 files changed, 255 insertions, 0 deletions
diff --git a/apps/web/src/lib/timeline/placement/resolve.ts b/apps/web/src/lib/timeline/placement/resolve.ts
new file mode 100644
index 00000000..235c789a
--- /dev/null
+++ b/apps/web/src/lib/timeline/placement/resolve.ts
@@ -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,
+ });
+}