Skip to main content

geop_core_geometry/nurb_curve/
compatible.rs

1//! Making curves *compatible* — one degree, one knot vector — so that they
2//! can be the rows of one surface, as skinning a surface through several
3//! curves needs: raising a curve's degree and refining its knots, neither of
4//! which changes the curve, and bringing a curve onto the domain `[0, 1]`.
5
6use geop_core_math::{
7    geop_error::{GeopError, GeopResult},
8    scalars::Scalar,
9    vector::Vector,
10};
11
12use super::NurbCurve;
13use crate::{
14    aabb::compute_aabb,
15    knot_insertion::{insert, is_clamped_at},
16    spline::interior_knots,
17};
18
19impl<const D: usize> NurbCurve<D> {
20    /// The same curve reparametrized from its domain onto `[0, 1]`: its
21    /// knots mapped linearly, its control points unchanged. A curve already
22    /// on exactly `[0, 1]` is returned as it is: mapping its knots would
23    /// only round them outwards.
24    pub fn with_unit_domain(&self) -> GeopResult<Self> {
25        let (t0, t1) = self.domain();
26        let exactly = |t: Scalar, x: Scalar| t.is_subset_of(x) && x.is_subset_of(t);
27        if exactly(t0, Scalar::ZERO) && exactly(t1, Scalar::ONE) {
28            return Ok(self.clone());
29        }
30        let span = t1.sub(t0);
31        let knots = self
32            .knot_vector
33            .iter()
34            .map(|&k| k.sub(t0).div(span))
35            .collect::<GeopResult<Vec<Scalar>>>()?;
36        NurbCurve::try_new(self.degree, self.control_points.clone(), knots)
37    }
38
39    /// The same curve one degree higher, for a curve clamped at both ends.
40    ///
41    /// Cut into its Bézier pieces first — every interior knot inserted up to
42    /// multiplicity `degree` — each piece is raised on its own:
43    /// `Q_i = i/(p+1) P_{i-1} + (1 - i/(p+1)) P_i` on the homogeneous control
44    /// points, exact for rational curves too. The pieces stay joined as they
45    /// were, every interior knot now of multiplicity `degree + 1`.
46    pub fn elevate_degree(&self) -> GeopResult<Self> {
47        let p = self.degree;
48        let n = self.control_points.len();
49        if !(is_clamped_at(&self.knot_vector, p, true)
50            && is_clamped_at(&self.knot_vector, p, false))
51        {
52            return Err(GeopError::new(format!(
53                "elevate_degree: the knot vector {:?} is not clamped at both ends",
54                self.knot_vector
55            )));
56        }
57        let interior = interior_knots(&self.knot_vector, p, n);
58        let mut knots = self.knot_vector.clone();
59        let mut rows = [self.control_points.clone()];
60        for &(u, m) in &interior {
61            if m > p {
62                return Err(GeopError::new(format!(
63                    "elevate_degree: the knot {u:?} has multiplicity {m}, more than the degree {p}"
64                )));
65            }
66            for _ in m..p {
67                insert(&mut knots, &mut rows, p, u);
68            }
69        }
70        let [pts] = rows;
71        let pieces = interior.len() + 1;
72        debug_assert_eq!(pts.len(), p * pieces + 1);
73        let mut out: Vec<Vector<D>> = Vec::with_capacity((p + 1) * pieces + 1);
74        out.push(pts[0]);
75        for piece in pts.windows(p + 1).step_by(p.max(1)) {
76            for i in 1..=p {
77                let alpha = Scalar::from_ratio(i as i64, (p + 1) as i64)?;
78                out.push(Vector::interpolate(&piece[i], &piece[i - 1], alpha));
79            }
80            out.push(piece[p]);
81        }
82        let (start, end) = self.domain();
83        let mut new_knots = vec![start; p + 2];
84        for &(u, _) in &interior {
85            new_knots.extend(std::iter::repeat_n(u, p + 1));
86        }
87        new_knots.extend(std::iter::repeat_n(end, p + 2));
88        NurbCurve::try_new(p + 1, out, new_knots)
89    }
90
91    /// The same single-span (Bézier) curve with its end weights one, as a
92    /// clamped curve's ends are in a profile — what a split of a rational
93    /// curve, an arc, leaves otherwise.
94    ///
95    /// Scaling the `i`-th weight by `c^i` — every homogeneous control point
96    /// with it — reparametrizes a rational Bézier curve without changing it,
97    /// for any `c > 0`: `c^p = w_0 / w_p` brings both ends to one weight,
98    /// and dividing by `w_0` makes that one. `c` is a free choice; the ends
99    /// are divided by their own weights, so they come out at one whatever
100    /// `c` rounded to. A curve whose ends already weigh exactly one is
101    /// returned as it is.
102    pub fn with_unit_end_weights(&self) -> GeopResult<Self> {
103        let p = self.degree;
104        let n = self.control_points.len();
105        let weight = |i: usize| self.control_points[i][D - 1];
106        if weight(0).is_exactly(Scalar::ONE) && weight(n - 1).is_exactly(Scalar::ONE) {
107            return Ok(self.clone());
108        }
109        if n != p + 1 {
110            return Err(GeopError::new(format!(
111                "with_unit_end_weights: a curve of {n} control points and degree {p} is not a single span, and its end weights {:?}, {:?} are not one",
112                weight(0),
113                weight(n - 1)
114            )));
115        }
116        let c = (weight(0).to_f64() / weight(n - 1).to_f64()).powf(1.0 / p as f64);
117        let control_points = (0..n)
118            .map(|i| {
119                let factor = if i == 0 {
120                    Scalar::ONE.div(weight(0))?
121                } else if i == n - 1 {
122                    Scalar::ONE.div(weight(n - 1))?
123                } else {
124                    Scalar::from_f64(c.powi(i as i32)).div(weight(0))?
125                };
126                Ok(self.control_points[i].prod_scalar(factor))
127            })
128            .collect::<GeopResult<Vec<_>>>()?;
129        NurbCurve::try_new(p, control_points, self.knot_vector.clone())
130    }
131
132    /// `curves`, on one domain and clamped, made compatible: each raised to
133    /// the highest degree among them, and given every knot any of them has,
134    /// at the highest multiplicity any of them has it — so that they all end
135    /// up on one knot vector. Every curve stays the curve it was.
136    ///
137    /// Knots of different curves that could be equal are taken as one, the
138    /// union of their enclosures, which every curve then carries: an honest
139    /// enclosure of each curve's own knot.
140    pub fn compatible(curves: &[Self]) -> GeopResult<Vec<Self>> {
141        let Some(first) = curves.first() else {
142            return Ok(Vec::new());
143        };
144        let (start, end) = first.domain();
145        let degree = curves.iter().map(|c| c.degree).max().unwrap_or(0);
146        let mut out = Vec::with_capacity(curves.len());
147        for curve in curves {
148            let (s, e) = curve.domain();
149            if !(s.could_be_equal(start) && e.could_be_equal(end)) {
150                return Err(GeopError::new(format!(
151                    "compatible: domains ({s:?}, {e:?}) and ({start:?}, {end:?}) differ"
152                )));
153            }
154            let mut curve = curve.clone();
155            // Each raise one degree.
156            for _ in curve.degree..degree {
157                curve = curve.elevate_degree()?;
158            }
159            out.push(curve);
160        }
161
162        let mut merged: Vec<(Scalar, usize)> = Vec::new();
163        for curve in &out {
164            for (u, m) in interior_knots(&curve.knot_vector, degree, curve.control_points.len()) {
165                match merged.iter_mut().find(|(v, _)| v.could_be_equal(u)) {
166                    Some((v, k)) => {
167                        *v = v.union(u);
168                        *k = (*k).max(m);
169                    }
170                    None => merged.push((u, m)),
171                }
172            }
173        }
174        for curve in &mut out {
175            let mut knots = curve.knot_vector.clone();
176            let mut rows = [curve.control_points.clone()];
177            for &(u, m) in &merged {
178                let have = interior_knots(&knots, degree, rows[0].len())
179                    .iter()
180                    .find(|(v, _)| v.could_be_equal(u))
181                    .map_or(0, |&(_, k)| k);
182                for _ in have..m {
183                    insert(&mut knots, &mut rows, degree, u);
184                }
185            }
186            let [pts] = rows;
187            curve.control_points = pts;
188            curve.knot_vector = knots;
189        }
190
191        // One knot vector for all: each knot the union of every curve's.
192        let len = out[0].knot_vector.len();
193        if out.iter().any(|c| c.knot_vector.len() != len) {
194            return Err(GeopError::new(format!(
195                "compatible: knot vectors of different lengths after merging: {:?}",
196                out.iter()
197                    .map(|c| c.knot_vector.clone())
198                    .collect::<Vec<_>>()
199            )));
200        }
201        let common: Vec<Scalar> = (0..len)
202            .map(|k| {
203                out.iter()
204                    .map(|c| c.knot_vector[k])
205                    .reduce(|a, b| a.union(b))
206                    .expect("at least one curve")
207            })
208            .collect();
209        for curve in &mut out {
210            curve.knot_vector = common.clone();
211            curve.aabb = compute_aabb(&curve.control_points);
212        }
213        Ok(out)
214    }
215
216    /// The chain `curves`, each starting where the one before ends, as one
217    /// curve on `[0, 1]`, the `i`-th of `n` curves on `[i / n, (i + 1) / n]`
218    /// (see [`Self::join_at`]).
219    pub fn join(curves: &[Self]) -> GeopResult<Self> {
220        let n = curves.len();
221        if n == 1 {
222            return curves[0].with_unit_domain()?.with_unit_end_weights();
223        }
224        let count = Scalar::from_f64(n as f64);
225        join_placed(
226            curves,
227            (Scalar::ZERO, Scalar::ONE),
228            |i, k| k.add(Scalar::from_f64(i as f64)).div(count),
229            |i| Scalar::from_ratio((i + 1) as i64, n as i64),
230        )
231    }
232
233    /// The chain `curves`, each starting where the one before ends, as one
234    /// curve, the `i`-th on `[breaks[i], breaks[i + 1]]`: each brought onto
235    /// its own unit domain with its ends weighted one (see
236    /// [`Self::with_unit_end_weights`]) and raised to the highest degree
237    /// among them, their knot vectors laid end to end, every joint a knot
238    /// of full multiplicity. A joint's control point is the union of the end
239    /// of the curve before it and the start of the next — two enclosures of
240    /// the one point — and an error if they could not be equal. Every curve
241    /// stays the curve it was, on its new piece of the domain.
242    pub fn join_at(curves: &[Self], breaks: &[Scalar]) -> GeopResult<Self> {
243        let n = curves.len();
244        if breaks.len() != n + 1 || !breaks.windows(2).all(|w| w[0].definitely_less(w[1])) {
245            return Err(GeopError::new(format!(
246                "join_at: {n} curves need {} strictly increasing breaks, not {breaks:?}",
247                n + 1
248            )));
249        }
250        join_placed(
251            curves,
252            (breaks[0], breaks[n]),
253            |i, k| Ok(breaks[i].add(k.mul(breaks[i + 1].sub(breaks[i])))),
254            |i| Ok(breaks[i + 1]),
255        )
256    }
257
258    /// The same curve between the parameters `from[0]` and `from[n - 1]`,
259    /// reparametrized piece by piece: the stretch between `from[i]` and
260    /// `from[i + 1]` onto `[to[i], to[i + 1]]`, linearly, so that the curve
261    /// is at `to[i]` where it was at `from[i]`. What lies outside `from` is
262    /// cut off. Both are strictly increasing; a `from` that could be an end
263    /// of the domain is that end.
264    ///
265    /// Every `from[i]` becomes a joint of full multiplicity (see
266    /// [`Self::join_at`]). A rational curve is cut at its knots as well, so
267    /// that each piece can be weighted one at its ends: its weight is then
268    /// one at every joint, and so at every `to[i]`, as a polynomial curve's
269    /// is everywhere. Where a piece's own knots land between `to[i]` and
270    /// `to[i + 1]` is a free choice, so they are sharpened.
271    ///
272    /// The `from` are used as given: like [`Self::split`]'s, they must be
273    /// narrow, refined rather than sharpened.
274    pub fn reparametrized(&self, from: &[Scalar], to: &[Scalar]) -> GeopResult<Self> {
275        let n = from.len();
276        let ctx = |e: GeopError| {
277            e.with_context(format!(
278                "NurbCurve::reparametrized(from={from:?}, to={to:?}): domain={:?}",
279                self.domain()
280            ))
281        };
282        if n < 2 || to.len() != n {
283            return Err(ctx(GeopError::new(format!(
284                "{n} parameters to map onto {} others",
285                to.len()
286            ))));
287        }
288        let (t0, t1) = self.domain();
289        let mut from = from.to_vec();
290        if from[0].could_be_equal(t0) {
291            from[0] = t0;
292        }
293        if from[n - 1].could_be_equal(t1) {
294            from[n - 1] = t1;
295        }
296        let increasing = |x: &[Scalar]| x.windows(2).all(|w| w[0].definitely_less(w[1]));
297        if !increasing(&from) || !increasing(to) {
298            return Err(ctx(GeopError::new(
299                "the parameters must be strictly increasing",
300            )));
301        }
302        let mut rest = self.sub_curve(from[0], from[n - 1]).map_err(ctx)?;
303        let mut pieces = Vec::with_capacity(n - 1);
304        for &t in &from[1..n - 1] {
305            let (left, right) = rest.split(t).map_err(ctx)?;
306            pieces.push(left);
307            rest = right;
308        }
309        pieces.push(rest);
310        let rational = self
311            .control_points
312            .iter()
313            .any(|p| !p[D - 1].is_exactly(Scalar::ONE));
314        let mut curves = Vec::new();
315        let mut breaks = vec![to[0]];
316        for (i, mut piece) in pieces.into_iter().enumerate() {
317            let parts = if rational {
318                piece.bezier_pieces().map_err(ctx)?
319            } else {
320                // Every weight is one, and so is every blend of them: the
321                // knot insertion cutting the pieces would only round it.
322                for cp in &mut piece.control_points {
323                    cp[D - 1] = Scalar::ONE;
324                }
325                vec![piece]
326            };
327            let scale = to[i + 1]
328                .sub(to[i])
329                .div(from[i + 1].sub(from[i]))
330                .map_err(ctx)?;
331            let last = parts.len() - 1;
332            for (k, part) in parts.into_iter().enumerate() {
333                breaks.push(if k == last {
334                    to[i + 1]
335                } else {
336                    to[i].add(part.domain().1.sub(from[i]).mul(scale)).sharpen()
337                });
338                curves.push(part);
339            }
340        }
341        NurbCurve::join_at(&curves, &breaks).map_err(ctx)
342    }
343}
344
345/// The chain `curves` as one curve on `ends` (see [`NurbCurve::join_at`]):
346/// the `i`-th curve's interior knot `k`, on its unit domain, at
347/// `place(i, k)`, its end at `joint(i)`.
348fn join_placed<const D: usize>(
349    curves: &[NurbCurve<D>],
350    (start, end): (Scalar, Scalar),
351    place: impl Fn(usize, Scalar) -> GeopResult<Scalar>,
352    joint: impl Fn(usize) -> GeopResult<Scalar>,
353) -> GeopResult<NurbCurve<D>> {
354    let n = curves.len();
355    if n == 0 {
356        return Err(GeopError::new("join: no curves to join"));
357    }
358    let mut pieces = curves
359        .iter()
360        .map(|c| c.with_unit_domain()?.with_unit_end_weights())
361        .collect::<GeopResult<Vec<_>>>()?;
362    let degree = pieces.iter().map(|c| c.degree).max().unwrap_or(0);
363    for piece in &mut pieces {
364        // Each raise one degree.
365        for _ in piece.degree..degree {
366            *piece = piece.elevate_degree()?;
367        }
368    }
369    let mut points: Vec<Vector<D>> = Vec::new();
370    let mut knots = vec![start; degree + 1];
371    for (i, piece) in pieces.iter().enumerate() {
372        let len = piece.control_points.len();
373        if let Some(end) = points.last_mut() {
374            let start = &piece.control_points[0];
375            if !end.could_be_equal(start) {
376                return Err(GeopError::new(format!(
377                    "join: curve {i} starts at {start:?}, not where the one before ends, {end:?}"
378                )));
379            }
380            *end = end.union(start);
381            points.extend_from_slice(&piece.control_points[1..]);
382        } else {
383            points.extend_from_slice(&piece.control_points);
384        }
385        for &k in &piece.knot_vector[degree + 1..len] {
386            knots.push(place(i, k)?);
387        }
388        if i + 1 < n {
389            knots.extend(std::iter::repeat_n(joint(i)?, degree));
390        }
391    }
392    knots.extend(std::iter::repeat_n(end, degree + 1));
393    NurbCurve::try_new(degree, points, knots)
394}
395
396#[cfg(test)]
397mod tests {
398    use crate::nurb_curve::{NurbCurve, NurbCurve3D};
399    use crate::test_fixtures::{line, weighted};
400    use geop_core_math::{scalars::Scalar, vector::Vector3};
401
402    fn quarter_arc() -> NurbCurve3D {
403        let f = Scalar::from_f64;
404        NurbCurve::try_new(
405            2,
406            vec![
407                weighted(1., 0., 0., 1.),
408                weighted(1., 1., 0., std::f64::consts::SQRT_2 / 2.0),
409                weighted(0., 1., 0., 1.),
410            ],
411            vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
412        )
413        .unwrap()
414    }
415
416    fn cubic_spline() -> NurbCurve3D {
417        let f = Scalar::from_f64;
418        NurbCurve::try_new(
419            3,
420            vec![
421                weighted(0., 0., 0., 1.),
422                weighted(1., 2., 0., 1.),
423                weighted(2., -1., 1., 1.),
424                weighted(3., 1., 0., 1.),
425                weighted(4., 0., 2., 1.),
426            ],
427            vec![
428                f(0.),
429                f(0.),
430                f(0.),
431                f(0.),
432                f(0.5),
433                f(1.),
434                f(1.),
435                f(1.),
436                f(1.),
437            ],
438        )
439        .unwrap()
440    }
441
442    fn assert_same_curve(a: &NurbCurve3D, b: &NurbCurve3D) {
443        for i in 0..=16 {
444            let t = Scalar::from_ratio(i, 16).unwrap();
445            let (pa, pb) = (a.evaluate(t).unwrap(), b.evaluate(t).unwrap());
446            assert!(pa.could_be_equal(&pb), "at {t:?}: {pa:?} vs {pb:?}");
447        }
448    }
449
450    /// Raising the degree keeps the curve: a rational arc, and a cubic
451    /// spline with an interior knot.
452    #[test]
453    fn elevate_degree_keeps_the_curve() {
454        for curve in [quarter_arc(), cubic_spline(), line([0.; 3], [2., 1., 3.])] {
455            let raised = curve.elevate_degree().unwrap();
456            assert_eq!(raised.degree, curve.degree + 1);
457            assert_same_curve(&curve, &raised);
458            let twice = raised.elevate_degree().unwrap();
459            assert_same_curve(&curve, &twice);
460        }
461    }
462
463    /// A line, an arc and a spline brought onto one degree and one knot
464    /// vector, each still the curve it was.
465    #[test]
466    fn compatible_curves_share_degree_and_knots() {
467        let curves = [line([0.; 3], [2., 1., 3.]), quarter_arc(), cubic_spline()];
468        let out = NurbCurve::compatible(&curves).unwrap();
469        for (before, after) in curves.iter().zip(&out) {
470            assert_eq!(after.degree, 3);
471            assert_eq!(after.control_points.len(), out[0].control_points.len());
472            for (a, b) in after.knot_vector.iter().zip(&out[0].knot_vector) {
473                assert!(a.is_subset_of(*b) && b.is_subset_of(*a));
474            }
475            assert_same_curve(before, after);
476        }
477    }
478
479    /// The second half of a split curve, brought onto `[0, 1]`.
480    #[test]
481    fn with_unit_domain_rescales_the_knots() {
482        let curve = cubic_spline();
483        let (_, right) = curve.split(Scalar::from_f64(0.25)).unwrap();
484        let unit = right.with_unit_domain().unwrap();
485        let (t0, t1) = unit.domain();
486        assert!(t0.could_be_equal(Scalar::ZERO) && t1.could_be_equal(Scalar::ONE));
487        let mid = unit.evaluate(Scalar::from_f64(0.5)).unwrap();
488        assert!(mid.could_be_equal(&curve.evaluate(Scalar::from_f64(0.625)).unwrap()));
489    }
490
491    /// Half of a quarter arc, its end weights brought back to one: still
492    /// the same arc.
493    #[test]
494    fn unit_end_weights_keep_the_curve() {
495        let (left, _) = quarter_arc().split(Scalar::from_f64(0.5)).unwrap();
496        let left = left.with_unit_domain().unwrap();
497        let unit = left.with_unit_end_weights().unwrap();
498        for cp in [unit.control_points[0], unit.control_points[2]] {
499            assert!(cp[3].could_be_equal(Scalar::ONE));
500        }
501        // A different parametrization of the same points: every point of it
502        // lies on the unit circle, and the ends are where they were.
503        for i in 0..=8 {
504            let p = unit.evaluate(Scalar::from_ratio(i, 8).unwrap()).unwrap();
505            assert!(p.norm_sq().could_be_equal(Scalar::ONE), "{p:?}");
506        }
507        assert!(
508            unit.evaluate(Scalar::ONE)
509                .unwrap()
510                .could_be_equal(&left.evaluate(Scalar::ONE).unwrap())
511        );
512    }
513
514    /// An arc and a line on from its end, joined: one quadratic on
515    /// `[0, 1]`, the arc on its first half, the line on its second. A
516    /// curve starting elsewhere does not join.
517    #[test]
518    fn join_lays_curves_end_to_end() {
519        let f = Scalar::from_f64;
520        let on = NurbCurve::try_new(
521            1,
522            vec![weighted(0., 1., 0., 1.), weighted(-1., 1., 0., 1.)],
523            vec![f(0.), f(0.), f(1.), f(1.)],
524        )
525        .unwrap();
526        let arc = quarter_arc();
527        let joined = NurbCurve::join(&[arc.clone(), on.clone()]).unwrap();
528        assert_eq!(joined.degree, 2);
529        for i in 0..=8 {
530            let t = Scalar::from_ratio(i, 8).unwrap();
531            let half = |k: f64| t.add(f(k)).div(Scalar::TWO).unwrap();
532            let (a, b) = (
533                joined.evaluate(half(0.0)).unwrap(),
534                arc.evaluate(t).unwrap(),
535            );
536            assert!(a.could_be_equal(&b), "{a:?} vs {b:?}");
537            let (a, b) = (joined.evaluate(half(1.0)).unwrap(), on.evaluate(t).unwrap());
538            assert!(a.could_be_equal(&b), "{a:?} vs {b:?}");
539        }
540        assert!(NurbCurve::join(&[arc, line([0.; 3], [2., 1., 3.])]).is_err());
541    }
542
543    /// A quarter arc and a spline reparametrized piece by piece, cut short
544    /// at the start: where they were at `from[i]` they are at `to[i]`,
545    /// still on the curve they were, the arc weighing one there.
546    #[test]
547    fn reparametrized_maps_pieces() {
548        let f = Scalar::from_f64;
549        let from = [f(0.1), f(0.3), f(1.0)];
550        let to = [f(0.0), f(0.6), f(1.0)];
551        for curve in [quarter_arc(), cubic_spline()] {
552            let mapped = curve.reparametrized(&from, &to).unwrap();
553            let (t0, t1) = mapped.domain();
554            assert!(t0.could_be_equal(Scalar::ZERO) && t1.could_be_equal(Scalar::ONE));
555            for (a, b) in from.iter().zip(&to) {
556                let (p, q) = (curve.evaluate(*a).unwrap(), mapped.evaluate(*b).unwrap());
557                assert!(p.could_be_equal(&q), "{p:?} vs {q:?}");
558            }
559            let joint = mapped
560                .control_points
561                .iter()
562                .find(|cp| {
563                    Vector3::from_array([cp[0], cp[1], cp[2]])
564                        .prod_scalar(Scalar::ONE.div(cp[3]).unwrap())
565                        .could_be_equal(&curve.evaluate(from[1]).unwrap())
566                })
567                .expect("a control point at the joint");
568            assert!(joint[3].could_be_equal(Scalar::ONE), "{joint:?}");
569            if curve.degree == 2 {
570                for i in 0..=8 {
571                    let p = mapped.evaluate(Scalar::from_ratio(i, 8).unwrap()).unwrap();
572                    assert!(p.norm_sq().could_be_equal(Scalar::ONE), "{p:?}");
573                }
574            }
575        }
576    }
577}