geop_core_geometry/nurb_surface/project.rs
1use geop_core_math::{
2 geop_error::GeopResult,
3 scalars::Scalar,
4 solvers::{
5 iterate::{End, Repeat, Step, iterate},
6 linear::solve_symmetric2,
7 },
8 vector::Vector3,
9};
10
11use super::NurbSurface;
12use crate::{contains::surface::surface_could_contain, search::PROJECT_ITERATIONS};
13
14/// How often [`NurbSurface::enclose_foot`] grows its box before it gives
15/// up: how hard it tries, not what a box it proves means.
16const TRIES: usize = 6;
17
18/// The foot point of a moving point (see [`NurbSurface::foot_moving`]):
19/// its `(u, v)`, the point, its velocity, the surface's unit normal there
20/// and how fast that turns — each over the box the foot was proven in.
21#[derive(Clone, Debug)]
22pub struct MovingFoot {
23 pub at: (Scalar, Scalar),
24 pub point: Vector3,
25 pub velocity: Vector3,
26 pub normal: Vector3,
27 pub turning: Vector3,
28}
29
30impl NurbSurface<4> {
31 /// Fixed-iteration-count Newton foot-point projection of `target` onto
32 /// this surface, starting from `(u0, v0)`.
33 ///
34 /// Each step is Newton's on the squared distance: it solves the 2x2
35 /// system `H·Δ = r` where `r = (residual·Su, residual·Sv)` and `H` its
36 /// Hessian, `[[Su·Su − residual·Suu, Su·Sv − residual·Suv], [·, Sv·Sv −
37 /// residual·Svv]]`, then clamps `(u, v)` into `domain_u()` /
38 /// `domain_v()` before the next iteration. Where `H` is not definitely
39 /// positive definite — far from the foot point, or by a pole — the step
40 /// is Gauss-Newton's, `H` without the second derivatives (the first
41 /// fundamental form). Without them always, the step over a curved
42 /// surface overshoots by the target's distance over the surface's
43 /// radius of curvature: a target twice the radius off a cylinder, a
44 /// ball rounding a fill of its own radius, swung from side to side of
45 /// its foot point for ever. Runs `iterations` times with no
46 /// convergence tolerance — interval scalars can't judge "close enough" —
47 /// but stops early, with the identical result, once the sharpened
48 /// iterate repeats one before it exactly — a fixed point, or a cycle in
49 /// the last bit ([`iterate`]).
50 ///
51 /// `J` is singular exactly where the surface's own parametrization is —
52 /// a coordinate-singular pole (e.g. the apex of a `revolve`d disk cap,
53 /// where every `v` collapses to one point and `Sv = 0`). There only the
54 /// collapsed parameter stops moving the point; the other still does, so
55 /// that iteration takes the Newton step along it alone — down the
56 /// meridian, off the pole, after which the full step takes over. A seed
57 /// on the pole would otherwise never leave it, whatever the target. For
58 /// a target at the pole that step is zero, and `(u, v)` stays. Where
59 /// neither derivative is definitely nonzero, no step is taken, and
60 /// `(u, v)` is the seed the previous iteration left.
61 pub fn project(
62 &self,
63 target: Vector3,
64 u0: Scalar,
65 v0: Scalar,
66 iterations: usize,
67 ) -> GeopResult<(Scalar, Scalar)> {
68 let (u_lo, u_hi) = self.domain_u();
69 let (v_lo, v_hi) = self.domain_v();
70 let seed = (u0.clamped(u_lo, u_hi), v0.clamped(v_lo, v_hi));
71 // Sharpened every iteration but the last: Newton is iterative
72 // *refinement* of a foot point this routine gets to choose, not
73 // propagation of a measured uncertainty, so any single value inside
74 // the current iterate is an equally valid starting point for the
75 // next step (see `Scalar::sharpen`). Left unsharpened, each step's
76 // `du`/`dv` widens `(u, v)` further; `clamped` cannot pull that
77 // back, since an interval merely *straddling* a domain bound is
78 // neither definitely inside nor definitely outside it. The widened
79 // parameter then reaches `evaluate`, where a wide `t` makes the de
80 // Boor weight straddle zero and the whole evaluation fail — even
81 // though the seed was sharp and well inside the domain. Sharpen
82 // *before* clamping, not after: on a sharp value `clamped`'s
83 // three-valued comparisons are exact, so the result is guaranteed
84 // inside `[lo, hi]`.
85 //
86 // The *last* iteration's answer is not sharpened: it is not a seed
87 // for anything. `du`/`dv` inherit the width of `target` (through
88 // `r`), so that final width is the honest statement of how
89 // precisely an uncertain target pins down a foot point. Sharpening
90 // it away would return a single sharp `(u, v)` for a target that
91 // only ever determined a range of them.
92 //
93 // A seed the sharpened iteration has started from before ends it:
94 // most projections from a nearby seed end in a fixed point or a
95 // cycle in the last bit, and would run out all their iterations
96 // ([`Repeat::Cycle`]).
97 let clamp = |(u, v): (Scalar, Scalar)| (u.clamped(u_lo, u_hi), v.clamped(v_lo, v_hi));
98 let iterated = iterate(
99 seed,
100 seed,
101 iterations,
102 Repeat::Cycle,
103 |&(u, v)| (clamp((u.sharpen(), v.sharpen())), clamp((u, v))),
104 |&(u, v)| -> GeopResult<Step<(Scalar, Scalar)>> {
105 let p = self.evaluate(u, v)?;
106 let (su, sv) = self.derivatives(u, v)?;
107 let r = target.sub(&p);
108
109 let a11 = su.prod_dot(&su);
110 let a12 = su.prod_dot(&sv);
111 let a22 = sv.prod_dot(&sv);
112 let b1 = su.prod_dot(&r);
113 let b2 = sv.prod_dot(&r);
114
115 // Newton's step where the Hessian is positive definite.
116 let [suu, suv, svv] = self.second_derivatives(u, v)?;
117 let (h11, h12, h22) = (
118 a11.sub(r.prod_dot(&suu)),
119 a12.sub(r.prod_dot(&suv)),
120 a22.sub(r.prod_dot(&svv)),
121 );
122 let definite = h11.definitely_greater(Scalar::ZERO)
123 && h11
124 .mul(h22)
125 .sub(h12.mul(h12))
126 .definitely_greater(Scalar::ZERO);
127 if definite && let Ok((du, dv)) = solve_symmetric2((h11, h12, h22), (b1, b2)) {
128 return Ok(Step::Next((u.add(du), v.add(dv))));
129 }
130
131 let (du, dv) = match solve_symmetric2((a11, a12, a22), (b1, b2)) {
132 Ok(step) => step,
133 // Singular: along the one derivative that moves the point.
134 _ if a22.definitely_greater(Scalar::ZERO) => {
135 match self.off_pole(target, &p, (v, v_lo, v_hi), (u, u_lo, u_hi), false)? {
136 Some((along, d)) => (along.sub(u), d),
137 None => return Ok(Step::Stuck),
138 }
139 }
140 _ if a11.definitely_greater(Scalar::ZERO) => {
141 match self.off_pole(target, &p, (u, u_lo, u_hi), (v, v_lo, v_hi), true)? {
142 Some((along, d)) => (d, along.sub(v)),
143 None => return Ok(Step::Stuck),
144 }
145 }
146 _ => return Ok(Step::Stuck),
147 };
148 Ok(Step::Next((u.add(du), v.add(dv))))
149 },
150 )?;
151 // A step that cannot be taken cannot be taken from the same seed
152 // later either: the seed is where the iteration stays.
153 Ok(match iterated.end {
154 End::Stuck => iterated.seed,
155 End::Done | End::Exhausted => iterated.answer,
156 })
157 }
158
159 /// Where `point`, a point of this surface, lies on it: its `(u, v)`, or
160 /// `None` if it is not on the surface.
161 ///
162 /// Subdivide to isolate, then Newton to refine (see `AGENTS.md`): the
163 /// containment search ([`surface_could_contain`], with `max_nodes` and
164 /// `min_subdivision_size` bounding its effort) proves the point lies in
165 /// a box only `min_subdivision_size` narrow, and [`Self::project`] from
166 /// the box's midpoint polishes that to what the point's own width
167 /// allows. Both enclose the same foot point, so the answer is their
168 /// intersection. The refinement only tightens: where Newton fails or
169 /// leaves the box, the box is the answer.
170 ///
171 /// A point that could be a pole lies on the row collapsing to it (see
172 /// AGENTS.md, "A pole is one vertex at many `(u, v)`"), whatever Newton
173 /// settled on beside it — where the Jacobian is singular, it stops
174 /// where it can step no further, a hair off the row: the row's
175 /// parameter is kept as one of the point's too.
176 pub fn locate(
177 &self,
178 point: &Vector3,
179 max_nodes: usize,
180 min_subdivision_size: Scalar,
181 ) -> GeopResult<Option<(Scalar, Scalar)>> {
182 let Some((u, v)) = surface_could_contain(self, point, max_nodes, min_subdivision_size)?
183 else {
184 return Ok(None);
185 };
186 let (mut lu, mut lv) =
187 match self.project(*point, u.sharpen(), v.sharpen(), PROJECT_ITERATIONS) {
188 Ok((ru, rv)) if ru.could_be_equal(u) && rv.could_be_equal(v) => {
189 (u.intersect(ru), v.intersect(rv))
190 }
191 _ => (u, v),
192 };
193 let ((u0, u1), (v0, v1)) = (self.domain_u(), self.domain_v());
194 for (u_fixed, first) in self.rows_touching(u, v) {
195 let Some((pole, _)) = self.pole(u_fixed, first) else {
196 continue;
197 };
198 if !point.could_be_equal(&pole) {
199 continue;
200 }
201 match (u_fixed, first) {
202 (true, true) => lu = lu.union(u0),
203 (true, false) => lu = lu.union(u1),
204 (false, true) => lv = lv.union(v0),
205 (false, false) => lv = lv.union(v1),
206 }
207 }
208 Ok(Some((lu, lv)))
209 }
210
211 /// The `(u, v)` of the foot point of every point of the box `target`
212 /// near `near`, proven: a box in which each of those points has one
213 /// foot point and no other, `None` where that cannot be shown.
214 ///
215 /// A foot point is where the gradient of half the squared distance,
216 /// `g = (Su·(S − P), Sv·(S − P))`, vanishes. Krawczyk's operator
217 /// `K(X) = c − Y g(c) + (I − Y J(X)) (X − c)`, with `J` the Hessian of
218 /// the distance over the box `X` (and every `P` of `target`), `c` its
219 /// middle and `Y` the inverse of the Hessian at `c` — a preconditioner,
220 /// a free choice — holds every zero of `g` in `X`; where it lies within
221 /// `X`, there is exactly one, and it is in `K(X)` too. `X` starts as
222 /// twice the Newton step from `near` and grows where `K` reaches past
223 /// it, a few times: how hard the search tries. It must keep within the
224 /// domain, so a target whose foot is on the domain's edge needs the
225 /// surface carried on past it ([`Self::extended`]).
226 ///
227 /// Over a box of targets along a curve on the surface, the box found
228 /// holds the curve's `(u, v)` over that stretch: what the normal and
229 /// its turn are enclosed over (see [`Self::normal_turning`]).
230 pub fn enclose_foot(
231 &self,
232 target: &Vector3,
233 near: (Scalar, Scalar),
234 ) -> GeopResult<Option<(Scalar, Scalar)>> {
235 // The gradient over `(u, v)` and its Hessian, both over the target.
236 let at = |u: Scalar, v: Scalar| -> GeopResult<([Scalar; 2], [Scalar; 3])> {
237 let s = self.partials(u, v, 2)?;
238 let r = s[0][0].sub(target);
239 let (su, sv) = (s[1][0], s[0][1]);
240 Ok((
241 [su.prod_dot(&r), sv.prod_dot(&r)],
242 [
243 su.prod_dot(&su).add(s[2][0].prod_dot(&r)),
244 su.prod_dot(&sv).add(s[1][1].prod_dot(&r)),
245 sv.prod_dot(&sv).add(s[0][2].prod_dot(&r)),
246 ],
247 ))
248 };
249 // About `near` first, and where that is too far for the Hessian to
250 // stay near its value there, about the foot of the target's middle,
251 // Newton's from `near`: a free choice of where the box is, which
252 // the operator then proves or not.
253 if let Some(feet) = self.krawczyk_foot(near, &at)? {
254 return Ok(Some(feet));
255 }
256 let middle = Vector3::from_array(target.to_array().map(Scalar::midpoint));
257 let (u, v) = self.project(
258 middle,
259 near.0.midpoint(),
260 near.1.midpoint(),
261 PROJECT_ITERATIONS,
262 )?;
263 self.krawczyk_foot((u, v), &at)
264 }
265
266 /// The foot point of a point moving through the box `target` at
267 /// `velocity`, near `near`, proven as [`Self::enclose_foot`] proves it,
268 /// and how it moves: `None` where that cannot be shown.
269 ///
270 /// The foot keeps the gradient `g` of half the squared distance zero,
271 /// so `H (u', v') = (Su·P', Sv·P')` for the Hessian `H` of that
272 /// distance: the foot moves at `Su u' + Sv v'`, and the normal there
273 /// turns as [`Self::normal_turning`] says for that motion. Over the box
274 /// of feet, every one an enclosure over the whole box of targets.
275 pub fn foot_moving(
276 &self,
277 target: &Vector3,
278 velocity: &Vector3,
279 near: (Scalar, Scalar),
280 ) -> GeopResult<Option<MovingFoot>> {
281 let Some((u, v)) = self.enclose_foot(target, near)? else {
282 return Ok(None);
283 };
284 let s = self.partials(u, v, 2)?;
285 let r = s[0][0].sub(target);
286 let (su, sv) = (s[1][0], s[0][1]);
287 let hessian = (
288 su.prod_dot(&su).add(s[2][0].prod_dot(&r)),
289 su.prod_dot(&sv).add(s[1][1].prod_dot(&r)),
290 sv.prod_dot(&sv).add(s[0][2].prod_dot(&r)),
291 );
292 let Ok((du, dv)) =
293 solve_symmetric2(hessian, (su.prod_dot(velocity), sv.prod_dot(velocity)))
294 else {
295 return Ok(None);
296 };
297 let moving = su.prod_scalar(du).add(&sv.prod_scalar(dv));
298 let (normal, turning) = self.normal_turning(u, v, &moving)?;
299 Ok(Some(MovingFoot {
300 at: (u, v),
301 point: s[0][0],
302 velocity: moving,
303 normal,
304 turning,
305 }))
306 }
307
308 /// [`Self::enclose_foot`]'s operator, about `near`, the gradient and its
309 /// Hessian over a box `at` gives.
310 #[allow(clippy::type_complexity)]
311 fn krawczyk_foot(
312 &self,
313 near: (Scalar, Scalar),
314 at: &dyn Fn(Scalar, Scalar) -> GeopResult<([Scalar; 2], [Scalar; 3])>,
315 ) -> GeopResult<Option<(Scalar, Scalar)>> {
316 let ((u_lo, u_hi), (v_lo, v_hi)) = (self.domain_u(), self.domain_v());
317 let mut c = (near.0.midpoint(), near.1.midpoint());
318 let (mut gc, h) = at(c.0, c.1)?;
319 let [a, b, d] = h.map(|x| x.to_f64());
320 let det = a * d - b * b;
321 if !det.is_finite() || det == 0.0 {
322 return Ok(None);
323 }
324 // The preconditioner, sharp: `Y = H(c)⁻¹`.
325 let y = [[d / det, -b / det], [-b / det, a / det]].map(|row| row.map(Scalar::from_f64));
326 let times = |y: &[[Scalar; 2]; 2], g: [Scalar; 2]| {
327 [
328 y[0][0].mul(g[0]).add(y[0][1].mul(g[1])),
329 y[1][0].mul(g[0]).add(y[1][1].mul(g[1])),
330 ]
331 };
332 let step = times(&y, gc);
333 let mut radius = step.map(|s| 2.0 * s.abs().upper().to_f64());
334 for r in &mut radius {
335 *r = r.max(f64::EPSILON);
336 }
337 for _ in 0..TRIES {
338 let x = (
339 Scalar::new(c.0.to_f64() - radius[0], c.0.to_f64() + radius[0]),
340 Scalar::new(c.1.to_f64() - radius[1], c.1.to_f64() + radius[1]),
341 );
342 if x.0.could_be_less(u_lo) || x.0.could_be_greater(u_hi) {
343 return Ok(None);
344 }
345 if x.1.could_be_less(v_lo) || x.1.could_be_greater(v_hi) {
346 return Ok(None);
347 }
348 let newton = times(&y, gc);
349 let (_, [h11, h12, h22]) = at(x.0, x.1)?;
350 // `I − Y J(X)`.
351 let m = [
352 [
353 Scalar::ONE.sub(y[0][0].mul(h11).add(y[0][1].mul(h12))),
354 y[0][0].mul(h12).add(y[0][1].mul(h22)).neg(),
355 ],
356 [
357 y[1][0].mul(h11).add(y[1][1].mul(h12)).neg(),
358 Scalar::ONE.sub(y[1][0].mul(h12).add(y[1][1].mul(h22))),
359 ],
360 ];
361 let off = times(&m, [x.0.sub(c.0), x.1.sub(c.1)]);
362 let k = (
363 c.0.sub(newton[0]).add(off[0]),
364 c.1.sub(newton[1]).add(off[1]),
365 );
366 let inside = |k: Scalar, x: Scalar| {
367 k.lower().definitely_greater(x.lower()) && k.upper().definitely_less(x.upper())
368 };
369 if inside(k.0, x.0) && inside(k.1, x.1) {
370 return Ok(Some((k.0.intersect(x.0), k.1.intersect(x.1))));
371 }
372 // Wider about the middle of what the operator gave.
373 c = (k.0.midpoint(), k.1.midpoint());
374 gc = at(c.0, c.1)?.0;
375 radius = [
376 4.0 * radius[0].max(k.0.width().to_f64()),
377 4.0 * radius[1].max(k.1.width().to_f64()),
378 ];
379 }
380 Ok(None)
381 }
382
383 /// The foot point of `target` as [`Self::project`] finds it, within the
384 /// surface's domain: the foot point lies on the patch, so its `(u, v)`
385 /// are in the domain as much as where the projection says, and both
386 /// enclosures hold at once.
387 pub fn project_in_domain(
388 &self,
389 target: Vector3,
390 u0: Scalar,
391 v0: Scalar,
392 iterations: usize,
393 ) -> GeopResult<(Scalar, Scalar)> {
394 let (u, v) = self.project(target, u0, v0, iterations)?;
395 let ((u_lo, u_hi), (v_lo, v_hi)) = (self.domain_u(), self.domain_v());
396 Ok((u.intersect(u_lo.union(u_hi)), v.intersect(v_lo.union(v_hi))))
397 }
398
399 /// The step along a meridian at `p`, where the Jacobian is singular,
400 /// towards `target`: at a pole, or a hair off one, where the derivative
401 /// along the collapsed row is below what the arithmetic resolves. The
402 /// collapsed parameter does not move the point there, so which meridian
403 /// to go along is a free choice — `collapsed` is its value now and its
404 /// domain; `fixed`, the other parameter's value and domain, is where
405 /// the pole is. Of the meridians at the collapsed domain's ends, its
406 /// middle and where it is now, the one the Newton step along goes
407 /// furthest towards the target on: `(meridian, step)`, or `None` if
408 /// none moves. `u_collapsed` says the collapsed parameter is `v`.
409 ///
410 /// The step is kept within the domain, as every step is: a target whose
411 /// foot point is the pole itself — behind the patch, on the far side of
412 /// the pole from it — is reached by a step onto the pole, exactly.
413 /// Refusing that step left the foot point creeping towards the pole and
414 /// stopping a hair off it, where the surface's normal, `S_u × S_v` with
415 /// `S_u` of the order of that hair, is lost in rounding; the pole itself
416 /// has a normal (`pole_normal`). From the pole, a step onto it again is
417 /// no step, and a meridian leading away from the target is not taken.
418 fn off_pole(
419 &self,
420 target: Vector3,
421 p: &Vector3,
422 (fixed, fixed_lo, fixed_hi): (Scalar, Scalar, Scalar),
423 (now, lo, hi): (Scalar, Scalar, Scalar),
424 u_collapsed: bool,
425 ) -> GeopResult<Option<(Scalar, Scalar)>> {
426 let r = target.sub(p);
427 let middle = Scalar::interpolate(lo, hi, Scalar::from_f64(0.5));
428 let mut best: Option<(Scalar, Scalar, f64)> = None;
429 for along in [now, lo, middle, hi] {
430 let (su, sv) = if u_collapsed {
431 self.derivatives(fixed, along)?
432 } else {
433 self.derivatives(along, fixed)?
434 };
435 let meridian = if u_collapsed { su } else { sv };
436 let toward = meridian.prod_dot(&r);
437 let Ok(step) = toward.div(meridian.prod_dot(&meridian)) else {
438 continue;
439 };
440 let to = fixed.add(step).clamped(fixed_lo, fixed_hi);
441 let step = to.sub(fixed);
442 if step.could_be_equal(Scalar::ZERO) {
443 continue;
444 }
445 // How far towards the target, to first order: `toward * step`.
446 let progress = toward.mul(step).midpoint().to_f64();
447 if best.as_ref().is_none_or(|(_, _, b)| progress > *b) {
448 best = Some((along, step, progress));
449 }
450 }
451 Ok(best.map(|(along, step, _)| (along, step)))
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use crate::nurb_surface::NurbSurface;
458 use crate::search::PROJECT_ITERATIONS;
459 use crate::test_fixtures::{hom, revolved};
460 use geop_core_math::{scalars::Scalar, vector::Vector3};
461
462 /// Flat unit patch in the xy-plane.
463 fn flat_xy() -> NurbSurface<4> {
464 let f = Scalar::from_f64;
465 NurbSurface::try_new(
466 1,
467 1,
468 vec![
469 hom(0., 0., 0., 1.),
470 hom(0., 1., 0., 1.),
471 hom(1., 0., 0., 1.),
472 hom(1., 1., 0., 1.),
473 ],
474 vec![f(0.), f(0.), f(1.), f(1.)],
475 vec![f(0.), f(0.), f(1.), f(1.)],
476 )
477 .unwrap()
478 }
479
480 #[test]
481 fn project_flat_surface_from_directly_above() {
482 let s = flat_xy();
483 let target = geop_core_math::vector::Vector3::from_array([
484 Scalar::from_f64(0.3),
485 Scalar::from_f64(0.7),
486 Scalar::from_f64(2.0),
487 ]);
488 let (u, v) = s
489 .project(target, Scalar::from_f64(0.5), Scalar::from_f64(0.5), 5)
490 .unwrap();
491 assert!(u.could_be_equal(Scalar::from_f64(0.3)));
492 assert!(v.could_be_equal(Scalar::from_f64(0.7)));
493 }
494
495 /// Non-planar bilinear "saddle" patch: corner heights 0,1,1,0 over
496 /// x,y ∈ [0,2] — same fixture used by `normal.rs`'s tests.
497 fn bent_surface() -> NurbSurface<4> {
498 let f = Scalar::from_f64;
499 NurbSurface::try_new(
500 1,
501 1,
502 vec![
503 hom(0., 0., 0., 1.),
504 hom(0., 2., 1., 1.),
505 hom(2., 0., 1., 1.),
506 hom(2., 2., 0., 1.),
507 ],
508 vec![f(0.), f(0.), f(1.), f(1.)],
509 vec![f(0.), f(0.), f(1.), f(1.)],
510 )
511 .unwrap()
512 }
513
514 /// A target behind a quarter disc's pole, on the far side of it from
515 /// the quarter: its foot point is the pole. From a seed a hair off the
516 /// pole, where the derivative along the collapsed row is lost in
517 /// rounding, the projection lands on the pole exactly — where the
518 /// surface has a normal.
519 #[test]
520 fn project_onto_the_pole_from_a_hair_off_it() {
521 let disc = revolved(0., 0.);
522 let f = Scalar::from_f64;
523 let target = Vector3::from_array([f(-1e-17), f(-8e-14), f(1.)]);
524 let (u, v) = disc
525 .project_in_domain(target, f(1.), f(2.7755575615628914e-17), PROJECT_ITERATIONS)
526 .unwrap();
527 assert!(v.could_be_equal(Scalar::ZERO), "{v:?}");
528 let normal = disc.normal(u, v).unwrap();
529 assert!(normal[2].abs().could_be_equal(Scalar::ONE), "{normal:?}");
530 }
531
532 #[test]
533 fn project_onto_bent_surface_converges() {
534 let s = bent_surface();
535 let expected_u = Scalar::from_f64(0.4);
536 let expected_v = Scalar::from_f64(0.6);
537 let on_surface = s.evaluate(expected_u, expected_v).unwrap();
538 let (u, v) = s
539 .project(on_surface, Scalar::from_f64(0.5), Scalar::from_f64(0.5), 5)
540 .unwrap();
541 let result = s.evaluate(u, v).unwrap();
542 assert!(result[0].could_be_equal(on_surface[0]));
543 assert!(result[1].could_be_equal(on_surface[1]));
544 assert!(result[2].could_be_equal(on_surface[2]));
545 }
546
547 /// The feet of a stretch of points on a cone, proven to lie in one box
548 /// that holds their `(u, v)`; of one point, a box as narrow as the
549 /// rounding; and of a point past the domain's edge, none.
550 #[test]
551 fn enclose_foot_holds_the_feet_of_a_box() {
552 let cone = revolved(1., 0.);
553 let v = Scalar::from_f64(0.5);
554 let stretch = Scalar::new(0.30, 0.32);
555 let points = cone.evaluate(stretch, v).unwrap();
556 let (u_box, v_box) = cone
557 .enclose_foot(&points, (Scalar::from_f64(0.31), v))
558 .unwrap()
559 .expect("proven");
560 assert!(
561 stretch.is_subset_of(u_box) && v.is_subset_of(v_box),
562 "{u_box:?} {v_box:?}"
563 );
564 assert!(u_box.width().to_f64() < 0.1 && v_box.width().to_f64() < 0.1);
565 let u = Scalar::from_f64(0.31);
566 let point = cone.evaluate(u, v).unwrap();
567 let (u_box, v_box) = cone
568 .enclose_foot(&point, (Scalar::from_f64(0.3), Scalar::from_f64(0.45)))
569 .unwrap()
570 .expect("proven");
571 assert!(
572 u.is_subset_of(u_box) && v.is_subset_of(v_box),
573 "{u_box:?} {v_box:?}"
574 );
575 assert!(u_box.width().to_f64() < 1e-12 && v_box.width().to_f64() < 1e-12);
576 // On the rim, the box round its foot would leave the domain.
577 let rim = cone.evaluate(u, Scalar::ONE).unwrap();
578 assert!(cone.enclose_foot(&rim, (u, Scalar::ONE)).unwrap().is_none());
579 }
580}