use brepkit_math::curves::{Circle3D, Line3D};
use brepkit_math::traits::ParametricCurve;
use brepkit_math::vec::Point3;
use super::CurveProjection;
const MAX_ITER: usize = 50;
const PARAM_TOL: f64 = 1e-10;
const N_SAMPLES: usize = 64;
#[must_use]
pub fn point_to_line(point: Point3, line: &Line3D, t_start: f64, t_end: f64) -> CurveProjection {
let t_unclamped = line.project(point);
let t = t_unclamped.clamp(t_start, t_end);
let closest = line.evaluate(t);
let diff = closest - point;
let distance = (diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z()).sqrt();
CurveProjection {
distance,
point: closest,
parameter: t,
}
}
#[must_use]
pub fn point_to_circle(point: Point3, circle: &Circle3D) -> CurveProjection {
let t = circle.project(point);
let closest = circle.evaluate(t);
let diff = closest - point;
let distance = (diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z()).sqrt();
CurveProjection {
distance,
point: closest,
parameter: t,
}
}
#[must_use]
pub fn point_to_curve<C: ParametricCurve>(
point: Point3,
curve: &C,
t_start: f64,
t_end: f64,
) -> CurveProjection {
if t_end <= t_start {
let p = curve.evaluate(t_start);
return CurveProjection {
distance: (p - point).length(),
point: p,
parameter: t_start,
};
}
let step = (t_end - t_start) / (N_SAMPLES - 1) as f64;
let mut best_t = t_start;
let mut best_dist_sq = f64::INFINITY;
for i in 0..N_SAMPLES {
let t = if i == N_SAMPLES - 1 {
t_end
} else {
t_start + i as f64 * step
};
let p = curve.evaluate(t);
let diff = p - point;
let d2 = diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z();
if d2 < best_dist_sq {
best_dist_sq = d2;
best_t = t;
}
}
let h = (t_end - t_start) * 1e-6;
let h = h.max(1e-9);
let mut t = best_t;
for _ in 0..MAX_ITER {
let p = curve.evaluate(t);
let diff = p - point;
let t_fwd = (t + h).min(t_end);
let t_bwd = (t - h).max(t_start);
let p_fwd = curve.evaluate(t_fwd);
let p_bwd = curve.evaluate(t_bwd);
let inv2h = 1.0 / (t_fwd - t_bwd);
let vel_x = (p_fwd.x() - p_bwd.x()) * inv2h;
let vel_y = (p_fwd.y() - p_bwd.y()) * inv2h;
let vel_z = (p_fwd.z() - p_bwd.z()) * inv2h;
let f = diff.x() * vel_x + diff.y() * vel_y + diff.z() * vel_z;
let vel_sq = vel_x * vel_x + vel_y * vel_y + vel_z * vel_z;
if vel_sq < f64::EPSILON {
break;
}
let delta = f / vel_sq;
let t_new = (t - delta).clamp(t_start, t_end);
if (t_new - t).abs() < PARAM_TOL {
t = t_new;
break;
}
t = t_new;
}
let closest = curve.evaluate(t);
let diff = closest - point;
let distance = (diff.x() * diff.x() + diff.y() * diff.y() + diff.z() * diff.z()).sqrt();
CurveProjection {
distance,
point: closest,
parameter: t,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use brepkit_math::vec::Vec3;
use std::f64::consts::{PI, TAU};
#[test]
fn line_point_above_midpoint() {
let line = Line3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)).unwrap();
let proj = point_to_line(Point3::new(5.0, 3.0, 0.0), &line, 0.0, 10.0);
assert!(
(proj.parameter - 5.0).abs() < 1e-12,
"param={}",
proj.parameter
);
assert!(
(proj.distance - 3.0).abs() < 1e-12,
"dist={}",
proj.distance
);
assert!((proj.point.x() - 5.0).abs() < 1e-12);
assert!((proj.point.y()).abs() < 1e-12);
}
#[test]
fn line_point_before_start_clamps() {
let line = Line3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)).unwrap();
let proj = point_to_line(Point3::new(-5.0, 0.0, 0.0), &line, 0.0, 10.0);
assert!(
(proj.parameter - 0.0).abs() < 1e-12,
"param={}",
proj.parameter
);
assert!(
(proj.distance - 5.0).abs() < 1e-12,
"dist={}",
proj.distance
);
}
#[test]
fn line_point_past_end_clamps() {
let line = Line3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)).unwrap();
let proj = point_to_line(Point3::new(15.0, 0.0, 0.0), &line, 0.0, 10.0);
assert!(
(proj.parameter - 10.0).abs() < 1e-12,
"param={}",
proj.parameter
);
assert!(
(proj.distance - 5.0).abs() < 1e-12,
"dist={}",
proj.distance
);
}
#[test]
fn line_point_on_line_zero_distance() {
let line = Line3D::new(Point3::new(1.0, 2.0, 3.0), Vec3::new(0.0, 0.0, 1.0)).unwrap();
let proj = point_to_line(Point3::new(1.0, 2.0, 5.0), &line, 0.0, 20.0);
assert!(proj.distance < 1e-12, "dist={}", proj.distance);
assert!(
(proj.parameter - 2.0).abs() < 1e-12,
"param={}",
proj.parameter
);
}
#[test]
fn circle_point_on_positive_y_axis() {
let circle =
Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let q = Point3::new(0.0, 5.0, 0.0);
let proj = point_to_circle(q, &circle);
assert!(
(proj.distance - 4.0).abs() < 1e-12,
"dist={}",
proj.distance
);
let r = (proj.point.x() * proj.point.x()
+ proj.point.y() * proj.point.y()
+ proj.point.z() * proj.point.z())
.sqrt();
assert!((r - 1.0).abs() < 1e-12, "closest not on circle: r={r}");
assert!(proj.point.x().abs() < 1e-12, "x={}", proj.point.x());
assert!((proj.point.y() - 1.0).abs() < 1e-12, "y={}", proj.point.y());
}
#[test]
fn circle_point_on_axis_distance_equals_radius() {
let r = 3.0_f64;
let circle =
Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), r).unwrap();
let proj = point_to_circle(Point3::new(0.0, 0.0, 5.0), &circle);
let expected = (r * r + 25.0_f64).sqrt();
assert!(
(proj.distance - expected).abs() < 1e-12,
"dist={}",
proj.distance
);
}
#[test]
fn circle_point_in_plane() {
let circle =
Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap();
let proj = point_to_circle(Point3::new(3.0, 0.0, 0.0), &circle);
assert!(
(proj.distance - 1.0).abs() < 1e-12,
"dist={}",
proj.distance
);
let r = (proj.point.x() * proj.point.x()
+ proj.point.y() * proj.point.y()
+ proj.point.z() * proj.point.z())
.sqrt();
assert!((r - 2.0).abs() < 1e-12, "closest not on circle: r={r}");
assert!((proj.point.x() - 2.0).abs() < 1e-12, "x={}", proj.point.x());
assert!(proj.point.y().abs() < 1e-12, "y={}", proj.point.y());
}
#[test]
fn generic_circle_matches_analytic() {
let circle =
Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0).unwrap();
let q = Point3::new(0.0, 5.0, 0.0);
let analytic = point_to_circle(q, &circle);
let generic = point_to_curve(q, &circle, 0.0, TAU);
assert!(
(analytic.distance - generic.distance).abs() < 1e-6,
"analytic={} generic={}",
analytic.distance,
generic.distance
);
assert!(
(analytic.parameter - generic.parameter).abs() < 1e-6,
"analytic_t={} generic_t={}",
analytic.parameter,
generic.parameter
);
}
#[test]
fn generic_stationarity_condition_satisfied() {
let r = 5.0_f64;
let circle =
Circle3D::new(Point3::new(1.0, 2.0, 3.0), Vec3::new(0.0, 0.0, 1.0), r).unwrap();
let q = Point3::new(3.0, 7.0, 3.0);
let proj = point_to_curve(q, &circle, 0.0, TAU);
let p = circle.evaluate(proj.parameter);
let tan = circle.tangent(proj.parameter);
let diff = p - q;
let dot = diff.x() * tan.x() + diff.y() * tan.y() + diff.z() * tan.z();
assert!(dot.abs() < 1e-6, "stationarity violated: dot={dot}");
}
#[test]
fn generic_bounded_domain_clamping() {
let circle =
Circle3D::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
let q = circle.evaluate(3.0 * PI / 2.0);
let proj = point_to_curve(q, &circle, 0.0, PI);
assert!(
proj.parameter < 1e-10 || (proj.parameter - PI).abs() < 1e-10,
"expected endpoint, got t={}",
proj.parameter
);
assert!(
(proj.distance - 2.0_f64.sqrt()).abs() < 1e-6,
"dist={}",
proj.distance
);
}
}