280 lines
6.4 KiB
TypeScript
280 lines
6.4 KiB
TypeScript
import type { SceneTracks, TimelineElement, TimelineTrack } from "@/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: SceneTracks;
|
|
afterTracks: SceneTracks;
|
|
}): RippleAdjustment[] {
|
|
const beforeTrackList = [
|
|
...beforeTracks.overlay,
|
|
beforeTracks.main,
|
|
...beforeTracks.audio,
|
|
];
|
|
const afterTrackList = [
|
|
...afterTracks.overlay,
|
|
afterTracks.main,
|
|
...afterTracks.audio,
|
|
];
|
|
const afterTracksById = new Map(afterTrackList.map((track) => [track.id, track]));
|
|
const allAfterElementIds = new Set(
|
|
afterTrackList.flatMap((track) => track.elements.map((element) => element.id)),
|
|
);
|
|
|
|
return beforeTrackList.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) {
|
|
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 <= 0) {
|
|
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) {
|
|
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 ||
|
|
overlappingInterval.startTime >= remainingInterval.endTime
|
|
) {
|
|
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) {
|
|
return;
|
|
}
|
|
|
|
intervals.push({ startTime, endTime });
|
|
}
|