1use crate::curve::interior_knots;
2use crate::{NurbsCurve, NurbsSurface, Vec3, Vec4};
3use serde::Serialize;
4
5const EPSILON: f64 = crate::tolerance::FP_FLOOR;
6const LINEAR_TOLERANCE: f64 = crate::tolerance::DEFAULT_MODEL;
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
24pub(crate) fn 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 switch!("BREP_PROJECTION_WITNESS", true)
40}
41
42fn point_segment_distance_squared(point: Vec3, a: Vec3, b: Vec3) -> f64 {
48 point.distance_to_segment_squared(a, b)
49}
50
51#[allow(clippy::too_many_arguments)]
59fn curve_newton(
60 curve: &NurbsCurve,
61 point: Vec3,
62 seed: f64,
63 start: f64,
64 end: f64,
65 closed: bool,
66 best_u: &mut f64,
67 best_distance_squared: &mut f64,
68) -> Result<f64, String> {
69 let (last, nearest, squared) = curve_foot_newton_state(curve, point, seed, [start, end],
70 CurveFootPolicy {
71 domain: if closed { FootDomain::Wrap } else { FootDomain::Clamp },
72 iterations: MAX_NEWTON_ITERATIONS,
73 slope_guard: EPSILON,
74 step_limit: (end - start) / 4.0,
75 parameter_tolerance: 1e-15 * (end - start),
76 mode: FootMode::Witness,
77 })?;
78 if squared < *best_distance_squared {
79 *best_distance_squared = squared;
80 *best_u = nearest;
81 }
82 Ok(last)
83}
84
85fn bracketed_nearest(curve: &NurbsCurve, point: Vec3, low: f64, high: f64) -> Result<f64, String> {
93 const CONTRACTION: f64 = 0.618_033_988_749_894_9;
96 const CONTRACTIONS: usize = 64;
97 let width = high - low;
98 if !(width > 0.0) {
99 return Ok(low);
100 }
101 let (mut low, mut high) = (low, high);
102 let mut c = high - CONTRACTION * (high - low);
103 let mut d = low + CONTRACTION * (high - low);
104 let mut value_c = curve.evaluate(c)?.sub(point).length_squared();
105 let mut value_d = curve.evaluate(d)?.sub(point).length_squared();
106 for _ in 0..CONTRACTIONS {
107 if high - low <= 1e-15 * width {
108 break;
109 }
110 if value_c < value_d {
111 high = d;
112 d = c;
113 value_d = value_c;
114 c = high - CONTRACTION * (high - low);
115 value_c = curve.evaluate(c)?.sub(point).length_squared();
116 } else {
117 low = c;
118 c = d;
119 value_c = value_d;
120 d = low + CONTRACTION * (high - low);
121 value_d = curve.evaluate(d)?.sub(point).length_squared();
122 }
123 }
124 Ok((low + high) * 0.5)
125}
126
127pub fn project_point_to_curve(curve: &NurbsCurve, point: Vec3) -> Result<CurveProjection, String> {
128 if projection_memo_enabled() {
129 if let Some(key) = straight_projection_key(curve, point) {
130 if let Some(hit) = PROJECTION_MEMO.with(|memo| memo.borrow().get(&key).copied()) {
131 return Ok(hit);
132 }
133 let projection = project_point_to_curve_uncached(curve, point)?;
134 PROJECTION_MEMO.with(|memo| {
135 let mut memo = memo.borrow_mut();
136 if memo.len() >= PROJECTION_MEMO_ENTRIES {
137 memo.clear();
138 }
139 memo.insert(key, projection);
140 });
141 return Ok(projection);
142 }
143 }
144 project_point_to_curve_uncached(curve, point)
145}
146
147const PROJECTION_MEMO_ENTRIES: usize = 4096;
156
157thread_local! {
158 static PROJECTION_MEMO: std::cell::RefCell<rustc_hash::FxHashMap<[u64; 16], CurveProjection>> = Default::default();
159}
160
161fn straight_projection_key(curve: &NurbsCurve, point: Vec3) -> Option<[u64; 16]> {
162 if curve.degree != 1 || curve.control_points.len() != 2 || curve.knots.len() != 4 {
163 return None;
164 }
165 let [a, b] = [curve.control_points[0], curve.control_points[1]];
166 let k = &curve.knots;
167 Some([
168 1, k[0].to_bits(), k[1].to_bits(), k[2].to_bits(), k[3].to_bits(),
169 a.x.to_bits(), a.y.to_bits(), a.z.to_bits(), a.w.to_bits(),
170 b.x.to_bits(), b.y.to_bits(), b.z.to_bits(), b.w.to_bits(),
171 point.x.to_bits(), point.y.to_bits(), point.z.to_bits(),
172 ])
173}
174
175fn projection_memo_enabled() -> bool {
176 switch!("BREP_PROJECTION_MEMO", true)
177}
178
179
180fn project_point_to_curve_uncached(curve: &NurbsCurve, point: Vec3) -> Result<CurveProjection, String> {
181 let [start, end] = curve.domain()?;
182 let mut breaks = vec![start];
183 breaks.extend(interior_knots(&curve.knots, curve.degree));
184 breaks.push(end);
185 let samples_per_span = 4usize.max(curve.degree + 2);
186 let mut best_u = start;
187 let mut best_distance_squared = f64::INFINITY;
188 let mut start_point = None;
193 let mut end_point = None;
194 let mut previous: Option<(f64, Vec3)> = None;
197 let mut witness_distance_squared = f64::INFINITY;
198 let mut witness_low = start;
199 let mut witness_high = start;
200 for pair in breaks.windows(2) {
201 for index in 0..=samples_per_span {
202 let parameter = pair[0] + (pair[1] - pair[0]) * index as f64 / samples_per_span as f64;
203 let sample = curve.evaluate(parameter)?;
204 if start_point.is_none() {
205 start_point = Some(sample);
206 }
207 end_point = Some(sample);
208 let distance_squared = sample.sub(point).length_squared();
209 if distance_squared < best_distance_squared {
210 best_distance_squared = distance_squared;
211 best_u = parameter;
212 }
213 if let Some((previous_parameter, previous_sample)) = previous {
214 let segment = point_segment_distance_squared(point, previous_sample, sample);
215 if segment < witness_distance_squared {
216 witness_distance_squared = segment;
217 witness_low = previous_parameter;
218 witness_high = parameter;
219 }
220 }
221 previous = Some((parameter, sample));
222 }
223 }
224 const CLOSED_SEAM_TOLERANCE: f64 = 1e-6;
237 let closed = match (start_point, end_point) {
238 (Some(first), Some(last)) => first.sub(last).length() <= CLOSED_SEAM_TOLERANCE,
239 _ => false,
240 };
241
242 let parameter = curve_newton(
243 curve,
244 point,
245 best_u,
246 start,
247 end,
248 closed,
249 &mut best_u,
250 &mut best_distance_squared,
251 )?;
252 for candidate in [start, end, parameter] {
253 let distance_squared = curve.evaluate(candidate)?.sub(point).length_squared();
254 if distance_squared < best_distance_squared {
255 best_distance_squared = distance_squared;
256 best_u = candidate;
257 }
258 }
259 const WITNESS_FACTOR: f64 = 2.0;
291 if witness_distance_squared * (WITNESS_FACTOR * WITNESS_FACTOR) < best_distance_squared
292 && witness_rescue_enabled()
293 {
294 let bracketed = bracketed_nearest(curve, point, witness_low, witness_high)?;
295 let polished = curve_newton(
296 curve,
297 point,
298 bracketed,
299 start,
300 end,
301 closed,
302 &mut best_u,
303 &mut best_distance_squared,
304 )?;
305 for candidate in [bracketed, polished] {
306 let distance_squared = curve.evaluate(candidate)?.sub(point).length_squared();
307 if distance_squared < best_distance_squared {
308 best_distance_squared = distance_squared;
309 best_u = candidate;
310 }
311 }
312 }
313 let projected = curve.evaluate(best_u)?;
314 Ok(CurveProjection {
315 u: best_u,
316 point: projected,
317 distance: projected.sub(point).length(),
318 })
319}
320
321fn project_linear_revolution(
322 surface: &NurbsSurface,
323 point: Vec3,
324) -> Result<Option<SurfaceProjection>, String> {
325 let rows = &surface.control_points;
326 if surface.degree_u != 2
327 || surface.degree_v != 1
328 || rows.len() < 5
329 || rows[0].len() != 2
330 || !surface.closed_directions()?.0
331 {
332 return Ok(None);
333 }
334 let base = rows[0][0].point()?.add(rows[4][0].point()?).scale(0.5);
335 let top = rows[0][1].point()?.add(rows[4][1].point()?).scale(0.5);
336 let axis_vector = top.sub(base);
337 let height = axis_vector.length();
338 if height <= EPSILON {
339 return Ok(None);
340 }
341 let axis = axis_vector.normalized()?;
342 let [v0, v1] = surface.domain_v()?;
343 let axial = point.sub(base).dot(axis).clamp(0.0, height);
344 let v = v0 + (v1 - v0) * axial / height;
345 let circle = surface.iso_curve_v(v)?;
346 let projection = project_point_to_curve(&circle, point)?;
347 let [u0, u1] = surface.domain_u()?;
348 let u = projection.u.clamp(u0, u1);
349 let projected = surface.evaluate(u, v)?;
350 Ok(Some(SurfaceProjection {
351 u,
352 v,
353 point: projected,
354 distance: projected.sub(point).length(),
355 }))
356}
357
358#[derive(Clone, Copy)]
359struct NewtonResult {
360 u: f64,
361 v: f64,
362 distance_squared: f64,
363 converged: bool,
364}
365
366fn surface_newton(
367 surface: &NurbsSurface,
368 point: Vec3,
369 seed_u: f64,
370 seed_v: f64,
371 domains: [f64; 4],
372 closed_u: bool,
373 closed_v: bool,
374) -> Result<NewtonResult, String> {
375 let [u0, u1, v0, v1] = domains;
376 let mut u = seed_u;
377 let mut v = seed_v;
378 let mut converged = false;
379 let mut best = NewtonResult {
380 u,
381 v,
382 distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
383 converged,
384 };
385 for _ in 0..MAX_NEWTON_ITERATIONS {
386 let derivatives = surface.derivatives_small(u, v, 2)?;
387 let residual = derivatives[0][0].sub(point);
388 let distance_squared = residual.length_squared();
389 if distance_squared < best.distance_squared {
390 best.u = u;
391 best.v = v;
392 best.distance_squared = distance_squared;
393 }
394 let f = derivatives[1][0].dot(residual);
395 let g = derivatives[0][1].dot(residual);
396 let residual_length = distance_squared.sqrt();
397 if residual_length <= LINEAR_TOLERANCE {
398 converged = true;
399 break;
400 }
401 let tangent_u_length = derivatives[1][0].length();
402 let tangent_v_length = derivatives[0][1].length();
403 let cosine_u = if tangent_u_length * residual_length <= EPSILON {
404 0.0
405 } else {
406 f.abs() / (tangent_u_length * residual_length)
407 };
408 let cosine_v = if tangent_v_length * residual_length <= EPSILON {
409 0.0
410 } else {
411 g.abs() / (tangent_v_length * residual_length)
412 };
413 if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
414 converged = true;
415 break;
416 }
417 let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
418 let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
419 let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
420 let determinant = j00 * j11 - j01 * j01;
421 if determinant.abs() <= EPSILON {
422 break;
423 }
424 let mut du = (-f * j11 + g * j01) / determinant;
425 let mut dv = (-g * j00 + f * j01) / determinant;
426 du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
427 dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
428 let next_u = fit_parameter(u + du, u0, u1, closed_u);
429 let next_v = fit_parameter(v + dv, v0, v1, closed_v);
430 let stalled =
431 (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
432 u = next_u;
433 v = next_v;
434 if stalled {
435 break;
436 }
437 }
438 let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
439 if final_distance_squared < best.distance_squared {
440 best.u = u;
441 best.v = v;
442 best.distance_squared = final_distance_squared;
443 }
444 best.converged = converged;
445 Ok(best)
446}
447
448#[track_caller]
454pub fn project_point_to_surface(
455 surface: &NurbsSurface,
456 point: Vec3,
457) -> Result<SurfaceProjection, String> {
458 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_PROJECT, surface);
459 if let Some(analytic) = surface.analytic() {
463 if let Some(projection) = analytic.project(surface, point) {
464 return Ok(projection);
465 }
466 }
467 if let Some(ellipsoid) = surface.affine_sphere() {
468 if let Some(projection) = ellipsoid.project(surface, point) {
469 return Ok(projection);
470 }
471 }
472 project_point_to_surface_general(surface, point)
473}
474
475pub fn project_point_to_surface_seeded(
485 surface: &NurbsSurface,
486 point: Vec3,
487 seed_u: f64,
488 seed_v: f64,
489) -> Result<SurfaceProjection, String> {
490 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_SEEDED, surface);
491 if let Some(ellipsoid) = surface.affine_sphere() {
495 if let Some(projection) = ellipsoid.project(surface, point) {
496 return Ok(projection);
497 }
498 }
499 let [u0, u1] = surface.domain_u()?;
500 let [v0, v1] = surface.domain_v()?;
501 let (closed_u, closed_v) = surface.closed_directions()?;
502 let result = surface_newton(
503 surface,
504 point,
505 seed_u,
506 seed_v,
507 [u0, u1, v0, v1],
508 closed_u,
509 closed_v,
510 )?;
511 let projected = surface.evaluate(result.u, result.v)?;
512 Ok(SurfaceProjection {
513 u: result.u,
514 v: result.v,
515 point: projected,
516 distance: projected.sub(point).length(),
517 })
518}
519
520#[track_caller]
554pub(crate) fn project_point_to_surface_from_seed(
555 surface: &NurbsSurface,
556 point: Vec3,
557 seed: [f64; 2],
558) -> Result<SurfaceProjection, String> {
559 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_SEED, surface);
560 if let Some(analytic) = surface.analytic() {
561 if let Some(projection) = analytic.project(surface, point) {
562 return Ok(projection);
563 }
564 }
565 if let Some(ellipsoid) = surface.affine_sphere() {
566 if let Some(projection) = ellipsoid.project(surface, point) {
567 return Ok(projection);
568 }
569 }
570 let caller = std::panic::Location::caller();
571 if !seeded_answer_admitted(caller) {
572 return project_point_to_surface_general(surface, point);
573 }
574 let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
575 if seed_audit_enabled() {
576 let nearest = project_point_to_surface_general(surface, point)?;
577 if seeded.point.sub(nearest.point).length() > LINEAR_TOLERANCE {
578 seed_audit_record(caller, surface, seed, &seeded, &nearest);
579 }
580 }
581 Ok(seeded)
582}
583
584fn seeded_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
587 static SITES: std::sync::OnceLock<Option<Option<Vec<String>>>> = std::sync::OnceLock::new();
588 let sites = SITES.get_or_init(|| {
589 if std::env::var("BREP_PROJECTION_FROM_SEED").as_deref() == Ok("0") {
590 return None;
591 }
592 Some(std::env::var("BREP_PROJECTION_FROM_SEED_SITES").ok().map(|value| {
593 value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect()
594 }))
595 });
596 match sites {
597 None => false,
598 Some(None) => true,
599 Some(Some(only)) => {
600 let location = format!("{}:{}", caller.file(), caller.line());
601 only.iter().any(|site| location.contains(site.as_str()))
602 }
603 }
604}
605
606fn seed_audit_enabled() -> bool {
607 debug_flag!("BREP_PROJECTION_AUDIT")
608}
609
610fn seed_audit_record(
614 caller: &std::panic::Location<'_>,
615 surface: &NurbsSurface,
616 seed: [f64; 2],
617 seeded: &SurfaceProjection,
618 nearest: &SurfaceProjection,
619) {
620 use std::io::Write;
621 static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
622 std::sync::OnceLock::new();
623 let sink = SINK.get_or_init(|| {
624 let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
625 let path = std::path::Path::new(&directory)
626 .join(format!("projection-seed-audit-{}.tsv", std::process::id()));
627 std::fs::OpenOptions::new()
628 .create(true)
629 .append(true)
630 .open(path)
631 .ok()
632 .map(std::sync::Mutex::new)
633 });
634 if let Some(file) = sink {
635 if let Ok(mut file) = file.lock() {
636 let _ = writeln!(
637 file,
638 "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.6e}\t{}x{} cp {}x{}",
639 caller.file(),
640 caller.line(),
641 seeded.distance,
642 nearest.distance,
643 seed[0],
644 seed[1],
645 seeded.u,
646 seeded.v,
647 nearest.u,
648 nearest.v,
649 seeded.point.sub(nearest.point).length(),
650 surface.degree_u,
651 surface.degree_v,
652 surface.control_points.len(),
653 surface.control_points.first().map(Vec::len).unwrap_or(0),
654 );
655 }
656 }
657}
658
659fn projection_seed_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
664 if let Some(grid) = surface.projection_grid.get() {
665 return Ok(grid);
666 }
667 let [u0, u1] = surface.domain_u()?;
668 let [v0, v1] = surface.domain_v()?;
669 let (closed_u, closed_v) = surface.closed_directions()?;
670 let mut breaks_u = vec![u0];
671 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
672 breaks_u.push(u1);
673 let mut breaks_v = vec![v0];
674 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
675 breaks_v.push(v1);
676 let samples_u = if closed_u {
677 8usize.max(surface.degree_u * 4)
678 } else {
679 3usize.max(surface.degree_u + 1)
680 };
681 let samples_v = if closed_v {
682 8usize.max(surface.degree_v * 4)
683 } else {
684 3usize.max(surface.degree_v + 1)
685 };
686 let mut grid = Vec::new();
687 for u_pair in breaks_u.windows(2) {
688 for v_pair in breaks_v.windows(2) {
689 for i in 0..=samples_u {
690 for j in 0..=samples_v {
691 let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
692 let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
693 grid.push((u, v, surface.evaluate(u, v)?));
694 }
695 }
696 }
697 }
698 Ok(surface.projection_grid.get_or_init(|| grid))
699}
700
701fn projection_dense_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
708 if let Some(grid) = surface.projection_dense_grid.get() {
709 return Ok(grid);
710 }
711 let [u0, u1] = surface.domain_u()?;
712 let [v0, v1] = surface.domain_v()?;
713 let mut breaks_u = vec![u0];
714 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
715 breaks_u.push(u1);
716 let mut breaks_v = vec![v0];
717 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
718 breaks_v.push(v1);
719 let count_u = 32usize.max((breaks_u.len() - 1) * 8);
720 let count_v = 16usize.max((breaks_v.len() - 1) * 8);
721 let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
722 for i in 0..=count_u {
723 for j in 0..=count_v {
724 let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
725 let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
726 grid.push((u, v, surface.evaluate(u, v)?));
727 }
728 }
729 Ok(surface.projection_dense_grid.get_or_init(|| grid))
730}
731
732const RING_EXPONENTS: usize = 22;
736const RING_INDICES: usize = 33; #[inline]
739fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
740 (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
741}
742
743fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
750 if let Some(grid) = surface.projection_ring_grid.get() {
751 return Ok(grid);
752 }
753 let [u0, u1] = surface.domain_u()?;
754 let [v0, v1] = surface.domain_v()?;
755 let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
756 for exponent in 1..=RING_EXPONENTS as i32 {
757 let offset = 1.0 / 2f64.powi(exponent);
758 let e = exponent as usize - 1;
759 let v_lo = v0 + (v1 - v0) * offset;
760 let v_hi = v1 - (v1 - v0) * offset;
761 let u_lo = u0 + (u1 - u0) * offset;
762 let u_hi = u1 - (u1 - u0) * offset;
763 for index in 0..=32 {
764 let u = u0 + (u1 - u0) * index as f64 / 32.0;
765 grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
766 grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
767 }
768 for index in 0..=32 {
769 let v = v0 + (v1 - v0) * index as f64 / 32.0;
770 grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
771 grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
772 }
773 }
774 Ok(surface.projection_ring_grid.get_or_init(|| grid))
775}
776
777fn nearest_seed_answer(
787 surface: &NurbsSurface,
788 point: Vec3,
789 [u0, u1, v0, v1]: [f64; 4],
790 closed_u: bool,
791 closed_v: bool,
792) -> Result<NewtonResult, String> {
793 let mut best = NewtonResult {
794 u: u0,
795 v: v0,
796 distance_squared: f64::INFINITY,
797 converged: false,
798 };
799 for &(u, v, sample) in projection_seed_grid(surface)? {
800 let distance_squared = sample.sub(point).length_squared();
801 if distance_squared < best.distance_squared {
802 best.u = u;
803 best.v = v;
804 best.distance_squared = distance_squared;
805 }
806 }
807 if let Some(revolution) = project_linear_revolution(surface, point)? {
808 if revolution.distance * revolution.distance < best.distance_squared {
809 best.u = revolution.u;
810 best.v = revolution.v;
811 best.distance_squared = revolution.distance * revolution.distance;
812 }
813 }
814 let first_newton = surface_newton(
815 surface,
816 point,
817 best.u,
818 best.v,
819 [u0, u1, v0, v1],
820 closed_u,
821 closed_v,
822 )?;
823 if first_newton.distance_squared < best.distance_squared {
824 best = first_newton;
825 }
826 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
827 let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
828 if degenerate || !first_newton.converged {
829 let mut grid = best;
830 for &(u, v, sample) in projection_dense_grid(surface)? {
831 let distance_squared = sample.sub(point).length_squared();
832 if distance_squared < grid.distance_squared {
833 grid.u = u;
834 grid.v = v;
835 grid.distance_squared = distance_squared;
836 }
837 }
838 if grid.distance_squared < best.distance_squared {
839 best = grid;
840 }
841 let polished = surface_newton(
842 surface,
843 point,
844 grid.u,
845 grid.v,
846 [u0, u1, v0, v1],
847 closed_u,
848 closed_v,
849 )?;
850 if polished.distance_squared < best.distance_squared {
851 best = polished;
852 }
853
854 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
855 if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
856 let ring_grid = projection_ring_grid(surface)?;
857 let mut ring = best;
858 let scan = |side: usize,
862 exponent_index: usize,
863 along_u: bool,
864 fixed: f64,
865 candidate: &mut NewtonResult| {
866 for index in 0..=32 {
867 let moving = if along_u {
868 u0 + (u1 - u0) * index as f64 / 32.0
869 } else {
870 v0 + (v1 - v0) * index as f64 / 32.0
871 };
872 let (u, v) = if along_u {
873 (moving, fixed)
874 } else {
875 (fixed, moving)
876 };
877 let distance_squared = ring_grid
878 [ring_slot(side, exponent_index, index as usize)]
879 .sub(point)
880 .length_squared();
881 if distance_squared < candidate.distance_squared {
882 candidate.u = u;
883 candidate.v = v;
884 candidate.distance_squared = distance_squared;
885 }
886 }
887 };
888 for exponent in 1..=22 {
889 let offset = 1.0 / 2f64.powi(exponent);
890 let e = exponent as usize - 1;
891 if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
892 scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
893 }
894 if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
895 scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
896 }
897 if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
898 scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
899 }
900 if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
901 scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
902 }
903 }
904 if ring.distance_squared < best.distance_squared {
905 best = ring;
906 }
907 let polished = surface_newton(
908 surface,
909 point,
910 ring.u,
911 ring.v,
912 [u0, u1, v0, v1],
913 closed_u,
914 closed_v,
915 )?;
916 if polished.distance_squared < best.distance_squared {
917 best = polished;
918 }
919 }
920 }
921 Ok(best)
922}
923
924fn global_stage_enabled() -> bool {
929 switch!("BREP_PROJECTION_GLOBAL", true)
930}
931
932pub(crate) fn global_site_filter_active() -> bool {
941 static ACTIVE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
946 *ACTIVE.get_or_init(|| {
947 std::env::var_os("BREP_PROJECTION_GLOBAL_SITES").is_some() || std::env::var_os("BREP_PROJECTION_GLOBAL_EXCEPT").is_some()
948 })
949}
950
951fn global_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
952 fn list(name: &str) -> Option<Vec<String>> {
953 std::env::var(name)
954 .ok()
955 .map(|value| value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect())
956 }
957 static SITES: std::sync::OnceLock<(Option<Vec<String>>, Option<Vec<String>>)> =
958 std::sync::OnceLock::new();
959 let (only, except) = SITES.get_or_init(|| {
960 (list("BREP_PROJECTION_GLOBAL_SITES"), list("BREP_PROJECTION_GLOBAL_EXCEPT"))
961 });
962 if only.is_none() && except.is_none() {
963 return true;
964 }
965 let location = format!("{}:{}", caller.file(), caller.line());
966 let named = |sites: &Vec<String>| sites.iter().any(|site| location.contains(site.as_str()));
967 only.as_ref().is_none_or(named) && !except.as_ref().is_some_and(named)
968}
969
970fn audit_record(caller: &std::panic::Location<'_>, surface: &NurbsSurface, local: &NewtonResult, global: &NewtonResult) {
976 use std::io::Write;
977 static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
978 std::sync::OnceLock::new();
979 let sink = SINK.get_or_init(|| {
980 let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
981 let path = std::path::Path::new(&directory)
982 .join(format!("projection-audit-{}.tsv", std::process::id()));
983 std::fs::OpenOptions::new()
984 .create(true)
985 .append(true)
986 .open(path)
987 .ok()
988 .map(std::sync::Mutex::new)
989 });
990 if let Some(file) = sink {
991 if let Ok(mut file) = file.lock() {
992 let _ = writeln!(
993 file,
994 "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{}x{} cp {}x{}",
995 caller.file(),
996 caller.line(),
997 local.distance_squared.sqrt(),
998 global.distance_squared.sqrt(),
999 local.u,
1000 local.v,
1001 global.u,
1002 global.v,
1003 surface.degree_u,
1004 surface.degree_v,
1005 surface.control_points.len(),
1006 surface.control_points.first().map(Vec::len).unwrap_or(0),
1007 );
1008 }
1009 }
1010}
1011
1012#[track_caller]
1088pub(crate) fn project_point_to_surface_general(
1089 surface: &NurbsSurface,
1090 point: Vec3,
1091) -> Result<SurfaceProjection, String> {
1092 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_GENERAL, surface);
1093 let [u0, u1] = surface.domain_u()?;
1094 let [v0, v1] = surface.domain_v()?;
1095 let (closed_u, closed_v) = surface.closed_directions()?;
1096 let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1097 let mut best = local;
1098 if global_stage_enabled() {
1099 global_minimum(
1100 surface,
1101 point,
1102 [u0, u1, v0, v1],
1103 (closed_u, closed_v),
1104 &mut best,
1105 &mut GlobalStats::default(),
1106 )?;
1107 if best.u != local.u || best.v != local.v {
1108 let caller = std::panic::Location::caller();
1109 if global_answer_admitted(caller) {
1110 audit_record(caller, surface, &local, &best);
1111 } else {
1112 best = local;
1113 }
1114 }
1115 }
1116 let projected = surface.evaluate(best.u, best.v)?;
1117 Ok(SurfaceProjection {
1118 u: best.u,
1119 v: best.v,
1120 point: projected,
1121 distance: projected.sub(point).length(),
1122 })
1123}
1124
1125pub(crate) fn project_point_to_surface_from_nearest_seed(
1135 surface: &NurbsSurface,
1136 point: Vec3,
1137) -> Result<SurfaceProjection, String> {
1138 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_NEAREST_SEED, surface);
1139 if let Some(analytic) = surface.analytic() {
1140 if let Some(projection) = analytic.project(surface, point) {
1141 return Ok(projection);
1142 }
1143 }
1144 if let Some(ellipsoid) = surface.affine_sphere() {
1145 if let Some(projection) = ellipsoid.project(surface, point) {
1146 return Ok(projection);
1147 }
1148 }
1149 let [u0, u1] = surface.domain_u()?;
1150 let [v0, v1] = surface.domain_v()?;
1151 let (closed_u, closed_v) = surface.closed_directions()?;
1152 let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1153 let projected = surface.evaluate(local.u, local.v)?;
1154 Ok(SurfaceProjection {
1155 u: local.u,
1156 v: local.v,
1157 point: projected,
1158 distance: projected.sub(point).length(),
1159 })
1160}
1161
1162#[doc(hidden)]
1165pub fn project_point_to_surface_general_lanes(
1166 surface: &NurbsSurface,
1167 point: Vec3,
1168 run_global: bool,
1169) -> Result<(SurfaceProjection, GlobalStats), String> {
1170 let [u0, u1] = surface.domain_u()?;
1171 let [v0, v1] = surface.domain_v()?;
1172 let (closed_u, closed_v) = surface.closed_directions()?;
1173 let mut best = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1174 let mut stats = GlobalStats::default();
1175 if run_global {
1176 global_minimum(surface, point, [u0, u1, v0, v1], (closed_u, closed_v), &mut best, &mut stats)?;
1177 }
1178 let projected = surface.evaluate(best.u, best.v)?;
1179 Ok((
1180 SurfaceProjection {
1181 u: best.u,
1182 v: best.v,
1183 point: projected,
1184 distance: projected.sub(point).length(),
1185 },
1186 stats,
1187 ))
1188}
1189
1190#[doc(hidden)]
1193#[derive(Clone, Debug, Default)]
1194pub struct BasinCensus {
1195 pub cells: usize,
1198 pub descents: usize,
1200 pub unconverged: usize,
1202 pub cells_with_two_basins: usize,
1206 pub foot_cells_with_two_basins: usize,
1209 pub least: f64,
1211 pub least_uv: (f64, f64),
1212 pub foot_cell_descents: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)>,
1216}
1217
1218#[doc(hidden)]
1223pub fn project_point_to_surface_basin_census(
1224 surface: &NurbsSurface,
1225 point: Vec3,
1226 answer_distance: f64,
1227 bar: f64,
1228) -> Result<BasinCensus, String> {
1229 let mut census = BasinCensus {
1230 least: f64::INFINITY,
1231 ..BasinCensus::default()
1232 };
1233 let Some(cells) = projection_cells(surface)? else {
1234 return Ok(census);
1235 };
1236 let columns = cells.vs.len() - 1;
1237 let mut spreads: Vec<(usize, f64)> = Vec::new();
1238 let mut runs: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)> = Vec::new();
1239 for (cell, cell_box) in cells.boxes.iter().enumerate() {
1240 if cell_box.lower_bound(point) >= answer_distance {
1241 continue;
1242 }
1243 census.cells += 1;
1244 let (row, column) = (cell / columns, cell % columns);
1245 let rectangle = [cells.us[row], cells.us[row + 1], cells.vs[column], cells.vs[column + 1]];
1246 let middle = ((rectangle[0] + rectangle[1]) * 0.5, (rectangle[2] + rectangle[3]) * 0.5);
1247 let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
1248 for seed in [
1249 (rectangle[0], rectangle[2]),
1250 (rectangle[1], rectangle[2]),
1251 (rectangle[0], rectangle[3]),
1252 (rectangle[1], rectangle[3]),
1253 middle,
1254 ] {
1255 census.descents += 1;
1256 let (found, _) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1257 let distance = found.distance_squared.sqrt();
1258 runs.push((cell, rectangle, seed, (found.u, found.v), distance, found.converged));
1259 if distance < census.least {
1260 census.least = distance;
1261 census.least_uv = (found.u, found.v);
1262 }
1263 if !found.converged {
1264 census.unconverged += 1;
1265 continue;
1266 }
1267 low = low.min(distance.max(LINEAR_TOLERANCE));
1268 high = high.max(distance.max(LINEAR_TOLERANCE));
1269 }
1270 if high - low > bar {
1271 spreads.push((cell, high - low));
1272 }
1273 }
1274 census.cells_with_two_basins = spreads.len();
1275 let (u, v) = census.least_uv;
1276 let foot_cells: Vec<usize> = spreads
1277 .iter()
1278 .map(|&(cell, _)| cell)
1279 .filter(|&cell| {
1280 let (row, column) = (cell / columns, cell % columns);
1281 cells.us[row] <= u && u <= cells.us[row + 1] && cells.vs[column] <= v && v <= cells.vs[column + 1]
1282 })
1283 .collect();
1284 census.foot_cells_with_two_basins = foot_cells.len();
1285 census.foot_cell_descents = runs.into_iter().filter(|run| foot_cells.contains(&run.0)).collect();
1286 Ok(census)
1287}
1288
1289#[doc(hidden)]
1291#[derive(Clone, Copy, Debug, Default)]
1292pub struct GlobalStats {
1293 pub settled_by_floor: bool,
1295 pub candidates: usize,
1297 pub refinements: usize,
1299 pub continuations: usize,
1301 pub replaced: bool,
1303 pub hull_lower_bound: f64,
1306}
1307
1308#[derive(Clone, Debug)]
1311struct CellBox {
1312 axes: [Vec3; 3],
1313 low: [f64; 3],
1314 high: [f64; 3],
1315}
1316
1317impl CellBox {
1318 fn lower_bound(&self, point: Vec3) -> f64 {
1321 let mut squared = 0.0;
1322 for axis in 0..3 {
1323 let coordinate = point.dot(self.axes[axis]);
1324 let excess = (self.low[axis] - coordinate).max(coordinate - self.high[axis]).max(0.0);
1325 squared += excess * excess;
1326 }
1327 squared.sqrt()
1328 }
1329}
1330
1331#[derive(Clone, Copy, Debug)]
1334struct CellTreeNode {
1335 centre: Vec3,
1336 radius: f64,
1337 children: Option<(u32, u32)>,
1338 cell: u32,
1339}
1340
1341#[derive(Clone, Debug)]
1344pub(crate) struct ProjectionCells {
1345 us: Vec<f64>,
1346 vs: Vec<f64>,
1347 nodes: Vec<Vec3>,
1349 boxes: Vec<CellBox>,
1351 tree: Vec<CellTreeNode>,
1353 resolution: f64,
1354}
1355
1356fn lattice_parameters(knots: &[f64], degree: usize, closed: bool) -> Vec<f64> {
1360 let [start, end] = crate::curve::knot_domain(knots, degree);
1361 let samples = if closed {
1362 8usize.max(degree * 4)
1363 } else {
1364 3usize.max(degree + 1)
1365 };
1366 let mut breaks = vec![start];
1367 breaks.extend(interior_knots(knots, degree));
1368 breaks.push(end);
1369 let mut parameters = Vec::with_capacity((breaks.len() - 1) * samples + 1);
1370 for pair in breaks.windows(2) {
1371 for index in 0..samples {
1372 parameters.push(pair[0] + (pair[1] - pair[0]) * index as f64 / samples as f64);
1373 }
1374 }
1375 parameters.push(end);
1376 parameters
1377}
1378
1379fn refine_to_lattice(net: &mut Vec<Vec<Vec4>>, knots: &mut Vec<f64>, degree: usize, parameters: &[f64]) {
1388 let p = degree;
1389 let n = net.len() - 1;
1390 let mut inserted = Vec::new();
1391 for ¶meter in parameters {
1392 let multiplicity = knots.iter().filter(|&&knot| knot == parameter).count();
1393 for _ in multiplicity..p {
1394 inserted.push(parameter);
1395 }
1396 }
1397 if inserted.is_empty() || p == 0 {
1398 return;
1399 }
1400 inserted.sort_by(f64::total_cmp);
1401 let find_span = |parameter: f64| -> usize {
1402 if parameter >= knots[n + 1] {
1403 return n;
1404 }
1405 (knots.partition_point(|&knot| knot <= parameter) - 1).clamp(p, n)
1406 };
1407 let r = inserted.len() - 1;
1408 let m = n + p + 1;
1409 let a = find_span(inserted[0]);
1410 let b = find_span(inserted[r]) + 1;
1411 let columns = net[0].len();
1412 let blank = vec![
1413 Vec4 {
1414 x: 0.0,
1415 y: 0.0,
1416 z: 0.0,
1417 w: 0.0,
1418 };
1419 columns
1420 ];
1421 let mut refined = vec![blank; n + r + 2];
1422 let mut refined_knots = vec![0.0; m + r + 2];
1423 for j in 0..=a - p {
1424 refined[j] = net[j].clone();
1425 }
1426 for j in b - 1..=n {
1427 refined[j + r + 1] = net[j].clone();
1428 }
1429 refined_knots[..=a].copy_from_slice(&knots[..=a]);
1430 for j in b + p..=m {
1431 refined_knots[j + r + 1] = knots[j];
1432 }
1433 let mut i = b + p - 1;
1434 let mut k = b + p + r;
1435 for j in (0..=r).rev() {
1436 while inserted[j] <= knots[i] && i > a {
1437 refined[k - p - 1] = net[i - p - 1].clone();
1438 refined_knots[k] = knots[i];
1439 k -= 1;
1440 i -= 1;
1441 }
1442 refined[k - p - 1] = refined[k - p].clone();
1443 for l in 1..=p {
1444 let index = k - p + l;
1445 let numerator = refined_knots[k + l] - inserted[j];
1446 if numerator == 0.0 {
1447 refined[index - 1] = refined[index].clone();
1448 } else {
1449 let alpha = numerator / (refined_knots[k + l] - knots[i - p + l]);
1450 let (head, tail) = refined.split_at_mut(index);
1451 for (before, &here) in head[index - 1].iter_mut().zip(&tail[0]) {
1452 *before = before.scale(alpha).add(here.scale(1.0 - alpha));
1453 }
1454 }
1455 }
1456 refined_knots[k] = inserted[j];
1457 k = k.saturating_sub(1);
1458 }
1459 *net = refined;
1460 *knots = refined_knots;
1461}
1462
1463fn supporting_range(knots: &[f64], degree: usize, count: usize, low: f64, high: f64) -> (usize, usize) {
1466 let first_span = knots.partition_point(|&knot| knot <= low).saturating_sub(1);
1467 let last_span = knots.partition_point(|&knot| knot < high).saturating_sub(1);
1468 (
1469 first_span.saturating_sub(degree).min(count - 1),
1470 last_span.max(first_span).min(count - 1),
1471 )
1472}
1473
1474fn unit_or(vector: Vec3, fallback: Vec3) -> Vec3 {
1475 let length = vector.length();
1476 if length > 0.0 && length.is_finite() {
1477 vector.scale(1.0 / length)
1478 } else {
1479 fallback
1480 }
1481}
1482
1483fn enclosing_sphere(a: (Vec3, f64), b: (Vec3, f64)) -> (Vec3, f64) {
1484 let offset = b.0.sub(a.0);
1485 let separation = offset.length();
1486 if separation + b.1 <= a.1 {
1487 return a;
1488 }
1489 if separation + a.1 <= b.1 {
1490 return b;
1491 }
1492 let radius = (separation + a.1 + b.1) * 0.5;
1493 let centre = a.0.add(offset.scale((radius - a.1) / separation));
1494 (centre, radius)
1495}
1496
1497fn build_cell_tree(
1498 tree: &mut Vec<CellTreeNode>,
1499 spheres: &[(Vec3, f64)],
1500 columns: usize,
1501 [row_low, row_high, column_low, column_high]: [usize; 4],
1502) -> u32 {
1503 if row_high - row_low == 1 && column_high - column_low == 1 {
1504 let cell = row_low * columns + column_low;
1505 tree.push(CellTreeNode {
1506 centre: spheres[cell].0,
1507 radius: spheres[cell].1,
1508 children: None,
1509 cell: cell as u32,
1510 });
1511 return (tree.len() - 1) as u32;
1512 }
1513 let (first, second) = if row_high - row_low >= column_high - column_low {
1514 let middle = (row_low + row_high) / 2;
1515 (
1516 [row_low, middle, column_low, column_high],
1517 [middle, row_high, column_low, column_high],
1518 )
1519 } else {
1520 let middle = (column_low + column_high) / 2;
1521 (
1522 [row_low, row_high, column_low, middle],
1523 [row_low, row_high, middle, column_high],
1524 )
1525 };
1526 let first = build_cell_tree(tree, spheres, columns, first);
1527 let second = build_cell_tree(tree, spheres, columns, second);
1528 let a = tree[first as usize];
1529 let b = tree[second as usize];
1530 let (centre, radius) = enclosing_sphere((a.centre, a.radius), (b.centre, b.radius));
1534 tree.push(CellTreeNode {
1535 centre,
1536 radius: radius * (1.0 + 4.0 * f64::EPSILON),
1537 children: Some((first, second)),
1538 cell: u32::MAX,
1539 });
1540 (tree.len() - 1) as u32
1541}
1542
1543fn projection_cells(surface: &NurbsSurface) -> Result<Option<&ProjectionCells>, String> {
1544 if let Some(cells) = surface.projection_cells.get() {
1545 return Ok(cells.as_ref());
1546 }
1547 let cells = build_projection_cells(surface)?;
1548 Ok(surface.projection_cells.get_or_init(|| cells).as_ref())
1549}
1550
1551fn build_projection_cells(surface: &NurbsSurface) -> Result<Option<ProjectionCells>, String> {
1552 let (closed_u, closed_v) = surface.closed_directions()?;
1553 let us = lattice_parameters(&surface.knots_u, surface.degree_u, closed_u);
1554 let vs = lattice_parameters(&surface.knots_v, surface.degree_v, closed_v);
1555 if us.len() < 2 || vs.len() < 2 {
1556 return Ok(None);
1557 }
1558 let mut scale = 0.0f64;
1559 for control in surface.control_points.iter().flatten() {
1560 let point = control.point()?;
1561 scale = scale.max(point.x.abs()).max(point.y.abs()).max(point.z.abs());
1562 }
1563 let resolution =
1564 ((surface.degree_u + 1) * (surface.degree_v + 1)) as f64 * f64::EPSILON * scale;
1565
1566 let mut net = surface.control_points.clone();
1568 let mut knots_u = surface.knots_u.clone();
1569 refine_to_lattice(&mut net, &mut knots_u, surface.degree_u, &us);
1570 let columns = net.first().map(Vec::len).unwrap_or(0);
1571 let mut transposed: Vec<Vec<Vec4>> =
1572 (0..columns).map(|column| net.iter().map(|row| row[column]).collect()).collect();
1573 let mut knots_v = surface.knots_v.clone();
1574 refine_to_lattice(&mut transposed, &mut knots_v, surface.degree_v, &vs);
1575 let rows = transposed.first().map(Vec::len).unwrap_or(0);
1576 let columns = transposed.len();
1577 if rows == 0 || columns == 0 {
1578 return Ok(None);
1579 }
1580 let mut points = vec![Vec3::default(); rows * columns];
1581 for (column, fibre) in transposed.iter().enumerate() {
1582 for (row, control) in fibre.iter().enumerate() {
1583 points[row * columns + column] = control.point()?;
1584 }
1585 }
1586
1587 let mut nodes = Vec::with_capacity(us.len() * vs.len());
1588 for &u in &us {
1589 for &v in &vs {
1590 nodes.push(surface.evaluate(u, v)?);
1591 }
1592 }
1593
1594 let cell_rows = us.len() - 1;
1595 let cell_columns = vs.len() - 1;
1596 let mut boxes = Vec::with_capacity(cell_rows * cell_columns);
1597 let mut spheres = Vec::with_capacity(cell_rows * cell_columns);
1598 for i in 0..cell_rows {
1599 let (row_low, row_high) =
1600 supporting_range(&knots_u, surface.degree_u, rows, us[i], us[i + 1]);
1601 for j in 0..cell_columns {
1602 let (column_low, column_high) =
1603 supporting_range(&knots_v, surface.degree_v, columns, vs[j], vs[j + 1]);
1604 let at = |row: usize, column: usize| points[row * columns + column];
1605 let c00 = at(row_low, column_low);
1606 let c10 = at(row_high, column_low);
1607 let c01 = at(row_low, column_high);
1608 let c11 = at(row_high, column_high);
1609 let along_u = c10.sub(c00).add(c11.sub(c01));
1610 let along_v = c01.sub(c00).add(c11.sub(c10));
1611 let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
1612 let normal = unit_or(e1.cross(along_v), e1.perpendicular()?);
1613 let e2 = unit_or(normal.cross(e1), e1.perpendicular()?);
1614 let e3 = e1.cross(e2);
1615 let axes = [e1, e2, e3];
1616 let mut low = [f64::INFINITY; 3];
1617 let mut high = [f64::NEG_INFINITY; 3];
1618 for row in row_low..=row_high {
1619 for column in column_low..=column_high {
1620 let point = at(row, column);
1621 for axis in 0..3 {
1622 let coordinate = point.dot(axes[axis]);
1623 low[axis] = low[axis].min(coordinate);
1624 high[axis] = high[axis].max(coordinate);
1625 }
1626 }
1627 }
1628 for axis in 0..3 {
1629 low[axis] -= resolution;
1630 high[axis] += resolution;
1631 }
1632 let mut centre = Vec3::default();
1633 let mut half_diagonal_squared = 0.0;
1634 for axis in 0..3 {
1635 centre = centre.add(axes[axis].scale((low[axis] + high[axis]) * 0.5));
1636 let half = (high[axis] - low[axis]) * 0.5;
1637 half_diagonal_squared += half * half;
1638 }
1639 if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
1640 return Ok(None);
1641 }
1642 spheres.push((centre, half_diagonal_squared.sqrt() + resolution));
1643 boxes.push(CellBox { axes, low, high });
1644 }
1645 }
1646 let mut tree = Vec::with_capacity(2 * boxes.len());
1647 build_cell_tree(&mut tree, &spheres, cell_columns, [0, cell_rows, 0, cell_columns]);
1648 Ok(Some(ProjectionCells {
1649 us,
1650 vs,
1651 nodes,
1652 boxes,
1653 tree,
1654 resolution,
1655 }))
1656}
1657
1658fn optimality(
1663 derivatives: &[[Vec3; 3]; 3],
1664 point: Vec3,
1665 (u, v): (f64, f64),
1666 [ua, ub, va, vb]: [f64; 4],
1667 periodic: (bool, bool),
1668) -> Optimality {
1669 let residual = derivatives[0][0].sub(point);
1670 let residual_length = residual.length();
1671 let su = derivatives[1][0];
1672 let sv = derivatives[0][1];
1673 let f = su.dot(residual);
1674 let g = sv.dot(residual);
1675 let metric_u = su.length_squared();
1676 let metric_v = sv.length_squared();
1677 let stationary_u = metric_u.sqrt() * residual_length <= EPSILON
1678 || f.abs() <= 1e-10 * metric_u.sqrt() * residual_length;
1679 let stationary_v = metric_v.sqrt() * residual_length <= EPSILON
1680 || g.abs() <= 1e-10 * metric_v.sqrt() * residual_length;
1681 let pinned_u = !periodic.0 && ((u <= ua && f > 0.0) || (u >= ub && f < 0.0));
1682 let pinned_v = !periodic.1 && ((v <= va && g > 0.0) || (v >= vb && g < 0.0));
1683 let j00 = derivatives[2][0].dot(residual) + metric_u;
1684 let j01 = derivatives[1][1].dot(residual) + su.dot(sv);
1685 let j11 = derivatives[0][2].dot(residual) + metric_v;
1686 let determinant = j00 * j11 - j01 * j01;
1687 let convex = match (!pinned_u, !pinned_v) {
1688 (true, true) => j00 > 0.0 && determinant > 0.0,
1689 (true, false) => j00 > 0.0,
1690 (false, true) => j11 > 0.0,
1691 (false, false) => true,
1692 };
1693 Optimality {
1694 residual_length,
1695 gradient: (f, g),
1696 metric: (metric_u, metric_v),
1697 hessian: (j00, j01, j11),
1698 determinant,
1699 pinned: (pinned_u, pinned_v),
1700 convex,
1701 minimum: residual_length <= LINEAR_TOLERANCE
1702 || ((stationary_u || pinned_u) && (stationary_v || pinned_v) && convex),
1703 }
1704}
1705
1706struct Optimality {
1707 residual_length: f64,
1708 gradient: (f64, f64),
1709 metric: (f64, f64),
1710 hessian: (f64, f64, f64),
1711 determinant: f64,
1712 pinned: (bool, bool),
1713 convex: bool,
1714 minimum: bool,
1715}
1716
1717fn is_verified_minimum(
1718 surface: &NurbsSurface,
1719 point: Vec3,
1720 at: (f64, f64),
1721 rectangle: [f64; 4],
1722 periodic: (bool, bool),
1723) -> Result<bool, String> {
1724 let derivatives = surface.derivatives_small(at.0, at.1, 2)?;
1725 Ok(optimality(&derivatives, point, at, rectangle, periodic).minimum)
1726}
1727
1728fn confined_descent(
1744 surface: &NurbsSurface,
1745 point: Vec3,
1746 seed: (f64, f64),
1747 rectangle: [f64; 4],
1748 periodic: (bool, bool),
1749) -> Result<(NewtonResult, bool), String> {
1750 let [ua, ub, va, vb] = rectangle;
1751 let mut u = fit_parameter(seed.0, ua, ub, periodic.0);
1752 let mut v = fit_parameter(seed.1, va, vb, periodic.1);
1753 let mut converged = false;
1754 let mut current = surface.evaluate(u, v)?.sub(point).length_squared();
1755 for _ in 0..MAX_NEWTON_ITERATIONS {
1756 let derivatives = surface.derivatives_small(u, v, 2)?;
1757 let state = optimality(&derivatives, point, (u, v), rectangle, periodic);
1758 current = state.residual_length * state.residual_length;
1759 if state.minimum {
1760 converged = true;
1761 break;
1762 }
1763 let (f, g) = state.gradient;
1764 let (metric_u, metric_v) = state.metric;
1765 let (j00, j01, j11) = state.hessian;
1766 let determinant = state.determinant;
1767 let (free_u, free_v) = (!state.pinned.0, !state.pinned.1);
1768 let (mut du, mut dv) = match (free_u, free_v) {
1769 (true, true) if state.convex => (
1770 (-f * j11 + g * j01) / determinant,
1771 (-g * j00 + f * j01) / determinant,
1772 ),
1773 (true, false) if state.convex => (-f / j00, 0.0),
1774 (false, true) if state.convex => (0.0, -g / j11),
1775 _ => (
1776 if free_u && metric_u > 0.0 { -f / metric_u } else { 0.0 },
1777 if free_v && metric_v > 0.0 { -g / metric_v } else { 0.0 },
1778 ),
1779 };
1780 if !(du.is_finite() && dv.is_finite()) || (du == 0.0 && dv == 0.0) {
1781 break;
1782 }
1783 let mut moved = false;
1784 let taken = |next: f64, here: f64, low: f64, high: f64, wraps: bool| {
1786 let raw = next - here;
1787 if wraps {
1788 raw - (high - low) * (raw / (high - low)).round()
1789 } else {
1790 raw
1791 }
1792 };
1793 for _ in 0..40 {
1794 let next_u = fit_parameter(u + du, ua, ub, periodic.0);
1795 let next_v = fit_parameter(v + dv, va, vb, periodic.1);
1796 if next_u == u && next_v == v {
1797 break;
1798 }
1799 let trial = surface.evaluate(next_u, next_v)?.sub(point).length_squared();
1800 if trial <= current {
1801 moved = taken(next_u, u, ua, ub, periodic.0).abs() > 1e-15 * (ub - ua)
1802 || taken(next_v, v, va, vb, periodic.1).abs() > 1e-15 * (vb - va);
1803 u = next_u;
1804 v = next_v;
1805 current = trial;
1806 break;
1807 }
1808 du *= 0.5;
1809 dv *= 0.5;
1810 }
1811 if !moved {
1812 break;
1813 }
1814 }
1815 let result = NewtonResult {
1816 u,
1817 v,
1818 distance_squared: current,
1819 converged,
1820 };
1821 let on_edge =
1822 (!periodic.0 && (u <= ua || u >= ub)) || (!periodic.1 && (v <= va || v >= vb));
1823 Ok((result, on_edge))
1824}
1825
1826#[allow(clippy::too_many_arguments)]
1829fn global_minimum(
1830 surface: &NurbsSurface,
1831 point: Vec3,
1832 domains: [f64; 4],
1833 periodic: (bool, bool),
1834 best: &mut NewtonResult,
1835 stats: &mut GlobalStats,
1836) -> Result<(), String> {
1837 if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
1838 stats.settled_by_floor = true;
1839 return Ok(());
1840 }
1841 let Some(cells) = projection_cells(surface)? else {
1842 return Ok(());
1843 };
1844 let resolution = cells.resolution;
1845 let columns = cells.vs.len() - 1;
1846 let columns_u = cells.us.len() - 1;
1847 let mut bound = best.distance_squared.sqrt() - resolution;
1848
1849 let mut candidates: Vec<(f64, usize)> = Vec::new();
1852 let mut excluded = f64::INFINITY;
1853 let mut stack: Vec<u32> = Vec::with_capacity(64);
1854 stack.push((cells.tree.len() - 1) as u32);
1855 while let Some(index) = stack.pop() {
1856 let node = cells.tree[index as usize];
1857 let sphere_bound = (point.sub(node.centre).length() - node.radius).max(0.0);
1858 if sphere_bound >= bound {
1859 excluded = excluded.min(sphere_bound);
1860 continue;
1861 }
1862 match node.children {
1863 Some((first, second)) => {
1864 stack.push(first);
1865 stack.push(second);
1866 }
1867 None => {
1868 let cell = node.cell as usize;
1869 let box_bound = cells.boxes[cell].lower_bound(point);
1870 if box_bound < bound {
1871 candidates.push((box_bound, cell));
1872 } else {
1873 excluded = excluded.min(box_bound);
1874 }
1875 }
1876 }
1877 }
1878 stats.candidates = candidates.len();
1879 candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
1880 stats.hull_lower_bound = candidates
1881 .first()
1882 .map_or(excluded, |&(least, _)| least.min(excluded));
1883
1884 let cell_of = |u: f64, v: f64| {
1890 let row = (cells.us.partition_point(|&value| value <= u).max(1) - 1).min(columns_u - 1);
1891 let column = (cells.vs.partition_point(|&value| value <= v).max(1) - 1).min(columns - 1);
1892 row * columns + column
1893 };
1894 let mut settled: Vec<usize> = Vec::new();
1895 if is_verified_minimum(surface, point, (best.u, best.v), domains, periodic)? {
1896 settled.push(cell_of(best.u, best.v));
1897 }
1898 for (cell_bound, cell) in candidates {
1899 if cell_bound >= bound {
1900 break;
1901 }
1902 let row = cell / columns;
1903 let column = cell % columns;
1904 let rectangle = [
1905 cells.us[row],
1906 cells.us[row + 1],
1907 cells.vs[column],
1908 cells.vs[column + 1],
1909 ];
1910 if settled.contains(&cell) {
1911 continue;
1912 }
1913 let mut seed = (rectangle[0], rectangle[2]);
1914 let mut seed_distance = f64::INFINITY;
1915 for (node_row, node_column) in
1916 [(row, column), (row + 1, column), (row, column + 1), (row + 1, column + 1)]
1917 {
1918 let distance = cells.nodes[node_row * cells.vs.len() + node_column]
1919 .sub(point)
1920 .length_squared();
1921 if distance < seed_distance {
1922 seed_distance = distance;
1923 seed = (cells.us[node_row], cells.vs[node_column]);
1924 }
1925 }
1926 stats.refinements += 1;
1927 let (confined, on_edge) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1928 let mut found = confined;
1929 if on_edge && confined.distance_squared.sqrt() < bound {
1930 stats.continuations += 1;
1933 let (continued, _) =
1934 confined_descent(surface, point, (confined.u, confined.v), domains, periodic)?;
1935 if continued.distance_squared < found.distance_squared {
1936 found = continued;
1937 }
1938 if continued.converged {
1939 settled.push(cell_of(continued.u, continued.v));
1940 }
1941 }
1942 if found.distance_squared.sqrt() < bound {
1943 *best = found;
1944 bound = best.distance_squared.sqrt() - resolution;
1945 stats.replaced = true;
1946 if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
1947 return Ok(());
1948 }
1949 }
1950 }
1951 Ok(())
1952}
1953
1954
1955
1956#[derive(Clone, Copy)]
1958pub(crate) enum FootDomain {
1959 Clamp,
1960 Wrap,
1961 Extended,
1962}
1963
1964#[derive(Clone, Copy)]
1965pub(crate) struct CurveFootPolicy {
1966 pub domain: FootDomain,
1967 pub iterations: usize,
1968 pub slope_guard: f64,
1969 pub step_limit: f64,
1970 pub parameter_tolerance: f64,
1971 pub mode: FootMode,
1972}
1973
1974#[derive(Clone, Copy)]
1976pub(crate) enum FootMode {
1977 Monotone,
1978 Witness,
1979 Stationary { positive_slope: bool, relative_step: bool },
1980 Backtracked,
1981 Planar,
1982}
1983
1984fn curve_foot_newton_state(
1987 curve: &NurbsCurve,
1988 point: Vec3,
1989 seed: f64,
1990 bracket: [f64; 2],
1991 policy: CurveFootPolicy,
1992) -> Result<(f64, f64, f64), String> {
1993 let fit = |u| match policy.domain {
1994 FootDomain::Clamp => fit_parameter(u, bracket[0], bracket[1], false),
1995 FootDomain::Wrap => fit_parameter(u, bracket[0], bracket[1], true),
1996 FootDomain::Extended => u,
1997 };
1998 let extended = matches!(policy.domain, FootDomain::Extended) || matches!(policy.mode, FootMode::Backtracked);
1999 let planar = matches!(policy.mode, FootMode::Planar);
2000 let project = |p: Vec3| if planar { Vec3::new(p.x, p.y, 0.0) } else { p };
2001 let point = project(point);
2002 let distance = |u| -> Result<f64, String> {
2003 let p = if extended { curve.derivatives_extended(u, 0)?[0] } else { curve.evaluate(u)? };
2004 Ok(project(p).sub(point).length())
2005 };
2006 let monotone = matches!(policy.mode, FootMode::Monotone | FootMode::Backtracked);
2007 let witness = matches!(policy.mode, FootMode::Witness);
2008 let mut u = if witness { seed } else { fit(seed) };
2009 let mut nearest_u = seed;
2010 let mut nearest_squared = f64::INFINITY;
2011 let mut best = if monotone { distance(u)? } else { f64::INFINITY };
2012 for _ in 0..policy.iterations {
2013 let derivatives = if extended {
2014 let values = curve.derivatives_extended(u, 2)?;
2015 [values[0], values[1], values[2]]
2016 } else if matches!(policy.mode, FootMode::Stationary { .. })
2017 || (planar && !crate::curve::small_curve_derivatives_enabled()) {
2018 let values = curve.derivatives(u, 2)?;
2019 [values[0], values[1], values[2]]
2020 } else {
2021 curve.derivatives_small(u, 2)?
2022 }.map(project);
2023 let offset = derivatives[0].sub(point);
2024 let numerator = offset.dot(derivatives[1]);
2025 let denominator = derivatives[1].dot(derivatives[1]) + offset.dot(derivatives[2]);
2026 if witness {
2027 let squared = offset.length_squared();
2028 if squared < nearest_squared {
2029 nearest_u = u;
2030 nearest_squared = squared;
2031 }
2032 let residual_length = squared.sqrt();
2033 if residual_length <= LINEAR_TOLERANCE
2034 || numerator.abs() <= EPSILON + 1e-10 * derivatives[1].length() * residual_length {
2035 break;
2036 }
2037 }
2038 let singular = if planar { denominator.abs() < policy.slope_guard }
2039 else { denominator.abs() <= policy.slope_guard };
2040 if singular || (matches!(policy.mode, FootMode::Stationary { positive_slope: true, .. }) && !(denominator > 0.0)) { break; }
2041 let mut step = if matches!(policy.mode, FootMode::Backtracked) && !(denominator > 0.0) {
2042 policy.step_limit.copysign(numerator)
2043 } else {
2044 (numerator / denominator).clamp(-policy.step_limit, policy.step_limit)
2045 };
2046 let mut next = fit(u - step);
2047 let mut next_distance = if monotone { distance(next)? } else { f64::INFINITY };
2048 if matches!(policy.mode, FootMode::Backtracked) {
2049 for _ in 0..30 {
2050 if next_distance < best { break; }
2051 step *= 0.5;
2052 next = fit(u - step);
2053 next_distance = distance(next)?;
2054 }
2055 if !(next_distance < best) { break; }
2056 } else if monotone && next_distance >= best { break; }
2057 let moved = if planar || matches!(policy.domain, FootDomain::Extended) { step }
2058 else if matches!(policy.domain, FootDomain::Wrap) {
2059 let period = bracket[1] - bracket[0];
2060 let raw = next - u;
2061 raw - period * (raw / period).round()
2062 } else { next - u };
2063 let tolerance = if matches!(policy.mode, FootMode::Stationary { relative_step: true, .. }) {
2064 policy.parameter_tolerance * (1.0 + u.abs())
2065 } else { policy.parameter_tolerance };
2066 let small = if planar || matches!(policy.mode, FootMode::Backtracked) { moved.abs() < tolerance }
2067 else { moved.abs() <= tolerance };
2068 if small && matches!(policy.mode, FootMode::Monotone) { break; }
2069 u = next;
2070 best = next_distance;
2071 if small { break; }
2072 }
2073 if witness { return Ok((u, nearest_u, nearest_squared)); }
2074 if !monotone { best = distance(u)?; }
2075 Ok((u, u, best * best))
2076}
2077
2078pub(crate) fn curve_foot_newton(
2079 curve: &NurbsCurve, point: Vec3, seed: f64, bracket: [f64; 2], policy: CurveFootPolicy,
2080) -> Result<(f64, f64), String> {
2081 let (_, nearest, squared) = curve_foot_newton_state(curve, point, seed, bracket, policy)?;
2082 Ok((nearest, squared))
2083}