From 19c434fc5d5114b9b5b635a0054bbd5d719b16db Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 28 Sep 2026 17:56:01 +0300 Subject: [PATCH] fix(snap): perpendicular onto circles/arcs/ellipses uses the exact curve, not tessellation chords Fixes #1495 Co-Authored-By: Claude Opus 5.5 --- src/snap.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/snap.rs b/src/snap.rs index 243d1feae..0ef9ead29 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -1603,7 +1603,29 @@ impl Snapper { if self.is_on(SnapType::Perpendicular) { if let Some(q) = self.from_point { if let Some(segments) = &local_segments { + // A curved wire's feet come from its exact geometry once, + // not from each chord of the tessellation. (#1495) + let mut exact: HashMap = HashMap::default(); for seg in segments { + let wire = wires.source_wire(seg.wire); + let handled = *exact.entry(seg.wire).or_insert_with(|| { + let Some(feet) = wire.and_then(|w| exact_perpendicular_feet(w, q)) + else { + return false; + }; + for foot in feet { + try_pt( + foot, + SnapType::Perpendicular, + wire.and_then(wire_source), + None, + ); + } + true + }); + if handled { + continue; + } if let Some(foot) = perp_foot(q, seg.a, seg.b) { try_pt( foot, @@ -1616,6 +1638,12 @@ impl Snapper { } else { for wire in in_range_wires.iter() { let src = wire_source(wire); + if let Some(feet) = exact_perpendicular_feet(wire, q) { + for foot in feet { + try_pt(foot, SnapType::Perpendicular, src, None); + } + 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, src, None); @@ -3022,6 +3050,21 @@ fn curves_in_frame(wire: &WireModel, frame: &WirePlane, tol: f64) -> Option Option> { + let frame = wire_plane(wire)?; + let curves = curves_in_frame(wire, &frame, 1e-7)?; + let from = frame.to_2d(from); + Some( + curves + .iter() + .flat_map(|curve| kernel::geom2d::perpendicular_from(curve, from)) + .map(|foot| frame.to_3d(foot.point)) + .collect(), + ) +} + pub(crate) fn exact_curve_intersections( wire_a: &WireModel, wire_b: &WireModel,