Skip to main content

geop_core_geometry/nurb_surface/
split.rs

1use geop_core_math::{
2    geop_error::{GeopError, GeopResult},
3    scalars::Scalar,
4    vector::Vector,
5};
6
7use super::NurbSurface;
8use crate::{aabb::compute_aabb, knot_insertion, nurb_curve::NurbCurve};
9
10/// A parameter direction of a tensor-product surface.
11#[derive(Clone, Copy)]
12enum Dir {
13    U,
14    V,
15}
16
17impl<const D: usize> NurbSurface<D> {
18    fn degree_in(&self, dir: Dir) -> usize {
19        match dir {
20            Dir::U => self.degree_u,
21            Dir::V => self.degree_v,
22        }
23    }
24
25    fn knots_in(&self, dir: Dir) -> &Vec<Scalar> {
26        match dir {
27            Dir::U => &self.knot_vector_u,
28            Dir::V => &self.knot_vector_v,
29        }
30    }
31
32    /// The control net as rows running along `dir` (see `knot_insertion`):
33    /// the `num_v` columns `P[·][j]` for `U`, the `num_u` rows `P[i][·]` for
34    /// `V`.
35    fn rows_along(&self, dir: Dir) -> Vec<Vec<Vector<D>>> {
36        let (nu, nv) = (self.num_u, self.num_v);
37        match dir {
38            Dir::U => (0..nv)
39                .map(|j| (0..nu).map(|i| self.control_points[i * nv + j]).collect())
40                .collect(),
41            Dir::V => self.control_points.chunks(nv).map(<[_]>::to_vec).collect(),
42        }
43    }
44
45    /// This surface with the `dir` knots and rows replaced — the inverse of
46    /// [`Self::rows_along`].
47    fn with_rows(&self, dir: Dir, knots: Vec<Scalar>, rows: Vec<Vec<Vector<D>>>) -> Self {
48        let along = rows[0].len();
49        let (num_u, num_v, control_points): (usize, usize, Vec<Vector<D>>) = match dir {
50            Dir::U => (
51                along,
52                rows.len(),
53                (0..along)
54                    .flat_map(|i| rows.iter().map(move |col| col[i]))
55                    .collect(),
56            ),
57            Dir::V => (rows.len(), along, rows.into_iter().flatten().collect()),
58        };
59        let (knot_vector_u, knot_vector_v) = match dir {
60            Dir::U => (knots, self.knot_vector_v.clone()),
61            Dir::V => (self.knot_vector_u.clone(), knots),
62        };
63        NurbSurface {
64            degree_u: self.degree_u,
65            degree_v: self.degree_v,
66            num_u,
67            num_v,
68            aabb: compute_aabb(&control_points),
69            control_points,
70            knot_vector_u,
71            knot_vector_v,
72        }
73    }
74
75    fn split_in(&self, dir: Dir, t: Scalar) -> GeopResult<(Self, Self)> {
76        let mut knots = self.knots_in(dir).clone();
77        let mut rows = self.rows_along(dir);
78        let (right_knots, right_rows) =
79            knot_insertion::split(&mut knots, &mut rows, self.degree_in(dir), t)?;
80        Ok((
81            self.with_rows(dir, knots, rows),
82            self.with_rows(dir, right_knots, right_rows),
83        ))
84    }
85
86    fn restrict_in(&self, dir: Dir, t0: Scalar, t1: Scalar) -> GeopResult<Self> {
87        let mut knots = self.knots_in(dir).clone();
88        let mut rows = self.rows_along(dir);
89        knot_insertion::restrict(&mut knots, &mut rows, self.degree_in(dir), t0, t1)?;
90        Ok(self.with_rows(dir, knots, rows))
91    }
92
93    /// Split at parameter `t` in the **u** direction.
94    ///
95    /// `t` must lie strictly inside the u domain.  Returns `(left, right)`.
96    ///
97    /// `t` is used exactly as given — **not** sharpened here, see `NurbCurve::split`'s own
98    /// doc comment for why (this is the same Boehm-insertion construction,
99    /// one dimension up: an unsharpened `t` carried into the new knot
100    /// vector lets `alpha = (t - e) / (s - e)` blow up over repeated splits
101    /// as `s - e` shrinks while `t`'s own width doesn't). Every current caller
102    /// therefore sharpens its own midpoint before calling; a caller splitting
103    /// at a *located* parameter must validate the sharpened value it is about
104    /// to use, not the wide one it started from.
105    pub fn split_u(&self, t: Scalar) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)> {
106        self.split_in(Dir::U, t)
107    }
108
109    /// Split at parameter `t` in the **v** direction.
110    ///
111    /// `t` must lie strictly inside the v domain.  Returns `(left, right)`.
112    /// `t` is used exactly as given — **not** sharpened here, same as [`Self::split_u`].
113    pub fn split_v(&self, t: Scalar) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)> {
114        self.split_in(Dir::V, t)
115    }
116
117    fn split_in_mid(&self, dir: Dir) -> GeopResult<(Self, Self)> {
118        let knots = self.knots_in(dir);
119        let n = match dir {
120            Dir::U => self.num_u,
121            Dir::V => self.num_v,
122        };
123        let (t0, t1) = (knots[self.degree_in(dir)], knots[n]);
124        // A self-chosen subdivision point: any value in the interval cuts
125        // it equally well, so sharpening loses no accuracy and keeps
126        // repeated splits from compounding width (see AGENTS.md).
127        let mid = t0.add(t1).div(Scalar::TWO)?.sharpen();
128        self.split_in(dir, mid)
129    }
130
131    /// The iso-line of the surface at `u = at` (`u_fixed`), or at `v = at`,
132    /// as a curve along the other parameter, on its knots: a row of the
133    /// control net, exactly, where `at` is a clamped end of the domain or a
134    /// knot of full multiplicity, and otherwise the row knot insertion
135    /// makes there. `at` is used exactly as given, as by [`Self::split_u`].
136    /// Fails for an `at` outside the domain, or at an end that is not
137    /// clamped.
138    pub fn iso_curve(&self, u_fixed: bool, at: Scalar) -> GeopResult<NurbCurve<D>> {
139        let (dir, other) = if u_fixed {
140            (Dir::U, Dir::V)
141        } else {
142            (Dir::V, Dir::U)
143        };
144        let (knots, degree) = (self.knots_in(dir), self.degree_in(dir));
145        let n = self.rows_along(dir)[0].len();
146        let (lo, hi) = (knots[degree], knots[n]);
147        let ends = |first: bool| -> GeopResult<Vec<Vector<D>>> {
148            if !knot_insertion::is_clamped_at(knots, degree, first) {
149                return Err(GeopError::new(format!(
150                    "NurbSurface::iso_curve: the domain is not clamped at {at:?}"
151                )));
152            }
153            let i = if first { 0 } else { n - 1 };
154            Ok(self.rows_along(dir).iter().map(|row| row[i]).collect())
155        };
156        let points = if at.is_exactly(lo) {
157            ends(true)?
158        } else if at.is_exactly(hi) {
159            ends(false)?
160        } else {
161            let (before, _) = self.split_in(dir, at)?;
162            let rows = before.rows_along(dir);
163            rows.iter().map(|row| row[row.len() - 1]).collect()
164        };
165        NurbCurve::try_new(self.degree_in(other), points, self.knots_in(other).clone())
166    }
167
168    /// Split at the midpoint of the **u** domain.
169    pub fn split_u_mid(&self) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)> {
170        self.split_in_mid(Dir::U)
171    }
172
173    /// Split at the midpoint of the **v** domain.
174    pub fn split_v_mid(&self) -> GeopResult<(NurbSurface<D>, NurbSurface<D>)> {
175        self.split_in_mid(Dir::V)
176    }
177
178    /// The same surface with every span split into `pieces[0]` along `u`
179    /// and `pieces[1]` along `v`, at sharp knots: more control points to
180    /// fit on.
181    pub(crate) fn refined(&self, pieces: [usize; 2]) -> GeopResult<Self> {
182        let mut out = self.clone();
183        for (dir, pieces) in [Dir::U, Dir::V].into_iter().zip(pieces) {
184            let degree = out.degree_in(dir);
185            let mut knots = out.knots_in(dir).clone();
186            let mut rows = out.rows_along(dir);
187            let breaks = crate::spline::breakpoints(&knots, degree, rows[0].len());
188            for pair in breaks.windows(2) {
189                for k in 1..pieces {
190                    let f = Scalar::from_ratio(k as i64, pieces as i64)?;
191                    knot_insertion::insert(
192                        &mut knots,
193                        &mut rows,
194                        degree,
195                        Scalar::fraction(pair[0], pair[1], f),
196                    );
197                }
198            }
199            out = out.with_rows(dir, knots, rows);
200        }
201        Ok(out)
202    }
203
204    /// The same surface with the knots `at` inserted along `u`, each once:
205    /// more control points to fit on where they are.
206    pub(crate) fn with_knots_u(&self, at: &[Scalar]) -> Self {
207        let mut knots = self.knot_vector_u.clone();
208        let mut rows = self.rows_along(Dir::U);
209        for &t in at {
210            knot_insertion::insert(&mut knots, &mut rows, self.degree_u, t);
211        }
212        self.with_rows(Dir::U, knots, rows)
213    }
214
215    /// This surface restricted to `u ∈ [u0, u1]`, `v ∈ [v0, v1]`, with the
216    /// same cut rule as `NurbCurve::sub_curve`: only bounds strictly inside
217    /// the domain are cut at, so the result covers at least the requested
218    /// box ∩ domain. Bounds are used exactly as given and must be narrow.
219    pub fn sub_surface(
220        &self,
221        (u0, u1): (Scalar, Scalar),
222        (v0, v1): (Scalar, Scalar),
223    ) -> GeopResult<Self> {
224        self.restrict_in(Dir::U, u0, u1)?
225            .restrict_in(Dir::V, v0, v1)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use crate::test_fixtures::bilinear;
232    use geop_core_math::scalars::Scalar;
233
234    #[test]
235    fn split_u_junction_matches() {
236        let orig = bilinear();
237        let (left, right) = orig.split_u(Scalar::from_f64(0.5)).unwrap();
238        for v_val in [0.0_f64, 0.5, 1.0] {
239            let v = Scalar::from_f64(v_val);
240            let o = orig.evaluate(Scalar::from_f64(0.5), v).unwrap();
241            let l = left.evaluate(Scalar::from_f64(0.5), v).unwrap();
242            let r = right.evaluate(Scalar::from_f64(0.5), v).unwrap();
243            for c in 0..3 {
244                assert!(
245                    o[c].could_be_equal(l[c]),
246                    "split_u left mismatch v={v_val} c={c}"
247                );
248                assert!(
249                    o[c].could_be_equal(r[c]),
250                    "split_u right mismatch v={v_val} c={c}"
251                );
252            }
253        }
254    }
255
256    #[test]
257    fn split_u_left_start_matches() {
258        let orig = bilinear();
259        let (left, _) = orig.split_u(Scalar::from_f64(0.5)).unwrap();
260        for v_val in [0.0_f64, 0.5, 1.0] {
261            let v = Scalar::from_f64(v_val);
262            let o = orig.evaluate(Scalar::ZERO, v).unwrap();
263            let l = left.evaluate(Scalar::ZERO, v).unwrap();
264            for c in 0..3 {
265                assert!(o[c].could_be_equal(l[c]), "c={c} v={v_val}");
266            }
267        }
268    }
269
270    #[test]
271    fn split_u_right_end_matches() {
272        let orig = bilinear();
273        let (_, right) = orig.split_u(Scalar::from_f64(0.5)).unwrap();
274        for v_val in [0.0_f64, 0.5, 1.0] {
275            let v = Scalar::from_f64(v_val);
276            let o = orig.evaluate(Scalar::ONE, v).unwrap();
277            let r = right.evaluate(Scalar::ONE, v).unwrap();
278            for c in 0..3 {
279                assert!(o[c].could_be_equal(r[c]), "c={c} v={v_val}");
280            }
281        }
282    }
283
284    #[test]
285    fn split_u_at_boundary_returns_err() {
286        let s = bilinear();
287        assert!(s.split_u(Scalar::ZERO).is_err());
288        assert!(s.split_u(Scalar::ONE).is_err());
289    }
290
291    #[test]
292    fn split_v_junction_matches() {
293        let orig = bilinear();
294        let (left, right) = orig.split_v(Scalar::from_f64(0.5)).unwrap();
295        for u_val in [0.0_f64, 0.5, 1.0] {
296            let u = Scalar::from_f64(u_val);
297            let o = orig.evaluate(u, Scalar::from_f64(0.5)).unwrap();
298            let l = left.evaluate(u, Scalar::from_f64(0.5)).unwrap();
299            let r = right.evaluate(u, Scalar::from_f64(0.5)).unwrap();
300            for c in 0..3 {
301                assert!(
302                    o[c].could_be_equal(l[c]),
303                    "split_v left mismatch u={u_val} c={c}"
304                );
305                assert!(
306                    o[c].could_be_equal(r[c]),
307                    "split_v right mismatch u={u_val} c={c}"
308                );
309            }
310        }
311    }
312
313    #[test]
314    fn split_v_left_start_matches() {
315        let orig = bilinear();
316        let (left, _) = orig.split_v(Scalar::from_f64(0.5)).unwrap();
317        for u_val in [0.0_f64, 0.5, 1.0] {
318            let u = Scalar::from_f64(u_val);
319            let o = orig.evaluate(u, Scalar::ZERO).unwrap();
320            let l = left.evaluate(u, Scalar::ZERO).unwrap();
321            for c in 0..3 {
322                assert!(o[c].could_be_equal(l[c]), "c={c} u={u_val}");
323            }
324        }
325    }
326
327    #[test]
328    fn split_v_right_end_matches() {
329        let orig = bilinear();
330        let (_, right) = orig.split_v(Scalar::from_f64(0.5)).unwrap();
331        for u_val in [0.0_f64, 0.5, 1.0] {
332            let u = Scalar::from_f64(u_val);
333            let o = orig.evaluate(u, Scalar::ONE).unwrap();
334            let r = right.evaluate(u, Scalar::ONE).unwrap();
335            for c in 0..3 {
336                assert!(o[c].could_be_equal(r[c]), "c={c} u={u_val}");
337            }
338        }
339    }
340
341    #[test]
342    fn split_v_at_boundary_returns_err() {
343        let s = bilinear();
344        assert!(s.split_v(Scalar::ZERO).is_err());
345        assert!(s.split_v(Scalar::ONE).is_err());
346    }
347
348    #[test]
349    fn split_u_domains() {
350        let orig = bilinear();
351        let (left, right) = orig.split_u(Scalar::from_f64(0.5)).unwrap();
352        let (lu_min, lu_max) = left.domain_u();
353        let (ru_min, ru_max) = right.domain_u();
354        assert!(lu_min.could_be_equal(Scalar::ZERO));
355        assert!(lu_max.could_be_equal(Scalar::from_f64(0.5)));
356        assert!(ru_min.could_be_equal(Scalar::from_f64(0.5)));
357        assert!(ru_max.could_be_equal(Scalar::ONE));
358    }
359
360    #[test]
361    fn split_v_domains() {
362        let orig = bilinear();
363        let (left, right) = orig.split_v(Scalar::from_f64(0.5)).unwrap();
364        let (lv_min, lv_max) = left.domain_v();
365        let (rv_min, rv_max) = right.domain_v();
366        assert!(lv_min.could_be_equal(Scalar::ZERO));
367        assert!(lv_max.could_be_equal(Scalar::from_f64(0.5)));
368        assert!(rv_min.could_be_equal(Scalar::from_f64(0.5)));
369        assert!(rv_max.could_be_equal(Scalar::ONE));
370    }
371}