Skip to main content

geop_core_geometry/nurb_surface/
gordon.rs

1//! The Gordon surface through a network of curves
2//! ([`NurbSurface3D::gordon`]): curves in two directions, each crossing
3//! every curve of the other once, interpolated all at once.
4//!
5//! It is the Coons construction (see [`NurbSurface3D::coons`]) carried from
6//! four sides to any number of curves: the surface skinned through the `u`
7//! curves, plus the one skinned through the `v` curves, less the
8//! tensor-product surface through the points where they cross. Each of the
9//! three runs through the crossings; along a `u` curve the second and the
10//! third agree, so the sum is the `u` curve there, and the same holds for
11//! the `v` curves. Everything is done on homogeneous control points, so it
12//! is exact for rational curves too.
13
14use geop_core_math::{
15    geop_error::{GeopError, GeopResult},
16    scalars::Scalar,
17    vector::{Vector3, Vector4},
18};
19
20use super::NurbSurface3D;
21use crate::nurb_curve::{NurbCurve, NurbCurve3D};
22
23/// The degree a network is skinned with across its curves: cubic, or lower
24/// where there are too few curves for it.
25const SKIN_DEGREE: usize = 3;
26
27/// Where a family of curves is to cross the curves of the other family, on
28/// the domain `[0, 1]` they are brought onto: per crossed curve, where each
29/// of the family crosses it, as a fraction of its stretch between its first
30/// and last crossing, averaged over the family. Which parameters is a free
31/// choice — any increasing ones make the same surface pass through the
32/// same curves — and the average keeps each curve's own speed nearly as it
33/// was, so the parameters are sharpened, and the ends are exactly 0 and 1.
34fn common_parameters(crossings: &[Vec<Scalar>]) -> GeopResult<Vec<Scalar>> {
35    let n = crossings[0].len();
36    let count = Scalar::from_i64(crossings.len() as i64);
37    let mut params = vec![Scalar::ZERO; n];
38    params[n - 1] = Scalar::ONE;
39    for (i, param) in params.iter_mut().enumerate().take(n - 1).skip(1) {
40        let mut sum = Scalar::ZERO;
41        for c in crossings {
42            sum = sum.add(c[i].sub(c[0]).div(c[n - 1].sub(c[0]))?);
43        }
44        *param = sum.div(count)?.sharpen();
45    }
46    if !params.windows(2).all(|w| w[0].definitely_less(w[1])) {
47        return Err(GeopError::new(format!(
48            "NurbSurface::gordon: the crossings {crossings:?} do not each run in one order"
49        )));
50    }
51    Ok(params)
52}
53
54/// The surface skinned through `rows` — compatible curves along `u`, on
55/// one knot vector — the `j`-th at `v = params[j]`: each column of their
56/// control points interpolated across them (see
57/// [`NurbCurve::interpolate_homogeneous`]). Its first and last rows are the
58/// first and last curves' control points: that is what the interpolation
59/// gives there exactly, and taking them as they are leaves out its
60/// rounding.
61fn skin(rows: &[NurbCurve3D], params: &[Scalar]) -> GeopResult<NurbSurface3D> {
62    let first = &rows[0];
63    let nu = first.control_points.len();
64    let mut columns = Vec::with_capacity(nu);
65    for k in 0..nu {
66        let values: Vec<Vector4> = rows.iter().map(|r| r.control_points[k]).collect();
67        let mut column = NurbCurve::interpolate_homogeneous(&values, params, SKIN_DEGREE)?;
68        let last = column.control_points.len() - 1;
69        column.control_points[0] = values[0];
70        column.control_points[last] = values[values.len() - 1];
71        columns.push(column);
72    }
73    let nv = columns[0].control_points.len();
74    let control_points = (0..nu)
75        .flat_map(|i| (0..nv).map(move |j| (i, j)))
76        .map(|(i, j)| columns[i].control_points[j])
77        .collect();
78    NurbSurface3D::try_new(
79        first.degree,
80        columns[0].degree,
81        control_points,
82        first.knot_vector.clone(),
83        columns[0].knot_vector.clone(),
84    )
85}
86
87impl NurbSurface3D {
88    /// The same surface with `u` and `v` swapped.
89    pub(crate) fn transposed(&self) -> Self {
90        let (nu, nv) = (self.num_u, self.num_v);
91        let control_points = (0..nv)
92            .flat_map(|j| (0..nu).map(move |i| (i, j)))
93            .map(|(i, j)| self.control_points[i * nv + j])
94            .collect();
95        Self {
96            degree_u: self.degree_v,
97            degree_v: self.degree_u,
98            num_u: nv,
99            num_v: nu,
100            control_points,
101            knot_vector_u: self.knot_vector_v.clone(),
102            knot_vector_v: self.knot_vector_u.clone(),
103            aabb: self.aabb,
104        }
105    }
106
107    /// Its rows of control points along `u`, as curves, one per `v` index.
108    fn rows_along_u(&self) -> GeopResult<Vec<NurbCurve3D>> {
109        (0..self.num_v)
110            .map(|j| {
111                let points = (0..self.num_u)
112                    .map(|i| self.control_points[i * self.num_v + j])
113                    .collect();
114                NurbCurve::try_new(self.degree_u, points, self.knot_vector_u.clone())
115            })
116            .collect()
117    }
118
119    /// `surfaces`, all on one domain, made compatible along `u` (see
120    /// [`NurbCurve::compatible`]): one degree and one knot vector, every
121    /// surface still the surface it was.
122    fn compatible_along_u(surfaces: &[Self]) -> GeopResult<Vec<Self>> {
123        let mut rows = Vec::new();
124        for s in surfaces {
125            rows.extend(s.rows_along_u()?);
126        }
127        let mut rows = NurbCurve::compatible(&rows)?.into_iter();
128        surfaces
129            .iter()
130            .map(|s| {
131                let curves: Vec<_> = rows.by_ref().take(s.num_v).collect();
132                let num_u = curves[0].control_points.len();
133                let control_points = (0..num_u)
134                    .flat_map(|i| (0..s.num_v).map(move |j| (i, j)))
135                    .map(|(i, j)| curves[j].control_points[i])
136                    .collect();
137                NurbSurface3D::try_new(
138                    curves[0].degree,
139                    s.degree_v,
140                    control_points,
141                    curves[0].knot_vector.clone(),
142                    s.knot_vector_v.clone(),
143                )
144            })
145            .collect()
146    }
147
148    /// The Gordon surface through a network of curves on `[0, 1]²`: the
149    /// curves `u_curves` along `u`, in order of increasing `v`, and
150    /// `v_curves` along `v`, in order of increasing `u`, each `u` curve
151    /// crossing each `v` curve once. `u_crossings[j][i]` is the parameter of
152    /// `u_curves[j]` where it crosses `v_curves[i]`, `v_crossings[i][j]`
153    /// that of `v_curves[i]` there; both increase along each curve. Two
154    /// curves at least in each direction.
155    ///
156    /// Each curve is reparametrized (see [`NurbCurve::reparametrized`]) so
157    /// that the curves cross at common parameters, chosen as
158    /// `common_parameters` says, and cut to the stretch between its first
159    /// and last crossing: the surface's sides are the first and last curves
160    /// of each family. The crossing points are each the union of where the
161    /// two curves have them — an error naming both if they could not be
162    /// one point. The surface is then the sum of the two skinned surfaces
163    /// less the tensor-product one, each of degree three across the curves,
164    /// or lower for fewer than four. Its border rows are the outer curves'
165    /// control points, as [`NurbSurface3D::coons`]' are, so it interpolates
166    /// them exactly, and with two curves each way it is their Coons patch.
167    ///
168    /// Across a crossed curve the surface is as smooth as the curves'
169    /// reparametrizations: where they cross every curve at the same
170    /// fraction of its length, smooth; otherwise each curve's speed changes
171    /// there, and the surface may bend across it.
172    pub fn gordon(
173        u_curves: &[NurbCurve3D],
174        v_curves: &[NurbCurve3D],
175        u_crossings: &[Vec<Scalar>],
176        v_crossings: &[Vec<Scalar>],
177    ) -> GeopResult<Self> {
178        let (m, n) = (u_curves.len(), v_curves.len());
179        let shaped = |crossings: &[Vec<Scalar>], curves: usize, each: usize| {
180            crossings.len() == curves && crossings.iter().all(|c| c.len() == each)
181        };
182        if m < 2 || n < 2 || !shaped(u_crossings, m, n) || !shaped(v_crossings, n, m) {
183            return Err(GeopError::new(format!(
184                "NurbSurface::gordon: {m} u curves and {n} v curves, two at least each way, need the crossings of each with every other, not {u_crossings:?} and {v_crossings:?}"
185            )));
186        }
187        let u_params = common_parameters(u_crossings)?;
188        let v_params = common_parameters(v_crossings)?;
189        let reparametrized =
190            |curves: &[NurbCurve3D], crossings: &[Vec<Scalar>], params: &[Scalar]| {
191                curves
192                    .iter()
193                    .zip(crossings)
194                    .map(|(c, x)| c.reparametrized(x, params))
195                    .collect::<GeopResult<Vec<_>>>()
196            };
197        let u = reparametrized(u_curves, u_crossings, &u_params)?;
198        let v = reparametrized(v_curves, v_crossings, &v_params)?;
199
200        // The crossings, `points[j][i]` where `u[j]` crosses `v[i]`, of
201        // weight one: every curve's weight is one at its crossings.
202        let mut points = Vec::with_capacity(m);
203        for (j, uj) in u.iter().enumerate() {
204            let mut row = Vec::with_capacity(n);
205            for (i, vi) in v.iter().enumerate() {
206                let a = uj.evaluate(u_params[i])?;
207                let b = vi.evaluate(v_params[j])?;
208                if !a.could_be_equal(&b) {
209                    return Err(GeopError::new(format!(
210                        "NurbSurface::gordon: u curve {j} is at {a:?} where it crosses v curve {i}, which is at {b:?}"
211                    )));
212                }
213                let p: Vector3 = a.union(&b);
214                row.push(p.homogeneous(Scalar::ONE));
215            }
216            points.push(row);
217        }
218        let rational = u.iter().chain(&v).any(|c| {
219            c.control_points
220                .iter()
221                .any(|p| !p[3].is_exactly(Scalar::ONE))
222        });
223
224        let along_u = skin(&NurbCurve::compatible(&u)?, &v_params)?;
225        let along_v = skin(&NurbCurve::compatible(&v)?, &u_params)?.transposed();
226        let through_points = {
227            let rows = points
228                .iter()
229                .map(|row| NurbCurve::interpolate_homogeneous(row, &u_params, SKIN_DEGREE))
230                .collect::<GeopResult<Vec<_>>>()?;
231            skin(&rows, &v_params)?
232        };
233        let surfaces = Self::compatible_along_u(&[along_u, along_v, through_points])?;
234        let transposed: Vec<Self> = surfaces.iter().map(Self::transposed).collect();
235        let surfaces: Vec<Self> = Self::compatible_along_u(&transposed)?
236            .iter()
237            .map(Self::transposed)
238            .collect();
239        let [along_u, along_v, through_points] = &surfaces[..] else {
240            unreachable!("three surfaces");
241        };
242
243        let (nu, nv) = (along_u.num_u, along_u.num_v);
244        let mut control_points = Vec::with_capacity(nu * nv);
245        for i in 0..nu {
246            for j in 0..nv {
247                let k = i * nv + j;
248                let (on_u_side, on_v_side) = (j == 0 || j == nv - 1, i == 0 || i == nu - 1);
249                let mut p = match (on_u_side, on_v_side) {
250                    (true, true) => along_u.control_points[k].union(&along_v.control_points[k]),
251                    (true, false) => along_u.control_points[k],
252                    (false, true) => along_v.control_points[k],
253                    (false, false) => along_u.control_points[k]
254                        .add(&along_v.control_points[k])
255                        .sub(&through_points.control_points[k]),
256                };
257                if !rational {
258                    // Every weight is one, and so is every blend of them:
259                    // the arithmetic above would only round it.
260                    p[3] = Scalar::ONE;
261                }
262                control_points.push(p);
263            }
264        }
265        NurbSurface3D::try_new(
266            along_u.degree_u,
267            along_u.degree_v,
268            control_points,
269            along_u.knot_vector_u.clone(),
270            along_u.knot_vector_v.clone(),
271        )
272    }
273}
274
275#[cfg(test)]
276mod tests;