use brepkit_math::vec::Point3;
#[must_use]
pub fn segment_segment_distance(
a0: Point3,
a1: Point3,
b0: Point3,
b1: Point3,
) -> (f64, Point3, Point3) {
let d1 = a1 - a0; let d2 = b1 - b0; let r = a0 - b0;
let a = d1.dot(d1); let e = d2.dot(d2); let f = d2.dot(r);
if a <= 1e-30 && e <= 1e-30 {
let diff = a0 - b0;
let dist = (diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z()).sqrt();
return (dist, a0, b0);
}
let (s, t);
if a <= 1e-30 {
s = 0.0;
t = (f / e).clamp(0.0, 1.0);
} else {
let c = d1.dot(r);
if e <= 1e-30 {
t = 0.0;
s = (-c / a).clamp(0.0, 1.0);
} else {
let b = d1.dot(d2);
let denom = a * e - b * b;
s = if denom.abs() > 1e-30 {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.0
};
let t_raw = (b * s + f) / e;
t = t_raw.clamp(0.0, 1.0);
}
}
let s = if a > 1e-30 {
let b = d1.dot(d2);
let c = d1.dot(r);
((b * t - c) / a).clamp(0.0, 1.0)
} else {
s
};
let closest_a = a0 + d1 * s;
let closest_b = b0 + d2 * t;
let diff = closest_a - closest_b;
let dist = (diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z()).sqrt();
(dist, closest_a, closest_b)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn dist3(p: Point3, q: Point3) -> f64 {
let d = p - q;
(d.x() * d.x() + d.y() * d.y() + d.z() * d.z()).sqrt()
}
#[test]
fn parallel_segments_same_line() {
let (dist, pa, pb) = segment_segment_distance(
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(3.0, 0.0, 0.0),
Point3::new(4.0, 0.0, 0.0),
);
assert!(approx_eq(dist, 2.0, 1e-12), "dist={dist}");
assert!(approx_eq(pa.x(), 1.0, 1e-12), "pa.x={}", pa.x());
assert!(approx_eq(pb.x(), 3.0, 1e-12), "pb.x={}", pb.x());
}
#[test]
fn skew_segments_closest_approach() {
let (dist, pa, pb) = segment_segment_distance(
Point3::new(-2.0, 0.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
Point3::new(0.0, -2.0, 1.0),
Point3::new(0.0, 2.0, 1.0),
);
assert!(approx_eq(dist, 1.0, 1e-12), "dist={dist}");
assert!(
approx_eq(pa.x(), 0.0, 1e-12)
&& approx_eq(pa.y(), 0.0, 1e-12)
&& approx_eq(pa.z(), 0.0, 1e-12),
"pa={:?}",
pa
);
assert!(
approx_eq(pb.x(), 0.0, 1e-12)
&& approx_eq(pb.y(), 0.0, 1e-12)
&& approx_eq(pb.z(), 1.0, 1e-12),
"pb={:?}",
pb
);
}
#[test]
fn intersecting_segments_zero_distance() {
let (dist, pa, pb) = segment_segment_distance(
Point3::new(0.0, 1.0, 0.0),
Point3::new(2.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 2.0, 0.0),
);
assert!(approx_eq(dist, 0.0, 1e-12), "dist={dist}");
assert!(dist3(pa, pb) < 1e-12, "pa≠pb: pa={:?} pb={:?}", pa, pb);
}
#[test]
fn degenerate_both_points() {
let (dist, pa, pb) = segment_segment_distance(
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(4.0, 0.0, 0.0),
Point3::new(4.0, 0.0, 0.0),
);
assert!(approx_eq(dist, 3.0, 1e-12), "dist={dist}");
assert!(dist3(pa, Point3::new(1.0, 0.0, 0.0)) < 1e-12, "pa={:?}", pa);
assert!(dist3(pb, Point3::new(4.0, 0.0, 0.0)) < 1e-12, "pb={:?}", pb);
}
#[test]
fn degenerate_a_is_point() {
let (dist, _pa, pb) = segment_segment_distance(
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 2.0, 0.0),
Point3::new(0.0, 4.0, 0.0),
);
assert!(approx_eq(dist, 2.0, 1e-12), "dist={dist}");
assert!(dist3(pb, Point3::new(0.0, 2.0, 0.0)) < 1e-12, "pb={:?}", pb);
}
#[test]
fn degenerate_b_is_point() {
let (dist, pa, _pb) = segment_segment_distance(
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 1.0),
Point3::new(0.0, 0.0, 3.0),
Point3::new(0.0, 0.0, 3.0),
);
assert!(approx_eq(dist, 2.0, 1e-12), "dist={dist}");
assert!(dist3(pa, Point3::new(0.0, 0.0, 1.0)) < 1e-12, "pa={:?}", pa);
}
#[test]
fn closest_points_lie_on_respective_segments() {
let a0 = Point3::new(1.0, 0.0, 0.0);
let a1 = Point3::new(5.0, 0.0, 0.0);
let b0 = Point3::new(3.0, 1.0, 2.0);
let b1 = Point3::new(3.0, 4.0, 5.0);
let (dist, pa, pb) = segment_segment_distance(a0, a1, b0, b1);
let da = a1 - a0;
let len_a = (da.x() * da.x() + da.y() * da.y() + da.z() * da.z()).sqrt();
let pa_a0 = pa - a0;
let s = (pa_a0.x() * da.x() + pa_a0.y() * da.y() + pa_a0.z() * da.z()) / (len_a * len_a);
assert!((-1e-12..=1.0 + 1e-12).contains(&s), "s out of [0,1]: s={s}");
let db = b1 - b0;
let len_b = (db.x() * db.x() + db.y() * db.y() + db.z() * db.z()).sqrt();
let pb_b0 = pb - b0;
let t = (pb_b0.x() * db.x() + pb_b0.y() * db.y() + pb_b0.z() * db.z()) / (len_b * len_b);
assert!((-1e-12..=1.0 + 1e-12).contains(&t), "t out of [0,1]: t={t}");
assert!(
approx_eq(dist, dist3(pa, pb), 1e-12),
"dist mismatch: dist={dist} actual={}",
dist3(pa, pb)
);
}
}