From 6a767390dd24964f421a253cdb29cc970bffebdb Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:02:21 -0300 Subject: [PATCH 1/6] fix(otrack): allow Extension tracking intersections while preserving point snap priority --- src/app/update/viewport.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index e8b0198ef..859b5234b 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1846,7 +1846,12 @@ impl OpenCADStudio { if axis_lock.is_none() && otrack_hit.is_none() - && !snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid) + && !snap_hit.is_some_and(|s| { + !matches!( + s.snap_type, + crate::snap::SnapType::Grid | crate::snap::SnapType::Extension + ) + }) { let base = grip.origin_world; let ucs_xf = self.tabs[i].ucs_xform(); @@ -2682,9 +2687,13 @@ impl OpenCADStudio { // Otherwise the endpoint glyph can appear at the // corner while the preview/accepted point follows the // tracking line instead. - !self.tabs[i] - .snap_result - .is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid) + !self.tabs[i].snap_result.is_some_and(|s| { + !matches!( + s.snap_type, + crate::snap::SnapType::Grid + | crate::snap::SnapType::Extension + ) + }) }) { h.aligned } else { From dd3b18e55004968aa5107f733de45efb8dcfce1a Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:02:39 -0300 Subject: [PATCH 2/6] docs: clarify extension exception in OTRACK priority guards --- src/app/update/viewport.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 859b5234b..4c819a3e6 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2683,10 +2683,9 @@ impl OpenCADStudio { .unwrap_or(cursor_world); axis_lock_apply(point, base, dir) } else if let Some(h) = otrack_hit.filter(|_| { - // A real object snap must win over tracking alignment. - // Otherwise the endpoint glyph can appear at the - // corner while the preview/accepted point follows the - // tracking line instead. + // Exact object snaps win over tracking alignment, but + // Extension is a construction guide and must remain + // eligible for OTRACK intersections. !self.tabs[i].snap_result.is_some_and(|s| { !matches!( s.snap_type, @@ -4560,8 +4559,8 @@ impl OpenCADStudio { None }; if let Some(h) = otrack.filter(|_| { - // Match the hover path: never override a genuine OSNAP - // endpoint/intersection with a tracking projection. + // Match the hover path: preserve exact object snaps, but + // allow Extension to resolve a crossing with OTRACK. !snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid) }) { pt = h.aligned; From 52e8cd1002acdb4c34c432ccb7c9c9b3cb145734 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:10:08 -0300 Subject: [PATCH 3/6] fix(otrack): commit Extension intersection at tracked crossing on click --- src/app/update/viewport.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 4c819a3e6..14797ccb1 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -4561,8 +4561,15 @@ impl OpenCADStudio { if let Some(h) = otrack.filter(|_| { // Match the hover path: preserve exact object snaps, but // allow Extension to resolve a crossing with OTRACK. - !snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid) + !snap_hit.is_some_and(|s| { + !matches!( + s.snap_type, + crate::snap::SnapType::Grid | crate::snap::SnapType::Extension + ) + }) }) { + // Commit the actual OTRACK intersection, not the cursor + // projection onto Extension. pt = h.aligned; if clamp_world_xy { pt.z = 0.0; From 0004113e0a895918461e83340503f55831e9c027 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:22:09 -0300 Subject: [PATCH 4/6] refine(selection): allow 250ms before a small pointer drag starts --- src/app/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 691b6dcaa..4a6b1550b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -380,7 +380,7 @@ use iced::window; use iced::{mouse, Point, Task, Theme}; use std::sync::Arc; -pub(super) const POLY_START_DELAY_MS: u128 = 150; +pub(super) const POLY_START_DELAY_MS: u128 = 250; /// Squared screen distance (12 px) past which a left press is a drag /// without waiting for `POLY_START_DELAY_MS`. pub(super) const DRAG_START_FAST_PX2: f32 = 144.0; From 8ebe3fc89f097aff41cdd41413581dc2dd389846 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:31:04 -0300 Subject: [PATCH 5/6] fix(selection): prevent incidental mouse motion from starting lasso --- src/app/update/viewport.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 14797ccb1..b3fd05be2 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1323,12 +1323,17 @@ impl OpenCADStudio { .left_press_time .map(|t| Instant::now().duration_since(t).as_millis()) .unwrap_or(u128::MAX); - // The delay keeps a click made while the mouse is still moving - // from turning into a drag; a press that has already travelled - // well past click jitter is a drag at once, or a fast flick - // released inside the delay read as a click. (#1808) - let dragged = - dist2 > 9.0 && (elapsed_ms >= POLY_START_DELAY_MS || dist2 > DRAG_START_FAST_PX2); + // Selection gestures need a wider dead zone than ordinary + // pointer clicks. In particular, a quick press/release with + // residual mouse motion must not start a freeform lasso. + // Keep the existing delay and reserve immediate dragging for + // an unmistakable movement (24 px); after the delay, require + // at least 12 px. Grip editing follows its separate path. + const SELECTION_DRAG_MIN_PX2: f32 = 12.0 * 12.0; + const SELECTION_DRAG_FAST_PX2: f32 = 24.0 * 24.0; + let dragged = dist2 > SELECTION_DRAG_MIN_PX2 + && (elapsed_ms >= POLY_START_DELAY_MS + || dist2 > SELECTION_DRAG_FAST_PX2); if !sel.input.left_dragging && dragged { sel.input.left_dragging = true; if self.pick_drag_rect { From ec44bcdd71123f982111a15bcc1d881aea778678 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:31:16 -0300 Subject: [PATCH 6/6] cleanup: remove unused generic drag threshold import --- src/app/update/viewport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index b3fd05be2..1b3b5008f 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -8,7 +8,7 @@ use crate::app::helpers::{ polar_constrain_if_near, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind, }; -use crate::app::{Message, OpenCADStudio, DRAG_START_FAST_PX2, POLY_START_DELAY_MS}; +use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS}; use crate::modules::ModuleEvent; use crate::scene::model::object::GripApply; use crate::scene::pick::grip::{