summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/group-move/resolve-move.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/timeline/group-move/resolve-move.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/timeline/group-move/resolve-move.ts')
-rw-r--r--apps/web/src/lib/timeline/group-move/resolve-move.ts470
1 files changed, 0 insertions, 470 deletions
diff --git a/apps/web/src/lib/timeline/group-move/resolve-move.ts b/apps/web/src/lib/timeline/group-move/resolve-move.ts
deleted file mode 100644
index fbe4cbda..00000000
--- a/apps/web/src/lib/timeline/group-move/resolve-move.ts
+++ /dev/null
@@ -1,470 +0,0 @@
-import type { SceneTracks } from "@/lib/timeline";
-import { getTrackTypeForElementType } from "@/lib/timeline/placement/compatibility";
-import { canPlaceTimeSpansOnTrack } from "@/lib/timeline/placement/overlap";
-import type {
- GroupMoveResult,
- MoveGroup,
- PlannedElementMove,
- PlannedTrackCreation,
-} from "./types";
-import {
- getDisplayTracks,
- getTrackPlacementByDisplayIndex,
- getTrackPlacementById,
-} from "./track-placement";
-
-type GroupMoveTarget =
- | {
- kind: "existingTrack";
- anchorTargetTrackId: string;
- }
- | {
- kind: "newTracks";
- anchorInsertIndex: number;
- newTrackIds: string[];
- };
-
-export function resolveGroupMove({
- group,
- tracks,
- anchorStartTime,
- target,
-}: {
- group: MoveGroup;
- tracks: SceneTracks;
- anchorStartTime: number;
- target: GroupMoveTarget;
-}): GroupMoveResult | null {
- if (target.kind === "newTracks") {
- return resolveNewTrackMove({
- group,
- tracks,
- anchorStartTime,
- anchorInsertIndex: target.anchorInsertIndex,
- newTrackIds: target.newTrackIds,
- });
- }
-
- return resolveExistingTrackMove({
- group,
- tracks,
- anchorStartTime,
- anchorTargetTrackId: target.anchorTargetTrackId,
- });
-}
-
-function resolveExistingTrackMove({
- group,
- tracks,
- anchorStartTime,
- anchorTargetTrackId,
-}: {
- group: MoveGroup;
- tracks: SceneTracks;
- anchorStartTime: number;
- anchorTargetTrackId: string;
-}): GroupMoveResult | null {
- const anchorTargetPlacement = getTrackPlacementById({
- tracks,
- trackId: anchorTargetTrackId,
- });
- if (!anchorTargetPlacement) {
- return null;
- }
-
- const targetTrackIdsByElementId = resolveExistingTrackIdsByElementId({
- group,
- tracks,
- anchorTargetDisplayIndex: anchorTargetPlacement.displayIndex,
- });
- if (!targetTrackIdsByElementId) {
- return null;
- }
-
- const clampedAnchorStartTime = clampAnchorStartTime({
- group,
- tracks,
- anchorStartTime,
- targetTrackIdsByElementId,
- });
-
- const moves = group.members.map((member) => ({
- sourceTrackId: member.trackId,
- targetTrackId:
- targetTrackIdsByElementId.get(member.elementId) ?? member.trackId,
- elementId: member.elementId,
- newStartTime: clampedAnchorStartTime + member.timeOffset,
- }));
-
- if (!canApplyMovesToExistingTracks({ tracks, moves })) {
- return null;
- }
-
- return {
- moves,
- createTracks: [],
- targetSelection: moves.map(({ elementId, targetTrackId }) => ({
- trackId: targetTrackId,
- elementId,
- })),
- };
-}
-
-function resolveNewTrackMove({
- group,
- tracks,
- anchorStartTime,
- anchorInsertIndex,
- newTrackIds,
-}: {
- group: MoveGroup;
- tracks: SceneTracks;
- anchorStartTime: number;
- anchorInsertIndex: number;
- newTrackIds: string[];
-}): GroupMoveResult | null {
- const sortedMembers = [...group.members].sort(
- (leftMember, rightMember) =>
- leftMember.displayIndex - rightMember.displayIndex,
- );
- const anchorMemberIndex = sortedMembers.findIndex(
- (member) => member.elementId === group.anchor.elementId,
- );
- if (anchorMemberIndex < 0 || newTrackIds.length < sortedMembers.length) {
- return null;
- }
-
- const hasAudioMember = sortedMembers.some(
- (member) => member.trackSection === "audio",
- );
- const hasNonAudioMember = sortedMembers.some(
- (member) => member.trackSection !== "audio",
- );
- if (hasAudioMember && hasNonAudioMember) {
- return null;
- }
-
- const clampedAnchorStartTime = clampAnchorStartTime({
- group,
- tracks,
- anchorStartTime,
- targetTrackIdsByElementId: new Map(),
- });
- const blockStartIndex = hasAudioMember
- ? clampAudioInsertIndex({
- tracks,
- insertIndex: anchorInsertIndex - anchorMemberIndex,
- })
- : Math.max(
- 0,
- Math.min(anchorInsertIndex - anchorMemberIndex, tracks.overlay.length),
- );
-
- const createTracks: PlannedTrackCreation[] = sortedMembers.map(
- (member, memberIndex) => ({
- id: newTrackIds[memberIndex],
- type: getTrackTypeForElementType({
- elementType: member.elementType,
- }),
- index: blockStartIndex + memberIndex,
- }),
- );
- const moves = sortedMembers.map((member, memberIndex) => ({
- sourceTrackId: member.trackId,
- targetTrackId: newTrackIds[memberIndex],
- elementId: member.elementId,
- newStartTime: clampedAnchorStartTime + member.timeOffset,
- }));
-
- return {
- moves,
- createTracks,
- targetSelection: moves.map(({ elementId, targetTrackId }) => ({
- trackId: targetTrackId,
- elementId,
- })),
- };
-}
-
-function clampAudioInsertIndex({
- tracks,
- insertIndex,
-}: {
- tracks: SceneTracks;
- insertIndex: number;
-}): number {
- const minimumAudioInsertIndex = tracks.overlay.length + 1;
- return Math.max(
- minimumAudioInsertIndex,
- Math.min(insertIndex, minimumAudioInsertIndex + tracks.audio.length),
- );
-}
-
-function resolveExistingTrackIdsByElementId({
- group,
- tracks,
- anchorTargetDisplayIndex,
-}: {
- group: MoveGroup;
- tracks: SceneTracks;
- anchorTargetDisplayIndex: number;
-}): Map<string, string> | null {
- const sortedMembers = [...group.members].sort(
- (leftMember, rightMember) =>
- leftMember.displayIndex - rightMember.displayIndex,
- );
- const anchorMemberIndex = sortedMembers.findIndex(
- (member) => member.elementId === group.anchor.elementId,
- );
- if (anchorMemberIndex < 0) {
- return null;
- }
-
- const targetTrackIdsByElementId = new Map<string, string>();
- const usedTrackIds = new Set<string>();
- const anchorPlacement = getTrackPlacementByDisplayIndex({
- tracks,
- displayIndex: anchorTargetDisplayIndex,
- });
- if (!anchorPlacement) {
- return null;
- }
-
- targetTrackIdsByElementId.set(
- group.anchor.elementId,
- anchorPlacement.trackId,
- );
- usedTrackIds.add(anchorPlacement.trackId);
-
- let upperBoundaryIndex = anchorTargetDisplayIndex;
- for (
- let memberIndex = anchorMemberIndex - 1;
- memberIndex >= 0;
- memberIndex -= 1
- ) {
- const member = sortedMembers[memberIndex];
- const targetPlacement = findCompatibleTrackPlacement({
- tracks,
- requiredTrackType: getTrackTypeForElementType({
- elementType: member.elementType,
- }),
- startDisplayIndex: upperBoundaryIndex - 1,
- step: -1,
- usedTrackIds,
- });
- if (!targetPlacement) {
- return null;
- }
-
- targetTrackIdsByElementId.set(member.elementId, targetPlacement.trackId);
- usedTrackIds.add(targetPlacement.trackId);
- upperBoundaryIndex = targetPlacement.displayIndex;
- }
-
- let lowerBoundaryIndex = anchorTargetDisplayIndex;
- for (
- let memberIndex = anchorMemberIndex + 1;
- memberIndex < sortedMembers.length;
- memberIndex += 1
- ) {
- const member = sortedMembers[memberIndex];
- const targetPlacement = findCompatibleTrackPlacement({
- tracks,
- requiredTrackType: getTrackTypeForElementType({
- elementType: member.elementType,
- }),
- startDisplayIndex: lowerBoundaryIndex + 1,
- step: 1,
- usedTrackIds,
- });
- if (!targetPlacement) {
- return null;
- }
-
- targetTrackIdsByElementId.set(member.elementId, targetPlacement.trackId);
- usedTrackIds.add(targetPlacement.trackId);
- lowerBoundaryIndex = targetPlacement.displayIndex;
- }
-
- return targetTrackIdsByElementId;
-}
-
-function findCompatibleTrackPlacement({
- tracks,
- requiredTrackType,
- startDisplayIndex,
- step,
- usedTrackIds,
-}: {
- tracks: SceneTracks;
- requiredTrackType: ReturnType<typeof getTrackTypeForElementType>;
- startDisplayIndex: number;
- step: -1 | 1;
- usedTrackIds: Set<string>;
-}) {
- for (
- let displayIndex = startDisplayIndex;
- displayIndex >= 0 &&
- displayIndex < tracks.overlay.length + 1 + tracks.audio.length;
- displayIndex += step
- ) {
- const placement = getTrackPlacementByDisplayIndex({
- tracks,
- displayIndex,
- });
- if (!placement) {
- continue;
- }
-
- if (
- placement.trackType === requiredTrackType &&
- !usedTrackIds.has(placement.trackId)
- ) {
- return placement;
- }
- }
-
- return null;
-}
-
-function clampAnchorStartTime({
- group,
- tracks,
- anchorStartTime,
- targetTrackIdsByElementId,
-}: {
- group: MoveGroup;
- tracks: SceneTracks;
- anchorStartTime: number;
- targetTrackIdsByElementId: Map<string, string>;
-}): number {
- const minimumAnchorStartTime = Math.max(
- 0,
- ...group.members.map((member) => -member.timeOffset),
- );
- let clampedAnchorStartTime = Math.max(
- minimumAnchorStartTime,
- anchorStartTime,
- );
-
- const memberOnMainTrack = group.members.find(
- (member) =>
- targetTrackIdsByElementId.get(member.elementId) === tracks.main.id,
- );
- if (!memberOnMainTrack) {
- return clampedAnchorStartTime;
- }
-
- const movingElementIds = new Set(
- group.members.map((member) => member.elementId),
- );
- const requestedMainStartTime =
- clampedAnchorStartTime + memberOnMainTrack.timeOffset;
- const earliestStationaryMainStartTime = tracks.main.elements
- .filter((element) => !movingElementIds.has(element.id))
- .reduce<number | null>((earliestStartTime, element) => {
- if (earliestStartTime == null || element.startTime < earliestStartTime) {
- return element.startTime;
- }
-
- return earliestStartTime;
- }, null);
- if (
- earliestStationaryMainStartTime == null ||
- requestedMainStartTime <= earliestStationaryMainStartTime
- ) {
- clampedAnchorStartTime = Math.max(
- minimumAnchorStartTime,
- -memberOnMainTrack.timeOffset,
- );
- }
-
- return clampedAnchorStartTime;
-}
-
-function canApplyMovesToExistingTracks({
- tracks,
- moves,
-}: {
- tracks: SceneTracks;
- moves: PlannedElementMove[];
-}): boolean {
- const movingElementIds = new Set(moves.map((move) => move.elementId));
- const sourceElements = new Map(
- getDisplayTracks({ tracks }).flatMap((track) =>
- track.elements.map((element) => [element.id, element] as const),
- ),
- );
- const movesByTargetTrackId = new Map<string, PlannedElementMove[]>();
- for (const move of moves) {
- const targetMoves = movesByTargetTrackId.get(move.targetTrackId) ?? [];
- targetMoves.push(move);
- movesByTargetTrackId.set(move.targetTrackId, targetMoves);
- }
-
- for (const [targetTrackId, targetMoves] of movesByTargetTrackId) {
- const targetPlacement = getTrackPlacementById({
- tracks,
- trackId: targetTrackId,
- });
- if (!targetPlacement) {
- return false;
- }
-
- const targetTrack = getDisplayTracks({ tracks })[
- targetPlacement.displayIndex
- ];
- if (!targetTrack) {
- return false;
- }
-
- const timeSpans = targetMoves.map((move) => {
- const sourceElement = sourceElements.get(move.elementId);
- return {
- startTime: move.newStartTime,
- duration: sourceElement?.duration ?? 0,
- };
- });
- if (hasOverlappingTimeSpans({ timeSpans })) {
- return false;
- }
-
- if (
- !canPlaceTimeSpansOnTrack({
- track: {
- elements: targetTrack.elements.filter(
- (element) => !movingElementIds.has(element.id),
- ),
- },
- timeSpans,
- })
- ) {
- return false;
- }
- }
-
- return true;
-}
-
-function hasOverlappingTimeSpans({
- timeSpans,
-}: {
- timeSpans: Array<{ startTime: number; duration: number }>;
-}): boolean {
- const sortedSpans = [...timeSpans].sort(
- (leftSpan, rightSpan) => leftSpan.startTime - rightSpan.startTime,
- );
-
- for (let spanIndex = 1; spanIndex < sortedSpans.length; spanIndex += 1) {
- const previousSpan = sortedSpans[spanIndex - 1];
- const currentSpan = sortedSpans[spanIndex];
- if (
- previousSpan.startTime + previousSpan.duration >
- currentSpan.startTime
- ) {
- return true;
- }
- }
-
- return false;
-}