Skip to content

feat: dead zone logic for auto-zoom to prevent jittery segments#1664

Open
namearth5005 wants to merge 7 commits intoCapSoftware:mainfrom
namearth5005:feat/auto-zoom-dead-zone
Open

feat: dead zone logic for auto-zoom to prevent jittery segments#1664
namearth5005 wants to merge 7 commits intoCapSoftware:mainfrom
namearth5005:feat/auto-zoom-dead-zone

Conversation

@namearth5005
Copy link

@namearth5005 namearth5005 commented Mar 18, 2026

Summary

Adds dead zone filtering to auto-zoom segment generation (refs #1646, builds on #1663):

  • New dead_zone_radius config field (default 0.1 = 10% of screen) — if cursor stays within this radius of a previous click group's centroid, clicks merge into the same group regardless of time gap
  • Modified click grouping — dead zone check runs alongside the existing time+spatial proximity check, using centroid distance
  • 3 new tests — merges nearby clicks, separates distant clicks, disables at radius 0.0
  • TypeScript defaults updated to include the new field

How it works

Before: Two clicks 4s apart at (0.5, 0.5) and (0.55, 0.55) → 2 separate zoom segments → jittery zoom-in/out

After: Same clicks with dead_zone_radius: 0.1 → grouped into 1 segment → smooth experience

Backward compatibility

dead_zone_radius defaults to 0.1 via #[serde(default)]. Existing recordings are unaffected since auto-zoom segments are regenerated from click data, not stored with this parameter.

Test plan

  • dead_zone_merges_nearby_clicks — clicks within radius merge to 1 segment
  • dead_zone_allows_distant_clicks — clicks outside radius stay separate
  • dead_zone_zero_disables — radius 0.0 produces same behavior as before
  • Manual: record with repeated clicks in same area → single smooth zoom instead of flickering

Greptile Summary

This PR adds a configurable dead_zone_radius to the auto-zoom click-grouping algorithm so that spatially close clicks are merged into a single zoom segment even when the time gap between them exceeds click_group_time_threshold, preventing jittery zoom-in/out on repeated interactions with the same UI element. It also lifts all previously hard-coded zoom constants into a new AutoZoomConfig struct and exposes three of them (zoom amount, sensitivity, smoothing) as UI sliders in the Experimental settings page.

Key observations:

  • The dead zone centroid is the running average of all positions already in the group. Each new click slightly shifts the centroid, which may then accept the next click that is slightly further away; over many incremental clicks this can cause the dead zone to slowly drift across the screen (see inline comment for a worked example). Using the first click in the group as a fixed anchor would eliminate this behaviour.
  • The dead zone check scans all groups with no temporal guard; a click on the same button 2 minutes later will silently merge into the original group, potentially producing a multi-minute zoom segment. This is explicitly intentional per the PR description, but deserves an in-code comment explaining the trade-off.
  • The dead_zone_merges_nearby_clicks test assertion segments.len() <= 1 passes even when zero segments are produced, which would indicate the feature is broken rather than working; it should assert == 1.
  • dead_zone_radius is included in AutoZoomConfig and its TypeScript defaults, but no UI slider is exposed for it, so users cannot tune the feature that this PR is specifically introducing.

Confidence Score: 3/5

  • The core algorithm works for the common case but has two algorithmic edge cases (centroid drift and unbounded temporal merging) that can produce unexpectedly long zoom segments in longer recordings.
  • The refactor from hard-coded constants to AutoZoomConfig is clean and backward-compatible. The dead zone logic is generally sound for short recordings and the targeted use-case (same button clicked repeatedly). However, the lack of any time bound on the dead zone merge and the centroid drift property are real algorithmic concerns that could silently degrade the user experience in longer recordings — neither is guarded against in the tests. The weak test assertion also means the core happy-path test could pass even if the merge produced no segment at all.
  • apps/desktop/src-tauri/src/recording.rs — dead zone grouping logic and test assertions

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/recording.rs Core implementation of dead zone logic and refactor of hardcoded constants into AutoZoomConfig; centroid drift and unbounded temporal merging are concerns, and the new test assertion is weaker than it should be.
crates/project/src/configuration.rs Adds well-structured AutoZoomConfig struct with sensible defaults and proper serde/specta derives; no issues found.
apps/desktop/src-tauri/src/general_settings.rs Adds auto_zoom_config field to GeneralSettingsStore with correct serde default and Default impl; straightforward and correct.
apps/desktop/src-tauri/src/lib.rs Threads AppHandle into the Tauri command to read auto_zoom_config from persisted settings; uses a double-unwrap with fallback to default which silently discards read errors, but this is consistent with settings-reading patterns elsewhere.
apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx Adds SettingSlider component and UI for zoom amount, sensitivity, and smoothing; handleConfigChange is intentionally synchronous (consistent with handleChange which also doesn't await generalSettingsStore.set); dead_zone_radius has no corresponding UI control, so users can't tune the core new feature from the settings page.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[New click event] --> B{Has position?}
    B -- No --> C[time_and_spatial check only]
    B -- Yes --> D{dead_zone_radius > 0?}
    D -- No --> C
    D -- Yes --> E[Iterate existing click_groups]
    C --> E

    E --> F{time_and_spatial:\ntime_close AND spatial_close?}
    F -- Yes --> G[Merge into group\nbreak]
    F -- No --> H{in_dead_zone:\ndist to running centroid < radius?}
    H -- Yes --> G
    H -- No --> I{More groups?}
    I -- Yes --> E
    I -- No --> J[Create new group]

    G --> K[Recompute centroid\ncentroid drifts toward new click]
    J --> L[Centroid = single click position]

    K --> M[Generate interval:\ngroup_start − pre_padding .. group_end + post_padding]
    L --> M
    M --> N{duration ≥ min_segment_duration?}
    N -- Yes --> O[Add ZoomSegment]
    N -- No --> P[Discard]
Loading

Comments Outside Diff (2)

  1. apps/desktop/src-tauri/src/recording.rs, line 2571-2580 (link)

    P2 Weak assertion allows a false pass on zero segments

    The assertion segments.len() <= 1 also passes when segments is empty (0 segments). This means if the dead zone merge logic silently failed to group the two nearby clicks but still produced zero segments (e.g. the merged group's interval was filtered out), the test would still pass, giving a false green. Since the two merged clicks form an interval of [0.6s, 6.8s] (duration 6.2s), which comfortably exceeds min_segment_duration = 1.0s, exactly 1 segment should always be produced. The assertion should be tightened to == 1.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/recording.rs
    Line: 2571-2580
    
    Comment:
    **Weak assertion allows a false pass on zero segments**
    
    The assertion `segments.len() <= 1` also passes when `segments` is empty (0 segments). This means if the dead zone merge logic silently failed to group the two nearby clicks but still produced zero segments (e.g. the merged group's interval was filtered out), the test would still pass, giving a false green. Since the two merged clicks form an interval of `[0.6s, 6.8s]` (duration 6.2s), which comfortably exceeds `min_segment_duration = 1.0s`, exactly 1 segment should always be produced. The assertion should be tightened to `== 1`.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. apps/desktop/src-tauri/src/recording.rs, line 2093-2115 (link)

    P2 Dead zone applies to all groups regardless of time, can create very long zoom segments

    The in_dead_zone check iterates over all existing click_groups with no temporal filter. This means that if a user clicks at position (0.5, 0.5) at t=0s, clicks somewhere else for a long stretch, and then clicks at (0.51, 0.51) at t=120s, the second click will merge into the very first group — producing a single zoom segment spanning over 2 minutes once the post-padding is applied.

    The PR description calls this intentional ("regardless of time gap"), but in a long recording this silently creates a single enormous zoom segment from the union of [group_start − click_pre_padding, last_click + click_post_padding], which is almost certainly not what the user expects.

    A practical guard would be to bound the dead zone merge by the same click_group_time_threshold used for normal spatial grouping, or by some separate configurable dead zone time limit (e.g. dead_zone_time_limit_secs). At minimum the behaviour should be documented in a comment so future reviewers understand the intentional "any time gap" design.

    // Consider adding a temporal guard, e.g.:
    let time_ok_for_dead_zone = dead_zone_time_limit == 0.0
        || (click_time - clicks[*group.last().unwrap()].time_ms / 1000.0).abs()
            < dead_zone_time_limit;
    
    let in_dead_zone = time_ok_for_dead_zone
        && dead_zone_radius > 0.0
        && click_pos.is_some()
        && { /* centroid check */ };
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/recording.rs
    Line: 2093-2115
    
    Comment:
    **Dead zone applies to all groups regardless of time, can create very long zoom segments**
    
    The `in_dead_zone` check iterates over **all** existing `click_groups` with no temporal filter. This means that if a user clicks at position (0.5, 0.5) at t=0s, clicks somewhere else for a long stretch, and then clicks at (0.51, 0.51) at t=120s, the second click will merge into the very first group — producing a single zoom segment spanning over 2 minutes once the post-padding is applied.
    
    The PR description calls this intentional ("regardless of time gap"), but in a long recording this silently creates a single enormous zoom segment from the union of `[group_start − click_pre_padding, last_click + click_post_padding]`, which is almost certainly not what the user expects.
    
    A practical guard would be to bound the dead zone merge by the same `click_group_time_threshold` used for normal spatial grouping, or by some separate configurable dead zone time limit (e.g. `dead_zone_time_limit_secs`). At minimum the behaviour should be documented in a comment so future reviewers understand the intentional "any time gap" design.
    
    ```rust
    // Consider adding a temporal guard, e.g.:
    let time_ok_for_dead_zone = dead_zone_time_limit == 0.0
        || (click_time - clicks[*group.last().unwrap()].time_ms / 1000.0).abs()
            < dead_zone_time_limit;
    
    let in_dead_zone = time_ok_for_dead_zone
        && dead_zone_radius > 0.0
        && click_pos.is_some()
        && { /* centroid check */ };
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/recording.rs
Line: 2571-2580

Comment:
**Weak assertion allows a false pass on zero segments**

The assertion `segments.len() <= 1` also passes when `segments` is empty (0 segments). This means if the dead zone merge logic silently failed to group the two nearby clicks but still produced zero segments (e.g. the merged group's interval was filtered out), the test would still pass, giving a false green. Since the two merged clicks form an interval of `[0.6s, 6.8s]` (duration 6.2s), which comfortably exceeds `min_segment_duration = 1.0s`, exactly 1 segment should always be produced. The assertion should be tightened to `== 1`.

```suggestion
        assert_eq!(
            segments.len(),
            1,
            "nearby clicks within dead zone should merge into exactly 1 segment, got {}",
            segments.len()
        );
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/recording.rs
Line: 2093-2115

Comment:
**Dead zone applies to all groups regardless of time, can create very long zoom segments**

The `in_dead_zone` check iterates over **all** existing `click_groups` with no temporal filter. This means that if a user clicks at position (0.5, 0.5) at t=0s, clicks somewhere else for a long stretch, and then clicks at (0.51, 0.51) at t=120s, the second click will merge into the very first group — producing a single zoom segment spanning over 2 minutes once the post-padding is applied.

The PR description calls this intentional ("regardless of time gap"), but in a long recording this silently creates a single enormous zoom segment from the union of `[group_start − click_pre_padding, last_click + click_post_padding]`, which is almost certainly not what the user expects.

A practical guard would be to bound the dead zone merge by the same `click_group_time_threshold` used for normal spatial grouping, or by some separate configurable dead zone time limit (e.g. `dead_zone_time_limit_secs`). At minimum the behaviour should be documented in a comment so future reviewers understand the intentional "any time gap" design.

```rust
// Consider adding a temporal guard, e.g.:
let time_ok_for_dead_zone = dead_zone_time_limit == 0.0
    || (click_time - clicks[*group.last().unwrap()].time_ms / 1000.0).abs()
        < dead_zone_time_limit;

let in_dead_zone = time_ok_for_dead_zone
    && dead_zone_radius > 0.0
    && click_pos.is_some()
    && { /* centroid check */ };
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/recording.rs
Line: 2101-2108

Comment:
**Centroid drift: repeated near-boundary clicks can shift the dead zone across the screen**

The centroid used for the dead zone check is recomputed as the average of **all** positions already in the group, including those added via previous dead zone merges. Each time a click near the boundary of the dead zone is accepted, the centroid shifts slightly in that direction, which may then accept the next click that is slightly further away, and so on.

Concretely with `dead_zone_radius = 0.1`:
- Click at `(0.50, 0.50)` → centroid `(0.50, 0.50)`
- Click at `(0.56, 0.56)` (dist ≈ 0.085 < 0.1) → merges, centroid shifts to `(0.53, 0.53)`
- Click at `(0.62, 0.62)` (dist from new centroid ≈ 0.127 > 0.1) — just misses in this case, but with slightly smaller steps the group can creep across the screen.

For the typical use case (same button clicked repeatedly) this is harmless, but a user doing a slow drag-with-pauses could inadvertently merge distant clicks into one group. One mitigation is to compute the centroid only from the **first** click in the group (i.e. the "anchor"), rather than from the running average of all members.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "feat(recording): add..."

Greptile also left 1 inline comment on this PR.

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

Comment on lines +2101 to +2108
} else {
let count = group_positions.len() as f64;
let centroid_x = group_positions.iter().map(|(x, _)| x).sum::<f64>() / count;
let centroid_y = group_positions.iter().map(|(_, y)| y).sum::<f64>() / count;
let dx = cx - centroid_x;
let dy = cy - centroid_y;
(dx * dx + dy * dy).sqrt() < dead_zone_radius
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Centroid drift: repeated near-boundary clicks can shift the dead zone across the screen

The centroid used for the dead zone check is recomputed as the average of all positions already in the group, including those added via previous dead zone merges. Each time a click near the boundary of the dead zone is accepted, the centroid shifts slightly in that direction, which may then accept the next click that is slightly further away, and so on.

Concretely with dead_zone_radius = 0.1:

  • Click at (0.50, 0.50) → centroid (0.50, 0.50)
  • Click at (0.56, 0.56) (dist ≈ 0.085 < 0.1) → merges, centroid shifts to (0.53, 0.53)
  • Click at (0.62, 0.62) (dist from new centroid ≈ 0.127 > 0.1) — just misses in this case, but with slightly smaller steps the group can creep across the screen.

For the typical use case (same button clicked repeatedly) this is harmless, but a user doing a slow drag-with-pauses could inadvertently merge distant clicks into one group. One mitigation is to compute the centroid only from the first click in the group (i.e. the "anchor"), rather than from the running average of all members.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/recording.rs
Line: 2101-2108

Comment:
**Centroid drift: repeated near-boundary clicks can shift the dead zone across the screen**

The centroid used for the dead zone check is recomputed as the average of **all** positions already in the group, including those added via previous dead zone merges. Each time a click near the boundary of the dead zone is accepted, the centroid shifts slightly in that direction, which may then accept the next click that is slightly further away, and so on.

Concretely with `dead_zone_radius = 0.1`:
- Click at `(0.50, 0.50)` → centroid `(0.50, 0.50)`
- Click at `(0.56, 0.56)` (dist ≈ 0.085 < 0.1) → merges, centroid shifts to `(0.53, 0.53)`
- Click at `(0.62, 0.62)` (dist from new centroid ≈ 0.127 > 0.1) — just misses in this case, but with slightly smaller steps the group can creep across the screen.

For the typical use case (same button clicked repeatedly) this is harmless, but a user doing a slow drag-with-pauses could inadvertently merge distant clicks into one group. One mitigation is to compute the centroid only from the **first** click in the group (i.e. the "anchor"), rather than from the running average of all members.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant