Skip to main content

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}