From c964e365c3442032858290a0685603434afca181 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 28 Sep 2026 18:32:50 +0300 Subject: [PATCH] fix(snap): parallel snap keeps its reference, base and lock in f64 The reference direction, base point and locked point went through f32, so the drawn line drifted off true parallel. Fixes #1387 Co-Authored-By: Claude Opus 5.5 --- src/app/update/viewport.rs | 8 +++---- src/app/view/mod.rs | 2 +- src/snap.rs | 49 +++++++++++++++++++------------------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 1cf4958cb..1537fc6f5 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2293,7 +2293,7 @@ impl OpenCADStudio { Instant::now(), ); self.snapper.update_parallel( - cursor_world.as_vec3(), + cursor_world, &snap_candidates, view_rot, eye, @@ -2324,8 +2324,8 @@ impl OpenCADStudio { // (#277) if axis_lock.is_none() && self.tabs[i].snap_result.is_none() && otrack_hit.is_none() { if let Some(par) = self.snapper.parallel_snap( - cursor_world.as_vec3(), - self.last_point.map(|p| p.as_vec3()), + cursor_world, + self.last_point, view_rot, eye, bounds, @@ -2333,7 +2333,7 @@ impl OpenCADStudio { if let (Some(base), Some((dir, _))) = (self.last_point, self.snapper.parallel_ref) { - self.otrack_active = Some((base, dir.as_dvec3())); + self.otrack_active = Some((base, dir)); self.otrack_cross = None; } self.tabs[i].snap_result = Some(par); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index a525068ce..95e99ee72 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -789,7 +789,7 @@ bg={bg_ms:.1}ms n={view_count}" let parallel_ref_marker: Option = match (otrack_proj, self.snapper.parallel_ref) { (Some((view_rot, eye, ob)), Some((_, pt))) => { - let s = ost_project(pt.as_dvec3(), view_rot, eye, ob); + let s = ost_project(pt, view_rot, eye, ob); (s.x.is_finite() && s.y.is_finite()).then_some(s) } _ => None, diff --git a/src/snap.rs b/src/snap.rs index 0ef9ead29..dad82e13e 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -252,11 +252,11 @@ pub struct Snapper { /// parallel to this, the point locks onto that parallel line; the point half /// marks the reference on screen. Works with only the Parallel object snap /// on — independent of OTRACK (#277). - pub parallel_ref: Option<(Vec3, Vec3)>, + pub parallel_ref: Option<(DVec3, DVec3)>, /// Dwell state for acquiring/removing `parallel_ref`: the candidate line /// direction + point under the cursor, when it was first hovered, and /// whether this dwell has already fired (so it acquires/toggles once). - parallel_dwell: Option<(Vec3, Vec3, Instant, bool)>, + parallel_dwell: Option<(DVec3, DVec3, Instant, bool)>, /// One-shot snap override (Shift+RMB menu): the (enabled set, snap on) /// pair saved when the override engaged, restored when it is consumed by /// the next point pick or cancelled. While `Some`, `enabled` holds only @@ -1025,7 +1025,7 @@ impl Snapper { /// is undefined. Call on every viewport move. (#277) pub fn update_parallel( &mut self, - cursor_world: Vec3, + cursor_world: DVec3, wires: &W, view_rot: glam::Mat4, eye: glam::DVec3, @@ -1038,7 +1038,7 @@ impl Snapper { return; } const PAR_DWELL_MS: u128 = 150; - let parallel = |a: Vec3, b: Vec3| (a.x * b.x + a.y * b.y).abs() > 0.9998; + let parallel = |a: DVec3, b: DVec3| (a.x * b.x + a.y * b.y).abs() > 0.9998; let Some((dir, pt)) = nearest_segment( cursor_world, wires, @@ -1081,8 +1081,8 @@ impl Snapper { /// of that line). Independent of OTRACK. (#277) pub fn parallel_snap( &self, - cursor_world: Vec3, - base: Option, + cursor_world: DVec3, + base: Option, view_rot: glam::Mat4, eye: glam::DVec3, bounds: iced::Rectangle, @@ -1095,18 +1095,18 @@ impl Snapper { let d = cursor_world - base; // Need a bit of travel from the base, and the cursor must be pulling // roughly along the reference (not backward-only noise near the base). - if (d.x * d.x + d.y * d.y).sqrt() < self.grid_spacing * 0.01 { + if (d.x * d.x + d.y * d.y).sqrt() < self.grid_spacing as f64 * 0.01 { return None; } let t = d.x * dir.x + d.y * dir.y; let locked = base + dir * t; - let sl = world_to_screen(locked.as_dvec3(), view_rot, eye, bounds); - let sc = world_to_screen(cursor_world.as_dvec3(), view_rot, eye, bounds); + let sl = world_to_screen(locked, view_rot, eye, bounds); + let sc = world_to_screen(cursor_world, view_rot, eye, bounds); if dist2(sl, sc) > self.osnap_radius_px * self.osnap_radius_px { return None; // cursor not near the parallel line — don't lock } Some(SnapResult { - world: locked.as_dvec3(), + world: locked, screen: sl, snap_type: SnapType::Parallel, tangent_obj: None, @@ -3475,16 +3475,16 @@ pub(crate) fn foot_on_triangle( /// Center snap hint and "parallel to a curve" is meaningless. Used to acquire /// the Parallel-snap reference. (#277) fn nearest_segment( - cursor_world: Vec3, + cursor_world: DVec3, wires: &W, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle, aperture_px: f32, -) -> Option<(Vec3, Vec3)> { - let cs = world_to_screen(cursor_world.as_dvec3(), view_rot, eye, bounds); +) -> Option<(DVec3, DVec3)> { + let cs = world_to_screen(cursor_world, view_rot, eye, bounds); let mut best_d2 = aperture_px * aperture_px; - let mut best: Option<(Vec3, Vec3)> = None; + let mut best: Option<(DVec3, DVec3)> = None; if let Some(segments) = indexed_segments(wires) { for segment in segments { let Some(wire) = wires.source_wire(segment.wire) else { @@ -3506,9 +3506,8 @@ fn nearest_segment( let l = (dx * dx + dy * dy).sqrt(); if l > 1e-9 { best_d2 = d2; - let dir = Vec3::new((dx / l) as f32, (dy / l) as f32, 0.0); - let np = - nearest_on_segment(cursor_world.as_dvec3(), segment.a, segment.b).as_vec3(); + let dir = DVec3::new(dx / l, dy / l, 0.0); + let np = nearest_on_segment(cursor_world, segment.a, segment.b); best = Some((dir, np)); } } @@ -3538,8 +3537,8 @@ fn nearest_segment( let l = (dx * dx + dy * dy).sqrt(); if l > 1e-9 { best_d2 = d2; - let dir = Vec3::new((dx / l) as f32, (dy / l) as f32, 0.0); - let np = nearest_on_segment(cursor_world.as_dvec3(), a, b).as_vec3(); + let dir = DVec3::new(dx / l, dy / l, 0.0); + let np = nearest_on_segment(cursor_world, a, b); best = Some((dir, np)); } } @@ -3552,16 +3551,16 @@ fn nearest_segment( /// line through `line_pt` along `line_dir`. Used to tell whether a hovered line /// is the acquired parallel reference (same line) regardless of zoom. (#277) fn screen_perp_dist( - q: Vec3, - line_pt: Vec3, - line_dir: Vec3, + q: DVec3, + line_pt: DVec3, + line_dir: DVec3, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle, ) -> f32 { - let sq = world_to_screen(q.as_dvec3(), view_rot, eye, bounds); - let s0 = world_to_screen(line_pt.as_dvec3(), view_rot, eye, bounds); - let s1 = world_to_screen((line_pt + line_dir).as_dvec3(), view_rot, eye, bounds); + let sq = world_to_screen(q, view_rot, eye, bounds); + let s0 = world_to_screen(line_pt, view_rot, eye, bounds); + let s1 = world_to_screen(line_pt + line_dir, view_rot, eye, bounds); let ex = s1.x - s0.x; let ey = s1.y - s0.y; let l = (ex * ex + ey * ey).sqrt();