mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: ripple support for element resize
This commit is contained in:
@@ -73,6 +73,7 @@ export class TimelineManager {
|
|||||||
startTime,
|
startTime,
|
||||||
duration,
|
duration,
|
||||||
pushHistory = true,
|
pushHistory = true,
|
||||||
|
rippleEnabled = false,
|
||||||
}: {
|
}: {
|
||||||
elementId: string;
|
elementId: string;
|
||||||
trimStart: number;
|
trimStart: number;
|
||||||
@@ -80,6 +81,7 @@ export class TimelineManager {
|
|||||||
startTime?: number;
|
startTime?: number;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
pushHistory?: boolean;
|
pushHistory?: boolean;
|
||||||
|
rippleEnabled?: boolean;
|
||||||
}): void {
|
}): void {
|
||||||
const command = new UpdateElementTrimCommand({
|
const command = new UpdateElementTrimCommand({
|
||||||
elementId,
|
elementId,
|
||||||
@@ -87,6 +89,7 @@ export class TimelineManager {
|
|||||||
trimEnd,
|
trimEnd,
|
||||||
startTime,
|
startTime,
|
||||||
duration,
|
duration,
|
||||||
|
rippleEnabled,
|
||||||
});
|
});
|
||||||
if (pushHistory) {
|
if (pushHistory) {
|
||||||
this.editor.command.execute({ command });
|
this.editor.command.execute({ command });
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ export function useTimelineElementResize({
|
|||||||
const activeProject = editor.project.getActive();
|
const activeProject = editor.project.getActive();
|
||||||
const isShiftHeldRef = useShiftKey();
|
const isShiftHeldRef = useShiftKey();
|
||||||
const snappingEnabled = useTimelineStore((state) => state.snappingEnabled);
|
const snappingEnabled = useTimelineStore((state) => state.snappingEnabled);
|
||||||
|
const rippleEditingEnabled = useTimelineStore(
|
||||||
|
(state) => state.rippleEditingEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
const [resizing, setResizing] = useState<ResizeState | null>(null);
|
const [resizing, setResizing] = useState<ResizeState | null>(null);
|
||||||
const [currentTrimStart, setCurrentTrimStart] = useState(element.trimStart);
|
const [currentTrimStart, setCurrentTrimStart] = useState(element.trimStart);
|
||||||
@@ -354,6 +356,7 @@ export function useTimelineElementResize({
|
|||||||
trimEnd: finalTrimEnd,
|
trimEnd: finalTrimEnd,
|
||||||
startTime: startTimeChanged ? finalStartTime : undefined,
|
startTime: startTimeChanged ? finalStartTime : undefined,
|
||||||
duration: durationChanged ? finalDuration : undefined,
|
duration: durationChanged ? finalDuration : undefined,
|
||||||
|
rippleEnabled: rippleEditingEnabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +369,7 @@ export function useTimelineElementResize({
|
|||||||
element.id,
|
element.id,
|
||||||
onResizeStateChange,
|
onResizeStateChange,
|
||||||
onSnapPointChange,
|
onSnapPointChange,
|
||||||
|
rippleEditingEnabled,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Command } from "@/lib/commands/base-command";
|
|||||||
import type { TimelineTrack } from "@/types/timeline";
|
import type { TimelineTrack } from "@/types/timeline";
|
||||||
import { EditorCore } from "@/core";
|
import { EditorCore } from "@/core";
|
||||||
import { clampAnimationsToDuration } from "@/lib/animation";
|
import { clampAnimationsToDuration } from "@/lib/animation";
|
||||||
|
import { rippleShiftElements } from "@/lib/timeline";
|
||||||
|
|
||||||
export class UpdateElementTrimCommand extends Command {
|
export class UpdateElementTrimCommand extends Command {
|
||||||
private savedState: TimelineTrack[] | null = null;
|
private savedState: TimelineTrack[] | null = null;
|
||||||
@@ -10,6 +11,7 @@ export class UpdateElementTrimCommand extends Command {
|
|||||||
private readonly trimEnd: number;
|
private readonly trimEnd: number;
|
||||||
private readonly startTime: number | undefined;
|
private readonly startTime: number | undefined;
|
||||||
private readonly duration: number | undefined;
|
private readonly duration: number | undefined;
|
||||||
|
private readonly rippleEnabled: boolean;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
elementId,
|
elementId,
|
||||||
@@ -17,12 +19,14 @@ export class UpdateElementTrimCommand extends Command {
|
|||||||
trimEnd,
|
trimEnd,
|
||||||
startTime,
|
startTime,
|
||||||
duration,
|
duration,
|
||||||
|
rippleEnabled = false,
|
||||||
}: {
|
}: {
|
||||||
elementId: string;
|
elementId: string;
|
||||||
trimStart: number;
|
trimStart: number;
|
||||||
trimEnd: number;
|
trimEnd: number;
|
||||||
startTime?: number;
|
startTime?: number;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
|
rippleEnabled?: boolean;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
this.elementId = elementId;
|
this.elementId = elementId;
|
||||||
@@ -30,6 +34,7 @@ export class UpdateElementTrimCommand extends Command {
|
|||||||
this.trimEnd = trimEnd;
|
this.trimEnd = trimEnd;
|
||||||
this.startTime = startTime;
|
this.startTime = startTime;
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
|
this.rippleEnabled = rippleEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): void {
|
||||||
@@ -37,25 +42,52 @@ export class UpdateElementTrimCommand extends Command {
|
|||||||
this.savedState = editor.timeline.getTracks();
|
this.savedState = editor.timeline.getTracks();
|
||||||
|
|
||||||
const updatedTracks = this.savedState.map((track) => {
|
const updatedTracks = this.savedState.map((track) => {
|
||||||
const newElements = track.elements.map((element) => {
|
const targetElement = track.elements.find(
|
||||||
if (element.id !== this.elementId) {
|
(element) => element.id === this.elementId,
|
||||||
return element;
|
);
|
||||||
}
|
if (!targetElement) return track;
|
||||||
|
|
||||||
const nextDuration = this.duration ?? element.duration;
|
const nextDuration = this.duration ?? targetElement.duration;
|
||||||
return {
|
const nextStartTime = this.startTime ?? targetElement.startTime;
|
||||||
...element,
|
|
||||||
|
const oldEndTime = targetElement.startTime + targetElement.duration;
|
||||||
|
const newEndTime = nextStartTime + nextDuration;
|
||||||
|
const shiftAmount = oldEndTime - newEndTime;
|
||||||
|
|
||||||
|
const updatedElement = {
|
||||||
|
...targetElement,
|
||||||
trimStart: this.trimStart,
|
trimStart: this.trimStart,
|
||||||
trimEnd: this.trimEnd,
|
trimEnd: this.trimEnd,
|
||||||
startTime: this.startTime ?? element.startTime,
|
startTime: nextStartTime,
|
||||||
duration: nextDuration,
|
duration: nextDuration,
|
||||||
animations: clampAnimationsToDuration({
|
animations: clampAnimationsToDuration({
|
||||||
animations: element.animations,
|
animations: targetElement.animations,
|
||||||
duration: nextDuration,
|
duration: nextDuration,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.rippleEnabled && Math.abs(shiftAmount) > 0) {
|
||||||
|
const shiftedOthers = rippleShiftElements({
|
||||||
|
elements: track.elements.filter((element) => element.id !== this.elementId),
|
||||||
|
afterTime: oldEndTime,
|
||||||
|
shiftAmount,
|
||||||
});
|
});
|
||||||
return { ...track, elements: newElements } as typeof track;
|
return {
|
||||||
|
...track,
|
||||||
|
elements: track.elements.map((element) =>
|
||||||
|
element.id === this.elementId
|
||||||
|
? updatedElement
|
||||||
|
: (shiftedOthers.find((shifted) => shifted.id === element.id) ?? element)
|
||||||
|
),
|
||||||
|
} as typeof track;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
elements: track.elements.map((element) =>
|
||||||
|
element.id === this.elementId ? updatedElement : element
|
||||||
|
),
|
||||||
|
} as typeof track;
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.timeline.updateTracks(updatedTracks);
|
editor.timeline.updateTracks(updatedTracks);
|
||||||
|
|||||||
Reference in New Issue
Block a user