Skip to main content

geop_core_geometry/nurb_curve/
extend.rs

1//! Carrying a curve on past one of its ends: its end piece continued as the
2//! polynomial (or, rational, the projected polynomial) it already is.
3
4use geop_core_math::{
5    geop_error::{GeopError, GeopResult},
6    scalars::Scalar,
7    vector::Vector,
8};
9
10use super::NurbCurve;
11use crate::knot_insertion::{insert, is_clamped_at};
12
13/// The control points of a Bézier piece over local parameters `[0, s]`,
14/// for `points` its control points over `[0, 1]`: the left half of de
15/// Casteljau's subdivision at `s`, which for `s > 1` extrapolates.
16fn continued<const D: usize>(points: &[Vector<D>], s: Scalar) -> Vec<Vector<D>> {
17    let mut level = points.to_vec();
18    let mut out = vec![level[0]];
19    for r in 1..points.len() {
20        for i in 0..points.len() - r {
21            level[i] = Vector::interpolate(&level[i], &level[i + 1], s);
22        }
23        out.push(level[0]);
24    }
25    out
26}
27
28/// The control points of the Bézier piece over local parameters `[1, s]`,
29/// for `points` its control points over `[0, 1]`: the piece the curve is
30/// carried on by, past where it ended. Over `[0, s]` first ([`continued`]),
31/// then the right half of that split at `1 / s` — the last point of each
32/// level of de Casteljau's scheme.
33fn beyond<const D: usize>(points: &[Vector<D>], s: Scalar) -> GeopResult<Vec<Vector<D>>> {
34    let mut level = continued(points, s);
35    let t = Scalar::ONE.div(s)?;
36    let n = level.len();
37    let mut out = vec![level[n - 1]];
38    for r in 1..n {
39        for i in 0..n - r {
40            level[i] = Vector::interpolate(&level[i], &level[i + 1], t);
41        }
42        out.push(level[n - r - 1]);
43    }
44    out.reverse();
45    Ok(out)
46}
47
48impl<const D: usize> NurbCurve<D> {
49    /// The same curve carried on by `by` of its parameter past its end
50    /// (`at_end`) or before its start: its end piece continued as the
51    /// polynomial it is, so the curve stays as smooth as it was where it
52    /// used to end. The domain grows by `by` on that side; the rest of the
53    /// curve keeps its parametrization.
54    ///
55    /// The end piece is isolated as a Bézier piece by knot insertion, then
56    /// subdivided at a parameter past its end — de Casteljau extrapolates
57    /// as exactly as it interpolates, on the homogeneous control points, so
58    /// a rational curve continues as itself: an arc continues round its
59    /// circle. The curve as it was is kept as it was, control point for
60    /// control point, and the continuation joined on as a piece of its own
61    /// at a knot of full multiplicity: where it used to run, the carried-on
62    /// curve is exactly as narrow as before. Fails for a knot vector not clamped at that end, and where
63    /// the continued weights stop being positive — an arc carried round
64    /// too far.
65    pub fn extended(&self, at_end: bool, by: Scalar) -> GeopResult<Self> {
66        let p = self.degree;
67        if !is_clamped_at(&self.knot_vector, p, !at_end) {
68            return Err(GeopError::new(format!(
69                "NurbCurve::extended: the knot vector {:?} is not clamped at the end to extend",
70                self.knot_vector
71            )));
72        }
73        if !by.definitely_greater(Scalar::ZERO) {
74            return Err(GeopError::new(format!(
75                "NurbCurve::extended: extending by {by:?}, which is not greater than zero"
76            )));
77        }
78        if !at_end {
79            // Mirrored: the start continued is the end of the reversed
80            // curve continued, which puts the domain's new start at the
81            // reversed curve's new end.
82            let (t0, t1) = self.domain();
83            let reversed = self.reverse().extended(true, by)?;
84            let mut out = reversed.reverse();
85            // `reverse` keeps the domain `[t0, t1 + by]`; shift it to
86            // `[t0 - by, t1]`.
87            for k in &mut out.knot_vector {
88                *k = k.sub(by);
89            }
90            debug_assert!(
91                out.domain().1.could_be_equal(t1) && out.domain().0.could_be_equal(t0.sub(by))
92            );
93            return Ok(out);
94        }
95        let n = self.control_points.len();
96        let end = self.knot_vector[n];
97        // Where the end piece starts: the last knot before the end.
98        let start = self.knot_vector[..n]
99            .iter()
100            .rev()
101            .find(|k| k.definitely_less(end))
102            .copied()
103            .ok_or_else(|| GeopError::new("NurbCurve::extended: the curve has no span"))?;
104        let mut knots = self.knot_vector.clone();
105        let mut rows = [self.control_points.clone()];
106        let multiplicity = knots.iter().filter(|k| k.could_be_equal(start)).count();
107        for _ in multiplicity.min(p)..p {
108            insert(&mut knots, &mut rows, p, start);
109        }
110        let [isolated] = rows;
111        let m = isolated.len();
112        let s = end.add(by).sub(start).div(end.sub(start))?;
113        let tail = beyond(&isolated[m - p - 1..], s)?;
114        // The curve's last control point and the continuation's first are
115        // both its end point, two enclosures of it: what they share.
116        let mut points = self.control_points.clone();
117        let last = points.last_mut().expect("a curve has control points");
118        if !last.could_be_equal(&tail[0]) {
119            return Err(GeopError::new(format!(
120                "NurbCurve::extended: the continuation starts at {:?}, not at the end {last:?}",
121                tail[0]
122            )));
123        }
124        *last = Vector::from_array(std::array::from_fn(|k| last[k].intersect(tail[0][k])));
125        points.extend_from_slice(&tail[1..]);
126        let mut knots = self.knot_vector[..self.knot_vector.len() - 1].to_vec();
127        knots.extend(std::iter::repeat_n(end.add(by), p + 1));
128        NurbCurve::try_new(p, points, knots)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use crate::test_fixtures::weighted;
135    use geop_core_math::scalars::Scalar;
136
137    use crate::nurb_curve::{NurbCurve, NurbCurve3D};
138
139    /// A cubic with an interior knot, continued at either end: the same
140    /// curve where it was, and on its own polynomial beyond.
141    #[test]
142    fn cubic_continues_as_itself() {
143        let f = Scalar::from_f64;
144        let curve: NurbCurve3D = NurbCurve::try_new(
145            3,
146            vec![
147                weighted(0., 0., 0., 1.),
148                weighted(1., 2., 0., 1.),
149                weighted(2., -1., 1., 1.),
150                weighted(3., 1., 0., 1.),
151                weighted(4., 0., 2., 1.),
152            ],
153            [0., 0., 0., 0., 0.5, 1., 1., 1., 1.].map(f).to_vec(),
154        )
155        .unwrap();
156        let longer = curve.extended(true, f(0.25)).unwrap();
157        let (t0, t1) = longer.domain();
158        assert!(t0.could_be_equal(f(0.)) && t1.could_be_equal(f(1.25)));
159        for i in 0..=8 {
160            let t = Scalar::from_ratio(i, 8).unwrap();
161            assert!(
162                longer
163                    .evaluate(t)
164                    .unwrap()
165                    .could_be_equal(&curve.evaluate(t).unwrap())
166            );
167        }
168        // The last piece's polynomial, from the original piece's control
169        // points at a parameter past it, by de Casteljau directly.
170        let earlier = curve.extended(false, f(0.5)).unwrap();
171        let (t0, t1) = earlier.domain();
172        assert!(t0.could_be_equal(f(-0.5)) && t1.could_be_equal(f(1.)));
173        for i in 0..=8 {
174            let t = Scalar::from_ratio(i, 8).unwrap();
175            assert!(
176                earlier
177                    .evaluate(t)
178                    .unwrap()
179                    .could_be_equal(&curve.evaluate(t).unwrap())
180            );
181        }
182    }
183
184    /// A quarter circle continued at its end stays on its circle.
185    #[test]
186    fn arc_continues_round_its_circle() {
187        let f = Scalar::from_f64;
188        let arc: NurbCurve3D = NurbCurve::try_new(
189            2,
190            vec![
191                weighted(1., 0., 0., 1.),
192                weighted(1., 1., 0., std::f64::consts::SQRT_2 / 2.0),
193                weighted(0., 1., 0., 1.),
194            ],
195            [0., 0., 0., 1., 1., 1.].map(f).to_vec(),
196        )
197        .unwrap();
198        let longer = arc.extended(true, f(0.3)).unwrap();
199        for i in 0..=13 {
200            let t = Scalar::from_ratio(i, 10).unwrap();
201            let p = longer.evaluate(t).unwrap();
202            assert!(p.norm_sq().could_be_equal(Scalar::ONE), "{t:?}: {p:?}");
203        }
204    }
205}