From 70a6d58b8432e455ab1443d45bc23dd70b363bea Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 31 Mar 2026 15:47:30 +0200 Subject: refactor: centralize track placement into its own module --- apps/web/src/lib/timeline/track-utils.ts | 235 ------------------------------- 1 file changed, 235 deletions(-) (limited to 'apps/web/src/lib/timeline/track-utils.ts') diff --git a/apps/web/src/lib/timeline/track-utils.ts b/apps/web/src/lib/timeline/track-utils.ts index 0034c72e..cc9e67c4 100644 --- a/apps/web/src/lib/timeline/track-utils.ts +++ b/apps/web/src/lib/timeline/track-utils.ts @@ -1,21 +1,17 @@ import type { TrackType, TimelineTrack, - ElementType, VideoTrack, AudioTrack, GraphicTrack, TextTrack, EffectTrack, - TimelineElement, } from "@/lib/timeline"; import { - ELEMENT_TRACK_MAP, TRACK_CONFIG, ELEMENT_TYPE_CONFIG, TRACK_GAP, } from "@/constants/timeline-constants"; -import { generateUUID } from "@/utils/id"; export function canTracktHaveAudio( track: TimelineTrack, @@ -64,234 +60,3 @@ export function getTotalTracksHeight({ const gapsHeight = Math.max(0, tracks.length - 1) * TRACK_GAP; return tracksHeight + gapsHeight; } - -export function buildEmptyTrack({ - id, - type, - name, -}: { - id: string; - type: TrackType; - name?: string; -}): TimelineTrack { - const trackName = name ?? TRACK_CONFIG[type].defaultName; - - switch (type) { - case "video": - return { - id, - name: trackName, - type: "video", - elements: [], - hidden: false, - muted: false, - isMain: false, - }; - case "text": - return { - id, - name: trackName, - type: "text", - elements: [], - hidden: false, - }; - case "graphic": - return { - id, - name: trackName, - type: "graphic", - elements: [], - hidden: false, - }; - case "audio": - return { - id, - name: trackName, - type: "audio", - elements: [], - muted: false, - }; - case "effect": - return { - id, - name: trackName, - type: "effect", - elements: [], - hidden: false, - }; - default: - throw new Error(`Unsupported track type: ${type}`); - } -} - -export function getDefaultInsertIndexForTrack({ - tracks, - trackType, -}: { - tracks: TimelineTrack[]; - trackType: TrackType; -}): number { - if (trackType === "audio") { - return tracks.length; - } - - if (trackType === "effect") { - return 0; - } - - const mainTrackIndex = tracks.findIndex((track) => isMainTrack(track)); - if (mainTrackIndex >= 0) { - return mainTrackIndex; - } - - const firstAudioTrackIndex = tracks.findIndex( - (track) => track.type === "audio", - ); - if (firstAudioTrackIndex >= 0) { - return firstAudioTrackIndex; - } - - return tracks.length; -} - -export function getHighestInsertIndexForTrack({ - tracks, - trackType, -}: { - tracks: TimelineTrack[]; - trackType: TrackType; -}): number { - const mainTrackIndex = tracks.findIndex((track) => isMainTrack(track)); - - if (trackType === "audio") { - return mainTrackIndex >= 0 ? mainTrackIndex + 1 : tracks.length; - } - - return 0; -} - -export function isMainTrack(track: TimelineTrack): track is VideoTrack { - return track.type === "video" && track.isMain === true; -} - -export function getMainTrack({ - tracks, -}: { - tracks: TimelineTrack[]; -}): TimelineTrack | null { - return tracks.find((track) => isMainTrack(track)) ?? null; -} - -export function ensureMainTrack({ - tracks, -}: { - tracks: TimelineTrack[]; -}): TimelineTrack[] { - const hasMainTrack = tracks.some((track) => isMainTrack(track)); - - if (!hasMainTrack) { - const mainTrack: TimelineTrack = { - id: generateUUID(), - name: "Main Track", - type: "video", - elements: [], - muted: false, - isMain: true, - hidden: false, - }; - return [mainTrack, ...tracks]; - } - - return tracks; -} - -export function canElementGoOnTrack({ - elementType, - trackType, -}: { - elementType: ElementType; - trackType: TrackType; -}): boolean { - return ELEMENT_TRACK_MAP[elementType] === trackType; -} - -export function validateElementTrackCompatibility({ - element, - track, -}: { - element: { type: ElementType }; - track: { type: TrackType }; -}): { isValid: boolean; errorMessage?: string } { - const isValid = canElementGoOnTrack({ - elementType: element.type, - trackType: track.type, - }); - - if (!isValid) { - return { - isValid: false, - errorMessage: `${element.type} elements cannot be placed on ${track.type} tracks`, - }; - } - - return { isValid: true }; -} - -export function getEarliestMainTrackElement({ - tracks, - excludeElementId, -}: { - tracks: TimelineTrack[]; - excludeElementId?: string; -}): TimelineElement | null { - const mainTrack = getMainTrack({ tracks }); - if (!mainTrack) { - return null; - } - - const elements = mainTrack.elements.filter( - (element) => !excludeElementId || element.id !== excludeElementId, - ); - - if (elements.length === 0) { - return null; - } - - return elements.reduce((earliest, element) => - element.startTime < earliest.startTime ? element : earliest, - ); -} - -export function enforceMainTrackStart({ - tracks, - targetTrackId, - requestedStartTime, - excludeElementId, -}: { - tracks: TimelineTrack[]; - targetTrackId: string; - requestedStartTime: number; - excludeElementId?: string; -}): number { - const mainTrack = getMainTrack({ tracks }); - if (!mainTrack || mainTrack.id !== targetTrackId) { - return requestedStartTime; - } - - const earliestElement = getEarliestMainTrackElement({ - tracks, - excludeElementId, - }); - - if (!earliestElement) { - return 0; - } - - // main track must always start at time 0; if this element would - // become the earliest, pin it to the start - if (requestedStartTime <= earliestElement.startTime) { - return 0; - } - - return requestedStartTime; -} -- cgit v1.3.1