1use std::cmp::Ordering;
2
3use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
4use axiolid_core::{Point3, Scalar};
5use axiolid_evaluate::surface::bspline_jet;
6use axiolid_surface::BSplineSurface;
7
8use crate::{
9 certified_bezier::{
10 distance_to_box_lower, distance_to_point_interval_upper, next_up, representative_distance,
11 },
12 certified_projection::{
13 CertifiedSurfaceProjection3, CertifiedSurfaceProjectionOptions, ParameterInterval,
14 SurfaceParameterBox, SurfaceProjectionCertificate3, SurfaceProjectionUnresolvedReason,
15 },
16 certified_refinement::RefinementBudget,
17 certified_surface_bezier::{
18 piecewise_bezier_patches, piecewise_periodic_bezier_patches, Patch,
19 },
20 PeriodicBSplineSurface,
21};
22
23const DIMENSIONS: usize = 3;
24
25#[derive(Debug, Clone)]
27pub(crate) enum SurfaceSearch {
28 Finished(CertifiedSurfaceProjection3),
30 Separated {
34 distance_lower_bound: Scalar,
36 },
37}
38
39fn finished(search: SurfaceSearch) -> GeomResult<CertifiedSurfaceProjection3> {
41 match search {
42 SurfaceSearch::Finished(projection) => Ok(projection),
43 SurfaceSearch::Separated { .. } => Err(GeomError::Degenerate(
44 "surface projection stopped at a separation threshold it was not given".to_owned(),
45 )),
46 }
47}
48
49#[derive(Debug, Clone, Copy)]
50struct Pending {
51 patch_index: usize,
52 domain: SurfaceParameterBox,
53 lower: Scalar,
54 depth: u16,
55 serial: u32,
56}
57
58#[derive(Debug, Clone, Copy)]
59struct Candidate {
60 u: Scalar,
61 v: Scalar,
62 point: Point3,
63 distance: Scalar,
64 upper: Scalar,
65}
66
67#[derive(Debug, Clone, Copy)]
68enum SplitAxis {
69 U,
70 V,
71}
72
73pub fn project_surface_certified(
83 surface: &BSplineSurface,
84 target: Point3,
85 options: CertifiedSurfaceProjectionOptions,
86) -> GeomResult<CertifiedSurfaceProjection3> {
87 validate_query(surface, target)?;
88 finished(project_surface_certified_with_modes(
89 surface, target, options, false, false, None,
90 )?)
91}
92
93pub(crate) fn search_surface_certified(
96 surface: &BSplineSurface,
97 target: Point3,
98 options: CertifiedSurfaceProjectionOptions,
99 separation: Scalar,
100) -> GeomResult<SurfaceSearch> {
101 validate_query(surface, target)?;
102 project_surface_certified_with_modes(surface, target, options, false, false, Some(separation))
103}
104
105pub fn project_periodic_surface_certified(
113 surface: &PeriodicBSplineSurface,
114 target: Point3,
115 options: CertifiedSurfaceProjectionOptions,
116) -> GeomResult<CertifiedSurfaceProjection3> {
117 validate_target(target)?;
118 finished(project_surface_certified_with_modes(
119 surface.as_bspline_surface(),
120 target,
121 options,
122 surface.u_is_periodic(),
123 surface.v_is_periodic(),
124 None,
125 )?)
126}
127
128pub(crate) fn search_periodic_surface_certified(
131 surface: &PeriodicBSplineSurface,
132 target: Point3,
133 options: CertifiedSurfaceProjectionOptions,
134 separation: Scalar,
135) -> GeomResult<SurfaceSearch> {
136 validate_target(target)?;
137 project_surface_certified_with_modes(
138 surface.as_bspline_surface(),
139 target,
140 options,
141 surface.u_is_periodic(),
142 surface.v_is_periodic(),
143 Some(separation),
144 )
145}
146
147fn project_surface_certified_with_modes(
148 surface: &BSplineSurface,
149 target: Point3,
150 options: CertifiedSurfaceProjectionOptions,
151 u_periodic: bool,
152 v_periodic: bool,
153 separation: Option<Scalar>,
154) -> GeomResult<SurfaceSearch> {
155 let target_array = target.to_array();
156 let mut budget = RefinementBudget::new(options.max_work(), "certified surface projection");
157 let patches = if u_periodic || v_periodic {
158 piecewise_periodic_bezier_patches(surface, u_periodic, v_periodic, &mut budget)?
159 } else {
160 piecewise_bezier_patches(surface, &mut budget)?
161 };
162 if patches.is_empty() {
163 return Err(GeomError::InvalidInput(
164 "certified surface projection requires at least one Bezier patch".to_owned(),
165 ));
166 }
167
168 let root_work = u128::try_from(patches.len()).map_err(|_| search_overflow())?;
169 budget.charge(Some(root_work))?;
170 let mut pending = Vec::new();
171 pending
172 .try_reserve_exact(patches.len())
173 .map_err(|_| allocation_error())?;
174 let mut candidate = None;
175 let mut visited_nodes = 0_u32;
176 let mut serial = 0_u32;
177
178 for (patch_index, patch) in patches.iter().enumerate() {
179 let domain = patch_domain(patch);
180 budget.charge(Some(patch.representative_bound_work()?))?;
181 let lower = patch_lower(patch, target_array)?;
182 update_candidate(
183 &mut candidate,
184 sample(surface, patch, domain, target_array)?,
185 );
186 pending.push(Pending {
187 patch_index,
188 domain,
189 lower,
190 depth: 0,
191 serial,
192 });
193 serial = serial.checked_add(1).ok_or_else(search_overflow)?;
194 visited_nodes = visited_nodes.checked_add(1).ok_or_else(search_overflow)?;
195 }
196
197 let mut candidate = candidate.ok_or_else(|| {
198 GeomError::Degenerate("surface projection could not construct an upper witness".to_owned())
199 })?;
200
201 loop {
202 pending.retain(|record| can_contain_global_minimizer(record.lower, candidate.upper));
203 if pending.is_empty() {
204 return Err(GeomError::Degenerate(
205 "outward surface projection bounds excluded every candidate".to_owned(),
206 ));
207 }
208
209 let lower = global_lower(&pending)?;
210 if separation.is_some_and(|threshold| lower > threshold) {
214 return Ok(SurfaceSearch::Separated {
215 distance_lower_bound: lower,
216 });
217 }
218 let parameter_ready = pending.iter().try_fold(true, |ready, record| {
219 Ok::<_, GeomError>(
220 ready
221 && interval_width(record.domain.u)? <= options.parameter_tolerance()
222 && interval_width(record.domain.v)? <= options.parameter_tolerance(),
223 )
224 })?;
225 let gap = certified_gap(candidate.upper, lower)?;
226 if gap <= options.distance_tolerance().linear() && parameter_ready {
227 let boxes = take_sorted_boxes(pending)?;
228 return Ok(SurfaceSearch::Finished(
229 CertifiedSurfaceProjection3::Complete(certificate(
230 candidate,
231 lower,
232 boxes,
233 visited_nodes,
234 )),
235 ));
236 }
237
238 let selected = select_refinable(
239 &pending,
240 options.max_depth(),
241 gap > options.distance_tolerance().linear(),
242 )?;
243 let Some(selected_index) = selected else {
244 let reason = if pending
245 .iter()
246 .any(|record| record.depth >= options.max_depth())
247 {
248 SurfaceProjectionUnresolvedReason::DepthLimit
249 } else {
250 SurfaceProjectionUnresolvedReason::FloatingPointNoProgress
251 };
252 let boxes = take_sorted_boxes(pending)?;
253 return Ok(SurfaceSearch::Finished(
254 CertifiedSurfaceProjection3::Unresolved {
255 certificate: certificate(candidate, lower, boxes, visited_nodes),
256 reason,
257 },
258 ));
259 };
260
261 let record = pending.swap_remove(selected_index);
262 let (axis, midpoint) = split_choice(record.domain)?.ok_or_else(|| {
263 GeomError::Degenerate("selected surface parameter box cannot advance".to_owned())
264 })?;
265 let child_depth = record.depth.checked_add(1).ok_or_else(search_overflow)?;
266 let patch = patches.get(record.patch_index).ok_or_else(|| {
267 GeomError::Degenerate("surface projection patch index escaped its catalog".to_owned())
268 })?;
269 let child_node_work = patch
270 .restriction_bound_work()?
271 .checked_add(patch.representative_bound_work()?)
272 .and_then(|work| work.checked_add(1))
273 .ok_or_else(search_overflow)?;
274 let child_work = child_node_work.checked_mul(2).ok_or_else(search_overflow)?;
275 budget.charge(Some(child_work))?;
276 let child_domains = split_domain(record.domain, axis, midpoint);
277 let children = [
278 make_pending(
279 &patches,
280 record.patch_index,
281 child_domains.0,
282 child_depth,
283 serial,
284 target_array,
285 )?,
286 make_pending(
287 &patches,
288 record.patch_index,
289 child_domains.1,
290 child_depth,
291 serial.checked_add(1).ok_or_else(search_overflow)?,
292 target_array,
293 )?,
294 ];
295 serial = serial.checked_add(2).ok_or_else(search_overflow)?;
296 visited_nodes = visited_nodes.checked_add(2).ok_or_else(search_overflow)?;
297
298 for child in &children {
299 update_resolved_candidate(
300 &mut candidate,
301 sample(
302 surface,
303 &patches[child.patch_index],
304 child.domain,
305 target_array,
306 )?,
307 );
308 }
309 for child in children {
310 if can_contain_global_minimizer(child.lower, candidate.upper) {
311 try_push(&mut pending, child)?;
312 }
313 }
314 }
315}
316
317fn validate_query(surface: &BSplineSurface, target: Point3) -> GeomResult<()> {
318 validate_target(target)?;
319 if surface.u_closed || surface.v_closed {
320 return Err(GeomError::Unsupported {
321 backend: BackendId::new("axiolid-nurbs"),
322 operation: Operation::SpatialQuery,
323 });
324 }
325 Ok(())
326}
327
328fn validate_target(target: Point3) -> GeomResult<()> {
329 if !target.is_finite() {
330 return Err(GeomError::InvalidInput(
331 "surface projection target must be finite".to_owned(),
332 ));
333 }
334 Ok(())
335}
336
337fn patch_domain(patch: &Patch) -> SurfaceParameterBox {
338 SurfaceParameterBox {
339 u: ParameterInterval {
340 start: patch.u_start,
341 end: patch.u_end,
342 },
343 v: ParameterInterval {
344 start: patch.v_start,
345 end: patch.v_end,
346 },
347 }
348}
349
350fn patch_lower(patch: &Patch, target: [Scalar; 3]) -> GeomResult<Scalar> {
351 let bounds = patch.coordinate_intervals()?;
352 distance_to_box_lower(
353 target,
354 [bounds[0].lower(), bounds[1].lower(), bounds[2].lower()],
355 [bounds[0].upper(), bounds[1].upper(), bounds[2].upper()],
356 DIMENSIONS,
357 )
358}
359
360fn restricted_lower(
361 patch: &Patch,
362 domain: SurfaceParameterBox,
363 target: [Scalar; 3],
364) -> GeomResult<Scalar> {
365 let restricted = patch.restrict(domain.u.start, domain.u.end, domain.v.start, domain.v.end)?;
366 patch_lower(&restricted, target)
367}
368
369fn sample(
370 surface: &BSplineSurface,
371 patch: &Patch,
372 domain: SurfaceParameterBox,
373 target: [Scalar; 3],
374) -> GeomResult<Candidate> {
375 let u = representative_parameter(domain.u);
376 let v = representative_parameter(domain.v);
377 let enclosed = patch.point_at(u, v)?.euclidean()?;
378 let upper = distance_to_point_interval_upper(target, enclosed, DIMENSIONS)?;
379 let point = bspline_jet(surface, u, v)?.point;
380 if !point.is_finite() {
381 return Err(GeomError::Degenerate(
382 "surface projection scalar representative is non-finite".to_owned(),
383 ));
384 }
385 let distance = representative_distance(point.to_array(), target, DIMENSIONS)?;
386 Ok(Candidate {
387 u,
388 v,
389 point,
390 distance,
391 upper,
392 })
393}
394
395fn representative_parameter(interval: ParameterInterval) -> Scalar {
396 let middle = interval.start * 0.5 + interval.end * 0.5;
397 if middle > interval.start && middle < interval.end {
398 middle
399 } else {
400 interval.start
401 }
402}
403
404fn update_candidate(current: &mut Option<Candidate>, next: Candidate) {
405 let replace = current.is_none_or(|best| candidate_order(next, best) == Ordering::Less);
406 if replace {
407 *current = Some(next);
408 }
409}
410
411fn update_resolved_candidate(current: &mut Candidate, next: Candidate) {
412 if candidate_order(next, *current) == Ordering::Less {
413 *current = next;
414 }
415}
416
417fn candidate_order(first: Candidate, second: Candidate) -> Ordering {
418 first
419 .upper
420 .total_cmp(&second.upper)
421 .then_with(|| first.u.total_cmp(&second.u))
422 .then_with(|| first.v.total_cmp(&second.v))
423}
424
425fn make_pending(
426 patches: &[Patch],
427 patch_index: usize,
428 domain: SurfaceParameterBox,
429 depth: u16,
430 serial: u32,
431 target: [Scalar; 3],
432) -> GeomResult<Pending> {
433 let patch = patches.get(patch_index).ok_or_else(|| {
434 GeomError::Degenerate("surface projection patch index escaped its catalog".to_owned())
435 })?;
436 Ok(Pending {
437 patch_index,
438 domain,
439 lower: restricted_lower(patch, domain, target)?,
440 depth,
441 serial,
442 })
443}
444
445fn select_refinable(
446 pending: &[Pending],
447 max_depth: u16,
448 improve_gap: bool,
449) -> GeomResult<Option<usize>> {
450 let mut selected = None;
451 for (index, record) in pending.iter().enumerate() {
452 if record.depth >= max_depth || split_choice(record.domain)?.is_none() {
453 continue;
454 }
455 selected = match selected {
456 None => Some(index),
457 Some(before) => {
458 let ordering = if improve_gap {
459 lower_order(record, &pending[before])?
460 } else {
461 widest_order(record, &pending[before])?
462 };
463 Some(if ordering == Ordering::Less {
464 index
465 } else {
466 before
467 })
468 }
469 };
470 }
471 Ok(selected)
472}
473
474fn lower_order(first: &Pending, second: &Pending) -> GeomResult<Ordering> {
475 Ok(first
476 .lower
477 .total_cmp(&second.lower)
478 .then_with(|| first.depth.cmp(&second.depth))
479 .then_with(|| first.patch_index.cmp(&second.patch_index))
480 .then_with(|| first.domain.u.start.total_cmp(&second.domain.u.start))
481 .then_with(|| first.domain.v.start.total_cmp(&second.domain.v.start))
482 .then_with(|| first.serial.cmp(&second.serial)))
483}
484
485fn widest_order(first: &Pending, second: &Pending) -> GeomResult<Ordering> {
486 let first_width = interval_width(first.domain.u)?.max(interval_width(first.domain.v)?);
487 let second_width = interval_width(second.domain.u)?.max(interval_width(second.domain.v)?);
488 Ok(second_width
489 .total_cmp(&first_width)
490 .then_with(|| first.patch_index.cmp(&second.patch_index))
491 .then_with(|| first.domain.u.start.total_cmp(&second.domain.u.start))
492 .then_with(|| first.domain.v.start.total_cmp(&second.domain.v.start))
493 .then_with(|| first.serial.cmp(&second.serial)))
494}
495
496fn split_choice(domain: SurfaceParameterBox) -> GeomResult<Option<(SplitAxis, Scalar)>> {
497 let u_width = interval_width(domain.u)?;
498 let v_width = interval_width(domain.v)?;
499 let u_midpoint = advancing_midpoint(domain.u);
500 let v_midpoint = advancing_midpoint(domain.v);
501 Ok(match (u_midpoint, v_midpoint) {
502 (Some(u), Some(_)) if u_width >= v_width => Some((SplitAxis::U, u)),
503 (Some(_), Some(v)) => Some((SplitAxis::V, v)),
504 (Some(u), None) => Some((SplitAxis::U, u)),
505 (None, Some(v)) => Some((SplitAxis::V, v)),
506 (None, None) => None,
507 })
508}
509
510fn advancing_midpoint(interval: ParameterInterval) -> Option<Scalar> {
511 let middle = interval.start * 0.5 + interval.end * 0.5;
512 (middle > interval.start && middle < interval.end).then_some(middle)
513}
514
515fn split_domain(
516 domain: SurfaceParameterBox,
517 axis: SplitAxis,
518 midpoint: Scalar,
519) -> (SurfaceParameterBox, SurfaceParameterBox) {
520 match axis {
521 SplitAxis::U => (
522 SurfaceParameterBox {
523 u: ParameterInterval {
524 start: domain.u.start,
525 end: midpoint,
526 },
527 v: domain.v,
528 },
529 SurfaceParameterBox {
530 u: ParameterInterval {
531 start: midpoint,
532 end: domain.u.end,
533 },
534 v: domain.v,
535 },
536 ),
537 SplitAxis::V => (
538 SurfaceParameterBox {
539 u: domain.u,
540 v: ParameterInterval {
541 start: domain.v.start,
542 end: midpoint,
543 },
544 },
545 SurfaceParameterBox {
546 u: domain.u,
547 v: ParameterInterval {
548 start: midpoint,
549 end: domain.v.end,
550 },
551 },
552 ),
553 }
554}
555
556fn interval_width(interval: ParameterInterval) -> GeomResult<Scalar> {
557 let width = interval.end - interval.start;
558 if width.is_finite() && width >= 0.0 {
559 Ok(width)
560 } else {
561 Err(GeomError::Degenerate(
562 "surface projection native parameter width is non-finite".to_owned(),
563 ))
564 }
565}
566
567fn global_lower(pending: &[Pending]) -> GeomResult<Scalar> {
568 let lower = pending
569 .iter()
570 .map(|record| record.lower)
571 .fold(Scalar::INFINITY, Scalar::min);
572 if lower.is_finite() {
573 Ok(lower)
574 } else {
575 Err(GeomError::Degenerate(
576 "surface projection global lower bound is non-finite".to_owned(),
577 ))
578 }
579}
580
581fn can_contain_global_minimizer(lower: Scalar, attained_upper: Scalar) -> bool {
582 lower <= attained_upper
583}
584
585fn certified_gap(upper: Scalar, lower: Scalar) -> GeomResult<Scalar> {
586 if lower > upper {
587 return Err(GeomError::Degenerate(
588 "surface projection lower bound exceeds its attained upper bound".to_owned(),
589 ));
590 }
591 let gap = next_up((upper - lower).max(0.0));
592 if gap.is_finite() {
593 Ok(gap)
594 } else {
595 Err(GeomError::Degenerate(
596 "surface projection distance gap is non-finite".to_owned(),
597 ))
598 }
599}
600
601fn take_sorted_boxes(pending: Vec<Pending>) -> GeomResult<Vec<SurfaceParameterBox>> {
602 let mut boxes = Vec::new();
603 boxes
604 .try_reserve_exact(pending.len())
605 .map_err(|_| allocation_error())?;
606 for record in pending {
607 boxes.push(record.domain);
608 }
609 boxes.sort_by(|first, second| {
610 first
611 .u
612 .start
613 .total_cmp(&second.u.start)
614 .then_with(|| first.v.start.total_cmp(&second.v.start))
615 .then_with(|| first.u.end.total_cmp(&second.u.end))
616 .then_with(|| first.v.end.total_cmp(&second.v.end))
617 });
618 Ok(boxes)
619}
620
621fn certificate(
622 candidate: Candidate,
623 lower: Scalar,
624 boxes: Vec<SurfaceParameterBox>,
625 visited_nodes: u32,
626) -> SurfaceProjectionCertificate3 {
627 SurfaceProjectionCertificate3 {
628 u: candidate.u,
629 v: candidate.v,
630 point: candidate.point,
631 distance: candidate.distance,
632 distance_lower_bound: lower,
633 distance_upper_bound: candidate.upper,
634 possible_minimizer_boxes: boxes,
635 visited_nodes,
636 }
637}
638
639fn try_push<T>(values: &mut Vec<T>, value: T) -> GeomResult<()> {
640 values.try_reserve(1).map_err(|_| allocation_error())?;
641 values.push(value);
642 Ok(())
643}
644
645fn allocation_error() -> GeomError {
646 GeomError::BudgetExceeded {
647 resource: "certified surface projection allocation",
648 }
649}
650
651fn search_overflow() -> GeomError {
652 GeomError::BudgetExceeded {
653 resource: "certified surface projection search nodes",
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::can_contain_global_minimizer;
660
661 #[test]
662 fn equality_is_retained() {
663 assert!(can_contain_global_minimizer(1.0, 1.0));
664 }
665}