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<S: Scalar, const D: usize> NurbCurve<S, 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    /// What `t` must be is *narrow*, not sharp. Boehm insertion cannot absorb
16    /// a wide parameter: its width flows into `alpha = (t - e) / (s - e)` — `s - e` shrinks with every successive
17    /// split while an unsharpened width does not, so their ratio widens
18    /// without bound — and on into the sub-curves' control points, until
19    /// downstream subdivision searches stop converging.
20    ///
21    /// Callers used to meet that by sharpening, which was a real geometric
22    /// error: it moved the cut to the interval's midpoint rather than the
23    /// point actually located, off by `|t_mid - t*| x |C'(t)|`, which is how
24    /// an edge endpoint ended up ~1e-8 from the vertex it was anchored to.
25    /// They now Newton-refine instead — see
26    /// [`NurbCurve::refine_parameter_at_point`] and
27    /// `intersection::curve_surface::refine_crossing` — which yields a
28    /// parameter that is narrow *and* still an honest enclosure. Subdivision
29    /// isolates the solution, Newton polishes it; neither does the other's
30    /// job. No caller on the split path sharpens any more.
31    ///
32    /// See "Sharpen only where the value is a free choice" in `AGENTS.md`.
33    pub fn split(&self, t: S) -> GeopResult<(NurbCurve<S, D>, NurbCurve<S, D>)> {
34        let p = self.degree;
35        let mut knots = self.knot_vector.clone();
36        let mut rows = [self.control_points.clone()];
37        let (right_knots, right_rows) = knot_insertion::split(&mut knots, &mut rows, p, t)?;
38        let [pts] = rows;
39        let right_pts = right_rows.into_iter().next().unwrap_or_default();
40        Ok((
41            NurbCurve {
42                aabb: compute_aabb(&pts),
43                degree: p,
44                control_points: pts,
45                knot_vector: knots,
46            },
47            NurbCurve {
48                aabb: compute_aabb(&right_pts),
49                degree: p,
50                control_points: right_pts,
51                knot_vector: right_knots,
52            },
53        ))
54    }
55
56    /// This curve restricted to `[t0, t1]`, cut in one pass: each bound that
57    /// lies strictly inside the domain (`definitely_greater` the start /
58    /// `definitely_less` the end) is inserted to full multiplicity and the
59    /// outside is dropped; a bound that doesn't is not cut at, so the result
60    /// always covers at least `[t0, t1] ∩ domain`. Cheaper than two
61    /// [`Self::split`]s: no discarded piece or intermediate curve is built.
62    ///
63    /// Like `split`, the bounds are used exactly as given and must be narrow.
64    pub fn sub_curve(&self, t0: S, t1: S) -> GeopResult<NurbCurve<S, D>> {
65        let p = self.degree;
66        let mut knots = self.knot_vector.clone();
67        let mut rows = [self.control_points.clone()];
68        knot_insertion::restrict(&mut knots, &mut rows, p, t0, t1)?;
69        let [pts] = rows;
70        Ok(NurbCurve {
71            aabb: compute_aabb(&pts),
72            degree: p,
73            control_points: pts,
74            knot_vector: knots,
75        })
76    }
77
78    /// Split at the midpoint of the parameter domain.
79    pub fn split_mid(&self) -> GeopResult<(NurbCurve<S, D>, NurbCurve<S, D>)> {
80        let (t0, t1) = self.domain();
81        // A self-chosen subdivision point: any value in the interval cuts
82        // it equally well, so sharpening loses no accuracy and keeps
83        // repeated splits from compounding width (see AGENTS.md).
84        let mid = t0.add(t1).div(S::TWO)?.sharpen();
85        self.split(mid)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use crate::nurb_curve::NurbCurve;
92    use geop_core_math::for_all_scalars;
93    use geop_core_math::{scalars::Scalar, vector::Vector4};
94
95    fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
96        Vector4::from_array([
97            S::from_f64(x),
98            S::from_f64(y),
99            S::from_f64(z),
100            S::from_f64(w),
101        ])
102    }
103
104    fn line<S: Scalar>() -> NurbCurve<S, 4> {
105        NurbCurve::try_new(
106            1,
107            vec![pt(0.0, 0.0, 0.0, 1.0), pt(1.0, 0.0, 0.0, 1.0)],
108            vec![
109                S::from_f64(0.0),
110                S::from_f64(0.0),
111                S::from_f64(1.0),
112                S::from_f64(1.0),
113            ],
114        )
115        .unwrap()
116    }
117
118    fn check_split_line_halves_domain<S: Scalar>() {
119        let (left, right) = line::<S>().split(S::from_f64(0.5)).unwrap();
120
121        let l0 = left.evaluate(S::ZERO).unwrap();
122        assert!(l0[0].could_be_equal(S::ZERO));
123
124        let l1 = left.evaluate(S::from_f64(0.5)).unwrap();
125        let r0 = right.evaluate(S::from_f64(0.5)).unwrap();
126        assert!(l1[0].could_be_equal(r0[0]));
127        assert!(l1[0].could_be_equal(S::from_f64(0.5)));
128
129        let r1 = right.evaluate(S::ONE).unwrap();
130        assert!(r1[0].could_be_equal(S::ONE));
131    }
132    #[test]
133    fn split_line_halves_domain() {
134        for_all_scalars!(check_split_line_halves_domain);
135    }
136
137    fn check_split_preserves_points_on_curve<S: Scalar>() {
138        let curve = line::<S>();
139        let (left, right) = curve.split(S::from_f64(0.25)).unwrap();
140
141        let orig = curve.evaluate(S::from_f64(0.1)).unwrap();
142        let from_left = left.evaluate(S::from_f64(0.1)).unwrap();
143        assert!(orig[0].could_be_equal(from_left[0]));
144
145        let orig2 = curve.evaluate(S::from_f64(0.75)).unwrap();
146        let from_right = right.evaluate(S::from_f64(0.75)).unwrap();
147        assert!(orig2[0].could_be_equal(from_right[0]));
148    }
149    #[test]
150    fn split_preserves_points_on_curve() {
151        for_all_scalars!(check_split_preserves_points_on_curve);
152    }
153
154    fn check_split_at_boundary_returns_err<S: Scalar>() {
155        let c = line::<S>();
156        assert!(c.split(S::ZERO).is_err());
157        assert!(c.split(S::ONE).is_err());
158    }
159    #[test]
160    fn split_at_boundary_returns_err() {
161        for_all_scalars!(check_split_at_boundary_returns_err);
162    }
163
164    fn check_split_cubic_at_existing_knot<S: Scalar>() {
165        let f = S::from_f64;
166        let curve = NurbCurve::try_new(
167            3,
168            vec![
169                pt(0.0, 0.0, 0.0, 1.0),
170                pt(0.25, 1.0, 0.0, 1.0),
171                pt(0.5, 0.0, 0.0, 1.0),
172                pt(0.75, 1.0, 0.0, 1.0),
173                pt(1.0, 0.0, 0.0, 1.0),
174            ],
175            vec![
176                f(0.0),
177                f(0.0),
178                f(0.0),
179                f(0.0),
180                f(0.5),
181                f(1.0),
182                f(1.0),
183                f(1.0),
184                f(1.0),
185            ],
186        )
187        .unwrap();
188
189        let t_split = f(0.5);
190        let (left, right) = curve.split(t_split).unwrap();
191
192        let orig_at_split = curve.evaluate(t_split).unwrap();
193        let left_at_split = left.evaluate(t_split).unwrap();
194        let right_at_split = right.evaluate(t_split).unwrap();
195
196        for c in 0..3 {
197            assert!(
198                orig_at_split[c].could_be_equal(left_at_split[c]),
199                "left junction mismatch at coord {c}"
200            );
201            assert!(
202                orig_at_split[c].could_be_equal(right_at_split[c]),
203                "right junction mismatch at coord {c}"
204            );
205        }
206    }
207    #[test]
208    fn split_cubic_at_existing_knot() {
209        for_all_scalars!(check_split_cubic_at_existing_knot);
210    }
211}