Geometry: the closest point on a full circle just before its start is found before its end

Seg2::closest_param starts Newton from the nearest of 65 samples. On a closed segment (a full
circle) the start and end samples are one point and the first wins the tie; for a point just
before the start (in the sweep's sense) Newton then wants a parameter below 0, the clamp holds
it at 0, and the answer is the start point, off by up to a sample spacing (0.15 mm on a 5 mm
circle). The record tests' face-to-segment matching read an SMLib cylinder's side as lying
over no segment because of it. A closed segment whose answer lands on an end now also tries
Newton from the other end and keeps the nearer; every other answer is bit for bit as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 22:16:13 -04:00
committed by Irakli Kourtis
parent d0daee90ff
commit 3c680d21a9
+28 -2
View File
@@ -384,8 +384,23 @@ impl Seg2 {
_ => {}
}
let n = 64;
let mut t =
(0..=n).map(|i| i as f64 / n as f64).min_by(|x, y| self.point_at(*x).dist(p).total_cmp(&self.point_at(*y).dist(p))).unwrap_or(0.0);
let t = (0..=n).map(|i| i as f64 / n as f64).min_by(|x, y| self.point_at(*x).dist(p).total_cmp(&self.point_at(*y).dist(p))).unwrap_or(0.0);
let t = self.newton_closest(p, t);
// A closed segment (a full circle) starts where it ends: a point just before its start is
// nearest just before its end, which Newton from the start (held there by the clamp)
// never reaches. Try from the other end too.
let (a, b) = (self.point_at(0.0), self.point_at(1.0));
if (t == 0.0 || t == 1.0) && a.dist(b) <= 1e-12 * (1.0 + a.len()) {
let other = self.newton_closest(p, 1.0 - t);
if self.point_at(other).dist(p) < self.point_at(t).dist(p) {
return other;
}
}
t
}
/// Newton's method for the parameter of the point nearest `p`, from `t`, within [0, 1].
fn newton_closest(&self, p: Vec2, mut t: f64) -> f64 {
for _ in 0..30 {
let (q, d) = (self.point_at(t), self.derivative(t));
let (d2, h) = ((self.derivative((t + 1e-6).min(1.0)) - self.derivative((t - 1e-6).max(0.0))) / 2e-6, q - p);
@@ -403,6 +418,7 @@ impl Seg2 {
}
t
}
/// The two halves of the segment (same kind, exact).
pub fn split_half(&self) -> (Seg2, Seg2) {
match *self {
@@ -965,6 +981,16 @@ mod tests {
assert!(s.point_at(s.closest_param(on_curve)).dist(on_curve) < 1e-9);
}
}
// A full circle's points just before its start, either way round: nearest just before
// its end, not at its start (Newton from the start stays held there).
for sweep in [TAU, -TAU] {
let circle = Seg2::Arc { center: Vec2::new(1.0, 2.0), radius: 5.0, start: 0.6131, sweep };
for delta in [1e-4, 0.01, 0.04] {
let before = 0.6131 - delta * sweep.signum();
let p = Vec2::new(1.0, 2.0) + Vec2::from_angle(before) * 4.99;
assert!((circle.point_at(circle.closest_param(p)).dist(p) - 0.01).abs() < 1e-9, "sweep {sweep}, {delta}");
}
}
}
/// The sweep visits only the edges crossing each slab; it must give the triangles that