diff options
Diffstat (limited to 'apps/web/src/lib/commands/timeline')
30 files changed, 524 insertions, 372 deletions
diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste.ts b/apps/web/src/lib/commands/timeline/clipboard/paste.ts index 9fa4b83b..6ccf96fb 100644 --- a/apps/web/src/lib/commands/timeline/clipboard/paste.ts +++ b/apps/web/src/lib/commands/timeline/clipboard/paste.ts @@ -1,21 +1,16 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; import type { - TimelineTrack, + SceneTracks, TimelineElement, ClipboardItem, } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; -import { - applyPlacement, - resolveTrackPlacement, - isMainTrack, - enforceMainTrackStart, -} from "@/lib/timeline/placement"; +import { applyPlacement, resolveTrackPlacement, enforceMainTrackStart } from "@/lib/timeline/placement"; import { cloneAnimations } from "@/lib/animation"; export class PasteCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private pastedElements: { trackId: string; elementId: string }[] = []; private readonly time: number; private readonly clipboardItems: ClipboardItem[]; @@ -36,14 +31,14 @@ export class PasteCommand extends Command { if (this.clipboardItems.length === 0) return undefined; const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; this.pastedElements = []; const minStart = Math.min( ...this.clipboardItems.map((item) => item.element.startTime), ); - let updatedTracks = [...this.savedState]; + let updatedTracks = this.savedState; const itemsByTrackId = groupClipboardItemsByTrackId({ clipboardItems: this.clipboardItems, }); @@ -60,9 +55,11 @@ export class PasteCommand extends Command { } const trackType = items[0].trackType; - const sourceTrackIndex = updatedTracks.findIndex( - (track) => track.id === trackId, - ); + const sourceTrackIndex = [ + ...updatedTracks.overlay, + updatedTracks.main, + ...updatedTracks.audio, + ].findIndex((track) => track.id === trackId); const placementResult = resolveTrackPlacement({ tracks: updatedTracks, trackType, @@ -78,8 +75,15 @@ export class PasteCommand extends Command { let elementsForPlacement = elementsToAdd; if (placementResult.kind === "existingTrack") { - const targetTrack = updatedTracks[placementResult.trackIndex]; - if (isMainTrack(targetTrack)) { + const targetTrack = + placementResult.trackIndex < updatedTracks.overlay.length + ? updatedTracks.overlay[placementResult.trackIndex] + : placementResult.trackIndex === updatedTracks.overlay.length + ? updatedTracks.main + : updatedTracks.audio[ + placementResult.trackIndex - updatedTracks.overlay.length - 1 + ]; + if (targetTrack?.id === updatedTracks.main.id) { const earliestElement = elementsToAdd.reduce((earliest, element) => element.startTime < earliest.startTime ? element : earliest, ); diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/lib/commands/timeline/element/delete-elements.ts index c8cc4494..8d93c6ba 100644 --- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/delete-elements.ts @@ -1,9 +1,28 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; import { EditorCore } from "@/core"; +import type { TimelineTrack } from "@/lib/timeline"; + +function removeTrackElements<TTrack extends TimelineTrack>({ + track, + elements, +}: { + track: TTrack; + elements: { trackId: string; elementId: string }[]; +}): TTrack { + const nextElements = track.elements.filter( + (element) => + !elements.some( + (target) => + target.trackId === track.id && target.elementId === element.id, + ), + ); + + return { ...track, elements: nextElements } as TTrack; +} export class DeleteElementsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly elements: { trackId: string; elementId: string }[]; constructor({ @@ -17,28 +36,20 @@ export class DeleteElementsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = this.savedState.map((track) => { - const elementsToDeleteOnTrack = this.elements.filter( - (target) => target.trackId === track.id, - ); - - if (elementsToDeleteOnTrack.length === 0) { - return track; - } - - const elements = track.elements.filter( - (element) => - !this.elements.some( - (target) => - target.trackId === track.id && - target.elementId === element.id, - ), - ); - - return { ...track, elements } as typeof track; - }); + const updatedTracks: SceneTracks = { + overlay: this.savedState.overlay.map((track) => + removeTrackElements({ track, elements: this.elements }), + ), + main: removeTrackElements({ + track: this.savedState.main, + elements: this.elements, + }), + audio: this.savedState.audio.map((track) => + removeTrackElements({ track, elements: this.elements }), + ), + }; editor.timeline.updateTracks(updatedTracks); diff --git a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts index 35be3da2..ab7a4a25 100644 --- a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts @@ -1,5 +1,5 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; import { EditorCore } from "@/core"; import { applyPlacement, resolveTrackPlacement } from "@/lib/timeline/placement"; @@ -11,7 +11,7 @@ interface DuplicateElementsParams { export class DuplicateElementsCommand extends Command { private duplicatedElements: { trackId: string; elementId: string }[] = []; - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private elements: DuplicateElementsParams["elements"]; constructor({ elements }: DuplicateElementsParams) { @@ -21,12 +21,16 @@ export class DuplicateElementsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; this.duplicatedElements = []; - let updatedTracks = [...this.savedState]; + let updatedTracks = this.savedState; - for (const track of this.savedState) { + for (const track of [ + ...this.savedState.overlay, + this.savedState.main, + ...this.savedState.audio, + ]) { const elementsToDuplicate = this.elements.filter( (elementEntry) => elementEntry.trackId === track.id, ); diff --git a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts index 0c117f3b..902ca341 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/add-effect.ts @@ -1,7 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; import { buildDefaultEffectInstance } from "@/lib/effects"; function addEffectToElement({ @@ -17,7 +17,7 @@ function addEffectToElement({ } export class AddClipEffectCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private effectId: string | null = null; private readonly trackId: string; private readonly elementId: string; @@ -40,9 +40,9 @@ export class AddClipEffectCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/effects/index.ts b/apps/web/src/lib/commands/timeline/element/effects/index.ts index b03c51e6..133ce661 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/index.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/index.ts @@ -1,5 +1,5 @@ -export { AddClipEffectCommand } from "./add-effect";
-export { RemoveClipEffectCommand } from "./remove-effect";
-export { ToggleClipEffectCommand } from "./toggle-effect";
-export { UpdateClipEffectParamsCommand } from "./update-effect-params";
-export { ReorderClipEffectsCommand } from "./reorder-effect";
+export { AddClipEffectCommand } from "./add-effect"; +export { RemoveClipEffectCommand } from "./remove-effect"; +export { ToggleClipEffectCommand } from "./toggle-effect"; +export { UpdateClipEffectParamsCommand } from "./update-effect-params"; +export { ReorderClipEffectsCommand } from "./reorder-effect"; diff --git a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts index f876a1d1..f453f2cc 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/remove-effect.ts @@ -1,7 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; function removeEffectFromElement({ element, @@ -16,7 +16,7 @@ function removeEffectFromElement({ } export class RemoveClipEffectCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -38,9 +38,9 @@ export class RemoveClipEffectCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts index e63430be..28b7c4e1 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/reorder-effect.ts @@ -1,7 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; function reorderEffectsOnElement({ element, @@ -19,7 +19,7 @@ function reorderEffectsOnElement({ } export class ReorderClipEffectsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly fromIndex: number; @@ -45,9 +45,9 @@ export class ReorderClipEffectsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts b/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts index 0329f1a0..a703cd94 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/toggle-effect.ts @@ -1,7 +1,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; export function toggleEffectOnElement({ element, @@ -18,7 +18,7 @@ export function toggleEffectOnElement({ } export class ToggleClipEffectCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -40,9 +40,9 @@ export class ToggleClipEffectCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts b/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts index 058706ec..5e75c1f0 100644 --- a/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts +++ b/apps/web/src/lib/commands/timeline/element/effects/update-effect-params.ts @@ -1,8 +1,8 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; -import { isVisualElement, updateElementInTracks } from "@/lib/timeline"; +import { isVisualElement, updateElementInSceneTracks } from "@/lib/timeline"; import type { ParamValues } from "@/lib/params"; -import type { TimelineTrack, VisualElement } from "@/lib/timeline"; +import type { SceneTracks, VisualElement } from "@/lib/timeline"; function updateEffectParamsOnElement({ element, @@ -32,7 +32,7 @@ function updateEffectParamsOnElement({ } export class UpdateClipEffectParamsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -58,9 +58,9 @@ export class UpdateClipEffectParamsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/index.ts b/apps/web/src/lib/commands/timeline/element/index.ts index eabdc385..ff365801 100644 --- a/apps/web/src/lib/commands/timeline/element/index.ts +++ b/apps/web/src/lib/commands/timeline/element/index.ts @@ -1,11 +1,11 @@ -export { InsertElementCommand } from "./insert-element";
-export { DeleteElementsCommand } from "./delete-elements";
-export { DuplicateElementsCommand } from "./duplicate-elements";
-export { SplitElementsCommand } from "./split-elements";
-export { UpdateElementsCommand } from "./update-elements";
-export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation";
-export { MoveElementCommand } from "./move-elements";
-
-export * from "./keyframes";
-export * from "./effects";
-export * from "./masks";
+export { InsertElementCommand } from "./insert-element"; +export { DeleteElementsCommand } from "./delete-elements"; +export { DuplicateElementsCommand } from "./duplicate-elements"; +export { SplitElementsCommand } from "./split-elements"; +export { UpdateElementsCommand } from "./update-elements"; +export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation"; +export { MoveElementCommand } from "./move-elements"; + +export * from "./keyframes"; +export * from "./effects"; +export * from "./masks"; 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 b06631d8..0ac11ebd 100644 --- a/apps/web/src/lib/commands/timeline/element/insert-element.ts +++ b/apps/web/src/lib/commands/timeline/element/insert-element.ts @@ -2,6 +2,7 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; import type { CreateTimelineElement, + SceneTracks, TimelineTrack, TimelineElement, TrackType, @@ -30,7 +31,7 @@ export interface InsertElementParams { export class InsertElementCommand extends Command { private elementId: string; - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private targetTrackId: string | null = null; constructor({ element, placement }: InsertElementParams) { @@ -45,21 +46,22 @@ export class InsertElementCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); - - if (!this.savedState) { - console.error("Tracks not available"); - return; - } + this.savedState = editor.scenes.getActiveScene().tracks; if (!this.validateElementBasics({ element: this.element })) { return; } - const totalElementsInTimeline = this.savedState.reduce( - (total, t) => total + t.elements.length, - 0, - ); + const totalElementsInTimeline = + this.savedState.main.elements.length + + this.savedState.overlay.reduce( + (total, track) => total + track.elements.length, + 0, + ) + + this.savedState.audio.reduce( + (total, track) => total + track.elements.length, + 0, + ); const isFirstElement = totalElementsInTimeline === 0; const newElement = this.buildElement({ element: this.element }); @@ -194,9 +196,9 @@ export class InsertElementCommand extends Command { tracks, element, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; element: TimelineElement; - }): { updatedTracks: TimelineTrack[]; targetTrackId: string } | null { + }): { updatedTracks: SceneTracks; targetTrackId: string } | null { const placement = this.placement; if ( @@ -231,7 +233,11 @@ export class InsertElementCommand extends Command { }); if (!placementResult) { if (placement.mode === "explicit") { - const targetTrack = tracks.find((track) => track.id === placement.trackId); + const targetTrack = + tracks.main.id === placement.trackId + ? tracks.main + : tracks.overlay.find((track) => track.id === placement.trackId) ?? + tracks.audio.find((track) => track.id === placement.trackId); if (!targetTrack) { console.error("Track not found:", placement.trackId); return null; @@ -256,7 +262,7 @@ export class InsertElementCommand extends Command { } : element; - return applyPlacement({ + const appliedPlacement = applyPlacement({ tracks, placementResult, elements: [elementToPlace], @@ -265,5 +271,13 @@ export class InsertElementCommand extends Command { ? placement.insertIndex : undefined, }); + if (!appliedPlacement) { + return null; + } + + return { + updatedTracks: appliedPlacement.updatedTracks, + targetTrackId: appliedPlacement.targetTrackId, + }; } } diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts index 36f8a6b1..b1ef4aef 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/remove-effect-param-keyframe.ts @@ -1,12 +1,12 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; import { removeEffectParamKeyframe } from "@/lib/animation/effect-param-channel"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import { isVisualElement } from "@/lib/timeline/element-utils"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; export class RemoveEffectParamKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -36,9 +36,9 @@ export class RemoveEffectParamKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts index 35aa0860..c757055d 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts @@ -7,9 +7,9 @@ import { resolveAnimationTarget, } from "@/lib/animation"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import type { AnimationPath, AnimationValue } from "@/lib/animation/types"; -import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; function sampleValueBeforeRemoval({ element, @@ -86,7 +86,7 @@ function removeKeyframeAndPersist({ } export class RemoveKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly propertyPath: AnimationPath; @@ -112,9 +112,9 @@ export class RemoveKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts index 89491823..ad7f5651 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/retime-keyframe.ts @@ -1,12 +1,12 @@ import { EditorCore } from "@/core"; import { resolveAnimationTarget, retimeElementKeyframe } from "@/lib/animation"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import type { AnimationPath } from "@/lib/animation/types"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; export class RetimeKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly propertyPath: AnimationPath; @@ -36,9 +36,9 @@ export class RetimeKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts b/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts index f1eaab9d..9a4c2304 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/update-scalar-keyframe-curve.ts @@ -4,15 +4,15 @@ import { updateScalarKeyframeCurve, } from "@/lib/animation"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import type { AnimationPath, ScalarCurveKeyframePatch, } from "@/lib/animation/types"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; export class UpdateScalarKeyframeCurveCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly propertyPath: AnimationPath; @@ -46,9 +46,9 @@ export class UpdateScalarKeyframeCurveCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts index d0f01104..f436721a 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-effect-param-keyframe.ts @@ -5,13 +5,13 @@ import { resolveAnimationTarget, upsertPathKeyframe, } from "@/lib/animation"; -import { updateElementInTracks } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; import { isVisualElement } from "@/lib/timeline/element-utils"; import type { AnimationInterpolation } from "@/lib/animation/types"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; export class UpsertEffectParamKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly effectId: string; @@ -53,9 +53,9 @@ export class UpsertEffectParamKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts index bd56749a..f77e69b8 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/upsert-keyframe.ts @@ -1,8 +1,8 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; import { resolveAnimationTarget, upsertPathKeyframe } from "@/lib/animation"; -import { updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack } from "@/lib/timeline"; +import { updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; import type { AnimationPath, AnimationInterpolation, @@ -10,7 +10,7 @@ import type { } from "@/lib/animation/types"; export class UpsertKeyframeCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly propertyPath: AnimationPath; @@ -48,9 +48,9 @@ export class UpsertKeyframeCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/masks/index.ts b/apps/web/src/lib/commands/timeline/element/masks/index.ts index 5ceec931..acc47184 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/index.ts +++ b/apps/web/src/lib/commands/timeline/element/masks/index.ts @@ -1,2 +1,2 @@ -export { RemoveMaskCommand } from "./remove-mask";
-export { ToggleMaskInvertedCommand } from "./toggle-mask-inverted";
+export { RemoveMaskCommand } from "./remove-mask"; +export { ToggleMaskInvertedCommand } from "./toggle-mask-inverted"; diff --git a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts b/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts index b0daced7..9f6c5f77 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts +++ b/apps/web/src/lib/commands/timeline/element/masks/remove-mask.ts @@ -1,7 +1,7 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { isMaskableElement, updateElementInTracks } from "@/lib/timeline"; -import type { TimelineTrack, MaskableElement } from "@/lib/timeline"; +import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; +import type { SceneTracks, MaskableElement } from "@/lib/timeline"; function removeMaskFromElement({ element, @@ -16,7 +16,7 @@ function removeMaskFromElement({ } export class RemoveMaskCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly maskId: string; @@ -38,9 +38,9 @@ export class RemoveMaskCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts b/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts index bc812398..2b5c1694 100644 --- a/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts +++ b/apps/web/src/lib/commands/timeline/element/masks/toggle-mask-inverted.ts @@ -1,8 +1,8 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import { isMaskableElement, updateElementInTracks } from "@/lib/timeline"; +import { isMaskableElement, updateElementInSceneTracks } from "@/lib/timeline"; import type { Mask } from "@/lib/masks/types"; -import type { TimelineTrack, MaskableElement } from "@/lib/timeline"; +import type { SceneTracks, MaskableElement } from "@/lib/timeline"; export function toggleMaskInvertedOnElement({ element, @@ -27,7 +27,7 @@ export function toggleMaskInvertedOnElement({ } export class ToggleMaskInvertedCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly trackId: string; private readonly elementId: string; private readonly maskId: string; @@ -49,9 +49,9 @@ export class ToggleMaskInvertedCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const updatedTracks = updateElementInTracks({ + const updatedTracks = updateElementInSceneTracks({ tracks: this.savedState, trackId: this.trackId, elementId: this.elementId, diff --git a/apps/web/src/lib/commands/timeline/element/move-elements.ts b/apps/web/src/lib/commands/timeline/element/move-elements.ts index cf3afa6d..c1fa6897 100644 --- a/apps/web/src/lib/commands/timeline/element/move-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/move-elements.ts @@ -1,18 +1,23 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { EditorCore } from "@/core"; import type { - TimelineTrack, + SceneTracks, TimelineElement, TrackType, + TimelineTrack, } from "@/lib/timeline"; import { buildEmptyTrack, validateElementTrackCompatibility, enforceMainTrackStart, } from "@/lib/timeline/placement"; +import { + findTrackInSceneTracks, + updateTrackInSceneTracks, +} from "@/lib/timeline/track-element-update"; export class MoveElementCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly sourceTrackId: string; private readonly targetTrackId: string; private readonly elementId: string; @@ -42,11 +47,12 @@ export class MoveElementCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const sourceTrack = this.savedState.find( - (track) => track.id === this.sourceTrackId, - ); + const sourceTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.sourceTrackId, + }); const element = sourceTrack?.elements.find( (trackElement) => trackElement.id === this.elementId, ); @@ -55,15 +61,21 @@ export class MoveElementCommand extends Command { throw new Error("Source track or element not found"); } - let targetTrack = this.savedState.find((track) => track.id === this.targetTrackId); + let targetTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.targetTrackId, + }); let tracksToUpdate = this.savedState; if (!targetTrack && this.createTrack) { const newTrack = buildEmptyTrack({ id: this.targetTrackId, type: this.createTrack.type, }); - tracksToUpdate = [...this.savedState]; - tracksToUpdate.splice(this.createTrack.index, 0, newTrack); + tracksToUpdate = insertTrackAtDisplayIndex({ + tracks: this.savedState, + track: newTrack, + insertIndex: this.createTrack.index, + }); targetTrack = newTrack; } if (!targetTrack) { @@ -94,32 +106,34 @@ export class MoveElementCommand extends Command { const isSameTrack = this.sourceTrackId === this.targetTrackId; - const updatedTracks = tracksToUpdate.map((track): TimelineTrack => { - if (isSameTrack && track.id === this.sourceTrackId) { - return { - ...track, - elements: track.elements.map((trackElement) => - trackElement.id === this.elementId ? movedElement : trackElement, - ), - } as typeof track; - } - - if (track.id === this.sourceTrackId) { - const remainingElements = track.elements.filter( - (trackElement) => trackElement.id !== this.elementId, - ); - return { ...track, elements: remainingElements } as typeof track; - } - - if (track.id === this.targetTrackId) { - return { - ...track, - elements: [...track.elements, movedElement], - } as typeof track; - } - - return track; - }); + const updatedTracks = isSameTrack + ? updateTrackInSceneTracks({ + tracks: tracksToUpdate, + trackId: this.sourceTrackId, + update: (track) => ({ + ...track, + elements: track.elements.map((trackElement) => + trackElement.id === this.elementId ? movedElement : trackElement, + ), + }), + }) + : updateTrackInSceneTracks({ + tracks: updateTrackInSceneTracks({ + tracks: tracksToUpdate, + trackId: this.sourceTrackId, + update: (track) => ({ + ...track, + elements: track.elements.filter( + (trackElement) => trackElement.id !== this.elementId, + ), + }), + }), + trackId: this.targetTrackId, + update: (track) => ({ + ...track, + elements: [...track.elements, movedElement], + }), + }); editor.timeline.updateTracks(updatedTracks); return undefined; @@ -132,3 +146,41 @@ export class MoveElementCommand extends Command { } } } + +function insertTrackAtDisplayIndex({ + tracks, + track, + insertIndex, +}: { + tracks: SceneTracks; + track: TimelineTrack; + insertIndex: number; +}): SceneTracks { + if (track.type === "audio") { + const audioInsertIndex = Math.max( + 0, + Math.min(insertIndex - tracks.overlay.length - 1, tracks.audio.length), + ); + return { + ...tracks, + audio: [ + ...tracks.audio.slice(0, audioInsertIndex), + track, + ...tracks.audio.slice(audioInsertIndex), + ], + }; + } + + const overlayInsertIndex = Math.max( + 0, + Math.min(insertIndex, tracks.overlay.length), + ); + return { + ...tracks, + overlay: [ + ...tracks.overlay.slice(0, overlayInsertIndex), + track, + ...tracks.overlay.slice(overlayInsertIndex), + ], + }; +} diff --git a/apps/web/src/lib/commands/timeline/element/split-elements.ts b/apps/web/src/lib/commands/timeline/element/split-elements.ts index 79722b4c..9799f7e8 100644 --- a/apps/web/src/lib/commands/timeline/element/split-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/split-elements.ts @@ -1,5 +1,5 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; import { EditorCore } from "@/core"; import { isRetimableElement } from "@/lib/timeline"; @@ -7,7 +7,7 @@ import { splitAnimationsAtTime } from "@/lib/animation"; import { getSourceSpanAtClipTime } from "@/lib/retime"; export class SplitElementsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private rightSideElements: { trackId: string; elementId: string }[] = []; private readonly elements: { trackId: string; elementId: string }[]; private readonly splitTime: number; @@ -34,10 +34,12 @@ export class SplitElementsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; this.rightSideElements = []; - const updatedTracks = this.savedState.map((track) => { + const splitTrack = <TTrack extends { id: string; elements: TimelineElement[] }>( + track: TTrack, + ): TTrack => { const elementsToSplit = this.elements.filter( (target) => target.trackId === track.id, ); @@ -148,8 +150,14 @@ export class SplitElementsCommand extends Command { ]; }); - return { ...track, elements } as typeof track; - }); + return { ...track, elements } as TTrack; + }; + + const updatedTracks: SceneTracks = { + overlay: this.savedState.overlay.map((track) => splitTrack(track)), + main: splitTrack(this.savedState.main), + audio: this.savedState.audio.map((track) => splitTrack(track)), + }; editor.timeline.updateTracks(updatedTracks); diff --git a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts index fc195cba..36b3b7a7 100644 --- a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts +++ b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts @@ -3,18 +3,20 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; import { buildSeparatedAudioElement, canExtractSourceAudio, - canRecoverSourceAudio, + isSourceAudioSeparated, } from "@/lib/timeline/audio-separation"; -import { - applyPlacement, - resolveTrackPlacement, -} from "@/lib/timeline/placement"; -import { updateElementInTracks } from "@/lib/timeline/track-element-update"; -import type { TimelineTrack, VideoElement } from "@/lib/timeline/types"; +import { buildEmptyTrack } from "@/lib/timeline/placement"; +import { updateElementInSceneTracks } from "@/lib/timeline/track-element-update"; +import type { + AudioTrack, + SceneTracks, + TimelineElement, + VideoElement, +} from "@/lib/timeline/types"; import { generateUUID } from "@/utils/id"; export class ToggleSourceAudioSeparationCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; constructor( private readonly params: { @@ -27,27 +29,27 @@ export class ToggleSourceAudioSeparationCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const trackIndex = this.savedState.findIndex( + const sourceTrack = [ + ...this.savedState.overlay, + this.savedState.main, + ...this.savedState.audio, + ].find( (track) => track.id === this.params.trackId, ); - if (trackIndex < 0) { + if (!sourceTrack) { return; } - - const sourceTrack = this.savedState[trackIndex]; const sourceElement = sourceTrack.elements.find( (element) => element.id === this.params.elementId, - ); - if (!sourceElement) { - return; - } - if (sourceElement.type !== "video") { + ) as TimelineElement | undefined; + if (!sourceElement || sourceElement.type !== "video") { return; } + const videoElement: VideoElement = sourceElement; - if (canRecoverSourceAudio(sourceElement)) { + if (isSourceAudioSeparated({ element: videoElement })) { editor.timeline.updateTracks( updateSourceAudioEnabled({ tracks: this.savedState, @@ -62,47 +64,34 @@ export class ToggleSourceAudioSeparationCommand extends Command { const mediaAsset = editor .media .getAssets() - .find((asset) => asset.id === sourceElement.mediaId); - if (!canExtractSourceAudio(sourceElement, mediaAsset)) { + .find((asset) => asset.id === videoElement.mediaId); + if (!canExtractSourceAudio(videoElement, mediaAsset)) { return; } - if (sourceElement.duration <= 0) { + if (videoElement.duration <= 0) { return; } const separatedAudioElement = { ...buildSeparatedAudioElement({ - sourceElement, + sourceElement: videoElement, }), id: generateUUID(), }; - const placementResult = resolveTrackPlacement({ - tracks: this.savedState, - trackType: "audio", - timeSpans: [ - { - startTime: separatedAudioElement.startTime, - duration: separatedAudioElement.duration, - }, - ], - strategy: { type: "aboveSource", sourceTrackIndex: trackIndex }, - }); - if (!placementResult) { - return; - } - - const appliedPlacement = applyPlacement({ - tracks: this.savedState, - placementResult, + const newAudioTrack = { + ...buildEmptyTrack({ + id: generateUUID(), + type: "audio", + }), elements: [separatedAudioElement], - }); - if (!appliedPlacement) { - return; - } + } as AudioTrack; editor.timeline.updateTracks( updateSourceAudioEnabled({ - tracks: appliedPlacement.updatedTracks, + tracks: { + ...this.savedState, + audio: [...this.savedState.audio, newAudioTrack], + }, trackId: this.params.trackId, elementId: this.params.elementId, isSourceAudioEnabled: false, @@ -126,12 +115,12 @@ function updateSourceAudioEnabled({ elementId, isSourceAudioEnabled, }: { - tracks: TimelineTrack[]; + tracks: SceneTracks; trackId: string; elementId: string; isSourceAudioEnabled: boolean; -}): TimelineTrack[] { - return updateElementInTracks({ +}): SceneTracks { + return updateElementInSceneTracks({ tracks, trackId, elementId, diff --git a/apps/web/src/lib/commands/timeline/element/update-elements.ts b/apps/web/src/lib/commands/timeline/element/update-elements.ts index abeec842..54d1ca88 100644 --- a/apps/web/src/lib/commands/timeline/element/update-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/update-elements.ts @@ -1,11 +1,14 @@ import { EditorCore } from "@/core"; import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; -import { updateElementInTracks } from "@/lib/timeline"; +import type { SceneTracks, TimelineElement } from "@/lib/timeline"; +import { + findTrackInSceneTracks, + updateElementInSceneTracks, +} from "@/lib/timeline"; import { applyElementUpdate } from "@/lib/timeline/update-pipeline"; export class UpdateElementsCommand extends Command { - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; private readonly updates: Array<{ trackId: string; elementId: string; @@ -27,13 +30,14 @@ export class UpdateElementsCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; let updatedTracks = this.savedState; for (const updateEntry of this.updates) { - const currentTrack = updatedTracks.find( - (track) => track.id === updateEntry.trackId, - ); + const currentTrack = findTrackInSceneTracks({ + tracks: updatedTracks, + trackId: updateEntry.trackId, + }); const currentElement = currentTrack?.elements.find( (element) => element.id === updateEntry.elementId, ); @@ -50,7 +54,7 @@ export class UpdateElementsCommand extends Command { }, }); - updatedTracks = updateElementInTracks({ + updatedTracks = updateElementInSceneTracks({ tracks: updatedTracks, trackId: updateEntry.trackId, elementId: updateEntry.elementId, diff --git a/apps/web/src/lib/commands/timeline/index.ts b/apps/web/src/lib/commands/timeline/index.ts index d0bfd5e9..cb285d94 100644 --- a/apps/web/src/lib/commands/timeline/index.ts +++ b/apps/web/src/lib/commands/timeline/index.ts @@ -1,5 +1,5 @@ -export * from "./track";
-export * from "./element";
-export * from "./clipboard";
-
-export { TracksSnapshotCommand } from "./tracks-snapshot";
+export * from "./track"; +export * from "./element"; +export * from "./clipboard"; + +export { TracksSnapshotCommand } from "./tracks-snapshot"; diff --git a/apps/web/src/lib/commands/timeline/track/add-track.ts b/apps/web/src/lib/commands/timeline/track/add-track.ts index 29bdbb0a..66b9c348 100644 --- a/apps/web/src/lib/commands/timeline/track/add-track.ts +++ b/apps/web/src/lib/commands/timeline/track/add-track.ts @@ -1,5 +1,5 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TrackType, TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks, TrackType } from "@/lib/timeline"; import { generateUUID } from "@/utils/id"; import { EditorCore } from "@/core"; import { @@ -9,7 +9,7 @@ import { export class AddTrackCommand extends Command { private trackId: string; - private savedState: TimelineTrack[] | null = null; + private savedState: SceneTracks | null = null; constructor( private type: TrackType, @@ -21,21 +21,28 @@ export class AddTrackCommand extends Command { execute(): CommandResult | undefined { const editor = EditorCore.getInstance(); - this.savedState = editor.timeline.getTracks(); + this.savedState = editor.scenes.getActiveScene().tracks; - const newTrack: TimelineTrack = buildEmptyTrack({ - id: this.trackId, - type: this.type, - }); - - const updatedTracks = [...(this.savedState || [])]; const insertIndex = this.index ?? getDefaultInsertIndexForTrack({ - tracks: updatedTracks, + tracks: this.savedState, trackType: this.type, }); - updatedTracks.splice(insertIndex, 0, newTrack); + + const updatedTracks = + this.type === "audio" + ? buildAudioTrackState({ + tracks: this.savedState, + insertIndex, + trackId: this.trackId, + }) + : buildOverlayTrackState({ + tracks: this.savedState, + insertIndex, + trackId: this.trackId, + trackType: this.type, + }); editor.timeline.updateTracks(updatedTracks); return undefined; @@ -52,3 +59,60 @@ export class AddTrackCommand extends Command { return this.trackId; } } + +function buildAudioTrackState({ + tracks, + insertIndex, + trackId, +}: { + tracks: SceneTracks; + insertIndex: number; + trackId: string; +}): SceneTracks { + const audioInsertIndex = Math.max( + 0, + insertIndex - tracks.overlay.length - 1, + ); + const newTrack = buildEmptyTrack({ + id: trackId, + type: "audio", + }); + return { + ...tracks, + audio: [ + ...tracks.audio.slice(0, audioInsertIndex), + newTrack, + ...tracks.audio.slice(audioInsertIndex), + ], + }; +} + +function buildOverlayTrackState({ + tracks, + insertIndex, + trackId, + trackType, +}: { + tracks: SceneTracks; + insertIndex: number; + trackId: string; + trackType: Exclude<TrackType, "audio">; +}): SceneTracks { + const overlayInsertIndex = Math.min(insertIndex, tracks.overlay.length); + const newTrack = + trackType === "video" + ? buildEmptyTrack({ id: trackId, type: "video" }) + : trackType === "text" + ? buildEmptyTrack({ id: trackId, type: "text" }) + : trackType === "graphic" + ? buildEmptyTrack({ id: trackId, type: "graphic" }) + : buildEmptyTrack({ id: trackId, type: "effect" }); + return { + ...tracks, + overlay: [ + ...tracks.overlay.slice(0, overlayInsertIndex), + newTrack, + ...tracks.overlay.slice(overlayInsertIndex), + ], + }; +} diff --git a/apps/web/src/lib/commands/timeline/track/remove-track.ts b/apps/web/src/lib/commands/timeline/track/remove-track.ts index c2ea7ef0..5fcaa898 100644 --- a/apps/web/src/lib/commands/timeline/track/remove-track.ts +++ b/apps/web/src/lib/commands/timeline/track/remove-track.ts @@ -1,35 +1,30 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command";
-import { EditorCore } from "@/core";
-import type { TimelineTrack } from "@/lib/timeline";
-import { getMainTrack } from "@/lib/timeline/placement";
-
-export class RemoveTrackCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
-
- constructor(private trackId: string) {
- super();
- }
-
- execute(): CommandResult | undefined {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
- const targetTrack = this.savedState.find(
- (track) => track.id === this.trackId,
- );
- const mainTrack = getMainTrack({ tracks: this.savedState });
- if (mainTrack?.id === targetTrack?.id) {
- return;
- }
- const updatedTracks = this.savedState.filter(
- (track) => track.id !== this.trackId,
- );
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import { EditorCore } from "@/core"; +import type { SceneTracks } from "@/lib/timeline"; + +export class RemoveTrackCommand extends Command { + private savedState: SceneTracks | null = null; + + constructor(private trackId: string) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + const updatedTracks: SceneTracks = { + ...this.savedState, + overlay: this.savedState.overlay.filter((track) => track.id !== this.trackId), + audio: this.savedState.audio.filter((track) => track.id !== this.trackId), + }; + editor.timeline.updateTracks(updatedTracks); + return undefined; + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts b/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts index 513fffc7..7ffe2fd7 100644 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts +++ b/apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts @@ -1,39 +1,41 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { TimelineTrack } from "@/lib/timeline";
-import { EditorCore } from "@/core";
-import { canTrackHaveAudio } from "@/lib/timeline";
-
-export class ToggleTrackMuteCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
-
- constructor(private trackId: string) {
- super();
- }
-
- execute(): CommandResult | undefined {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const targetTrack = this.savedState.find(
- (track) => track.id === this.trackId,
- );
- if (!targetTrack) {
- return;
- }
-
- const updatedTracks = this.savedState.map((track) =>
- track.id === this.trackId && canTrackHaveAudio(track)
- ? { ...track, muted: !track.muted }
- : track,
- );
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { SceneTracks } from "@/lib/timeline"; +import { EditorCore } from "@/core"; +import { canTrackHaveAudio, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/lib/timeline"; + +export class ToggleTrackMuteCommand extends Command { + private savedState: SceneTracks | null = null; + + constructor(private trackId: string) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + + const targetTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.trackId, + }); + if (!targetTrack) { + return; + } + + const updatedTracks = updateTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.trackId, + update: (track) => + canTrackHaveAudio(track) ? { ...track, muted: !track.muted } : track, + }); + + editor.timeline.updateTracks(updatedTracks); + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts b/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts index 077c21db..2292c83a 100644 --- a/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts +++ b/apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts @@ -1,40 +1,45 @@ -import { Command, type CommandResult } from "@/lib/commands/base-command";
-import type { TimelineTrack } from "@/lib/timeline";
-import { EditorCore } from "@/core";
-import { canTrackBeHidden } from "@/lib/timeline";
-
-export class ToggleTrackVisibilityCommand extends Command {
- private savedState: TimelineTrack[] | null = null;
-
- constructor(private trackId: string) {
- super();
- }
-
- execute(): CommandResult | undefined {
- const editor = EditorCore.getInstance();
- this.savedState = editor.timeline.getTracks();
-
- const targetTrack = this.savedState.find(
- (track) => track.id === this.trackId,
- );
- if (!targetTrack) {
- return;
- }
-
- const updatedTracks = this.savedState.map((track) => {
- if (track.id === this.trackId && canTrackBeHidden(track)) {
- return { ...track, hidden: !track.hidden };
- }
- return track;
- });
-
- editor.timeline.updateTracks(updatedTracks);
- }
-
- undo(): void {
- if (this.savedState) {
- const editor = EditorCore.getInstance();
- editor.timeline.updateTracks(this.savedState);
- }
- }
-}
+import { Command, type CommandResult } from "@/lib/commands/base-command"; +import type { SceneTracks } from "@/lib/timeline"; +import { EditorCore } from "@/core"; +import { canTrackBeHidden, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/lib/timeline"; + +export class ToggleTrackVisibilityCommand extends Command { + private savedState: SceneTracks | null = null; + + constructor(private trackId: string) { + super(); + } + + execute(): CommandResult | undefined { + const editor = EditorCore.getInstance(); + this.savedState = editor.scenes.getActiveScene().tracks; + + const targetTrack = findTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.trackId, + }); + if (!targetTrack) { + return; + } + + const updatedTracks = updateTrackInSceneTracks({ + tracks: this.savedState, + trackId: this.trackId, + update: (track) => { + if (canTrackBeHidden(track)) { + return { ...track, hidden: !track.hidden }; + } + return track; + }, + }); + + editor.timeline.updateTracks(updatedTracks); + } + + undo(): void { + if (this.savedState) { + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } + } +} diff --git a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts b/apps/web/src/lib/commands/timeline/tracks-snapshot.ts index 2493f19b..daed6a9e 100644 --- a/apps/web/src/lib/commands/timeline/tracks-snapshot.ts +++ b/apps/web/src/lib/commands/timeline/tracks-snapshot.ts @@ -1,11 +1,11 @@ import { Command, type CommandResult } from "@/lib/commands/base-command"; -import type { TimelineTrack } from "@/lib/timeline"; +import type { SceneTracks } from "@/lib/timeline"; import { EditorCore } from "@/core"; export class TracksSnapshotCommand extends Command { constructor( - private before: TimelineTrack[], - private after: TimelineTrack[], + private before: SceneTracks, + private after: SceneTracks, ) { super(); } |
