geop_core_geometry/nurb_surface/project.rs
1use geop_core_math::{geop_error::GeopResult, scalars::Scalar, vector::Vector3};
2
3use super::NurbSurface;
4
5/// Clamp `x` into `[lo, hi]` using three-valued comparisons.
6pub fn clamp<S: Scalar>(x: S, lo: S, hi: S) -> S {
7 if x.definitely_less(lo) {
8 lo
9 } else if x.definitely_greater(hi) {
10 hi
11 } else {
12 x
13 }
14}
15
16impl<S: Scalar> NurbSurface<S, 4> {
17 /// Fixed-iteration-count Newton foot-point projection of `target` onto
18 /// this surface, starting from `(u0, v0)`.
19 ///
20 /// Each step solves the exact 2x2 system `J·Δ = r` where
21 /// `r = (residual·Su, residual·Sv)` and `J = [[Su·Su, Su·Sv], [Su·Sv,
22 /// Sv·Sv]]` (the first fundamental form — exact for the degree-1
23 /// bilinear/low-degree patches used in this crate's tests, since second
24 /// derivatives are dropped), then clamps `(u, v)` into `domain_u()` /
25 /// `domain_v()` before the next iteration. Runs `iterations` times with no
26 /// convergence tolerance — interval scalars can't judge "close enough" —
27 /// but stops early, with the identical result, once the sharpened
28 /// iterate reaches an exact fixed point (see the loop).
29 ///
30 /// `J` is singular exactly where the surface's own parametrization is —
31 /// a coordinate-singular pole (e.g. the apex of a `revolve`d disk cap,
32 /// where every `v` collapses to one point and `Sv = 0`). Rather than
33 /// erroring there (`div` by an exactly- or interval-possibly-zero
34 /// determinant), that iteration's update is simply skipped, leaving
35 /// `(u, v)` exactly where the *previous*, non-singular iteration left
36 /// it. For a target genuinely at or very near such a pole, earlier
37 /// iterations still pull `(u, v)` right up to the singular edge_loop
38 /// before this kicks in, so the frozen result is still a meaningful
39 /// (if imprecise right at the pole) answer — good enough for a caller
40 /// to then recognize "this converged onto a known singular vertex" and
41 /// handle it explicitly, instead of the whole projection just failing.
42 pub fn project(
43 &self,
44 target: Vector3<S>,
45 u0: S,
46 v0: S,
47 iterations: usize,
48 ) -> GeopResult<(S, S)> {
49 let (u_lo, u_hi) = self.domain_u();
50 let (v_lo, v_hi) = self.domain_v();
51 let mut u = clamp(u0, u_lo, u_hi);
52 let mut v = clamp(v0, v_lo, v_hi);
53
54 for iteration in 0..iterations {
55 let p = self.evaluate(u, v)?;
56 let (su, sv) = self.derivatives(u, v)?;
57 let r = target.sub(&p);
58
59 let a11 = su.prod_dot(&su);
60 let a12 = su.prod_dot(&sv);
61 let a22 = sv.prod_dot(&sv);
62 let b1 = su.prod_dot(&r);
63 let b2 = sv.prod_dot(&r);
64
65 let det = a11.mul(a22).sub(a12.mul(a12));
66 let du = b1.mul(a22).sub(b2.mul(a12)).div(det);
67 let dv = a11.mul(b2).sub(a12.mul(b1)).div(det);
68 let (Ok(du), Ok(dv)) = (du, dv) else {
69 continue;
70 };
71
72 // Sharpened every iteration, not just at the end: Newton is
73 // iterative *refinement* of a foot point this routine gets to
74 // choose, not propagation of a measured uncertainty, so any
75 // single value inside the current iterate is an equally valid
76 // starting point for the next step (see `Scalar::sharpen`).
77 // Left unsharpened, each step's `du`/`dv` widens `(u, v)`
78 // further; `clamp` cannot pull that back, since an interval
79 // merely *straddling* a domain bound is neither definitely
80 // inside nor definitely outside it. The widened parameter then
81 // reaches `evaluate`, where a wide `t` makes the de Boor
82 // weight straddle zero and the whole evaluation fail — even
83 // though the seed was sharp and well inside the domain.
84 // Sharpen *before* clamping, not after: on a sharp value
85 // `clamp`'s three-valued comparisons are exact, so the result
86 // is guaranteed inside `[lo, hi]`. The other order can escape
87 // the domain again — collapsing an interval that straddles a
88 // bound to its midpoint can land just past that bound, and the
89 // next `evaluate` then rejects it as out of domain.
90 //
91 // The *last* iteration is the exception: its result is not a
92 // seed for anything, it is the answer this function returns.
93 // `du`/`dv` inherit the width of `target` (through `r`), so
94 // that final width is the honest statement of how precisely an
95 // uncertain target pins down a foot point. Sharpening it away
96 // would return a single sharp `(u, v)` for a target that only
97 // ever determined a range of them — an answer claiming more
98 // precision than the input carried, which then fails to agree
99 // with anything else derived from the same uncertain point.
100 let (next_u, next_v) = (u.add(du), v.add(dv));
101 if iteration + 1 == iterations {
102 return Ok((clamp(next_u, u_lo, u_hi), clamp(next_v, v_lo, v_hi)));
103 }
104 let (sharp_u, sharp_v) = (
105 clamp(next_u.sharpen(), u_lo, u_hi),
106 clamp(next_v.sharpen(), v_lo, v_hi),
107 );
108 // A fixed point of the sharpened iteration: every remaining
109 // iterate would reproduce `(u, v)` exactly (each is a
110 // deterministic function of the same sharp seed), and so would
111 // the final unsharpened step, which starts from it too. Taking
112 // that final step now returns the identical result sooner — not
113 // a convergence tolerance, since nothing is judged "close
114 // enough": the iterate is bit-for-bit unchanged.
115 let same = |a: S, b: S| a.is_subset_of(b) && b.is_subset_of(a);
116 if same(sharp_u, u) && same(sharp_v, v) {
117 return Ok((clamp(next_u, u_lo, u_hi), clamp(next_v, v_lo, v_hi)));
118 }
119 u = sharp_u;
120 v = sharp_v;
121 }
122
123 Ok((u, v))
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use crate::nurb_surface::NurbSurface;
130 use geop_core_math::for_all_scalars;
131 use geop_core_math::{scalars::Scalar, vector::Vector4};
132
133 fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
134 Vector4::from_array([
135 S::from_f64(x),
136 S::from_f64(y),
137 S::from_f64(z),
138 S::from_f64(w),
139 ])
140 }
141
142 /// Flat unit patch in the xy-plane.
143 fn flat_xy<S: Scalar>() -> NurbSurface<S, 4> {
144 let f = S::from_f64;
145 NurbSurface::try_new(
146 1,
147 1,
148 vec![
149 pt(0., 0., 0., 1.),
150 pt(0., 1., 0., 1.),
151 pt(1., 0., 0., 1.),
152 pt(1., 1., 0., 1.),
153 ],
154 vec![f(0.), f(0.), f(1.), f(1.)],
155 vec![f(0.), f(0.), f(1.), f(1.)],
156 )
157 .unwrap()
158 }
159
160 fn check_project_flat_surface_from_directly_above<S: Scalar>() {
161 let s = flat_xy::<S>();
162 let target = geop_core_math::vector::Vector3::from_array([
163 S::from_f64(0.3),
164 S::from_f64(0.7),
165 S::from_f64(2.0),
166 ]);
167 let (u, v) = s
168 .project(target, S::from_f64(0.5), S::from_f64(0.5), 5)
169 .unwrap();
170 assert!(u.could_be_equal(S::from_f64(0.3)));
171 assert!(v.could_be_equal(S::from_f64(0.7)));
172 }
173 #[test]
174 fn project_flat_surface_from_directly_above() {
175 for_all_scalars!(check_project_flat_surface_from_directly_above);
176 }
177
178 /// Non-planar bilinear "saddle" patch: corner heights 0,1,1,0 over
179 /// x,y ∈ [0,2] — same fixture used by `normal.rs`'s tests.
180 fn bent_surface<S: Scalar>() -> NurbSurface<S, 4> {
181 let f = S::from_f64;
182 NurbSurface::try_new(
183 1,
184 1,
185 vec![
186 pt(0., 0., 0., 1.),
187 pt(0., 2., 1., 1.),
188 pt(2., 0., 1., 1.),
189 pt(2., 2., 0., 1.),
190 ],
191 vec![f(0.), f(0.), f(1.), f(1.)],
192 vec![f(0.), f(0.), f(1.), f(1.)],
193 )
194 .unwrap()
195 }
196
197 fn check_project_onto_bent_surface_converges<S: Scalar>() {
198 let s = bent_surface::<S>();
199 let expected_u = S::from_f64(0.4);
200 let expected_v = S::from_f64(0.6);
201 let on_surface = s.evaluate(expected_u, expected_v).unwrap();
202 let (u, v) = s
203 .project(on_surface, S::from_f64(0.5), S::from_f64(0.5), 5)
204 .unwrap();
205 let result = s.evaluate(u, v).unwrap();
206 assert!(result[0].could_be_equal(on_surface[0]));
207 assert!(result[1].could_be_equal(on_surface[1]));
208 assert!(result[2].could_be_equal(on_surface[2]));
209 }
210 #[test]
211 fn project_onto_bent_surface_converges() {
212 for_all_scalars!(check_project_onto_bent_surface_converges);
213 }
214}