mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
190 lines
4.8 KiB
TypeScript
190 lines
4.8 KiB
TypeScript
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<string, ClipboardItem[]> {
|
|
const groupedItems = new Map<string, ClipboardItem[]>();
|
|
|
|
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;
|
|
}
|
|
|