diff options
Diffstat (limited to 'apps/web/src/lib/timeline/placement')
| -rw-r--r-- | apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts | 238 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/placement/apply.ts | 149 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/placement/index.ts | 8 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/placement/insert-index.ts | 33 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/placement/main-track.ts | 56 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/placement/resolve.ts | 43 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline/placement/track-factory.ts | 66 |
7 files changed, 371 insertions, 222 deletions
diff --git a/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts b/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts index 5ddccd90..4d39f5d8 100644 --- a/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts +++ b/apps/web/src/lib/timeline/placement/__tests__/resolve.test.ts @@ -4,6 +4,8 @@ import type { AudioTrack, GraphicElement, GraphicTrack, + OverlayTrack, + SceneTracks, TextElement, TextTrack, TimelineTrack, @@ -134,53 +136,45 @@ type BuildTrackParams = id: string; type: "audio"; elements?: AudioTrack["elements"]; - isMain?: boolean; } | { id: string; type: "graphic"; elements?: GraphicTrack["elements"]; - isMain?: boolean; } | { id: string; type: "text"; elements?: TextTrack["elements"]; - isMain?: boolean; } | { id: string; type: "video"; elements?: VideoTrack["elements"]; - isMain?: boolean; }; function buildTrack(params: { id: string; type: "audio"; elements?: AudioTrack["elements"]; - isMain?: boolean; }): AudioTrack; function buildTrack(params: { id: string; type: "graphic"; elements?: GraphicTrack["elements"]; - isMain?: boolean; }): GraphicTrack; function buildTrack(params: { id: string; type: "text"; elements?: TextTrack["elements"]; - isMain?: boolean; }): TextTrack; function buildTrack(params: { id: string; type: "video"; elements?: VideoTrack["elements"]; - isMain?: boolean; }): VideoTrack; function buildTrack(params: BuildTrackParams): TimelineTrack { - const { id, type, isMain = false } = params; + const { id, type } = params; switch (type) { case "audio": @@ -213,7 +207,6 @@ function buildTrack(params: BuildTrackParams): TimelineTrack { type: "video", name: id, elements: params.elements ?? [], - isMain, muted: false, hidden: false, }; @@ -234,9 +227,32 @@ function buildTimeSpan({ return { startTime, duration, excludeElementId }; } +function buildSceneTracks({ + overlay = [], + main, + audio = [], +}: { + overlay?: Array<OverlayTrack>; + main?: VideoTrack; + audio?: Array<AudioTrack>; +}): SceneTracks { + return { + overlay, + main: + main ?? + buildTrack({ + id: "video-main", + type: "video", + }), + audio, + }; +} + describe("resolveTrackPlacement", () => { test("explicit returns the requested compatible track", () => { - const tracks = [buildTrack({ id: "text-1", type: "text" })]; + const tracks = buildSceneTracks({ + overlay: [buildTrack({ id: "text-1", type: "text" })], + }); expect( resolveTrackPlacement({ @@ -254,7 +270,9 @@ describe("resolveTrackPlacement", () => { }); test("explicit rejects missing and incompatible tracks", () => { - const tracks = [buildTrack({ id: "video-1", type: "video", isMain: true })]; + const tracks = buildSceneTracks({ + main: buildTrack({ id: "video-1", type: "video" }), + }); expect( resolveTrackPlacement({ @@ -276,16 +294,18 @@ describe("resolveTrackPlacement", () => { }); test("firstAvailable picks the first compatible track without overlap", () => { - const tracks = [ - buildTrack({ - id: "text-1", - type: "text", - elements: [ - buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }), - ], - }), - buildTrack({ id: "text-2", type: "text" }), - ]; + const tracks = buildSceneTracks({ + overlay: [ + buildTrack({ + id: "text-1", + type: "text", + elements: [ + buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }), + ], + }), + buildTrack({ id: "text-2", type: "text" }), + ], + }); expect( resolveTrackPlacement({ @@ -303,21 +323,22 @@ describe("resolveTrackPlacement", () => { }); test("firstAvailable creates a new track when all compatible tracks are full", () => { - const tracks = [ - buildTrack({ - id: "graphic-1", - type: "graphic", - elements: [ - buildElement({ id: "a", type: "graphic", startTime: 0, duration: 5 }), - ], - }), - buildTrack({ + const tracks = buildSceneTracks({ + overlay: [ + buildTrack({ + id: "graphic-1", + type: "graphic", + elements: [ + buildElement({ id: "a", type: "graphic", startTime: 0, duration: 5 }), + ], + }), + ], + main: buildTrack({ id: "video-main", type: "video", - isMain: true, }), - buildTrack({ id: "audio-1", type: "audio" }), - ]; + audio: [buildTrack({ id: "audio-1", type: "audio" })], + }); expect( resolveTrackPlacement({ @@ -335,7 +356,9 @@ describe("resolveTrackPlacement", () => { }); test("preferIndex uses the preferred track when it fits", () => { - const tracks = [buildTrack({ id: "audio-1", type: "audio" })]; + const tracks = buildSceneTracks({ + audio: [buildTrack({ id: "audio-1", type: "audio" })], + }); expect( resolveTrackPlacement({ @@ -349,18 +372,18 @@ describe("resolveTrackPlacement", () => { }, }), ).toEqual({ - kind: "existingTrack", - trackId: "audio-1", - trackIndex: 0, + kind: "newTrack", trackType: "audio", + insertIndex: 1, + insertPosition: "below", }); }); test("preferIndex creates a new overlay track above the main track", () => { - const tracks = [ - buildTrack({ id: "video-main", type: "video", isMain: true }), - buildTrack({ id: "audio-1", type: "audio" }), - ]; + const tracks = buildSceneTracks({ + main: buildTrack({ id: "video-main", type: "video" }), + audio: [buildTrack({ id: "audio-1", type: "audio" })], + }); expect( resolveTrackPlacement({ @@ -382,11 +405,11 @@ describe("resolveTrackPlacement", () => { }); test("preferIndex keeps audio tracks below the main track", () => { - const tracks = [ - buildTrack({ id: "text-1", type: "text" }), - buildTrack({ id: "video-main", type: "video", isMain: true }), - buildTrack({ id: "audio-1", type: "audio" }), - ]; + const tracks = buildSceneTracks({ + overlay: [buildTrack({ id: "text-1", type: "text" })], + main: buildTrack({ id: "video-main", type: "video" }), + audio: [buildTrack({ id: "audio-1", type: "audio" })], + }); expect( resolveTrackPlacement({ @@ -409,17 +432,19 @@ describe("resolveTrackPlacement", () => { }); test("aboveSource tries the track above source, then any compatible track", () => { - const tracks = [ - buildTrack({ id: "text-top", type: "text" }), - buildTrack({ - id: "text-middle", - type: "text", - elements: [ - buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }), - ], - }), - buildTrack({ id: "text-source", type: "text" }), - ]; + const tracks = buildSceneTracks({ + overlay: [ + buildTrack({ id: "text-top", type: "text" }), + buildTrack({ + id: "text-middle", + type: "text", + elements: [ + buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }), + ], + }), + buildTrack({ id: "text-source", type: "text" }), + ], + }); expect( resolveTrackPlacement({ @@ -436,23 +461,25 @@ describe("resolveTrackPlacement", () => { }); }); - test("aboveSource creates a new track near the source when none fit", () => { - const tracks = [ - buildTrack({ - id: "text-top", - type: "text", - elements: [ - buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }), - ], - }), - buildTrack({ - id: "text-source", - type: "text", - elements: [ - buildElement({ id: "b", type: "text", startTime: 0, duration: 5 }), - ], - }), - ]; + test("aboveSource creates a new overlay track in the overlay zone when none fit", () => { + const tracks = buildSceneTracks({ + overlay: [ + buildTrack({ + id: "text-top", + type: "text", + elements: [ + buildElement({ id: "a", type: "text", startTime: 0, duration: 5 }), + ], + }), + buildTrack({ + id: "text-source", + type: "text", + elements: [ + buildElement({ id: "b", type: "text", startTime: 0, duration: 5 }), + ], + }), + ], + }); expect( resolveTrackPlacement({ @@ -464,16 +491,16 @@ describe("resolveTrackPlacement", () => { ).toEqual({ kind: "newTrack", trackType: "text", - insertIndex: 1, + insertIndex: 0, insertPosition: null, }); }); test("alwaysNew honors highest and default insertion rules", () => { - const tracks = [ - buildTrack({ id: "video-main", type: "video", isMain: true }), - buildTrack({ id: "audio-1", type: "audio" }), - ]; + const tracks = buildSceneTracks({ + main: buildTrack({ id: "video-main", type: "video" }), + audio: [buildTrack({ id: "audio-1", type: "audio" })], + }); expect( resolveTrackPlacement({ @@ -505,16 +532,18 @@ describe("resolveTrackPlacement", () => { }); test("batch time spans reject tracks when any span overlaps", () => { - const tracks = [ - buildTrack({ - id: "audio-1", - type: "audio", - elements: [ - buildElement({ id: "a", type: "audio", startTime: 0, duration: 2 }), - buildElement({ id: "b", type: "audio", startTime: 5, duration: 2 }), - ], - }), - ]; + const tracks = buildSceneTracks({ + audio: [ + buildTrack({ + id: "audio-1", + type: "audio", + elements: [ + buildElement({ id: "a", type: "audio", startTime: 0, duration: 2 }), + buildElement({ id: "b", type: "audio", startTime: 5, duration: 2 }), + ], + }), + ], + }); expect( resolveTrackPlacement({ @@ -534,10 +563,10 @@ describe("resolveTrackPlacement", () => { }); }); - test("handles empty timelines, single tracks, and track-type derivation", () => { + test("handles main-only timelines, single tracks, and track-type derivation", () => { expect( resolveTrackPlacement({ - tracks: [], + tracks: buildSceneTracks({}), elementType: "video", timeSpans: [buildTimeSpan({ startTime: 0, duration: 3 })], strategy: { @@ -551,12 +580,14 @@ describe("resolveTrackPlacement", () => { kind: "newTrack", trackType: "video", insertIndex: 0, - insertPosition: null, + insertPosition: "above", }); expect( resolveTrackPlacement({ - tracks: [buildTrack({ id: "audio-1", type: "audio" })], + tracks: buildSceneTracks({ + audio: [buildTrack({ id: "audio-1", type: "audio" })], + }), elementType: "audio", timeSpans: [], strategy: { type: "alwaysNew", position: "default" }, @@ -564,22 +595,21 @@ describe("resolveTrackPlacement", () => { ).toEqual({ kind: "newTrack", trackType: "audio", - insertIndex: 1, + insertIndex: 2, insertPosition: null, }); }); test("existingTrack on main video includes adjustedStartTime when start snaps", () => { - const tracks = [ - buildTrack({ + const tracks = buildSceneTracks({ + main: buildTrack({ id: "video-main", type: "video", - isMain: true, elements: [ buildElement({ id: "a", type: "video", startTime: 5, duration: 5 }), ], }), - ]; + }); expect( resolveTrackPlacement({ @@ -598,11 +628,11 @@ describe("resolveTrackPlacement", () => { }); test("preferIndex uses vertical drag direction when hovered track is incompatible", () => { - const tracks = [ - buildTrack({ id: "text-1", type: "text" }), - buildTrack({ id: "video-main", type: "video", isMain: true }), - buildTrack({ id: "audio-1", type: "audio" }), - ]; + const tracks = buildSceneTracks({ + overlay: [buildTrack({ id: "text-1", type: "text" })], + main: buildTrack({ id: "video-main", type: "video" }), + audio: [buildTrack({ id: "audio-1", type: "audio" })], + }); expect( resolveTrackPlacement({ diff --git a/apps/web/src/lib/timeline/placement/apply.ts b/apps/web/src/lib/timeline/placement/apply.ts index 4ff5d50d..639b1b42 100644 --- a/apps/web/src/lib/timeline/placement/apply.ts +++ b/apps/web/src/lib/timeline/placement/apply.ts @@ -1,7 +1,18 @@ -import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; +import type { + AudioTrack, + EffectTrack, + GraphicTrack, + OverlayTrack, + SceneTracks, + TextTrack, + TimelineElement, + TimelineTrack, + VideoTrack, +} from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; import { buildEmptyTrack } from "./track-factory"; import type { PlacementResult } from "./types"; +import { updateTrackInSceneTracks } from "@/lib/timeline/track-element-update"; export function applyPlacement({ tracks, @@ -9,37 +20,139 @@ export function applyPlacement({ elements, newTrackInsertIndexOverride, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; placementResult: PlacementResult; elements: TimelineElement[]; newTrackInsertIndexOverride?: number; -}): { updatedTracks: TimelineTrack[]; targetTrackId: string } | null { +}): { updatedTracks: SceneTracks; targetTrackId: string } | null { + const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio]; if (placementResult.kind === "existingTrack") { - const targetTrack = tracks[placementResult.trackIndex]; + const targetTrack = orderedTracks[placementResult.trackIndex]; if (!targetTrack) { return null; } - const updatedTracks = tracks.map((track, trackIndex) => - trackIndex === placementResult.trackIndex - ? { - ...track, - elements: [...track.elements, ...elements], - } - : track, - ) as TimelineTrack[]; + const updatedTracks = updateTrackInSceneTracks({ + tracks, + trackId: targetTrack.id, + update: (track) => ({ + ...track, + elements: [...track.elements, ...elements], + }), + }); return { updatedTracks, targetTrackId: targetTrack.id }; } const newTrackId = generateUUID(); - const newTrack = { - ...buildEmptyTrack({ id: newTrackId, type: placementResult.trackType }), - elements, - } as TimelineTrack; const insertIndex = newTrackInsertIndexOverride ?? placementResult.insertIndex; - const updatedTracks = [...tracks]; - updatedTracks.splice(insertIndex, 0, newTrack); + const updatedTracks = + placementResult.trackType === "audio" + ? { + ...tracks, + audio: insertIntoAudioTracks({ + tracks, + insertIndex, + track: buildPlacedAudioTrack({ + id: newTrackId, + elements, + }), + }), + } + : { + ...tracks, + overlay: insertIntoOverlayTracks({ + tracks, + insertIndex, + track: buildPlacedOverlayTrack({ + id: newTrackId, + type: placementResult.trackType, + elements, + }), + }), + }; return { updatedTracks, targetTrackId: newTrackId }; } + +function insertIntoOverlayTracks({ + tracks, + insertIndex, + track, +}: { + tracks: SceneTracks; + insertIndex: number; + track: OverlayTrack; +}): OverlayTrack[] { + const normalizedInsertIndex = Math.max( + 0, + Math.min(insertIndex, tracks.overlay.length), + ); + const nextTracks = [...tracks.overlay]; + nextTracks.splice(normalizedInsertIndex, 0, track); + return nextTracks; +} + +function insertIntoAudioTracks({ + tracks, + insertIndex, + track, +}: { + tracks: SceneTracks; + insertIndex: number; + track: AudioTrack; +}): AudioTrack[] { + const audioInsertIndex = Math.max( + 0, + Math.min(insertIndex - tracks.overlay.length - 1, tracks.audio.length), + ); + const nextTracks = [...tracks.audio]; + nextTracks.splice(audioInsertIndex, 0, track); + return nextTracks; +} + +function buildPlacedAudioTrack({ + id, + elements, +}: { + id: string; + elements: TimelineElement[]; +}): AudioTrack { + return { + ...buildEmptyTrack({ id, type: "audio" }), + elements: elements as AudioTrack["elements"], + }; +} + +function buildPlacedOverlayTrack({ + id, + type, + elements, +}: { + id: string; + type: Exclude<OverlayTrack["type"], "audio">; + elements: TimelineElement[]; +}): OverlayTrack { + switch (type) { + case "video": + return { + ...buildEmptyTrack({ id, type: "video" }), + elements: elements as VideoTrack["elements"], + }; + case "text": + return { + ...buildEmptyTrack({ id, type: "text" }), + elements: elements as TextTrack["elements"], + }; + case "graphic": + return { + ...buildEmptyTrack({ id, type: "graphic" }), + elements: elements as GraphicTrack["elements"], + }; + case "effect": + return { + ...buildEmptyTrack({ id, type: "effect" }), + elements: elements as EffectTrack["elements"], + }; + } +} diff --git a/apps/web/src/lib/timeline/placement/index.ts b/apps/web/src/lib/timeline/placement/index.ts index 091acc33..530cb69f 100644 --- a/apps/web/src/lib/timeline/placement/index.ts +++ b/apps/web/src/lib/timeline/placement/index.ts @@ -1,13 +1,7 @@ export { applyPlacement } from "./apply"; export { canElementGoOnTrack, validateElementTrackCompatibility } from "./compatibility"; export { getDefaultInsertIndexForTrack, getHighestInsertIndexForTrack } from "./insert-index"; -export { - enforceMainTrackStart, - ensureMainTrack, - getEarliestMainTrackElement, - getMainTrack, - isMainTrack, -} from "./main-track"; +export { MAIN_TRACK_NAME, enforceMainTrackStart, getEarliestMainTrackElement } from "./main-track"; export { resolveTrackPlacement } from "./resolve"; export { buildEmptyTrack } from "./track-factory"; export type { diff --git a/apps/web/src/lib/timeline/placement/insert-index.ts b/apps/web/src/lib/timeline/placement/insert-index.ts index fecb893a..04fa5a4a 100644 --- a/apps/web/src/lib/timeline/placement/insert-index.ts +++ b/apps/web/src/lib/timeline/placement/insert-index.ts @@ -1,44 +1,32 @@ -import type { TrackType, TimelineTrack } from "@/lib/timeline"; -import { isMainTrack } from "./main-track"; +import type { SceneTracks, TrackType } from "@/lib/timeline"; export function getDefaultInsertIndexForTrack({ tracks, trackType, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; trackType: TrackType; }): number { if (trackType === "audio") { - return tracks.length; + return tracks.overlay.length + 1 + tracks.audio.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; + return tracks.overlay.length; } export function getHighestInsertIndexForTrack({ tracks, trackType, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; trackType: TrackType; }): number { - const mainTrackIndex = tracks.findIndex((track) => isMainTrack(track)); if (trackType === "audio") { - return mainTrackIndex >= 0 ? mainTrackIndex + 1 : tracks.length; + return tracks.overlay.length + 1; } return 0; @@ -50,12 +38,13 @@ export function resolvePreferredNewTrackPlacement({ preferredIndex, direction, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; trackType: TrackType; preferredIndex: number; direction: "above" | "below"; }): { insertIndex: number; insertPosition: "above" | "below" | null } { - if (tracks.length === 0) { + const trackCount = tracks.overlay.length + 1 + tracks.audio.length; + if (trackCount === 0) { return { insertIndex: 0, insertPosition: trackType === "audio" ? "below" : null, @@ -64,9 +53,9 @@ export function resolvePreferredNewTrackPlacement({ const safePreferredIndex = Math.min( Math.max(preferredIndex, 0), - tracks.length - 1, + trackCount - 1, ); - const mainTrackIndex = tracks.findIndex((track) => isMainTrack(track)); + const mainTrackIndex = tracks.overlay.length; if (trackType === "audio") { if (safePreferredIndex <= mainTrackIndex) { diff --git a/apps/web/src/lib/timeline/placement/main-track.ts b/apps/web/src/lib/timeline/placement/main-track.ts index 1783352c..7e206517 100644 --- a/apps/web/src/lib/timeline/placement/main-track.ts +++ b/apps/web/src/lib/timeline/placement/main-track.ts @@ -1,55 +1,14 @@ -import type { TimelineElement, TimelineTrack, VideoTrack } from "@/lib/timeline"; -import { generateUUID } from "@/utils/id"; +import type { SceneTracks, TimelineElement, VideoTrack } from "@/lib/timeline"; -const MAIN_TRACK_NAME = "Main Track"; - -export function isMainTrack(track: TimelineTrack): track is VideoTrack { - return track.type === "video" && track.isMain === true; -} - -export function getMainTrack({ - tracks, -}: { - tracks: TimelineTrack[]; -}): VideoTrack | null { - return tracks.find((track) => isMainTrack(track)) ?? null; -} - -export function ensureMainTrack({ - tracks, -}: { - tracks: TimelineTrack[]; -}): TimelineTrack[] { - if (tracks.some((track) => isMainTrack(track))) { - return tracks; - } - - return [ - { - id: generateUUID(), - name: MAIN_TRACK_NAME, - type: "video", - elements: [], - muted: false, - isMain: true, - hidden: false, - }, - ...tracks, - ]; -} +export const MAIN_TRACK_NAME = "Main Track"; export function getEarliestMainTrackElement({ - tracks, + mainTrack, excludeElementId, }: { - tracks: TimelineTrack[]; + mainTrack: VideoTrack; excludeElementId?: string; }): TimelineElement | null { - const mainTrack = getMainTrack({ tracks }); - if (!mainTrack) { - return null; - } - const elements = mainTrack.elements.filter((element) => { return !excludeElementId || element.id !== excludeElementId; }); @@ -70,18 +29,17 @@ export function enforceMainTrackStart({ requestedStartTime, excludeElementId, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; targetTrackId: string; requestedStartTime: number; excludeElementId?: string; }): number { - const mainTrack = getMainTrack({ tracks }); - if (!mainTrack || mainTrack.id !== targetTrackId) { + if (tracks.main.id !== targetTrackId) { return requestedStartTime; } const earliestElement = getEarliestMainTrackElement({ - tracks, + mainTrack: tracks.main, excludeElementId, }); if (!earliestElement) { diff --git a/apps/web/src/lib/timeline/placement/resolve.ts b/apps/web/src/lib/timeline/placement/resolve.ts index d0417d0a..67943912 100644 --- a/apps/web/src/lib/timeline/placement/resolve.ts +++ b/apps/web/src/lib/timeline/placement/resolve.ts @@ -1,4 +1,4 @@ -import type { TrackType, TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TrackType, TimelineTrack } from "@/lib/timeline"; import { getDefaultInsertIndexForTrack, getHighestInsertIndexForTrack, @@ -15,7 +15,7 @@ import type { } from "./types"; type ResolveTrackPlacementParams = PlacementSubject & { - tracks: TimelineTrack[]; + tracks: SceneTracks; timeSpans: PlacementTimeSpan[]; strategy: PlacementStrategy; }; @@ -28,7 +28,7 @@ function buildExistingTrackResult({ }: { track: TimelineTrack; trackIndex: number; - tracks: TimelineTrack[]; + tracks: SceneTracks; timeSpans: PlacementTimeSpan[]; }): PlacementResult { const firstSpan = timeSpans[0]; @@ -90,7 +90,7 @@ function resolveAlwaysNewTrack({ trackType, position, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; trackType: TrackType; position: "highest" | "default"; }): PlacementResult { @@ -134,6 +134,7 @@ export function resolveTrackPlacement({ tracks, ...placement }: ResolveTrackPlacementParams): PlacementResult | null { + const orderedTracks = [...tracks.overlay, tracks.main, ...tracks.audio]; const trackType = "trackType" in placement ? placement.trackType @@ -143,30 +144,35 @@ export function resolveTrackPlacement({ const { timeSpans, strategy } = placement; if (strategy.type === "explicit") { - const trackIndex = tracks.findIndex( + const trackIndex = orderedTracks.findIndex( (track) => track.id === strategy.trackId, ); if (trackIndex < 0) { return null; } - const track = tracks[trackIndex]; + const track = orderedTracks[trackIndex]; if (track.type !== trackType) { return null; } - return buildExistingTrackResult({ track, trackIndex, tracks, timeSpans }); + return buildExistingTrackResult({ + track, + trackIndex, + tracks, + timeSpans, + }); } if (strategy.type === "firstAvailable") { const existingTrackIndex = findFirstAvailableTrackIndex({ - tracks, + tracks: orderedTracks, trackType, timeSpans, }); if (existingTrackIndex >= 0) { return buildExistingTrackResult({ - track: tracks[existingTrackIndex], + track: orderedTracks[existingTrackIndex], trackIndex: existingTrackIndex, tracks, timeSpans, @@ -181,7 +187,7 @@ export function resolveTrackPlacement({ } if (strategy.type === "preferIndex") { - const preferredTrack = tracks[strategy.trackIndex]; + const preferredTrack = orderedTracks[strategy.trackIndex]; const isPreferredTrackCompatible = !!preferredTrack && preferredTrack.type === trackType; const canUseExistingTrack = @@ -220,7 +226,7 @@ export function resolveTrackPlacement({ if (strategy.type === "aboveSource") { const aboveTrackIndex = strategy.sourceTrackIndex - 1; - const aboveTrack = tracks[aboveTrackIndex]; + const aboveTrack = orderedTracks[aboveTrackIndex]; if ( aboveTrack && aboveTrack.type === trackType && @@ -238,26 +244,23 @@ export function resolveTrackPlacement({ } const firstAvailableTrackIndex = findFirstAvailableTrackIndex({ - tracks, + tracks: orderedTracks, trackType, timeSpans, }); if (firstAvailableTrackIndex >= 0) { return buildExistingTrackResult({ - track: tracks[firstAvailableTrackIndex], + track: orderedTracks[firstAvailableTrackIndex], trackIndex: firstAvailableTrackIndex, tracks, timeSpans, }); } - const insertIndex = - strategy.sourceTrackIndex >= 0 - ? strategy.sourceTrackIndex - : getHighestInsertIndexForTrack({ - tracks, - trackType, - }); + const insertIndex = getHighestInsertIndexForTrack({ + tracks, + trackType, + }); return buildNewTrackResult({ trackType, diff --git a/apps/web/src/lib/timeline/placement/track-factory.ts b/apps/web/src/lib/timeline/placement/track-factory.ts index ee056261..b73c5155 100644 --- a/apps/web/src/lib/timeline/placement/track-factory.ts +++ b/apps/web/src/lib/timeline/placement/track-factory.ts @@ -1,5 +1,13 @@ import { DEFAULT_TRACK_NAMES } from "@/lib/timeline/tracks"; -import type { TrackType, TimelineTrack } from "@/lib/timeline"; +import type { + AudioTrack, + EffectTrack, + GraphicTrack, + TextTrack, + TrackType, + TimelineTrack, + VideoTrack, +} from "@/lib/timeline"; export function buildEmptyTrack({ id, @@ -7,6 +15,61 @@ export function buildEmptyTrack({ name, }: { id: string; + type: "video"; + name?: string; +}): VideoTrack; +export function buildEmptyTrack({ + id, + type, + name, +}: { + id: string; + type: "text"; + name?: string; +}): TextTrack; +export function buildEmptyTrack({ + id, + type, + name, +}: { + id: string; + type: "audio"; + name?: string; +}): AudioTrack; +export function buildEmptyTrack({ + id, + type, + name, +}: { + id: string; + type: "graphic"; + name?: string; +}): GraphicTrack; +export function buildEmptyTrack({ + id, + type, + name, +}: { + id: string; + type: "effect"; + name?: string; +}): EffectTrack; + +export function buildEmptyTrack({ + id, + type, + name, +}: { + id: string; + type: TrackType; + name?: string; +}): TimelineTrack; +export function buildEmptyTrack({ + id, + type, + name, +}: { + id: string; type: TrackType; name?: string; }): TimelineTrack { @@ -21,7 +84,6 @@ export function buildEmptyTrack({ elements: [], hidden: false, muted: false, - isMain: false, }; case "text": return { |
