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 surface_newton_impl(surface, point, seed_u, seed_v, domains, closed_u, closed_v,
376 switch!("BREP_PROJECTION_LAZY_DERIVATIVES", false))
377}
378
379fn surface_newton_impl(
384 surface: &NurbsSurface,
385 point: Vec3,
386 seed_u: f64,
387 seed_v: f64,
388 domains: [f64; 4],
389 closed_u: bool,
390 closed_v: bool,
391 lazy: bool,
392) -> Result<NewtonResult, String> {
393 let [u0, u1, v0, v1] = domains;
394 let mut u = seed_u;
395 let mut v = seed_v;
396 let mut converged = false;
397 let mut best = NewtonResult {
398 u,
399 v,
400 distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
401 converged,
402 };
403 for iteration in 0..MAX_NEWTON_ITERATIONS {
404 crate::imprint::work_profile::add(|c| c.newton_iterations += 1);
405 let derivatives = surface.derivatives_small(u, v, if lazy { 0 } else { 2 })?;
406 let residual = derivatives[0][0].sub(point);
407 let distance_squared = residual.length_squared();
408 if distance_squared < best.distance_squared {
409 best.u = u;
410 best.v = v;
411 best.distance_squared = distance_squared;
412 }
413 let residual_length = distance_squared.sqrt();
414 if residual_length <= LINEAR_TOLERANCE {
415 crate::imprint::work_profile::add(|c| { c.newton_unused += 1; c.newton_unused_first += u64::from(iteration == 0); });
416 converged = true;
417 break;
418 }
419 let derivatives = if lazy { surface.derivatives_small(u, v, 2)? } else { derivatives };
420 let f = derivatives[1][0].dot(residual);
421 let g = derivatives[0][1].dot(residual);
422 let tangent_u_length = derivatives[1][0].length();
423 let tangent_v_length = derivatives[0][1].length();
424 let cosine_u = if tangent_u_length * residual_length <= EPSILON {
425 0.0
426 } else {
427 f.abs() / (tangent_u_length * residual_length)
428 };
429 let cosine_v = if tangent_v_length * residual_length <= EPSILON {
430 0.0
431 } else {
432 g.abs() / (tangent_v_length * residual_length)
433 };
434 if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
435 converged = true;
436 break;
437 }
438 let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
439 let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
440 let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
441 let determinant = j00 * j11 - j01 * j01;
442 if determinant.abs() <= EPSILON {
443 break;
444 }
445 let mut du = (-f * j11 + g * j01) / determinant;
446 let mut dv = (-g * j00 + f * j01) / determinant;
447 du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
448 dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
449 let next_u = fit_parameter(u + du, u0, u1, closed_u);
450 let next_v = fit_parameter(v + dv, v0, v1, closed_v);
451 let stalled =
452 (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
453 u = next_u;
454 v = next_v;
455 if stalled {
456 break;
457 }
458 }
459 let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
460 if final_distance_squared < best.distance_squared {
461 best.u = u;
462 best.v = v;
463 best.distance_squared = final_distance_squared;
464 }
465 best.converged = converged;
466 Ok(best)
467}
468
469#[track_caller]
475pub fn project_point_to_surface(
476 surface: &NurbsSurface,
477 point: Vec3,
478) -> Result<SurfaceProjection, String> {
479 crate::imprint::work_profile::add(|c| c.projection_global += 1);
480 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_PROJECT, surface);
481 if let Some(analytic) = surface.analytic() {
485 if let Some(projection) = analytic.project(surface, point) {
486 return Ok(projection);
487 }
488 }
489 if let Some(ellipsoid) = surface.affine_sphere() {
490 if let Some(projection) = ellipsoid.project(surface, point) {
491 return Ok(projection);
492 }
493 }
494 project_point_to_surface_general(surface, point)
495}
496
497pub fn project_point_to_surface_seeded(
507 surface: &NurbsSurface,
508 point: Vec3,
509 seed_u: f64,
510 seed_v: f64,
511) -> Result<SurfaceProjection, String> {
512 crate::imprint::work_profile::add(|c| c.projection_seeded += 1);
513 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::SEEDED);
514 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_SEEDED, surface);
515 if let Some(ellipsoid) = surface.affine_sphere() {
519 if let Some(projection) = ellipsoid.project(surface, point) {
520 return Ok(projection);
521 }
522 }
523 let [u0, u1] = surface.domain_u()?;
524 let [v0, v1] = surface.domain_v()?;
525 let (closed_u, closed_v) = surface.closed_directions()?;
526 let result = surface_newton(
527 surface,
528 point,
529 seed_u,
530 seed_v,
531 [u0, u1, v0, v1],
532 closed_u,
533 closed_v,
534 )?;
535 let projected = surface.evaluate(result.u, result.v)?;
536 Ok(SurfaceProjection {
537 u: result.u,
538 v: result.v,
539 point: projected,
540 distance: projected.sub(point).length(),
541 })
542}
543
544#[track_caller]
578pub(crate) fn project_point_to_surface_from_seed(
579 surface: &NurbsSurface,
580 point: Vec3,
581 seed: [f64; 2],
582) -> Result<SurfaceProjection, String> {
583 crate::imprint::work_profile::add(|c| c.projection_from_seed += 1);
584 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::SEEDED);
585 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_SEED, surface);
586 if let Some(analytic) = surface.analytic() {
587 if let Some(projection) = analytic.project(surface, point) {
588 return Ok(projection);
589 }
590 }
591 if let Some(ellipsoid) = surface.affine_sphere() {
592 if let Some(projection) = ellipsoid.project(surface, point) {
593 return Ok(projection);
594 }
595 }
596 let caller = std::panic::Location::caller();
597 if !seeded_answer_admitted(caller) {
598 return project_point_to_surface_general(surface, point);
599 }
600 let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
601 if seed_audit_enabled() {
602 let nearest = project_point_to_surface_general(surface, point)?;
603 if seeded.point.sub(nearest.point).length() > LINEAR_TOLERANCE {
604 seed_audit_record(caller, surface, seed, &seeded, &nearest);
605 }
606 }
607 Ok(seeded)
608}
609
610fn seeded_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
613 static SITES: std::sync::OnceLock<Option<Option<Vec<String>>>> = std::sync::OnceLock::new();
614 let sites = SITES.get_or_init(|| {
615 if std::env::var("BREP_PROJECTION_FROM_SEED").as_deref() == Ok("0") {
616 return None;
617 }
618 Some(std::env::var("BREP_PROJECTION_FROM_SEED_SITES").ok().map(|value| {
619 value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect()
620 }))
621 });
622 match sites {
623 None => false,
624 Some(None) => true,
625 Some(Some(only)) => {
626 let location = format!("{}:{}", caller.file(), caller.line());
627 only.iter().any(|site| location.contains(site.as_str()))
628 }
629 }
630}
631
632fn seed_audit_enabled() -> bool {
633 debug_flag!("BREP_PROJECTION_AUDIT")
634}
635
636fn seed_audit_record(
640 caller: &std::panic::Location<'_>,
641 surface: &NurbsSurface,
642 seed: [f64; 2],
643 seeded: &SurfaceProjection,
644 nearest: &SurfaceProjection,
645) {
646 use std::io::Write;
647 static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
648 std::sync::OnceLock::new();
649 let sink = SINK.get_or_init(|| {
650 let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
651 let path = std::path::Path::new(&directory)
652 .join(format!("projection-seed-audit-{}.tsv", std::process::id()));
653 std::fs::OpenOptions::new()
654 .create(true)
655 .append(true)
656 .open(path)
657 .ok()
658 .map(std::sync::Mutex::new)
659 });
660 if let Some(file) = sink {
661 if let Ok(mut file) = file.lock() {
662 let _ = writeln!(
663 file,
664 "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.6e}\t{}x{} cp {}x{}",
665 caller.file(),
666 caller.line(),
667 seeded.distance,
668 nearest.distance,
669 seed[0],
670 seed[1],
671 seeded.u,
672 seeded.v,
673 nearest.u,
674 nearest.v,
675 seeded.point.sub(nearest.point).length(),
676 surface.degree_u,
677 surface.degree_v,
678 surface.control_points.len(),
679 surface.control_points.first().map(Vec::len).unwrap_or(0),
680 );
681 }
682 }
683}
684
685fn projection_seed_grid(surface: &NurbsSurface) -> Result<&SampleGrid, String> {
690 if let Some(grid) = surface.projection_grid.get() {
691 return Ok(grid);
692 }
693 let [u0, u1] = surface.domain_u()?;
694 let [v0, v1] = surface.domain_v()?;
695 let (closed_u, closed_v) = surface.closed_directions()?;
696 let mut breaks_u = vec![u0];
697 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
698 breaks_u.push(u1);
699 let mut breaks_v = vec![v0];
700 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
701 breaks_v.push(v1);
702 let samples_u = if closed_u {
703 8usize.max(surface.degree_u * 4)
704 } else {
705 3usize.max(surface.degree_u + 1)
706 };
707 let samples_v = if closed_v {
708 8usize.max(surface.degree_v * 4)
709 } else {
710 3usize.max(surface.degree_v + 1)
711 };
712 let mut grid = Vec::new();
713 for u_pair in breaks_u.windows(2) {
714 for v_pair in breaks_v.windows(2) {
715 for i in 0..=samples_u {
716 for j in 0..=samples_v {
717 let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
718 let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
719 grid.push((u, v, surface.evaluate(u, v)?));
720 }
721 }
722 }
723 }
724 Ok(surface.projection_grid.get_or_init(|| SampleGrid::new(grid)))
725}
726
727fn projection_dense_grid(surface: &NurbsSurface) -> Result<&SampleGrid, String> {
734 if let Some(grid) = surface.projection_dense_grid.get() {
735 return Ok(grid);
736 }
737 let [u0, u1] = surface.domain_u()?;
738 let [v0, v1] = surface.domain_v()?;
739 let mut breaks_u = vec![u0];
740 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
741 breaks_u.push(u1);
742 let mut breaks_v = vec![v0];
743 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
744 breaks_v.push(v1);
745 let count_u = 32usize.max((breaks_u.len() - 1) * 8);
746 let count_v = 16usize.max((breaks_v.len() - 1) * 8);
747 let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
748 for i in 0..=count_u {
749 for j in 0..=count_v {
750 let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
751 let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
752 grid.push((u, v, surface.evaluate(u, v)?));
753 }
754 }
755 Ok(surface.projection_dense_grid.get_or_init(|| SampleGrid::new(grid)))
756}
757
758const SAMPLE_BLOCK: usize = 32;
760
761const GROUP_BLOCKS: usize = 32;
764
765#[derive(Clone, Debug)]
770pub(crate) struct SampleGrid {
771 samples: Vec<(f64, f64, Vec3)>,
772 blocks: Vec<SampleBlock>,
773 groups: Vec<SampleBlock>,
778}
779
780#[derive(Clone, Copy, Debug)]
783struct SampleBlock {
784 low: Vec3,
785 high: Vec3,
786 prunable: bool,
787}
788
789impl SampleGrid {
790 fn new(samples: Vec<(f64, f64, Vec3)>) -> Self {
791 let blocks = samples
792 .chunks(SAMPLE_BLOCK)
793 .map(|chunk| {
794 let mut low = chunk[0].2;
795 let mut high = low;
796 let mut prunable = true;
797 for &(_, _, p) in chunk {
798 prunable &= p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
799 low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
800 high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
801 }
802 SampleBlock { low, high, prunable }
803 })
804 .collect::<Vec<_>>();
805 let groups = blocks
806 .chunks(GROUP_BLOCKS)
807 .map(|chunk| {
808 let mut group = chunk[0];
809 for block in chunk {
810 group.low = Vec3::new(group.low.x.min(block.low.x), group.low.y.min(block.low.y), group.low.z.min(block.low.z));
811 group.high = Vec3::new(group.high.x.max(block.high.x), group.high.y.max(block.high.y), group.high.z.max(block.high.z));
812 group.prunable &= block.prunable;
813 }
814 group
815 })
816 .collect();
817 Self { samples, blocks, groups }
818 }
819}
820
821impl SampleBlock {
822 fn lower_bound(&self, point: Vec3) -> f64 {
830 let gap = |p: f64, low: f64, high: f64| {
831 if p < low {
832 low - p
833 } else if p > high {
834 p - high
835 } else {
836 0.0
837 }
838 };
839 let x = gap(point.x, self.low.x, self.high.x);
840 let y = gap(point.y, self.low.y, self.high.y);
841 let z = gap(point.z, self.low.z, self.high.z);
842 x * x + y * y + z * z
843 }
844}
845
846fn pruned_scan() -> bool {
849 switch!("BREP_PROJECTION_PRUNED_SCAN", true)
850}
851
852fn nearest_sample(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult) {
862 nearest_sample_with(grid, point, best, pruned_scan(), bound_first_scan());
863}
864
865fn bound_first_scan() -> bool {
881 switch!("BREP_PROJECTION_BOUND_FIRST", true)
882}
883
884fn nearest_sample_with(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult, pruned: bool, bound_first: bool) {
894 if !(pruned && bound_first) {
895 for (block, chunk) in grid.blocks.iter().zip(grid.samples.chunks(SAMPLE_BLOCK)) {
896 if pruned && block.prunable && block.lower_bound(point) >= best.distance_squared {
897 continue;
898 }
899 scan_run(chunk, point, best);
900 }
901 return;
902 }
903 let mut bound = best.distance_squared;
906 let lowest_group = lowest_floor(&grid.groups, 0..grid.groups.len(), point);
907 if let Some(group) = lowest_group {
908 let runs = group * GROUP_BLOCKS..((group + 1) * GROUP_BLOCKS).min(grid.blocks.len());
909 if let Some(run) = lowest_floor(&grid.blocks, runs, point) {
910 let start = run * SAMPLE_BLOCK;
911 let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
912 for &(_, _, sample) in &grid.samples[start..end] {
913 let distance_squared = sample.sub(point).length_squared();
914 if distance_squared < bound {
915 bound = distance_squared;
916 }
917 }
918 }
919 }
920 let skip = |box_: &SampleBlock, held: f64| {
921 if !box_.prunable {
922 return false;
923 }
924 let floor = box_.lower_bound(point);
925 floor >= held || floor > bound
926 };
927 for (group_index, group) in grid.groups.iter().enumerate() {
928 if skip(group, best.distance_squared) {
929 continue;
930 }
931 let first = group_index * GROUP_BLOCKS;
932 let last = (first + GROUP_BLOCKS).min(grid.blocks.len());
933 for run in first..last {
934 if skip(&grid.blocks[run], best.distance_squared) {
935 continue;
936 }
937 let start = run * SAMPLE_BLOCK;
938 let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
939 scan_run(&grid.samples[start..end], point, best);
940 }
941 }
942}
943
944fn scan_run(run: &[(f64, f64, Vec3)], point: Vec3, best: &mut NewtonResult) {
947 for &(u, v, sample) in run {
948 let distance_squared = sample.sub(point).length_squared();
949 if distance_squared < best.distance_squared {
950 best.u = u;
951 best.v = v;
952 best.distance_squared = distance_squared;
953 }
954 }
955}
956
957fn lowest_floor(boxes: &[SampleBlock], range: std::ops::Range<usize>, point: Vec3) -> Option<usize> {
960 let mut lowest: Option<(usize, f64)> = None;
961 for index in range {
962 let box_ = &boxes[index];
963 if !box_.prunable {
964 continue;
965 }
966 let floor = box_.lower_bound(point);
967 if lowest.is_none_or(|(_, held)| floor < held) {
968 lowest = Some((index, floor));
969 }
970 }
971 lowest.map(|(index, _)| index)
972}
973
974const RING_EXPONENTS: usize = 22;
978const RING_INDICES: usize = 33; #[inline]
981fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
982 (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
983}
984
985fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
992 if let Some(grid) = surface.projection_ring_grid.get() {
993 return Ok(grid);
994 }
995 let [u0, u1] = surface.domain_u()?;
996 let [v0, v1] = surface.domain_v()?;
997 let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
998 for exponent in 1..=RING_EXPONENTS as i32 {
999 let offset = 1.0 / 2f64.powi(exponent);
1000 let e = exponent as usize - 1;
1001 let v_lo = v0 + (v1 - v0) * offset;
1002 let v_hi = v1 - (v1 - v0) * offset;
1003 let u_lo = u0 + (u1 - u0) * offset;
1004 let u_hi = u1 - (u1 - u0) * offset;
1005 for index in 0..=32 {
1006 let u = u0 + (u1 - u0) * index as f64 / 32.0;
1007 grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
1008 grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
1009 }
1010 for index in 0..=32 {
1011 let v = v0 + (v1 - v0) * index as f64 / 32.0;
1012 grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
1013 grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
1014 }
1015 }
1016 Ok(surface.projection_ring_grid.get_or_init(|| grid))
1017}
1018
1019fn nearest_seed_answer(
1029 surface: &NurbsSurface,
1030 point: Vec3,
1031 [u0, u1, v0, v1]: [f64; 4],
1032 closed_u: bool,
1033 closed_v: bool,
1034) -> Result<NewtonResult, String> {
1035 let mut best = NewtonResult {
1036 u: u0,
1037 v: v0,
1038 distance_squared: f64::INFINITY,
1039 converged: false,
1040 };
1041 nearest_sample(projection_seed_grid(surface)?, point, &mut best);
1042 if let Some(revolution) = project_linear_revolution(surface, point)? {
1043 if revolution.distance * revolution.distance < best.distance_squared {
1044 best.u = revolution.u;
1045 best.v = revolution.v;
1046 best.distance_squared = revolution.distance * revolution.distance;
1047 }
1048 }
1049 let first_newton = surface_newton(
1050 surface,
1051 point,
1052 best.u,
1053 best.v,
1054 [u0, u1, v0, v1],
1055 closed_u,
1056 closed_v,
1057 )?;
1058 if first_newton.distance_squared < best.distance_squared {
1059 best = first_newton;
1060 }
1061 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
1062 let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
1063 if degenerate || !first_newton.converged {
1064 let mut grid = best;
1065 nearest_sample(projection_dense_grid(surface)?, point, &mut grid);
1066 if grid.distance_squared < best.distance_squared {
1067 best = grid;
1068 }
1069 let polished = surface_newton(
1070 surface,
1071 point,
1072 grid.u,
1073 grid.v,
1074 [u0, u1, v0, v1],
1075 closed_u,
1076 closed_v,
1077 )?;
1078 if polished.distance_squared < best.distance_squared {
1079 best = polished;
1080 }
1081
1082 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
1083 if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
1084 let ring_grid = projection_ring_grid(surface)?;
1085 let mut ring = best;
1086 let scan = |side: usize,
1090 exponent_index: usize,
1091 along_u: bool,
1092 fixed: f64,
1093 candidate: &mut NewtonResult| {
1094 for index in 0..=32 {
1095 let moving = if along_u {
1096 u0 + (u1 - u0) * index as f64 / 32.0
1097 } else {
1098 v0 + (v1 - v0) * index as f64 / 32.0
1099 };
1100 let (u, v) = if along_u {
1101 (moving, fixed)
1102 } else {
1103 (fixed, moving)
1104 };
1105 let distance_squared = ring_grid
1106 [ring_slot(side, exponent_index, index as usize)]
1107 .sub(point)
1108 .length_squared();
1109 if distance_squared < candidate.distance_squared {
1110 candidate.u = u;
1111 candidate.v = v;
1112 candidate.distance_squared = distance_squared;
1113 }
1114 }
1115 };
1116 for exponent in 1..=22 {
1117 let offset = 1.0 / 2f64.powi(exponent);
1118 let e = exponent as usize - 1;
1119 if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
1120 scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
1121 }
1122 if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
1123 scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
1124 }
1125 if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
1126 scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
1127 }
1128 if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
1129 scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
1130 }
1131 }
1132 if ring.distance_squared < best.distance_squared {
1133 best = ring;
1134 }
1135 let polished = surface_newton(
1136 surface,
1137 point,
1138 ring.u,
1139 ring.v,
1140 [u0, u1, v0, v1],
1141 closed_u,
1142 closed_v,
1143 )?;
1144 if polished.distance_squared < best.distance_squared {
1145 best = polished;
1146 }
1147 }
1148 }
1149 Ok(best)
1150}
1151
1152fn global_stage_enabled() -> bool {
1157 switch!("BREP_PROJECTION_GLOBAL", true)
1158}
1159
1160pub(crate) fn global_site_filter_active() -> bool {
1169 static ACTIVE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1174 *ACTIVE.get_or_init(|| {
1175 std::env::var_os("BREP_PROJECTION_GLOBAL_SITES").is_some() || std::env::var_os("BREP_PROJECTION_GLOBAL_EXCEPT").is_some()
1176 })
1177}
1178
1179fn global_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
1180 fn list(name: &str) -> Option<Vec<String>> {
1181 std::env::var(name)
1182 .ok()
1183 .map(|value| value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect())
1184 }
1185 static SITES: std::sync::OnceLock<(Option<Vec<String>>, Option<Vec<String>>)> =
1186 std::sync::OnceLock::new();
1187 let (only, except) = SITES.get_or_init(|| {
1188 (list("BREP_PROJECTION_GLOBAL_SITES"), list("BREP_PROJECTION_GLOBAL_EXCEPT"))
1189 });
1190 if only.is_none() && except.is_none() {
1191 return true;
1192 }
1193 let location = format!("{}:{}", caller.file(), caller.line());
1194 let named = |sites: &Vec<String>| sites.iter().any(|site| location.contains(site.as_str()));
1195 only.as_ref().is_none_or(named) && !except.as_ref().is_some_and(named)
1196}
1197
1198fn audit_record(caller: &std::panic::Location<'_>, surface: &NurbsSurface, local: &NewtonResult, global: &NewtonResult) {
1204 use std::io::Write;
1205 static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
1206 std::sync::OnceLock::new();
1207 let sink = SINK.get_or_init(|| {
1208 let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
1209 let path = std::path::Path::new(&directory)
1210 .join(format!("projection-audit-{}.tsv", std::process::id()));
1211 std::fs::OpenOptions::new()
1212 .create(true)
1213 .append(true)
1214 .open(path)
1215 .ok()
1216 .map(std::sync::Mutex::new)
1217 });
1218 if let Some(file) = sink {
1219 if let Ok(mut file) = file.lock() {
1220 let _ = writeln!(
1221 file,
1222 "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{}x{} cp {}x{}",
1223 caller.file(),
1224 caller.line(),
1225 local.distance_squared.sqrt(),
1226 global.distance_squared.sqrt(),
1227 local.u,
1228 local.v,
1229 global.u,
1230 global.v,
1231 surface.degree_u,
1232 surface.degree_v,
1233 surface.control_points.len(),
1234 surface.control_points.first().map(Vec::len).unwrap_or(0),
1235 );
1236 }
1237 }
1238}
1239
1240#[track_caller]
1316pub(crate) fn project_point_to_surface_general(
1317 surface: &NurbsSurface,
1318 point: Vec3,
1319) -> Result<SurfaceProjection, String> {
1320 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_GENERAL, surface);
1321 let [u0, u1] = surface.domain_u()?;
1322 let [v0, v1] = surface.domain_v()?;
1323 let (closed_u, closed_v) = surface.closed_directions()?;
1324 let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1325 let mut best = local;
1326 if global_stage_enabled() {
1327 global_minimum(
1328 surface,
1329 point,
1330 [u0, u1, v0, v1],
1331 (closed_u, closed_v),
1332 &mut best,
1333 &mut GlobalStats::default(),
1334 )?;
1335 if best.u != local.u || best.v != local.v {
1336 let caller = std::panic::Location::caller();
1337 if global_answer_admitted(caller) {
1338 audit_record(caller, surface, &local, &best);
1339 } else {
1340 best = local;
1341 }
1342 }
1343 }
1344 let projected = surface.evaluate(best.u, best.v)?;
1345 Ok(SurfaceProjection {
1346 u: best.u,
1347 v: best.v,
1348 point: projected,
1349 distance: projected.sub(point).length(),
1350 })
1351}
1352
1353pub(crate) fn project_point_to_surface_from_nearest_seed(
1363 surface: &NurbsSurface,
1364 point: Vec3,
1365) -> Result<SurfaceProjection, String> {
1366 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_NEAREST_SEED, surface);
1367 if let Some(analytic) = surface.analytic() {
1368 if let Some(projection) = analytic.project(surface, point) {
1369 return Ok(projection);
1370 }
1371 }
1372 if let Some(ellipsoid) = surface.affine_sphere() {
1373 if let Some(projection) = ellipsoid.project(surface, point) {
1374 return Ok(projection);
1375 }
1376 }
1377 let [u0, u1] = surface.domain_u()?;
1378 let [v0, v1] = surface.domain_v()?;
1379 let (closed_u, closed_v) = surface.closed_directions()?;
1380 let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1381 let projected = surface.evaluate(local.u, local.v)?;
1382 Ok(SurfaceProjection {
1383 u: local.u,
1384 v: local.v,
1385 point: projected,
1386 distance: projected.sub(point).length(),
1387 })
1388}
1389
1390#[doc(hidden)]
1393pub fn project_point_to_surface_general_lanes(
1394 surface: &NurbsSurface,
1395 point: Vec3,
1396 run_global: bool,
1397) -> Result<(SurfaceProjection, GlobalStats), String> {
1398 let [u0, u1] = surface.domain_u()?;
1399 let [v0, v1] = surface.domain_v()?;
1400 let (closed_u, closed_v) = surface.closed_directions()?;
1401 let mut best = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1402 let mut stats = GlobalStats::default();
1403 if run_global {
1404 global_minimum(surface, point, [u0, u1, v0, v1], (closed_u, closed_v), &mut best, &mut stats)?;
1405 }
1406 let projected = surface.evaluate(best.u, best.v)?;
1407 Ok((
1408 SurfaceProjection {
1409 u: best.u,
1410 v: best.v,
1411 point: projected,
1412 distance: projected.sub(point).length(),
1413 },
1414 stats,
1415 ))
1416}
1417
1418#[doc(hidden)]
1421#[derive(Clone, Debug, Default)]
1422pub struct BasinCensus {
1423 pub cells: usize,
1426 pub descents: usize,
1428 pub unconverged: usize,
1430 pub cells_with_two_basins: usize,
1434 pub foot_cells_with_two_basins: usize,
1437 pub least: f64,
1439 pub least_uv: (f64, f64),
1440 pub foot_cell_descents: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)>,
1444}
1445
1446#[doc(hidden)]
1451pub fn project_point_to_surface_basin_census(
1452 surface: &NurbsSurface,
1453 point: Vec3,
1454 answer_distance: f64,
1455 bar: f64,
1456) -> Result<BasinCensus, String> {
1457 let mut census = BasinCensus {
1458 least: f64::INFINITY,
1459 ..BasinCensus::default()
1460 };
1461 let Some(cells) = projection_cells(surface)? else {
1462 return Ok(census);
1463 };
1464 let columns = cells.vs.len() - 1;
1465 let mut spreads: Vec<(usize, f64)> = Vec::new();
1466 let mut runs: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)> = Vec::new();
1467 for (cell, cell_box) in cells.boxes.iter().enumerate() {
1468 if cell_box.lower_bound(point) >= answer_distance {
1469 continue;
1470 }
1471 census.cells += 1;
1472 let (row, column) = (cell / columns, cell % columns);
1473 let rectangle = [cells.us[row], cells.us[row + 1], cells.vs[column], cells.vs[column + 1]];
1474 let middle = ((rectangle[0] + rectangle[1]) * 0.5, (rectangle[2] + rectangle[3]) * 0.5);
1475 let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
1476 for seed in [
1477 (rectangle[0], rectangle[2]),
1478 (rectangle[1], rectangle[2]),
1479 (rectangle[0], rectangle[3]),
1480 (rectangle[1], rectangle[3]),
1481 middle,
1482 ] {
1483 census.descents += 1;
1484 let (found, _) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1485 let distance = found.distance_squared.sqrt();
1486 runs.push((cell, rectangle, seed, (found.u, found.v), distance, found.converged));
1487 if distance < census.least {
1488 census.least = distance;
1489 census.least_uv = (found.u, found.v);
1490 }
1491 if !found.converged {
1492 census.unconverged += 1;
1493 continue;
1494 }
1495 low = low.min(distance.max(LINEAR_TOLERANCE));
1496 high = high.max(distance.max(LINEAR_TOLERANCE));
1497 }
1498 if high - low > bar {
1499 spreads.push((cell, high - low));
1500 }
1501 }
1502 census.cells_with_two_basins = spreads.len();
1503 let (u, v) = census.least_uv;
1504 let foot_cells: Vec<usize> = spreads
1505 .iter()
1506 .map(|&(cell, _)| cell)
1507 .filter(|&cell| {
1508 let (row, column) = (cell / columns, cell % columns);
1509 cells.us[row] <= u && u <= cells.us[row + 1] && cells.vs[column] <= v && v <= cells.vs[column + 1]
1510 })
1511 .collect();
1512 census.foot_cells_with_two_basins = foot_cells.len();
1513 census.foot_cell_descents = runs.into_iter().filter(|run| foot_cells.contains(&run.0)).collect();
1514 Ok(census)
1515}
1516
1517#[doc(hidden)]
1519#[derive(Clone, Copy, Debug, Default)]
1520pub struct GlobalStats {
1521 pub settled_by_floor: bool,
1523 pub candidates: usize,
1525 pub refinements: usize,
1527 pub continuations: usize,
1529 pub replaced: bool,
1531 pub hull_lower_bound: f64,
1534}
1535
1536#[derive(Clone, Debug)]
1539struct CellBox {
1540 axes: [Vec3; 3],
1541 low: [f64; 3],
1542 high: [f64; 3],
1543}
1544
1545impl CellBox {
1546 fn lower_bound(&self, point: Vec3) -> f64 {
1549 let mut squared = 0.0;
1550 for axis in 0..3 {
1551 let coordinate = point.dot(self.axes[axis]);
1552 let excess = (self.low[axis] - coordinate).max(coordinate - self.high[axis]).max(0.0);
1553 squared += excess * excess;
1554 }
1555 squared.sqrt()
1556 }
1557}
1558
1559#[derive(Clone, Copy, Debug)]
1562struct CellTreeNode {
1563 centre: Vec3,
1564 radius: f64,
1565 children: Option<(u32, u32)>,
1566 cell: u32,
1567}
1568
1569#[derive(Clone, Debug)]
1572pub(crate) struct ProjectionCells {
1573 us: Vec<f64>,
1574 vs: Vec<f64>,
1575 nodes: Vec<Vec3>,
1577 boxes: Vec<CellBox>,
1579 tree: Vec<CellTreeNode>,
1581 resolution: f64,
1582}
1583
1584fn lattice_parameters(knots: &[f64], degree: usize, closed: bool) -> Vec<f64> {
1588 let [start, end] = crate::curve::knot_domain(knots, degree);
1589 let samples = if closed {
1590 8usize.max(degree * 4)
1591 } else {
1592 3usize.max(degree + 1)
1593 };
1594 let mut breaks = vec![start];
1595 breaks.extend(interior_knots(knots, degree));
1596 breaks.push(end);
1597 let mut parameters = Vec::with_capacity((breaks.len() - 1) * samples + 1);
1598 for pair in breaks.windows(2) {
1599 for index in 0..samples {
1600 parameters.push(pair[0] + (pair[1] - pair[0]) * index as f64 / samples as f64);
1601 }
1602 }
1603 parameters.push(end);
1604 parameters
1605}
1606
1607fn refine_to_lattice(net: &mut Vec<Vec<Vec4>>, knots: &mut Vec<f64>, degree: usize, parameters: &[f64]) {
1616 let p = degree;
1617 let n = net.len() - 1;
1618 let mut inserted = Vec::new();
1619 for ¶meter in parameters {
1620 let multiplicity = knots.iter().filter(|&&knot| knot == parameter).count();
1621 for _ in multiplicity..p {
1622 inserted.push(parameter);
1623 }
1624 }
1625 if inserted.is_empty() || p == 0 {
1626 return;
1627 }
1628 inserted.sort_by(f64::total_cmp);
1629 let find_span = |parameter: f64| -> usize {
1630 if parameter >= knots[n + 1] {
1631 return n;
1632 }
1633 (knots.partition_point(|&knot| knot <= parameter) - 1).clamp(p, n)
1634 };
1635 let r = inserted.len() - 1;
1636 let m = n + p + 1;
1637 let a = find_span(inserted[0]);
1638 let b = find_span(inserted[r]) + 1;
1639 let columns = net[0].len();
1640 let blank = vec![
1641 Vec4 {
1642 x: 0.0,
1643 y: 0.0,
1644 z: 0.0,
1645 w: 0.0,
1646 };
1647 columns
1648 ];
1649 let mut refined = vec![blank; n + r + 2];
1650 let mut refined_knots = vec![0.0; m + r + 2];
1651 for j in 0..=a - p {
1652 refined[j] = net[j].clone();
1653 }
1654 for j in b - 1..=n {
1655 refined[j + r + 1] = net[j].clone();
1656 }
1657 refined_knots[..=a].copy_from_slice(&knots[..=a]);
1658 for j in b + p..=m {
1659 refined_knots[j + r + 1] = knots[j];
1660 }
1661 let mut i = b + p - 1;
1662 let mut k = b + p + r;
1663 for j in (0..=r).rev() {
1664 while inserted[j] <= knots[i] && i > a {
1665 refined[k - p - 1] = net[i - p - 1].clone();
1666 refined_knots[k] = knots[i];
1667 k -= 1;
1668 i -= 1;
1669 }
1670 refined[k - p - 1] = refined[k - p].clone();
1671 for l in 1..=p {
1672 let index = k - p + l;
1673 let numerator = refined_knots[k + l] - inserted[j];
1674 if numerator == 0.0 {
1675 refined[index - 1] = refined[index].clone();
1676 } else {
1677 let alpha = numerator / (refined_knots[k + l] - knots[i - p + l]);
1678 let (head, tail) = refined.split_at_mut(index);
1679 for (before, &here) in head[index - 1].iter_mut().zip(&tail[0]) {
1680 *before = before.scale(alpha).add(here.scale(1.0 - alpha));
1681 }
1682 }
1683 }
1684 refined_knots[k] = inserted[j];
1685 k = k.saturating_sub(1);
1686 }
1687 *net = refined;
1688 *knots = refined_knots;
1689}
1690
1691fn supporting_range(knots: &[f64], degree: usize, count: usize, low: f64, high: f64) -> (usize, usize) {
1694 let first_span = knots.partition_point(|&knot| knot <= low).saturating_sub(1);
1695 let last_span = knots.partition_point(|&knot| knot < high).saturating_sub(1);
1696 (
1697 first_span.saturating_sub(degree).min(count - 1),
1698 last_span.max(first_span).min(count - 1),
1699 )
1700}
1701
1702fn unit_or(vector: Vec3, fallback: Vec3) -> Vec3 {
1703 let length = vector.length();
1704 if length > 0.0 && length.is_finite() {
1705 vector.scale(1.0 / length)
1706 } else {
1707 fallback
1708 }
1709}
1710
1711fn enclosing_sphere(a: (Vec3, f64), b: (Vec3, f64)) -> (Vec3, f64) {
1712 let offset = b.0.sub(a.0);
1713 let separation = offset.length();
1714 if separation + b.1 <= a.1 {
1715 return a;
1716 }
1717 if separation + a.1 <= b.1 {
1718 return b;
1719 }
1720 let radius = (separation + a.1 + b.1) * 0.5;
1721 let centre = a.0.add(offset.scale((radius - a.1) / separation));
1722 (centre, radius)
1723}
1724
1725fn build_cell_tree(
1726 tree: &mut Vec<CellTreeNode>,
1727 spheres: &[(Vec3, f64)],
1728 columns: usize,
1729 [row_low, row_high, column_low, column_high]: [usize; 4],
1730) -> u32 {
1731 if row_high - row_low == 1 && column_high - column_low == 1 {
1732 let cell = row_low * columns + column_low;
1733 tree.push(CellTreeNode {
1734 centre: spheres[cell].0,
1735 radius: spheres[cell].1,
1736 children: None,
1737 cell: cell as u32,
1738 });
1739 return (tree.len() - 1) as u32;
1740 }
1741 let (first, second) = if row_high - row_low >= column_high - column_low {
1742 let middle = (row_low + row_high) / 2;
1743 (
1744 [row_low, middle, column_low, column_high],
1745 [middle, row_high, column_low, column_high],
1746 )
1747 } else {
1748 let middle = (column_low + column_high) / 2;
1749 (
1750 [row_low, row_high, column_low, middle],
1751 [row_low, row_high, middle, column_high],
1752 )
1753 };
1754 let first = build_cell_tree(tree, spheres, columns, first);
1755 let second = build_cell_tree(tree, spheres, columns, second);
1756 let a = tree[first as usize];
1757 let b = tree[second as usize];
1758 let (centre, radius) = enclosing_sphere((a.centre, a.radius), (b.centre, b.radius));
1762 tree.push(CellTreeNode {
1763 centre,
1764 radius: radius * (1.0 + 4.0 * f64::EPSILON),
1765 children: Some((first, second)),
1766 cell: u32::MAX,
1767 });
1768 (tree.len() - 1) as u32
1769}
1770
1771fn projection_cells(surface: &NurbsSurface) -> Result<Option<&ProjectionCells>, String> {
1772 if let Some(cells) = surface.projection_cells.get() {
1773 return Ok(cells.as_ref());
1774 }
1775 let cells = build_projection_cells(surface)?;
1776 Ok(surface.projection_cells.get_or_init(|| cells).as_ref())
1777}
1778
1779fn build_projection_cells(surface: &NurbsSurface) -> Result<Option<ProjectionCells>, String> {
1780 let (closed_u, closed_v) = surface.closed_directions()?;
1781 let us = lattice_parameters(&surface.knots_u, surface.degree_u, closed_u);
1782 let vs = lattice_parameters(&surface.knots_v, surface.degree_v, closed_v);
1783 if us.len() < 2 || vs.len() < 2 {
1784 return Ok(None);
1785 }
1786 let mut scale = 0.0f64;
1787 for control in surface.control_points.iter().flatten() {
1788 let point = control.point()?;
1789 scale = scale.max(point.x.abs()).max(point.y.abs()).max(point.z.abs());
1790 }
1791 let resolution =
1792 ((surface.degree_u + 1) * (surface.degree_v + 1)) as f64 * f64::EPSILON * scale;
1793
1794 let mut net = surface.control_points.clone();
1796 let mut knots_u = surface.knots_u.clone();
1797 refine_to_lattice(&mut net, &mut knots_u, surface.degree_u, &us);
1798 let columns = net.first().map(Vec::len).unwrap_or(0);
1799 let mut transposed: Vec<Vec<Vec4>> =
1800 (0..columns).map(|column| net.iter().map(|row| row[column]).collect()).collect();
1801 let mut knots_v = surface.knots_v.clone();
1802 refine_to_lattice(&mut transposed, &mut knots_v, surface.degree_v, &vs);
1803 let rows = transposed.first().map(Vec::len).unwrap_or(0);
1804 let columns = transposed.len();
1805 if rows == 0 || columns == 0 {
1806 return Ok(None);
1807 }
1808 let mut points = vec![Vec3::default(); rows * columns];
1809 for (column, fibre) in transposed.iter().enumerate() {
1810 for (row, control) in fibre.iter().enumerate() {
1811 points[row * columns + column] = control.point()?;
1812 }
1813 }
1814
1815 let mut nodes = Vec::with_capacity(us.len() * vs.len());
1816 for &u in &us {
1817 for &v in &vs {
1818 nodes.push(surface.evaluate(u, v)?);
1819 }
1820 }
1821
1822 let cell_rows = us.len() - 1;
1823 let cell_columns = vs.len() - 1;
1824 let mut boxes = Vec::with_capacity(cell_rows * cell_columns);
1825 let mut spheres = Vec::with_capacity(cell_rows * cell_columns);
1826 for i in 0..cell_rows {
1827 let (row_low, row_high) =
1828 supporting_range(&knots_u, surface.degree_u, rows, us[i], us[i + 1]);
1829 for j in 0..cell_columns {
1830 let (column_low, column_high) =
1831 supporting_range(&knots_v, surface.degree_v, columns, vs[j], vs[j + 1]);
1832 let at = |row: usize, column: usize| points[row * columns + column];
1833 let c00 = at(row_low, column_low);
1834 let c10 = at(row_high, column_low);
1835 let c01 = at(row_low, column_high);
1836 let c11 = at(row_high, column_high);
1837 let along_u = c10.sub(c00).add(c11.sub(c01));
1838 let along_v = c01.sub(c00).add(c11.sub(c10));
1839 let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
1840 let normal = unit_or(e1.cross(along_v), e1.perpendicular()?);
1841 let e2 = unit_or(normal.cross(e1), e1.perpendicular()?);
1842 let e3 = e1.cross(e2);
1843 let axes = [e1, e2, e3];
1844 let mut low = [f64::INFINITY; 3];
1845 let mut high = [f64::NEG_INFINITY; 3];
1846 for row in row_low..=row_high {
1847 for column in column_low..=column_high {
1848 let point = at(row, column);
1849 for axis in 0..3 {
1850 let coordinate = point.dot(axes[axis]);
1851 low[axis] = low[axis].min(coordinate);
1852 high[axis] = high[axis].max(coordinate);
1853 }
1854 }
1855 }
1856 for axis in 0..3 {
1857 low[axis] -= resolution;
1858 high[axis] += resolution;
1859 }
1860 let mut centre = Vec3::default();
1861 let mut half_diagonal_squared = 0.0;
1862 for axis in 0..3 {
1863 centre = centre.add(axes[axis].scale((low[axis] + high[axis]) * 0.5));
1864 let half = (high[axis] - low[axis]) * 0.5;
1865 half_diagonal_squared += half * half;
1866 }
1867 if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
1868 return Ok(None);
1869 }
1870 spheres.push((centre, half_diagonal_squared.sqrt() + resolution));
1871 boxes.push(CellBox { axes, low, high });
1872 }
1873 }
1874 let mut tree = Vec::with_capacity(2 * boxes.len());
1875 build_cell_tree(&mut tree, &spheres, cell_columns, [0, cell_rows, 0, cell_columns]);
1876 Ok(Some(ProjectionCells {
1877 us,
1878 vs,
1879 nodes,
1880 boxes,
1881 tree,
1882 resolution,
1883 }))
1884}
1885
1886fn optimality(
1891 derivatives: &[[Vec3; 3]; 3],
1892 point: Vec3,
1893 (u, v): (f64, f64),
1894 [ua, ub, va, vb]: [f64; 4],
1895 periodic: (bool, bool),
1896) -> Optimality {
1897 let residual = derivatives[0][0].sub(point);
1898 let residual_length = residual.length();
1899 let su = derivatives[1][0];
1900 let sv = derivatives[0][1];
1901 let f = su.dot(residual);
1902 let g = sv.dot(residual);
1903 let metric_u = su.length_squared();
1904 let metric_v = sv.length_squared();
1905 let stationary_u = metric_u.sqrt() * residual_length <= EPSILON
1906 || f.abs() <= 1e-10 * metric_u.sqrt() * residual_length;
1907 let stationary_v = metric_v.sqrt() * residual_length <= EPSILON
1908 || g.abs() <= 1e-10 * metric_v.sqrt() * residual_length;
1909 let pinned_u = !periodic.0 && ((u <= ua && f > 0.0) || (u >= ub && f < 0.0));
1910 let pinned_v = !periodic.1 && ((v <= va && g > 0.0) || (v >= vb && g < 0.0));
1911 let j00 = derivatives[2][0].dot(residual) + metric_u;
1912 let j01 = derivatives[1][1].dot(residual) + su.dot(sv);
1913 let j11 = derivatives[0][2].dot(residual) + metric_v;
1914 let determinant = j00 * j11 - j01 * j01;
1915 let convex = match (!pinned_u, !pinned_v) {
1916 (true, true) => j00 > 0.0 && determinant > 0.0,
1917 (true, false) => j00 > 0.0,
1918 (false, true) => j11 > 0.0,
1919 (false, false) => true,
1920 };
1921 Optimality {
1922 residual_length,
1923 gradient: (f, g),
1924 metric: (metric_u, metric_v),
1925 hessian: (j00, j01, j11),
1926 determinant,
1927 pinned: (pinned_u, pinned_v),
1928 convex,
1929 minimum: residual_length <= LINEAR_TOLERANCE
1930 || ((stationary_u || pinned_u) && (stationary_v || pinned_v) && convex),
1931 }
1932}
1933
1934struct Optimality {
1935 residual_length: f64,
1936 gradient: (f64, f64),
1937 metric: (f64, f64),
1938 hessian: (f64, f64, f64),
1939 determinant: f64,
1940 pinned: (bool, bool),
1941 convex: bool,
1942 minimum: bool,
1943}
1944
1945fn is_verified_minimum(
1946 surface: &NurbsSurface,
1947 point: Vec3,
1948 at: (f64, f64),
1949 rectangle: [f64; 4],
1950 periodic: (bool, bool),
1951) -> Result<bool, String> {
1952 let derivatives = surface.derivatives_small(at.0, at.1, 2)?;
1953 Ok(optimality(&derivatives, point, at, rectangle, periodic).minimum)
1954}
1955
1956fn confined_descent(
1972 surface: &NurbsSurface,
1973 point: Vec3,
1974 seed: (f64, f64),
1975 rectangle: [f64; 4],
1976 periodic: (bool, bool),
1977) -> Result<(NewtonResult, bool), String> {
1978 let [ua, ub, va, vb] = rectangle;
1979 let mut u = fit_parameter(seed.0, ua, ub, periodic.0);
1980 let mut v = fit_parameter(seed.1, va, vb, periodic.1);
1981 let mut converged = false;
1982 let mut current = surface.evaluate(u, v)?.sub(point).length_squared();
1983 for _ in 0..MAX_NEWTON_ITERATIONS {
1984 let derivatives = surface.derivatives_small(u, v, 2)?;
1985 let state = optimality(&derivatives, point, (u, v), rectangle, periodic);
1986 current = state.residual_length * state.residual_length;
1987 if state.minimum {
1988 converged = true;
1989 break;
1990 }
1991 let (f, g) = state.gradient;
1992 let (metric_u, metric_v) = state.metric;
1993 let (j00, j01, j11) = state.hessian;
1994 let determinant = state.determinant;
1995 let (free_u, free_v) = (!state.pinned.0, !state.pinned.1);
1996 let (mut du, mut dv) = match (free_u, free_v) {
1997 (true, true) if state.convex => (
1998 (-f * j11 + g * j01) / determinant,
1999 (-g * j00 + f * j01) / determinant,
2000 ),
2001 (true, false) if state.convex => (-f / j00, 0.0),
2002 (false, true) if state.convex => (0.0, -g / j11),
2003 _ => (
2004 if free_u && metric_u > 0.0 { -f / metric_u } else { 0.0 },
2005 if free_v && metric_v > 0.0 { -g / metric_v } else { 0.0 },
2006 ),
2007 };
2008 if !(du.is_finite() && dv.is_finite()) || (du == 0.0 && dv == 0.0) {
2009 break;
2010 }
2011 let mut moved = false;
2012 let taken = |next: f64, here: f64, low: f64, high: f64, wraps: bool| {
2014 let raw = next - here;
2015 if wraps {
2016 raw - (high - low) * (raw / (high - low)).round()
2017 } else {
2018 raw
2019 }
2020 };
2021 for _ in 0..40 {
2022 let next_u = fit_parameter(u + du, ua, ub, periodic.0);
2023 let next_v = fit_parameter(v + dv, va, vb, periodic.1);
2024 if next_u == u && next_v == v {
2025 break;
2026 }
2027 let trial = surface.evaluate(next_u, next_v)?.sub(point).length_squared();
2028 if trial <= current {
2029 moved = taken(next_u, u, ua, ub, periodic.0).abs() > 1e-15 * (ub - ua)
2030 || taken(next_v, v, va, vb, periodic.1).abs() > 1e-15 * (vb - va);
2031 u = next_u;
2032 v = next_v;
2033 current = trial;
2034 break;
2035 }
2036 du *= 0.5;
2037 dv *= 0.5;
2038 }
2039 if !moved {
2040 break;
2041 }
2042 }
2043 let result = NewtonResult {
2044 u,
2045 v,
2046 distance_squared: current,
2047 converged,
2048 };
2049 let on_edge =
2050 (!periodic.0 && (u <= ua || u >= ub)) || (!periodic.1 && (v <= va || v >= vb));
2051 Ok((result, on_edge))
2052}
2053
2054#[allow(clippy::too_many_arguments)]
2057fn global_minimum(
2058 surface: &NurbsSurface,
2059 point: Vec3,
2060 domains: [f64; 4],
2061 periodic: (bool, bool),
2062 best: &mut NewtonResult,
2063 stats: &mut GlobalStats,
2064) -> Result<(), String> {
2065 if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
2066 stats.settled_by_floor = true;
2067 return Ok(());
2068 }
2069 let Some(cells) = projection_cells(surface)? else {
2070 return Ok(());
2071 };
2072 let resolution = cells.resolution;
2073 let columns = cells.vs.len() - 1;
2074 let columns_u = cells.us.len() - 1;
2075 let mut bound = best.distance_squared.sqrt() - resolution;
2076
2077 let mut candidates: Vec<(f64, usize)> = Vec::new();
2080 let mut excluded = f64::INFINITY;
2081 let mut stack: Vec<u32> = Vec::with_capacity(64);
2082 stack.push((cells.tree.len() - 1) as u32);
2083 while let Some(index) = stack.pop() {
2084 let node = cells.tree[index as usize];
2085 let sphere_bound = (point.sub(node.centre).length() - node.radius).max(0.0);
2086 if sphere_bound >= bound {
2087 excluded = excluded.min(sphere_bound);
2088 continue;
2089 }
2090 match node.children {
2091 Some((first, second)) => {
2092 stack.push(first);
2093 stack.push(second);
2094 }
2095 None => {
2096 let cell = node.cell as usize;
2097 let box_bound = cells.boxes[cell].lower_bound(point);
2098 if box_bound < bound {
2099 candidates.push((box_bound, cell));
2100 } else {
2101 excluded = excluded.min(box_bound);
2102 }
2103 }
2104 }
2105 }
2106 stats.candidates = candidates.len();
2107 candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
2108 stats.hull_lower_bound = candidates
2109 .first()
2110 .map_or(excluded, |&(least, _)| least.min(excluded));
2111
2112 let cell_of = |u: f64, v: f64| {
2118 let row = (cells.us.partition_point(|&value| value <= u).max(1) - 1).min(columns_u - 1);
2119 let column = (cells.vs.partition_point(|&value| value <= v).max(1) - 1).min(columns - 1);
2120 row * columns + column
2121 };
2122 let mut settled: Vec<usize> = Vec::new();
2123 if is_verified_minimum(surface, point, (best.u, best.v), domains, periodic)? {
2124 settled.push(cell_of(best.u, best.v));
2125 }
2126 for (cell_bound, cell) in candidates {
2127 if cell_bound >= bound {
2128 break;
2129 }
2130 let row = cell / columns;
2131 let column = cell % columns;
2132 let rectangle = [
2133 cells.us[row],
2134 cells.us[row + 1],
2135 cells.vs[column],
2136 cells.vs[column + 1],
2137 ];
2138 if settled.contains(&cell) {
2139 continue;
2140 }
2141 let mut seed = (rectangle[0], rectangle[2]);
2142 let mut seed_distance = f64::INFINITY;
2143 for (node_row, node_column) in
2144 [(row, column), (row + 1, column), (row, column + 1), (row + 1, column + 1)]
2145 {
2146 let distance = cells.nodes[node_row * cells.vs.len() + node_column]
2147 .sub(point)
2148 .length_squared();
2149 if distance < seed_distance {
2150 seed_distance = distance;
2151 seed = (cells.us[node_row], cells.vs[node_column]);
2152 }
2153 }
2154 stats.refinements += 1;
2155 let (confined, on_edge) = confined_descent(surface, point, seed, rectangle, (false, false))?;
2156 let mut found = confined;
2157 if on_edge && confined.distance_squared.sqrt() < bound {
2158 stats.continuations += 1;
2161 let (continued, _) =
2162 confined_descent(surface, point, (confined.u, confined.v), domains, periodic)?;
2163 if continued.distance_squared < found.distance_squared {
2164 found = continued;
2165 }
2166 if continued.converged {
2167 settled.push(cell_of(continued.u, continued.v));
2168 }
2169 }
2170 if found.distance_squared.sqrt() < bound {
2171 *best = found;
2172 bound = best.distance_squared.sqrt() - resolution;
2173 stats.replaced = true;
2174 if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
2175 return Ok(());
2176 }
2177 }
2178 }
2179 Ok(())
2180}
2181
2182
2183
2184#[derive(Clone, Copy)]
2186pub(crate) enum FootDomain {
2187 Clamp,
2188 Wrap,
2189 Extended,
2190}
2191
2192#[derive(Clone, Copy)]
2193pub(crate) struct CurveFootPolicy {
2194 pub domain: FootDomain,
2195 pub iterations: usize,
2196 pub slope_guard: f64,
2197 pub step_limit: f64,
2198 pub parameter_tolerance: f64,
2199 pub mode: FootMode,
2200}
2201
2202#[derive(Clone, Copy)]
2204pub(crate) enum FootMode {
2205 Monotone,
2206 Witness,
2207 Stationary { positive_slope: bool, relative_step: bool },
2208 Backtracked,
2209 Planar,
2210}
2211
2212fn curve_foot_newton_state(
2215 curve: &NurbsCurve,
2216 point: Vec3,
2217 seed: f64,
2218 bracket: [f64; 2],
2219 policy: CurveFootPolicy,
2220) -> Result<(f64, f64, f64), String> {
2221 let fit = |u| match policy.domain {
2222 FootDomain::Clamp => fit_parameter(u, bracket[0], bracket[1], false),
2223 FootDomain::Wrap => fit_parameter(u, bracket[0], bracket[1], true),
2224 FootDomain::Extended => u,
2225 };
2226 let extended = matches!(policy.domain, FootDomain::Extended) || matches!(policy.mode, FootMode::Backtracked);
2227 let planar = matches!(policy.mode, FootMode::Planar);
2228 let project = |p: Vec3| if planar { Vec3::new(p.x, p.y, 0.0) } else { p };
2229 let point = project(point);
2230 let distance = |u| -> Result<f64, String> {
2231 let p = if extended { curve.derivatives_extended(u, 0)?[0] } else { curve.evaluate(u)? };
2232 Ok(project(p).sub(point).length())
2233 };
2234 let monotone = matches!(policy.mode, FootMode::Monotone | FootMode::Backtracked);
2235 let witness = matches!(policy.mode, FootMode::Witness);
2236 let mut u = if witness { seed } else { fit(seed) };
2237 let mut nearest_u = seed;
2238 let mut nearest_squared = f64::INFINITY;
2239 let mut best = if monotone { distance(u)? } else { f64::INFINITY };
2240 for _ in 0..policy.iterations {
2241 let derivatives = if extended {
2242 let values = curve.derivatives_extended(u, 2)?;
2243 [values[0], values[1], values[2]]
2244 } else if matches!(policy.mode, FootMode::Stationary { .. })
2245 || (planar && !crate::curve::small_curve_derivatives_enabled()) {
2246 let values = curve.derivatives(u, 2)?;
2247 [values[0], values[1], values[2]]
2248 } else {
2249 curve.derivatives_small(u, 2)?
2250 }.map(project);
2251 let offset = derivatives[0].sub(point);
2252 let numerator = offset.dot(derivatives[1]);
2253 let denominator = derivatives[1].dot(derivatives[1]) + offset.dot(derivatives[2]);
2254 if witness {
2255 let squared = offset.length_squared();
2256 if squared < nearest_squared {
2257 nearest_u = u;
2258 nearest_squared = squared;
2259 }
2260 let residual_length = squared.sqrt();
2261 if residual_length <= LINEAR_TOLERANCE
2262 || numerator.abs() <= EPSILON + 1e-10 * derivatives[1].length() * residual_length {
2263 break;
2264 }
2265 }
2266 let singular = if planar { denominator.abs() < policy.slope_guard }
2267 else { denominator.abs() <= policy.slope_guard };
2268 if singular || (matches!(policy.mode, FootMode::Stationary { positive_slope: true, .. }) && !(denominator > 0.0)) { break; }
2269 let mut step = if matches!(policy.mode, FootMode::Backtracked) && !(denominator > 0.0) {
2270 policy.step_limit.copysign(numerator)
2271 } else {
2272 (numerator / denominator).clamp(-policy.step_limit, policy.step_limit)
2273 };
2274 let mut next = fit(u - step);
2275 let mut next_distance = if monotone { distance(next)? } else { f64::INFINITY };
2276 if matches!(policy.mode, FootMode::Backtracked) {
2277 for _ in 0..30 {
2278 if next_distance < best { break; }
2279 step *= 0.5;
2280 next = fit(u - step);
2281 next_distance = distance(next)?;
2282 }
2283 if !(next_distance < best) { break; }
2284 } else if monotone && next_distance >= best { break; }
2285 let moved = if planar || matches!(policy.domain, FootDomain::Extended) { step }
2286 else if matches!(policy.domain, FootDomain::Wrap) {
2287 let period = bracket[1] - bracket[0];
2288 let raw = next - u;
2289 raw - period * (raw / period).round()
2290 } else { next - u };
2291 let tolerance = if matches!(policy.mode, FootMode::Stationary { relative_step: true, .. }) {
2292 policy.parameter_tolerance * (1.0 + u.abs())
2293 } else { policy.parameter_tolerance };
2294 let small = if planar || matches!(policy.mode, FootMode::Backtracked) { moved.abs() < tolerance }
2295 else { moved.abs() <= tolerance };
2296 if small && matches!(policy.mode, FootMode::Monotone) { break; }
2297 u = next;
2298 best = next_distance;
2299 if small { break; }
2300 }
2301 if witness { return Ok((u, nearest_u, nearest_squared)); }
2302 if !monotone { best = distance(u)?; }
2303 Ok((u, u, best * best))
2304}
2305
2306pub(crate) fn curve_foot_newton(
2307 curve: &NurbsCurve, point: Vec3, seed: f64, bracket: [f64; 2], policy: CurveFootPolicy,
2308) -> Result<(f64, f64), String> {
2309 let (_, nearest, squared) = curve_foot_newton_state(curve, point, seed, bracket, policy)?;
2310 Ok((nearest, squared))
2311}
2312