From ca8d249173d3c435ad6a3c04be0aa43f9854a6eb Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 17 Jun 2026 04:59:06 +0300 Subject: [PATCH] fix(otrack): add 250ms dwell timeout before acquiring a tracking point A tracking point was created as soon as a snap point was highlighted, so moving the mouse across the view spawned accidental tracking points. Acquisition now requires the cursor to rest near the snap point for 250ms (wall-clock) rather than a fixed number of move events. Closes #110 Co-Authored-By: Claude Opus 4.8 --- src/app/update.rs | 8 ++++++-- src/snap/mod.rs | 38 +++++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/app/update.rs b/src/app/update.rs index 4e3c2cdfc..ab822bc5b 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2481,8 +2481,12 @@ impl OpenCADStudio { // distance can place a point along it — issue #69). let otrack_hit = { let snap_world = self.tabs[i].snap_result.map(|s| s.world); - self.snapper - .update_otrack_dwell(snap_world, view_proj, bounds); + self.snapper.update_otrack_dwell( + snap_world, + view_proj, + bounds, + Instant::now(), + ); if self.tabs[i].snap_result.is_none() { let step = if self.polar_mode { Some(self.polar_increment_deg) diff --git a/src/snap/mod.rs b/src/snap/mod.rs index 2d07cc693..9828b9551 100644 --- a/src/snap/mod.rs +++ b/src/snap/mod.rs @@ -5,6 +5,7 @@ //! Extension, Insertion, Perpendicular, Nearest, ApparentIntersection, Grid, Tangent use glam::{Mat4, Vec3}; +use iced::time::Instant; use iced::{Point, Rectangle}; use crate::command::TangentObject; @@ -98,8 +99,10 @@ pub struct Snapper { pub tracking_points: Vec, /// Last snap world position (for dwell detection). pub last_snap_world: Option, - /// How many consecutive moves the cursor has been near last_snap_world. - pub dwell_count: u32, + /// When the cursor first rested near `last_snap_world`. + pub dwell_since: Option, + /// Whether the current dwell already acquired/removed a point (fire once). + pub dwell_acquired: bool, } impl Default for Snapper { @@ -121,7 +124,8 @@ impl Default for Snapper { otrack_enabled: false, tracking_points: Vec::new(), last_snap_world: None, - dwell_count: 0, + dwell_since: None, + dwell_acquired: false, } } } @@ -172,19 +176,25 @@ impl Snapper { snap_world: Option, view_proj: glam::Mat4, bounds: iced::Rectangle, + now: Instant, ) { if !self.otrack_enabled { - self.dwell_count = 0; self.last_snap_world = None; + self.dwell_since = None; + self.dwell_acquired = false; return; } - const DWELL_THRESHOLD: u32 = 4; + // The cursor must rest near a snap point for this long before it is + // acquired, so that brushing past snap points while moving the mouse + // does not create accidental tracking points. + const DWELL_MS: u128 = 250; const DWELL_PX: f32 = 8.0; match snap_world { None => { - self.dwell_count = 0; self.last_snap_world = None; + self.dwell_since = None; + self.dwell_acquired = false; } Some(p) => { // Convert to screen to measure pixel distance. @@ -198,8 +208,11 @@ impl Snapper { false }; if is_same { - self.dwell_count += 1; - if self.dwell_count == DWELL_THRESHOLD { + let elapsed = self + .dwell_since + .map_or(0, |t| now.duration_since(t).as_millis()); + if !self.dwell_acquired && elapsed >= DWELL_MS { + self.dwell_acquired = true; // Dwelling over an already-acquired point removes it; // otherwise acquire it (max 4 tracked points). let existing = self.tracking_points.iter().position(|t| { @@ -219,8 +232,9 @@ impl Snapper { } } } else { - self.dwell_count = 1; self.last_snap_world = Some(p); + self.dwell_since = Some(now); + self.dwell_acquired = false; } } } @@ -291,8 +305,9 @@ impl Snapper { /// Clear all acquired tracking points (e.g. when command ends). pub fn clear_tracking(&mut self) { self.tracking_points.clear(); - self.dwell_count = 0; self.last_snap_world = None; + self.dwell_since = None; + self.dwell_acquired = false; } /// Only runs Tangent snap — used when a command needs object picks via tangent. @@ -317,7 +332,8 @@ impl Snapper { otrack_enabled: false, tracking_points: Vec::new(), last_snap_world: None, - dwell_count: 0, + dwell_since: None, + dwell_acquired: false, }; tmp.snap(cursor_world, cursor_screen, wires, view_proj, bounds) }