refactor: move track pruning into a post-command reactor

This commit is contained in:
Maze Winther 2026-03-31 23:39:43 +02:00
parent f8fb238b22
commit dc82826951
6 changed files with 469 additions and 465 deletions

View File

@ -1,53 +1,63 @@
import { PlaybackManager } from "./managers/playback-manager"; import { PlaybackManager } from "./managers/playback-manager";
import { TimelineManager } from "./managers/timeline-manager"; import { TimelineManager } from "./managers/timeline-manager";
import { ScenesManager } from "./managers/scenes-manager"; import { ScenesManager } from "./managers/scenes-manager";
import { ProjectManager } from "./managers/project-manager"; import { ProjectManager } from "./managers/project-manager";
import { MediaManager } from "./managers/media-manager"; import { MediaManager } from "./managers/media-manager";
import { RendererManager } from "./managers/renderer-manager"; import { RendererManager } from "./managers/renderer-manager";
import { CommandManager } from "./managers/commands"; import { CommandManager } from "./managers/commands";
import { SaveManager } from "./managers/save-manager"; import { SaveManager } from "./managers/save-manager";
import { AudioManager } from "./managers/audio-manager"; import { AudioManager } from "./managers/audio-manager";
import { SelectionManager } from "./managers/selection-manager"; import { SelectionManager } from "./managers/selection-manager";
import { registerDefaultEffects } from "@/lib/effects"; import { registerDefaultEffects } from "@/lib/effects";
import { registerDefaultMasks } from "@/lib/masks"; import { registerDefaultMasks } from "@/lib/masks";
import { isMainTrack } from "@/lib/timeline/placement";
export class EditorCore {
private static instance: EditorCore | null = null; export class EditorCore {
public readonly timeline: TimelineManager; private static instance: EditorCore | null = null;
public readonly command: CommandManager; public readonly timeline: TimelineManager;
public readonly playback: PlaybackManager; public readonly command: CommandManager;
public readonly scenes: ScenesManager; public readonly playback: PlaybackManager;
public readonly project: ProjectManager; public readonly scenes: ScenesManager;
public readonly media: MediaManager; public readonly project: ProjectManager;
public readonly renderer: RendererManager; public readonly media: MediaManager;
public readonly save: SaveManager; public readonly renderer: RendererManager;
public readonly audio: AudioManager; public readonly save: SaveManager;
public readonly selection: SelectionManager; public readonly audio: AudioManager;
public readonly selection: SelectionManager;
private constructor() {
registerDefaultEffects(); private constructor() {
registerDefaultMasks(); registerDefaultEffects();
this.command = new CommandManager(this); registerDefaultMasks();
this.timeline = new TimelineManager(this); this.command = new CommandManager(this);
this.playback = new PlaybackManager(this); this.timeline = new TimelineManager(this);
this.scenes = new ScenesManager(this); this.playback = new PlaybackManager(this);
this.project = new ProjectManager(this); this.scenes = new ScenesManager(this);
this.media = new MediaManager(this); this.project = new ProjectManager(this);
this.renderer = new RendererManager(this); this.media = new MediaManager(this);
this.save = new SaveManager(this); this.renderer = new RendererManager(this);
this.audio = new AudioManager(this); this.save = new SaveManager(this);
this.selection = new SelectionManager(this); this.audio = new AudioManager(this);
this.save.start(); this.selection = new SelectionManager(this);
} this.command.registerReactor(() => {
const tracks = this.timeline.getTracks();
static getInstance(): EditorCore { const prunedTracks = tracks.filter(
if (!EditorCore.instance) { (track) => track.elements.length > 0 || isMainTrack(track),
EditorCore.instance = new EditorCore(); );
} if (prunedTracks.length !== tracks.length) {
return EditorCore.instance; this.timeline.updateTracks(prunedTracks);
} }
});
static reset(): void { this.save.start();
EditorCore.instance = null; }
}
} static getInstance(): EditorCore {
if (!EditorCore.instance) {
EditorCore.instance = new EditorCore();
}
return EditorCore.instance;
}
static reset(): void {
EditorCore.instance = null;
}
}

View File

