diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 6e7c324236..a804c5f90c 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -197,6 +197,12 @@ export function ClipTrack( const split = () => editorState.timeline.interactMode === "split"; + // Shared state for tracking which segment is being dragged and its offset + const [activeDragState, setActiveDragState] = createSignal(null); + return ( { const ds = startHandleDrag(); - const offset = ds ? ds.offset / segment.timescale : 0; + const currentOffset = ds ? ds.offset / segment.timescale : 0; + + // Check if any previous segment is being dragged + const dragState = activeDragState(); + let previousDragOffset = 0; + if (dragState && dragState.segmentIndex < i()) { + // Apply the drag offset from the previous segment + previousDragOffset = dragState.offset / segment.timescale; + } + + // Calculate proposed start position + const proposedStart = + prevDuration() + currentOffset + previousDragOffset; + + // Ensure we never visually overlap with the previous segment + // prevDuration already includes previous segments' durations + const clampedStart = Math.max(proposedStart, prevDuration()); return { - start: Math.max(prevDuration() + offset, 0), + start: Math.max(clampedStart, 0), end: - prevDuration() + - offset + + clampedStart + (segment.end - segment.start) / segment.timescale, timescale: segment.timescale, recordingSegment: segment.recordingSegment, @@ -385,6 +406,7 @@ export function ClipTrack( isSelected() ? "wobble-wrapper border-gray-12" : "border-transparent", + startHandleDrag() && "z-50", )} innerClass="ring-blue-9" segment={relativeSegment()} @@ -522,11 +544,6 @@ export function ClipTrack( ); const prevSegment = segments()[i() - 1]; - const prevSegmentIsSameClip = - prevSegment?.recordingSegment !== undefined - ? prevSegment.recordingSegment === - segment.recordingSegment - : false; function update(event: MouseEvent) { const newStart = @@ -535,20 +552,33 @@ export function ClipTrack( secsPerPixel() * segment.timescale; + // Calculate the minimum allowed start position + // Must not overlap with previous segment, regardless of clip + const minAllowedStart = prevSegment ? prevSegment.end : 0; + const clampedStart = Math.min( Math.max( newStart, - prevSegmentIsSameClip ? prevSegment.end : 0, + minAllowedStart, segment.end - maxDuration, ), segment.end - 1, ); + const offset = clampedStart - initialStart; + setStartHandleDrag({ - offset: clampedStart - initialStart, + offset: offset, initialStart, }); + requestAnimationFrame(() => { + setActiveDragState({ + segmentIndex: i(), + offset: offset, + }); + }); + setProject( "timeline", "segments", @@ -562,8 +592,8 @@ export function ClipTrack( createRoot((dispose) => { onCleanup(() => { resumeHistory(); - console.log("NUL"); setStartHandleDrag(null); + setActiveDragState(null); onHandleReleased(); }); diff --git a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx index 1d476bdf8b..49c676bea3 100644 --- a/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx @@ -116,23 +116,27 @@ export function ZoomTrack(props: { if (availableTime < minDuration) return; } - if (nextSegment.value[0].start - previewTime < 1) - return { - index: nextSegment.value[1], - start: nextSegment.value[0].start - minDuration, - end: nextSegment.value[0].start, - max: nextSegment.value[0].start, - }; + // Check if there's space between preview time and next segment + const spaceToNext = nextSegment.value[0].start - previewTime; + if (spaceToNext < minDuration) { + // If there's not enough space for minimum duration, don't show proposed zoom + return; + } } + const max = nextSegment.pipe( + Option.map(([s]) => s.start), + Option.getOrElse(() => totalDuration()), + ); + + // Check if there's enough space before returning + if (max - previewTime < minDuration) return; + return { index: nextSegment.pipe(Option.map(([_, i]) => i)), start: previewTime, end: previewTime + minDuration, - max: nextSegment.pipe( - Option.map(([s]) => s.start), - Option.getOrElse(() => totalDuration()), - ), + max: max, }; }; @@ -418,7 +422,7 @@ export function ZoomTrack(props: { return ( {(details) => (