Skip to content

Commit a65aef2

Browse files
committed
feat: implement track compatibility logic for element dropping in timeline
1 parent af67915 commit a65aef2

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

apps/web/src/lib/timeline/drop-utils.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,49 @@ function getMainTrackIndex({ tracks }: { tracks: TimelineTrack[] }): number {
6565
return tracks.findIndex((track) => isMainTrack(track));
6666
}
6767

68+
function findBestCompatibleTrack({
69+
elementType,
70+
tracks,
71+
xPosition,
72+
elementDuration,
73+
excludeElementId,
74+
}: {
75+
elementType: ElementType;
76+
tracks: TimelineTrack[];
77+
xPosition: number;
78+
elementDuration: number;
79+
excludeElementId?: string;
80+
}): number | null {
81+
const endTime = xPosition + elementDuration;
82+
83+
// video/image → prefer main track first
84+
if (elementType === "video" || elementType === "image") {
85+
const mainIdx = getMainTrackIndex({ tracks });
86+
if (mainIdx >= 0) {
87+
const hasOverlap = wouldElementOverlap({
88+
elements: tracks[mainIdx].elements,
89+
startTime: xPosition,
90+
endTime,
91+
excludeElementId,
92+
});
93+
if (!hasOverlap) return mainIdx;
94+
}
95+
}
96+
97+
for (let i = 0; i < tracks.length; i++) {
98+
if (!isCompatible({ elementType, trackType: tracks[i].type })) continue;
99+
const hasOverlap = wouldElementOverlap({
100+
elements: tracks[i].elements,
101+
startTime: xPosition,
102+
endTime,
103+
excludeElementId,
104+
});
105+
if (!hasOverlap) return i;
106+
}
107+
108+
return null;
109+
}
110+
68111
function findInsertIndex({
69112
elementType,
70113
tracks,
@@ -138,6 +181,31 @@ export function computeDropTarget({
138181
const trackAtMouse = getTrackAtY({ mouseY, tracks, verticalDragDirection });
139182

140183
if (!trackAtMouse) {
184+
const compatibleIndex = findBestCompatibleTrack({
185+
elementType,
186+
tracks,
187+
xPosition,
188+
elementDuration,
189+
excludeElementId,
190+
});
191+
192+
if (compatibleIndex !== null) {
193+
const targetTrack = tracks[compatibleIndex];
194+
const adjustedXPosition = enforceMainTrackStart({
195+
tracks,
196+
targetTrackId: targetTrack.id,
197+
requestedStartTime: xPosition,
198+
excludeElementId,
199+
});
200+
201+
return {
202+
trackIndex: compatibleIndex,
203+
isNewTrack: false,
204+
insertPosition: null,
205+
xPosition: adjustedXPosition,
206+
};
207+
}
208+
141209
const isAboveAllTracks = mouseY < 0;
142210

143211
if (elementType === "audio") {
@@ -203,6 +271,31 @@ export function computeDropTarget({
203271
};
204272
}
205273

274+
const fallbackIndex = findBestCompatibleTrack({
275+
elementType,
276+
tracks,
277+
xPosition,
278+
elementDuration,
279+
excludeElementId,
280+
});
281+
282+
if (fallbackIndex !== null) {
283+
const fallbackTrack = tracks[fallbackIndex];
284+
const adjustedXPosition = enforceMainTrackStart({
285+
tracks,
286+
targetTrackId: fallbackTrack.id,
287+
requestedStartTime: xPosition,
288+
excludeElementId,
289+
});
290+
291+
return {
292+
trackIndex: fallbackIndex,
293+
isNewTrack: false,
294+
insertPosition: null,
295+
xPosition: adjustedXPosition,
296+
};
297+
}
298+
206299
let insertAbove = isInUpperHalf;
207300
if (!isTrackCompatible && verticalDragDirection) {
208301
insertAbove = verticalDragDirection === "up";

0 commit comments

Comments
 (0)