mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
refactor: unify element updates into pipeline and centralize ripple
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
import { TIME_EPSILON_SECONDS } from "@/constants/animation-constants";
|
||||
import type { TimelineElement, TimelineTrack } from "@/lib/timeline/types";
|
||||
import type { RippleAdjustment } from "./apply";
|
||||
|
||||
interface Interval {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
interface ElementSpan extends Interval {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function computeRippleAdjustments({
|
||||
beforeTracks,
|
||||
afterTracks,
|
||||
}: {
|
||||
beforeTracks: TimelineTrack[];
|
||||
afterTracks: TimelineTrack[];
|
||||
}): RippleAdjustment[] {
|
||||
const afterTracksById = new Map(afterTracks.map((track) => [track.id, track]));
|
||||
const allAfterElementIds = new Set(
|
||||
afterTracks.flatMap((track) => track.elements.map((element) => element.id)),
|
||||
);
|
||||
|
||||
return beforeTracks.flatMap((beforeTrack): RippleAdjustment[] =>
|
||||
computeTrackRippleAdjustments({
|
||||
trackId: beforeTrack.id,
|
||||
beforeElements: beforeTrack.elements,
|
||||
afterElements: afterTracksById.get(beforeTrack.id)?.elements ?? [],
|
||||
allAfterElementIds,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function computeTrackRippleAdjustments({
|
||||
trackId,
|
||||
beforeElements,
|
||||
afterElements,
|
||||
allAfterElementIds,
|
||||
}: {
|
||||
trackId: string;
|
||||
beforeElements: TimelineElement[];
|
||||
afterElements: TimelineElement[];
|
||||
allAfterElementIds: Set<string>;
|
||||
}): RippleAdjustment[] {
|
||||
const beforeElementsById = buildElementSpanMap({ elements: beforeElements });
|
||||
const afterElementsById = buildElementSpanMap({ elements: afterElements });
|
||||
const { vacatedIntervals, joinedIntervals } = collectTrackIntervals({
|
||||
beforeElementsById,
|
||||
afterElementsById,
|
||||
allAfterElementIds,
|
||||
});
|
||||
const freedIntervals = subtractIntervalSets({
|
||||
sourceIntervals: vacatedIntervals,
|
||||
overlappingIntervals: joinedIntervals,
|
||||
});
|
||||
|
||||
return buildAdjustments({ trackId, intervals: freedIntervals });
|
||||
}
|
||||
|
||||
function buildElementSpanMap({
|
||||
elements,
|
||||
}: {
|
||||
elements: TimelineElement[];
|
||||
}): Map<string, ElementSpan> {
|
||||
return new Map(
|
||||
elements.map((element) => [
|
||||
element.id,
|
||||
{
|
||||
id: element.id,
|
||||
startTime: element.startTime,
|
||||
endTime: element.startTime + element.duration,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function collectTrackIntervals({
|
||||
beforeElementsById,
|
||||
afterElementsById,
|
||||
allAfterElementIds,
|
||||
}: {
|
||||
beforeElementsById: Map<string, ElementSpan>;
|
||||
afterElementsById: Map<string, ElementSpan>;
|
||||
allAfterElementIds: Set<string>;
|
||||
}): {
|
||||
vacatedIntervals: Interval[];
|
||||
joinedIntervals: Interval[];
|
||||
} {
|
||||
const vacatedIntervals: Interval[] = [];
|
||||
const joinedIntervals: Interval[] = [];
|
||||
|
||||
for (const beforeElement of beforeElementsById.values()) {
|
||||
const afterElement = afterElementsById.get(beforeElement.id);
|
||||
if (!afterElement) {
|
||||
const wasMovedToAnotherTrack = allAfterElementIds.has(beforeElement.id);
|
||||
if (!wasMovedToAnotherTrack) {
|
||||
pushInterval({
|
||||
intervals: vacatedIntervals,
|
||||
startTime: beforeElement.startTime,
|
||||
endTime: beforeElement.endTime,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (beforeElement.endTime > afterElement.endTime + TIME_EPSILON_SECONDS) {
|
||||
pushInterval({
|
||||
intervals: vacatedIntervals,
|
||||
startTime: afterElement.endTime,
|
||||
endTime: beforeElement.endTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const afterElement of afterElementsById.values()) {
|
||||
if (beforeElementsById.has(afterElement.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pushInterval({
|
||||
intervals: joinedIntervals,
|
||||
startTime: afterElement.startTime,
|
||||
endTime: afterElement.endTime,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
vacatedIntervals: normalizeIntervals({ intervals: vacatedIntervals }),
|
||||
joinedIntervals: normalizeIntervals({ intervals: joinedIntervals }),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAdjustments({
|
||||
trackId,
|
||||
intervals,
|
||||
}: {
|
||||
trackId: string;
|
||||
intervals: Interval[];
|
||||
}): RippleAdjustment[] {
|
||||
return intervals.flatMap((interval): RippleAdjustment[] => {
|
||||
const shiftAmount = interval.endTime - interval.startTime;
|
||||
if (shiftAmount <= TIME_EPSILON_SECONDS) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
trackId,
|
||||
afterTime: interval.endTime,
|
||||
shiftAmount,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function subtractIntervalSets({
|
||||
sourceIntervals,
|
||||
overlappingIntervals,
|
||||
}: {
|
||||
sourceIntervals: Interval[];
|
||||
overlappingIntervals: Interval[];
|
||||
}): Interval[] {
|
||||
const normalizedSourceIntervals = normalizeIntervals({
|
||||
intervals: sourceIntervals,
|
||||
});
|
||||
const normalizedOverlappingIntervals = normalizeIntervals({
|
||||
intervals: overlappingIntervals,
|
||||
});
|
||||
|
||||
return normalizedSourceIntervals.flatMap((sourceInterval) =>
|
||||
subtractSingleInterval({
|
||||
sourceInterval,
|
||||
overlappingIntervals: normalizedOverlappingIntervals,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeIntervals({
|
||||
intervals,
|
||||
}: {
|
||||
intervals: Interval[];
|
||||
}): Interval[] {
|
||||
const validIntervals: Interval[] = [];
|
||||
for (const interval of intervals) {
|
||||
pushInterval({
|
||||
intervals: validIntervals,
|
||||
startTime: interval.startTime,
|
||||
endTime: interval.endTime,
|
||||
});
|
||||
}
|
||||
|
||||
const sortedIntervals = validIntervals.sort(
|
||||
(leftInterval, rightInterval) =>
|
||||
leftInterval.startTime - rightInterval.startTime,
|
||||
);
|
||||
|
||||
if (sortedIntervals.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const mergedIntervals: Interval[] = [{ ...sortedIntervals[0] }];
|
||||
for (const interval of sortedIntervals.slice(1)) {
|
||||
const previousInterval = mergedIntervals[mergedIntervals.length - 1];
|
||||
if (interval.startTime <= previousInterval.endTime + TIME_EPSILON_SECONDS) {
|
||||
previousInterval.endTime = Math.max(
|
||||
previousInterval.endTime,
|
||||
interval.endTime,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
mergedIntervals.push({ ...interval });
|
||||
}
|
||||
|
||||
return mergedIntervals;
|
||||
}
|
||||
|
||||
function subtractSingleInterval({
|
||||
sourceInterval,
|
||||
overlappingIntervals,
|
||||
}: {
|
||||
sourceInterval: Interval;
|
||||
overlappingIntervals: Interval[];
|
||||
}): Interval[] {
|
||||
let remainingIntervals: Interval[] = [{ ...sourceInterval }];
|
||||
|
||||
for (const overlappingInterval of overlappingIntervals) {
|
||||
remainingIntervals = remainingIntervals.flatMap((remainingInterval) => {
|
||||
if (
|
||||
overlappingInterval.endTime <=
|
||||
remainingInterval.startTime + TIME_EPSILON_SECONDS ||
|
||||
overlappingInterval.startTime >=
|
||||
remainingInterval.endTime - TIME_EPSILON_SECONDS
|
||||
) {
|
||||
return [remainingInterval];
|
||||
}
|
||||
|
||||
const nextIntervals: Interval[] = [];
|
||||
pushInterval({
|
||||
intervals: nextIntervals,
|
||||
startTime: remainingInterval.startTime,
|
||||
endTime: overlappingInterval.startTime,
|
||||
});
|
||||
pushInterval({
|
||||
intervals: nextIntervals,
|
||||
startTime: overlappingInterval.endTime,
|
||||
endTime: remainingInterval.endTime,
|
||||
});
|
||||
return nextIntervals;
|
||||
});
|
||||
|
||||
if (remainingIntervals.length === 0) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return remainingIntervals;
|
||||
}
|
||||
|
||||
function pushInterval({
|
||||
intervals,
|
||||
startTime,
|
||||
endTime,
|
||||
}: { intervals: Interval[]; startTime: number; endTime: number }): void {
|
||||
if (endTime - startTime <= TIME_EPSILON_SECONDS) {
|
||||
return;
|
||||
}
|
||||
|
||||
intervals.push({ startTime, endTime });
|
||||
}
|
||||
Reference in New Issue
Block a user