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), LINEAR_TOLERANCE)
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 linear_tolerance: f64,
393) -> Result<NewtonResult, String> {
394 let [u0, u1, v0, v1] = domains;
395 let mut u = seed_u;
396 let mut v = seed_v;
397 let mut converged = false;
398 let mut best = NewtonResult {
399 u,
400 v,
401 distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
402 converged,
403 };
404 for iteration in 0..MAX_NEWTON_ITERATIONS {
405 crate::imprint::work_profile::add(|c| c.newton_iterations += 1);
406 let derivatives = surface.derivatives_small(u, v, if lazy { 0 } else { 2 })?;
407 let residual = derivatives[0][0].sub(point);
408 let distance_squared = residual.length_squared();
409 if distance_squared < best.distance_squared {
410 best.u = u;
411 best.v = v;
412 best.distance_squared = distance_squared;
413 }
414 let residual_length = distance_squared.sqrt();
415 if residual_length <= linear_tolerance {
416 crate::imprint::work_profile::add(|c| { c.newton_unused += 1; c.newton_unused_first += u64::from(iteration == 0); });
417 converged = true;
418 break;
419 }
420 let derivatives = if lazy { surface.derivatives_small(u, v, 2)? } else { derivatives };
421 let f = derivatives[1][0].dot(residual);
422 let g = derivatives[0][1].dot(residual);
423 let tangent_u_length = derivatives[1][0].length();
424 let tangent_v_length = derivatives[0][1].length();
425 let cosine_u = if tangent_u_length * residual_length <= EPSILON {
426 0.0
427 } else {
428 f.abs() / (tangent_u_length * residual_length)
429 };
430 let cosine_v = if tangent_v_length * residual_length <= EPSILON {
431 0.0
432 } else {
433 g.abs() / (tangent_v_length * residual_length)
434 };
435 if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
436 converged = true;
437 break;
438 }
439 let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
440 let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
441 let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
442 let determinant = j00 * j11 - j01 * j01;
443 if determinant.abs() <= EPSILON {
444 break;
445 }
446 let mut du = (-f * j11 + g * j01) / determinant;
447 let mut dv = (-g * j00 + f * j01) / determinant;
448 du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
449 dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
450 let next_u = fit_parameter(u + du, u0, u1, closed_u);
451 let next_v = fit_parameter(v + dv, v0, v1, closed_v);
452 let stalled =
453 (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
454 u = next_u;
455 v = next_v;
456 if stalled {
457 break;
458 }
459 }
460 let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
461 if final_distance_squared < best.distance_squared {
462 best.u = u;
463 best.v = v;
464 best.distance_squared = final_distance_squared;
465 }
466 best.converged = converged;
467 Ok(best)
468}
469
470pub(crate) fn refine_trim_foot(
474 surface: &NurbsSurface, point: Vec3, foot: SurfaceProjection,
475) -> Result<SurfaceProjection, String> {
476 let [u0, u1] = surface.domain_u()?;
477 let [v0, v1] = surface.domain_v()?;
478 let (cu, cv) = surface.closed_directions()?;
479 let result = surface_newton_impl(surface, point, foot.u, foot.v,
480 [u0, u1, v0, v1], cu, cv, false, 1e-11)?;
481 let projected = surface.evaluate(result.u, result.v)?;
482 Ok(SurfaceProjection { u: result.u, v: result.v, point: projected,
483 distance: projected.sub(point).length() })
484}
485
486#[track_caller]
492pub fn project_point_to_surface(
493 surface: &NurbsSurface,
494 point: Vec3,
495) -> Result<SurfaceProjection, String> {
496 crate::imprint::work_profile::add(|c| c.projection_global += 1);
497 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_PROJECT, surface);
498 if let Some(analytic) = surface.analytic() {
502 if let Some(projection) = analytic.project(surface, point) {
503 return Ok(projection);
504 }
505 }
506 if let Some(ellipsoid) = surface.affine_sphere() {
507 if let Some(projection) = ellipsoid.project(surface, point) {
508 return Ok(projection);
509 }
510 }
511 project_point_to_surface_general(surface, point)
512}
513
514pub fn project_point_to_surface_seeded(
524 surface: &NurbsSurface,
525 point: Vec3,
526 seed_u: f64,
527 seed_v: f64,
528) -> Result<SurfaceProjection, String> {
529 crate::imprint::work_profile::add(|c| c.projection_seeded += 1);
530 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::SEEDED);
531 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_SEEDED, surface);
532 if let Some(ellipsoid) = surface.affine_sphere() {
536 if let Some(projection) = ellipsoid.project(surface, point) {
537 return Ok(projection);
538 }
539 }
540 let [u0, u1] = surface.domain_u()?;
541 let [v0, v1] = surface.domain_v()?;
542 let (closed_u, closed_v) = surface.closed_directions()?;
543 let result = surface_newton(
544 surface,
545 point,
546 seed_u,
547 seed_v,
548 [u0, u1, v0, v1],
549 closed_u,
550 closed_v,
551 )?;
552 let projected = surface.evaluate(result.u, result.v)?;
553 Ok(SurfaceProjection {
554 u: result.u,
555 v: result.v,
556 point: projected,
557 distance: projected.sub(point).length(),
558 })
559}
560
561#[track_caller]
595pub(crate) fn project_point_to_surface_from_seed(
596 surface: &NurbsSurface,
597 point: Vec3,
598 seed: [f64; 2],
599) -> Result<SurfaceProjection, String> {
600 crate::imprint::work_profile::add(|c| c.projection_from_seed += 1);
601 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::SEEDED);
602 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_SEED, surface);
603 if let Some(analytic) = surface.analytic() {
604 if let Some(projection) = analytic.project(surface, point) {
605 return Ok(projection);
606 }
607 }
608 if let Some(ellipsoid) = surface.affine_sphere() {
609 if let Some(projection) = ellipsoid.project(surface, point) {
610 return Ok(projection);
611 }
612 }
613 let caller = std::panic::Location::caller();
614 if !seeded_answer_admitted(caller) {
615 return project_point_to_surface_general(surface, point);
616 }
617 let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
618 if seed_audit_enabled() {
619 let nearest = project_point_to_surface_general(surface, point)?;
620 if seeded.point.sub(nearest.point).length() > LINEAR_TOLERANCE {
621 seed_audit_record(caller, surface, seed, &seeded, &nearest);
622 }
623 }
624 Ok(seeded)
625}
626
627fn seeded_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
630 static SITES: std::sync::OnceLock<Option<Option<Vec<String>>>> = std::sync::OnceLock::new();
631 let sites = SITES.get_or_init(|| {
632 if std::env::var("BREP_PROJECTION_FROM_SEED").as_deref() == Ok("0") {
633 return None;
634 }
635 Some(std::env::var("BREP_PROJECTION_FROM_SEED_SITES").ok().map(|value| {
636 value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect()
637 }))
638 });
639 match sites {
640 None => false,
641 Some(None) => true,
642 Some(Some(only)) => {
643 let location = format!("{}:{}", caller.file(), caller.line());
644 only.iter().any(|site| location.contains(site.as_str()))
645 }
646 }
647}
648
649fn seed_audit_enabled() -> bool {
650 debug_flag!("BREP_PROJECTION_AUDIT")
651}
652
653fn seed_audit_record(
657 caller: &std::panic::Location<'_>,
658 surface: &NurbsSurface,
659 seed: [f64; 2],
660 seeded: &SurfaceProjection,
661 nearest: &SurfaceProjection,
662) {
663 use std::io::Write;
664 static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
665 std::sync::OnceLock::new();
666 let sink = SINK.get_or_init(|| {
667 let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
668 let path = std::path::Path::new(&directory)
669 .join(format!("projection-seed-audit-{}.tsv", std::process::id()));
670 std::fs::OpenOptions::new()
671 .create(true)
672 .append(true)
673 .open(path)
674 .ok()
675 .map(std::sync::Mutex::new)
676 });
677 if let Some(file) = sink {
678 if let Ok(mut file) = file.lock() {
679 let _ = writeln!(
680 file,
681 "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.6e}\t{}x{} cp {}x{}",
682 caller.file(),
683 caller.line(),
684 seeded.distance,
685 nearest.distance,
686 seed[0],
687 seed[1],
688 seeded.u,
689 seeded.v,
690 nearest.u,
691 nearest.v,
692 seeded.point.sub(nearest.point).length(),
693 surface.degree_u,
694 surface.degree_v,
695 surface.control_points.len(),
696 surface.control_points.first().map(Vec::len).unwrap_or(0),
697 );
698 }
699 }
700}
701
702fn projection_seed_grid(surface: &NurbsSurface) -> Result<&SampleGrid, String> {
707 if let Some(grid) = surface.projection_grid.get() {
708 return Ok(grid);
709 }
710 let [u0, u1] = surface.domain_u()?;
711 let [v0, v1] = surface.domain_v()?;
712 let (closed_u, closed_v) = surface.closed_directions()?;
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 samples_u = if closed_u {
720 8usize.max(surface.degree_u * 4)
721 } else {
722 3usize.max(surface.degree_u + 1)
723 };
724 let samples_v = if closed_v {
725 8usize.max(surface.degree_v * 4)
726 } else {
727 3usize.max(surface.degree_v + 1)
728 };
729 let mut grid = Vec::new();
730 for u_pair in breaks_u.windows(2) {
731 for v_pair in breaks_v.windows(2) {
732 for i in 0..=samples_u {
733 for j in 0..=samples_v {
734 let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
735 let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
736 grid.push((u, v, surface.evaluate(u, v)?));
737 }
738 }
739 }
740 }
741 Ok(surface.projection_grid.get_or_init(|| SampleGrid::new(grid)))
742}
743
744fn projection_dense_grid(surface: &NurbsSurface) -> Result<&SampleGrid, String> {
751 if let Some(grid) = surface.projection_dense_grid.get() {
752 return Ok(grid);
753 }
754 let [u0, u1] = surface.domain_u()?;
755 let [v0, v1] = surface.domain_v()?;
756 let mut breaks_u = vec![u0];
757 breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
758 breaks_u.push(u1);
759 let mut breaks_v = vec![v0];
760 breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
761 breaks_v.push(v1);
762 let count_u = 32usize.max((breaks_u.len() - 1) * 8);
763 let count_v = 16usize.max((breaks_v.len() - 1) * 8);
764 let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
765 for i in 0..=count_u {
766 for j in 0..=count_v {
767 let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
768 let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
769 grid.push((u, v, surface.evaluate(u, v)?));
770 }
771 }
772 Ok(surface.projection_dense_grid.get_or_init(|| SampleGrid::with_tree(grid)))
773}
774
775const SAMPLE_BLOCK: usize = 32;
777
778const GROUP_BLOCKS: usize = 32;
781
782#[derive(Clone, Debug)]
787pub(crate) struct SampleGrid {
788 samples: Vec<(f64, f64, Vec3)>,
789 blocks: Vec<SampleBlock>,
790 groups: Vec<SampleBlock>,
795 tree: Vec<Vec<SampleBlock>>,
801}
802
803const TREE_FANOUT: usize = 4;
805
806fn union_boxes(boxes: &[SampleBlock]) -> Vec<SampleBlock> {
808 boxes
809 .chunks(TREE_FANOUT)
810 .map(|chunk| {
811 let mut union = chunk[0];
812 for block in chunk {
813 union.low = Vec3::new(union.low.x.min(block.low.x), union.low.y.min(block.low.y), union.low.z.min(block.low.z));
814 union.high = Vec3::new(union.high.x.max(block.high.x), union.high.y.max(block.high.y), union.high.z.max(block.high.z));
815 union.prunable &= block.prunable;
816 }
817 union
818 })
819 .collect()
820}
821
822#[derive(Clone, Copy, Debug)]
825struct SampleBlock {
826 low: Vec3,
827 high: Vec3,
828 prunable: bool,
829}
830
831impl SampleGrid {
832 fn new(samples: Vec<(f64, f64, Vec3)>) -> Self {
833 let blocks = samples
834 .chunks(SAMPLE_BLOCK)
835 .map(|chunk| {
836 let mut low = chunk[0].2;
837 let mut high = low;
838 let mut prunable = true;
839 for &(_, _, p) in chunk {
840 prunable &= p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
841 low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
842 high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
843 }
844 SampleBlock { low, high, prunable }
845 })
846 .collect::<Vec<_>>();
847 let groups = blocks
848 .chunks(GROUP_BLOCKS)
849 .map(|chunk| {
850 let mut group = chunk[0];
851 for block in chunk {
852 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));
853 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));
854 group.prunable &= block.prunable;
855 }
856 group
857 })
858 .collect();
859 Self { samples, blocks, groups, tree: Vec::new() }
860 }
861
862 fn with_tree(samples: Vec<(f64, f64, Vec3)>) -> Self {
869 let mut grid = Self::new(samples);
870 if grid.blocks.len() > GROUP_BLOCKS {
871 grid.tree.push(union_boxes(&grid.blocks));
872 while grid.tree.last().is_some_and(|level| level.len() > 1) {
873 let next = union_boxes(grid.tree.last().unwrap());
874 grid.tree.push(next);
875 }
876 }
877 grid
878 }
879}
880
881impl SampleBlock {
882 fn lower_bound(&self, point: Vec3) -> f64 {
890 let gap = |p: f64, low: f64, high: f64| {
891 if p < low {
892 low - p
893 } else if p > high {
894 p - high
895 } else {
896 0.0
897 }
898 };
899 let x = gap(point.x, self.low.x, self.high.x);
900 let y = gap(point.y, self.low.y, self.high.y);
901 let z = gap(point.z, self.low.z, self.high.z);
902 x * x + y * y + z * z
903 }
904}
905
906fn pruned_scan() -> bool {
909 switch!("BREP_PROJECTION_PRUNED_SCAN", true)
910}
911
912fn nearest_sample(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult) {
922 if !grid.tree.is_empty() && scan_tree() && pruned_scan() && bound_first_scan() {
923 nearest_sample_tree(grid, point, best);
924 return;
925 }
926 nearest_sample_with(grid, point, best, pruned_scan(), bound_first_scan());
927}
928
929fn scan_tree() -> bool {
941 switch!("BREP_PROJECTION_SCAN_TREE", true)
942}
943
944fn scan_hint() -> bool {
954 switch!("BREP_PROJECTION_SCAN_HINT", true)
955}
956
957const HINT_SLOTS: usize = 8;
958
959thread_local! {
960 static SCAN_HINTS: std::cell::Cell<[(usize, usize); HINT_SLOTS]> =
962 const { std::cell::Cell::new([(0, usize::MAX); HINT_SLOTS]) };
963}
964
965fn hint_slot(grid: &SampleGrid) -> (usize, usize) {
966 let address = grid.samples.as_ptr() as usize;
967 (address, (address >> 4) % HINT_SLOTS)
968}
969
970fn hinted_bound(grid: &SampleGrid, point: Vec3, mut bound: f64) -> f64 {
977 if !scan_hint() {
978 return bound;
979 }
980 let (address, slot) = hint_slot(grid);
981 let (held, run) = SCAN_HINTS.with(|hints| hints.get()[slot]);
982 if held != address || run >= grid.blocks.len() || !grid.blocks[run].prunable {
983 return bound;
984 }
985 let start = run * SAMPLE_BLOCK;
986 let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
987 for &(_, _, sample) in &grid.samples[start..end] {
988 let distance_squared = sample.sub(point).length_squared();
989 if distance_squared < bound {
990 bound = distance_squared;
991 }
992 }
993 bound
994}
995
996fn remember_hint(grid: &SampleGrid, run: Option<usize>) {
998 if let Some(run) = run.filter(|_| scan_hint()) {
999 let (address, slot) = hint_slot(grid);
1000 SCAN_HINTS.with(|hints| {
1001 let mut all = hints.get();
1002 all[slot] = (address, run);
1003 hints.set(all);
1004 });
1005 }
1006}
1007
1008fn nearest_sample_tree(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult) {
1019 let top = grid.tree.len() - 1;
1020 let mut bound = best.distance_squared;
1021 let mut node = 0usize;
1023 let mut reached = true;
1024 for level in (0..=top).rev() {
1025 let children = |count: usize| node * TREE_FANOUT..((node + 1) * TREE_FANOUT).min(count);
1026 let below = if level == 0 { &grid.blocks } else { &grid.tree[level - 1] };
1027 match lowest_floor(below, children(below.len()), point) {
1028 Some(child) => node = child,
1029 None => {
1030 reached = false;
1031 break;
1032 }
1033 }
1034 }
1035 if reached {
1036 let start = node * SAMPLE_BLOCK;
1037 let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
1038 for &(_, _, sample) in &grid.samples[start..end] {
1039 let distance_squared = sample.sub(point).length_squared();
1040 if distance_squared < bound {
1041 bound = distance_squared;
1042 }
1043 }
1044 }
1045 let bound = hinted_bound(grid, point, bound);
1046 let skip = |box_: &SampleBlock, held: f64| {
1047 if !box_.prunable {
1048 return false;
1049 }
1050 let floor = box_.lower_bound(point);
1051 floor >= held || floor > bound
1052 };
1053 let mut answer_run = None;
1054 let mut stack: Vec<(usize, usize)> = Vec::with_capacity(TREE_FANOUT * (top + 2));
1056 stack.push((top + 1, 0));
1057 while let Some((level, index)) = stack.pop() {
1058 if level == 0 {
1059 if skip(&grid.blocks[index], best.distance_squared) {
1060 continue;
1061 }
1062 let start = index * SAMPLE_BLOCK;
1063 let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
1064 let held = best.distance_squared;
1065 scan_run(&grid.samples[start..end], point, best);
1066 if best.distance_squared < held {
1067 answer_run = Some(index);
1068 }
1069 continue;
1070 }
1071 if skip(&grid.tree[level - 1][index], best.distance_squared) {
1072 continue;
1073 }
1074 let count = if level == 1 { grid.blocks.len() } else { grid.tree[level - 2].len() };
1075 let children = index * TREE_FANOUT..((index + 1) * TREE_FANOUT).min(count);
1076 for child in children.rev() {
1078 stack.push((level - 1, child));
1079 }
1080 }
1081 remember_hint(grid, answer_run);
1082}
1083
1084fn bound_first_scan() -> bool {
1100 switch!("BREP_PROJECTION_BOUND_FIRST", true)
1101}
1102
1103fn nearest_sample_with(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult, pruned: bool, bound_first: bool) {
1113 if !(pruned && bound_first) {
1114 for (block, chunk) in grid.blocks.iter().zip(grid.samples.chunks(SAMPLE_BLOCK)) {
1115 if pruned && block.prunable && block.lower_bound(point) >= best.distance_squared {
1116 continue;
1117 }
1118 scan_run(chunk, point, best);
1119 }
1120 return;
1121 }
1122 let mut bound = best.distance_squared;
1125 let lowest_group = lowest_floor(&grid.groups, 0..grid.groups.len(), point);
1126 if let Some(group) = lowest_group {
1127 let runs = group * GROUP_BLOCKS..((group + 1) * GROUP_BLOCKS).min(grid.blocks.len());
1128 if let Some(run) = lowest_floor(&grid.blocks, runs, point) {
1129 let start = run * SAMPLE_BLOCK;
1130 let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
1131 for &(_, _, sample) in &grid.samples[start..end] {
1132 let distance_squared = sample.sub(point).length_squared();
1133 if distance_squared < bound {
1134 bound = distance_squared;
1135 }
1136 }
1137 }
1138 }
1139 let bound = hinted_bound(grid, point, bound);
1140 let skip = |box_: &SampleBlock, held: f64| {
1141 if !box_.prunable {
1142 return false;
1143 }
1144 let floor = box_.lower_bound(point);
1145 floor >= held || floor > bound
1146 };
1147 let mut answer_run = None;
1148 for (group_index, group) in grid.groups.iter().enumerate() {
1149 if skip(group, best.distance_squared) {
1150 continue;
1151 }
1152 let first = group_index * GROUP_BLOCKS;
1153 let last = (first + GROUP_BLOCKS).min(grid.blocks.len());
1154 for run in first..last {
1155 if skip(&grid.blocks[run], best.distance_squared) {
1156 continue;
1157 }
1158 let start = run * SAMPLE_BLOCK;
1159 let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
1160 let held = best.distance_squared;
1161 scan_run(&grid.samples[start..end], point, best);
1162 if best.distance_squared < held {
1163 answer_run = Some(run);
1164 }
1165 }
1166 }
1167 remember_hint(grid, answer_run);
1168}
1169
1170fn scan_run(run: &[(f64, f64, Vec3)], point: Vec3, best: &mut NewtonResult) {
1173 for &(u, v, sample) in run {
1174 let distance_squared = sample.sub(point).length_squared();
1175 if distance_squared < best.distance_squared {
1176 best.u = u;
1177 best.v = v;
1178 best.distance_squared = distance_squared;
1179 }
1180 }
1181}
1182
1183fn lowest_floor(boxes: &[SampleBlock], range: std::ops::Range<usize>, point: Vec3) -> Option<usize> {
1186 let mut lowest: Option<(usize, f64)> = None;
1187 for index in range {
1188 let box_ = &boxes[index];
1189 if !box_.prunable {
1190 continue;
1191 }
1192 let floor = box_.lower_bound(point);
1193 if lowest.is_none_or(|(_, held)| floor < held) {
1194 lowest = Some((index, floor));
1195 }
1196 }
1197 lowest.map(|(index, _)| index)
1198}
1199
1200const RING_EXPONENTS: usize = 22;
1204const RING_INDICES: usize = 33; #[inline]
1207fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
1208 (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
1209}
1210
1211fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
1218 if let Some(grid) = surface.projection_ring_grid.get() {
1219 return Ok(grid);
1220 }
1221 let [u0, u1] = surface.domain_u()?;
1222 let [v0, v1] = surface.domain_v()?;
1223 let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
1224 for exponent in 1..=RING_EXPONENTS as i32 {
1225 let offset = 1.0 / 2f64.powi(exponent);
1226 let e = exponent as usize - 1;
1227 let v_lo = v0 + (v1 - v0) * offset;
1228 let v_hi = v1 - (v1 - v0) * offset;
1229 let u_lo = u0 + (u1 - u0) * offset;
1230 let u_hi = u1 - (u1 - u0) * offset;
1231 for index in 0..=32 {
1232 let u = u0 + (u1 - u0) * index as f64 / 32.0;
1233 grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
1234 grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
1235 }
1236 for index in 0..=32 {
1237 let v = v0 + (v1 - v0) * index as f64 / 32.0;
1238 grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
1239 grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
1240 }
1241 }
1242 Ok(surface.projection_ring_grid.get_or_init(|| grid))
1243}
1244
1245fn nearest_seed_answer(
1255 surface: &NurbsSurface,
1256 point: Vec3,
1257 [u0, u1, v0, v1]: [f64; 4],
1258 closed_u: bool,
1259 closed_v: bool,
1260) -> Result<NewtonResult, String> {
1261 let mut best = NewtonResult {
1262 u: u0,
1263 v: v0,
1264 distance_squared: f64::INFINITY,
1265 converged: false,
1266 };
1267 nearest_sample(projection_seed_grid(surface)?, point, &mut best);
1268 if let Some(revolution) = project_linear_revolution(surface, point)? {
1269 if revolution.distance * revolution.distance < best.distance_squared {
1270 best.u = revolution.u;
1271 best.v = revolution.v;
1272 best.distance_squared = revolution.distance * revolution.distance;
1273 }
1274 }
1275 let first_newton = surface_newton(
1276 surface,
1277 point,
1278 best.u,
1279 best.v,
1280 [u0, u1, v0, v1],
1281 closed_u,
1282 closed_v,
1283 )?;
1284 if first_newton.distance_squared < best.distance_squared {
1285 best = first_newton;
1286 }
1287 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
1288 let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
1289 if degenerate || !first_newton.converged {
1290 let mut grid = best;
1291 nearest_sample(projection_dense_grid(surface)?, point, &mut grid);
1292 if grid.distance_squared < best.distance_squared {
1293 best = grid;
1294 }
1295 let polished = surface_newton(
1296 surface,
1297 point,
1298 grid.u,
1299 grid.v,
1300 [u0, u1, v0, v1],
1301 closed_u,
1302 closed_v,
1303 )?;
1304 if polished.distance_squared < best.distance_squared {
1305 best = polished;
1306 }
1307
1308 let (_, su, sv) = surface.deriv1(best.u, best.v)?;
1309 if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
1310 let ring_grid = projection_ring_grid(surface)?;
1311 let mut ring = best;
1312 let scan = |side: usize,
1316 exponent_index: usize,
1317 along_u: bool,
1318 fixed: f64,
1319 candidate: &mut NewtonResult| {
1320 for index in 0..=32 {
1321 let moving = if along_u {
1322 u0 + (u1 - u0) * index as f64 / 32.0
1323 } else {
1324 v0 + (v1 - v0) * index as f64 / 32.0
1325 };
1326 let (u, v) = if along_u {
1327 (moving, fixed)
1328 } else {
1329 (fixed, moving)
1330 };
1331 let distance_squared = ring_grid
1332 [ring_slot(side, exponent_index, index as usize)]
1333 .sub(point)
1334 .length_squared();
1335 if distance_squared < candidate.distance_squared {
1336 candidate.u = u;
1337 candidate.v = v;
1338 candidate.distance_squared = distance_squared;
1339 }
1340 }
1341 };
1342 for exponent in 1..=22 {
1343 let offset = 1.0 / 2f64.powi(exponent);
1344 let e = exponent as usize - 1;
1345 if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
1346 scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
1347 }
1348 if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
1349 scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
1350 }
1351 if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
1352 scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
1353 }
1354 if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
1355 scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
1356 }
1357 }
1358 if ring.distance_squared < best.distance_squared {
1359 best = ring;
1360 }
1361 let polished = surface_newton(
1362 surface,
1363 point,
1364 ring.u,
1365 ring.v,
1366 [u0, u1, v0, v1],
1367 closed_u,
1368 closed_v,
1369 )?;
1370 if polished.distance_squared < best.distance_squared {
1371 best = polished;
1372 }
1373 }
1374 }
1375 Ok(best)
1376}
1377
1378fn global_stage_enabled() -> bool {
1383 switch!("BREP_PROJECTION_GLOBAL", true)
1384}
1385
1386pub(crate) fn global_site_filter_active() -> bool {
1395 static ACTIVE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1400 *ACTIVE.get_or_init(|| {
1401 std::env::var_os("BREP_PROJECTION_GLOBAL_SITES").is_some() || std::env::var_os("BREP_PROJECTION_GLOBAL_EXCEPT").is_some()
1402 })
1403}
1404
1405fn global_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
1406 fn list(name: &str) -> Option<Vec<String>> {
1407 std::env::var(name)
1408 .ok()
1409 .map(|value| value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect())
1410 }
1411 static SITES: std::sync::OnceLock<(Option<Vec<String>>, Option<Vec<String>>)> =
1412 std::sync::OnceLock::new();
1413 let (only, except) = SITES.get_or_init(|| {
1414 (list("BREP_PROJECTION_GLOBAL_SITES"), list("BREP_PROJECTION_GLOBAL_EXCEPT"))
1415 });
1416 if only.is_none() && except.is_none() {
1417 return true;
1418 }
1419 let location = format!("{}:{}", caller.file(), caller.line());
1420 let named = |sites: &Vec<String>| sites.iter().any(|site| location.contains(site.as_str()));
1421 only.as_ref().is_none_or(named) && !except.as_ref().is_some_and(named)
1422}
1423
1424fn audit_record(caller: &std::panic::Location<'_>, surface: &NurbsSurface, local: &NewtonResult, global: &NewtonResult) {
1430 use std::io::Write;
1431 static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
1432 std::sync::OnceLock::new();
1433 let sink = SINK.get_or_init(|| {
1434 let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
1435 let path = std::path::Path::new(&directory)
1436 .join(format!("projection-audit-{}.tsv", std::process::id()));
1437 std::fs::OpenOptions::new()
1438 .create(true)
1439 .append(true)
1440 .open(path)
1441 .ok()
1442 .map(std::sync::Mutex::new)
1443 });
1444 if let Some(file) = sink {
1445 if let Ok(mut file) = file.lock() {
1446 let _ = writeln!(
1447 file,
1448 "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{}x{} cp {}x{}",
1449 caller.file(),
1450 caller.line(),
1451 local.distance_squared.sqrt(),
1452 global.distance_squared.sqrt(),
1453 local.u,
1454 local.v,
1455 global.u,
1456 global.v,
1457 surface.degree_u,
1458 surface.degree_v,
1459 surface.control_points.len(),
1460 surface.control_points.first().map(Vec::len).unwrap_or(0),
1461 );
1462 }
1463 }
1464}
1465
1466#[track_caller]
1542pub(crate) fn project_point_to_surface_general(
1543 surface: &NurbsSurface,
1544 point: Vec3,
1545) -> Result<SurfaceProjection, String> {
1546 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_GENERAL, surface);
1547 let [u0, u1] = surface.domain_u()?;
1548 let [v0, v1] = surface.domain_v()?;
1549 let (closed_u, closed_v) = surface.closed_directions()?;
1550 let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1551 let mut best = local;
1552 if global_stage_enabled() {
1553 global_minimum(
1554 surface,
1555 point,
1556 [u0, u1, v0, v1],
1557 (closed_u, closed_v),
1558 &mut best,
1559 &mut GlobalStats::default(),
1560 )?;
1561 if best.u != local.u || best.v != local.v {
1562 let caller = std::panic::Location::caller();
1563 if global_answer_admitted(caller) {
1564 audit_record(caller, surface, &local, &best);
1565 } else {
1566 best = local;
1567 }
1568 }
1569 }
1570 let projected = surface.evaluate(best.u, best.v)?;
1571 Ok(SurfaceProjection {
1572 u: best.u,
1573 v: best.v,
1574 point: projected,
1575 distance: projected.sub(point).length(),
1576 })
1577}
1578
1579pub(crate) fn project_point_to_surface_from_nearest_seed(
1589 surface: &NurbsSurface,
1590 point: Vec3,
1591) -> Result<SurfaceProjection, String> {
1592 let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_NEAREST_SEED, surface);
1593 if let Some(analytic) = surface.analytic() {
1594 if let Some(projection) = analytic.project(surface, point) {
1595 return Ok(projection);
1596 }
1597 }
1598 if let Some(ellipsoid) = surface.affine_sphere() {
1599 if let Some(projection) = ellipsoid.project(surface, point) {
1600 return Ok(projection);
1601 }
1602 }
1603 let [u0, u1] = surface.domain_u()?;
1604 let [v0, v1] = surface.domain_v()?;
1605 let (closed_u, closed_v) = surface.closed_directions()?;
1606 let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1607 let projected = surface.evaluate(local.u, local.v)?;
1608 Ok(SurfaceProjection {
1609 u: local.u,
1610 v: local.v,
1611 point: projected,
1612 distance: projected.sub(point).length(),
1613 })
1614}
1615
1616#[doc(hidden)]
1619pub fn project_point_to_surface_general_lanes(
1620 surface: &NurbsSurface,
1621 point: Vec3,
1622 run_global: bool,
1623) -> Result<(SurfaceProjection, GlobalStats), String> {
1624 let [u0, u1] = surface.domain_u()?;
1625 let [v0, v1] = surface.domain_v()?;
1626 let (closed_u, closed_v) = surface.closed_directions()?;
1627 let mut best = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1628 let mut stats = GlobalStats::default();
1629 if run_global {
1630 global_minimum(surface, point, [u0, u1, v0, v1], (closed_u, closed_v), &mut best, &mut stats)?;
1631 }
1632 let projected = surface.evaluate(best.u, best.v)?;
1633 Ok((
1634 SurfaceProjection {
1635 u: best.u,
1636 v: best.v,
1637 point: projected,
1638 distance: projected.sub(point).length(),
1639 },
1640 stats,
1641 ))
1642}
1643
1644#[doc(hidden)]
1647#[derive(Clone, Debug, Default)]
1648pub struct BasinCensus {
1649 pub cells: usize,
1652 pub descents: usize,
1654 pub unconverged: usize,
1656 pub cells_with_two_basins: usize,
1660 pub foot_cells_with_two_basins: usize,
1663 pub least: f64,
1665 pub least_uv: (f64, f64),
1666 pub foot_cell_descents: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)>,
1670}
1671
1672#[doc(hidden)]
1677pub fn project_point_to_surface_basin_census(
1678 surface: &NurbsSurface,
1679 point: Vec3,
1680 answer_distance: f64,
1681 bar: f64,
1682) -> Result<BasinCensus, String> {
1683 let mut census = BasinCensus {
1684 least: f64::INFINITY,
1685 ..BasinCensus::default()
1686 };
1687 let Some(cells) = projection_cells(surface)? else {
1688 return Ok(census);
1689 };
1690 let columns = cells.vs.len() - 1;
1691 let mut spreads: Vec<(usize, f64)> = Vec::new();
1692 let mut runs: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)> = Vec::new();
1693 for (cell, cell_box) in cells.boxes.iter().enumerate() {
1694 if cell_box.lower_bound(point) >= answer_distance {
1695 continue;
1696 }
1697 census.cells += 1;
1698 let (row, column) = (cell / columns, cell % columns);
1699 let rectangle = [cells.us[row], cells.us[row + 1], cells.vs[column], cells.vs[column + 1]];
1700 let middle = ((rectangle[0] + rectangle[1]) * 0.5, (rectangle[2] + rectangle[3]) * 0.5);
1701 let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
1702 for seed in [
1703 (rectangle[0], rectangle[2]),
1704 (rectangle[1], rectangle[2]),
1705 (rectangle[0], rectangle[3]),
1706 (rectangle[1], rectangle[3]),
1707 middle,
1708 ] {
1709 census.descents += 1;
1710 let (found, _) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1711 let distance = found.distance_squared.sqrt();
1712 runs.push((cell, rectangle, seed, (found.u, found.v), distance, found.converged));
1713 if distance < census.least {
1714 census.least = distance;
1715 census.least_uv = (found.u, found.v);
1716 }
1717 if !found.converged {
1718 census.unconverged += 1;
1719 continue;
1720 }
1721 low = low.min(distance.max(LINEAR_TOLERANCE));
1722 high = high.max(distance.max(LINEAR_TOLERANCE));
1723 }
1724 if high - low > bar {
1725 spreads.push((cell, high - low));
1726 }
1727 }
1728 census.cells_with_two_basins = spreads.len();
1729 let (u, v) = census.least_uv;
1730 let foot_cells: Vec<usize> = spreads
1731 .iter()
1732 .map(|&(cell, _)| cell)
1733 .filter(|&cell| {
1734 let (row, column) = (cell / columns, cell % columns);
1735 cells.us[row] <= u && u <= cells.us[row + 1] && cells.vs[column] <= v && v <= cells.vs[column + 1]
1736 })
1737 .collect();
1738 census.foot_cells_with_two_basins = foot_cells.len();
1739 census.foot_cell_descents = runs.into_iter().filter(|run| foot_cells.contains(&run.0)).collect();
1740 Ok(census)
1741}
1742
1743#[doc(hidden)]
1745#[derive(Clone, Copy, Debug, Default)]
1746pub struct GlobalStats {
1747 pub settled_by_floor: bool,
1749 pub candidates: usize,
1751 pub refinements: usize,
1753 pub continuations: usize,
1755 pub replaced: bool,
1757 pub hull_lower_bound: f64,
1760}
1761
1762#[derive(Clone, Debug)]
1765struct CellBox {
1766 axes: [Vec3; 3],
1767 low: [f64; 3],
1768 high: [f64; 3],
1769}
1770
1771impl CellBox {
1772 fn lower_bound(&self, point: Vec3) -> f64 {
1775 let mut squared = 0.0;
1776 for axis in 0..3 {
1777 let coordinate = point.dot(self.axes[axis]);
1778 let excess = (self.low[axis] - coordinate).max(coordinate - self.high[axis]).max(0.0);
1779 squared += excess * excess;
1780 }
1781 squared.sqrt()
1782 }
1783}
1784
1785#[derive(Clone, Copy, Debug)]
1788struct CellTreeNode {
1789 centre: Vec3,
1790 radius: f64,
1791 children: Option<(u32, u32)>,
1792 cell: u32,
1793}
1794
1795#[derive(Clone, Debug)]
1798pub(crate) struct ProjectionCells {
1799 us: Vec<f64>,
1800 vs: Vec<f64>,
1801 nodes: Vec<Vec3>,
1803 boxes: Vec<CellBox>,
1805 tree: Vec<CellTreeNode>,
1807 node_boxes: Vec<CellBox>,
1811 node_margin: f64,
1813 scale: f64,
1815 resolution: f64,
1816}
1817
1818fn lattice_parameters(knots: &[f64], degree: usize, closed: bool) -> Vec<f64> {
1822 let [start, end] = crate::curve::knot_domain(knots, degree);
1823 let samples = if closed {
1824 8usize.max(degree * 4)
1825 } else {
1826 3usize.max(degree + 1)
1827 };
1828 let mut breaks = vec![start];
1829 breaks.extend(interior_knots(knots, degree));
1830 breaks.push(end);
1831 let mut parameters = Vec::with_capacity((breaks.len() - 1) * samples + 1);
1832 for pair in breaks.windows(2) {
1833 for index in 0..samples {
1834 parameters.push(pair[0] + (pair[1] - pair[0]) * index as f64 / samples as f64);
1835 }
1836 }
1837 parameters.push(end);
1838 parameters
1839}
1840
1841fn refine_to_lattice(net: &mut Vec<Vec<Vec4>>, knots: &mut Vec<f64>, degree: usize, parameters: &[f64]) {
1850 let p = degree;
1851 let n = net.len() - 1;
1852 let mut inserted = Vec::new();
1853 for ¶meter in parameters {
1854 let multiplicity = knots.iter().filter(|&&knot| knot == parameter).count();
1855 for _ in multiplicity..p {
1856 inserted.push(parameter);
1857 }
1858 }
1859 if inserted.is_empty() || p == 0 {
1860 return;
1861 }
1862 inserted.sort_by(f64::total_cmp);
1863 let find_span = |parameter: f64| -> usize {
1864 if parameter >= knots[n + 1] {
1865 return n;
1866 }
1867 (knots.partition_point(|&knot| knot <= parameter) - 1).clamp(p, n)
1868 };
1869 let r = inserted.len() - 1;
1870 let m = n + p + 1;
1871 let a = find_span(inserted[0]);
1872 let b = find_span(inserted[r]) + 1;
1873 let columns = net[0].len();
1874 let blank = vec![
1875 Vec4 {
1876 x: 0.0,
1877 y: 0.0,
1878 z: 0.0,
1879 w: 0.0,
1880 };
1881 columns
1882 ];
1883 let mut refined = vec![blank; n + r + 2];
1884 let mut refined_knots = vec![0.0; m + r + 2];
1885 for j in 0..=a - p {
1886 refined[j] = net[j].clone();
1887 }
1888 for j in b - 1..=n {
1889 refined[j + r + 1] = net[j].clone();
1890 }
1891 refined_knots[..=a].copy_from_slice(&knots[..=a]);
1892 for j in b + p..=m {
1893 refined_knots[j + r + 1] = knots[j];
1894 }
1895 let mut i = b + p - 1;
1896 let mut k = b + p + r;
1897 for j in (0..=r).rev() {
1898 while inserted[j] <= knots[i] && i > a {
1899 refined[k - p - 1] = net[i - p - 1].clone();
1900 refined_knots[k] = knots[i];
1901 k -= 1;
1902 i -= 1;
1903 }
1904 refined[k - p - 1] = refined[k - p].clone();
1905 for l in 1..=p {
1906 let index = k - p + l;
1907 let numerator = refined_knots[k + l] - inserted[j];
1908 if numerator == 0.0 {
1909 refined[index - 1] = refined[index].clone();
1910 } else {
1911 let alpha = numerator / (refined_knots[k + l] - knots[i - p + l]);
1912 let (head, tail) = refined.split_at_mut(index);
1913 for (before, &here) in head[index - 1].iter_mut().zip(&tail[0]) {
1914 *before = before.scale(alpha).add(here.scale(1.0 - alpha));
1915 }
1916 }
1917 }
1918 refined_knots[k] = inserted[j];
1919 k = k.saturating_sub(1);
1920 }
1921 *net = refined;
1922 *knots = refined_knots;
1923}
1924
1925fn supporting_range(knots: &[f64], degree: usize, count: usize, low: f64, high: f64) -> (usize, usize) {
1928 let first_span = knots.partition_point(|&knot| knot <= low).saturating_sub(1);
1929 let last_span = knots.partition_point(|&knot| knot < high).saturating_sub(1);
1930 (
1931 first_span.saturating_sub(degree).min(count - 1),
1932 last_span.max(first_span).min(count - 1),
1933 )
1934}
1935
1936fn unit_or(vector: Vec3, fallback: Vec3) -> Vec3 {
1937 let length = vector.length();
1938 if length > 0.0 && length.is_finite() {
1939 vector.scale(1.0 / length)
1940 } else {
1941 fallback
1942 }
1943}
1944
1945fn enclosing_sphere(a: (Vec3, f64), b: (Vec3, f64)) -> (Vec3, f64) {
1946 let offset = b.0.sub(a.0);
1947 let separation = offset.length();
1948 if separation + b.1 <= a.1 {
1949 return a;
1950 }
1951 if separation + a.1 <= b.1 {
1952 return b;
1953 }
1954 let radius = (separation + a.1 + b.1) * 0.5;
1955 let centre = a.0.add(offset.scale((radius - a.1) / separation));
1956 (centre, radius)
1957}
1958
1959struct NodeBoxInputs<'a> {
1963 boxes: &'a [CellBox],
1964 nodes: &'a [Vec3],
1965 lattice_columns: usize,
1966 margin: f64,
1967}
1968
1969fn enclosing_node_box(corners: [Vec3; 4], children: [&CellBox; 2], margin: f64) -> CellBox {
1974 let unbounded = || CellBox {
1975 axes: [Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 1.0)],
1976 low: [f64::NEG_INFINITY; 3],
1977 high: [f64::INFINITY; 3],
1978 };
1979 let [c00, c10, c01, c11] = corners;
1980 let along_u = c10.sub(c00).add(c11.sub(c01));
1981 let along_v = c01.sub(c00).add(c11.sub(c10));
1982 let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
1983 let Ok(fallback) = e1.perpendicular() else {
1984 return unbounded();
1985 };
1986 let normal = unit_or(e1.cross(along_v), fallback);
1987 let e2 = unit_or(normal.cross(e1), fallback);
1988 let e3 = e1.cross(e2);
1989 let axes = [e1, e2, e3];
1990 let mut low = [f64::INFINITY; 3];
1991 let mut high = [f64::NEG_INFINITY; 3];
1992 for child in children {
1993 for corner in 0..8 {
1994 let mut point = Vec3::default();
1995 for axis in 0..3 {
1996 let value = if corner >> axis & 1 == 0 { child.low[axis] } else { child.high[axis] };
1997 point = point.add(child.axes[axis].scale(value));
1998 }
1999 for axis in 0..3 {
2000 let coordinate = point.dot(axes[axis]);
2001 low[axis] = low[axis].min(coordinate);
2002 high[axis] = high[axis].max(coordinate);
2003 }
2004 }
2005 }
2006 for axis in 0..3 {
2007 low[axis] -= margin;
2008 high[axis] += margin;
2009 }
2010 if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
2011 return unbounded();
2012 }
2013 CellBox { axes, low, high }
2014}
2015
2016fn build_cell_tree(
2017 tree: &mut Vec<CellTreeNode>,
2018 node_boxes: &mut Vec<CellBox>,
2019 inputs: &NodeBoxInputs<'_>,
2020 spheres: &[(Vec3, f64)],
2021 columns: usize,
2022 [row_low, row_high, column_low, column_high]: [usize; 4],
2023) -> u32 {
2024 if row_high - row_low == 1 && column_high - column_low == 1 {
2025 let cell = row_low * columns + column_low;
2026 node_boxes.push(inputs.boxes[cell].clone());
2027 tree.push(CellTreeNode {
2028 centre: spheres[cell].0,
2029 radius: spheres[cell].1,
2030 children: None,
2031 cell: cell as u32,
2032 });
2033 return (tree.len() - 1) as u32;
2034 }
2035 let (first, second) = if row_high - row_low >= column_high - column_low {
2036 let middle = (row_low + row_high) / 2;
2037 (
2038 [row_low, middle, column_low, column_high],
2039 [middle, row_high, column_low, column_high],
2040 )
2041 } else {
2042 let middle = (column_low + column_high) / 2;
2043 (
2044 [row_low, row_high, column_low, middle],
2045 [row_low, row_high, middle, column_high],
2046 )
2047 };
2048 let first = build_cell_tree(tree, node_boxes, inputs, spheres, columns, first);
2049 let second = build_cell_tree(tree, node_boxes, inputs, spheres, columns, second);
2050 let a = tree[first as usize];
2051 let b = tree[second as usize];
2052 let lattice = |row: usize, column: usize| inputs.nodes[row * inputs.lattice_columns + column];
2053 node_boxes.push(enclosing_node_box(
2054 [
2055 lattice(row_low, column_low),
2056 lattice(row_high, column_low),
2057 lattice(row_low, column_high),
2058 lattice(row_high, column_high),
2059 ],
2060 [&node_boxes[first as usize], &node_boxes[second as usize]],
2061 inputs.margin,
2062 ));
2063 let (centre, radius) = enclosing_sphere((a.centre, a.radius), (b.centre, b.radius));
2067 tree.push(CellTreeNode {
2068 centre,
2069 radius: radius * (1.0 + 4.0 * f64::EPSILON),
2070 children: Some((first, second)),
2071 cell: u32::MAX,
2072 });
2073 (tree.len() - 1) as u32
2074}
2075
2076fn projection_cells(surface: &NurbsSurface) -> Result<Option<&ProjectionCells>, String> {
2077 if let Some(cells) = surface.projection_cells.get() {
2078 return Ok(cells.as_ref());
2079 }
2080 let cells = build_projection_cells(surface)?;
2081 Ok(surface.projection_cells.get_or_init(|| cells).as_ref())
2082}
2083
2084fn build_projection_cells(surface: &NurbsSurface) -> Result<Option<ProjectionCells>, String> {
2085 let (closed_u, closed_v) = surface.closed_directions()?;
2086 let us = lattice_parameters(&surface.knots_u, surface.degree_u, closed_u);
2087 let vs = lattice_parameters(&surface.knots_v, surface.degree_v, closed_v);
2088 if us.len() < 2 || vs.len() < 2 {
2089 return Ok(None);
2090 }
2091 let mut scale = 0.0f64;
2092 for control in surface.control_points.iter().flatten() {
2093 let point = control.point()?;
2094 scale = scale.max(point.x.abs()).max(point.y.abs()).max(point.z.abs());
2095 }
2096 let resolution =
2097 ((surface.degree_u + 1) * (surface.degree_v + 1)) as f64 * f64::EPSILON * scale;
2098
2099 let mut net = surface.control_points.clone();
2101 let mut knots_u = surface.knots_u.clone();
2102 refine_to_lattice(&mut net, &mut knots_u, surface.degree_u, &us);
2103 let columns = net.first().map(Vec::len).unwrap_or(0);
2104 let mut transposed: Vec<Vec<Vec4>> =
2105 (0..columns).map(|column| net.iter().map(|row| row[column]).collect()).collect();
2106 let mut knots_v = surface.knots_v.clone();
2107 refine_to_lattice(&mut transposed, &mut knots_v, surface.degree_v, &vs);
2108 let rows = transposed.first().map(Vec::len).unwrap_or(0);
2109 let columns = transposed.len();
2110 if rows == 0 || columns == 0 {
2111 return Ok(None);
2112 }
2113 let mut points = vec![Vec3::default(); rows * columns];
2114 for (column, fibre) in transposed.iter().enumerate() {
2115 for (row, control) in fibre.iter().enumerate() {
2116 points[row * columns + column] = control.point()?;
2117 }
2118 }
2119
2120 let mut nodes = Vec::with_capacity(us.len() * vs.len());
2121 for &u in &us {
2122 for &v in &vs {
2123 nodes.push(surface.evaluate(u, v)?);
2124 }
2125 }
2126
2127 let cell_rows = us.len() - 1;
2128 let cell_columns = vs.len() - 1;
2129 let mut boxes = Vec::with_capacity(cell_rows * cell_columns);
2130 let mut spheres = Vec::with_capacity(cell_rows * cell_columns);
2131 for i in 0..cell_rows {
2132 let (row_low, row_high) =
2133 supporting_range(&knots_u, surface.degree_u, rows, us[i], us[i + 1]);
2134 for j in 0..cell_columns {
2135 let (column_low, column_high) =
2136 supporting_range(&knots_v, surface.degree_v, columns, vs[j], vs[j + 1]);
2137 let at = |row: usize, column: usize| points[row * columns + column];
2138 let c00 = at(row_low, column_low);
2139 let c10 = at(row_high, column_low);
2140 let c01 = at(row_low, column_high);
2141 let c11 = at(row_high, column_high);
2142 let along_u = c10.sub(c00).add(c11.sub(c01));
2143 let along_v = c01.sub(c00).add(c11.sub(c10));
2144 let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
2145 let normal = unit_or(e1.cross(along_v), e1.perpendicular()?);
2146 let e2 = unit_or(normal.cross(e1), e1.perpendicular()?);
2147 let e3 = e1.cross(e2);
2148 let axes = [e1, e2, e3];
2149 let mut low = [f64::INFINITY; 3];
2150 let mut high = [f64::NEG_INFINITY; 3];
2151 for row in row_low..=row_high {
2152 for column in column_low..=column_high {
2153 let point = at(row, column);
2154 for axis in 0..3 {
2155 let coordinate = point.dot(axes[axis]);
2156 low[axis] = low[axis].min(coordinate);
2157 high[axis] = high[axis].max(coordinate);
2158 }
2159 }
2160 }
2161 for axis in 0..3 {
2162 low[axis] -= resolution;
2163 high[axis] += resolution;
2164 }
2165 let mut centre = Vec3::default();
2166 let mut half_diagonal_squared = 0.0;
2167 for axis in 0..3 {
2168 centre = centre.add(axes[axis].scale((low[axis] + high[axis]) * 0.5));
2169 let half = (high[axis] - low[axis]) * 0.5;
2170 half_diagonal_squared += half * half;
2171 }
2172 if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
2173 return Ok(None);
2174 }
2175 spheres.push((centre, half_diagonal_squared.sqrt() + resolution));
2176 boxes.push(CellBox { axes, low, high });
2177 }
2178 }
2179 let mut tree = Vec::with_capacity(2 * boxes.len());
2180 let mut node_boxes = Vec::with_capacity(2 * boxes.len());
2181 let node_margin = NODE_BOX_MARGIN * scale;
2186 let inputs = NodeBoxInputs { boxes: &boxes, nodes: &nodes, lattice_columns: vs.len(), margin: node_margin };
2187 build_cell_tree(&mut tree, &mut node_boxes, &inputs, &spheres, cell_columns, [0, cell_rows, 0, cell_columns]);
2188 if !(node_margin > 0.0 && node_margin.is_finite()) {
2189 node_boxes = Vec::new();
2190 }
2191 Ok(Some(ProjectionCells {
2192 us,
2193 vs,
2194 nodes,
2195 boxes,
2196 tree,
2197 node_boxes,
2198 node_margin,
2199 scale,
2200 resolution,
2201 }))
2202}
2203
2204fn optimality(
2209 derivatives: &[[Vec3; 3]; 3],
2210 point: Vec3,
2211 (u, v): (f64, f64),
2212 [ua, ub, va, vb]: [f64; 4],
2213 periodic: (bool, bool),
2214) -> Optimality {
2215 let residual = derivatives[0][0].sub(point);
2216 let residual_length = residual.length();
2217 let su = derivatives[1][0];
2218 let sv = derivatives[0][1];
2219 let f = su.dot(residual);
2220 let g = sv.dot(residual);
2221 let metric_u = su.length_squared();
2222 let metric_v = sv.length_squared();
2223 let stationary_u = metric_u.sqrt() * residual_length <= EPSILON
2224 || f.abs() <= 1e-10 * metric_u.sqrt() * residual_length;
2225 let stationary_v = metric_v.sqrt() * residual_length <= EPSILON
2226 || g.abs() <= 1e-10 * metric_v.sqrt() * residual_length;
2227 let pinned_u = !periodic.0 && ((u <= ua && f > 0.0) || (u >= ub && f < 0.0));
2228 let pinned_v = !periodic.1 && ((v <= va && g > 0.0) || (v >= vb && g < 0.0));
2229 let j00 = derivatives[2][0].dot(residual) + metric_u;
2230 let j01 = derivatives[1][1].dot(residual) + su.dot(sv);
2231 let j11 = derivatives[0][2].dot(residual) + metric_v;
2232 let determinant = j00 * j11 - j01 * j01;
2233 let convex = match (!pinned_u, !pinned_v) {
2234 (true, true) => j00 > 0.0 && determinant > 0.0,
2235 (true, false) => j00 > 0.0,
2236 (false, true) => j11 > 0.0,
2237 (false, false) => true,
2238 };
2239 Optimality {
2240 residual_length,
2241 gradient: (f, g),
2242 metric: (metric_u, metric_v),
2243 hessian: (j00, j01, j11),
2244 determinant,
2245 pinned: (pinned_u, pinned_v),
2246 convex,
2247 minimum: residual_length <= LINEAR_TOLERANCE
2248 || ((stationary_u || pinned_u) && (stationary_v || pinned_v) && convex),
2249 }
2250}
2251
2252struct Optimality {
2253 residual_length: f64,
2254 gradient: (f64, f64),
2255 metric: (f64, f64),
2256 hessian: (f64, f64, f64),
2257 determinant: f64,
2258 pinned: (bool, bool),
2259 convex: bool,
2260 minimum: bool,
2261}
2262
2263fn is_verified_minimum(
2264 surface: &NurbsSurface,
2265 point: Vec3,
2266 at: (f64, f64),
2267 rectangle: [f64; 4],
2268 periodic: (bool, bool),
2269) -> Result<bool, String> {
2270 let derivatives = surface.derivatives_small(at.0, at.1, 2)?;
2271 Ok(optimality(&derivatives, point, at, rectangle, periodic).minimum)
2272}
2273
2274fn confined_descent(
2290 surface: &NurbsSurface,
2291 point: Vec3,
2292 seed: (f64, f64),
2293 rectangle: [f64; 4],
2294 periodic: (bool, bool),
2295) -> Result<(NewtonResult, bool), String> {
2296 let [ua, ub, va, vb] = rectangle;
2297 let mut u = fit_parameter(seed.0, ua, ub, periodic.0);
2298 let mut v = fit_parameter(seed.1, va, vb, periodic.1);
2299 let mut converged = false;
2300 let mut current = surface.evaluate(u, v)?.sub(point).length_squared();
2301 for _ in 0..MAX_NEWTON_ITERATIONS {
2302 let derivatives = surface.derivatives_small(u, v, 2)?;
2303 let state = optimality(&derivatives, point, (u, v), rectangle, periodic);
2304 current = state.residual_length * state.residual_length;
2305 if state.minimum {
2306 converged = true;
2307 break;
2308 }
2309 let (f, g) = state.gradient;
2310 let (metric_u, metric_v) = state.metric;
2311 let (j00, j01, j11) = state.hessian;
2312 let determinant = state.determinant;
2313 let (free_u, free_v) = (!state.pinned.0, !state.pinned.1);
2314 let (mut du, mut dv) = match (free_u, free_v) {
2315 (true, true) if state.convex => (
2316 (-f * j11 + g * j01) / determinant,
2317 (-g * j00 + f * j01) / determinant,
2318 ),
2319 (true, false) if state.convex => (-f / j00, 0.0),
2320 (false, true) if state.convex => (0.0, -g / j11),
2321 _ => (
2322 if free_u && metric_u > 0.0 { -f / metric_u } else { 0.0 },
2323 if free_v && metric_v > 0.0 { -g / metric_v } else { 0.0 },
2324 ),
2325 };
2326 if !(du.is_finite() && dv.is_finite()) || (du == 0.0 && dv == 0.0) {
2327 break;
2328 }
2329 let mut moved = false;
2330 let taken = |next: f64, here: f64, low: f64, high: f64, wraps: bool| {
2332 let raw = next - here;
2333 if wraps {
2334 raw - (high - low) * (raw / (high - low)).round()
2335 } else {
2336 raw
2337 }
2338 };
2339 for _ in 0..40 {
2340 let next_u = fit_parameter(u + du, ua, ub, periodic.0);
2341 let next_v = fit_parameter(v + dv, va, vb, periodic.1);
2342 if next_u == u && next_v == v {
2343 break;
2344 }
2345 let trial = surface.evaluate(next_u, next_v)?.sub(point).length_squared();
2346 if trial <= current {
2347 moved = taken(next_u, u, ua, ub, periodic.0).abs() > 1e-15 * (ub - ua)
2348 || taken(next_v, v, va, vb, periodic.1).abs() > 1e-15 * (vb - va);
2349 u = next_u;
2350 v = next_v;
2351 current = trial;
2352 break;
2353 }
2354 du *= 0.5;
2355 dv *= 0.5;
2356 }
2357 if !moved {
2358 break;
2359 }
2360 }
2361 let result = NewtonResult {
2362 u,
2363 v,
2364 distance_squared: current,
2365 converged,
2366 };
2367 let on_edge =
2368 (!periodic.0 && (u <= ua || u >= ub)) || (!periodic.1 && (v <= va || v >= vb));
2369 Ok((result, on_edge))
2370}
2371
2372const NODE_BOX_MARGIN: f64 = 1e-9;
2375
2376fn node_boxes_enabled() -> bool {
2380 switch!("BREP_PROJECTION_NODE_BOXES", true)
2381}
2382
2383fn hull_candidates(cells: &ProjectionCells, point: Vec3, bound: f64, node_boxes: bool) -> (Vec<(f64, usize)>, f64) {
2404 let read_error = 32.0 * f64::EPSILON * (point.x.abs() + point.y.abs() + point.z.abs() + 4.0 * cells.scale);
2405 let boxes = node_boxes && !cells.node_boxes.is_empty() && 4.0 * read_error <= cells.node_margin;
2406 let mut candidates: Vec<(f64, usize)> = Vec::new();
2407 let mut excluded = f64::INFINITY;
2408 let mut stack: Vec<u32> = Vec::with_capacity(64);
2409 stack.push((cells.tree.len() - 1) as u32);
2410 while let Some(index) = stack.pop() {
2411 let node = cells.tree[index as usize];
2412 let sphere_bound = (point.sub(node.centre).length() - node.radius).max(0.0);
2413 if sphere_bound >= bound {
2414 excluded = excluded.min(sphere_bound);
2415 continue;
2416 }
2417 match node.children {
2418 Some((first, second)) => {
2419 if boxes {
2420 let box_bound = cells.node_boxes[index as usize].lower_bound(point);
2421 if box_bound >= bound && box_bound > 2.0 * read_error {
2422 excluded = excluded.min(box_bound);
2423 continue;
2424 }
2425 }
2426 stack.push(first);
2427 stack.push(second);
2428 }
2429 None => {
2430 let cell = node.cell as usize;
2431 let box_bound = cells.boxes[cell].lower_bound(point);
2432 if box_bound < bound {
2433 candidates.push((box_bound, cell));
2434 } else {
2435 excluded = excluded.min(box_bound);
2436 }
2437 }
2438 }
2439 }
2440 (candidates, excluded)
2441}
2442
2443#[allow(clippy::too_many_arguments)]
2446fn global_minimum(
2447 surface: &NurbsSurface,
2448 point: Vec3,
2449 domains: [f64; 4],
2450 periodic: (bool, bool),
2451 best: &mut NewtonResult,
2452 stats: &mut GlobalStats,
2453) -> Result<(), String> {
2454 if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
2455 stats.settled_by_floor = true;
2456 return Ok(());
2457 }
2458 let Some(cells) = projection_cells(surface)? else {
2459 return Ok(());
2460 };
2461 let resolution = cells.resolution;
2462 let columns = cells.vs.len() - 1;
2463 let columns_u = cells.us.len() - 1;
2464 let mut bound = best.distance_squared.sqrt() - resolution;
2465
2466 let (mut candidates, excluded) = hull_candidates(cells, point, bound, node_boxes_enabled());
2469 stats.candidates = candidates.len();
2470 candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
2471 stats.hull_lower_bound = candidates
2472 .first()
2473 .map_or(excluded, |&(least, _)| least.min(excluded));
2474
2475 let cell_of = |u: f64, v: f64| {
2481 let row = (cells.us.partition_point(|&value| value <= u).max(1) - 1).min(columns_u - 1);
2482 let column = (cells.vs.partition_point(|&value| value <= v).max(1) - 1).min(columns - 1);
2483 row * columns + column
2484 };
2485 let mut settled: Vec<usize> = Vec::new();
2486 if is_verified_minimum(surface, point, (best.u, best.v), domains, periodic)? {
2487 settled.push(cell_of(best.u, best.v));
2488 }
2489 for (cell_bound, cell) in candidates {
2490 if cell_bound >= bound {
2491 break;
2492 }
2493 let row = cell / columns;
2494 let column = cell % columns;
2495 let rectangle = [
2496 cells.us[row],
2497 cells.us[row + 1],
2498 cells.vs[column],
2499 cells.vs[column + 1],
2500 ];
2501 if settled.contains(&cell) {
2502 continue;
2503 }
2504 let mut seed = (rectangle[0], rectangle[2]);
2505 let mut seed_distance = f64::INFINITY;
2506 for (node_row, node_column) in
2507 [(row, column), (row + 1, column), (row, column + 1), (row + 1, column + 1)]
2508 {
2509 let distance = cells.nodes[node_row * cells.vs.len() + node_column]
2510 .sub(point)
2511 .length_squared();
2512 if distance < seed_distance {
2513 seed_distance = distance;
2514 seed = (cells.us[node_row], cells.vs[node_column]);
2515 }
2516 }
2517 stats.refinements += 1;
2518 let (confined, on_edge) = confined_descent(surface, point, seed, rectangle, (false, false))?;
2519 let mut found = confined;
2520 if on_edge && confined.distance_squared.sqrt() < bound {
2521 stats.continuations += 1;
2524 let (continued, _) =
2525 confined_descent(surface, point, (confined.u, confined.v), domains, periodic)?;
2526 if continued.distance_squared < found.distance_squared {
2527 found = continued;
2528 }
2529 if continued.converged {
2530 settled.push(cell_of(continued.u, continued.v));
2531 }
2532 }
2533 if found.distance_squared.sqrt() < bound {
2534 *best = found;
2535 bound = best.distance_squared.sqrt() - resolution;
2536 stats.replaced = true;
2537 if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
2538 return Ok(());
2539 }
2540 }
2541 }
2542 Ok(())
2543}
2544
2545
2546
2547#[derive(Clone, Copy)]
2549pub(crate) enum FootDomain {
2550 Clamp,
2551 Wrap,
2552 Extended,
2553}
2554
2555#[derive(Clone, Copy)]
2556pub(crate) struct CurveFootPolicy {
2557 pub domain: FootDomain,
2558 pub iterations: usize,
2559 pub slope_guard: f64,
2560 pub step_limit: f64,
2561 pub parameter_tolerance: f64,
2562 pub mode: FootMode,
2563}
2564
2565#[derive(Clone, Copy)]
2567pub(crate) enum FootMode {
2568 Monotone,
2569 Witness,
2570 Stationary { positive_slope: bool, relative_step: bool },
2571 Backtracked,
2572 Planar,
2573}
2574
2575fn curve_foot_newton_state(
2578 curve: &NurbsCurve,
2579 point: Vec3,
2580 seed: f64,
2581 bracket: [f64; 2],
2582 policy: CurveFootPolicy,
2583) -> Result<(f64, f64, f64), String> {
2584 let fit = |u| match policy.domain {
2585 FootDomain::Clamp => fit_parameter(u, bracket[0], bracket[1], false),
2586 FootDomain::Wrap => fit_parameter(u, bracket[0], bracket[1], true),
2587 FootDomain::Extended => u,
2588 };
2589 let extended = matches!(policy.domain, FootDomain::Extended) || matches!(policy.mode, FootMode::Backtracked);
2590 let planar = matches!(policy.mode, FootMode::Planar);
2591 let project = |p: Vec3| if planar { Vec3::new(p.x, p.y, 0.0) } else { p };
2592 let point = project(point);
2593 let distance = |u| -> Result<f64, String> {
2594 let p = if extended { curve.derivatives_extended(u, 0)?[0] } else { curve.evaluate(u)? };
2595 Ok(project(p).sub(point).length())
2596 };
2597 let monotone = matches!(policy.mode, FootMode::Monotone | FootMode::Backtracked);
2598 let witness = matches!(policy.mode, FootMode::Witness);
2599 let mut u = if witness { seed } else { fit(seed) };
2600 let mut nearest_u = seed;
2601 let mut nearest_squared = f64::INFINITY;
2602 let mut best = if monotone { distance(u)? } else { f64::INFINITY };
2603 for _ in 0..policy.iterations {
2604 let derivatives = if extended {
2605 let values = curve.derivatives_extended(u, 2)?;
2606 [values[0], values[1], values[2]]
2607 } else if matches!(policy.mode, FootMode::Stationary { .. })
2608 || (planar && !crate::curve::small_curve_derivatives_enabled()) {
2609 let values = curve.derivatives(u, 2)?;
2610 [values[0], values[1], values[2]]
2611 } else {
2612 curve.derivatives_small(u, 2)?
2613 }.map(project);
2614 let offset = derivatives[0].sub(point);
2615 let numerator = offset.dot(derivatives[1]);
2616 let denominator = derivatives[1].dot(derivatives[1]) + offset.dot(derivatives[2]);
2617 if witness {
2618 let squared = offset.length_squared();
2619 if squared < nearest_squared {
2620 nearest_u = u;
2621 nearest_squared = squared;
2622 }
2623 let residual_length = squared.sqrt();
2624 if residual_length <= LINEAR_TOLERANCE
2625 || numerator.abs() <= EPSILON + 1e-10 * derivatives[1].length() * residual_length {
2626 break;
2627 }
2628 }
2629 let singular = if planar { denominator.abs() < policy.slope_guard }
2630 else { denominator.abs() <= policy.slope_guard };
2631 if singular || (matches!(policy.mode, FootMode::Stationary { positive_slope: true, .. }) && !(denominator > 0.0)) { break; }
2632 let mut step = if matches!(policy.mode, FootMode::Backtracked) && !(denominator > 0.0) {
2633 policy.step_limit.copysign(numerator)
2634 } else {
2635 (numerator / denominator).clamp(-policy.step_limit, policy.step_limit)
2636 };
2637 let mut next = fit(u - step);
2638 let mut next_distance = if monotone { distance(next)? } else { f64::INFINITY };
2639 if matches!(policy.mode, FootMode::Backtracked) {
2640 for _ in 0..30 {
2641 if next_distance < best { break; }
2642 step *= 0.5;
2643 next = fit(u - step);
2644 next_distance = distance(next)?;
2645 }
2646 if !(next_distance < best) { break; }
2647 } else if monotone && next_distance >= best { break; }
2648 let moved = if planar || matches!(policy.domain, FootDomain::Extended) { step }
2649 else if matches!(policy.domain, FootDomain::Wrap) {
2650 let period = bracket[1] - bracket[0];
2651 let raw = next - u;
2652 raw - period * (raw / period).round()
2653 } else { next - u };
2654 let tolerance = if matches!(policy.mode, FootMode::Stationary { relative_step: true, .. }) {
2655 policy.parameter_tolerance * (1.0 + u.abs())
2656 } else { policy.parameter_tolerance };
2657 let small = if planar || matches!(policy.mode, FootMode::Backtracked) { moved.abs() < tolerance }
2658 else { moved.abs() <= tolerance };
2659 if small && matches!(policy.mode, FootMode::Monotone) { break; }
2660 u = next;
2661 best = next_distance;
2662 if small { break; }
2663 }
2664 if witness { return Ok((u, nearest_u, nearest_squared)); }
2665 if !monotone { best = distance(u)?; }
2666 Ok((u, u, best * best))
2667}
2668
2669pub(crate) fn curve_foot_newton(
2670 curve: &NurbsCurve, point: Vec3, seed: f64, bracket: [f64; 2], policy: CurveFootPolicy,
2671) -> Result<(f64, f64), String> {
2672 let (_, nearest, squared) = curve_foot_newton_state(curve, point, seed, bracket, policy)?;
2673 Ok((nearest, squared))
2674}
2675
2676