tighten click-cluster zoom suggestions

This commit is contained in:
webadderall
2026-05-02 20:21:35 +10:00
parent d4b5cf77ec
commit 676d01827f
2 changed files with 54 additions and 9 deletions
@@ -10,16 +10,19 @@ function makeClick(timeMs: number, cx = 0.5, cy = 0.5): CursorTelemetryPoint {
return { timeMs, cx, cy, interactionType: "click" };
}
/** Wraps a list of click samples with surrounding move events so the telemetry
* passes the `normalizedSamples.length < 2` guard. */
function makeMove(timeMs: number, cx = 0.5, cy = 0.5): CursorTelemetryPoint {
return { timeMs, cx, cy, interactionType: "move" };
}
/** Wraps click samples with surrounding move events to mimic real mixed telemetry. */
function withMoves(
clicks: CursorTelemetryPoint[],
totalMs: number,
): CursorTelemetryPoint[] {
return [
{ timeMs: 0, cx: 0.5, cy: 0.5, interactionType: "move" },
makeMove(0),
...clicks,
{ timeMs: totalMs, cx: 0.5, cy: 0.5, interactionType: "move" },
makeMove(totalMs),
];
}
@@ -43,6 +46,17 @@ describe("buildInteractionZoomSuggestions (click-cluster logic)", () => {
expect(s.end).toBe(5_000 + CLICK_CLUSTER_PAD_MS);
});
it("accepts a single explicit click sample without needing surrounding moves", () => {
const result = buildInteractionZoomSuggestions({
cursorTelemetry: [makeClick(5_000)],
totalMs: TOTAL_MS,
defaultDurationMs: 3_000,
});
expect(result.status).toBe("ok");
expect(result.suggestions).toHaveLength(1);
});
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)],
@@ -123,6 +137,24 @@ describe("buildInteractionZoomSuggestions (click-cluster logic)", () => {
expect(result.suggestions).toHaveLength(0);
});
it("ignores dwell-derived click-like heuristics when there are no explicit clicks", () => {
const telemetry: CursorTelemetryPoint[] = [
makeMove(0, 0.5, 0.5),
makeMove(200, 0.5005, 0.5005),
makeMove(400, 0.5008, 0.5008),
makeMove(600, 0.501, 0.501),
];
const result = buildInteractionZoomSuggestions({
cursorTelemetry: telemetry,
totalMs: TOTAL_MS,
defaultDurationMs: 3_000,
});
expect(result.status).toBe("no-interactions");
expect(result.suggestions).toHaveLength(0);
});
it("skips clusters that overlap reserved spans", () => {
const click = 5_000;
@@ -19,6 +19,7 @@ export interface CursorInteractionCandidate extends ZoomDwellCandidate {
| "dropdown-open"
| "text-selection"
| "text-field-click";
source: "explicit" | "heuristic";
}
export interface SuggestedZoomRegion {
@@ -213,6 +214,7 @@ export function detectInteractionCandidates(
focus: { cx: clickSample.cx, cy: clickSample.cy },
strength: baseStrength,
kind,
source: "explicit",
});
}
@@ -220,12 +222,12 @@ export function detectInteractionCandidates(
const dwellCandidates = detectZoomDwellCandidates(samples).map<CursorInteractionCandidate>(
(candidate) => {
if (candidate.strength >= 1100) {
return { ...candidate, kind: "text-focus-like" };
return { ...candidate, kind: "text-focus-like", source: "heuristic" };
}
if (candidate.strength <= 800) {
return { ...candidate, kind: "click-like" };
return { ...candidate, kind: "click-like", source: "heuristic" };
}
return { ...candidate, kind: "dwell" };
return { ...candidate, kind: "dwell", source: "heuristic" };
},
);
@@ -249,6 +251,7 @@ export function detectInteractionCandidates(
},
strength: prev.strength + curr.strength + 500,
kind: "double-click-like",
source: "heuristic",
});
}
}
@@ -345,13 +348,23 @@ export function buildInteractionZoomSuggestions(params: {
}
const normalizedSamples = normalizeCursorTelemetry(cursorTelemetry, totalMs);
if (normalizedSamples.length < 2) {
if (normalizedSamples.length === 0) {
return { status: "no-telemetry", suggestions: [] };
}
if (
normalizedSamples.length === 1 &&
normalizedSamples[0].interactionType !== "click" &&
normalizedSamples[0].interactionType !== "double-click" &&
normalizedSamples[0].interactionType !== "right-click" &&
normalizedSamples[0].interactionType !== "middle-click"
) {
return { status: "no-telemetry", suggestions: [] };
}
// Only use explicit click events (uiohook telemetry) – ignore dwell heuristics
const clickCandidates = detectInteractionCandidates(normalizedSamples).filter(
(c) => c.kind === "click-like" || c.kind === "double-click-like" || c.kind === "dropdown-open" || c.kind === "text-field-click" || c.kind === "text-selection",
(candidate) => candidate.source === "explicit",
);
if (clickCandidates.length === 0) {