diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-07 04:41:27 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-07 04:41:27 +0200 |
| commit | cbd1b8212370c67b34a70dccd21bd82de72fead2 (patch) | |
| tree | d97cbee2aae22c22b59c59a50e9f8a6af0ae8ccf /apps/web/src/lib/commands/timeline/element | |
| parent | 4f7d401a974047d37673ee8e3840dceaaddc295c (diff) | |
| download | opencut-cbd1b8212370c67b34a70dccd21bd82de72fead2.tar.gz | |
refactor: structured track layout per scene
Diffstat (limited to 'apps/web/src/lib/commands/timeline/element')
23 files changed, 306 insertions, 224 deletions
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, |
