feat: ensure elements on main track starts at 0
This commit is contained in:
parent
00cf7f6ab7
commit
9b5fd6eb07
|
|
@ -10,6 +10,8 @@ import { wouldElementOverlap } from "@/lib/timeline/element-utils";
|
|||
import {
|
||||
buildEmptyTrack,
|
||||
getHighestInsertIndexForTrack,
|
||||
isMainTrack,
|
||||
enforceMainTrackStart,
|
||||
} from "@/lib/timeline/track-utils";
|
||||
|
||||
export class PasteCommand extends Command {
|
||||
|
|
@ -65,11 +67,32 @@ export class PasteCommand extends Command {
|
|||
|
||||
if (resolvedTargetIndex >= 0) {
|
||||
const targetTrack = updatedTracks[resolvedTargetIndex];
|
||||
let adjustedElements = elementsToAdd;
|
||||
|
||||
if (isMainTrack(targetTrack)) {
|
||||
const earliestElement = elementsToAdd.reduce((earliest, element) =>
|
||||
element.startTime < earliest.startTime ? element : earliest,
|
||||
);
|
||||
const adjustedEarliestStartTime = enforceMainTrackStart({
|
||||
tracks: updatedTracks,
|
||||
targetTrackId: targetTrack.id,
|
||||
requestedStartTime: earliestElement.startTime,
|
||||
});
|
||||
const delta = adjustedEarliestStartTime - earliestElement.startTime;
|
||||
|
||||
if (delta !== 0) {
|
||||
adjustedElements = elementsToAdd.map((element) => ({
|
||||
...element,
|
||||
startTime: Math.max(0, element.startTime + delta),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
updatedTracks[resolvedTargetIndex] = {
|
||||
...targetTrack,
|
||||
elements: [...targetTrack.elements, ...elementsToAdd],
|
||||
elements: [...targetTrack.elements, ...adjustedElements],
|
||||
} as TimelineTrack;
|
||||
for (const element of elementsToAdd) {
|
||||
for (const element of adjustedElements) {
|
||||
this.pastedElements.push({
|
||||
trackId: targetTrack.id,
|
||||
elementId: element.id,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
canElementGoOnTrack,
|
||||
getDefaultInsertIndexForTrack,
|
||||
validateElementTrackCompatibility,
|
||||
enforceMainTrackStart,
|
||||
} from "@/lib/timeline/track-utils";
|
||||
import type { MediaAsset } from "@/types/assets";
|
||||
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
|
||||
|
|
@ -204,9 +205,18 @@ export class InsertElementCommand extends Command {
|
|||
return null;
|
||||
}
|
||||
|
||||
const adjustedElement = this.adjustElementForMainTrack({
|
||||
tracks,
|
||||
targetTrackId: targetTrack.id,
|
||||
element,
|
||||
});
|
||||
|
||||
const updatedTracks = tracks.map((track) =>
|
||||
track.id === targetTrack.id
|
||||
? { ...track, elements: [...track.elements, element] }
|
||||
? {
|
||||
...track,
|
||||
elements: [...track.elements, adjustedElement],
|
||||
}
|
||||
: track,
|
||||
) as TimelineTrack[];
|
||||
|
||||
|
|
@ -248,9 +258,18 @@ export class InsertElementCommand extends Command {
|
|||
});
|
||||
|
||||
if (existingTrack) {
|
||||
const adjustedElement = this.adjustElementForMainTrack({
|
||||
tracks,
|
||||
targetTrackId: existingTrack.id,
|
||||
element,
|
||||
});
|
||||
|
||||
const updatedTracks = tracks.map((track) =>
|
||||
track.id === existingTrack.id
|
||||
? { ...track, elements: [...track.elements, element] }
|
||||
? {
|
||||
...track,
|
||||
elements: [...track.elements, adjustedElement],
|
||||
}
|
||||
: track,
|
||||
) as TimelineTrack[];
|
||||
|
||||
|
|
@ -298,6 +317,23 @@ export class InsertElementCommand extends Command {
|
|||
});
|
||||
}
|
||||
|
||||
private adjustElementForMainTrack({
|
||||
tracks,
|
||||
targetTrackId,
|
||||
element,
|
||||
}: {
|
||||
tracks: TimelineTrack[];
|
||||
targetTrackId: string;
|
||||
element: TimelineElement;
|
||||
}): TimelineElement {
|
||||
const adjustedStartTime = enforceMainTrackStart({
|
||||
tracks,
|
||||
targetTrackId,
|
||||
requestedStartTime: element.startTime,
|
||||
});
|
||||
return { ...element, startTime: adjustedStartTime };
|
||||
}
|
||||
|
||||
private getTrackTypeForElement({
|
||||
element,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
buildEmptyTrack,
|
||||
isMainTrack,
|
||||
validateElementTrackCompatibility,
|
||||
enforceMainTrackStart,
|
||||
} from "@/lib/timeline/track-utils";
|
||||
|
||||
export class MoveElementCommand extends Command {
|
||||
|
|
@ -66,9 +67,16 @@ export class MoveElementCommand extends Command {
|
|||
return;
|
||||
}
|
||||
|
||||
const adjustedStartTime = enforceMainTrackStart({
|
||||
tracks: tracksToUpdate,
|
||||
targetTrackId: this.targetTrackId,
|
||||
requestedStartTime: this.newStartTime,
|
||||
excludeElementId: this.elementId,
|
||||
});
|
||||
|
||||
const movedElement: TimelineElement = {
|
||||
...element,
|
||||
startTime: this.newStartTime,
|
||||
startTime: adjustedStartTime,
|
||||
};
|
||||
|
||||
const isSameTrack = this.sourceTrackId === this.targetTrackId;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Command } from "@/lib/commands/base-command";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
import { EditorCore } from "@/core";
|
||||
import { enforceMainTrackStart } from "@/lib/timeline/track-utils";
|
||||
|
||||
export class UpdateElementStartTimeCommand extends Command {
|
||||
private savedState: TimelineTrack[] | null = null;
|
||||
|
|
@ -16,7 +17,8 @@ export class UpdateElementStartTimeCommand extends Command {
|
|||
const editor = EditorCore.getInstance();
|
||||
this.savedState = editor.timeline.getTracks();
|
||||
|
||||
const updatedTracks = this.savedState.map((track) => {
|
||||
const currentTracks = this.savedState;
|
||||
const updatedTracks = currentTracks.map((track) => {
|
||||
const hasElementsToUpdate = this.elements.some(
|
||||
(el) => el.trackId === track.id,
|
||||
);
|
||||
|
|
@ -29,9 +31,19 @@ export class UpdateElementStartTimeCommand extends Command {
|
|||
const shouldUpdate = this.elements.some(
|
||||
(el) => el.elementId === element.id && el.trackId === track.id,
|
||||
);
|
||||
return shouldUpdate
|
||||
? { ...element, startTime: Math.max(0, this.startTime) }
|
||||
: element;
|
||||
if (!shouldUpdate) {
|
||||
return element;
|
||||
}
|
||||
|
||||
const baseStartTime = Math.max(0, this.startTime);
|
||||
const adjustedStartTime = enforceMainTrackStart({
|
||||
tracks: currentTracks,
|
||||
targetTrackId: track.id,
|
||||
requestedStartTime: baseStartTime,
|
||||
excludeElementId: element.id,
|
||||
});
|
||||
|
||||
return { ...element, startTime: adjustedStartTime };
|
||||
});
|
||||
return { ...track, elements: newElements } as typeof track;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { TimelineTrack, ElementType } from "@/types/timeline";
|
|||
import { TRACK_HEIGHTS, TRACK_GAP } from "@/constants/timeline-constants";
|
||||
import { wouldElementOverlap } from "./element-utils";
|
||||
import type { ComputeDropTargetParams, DropTarget } from "@/types/timeline";
|
||||
import { isMainTrack } from "./track-utils";
|
||||
import { isMainTrack, enforceMainTrackStart } from "./track-utils";
|
||||
|
||||
function getTrackAtY({
|
||||
mouseY,
|
||||
|
|
@ -185,11 +185,21 @@ export function computeDropTarget({
|
|||
});
|
||||
|
||||
if (isTrackCompatible && !hasOverlap) {
|
||||
const targetTrack = tracks[trackIndex];
|
||||
// safe: snap to 0 only happens when element becomes the new earliest,
|
||||
// meaning the space before the current earliest is empty
|
||||
const adjustedXPosition = enforceMainTrackStart({
|
||||
tracks,
|
||||
targetTrackId: targetTrack.id,
|
||||
requestedStartTime: xPosition,
|
||||
excludeElementId,
|
||||
});
|
||||
|
||||
return {
|
||||
trackIndex,
|
||||
isNewTrack: false,
|
||||
insertPosition: null,
|
||||
xPosition,
|
||||
xPosition: adjustedXPosition,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
AudioTrack,
|
||||
StickerTrack,
|
||||
TextTrack,
|
||||
TimelineElement,
|
||||
} from "@/types/timeline";
|
||||
import {
|
||||
TRACK_COLORS,
|
||||
|
|
@ -242,3 +243,62 @@ export function validateElementTrackCompatibility({
|
|||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
export function getEarliestMainTrackElement({
|
||||
tracks,
|
||||
excludeElementId,
|
||||
}: {
|
||||
tracks: TimelineTrack[];
|
||||
excludeElementId?: string;
|
||||
}): TimelineElement | null {
|
||||
const mainTrack = getMainTrack({ tracks });
|
||||
if (!mainTrack) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elements = mainTrack.elements.filter(
|
||||
(element) => !excludeElementId || element.id !== excludeElementId,
|
||||
);
|
||||
|
||||
if (elements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return elements.reduce((earliest, element) =>
|
||||
element.startTime < earliest.startTime ? element : earliest,
|
||||
);
|
||||
}
|
||||
|
||||
export function enforceMainTrackStart({
|
||||
tracks,
|
||||
targetTrackId,
|
||||
requestedStartTime,
|
||||
excludeElementId,
|
||||
}: {
|
||||
tracks: TimelineTrack[];
|
||||
targetTrackId: string;
|
||||
requestedStartTime: number;
|
||||
excludeElementId?: string;
|
||||
}): number {
|
||||
const mainTrack = getMainTrack({ tracks });
|
||||
if (!mainTrack || mainTrack.id !== targetTrackId) {
|
||||
return requestedStartTime;
|
||||
}
|
||||
|
||||
const earliestElement = getEarliestMainTrackElement({
|
||||
tracks,
|
||||
excludeElementId,
|
||||
});
|
||||
|
||||
if (!earliestElement) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// main track must always start at time 0; if this element would
|
||||
// become the earliest, pin it to the start
|
||||
if (requestedStartTime <= earliestElement.startTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return requestedStartTime;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue