1use crate::curve::interior_knots;
2use crate::{NurbsCurve, NurbsSurface, Vec3, Vec4};
3use serde::Serialize;
4
5const EPSILON: f64 = 1e-12;
6const LINEAR_TOLERANCE: f64 = 1e-7;
7const MAX_NEWTON_ITERATIONS: usize = 50;
8
9#[derive(Clone, Copy, Debug, Serialize)]
10pub struct CurveProjection {
11 pub u: f64,
12 pub point: Vec3,
13 pub distance: f64,
14}
15
16#[derive(Clone, Copy, Debug, Serialize)]
17pub struct SurfaceProjection {
18 pub u: f64,
19 pub v: f64,
20 pub point: Vec3,
21 pub distance: f64,
22}
23
24fn fit_parameter(value: f64, minimum: f64, maximum: f64, closed: bool) -> f64 {
25 if closed {
26 let period = maximum - minimum;
27 (value - minimum).rem_euclid(period) + minimum
28 } else {
29 value.clamp(minimum, maximum)
30 }
31}
32
33fn witness_rescue_enabled() -> bool {
39 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
40 *ENABLED.get_or_init(|| std::env::var("BREP_PROJECTION_WITNESS").as_deref() != Ok("0"))
41}
42
43fn point_segment_distance_squared(point: Vec3, a: Vec3, b: Vec3) -> f64 {
49 let along = b.sub(a);
50 let length_squared = along.length_squared();
51 let fraction = if length_squared > 0.0 {
52 (point.sub(a).dot(along) / length_squared).clamp(0.0, 1.0)
53 } else {
54 0.0
55 };
56 a.add(along.scale(fraction)).sub(point).length_squared()
57}
58
59#[allow(clippy::too_many_arguments)]
67fn curve_newton(
68 curve: &NurbsCurve,
69 point: Vec3,
70 seed: f64,
71 start: f64,
72 end: f64,
73 closed: bool,
74 best_u: &mut f64,
75 best_distance_squared: &mut f64,
76) -> Result<f64, String> {
77 let mut parameter = seed;
78 for _ in 0..MAX_NEWTON_ITERATIONS {
79 let derivatives = curve.derivatives_small(parameter, 2)?;
80 let residual = derivatives[0].sub(point);
81 let f = derivatives[1].dot(residual);
82 let derivative = derivatives[2].dot(residual) + derivatives[1].length_squared();
83 let distance_squared = residual.length_squared();
84 if distance_squared < *best_distance_squared {
85 *best_distance_squared = distance_squared;
86 *best_u = parameter;
87 }
88 let residual_length = distance_squared.sqrt();
89 let tangent_length = derivatives[1].length();
90 if residual_length <= LINEAR_TOLERANCE
91 || f.abs() <= EPSILON + 1e-10 * tangent_length * residual_length
92 || derivative.abs() <= EPSILON
93 {
94 break;
95 }
96 let mut step = -f / derivative;
97 let maximum_step = (end - start) / 4.0;
98 if step.abs() > maximum_step {
99 step = step.signum() * maximum_step;
100 }
101 let next = fit_parameter(parameter + step, start, end, closed);
104 let moved = if closed {
109 let period = end - start;
110 let raw = next - parameter;
111 raw - period * (raw / period).round()
112 } else {
113 next - parameter
114 };
115 if moved.abs() <= 1e-15 * (end - start) {
116 parameter = next;
117 break;
118 }
119 parameter = next;
120 }
121 Ok(parameter)
122}
123
124fn bracketed_nearest(curve: &NurbsCurve, point: Vec3, low: f64, high: f64) -> Result<f64, String> {
132 const CONTRACTION: f64 = 0.618_033_988_749_894_9;
135 const CONTRACTIONS: usize = 64;
136 let width = high - low;
137 if !(width > 0.0) {
138 return Ok(low);
139 }
140 let (mut low, mut high) = (low, high);
141 let mut c = high - CONTRACTION * (high - low);
142 let mut d = low + CONTRACTION * (high - low);
143 let mut value_c = curve.evaluate(c)?.sub(point).length_squared();
144 let mut value_d = curve.evaluate(d)?.sub(point).length_squared();
145 for _ in 0..CONTRACTIONS {
146 if high - low <= 1e-15 * width {
147 break;
148 }
149 if value_c < value_d {
150 high = d;
151 d = c;
152 value_d = value_c;
153 c = high - CONTRACTION * (high - low);
154 value_c = curve.evaluate(c)?.sub(point).length_squared();
155 } else {
156 low = c;
157 c = d;
158 value_c = value_d;
159 d = low + CONTRACTION * (high - low);
160 value_d = curve.evaluate(d)?.sub(point).length_squared();
161 }
162 }
163 Ok((low + high) * 0.5)
164}
165
166pub fn project_point_to_curve(curve: &NurbsCurve, point: Vec3) -> Result<CurveProjection, String> {
167 let [start, end] = curve.domain()?;
168 let mut breaks = vec![start];
169 breaks.extend(interior_knots(&curve.knots, curve.degree));
170 breaks.push(end);
171 let samples_per_span = 4usize.max(curve.degree + 2);
172 let mut best_u = start;
173 let mut best_distance_squared = f64::INFINITY;
174 let mut start_point = None;
179 let mut end_point = None;
180 let mut previous: Option<(f64, Vec3)> = None;
183 let mut witness_distance_squared = f64::INFINITY;
184 let mut witness_low = start;
185 let mut witness_high = start;
186 for pair in breaks.windows(2) {
187 for index in 0..=samples_per_span {
188 let parameter = pair[0] + (pair[1] - pair[0]) * index as f64 / samples_per_span as f64;
189 let sample = curve.evaluate(parameter)?;
190 if start_point.is_none() {
191 start_point = Some(sample);
192 }
193 end_point = Some(sample);
194 let distance_squared = sample.sub(point).length_squared();
195 if distance_squared < best_distance_squared {
196 best_distance_squared = distance_squared;
197 best_u = parameter;
198 }
199 if let Some((previous_parameter, previous_sample)) = previous {
200 let segment = point_segment_distance_squared(point, previous_sample, sample);
201 if segment < witness_distance_squared {
202 witness_distance_squared = segment;
203 witness_low = previous_parameter;
204 witness_high = parameter;
205 }
206 }
207 previous = Some((parameter, sample));
208 }
209 }
210 const CLOSED_SEAM_TOLERANCE: f64 = 1e-6;
223 let closed = match (start_point, end_point) {
224 (Some(first), Some(last)) => first.sub(last).length() <= CLOSED_SEAM_TOLERANCE,
225 _ => false,
226 };
227
228 let parameter = curve_newton(
229 curve,
230 point,
231 best_u,
232 start,
233 end,
234 closed,
235 &mut best_u,
236 &mut best_distance_squared,
237 )?;
238 for candidate in [start, end, parameter] {
239 let distance_squared = curve.evaluate(candidate)?.sub(point).length_squared();
240 if distance_squared < best_distance_squared {
241 best_distance_squared = distance_squared;
242 best_u = candidate;
243 }
244 }
245 const WITNESS_FACTOR: f64 = 2.0;
277 if witness_distance_squared * (WITNESS_FACTOR * WITNESS_FACTOR) < best_distance_squared
278 && witness_rescue_enabled()
279 {
280 let bracketed = bracketed_nearest(curve, point, witness_low, witness_high)?;
281 let polished = curve_newton(
282 curve,
283 point,
284 bracketed,
285 start,
286 end,
287 closed,
288 &mut best_u,
289 &mut best_distance_squared,
290 )?;
291 for candidate in [bracketed, polished] {
292 let distance_squared = curve.evaluate(candidate)?.sub(point).length_squared();
293 if distance_squared < best_distance_squared {
294 best_distance_squared = distance_squared;
295 best_u = candidate;
296 }
297 }
298 }
299 let projected = curve.evaluate(best_u)?;
300 Ok(CurveProjection {
301 u: best_u,
302 point: projected,
303 distance: projected.sub(point).length(),
304 })
305}
306
307fn project_linear_revolution(
308 surface: &NurbsSurface,
309 point: Vec3,
310) -> Result<Option<SurfaceProjection>, String> {
311 let rows = &surface.control_points;
312 if surface.degree_u != 2
313 || surface.degree_v != 1
314 || rows.len() < 5
315 || rows[0].len() != 2
316 || !surface.closed_directions()?.0
317 {
318 return Ok(None);
319 }
320 let base = rows[0][0].point()?.add(rows[4][0].point()?).scale(0.5);
321 let top = rows[0][1].point()?.add(rows[4][1].point()?).scale(0.5);
322 let axis_vector = top.sub(base);
323 let height = axis_vector.length();
324 if height <= EPSILON {
325 return Ok(None);
326 }
327 let axis = axis_vector.normalized()?;
328 let [v0, v1] = surface.domain_v()?;
329 let axial = point.sub(base).dot(axis).clamp(0.0, height);
330 let v = v0 + (v1 - v0) * axial / height;
331 let circle = surface.iso_curve_v(v)?;
332 let projection = project_point_to_curve(&circle, point)?;
333 let [u0, u1] = surface.domain_u()?;
334 let u = projection.u.clamp(u0, u1);
335 let projected = surface.evaluate(u, v)?;
336 Ok(Some(SurfaceProjection {
337 u,
338 v,
339 point: projected,
340 distance: projected.sub(point).length(),
341 }))
342}
343
344#[derive(Clone, Copy)]
345struct NewtonResult {
346 u: f64,
347 v: f64,
348 distance_squared: f64,
349 converged: bool,
350}
351
352fn surface_newton(
353 surface: &NurbsSurface,
354 point: Vec3,
355 seed_u: f64,
356 seed_v: f64,
357 domains: [f64; 4],
358 closed_u: bool,
359 closed_v: bool,
360) -> Result<NewtonResult, String> {
361 let [u0, u1, v0, v1] = domains;
362 let mut u = seed_u;
363 let mut v = seed_v;
364 let mut converged = false;
365 let mut best = NewtonResult {
366 u,
367 v,
368 distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
369 converged,
370 };
371 for _ in 0..MAX_NEWTON_ITERATIONS {
372 let derivatives = surface.derivatives_small(u, v, 2)?;
373 let residual = derivatives[0][0].sub(point);
374 let distance_squared = residual.length_squared();
375 if distance_squared < best.distance_squared {
376 best.u = u;
377 best.v = v;
378 best.distance_squared = distance_squared;
379 }
380 let f = derivatives[1][0].dot(residual);
381 let g = derivatives[0][1].dot(residual);
382 let residual_length = distance_squared.sqrt();
383 if residual_length <= LINEAR_TOLERANCE {
384 converged = true;
385 break;
386 }
387 let tangent_u_length = derivatives[1][0].length();
388 let tangent_v_length = derivatives[0][1].length();
389 let cosine_u = if tangent_u_length * residual_length <= EPSILON {
390 0.0
391 } else {
392 f.abs() / (tangent_u_length * residual_length)
393 };
394 let cosine_v = if tangent_v_length * residual_length <= EPSILON {
395 0.0
396 } else {
397 g.abs() / (tangent_v_length * residual_length)
398 };
399 if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
400 converged = true;
401 break;
402 }
403 let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
404 let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
405 let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
406 let determinant = j00 * j11 - j01 * j01;
407 if determinant.abs() <= EPSILON {
408 break;
409 }
410 let mut du = (-f * j11 + g * j01) / determinant;
411 let mut dv = (-g * j00 + f * j01) / determinant;
412 du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
413 dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
414 let next_u = fit_parameter(u + du, u0, u1, closed_u);
415 let next_v = fit_parameter(v + dv, v0, v1, closed_v);
416 let stalled =
417 (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
418 u = next_u;
419 v = next_v;
420 if stalled {
421 break;
422 }
423 }
424 let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
425 if final_distance_squared < best.distance_squared {
426 best.u = u;
427 best.v = v;
428 best.distance_squared = final_distance_squared;
429 }
430 best.converged = converged;
431 Ok(best)
432}
433
434#[track_caller]
440pub fn project_point_to_surface(
441 surface: &NurbsSurface,
442 point: Vec3,
443) -> Result<SurfaceProjection, String> {
444 if let Some(analytic) = surface.analytic() {
448 if let Some(projection) = analytic.project(surface, point) {
449 return Ok(projection);
450 }
451 }
452 project_point_to_surface_general(surface, point)
453}
454
455pub fn project_point_to_surface_seeded(
465 surface: &NurbsSurface,
466 point: Vec3,
467 seed_u: f64,
468 seed_v: f64,
469) -> Result<SurfaceProjection, String> {
470 let [u0, u1] = surface.domain_u()?;
471 let [v0, v1] = surface.domain_v()?;
472 let (closed_u, closed_v) = surface.closed_directions()?;
473 let result = surface_newton(
474 surface,
475 point,
476 seed_u,
477 seed_v,
478 [u0, u1, v0, v1],
479 closed_u,
480 closed_v,
481 )?;
482 let projected = surface.evaluate(result.u, result.v)?;
483 Ok(SurfaceProjection {
484 u: result.u,
485 v: result.v,
486 point: projected,
487 distance: projected.sub(point).length(),
488 })
489}
490
491#[track_caller]
525pub(crate) fn project_point_to_surface_from_seed(
526 surface: &NurbsSurface,
527 point: Vec3,
528 seed: [f64; 2],
529) -> Result<SurfaceProjection, String> {
530 if let Some(analytic) = surface.analytic() {
531 if let Some(projection) = analytic.project(surface, point) {
532 return Ok(projection);
533 }
534 }
535 let caller = std::panic::Location::caller();
536 if !seeded_answer_admitted(caller) {
537 return project_point_to_surface_general(surface, point);
538 }
539 let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
540 if seed_audit_enabled() {
541 let nearest = project_point_to_surface_general(surface, point)?;
542 if seeded.point.sub(nearest.point).length() > LINEAR_TOLERANCE {
543 seed_audit_record(caller, surface, seed, &seeded, &nearest);
544 }
545 }
546 Ok(seeded)
547}
548
549fn seeded_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
552 static SITES: std::sync::OnceLock<Option<Option<Vec<String>>>> = std::sync::OnceLock::new();
553 let sites = SITES.get_or_init(|| {
554 if std::env::var("BREP_PROJECTION_FROM_SEED").as_deref() == Ok("0") {
555 return None;
556 }
557 Some(std::env::var("BREP_PROJECTION_FROM_SEED_SITES").ok().map(|value| {
558 value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect()
559 }))
560 });
561 match sites {
562 None => false,
563 Some(None) => true,
564 Some(Some(only)) => {
565 let location = format!("{}:{}", caller.file(), caller.line());
566 only.iter().any(|site| location.contains(site.as_str()))
567 }
568 }
569}
570
571fn seed_audit_enabled() -> bool {
572 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
573 *ENABLED.get_or_init(|| std::env::var_os("BREP_PROJECTION_AUDIT").is_some())
574}
575
576fn seed_audit_record(
580 caller: &std::panic::Location<'_>,
581 surface: &NurbsSurface,
582 seed: [f64; 2],
583 seeded: &SurfaceProjection,
584 nearest: &SurfaceProjection,
585) {
586 use std::io::Write;
587 static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
588 std::sync::OnceLock::new();
589 let sink = SINK.get_or_init(|| {
590 let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
591 let path = std::path::Path::new(&directory)
592 .join(format!("projection-seed-audit-{}.tsv", std::process::id()));
593 std::fs::OpenOptions::new()
594 .create(true)
595 .append(true)
596 .open(path)
597 .ok()
598 .map(std::sync::Mutex::new)
599 });
600 if let Some(file) = sink {
601 if let Ok(mut file) = file.lock() {
602 let _ = writeln!(
603 file,
604 "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.6e}\t{}x{} cp {}x{}",
605 caller.file(),
606 caller.line(),
607 seeded.distance,
608 nearest.distance,
609 seed[0],
610 seed[1],
611 seeded.u,
612 seeded.v,
613 nearest.u,
614 nearest.v,
615 seeded.point.sub(nearest.point).length(),
616 surface.degree_u,
617 surface.degree_v,
618 surface.control_points.len(),
619 surface.control_points.first().map(Vec::len).unwrap_or(0),
620 );
621 }
622 }
623}
624
625fn projection_seed_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
630 if let Some(grid) = surface.projection_grid.get() {
631 return Ok(grid);
632 }
633 let [u0, u1] = surface.domain_u()?;
634 let [v0, v1] = surface.domain_v()?;
635 let (closed_u, closed_v) = surface.closed_directions()?;
636 let mut breaks_u = vec![u0];
637 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
638 breaks_u.push(u1);
639 let mut breaks_v = vec![v0];
640 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
641 breaks_v.push(v1);
642 let samples_u = if closed_u {
643 8usize.max(surface.degree_u * 4)
644 } else {
645 3usize.max(surface.degree_u + 1)
646 };
647 let samples_v = if closed_v {
648 8usize.max(surface.degree_v * 4)
649 } else {
650 3usize.max(surface.degree_v + 1)
651 };
652 let mut grid = Vec::new();
653 for u_pair in breaks_u.windows(2) {
654 for v_pair in breaks_v.windows(2) {
655 for i in 0..=samples_u {
656 for j in 0..=samples_v {
657 let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
658 let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
659 grid.push((u, v, surface.evaluate(u, v)?));
660 }
661 }
662 }
663 }
664 Ok(surface.projection_grid.get_or_init(|| grid))
665}
666
667fn projection_dense_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
674 if let Some(grid) = surface.projection_dense_grid.get() {
675 return Ok(grid);
676 }
677 let [u0, u1] = surface.domain_u()?;
678 let [v0, v1] = surface.domain_v()?;
679 let mut breaks_u = vec![u0];
680 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
681 breaks_u.push(u1);
682 let mut breaks_v = vec![v0];
683 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
684 breaks_v.push(v1);
685 let count_u = 32usize.max((breaks_u.len() - 1) * 8);
686 let count_v = 16usize.max((breaks_v.len() - 1) * 8);
687 let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
688 for i in 0..=count_u {
689 for j in 0..=count_v {
690 let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
691 let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
692 grid.push((u, v, surface.evaluate(u, v)?));
693 }
694 }
695 Ok(surface.projection_dense_grid.get_or_init(|| grid))
696}
697
698const RING_EXPONENTS: usize = 22;
702const RING_INDICES: usize = 33; #[inline]
705fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
706 (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
707}
708
709fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
716 if let Some(grid) = surface.projection_ring_grid.get() {
717 return Ok(grid);
718 }
719 let [u0, u1] = surface.domain_u()?;
720 let [v0, v1] = surface.domain_v()?;
721 let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
722 for exponent in 1..=RING_EXPONENTS as i32 {
723 let offset = 1.0 / 2f64.powi(exponent);
724 let e = exponent as usize - 1;
725 let v_lo = v0 + (v1 - v0) * offset;
726 let v_hi = v1 - (v1 - v0) * offset;
727 let u_lo = u0 + (u1 - u0) * offset;
728 let u_hi = u1 - (u1 - u0) * offset;
729 for index in 0..=32 {
730 let u = u0 + (u1 - u0) * index as f64 / 32.0;
731 grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
732 grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
733 }
734 for index in 0..=32 {
735 let v = v0 + (v1 - v0) * index as f64 / 32.0;
736 grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
737 grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
738 }
739 }
740 Ok(surface.projection_ring_grid.get_or_init(|| grid))
741}
742
743fn nearest_seed_answer(
753 surface: &NurbsSurface,
754 point: Vec3,
755 [u0, u1, v0, v1]: [f64; 4],
756 closed_u: bool,
757 closed_v: bool,
758) -> Result<NewtonResult, String> {
759 let mut best = NewtonResult {
760 u: u0,
761 v: v0,
762 distance_squared: f64::INFINITY,
763 converged: false,
764 };
765 for &(u, v, sample) in projection_seed_grid(surface)? {
766 let distance_squared = sample.sub(point).length_squared();
767 if distance_squared < best.distance_squared {
768 best.u = u;
769 best.v = v;
770 best.distance_squared = distance_squared;
771 }
772 }
773 if let Some(revolution) = project_linear_revolution(surface, point)? {
774 if revolution.distance * revolution.distance < best.distance_squared {
775 best.u = revolution.u;
776 best.v = revolution.v;
777 best.distance_squared = revolution.distance * revolution.distance;
778 }
779 }
780 let first_newton = surface_newton(
781 surface,
782 point,
783 best.u,
784 best.v,
785 [u0, u1, v0, v1],
786 closed_u,
787 closed_v,
788 )?;
789 if first_newton.distance_squared < best.distance_squared {
790 best = first_newton;
791 }
792 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
793 let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
794 if degenerate || !first_newton.converged {
795 let mut grid = best;
796 for &(u, v, sample) in projection_dense_grid(surface)? {
797 let distance_squared = sample.sub(point).length_squared();
798 if distance_squared < grid.distance_squared {
799 grid.u = u;
800 grid.v = v;
801 grid.distance_squared = distance_squared;
802 }
803 }
804 if grid.distance_squared < best.distance_squared {
805 best = grid;
806 }
807 let polished = surface_newton(
808 surface,
809 point,
810 grid.u,
811 grid.v,
812 [u0, u1, v0, v1],
813 closed_u,
814 closed_v,
815 )?;
816 if polished.distance_squared < best.distance_squared {
817 best = polished;
818 }
819
820 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
821 if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
822 let ring_grid = projection_ring_grid(surface)?;
823 let mut ring = best;
824 let scan = |side: usize,
828 exponent_index: usize,
829 along_u: bool,
830 fixed: f64,
831 candidate: &mut NewtonResult| {
832 for index in 0..=32 {
833 let moving = if along_u {
834 u0 + (u1 - u0) * index as f64 / 32.0
835 } else {
836 v0 + (v1 - v0) * index as f64 / 32.0
837 };
838 let (u, v) = if along_u {
839 (moving, fixed)
840 } else {
841 (fixed, moving)
842 };
843 let distance_squared = ring_grid
844 [ring_slot(side, exponent_index, index as usize)]
845 .sub(point)
846 .length_squared();
847 if distance_squared < candidate.distance_squared {
848 candidate.u = u;
849 candidate.v = v;
850 candidate.distance_squared = distance_squared;
851 }
852 }
853 };
854 for exponent in 1..=22 {
855 let offset = 1.0 / 2f64.powi(exponent);
856 let e = exponent as usize - 1;
857 if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
858 scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
859 }
860 if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
861 scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
862 }
863 if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
864 scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
865 }
866 if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
867 scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
868 }
869 }
870 if ring.distance_squared < best.distance_squared {
871 best = ring;
872 }
873 let polished = surface_newton(
874 surface,
875 point,
876 ring.u,
877 ring.v,
878 [u0, u1, v0, v1],
879 closed_u,
880 closed_v,
881 )?;
882 if polished.distance_squared < best.distance_squared {
883 best = polished;
884 }
885 }
886 }
887 Ok(best)
888}
889
890fn global_stage_enabled() -> bool {
895 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
896 *ENABLED.get_or_init(|| std::env::var("BREP_PROJECTION_GLOBAL").as_deref() != Ok("0"))
897}
898
899pub(crate) fn global_site_filter_active() -> bool {
908 std::env::var_os("BREP_PROJECTION_GLOBAL_SITES").is_some() || std::env::var_os("BREP_PROJECTION_GLOBAL_EXCEPT").is_some()
909}
910
911fn global_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
912 fn list(name: &str) -> Option<Vec<String>> {
913 std::env::var(name)
914 .ok()
915 .map(|value| value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect())
916 }
917 static SITES: std::sync::OnceLock<(Option<Vec<String>>, Option<Vec<String>>)> =
918 std::sync::OnceLock::new();
919 let (only, except) = SITES.get_or_init(|| {
920 (list("BREP_PROJECTION_GLOBAL_SITES"), list("BREP_PROJECTION_GLOBAL_EXCEPT"))
921 });
922 if only.is_none() && except.is_none() {
923 return true;
924 }
925 let location = format!("{}:{}", caller.file(), caller.line());
926 let named = |sites: &Vec<String>| sites.iter().any(|site| location.contains(site.as_str()));
927 only.as_ref().is_none_or(named) && !except.as_ref().is_some_and(named)
928}
929
930fn audit_record(caller: &std::panic::Location<'_>, surface: &NurbsSurface, local: &NewtonResult, global: &NewtonResult) {
936 use std::io::Write;
937 static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
938 std::sync::OnceLock::new();
939 let sink = SINK.get_or_init(|| {
940 let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
941 let path = std::path::Path::new(&directory)
942 .join(format!("projection-audit-{}.tsv", std::process::id()));
943 std::fs::OpenOptions::new()
944 .create(true)
945 .append(true)
946 .open(path)
947 .ok()
948 .map(std::sync::Mutex::new)
949 });
950 if let Some(file) = sink {
951 if let Ok(mut file) = file.lock() {
952 let _ = writeln!(
953 file,
954 "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{}x{} cp {}x{}",
955 caller.file(),
956 caller.line(),
957 local.distance_squared.sqrt(),
958 global.distance_squared.sqrt(),
959 local.u,
960 local.v,
961 global.u,
962 global.v,
963 surface.degree_u,
964 surface.degree_v,
965 surface.control_points.len(),
966 surface.control_points.first().map(Vec::len).unwrap_or(0),
967 );
968 }
969 }
970}
971
972#[track_caller]
1048pub(crate) fn project_point_to_surface_general(
1049 surface: &NurbsSurface,
1050 point: Vec3,
1051) -> Result<SurfaceProjection, String> {
1052 let [u0, u1] = surface.domain_u()?;
1053 let [v0, v1] = surface.domain_v()?;
1054 let (closed_u, closed_v) = surface.closed_directions()?;
1055 let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1056 let mut best = local;
1057 if global_stage_enabled() {
1058 global_minimum(
1059 surface,
1060 point,
1061 [u0, u1, v0, v1],
1062 (closed_u, closed_v),
1063 &mut best,
1064 &mut GlobalStats::default(),
1065 )?;
1066 if best.u != local.u || best.v != local.v {
1067 let caller = std::panic::Location::caller();
1068 if global_answer_admitted(caller) {
1069 audit_record(caller, surface, &local, &best);
1070 } else {
1071 best = local;
1072 }
1073 }
1074 }
1075 let projected = surface.evaluate(best.u, best.v)?;
1076 Ok(SurfaceProjection {
1077 u: best.u,
1078 v: best.v,
1079 point: projected,
1080 distance: projected.sub(point).length(),
1081 })
1082}
1083
1084pub(crate) fn project_point_to_surface_from_nearest_seed(
1094 surface: &NurbsSurface,
1095 point: Vec3,
1096) -> Result<SurfaceProjection, String> {
1097 if let Some(analytic) = surface.analytic() {
1098 if let Some(projection) = analytic.project(surface, point) {
1099 return Ok(projection);
1100 }
1101 }
1102 let [u0, u1] = surface.domain_u()?;
1103 let [v0, v1] = surface.domain_v()?;
1104 let (closed_u, closed_v) = surface.closed_directions()?;
1105 let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1106 let projected = surface.evaluate(local.u, local.v)?;
1107 Ok(SurfaceProjection {
1108 u: local.u,
1109 v: local.v,
1110 point: projected,
1111 distance: projected.sub(point).length(),
1112 })
1113}
1114
1115#[doc(hidden)]
1118pub fn project_point_to_surface_general_lanes(
1119 surface: &NurbsSurface,
1120 point: Vec3,
1121 run_global: bool,
1122) -> Result<(SurfaceProjection, GlobalStats), String> {
1123 let [u0, u1] = surface.domain_u()?;
1124 let [v0, v1] = surface.domain_v()?;
1125 let (closed_u, closed_v) = surface.closed_directions()?;
1126 let mut best = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1127 let mut stats = GlobalStats::default();
1128 if run_global {
1129 global_minimum(surface, point, [u0, u1, v0, v1], (closed_u, closed_v), &mut best, &mut stats)?;
1130 }
1131 let projected = surface.evaluate(best.u, best.v)?;
1132 Ok((
1133 SurfaceProjection {
1134 u: best.u,
1135 v: best.v,
1136 point: projected,
1137 distance: projected.sub(point).length(),
1138 },
1139 stats,
1140 ))
1141}
1142
1143#[doc(hidden)]
1146#[derive(Clone, Debug, Default)]
1147pub struct BasinCensus {
1148 pub cells: usize,
1151 pub descents: usize,
1153 pub unconverged: usize,
1155 pub cells_with_two_basins: usize,
1159 pub foot_cells_with_two_basins: usize,
1162 pub least: f64,
1164 pub least_uv: (f64, f64),
1165 pub foot_cell_descents: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)>,
1169}
1170
1171#[doc(hidden)]
1176pub fn project_point_to_surface_basin_census(
1177 surface: &NurbsSurface,
1178 point: Vec3,
1179 answer_distance: f64,
1180 bar: f64,
1181) -> Result<BasinCensus, String> {
1182 let mut census = BasinCensus {
1183 least: f64::INFINITY,
1184 ..BasinCensus::default()
1185 };
1186 let Some(cells) = projection_cells(surface)? else {
1187 return Ok(census);
1188 };
1189 let columns = cells.vs.len() - 1;
1190 let mut spreads: Vec<(usize, f64)> = Vec::new();
1191 let mut runs: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)> = Vec::new();
1192 for (cell, cell_box) in cells.boxes.iter().enumerate() {
1193 if cell_box.lower_bound(point) >= answer_distance {
1194 continue;
1195 }
1196 census.cells += 1;
1197 let (row, column) = (cell / columns, cell % columns);
1198 let rectangle = [cells.us[row], cells.us[row + 1], cells.vs[column], cells.vs[column + 1]];
1199 let middle = ((rectangle[0] + rectangle[1]) * 0.5, (rectangle[2] + rectangle[3]) * 0.5);
1200 let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
1201 for seed in [
1202 (rectangle[0], rectangle[2]),
1203 (rectangle[1], rectangle[2]),
1204 (rectangle[0], rectangle[3]),
1205 (rectangle[1], rectangle[3]),
1206 middle,
1207 ] {
1208 census.descents += 1;
1209 let (found, _) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1210 let distance = found.distance_squared.sqrt();
1211 runs.push((cell, rectangle, seed, (found.u, found.v), distance, found.converged));
1212 if distance < census.least {
1213 census.least = distance;
1214 census.least_uv = (found.u, found.v);
1215 }
1216 if !found.converged {
1217 census.unconverged += 1;
1218 continue;
1219 }
1220 low = low.min(distance.max(LINEAR_TOLERANCE));
1221 high = high.max(distance.max(LINEAR_TOLERANCE));
1222 }
1223 if high - low > bar {
1224 spreads.push((cell, high - low));
1225 }
1226 }
1227 census.cells_with_two_basins = spreads.len();
1228 let (u, v) = census.least_uv;
1229 let foot_cells: Vec<usize> = spreads
1230 .iter()
1231 .map(|&(cell, _)| cell)
1232 .filter(|&cell| {
1233 let (row, column) = (cell / columns, cell % columns);
1234 cells.us[row] <= u && u <= cells.us[row + 1] && cells.vs[column] <= v && v <= cells.vs[column + 1]
1235 })
1236 .collect();
1237 census.foot_cells_with_two_basins = foot_cells.len();
1238 census.foot_cell_descents = runs.into_iter().filter(|run| foot_cells.contains(&run.0)).collect();
1239 Ok(census)
1240}
1241
1242#[doc(hidden)]
1244#[derive(Clone, Copy, Debug, Default)]
1245pub struct GlobalStats {
1246 pub settled_by_floor: bool,
1248 pub candidates: usize,
1250 pub refinements: usize,
1252 pub continuations: usize,
1254 pub replaced: bool,
1256 pub hull_lower_bound: f64,
1259}
1260
1261#[derive(Clone, Debug)]
1264struct CellBox {
1265 axes: [Vec3; 3],
1266 low: [f64; 3],
1267 high: [f64; 3],
1268}
1269
1270impl CellBox {
1271 fn lower_bound(&self, point: Vec3) -> f64 {
1274 let mut squared = 0.0;
1275 for axis in 0..3 {
1276 let coordinate = point.dot(self.axes[axis]);
1277 let excess = (self.low[axis] - coordinate).max(coordinate - self.high[axis]).max(0.0);
1278 squared += excess * excess;
1279 }
1280 squared.sqrt()
1281 }
1282}
1283
1284#[derive(Clone, Copy, Debug)]
1287struct CellTreeNode {
1288 centre: Vec3,
1289 radius: f64,
1290 children: Option<(u32, u32)>,
1291 cell: u32,
1292}
1293
1294#[derive(Clone, Debug)]
1297pub(crate) struct ProjectionCells {
1298 us: Vec<f64>,
1299 vs: Vec<f64>,
1300 nodes: Vec<Vec3>,
1302 boxes: Vec<CellBox>,
1304 tree: Vec<CellTreeNode>,
1306 resolution: f64,
1307}
1308
1309fn lattice_parameters(knots: &[f64], degree: usize, closed: bool) -> Vec<f64> {
1313 let [start, end] = crate::curve::knot_domain(knots, degree);
1314 let samples = if closed {
1315 8usize.max(degree * 4)
1316 } else {
1317 3usize.max(degree + 1)
1318 };
1319 let mut breaks = vec![start];
1320 breaks.extend(interior_knots(knots, degree));
1321 breaks.push(end);
1322 let mut parameters = Vec::with_capacity((breaks.len() - 1) * samples + 1);
1323 for pair in breaks.windows(2) {
1324 for index in 0..samples {
1325 parameters.push(pair[0] + (pair[1] - pair[0]) * index as f64 / samples as f64);
1326 }
1327 }
1328 parameters.push(end);
1329 parameters
1330}
1331
1332fn refine_to_lattice(net: &mut Vec<Vec<Vec4>>, knots: &mut Vec<f64>, degree: usize, parameters: &[f64]) {
1341 let p = degree;
1342 let n = net.len() - 1;
1343 let mut inserted = Vec::new();
1344 for ¶meter in parameters {
1345 let multiplicity = knots.iter().filter(|&&knot| knot == parameter).count();
1346 for _ in multiplicity..p {
1347 inserted.push(parameter);
1348 }
1349 }
1350 if inserted.is_empty() || p == 0 {
1351 return;
1352 }
1353 inserted.sort_by(f64::total_cmp);
1354 let find_span = |parameter: f64| -> usize {
1355 if parameter >= knots[n + 1] {
1356 return n;
1357 }
1358 (knots.partition_point(|&knot| knot <= parameter) - 1).clamp(p, n)
1359 };
1360 let r = inserted.len() - 1;
1361 let m = n + p + 1;
1362 let a = find_span(inserted[0]);
1363 let b = find_span(inserted[r]) + 1;
1364 let columns = net[0].len();
1365 let blank = vec![
1366 Vec4 {
1367 x: 0.0,
1368 y: 0.0,
1369 z: 0.0,
1370 w: 0.0,
1371 };
1372 columns
1373 ];
1374 let mut refined = vec![blank; n + r + 2];
1375 let mut refined_knots = vec![0.0; m + r + 2];
1376 for j in 0..=a - p {
1377 refined[j] = net[j].clone();
1378 }
1379 for j in b - 1..=n {
1380 refined[j + r + 1] = net[j].clone();
1381 }
1382 refined_knots[..=a].copy_from_slice(&knots[..=a]);
1383 for j in b + p..=m {
1384 refined_knots[j + r + 1] = knots[j];
1385 }
1386 let mut i = b + p - 1;
1387 let mut k = b + p + r;
1388 for j in (0..=r).rev() {
1389 while inserted[j] <= knots[i] && i > a {
1390 refined[k - p - 1] = net[i - p - 1].clone();
1391 refined_knots[k] = knots[i];
1392 k -= 1;
1393 i -= 1;
1394 }
1395 refined[k - p - 1] = refined[k - p].clone();
1396 for l in 1..=p {
1397 let index = k - p + l;
1398 let numerator = refined_knots[k + l] - inserted[j];
1399 if numerator == 0.0 {
1400 refined[index - 1] = refined[index].clone();
1401 } else {
1402 let alpha = numerator / (refined_knots[k + l] - knots[i - p + l]);
1403 let (head, tail) = refined.split_at_mut(index);
1404 for (before, &here) in head[index - 1].iter_mut().zip(&tail[0]) {
1405 *before = before.scale(alpha).add(here.scale(1.0 - alpha));
1406 }
1407 }
1408 }
1409 refined_knots[k] = inserted[j];
1410 k = k.saturating_sub(1);
1411 }
1412 *net = refined;
1413 *knots = refined_knots;
1414}
1415
1416fn supporting_range(knots: &[f64], degree: usize, count: usize, low: f64, high: f64) -> (usize, usize) {
1419 let first_span = knots.partition_point(|&knot| knot <= low).saturating_sub(1);
1420 let last_span = knots.partition_point(|&knot| knot < high).saturating_sub(1);
1421 (
1422 first_span.saturating_sub(degree).min(count - 1),
1423 last_span.max(first_span).min(count - 1),
1424 )
1425}
1426
1427fn unit_or(vector: Vec3, fallback: Vec3) -> Vec3 {
1428 let length = vector.length();
1429 if length > 0.0 && length.is_finite() {
1430 vector.scale(1.0 / length)
1431 } else {
1432 fallback
1433 }
1434}
1435
1436fn enclosing_sphere(a: (Vec3, f64), b: (Vec3, f64)) -> (Vec3, f64) {
1437 let offset = b.0.sub(a.0);
1438 let separation = offset.length();
1439 if separation + b.1 <= a.1 {
1440 return a;
1441 }
1442 if separation + a.1 <= b.1 {
1443 return b;
1444 }
1445 let radius = (separation + a.1 + b.1) * 0.5;
1446 let centre = a.0.add(offset.scale((radius - a.1) / separation));
1447 (centre, radius)
1448}
1449
1450fn build_cell_tree(
1451 tree: &mut Vec<CellTreeNode>,
1452 spheres: &[(Vec3, f64)],
1453 columns: usize,
1454 [row_low, row_high, column_low, column_high]: [usize; 4],
1455) -> u32 {
1456 if row_high - row_low == 1 && column_high - column_low == 1 {
1457 let cell = row_low * columns + column_low;
1458 tree.push(CellTreeNode {
1459 centre: spheres[cell].0,
1460 radius: spheres[cell].1,
1461 children: None,
1462 cell: cell as u32,
1463 });
1464 return (tree.len() - 1) as u32;
1465 }
1466 let (first, second) = if row_high - row_low >= column_high - column_low {
1467 let middle = (row_low + row_high) / 2;
1468 (
1469 [row_low, middle, column_low, column_high],
1470 [middle, row_high, column_low, column_high],
1471 )
1472 } else {
1473 let middle = (column_low + column_high) / 2;
1474 (
1475 [row_low, row_high, column_low, middle],
1476 [row_low, row_high, middle, column_high],
1477 )
1478 };
1479 let first = build_cell_tree(tree, spheres, columns, first);
1480 let second = build_cell_tree(tree, spheres, columns, second);
1481 let a = tree[first as usize];
1482 let b = tree[second as usize];
1483 let (centre, radius) = enclosing_sphere((a.centre, a.radius), (b.centre, b.radius));
1487 tree.push(CellTreeNode {
1488 centre,
1489 radius: radius * (1.0 + 4.0 * f64::EPSILON),
1490 children: Some((first, second)),
1491 cell: u32::MAX,
1492 });
1493 (tree.len() - 1) as u32
1494}
1495
1496fn projection_cells(surface: &NurbsSurface) -> Result<Option<&ProjectionCells>, String> {
1497 if let Some(cells) = surface.projection_cells.get() {
1498 return Ok(cells.as_ref());
1499 }
1500 let cells = build_projection_cells(surface)?;
1501 Ok(surface.projection_cells.get_or_init(|| cells).as_ref())
1502}
1503
1504fn build_projection_cells(surface: &NurbsSurface) -> Result<Option<ProjectionCells>, String> {
1505 let (closed_u, closed_v) = surface.closed_directions()?;
1506 let us = lattice_parameters(&surface.knots_u, surface.degree_u, closed_u);
1507 let vs = lattice_parameters(&surface.knots_v, surface.degree_v, closed_v);
1508 if us.len() < 2 || vs.len() < 2 {
1509 return Ok(None);
1510 }
1511 let mut scale = 0.0f64;
1512 for control in surface.control_points.iter().flatten() {
1513 let point = control.point()?;
1514 scale = scale.max(point.x.abs()).max(point.y.abs()).max(point.z.abs());
1515 }
1516 let resolution =
1517 ((surface.degree_u + 1) * (surface.degree_v + 1)) as f64 * f64::EPSILON * scale;
1518
1519 let mut net = surface.control_points.clone();
1521 let mut knots_u = surface.knots_u.clone();
1522 refine_to_lattice(&mut net, &mut knots_u, surface.degree_u, &us);
1523 let columns = net.first().map(Vec::len).unwrap_or(0);
1524 let mut transposed: Vec<Vec<Vec4>> =
1525 (0..columns).map(|column| net.iter().map(|row| row[column]).collect()).collect();
1526 let mut knots_v = surface.knots_v.clone();
1527 refine_to_lattice(&mut transposed, &mut knots_v, surface.degree_v, &vs);
1528 let rows = transposed.first().map(Vec::len).unwrap_or(0);
1529 let columns = transposed.len();
1530 if rows == 0 || columns == 0 {
1531 return Ok(None);
1532 }
1533 let mut points = vec![Vec3::default(); rows * columns];
1534 for (column, fibre) in transposed.iter().enumerate() {
1535 for (row, control) in fibre.iter().enumerate() {
1536 points[row * columns + column] = control.point()?;
1537 }
1538 }
1539
1540 let mut nodes = Vec::with_capacity(us.len() * vs.len());
1541 for &u in &us {
1542 for &v in &vs {
1543 nodes.push(surface.evaluate(u, v)?);
1544 }
1545 }
1546
1547 let cell_rows = us.len() - 1;
1548 let cell_columns = vs.len() - 1;
1549 let mut boxes = Vec::with_capacity(cell_rows * cell_columns);
1550 let mut spheres = Vec::with_capacity(cell_rows * cell_columns);
1551 for i in 0..cell_rows {
1552 let (row_low, row_high) =
1553 supporting_range(&knots_u, surface.degree_u, rows, us[i], us[i + 1]);
1554 for j in 0..cell_columns {
1555 let (column_low, column_high) =
1556 supporting_range(&knots_v, surface.degree_v, columns, vs[j], vs[j + 1]);
1557 let at = |row: usize, column: usize| points[row * columns + column];
1558 let c00 = at(row_low, column_low);
1559 let c10 = at(row_high, column_low);
1560 let c01 = at(row_low, column_high);
1561 let c11 = at(row_high, column_high);
1562 let along_u = c10.sub(c00).add(c11.sub(c01));
1563 let along_v = c01.sub(c00).add(c11.sub(c10));
1564 let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
1565 let normal = unit_or(e1.cross(along_v), e1.perpendicular()?);
1566 let e2 = unit_or(normal.cross(e1), e1.perpendicular()?);
1567 let e3 = e1.cross(e2);
1568 let axes = [e1, e2, e3];
1569 let mut low = [f64::INFINITY; 3];
1570 let mut high = [f64::NEG_INFINITY; 3];
1571 for row in row_low..=row_high {
1572 for column in column_low..=column_high {
1573 let point = at(row, column);
1574 for axis in 0..3 {
1575 let coordinate = point.dot(axes[axis]);
1576 low[axis] = low[axis].min(coordinate);
1577 high[axis] = high[axis].max(coordinate);
1578 }
1579 }
1580 }
1581 for axis in 0..3 {
1582 low[axis] -= resolution;
1583 high[axis] += resolution;
1584 }
1585 let mut centre = Vec3::default();
1586 let mut half_diagonal_squared = 0.0;
1587 for axis in 0..3 {
1588 centre = centre.add(axes[axis].scale((low[axis] + high[axis]) * 0.5));
1589 let half = (high[axis] - low[axis]) * 0.5;
1590 half_diagonal_squared += half * half;
1591 }
1592 if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
1593 return Ok(None);
1594 }
1595 spheres.push((centre, half_diagonal_squared.sqrt() + resolution));
1596 boxes.push(CellBox { axes, low, high });
1597 }
1598 }
1599 let mut tree = Vec::with_capacity(2 * boxes.len());
1600 build_cell_tree(&mut tree, &spheres, cell_columns, [0, cell_rows, 0, cell_columns]);
1601 Ok(Some(ProjectionCells {
1602 us,
1603 vs,
1604 nodes,
1605 boxes,
1606 tree,
1607 resolution,
1608 }))
1609}
1610
1611fn optimality(
1616 derivatives: &[[Vec3; 3]; 3],
1617 point: Vec3,
1618 (u, v): (f64, f64),
1619 [ua, ub, va, vb]: [f64; 4],
1620 periodic: (bool, bool),
1621) -> Optimality {
1622 let residual = derivatives[0][0].sub(point);
1623 let residual_length = residual.length();
1624 let su = derivatives[1][0];
1625 let sv = derivatives[0][1];
1626 let f = su.dot(residual);
1627 let g = sv.dot(residual);
1628 let metric_u = su.length_squared();
1629 let metric_v = sv.length_squared();
1630 let stationary_u = metric_u.sqrt() * residual_length <= EPSILON
1631 || f.abs() <= 1e-10 * metric_u.sqrt() * residual_length;
1632 let stationary_v = metric_v.sqrt() * residual_length <= EPSILON
1633 || g.abs() <= 1e-10 * metric_v.sqrt() * residual_length;
1634 let pinned_u = !periodic.0 && ((u <= ua && f > 0.0) || (u >= ub && f < 0.0));
1635 let pinned_v = !periodic.1 && ((v <= va && g > 0.0) || (v >= vb && g < 0.0));
1636 let j00 = derivatives[2][0].dot(residual) + metric_u;
1637 let j01 = derivatives[1][1].dot(residual) + su.dot(sv);
1638 let j11 = derivatives[0][2].dot(residual) + metric_v;
1639 let determinant = j00 * j11 - j01 * j01;
1640 let convex = match (!pinned_u, !pinned_v) {
1641 (true, true) => j00 > 0.0 && determinant > 0.0,
1642 (true, false) => j00 > 0.0,
1643 (false, true) => j11 > 0.0,
1644 (false, false) => true,
1645 };
1646 Optimality {
1647 residual_length,
1648 gradient: (f, g),
1649 metric: (metric_u, metric_v),
1650 hessian: (j00, j01, j11),
1651 determinant,
1652 pinned: (pinned_u, pinned_v),
1653 convex,
1654 minimum: residual_length <= LINEAR_TOLERANCE
1655 || ((stationary_u || pinned_u) && (stationary_v || pinned_v) && convex),
1656 }
1657}
1658
1659struct Optimality {
1660 residual_length: f64,
1661 gradient: (f64, f64),
1662 metric: (f64, f64),
1663 hessian: (f64, f64, f64),
1664 determinant: f64,
1665 pinned: (bool, bool),
1666 convex: bool,
1667 minimum: bool,
1668}
1669
1670fn is_verified_minimum(
1671 surface: &NurbsSurface,
1672 point: Vec3,
1673 at: (f64, f64),
1674 rectangle: [f64; 4],
1675 periodic: (bool, bool),
1676) -> Result<bool, String> {
1677 let derivatives = surface.derivatives_small(at.0, at.1, 2)?;
1678 Ok(optimality(&derivatives, point, at, rectangle, periodic).minimum)
1679}
1680
1681fn confined_descent(
1697 surface: &NurbsSurface,
1698 point: Vec3,
1699 seed: (f64, f64),
1700 rectangle: [f64; 4],
1701 periodic: (bool, bool),
1702) -> Result<(NewtonResult, bool), String> {
1703 let [ua, ub, va, vb] = rectangle;
1704 let mut u = fit_parameter(seed.0, ua, ub, periodic.0);
1705 let mut v = fit_parameter(seed.1, va, vb, periodic.1);
1706 let mut converged = false;
1707 let mut current = surface.evaluate(u, v)?.sub(point).length_squared();
1708 for _ in 0..MAX_NEWTON_ITERATIONS {
1709 let derivatives = surface.derivatives_small(u, v, 2)?;
1710 let state = optimality(&derivatives, point, (u, v), rectangle, periodic);
1711 current = state.residual_length * state.residual_length;
1712 if state.minimum {
1713 converged = true;
1714 break;
1715 }
1716 let (f, g) = state.gradient;
1717 let (metric_u, metric_v) = state.metric;
1718 let (j00, j01, j11) = state.hessian;
1719 let determinant = state.determinant;
1720 let (free_u, free_v) = (!state.pinned.0, !state.pinned.1);
1721 let (mut du, mut dv) = match (free_u, free_v) {
1722 (true, true) if state.convex => (
1723 (-f * j11 + g * j01) / determinant,
1724 (-g * j00 + f * j01) / determinant,
1725 ),
1726 (true, false) if state.convex => (-f / j00, 0.0),
1727 (false, true) if state.convex => (0.0, -g / j11),
1728 _ => (
1729 if free_u && metric_u > 0.0 { -f / metric_u } else { 0.0 },
1730 if free_v && metric_v > 0.0 { -g / metric_v } else { 0.0 },
1731 ),
1732 };
1733 if !(du.is_finite() && dv.is_finite()) || (du == 0.0 && dv == 0.0) {
1734 break;
1735 }
1736 let mut moved = false;
1737 let taken = |next: f64, here: f64, low: f64, high: f64, wraps: bool| {
1739 let raw = next - here;
1740 if wraps {
1741 raw - (high - low) * (raw / (high - low)).round()
1742 } else {
1743 raw
1744 }
1745 };
1746 for _ in 0..40 {
1747 let next_u = fit_parameter(u + du, ua, ub, periodic.0);
1748 let next_v = fit_parameter(v + dv, va, vb, periodic.1);
1749 if next_u == u && next_v == v {
1750 break;
1751 }
1752 let trial = surface.evaluate(next_u, next_v)?.sub(point).length_squared();
1753 if trial <= current {
1754 moved = taken(next_u, u, ua, ub, periodic.0).abs() > 1e-15 * (ub - ua)
1755 || taken(next_v, v, va, vb, periodic.1).abs() > 1e-15 * (vb - va);
1756 u = next_u;
1757 v = next_v;
1758 current = trial;
1759 break;
1760 }
1761 du *= 0.5;
1762 dv *= 0.5;
1763 }
1764 if !moved {
1765 break;
1766 }
1767 }
1768 let result = NewtonResult {
1769 u,
1770 v,
1771 distance_squared: current,
1772 converged,
1773 };
1774 let on_edge =
1775 (!periodic.0 && (u <= ua || u >= ub)) || (!periodic.1 && (v <= va || v >= vb));
1776 Ok((result, on_edge))
1777}
1778
1779#[allow(clippy::too_many_arguments)]
1782fn global_minimum(
1783 surface: &NurbsSurface,
1784 point: Vec3,
1785 domains: [f64; 4],
1786 periodic: (bool, bool),
1787 best: &mut NewtonResult,
1788 stats: &mut GlobalStats,
1789) -> Result<(), String> {
1790 if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
1791 stats.settled_by_floor = true;
1792 return Ok(());
1793 }
1794 let Some(cells) = projection_cells(surface)? else {
1795 return Ok(());
1796 };
1797 let resolution = cells.resolution;
1798 let columns = cells.vs.len() - 1;
1799 let columns_u = cells.us.len() - 1;
1800 let mut bound = best.distance_squared.sqrt() - resolution;
1801
1802 let mut candidates: Vec<(f64, usize)> = Vec::new();
1805 let mut excluded = f64::INFINITY;
1806 let mut stack: Vec<u32> = Vec::with_capacity(64);
1807 stack.push((cells.tree.len() - 1) as u32);
1808 while let Some(index) = stack.pop() {
1809 let node = cells.tree[index as usize];
1810 let sphere_bound = (point.sub(node.centre).length() - node.radius).max(0.0);
1811 if sphere_bound >= bound {
1812 excluded = excluded.min(sphere_bound);
1813 continue;
1814 }
1815 match node.children {
1816 Some((first, second)) => {
1817 stack.push(first);
1818 stack.push(second);
1819 }
1820 None => {
1821 let cell = node.cell as usize;
1822 let box_bound = cells.boxes[cell].lower_bound(point);
1823 if box_bound < bound {
1824 candidates.push((box_bound, cell));
1825 } else {
1826 excluded = excluded.min(box_bound);
1827 }
1828 }
1829 }
1830 }
1831 stats.candidates = candidates.len();
1832 candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
1833 stats.hull_lower_bound = candidates
1834 .first()
1835 .map_or(excluded, |&(least, _)| least.min(excluded));
1836
1837 let cell_of = |u: f64, v: f64| {
1843 let row = (cells.us.partition_point(|&value| value <= u).max(1) - 1).min(columns_u - 1);
1844 let column = (cells.vs.partition_point(|&value| value <= v).max(1) - 1).min(columns - 1);
1845 row * columns + column
1846 };
1847 let mut settled: Vec<usize> = Vec::new();
1848 if is_verified_minimum(surface, point, (best.u, best.v), domains, periodic)? {
1849 settled.push(cell_of(best.u, best.v));
1850 }
1851 for (cell_bound, cell) in candidates {
1852 if cell_bound >= bound {
1853 break;
1854 }
1855 let row = cell / columns;
1856 let column = cell % columns;
1857 let rectangle = [
1858 cells.us[row],
1859 cells.us[row + 1],
1860 cells.vs[column],
1861 cells.vs[column + 1],
1862 ];
1863 if settled.contains(&cell) {
1864 continue;
1865 }
1866 let mut seed = (rectangle[0], rectangle[2]);
1867 let mut seed_distance = f64::INFINITY;
1868 for (node_row, node_column) in
1869 [(row, column), (row + 1, column), (row, column + 1), (row + 1, column + 1)]
1870 {
1871 let distance = cells.nodes[node_row * cells.vs.len() + node_column]
1872 .sub(point)
1873 .length_squared();
1874 if distance < seed_distance {
1875 seed_distance = distance;
1876 seed = (cells.us[node_row], cells.vs[node_column]);
1877 }
1878 }
1879 stats.refinements += 1;
1880 let (confined, on_edge) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1881 let mut found = confined;
1882 if on_edge && confined.distance_squared.sqrt() < bound {
1883 stats.continuations += 1;
1886 let (continued, _) =
1887 confined_descent(surface, point, (confined.u, confined.v), domains, periodic)?;
1888 if continued.distance_squared < found.distance_squared {
1889 found = continued;
1890 }
1891 if continued.converged {
1892 settled.push(cell_of(continued.u, continued.v));
1893 }
1894 }
1895 if found.distance_squared.sqrt() < bound {
1896 *best = found;
1897 bound = best.distance_squared.sqrt() - resolution;
1898 stats.replaced = true;
1899 if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
1900 return Ok(());
1901 }
1902 }
1903 }
1904 Ok(())
1905}
1906