diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index b42fc2cf5..49e55f302 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1097,10 +1097,8 @@ impl OpenCADStudio { // Grip edits use the same tracking-point acquisition and OTRACK // resolution as normal interactive point commands. let otrack_hit = { - let snap_world = snap_hit.map(|s| s.world); - self.snapper.update_otrack_dwell( - snap_world, + snap_hit, &snap_candidates, view_rot, eye, @@ -1543,9 +1541,8 @@ impl OpenCADStudio { // to a tracking ray (and store the alignment so a typed // distance can place a point along it — issue #69). let otrack_hit = { - let snap_world = self.tabs[i].snap_result.map(|s| s.world); self.snapper.update_otrack_dwell( - snap_world, + self.tabs[i].snap_result, &snap_candidates, view_rot, eye, @@ -1562,7 +1559,11 @@ impl OpenCADStudio { bounds, Instant::now(), ); - if self.tabs[i].snap_result.is_none() { + if self.tabs[i].snap_result.is_none() + || self.tabs[i] + .snap_result + .is_some_and(|s| s.snap_type == crate::snap::SnapType::Extension) + { // A window corner is a free point, so neither the // Polar increment nor the Ortho lock may pull the // OTRACK alignment onto an axis (#291). @@ -2901,7 +2902,10 @@ impl OpenCADStudio { } // OTRACK alignment wins over ortho/polar; otherwise apply // ortho/polar relative to the last point. - let otrack = if !world_pt_locked && snap_hit.is_none() { + let otrack = if !world_pt_locked + && (snap_hit.is_none() + || snap_hit.is_some_and(|s| s.snap_type == crate::snap::SnapType::Extension)) + { let step = if self.polar_mode && !is_window_corner { Some(self.polar_increment_deg) } else { diff --git a/src/snap.rs b/src/snap.rs index 816e8de48..35b5a735f 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -285,7 +285,7 @@ impl Snapper { /// `snap_world` is the current snap result world point (if any). pub fn update_otrack_dwell( &mut self, - snap_world: Option, + snap: Option, wires: &W, view_rot: glam::Mat4, eye: glam::DVec3, @@ -302,6 +302,24 @@ impl Snapper { self.dwell_acquired = false; return; } + // Only stable geometric snap points may be acquired automatically for + // OTRACK. Continuous/contextual snaps such as Nearest, Perpendicular, + // Tangent and Extension can move along geometry and would otherwise + // create arbitrary tracking points while the cursor is hovering. (#716) + let snap_world = snap.and_then(|hit| { + matches!( + hit.snap_type, + SnapType::Endpoint + | SnapType::Midpoint + | SnapType::Center + | SnapType::Node + | SnapType::Quadrant + | SnapType::Intersection + | SnapType::Insertion + | SnapType::ApparentIntersection + ) + .then_some(hit.world) + }); // With OTRACK off, acquisition is Extension-driven, and Extension tracks // a line only from a real segment endpoint — so acquire endpoints only. // This stops a paused cursor on an extension foot (or a midpoint/centre) @@ -1186,33 +1204,30 @@ impl Snapper { } // ── Perpendicular — foot of perpendicular from the drawing base ── - // Drop the foot from the point the command is drawing *from* (so the - // new segment is truly perpendicular to the target). Only when there - // is no base point — e.g. picking the very first point — does it fall - // back to the cursor (a plain nearest-on-line). The candidate is gated - // on its screen distance to the cursor like every other snap, so it - // offers when the cursor is near the perpendicular foot. (#118) + // A perpendicular snap requires a real drawing base. Without one + // (for example while picking the first point of LINE) there is no + // meaningful perpendicular direction, so do not offer a candidate. + // Falling back to the cursor here would make Perpendicular behave + // like Nearest. (#716) if self.is_on(SnapType::Perpendicular) { - let q = self - .from_point - .map(|v| v.as_dvec3()) - .unwrap_or(cursor_world); - if let Some(segments) = &local_segments { - for seg in segments { - if let Some(foot) = perp_foot(q, seg.a, seg.b) { - try_pt(foot, SnapType::Perpendicular); - } - } - } else { - for wire in wires.iter() { - if !wire_in_range(wire) { - continue; - } - for i in 0..wire.points.len().saturating_sub(1) { - if let Some(foot) = perp_foot(q, wp_f64(wire, i), wp_f64(wire, i + 1)) { + if let Some(q) = self.from_point.map(|v| v.as_dvec3()) { + if let Some(segments) = &local_segments { + for seg in segments { + if let Some(foot) = perp_foot(q, seg.a, seg.b) { try_pt(foot, SnapType::Perpendicular); } } + } else { + for wire in wires.iter() { + if !wire_in_range(wire) { + continue; + } + for i in 0..wire.points.len().saturating_sub(1) { + if let Some(foot) = perp_foot(q, wp_f64(wire, i), wp_f64(wire, i + 1)) { + try_pt(foot, SnapType::Perpendicular); + } + } + } } } }