1use crate::MathError;
11use crate::nurbs::curve::NurbsCurve;
12use crate::nurbs::decompose::curve_to_bezier_segments;
13use crate::nurbs::surface::NurbsSurface;
14use crate::vec::Point3;
15
16const MAX_ITERATIONS: usize = 50;
18
19const SURFACE_GRID_SIZE: usize = 8;
21
22#[derive(Debug, Clone, Copy)]
28pub struct CurveProjection {
29 pub parameter: f64,
31 pub point: Point3,
33 pub distance: f64,
35}
36
37#[derive(Debug, Clone, Copy)]
39pub struct SurfaceProjection {
40 pub u: f64,
42 pub v: f64,
44 pub point: Point3,
46 pub distance: f64,
48}
49
50pub fn project_point_to_curve(
63 curve: &NurbsCurve,
64 point: Point3,
65 tolerance: f64,
66) -> Result<CurveProjection, MathError> {
67 let knots = curve.knots();
68 let p = curve.degree();
69 let u_min = knots[p];
70 let u_max = knots[knots.len() - p - 1];
71
72 let candidates = curve_coarse_search(curve, point)?;
73
74 let mut best_u = u_min;
76 let mut best_pt = curve.evaluate(u_min);
77 let mut best_dist = (best_pt - point).length();
78
79 for u_guess in candidates {
80 let (u_refined, pt_refined) =
81 curve_newton_refine(curve, point, u_guess, u_min, u_max, tolerance);
82 let dist = (pt_refined - point).length();
83 if dist < best_dist {
84 best_dist = dist;
85 best_u = u_refined;
86 best_pt = pt_refined;
87 }
88 }
89
90 Ok(CurveProjection {
91 parameter: best_u,
92 point: best_pt,
93 distance: best_dist,
94 })
95}
96
97#[allow(clippy::cast_precision_loss)]
103fn curve_coarse_search(curve: &NurbsCurve, point: Point3) -> Result<Vec<f64>, MathError> {
104 let segments = curve_to_bezier_segments(curve)?;
105
106 let mut samples: Vec<(f64, f64)> = Vec::new();
108
109 for seg in &segments {
110 let knots = seg.knots();
111 let p = seg.degree();
112 let u_start = knots[p];
113 let u_end = knots[knots.len() - p - 1];
114
115 let n_samples = (p + 1).max(5) * 2;
117 for i in 0..=n_samples {
118 let t = i as f64 / n_samples as f64;
119 let u = t.mul_add(u_end - u_start, u_start);
120 let pt = seg.evaluate(u);
121 let d_sq = (pt - point).length_squared();
122 samples.push((d_sq, u));
123 }
124 }
125
126 samples.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
128
129 let mut candidates = Vec::new();
131 let max_candidates = 5;
132 for &(_, u) in &samples {
133 if candidates.len() >= max_candidates {
134 break;
135 }
136 let dominated = candidates.iter().any(|&c: &f64| (c - u).abs() < 1e-10);
138 if !dominated {
139 candidates.push(u);
140 }
141 }
142
143 Ok(candidates)
144}
145
146#[allow(clippy::suspicious_operation_groupings)]
152fn curve_newton_refine(
153 curve: &NurbsCurve,
154 point: Point3,
155 u_init: f64,
156 u_min: f64,
157 u_max: f64,
158 tolerance: f64,
159) -> (f64, Point3) {
160 let tol_sq = tolerance * tolerance;
161 let mut u = u_init;
162 let mut best_u = u;
163 let mut best_dist_sq = f64::INFINITY;
164
165 for _ in 0..MAX_ITERATIONS {
166 let ders = curve.derivatives(u, 2);
167 let c_pt = Point3::new(ders[0].x(), ders[0].y(), ders[0].z());
168 let c_prime = ders[1]; let c_double_prime = ders[2]; let diff = c_pt - point; let dist_sq = diff.length_squared();
173
174 if dist_sq < best_dist_sq {
175 best_dist_sq = dist_sq;
176 best_u = u;
177 }
178
179 if dist_sq < tol_sq {
181 return (u, c_pt);
182 }
183
184 let f_val = c_prime.dot(diff);
186
187 let c_prime_len_sq = c_prime.length_squared();
190 if c_prime_len_sq > 1e-30 && dist_sq > tol_sq {
191 let cos_sq = (f_val * f_val) / (c_prime_len_sq * dist_sq);
192 if cos_sq < tol_sq {
193 return (u, c_pt);
194 }
195 }
196
197 let f_prime = c_double_prime.dot(diff) + c_prime_len_sq;
199
200 if f_prime.abs() < 1e-30 {
202 return (u, c_pt);
203 }
204
205 let delta_u = f_val / f_prime;
206 let u_new = (u - delta_u).clamp(u_min, u_max);
207
208 if u_new.is_nan() {
210 break;
211 }
212
213 let du = (u_new - u).abs();
215 if du < tolerance * (1.0 + u.abs()) {
216 let pt = curve.evaluate(u_new);
217 return (u_new, pt);
218 }
219
220 u = u_new;
221 }
222
223 let pt = curve.evaluate(best_u);
225 (best_u, pt)
226}
227
228pub fn project_point_to_surface(
242 surface: &NurbsSurface,
243 point: Point3,
244 tolerance: f64,
245) -> Result<SurfaceProjection, MathError> {
246 let (u_guess, v_guess) = surface_coarse_search(surface, point);
247
248 let knots_u = surface.knots_u();
249 let knots_v = surface.knots_v();
250 let pu = surface.degree_u();
251 let pv = surface.degree_v();
252 let u_min = knots_u[pu];
253 let u_max = knots_u[knots_u.len() - pu - 1];
254 let v_min = knots_v[pv];
255 let v_max = knots_v[knots_v.len() - pv - 1];
256
257 let (u_final, v_final, pt_final) = surface_newton_refine(
258 surface, point, u_guess, v_guess, u_min, u_max, v_min, v_max, tolerance,
259 )?;
260 let dist = (pt_final - point).length();
261
262 Ok(SurfaceProjection {
263 u: u_final,
264 v: v_final,
265 point: pt_final,
266 distance: dist,
267 })
268}
269
270#[allow(clippy::cast_precision_loss)]
273fn surface_coarse_search(surface: &NurbsSurface, point: Point3) -> (f64, f64) {
274 let knots_u = surface.knots_u();
275 let knots_v = surface.knots_v();
276 let pu = surface.degree_u();
277 let pv = surface.degree_v();
278 let u_min = knots_u[pu];
279 let u_max = knots_u[knots_u.len() - pu - 1];
280 let v_min = knots_v[pv];
281 let v_max = knots_v[knots_v.len() - pv - 1];
282
283 let mut best_u = u_min;
284 let mut best_v = v_min;
285 let mut best_dist_sq = f64::INFINITY;
286
287 let n = SURFACE_GRID_SIZE;
288 for i in 0..=n {
289 let u = (i as f64 / n as f64).mul_add(u_max - u_min, u_min);
290 for j in 0..=n {
291 let v = (j as f64 / n as f64).mul_add(v_max - v_min, v_min);
292 let pt = surface.evaluate(u, v);
293 let d_sq = (pt - point).length_squared();
294 if d_sq < best_dist_sq {
295 best_dist_sq = d_sq;
296 best_u = u;
297 best_v = v;
298 }
299 }
300 }
301
302 (best_u, best_v)
303}
304
305#[allow(clippy::too_many_arguments, clippy::similar_names)]
310#[allow(clippy::suspicious_operation_groupings)]
311fn surface_newton_refine(
312 surface: &NurbsSurface,
313 point: Point3,
314 u_init: f64,
315 v_init: f64,
316 u_min: f64,
317 u_max: f64,
318 v_min: f64,
319 v_max: f64,
320 tolerance: f64,
321) -> Result<(f64, f64, Point3), MathError> {
322 let mut u = u_init;
323 let mut v = v_init;
324
325 for _ in 0..MAX_ITERATIONS {
326 let ders = surface.derivatives(u, v, 1);
327 let s_pt = Point3::new(ders[0][0].x(), ders[0][0].y(), ders[0][0].z());
328 let deriv_u = ders[1][0]; let deriv_v = ders[0][1]; let r = s_pt - point; let dist = r.length();
334 if dist < tolerance {
335 return Ok((u, v, s_pt));
336 }
337
338 let du_len = deriv_u.length();
340 let dv_len = deriv_v.length();
341 let dot_du_r = deriv_u.dot(r);
342 let dot_dv_r = deriv_v.dot(r);
343 if du_len > 0.0 && dv_len > 0.0 {
344 let cos_u = dot_du_r.abs() / (du_len * dist);
345 let cos_v = dot_dv_r.abs() / (dv_len * dist);
346 if cos_u < tolerance && cos_v < tolerance {
347 return Ok((u, v, s_pt));
348 }
349 }
350
351 let j00 = deriv_u.dot(deriv_u);
355 let j01 = deriv_u.dot(deriv_v);
356 let j11 = deriv_v.dot(deriv_v);
357 let rhs0 = -dot_du_r;
359 let rhs1 = -dot_dv_r;
360
361 let det = j00.mul_add(j11, -(j01 * j01));
365 let (delta_u, delta_v) = if det.abs() < (j00 + j11).max(1e-30) * 1e-12 {
366 let lambda = (j00 + j11).max(1e-10) * 1e-4;
371 let j00r = j00 + lambda;
372 let j11r = j11 + lambda;
373 let det_r = j00r.mul_add(j11r, -(j01 * j01));
374 if det_r.abs() < 1e-30 {
375 if j00 > j11 {
378 (rhs0 / j00.max(1e-30), 0.0)
379 } else if j11 > 1e-30 {
380 (0.0, rhs1 / j11.max(1e-30))
381 } else {
382 return Ok((u, v, s_pt));
383 }
384 } else {
385 (
386 rhs0.mul_add(j11r, -(rhs1 * j01)) / det_r,
387 j00r.mul_add(rhs1, -(j01 * rhs0)) / det_r,
388 )
389 }
390 } else {
391 (
392 rhs0.mul_add(j11, -(rhs1 * j01)) / det,
393 j00.mul_add(rhs1, -(j01 * rhs0)) / det,
394 )
395 };
396
397 let u_new = (u + delta_u).clamp(u_min, u_max);
398 let v_new = (v + delta_v).clamp(v_min, v_max);
399
400 let step = (deriv_u * (u_new - u) + deriv_v * (v_new - v)).length();
402 if step < tolerance {
403 let pt = surface.evaluate(u_new, v_new);
404 return Ok((u_new, v_new, pt));
405 }
406
407 u = u_new;
408 v = v_new;
409 }
410
411 Err(MathError::ConvergenceFailure {
412 iterations: MAX_ITERATIONS,
413 })
414}
415
416#[cfg(test)]
421#[allow(clippy::expect_used)]
422mod tests {
423 use super::*;
424
425 const TOL: f64 = 1e-8;
426
427 fn line_curve() -> NurbsCurve {
429 NurbsCurve::new(
430 1,
431 vec![0.0, 0.0, 1.0, 1.0],
432 vec![Point3::new(0.0, 0.0, 0.0), Point3::new(10.0, 0.0, 0.0)],
433 vec![1.0, 1.0],
434 )
435 .expect("valid line")
436 }
437
438 fn quarter_circle() -> NurbsCurve {
440 let w = std::f64::consts::FRAC_1_SQRT_2;
441 NurbsCurve::new(
442 2,
443 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
444 vec![
445 Point3::new(1.0, 0.0, 0.0),
446 Point3::new(1.0, 1.0, 0.0),
447 Point3::new(0.0, 1.0, 0.0),
448 ],
449 vec![1.0, w, 1.0],
450 )
451 .expect("valid quarter circle")
452 }
453
454 fn cubic_bezier() -> NurbsCurve {
456 NurbsCurve::new(
457 3,
458 vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
459 vec![
460 Point3::new(0.0, 0.0, 0.0),
461 Point3::new(1.0, 2.0, 0.0),
462 Point3::new(3.0, 2.0, 0.0),
463 Point3::new(4.0, 0.0, 0.0),
464 ],
465 vec![1.0, 1.0, 1.0, 1.0],
466 )
467 .expect("valid cubic")
468 }
469
470 fn flat_patch() -> NurbsSurface {
472 NurbsSurface::new(
473 1,
474 1,
475 vec![0.0, 0.0, 1.0, 1.0],
476 vec![0.0, 0.0, 1.0, 1.0],
477 vec![
478 vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
479 vec![Point3::new(0.0, 1.0, 0.0), Point3::new(1.0, 1.0, 0.0)],
480 ],
481 vec![vec![1.0, 1.0], vec![1.0, 1.0]],
482 )
483 .expect("valid flat patch")
484 }
485
486 #[test]
489 fn project_to_line() {
490 let c = line_curve();
491 let res =
493 project_point_to_curve(&c, Point3::new(5.0, 3.0, 0.0), TOL).expect("should converge");
494 assert!((res.parameter - 0.5).abs() < TOL, "u={}", res.parameter);
495 assert!((res.point.x() - 5.0).abs() < TOL);
496 assert!((res.point.y()).abs() < TOL);
497 assert!((res.distance - 3.0).abs() < TOL, "dist={}", res.distance);
498 }
499
500 #[test]
501 #[allow(clippy::suboptimal_flops)]
502 fn project_to_circle() {
503 let c = quarter_circle();
504 let res =
506 project_point_to_curve(&c, Point3::new(2.0, 2.0, 0.0), TOL).expect("should converge");
507 let expected = std::f64::consts::FRAC_1_SQRT_2;
508 assert!(
509 (res.point.x() - expected).abs() < 1e-6,
510 "x={} expected={}",
511 res.point.x(),
512 expected
513 );
514 assert!(
515 (res.point.y() - expected).abs() < 1e-6,
516 "y={} expected={}",
517 res.point.y(),
518 expected
519 );
520 let expected_dist = 2.0_f64.hypot(2.0) - 1.0;
522 assert!(
523 (res.distance - expected_dist).abs() < 1e-6,
524 "dist={} expected={}",
525 res.distance,
526 expected_dist
527 );
528 }
529
530 #[test]
531 fn project_endpoint() {
532 let c = cubic_bezier();
533 let res =
535 project_point_to_curve(&c, Point3::new(0.0, 0.01, 0.0), TOL).expect("should converge");
536 assert!(res.distance < 0.02, "dist={}", res.distance);
537 assert!(res.parameter < 0.1, "u={}", res.parameter);
538 }
539
540 #[test]
541 fn project_far_point() {
542 let c = cubic_bezier();
543 let res =
545 project_point_to_curve(&c, Point3::new(2.0, 100.0, 0.0), TOL).expect("should converge");
546 assert!(res.point.y() > 0.0);
548 assert!(res.distance < 100.0);
549 }
550
551 #[test]
552 fn project_on_curve() {
553 let c = cubic_bezier();
554 let u_orig = 0.3;
556 let pt_on = c.evaluate(u_orig);
557 let res = project_point_to_curve(&c, pt_on, TOL).expect("should converge");
558 assert!(res.distance < TOL, "dist={}", res.distance);
559 assert!(
560 (res.parameter - u_orig).abs() < 1e-4,
561 "u={} expected={}",
562 res.parameter,
563 u_orig
564 );
565 }
566
567 #[test]
570 fn project_to_flat_quad() {
571 let s = flat_patch();
572 let res =
574 project_point_to_surface(&s, Point3::new(0.5, 0.5, 3.0), TOL).expect("should converge");
575 assert!((res.point.x() - 0.5).abs() < TOL, "x={}", res.point.x());
576 assert!((res.point.y() - 0.5).abs() < TOL, "y={}", res.point.y());
577 assert!((res.point.z()).abs() < TOL, "z={}", res.point.z());
578 assert!((res.distance - 3.0).abs() < TOL, "dist={}", res.distance);
579 }
580
581 #[test]
582 fn project_on_surface() {
583 let s = flat_patch();
584 let res =
586 project_point_to_surface(&s, Point3::new(0.3, 0.7, 0.0), TOL).expect("should converge");
587 assert!(res.distance < TOL, "dist={}", res.distance);
588 }
589
590 #[test]
591 fn project_above_surface() {
592 let s = flat_patch();
593 let res =
595 project_point_to_surface(&s, Point3::new(0.5, 0.5, 1.0), TOL).expect("should converge");
596 assert!(
597 (res.distance - 1.0).abs() < TOL,
598 "dist={} expected=1.0",
599 res.distance
600 );
601 assert!((res.u - 0.5).abs() < TOL, "u={}", res.u);
602 assert!((res.v - 0.5).abs() < TOL, "v={}", res.v);
603 }
604
605 fn apex_patch() -> NurbsSurface {
616 NurbsSurface::new(
617 1,
618 1,
619 vec![0.0, 0.0, 1.0, 1.0],
620 vec![0.0, 0.0, 1.0, 1.0],
621 vec![
622 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)], ],
625 vec![vec![1.0, 1.0], vec![1.0, 1.0]],
626 )
627 .expect("valid apex patch")
628 }
629
630 #[test]
638 fn project_to_apex_singularity() {
639 let s = apex_patch();
640 let res = project_point_to_surface(&s, Point3::new(0.0, 1.0, 0.0), 1e-6)
641 .expect("should converge at cone apex singularity");
642 assert!(
644 res.point.x().abs() < 1e-6 && res.point.y().abs() < 1e-6 && res.point.z().abs() < 1e-6,
645 "nearest point should be apex, got ({:.4},{:.4},{:.4})",
646 res.point.x(),
647 res.point.y(),
648 res.point.z()
649 );
650 assert!(
651 (res.distance - 1.0).abs() < 1e-6,
652 "distance to apex should be 1.0, got {:.8}",
653 res.distance
654 );
655 }
656
657 #[test]
660 fn project_near_apex_off_axis() {
661 let s = apex_patch();
662 let res = project_point_to_surface(&s, Point3::new(0.02, 0.3, 0.05), 1e-6)
665 .expect("should converge near apex");
666 assert!(
667 (res.distance - 0.3).abs() < 0.02,
668 "expected distance ≈ 0.3, got {:.6}",
669 res.distance
670 );
671 assert!(
673 (res.point.z() - 0.05).abs() < 0.02,
674 "nearest point z should be ≈ 0.05, got z={:.4}",
675 res.point.z()
676 );
677 }
678}