mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
183 lines
5.1 KiB
TypeScript
183 lines
5.1 KiB
TypeScript
import {
|
|
Command,
|
|
createElementSelectionResult,
|
|
type CommandResult,
|
|
} from "@/commands/base-command";
|
|
import type { SceneTracks, TimelineElement } from "@/timeline";
|
|
import { generateUUID } from "@/utils/id";
|
|
import { EditorCore } from "@/core";
|
|
import { isRetimableElement } from "@/timeline";
|
|
import { splitAnimationsAtTime } from "@/animation";
|
|
import { getSourceSpanAtClipTime } from "@/retime";
|
|
|
|
export class SplitElementsCommand extends Command {
|
|
private savedState: SceneTracks | null = null;
|
|
private rightSideElements: { trackId: string; elementId: string }[] = [];
|
|
private readonly elements: { trackId: string; elementId: string }[];
|
|
private readonly splitTime: number;
|
|
private readonly retainSide: "both" | "left" | "right";
|
|
|
|
constructor({
|
|
elements,
|
|
splitTime,
|
|
retainSide = "both",
|
|
}: {
|
|
elements: { trackId: string; elementId: string }[];
|
|
splitTime: number;
|
|
retainSide?: "both" | "left" | "right";
|
|
}) {
|
|
super();
|
|
this.elements = elements;
|
|
this.splitTime = splitTime;
|
|
this.retainSide = retainSide;
|
|
}
|
|
|
|
getRightSideElements(): { trackId: string; elementId: string }[] {
|
|
return this.rightSideElements;
|
|
}
|
|
|
|
execute(): CommandResult | undefined {
|
|
const editor = EditorCore.getInstance();
|
|
this.savedState = editor.scenes.getActiveScene().tracks;
|
|
this.rightSideElements = [];
|
|
|
|
const splitTrack = <
|
|
TTrack extends { id: string; elements: TimelineElement[] },
|
|
>(
|
|
track: TTrack,
|
|
): TTrack => {
|
|
const elementsToSplit = this.elements.filter(
|
|
(target) => target.trackId === track.id,
|
|
);
|
|
|
|
if (elementsToSplit.length === 0) {
|
|
return track;
|
|
}
|
|
|
|
const elements = track.elements.flatMap((element) => {
|
|
const shouldSplit = elementsToSplit.some(
|
|
(target) => target.elementId === element.id,
|
|
);
|
|
|
|
if (!shouldSplit) {
|
|
return [element];
|
|
}
|
|
|
|
const effectiveStart = element.startTime;
|
|
const effectiveEnd = element.startTime + element.duration;
|
|
|
|
if (
|
|
this.splitTime <= effectiveStart ||
|
|
this.splitTime >= effectiveEnd
|
|
) {
|
|
return [element];
|
|
}
|
|
|
|
const relativeTime = this.splitTime - element.startTime;
|
|
const leftVisibleDuration = relativeTime;
|
|
const rightVisibleDuration = element.duration - relativeTime;
|
|
const retimeRef = isRetimableElement(element)
|
|
? element.retime
|
|
: undefined;
|
|
const leftSourceSpan = getSourceSpanAtClipTime({
|
|
clipTime: leftVisibleDuration,
|
|
retime: retimeRef,
|
|
});
|
|
const totalSourceSpan = getSourceSpanAtClipTime({
|
|
clipTime: element.duration,
|
|
retime: retimeRef,
|
|
});
|
|
const rightSourceSpan = totalSourceSpan - leftSourceSpan;
|
|
const { leftAnimations, rightAnimations } = splitAnimationsAtTime({
|
|
animations: element.animations,
|
|
splitTime: relativeTime,
|
|
shouldIncludeSplitBoundary: true,
|
|
});
|
|
let splitResult: TimelineElement[];
|
|
|
|
if (this.retainSide === "left") {
|
|
splitResult = [
|
|
{
|
|
...element,
|
|
duration: leftVisibleDuration,
|
|
trimEnd: element.trimEnd + rightSourceSpan,
|
|
name: `${element.name} (left)`,
|
|
animations: leftAnimations,
|
|
...(retimeRef !== undefined ? { retime: retimeRef } : {}),
|
|
},
|
|
];
|
|
} else if (this.retainSide === "right") {
|
|
const newId = generateUUID();
|
|
this.rightSideElements.push({
|
|
trackId: track.id,
|
|
elementId: newId,
|
|
});
|
|
splitResult = [
|
|
{
|
|
...element,
|
|
id: newId,
|
|
startTime: this.splitTime,
|
|
duration: rightVisibleDuration,
|
|
trimStart: element.trimStart + leftSourceSpan,
|
|
name: `${element.name} (right)`,
|
|
animations: rightAnimations,
|
|
...(retimeRef !== undefined ? { retime: retimeRef } : {}),
|
|
},
|
|
];
|
|
} else {
|
|
// "both" - split into two pieces
|
|
const secondElementId = generateUUID();
|
|
this.rightSideElements.push({
|
|
trackId: track.id,
|
|
elementId: secondElementId,
|
|
});
|
|
splitResult = [
|
|
{
|
|
...element,
|
|
duration: leftVisibleDuration,
|
|
trimEnd: element.trimEnd + rightSourceSpan,
|
|
name: `${element.name} (left)`,
|
|
animations: leftAnimations,
|
|
...(retimeRef !== undefined ? { retime: retimeRef } : {}),
|
|
},
|
|
{
|
|
...element,
|
|
id: secondElementId,
|
|
startTime: this.splitTime,
|
|
duration: rightVisibleDuration,
|
|
trimStart: element.trimStart + leftSourceSpan,
|
|
name: `${element.name} (right)`,
|
|
animations: rightAnimations,
|
|
...(retimeRef !== undefined ? { retime: retimeRef } : {}),
|
|
},
|
|
];
|
|
}
|
|
|
|
return splitResult;
|
|
});
|
|
|
|
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);
|
|
|
|
if (this.rightSideElements.length > 0) {
|
|
return createElementSelectionResult(this.rightSideElements);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
undo(): void {
|
|
if (this.savedState) {
|
|
const editor = EditorCore.getInstance();
|
|
editor.timeline.updateTracks(this.savedState);
|
|
}
|
|
}
|
|
}
|