Skip to main content

geop_core_geometry/nurb_curve/
split.rs

1use geop_core_math::{geop_error::GeopResult, scalars::Scalar};
2
3use super::NurbCurve;
4use crate::{aabb::compute_aabb, knot_insertion};
5
6impl<const D: usize> NurbCurve<D> {
7    /// Split at parameter `t` (must be strictly inside the domain).
8    /// Returns `(left, right)` sharing the junction value.
9    ///
10    /// `t` is used exactly as given — it is **not** sharpened here. Whether
11    /// `t`'s width may be discarded belongs to whoever produced it, since
12    /// only they know what it means, so this function neither assumes nor
13    /// imposes an answer.
14    ///
15    /// A wide `t` gives every piece the curve could be cut into there, in
16    /// one: its width flows into `alpha = (t - e) / (s - e)`, into the
17    /// pieces' control points and into their end knot. A piece cut again
18    /// is then found wider at the next cut, and its pieces wider still.
19    /// To end a piece at a point, cut at a sharp parameter of the refined
20    /// enclosure of where the curve reaches it
21    /// ([`NurbCurve::split_parameter_at_point`]): which one is a free
22    /// choice, and the pieces then carry no more than the rounding of
23    /// the cut.
24    ///
25    /// See "Sharpen only where the value is a free choice" in `AGENTS.md`.
26    pub fn split(&self, t: Scalar) -> GeopResult<(NurbCurve<D>, NurbCurve<D>)> {
27        let p = self.degree;
28        let mut knots = self.knot_vector.clone();
29        let mut rows = [self.control_points.clone()];
30        let (right_knots, right_rows) = knot_insertion::split(&mut knots, &mut rows, p, t)?;
31        let [pts] = rows;
32        let right_pts = right_rows.into_iter().next().unwrap_or_default();
33        Ok((
34            NurbCurve {
35                aabb: compute_aabb(&pts),
36                degree: p,
37                control_points: pts,
38                knot_vector: knots,
39            },
40            NurbCurve {
41                aabb: compute_aabb(&right_pts),
42                degree: p,
43                control_points: right_pts,
44                knot_vector: right_knots,
45            },
46        ))
47    }
48
49    /// This curve restricted to `[t0, t1]`, cut in one pass: each bound that
50    /// lies strictly inside the domain (`definitely_greater` the start /
51    /// `definitely_less` the end) is inserted to full multiplicity and the
52    /// outside is dropped; a bound that doesn't is not cut at, so the result
53    /// always covers at least `[t0, t1] ∩ domain`. Cheaper than two
54    /// [`Self::split`]s: no discarded piece or intermediate curve is built.
55    ///
56    /// Like `split`, the bounds are used exactly as given and must be narrow.
57    pub fn sub_curve(&self, t0: Scalar, t1: Scalar) -> GeopResult<NurbCurve<D>> {
58        let p = self.degree;
59        let mut knots = self.knot_vector.clone();
60        let mut rows = [self.control_points.clone()];
61        knot_insertion::restrict(&mut knots, &mut rows, p, t0, t1)?;
62        let [pts] = rows;
63        Ok(NurbCurve {
64            aabb: compute_aabb(&pts),
65            degree: p,
66            control_points: pts,
67            knot_vector: knots,
68        })
69    }
70
71    /// The curve cut at every interior knot: its polynomial (or rational)
72    /// pieces, in order.
73    pub fn bezier_pieces(&self) -> GeopResult<Vec<Self>> {
74        let end = self.domain().1;
75        let interior = &self.knot_vector[self.degree + 1..self.control_points.len()];
76        let mut rest = self.clone();
77        let mut pieces = Vec::new();
78        for &k in interior {
79            if k.definitely_greater(rest.domain().0) && k.definitely_less(end) {
80                let (left, right) = rest.split(k)?;
81                pieces.push(left);
82                rest = right;
83            }
84        }
85        pieces.push(rest);
86        Ok(pieces)
87    }
88
89    /// Split at the midpoint of the parameter domain.
90    pub fn split_mid(&self) -> GeopResult<(NurbCurve<D>, NurbCurve<D>)> {
91        // A self-chosen subdivision point, sharp, so repeated splits do not
92        // compound width.
93        self.split(self.mid_parameter()?)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use crate::nurb_curve::NurbCurve;
100    use crate::test_fixtures::{hom, line};
101    use geop_core_math::scalars::Scalar;
102
103    #[test]
104    fn split_line_halves_domain() {
105        let (left, right) = line([0.; 3], [1., 0., 0.])
106            .split(Scalar::from_f64(0.5))
107            .unwrap();
108
109        let l0 = left.evaluate(Scalar::ZERO).unwrap();
110        assert!(l0[0].could_be_equal(Scalar::ZERO));
111
112        let l1 = left.evaluate(Scalar::from_f64(0.5)).unwrap();
113        let r0 = right.evaluate(Scalar::from_f64(0.5)).unwrap();
114        assert!(l1[0].could_be_equal(r0[0]));
115        assert!(l1[0].could_be_equal(Scalar::from_f64(0.5)));
116
117        let r1 = right.evaluate(Scalar::ONE).unwrap();
118        assert!(r1[0].could_be_equal(Scalar::ONE));
119    }
120
121    #[test]
122    fn split_preserves_points_on_curve() {
123        let curve = line([0.; 3], [1., 0., 0.]);
124        let (left, right) = curve.split(Scalar::from_f64(0.25)).unwrap();
125
126        let orig = curve.evaluate(Scalar::from_f64(0.1)).unwrap();
127        let from_left = left.evaluate(Scalar::from_f64(0.1)).unwrap();
128        assert!(orig[0].could_be_equal(from_left[0]));
129
130        let orig2 = curve.evaluate(Scalar::from_f64(0.75)).unwrap();
131        let from_right = right.evaluate(Scalar::from_f64(0.75)).unwrap();
132        assert!(orig2[0].could_be_equal(from_right[0]));
133    }
134
135    #[test]
136    fn split_at_boundary_returns_err() {
137        let c = line([0.; 3], [1., 0., 0.]);
138        assert!(c.split(Scalar::ZERO).is_err());
139        assert!(c.split(Scalar::ONE).is_err());
140    }
141
142    /// A quarter of a disc's rim cut again and again, as slots cut into a
143    /// disc one after another cut it: each cut where a plane crosses the
144    /// piece left by the cut before, at the vertex the piece gives there,
145    /// as `Model::split_edge_at_vertex` cuts. Cut once, the piece is
146    /// `2e-15` wide; each further cut adds one subdivision's rounding and
147    /// nothing more. Cut at the whole refined parameter, the width grew
148    /// threefold per cut, to `0.08` by the 27th, and the 28th failed.
149    #[test]
150    fn a_rim_cut_many_times_stays_as_narrow_as_cut_once() {
151        use crate::contains::curve::curve_could_contain;
152        use geop_core_math::vector::Vector3;
153        let f = Scalar::from_f64;
154        let v = |x: f64, y: f64, z: f64| Vector3::from_array([f(x), f(y), f(z)]);
155        let mut piece = NurbCurve::<4>::arc(
156            v(0.0, -4.0, 1.0),
157            v(4.0, -4.0, 1.0),
158            v(4.0, 0.0, 1.0),
159            crate::nurb_curve::sqrt2_over_2(),
160        )
161        .unwrap();
162        for k in 1..=40 {
163            let angle = -std::f64::consts::FRAC_PI_2 * (1.0 - k as f64 / 41.0);
164            let crossing = v(4.0 * angle.cos(), 4.0 * angle.sin(), 1.0);
165            let t = curve_could_contain(&piece, &crossing, 10_000, f(1e-4))
166                .unwrap()
167                .unwrap();
168            let t = piece.refine_parameter_at_point(t, &crossing);
169            let vertex = piece.evaluate(t).unwrap();
170            let t = piece.split_parameter_at_point(t, &vertex, &vertex).unwrap();
171            piece = piece.split(t).unwrap().1;
172            let widest = piece
173                .control_points
174                .iter()
175                .flat_map(|p| (0..4).map(move |c| p[c].width().to_f64()))
176                .fold(0.0, f64::max);
177            assert!(widest < 1e-13, "cut {k}: {widest:e}");
178            let z = piece.evaluate(piece.mid_parameter().unwrap()).unwrap()[2];
179            assert!(
180                z.could_be_equal(Scalar::ONE) && z.width().to_f64() < 1e-13,
181                "cut {k}: {z:?}"
182            );
183        }
184    }
185
186    /// A quadratic joined at a knot of multiplicity two that is an
187    /// enclosure, as a curve carried on past its end is: cut back at a
188    /// sharp parameter the knot could be, it starts at that knot repeated,
189    /// not at two enclosures of it which a file of knots and
190    /// multiplicities would write as two decreasing knots (a cup's rim,
191    /// shelled, read back from STEP).
192    #[test]
193    fn cut_at_a_knot_it_could_be_repeats_that_knot() {
194        let f = Scalar::from_f64;
195        let joint = f(0.5 - 1e-15).union(f(0.5 + 1e-15));
196        let curve = NurbCurve::try_new(
197            2,
198            vec![
199                hom(0.0, 0.0, 0.0, 1.0),
200                hom(1.0, 1.0, 0.0, 1.0),
201                hom(2.0, 0.0, 0.0, 1.0),
202                hom(3.0, 1.0, 0.0, 1.0),
203                hom(4.0, 0.0, 0.0, 1.0),
204            ],
205            vec![f(0.0), f(0.0), f(0.0), joint, joint, f(1.0), f(1.0), f(1.0)],
206        )
207        .unwrap();
208        let back = curve.sub_curve(f(0.5 + 2e-16), f(1.0)).unwrap();
209        assert_eq!(back.knot_vector[..3], [joint; 3]);
210        assert_eq!(back.control_points[0], curve.control_points[2]);
211    }
212
213    #[test]
214    fn split_cubic_at_existing_knot() {
215        let f = Scalar::from_f64;
216        let curve = NurbCurve::try_new(
217            3,
218            vec![
219                hom(0.0, 0.0, 0.0, 1.0),
220                hom(0.25, 1.0, 0.0, 1.0),
221                hom(0.5, 0.0, 0.0, 1.0),
222                hom(0.75, 1.0, 0.0, 1.0),
223                hom(1.0, 0.0, 0.0, 1.0),
224            ],
225            vec![
226                f(0.0),
227                f(0.0),
228                f(0.0),
229                f(0.0),
230                f(0.5),
231                f(1.0),
232                f(1.0),
233                f(1.0),
234                f(1.0),
235            ],
236        )
237        .unwrap();
238
239        let t_split = f(0.5);
240        let (left, right) = curve.split(t_split).unwrap();
241
242        let orig_at_split = curve.evaluate(t_split).unwrap();
243        let left_at_split = left.evaluate(t_split).unwrap();
244        let right_at_split = right.evaluate(t_split).unwrap();
245
246        for c in 0..3 {
247            assert!(
248                orig_at_split[c].could_be_equal(left_at_split[c]),
249                "left junction mismatch at coord {c}"
250            );
251            assert!(
252                orig_at_split[c].could_be_equal(right_at_split[c]),
253                "right junction mismatch at coord {c}"
254            );
255        }
256    }
257}