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 --- .../commands/timeline/element/insert-element.ts | 208 ++++++--------------- 1 file changed, 57 insertions(+), 151 deletions(-) (limited to 'apps/web/src/lib/commands/timeline/element/insert-element.ts') diff --git a/apps/web/src/lib/commands/timeline/element/insert-element.ts b/apps/web/src/lib/commands/timeline/element/insert-element.ts index e6d6ee99..d224cfdb 100644 --- a/apps/web/src/lib/commands/timeline/element/insert-element.ts +++ b/apps/web/src/lib/commands/timeline/element/insert-element.ts @@ -5,23 +5,18 @@ import type { TimelineTrack, TimelineElement, TrackType, - ElementType, } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; +import { requiresMediaId } from "@/lib/timeline/element-utils"; +import type { MediaAsset } from "@/lib/media/types"; +import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; +import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics"; import { - requiresMediaId, - wouldElementOverlap, -} from "@/lib/timeline/element-utils"; -import { - buildEmptyTrack, + applyPlacement, canElementGoOnTrack, - getDefaultInsertIndexForTrack, + resolveTrackPlacement, validateElementTrackCompatibility, - enforceMainTrackStart, -} from "@/lib/timeline/track-utils"; -import type { MediaAsset } from "@/lib/media/types"; -import { ELEMENT_TRACK_MAP, TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics"; +} from "@/lib/track-placement"; type InsertElementPlacement = | { mode: "explicit"; trackId: string } @@ -67,7 +62,7 @@ export class InsertElementCommand extends Command { const isFirstElement = totalElementsInTimeline === 0; const newElement = this.buildElement({ element: this.element }); - const updateResult = this.resolveTracksWithElement({ + const updateResult = this.applyPlacementResult({ tracks: this.savedState, element: newElement, }); @@ -190,7 +185,7 @@ export class InsertElementCommand extends Command { return true; } - private resolveTracksWithElement({ + private applyPlacementResult({ tracks, element, }: { @@ -199,160 +194,71 @@ export class InsertElementCommand extends Command { }): { updatedTracks: TimelineTrack[]; targetTrackId: string } | null { const placement = this.placement; - if (placement.mode === "explicit") { - const targetTrack = tracks.find( - (track) => track.id === placement.trackId, - ); - - if (!targetTrack) { - console.error("Track not found:", placement.trackId); - return null; - } - - const validation = validateElementTrackCompatibility({ - element, - track: targetTrack, - }); - - if (!validation.isValid) { - console.error(validation.errorMessage); - return null; - } - - const adjustedElement = this.adjustElementForMainTrack({ - tracks, - targetTrackId: targetTrack.id, - element, - }); - - const updatedTracks = tracks.map((track) => - track.id === targetTrack.id - ? { - ...track, - elements: [...track.elements, adjustedElement], - } - : track, - ) as TimelineTrack[]; - - return { updatedTracks, targetTrackId: targetTrack.id }; - } - - const trackType = - placement.trackType ?? this.getTrackTypeForElement({ element }); - if ( + placement.mode === "auto" && placement.trackType && !canElementGoOnTrack({ elementType: element.type, - trackType, + trackType: placement.trackType, }) ) { console.error( - `${element.type} elements cannot be placed on ${trackType} tracks`, + `${element.type} elements cannot be placed on ${placement.trackType} tracks`, ); return null; } - const elementEndTime = element.startTime + element.duration; - const existingTrack = tracks.find((track) => { - if ( - !canElementGoOnTrack({ - elementType: element.type, - trackType: track.type, - }) - ) { - return false; - } - - return !wouldElementOverlap({ - elements: track.elements, - startTime: element.startTime, - endTime: elementEndTime, - }); - }); - - if (existingTrack) { - const adjustedElement = this.adjustElementForMainTrack({ - tracks, - targetTrackId: existingTrack.id, - element, - }); - - const updatedTracks = tracks.map((track) => - track.id === existingTrack.id - ? { - ...track, - elements: [...track.elements, adjustedElement], - } - : track, - ) as TimelineTrack[]; - - return { updatedTracks, targetTrackId: existingTrack.id }; - } - - const newTrackId = generateUUID(); - const newTrack = buildEmptyTrack({ - id: newTrackId, - type: trackType, + const placementResult = resolveTrackPlacement({ + tracks, + ...(placement.mode === "auto" && placement.trackType + ? { trackType: placement.trackType } + : { elementType: element.type }), + timeSpans: [ + { + startTime: element.startTime, + duration: element.duration, + }, + ], + strategy: + placement.mode === "explicit" + ? { type: "explicit", trackId: placement.trackId } + : { type: "firstAvailable" }, }); - const newTrackWithElement = { - ...newTrack, - elements: [...newTrack.elements, element], - } as TimelineTrack; - - const updatedTracks = [...tracks]; - const insertIndex = - placement.insertIndex ?? - this.getAutoInsertIndex({ tracks: updatedTracks, trackType }); - updatedTracks.splice(insertIndex, 0, newTrackWithElement); - - return { updatedTracks, targetTrackId: newTrackId }; - } - - private getAutoInsertIndex({ - tracks, - trackType, - }: { - tracks: TimelineTrack[]; - trackType: TrackType; - }): number { - if (trackType === "text") { - const firstVideoTrackIndex = tracks.findIndex( - (track) => track.type === "video", - ); - if (firstVideoTrackIndex >= 0) { - return firstVideoTrackIndex; + if (!placementResult) { + if (placement.mode === "explicit") { + const targetTrack = tracks.find((track) => track.id === placement.trackId); + if (!targetTrack) { + console.error("Track not found:", placement.trackId); + return null; + } + + const validation = validateElementTrackCompatibility({ + element, + track: targetTrack, + }); + console.error(validation.errorMessage); } + + return null; } - return getDefaultInsertIndexForTrack({ - tracks, - trackType, - }); - } + const elementToPlace = + placementResult.kind === "existingTrack" + ? { + ...element, + startTime: + placementResult.adjustedStartTime ?? element.startTime, + } + : element; - private adjustElementForMainTrack({ - tracks, - targetTrackId, - element, - }: { - tracks: TimelineTrack[]; - targetTrackId: string; - element: TimelineElement; - }): TimelineElement { - const adjustedStartTime = enforceMainTrackStart({ + return applyPlacement({ tracks, - targetTrackId, - requestedStartTime: element.startTime, + placementResult, + elements: [elementToPlace], + newTrackInsertIndexOverride: + placement.mode === "auto" && typeof placement.insertIndex === "number" + ? placement.insertIndex + : undefined, }); - return { ...element, startTime: adjustedStartTime }; - } - - private getTrackTypeForElement({ - element, - }: { - element: { type: ElementType }; - }): TrackType { - return ELEMENT_TRACK_MAP[element.type]; } } -- cgit v1.3.1