Skip to content

Commit e92e3e3

Browse files
EtienneLescotclaude
andcommitted
fix(zoom): auto-placed zoom regions follow the cursor for their whole span
Auto zoom regions (created by the magic-wand cursor-dwell suggestion pass) were built with focusMode left undefined unless the separate, global "Auto Focus All" toggle was on. That meant the pan/zoom used the static dwell-centroid focus captured at suggestion time and never tracked the cursor afterward, even though the cursor-follow interpolation logic (zoomRegionUtils/cursorFollowUtils) already existed and just wasn't wired up by default for these regions. Auto-suggested regions now always get focusMode "auto" so they pan to track the cursor telemetry across their whole span, independent of the "Auto Focus All" toggle (which still only controls the default for manually-drawn zoom regions). Fixes #72 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent d5966ed commit e92e3e3

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

src/components/video-editor/VideoEditor.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,11 @@ export default function VideoEditor() {
11361136

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

11621167
// Auto-suggest zooms once per fresh recording (no existing zooms, telemetry
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { CursorTelemetryPoint, ZoomRegion } from "../types";
3+
import { DEFAULT_ZOOM_DEPTH, ZOOM_DEPTH_SCALES } from "../types";
4+
import { findDominantRegion } from "./zoomRegionUtils";
5+
6+
/**
7+
* Regression coverage for issue #72: an auto-placed zoom region must pan to follow
8+
* the cursor for its whole span, not freeze at the focus point captured when the
9+
* region was created/suggested.
10+
*/
11+
describe("findDominantRegion — auto-zoom cursor following", () => {
12+
const baseRegion: ZoomRegion = {
13+
id: "zoom-1",
14+
startMs: 0,
15+
endMs: 4000,
16+
depth: DEFAULT_ZOOM_DEPTH,
17+
customScale: ZOOM_DEPTH_SCALES[DEFAULT_ZOOM_DEPTH],
18+
// The static focus captured at suggestion time (e.g. the dwell centroid) — should
19+
// be ignored in favor of the live cursor position once focusMode is "auto". Kept
20+
// within the depth-3 focus bounds (~0.28-0.72) so clamping doesn't distort assertions.
21+
focus: { cx: 0.35, cy: 0.5 },
22+
focusMode: "auto",
23+
source: "auto",
24+
};
25+
26+
// Cursor sweeps steadily from the left edge to the right edge across the region.
27+
const movingTelemetry: CursorTelemetryPoint[] = [
28+
{ timeMs: 0, cx: 0.1, cy: 0.5 },
29+
{ timeMs: 2000, cx: 0.5, cy: 0.5 },
30+
{ timeMs: 4000, cx: 0.9, cy: 0.5 },
31+
];
32+
33+
it("tracks the cursor across the region instead of freezing at the initial focus", () => {
34+
const early = findDominantRegion([baseRegion], 200, { cursorTelemetry: movingTelemetry });
35+
const mid = findDominantRegion([baseRegion], 2000, { cursorTelemetry: movingTelemetry });
36+
const late = findDominantRegion([baseRegion], 3800, { cursorTelemetry: movingTelemetry });
37+
38+
expect(early.region).not.toBeNull();
39+
expect(mid.region).not.toBeNull();
40+
expect(late.region).not.toBeNull();
41+
42+
// The focus must move meaningfully between samples (cursor-following), not stay pinned.
43+
expect(mid.region?.focus.cx).toBeGreaterThan(early.region?.focus.cx ?? 0);
44+
expect(late.region?.focus.cx).toBeGreaterThan(mid.region?.focus.cx ?? 0);
45+
46+
// And it must not equal the static creation-time focus baked into the region.
47+
expect(mid.region?.focus.cx).not.toBeCloseTo(baseRegion.focus.cx, 2);
48+
});
49+
50+
it("stays frozen at the static focus when focusMode is not auto (manual regions unaffected)", () => {
51+
const manualRegion: ZoomRegion = { ...baseRegion, focusMode: "manual", source: "manual" };
52+
53+
const early = findDominantRegion([manualRegion], 200, { cursorTelemetry: movingTelemetry });
54+
const late = findDominantRegion([manualRegion], 3800, { cursorTelemetry: movingTelemetry });
55+
56+
expect(early.region?.focus.cx).toBeCloseTo(manualRegion.focus.cx, 5);
57+
expect(late.region?.focus.cx).toBeCloseTo(manualRegion.focus.cx, 5);
58+
});
59+
});

0 commit comments

Comments
 (0)