@ -11,6 +11,7 @@ interface CommandHistoryEntry {
export class CommandManager { export class CommandManager {
private history: CommandHistoryEntry[] = []; private history: CommandHistoryEntry[] = [];
private redoStack: CommandHistoryEntry[] = []; private redoStack: CommandHistoryEntry[] = [];
private reactors: Array<() => void> = [];
constructor(private editor: EditorCore) {} constructor(private editor: EditorCore) {}
@ -18,6 +19,7 @@ export class CommandManager {
const previousSelection = this.getSelectionSnapshot(); const previousSelection = this.getSelectionSnapshot();
const result = command.execute(); const result = command.execute();
const selectionOverride = this.applySelectionOverride(result); const selectionOverride = this.applySelectionOverride(result);
this.runReactors();
this.history.push({ this.history.push({
command, command,
previousSelection, previousSelection,
@ -35,6 +37,10 @@ export class CommandManager {
this.redoStack = []; this.redoStack = [];
} }
registerReactor(reactor: () => void): void {
this.reactors.push(reactor);
}
undo(): void { undo(): void {
if (this.history.length === 0) return; if (this.history.length === 0) return;
const entry = this.history.pop(); const entry = this.history.pop();
@ -64,6 +70,7 @@ export class CommandManager {
const previousSelection = this.getSelectionSnapshot(); const previousSelection = this.getSelectionSnapshot();
const result = entry.command.redo(); const result = entry.command.redo();
const selectionOverride = this.applySelectionOverride(result); const selectionOverride = this.applySelectionOverride(result);
this.runReactors();
this.history.push({ this.history.push({
command: entry.command, command: entry.command,
@ -100,4 +107,10 @@ export class CommandManager {
this.editor.selection.setSelectedElements({ elements: selectionOverride }); this.editor.selection.setSelectedElements({ elements: selectionOverride });
return selectionOverride; return selectionOverride;
} }
private runReactors(): void {
for (const reactor of this.reactors) {
reactor();
}
}
} }

View File

@ -1,184 +1,184 @@
import { Command, type CommandResult } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import type { import type {
TimelineTrack, TimelineTrack,
TimelineElement, TimelineElement,
ClipboardItem, ClipboardItem,
} from "@/lib/timeline"; } from "@/lib/timeline";
import { generateUUID } from "@/utils/id"; import { generateUUID } from "@/utils/id";
import { import {
applyPlacement, applyPlacement,
resolveTrackPlacement, resolveTrackPlacement,
isMainTrack, isMainTrack,
enforceMainTrackStart, enforceMainTrackStart,
} from "@/lib/timeline/placement"; } from "@/lib/timeline/placement";
import { cloneAnimations } from "@/lib/animation"; import { cloneAnimations } from "@/lib/animation";
export class PasteCommand extends Command { export class PasteCommand extends Command {
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private pastedElements: { trackId: string; elementId: string }[] = []; private pastedElements: { trackId: string; elementId: string }[] = [];
private readonly time: number; private readonly time: number;
private readonly clipboardItems: ClipboardItem[]; private readonly clipboardItems: ClipboardItem[];
constructor({ constructor({
time, time,
clipboardItems, clipboardItems,
}: { }: {
time: number; time: number;
clipboardItems: ClipboardItem[]; clipboardItems: ClipboardItem[];
}) { }) {
super(); super();
this.time = time; this.time = time;
this.clipboardItems = clipboardItems; this.clipboardItems = clipboardItems;
} }
execute(): CommandResult | undefined { execute(): CommandResult | undefined {
if (this.clipboardItems.length === 0) return; if (this.clipboardItems.length === 0) return;
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
this.pastedElements = []; this.pastedElements = [];
const minStart = Math.min( const minStart = Math.min(
...this.clipboardItems.map((item) => item.element.startTime), ...this.clipboardItems.map((item) => item.element.startTime),
); );
let updatedTracks = [...this.savedState]; let updatedTracks = [...this.savedState];
const itemsByTrackId = groupClipboardItemsByTrackId({ const itemsByTrackId = groupClipboardItemsByTrackId({
clipboardItems: this.clipboardItems, clipboardItems: this.clipboardItems,
}); });
for (const [trackId, items] of itemsByTrackId) { for (const [trackId, items] of itemsByTrackId) {
const elementsToAdd = buildPastedElements({ const elementsToAdd = buildPastedElements({
items, items,
minStart, minStart,
time: this.time, time: this.time,
}); });
if (elementsToAdd.length === 0) { if (elementsToAdd.length === 0) {
continue; continue;
} }
const trackType = items[0].trackType; const trackType = items[0].trackType;
const sourceTrackIndex = updatedTracks.findIndex( const sourceTrackIndex = updatedTracks.findIndex(
(track) => track.id === trackId, (track) => track.id === trackId,
); );
const placementResult = resolveTrackPlacement({ const placementResult = resolveTrackPlacement({
tracks: updatedTracks, tracks: updatedTracks,
trackType, trackType,
timeSpans: elementsToAdd.map((element) => ({ timeSpans: elementsToAdd.map((element) => ({
startTime: element.startTime, startTime: element.startTime,
duration: element.duration, duration: element.duration,
})), })),
strategy: { type: "aboveSource", sourceTrackIndex }, strategy: { type: "aboveSource", sourceTrackIndex },
}); });
if (!placementResult) { if (!placementResult) {
continue; continue;
} }
let elementsForPlacement = elementsToAdd; let elementsForPlacement = elementsToAdd;
if (placementResult.kind === "existingTrack") { if (placementResult.kind === "existingTrack") {
const targetTrack = updatedTracks[placementResult.trackIndex]; const targetTrack = updatedTracks[placementResult.trackIndex];
if (isMainTrack(targetTrack)) { if (isMainTrack(targetTrack)) {
const earliestElement = elementsToAdd.reduce((earliest, element) => const earliestElement = elementsToAdd.reduce((earliest, element) =>
element.startTime < earliest.startTime ? element : earliest, element.startTime < earliest.startTime ? element : earliest,
); );
const adjustedEarliestStartTime = enforceMainTrackStart({ const adjustedEarliestStartTime = enforceMainTrackStart({
tracks: updatedTracks, tracks: updatedTracks,
targetTrackId: targetTrack.id, targetTrackId: targetTrack.id,
requestedStartTime: earliestElement.startTime, requestedStartTime: earliestElement.startTime,
}); });
const delta = adjustedEarliestStartTime - earliestElement.startTime; const delta = adjustedEarliestStartTime - earliestElement.startTime;
if (delta !== 0) { if (delta !== 0) {
elementsForPlacement = elementsToAdd.map((element) => ({ elementsForPlacement = elementsToAdd.map((element) => ({
...element, ...element,
startTime: Math.max(0, element.startTime + delta), startTime: Math.max(0, element.startTime + delta),
})); }));
} }
} }
} }
const applied = applyPlacement({ const applied = applyPlacement({
tracks: updatedTracks, tracks: updatedTracks,
placementResult, placementResult,
elements: elementsForPlacement, elements: elementsForPlacement,
}); });
if (!applied) { if (!applied) {
continue; continue;
} }
updatedTracks = applied.updatedTracks; updatedTracks = applied.updatedTracks;
for (const element of elementsForPlacement) { for (const element of elementsForPlacement) {
this.pastedElements.push({ this.pastedElements.push({
trackId: applied.targetTrackId, trackId: applied.targetTrackId,
elementId: element.id, elementId: element.id,
}); });
} }
} }
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
if (this.pastedElements.length > 0) { if (this.pastedElements.length > 0) {
return { select: this.pastedElements }; return { select: this.pastedElements };
} }
} }
undo(): void { undo(): void {
if (this.savedState) { if (this.savedState) {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState); editor.timeline.updateTracks(this.savedState);
} }
} }
getPastedElements(): { trackId: string; elementId: string }[] { getPastedElements(): { trackId: string; elementId: string }[] {
return this.pastedElements; return this.pastedElements;
} }
} }
function groupClipboardItemsByTrackId({ function groupClipboardItemsByTrackId({
clipboardItems, clipboardItems,
}: { }: {
clipboardItems: ClipboardItem[]; clipboardItems: ClipboardItem[];
}): Map<string, ClipboardItem[]> { }): Map<string, ClipboardItem[]> {
const groupedItems = new Map<string, ClipboardItem[]>(); const groupedItems = new Map<string, ClipboardItem[]>();
for (const item of clipboardItems) { for (const item of clipboardItems) {
const existingItems = groupedItems.get(item.trackId) ?? []; const existingItems = groupedItems.get(item.trackId) ?? [];
groupedItems.set(item.trackId, [...existingItems, item]); groupedItems.set(item.trackId, [...existingItems, item]);
} }
return groupedItems; return groupedItems;
} }
function buildPastedElements({ function buildPastedElements({
items, items,
minStart, minStart,
time, time,
}: { }: {
items: ClipboardItem[]; items: ClipboardItem[];
minStart: number; minStart: number;
time: number; time: number;
}): TimelineElement[] { }): TimelineElement[] {
const elementsToAdd: TimelineElement[] = []; const elementsToAdd: TimelineElement[] = [];
for (const item of items) { for (const item of items) {
const relativeOffset = item.element.startTime - minStart; const relativeOffset = item.element.startTime - minStart;
const startTime = Math.max(0, time + relativeOffset); const startTime = Math.max(0, time + relativeOffset);
const newElementId = generateUUID(); const newElementId = generateUUID();
elementsToAdd.push({ elementsToAdd.push({
...item.element, ...item.element,
id: newElementId, id: newElementId,
startTime, startTime,
animations: cloneAnimations({ animations: cloneAnimations({
animations: item.element.animations, animations: item.element.animations,
shouldRegenerateKeyframeIds: true, shouldRegenerateKeyframeIds: true,
}), }),
} as TimelineElement); } as TimelineElement);
} }
return elementsToAdd; return elementsToAdd;
} }

View File

@ -1,84 +1,81 @@
import { Command, type CommandResult } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline"; import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { rippleShiftElements } from "@/lib/timeline"; import { rippleShiftElements } from "@/lib/timeline";
import { isMainTrack } from "@/lib/timeline/placement";
export class DeleteElementsCommand extends Command {
export class DeleteElementsCommand extends Command { private savedState: TimelineTrack[] | null = null;
private savedState: TimelineTrack[] | null = null; private readonly elements: { trackId: string; elementId: string }[];
private readonly elements: { trackId: string; elementId: string }[]; private readonly rippleEnabled: boolean;
private readonly rippleEnabled: boolean;
constructor({
constructor({ elements,
elements, rippleEnabled = false,
rippleEnabled = false, }: {
}: { elements: { trackId: string; elementId: string }[];
elements: { trackId: string; elementId: string }[]; rippleEnabled?: boolean;
rippleEnabled?: boolean; }) {
}) { super();
super(); this.elements = elements;
this.elements = elements; this.rippleEnabled = rippleEnabled;
this.rippleEnabled = rippleEnabled; }
}
execute(): CommandResult {
execute(): CommandResult { const editor = EditorCore.getInstance();
const editor = EditorCore.getInstance(); this.savedState = editor.timeline.getTracks();
this.savedState = editor.timeline.getTracks();
const updatedTracks = this.savedState.map((track) => {
const updatedTracks = this.savedState const elementsToDeleteOnTrack = this.elements.filter(
.map((track) => { (target) => target.trackId === track.id,
const elementsToDeleteOnTrack = this.elements.filter( );
(target) => target.trackId === track.id, const hasElementsToDelete = elementsToDeleteOnTrack.length > 0;
);
const hasElementsToDelete = elementsToDeleteOnTrack.length > 0; if (!hasElementsToDelete) {
return track;
if (!hasElementsToDelete) { }
return track;
} const deletedElementInfos = elementsToDeleteOnTrack
.map((target) =>
const deletedElementInfos = elementsToDeleteOnTrack track.elements.find((element) => element.id === target.elementId),
.map((target) => )
track.elements.find((element) => element.id === target.elementId), .filter((element): element is NonNullable<typeof element> => element !== undefined)
) .map((element) => ({ startTime: element.startTime, duration: element.duration }));
.filter((element): element is NonNullable<typeof element> => element !== undefined)
.map((element) => ({ startTime: element.startTime, duration: element.duration })); let elements = track.elements.filter(
(element) =>
let elements = track.elements.filter( !this.elements.some(
(element) => (target) =>
!this.elements.some( target.trackId === track.id && target.elementId === element.id,
(target) => ),
target.trackId === track.id && target.elementId === element.id, );
),
); if (this.rippleEnabled && deletedElementInfos.length > 0) {
const sortedByStartDesc = [...deletedElementInfos].sort(
if (this.rippleEnabled && deletedElementInfos.length > 0) { (a, b) => b.startTime - a.startTime,
const sortedByStartDesc = [...deletedElementInfos].sort( );
(a, b) => b.startTime - a.startTime, for (const { startTime, duration } of sortedByStartDesc) {
); elements = rippleShiftElements({
for (const { startTime, duration } of sortedByStartDesc) { elements,
elements = rippleShiftElements({ afterTime: startTime,
elements, shiftAmount: duration,
afterTime: startTime, });
shiftAmount: duration, }
}); }
}
} return { ...track, elements } as typeof track;
});
return { ...track, elements } as typeof track;
}) editor.timeline.updateTracks(updatedTracks);
.filter((track) => track.elements.length > 0 || isMainTrack(track));
return {
editor.timeline.updateTracks(updatedTracks); select: [],
};
return { }
select: [],
}; undo(): void {
} if (this.savedState) {
const editor = EditorCore.getInstance();
undo(): void { editor.timeline.updateTracks(this.savedState);
if (this.savedState) { }
const editor = EditorCore.getInstance(); }
editor.timeline.updateTracks(this.savedState); }
}
}
}

View File

@ -1,127 +1,127 @@
import { Command, type CommandResult } from "@/lib/commands/base-command"; import { Command, type CommandResult } from "@/lib/commands/base-command";
import type { TimelineElement, TimelineTrack } from "@/lib/timeline"; import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
import { generateUUID } from "@/utils/id"; import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core"; import { EditorCore } from "@/core";
import { applyPlacement, resolveTrackPlacement } from "@/lib/timeline/placement"; import { applyPlacement, resolveTrackPlacement } from "@/lib/timeline/placement";
import { cloneAnimations } from "@/lib/animation"; import { cloneAnimations } from "@/lib/animation";
interface DuplicateElementsParams { interface DuplicateElementsParams {
elements: { trackId: string; elementId: string }[]; elements: { trackId: string; elementId: string }[];
} }
export class DuplicateElementsCommand extends Command { export class DuplicateElementsCommand extends Command {
private duplicatedElements: { trackId: string; elementId: string }[] = []; private duplicatedElements: { trackId: string; elementId: string }[] = [];
private savedState: TimelineTrack[] | null = null; private savedState: TimelineTrack[] | null = null;
private elements: DuplicateElementsParams["elements"]; private elements: DuplicateElementsParams["elements"];
constructor({ elements }: DuplicateElementsParams) { constructor({ elements }: DuplicateElementsParams) {
super(); super();
this.elements = elements; this.elements = elements;
} }
execute(): CommandResult | undefined { execute(): CommandResult | undefined {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
this.savedState = editor.timeline.getTracks(); this.savedState = editor.timeline.getTracks();
this.duplicatedElements = []; this.duplicatedElements = [];
let updatedTracks = [...this.savedState]; let updatedTracks = [...this.savedState];
for (const track of this.savedState) { for (const track of this.savedState) {
const elementsToDuplicate = this.elements.filter( const elementsToDuplicate = this.elements.filter(
(elementEntry) => elementEntry.trackId === track.id, (elementEntry) => elementEntry.trackId === track.id,
); );
if (elementsToDuplicate.length === 0) { if (elementsToDuplicate.length === 0) {
continue; continue;
} }
const elementIdsToDuplicate = new Set( const elementIdsToDuplicate = new Set(
elementsToDuplicate.map((element) => element.elementId), elementsToDuplicate.map((element) => element.elementId),
); );
const newTrackElements: TimelineElement[] = []; const newTrackElements: TimelineElement[] = [];
for (const element of track.elements) { for (const element of track.elements) {
if (!elementIdsToDuplicate.has(element.id)) { if (!elementIdsToDuplicate.has(element.id)) {
continue; continue;
} }
const newId = generateUUID(); const newId = generateUUID();
newTrackElements.push( newTrackElements.push(
buildDuplicateElement({ buildDuplicateElement({
element, element,
id: newId, id: newId,
startTime: element.startTime, startTime: element.startTime,
}), }),
); );
} }
const placementResult = resolveTrackPlacement({ const placementResult = resolveTrackPlacement({
tracks: updatedTracks, tracks: updatedTracks,
trackType: track.type, trackType: track.type,
timeSpans: [], timeSpans: [],
strategy: { type: "alwaysNew", position: "highest" }, strategy: { type: "alwaysNew", position: "highest" },
}); });
if (!placementResult || placementResult.kind !== "newTrack") { if (!placementResult || placementResult.kind !== "newTrack") {
continue; continue;
} }
const applied = applyPlacement({ const applied = applyPlacement({
tracks: updatedTracks, tracks: updatedTracks,
placementResult, placementResult,
elements: newTrackElements, elements: newTrackElements,
}); });
if (!applied) { if (!applied) {
continue; continue;
} }
updatedTracks = applied.updatedTracks; updatedTracks = applied.updatedTracks;
for (const element of newTrackElements) { for (const element of newTrackElements) {
this.duplicatedElements.push({ this.duplicatedElements.push({
trackId: applied.targetTrackId, trackId: applied.targetTrackId,
elementId: element.id, elementId: element.id,
}); });
} }
} }
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
if (this.duplicatedElements.length > 0) { if (this.duplicatedElements.length > 0) {
return { return {
select: this.duplicatedElements, select: this.duplicatedElements,
}; };
} }
} }
undo(): void { undo(): void {
if (this.savedState) { if (this.savedState) {
const editor = EditorCore.getInstance(); const editor = EditorCore.getInstance();
editor.timeline.updateTracks(this.savedState); editor.timeline.updateTracks(this.savedState);
} }
} }
getDuplicatedElements(): { trackId: string; elementId: string }[] { getDuplicatedElements(): { trackId: string; elementId: string }[] {
return this.duplicatedElements; return this.duplicatedElements;
} }
} }
function buildDuplicateElement({ function buildDuplicateElement({
element, element,
id, id,
startTime, startTime,
}: { }: {
element: TimelineElement; element: TimelineElement;
id: string; id: string;
startTime: number; startTime: number;
}): TimelineElement { }): TimelineElement {
return { return {
...element, ...element,
id, id,
name: `${element.name} (copy)`, name: `${element.name} (copy)`,
startTime, startTime,
animations: cloneAnimations({ animations: cloneAnimations({
animations: element.animations, animations: element.animations,
shouldRegenerateKeyframeIds: true, shouldRegenerateKeyframeIds: true,
}), }),
}; };
} }

View File

@ -7,7 +7,6 @@ import type {
} from "@/lib/timeline"; } from "@/lib/timeline";
import { import {
buildEmptyTrack, buildEmptyTrack,
isMainTrack,
validateElementTrackCompatibility, validateElementTrackCompatibility,
enforceMainTrackStart, enforceMainTrackStart,
} from "@/lib/timeline/placement"; } from "@/lib/timeline/placement";
@ -100,7 +99,7 @@ export class MoveElementCommand extends Command {
const isSameTrack = this.sourceTrackId === this.targetTrackId; const isSameTrack = this.sourceTrackId === this.targetTrackId;
let updatedTracks = tracksToUpdate.map((track): TimelineTrack => { const updatedTracks = tracksToUpdate.map((track): TimelineTrack => {
if (isSameTrack && track.id === this.sourceTrackId) { if (isSameTrack && track.id === this.sourceTrackId) {
return { return {
...track, ...track,
@ -134,21 +133,6 @@ export class MoveElementCommand extends Command {
return track; return track;
}); });
if (!isSameTrack) {
const sourceTrackAfterMove = updatedTracks.find(
(track) => track.id === this.sourceTrackId,
);
if (
sourceTrackAfterMove &&
sourceTrackAfterMove.elements.length === 0 &&
!isMainTrack(sourceTrackAfterMove)
) {
updatedTracks = updatedTracks.filter(
(track) => track.id !== this.sourceTrackId,
);
}
}
editor.timeline.updateTracks(updatedTracks); editor.timeline.updateTracks(updatedTracks);
} }