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