Skip to main content

geop_core_geometry/nurb_surface/
fill.rs

1//! A closed loop of any number of curves filled smoothly
2//! ([`NurbSurface3D::fill`]): a flat inner polygon and a four-sided patch
3//! along every side of the loop, between the side, the spokes from its
4//! ends to the polygon and the polygon's side, each patch tangent to its
5//! neighbours across the spokes, to the polygon, and — where a face is
6//! given along a side of the loop — to that face.
7//!
8//! **Across a spoke.** The two patches meeting along a spoke share its
9//! tangent plane at every point of it exactly, by construction: the plane
10//! is spanned by the spoke's tangent `T(v)` and a field `V(v)` across it,
11//! both polynomials, and each patch's derivative across the spoke is
12//! `α(v) V(v) + β(v) T(v)`, its own linear `α` and `β` — which is that
13//! patch's row next to the spoke, a polynomial too (SMLib's cross-boundary
14//! derivative `B(v) = p(v) D(v) + q(v) T(v)`). Where the row's end
15//! weight is not one (a rational side), the row's weights run linearly
16//! along it and the patch is one degree higher along the spoke than the
17//! spoke, so the product still fits.
18//!
19//! **At the polygon.** Where two patches and the flat polygon meet, each
20//! tangent to the other two, the corner has to be a flat point of both
21//! patches: the shape operator of each vanishes along its polygon side and,
22//! being symmetric and shared across the spoke, everywhere (the vertex
23//! enclosure problem). So the spokes arrive *osculating* the polygon's
24//! plane — their last three control points in it — the field `V` is
25//! constant to first order there, and every control point of the patch
26//! within two of that corner lies in the plane.
27//!
28//! **At the loop.** At a corner of the loop the two patches and the faces
29//! along the two sides meet in one tangent plane: the plane of the two
30//! sides there, which a face given along either must share. The spoke
31//! leaves along the bisector of the corner in that plane, its first three
32//! control points in it. Along a side with a flat face the patch's row
33//! next to it lies in the face's plane, exactly; along a curved one it is
34//! leant as [`NurbSurface3D::tangent_to`] does, around the points of it
35//! the spokes' fields have set. Those set the twist at the corner, and it
36//! has to agree with how the face's normal turns along the side: the
37//! spoke's third control point and the field's second are moved off the
38//! tangent plane so that the tilt rises from nothing at the corner as the
39//! row's leaning has it further in (see [`twist`]). Where the patch still
40//! strays from the face, the knot spans it strays in are halved, and near
41//! a corner they shrink geometrically.
42//!
43//! Every position here is a free choice: the polygon's corners halfway to
44//! the loop's centre, the spokes' and the field's control points, the
45//! linear `α` and `β`. They are sharpened, so the patches share them
46//! exactly.
47
48use geop_core_math::{
49    geop_error::{GeopError, GeopResult},
50    scalars::Scalar,
51    solvers::linear::solve_symmetric2,
52    vector::{Vector3, Vector4},
53};
54
55use super::{
56    NurbSurface3D,
57    patch::{Neighbour, SIDES, TILT},
58};
59use crate::{
60    nurb_curve::{NurbCurve, NurbCurve3D, bezier_knots},
61    shape::Plane,
62};
63
64/// The degree of a spoke: its first three control points in the tangent
65/// plane at the loop's corner, its last three in the polygon's plane.
66const SPOKE_DEGREE: usize = 5;
67
68/// How often [`NurbSurface3D::fill`] splits the knot spans of its patches
69/// where they stray from a curved neighbour, at most: how hard it tries to
70/// follow the face's turning tangent plane, not what following it means.
71/// Each round halves the spans where it strayed, so near a corner, where
72/// the patch's twist is the spoke's and the tilt rises from nothing, the
73/// spans shrink geometrically.
74const MOST_SPLITS: usize = 16;
75
76/// A closed loop filled by [`NurbSurface3D::fill`].
77#[derive(Clone, Debug)]
78pub struct Fill {
79    /// Per side `k` of the loop, the patch along it, over `[0, 1]²`: side
80    /// `k` at `v = 0` running with `u`, spoke `k + 1` at `u = 1` running
81    /// with `v`, the inner polygon's side `k` at `v = 1` and spoke `k` at
82    /// `u = 0` — round it as [`NurbSurface3D::coons`] numbers its sides.
83    pub patches: Vec<NurbSurface3D>,
84    /// Per corner `k` of the loop, where side `k` starts: the spoke from
85    /// it to the inner polygon's corner `k`.
86    pub spokes: Vec<NurbCurve3D>,
87    /// The inner polygon's corners, running round as the loop does.
88    pub inner: Vec<Vector3>,
89    /// The plane the inner polygon lies in, its normal the side the loop
90    /// runs counter-clockwise round, as every patch's does.
91    pub plane: Plane,
92}
93
94impl NurbSurface3D {
95    /// The closed loop `sides`, three or more curves each starting where
96    /// the one before ends, filled smoothly (see the module docs): patches
97    /// that run exactly along every side, meet each other and the flat
98    /// inner polygon tangentially, and meet the face `neighbours[k]` names
99    /// along side `k`, if any, tangentially too — exactly where that face
100    /// is flat, leaving it at [`TILT`] towards its normal where it is
101    /// curved, as [`NurbSurface3D::tangent_to`] does.
102    ///
103    /// Refused, naming the corner: a loop that turns back on itself, sides
104    /// at a corner that do not lie in the tangent plane of a face given
105    /// along either, and faces along two sides meeting there at an angle.
106    pub fn fill(sides: &[NurbCurve3D], neighbours: &[Option<Neighbour<'_>>]) -> GeopResult<Fill> {
107        let n = sides.len();
108        if n < 3 || neighbours.len() != n {
109            return Err(GeopError::new(format!(
110                "NurbSurface::fill: {n} sides and {} neighbours: a loop of three sides at least, a neighbour or none along each",
111                neighbours.len()
112            )));
113        }
114        let curves = (sides.iter())
115            .map(NurbCurve3D::with_unit_domain)
116            .collect::<GeopResult<Vec<_>>>()?;
117        let corners = (curves.iter())
118            .map(NurbCurve3D::start)
119            .collect::<GeopResult<Vec<_>>>()?;
120        for k in 0..n {
121            let end = curves[k].end()?;
122            if !end.could_be_equal(&corners[(k + 1) % n]) {
123                return Err(GeopError::new(format!(
124                    "NurbSurface::fill: side {k} ends at {end:?}, but side {} starts at {:?}",
125                    (k + 1) % n,
126                    corners[(k + 1) % n]
127                )));
128            }
129        }
130        let (plane, inner) = inner_polygon(&corners)?;
131        let frames = (0..n)
132            .map(|k| Frame::at(k, &curves, neighbours, &plane, &inner))
133            .collect::<GeopResult<Vec<_>>>()?;
134        let curved = |k: usize| neighbours[k].as_ref().is_some_and(Neighbour::tilted);
135        // Per patch, the knots along its side added where it strayed.
136        let mut added: Vec<Vec<Scalar>> = vec![Vec::new(); n];
137        let mut strays = String::new();
138        for _ in 0..=MOST_SPLITS {
139            let (patches, spokes) = build(&curves, neighbours, &frames, &inner, &plane, &added)?;
140            let mut again = false;
141            for k in (0..n).filter(|&k| curved(k)) {
142                for stray in patches[k].misses(&outer(neighbours, k))? {
143                    // The knot span it strayed in, halved.
144                    let knots = &patches[k].knot_vector_u;
145                    let after = knots.iter().position(|t| t.definitely_greater(stray.at));
146                    let Some(after) = after else { continue };
147                    let half = knots[after - 1]
148                        .add(knots[after])
149                        .div(Scalar::TWO)?
150                        .sharpen();
151                    if !added[k].iter().any(|t| t.could_be_equal(half)) {
152                        added[k].push(half);
153                        again = true;
154                    }
155                    strays = format!("the patch along side {k} {}", stray.what);
156                }
157            }
158            if !again {
159                return Ok(Fill {
160                    patches,
161                    spokes,
162                    inner,
163                    plane,
164                });
165            }
166        }
167        Err(GeopError::new(format!(
168            "NurbSurface::fill: split its patches {MOST_SPLITS} times where they strayed from their curved neighbours, the fill still does not leave them at {TILT:e} radians: {strays}"
169        )))
170    }
171}
172
173/// The neighbours of the patch along side `k`: the face along it, at its
174/// side 0, and none elsewhere — the spokes and the polygon it is made
175/// tangent to by construction.
176fn outer<'a>(neighbours: &[Option<Neighbour<'a>>], k: usize) -> [Option<Neighbour<'a>>; SIDES] {
177    [neighbours[k].clone(), None, None, None]
178}
179
180/// The inner polygon of a loop with `corners`: each corner halfway to
181/// their average, moved onto the plane through that average square to the
182/// loop's (Newell) normal — all free choices, sharpened — and that plane.
183fn inner_polygon(corners: &[Vector3]) -> GeopResult<(Plane, Vec<Vector3>)> {
184    let n = corners.len();
185    let center = corners
186        .iter()
187        .fold(Vector3::zero(), |sum, p| sum.add(p))
188        .prod_scalar(Scalar::ONE.div(Scalar::from_i64(n as i64))?)
189        .sharpen();
190    let mut normal = [0.0f64; 3];
191    for k in 0..n {
192        let (a, b) = (
193            corners[k].to_f64_array(),
194            corners[(k + 1) % n].to_f64_array(),
195        );
196        normal[0] += (a[1] - b[1]) * (a[2] + b[2]);
197        normal[1] += (a[2] - b[2]) * (a[0] + b[0]);
198        normal[2] += (a[0] - b[0]) * (a[1] + b[1]);
199    }
200    let length = (normal[0].powi(2) + normal[1].powi(2) + normal[2].powi(2)).sqrt();
201    if length.is_nan() || length <= 0.0 {
202        return Err(GeopError::new(format!(
203            "NurbSurface::fill: the loop's corners {corners:?} enclose no area"
204        )));
205    }
206    let normal = Vector3::from_array(normal.map(|x| Scalar::from_f64(x / length)));
207    let plane = Plane {
208        point: center,
209        normal,
210    };
211    let half = Scalar::ONE.div(Scalar::TWO)?;
212    let inner: Vec<Vector3> = corners
213        .iter()
214        .map(|p| {
215            plane
216                .project(&center.add(&p.sub(&center).prod_scalar(half)))
217                .sharpen()
218        })
219        .collect();
220    let mut area = Scalar::ZERO;
221    for k in 0..n {
222        area = area.add(
223            inner[k]
224                .sub(&center)
225                .prod_cross(&inner[(k + 1) % n].sub(&center))
226                .prod_dot(&normal),
227        );
228    }
229    if !area.definitely_greater(Scalar::ZERO) {
230        return Err(GeopError::new(format!(
231            "NurbSurface::fill: the inner polygon {inner:?} does not run round its normal {normal:?}"
232        )));
233    }
234    Ok((plane, inner))
235}
236
237/// What a spoke is built from at its two ends: at the loop's corner `k`,
238/// the tangent plane's normal there and the way into the hole along it;
239/// at the polygon's corner, the way out of the polygon.
240struct Frame {
241    /// The tangent plane's unit normal at the loop's corner, on the side
242    /// the loop runs counter-clockwise round.
243    normal: Vector3,
244    /// The unit bisector of the corner, in the tangent plane, into the
245    /// hole.
246    into: Vector3,
247    /// The unit bisector of the polygon's outward normals at its corner.
248    out: Vector3,
249}
250
251impl Frame {
252    fn at(
253        k: usize,
254        curves: &[NurbCurve3D],
255        neighbours: &[Option<Neighbour<'_>>],
256        plane: &Plane,
257        inner: &[Vector3],
258    ) -> GeopResult<Self> {
259        let n = curves.len();
260        let before = (k + n - 1) % n;
261        let e = curves[k].tangent(Scalar::ZERO)?.normalize()?;
262        let f = curves[before].tangent(Scalar::ONE)?.neg().normalize()?;
263        // Oriented to the loop's side.
264        let oriented = |v: Vector3, what: &str| -> GeopResult<Vector3> {
265            let along = v.prod_dot(&plane.normal);
266            if along.definitely_greater(Scalar::ZERO) {
267                Ok(v)
268            } else if along.definitely_less(Scalar::ZERO) {
269                Ok(v.neg())
270            } else {
271                Err(GeopError::new(format!(
272                    "NurbSurface::fill: at corner {k} {what} {v:?} stands square to the loop's normal {:?}",
273                    plane.normal
274                )))
275            }
276        };
277        let mut given: Vec<(usize, Vector3)> = Vec::new();
278        let corner = curves[k].start()?;
279        for (side, t) in [(k, Scalar::ZERO), (before, Scalar::ONE)] {
280            if let Some(s) = &neighbours[side] {
281                let (normal, _) = s.at(&corner, &curves[side].tangent(t)?)?;
282                given.push((side, oriented(normal, "the normal of the face along side")?));
283            }
284        }
285        let normal = match given.as_slice() {
286            [(a, na), (b, nb)] => {
287                if !na.prod_cross(nb).norm_sq().could_be_equal(Scalar::ZERO) {
288                    return Err(GeopError::new(format!(
289                        "NurbSurface::fill: the faces along sides {b} and {a} meet at an angle at corner {k}, normals {nb:?} and {na:?}: the fill cannot be tangent to both"
290                    )));
291                }
292                *na
293            }
294            [(_, normal)] => *normal,
295            _ => {
296                let across = e.prod_cross(&f);
297                if across.norm_sq().definitely_greater(Scalar::ZERO) {
298                    oriented(across.normalize()?, "the plane of the sides")?
299                } else {
300                    let nu = plane.normal;
301                    nu.sub(&e.prod_scalar(nu.prod_dot(&e))).normalize()?
302                }
303            }
304        };
305        for (side, tangent) in [(k, e), (before, f)] {
306            if !tangent.prod_dot(&normal).could_be_equal(Scalar::ZERO) {
307                return Err(GeopError::new(format!(
308                    "NurbSurface::fill: side {side} leaves the tangent plane at corner {k}, normal {normal:?}, along {tangent:?}: the patches cannot be tangent there"
309                )));
310            }
311        }
312        let normal = normal.sharpen();
313        let turn = e.sub(&f);
314        if turn.norm_sq().could_be_equal(Scalar::ZERO) {
315            return Err(GeopError::new(format!(
316                "NurbSurface::fill: the loop turns back on itself at corner {k}"
317            )));
318        }
319        let into = normal.prod_cross(&turn).normalize()?.sharpen();
320        let outward = |a: usize, b: usize| -> GeopResult<Vector3> {
321            inner[b]
322                .sub(&inner[a])
323                .prod_cross(&plane.normal)
324                .normalize()
325        };
326        let out = outward(before, k)?
327            .add(&outward(k, (k + 1) % n)?)
328            .normalize()?
329            .sharpen();
330        Ok(Self { normal, into, out })
331    }
332}
333
334/// A spoke's control points from the loop's corner `from` to the polygon's
335/// corner `to`, leaving along `frame`'s bisector and arriving along its
336/// way out of the polygon, a fifth of the way between them per control
337/// point — free choices, sharpened — with its third control point lifted
338/// off the tangent plane by `lift` (see [`twist`]).
339fn spoke_points(from: Vector3, to: Vector3, frame: &Frame, lift: Scalar) -> Vec<Vector3> {
340    let step = Scalar::from_f64(to.sub(&from).norm().to_f64() / SPOKE_DEGREE as f64);
341    let along = |p: &Vector3, d: &Vector3, times: i64| {
342        p.add(&d.prod_scalar(step.mul(Scalar::from_i64(times))))
343            .sharpen()
344    };
345    vec![
346        from,
347        along(&from, &frame.into, 1),
348        along(&from, &frame.into, 2)
349            .add(&frame.normal.prod_scalar(lift))
350            .sharpen(),
351        along(&to, &frame.out, 2),
352        along(&to, &frame.out, 1),
353        to,
354    ]
355}
356
357fn spoke_curve(points: &[Vector3]) -> GeopResult<NurbCurve3D> {
358    NurbCurve::try_new(
359        SPOKE_DEGREE,
360        points.iter().map(|p| p.homogeneous(Scalar::ONE)).collect(),
361        bezier_knots(SPOKE_DEGREE),
362    )
363}
364
365/// The Bézier control points of `a(t) b(t)` over `[0, 1]`: `a` linear,
366/// given by its values at either end, `b` by its control points — one
367/// degree higher than `b`.
368fn times_linear(a: [Scalar; 2], b: &[Vector3]) -> GeopResult<Vec<Vector3>> {
369    let m = b.len() - 1;
370    let q = Scalar::from_i64((m + 1) as i64);
371    (0..=m + 1)
372        .map(|j| {
373            let mut out = Vector3::zero();
374            if j <= m {
375                let f = Scalar::from_i64((m + 1 - j) as i64).div(q)?;
376                out = out.add(&b[j].prod_scalar(a[0].mul(f)));
377            }
378            if j >= 1 {
379                let f = Scalar::from_i64(j as i64).div(q)?;
380                out = out.add(&b[j - 1].prod_scalar(a[1].mul(f)));
381            }
382            Ok(out)
383        })
384        .collect()
385}
386
387/// A spoke's tangent plane field: its control points `V`, constant to
388/// first order at both ends — across the spoke in the tangent plane at
389/// the loop's corner, turned off it by `lean` at its second (see
390/// [`twist`]), and across it in the polygon's plane at the other end.
391fn field(frame: &Frame, plane: &Plane, lean: Scalar) -> Vec<Vector3> {
392    let start = frame.normal.prod_cross(&frame.into).sharpen();
393    let end = frame.out.prod_cross(&plane.normal).sharpen();
394    vec![
395        start,
396        start.add(&frame.normal.prod_scalar(lean)).sharpen(),
397        start,
398        end,
399        end,
400        end,
401    ]
402}
403
404/// One patch's derivative across a spoke, in the units of its control
405/// points: `α V + β T`, `α` and `β` linear, at either end of the spoke.
406#[derive(Clone, Copy)]
407struct Across {
408    alpha: [Scalar; 2],
409    beta: [Scalar; 2],
410}
411
412impl Across {
413    /// `α` and `β` such that `α V + β T` is `ends` at either end of the
414    /// spoke — each decomposed in the plane of `V` and `T` there, a free
415    /// choice, sharpened.
416    fn of(ends: [Vector3; 2], v: [Vector3; 2], t: [Vector3; 2]) -> GeopResult<Self> {
417        let mut alpha = [Scalar::ZERO; 2];
418        let mut beta = [Scalar::ZERO; 2];
419        for e in 0..2 {
420            let (a, b) = solve_symmetric2(
421                (
422                    v[e].prod_dot(&v[e]),
423                    v[e].prod_dot(&t[e]),
424                    t[e].prod_dot(&t[e]),
425                ),
426                (v[e].prod_dot(&ends[e]), t[e].prod_dot(&ends[e])),
427            )?;
428            alpha[e] = a.sharpen();
429            beta[e] = b.sharpen();
430        }
431        Ok(Self { alpha, beta })
432    }
433
434    /// Its control points along the spoke, of degree [`SPOKE_DEGREE`] + 1:
435    /// `V` of [`SPOKE_DEGREE`] times a linear `α`, and the spoke's
436    /// derivative of one degree less times a linear `β`, raised twice.
437    fn points(&self, spoke: &[Vector3], v: &[Vector3]) -> GeopResult<Vec<Vector3>> {
438        let p = Scalar::from_i64(SPOKE_DEGREE as i64);
439        let t: Vec<Vector3> = spoke
440            .windows(2)
441            .map(|w| w[1].sub(&w[0]).prod_scalar(p))
442            .collect();
443        let tangent = times_linear([Scalar::ONE; 2], &times_linear(self.beta, &t)?)?;
444        let across = times_linear(self.alpha, v)?;
445        Ok(across.iter().zip(&tangent).map(|(a, b)| a.add(b)).collect())
446    }
447}
448
449/// A patch's row of control points next to a spoke, of degree
450/// [`SPOKE_DEGREE`] + 1 along it: the spoke times the row's weights, which
451/// run linearly between `weights`, the row's own at its ends, plus or
452/// minus (`into`: the spoke is at the patch's `u = 0`) the derivative
453/// across `across`, homogeneous.
454fn row_next_to(
455    spoke: &[Vector3],
456    across: &[Vector3],
457    weights: [Scalar; 2],
458    into: bool,
459) -> GeopResult<Vec<Vector4>> {
460    let weighted = times_linear(weights, spoke)?;
461    let q = Scalar::from_i64((SPOKE_DEGREE + 1) as i64);
462    (0..=SPOKE_DEGREE + 1)
463        .map(|j| {
464            let w = Scalar::interpolate(weights[0], weights[1], Scalar::from_i64(j as i64).div(q)?);
465            let h = if into {
466                weighted[j].add(&across[j])
467            } else {
468                weighted[j].sub(&across[j])
469            };
470            Ok(Vector4::from_array([h[0], h[1], h[2], w]))
471        })
472        .collect()
473}
474
475/// The point part and weight of a homogeneous control point.
476fn split(cp: &Vector4) -> (Vector3, Scalar) {
477    (Vector3::from_array([cp[0], cp[1], cp[2]]), cp[3])
478}
479
480/// The patch along side `k` before its rows next to the spokes, the
481/// polygon and the side are set: the Coons patch of the side, the spokes
482/// and the polygon's side, `SPOKE_DEGREE + 1` along the spokes, cubic at
483/// least along the side, each span along it split in two until it has
484/// `rows` control points along it at least, and the knots `added` put in.
485///
486/// Rows come from splitting rather than raising the degree: a row leant
487/// onto a curved face is a collocation, and of a high degree it rings
488/// where the tilt it follows rises from nothing at a corner.
489fn coons_quad(
490    side: &NurbCurve3D,
491    spokes: [&[Vector3]; 2],
492    inner: [Vector3; 2],
493    rows: usize,
494    added: &[Scalar],
495) -> GeopResult<NurbSurface3D> {
496    let start = spoke_curve(spokes[0])?;
497    let end = spoke_curve(spokes[1])?;
498    let back = NurbCurve3D::line(inner[1], inner[0])?;
499    let mut patch = NurbSurface3D::coons([side, &end, &back, &start.reverse()])?;
500    patch = patch.elevate_degree(false)?;
501    while patch.degree_u < 3 {
502        patch = patch.elevate_degree(true)?;
503    }
504    while patch.num_u < rows {
505        patch = patch.refined([2, 1])?;
506    }
507    Ok(patch.with_knots_u(added))
508}
509
510/// The spokes and the patches of the fill (see the module docs), the
511/// knots `added[k]` put into the patch along side `k`.
512fn build(
513    curves: &[NurbCurve3D],
514    neighbours: &[Option<Neighbour<'_>>],
515    frames: &[Frame],
516    inner: &[Vector3],
517    plane: &Plane,
518    added: &[Vec<Scalar>],
519) -> GeopResult<(Vec<NurbSurface3D>, Vec<NurbCurve3D>)> {
520    let n = curves.len();
521    let corner = |k: usize| curves[k % n].start();
522    let rows = |k: usize| {
523        if neighbours[k].as_ref().is_some_and(|s| !s.flat()) {
524            5
525        } else {
526            4
527        }
528    };
529    let quads = |spokes: &[Vec<Vector3>]| -> GeopResult<Vec<NurbSurface3D>> {
530        (0..n)
531            .map(|k| {
532                coons_quad(
533                    &curves[k],
534                    [&spokes[k], &spokes[(k + 1) % n]],
535                    [inner[k], inner[(k + 1) % n]],
536                    rows(k),
537                    &added[k],
538                )
539            })
540            .collect()
541    };
542    // First with spokes flat at the loop's corners: their ends, which
543    // decide the derivatives across them there, do not depend on the
544    // twist.
545    let flat: Vec<Vec<Vector3>> = (0..n)
546        .map(|k| Ok(spoke_points(corner(k)?, inner[k], &frames[k], Scalar::ZERO)))
547        .collect::<GeopResult<_>>()?;
548    let first = quads(&flat)?;
549    // Per spoke `k`: the derivative across it into the patch along side
550    // `k`, at its `u = 0`, and out of the patch along side `k - 1`, at its
551    // `u = 1`, both in the plane of the spoke and the field.
552    let mut acrosses = Vec::with_capacity(n);
553    for k in 0..n {
554        let (after, before) = (&first[k], &first[(k + n - 1) % n]);
555        let spoke = &flat[k];
556        let v = field(&frames[k], plane, Scalar::ZERO);
557        let p = SPOKE_DEGREE;
558        let t = [
559            spoke[1]
560                .sub(&spoke[0])
561                .prod_scalar(Scalar::from_i64(p as i64)),
562            spoke[p]
563                .sub(&spoke[p - 1])
564                .prod_scalar(Scalar::from_i64(p as i64)),
565        ];
566        let ends = |patch: &NurbSurface3D, i: usize, into: bool| -> GeopResult<[Vector3; 2]> {
567            let nv = patch.num_v;
568            let mut out = [Vector3::zero(); 2];
569            for (e, (j, on)) in [(0, spoke[0]), (nv - 1, spoke[p])].into_iter().enumerate() {
570                let (h, w) = split(&patch.control_points[i * nv + j]);
571                let d = h.sub(&on.prod_scalar(w));
572                out[e] = if into { d } else { d.neg() };
573            }
574            Ok(out)
575        };
576        let x = Across::of(ends(after, 1, true)?, [v[0], v[p]], t)?;
577        let y = Across::of(ends(before, before.num_u - 2, false)?, [v[0], v[p]], t)?;
578        for e in 0..2 {
579            if !x.alpha[e].mul(y.alpha[e]).definitely_greater(Scalar::ZERO) {
580                return Err(GeopError::new(format!(
581                    "NurbSurface::fill: the patches either side of spoke {k} fold over each other at its {} end: they leave it {:?} and {:?} across",
582                    if e == 0 { "outer" } else { "inner" },
583                    x.alpha[e],
584                    y.alpha[e]
585                )));
586            }
587        }
588        acrosses.push((x, y));
589    }
590    // The twist at each loop's corner, and the spokes and fields with it.
591    let mut spokes = Vec::with_capacity(n);
592    let mut fields = Vec::with_capacity(n);
593    for k in 0..n {
594        let (lift, lean) = twist(k, neighbours, &frames[k], &first, &flat, &acrosses)
595            .map_err(|e| e.with_context(format!("NurbSurface::fill: the twist at corner {k}")))?;
596        spokes.push(spoke_points(corner(k)?, inner[k], &frames[k], lift));
597        fields.push(field(&frames[k], plane, lean));
598    }
599    let mut patches = quads(&spokes)?;
600    for (k, patch) in patches.iter_mut().enumerate() {
601        set_rows(patch, k, &spokes, &fields, &acrosses, inner, plane)?;
602        if neighbours[k].is_some() {
603            let nu = patch.num_u;
604            *patch = patch
605                .tangent_rows(&outer(neighbours, k), &|i, j| {
606                    j == 1 && (i == 1 || i == nu - 2)
607                })
608                .map_err(|e| {
609                    e.with_context(format!("NurbSurface::fill: the patch along side {k}"))
610                })?;
611        }
612    }
613    let spokes = spokes
614        .iter()
615        .map(|s| spoke_curve(s))
616        .collect::<GeopResult<Vec<_>>>()?;
617    Ok((patches, spokes))
618}
619
620/// The rows of the patch along side `k` next to its spokes, from their
621/// fields, and next to the polygon, in its plane — refused where it would
622/// fold back over the polygon.
623fn set_rows(
624    patch: &mut NurbSurface3D,
625    k: usize,
626    spokes: &[Vec<Vector3>],
627    fields: &[Vec<Vector3>],
628    acrosses: &[(Across, Across)],
629    inner: &[Vector3],
630    plane: &Plane,
631) -> GeopResult<()> {
632    let n = spokes.len();
633    let next = (k + 1) % n;
634    let (nu, nv) = (patch.num_u, patch.num_v);
635    let weights = |i: usize| {
636        [
637            patch.control_points[i * nv][3],
638            patch.control_points[i * nv + nv - 1][3],
639        ]
640    };
641    let start = row_next_to(
642        &spokes[k],
643        &acrosses[k].0.points(&spokes[k], &fields[k])?,
644        weights(1),
645        true,
646    )?;
647    let end = row_next_to(
648        &spokes[next],
649        &acrosses[next].1.points(&spokes[next], &fields[next])?,
650        weights(nu - 2),
651        false,
652    )?;
653    for j in 1..nv - 1 {
654        patch.control_points[nv + j] = start[j].sharpen();
655        patch.control_points[(nu - 2) * nv + j] = end[j].sharpen();
656    }
657    for i in 2..nu - 2 {
658        let cp = patch.control_points[i * nv + nv - 2];
659        let (point, w) = (cp.dehomogenized()?, cp[3]);
660        patch.control_points[i * nv + nv - 2] = plane.project(&point).homogeneous(w);
661    }
662    let out = inner[next].sub(&inner[k]).prod_cross(&plane.normal);
663    for i in 0..nu {
664        let row = patch.control_points[i * nv + nv - 2].dehomogenized()?;
665        let on = patch.control_points[i * nv + nv - 1].dehomogenized()?;
666        if !row.sub(&on).prod_dot(&out).definitely_greater(Scalar::ZERO) {
667            return Err(GeopError::new(format!(
668                "NurbSurface::fill: the patch along side {k} folds back over the inner polygon at control point ({i}, {}), {row:?}",
669                nv - 2
670            )));
671        }
672    }
673    patch.recompute_aabb();
674    Ok(())
675}
676
677/// How far spoke `k`'s third control point is lifted off the tangent plane
678/// at its loop's corner, and the field's second turned off it: the twist
679/// there, which decides how the patches either side of the spoke start to
680/// leave the faces along the sides meeting there.
681///
682/// Along side `k`, at the patch's `u = 0`, the patch leaves the face at
683/// `f(u) = S_v(u, 0)·n(u)`, which is zero at the corner, where the spoke
684/// lies in the face's tangent plane. Its slope there, `S_uv·n + S_v·n'`,
685/// takes in how the face's normal turns along the side, `n'`, and the
686/// twist `S_uv`: the derivative of the field's `α V + β T` along the
687/// spoke, whose part along the normal is `5 α h_v + 20 β h_s` in the
688/// units of the patch's control points, for the field's turn `h_v` and the
689/// spoke's lift `h_s`. The slope is set to rise to [`TILT`] at the
690/// patch's first Greville abscissa along the side — a free choice of how
691/// a tilt that is nothing at the corner starts — along a curved face, and
692/// to zero along a flat one; along side `k - 1`, at that patch's `u = 1`,
693/// likewise, falling. Two faces give two conditions; one is met by the
694/// field's turn alone.
695fn twist(
696    k: usize,
697    neighbours: &[Option<Neighbour<'_>>],
698    frame: &Frame,
699    first: &[NurbSurface3D],
700    flat: &[Vec<Vector3>],
701    acrosses: &[(Across, Across)],
702) -> GeopResult<(Scalar, Scalar)> {
703    let n = neighbours.len();
704    let before = (k + n - 1) % n;
705    let curved = |s: usize| neighbours[s].as_ref().is_some_and(|s| !s.flat());
706    if !curved(k) && !curved(before) {
707        return Ok((Scalar::ZERO, Scalar::ZERO));
708    }
709    let spoke = &flat[k];
710    let p = Scalar::from_i64(SPOKE_DEGREE as i64);
711    // The spoke's derivative at the loop's corner.
712    let leaving = spoke[1].sub(&spoke[0]).prod_scalar(p);
713    let tilt = Scalar::from_f64(TILT).mul(leaving.norm());
714    let mut a = Vec::with_capacity(2);
715    let mut rhs = Vec::with_capacity(2);
716    for (side, into) in [(k, true), (before, false)] {
717        let Some(neighbour) = &neighbours[side] else {
718            continue;
719        };
720        let patch = &first[side];
721        let u = if into { Scalar::ZERO } else { Scalar::ONE };
722        let (along, _) = patch.derivatives(u, Scalar::ZERO)?;
723        let (normal, turn) = neighbour.turning(&spoke[0], &along).map_err(|e| {
724            e.with_context(format!(
725                "NurbSurface::fill: how the face along side {side} turns at corner {k}, {:?}, along {along:?}",
726                spoke[0]
727            ))
728        })?;
729        // The patch's derivative along `u` per unit of the field, at the
730        // corner: its degree over the knot span there.
731        let knots = &patch.knot_vector_u;
732        let d = patch.degree_u;
733        let (span, abscissa) = if into {
734            (
735                knots[d + 1].sub(knots[0]),
736                knots[2..=d + 1].iter().fold(Scalar::ZERO, |s, &x| s.add(x)),
737            )
738        } else {
739            let m = knots.len() - 1;
740            (
741                knots[m].sub(knots[m - d - 1]),
742                knots[m - d - 1..m - 1]
743                    .iter()
744                    .fold(Scalar::ZERO, |s, &x| s.add(knots[m].sub(x))),
745            )
746        };
747        // The lift and the turn move the patch along the frame's normal,
748        // which is the face's or its reverse.
749        let scale = Scalar::from_i64(d as i64)
750            .div(span)?
751            .mul(normal.prod_dot(&frame.normal));
752        let across = if into { acrosses[k].0 } else { acrosses[k].1 };
753        // The slope wanted, rising from the corner into the side: up to
754        // the tilt at the first abscissa in from the corner.
755        let slope = if !neighbour.tilted() {
756            Scalar::ZERO
757        } else {
758            tilt.mul(Scalar::from_i64(d as i64)).div(abscissa)?
759        };
760        // `f'` along `u`: positive rising from `u = 0`, negative towards
761        // `u = 1`.
762        let wanted = if into { slope } else { slope.neg() };
763        let by_lift = scale.mul(Scalar::from_i64(20)).mul(across.beta[0]);
764        let by_lean = scale.mul(Scalar::from_i64(5)).mul(across.alpha[0]);
765        a.push((by_lift, by_lean));
766        rhs.push(wanted.sub(leaving.prod_dot(&turn)));
767    }
768    Ok(match (a.as_slice(), rhs.as_slice()) {
769        ([(l0, t0), (l1, t1)], [r0, r1]) => {
770            let det = l0.mul(*t1).sub(t0.mul(*l1));
771            (
772                r0.mul(*t1).sub(t0.mul(*r1)).div(det)?.sharpen(),
773                l0.mul(*r1).sub(r0.mul(*l1)).div(det)?.sharpen(),
774            )
775        }
776        ([(_, t0)], [r0]) => (Scalar::ZERO, r0.div(*t0)?.sharpen()),
777        _ => (Scalar::ZERO, Scalar::ZERO),
778    })
779}
780
781#[cfg(test)]
782mod tests {
783    use geop_core_math::{scalars::Scalar, vector::Vector3};
784
785    use crate::nurb_curve::{NurbCurve3D, sqrt2_over_2};
786    use crate::nurb_surface::NurbSurface3D;
787
788    fn v(x: f64, y: f64, z: f64) -> Vector3 {
789        Vector3::from_array([x, y, z].map(Scalar::from_f64))
790    }
791
792    /// The quarter circle round the origin from `a` to `b`, unit vectors
793    /// square to each other.
794    fn quarter(a: Vector3, b: Vector3) -> NurbCurve3D {
795        NurbCurve3D::arc(a, a.add(&b), b, sqrt2_over_2()).unwrap()
796    }
797
798    /// Whether `a` and `b` are parallel to the rounding: the normals of a
799    /// rational patch evaluated come out a few `1e-12` wide.
800    fn parallel(a: &Vector3, b: &Vector3) -> bool {
801        a.prod_cross(b).norm().to_f64() < 1e-10
802    }
803
804    /// The three quarter circles round a sphere's octant, rational sides
805    /// meeting at right angles, filled: the patches run along the sides,
806    /// share their normal across every spoke and with the inner triangle
807    /// along its sides, all along them.
808    #[test]
809    fn an_octant_is_filled_smoothly() {
810        let (x, y, z) = (v(1., 0., 0.), v(0., 1., 0.), v(0., 0., 1.));
811        let sides = [quarter(x, y), quarter(y, z), quarter(z, x)];
812        let fill = NurbSurface3D::fill(&sides, &[None, None, None]).unwrap();
813        assert_eq!(fill.patches.len(), 3);
814        let s = |i: i64| Scalar::from_ratio(i, 16).unwrap();
815        for (k, patch) in fill.patches.iter().enumerate() {
816            let before = &fill.patches[(k + 2) % 3];
817            for i in 0..=16 {
818                // Along the side.
819                let on = patch.evaluate(s(i), Scalar::ZERO).unwrap();
820                let side = sides[k]
821                    .evaluate(sides[k].closest_point(&on).unwrap().0)
822                    .unwrap();
823                assert!(on.sub(&side).norm().to_f64() < 1e-12, "side {k} at {i}");
824                // Across spoke `k`, at `u = 0` of this patch and `u = 1` of
825                // the one before.
826                if i < 16 {
827                    let here = patch.normal(Scalar::ZERO, s(i)).unwrap();
828                    let there = before.normal(Scalar::ONE, s(i)).unwrap();
829                    assert!(
830                        parallel(&here, &there),
831                        "spoke {k} at {i}: {here:?} {there:?}"
832                    );
833                }
834                // Along the inner triangle.
835                if 0 < i && i < 16 {
836                    let n = patch.normal(s(i), Scalar::ONE).unwrap();
837                    assert!(
838                        parallel(&n, &fill.plane.normal),
839                        "inner side {k} at {i}: {n:?}"
840                    );
841                }
842            }
843        }
844    }
845}