Skip to main content

geop_core_geometry/nurb_surface/
patch.rs

1//! Surfaces spanned by curves: the ruled surface between two curves
2//! ([`NurbSurface3D::ruled`]), the Coons patch bounded by four
3//! ([`NurbSurface3D::coons`]), and such a patch made tangent to planes along
4//! its sides to the faces it continues ([`NurbSurface3D::tangent_to`]).
5//!
6//! Both constructions work on the homogeneous control points, so they are
7//! exact for rational curves too: a patch's side *is* the curve it was
8//! built from, control point for control point, not an approximation of it.
9
10use geop_core_math::{
11    geop_error::{GeopError, GeopResult},
12    scalars::Scalar,
13    solvers::{
14        linear::solve_pivoted,
15        repeat::{Step, repeat},
16    },
17    vector::{Vector, Vector3, Vector4},
18};
19
20use super::NurbSurface3D;
21use std::sync::Arc;
22
23use crate::{
24    nurb_curve::{NurbCurve, NurbCurve3D, basis_funs, bezier_knots},
25    search::{MAX_NODES, min_subdivision_size},
26    shape::Plane,
27    spline::{breakpoints, find_span},
28};
29
30/// The Greville abscissae of a B-spline of `degree` with `n` control points
31/// over `knots`: the coefficients with which its basis reproduces the
32/// linear function `t` — so a linear blend between two rows of control
33/// points, laid across that basis, is exactly the blend.
34fn greville(knots: &[Scalar], degree: usize, n: usize) -> GeopResult<Vec<Scalar>> {
35    let p = Scalar::from_i64(degree as i64);
36    (0..n)
37        .map(|i| {
38            knots[i + 1..=i + degree]
39                .iter()
40                .fold(Scalar::ZERO, |sum, &k| sum.add(k))
41                .div(p)
42        })
43        .collect()
44}
45
46/// `a` and `b` on the domain `[0, 1]`, made compatible (see
47/// [`NurbCurve::compatible`]).
48fn compatible_pair(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<(NurbCurve3D, NurbCurve3D)> {
49    let mut pair = NurbCurve::compatible(&[a.with_unit_domain()?, b.with_unit_domain()?])?;
50    let b = pair.pop().expect("two curves");
51    let a = pair.pop().expect("two curves");
52    Ok((a, b))
53}
54
55/// One side of a patch, as [`NurbSurface3D::coons`] numbers its curves:
56/// `v` at its lowest, `u` at its highest, `v` at its highest and `u` at its
57/// lowest, running round the patch.
58pub(super) const SIDES: usize = 4;
59
60impl NurbSurface3D {
61    /// The ruled surface between `a`, at `v = 0`, and `b`, at `v = 1`: the
62    /// straight lines joining their points of equal parameter, both brought
63    /// onto `[0, 1]`, which is the surface's `u`. Its sides along `u` are
64    /// the two curves exactly.
65    pub fn ruled(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<Self> {
66        let (a, b) = compatible_pair(a, b)?;
67        Self::ruled_compatible(&a, &b)
68    }
69
70    /// The ruled surface between `a`, at `v = 0`, and `b`, at `v = 1`, which
71    /// are already alike — of one degree, as many control points and knots —
72    /// taken as they are: its `u` runs over `a`'s knots, not brought onto
73    /// `[0, 1]`. Exact, since its sides along `u` are the curves' own control
74    /// points.
75    pub fn ruled_compatible(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<Self> {
76        if a.degree != b.degree
77            || a.control_points.len() != b.control_points.len()
78            || a.knot_vector.len() != b.knot_vector.len()
79        {
80            return Err(GeopError::new(format!(
81                "NurbSurface::ruled_compatible: the curves are not alike: {a:?} and {b:?}"
82            )));
83        }
84        let control_points = a
85            .control_points
86            .iter()
87            .zip(&b.control_points)
88            .flat_map(|(p, q)| [*p, *q])
89            .collect();
90        NurbSurface3D::try_new(
91            a.degree,
92            1,
93            control_points,
94            a.knot_vector.clone(),
95            bezier_knots(1),
96        )
97    }
98
99    /// The bilinear patch with corners `p00` at `(u, v) = (0, 0)`, `p10` at
100    /// `(1, 0)`, `p11` at `(1, 1)` and `p01` at `(0, 1)`, over `[0, 1]²` —
101    /// a plane's, where the four lie in one.
102    pub fn bilinear(p00: Vector3, p10: Vector3, p11: Vector3, p01: Vector3) -> GeopResult<Self> {
103        NurbSurface3D::try_new(
104            1,
105            1,
106            [p00, p01, p10, p11]
107                .map(|p| p.homogeneous(Scalar::ONE))
108                .to_vec(),
109            bezier_knots(1),
110            bezier_knots(1),
111        )
112    }
113
114    /// The bilinearly blended Coons patch bounded by `sides`, which run
115    /// round it end to end: from the corner at `(0, 0)` along `v = 0` to
116    /// `(1, 0)`, up `u = 1` to `(1, 1)`, back along `v = 1` to `(0, 1)` and
117    /// down `u = 0` to the start — counter-clockwise in `(u, v)`, so the
118    /// patch's normal is the one the loop winds around.
119    ///
120    /// The patch is the sum of the ruled surfaces between opposite sides,
121    /// less the bilinear patch of the corners, each laid on one control net
122    /// (the Greville abscissae turn a linear blend into control points
123    /// exactly): opposite sides are made compatible, and each blend is
124    /// taken of the homogeneous control points, so rational sides are fine.
125    /// Its border rows are the sides' control points themselves — that is
126    /// what the blend gives there exactly; taking them as they are only
127    /// leaves out the rounding of adding and taking away the same corners —
128    /// so the patch interpolates its sides exactly, and its corners are the
129    /// union of the two sides meeting there.
130    ///
131    /// The sides must end where the next one starts, with their end weights
132    /// one (or a single span, which can be brought to it): two sides meeting
133    /// at a corner must agree on its weight for the blend to meet them both.
134    pub fn coons(sides: [&NurbCurve3D; SIDES]) -> GeopResult<Self> {
135        let unit = |c: &NurbCurve3D| c.with_unit_domain()?.with_unit_end_weights();
136        let bottom = unit(sides[0])?;
137        let right = unit(sides[1])?;
138        let top = unit(&sides[2].reverse())?;
139        let left = unit(&sides[3].reverse())?;
140        let first = |c: &NurbCurve3D| c.start();
141        let last = |c: &NurbCurve3D| c.end();
142        for (k, (end, start)) in [
143            (last(&bottom)?, first(&right)?),
144            (last(&right)?, last(&top)?),
145            (first(&top)?, last(&left)?),
146            (first(&left)?, first(&bottom)?),
147        ]
148        .into_iter()
149        .enumerate()
150        {
151            if !end.could_be_equal(&start) {
152                return Err(GeopError::new(format!(
153                    "NurbSurface::coons: side {k} ends at {end:?}, but side {} starts at {start:?}",
154                    (k + 1) % SIDES
155                )));
156            }
157        }
158        let (bottom, top) = compatible_pair(&bottom, &top)?;
159        let (left, right) = compatible_pair(&left, &right)?;
160        let (nu, nv) = (bottom.control_points.len(), left.control_points.len());
161        let eta = greville(&bottom.knot_vector, bottom.degree, nu)?;
162        let xi = greville(&left.knot_vector, left.degree, nv)?;
163        let (b, t) = (&bottom.control_points, &top.control_points);
164        let (l, r) = (&left.control_points, &right.control_points);
165        let p00 = b[0].union(&l[0]);
166        let p10 = b[nu - 1].union(&r[0]);
167        let p01 = t[0].union(&l[nv - 1]);
168        let p11 = t[nu - 1].union(&r[nv - 1]);
169        let rational = [b, t, l, r]
170            .iter()
171            .any(|row| row.iter().any(|p| !p[3].is_exactly(Scalar::ONE)));
172
173        let mut control_points = Vec::with_capacity(nu * nv);
174        for i in 0..nu {
175            for j in 0..nv {
176                let mut p = match (i, j) {
177                    (0, 0) => p00,
178                    (0, _) if j == nv - 1 => p01,
179                    (_, 0) if i == nu - 1 => p10,
180                    _ if i == nu - 1 && j == nv - 1 => p11,
181                    (_, 0) => b[i],
182                    (0, _) => l[j],
183                    _ if j == nv - 1 => t[i],
184                    _ if i == nu - 1 => r[j],
185                    _ => {
186                        let across = Vector4::interpolate(&b[i], &t[i], xi[j]);
187                        let along = Vector4::interpolate(&l[j], &r[j], eta[i]);
188                        let corners = Vector4::interpolate(
189                            &Vector4::interpolate(&p00, &p01, xi[j]),
190                            &Vector4::interpolate(&p10, &p11, xi[j]),
191                            eta[i],
192                        );
193                        across.add(&along).sub(&corners)
194                    }
195                };
196                if !rational {
197                    // Every weight is one, and so is every blend of them:
198                    // the arithmetic above would only round it.
199                    p[3] = Scalar::ONE;
200                }
201                control_points.push(p);
202            }
203        }
204        NurbSurface3D::try_new(
205            bottom.degree,
206            left.degree,
207            control_points,
208            bottom.knot_vector.clone(),
209            left.knot_vector.clone(),
210        )
211    }
212
213    /// The same surface one degree higher in `u` (`along_u`) or in `v`, for
214    /// knots clamped at both ends: each row of control points along that
215    /// direction raised as a curve (see [`NurbCurve::elevate_degree`]).
216    pub fn elevate_degree(&self, along_u: bool) -> GeopResult<Self> {
217        let (nu, nv) = (self.num_u, self.num_v);
218        let (degree, knots, rows, len) = if along_u {
219            (self.degree_u, &self.knot_vector_u, nv, nu)
220        } else {
221            (self.degree_v, &self.knot_vector_v, nu, nv)
222        };
223        let index = |row: usize, k: usize| if along_u { k * nv + row } else { row * nv + k };
224        let raised = (0..rows)
225            .map(|row| {
226                let points = (0..len)
227                    .map(|k| self.control_points[index(row, k)])
228                    .collect();
229                NurbCurve::try_new(degree, points, knots.clone())?.elevate_degree()
230            })
231            .collect::<GeopResult<Vec<_>>>()?;
232        let new_len = raised[0].control_points.len();
233        let new_knots = raised[0].knot_vector.clone();
234        let (num_u, num_v) = if along_u {
235            (new_len, nv)
236        } else {
237            (nu, new_len)
238        };
239        let control_points = (0..num_u)
240            .flat_map(|i| (0..num_v).map(move |j| (i, j)))
241            .map(|(i, j)| {
242                if along_u {
243                    raised[j].control_points[i]
244                } else {
245                    raised[i].control_points[j]
246                }
247            })
248            .collect();
249        if along_u {
250            NurbSurface3D::try_new(
251                degree + 1,
252                self.degree_v,
253                control_points,
254                new_knots,
255                self.knot_vector_v.clone(),
256            )
257        } else {
258            NurbSurface3D::try_new(
259                self.degree_u,
260                degree + 1,
261                control_points,
262                self.knot_vector_u.clone(),
263                new_knots,
264            )
265        }
266    }
267
268    /// The same surface carried on by `by` of its `u` (`along_u`) or `v`
269    /// parameter past the end (`at_end`) or before the start of that
270    /// domain: each row of control points along that direction continued
271    /// as a curve (see [`NurbCurve::extended`]), so the surface goes on as
272    /// the polynomial it is there, as smooth as it was.
273    pub fn extended(&self, along_u: bool, at_end: bool, by: Scalar) -> GeopResult<Self> {
274        let (nu, nv) = (self.num_u, self.num_v);
275        let (degree, knots, rows, len) = if along_u {
276            (self.degree_u, &self.knot_vector_u, nv, nu)
277        } else {
278            (self.degree_v, &self.knot_vector_v, nu, nv)
279        };
280        let index = |row: usize, k: usize| if along_u { k * nv + row } else { row * nv + k };
281        let longer = (0..rows)
282            .map(|row| {
283                let points = (0..len)
284                    .map(|k| self.control_points[index(row, k)])
285                    .collect();
286                NurbCurve::try_new(degree, points, knots.clone())?.extended(at_end, by)
287            })
288            .collect::<GeopResult<Vec<_>>>()?;
289        let new_len = longer[0].control_points.len();
290        let new_knots = longer[0].knot_vector.clone();
291        let (num_u, num_v) = if along_u {
292            (new_len, nv)
293        } else {
294            (nu, new_len)
295        };
296        let control_points = (0..num_u)
297            .flat_map(|i| (0..num_v).map(move |j| (i, j)))
298            .map(|(i, j)| {
299                if along_u {
300                    longer[j].control_points[i]
301                } else {
302                    longer[i].control_points[j]
303                }
304            })
305            .collect();
306        if along_u {
307            NurbSurface3D::try_new(
308                degree,
309                self.degree_v,
310                control_points,
311                new_knots,
312                self.knot_vector_v.clone(),
313            )
314        } else {
315            NurbSurface3D::try_new(
316                self.degree_u,
317                degree,
318                control_points,
319                self.knot_vector_u.clone(),
320                new_knots,
321            )
322        }
323    }
324
325    /// The same patch made tangent along each side `k` (numbered as
326    /// [`NurbSurface3D::coons`] numbers them) that has a `sides[k]`, to the
327    /// face it names — a face ending at the side, the patch continuing it
328    /// smoothly.
329    ///
330    /// The patch is first raised in degree until each such side has a row
331    /// of its own next to it, not next to the opposite side. Each control
332    /// point of that row is then moved, along the face's normal, into the
333    /// face's tangent plane where the side is at that point's Greville
334    /// abscissa: the derivative across a side is a combination of that row
335    /// less the side, so a row in the tangent plane makes the patch tangent
336    /// there — along a curved face, that plane leant by [`TILT`] (see
337    /// below). Where two such rows cross, the point goes onto the line the
338    /// two planes meet in.
339    ///
340    /// Along a flat face the tangent plane is one plane, and the patch is
341    /// tangent to it exactly — rational or not. Along a curved one it
342    /// turns, and no NURBS row follows it exactly: the row is set so that
343    /// the patch leaves the face at [`TILT`] towards the face's normal at
344    /// each of those abscissae — where an approximate tangency would dip
345    /// through the face as often as not, a crease of a millionth of a
346    /// radian keeps the patch on one side of it — and proven over the whole
347    /// side to stay within twice that, on that side ([`NurbSurface3D::strays`]),
348    /// the patch refined along the side while it does not.
349    ///
350    /// The ends of that row are the second control points of the sides
351    /// meeting this one: they are curves the patch must keep, so they are
352    /// not moved, and must lie in the tangent plane at the corner — a side
353    /// leaving it there contradicts the tangency, and is refused. So is a
354    /// row that does not head away from the face: the patch would fold back
355    /// over the face it is to continue.
356    pub fn tangent_to(&self, sides: &[Option<Neighbour<'_>>; SIDES]) -> GeopResult<Self> {
357        let curved = |k: usize| sides[k].as_ref().is_some_and(Neighbour::tilted);
358        let mut base = self.clone();
359        let mut strays = String::new();
360        for _ in 0..=MOST_REFINEMENTS {
361            let patch = base.tangent_rows(sides, &|_, _| false)?;
362            match patch.misses(sides)?.into_iter().next() {
363                None => return Ok(patch),
364                Some(stray) => strays = stray.what,
365            }
366            let along = |even: bool| (0..SIDES).any(|k| curved(k) && (k % 2 == 0) == even);
367            base = base.refined([1 + usize::from(along(true)), 1 + usize::from(along(false))])?;
368        }
369        Err(GeopError::new(format!(
370            "NurbSurface::tangent_to: refined {MOST_REFINEMENTS} times along its curved neighbours, the patch still does not leave them at {TILT:e} radians: {strays}"
371        )))
372    }
373
374    /// The patch's rows next to the sides of `sides` set as
375    /// [`NurbSurface3D::tangent_to`] says, once, at its knots — but for the
376    /// control points `kept` names by `(i, j)`, which something else has
377    /// set (see [`NurbSurface3D::fill`]): they stay where they are, and the
378    /// rest of their row leans around them.
379    pub(super) fn tangent_rows(
380        &self,
381        sides: &[Option<Neighbour<'_>>; SIDES],
382        kept: &dyn Fn(usize, usize) -> bool,
383    ) -> GeopResult<Self> {
384        // Four rows across, wherever a side is made tangent: the row moved
385        // is then next to that side alone, and the patch leaves the
386        // opposite side as it did — a face made tangent to it later sees
387        // the patch as it was built.
388        let rows_needed = |a: usize, b: usize| {
389            if sides[a].is_some() || sides[b].is_some() {
390                4
391            } else {
392                0
393            }
394        };
395        let mut surface = self.clone();
396        // Raised along `v` for the sides 0 and 2, along `u` for 1 and 3,
397        // until there are as many rows as the faces need. Each raise adds
398        // a row at least.
399        for (along_u, needed) in [(false, rows_needed(0, 2)), (true, rows_needed(1, 3))] {
400            repeat(needed + 1, || -> GeopResult<_> {
401                let rows = if along_u {
402                    surface.num_u
403                } else {
404                    surface.num_v
405                };
406                if rows >= needed {
407                    return Ok(Step::Done(()));
408                }
409                surface = surface.elevate_degree(along_u)?;
410                Ok(Step::Again)
411            })?
412            .done("raising the patch's degree for its tangent planes")?;
413        }
414        let (nu, nv) = (surface.num_u, surface.num_v);
415        let at = |i: usize, j: usize| i * nv + j;
416        // Per side: the face's normal and the way away from it at each
417        // position along the side, where it is at that position's Greville
418        // abscissa.
419        let mut planes: [Vec<(Vector3, Vector3)>; SIDES] = Default::default();
420        for (k, side) in sides.iter().enumerate() {
421            let Some(side) = side else { continue };
422            let rows = SideRows::of(&surface, k);
423            for s in rows.abscissae(&surface)? {
424                let (point, along) = rows.point_along(&surface, s)?;
425                planes[k].push(side.at(&point, &along)?);
426            }
427            if side.flat() {
428                let plane = Plane::try_new(surface.evaluate_corner(&rows)?, planes[k][0].0)?;
429                for i in 0..rows.len() {
430                    let p = surface.control_points[rows.index(i).0].dehomogenized()?;
431                    if !plane.signed_distance(&p).could_be_equal(Scalar::ZERO) {
432                        return Err(GeopError::new(format!(
433                            "NurbSurface::tangent_to: side {k} leaves the plane of its flat neighbour at control point {p:?}"
434                        )));
435                    }
436                }
437            }
438        }
439        // Per side whose next row `(i, j)` is in: the side, the position
440        // along it, and the control point on the side that row leaves from.
441        let next_row = |k: usize, i: usize, j: usize| -> Option<(usize, (usize, usize))> {
442            match k {
443                0 if j == 1 => Some((i, (i, 0))),
444                1 if i == nu - 2 => Some((j, (nu - 1, j))),
445                2 if j == nv - 2 => Some((i, (i, nv - 1))),
446                3 if i == 1 => Some((j, (0, j))),
447                _ => None,
448            }
449        };
450        let original = surface.control_points.clone();
451        // Per side, the positions along it whose next row point is not the
452        // side's own to lean: two sides decide it, or it is kept.
453        let mut shared: [Vec<bool>; SIDES] = Default::default();
454        for k in 0..SIDES {
455            shared[k] = vec![false; planes[k].len()];
456        }
457        for i in 0..nu {
458            for j in 0..nv {
459                // The side, the position along it, the tangent plane
460                // through the point on the side, the plane the point goes
461                // into — that plane leant by [`TILT`] along a curved face —
462                // the way away from the face, and that point.
463                type Constraint = (usize, usize, Plane, Plane, Vector3, Vector3);
464                let mut constraints: Vec<Constraint> = Vec::new();
465                for k in 0..SIDES {
466                    let Some((position, (bi, bj))) = next_row(k, i, j) else {
467                        continue;
468                    };
469                    let Some(side) = &sides[k] else { continue };
470                    let (normal, away) = planes[k][position];
471                    let base = original[at(bi, bj)].dehomogenized()?;
472                    let leant = match side.tilted() {
473                        false => normal,
474                        true => normal.sub(&away.normalize()?.prod_scalar(Scalar::from_f64(TILT))),
475                    };
476                    constraints.push((
477                        k,
478                        position,
479                        Plane::try_new(base, normal)?,
480                        Plane::try_new(base, leant)?,
481                        away,
482                        base,
483                    ));
484                }
485                if constraints.is_empty() {
486                    continue;
487                }
488                let keep = kept(i, j);
489                if constraints.len() == 2 || keep {
490                    for (k, position, ..) in &constraints {
491                        shared[*k][*position] = true;
492                    }
493                }
494                let on_border = i == 0 || j == 0 || i == nu - 1 || j == nv - 1;
495                let cp = original[at(i, j)];
496                let p = cp.dehomogenized()?;
497                let moved = if on_border {
498                    // The second control point of a side meeting this one:
499                    // kept, and so it must be in the plane already.
500                    for (k, _, plane, ..) in &constraints {
501                        if !plane.signed_distance(&p).could_be_equal(Scalar::ZERO) {
502                            return Err(GeopError::new(format!(
503                                "NurbSurface::tangent_to: the side meeting side {k} at its corner leaves the face's tangent plane there, at {p:?}: the patch cannot be tangent to the face and keep that side"
504                            )));
505                        }
506                    }
507                    p
508                } else if keep {
509                    p
510                } else {
511                    match constraints.as_slice() {
512                        [(_, _, _, plane, ..)] => plane.project(&p),
513                        [(_, _, _, a, ..), (_, _, _, b, ..)] => {
514                            if a.normal
515                                .prod_cross(&b.normal)
516                                .norm_sq()
517                                .could_be_equal(Scalar::ZERO)
518                            {
519                                // One plane, met along two sides.
520                                a.project(&p)
521                            } else {
522                                a.intersect_plane(b)?.project(&p)
523                            }
524                        }
525                        _ => {
526                            return Err(GeopError::new(format!(
527                                "NurbSurface::tangent_to: control point ({i}, {j}) is in the rows of {} sides",
528                                constraints.len()
529                            )));
530                        }
531                    }
532                };
533                for (k, _, _, _, away, base) in &constraints {
534                    if !moved
535                        .sub(base)
536                        .prod_dot(away)
537                        .definitely_greater(Scalar::ZERO)
538                    {
539                        return Err(GeopError::new(format!(
540                            "NurbSurface::tangent_to: at control point ({i}, {j}) the patch does not leave side {k} away from the face it continues: it would fold back over it"
541                        )));
542                    }
543                }
544                if !on_border && !keep {
545                    surface.control_points[at(i, j)] = moved.homogeneous(cp[3]);
546                }
547            }
548        }
549        for (k, side) in sides.iter().enumerate() {
550            if side.as_ref().is_some_and(|s| !s.flat()) {
551                surface.lean(k, &planes[k], &shared[k])?;
552            }
553        }
554        surface.recompute_aabb();
555        Ok(surface)
556    }
557
558    /// The point where side `rows` starts.
559    fn evaluate_corner(&self, rows: &SideRows) -> GeopResult<Vector3> {
560        self.control_points[rows.index(0).0].dehomogenized()
561    }
562
563    /// The row next to side `k` moved along the face's normals `planes`
564    /// (per position along the side, at its Greville abscissa) so that the
565    /// patch leaves the face at [`TILT`] towards its normal at each of those
566    /// abscissae — but where the row's ends are, which the sides meeting
567    /// this one keep, and the points `shared` with another side's row: a
568    /// collocation, linear in how far each point moves.
569    ///
570    /// Where the patch is at `S(s)` on the side, the derivative into it is
571    /// a positive multiple of `Σ_i N_i(s) (w1_i (p1_i - S) - w0_i (p0_i -
572    /// S))` over the side's points `p0` and the next row's `p1`, weighted —
573    /// which moving `p1_i` by `c_i n_i` changes by `N_i(s) w1_i c_i n_i`.
574    /// How far each point moves is a free choice, sharpened.
575    fn lean(&mut self, k: usize, planes: &[(Vector3, Vector3)], shared: &[bool]) -> GeopResult<()> {
576        let rows = SideRows::of(self, k);
577        let (degree, knots) = rows.knots(self);
578        let len = rows.len();
579        let abscissae = rows.abscissae(self)?;
580        let free: Vec<usize> = (1..len - 1).filter(|&i| !shared[i]).collect();
581        if free.is_empty() {
582            return Ok(());
583        }
584        let point = |i: usize, row: usize| -> GeopResult<(Vector3, Scalar)> {
585            let cp = self.control_points[if row == 0 {
586                rows.index(i).0
587            } else {
588                rows.index(i).1
589            }];
590            Ok((cp.dehomogenized()?, cp[3]))
591        };
592        let mut a = Vec::with_capacity(free.len());
593        let mut rhs = Vec::with_capacity(free.len());
594        for &j in &free {
595            let s = abscissae[j];
596            let (u, v) = rows.uv(self, s);
597            let on_side = self.evaluate(u, v)?;
598            let normal = planes[j].0;
599            let span = find_span(degree, knots, len - 1, s)?;
600            let funs = basis_funs(span, s, degree, knots);
601            let mut into = Vector3::zero();
602            for (f, &n) in funs.iter().enumerate() {
603                let i = span - degree + f;
604                let ((p0, w0), (p1, w1)) = (point(i, 0)?, point(i, 1)?);
605                let term = p1
606                    .sub(&on_side)
607                    .prod_scalar(w1)
608                    .sub(&p0.sub(&on_side).prod_scalar(w0));
609                into = into.add(&term.prod_scalar(n));
610            }
611            let mut row = vec![Scalar::ZERO; free.len()];
612            for (f, &n) in funs.iter().enumerate() {
613                let i = span - degree + f;
614                if let Some(c) = free.iter().position(|&x| x == i) {
615                    row[c] = n.mul(point(i, 1)?.1).mul(planes[i].0.prod_dot(&normal));
616                }
617            }
618            a.push(row);
619            let target = Scalar::from_f64(TILT).mul(into.norm());
620            rhs.push(Vector::from_array([target.sub(into.prod_dot(&normal))]));
621        }
622        let moves = solve_pivoted(a, rhs).map_err(|e| {
623            e.with_context(format!(
624                "NurbSurface::tangent_to: leaning the row next to side {k} off its curved neighbour"
625            ))
626        })?;
627        let moved = (free.iter().zip(&moves))
628            .map(|(&i, by)| {
629                let (p, w) = point(i, 1)?;
630                Ok((
631                    i,
632                    p.add(&planes[i].0.prod_scalar(by[0]))
633                        .sharpen()
634                        .homogeneous(w),
635                ))
636            })
637            .collect::<GeopResult<Vec<_>>>()?;
638        for (i, cp) in moved {
639            self.control_points[rows.index(i).1] = cp;
640        }
641        Ok(())
642    }
643
644    /// Where the patch strays, along a side of `sides` whose face is
645    /// curved, from leaving that face as [`NurbSurface3D::lean`] has it:
646    /// away from the face, towards its normal at more than nothing and less
647    /// than twice [`TILT`] — or where that cannot be shown. Proven over the
648    /// whole side, not sampled (see `AGENTS.md`, "Do not decide from
649    /// samples").
650    ///
651    /// The patch leaves the face at `f(s) = into(s)·n(s)`, `into` its
652    /// derivative across the side and `n` the face's normal where the side
653    /// is at `s`. Over a stretch `I` of the side, by the mean value
654    /// theorem, `f(I) ⊆ f(c) + f'(I) (I − c)` with `f' = into'·n +
655    /// into·n'`: `into` and `into'` are the patch's partials over `I`, and
656    /// `n` and `n'` the face's normal and its turn over the box of `(u, v)`
657    /// its foot points over `I` are proven to lie in
658    /// ([`NurbSurface3D::enclose_foot`]). The bound's width goes as `|I|`
659    /// times `f'`, which is small where the patch follows the face, so a
660    /// tilt of a millionth of a radian is shown over stretches a thousandth
661    /// long; a stretch not shown either way is halved, within each knot
662    /// span, [`MOST_HALVINGS`] times at most.
663    ///
664    /// At a corner where the side meets another lying in the face's
665    /// tangent plane, `f` is zero: the patch touches the face there, and
666    /// leaves it on the right side next to it where `f'` has the right
667    /// sign over the stretch from the corner — rising from the start,
668    /// falling to the end.
669    pub(super) fn strays(&self, sides: &[Option<Neighbour<'_>>; SIDES]) -> GeopResult<Vec<Stray>> {
670        let mut strays = Vec::new();
671        for (k, side) in sides.iter().enumerate() {
672            let Some(side) = side.as_ref().filter(|s| s.tilted()) else {
673                continue;
674            };
675            let rows = SideRows::of(self, k);
676            let (degree, knots) = rows.knots(self);
677            let breaks = breakpoints(knots, degree, rows.len());
678            let ends = (breaks[0], breaks[breaks.len() - 1]);
679            let mut seed = None;
680            for pair in breaks.windows(2) {
681                let mut stack = vec![(pair[0], pair[1], 0usize)];
682                while let Some((lo, hi, depth)) = stack.pop() {
683                    let why = match self.leaves(&rows, side, (lo, hi), ends, &mut seed)? {
684                        Leaving::Proven => continue,
685                        Leaving::Unsure(_) if depth < MOST_HALVINGS => {
686                            let mid = lo.add(hi).div(Scalar::TWO)?.sharpen();
687                            stack.push((mid, hi, depth + 1));
688                            stack.push((lo, mid, depth + 1));
689                            continue;
690                        }
691                        Leaving::Unsure(why) => {
692                            format!("cannot be shown to leave it as it should: {why}")
693                        }
694                        Leaving::Strays(why) => why,
695                    };
696                    strays.push(Stray {
697                        at: lo.add(hi).div(Scalar::TWO)?,
698                        what: format!("along side {k} over [{lo:?}, {hi:?}] {why}"),
699                    });
700                    break;
701                }
702            }
703        }
704        Ok(strays)
705    }
706
707    /// Where the patch strays from a curved face along a side of `sides`
708    /// (see [`NurbSurface3D::strays`]): first where it is seen to at one of
709    /// its Greville abscissae along the side or halfway between, a cheap
710    /// look that says where to split the patch while it does, and decides
711    /// nothing — once it sees nothing, where the proof finds it does, or
712    /// cannot show it does not.
713    pub(super) fn misses(&self, sides: &[Option<Neighbour<'_>>; SIDES]) -> GeopResult<Vec<Stray>> {
714        let tilt = Scalar::from_f64(2.0 * TILT);
715        let mut seen = Vec::new();
716        for (k, side) in sides.iter().enumerate() {
717            let Some(side) = side.as_ref().filter(|s| s.tilted()) else {
718                continue;
719            };
720            let rows = SideRows::of(self, k);
721            let abscissae = rows.abscissae(self)?;
722            for pair in abscissae.windows(2) {
723                for s in [pair[0], pair[0].add(pair[1]).div(Scalar::TWO)?.sharpen()] {
724                    let (u, v) = rows.uv(self, s);
725                    let at = rows.jet(&self.partials(u, v, 2)?)?;
726                    let (normal, away) = side.at(&at.point, &rows.round(&at.along))?;
727                    let angle = at.into.prod_dot(&normal).div(at.into.norm())?;
728                    let away = at.into.prod_dot(&away).definitely_greater(Scalar::ZERO);
729                    let corner = s.is_exactly(abscissae[0]);
730                    let leaves_as_it_should = away
731                        && angle.definitely_greater(Scalar::ZERO)
732                        && angle.definitely_less(tilt);
733                    if !(corner || leaves_as_it_should) {
734                        seen.push(Stray {
735                            at: s,
736                            what: format!(
737                                "along side {k} at {s:?}, at {:?}, it leaves the face at {angle:?} radians towards its normal{}",
738                                at.point,
739                                if away { "" } else { ", not away from it" },
740                            ),
741                        });
742                    }
743                }
744            }
745        }
746        if seen.is_empty() {
747            return self.strays(sides);
748        }
749        Ok(seen)
750    }
751
752    /// Whether the patch leaves the face `side` as it should (see
753    /// [`NurbSurface3D::strays`]) over the stretch `lo..hi` of the side
754    /// `rows`, which runs between `ends`; `seed`, where the foot of the
755    /// stretch before was, is moved on to this one's.
756    fn leaves(
757        &self,
758        rows: &SideRows,
759        side: &Neighbour<'_>,
760        (lo, hi): (Scalar, Scalar),
761        ends: (Scalar, Scalar),
762        seed: &mut Option<(Scalar, Scalar)>,
763    ) -> GeopResult<Leaving> {
764        let stretch = lo.union(hi);
765        let jet = |s: Scalar| -> GeopResult<Jet> {
766            let (u, v) = rows.uv(self, s);
767            rows.jet(&self.partials(u, v, 2)?)
768        };
769        let over = jet(stretch)?;
770        let near = match *seed {
771            Some(near) => near,
772            None => side.foot(&jet(lo.add(hi).div(Scalar::TWO)?.sharpen())?.point)?,
773        };
774        let unsure = |point: &Vector3| {
775            Leaving::Unsure(format!(
776                "no foot point on the face proven for the side at {point:?}"
777            ))
778        };
779        let Some(feet) = side.carried.enclose_foot(&over.point, near)? else {
780            return Ok(unsure(&over.point));
781        };
782        let middle = (feet.0.midpoint(), feet.1.midpoint());
783        *seed = Some(middle);
784        let (n, turn) = side.carried.normal_turning(feet.0, feet.1, &over.along)?;
785        // Taken about the corner the stretch starts or ends at, if any:
786        // there the patch touches the face.
787        let (centre, rising) = if lo.is_exactly(ends.0) {
788            (lo, Some(true))
789        } else if hi.is_exactly(ends.1) {
790            (hi, Some(false))
791        } else {
792            (lo.add(hi).div(Scalar::TWO)?.sharpen(), None)
793        };
794        let at = jet(centre)?;
795        let Some(foot) = side.carried.enclose_foot(&at.point, middle)? else {
796            return Ok(unsure(&at.point));
797        };
798        let (n_at, _) = side.carried.normal_turning(foot.0, foot.1, &at.along)?;
799        let f_at = at.into.prod_dot(&n_at);
800        let slope = over.turn.prod_dot(&n).add(over.into.prod_dot(&turn));
801        let f = f_at.add(slope.mul(stretch.sub(centre)));
802        let tilt = Scalar::from_f64(2.0 * TILT);
803        // Within the tilt, and away from the face, over the whole stretch.
804        let within = tilt.mul(over.into.norm()).sub(f);
805        let away = over.into.prod_dot(&side.away(&n, &rows.round(&over.along)));
806        let away_at = at.into.prod_dot(&side.away(&n_at, &rows.round(&at.along)));
807        if f_at.definitely_less(Scalar::ZERO)
808            || tilt
809                .mul(at.into.norm())
810                .sub(f_at)
811                .definitely_less(Scalar::ZERO)
812            || away_at.definitely_less(Scalar::ZERO)
813        {
814            return Ok(Leaving::Strays(format!(
815                "at {centre:?}, at {:?}, it leaves the face at {:?} radians towards its normal, where it should at more than nothing and less than {tilt:?}{}",
816                at.point,
817                f_at.div(at.into.norm())?,
818                if away_at.definitely_less(Scalar::ZERO) {
819                    ", and not away from it"
820                } else {
821                    ""
822                }
823            )));
824        }
825        let positive = match rising {
826            Some(rising) if !f_at.definitely_greater(Scalar::ZERO) => {
827                if rising {
828                    slope.definitely_greater(Scalar::ZERO)
829                } else {
830                    slope.definitely_less(Scalar::ZERO)
831                }
832            }
833            _ => f.definitely_greater(Scalar::ZERO),
834        };
835        if positive
836            && within.definitely_greater(Scalar::ZERO)
837            && away.definitely_greater(Scalar::ZERO)
838        {
839            Ok(Leaving::Proven)
840        } else {
841            Ok(Leaving::Unsure(format!(
842                "over it the patch leaves the face at {:?} radians towards its normal, turning at {slope:?}",
843                f.div(over.into.norm())?
844            )))
845        }
846    }
847}
848
849/// What [`NurbSurface3D::leaves`] shows over a stretch of a side.
850enum Leaving {
851    /// The patch leaves the face as it should all over it.
852    Proven,
853    /// It does not, somewhere: how.
854    Strays(String),
855    /// Neither could be shown: what is known.
856    Unsure(String),
857}
858
859/// The patch along a side, at a parameter or over a stretch of them: the
860/// point, its derivative along the side by the side's parameter, the
861/// derivative across it into the patch, and how that turns along the side.
862struct Jet {
863    point: Vector3,
864    along: Vector3,
865    into: Vector3,
866    turn: Vector3,
867}
868
869/// How often [`NurbSurface3D::strays`] halves a stretch of a side that it
870/// can neither show the patch leaves the face as it should over, nor that
871/// it does not: how hard it tries, not what a stretch shown means.
872const MOST_HALVINGS: usize = 14;
873
874/// Where a patch made tangent to a curved face strays from leaving it as it
875/// should (see [`NurbSurface3D::strays`]).
876pub(super) struct Stray {
877    /// The patch's parameter along that side where.
878    pub at: Scalar,
879    /// How, for an error.
880    pub what: String,
881}
882
883/// The angle, in radians, at which a patch made tangent to a curved face
884/// leaves it towards its normal (see [`NurbSurface3D::tangent_to`]).
885pub const TILT: f64 = 1e-6;
886
887/// How often [`NurbSurface3D::tangent_to`] halves the patch's knot spans
888/// along a curved neighbour, at most: how hard it tries to follow the
889/// face's turning tangent plane, not what following it means.
890pub(super) const MOST_REFINEMENTS: usize = 6;
891
892/// A face a patch is made tangent to along one of its sides (see
893/// [`NurbSurface3D::tangent_to`], [`NurbSurface3D::fill`]): the surface the
894/// side lies on, and which side of it the face is on.
895#[derive(Clone, Debug)]
896pub struct Neighbour<'a> {
897    surface: &'a NurbSurface3D,
898    /// The surface carried on past every side by a quarter of its domain,
899    /// so that a box of `(u, v)` round a foot on its edge is still on it
900    /// (see [`NurbSurface3D::enclose_foot`]).
901    carried: Arc<NurbSurface3D>,
902    along: bool,
903    plane: Option<Plane>,
904    /// Whether the face is joined to the patch in one body (see
905    /// [`Neighbour::joined`]).
906    joined: bool,
907}
908
909impl<'a> Neighbour<'a> {
910    /// The face on `surface` along a side of a patch, the side lying on the
911    /// surface: running the side the way the patch runs round it (`along`)
912    /// — the face then lies to the right of the side, seen from its normal
913    /// — or the other way.
914    pub fn new(surface: &'a NurbSurface3D, along: bool) -> GeopResult<Self> {
915        // By a power of two, a free choice: the knots shifted by it stay
916        // what they were.
917        let quarter = |(lo, hi): (Scalar, Scalar)| {
918            Scalar::from_f64(2f64.powi((hi.sub(lo).to_f64() / 4.0).log2().floor() as i32))
919        };
920        let mut carried = surface.clone();
921        for along_u in [true, false] {
922            let by = quarter(if along_u {
923                surface.domain_u()
924            } else {
925                surface.domain_v()
926            });
927            for at_end in [false, true] {
928                carried = carried.extended(along_u, at_end, by)?;
929            }
930        }
931        Ok(Self {
932            surface,
933            carried: Arc::new(carried),
934            along,
935            plane: surface.as_plane()?,
936            joined: false,
937        })
938    }
939
940    /// The face on `surface` along a side of a patch as [`Neighbour::new`]
941    /// says, joined to the patch along that side in one body that the
942    /// kernel builds: both are its own, and no boolean ever has to tell
943    /// which side of the face the patch leaves it on. So the patch is made
944    /// tangent to it untilted, at its abscissae along the side, and that is
945    /// not proven between them: no [`TILT`] is needed where no approximate
946    /// tangency can dip through anything.
947    pub fn joined(surface: &'a NurbSurface3D, along: bool) -> GeopResult<Self> {
948        Ok(Self {
949            joined: true,
950            ..Self::new(surface, along)?
951        })
952    }
953
954    /// Whether the face is flat: its tangent plane one plane all along the
955    /// side, which a patch can be tangent to exactly.
956    pub(super) fn flat(&self) -> bool {
957        self.plane.is_some()
958    }
959
960    /// Whether the patch leaves the face at [`TILT`], proven: a curved face
961    /// not joined to it (see [`Neighbour::joined`]).
962    pub(super) fn tilted(&self) -> bool {
963        !self.flat() && !self.joined
964    }
965
966    /// Where the side, running round the patch at `along`, is at `point`:
967    /// the face's unit normal there, and the way in its tangent plane away
968    /// from the face, across the side.
969    pub(super) fn at(&self, point: &Vector3, along: &Vector3) -> GeopResult<(Vector3, Vector3)> {
970        let normal = match &self.plane {
971            Some(plane) => plane.normal,
972            None => {
973                let (u, v) = self.foot(point)?;
974                self.surface.normal(u, v)?
975            }
976        };
977        Ok((normal, self.away(&normal, along)))
978    }
979
980    /// The face's unit normal at `point` of the side, and how fast it turns
981    /// as the side runs on at `along` (see [`NurbSurface3D::normal_turning`]).
982    pub(super) fn turning(
983        &self,
984        point: &Vector3,
985        along: &Vector3,
986    ) -> GeopResult<(Vector3, Vector3)> {
987        match &self.plane {
988            Some(plane) => Ok((plane.normal, Vector3::zero())),
989            None => {
990                let (u, v) = self.foot(point)?;
991                self.surface.normal_turning(u, v, along)
992            }
993        }
994    }
995
996    /// The way away from the face across the side running at `along`,
997    /// where its normal is `normal`: the face lies to the left of the way
998    /// it runs the side, seen from its normal.
999    pub(super) fn away(&self, normal: &Vector3, along: &Vector3) -> Vector3 {
1000        if self.along {
1001            along.prod_cross(normal)
1002        } else {
1003            normal.prod_cross(along)
1004        }
1005    }
1006
1007    /// Where `point`, on the side, lies on the face's surface.
1008    fn foot(&self, point: &Vector3) -> GeopResult<(Scalar, Scalar)> {
1009        self.surface
1010            .locate(point, MAX_NODES, min_subdivision_size())?
1011            .ok_or_else(|| {
1012                GeopError::new(format!(
1013                    "Neighbour: the side does not lie on the face's surface at {point:?}"
1014                ))
1015            })
1016    }
1017}
1018
1019/// Side `k` of a patch's control net, as [`NurbSurface3D::coons`] numbers
1020/// them: its control points, and those of the row next to it.
1021struct SideRows {
1022    k: usize,
1023    nu: usize,
1024    nv: usize,
1025}
1026
1027impl SideRows {
1028    fn of(surface: &NurbSurface3D, k: usize) -> Self {
1029        Self {
1030            k,
1031            nu: surface.num_u,
1032            nv: surface.num_v,
1033        }
1034    }
1035
1036    /// Whether the side runs along `u`.
1037    fn along_u(&self) -> bool {
1038        self.k.is_multiple_of(2)
1039    }
1040
1041    /// How many control points it has.
1042    fn len(&self) -> usize {
1043        if self.along_u() { self.nu } else { self.nv }
1044    }
1045
1046    /// The index of its control point at position `i`, and of the next
1047    /// row's.
1048    fn index(&self, i: usize) -> (usize, usize) {
1049        let (nu, nv) = (self.nu, self.nv);
1050        let at = |a: usize, b: usize| a * nv + b;
1051        match self.k {
1052            0 => (at(i, 0), at(i, 1)),
1053            1 => (at(nu - 1, i), at(nu - 2, i)),
1054            2 => (at(i, nv - 1), at(i, nv - 2)),
1055            _ => (at(0, i), at(1, i)),
1056        }
1057    }
1058
1059    /// The degree and the knots along the side.
1060    fn knots<'s>(&self, surface: &'s NurbSurface3D) -> (usize, &'s [Scalar]) {
1061        if self.along_u() {
1062            (surface.degree_u, &surface.knot_vector_u)
1063        } else {
1064            (surface.degree_v, &surface.knot_vector_v)
1065        }
1066    }
1067
1068    /// The Greville abscissae of its control points.
1069    fn abscissae(&self, surface: &NurbSurface3D) -> GeopResult<Vec<Scalar>> {
1070        let (degree, knots) = self.knots(surface);
1071        greville(knots, degree, self.len())
1072    }
1073
1074    /// The patch along the side from its partials `d` (see
1075    /// [`NurbSurface3D::partials`]) where the side is.
1076    fn jet(&self, d: &[Vec<Vector3>]) -> GeopResult<Jet> {
1077        let (su, sv, suv) = (d[1][0], d[0][1], d[1][1]);
1078        let (along, into, turn) = match self.k {
1079            0 => (su, sv, suv),
1080            1 => (sv, su.neg(), suv.neg()),
1081            2 => (su, sv.neg(), suv.neg()),
1082            _ => (sv, su, suv),
1083        };
1084        Ok(Jet {
1085            point: d[0][0],
1086            along,
1087            into,
1088            turn,
1089        })
1090    }
1091
1092    /// `along`, the derivative along the side by its parameter, the way
1093    /// the side runs round the patch.
1094    fn round(&self, along: &Vector3) -> Vector3 {
1095        if self.k < 2 { *along } else { along.neg() }
1096    }
1097
1098    /// The point at `s` along the side, and the side's derivative there as
1099    /// it runs round the patch.
1100    fn point_along(&self, surface: &NurbSurface3D, s: Scalar) -> GeopResult<(Vector3, Vector3)> {
1101        let (u, v) = self.uv(surface, s);
1102        let (du, dv) = surface.derivatives(u, v)?;
1103        let along = match self.k {
1104            0 => du,
1105            1 => dv,
1106            2 => du.neg(),
1107            _ => dv.neg(),
1108        };
1109        Ok((surface.evaluate(u, v)?, along))
1110    }
1111
1112    /// The patch's `(u, v)` at `s` along the side.
1113    fn uv(&self, surface: &NurbSurface3D, s: Scalar) -> (Scalar, Scalar) {
1114        let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
1115        match self.k {
1116            0 => (s, v0),
1117            1 => (u1, s),
1118            2 => (s, v1),
1119            _ => (u0, s),
1120        }
1121    }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126    use crate::test_fixtures::{line, weighted};
1127    use geop_core_math::{scalars::Scalar, vector::Vector3};
1128
1129    use crate::{
1130        nurb_curve::{NurbCurve, NurbCurve3D},
1131        nurb_surface::{Neighbour, NurbSurface3D, TILT},
1132    };
1133
1134    fn v(x: f64, y: f64, z: f64) -> Vector3 {
1135        Vector3::from_array([x, y, z].map(Scalar::from_f64))
1136    }
1137
1138    /// The quarter circle in the plane `x = x0` from `(x0, 0, 0)` round
1139    /// the center `(x0, 0, 1)` to `(x0, 1, 1)`: it leaves the plane `z = 0`
1140    /// along `y`.
1141    fn rising_arc(x0: f64) -> NurbCurve3D {
1142        let f = Scalar::from_f64;
1143        NurbCurve::try_new(
1144            2,
1145            vec![
1146                weighted(x0, 0., 0., 1.),
1147                weighted(x0, 1., 0., std::f64::consts::SQRT_2 / 2.0),
1148                weighted(x0, 1., 1., 1.),
1149            ],
1150            vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
1151        )
1152        .unwrap()
1153    }
1154
1155    /// A cubic from the origin to `(1, 0, 0)`, with an interior knot,
1156    /// bending out of the plane `z = 0` by `lift`.
1157    fn wavy(lift: f64) -> NurbCurve3D {
1158        let f = Scalar::from_f64;
1159        NurbCurve::try_new(
1160            3,
1161            vec![
1162                weighted(0., 0., 0., 1.),
1163                weighted(0.3, -0.2, 0.4 * lift, 1.),
1164                weighted(0.6, 0.3, -0.2 * lift, 1.),
1165                weighted(0.8, -0.1, 0.3 * lift, 1.),
1166                weighted(1., 0., 0., 1.),
1167            ],
1168            vec![
1169                f(0.),
1170                f(0.),
1171                f(0.),
1172                f(0.),
1173                f(0.5),
1174                f(1.),
1175                f(1.),
1176                f(1.),
1177                f(1.),
1178            ],
1179        )
1180        .unwrap()
1181    }
1182
1183    fn assert_on(surface: &NurbSurface3D, curve: &NurbCurve3D, side: usize) {
1184        for k in 0..=8 {
1185            let s = Scalar::from_ratio(k, 8).unwrap();
1186            let (u, v) = match side {
1187                0 => (s, Scalar::ZERO),
1188                1 => (Scalar::ONE, s),
1189                2 => (Scalar::ONE.sub(s), Scalar::ONE),
1190                _ => (Scalar::ZERO, Scalar::ONE.sub(s)),
1191            };
1192            let on_surface = surface.evaluate(u, v).unwrap();
1193            let unit = curve.with_unit_domain().unwrap();
1194            let on_curve = unit.evaluate(s).unwrap();
1195            assert!(
1196                on_surface.could_be_equal(&on_curve),
1197                "side {side} at {s:?}: {on_surface:?} vs {on_curve:?}"
1198            );
1199        }
1200    }
1201
1202    /// A Coons patch of a wavy cubic, two quarter circles and a line: its
1203    /// border rows are the sides' own control points, and it runs along
1204    /// each side wherever it is evaluated.
1205    #[test]
1206    fn coons_interpolates_its_sides() {
1207        let sides = [
1208            wavy(1.0),
1209            line([1., 0., 0.], [1., 1., 1.]),
1210            line([1., 1., 1.], [0., 1., 1.]),
1211            rising_arc(0.).reverse(),
1212        ];
1213        let surface = NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]]).unwrap();
1214        // The cubic, on the patch's `u` knots, is its first row.
1215        let nv = surface.num_v;
1216        for (i, cp) in wavy(1.0).control_points.iter().enumerate() {
1217            let row = surface.control_points[i * nv];
1218            assert!(row.could_be_equal(cp), "{i}: {row:?} vs {cp:?}");
1219        }
1220        // The arc, raised to the cubic's degree... is the first column,
1221        // evaluated: every side lies on the patch.
1222        for (k, side) in sides.iter().enumerate() {
1223            assert_on(&surface, side, k);
1224        }
1225    }
1226
1227    /// Four lines of a square make the square itself, flat.
1228    #[test]
1229    fn coons_of_a_square_is_flat() {
1230        let c = [[0., 0., 0.], [2., 0., 0.], [2., 1., 0.], [0., 1., 0.]];
1231        let sides: Vec<_> = (0..4).map(|k| line(c[k], c[(k + 1) % 4])).collect();
1232        let surface = NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]]).unwrap();
1233        let plane = surface.as_plane().unwrap().expect("flat");
1234        assert!(plane.normal.could_be_equal(&v(0., 0., 1.)), "{plane:?}");
1235    }
1236
1237    /// Sides that do not meet are refused.
1238    #[test]
1239    fn coons_needs_a_closed_loop() {
1240        let a = line([0., 0., 0.], [1., 0., 0.]);
1241        let b = line([1., 0., 0.], [1., 1., 0.]);
1242        let c = line([1., 1., 0.], [0., 1., 0.]);
1243        let d = line([0., 1.5, 0.], [0., 0., 0.]);
1244        assert!(NurbSurface3D::coons([&a, &b, &c, &d]).is_err());
1245    }
1246
1247    /// The ruled surface between two curves runs along both.
1248    #[test]
1249    fn ruled_runs_along_both() {
1250        let a = wavy(1.0);
1251        let b = rising_arc(2.0);
1252        let surface = NurbSurface3D::ruled(&a, &b).unwrap();
1253        for k in 0..=8 {
1254            let s = Scalar::from_ratio(k, 8).unwrap();
1255            let pa = a.evaluate(s).unwrap();
1256            let pb = b.evaluate(s).unwrap();
1257            assert!(
1258                surface
1259                    .evaluate(s, Scalar::ZERO)
1260                    .unwrap()
1261                    .could_be_equal(&pa)
1262            );
1263            assert!(
1264                surface
1265                    .evaluate(s, Scalar::ONE)
1266                    .unwrap()
1267                    .could_be_equal(&pb)
1268            );
1269        }
1270    }
1271
1272    /// A patch rising from a wavy curve in the plane `z = 0` between two
1273    /// quarter circles that leave it along `y`: made tangent to the plane
1274    /// along its bottom side, its normal there is the plane's, all along
1275    /// the side — and its sides are where they were.
1276    #[test]
1277    fn tangent_to_a_plane() {
1278        // The top bends up and down: the blend alone does not keep the
1279        // plane's tangent.
1280        let top = {
1281            let f = Scalar::from_f64;
1282            NurbCurve::try_new(
1283                3,
1284                vec![
1285                    weighted(0., 1., 1., 1.),
1286                    weighted(0.3, 1., 1.5, 1.),
1287                    weighted(0.6, 1., 0.6, 1.),
1288                    weighted(0.8, 1., 1.3, 1.),
1289                    weighted(1., 1., 1., 1.),
1290                ],
1291                [0., 0., 0., 0., 0.5, 1., 1., 1., 1.].map(f).to_vec(),
1292            )
1293            .unwrap()
1294            .reverse()
1295        };
1296        let sides = [wavy(0.0), rising_arc(1.), top, rising_arc(0.).reverse()];
1297        let coons = NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]]).unwrap();
1298        let bends = (0..=8).any(|k| {
1299            let u = Scalar::from_ratio(k, 8).unwrap();
1300            let n = coons.normal(u, Scalar::ZERO).unwrap();
1301            !n.prod_cross(&v(0., 0., 1.))
1302                .norm_sq()
1303                .could_be_equal(Scalar::ZERO)
1304        });
1305        assert!(bends, "the Coons patch alone is not tangent to the plane");
1306        let floor = NurbSurface3D::bilinear(
1307            v(-1., -1., 0.),
1308            v(2., -1., 0.),
1309            v(2., 2., 0.),
1310            v(-1., 2., 0.),
1311        )
1312        .unwrap();
1313        // The floor runs the bottom side the other way: it lies on the
1314        // other side of it.
1315        let flat = || Some(Neighbour::new(&floor, false).unwrap());
1316        let surface = coons.tangent_to(&[flat(), None, None, None]).unwrap();
1317        for (k, side) in sides.iter().enumerate() {
1318            assert_on(&surface, side, k);
1319        }
1320        for k in 0..=8 {
1321            let u = Scalar::from_ratio(k, 8).unwrap();
1322            let n = surface.normal(u, Scalar::ZERO).unwrap();
1323            assert!(
1324                n.prod_cross(&v(0., 0., 1.))
1325                    .norm_sq()
1326                    .could_be_equal(Scalar::ZERO),
1327                "at {u:?}: {n:?}"
1328            );
1329        }
1330        // Straight sides leaving the plane at an angle cannot be kept.
1331        let steep = [
1332            line([0., 0., 0.], [1., 0., 0.]),
1333            line([1., 0., 0.], [1., 1., 1.]),
1334            line([1., 1., 1.], [0., 1., 1.]),
1335            line([0., 1., 1.], [0., 0., 0.]),
1336        ];
1337        let coons = NurbSurface3D::coons([&steep[0], &steep[1], &steep[2], &steep[3]]).unwrap();
1338        assert!(coons.tangent_to(&[flat(), None, None, None]).is_err());
1339    }
1340
1341    /// A bilinear patch through four corners, not in one plane, has them
1342    /// at the corners of its domain.
1343    #[test]
1344    fn bilinear_has_its_corners() {
1345        let corners = [
1346            v(0.3, -1.2, 2.5),
1347            v(4.1, 0.7, -0.3),
1348            v(2.2, 3.3, 1.1),
1349            v(-1.5, 2.0, 0.6),
1350        ];
1351        let surface =
1352            NurbSurface3D::bilinear(corners[0], corners[1], corners[2], corners[3]).unwrap();
1353        for (p, (u, w)) in corners.iter().zip([(0, 0), (1, 0), (1, 1), (0, 1)]) {
1354            let at = surface
1355                .evaluate(Scalar::from_i64(u), Scalar::from_i64(w))
1356                .unwrap();
1357            assert!(at.could_be_equal(p), "at ({u}, {w}): {at:?}, not {p:?}");
1358        }
1359    }
1360
1361    /// A quarter cylinder about the line `y = 0, z = 1`, from `x = 0` to
1362    /// `x = 1`, and a patch running on from its side at `x = 1` to `x = 2`.
1363    /// Its Coons patch leaves the cylinder at an angle, which the proof
1364    /// finds; made tangent, it is proven to leave it as it should all
1365    /// along the side, and the tilt seen there agrees.
1366    #[test]
1367    fn leaving_a_cylinder_is_proven() {
1368        let cylinder = NurbSurface3D::coons([
1369            &line([0., 0., 0.], [1., 0., 0.]),
1370            &rising_arc(1.),
1371            &line([1., 1., 1.], [0., 1., 1.]),
1372            &rising_arc(0.).reverse(),
1373        ])
1374        .unwrap();
1375        let sides = [
1376            rising_arc(1.),
1377            line([1., 1., 1.], [2., 1., 1.]),
1378            line([2., 1., 1.], [2., 0., 0.]),
1379            line([2., 0., 0.], [1., 0., 0.]),
1380        ];
1381        let plain = NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]]).unwrap();
1382        // The cylinder runs its side at `x = 1` as the patch does.
1383        let neighbours = [
1384            Some(Neighbour::new(&cylinder, true).unwrap()),
1385            None,
1386            None,
1387            None,
1388        ];
1389        let strays = plain.strays(&neighbours).unwrap();
1390        assert!(!strays.is_empty(), "the Coons patch is not tangent");
1391        let tangent = plain.tangent_to(&neighbours).unwrap();
1392        let strays = tangent.strays(&neighbours).unwrap();
1393        assert!(
1394            strays.is_empty(),
1395            "{:?}",
1396            strays.iter().map(|s| &s.what).collect::<Vec<_>>()
1397        );
1398        // Seen at points along the side, as the proof says.
1399        for i in 1..64 {
1400            let u = Scalar::from_ratio(i, 64).unwrap();
1401            let p = tangent.evaluate(u, Scalar::ZERO).unwrap();
1402            let out = v(0., p[1].to_f64(), p[2].to_f64() - 1.)
1403                .normalize()
1404                .unwrap();
1405            let (_, into) = tangent.derivatives(u, Scalar::ZERO).unwrap();
1406            let angle = into.prod_dot(&out).neg().div(into.norm()).unwrap().to_f64();
1407            assert!(angle > 0. && angle < 2. * TILT, "at {u:?}: {angle:e}");
1408        }
1409    }
1410}