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