1use crate::{NurbsCurve, NurbsSurface, Vec3};
2use serde::Serialize;
3
4const EPSILON: f64 = 1e-12;
5const LINEAR_TOLERANCE: f64 = 1e-7;
6const MAX_NEWTON_ITERATIONS: usize = 50;
7
8#[derive(Clone, Copy, Debug, Serialize)]
9pub struct CurveProjection {
10 pub u: f64,
11 pub point: Vec3,
12 pub distance: f64,
13}
14
15#[derive(Clone, Copy, Debug, Serialize)]
16pub struct SurfaceProjection {
17 pub u: f64,
18 pub v: f64,
19 pub point: Vec3,
20 pub distance: f64,
21}
22
23fn interior_knots(knots: &[f64], degree: usize) -> Vec<f64> {
24 let start = knots[degree];
25 let end = knots[knots.len() - 1 - degree];
26 let mut result = Vec::new();
27 for &knot in knots {
28 if knot <= start + 1e-12 || knot >= end - 1e-12 {
29 continue;
30 }
31 if result
32 .last()
33 .is_none_or(|previous: &f64| (*previous - knot).abs() > 1e-12)
34 {
35 result.push(knot);
36 }
37 }
38 result
39}
40
41fn fit_parameter(value: f64, minimum: f64, maximum: f64, closed: bool) -> f64 {
42 if closed {
43 let period = maximum - minimum;
44 (value - minimum).rem_euclid(period) + minimum
45 } else {
46 value.clamp(minimum, maximum)
47 }
48}
49
50pub fn project_point_to_curve(curve: &NurbsCurve, point: Vec3) -> Result<CurveProjection, String> {
51 let [start, end] = curve.domain()?;
52 let mut breaks = vec![start];
53 breaks.extend(interior_knots(&curve.knots, curve.degree));
54 breaks.push(end);
55 let samples_per_span = 4usize.max(curve.degree + 2);
56 let mut best_u = start;
57 let mut best_distance_squared = f64::INFINITY;
58 for pair in breaks.windows(2) {
59 for index in 0..=samples_per_span {
60 let parameter = pair[0] + (pair[1] - pair[0]) * index as f64 / samples_per_span as f64;
61 let distance_squared = curve.evaluate(parameter)?.sub(point).length_squared();
62 if distance_squared < best_distance_squared {
63 best_distance_squared = distance_squared;
64 best_u = parameter;
65 }
66 }
67 }
68
69 let mut parameter = best_u;
70 for _ in 0..MAX_NEWTON_ITERATIONS {
71 let derivatives = curve.derivatives_small(parameter, 2)?;
72 let residual = derivatives[0].sub(point);
73 let f = derivatives[1].dot(residual);
74 let derivative = derivatives[2].dot(residual) + derivatives[1].length_squared();
75 let distance_squared = residual.length_squared();
76 if distance_squared < best_distance_squared {
77 best_distance_squared = distance_squared;
78 best_u = parameter;
79 }
80 let residual_length = distance_squared.sqrt();
81 let tangent_length = derivatives[1].length();
82 if residual_length <= LINEAR_TOLERANCE
83 || f.abs() <= EPSILON + 1e-10 * tangent_length * residual_length
84 || derivative.abs() <= EPSILON
85 {
86 break;
87 }
88 let mut step = -f / derivative;
89 let maximum_step = (end - start) / 4.0;
90 if step.abs() > maximum_step {
91 step = step.signum() * maximum_step;
92 }
93 let next = (parameter + step).clamp(start, end);
94 if (next - parameter).abs() <= 1e-15 * (end - start) {
95 parameter = next;
96 break;
97 }
98 parameter = next;
99 }
100 for candidate in [start, end, parameter] {
101 let distance_squared = curve.evaluate(candidate)?.sub(point).length_squared();
102 if distance_squared < best_distance_squared {
103 best_distance_squared = distance_squared;
104 best_u = candidate;
105 }
106 }
107 let projected = curve.evaluate(best_u)?;
108 Ok(CurveProjection {
109 u: best_u,
110 point: projected,
111 distance: projected.sub(point).length(),
112 })
113}
114
115fn project_linear_revolution(
116 surface: &NurbsSurface,
117 point: Vec3,
118) -> Result<Option<SurfaceProjection>, String> {
119 let rows = &surface.control_points;
120 if surface.degree_u != 2
121 || surface.degree_v != 1
122 || rows.len() < 5
123 || rows[0].len() != 2
124 || !surface.closed_directions()?.0
125 {
126 return Ok(None);
127 }
128 let base = rows[0][0].point()?.add(rows[4][0].point()?).scale(0.5);
129 let top = rows[0][1].point()?.add(rows[4][1].point()?).scale(0.5);
130 let axis_vector = top.sub(base);
131 let height = axis_vector.length();
132 if height <= EPSILON {
133 return Ok(None);
134 }
135 let axis = axis_vector.normalized()?;
136 let [v0, v1] = surface.domain_v()?;
137 let axial = point.sub(base).dot(axis).clamp(0.0, height);
138 let v = v0 + (v1 - v0) * axial / height;
139 let circle = surface.iso_curve_v(v)?;
140 let projection = project_point_to_curve(&circle, point)?;
141 let [u0, u1] = surface.domain_u()?;
142 let u = projection.u.clamp(u0, u1);
143 let projected = surface.evaluate(u, v)?;
144 Ok(Some(SurfaceProjection {
145 u,
146 v,
147 point: projected,
148 distance: projected.sub(point).length(),
149 }))
150}
151
152#[derive(Clone, Copy)]
153struct NewtonResult {
154 u: f64,
155 v: f64,
156 distance_squared: f64,
157 converged: bool,
158}
159
160fn surface_newton(
161 surface: &NurbsSurface,
162 point: Vec3,
163 seed_u: f64,
164 seed_v: f64,
165 domains: [f64; 4],
166 closed_u: bool,
167 closed_v: bool,
168) -> Result<NewtonResult, String> {
169 let [u0, u1, v0, v1] = domains;
170 let mut u = seed_u;
171 let mut v = seed_v;
172 let mut converged = false;
173 let mut best = NewtonResult {
174 u,
175 v,
176 distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
177 converged,
178 };
179 for _ in 0..MAX_NEWTON_ITERATIONS {
180 let derivatives = surface.derivatives_small(u, v, 2)?;
181 let residual = derivatives[0][0].sub(point);
182 let distance_squared = residual.length_squared();
183 if distance_squared < best.distance_squared {
184 best.u = u;
185 best.v = v;
186 best.distance_squared = distance_squared;
187 }
188 let f = derivatives[1][0].dot(residual);
189 let g = derivatives[0][1].dot(residual);
190 let residual_length = distance_squared.sqrt();
191 if residual_length <= LINEAR_TOLERANCE {
192 converged = true;
193 break;
194 }
195 let tangent_u_length = derivatives[1][0].length();
196 let tangent_v_length = derivatives[0][1].length();
197 let cosine_u = if tangent_u_length * residual_length <= EPSILON {
198 0.0
199 } else {
200 f.abs() / (tangent_u_length * residual_length)
201 };
202 let cosine_v = if tangent_v_length * residual_length <= EPSILON {
203 0.0
204 } else {
205 g.abs() / (tangent_v_length * residual_length)
206 };
207 if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
208 converged = true;
209 break;
210 }
211 let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
212 let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
213 let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
214 let determinant = j00 * j11 - j01 * j01;
215 if determinant.abs() <= EPSILON {
216 break;
217 }
218 let mut du = (-f * j11 + g * j01) / determinant;
219 let mut dv = (-g * j00 + f * j01) / determinant;
220 du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
221 dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
222 let next_u = fit_parameter(u + du, u0, u1, closed_u);
223 let next_v = fit_parameter(v + dv, v0, v1, closed_v);
224 let stalled =
225 (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
226 u = next_u;
227 v = next_v;
228 if stalled {
229 break;
230 }
231 }
232 let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
233 if final_distance_squared < best.distance_squared {
234 best.u = u;
235 best.v = v;
236 best.distance_squared = final_distance_squared;
237 }
238 best.converged = converged;
239 Ok(best)
240}
241
242pub fn project_point_to_surface(
243 surface: &NurbsSurface,
244 point: Vec3,
245) -> Result<SurfaceProjection, String> {
246 if let Some(analytic) = surface.analytic() {
250 if let Some(projection) = analytic.project(surface, point) {
251 return Ok(projection);
252 }
253 }
254 project_point_to_surface_general(surface, point)
255}
256
257pub fn project_point_to_surface_seeded(
267 surface: &NurbsSurface,
268 point: Vec3,
269 seed_u: f64,
270 seed_v: f64,
271) -> Result<SurfaceProjection, String> {
272 let [u0, u1] = surface.domain_u()?;
273 let [v0, v1] = surface.domain_v()?;
274 let (closed_u, closed_v) = surface.closed_directions()?;
275 let result = surface_newton(
276 surface,
277 point,
278 seed_u,
279 seed_v,
280 [u0, u1, v0, v1],
281 closed_u,
282 closed_v,
283 )?;
284 let projected = surface.evaluate(result.u, result.v)?;
285 Ok(SurfaceProjection {
286 u: result.u,
287 v: result.v,
288 point: projected,
289 distance: projected.sub(point).length(),
290 })
291}
292
293fn projection_seed_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
298 if let Some(grid) = surface.projection_grid.get() {
299 return Ok(grid);
300 }
301 let [u0, u1] = surface.domain_u()?;
302 let [v0, v1] = surface.domain_v()?;
303 let (closed_u, closed_v) = surface.closed_directions()?;
304 let mut breaks_u = vec![u0];
305 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
306 breaks_u.push(u1);
307 let mut breaks_v = vec![v0];
308 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
309 breaks_v.push(v1);
310 let samples_u = if closed_u {
311 8usize.max(surface.degree_u * 4)
312 } else {
313 3usize.max(surface.degree_u + 1)
314 };
315 let samples_v = if closed_v {
316 8usize.max(surface.degree_v * 4)
317 } else {
318 3usize.max(surface.degree_v + 1)
319 };
320 let mut grid = Vec::new();
321 for u_pair in breaks_u.windows(2) {
322 for v_pair in breaks_v.windows(2) {
323 for i in 0..=samples_u {
324 for j in 0..=samples_v {
325 let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
326 let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
327 grid.push((u, v, surface.evaluate(u, v)?));
328 }
329 }
330 }
331 }
332 Ok(surface.projection_grid.get_or_init(|| grid))
333}
334
335fn projection_dense_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
342 if let Some(grid) = surface.projection_dense_grid.get() {
343 return Ok(grid);
344 }
345 let [u0, u1] = surface.domain_u()?;
346 let [v0, v1] = surface.domain_v()?;
347 let mut breaks_u = vec![u0];
348 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
349 breaks_u.push(u1);
350 let mut breaks_v = vec![v0];
351 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
352 breaks_v.push(v1);
353 let count_u = 32usize.max((breaks_u.len() - 1) * 8);
354 let count_v = 16usize.max((breaks_v.len() - 1) * 8);
355 let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
356 for i in 0..=count_u {
357 for j in 0..=count_v {
358 let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
359 let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
360 grid.push((u, v, surface.evaluate(u, v)?));
361 }
362 }
363 Ok(surface.projection_dense_grid.get_or_init(|| grid))
364}
365
366const RING_EXPONENTS: usize = 22;
370const RING_INDICES: usize = 33; #[inline]
373fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
374 (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
375}
376
377fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
384 if let Some(grid) = surface.projection_ring_grid.get() {
385 return Ok(grid);
386 }
387 let [u0, u1] = surface.domain_u()?;
388 let [v0, v1] = surface.domain_v()?;
389 let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
390 for exponent in 1..=RING_EXPONENTS as i32 {
391 let offset = 1.0 / 2f64.powi(exponent);
392 let e = exponent as usize - 1;
393 let v_lo = v0 + (v1 - v0) * offset;
394 let v_hi = v1 - (v1 - v0) * offset;
395 let u_lo = u0 + (u1 - u0) * offset;
396 let u_hi = u1 - (u1 - u0) * offset;
397 for index in 0..=32 {
398 let u = u0 + (u1 - u0) * index as f64 / 32.0;
399 grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
400 grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
401 }
402 for index in 0..=32 {
403 let v = v0 + (v1 - v0) * index as f64 / 32.0;
404 grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
405 grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
406 }
407 }
408 Ok(surface.projection_ring_grid.get_or_init(|| grid))
409}
410
411pub(crate) fn project_point_to_surface_general(
412 surface: &NurbsSurface,
413 point: Vec3,
414) -> Result<SurfaceProjection, String> {
415 let [u0, u1] = surface.domain_u()?;
416 let [v0, v1] = surface.domain_v()?;
417 let (closed_u, closed_v) = surface.closed_directions()?;
418 let mut best = NewtonResult {
419 u: u0,
420 v: v0,
421 distance_squared: f64::INFINITY,
422 converged: false,
423 };
424 for &(u, v, sample) in projection_seed_grid(surface)? {
425 let distance_squared = sample.sub(point).length_squared();
426 if distance_squared < best.distance_squared {
427 best.u = u;
428 best.v = v;
429 best.distance_squared = distance_squared;
430 }
431 }
432 if let Some(revolution) = project_linear_revolution(surface, point)? {
433 if revolution.distance * revolution.distance < best.distance_squared {
434 best.u = revolution.u;
435 best.v = revolution.v;
436 best.distance_squared = revolution.distance * revolution.distance;
437 }
438 }
439 let first_newton = surface_newton(
440 surface,
441 point,
442 best.u,
443 best.v,
444 [u0, u1, v0, v1],
445 closed_u,
446 closed_v,
447 )?;
448 if first_newton.distance_squared < best.distance_squared {
449 best = first_newton;
450 }
451 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
452 let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
453 if degenerate || !first_newton.converged {
454 let mut grid = best;
455 for &(u, v, sample) in projection_dense_grid(surface)? {
456 let distance_squared = sample.sub(point).length_squared();
457 if distance_squared < grid.distance_squared {
458 grid.u = u;
459 grid.v = v;
460 grid.distance_squared = distance_squared;
461 }
462 }
463 if grid.distance_squared < best.distance_squared {
464 best = grid;
465 }
466 let polished = surface_newton(
467 surface,
468 point,
469 grid.u,
470 grid.v,
471 [u0, u1, v0, v1],
472 closed_u,
473 closed_v,
474 )?;
475 if polished.distance_squared < best.distance_squared {
476 best = polished;
477 }
478
479 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
480 if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
481 let ring_grid = projection_ring_grid(surface)?;
482 let mut ring = best;
483 let scan = |side: usize,
487 exponent_index: usize,
488 along_u: bool,
489 fixed: f64,
490 candidate: &mut NewtonResult| {
491 for index in 0..=32 {
492 let moving = if along_u {
493 u0 + (u1 - u0) * index as f64 / 32.0
494 } else {
495 v0 + (v1 - v0) * index as f64 / 32.0
496 };
497 let (u, v) = if along_u {
498 (moving, fixed)
499 } else {
500 (fixed, moving)
501 };
502 let distance_squared = ring_grid
503 [ring_slot(side, exponent_index, index as usize)]
504 .sub(point)
505 .length_squared();
506 if distance_squared < candidate.distance_squared {
507 candidate.u = u;
508 candidate.v = v;
509 candidate.distance_squared = distance_squared;
510 }
511 }
512 };
513 for exponent in 1..=22 {
514 let offset = 1.0 / 2f64.powi(exponent);
515 let e = exponent as usize - 1;
516 if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
517 scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
518 }
519 if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
520 scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
521 }
522 if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
523 scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
524 }
525 if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
526 scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
527 }
528 }
529 if ring.distance_squared < best.distance_squared {
530 best = ring;
531 }
532 let polished = surface_newton(
533 surface,
534 point,
535 ring.u,
536 ring.v,
537 [u0, u1, v0, v1],
538 closed_u,
539 closed_v,
540 )?;
541 if polished.distance_squared < best.distance_squared {
542 best = polished;
543 }
544 }
545 }
546 let projected = surface.evaluate(best.u, best.v)?;
547 Ok(SurfaceProjection {
548 u: best.u,
549 v: best.v,
550 point: projected,
551 distance: projected.sub(point).length(),
552 })
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558 use crate::{make_arc, make_sphere_surface};
559
560 #[test]
561 fn curve_projection_finds_arc_footpoint() {
562 let arc = make_arc(
563 Vec3::default(),
564 Vec3::new(1.0, 0.0, 0.0),
565 Vec3::new(0.0, 1.0, 0.0),
566 5.0,
567 0.0,
568 std::f64::consts::PI,
569 )
570 .unwrap();
571 let projection = project_point_to_curve(&arc, Vec3::new(3.0, 4.0, 2.0)).unwrap();
572 assert!(projection.point.sub(Vec3::new(3.0, 4.0, 0.0)).length() < 1e-8);
573 assert!((projection.distance - 2.0).abs() < 1e-8);
574 }
575
576 #[test]
577 fn surface_projection_handles_sphere_near_pole() {
578 let sphere = make_sphere_surface(Vec3::default(), 5.0, Vec3::new(0.0, 1.0, 0.0)).unwrap();
579 let on_surface = Vec3::new(0.15, (25.0_f64 - 0.15 * 0.15).sqrt(), 0.0);
580 let projection = project_point_to_surface(&sphere, on_surface).unwrap();
581 assert!(projection.distance < 1e-7);
582 }
583}