From 99fff30c535de2fc6d5165bd51a9e26e13a98b1d Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Wed, 7 Oct 2026 20:02:09 -0300 Subject: [PATCH] fix: preserve perpendicular tracking after typed distance --- src/app/update/command.rs | 6 ++- src/app/update/dynamic.rs | 1 + src/snap.rs | 106 +++++++++++++++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 3a2b0e6d6..96d4c6ea9 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -977,7 +977,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { // A tracking ray carries its own base, so it needs no anchor: that is // how OTRACK and Extension accepted a bare distance for the *first* // point of a command, before any anchor exists. - let pt = if let Some((base, dir)) = self.active_distance_ray(i) { + let distance_ray = self.active_distance_ray(i); + let pt = if let Some((base, dir)) = distance_ray { base + dir * dist } else { let anchor = self.tabs[i] @@ -1010,6 +1011,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.dyn_coord_absolute = false; self.sync_dyn_fields(); self.reset_tracking_after_point(); + if let Some((_, dir)) = distance_ray { + self.snapper.remember_distance_reference(dir); + } self.push_ucs_to_cmd(i); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt))?; diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index 8ac3e1241..c9bf42f23 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -679,6 +679,7 @@ impl OpenCADStudio { self.dyn_coord_absolute = false; self.sync_dyn_fields(); self.reset_tracking_after_point(); + self.snapper.remember_distance_reference(dir); self.push_ucs_to_cmd(i); let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)); let task = result.map(|r| self.apply_cmd_result(r))?; diff --git a/src/snap.rs b/src/snap.rs index ab59a5f59..78d11b6ac 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -237,6 +237,11 @@ pub struct Snapper { /// consumed by Extension snap and extended-intersection logic. These rays /// belong only to OTRACK when Perpendicular OSNAP is enabled. (#695) pub tracking_perp_dirs: Vec>, + /// Perpendicular direction carried from a point placed by typed distance + /// along an Extension/OTRACK ray. It survives only until the next point + /// commit, so the source ray can still drive a perpendicular construction + /// through the newly placed point. + post_distance_perp_dir: Option, /// Last snap world position (for dwell detection). pub last_snap_world: Option, /// When the cursor first rested near `last_snap_world`. @@ -301,6 +306,7 @@ impl Default for Snapper { tracking_points: Vec::new(), tracking_dirs: Vec::new(), tracking_perp_dirs: Vec::new(), + post_distance_perp_dir: None, last_snap_world: None, dwell_since: None, dwell_acquired: false, @@ -751,7 +757,18 @@ impl Snapper { ucs_x: DVec3, ucs_y: DVec3, ) -> Option { - if !self.otrack_enabled || self.tracking_points.is_empty() { + // A point created by typed distance along Extension/OTRACK carries + // that source direction for exactly one following point. Perpendicular + // OSNAP may use it even when F11 tracking itself is off. + let carried_perp = if self.is_on(SnapType::Perpendicular) { + self.post_distance_perp_dir.zip(last_point) + } else { + None + }; + + if (!self.otrack_enabled || self.tracking_points.is_empty()) + && carried_perp.is_none() + { return None; } @@ -861,6 +878,18 @@ impl Snapper { } } } + // Typed distance along an Extension/OTRACK ray creates a new point + // that still knows the direction it came from. Offer exactly one + // perpendicular ray through that new point for the following pick. + if let Some((dir, origin)) = carried_perp { + rays.push(Ray { + origin, + dir, + group: self.tracking_points.len(), + kind: TrackingKind::Perpendicular, + }); + } + // OTRACK rays come first; the auxiliary rays appended below (polar from // last_point, ortho axis from last_point) only participate in // intersection locking, never in single-ray fallback. @@ -1006,11 +1035,21 @@ impl Snapper { best.map(|(_, h)| h) } + /// Carry the direction of a reference ray used by typed-distance entry + /// into the next point, as its perpendicular construction direction. + pub fn remember_distance_reference(&mut self, dir: DVec3) { + let planar = DVec3::new(dir.x, dir.y, 0.0); + self.post_distance_perp_dir = planar + .try_normalize() + .map(|unit| DVec3::new(-unit.y, unit.x, 0.0)); + } + /// Clear all acquired tracking points (e.g. when command ends). pub fn clear_tracking(&mut self) { self.tracking_points.clear(); self.tracking_dirs.clear(); self.tracking_perp_dirs.clear(); + self.post_distance_perp_dir = None; self.parallel_ref = None; self.parallel_dwell = None; self.last_snap_world = None; @@ -1152,6 +1191,7 @@ impl Snapper { tracking_points: Vec::new(), tracking_dirs: Vec::new(), tracking_perp_dirs: Vec::new(), + post_distance_perp_dir: None, last_snap_world: None, dwell_since: None, dwell_acquired: false, @@ -4165,6 +4205,70 @@ mod ext_tests { ); } + #[test] + fn typed_distance_reference_offers_perpendicular_for_next_point() { + let mut s = Snapper::default(); + s.enabled.insert(SnapType::Perpendicular); + // This must not depend on F11: the carried construction belongs to + // Perpendicular OSNAP itself. + s.otrack_enabled = false; + s.osnap_radius_px = 10.0; + + // The first point was placed along a horizontal Extension ray. + s.remember_distance_reference(DVec3::X); + let base = DVec3::new(2_000.0, 3_000.0, 0.0); + + let view_rot = Mat4::from_scale(Vec3::splat(0.0001)); + let eye = DVec3::ZERO; + let bounds = Rectangle { + x: 0.0, + y: 0.0, + width: 1000.0, + height: 1000.0, + }; + + // Pull almost vertically from the newly placed point. The preserved + // horizontal reference must produce a vertical perpendicular guide. + let cursor = base + DVec3::Y * 5_000.0 + DVec3::X * 0.05; + let hit = s + .otrack_snap( + cursor, + view_rot, + eye, + bounds, + None, + Some(base), + None, + false, + DVec3::X, + DVec3::Y, + ) + .expect("carried Extension direction should offer perpendicular"); + + assert_eq!(hit.kind, TrackingKind::Perpendicular); + assert!((hit.base - base).length() < 1e-12); + assert!((hit.aligned.x - base.x).abs() < 1e-12); + assert!((hit.aligned.y - (base.y + 5_000.0)).abs() < 1e-9); + + // It is deliberately one-step state: the next accepted point clears it. + s.clear_tracking(); + assert!( + s.otrack_snap( + cursor, + view_rot, + eye, + bounds, + None, + Some(base), + None, + false, + DVec3::X, + DVec3::Y, + ) + .is_none() + ); + } + #[test] fn tangent_points_are_perpendicular_to_the_radius() { let c = Vec3::new(0.0, 0.0, 0.0);