From 852fdd39fba12b442012146e417b9832e86832ae Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:45:45 +1100 Subject: [PATCH] make loop cursor trim-window aware --- src/components/video-editor/VideoEditor.tsx | 29 ++++--- .../videoPlayback/cursorLoopTelemetry.ts | 77 +++++++++++++++++-- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 4eb7dafd..8927f9cd 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -53,7 +53,7 @@ import { getAssetPath } from "@/lib/assetPath"; import { useShortcuts } from "@/contexts/ShortcutsContext"; import { matchesShortcut } from "@/lib/shortcuts"; import { detectInteractionCandidates, normalizeCursorTelemetry } from "./timeline/zoomSuggestionUtils"; -import { buildLoopedCursorTelemetry } from "./videoPlayback/cursorLoopTelemetry"; +import { buildLoopedCursorTelemetry, getDisplayedTimelineWindowMs } from "./videoPlayback/cursorLoopTelemetry"; import { findDominantRegion } from "./videoPlayback/zoomRegionUtils"; const LOOP_CURSOR_END_WINDOW_MS = 670; @@ -613,39 +613,46 @@ export default function VideoEditor() { return normalizeCursorTelemetry(cursorTelemetry, totalMs > 0 ? totalMs : Number.MAX_SAFE_INTEGER); }, [cursorTelemetry, duration]); + const displayedTimelineWindow = useMemo(() => { + const totalMs = Math.max(0, Math.round(duration * 1000)); + return getDisplayedTimelineWindowMs(totalMs, trimRegions); + }, [duration, trimRegions]); + const effectiveCursorTelemetry = useMemo(() => { if (!loopCursor) { return normalizedCursorTelemetry; } - const totalMs = Math.max(0, Math.round(duration * 1000)); - if (normalizedCursorTelemetry.length < 2 || totalMs <= 0) { + if (normalizedCursorTelemetry.length < 2 || displayedTimelineWindow.endMs <= displayedTimelineWindow.startMs) { return normalizedCursorTelemetry; } - return buildLoopedCursorTelemetry(normalizedCursorTelemetry, totalMs); - }, [loopCursor, normalizedCursorTelemetry, duration]); + return buildLoopedCursorTelemetry( + normalizedCursorTelemetry, + displayedTimelineWindow.endMs, + displayedTimelineWindow.startMs, + ); + }, [loopCursor, normalizedCursorTelemetry, displayedTimelineWindow]); const effectiveZoomRegions = useMemo(() => { if (!loopCursor || zoomRegions.length === 0) { return zoomRegions; } - const totalMs = Math.max(0, Math.round(duration * 1000)); - if (totalMs <= 0) { + if (displayedTimelineWindow.endMs <= displayedTimelineWindow.startMs) { return zoomRegions; } - const dominantAtStart = findDominantRegion(zoomRegions, 0, { connectZooms }).region; + const dominantAtStart = findDominantRegion(zoomRegions, displayedTimelineWindow.startMs, { connectZooms }).region; if (!dominantAtStart) { return zoomRegions; } - const endWindowStartMs = Math.max(0, totalMs - LOOP_CURSOR_END_WINDOW_MS); + const endWindowStartMs = Math.max(displayedTimelineWindow.startMs, displayedTimelineWindow.endMs - LOOP_CURSOR_END_WINDOW_MS); const loopEndRegion: ZoomRegion = { id: `${dominantAtStart.id}__loop-end-sync`, startMs: endWindowStartMs, - endMs: totalMs, + endMs: displayedTimelineWindow.endMs, depth: dominantAtStart.depth, focus: { cx: dominantAtStart.focus.cx, @@ -657,7 +664,7 @@ export default function VideoEditor() { ...zoomRegions.filter((region) => region.id !== loopEndRegion.id), loopEndRegion, ]; - }, [loopCursor, zoomRegions, duration, connectZooms]); + }, [loopCursor, zoomRegions, displayedTimelineWindow, connectZooms]); useEffect(() => { if (!videoPath || duration <= 0 || zoomRegions.length > 0 || normalizedCursorTelemetry.length < 2) { diff --git a/src/components/video-editor/videoPlayback/cursorLoopTelemetry.ts b/src/components/video-editor/videoPlayback/cursorLoopTelemetry.ts index fc66c518..4c513e19 100644 --- a/src/components/video-editor/videoPlayback/cursorLoopTelemetry.ts +++ b/src/components/video-editor/videoPlayback/cursorLoopTelemetry.ts @@ -1,4 +1,4 @@ -import type { CursorTelemetryPoint } from '../types'; +import type { CursorTelemetryPoint, TrimRegion } from '../types'; const LOOP_CURSOR_FREEZE_DURATION_MS = 670; const LOOP_CURSOR_RETURN_STEPS = 20; @@ -141,21 +141,30 @@ function findLatestSample(samples: CursorTelemetryPoint[], timeMs: number) { export function buildLoopedCursorTelemetry( samples: CursorTelemetryPoint[], totalDurationMs: number, + timelineStartMs = 0, ): CursorTelemetryPoint[] { if (!samples || samples.length === 0) { return []; } const timelineEndMs = Math.max(0, Math.round(totalDurationMs)); - if (timelineEndMs <= 0) { + const timelineStartClampedMs = clamp(Math.round(timelineStartMs), 0, timelineEndMs); + if (timelineEndMs <= timelineStartClampedMs) { return samples; } - const firstSample = samples[0]; - const lastSample = samples[samples.length - 1]; + const boundedSamples = samples.filter( + (sample) => sample.timeMs >= timelineStartClampedMs && sample.timeMs <= timelineEndMs, + ); + if (boundedSamples.length === 0) { + return samples; + } + + const firstSample = boundedSamples[0]; + const lastSample = boundedSamples[boundedSamples.length - 1]; const maxFreezeWindowMs = timelineEndMs - firstSample.timeMs; if (maxFreezeWindowMs <= 1) { - return samples; + return boundedSamples; } const freezeDurationMs = Math.min(LOOP_CURSOR_FREEZE_DURATION_MS, maxFreezeWindowMs); @@ -163,10 +172,10 @@ export function buildLoopedCursorTelemetry( const clampedSettleDurationMs = Math.min(LOOP_CURSOR_SETTLE_DURATION_MS, Math.max(0, freezeDurationMs - 1)); const returnMotionDurationMs = Math.max(1, freezeDurationMs - clampedSettleDurationMs); const sourceStartMs = firstSample.timeMs; - const sourceEndMs = Math.max(sourceStartMs, findLastMovingSampleTime(samples)); + const sourceEndMs = Math.max(sourceStartMs, findLastMovingSampleTime(boundedSamples)); const sourceDurationMs = Math.max(1, sourceEndMs - sourceStartMs); const playbackWindowMs = Math.max(1, motionEndMs); - const startingCursorType = findFirstStableCursorType(samples); + const startingCursorType = findFirstStableCursorType(boundedSamples); const loopedSamples: CursorTelemetryPoint[] = [ { ...firstSample, @@ -176,7 +185,7 @@ export function buildLoopedCursorTelemetry( }, ]; - for (const sample of samples) { + for (const sample of boundedSamples) { const progress = clamp((sample.timeMs - sourceStartMs) / sourceDurationMs, 0, 1); const mappedTimeMs = Math.round(playbackWindowMs * progress); @@ -230,4 +239,56 @@ export function buildLoopedCursorTelemetry( } return loopedSamples; +} + +export function getDisplayedTimelineEndMs(totalDurationMs: number, trimRegions: TrimRegion[]) { + return getDisplayedTimelineWindowMs(totalDurationMs, trimRegions).endMs; +} + +export function getDisplayedTimelineWindowMs(totalDurationMs: number, trimRegions: TrimRegion[]) { + const durationMs = Math.max(0, Math.round(totalDurationMs)); + if (durationMs <= 0) { + return { startMs: 0, endMs: 0 }; + } + + if (!trimRegions || trimRegions.length === 0) { + return { startMs: 0, endMs: durationMs }; + } + + const sortedTrimRegions = trimRegions + .map((region) => ({ + startMs: clamp(Math.round(region.startMs), 0, durationMs), + endMs: clamp(Math.round(region.endMs), 0, durationMs), + })) + .filter((region) => region.endMs > region.startMs) + .sort((a, b) => a.startMs - b.startMs); + + if (sortedTrimRegions.length === 0) { + return { startMs: 0, endMs: durationMs }; + } + + let cursorMs = 0; + let firstVisibleStartMs: number | null = null; + let lastVisibleEndMs = 0; + + for (const trimRegion of sortedTrimRegions) { + if (trimRegion.startMs > cursorMs) { + if (firstVisibleStartMs === null) { + firstVisibleStartMs = cursorMs; + } + lastVisibleEndMs = trimRegion.startMs; + } + cursorMs = Math.max(cursorMs, trimRegion.endMs); + } + + if (cursorMs < durationMs) { + if (firstVisibleStartMs === null) { + firstVisibleStartMs = cursorMs; + } + lastVisibleEndMs = durationMs; + } + + const clampedEndMs = clamp(lastVisibleEndMs, 0, durationMs); + const clampedStartMs = clamp(firstVisibleStartMs ?? 0, 0, clampedEndMs); + return { startMs: clampedStartMs, endMs: clampedEndMs }; } \ No newline at end of file