diff options
Diffstat (limited to 'apps/web/src/lib/commands')
9 files changed, 115 insertions, 315 deletions
diff --git a/apps/web/src/lib/commands/timeline/clipboard/paste.ts b/apps/web/src/lib/commands/timeline/clipboard/paste.ts index d9c54ec3..e1b43364 100644 --- a/apps/web/src/lib/commands/timeline/clipboard/paste.ts +++ b/apps/web/src/lib/commands/timeline/clipboard/paste.ts @@ -6,13 +6,12 @@ import type { ClipboardItem,
} from "@/lib/timeline";
import { generateUUID } from "@/utils/id";
-import { wouldElementOverlap } from "@/lib/timeline/element-utils";
import {
- buildEmptyTrack,
- getHighestInsertIndexForTrack,
+ applyPlacement,
+ resolveTrackPlacement,
isMainTrack,
enforceMainTrackStart,
-} from "@/lib/timeline/track-utils";
+} from "@/lib/timeline/placement";
import { cloneAnimations } from "@/lib/animation";
export class PasteCommand extends Command {
@@ -59,17 +58,22 @@ export class PasteCommand extends Command { const sourceTrackIndex = updatedTracks.findIndex(
(track) => track.id === trackId,
);
- const resolvedTargetIndex = resolveTargetTrackIndex({
+ const placementResult = resolveTrackPlacement({
tracks: updatedTracks,
- sourceTrackIndex,
trackType,
- elements: elementsToAdd,
+ timeSpans: elementsToAdd.map((element) => ({
+ startTime: element.startTime,
+ duration: element.duration,
+ })),
+ strategy: { type: "aboveSource", sourceTrackIndex },
});
+ if (!placementResult) {
+ continue;
+ }
- if (resolvedTargetIndex >= 0) {
- const targetTrack = updatedTracks[resolvedTargetIndex];
- let adjustedElements = elementsToAdd;
-
+ let elementsForPlacement = elementsToAdd;
+ if (placementResult.kind === "existingTrack") {
+ const targetTrack = updatedTracks[placementResult.trackIndex];
if (isMainTrack(targetTrack)) {
const earliestElement = elementsToAdd.reduce((earliest, element) =>
element.startTime < earliest.startTime ? element : earliest,
@@ -82,39 +86,28 @@ export class PasteCommand extends Command { const delta = adjustedEarliestStartTime - earliestElement.startTime;
if (delta !== 0) {
- adjustedElements = elementsToAdd.map((element) => ({
+ elementsForPlacement = elementsToAdd.map((element) => ({
...element,
startTime: Math.max(0, element.startTime + delta),
}));
}
}
-
- updatedTracks[resolvedTargetIndex] = {
- ...targetTrack,
- elements: [...targetTrack.elements, ...adjustedElements],
- } as TimelineTrack;
- for (const element of adjustedElements) {
- this.pastedElements.push({
- trackId: targetTrack.id,
- elementId: element.id,
- });
- }
- continue;
}
- const insertIndex = resolveInsertIndexForNewTrack({
+ const applied = applyPlacement({
tracks: updatedTracks,
- sourceTrackIndex,
- trackType,
+ placementResult,
+ elements: elementsForPlacement,
});
- const newTrack = buildTrackWithElements({
- trackType,
- elements: elementsToAdd,
- });
- updatedTracks.splice(insertIndex, 0, newTrack);
- for (const element of elementsToAdd) {
+ if (!applied) {
+ continue;
+ }
+
+ updatedTracks = applied.updatedTracks;
+
+ for (const element of elementsForPlacement) {
this.pastedElements.push({
- trackId: newTrack.id,
+ trackId: applied.targetTrackId,
elementId: element.id,
});
}
@@ -187,107 +180,3 @@ function buildPastedElements({ return elementsToAdd;
}
-function resolveTargetTrackIndex({
- tracks,
- sourceTrackIndex,
- trackType,
- elements,
-}: {
- tracks: TimelineTrack[];
- sourceTrackIndex: number;
- trackType: ClipboardItem["trackType"];
- elements: TimelineElement[];
-}): number {
- if (sourceTrackIndex >= 0) {
- const aboveIndex = sourceTrackIndex - 1;
- if (aboveIndex < 0) {
- return -1;
- }
-
- const aboveTrack = tracks[aboveIndex];
- if (aboveTrack.type !== trackType) {
- return -1;
- }
-
- const canPlaceOnAbove = canPlaceElementsOnTrack({
- track: aboveTrack,
- elements,
- });
- return canPlaceOnAbove ? aboveIndex : -1;
- }
-
- const highestCompatibleIndex = tracks.findIndex(
- (track) => track.type === trackType,
- );
- if (highestCompatibleIndex < 0) {
- return -1;
- }
-
- const highestCompatibleTrack = tracks[highestCompatibleIndex];
- const canPlaceOnHighest = canPlaceElementsOnTrack({
- track: highestCompatibleTrack,
- elements,
- });
-
- return canPlaceOnHighest ? highestCompatibleIndex : -1;
-}
-
-function resolveInsertIndexForNewTrack({
- tracks,
- sourceTrackIndex,
- trackType,
-}: {
- tracks: TimelineTrack[];
- sourceTrackIndex: number;
- trackType: ClipboardItem["trackType"];
-}): number {
- if (sourceTrackIndex >= 0) {
- return sourceTrackIndex;
- }
-
- const highestCompatibleIndex = tracks.findIndex(
- (track) => track.type === trackType,
- );
- if (highestCompatibleIndex >= 0) {
- return highestCompatibleIndex;
- }
-
- return getHighestInsertIndexForTrack({ tracks, trackType });
-}
-
-function canPlaceElementsOnTrack({
- track,
- elements,
-}: {
- track: TimelineTrack;
- elements: TimelineElement[];
-}): boolean {
- for (const element of elements) {
- const endTime = element.startTime + element.duration;
- const hasOverlap = wouldElementOverlap({
- elements: track.elements,
- startTime: element.startTime,
- endTime,
- });
- if (hasOverlap) {
- return false;
- }
- }
-
- return true;
-}
-
-function buildTrackWithElements({
- trackType,
- elements,
-}: {
- trackType: ClipboardItem["trackType"];
- elements: TimelineElement[];
-}): TimelineTrack {
- const newTrackId = generateUUID();
- const newTrackBase = buildEmptyTrack({ id: newTrackId, type: trackType });
- return {
- ...newTrackBase,
- elements,
- } as TimelineTrack;
-}
diff --git a/apps/web/src/lib/commands/timeline/element/delete-elements.ts b/apps/web/src/lib/commands/timeline/element/delete-elements.ts index 9b27a3c2..4bbb0d48 100644 --- a/apps/web/src/lib/commands/timeline/element/delete-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/delete-elements.ts @@ -1,7 +1,8 @@ import { Command } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core";
-import { isMainTrack, rippleShiftElements } from "@/lib/timeline";
+import { rippleShiftElements } from "@/lib/timeline";
+import { isMainTrack } from "@/lib/timeline/placement";
export class DeleteElementsCommand extends Command {
private savedState: TimelineTrack[] | null = null;
diff --git a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts index 346f8c73..a4f7ca82 100644 --- a/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/duplicate-elements.ts @@ -2,10 +2,7 @@ import { Command } from "@/lib/commands/base-command"; import type { TimelineElement, TimelineTrack } from "@/lib/timeline";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
-import {
- buildEmptyTrack,
- getHighestInsertIndexForTrack,
-} from "@/lib/timeline/track-utils";
+import { applyPlacement, resolveTrackPlacement } from "@/lib/timeline/placement";
import { cloneAnimations } from "@/lib/animation";
interface DuplicateElementsParams {
@@ -45,22 +42,12 @@ export class DuplicateElementsCommand extends Command { );
const newTrackElements: TimelineElement[] = [];
- const newTrackId = generateUUID();
- const newTrackBase = buildEmptyTrack({
- id: newTrackId,
- type: track.type,
- });
-
for (const element of track.elements) {
if (!elementIdsToDuplicate.has(element.id)) {
continue;
}
const newId = generateUUID();
- this.duplicatedElements.push({
- trackId: newTrackId,
- elementId: newId,
- });
newTrackElements.push(
buildDuplicateElement({
element,
@@ -70,16 +57,33 @@ export class DuplicateElementsCommand extends Command { );
}
- const newTrack = {
- ...newTrackBase,
- elements: newTrackElements,
- } as TimelineTrack;
-
- const insertIndex = getHighestInsertIndexForTrack({
+ const placementResult = resolveTrackPlacement({
tracks: updatedTracks,
trackType: track.type,
+ timeSpans: [],
+ strategy: { type: "alwaysNew", position: "highest" },
});
- updatedTracks.splice(insertIndex, 0, newTrack);
+ if (!placementResult || placementResult.kind !== "newTrack") {
+ continue;
+ }
+
+ const applied = applyPlacement({
+ tracks: updatedTracks,
+ placementResult,
+ elements: newTrackElements,
+ });
+ if (!applied) {
+ continue;
+ }
+
+ updatedTracks = applied.updatedTracks;
+
+ for (const element of newTrackElements) {
+ this.duplicatedElements.push({
+ trackId: applied.targetTrackId,
+ elementId: element.id,
+ });
+ }
}
editor.timeline.updateTracks(updatedTracks);
diff --git a/apps/web/src/lib/commands/timeline/element/insert-element.ts b/apps/web/src/lib/commands/timeline/element/insert-element.ts index e6d6ee99..cdff3565 100644 --- a/apps/web/src/lib/commands/timeline/element/insert-element.ts +++ b/apps/web/src/lib/commands/timeline/element/insert-element.ts @@ -5,23 +5,18 @@ import type { TimelineTrack,
TimelineElement,
TrackType,
- ElementType,
} from "@/lib/timeline";
import { generateUUID } from "@/utils/id";
+import { requiresMediaId } from "@/lib/timeline/element-utils";
+import type { MediaAsset } from "@/lib/media/types";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics";
import {
- requiresMediaId,
- wouldElementOverlap,
-} from "@/lib/timeline/element-utils";
-import {
- buildEmptyTrack,
+ applyPlacement,
canElementGoOnTrack,
- getDefaultInsertIndexForTrack,
+ resolveTrackPlacement,
validateElementTrackCompatibility,
- enforceMainTrackStart,
-} from "@/lib/timeline/track-utils";
-import type { MediaAsset } from "@/lib/media/types";
-import { ELEMENT_TRACK_MAP, TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
-import { graphicsRegistry, registerDefaultGraphics } from "@/lib/graphics";
+} from "@/lib/timeline/placement";
type InsertElementPlacement =
| { mode: "explicit"; trackId: string }
@@ -67,7 +62,7 @@ export class InsertElementCommand extends Command { const isFirstElement = totalElementsInTimeline === 0;
const newElement = this.buildElement({ element: this.element });
- const updateResult = this.resolveTracksWithElement({
+ const updateResult = this.applyPlacementResult({
tracks: this.savedState,
element: newElement,
});
@@ -190,7 +185,7 @@ export class InsertElementCommand extends Command { return true;
}
- private resolveTracksWithElement({
+ private applyPlacementResult({
tracks,
element,
}: {
@@ -199,160 +194,71 @@ export class InsertElementCommand extends Command { }): { updatedTracks: TimelineTrack[]; targetTrackId: string } | null {
const placement = this.placement;
- if (placement.mode === "explicit") {
- const targetTrack = tracks.find(
- (track) => track.id === placement.trackId,
- );
-
- if (!targetTrack) {
- console.error("Track not found:", placement.trackId);
- return null;
- }
-
- const validation = validateElementTrackCompatibility({
- element,
- track: targetTrack,
- });
-
- if (!validation.isValid) {
- console.error(validation.errorMessage);
- return null;
- }
-
- const adjustedElement = this.adjustElementForMainTrack({
- tracks,
- targetTrackId: targetTrack.id,
- element,
- });
-
- const updatedTracks = tracks.map((track) =>
- track.id === targetTrack.id
- ? {
- ...track,
- elements: [...track.elements, adjustedElement],
- }
- : track,
- ) as TimelineTrack[];
-
- return { updatedTracks, targetTrackId: targetTrack.id };
- }
-
- const trackType =
- placement.trackType ?? this.getTrackTypeForElement({ element });
-
if (
+ placement.mode === "auto" &&
placement.trackType &&
!canElementGoOnTrack({
elementType: element.type,
- trackType,
+ trackType: placement.trackType,
})
) {
console.error(
- `${element.type} elements cannot be placed on ${trackType} tracks`,
+ `${element.type} elements cannot be placed on ${placement.trackType} tracks`,
);
return null;
}
- const elementEndTime = element.startTime + element.duration;
- const existingTrack = tracks.find((track) => {
- if (
- !canElementGoOnTrack({
- elementType: element.type,
- trackType: track.type,
- })
- ) {
- return false;
- }
-
- return !wouldElementOverlap({
- elements: track.elements,
- startTime: element.startTime,
- endTime: elementEndTime,
- });
- });
-
- if (existingTrack) {
- const adjustedElement = this.adjustElementForMainTrack({
- tracks,
- targetTrackId: existingTrack.id,
- element,
- });
-
- const updatedTracks = tracks.map((track) =>
- track.id === existingTrack.id
- ? {
- ...track,
- elements: [...track.elements, adjustedElement],
- }
- : track,
- ) as TimelineTrack[];
-
- return { updatedTracks, targetTrackId: existingTrack.id };
- }
-
- const newTrackId = generateUUID();
- const newTrack = buildEmptyTrack({
- id: newTrackId,
- type: trackType,
+ const placementResult = resolveTrackPlacement({
+ tracks,
+ ...(placement.mode === "auto" && placement.trackType
+ ? { trackType: placement.trackType }
+ : { elementType: element.type }),
+ timeSpans: [
+ {
+ startTime: element.startTime,
+ duration: element.duration,
+ },
+ ],
+ strategy:
+ placement.mode === "explicit"
+ ? { type: "explicit", trackId: placement.trackId }
+ : { type: "firstAvailable" },
});
- const newTrackWithElement = {
- ...newTrack,
- elements: [...newTrack.elements, element],
- } as TimelineTrack;
+ if (!placementResult) {
+ if (placement.mode === "explicit") {
+ const targetTrack = tracks.find((track) => track.id === placement.trackId);
+ if (!targetTrack) {
+ console.error("Track not found:", placement.trackId);
+ return null;
+ }
- const updatedTracks = [...tracks];
- const insertIndex =
- placement.insertIndex ??
- this.getAutoInsertIndex({ tracks: updatedTracks, trackType });
- updatedTracks.splice(insertIndex, 0, newTrackWithElement);
-
- return { updatedTracks, targetTrackId: newTrackId };
- }
-
- private getAutoInsertIndex({
- tracks,
- trackType,
- }: {
- tracks: TimelineTrack[];
- trackType: TrackType;
- }): number {
- if (trackType === "text") {
- const firstVideoTrackIndex = tracks.findIndex(
- (track) => track.type === "video",
- );
- if (firstVideoTrackIndex >= 0) {
- return firstVideoTrackIndex;
+ const validation = validateElementTrackCompatibility({
+ element,
+ track: targetTrack,
+ });
+ console.error(validation.errorMessage);
}
+
+ return null;
}
- return getDefaultInsertIndexForTrack({
- tracks,
- trackType,
- });
- }
+ const elementToPlace =
+ placementResult.kind === "existingTrack"
+ ? {
+ ...element,
+ startTime:
+ placementResult.adjustedStartTime ?? element.startTime,
+ }
+ : element;
- private adjustElementForMainTrack({
- tracks,
- targetTrackId,
- element,
- }: {
- tracks: TimelineTrack[];
- targetTrackId: string;
- element: TimelineElement;
- }): TimelineElement {
- const adjustedStartTime = enforceMainTrackStart({
+ return applyPlacement({
tracks,
- targetTrackId,
- requestedStartTime: element.startTime,
+ placementResult,
+ elements: [elementToPlace],
+ newTrackInsertIndexOverride:
+ placement.mode === "auto" && typeof placement.insertIndex === "number"
+ ? placement.insertIndex
+ : undefined,
});
- return { ...element, startTime: adjustedStartTime };
- }
-
- private getTrackTypeForElement({
- element,
- }: {
- element: { type: ElementType };
- }): TrackType {
- return ELEMENT_TRACK_MAP[element.type];
}
}
diff --git a/apps/web/src/lib/commands/timeline/element/move-elements.ts b/apps/web/src/lib/commands/timeline/element/move-elements.ts index 24605d0b..73d07bff 100644 --- a/apps/web/src/lib/commands/timeline/element/move-elements.ts +++ b/apps/web/src/lib/commands/timeline/element/move-elements.ts @@ -10,7 +10,7 @@ import { isMainTrack,
validateElementTrackCompatibility,
enforceMainTrackStart,
-} from "@/lib/timeline/track-utils";
+} from "@/lib/timeline/placement";
import { rippleShiftElements } from "@/lib/timeline/ripple-utils";
export class MoveElementCommand extends Command {
diff --git a/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts b/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts index d640d001..ff81cff2 100644 --- a/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts +++ b/apps/web/src/lib/commands/timeline/element/update-element-start-time.ts @@ -1,7 +1,7 @@ import { Command } from "@/lib/commands/base-command";
import type { TimelineTrack } from "@/lib/timeline";
import { EditorCore } from "@/core";
-import { enforceMainTrackStart } from "@/lib/timeline/track-utils";
+import { enforceMainTrackStart } from "@/lib/timeline/placement";
export class UpdateElementStartTimeCommand extends Command {
private savedState: TimelineTrack[] | null = null;
diff --git a/apps/web/src/lib/commands/timeline/element/update-element-trim.ts b/apps/web/src/lib/commands/timeline/element/update-element-trim.ts index b2c77418..13ee66dd 100644 --- a/apps/web/src/lib/commands/timeline/element/update-element-trim.ts +++ b/apps/web/src/lib/commands/timeline/element/update-element-trim.ts @@ -3,7 +3,7 @@ import type { TimelineTrack } from "@/lib/timeline"; import { EditorCore } from "@/core"; import { clampAnimationsToDuration } from "@/lib/animation"; import { isRetimableElement, rippleShiftElements } from "@/lib/timeline"; -import { enforceMainTrackStart } from "@/lib/timeline/track-utils"; +import { enforceMainTrackStart } from "@/lib/timeline/placement"; export class UpdateElementTrimCommand extends Command { private savedState: TimelineTrack[] | null = null; diff --git a/apps/web/src/lib/commands/timeline/track/add-track.ts b/apps/web/src/lib/commands/timeline/track/add-track.ts index b1f0daa0..104c0462 100644 --- a/apps/web/src/lib/commands/timeline/track/add-track.ts +++ b/apps/web/src/lib/commands/timeline/track/add-track.ts @@ -5,7 +5,7 @@ import { EditorCore } from "@/core"; import {
buildEmptyTrack,
getDefaultInsertIndexForTrack,
-} from "@/lib/timeline/track-utils";
+} from "@/lib/timeline/placement";
export class AddTrackCommand extends Command {
private trackId: string;
diff --git a/apps/web/src/lib/commands/timeline/track/remove-track.ts b/apps/web/src/lib/commands/timeline/track/remove-track.ts index a9859a1a..f35edbe4 100644 --- a/apps/web/src/lib/commands/timeline/track/remove-track.ts +++ b/apps/web/src/lib/commands/timeline/track/remove-track.ts @@ -1,7 +1,7 @@ import { Command } from "@/lib/commands/base-command";
import { EditorCore } from "@/core";
import type { TimelineTrack } from "@/lib/timeline";
-import { getMainTrack } from "@/lib/timeline";
+import { getMainTrack } from "@/lib/timeline/placement";
export class RemoveTrackCommand extends Command {
private savedState: TimelineTrack[] | null = null;
|
