import type { SceneTracks, TimelineElement, TimelineTrack } from "@/model"; 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; }): 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 { 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; afterElementsById: Map; allAfterElementIds: Set; }): { 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 }); }