From dad073ebed46cfc28f34d463a5dab5dd542bc5b8 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 2 May 2026 20:38:07 +1000 Subject: [PATCH] harden explicit click zoom suggestions --- .../timeline/zoomSuggestionUtils.test.ts | 27 +++++++++++++++++-- .../timeline/zoomSuggestionUtils.ts | 21 ++++++++++----- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts b/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts index c7514668..77080bfc 100644 --- a/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts +++ b/src/components/video-editor/timeline/zoomSuggestionUtils.test.ts @@ -6,8 +6,13 @@ import { } from "./zoomSuggestionUtils"; import type { CursorTelemetryPoint } from "../types"; -function makeClick(timeMs: number, cx = 0.5, cy = 0.5): CursorTelemetryPoint { - return { timeMs, cx, cy, interactionType: "click" }; +function makeClick( + timeMs: number, + cx = 0.5, + cy = 0.5, + interactionType: CursorTelemetryPoint["interactionType"] = "click", +): CursorTelemetryPoint { + return { timeMs, cx, cy, interactionType }; } function makeMove(timeMs: number, cx = 0.5, cy = 0.5): CursorTelemetryPoint { @@ -57,6 +62,24 @@ describe("buildInteractionZoomSuggestions (click-cluster logic)", () => { expect(result.suggestions).toHaveLength(1); }); + it.each(["right-click", "middle-click"] as const)( + "accepts %s telemetry like a standard click", + (interactionType) => { + const result = buildInteractionZoomSuggestions({ + cursorTelemetry: withMoves([makeClick(5_000, 0.5, 0.5, interactionType)], TOTAL_MS), + totalMs: TOTAL_MS, + defaultDurationMs: 3_000, + }); + + expect(result.status).toBe("ok"); + expect(result.suggestions).toHaveLength(1); + + const [suggestion] = result.suggestions; + expect(suggestion.start).toBe(5_000 - CLICK_CLUSTER_PAD_MS); + expect(suggestion.end).toBe(5_000 + CLICK_CLUSTER_PAD_MS); + }, + ); + it("merges two clicks within 2500ms into one zoom track", () => { const telemetry = withMoves( [makeClick(4_000), makeClick(4_000 + CLICK_CLUSTER_MERGE_GAP_MS - 1)], diff --git a/src/components/video-editor/timeline/zoomSuggestionUtils.ts b/src/components/video-editor/timeline/zoomSuggestionUtils.ts index 52eddfe8..189c7604 100644 --- a/src/components/video-editor/timeline/zoomSuggestionUtils.ts +++ b/src/components/video-editor/timeline/zoomSuggestionUtils.ts @@ -43,6 +43,18 @@ export interface InteractionZoomSuggestionResult { export const CLICK_CLUSTER_MERGE_GAP_MS = 2500; /** Padding added before the first click and after the last click in a cluster. */ export const CLICK_CLUSTER_PAD_MS = 500; +const EXPLICIT_CLICK_TYPES = new Set>([ + "click", + "double-click", + "right-click", + "middle-click", +]); + +function isExplicitClickType( + interactionType: CursorTelemetryPoint["interactionType"], +): interactionType is NonNullable { + return typeof interactionType === "string" && EXPLICIT_CLICK_TYPES.has(interactionType); +} function normalizeTelemetrySample( sample: CursorTelemetryPoint, @@ -188,9 +200,7 @@ export function detectInteractionCandidates( samples: CursorTelemetryPoint[], ): CursorInteractionCandidate[] { // --- Phase 1: Explicit interaction events (from uiohook telemetry) --- - const clickEvents = samples.filter( - (s) => s.interactionType && s.interactionType !== "move" && s.interactionType !== "mouseup", - ); + const clickEvents = samples.filter((sample) => isExplicitClickType(sample.interactionType)); const explicitInteractionCandidates: CursorInteractionCandidate[] = []; @@ -354,10 +364,7 @@ export function buildInteractionZoomSuggestions(params: { if ( normalizedSamples.length === 1 && - normalizedSamples[0].interactionType !== "click" && - normalizedSamples[0].interactionType !== "double-click" && - normalizedSamples[0].interactionType !== "right-click" && - normalizedSamples[0].interactionType !== "middle-click" + !isExplicitClickType(normalizedSamples[0].interactionType) ) { return { status: "no-telemetry", suggestions: [] }; }