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] 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));