From 54fb20d343d0156165fe84d499e9b9853f7d36d8 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:13:03 -0300 Subject: [PATCH 01/11] fix(visual): anchor Extension and OTRACK guides to acquired ray direction --- src/app/view/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 37f166fe2..021f666e1 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -935,11 +935,17 @@ bg={bg_ms:.1}ms n={view_count}" // both are drawn — one guide alone hides what the point is (#1313). let otrack_lines: Vec<(iced::Point, iced::Point)> = match otrack_proj { Some((view_rot, eye, ob)) => { - let a = ost_project(tab.last_cursor_world, view_rot, eye, ob); self.otrack_active .into_iter() .chain(self.otrack_cross) - .filter_map(|(base, _dir)| { + .filter_map(|(base, dir)| { + // Draw the acquired geometrical ray, not the line + // from its origin to the moving cursor. The latter + // rotates slightly as the pointer moves away from + // the exact ray, making the Extension guide appear + // to slide across its acquired endpoint. + let direction = dir.try_normalize()?; + let a = ost_project(base + direction, view_rot, eye, ob); let b = ost_project(base, view_rot, eye, ob); (b.x.is_finite() && b.y.is_finite() From 4ad00fad335ac95a67e887121de8d322a5907001 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:24:28 -0300 Subject: [PATCH 02/11] fix(visual): keep Extension snap guide on acquired OTRACK ray --- src/ui/overlay.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 3a5103c17..bda4be947 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -1919,10 +1919,26 @@ impl canvas::Program for SelectionCanvas { // toward the snap point, so the guide line and the three // dots follow the actual extension path (#238). let dir = self.snap_ext_base.and_then(|base| { + // Reuse the acquired tracking ray when its origin + // matches this Extension base. Deriving a new ray + // from the moving snap marker makes the short OSNAP + // guide rotate even while OTRACK stays stable. + let acquired = self.otrack_lines.iter().find_map(|&(origin, tip)| { + let offset = (origin.x - base.x).hypot(origin.y - base.y); + if offset > 2.0 { + return None; + } + let dx = tip.x - origin.x; + let dy = tip.y - origin.y; + let len = dx.hypot(dy); + (len > 1e-3).then_some(Point::new(dx / len, dy / len)) + }); let dx = sp.x - base.x; let dy = sp.y - base.y; - let len = (dx * dx + dy * dy).sqrt(); - (len > 1e-3).then(|| (base, Point::new(dx / len, dy / len))) + let len = dx.hypot(dy); + acquired + .or_else(|| (len > 1e-3).then(|| Point::new(dx / len, dy / len))) + .map(|u| (base, u)) }); // Dashed guide line from that endpoint, through the snap // point and a little beyond, so the extension path is @@ -1935,7 +1951,11 @@ impl canvas::Program for SelectionCanvas { }, ..canvas::Stroke::default().with_color(marker).with_width(1.0) }; - let tip = Point::new(sp.x + u.x * 18.0, sp.y + u.y * 18.0); + let along = (sp.x - base.x) * u.x + (sp.y - base.y) * u.y; + let tip = Point::new( + base.x + u.x * (along + 18.0), + base.y + u.y * (along + 18.0), + ); // `base` is a projected entity endpoint: clip before // stroking (see `clip_seg`). if let Some((a, b)) = clip_seg(base, tip, bounds) { From e5e08a5a121b54276be326ee5302d04232ae7e22 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:45:40 -0300 Subject: [PATCH 03/11] fix(visual): project acquired Extension rays with a stable screen baseline --- src/app/view/mod.rs | 49 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 021f666e1..71a09aa82 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -914,6 +914,45 @@ bg={bg_ms:.1}ms n={view_count}" ob.y + (1.0 - ndc.y) * 0.5 * ob.height, ) }; + // Project a *ray* using a screen-space baseline long enough to + // survive f32 pixel rounding. Projecting base + one world unit can + // produce a subpixel delta at low zoom: normalizing that delta in + // the overlay magnifies the rounding error into a visibly wandering + // guide, even when the acquired world-space ray is fixed. + let ost_project_ray = |origin: glam::DVec3, + dir: glam::DVec3, + view_rot: glam::Mat4, + eye: glam::DVec3, + ob: iced::Rectangle| + -> Option<(iced::Point, iced::Point)> { + let b = ost_project(origin, view_rot, eye, ob); + if !b.x.is_finite() || !b.y.is_finite() { + return None; + } + let unit = dir.try_normalize()?; + let mut distance = 1.0_f64; + for _ in 0..24 { + let tip = ost_project(origin + unit * distance, view_rot, eye, ob); + let length = (tip.x - b.x).hypot(tip.y - b.y); + if length.is_finite() && length >= 64.0 { + return Some((b, tip)); + } + distance *= 2.0; + } + None + }; + // OSNAP Extension is also active with F11 / OTRACK turned off. + // Give its marker the actual acquired geometric direction instead + // of reconstructing a new ray from the moving snap cursor. + let snap_ext_dir: Option = tab + .snap_result + .filter(|hit| hit.snap_type == crate::snap::SnapType::Extension) + .and_then(|hit| hit.extension_origin.zip(hit.extension_dir)) + .and_then(|(origin, dir)| { + let (rot, eye, rect) = otrack_proj?; + let (base, tip) = ost_project_ray(origin, dir, rot, eye, rect)?; + Some(iced::Point::new(tip.x - base.x, tip.y - base.y)) + }); let ost_points: Vec = if let Some((view_rot, eye, ob)) = otrack_proj { self.snapper @@ -944,14 +983,7 @@ bg={bg_ms:.1}ms n={view_count}" // rotates slightly as the pointer moves away from // the exact ray, making the Extension guide appear // to slide across its acquired endpoint. - let direction = dir.try_normalize()?; - let a = ost_project(base + direction, view_rot, eye, ob); - let b = ost_project(base, view_rot, eye, ob); - (b.x.is_finite() - && b.y.is_finite() - && a.x.is_finite() - && a.y.is_finite()) - .then_some((b, a)) + ost_project_ray(base, dir, view_rot, eye, ob) }) .collect() } @@ -1184,6 +1216,7 @@ bg={bg_ms:.1}ms n={view_count}" snap_info, snap_ext_base, snap_ext_base2, + snap_ext_dir, grips, control_polygon, grip_clip, From 93abccc6ed1cd02f7909136b6a18db48bb1621a9 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:46:29 -0300 Subject: [PATCH 04/11] fix(visual): anchor Extension marker to captured ray even without OTRACK --- src/ui/overlay.rs | 112 +++++++++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 36 deletions(-) diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index bda4be947..8d2dec469 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -783,6 +783,62 @@ fn clip_seg(p0: Point, p1: Point, bounds: iced::Rectangle) -> Option<(Point, Poi )) } +/// Build a snap-guide end on the acquired screen ray, not on the noisy +/// cursor-to-base line. Returns the normalized ray and its forward tip. +fn extension_guide_tip( + base: Point, + snapped: Point, + acquired_direction: Option, +) -> Option<(Point, Point)> { + let delta = acquired_direction.unwrap_or(Point::new( + snapped.x - base.x, + snapped.y - base.y, + )); + let len = delta.x.hypot(delta.y); + if !len.is_finite() || len <= 1e-3 { + return None; + } + let u = Point::new(delta.x / len, delta.y / len); + let along = (snapped.x - base.x) * u.x + (snapped.y - base.y) * u.y; + let tip = Point::new( + base.x + u.x * (along + 18.0), + base.y + u.y * (along + 18.0), + ); + Some((u, tip)) +} + +#[cfg(test)] +mod extension_guide_tests { + use super::*; + + #[test] + fn acquired_extension_guide_stays_on_its_origin_when_cursor_wanders() { + let base = Point::new(100.0, 200.0); + for snap in [ + Point::new(140.0, 202.0), + Point::new(175.0, 193.0), + Point::new(240.0, 215.0), + ] { + let (unit, tip) = + extension_guide_tip(base, snap, Some(Point::new(300.0, 0.0))).unwrap(); + assert!((unit.y).abs() < 1e-5); + assert!((tip.y - base.y).abs() < 1e-5); + assert!((tip.x - (snap.x + 18.0)).abs() < 1e-5); + } + } + + #[test] + fn extension_guide_fallback_keeps_original_behavior_without_acquired_ray() { + let base = Point::new(100.0, 200.0); + let snap = Point::new(130.0, 240.0); + let (u, tip) = extension_guide_tip(base, snap, None).unwrap(); + assert!((u.x - 0.6).abs() < 1e-5); + assert!((u.y - 0.8).abs() < 1e-5); + assert!((tip.x - 140.8).abs() < 1e-4); + assert!((tip.y - 254.4).abs() < 1e-4); + } +} + pub fn compute_grid_step(distance: f32, fov_y: f32, bounds: iced::Rectangle) -> f32 { compute_grid_steps(1.0, 1.0, distance, fov_y, bounds, true).0 } @@ -954,6 +1010,7 @@ pub(crate) fn selection_overlay<'a>( snap: Option<(Point, SnapType)>, snap_ext_base: Option, snap_ext_base2: Option, + snap_ext_dir: Option, grips: Vec, control_polygon: Option<(Vec, bool)>, grip_clip: Option, @@ -989,6 +1046,7 @@ pub(crate) fn selection_overlay<'a>( snap, snap_ext_base, snap_ext_base2, + snap_ext_dir, grips, control_polygon, grip_clip, @@ -1038,6 +1096,7 @@ struct SelectionCanvas { /// Second extension-guide base, present only for an extended intersection so /// both crossing extensions stay drawn when the crossing is caught. (#247, #259) snap_ext_base2: Option, + snap_ext_dir: Option, grips: Vec, /// Selected spline control polygon and closed state. control_polygon: Option<(Vec, bool)>, @@ -1915,35 +1974,23 @@ impl canvas::Program for SelectionCanvas { frame.stroke(&path, stroke); } SnapType::Extension => { - // Unit direction from the endpoint the snap extends from - // toward the snap point, so the guide line and the three - // dots follow the actual extension path (#238). + // The engine provides the exact acquired world ray. + // Its long-baseline projected direction is passed by the + // view and remains stable even with OTRACK (F11) off. let dir = self.snap_ext_base.and_then(|base| { - // Reuse the acquired tracking ray when its origin - // matches this Extension base. Deriving a new ray - // from the moving snap marker makes the short OSNAP - // guide rotate even while OTRACK stays stable. - let acquired = self.otrack_lines.iter().find_map(|&(origin, tip)| { - let offset = (origin.x - base.x).hypot(origin.y - base.y); - if offset > 2.0 { - return None; - } - let dx = tip.x - origin.x; - let dy = tip.y - origin.y; - let len = dx.hypot(dy); - (len > 1e-3).then_some(Point::new(dx / len, dy / len)) + let acquired = self.snap_ext_dir.or_else(|| { + self.otrack_lines.iter().find_map(|&(origin, tip)| { + let offset = (origin.x - base.x).hypot(origin.y - base.y); + (offset <= 2.0).then_some(Point::new( + tip.x - origin.x, + tip.y - origin.y, + )) + }) }); - let dx = sp.x - base.x; - let dy = sp.y - base.y; - let len = dx.hypot(dy); - acquired - .or_else(|| (len > 1e-3).then(|| Point::new(dx / len, dy / len))) - .map(|u| (base, u)) + extension_guide_tip(base, sp, acquired) + .map(|(unit, tip)| (base, unit, tip)) }); - // Dashed guide line from that endpoint, through the snap - // point and a little beyond, so the extension path is - // visible as the cursor tracks along it. - if let Some((base, u)) = dir { + if let Some((base, _, tip)) = dir { let dash = canvas::Stroke { line_dash: canvas::LineDash { segments: &[4.0, 4.0], @@ -1951,20 +1998,13 @@ impl canvas::Program for SelectionCanvas { }, ..canvas::Stroke::default().with_color(marker).with_width(1.0) }; - let along = (sp.x - base.x) * u.x + (sp.y - base.y) * u.y; - let tip = Point::new( - base.x + u.x * (along + 18.0), - base.y + u.y * (along + 18.0), - ); - // `base` is a projected entity endpoint: clip before - // stroking (see `clip_seg`). if let Some((a, b)) = clip_seg(base, tip, bounds) { frame.stroke(&canvas::Path::line(a, b), dash); } } - // Three dots at the snap point, strung along the extension - // direction (horizontal fallback when the base is unknown). - let u = dir.map(|(_, u)| u).unwrap_or(Point::new(1.0, 0.0)); + // Three dots stay aligned with the *same* acquired ray + // used by the dashed guide (rather than cursor jitter). + let u = dir.map(|(_, u, _)| u).unwrap_or(Point::new(1.0, 0.0)); let r = 1.4_f32; for k in [-7.0_f32, 0.0, 7.0] { let dot = From 3f056ecd9d385cc1bb1854aaab87b17129179b33 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:13:14 -0300 Subject: [PATCH 05/11] fix(osnap): offset Extension guide origins into canvas coordinates --- src/app/update/viewport.rs | 74 ++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 1b3b5008f..83987266d 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -31,6 +31,56 @@ const UCS_ICON_PICK_PX: f32 = 10.0; /// Pixel half-width for clicking an icon arm (line segment). const UCS_ICON_ARM_PX: f32 = 6.0; +/// Snapper coordinates are local to the active pane, while the overlay +/// consumes canvas coordinates. Apply the same translation to the visible +/// snap glyph and both Extension guide origins. +fn snap_pixels_to_canvas(hit: &mut crate::snap::SnapResult, pane: iced::Rectangle) { + let translate = |point: &mut Point| { + point.x += pane.x; + point.y += pane.y; + }; + translate(&mut hit.screen); + if let Some(base) = hit.extension_base.as_mut() { + translate(base); + } + if let Some(base) = hit.extension_base2.as_mut() { + translate(base); + } +} + +#[cfg(test)] +mod extension_canvas_tests { + use super::*; + + #[test] + fn translates_extension_anchors_with_the_snap_marker() { + let mut hit = crate::snap::SnapResult { + world: glam::DVec3::ZERO, + screen: Point::new(150.0, 75.0), + snap_type: crate::snap::SnapType::Extension, + tangent_obj: None, + extension_base: Some(Point::new(100.0, 150.0)), + extension_base2: Some(Point::new(75.0, 200.0)), + extension_origin: None, + extension_dir: None, + viewport: None, + source: None, + secondary_source: None, + model_point: None, + }; + let pane = iced::Rectangle { + x: 32.0, + y: 56.0, + width: 600.0, + height: 400.0, + }; + snap_pixels_to_canvas(&mut hit, pane); + assert_eq!(hit.screen, Point::new(182.0, 131.0)); + assert_eq!(hit.extension_base, Some(Point::new(132.0, 206.0))); + assert_eq!(hit.extension_base2, Some(Point::new(107.0, 256.0))); + } +} + fn pt_pt_d2(a: Point, b: Point) -> f32 { (a.x - b.x).powi(2) + (a.y - b.y).powi(2) } @@ -1845,8 +1895,7 @@ impl OpenCADStudio { snap_hit.map(|s| s.world).unwrap_or(raw) }; if let Some(s) = self.tabs[i].snap_result.as_mut() { - s.screen.x += tile_b.x; - s.screen.y += tile_b.y; + snap_pixels_to_canvas(s, tile_b); } if axis_lock.is_none() @@ -2890,11 +2939,11 @@ impl OpenCADStudio { } } - // Snap glyph is positioned in canvas space; shift the - // tile-local snap screen point back to the full canvas. + // Move Extension's acquired endpoints along with the snap glyph. + // Previously only s.screen moved to canvas pixels, leaving the + // dashed Extension line offset from the actual endpoint. if let Some(s) = self.tabs[i].snap_result.as_mut() { - s.screen.x += tile_b.x; - s.screen.y += tile_b.y; + snap_pixels_to_canvas(s, tile_b); } // Give the command the current UCS + Ctrl state before it @@ -3428,18 +3477,7 @@ impl OpenCADStudio { let world = snap_hit.map(|s| s.world).unwrap_or(raw); self.tabs[i].snap_result = snap_hit; if let Some(s) = self.tabs[i].snap_result.as_mut() { - // Snap marker and its extension anchors are pane-local; lift them to - // absolute canvas px. - s.screen.x += tile_b.x; - s.screen.y += tile_b.y; - if let Some(base) = s.extension_base.as_mut() { - base.x += tile_b.x; - base.y += tile_b.y; - } - if let Some(base) = s.extension_base2.as_mut() { - base.x += tile_b.x; - base.y += tile_b.y; - } + snap_pixels_to_canvas(s, tile_b); } use codec::types::Vector3; From 6792c7265cdca8b636b2f034ecff4d0516386b30 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:16:45 -0300 Subject: [PATCH 06/11] fix(visual): anchor Extension at the acquired world endpoint projection --- src/app/view/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 71a09aa82..4588fad6e 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -941,6 +941,21 @@ bg={bg_ms:.1}ms n={view_count}" } None }; + // Draw the Extension start at the actual acquired *world* point, + // projected through the same camera/rect as the entity and guides. + // The snap engine's screen-space base is only pane-local and can + // differ from the final canvas coordinates used by the overlay. + // Keep that older value as a fallback for snaps without origin. + let snap_ext_base = tab + .snap_result + .filter(|hit| hit.snap_type == crate::snap::SnapType::Extension) + .and_then(|hit| hit.extension_origin) + .and_then(|origin| { + let (rot, eye, rect) = otrack_proj?; + let pixel = ost_project(origin, rot, eye, rect); + (pixel.x.is_finite() && pixel.y.is_finite()).then_some(pixel) + }) + .or(snap_ext_base); // OSNAP Extension is also active with F11 / OTRACK turned off. // Give its marker the actual acquired geometric direction instead // of reconstructing a new ray from the moving snap cursor. From d96476187f70dd83f389a3fffe5e6a3696443f9c Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:34:50 -0300 Subject: [PATCH 07/11] diagnostic: trace Extension anchor coordinates on demand --- src/app/view/mod.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 4588fad6e..46526a39f 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -968,6 +968,30 @@ bg={bg_ms:.1}ms n={view_count}" let (base, tip) = ost_project_ray(origin, dir, rot, eye, rect)?; Some(iced::Point::new(tip.x - base.x, tip.y - base.y)) }); + // Temporary visual diagnostic for the Extension-origin drift. + // Enable explicitly with OCS_EXT_DIAG=1; normal rendering is unchanged. + #[cfg(not(target_arch = "wasm32"))] + if std::env::var_os("OCS_EXT_DIAG").is_some() { + static SAMPLES: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + if let Some(hit) = tab + .snap_result + .filter(|hit| hit.snap_type == crate::snap::SnapType::Extension) + { + if SAMPLES.fetch_add(1, std::sync::atomic::Ordering::Relaxed) % 12 == 0 { + eprintln!( + "[EXTDIAG] screen={:?} engine_base={:?} drawn_base={:?} origin_world={:?} snap_world={:?} dir_world={:?} captured_points={:?}", + hit.screen, + hit.extension_base, + snap_ext_base, + hit.extension_origin, + hit.world, + hit.extension_dir, + self.snapper.tracking_points + ); + } + } + } let ost_points: Vec = if let Some((view_rot, eye, ob)) = otrack_proj { self.snapper From 903501d7b6913938e1b18f13478faff4c425fe03 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:35:02 -0300 Subject: [PATCH 08/11] diagnostic: show Extension base and solid guide on demand --- src/ui/overlay.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 8d2dec469..a084779a8 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -2002,6 +2002,25 @@ impl canvas::Program for SelectionCanvas { frame.stroke(&canvas::Path::line(a, b), dash); } } + // Diagnostic only: green dot = the actual guide + // origin given to the canvas; magenta = complete solid + // ray. This distinguishes a moving base from a dashed + // stroke/rendering issue without changing normal mode. + #[cfg(not(target_arch = "wasm32"))] + if std::env::var_os("OCS_EXT_DIAG").is_some() { + if let Some((base, _, tip)) = dir { + frame.stroke( + &canvas::Path::line(base, tip), + canvas::Stroke::default() + .with_color(Color::from_rgb(1.0, 0.0, 1.0)) + .with_width(1.0), + ); + frame.fill( + &canvas::Path::circle(base, 5.0), + Color::from_rgb(0.0, 1.0, 0.0), + ); + } + } // Three dots stay aligned with the *same* acquired ray // used by the dashed guide (rather than cursor jitter). let u = dir.map(|(_, u, _)| u).unwrap_or(Point::new(1.0, 0.0)); From 363534bb5c2665843e19909fafce3cf3c52f2bc4 Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:57:47 -0300 Subject: [PATCH 09/11] fix(visual): exclude Extension and intersection guides from SNAPMARKERSIZE transform --- src/ui/overlay.rs | 148 ++++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 76 deletions(-) diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index a084779a8..fb4dd80bc 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -1761,6 +1761,73 @@ impl canvas::Program for SelectionCanvas { style: canvas::Style::Solid(marker), ..Default::default() }; + // Construction guides are canvas-space geometry, NOT snap glyphs. + // They must be drawn before the SNAPMARKERSIZE transform below. + // Scaling a guide around the moving snap position makes a fixed + // acquired endpoint visibly slide as the cursor moves. + let extension_ray = self.snap_ext_base.and_then(|base| { + let acquired = self.snap_ext_dir.or_else(|| { + self.otrack_lines.iter().find_map(|&(origin, tip)| { + let offset = (origin.x - base.x).hypot(origin.y - base.y); + (offset <= 2.0) + .then_some(Point::new(tip.x - origin.x, tip.y - origin.y)) + }) + }); + extension_guide_tip(base, sp, acquired) + .map(|(unit, tip)| (base, unit, tip)) + }); + let guide_dash = canvas::Stroke { + line_dash: canvas::LineDash { + segments: &[4.0, 4.0], + offset: 0, + }, + ..canvas::Stroke::default().with_color(marker).with_width(1.0) + }; + match snap_type { + SnapType::Extension => { + if let Some((base, _, tip)) = extension_ray { + if let Some((a, b)) = clip_seg(base, tip, bounds) { + frame.stroke(&canvas::Path::line(a, b), guide_dash.clone()); + } + #[cfg(not(target_arch = "wasm32"))] + if std::env::var_os("OCS_EXT_DIAG").is_some() { + frame.stroke( + &canvas::Path::line(base, tip), + canvas::Stroke::default() + .with_color(Color::from_rgb(1.0, 0.0, 1.0)) + .with_width(1.0), + ); + frame.fill( + &canvas::Path::circle(base, 5.0), + Color::from_rgb(0.0, 1.0, 0.0), + ); + } + } + } + SnapType::Intersection => { + // Extended-intersection guide origins must also remain + // fixed when SNAPMARKERSIZE changes. + for base in [self.snap_ext_base, self.snap_ext_base2] + .into_iter() + .flatten() + { + let dx = sp.x - base.x; + let dy = sp.y - base.y; + let len = dx.hypot(dy); + if len > 1e-3 { + let tip = Point::new( + sp.x + dx / len * 18.0, + sp.y + dy / len * 18.0, + ); + if let Some((a, b)) = clip_seg(base, tip, bounds) { + frame.stroke(&canvas::Path::line(a, b), guide_dash.clone()); + } + } + } + } + _ => {} + } + // SNAPMARKERSIZE scales the glyphs drawn for the default 5 px. let scale = self.crosshair.snap_size.clamp(1.0, 20.0) / 5.0; frame.with_save(|frame| { @@ -1858,35 +1925,8 @@ impl canvas::Program for SelectionCanvas { frame.stroke(&path, stroke); } SnapType::Intersection => { - // An extended intersection sets one or two extension bases; - // draw a dashed guide from each endpoint through the crossing - // so both contributing extension paths stay visible. A real - // on-segment crossing carries no bases, so nothing draws here. - // (#247, #259) - for base in [self.snap_ext_base, self.snap_ext_base2] - .into_iter() - .flatten() - { - let dx = sp.x - base.x; - let dy = sp.y - base.y; - let len = (dx * dx + dy * dy).sqrt(); - if len > 1e-3 { - let dash = canvas::Stroke { - line_dash: canvas::LineDash { - segments: &[4.0, 4.0], - offset: 0, - }, - ..canvas::Stroke::default().with_color(marker).with_width(1.0) - }; - let tip = - Point::new(sp.x + dx / len * 18.0, sp.y + dy / len * 18.0); - // `base` is a projected entity endpoint: clip before - // stroking (see `clip_seg`). - if let Some((a, b)) = clip_seg(base, tip, bounds) { - frame.stroke(&canvas::Path::line(a, b), dash); - } - } - } + // The guide is drawn unscaled above; only the X glyph + // belongs inside the SNAPMARKERSIZE transform. let r = 5.0_f32; let p1 = canvas::Path::new(|b| { b.move_to(Point::new(sp.x - r, sp.y - r)); @@ -1974,53 +2014,9 @@ impl canvas::Program for SelectionCanvas { frame.stroke(&path, stroke); } SnapType::Extension => { - // The engine provides the exact acquired world ray. - // Its long-baseline projected direction is passed by the - // view and remains stable even with OTRACK (F11) off. - let dir = self.snap_ext_base.and_then(|base| { - let acquired = self.snap_ext_dir.or_else(|| { - self.otrack_lines.iter().find_map(|&(origin, tip)| { - let offset = (origin.x - base.x).hypot(origin.y - base.y); - (offset <= 2.0).then_some(Point::new( - tip.x - origin.x, - tip.y - origin.y, - )) - }) - }); - extension_guide_tip(base, sp, acquired) - .map(|(unit, tip)| (base, unit, tip)) - }); - if let Some((base, _, tip)) = dir { - let dash = canvas::Stroke { - line_dash: canvas::LineDash { - segments: &[4.0, 4.0], - offset: 0, - }, - ..canvas::Stroke::default().with_color(marker).with_width(1.0) - }; - if let Some((a, b)) = clip_seg(base, tip, bounds) { - frame.stroke(&canvas::Path::line(a, b), dash); - } - } - // Diagnostic only: green dot = the actual guide - // origin given to the canvas; magenta = complete solid - // ray. This distinguishes a moving base from a dashed - // stroke/rendering issue without changing normal mode. - #[cfg(not(target_arch = "wasm32"))] - if std::env::var_os("OCS_EXT_DIAG").is_some() { - if let Some((base, _, tip)) = dir { - frame.stroke( - &canvas::Path::line(base, tip), - canvas::Stroke::default() - .with_color(Color::from_rgb(1.0, 0.0, 1.0)) - .with_width(1.0), - ); - frame.fill( - &canvas::Path::circle(base, 5.0), - Color::from_rgb(0.0, 1.0, 0.0), - ); - } - } + // Only the three-dot marker is scaled. The actual + // guide and diagnostic dot are drawn in canvas space. + let dir = extension_ray; // Three dots stay aligned with the *same* acquired ray // used by the dashed guide (rather than cursor jitter). let u = dir.map(|(_, u, _)| u).unwrap_or(Point::new(1.0, 0.0)); From de990b5a1a601444a94a6136dfae80f614e662fa Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:07:10 -0300 Subject: [PATCH 10/11] chore(visual): remove temporary Extension anchor logging --- src/app/view/mod.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 46526a39f..4588fad6e 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -968,30 +968,6 @@ bg={bg_ms:.1}ms n={view_count}" let (base, tip) = ost_project_ray(origin, dir, rot, eye, rect)?; Some(iced::Point::new(tip.x - base.x, tip.y - base.y)) }); - // Temporary visual diagnostic for the Extension-origin drift. - // Enable explicitly with OCS_EXT_DIAG=1; normal rendering is unchanged. - #[cfg(not(target_arch = "wasm32"))] - if std::env::var_os("OCS_EXT_DIAG").is_some() { - static SAMPLES: std::sync::atomic::AtomicUsize = - std::sync::atomic::AtomicUsize::new(0); - if let Some(hit) = tab - .snap_result - .filter(|hit| hit.snap_type == crate::snap::SnapType::Extension) - { - if SAMPLES.fetch_add(1, std::sync::atomic::Ordering::Relaxed) % 12 == 0 { - eprintln!( - "[EXTDIAG] screen={:?} engine_base={:?} drawn_base={:?} origin_world={:?} snap_world={:?} dir_world={:?} captured_points={:?}", - hit.screen, - hit.extension_base, - snap_ext_base, - hit.extension_origin, - hit.world, - hit.extension_dir, - self.snapper.tracking_points - ); - } - } - } let ost_points: Vec = if let Some((view_rot, eye, ob)) = otrack_proj { self.snapper From 002cdd06853cda9e0737f401581895318643406a Mon Sep 17 00:00:00 2001 From: gianlucafiore <50465150+gianlucafiore@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:07:22 -0300 Subject: [PATCH 11/11] chore(visual): remove temporary Extension guide markers --- src/ui/overlay.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index fb4dd80bc..c3bfb1209 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -1789,19 +1789,6 @@ impl canvas::Program for SelectionCanvas { if let Some((a, b)) = clip_seg(base, tip, bounds) { frame.stroke(&canvas::Path::line(a, b), guide_dash.clone()); } - #[cfg(not(target_arch = "wasm32"))] - if std::env::var_os("OCS_EXT_DIAG").is_some() { - frame.stroke( - &canvas::Path::line(base, tip), - canvas::Stroke::default() - .with_color(Color::from_rgb(1.0, 0.0, 1.0)) - .with_width(1.0), - ); - frame.fill( - &canvas::Path::circle(base, 5.0), - Color::from_rgb(0.0, 1.0, 0.0), - ); - } } } SnapType::Intersection => {