axiolid_nurbs/
surface_projection.rs1use crate::axis::active_spans;
4use crate::projection::{ProjectionOptions, ProjectionStatus, SurfaceProjection};
5use axiolid_contracts::{GeomError, GeomResult};
6use axiolid_core::{Point3, Scalar};
7use axiolid_evaluate::surface::bspline_jet;
8use axiolid_surface::BSplineSurface;
9
10pub fn project_surface(
15 surface: &BSplineSurface,
16 target: Point3,
17 options: ProjectionOptions,
18) -> GeomResult<SurfaceProjection> {
19 if !target.is_finite() {
20 return Err(GeomError::InvalidInput(
21 "projection target must be finite".to_owned(),
22 ));
23 }
24 bspline_jet(surface, 0.0, 0.0)?;
25 let us = active_spans(
26 &surface.u_knots,
27 &surface.u_multiplicities,
28 surface.u_degree,
29 surface.control_points.len(),
30 )?;
31 let vc = surface.control_points.first().map_or(0, Vec::len);
32 let vs = active_spans(
33 &surface.v_knots,
34 &surface.v_multiplicities,
35 surface.v_degree,
36 vc,
37 )?;
38 let bounds = (us[0].0, us[us.len() - 1].1, vs[0].0, vs[vs.len() - 1].1);
39 let mut best: Option<SurfaceProjection> = None;
40 let mut starts = 0_u32;
41 for &(ua, ub) in &us {
42 for &(va, vb) in &vs {
43 for iu in 0..=options.samples_per_span() {
44 for iv in 0..=options.samples_per_span() {
45 starts = starts.checked_add(1).ok_or(GeomError::BudgetExceeded {
46 resource: "projection starts",
47 })?;
48 if starts > options.max_starts() {
49 return Err(GeomError::BudgetExceeded {
50 resource: "projection starts",
51 });
52 }
53 let u = ua
54 + (ub - ua) * Scalar::from(iu) / Scalar::from(options.samples_per_span());
55 let v = va
56 + (vb - va) * Scalar::from(iv) / Scalar::from(options.samples_per_span());
57 let candidate = refine(surface, target, u, v, bounds, options)?;
58 if best
59 .as_ref()
60 .is_none_or(|current| candidate.distance < current.distance)
61 {
62 best = Some(candidate);
63 }
64 }
65 }
66 }
67 }
68 best.ok_or_else(|| GeomError::Degenerate("projection produced no candidate".to_owned()))
69}
70
71fn refine(
72 surface: &BSplineSurface,
73 target: Point3,
74 mut u: Scalar,
75 mut v: Scalar,
76 bounds: (Scalar, Scalar, Scalar, Scalar),
77 options: ProjectionOptions,
78) -> GeomResult<SurfaceProjection> {
79 let (ulo, uhi, vlo, vhi) = bounds;
80 let mut iterations = 0;
81 let mut status = ProjectionStatus::BudgetExhausted;
82 for iteration in 0..options.max_iterations() {
83 iterations = iteration + 1;
84 let j = bspline_jet(surface, u, v)?;
85 let r = j.point - target;
86 let gu = r.dot(j.du);
87 let gv = r.dot(j.dv);
88 let scale = j.du.length().max(j.dv.length()).max(1.0);
89 if gu.hypot(gv) <= options.tolerance().linear() * scale {
90 status = ProjectionStatus::Converged;
91 break;
92 }
93 let huu = j.du.dot(j.du) + r.dot(j.duu);
94 let huv = j.du.dot(j.dv) + r.dot(j.duv);
95 let hvv = j.dv.dot(j.dv) + r.dot(j.dvv);
96 let det = huu * hvv - huv * huv;
97 let hscale = huu.abs().max(huv.abs()).max(hvv.abs()).max(1.0);
98 if !det.is_finite() || det.abs() <= Scalar::EPSILON * hscale * hscale {
99 break;
100 }
101 let du = (-gu * hvv + huv * gv) / det;
102 let dv = (huv * gu - huu * gv) / det;
103 let next_u = (u + du).clamp(ulo, uhi);
104 let next_v = (v + dv).clamp(vlo, vhi);
105 let movement = (j.du * (next_u - u) + j.dv * (next_v - v)).length();
106 u = next_u;
107 v = next_v;
108 if movement <= options.tolerance().linear() {
109 status = ProjectionStatus::Converged;
110 break;
111 }
112 }
113 let point = bspline_jet(surface, u, v)?.point;
114 let distance = point.distance(target);
115 if !distance.is_finite() {
116 return Err(GeomError::Degenerate(
117 "projection distance is non-finite".to_owned(),
118 ));
119 }
120 Ok(SurfaceProjection {
121 u,
122 v,
123 point,
124 distance,
125 iterations,
126 on_boundary: boundary(u, v, bounds),
127 status,
128 })
129}
130
131fn boundary(u: Scalar, v: Scalar, bounds: (Scalar, Scalar, Scalar, Scalar)) -> bool {
132 u == bounds.0 || u == bounds.1 || v == bounds.2 || v == bounds.3
133}