use crate::MathError;
use crate::nurbs::curve::NurbsCurve;
use crate::nurbs::surface::NurbsSurface;
use crate::vec::Point3;
const MAX_ITERATIONS: usize = 50;
const SURFACE_GRID_SIZE: usize = 8;
#[derive(Debug, Clone, Copy)]
pub struct CurveProjection {
pub parameter: f64,
pub point: Point3,
pub distance: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct SurfaceProjection {
pub u: f64,
pub v: f64,
pub point: Point3,
pub distance: f64,
}
#[allow(clippy::unnecessary_wraps)]
pub fn project_point_to_curve(
curve: &NurbsCurve,
point: Point3,
tolerance: f64,
) -> Result<CurveProjection, MathError> {
let u_min = curve.knots()[curve.degree()];
let candidates = curve_coarse_search(curve, point);
let mut best_u = u_min;
let mut best_pt = curve.evaluate(u_min);
let mut best_dist = (best_pt - point).length();
for (u_guess, lo, hi) in candidates {
let (u_refined, pt_refined) = curve_newton_refine(curve, point, u_guess, lo, hi, tolerance);
let dist = (pt_refined - point).length();
if dist < best_dist {
best_dist = dist;
best_u = u_refined;
best_pt = pt_refined;
}
}
Ok(CurveProjection {
parameter: best_u,
point: best_pt,
distance: best_dist,
})
}
#[allow(clippy::cast_precision_loss)]
fn curve_coarse_search(curve: &NurbsCurve, point: Point3) -> Vec<(f64, f64, f64)> {
type Span = (f64, f64, Vec<(f64, f64)>);
const SEEDS: usize = 5;
let knots = curve.knots();
let p = curve.degree();
let (lo, hi) = (knots[p], knots[knots.len() - p - 1]);
let n_samples = (p + 1).max(5) * 2;
let mut spans: Vec<Span> = Vec::new();
for span in knots.windows(2) {
let (u_start, u_end) = (span[0], span[1]);
if u_end <= u_start || u_start < lo || u_end > hi {
continue;
}
let top = if u_end < hi { u_end.next_down() } else { u_end };
let samples = (0..=n_samples)
.map(|i| {
let t = i as f64 / n_samples as f64;
let u = t.mul_add(u_end - u_start, u_start).min(top);
(u, (curve.evaluate(u) - point).length_squared())
})
.collect();
spans.push((u_start, top, samples));
}
let closest = |samples: &[(f64, f64)]| {
samples
.iter()
.copied()
.min_by(|a, b| a.1.total_cmp(&b.1))
.unwrap_or((0.0, f64::INFINITY))
};
let mut nearest: Vec<(f64, f64, f64, f64)> = spans
.iter()
.map(|(u_start, top, samples)| {
let (u, d) = closest(samples);
(d, u, *u_start, *top)
})
.collect();
nearest.sort_by(|a, b| a.0.total_cmp(&b.0));
nearest.truncate(SEEDS);
let mut minima: Vec<(f64, f64, f64, f64)> = Vec::new();
for (s, (u_start, top, samples)) in spans.iter().enumerate() {
let last = samples.len() - 1;
let mut in_run = false;
for (i, &(u, d)) in samples.iter().enumerate() {
let prev = if i > 0 {
Some(samples[i - 1].1)
} else {
s.checked_sub(1).map(|r| {
let before = &spans[r].2;
before[before.len() - 2].1
})
};
let next = if i < last {
Some(samples[i + 1].1)
} else {
spans.get(s + 1).map(|after| after.2[1].1)
};
let minimum = prev.is_none_or(|q| d <= q) && next.is_none_or(|q| d <= q);
if minimum && !in_run {
minima.push((d, u, *u_start, *top));
}
in_run = minimum;
}
}
minima.sort_by(|a, b| a.0.total_cmp(&b.0));
minima.truncate(SEEDS);
let mut seeds: Vec<(f64, f64, f64)> = Vec::new();
for (_, u, u_start, top) in nearest.into_iter().chain(minima) {
if !seeds
.iter()
.any(|s| s.0.to_bits() == u.to_bits() && s.1.to_bits() == u_start.to_bits())
{
seeds.push((u, u_start, top));
}
}
seeds
}
#[allow(clippy::suspicious_operation_groupings)]
fn curve_newton_refine(
curve: &NurbsCurve,
point: Point3,
u_init: f64,
u_min: f64,
u_max: f64,
tolerance: f64,
) -> (f64, Point3) {
let tol_sq = tolerance * tolerance;
let mut u = u_init;
let mut best: Option<(f64, Point3, f64)> = None;
for _ in 0..MAX_ITERATIONS {
let ders = curve.derivatives(u, 2);
let c_pt = Point3::new(ders[0].x(), ders[0].y(), ders[0].z());
let c_prime = ders[1]; let c_double_prime = ders[2]; let diff = c_pt - point;
let dist_sq = diff.length_squared();
if let Some((best_u, _, best_dist_sq)) = best
&& dist_sq >= best_dist_sq
{
if (u - best_u).abs() < tolerance * (1.0 + best_u.abs()) {
break;
}
u = 0.5 * (u + best_u);
continue;
}
best = Some((u, c_pt, dist_sq));
if dist_sq < tol_sq {
break;
}
let f_val = c_prime.dot(diff);
let c_prime_len_sq = c_prime.length_squared();
if c_prime_len_sq > 1e-30 && dist_sq > tol_sq {
let cos_sq = (f_val * f_val) / (c_prime_len_sq * dist_sq);
if cos_sq < tol_sq {
break;
}
}
let mut f_prime = c_double_prime.dot(diff) + c_prime_len_sq;
if f_prime <= 0.0 {
f_prime = c_prime_len_sq;
}
if f_prime.abs() < 1e-30 {
break;
}
let delta_u = f_val / f_prime;
let u_new = (u - delta_u).clamp(u_min, u_max);
if u_new.is_nan() {
break;
}
if (u_new - u).abs() < tolerance * (1.0 + u.abs()) {
let pt = curve.evaluate(u_new);
let d_sq = (pt - point).length_squared();
if d_sq < dist_sq {
best = Some((u_new, pt, d_sq));
}
break;
}
u = u_new;
}
best.map_or_else(|| (u_init, curve.evaluate(u_init)), |b| (b.0, b.1))
}
pub fn project_point_to_surface(
surface: &NurbsSurface,
point: Point3,
tolerance: f64,
) -> Result<SurfaceProjection, MathError> {
let (u_guess, v_guess) = surface_coarse_search(surface, point);
let knots_u = surface.knots_u();
let knots_v = surface.knots_v();
let pu = surface.degree_u();
let pv = surface.degree_v();
let u_min = knots_u[pu];
let u_max = knots_u[knots_u.len() - pu - 1];
let v_min = knots_v[pv];
let v_max = knots_v[knots_v.len() - pv - 1];
let wraps = surface.is_periodic_u() || surface.is_periodic_v();
let (u_final, v_final, pt_final) = surface_newton_refine(
surface, point, u_guess, v_guess, u_min, u_max, v_min, v_max, tolerance, wraps,
)
.or_else(|err| {
if wraps {
surface_newton_refine(
surface, point, u_guess, v_guess, u_min, u_max, v_min, v_max, tolerance, false,
)
} else {
Err(err)
}
})?;
let dist = (pt_final - point).length();
Ok(SurfaceProjection {
u: u_final,
v: v_final,
point: pt_final,
distance: dist,
})
}
#[allow(clippy::cast_precision_loss)]
fn surface_coarse_search(surface: &NurbsSurface, point: Point3) -> (f64, f64) {
let knots_u = surface.knots_u();
let knots_v = surface.knots_v();
let pu = surface.degree_u();
let pv = surface.degree_v();
let u_min = knots_u[pu];
let u_max = knots_u[knots_u.len() - pu - 1];
let v_min = knots_v[pv];
let v_max = knots_v[knots_v.len() - pv - 1];
let mut best_u = u_min;
let mut best_v = v_min;
let mut best_dist_sq = f64::INFINITY;
let n = SURFACE_GRID_SIZE;
for i in 0..=n {
let u = (i as f64 / n as f64).mul_add(u_max - u_min, u_min);
for j in 0..=n {
let v = (j as f64 / n as f64).mul_add(v_max - v_min, v_min);
let pt = surface.evaluate(u, v);
let d_sq = (pt - point).length_squared();
if d_sq < best_dist_sq {
best_dist_sq = d_sq;
best_u = u;
best_v = v;
}
}
}
(best_u, best_v)
}
#[allow(clippy::too_many_arguments, clippy::similar_names)]
#[allow(clippy::suspicious_operation_groupings)]
fn surface_newton_refine(
surface: &NurbsSurface,
point: Point3,
u_init: f64,
v_init: f64,
u_min: f64,
u_max: f64,
v_min: f64,
v_max: f64,
tolerance: f64,
wrap_closed: bool,
) -> Result<(f64, f64, Point3), MathError> {
let mut u = u_init;
let mut v = v_init;
let advance = |x: f64, delta: f64, lo: f64, hi: f64, closed: bool| -> (f64, f64) {
if closed && hi > lo {
(lo + (x + delta - lo).rem_euclid(hi - lo), delta)
} else {
let next = (x + delta).clamp(lo, hi);
(next, next - x)
}
};
let (closed_u, closed_v) = (
wrap_closed && surface.is_periodic_u(),
wrap_closed && surface.is_periodic_v(),
);
for _ in 0..MAX_ITERATIONS {
let ders = surface.derivatives(u, v, 1);
let s_pt = Point3::new(ders[0][0].x(), ders[0][0].y(), ders[0][0].z());
let deriv_u = ders[1][0]; let deriv_v = ders[0][1]; let r = s_pt - point;
let dist = r.length();
if dist < tolerance {
return Ok((u, v, s_pt));
}
let du_len = deriv_u.length();
let dv_len = deriv_v.length();
let dot_du_r = deriv_u.dot(r);
let dot_dv_r = deriv_v.dot(r);
if du_len > 0.0 && dv_len > 0.0 {
let cos_u = dot_du_r.abs() / (du_len * dist);
let cos_v = dot_dv_r.abs() / (dv_len * dist);
if cos_u < tolerance && cos_v < tolerance {
return Ok((u, v, s_pt));
}
}
let j00 = deriv_u.dot(deriv_u);
let j01 = deriv_u.dot(deriv_v);
let j11 = deriv_v.dot(deriv_v);
let rhs0 = -dot_du_r;
let rhs1 = -dot_dv_r;
let det = j00.mul_add(j11, -(j01 * j01));
let (delta_u, delta_v) = if det.abs() < (j00 + j11).max(1e-30) * 1e-12 {
let lambda = (j00 + j11).max(1e-10) * 1e-4;
let j00r = j00 + lambda;
let j11r = j11 + lambda;
let det_r = j00r.mul_add(j11r, -(j01 * j01));
if det_r.abs() < 1e-30 {
if j00 > j11 {
(rhs0 / j00.max(1e-30), 0.0)
} else if j11 > 1e-30 {
(0.0, rhs1 / j11.max(1e-30))
} else {
return Ok((u, v, s_pt));
}
} else {
(
rhs0.mul_add(j11r, -(rhs1 * j01)) / det_r,
j00r.mul_add(rhs1, -(j01 * rhs0)) / det_r,
)
}
} else {
(
rhs0.mul_add(j11, -(rhs1 * j01)) / det,
j00.mul_add(rhs1, -(j01 * rhs0)) / det,
)
};
let (u_new, step_u) = advance(u, delta_u, u_min, u_max, closed_u);
let (v_new, step_v) = advance(v, delta_v, v_min, v_max, closed_v);
let step = (deriv_u * step_u + deriv_v * step_v).length();
if step < tolerance {
let pt = surface.evaluate(u_new, v_new);
return Ok((u_new, v_new, pt));
}
u = u_new;
v = v_new;
}
Err(MathError::ConvergenceFailure {
iterations: MAX_ITERATIONS,
})
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use crate::vec::Vec3;
const TOL: f64 = 1e-8;
fn line_curve() -> NurbsCurve {
NurbsCurve::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(10.0, 0.0, 0.0)],
vec![1.0, 1.0],
)
.expect("valid line")
}
fn quarter_circle() -> NurbsCurve {
let w = std::f64::consts::FRAC_1_SQRT_2;
NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
],
vec![1.0, w, 1.0],
)
.expect("valid quarter circle")
}
fn cubic_bezier() -> NurbsCurve {
NurbsCurve::new(
3,
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 2.0, 0.0),
Point3::new(3.0, 2.0, 0.0),
Point3::new(4.0, 0.0, 0.0),
],
vec![1.0, 1.0, 1.0, 1.0],
)
.expect("valid cubic")
}
fn flat_patch() -> NurbsSurface {
NurbsSurface::new(
1,
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![0.0, 0.0, 1.0, 1.0],
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
vec![Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
],
vec![vec![1.0, 1.0], vec![1.0, 1.0]],
)
.expect("valid flat patch")
}
#[test]
fn project_to_line() {
let c = line_curve();
let res =
project_point_to_curve(&c, Point3::new(5.0, 3.0, 0.0), TOL).expect("should converge");
assert!((res.parameter - 0.5).abs() < TOL, "u={}", res.parameter);
assert!((res.point.x() - 5.0).abs() < TOL);
assert!((res.point.y()).abs() < TOL);
assert!((res.distance - 3.0).abs() < TOL, "dist={}", res.distance);
}
#[test]
#[allow(clippy::suboptimal_flops)]
fn project_to_circle() {
let c = quarter_circle();
let res =
project_point_to_curve(&c, Point3::new(2.0, 2.0, 0.0), TOL).expect("should converge");
let expected = std::f64::consts::FRAC_1_SQRT_2;
assert!(
(res.point.x() - expected).abs() < 1e-6,
"x={} expected={}",
res.point.x(),
expected
);
assert!(
(res.point.y() - expected).abs() < 1e-6,
"y={} expected={}",
res.point.y(),
expected
);
let expected_dist = 2.0_f64.hypot(2.0) - 1.0;
assert!(
(res.distance - expected_dist).abs() < 1e-6,
"dist={} expected={}",
res.distance,
expected_dist
);
}
#[test]
fn project_endpoint() {
let c = cubic_bezier();
let res =
project_point_to_curve(&c, Point3::new(0.0, 0.01, 0.0), TOL).expect("should converge");
assert!(res.distance < 0.02, "dist={}", res.distance);
assert!(res.parameter < 0.1, "u={}", res.parameter);
}
#[test]
fn project_far_point() {
let c = cubic_bezier();
let res =
project_point_to_curve(&c, Point3::new(2.0, 100.0, 0.0), TOL).expect("should converge");
assert!(res.point.y() > 0.0);
assert!(res.distance < 100.0);
}
#[test]
fn project_on_curve() {
let c = cubic_bezier();
let u_orig = 0.3;
let pt_on = c.evaluate(u_orig);
let res = project_point_to_curve(&c, pt_on, TOL).expect("should converge");
assert!(res.distance < TOL, "dist={}", res.distance);
assert!(
(res.parameter - u_orig).abs() < 1e-4,
"u={} expected={}",
res.parameter,
u_orig
);
}
#[test]
fn project_across_knot_spans() {
let c = NurbsCurve::new(
3,
vec![0.0, 0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 2.0, 0.0),
Point3::new(2.0, -1.0, 0.5),
Point3::new(3.0, 2.5, 0.0),
Point3::new(4.0, 0.0, -0.5),
Point3::new(5.0, 1.5, 0.0),
Point3::new(6.0, 0.0, 0.0),
],
vec![1.0; 7],
)
.expect("valid cubic");
let dense = dense_samples(&c);
for u in [0.25, 0.25 + 1e-3, 0.5 - 1e-3, 0.5, 0.75, 0.75 + 1e-3] {
let on = c.evaluate(u);
let res = project_point_to_curve(&c, on, TOL).expect("should converge");
assert!(res.distance < TOL, "u={u}: dist {}", res.distance);
for off in [Vec3::new(0.0, 0.3, 0.2), Vec3::new(0.1, -0.4, 0.0)] {
let p = on + off;
let res = project_point_to_curve(&c, p, TOL).expect("should converge");
let brute = closest_sample(&dense, p);
assert!(
res.distance <= brute + 1e-9,
"u={u}: {} > {brute}",
res.distance
);
}
}
}
fn dense_samples(c: &NurbsCurve) -> Vec<Point3> {
(0..=100_000)
.map(|k| c.evaluate(f64::from(k) / 100_000.0))
.collect()
}
fn closest_sample(dense: &[Point3], p: Point3) -> f64 {
dense
.iter()
.map(|q| (*q - p).length())
.fold(f64::INFINITY, f64::min)
}
#[test]
fn project_beside_a_rational_corner() {
let c = NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 1.0, 0.0),
Point3::new(0.8, 0.2, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.2, 0.2, 0.0),
Point3::new(2.0, 1.0, 0.0),
],
vec![1.0, 2.0, 1.0, 0.5, 1.0],
)
.expect("valid quadratic");
for u in [0.48, 0.495, 0.4999, 0.5, 0.5001, 0.505, 0.52] {
let res = project_point_to_curve(&c, c.evaluate(u), TOL).expect("should converge");
assert!(res.distance < TOL, "u={u}: dist {}", res.distance);
}
let corner = project_point_to_curve(&c, Point3::new(1.038, -0.282, 0.0), TOL)
.expect("should converge");
assert!(
(corner.parameter - 0.5).abs() < 1e-9,
"u={}",
corner.parameter
);
let dense = dense_samples(&c);
for i in 0..6 {
for j in 0..6 {
let p = Point3::new(0.5 + 0.2 * f64::from(i), -1.0 + 0.2 * f64::from(j), 0.0);
let res = project_point_to_curve(&c, p, TOL).expect("should converge");
let brute = closest_sample(&dense, p);
assert!(
res.distance <= brute + 1e-9,
"{p:?}: {} > {brute}",
res.distance
);
}
}
}
#[test]
fn project_beside_a_short_span() {
let pts = [
(0.369, 2.254, 0.523),
(1.49, 0.935, 1.239),
(2.611, 2.353, 0.691),
(3.864, 0.113, 0.841),
(4.381, 1.241, 0.004),
(5.108, 0.051, 1.203),
(6.235, 2.743, 1.368),
];
let c = NurbsCurve::new(
2,
vec![0.0, 0.0, 0.0, 0.404, 0.404, 0.5225, 0.5233, 1.0, 1.0, 1.0],
pts.iter().map(|&(x, y, z)| Point3::new(x, y, z)).collect(),
vec![1.0; 7],
)
.expect("valid quadratic");
for u in [0.526, 0.527, 0.528, 0.53, 0.535, 0.54] {
let res = project_point_to_curve(&c, c.evaluate(u), TOL).expect("should converge");
assert!(
res.distance < TOL,
"u={u}: at {} dist {}",
res.parameter,
res.distance
);
}
}
#[test]
fn project_where_newton_overshoots() {
let pts = [
(0.214, 2.374, 0.023),
(1.728, 0.844, 0.444),
(2.048, 2.959, 0.461),
(3.318, 2.28, 1.146),
(4.482, 0.889, 1.641),
(5.768, 1.669, 1.668),
(6.109, 0.189, 0.556),
(7.604, 0.684, 1.081),
(8.086, 1.522, 1.079),
(9.709, 2.279, 1.852),
(10.577, 2.794, 1.997),
(11.147, 0.728, 1.253),
(12.239, 2.713, 1.874),
];
let c = NurbsCurve::new(
4,
vec![
0.0, 0.0, 0.0, 0.0, 0.0, 0.247, 0.247, 0.247, 0.247, 0.877, 0.877, 0.913, 0.913,
1.0, 1.0, 1.0, 1.0, 1.0,
],
pts.iter().map(|&(x, y, z)| Point3::new(x, y, z)).collect(),
vec![
1.138, 1.092, 0.938, 1.464, 0.77, 1.44, 1.286, 0.536, 1.319, 0.675, 1.002, 0.793,
1.279,
],
)
.expect("valid quartic");
let res = project_point_to_curve(&c, c.evaluate(0.847), TOL).expect("should converge");
assert!(
res.distance < TOL,
"u={}: dist {}",
res.parameter,
res.distance
);
}
#[test]
fn project_to_flat_quad() {
let s = flat_patch();
let res =
project_point_to_surface(&s, Point3::new(0.5, 0.5, 3.0), TOL).expect("should converge");
assert!((res.point.x() - 0.5).abs() < TOL, "x={}", res.point.x());
assert!((res.point.y() - 0.5).abs() < TOL, "y={}", res.point.y());
assert!((res.point.z()).abs() < TOL, "z={}", res.point.z());
assert!((res.distance - 3.0).abs() < TOL, "dist={}", res.distance);
}
#[test]
fn project_on_surface() {
let s = flat_patch();
let res =
project_point_to_surface(&s, Point3::new(0.3, 0.7, 0.0), TOL).expect("should converge");
assert!(res.distance < TOL, "dist={}", res.distance);
}
#[test]
fn project_above_surface() {
let s = flat_patch();
let res =
project_point_to_surface(&s, Point3::new(0.5, 0.5, 1.0), TOL).expect("should converge");
assert!(
(res.distance - 1.0).abs() < TOL,
"dist={} expected=1.0",
res.distance
);
assert!((res.u - 0.5).abs() < TOL, "u={}", res.u);
assert!((res.v - 0.5).abs() < TOL, "v={}", res.v);
}
fn apex_patch() -> NurbsSurface {
NurbsSurface::new(
1,
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![0.0, 0.0, 1.0, 1.0],
vec![
vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 0.0)], vec![Point3::new(-1.0, 0.0, 1.0), Point3::new(1.0, 0.0, 1.0)], ],
vec![vec![1.0, 1.0], vec![1.0, 1.0]],
)
.expect("valid apex patch")
}
#[test]
fn project_to_apex_singularity() {
let s = apex_patch();
let res = project_point_to_surface(&s, Point3::new(0.0, 1.0, 0.0), 1e-6)
.expect("should converge at cone apex singularity");
assert!(
res.point.x().abs() < 1e-6 && res.point.y().abs() < 1e-6 && res.point.z().abs() < 1e-6,
"nearest point should be apex, got ({:.4},{:.4},{:.4})",
res.point.x(),
res.point.y(),
res.point.z()
);
assert!(
(res.distance - 1.0).abs() < 1e-6,
"distance to apex should be 1.0, got {:.8}",
res.distance
);
}
#[test]
fn project_near_apex_off_axis() {
let s = apex_patch();
let res = project_point_to_surface(&s, Point3::new(0.02, 0.3, 0.05), 1e-6)
.expect("should converge near apex");
assert!(
(res.distance - 0.3).abs() < 0.02,
"expected distance ≈ 0.3, got {:.6}",
res.distance
);
assert!(
(res.point.z() - 0.05).abs() < 0.02,
"nearest point z should be ≈ 0.05, got z={:.4}",
res.point.z()
);
}
}