use brepkit_math::curves::Line3D;
use brepkit_math::traits::ParametricCurve;
use brepkit_math::vec::Point3;
use super::ExtremaSolution;
const N_SAMPLES: usize = 32;
const MAX_ITER: usize = 50;
const PARAM_TOL: f64 = 1e-10;
#[must_use]
pub fn line_to_line(
l1: &Line3D,
t1_range: (f64, f64),
l2: &Line3D,
t2_range: (f64, f64),
) -> ExtremaSolution {
let d1 = l1.direction();
let d2 = l2.direction();
let r = l1.origin() - l2.origin();
let a = d1.dot(d1); let e = d2.dot(d2); let f = d2.dot(r);
let (s, t);
if a < 1e-30 && e < 1e-30 {
s = t1_range.0;
t = t2_range.0;
} else if a < 1e-30 {
s = t1_range.0;
t = (f / e).clamp(t2_range.0, t2_range.1);
} else {
let c = d1.dot(r);
if e < 1e-30 {
t = t2_range.0;
s = (-c / a).clamp(t1_range.0, t1_range.1);
} else {
let b = d1.dot(d2);
let denom = a * e - b * b;
let s_raw = if denom.abs() > 1e-30 {
(b * f - c * e) / denom
} else {
t1_range.0
};
s = s_raw.clamp(t1_range.0, t1_range.1);
let t_raw = (b * s + f) / e;
t = t_raw.clamp(t2_range.0, t2_range.1);
}
}
let s = if a > 1e-30 {
let b = d1.dot(d2);
let c = d1.dot(r);
((b * t - c) / a).clamp(t1_range.0, t1_range.1)
} else {
s
};
let pa = l1.evaluate(s);
let pb = l2.evaluate(t);
let diff = pa - pb;
let distance = (diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z()).sqrt();
ExtremaSolution {
distance,
point_a: pa,
point_b: pb,
param_a: s,
param_b: t,
}
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn curve_to_curve<C1: ParametricCurve, C2: ParametricCurve>(
c1: &C1,
t1_range: (f64, f64),
c2: &C2,
t2_range: (f64, f64),
) -> ExtremaSolution {
let (t1_start, t1_end) = t1_range;
let (t2_start, t2_end) = t2_range;
if t1_end <= t1_start || t2_end <= t2_start {
let p1 = c1.evaluate(t1_start);
let p2 = c2.evaluate(t2_start);
return ExtremaSolution {
distance: (p1 - p2).length(),
point_a: p1,
point_b: p2,
param_a: t1_start,
param_b: t2_start,
};
}
let step1 = (t1_end - t1_start) / (N_SAMPLES - 1) as f64;
let step2 = (t2_end - t2_start) / (N_SAMPLES - 1) as f64;
let mut best_t1 = t1_start;
let mut best_t2 = t2_start;
let mut best_dist_sq = f64::INFINITY;
let samples1: Vec<(f64, Point3)> = (0..N_SAMPLES)
.map(|i| {
let t = if i == N_SAMPLES - 1 {
t1_end
} else {
t1_start + i as f64 * step1
};
(t, c1.evaluate(t))
})
.collect();
let samples2: Vec<(f64, Point3)> = (0..N_SAMPLES)
.map(|i| {
let t = if i == N_SAMPLES - 1 {
t2_end
} else {
t2_start + i as f64 * step2
};
(t, c2.evaluate(t))
})
.collect();
for (t1, p1) in &samples1 {
for (t2, p2) in &samples2 {
let diff = *p1 - *p2;
let d2 = diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z();
if d2 < best_dist_sq {
best_dist_sq = d2;
best_t1 = *t1;
best_t2 = *t2;
}
}
}
let h1 = ((t1_end - t1_start) * 1e-6).max(1e-9);
let h2 = ((t2_end - t2_start) * 1e-6).max(1e-9);
let mut t1 = best_t1;
let mut t2 = best_t2;
for _ in 0..MAX_ITER {
let p1 = c1.evaluate(t1);
let p2 = c2.evaluate(t2);
let diff = p1 - p2;
let t1_fwd = (t1 + h1).min(t1_end);
let t1_bwd = (t1 - h1).max(t1_start);
let inv2h1 = 1.0 / (t1_fwd - t1_bwd);
let p1f = c1.evaluate(t1_fwd);
let p1b = c1.evaluate(t1_bwd);
let vel1 = brepkit_math::vec::Vec3::new(
(p1f.x() - p1b.x()) * inv2h1,
(p1f.y() - p1b.y()) * inv2h1,
(p1f.z() - p1b.z()) * inv2h1,
);
let t2_fwd = (t2 + h2).min(t2_end);
let t2_bwd = (t2 - h2).max(t2_start);
let inv2h2 = 1.0 / (t2_fwd - t2_bwd);
let p2f = c2.evaluate(t2_fwd);
let p2b = c2.evaluate(t2_bwd);
let vel2 = brepkit_math::vec::Vec3::new(
(p2f.x() - p2b.x()) * inv2h2,
(p2f.y() - p2b.y()) * inv2h2,
(p2f.z() - p2b.z()) * inv2h2,
);
let f1 = diff.dot(vel1);
let f2 = -diff.dot(vel2);
let j11 = vel1.dot(vel1);
let j12 = -vel1.dot(vel2);
let j22 = vel2.dot(vel2);
let det = j11 * j22 - j12 * j12;
if det.abs() < f64::EPSILON {
break;
}
let dt1 = (f1 * j22 - f2 * j12) / det;
let dt2 = (f2 * j11 - f1 * j12) / det;
let t1_new = (t1 - dt1).clamp(t1_start, t1_end);
let t2_new = (t2 - dt2).clamp(t2_start, t2_end);
if (t1_new - t1).abs() < PARAM_TOL && (t2_new - t2).abs() < PARAM_TOL {
t1 = t1_new;
t2 = t2_new;
break;
}
t1 = t1_new;
t2 = t2_new;
}
let pa = c1.evaluate(t1);
let pb = c2.evaluate(t2);
let diff = pa - pb;
let distance = (diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z()).sqrt();
ExtremaSolution {
distance,
point_a: pa,
point_b: pb,
param_a: t1,
param_b: t2,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use brepkit_math::curves::Circle3D;
use brepkit_math::vec::{Point3, Vec3};
use std::f64::consts::TAU;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn parallel_lines_unit_separation() {
let l1 = Line3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)).unwrap();
let l2 = Line3D::new(Point3::new(0.0, 1.0, 0.0), Vec3::new(1.0, 0.0, 0.0)).unwrap();
let sol = line_to_line(&l1, (0.0, 10.0), &l2, (0.0, 10.0));
assert!(approx(sol.distance, 1.0, 1e-12), "dist={}", sol.distance);
}
#[test]
fn perpendicular_skew_lines_closest_approach() {
let l1 = Line3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)).unwrap();
let l2 = Line3D::new(Point3::new(0.0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0)).unwrap();
let sol = line_to_line(&l1, (-5.0, 5.0), &l2, (-5.0, 5.0));
assert!(approx(sol.distance, 1.0, 1e-12), "dist={}", sol.distance);
assert!(
approx(sol.point_a.x(), 0.0, 1e-12),
"pa.x={}",
sol.point_a.x()
);
assert!(
approx(sol.point_b.y(), 0.0, 1e-12),
"pb.y={}",
sol.point_b.y()
);
}
#[test]
fn intersecting_lines_zero_distance() {
let l1 = Line3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)).unwrap();
let l2 = Line3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0)).unwrap();
let sol = line_to_line(&l1, (-5.0, 5.0), &l2, (-5.0, 5.0));
assert!(sol.distance < 1e-12, "dist={}", sol.distance);
}
#[test]
fn line_clamped_to_domain_endpoint() {
let l1 = Line3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)).unwrap();
let l2 = Line3D::new(Point3::new(5.0, 1.0, 0.0), Vec3::new(0.0, 1.0, 0.0)).unwrap();
let sol = line_to_line(&l1, (0.0, 2.0), &l2, (-5.0, 5.0));
assert!(
approx(sol.distance, 3.0, 1e-12),
"dist={} expected=3.0",
sol.distance
);
assert!(approx(sol.param_a, 2.0, 1e-10), "t1={}", sol.param_a);
}
#[test]
fn generic_circle_to_circle_concentric_coplanar() {
let c1 = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let c2 = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap();
let sol = curve_to_curve(&c1, (0.0, TAU), &c2, (0.0, TAU));
assert!(approx(sol.distance, 1.0, 1e-4), "dist={}", sol.distance);
}
#[test]
fn generic_circles_axially_offset() {
let c1 = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let c2 = Circle3D::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let sol = curve_to_curve(&c1, (0.0, TAU), &c2, (0.0, TAU));
assert!(approx(sol.distance, 3.0, 1e-4), "dist={}", sol.distance);
}
#[test]
fn closest_points_stationarity_condition() {
let c1 = Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let c2 = Circle3D::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let sol = curve_to_curve(&c1, (0.0, TAU), &c2, (0.0, TAU));
let diff = sol.point_a - sol.point_b;
let tan1 = c1.tangent(sol.param_a);
let dot = diff.dot(tan1);
assert!(dot.abs() < 1e-4, "stationarity: dot={dot}");
}
}