Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,11 @@ export default function VideoEditor() {

// Builds fresh "auto" zoom regions from cursor telemetry without overlapping
// existing ones. Used by both the on-load auto-suggest pass and the wand toggle.
// These regions always follow the cursor for their whole span (focusMode "auto") —
// that's the entire point of an auto-placed zoom: it should pan to track the cursor
// as it moves, not freeze at the dwell point that triggered the suggestion. This is
// independent of the global "Auto Focus All" toggle, which only affects the default
// for manually-drawn zoom regions.
const buildAutoZoomRegions = useCallback(
(existingRegions: ZoomRegion[]): ZoomRegion[] => {
const totalMs = Math.round(duration * 1000);
Expand All @@ -1152,11 +1157,11 @@ export default function VideoEditor() {
depth: DEFAULT_ZOOM_DEPTH,
customScale: ZOOM_DEPTH_SCALES[DEFAULT_ZOOM_DEPTH],
focus: clampFocusToDepth(suggestion.focus, DEFAULT_ZOOM_DEPTH),
focusMode: autoFocusAll ? ("auto" as const) : undefined,
focusMode: "auto" as const,
source: "auto" as const,
}));
},
[cursorTelemetry, duration, autoFocusAll],
[cursorTelemetry, duration],
);

// Auto-suggest zooms once per fresh recording (no existing zooms, telemetry
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import type { CursorTelemetryPoint, ZoomRegion } from "../types";
import { DEFAULT_ZOOM_DEPTH, ZOOM_DEPTH_SCALES } from "../types";
import { findDominantRegion } from "./zoomRegionUtils";

/**
* Regression coverage for issue #72: an auto-placed zoom region must pan to follow
* the cursor for its whole span, not freeze at the focus point captured when the
* region was created/suggested.
*/
describe("findDominantRegion — auto-zoom cursor following", () => {
const baseRegion: ZoomRegion = {
id: "zoom-1",
startMs: 0,
endMs: 4000,
depth: DEFAULT_ZOOM_DEPTH,
customScale: ZOOM_DEPTH_SCALES[DEFAULT_ZOOM_DEPTH],
// The static focus captured at suggestion time (e.g. the dwell centroid) — should
// be ignored in favor of the live cursor position once focusMode is "auto". Kept
// within the depth-3 focus bounds (~0.28-0.72) so clamping doesn't distort assertions.
focus: { cx: 0.35, cy: 0.5 },
focusMode: "auto",
source: "auto",
};

// Cursor sweeps steadily from the left edge to the right edge across the region.
const movingTelemetry: CursorTelemetryPoint[] = [
{ timeMs: 0, cx: 0.1, cy: 0.5 },
{ timeMs: 2000, cx: 0.5, cy: 0.5 },
{ timeMs: 4000, cx: 0.9, cy: 0.5 },
];

it("tracks the cursor across the region instead of freezing at the initial focus", () => {
const early = findDominantRegion([baseRegion], 200, { cursorTelemetry: movingTelemetry });
const mid = findDominantRegion([baseRegion], 2000, { cursorTelemetry: movingTelemetry });
const late = findDominantRegion([baseRegion], 3800, { cursorTelemetry: movingTelemetry });

expect(early.region).not.toBeNull();
expect(mid.region).not.toBeNull();
expect(late.region).not.toBeNull();

// The focus must move meaningfully between samples (cursor-following), not stay pinned.
expect(mid.region?.focus.cx).toBeGreaterThan(early.region?.focus.cx ?? 0);
expect(late.region?.focus.cx).toBeGreaterThan(mid.region?.focus.cx ?? 0);

// And it must not equal the static creation-time focus baked into the region.
expect(mid.region?.focus.cx).not.toBeCloseTo(baseRegion.focus.cx, 2);
});

it("stays frozen at the static focus when focusMode is not auto (manual regions unaffected)", () => {
const manualRegion: ZoomRegion = { ...baseRegion, focusMode: "manual", source: "manual" };

const early = findDominantRegion([manualRegion], 200, { cursorTelemetry: movingTelemetry });
const late = findDominantRegion([manualRegion], 3800, { cursorTelemetry: movingTelemetry });

expect(early.region?.focus.cx).toBeCloseTo(manualRegion.focus.cx, 5);
expect(late.region?.focus.cx).toBeCloseTo(manualRegion.focus.cx, 5);
});
});
Loading