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