axiolid_nurbs/
transform.rs

1//! Exact shape-preserving NURBS transformations.
2
3use axiolid_contracts::{GeomError, GeomResult};
4use axiolid_core::{Point2, Point3, Scalar};
5use axiolid_curve::{BSplineCurve, BSplineCurve2, BSplineCurve3};
6use axiolid_evaluate::curve::{bspline_jet2, bspline_jet3};
7
8/// Reverse a planar B-spline curve without changing its image.
9pub fn reverse2(curve: &BSplineCurve2) -> GeomResult<BSplineCurve2> {
10    bspline_jet2(curve, 0.0)?;
11    reverse(curve)
12}
13
14/// Reverse a spatial B-spline curve without changing its image.
15pub fn reverse3(curve: &BSplineCurve3) -> GeomResult<BSplineCurve3> {
16    bspline_jet3(curve, 0.0)?;
17    reverse(curve)
18}
19
20/// Insert one interior knot into a planar curve in homogeneous coordinates.
21///
22/// The represented rational or polynomial curve is unchanged. Endpoint knots
23/// and knots whose multiplicity already reaches the degree are rejected.
24pub fn insert_knot2(curve: &BSplineCurve2, parameter: Scalar) -> GeomResult<BSplineCurve2> {
25    bspline_jet2(curve, parameter)?;
26    insert(
27        curve,
28        parameter,
29        |p| [p.x, p.y],
30        |p| Point2::new(p[0], p[1]),
31    )
32}
33
34/// Insert one interior knot into a spatial curve in homogeneous coordinates.
35///
36/// The represented rational or polynomial curve is unchanged. Endpoint knots
37/// and knots whose multiplicity already reaches the degree are rejected.
38pub fn insert_knot3(curve: &BSplineCurve3, parameter: Scalar) -> GeomResult<BSplineCurve3> {
39    bspline_jet3(curve, parameter)?;
40    insert(
41        curve,
42        parameter,
43        |p| [p.x, p.y, p.z],
44        |p| Point3::new(p[0], p[1], p[2]),
45    )
46}
47
48/// Split a planar curve exactly at an interior parameter.
49///
50/// Both output curves include the shared cut point and are marked open.
51pub fn split2(
52    curve: &BSplineCurve2,
53    parameter: Scalar,
54) -> GeomResult<(BSplineCurve2, BSplineCurve2)> {
55    bspline_jet2(curve, parameter)?;
56    let mut refined = curve.clone();
57    check_interior(&refined, parameter)?;
58    while multiplicity(&refined, parameter) < usize::from(refined.degree) {
59        refined = insert_knot2(&refined, parameter)?;
60    }
61    split_ready(&refined, parameter)
62}
63
64/// Split a spatial curve exactly at an interior parameter.
65///
66/// Both output curves include the shared cut point and are marked open.
67pub fn split3(
68    curve: &BSplineCurve3,
69    parameter: Scalar,
70) -> GeomResult<(BSplineCurve3, BSplineCurve3)> {
71    bspline_jet3(curve, parameter)?;
72    let mut refined = curve.clone();
73    check_interior(&refined, parameter)?;
74    while multiplicity(&refined, parameter) < usize::from(refined.degree) {
75        refined = insert_knot3(&refined, parameter)?;
76    }
77    split_ready(&refined, parameter)
78}
79
80/// Decompose a planar B-spline into exact rational/polynomial Bézier segments.
81pub fn bezier_segments2(curve: &BSplineCurve2) -> GeomResult<Vec<BSplineCurve2>> {
82    bspline_jet2(curve, 0.0)?;
83    decompose(curve, split2)
84}
85
86/// Decompose a spatial B-spline into exact rational/polynomial Bézier segments.
87pub fn bezier_segments3(curve: &BSplineCurve3) -> GeomResult<Vec<BSplineCurve3>> {
88    bspline_jet3(curve, 0.0)?;
89    decompose(curve, split3)
90}
91
92fn reverse<P: Clone>(curve: &BSplineCurve<P>) -> GeomResult<BSplineCurve<P>> {
93    let (knots, multiplicities) = crate::axis::reverse_axis(&curve.knots, &curve.multiplicities)?;
94    let mut control_points = curve.control_points.clone();
95    control_points.reverse();
96    let weights = curve.weights.as_ref().map(|weights| {
97        let mut reversed = weights.clone();
98        reversed.reverse();
99        reversed
100    });
101    Ok(BSplineCurve {
102        degree: curve.degree,
103        control_points,
104        knots,
105        multiplicities,
106        weights,
107        knot_spec: curve.knot_spec,
108        closed: curve.closed,
109        self_intersect: curve.self_intersect,
110    })
111}
112
113fn check_interior<P>(curve: &BSplineCurve<P>, parameter: Scalar) -> GeomResult<()> {
114    if !parameter.is_finite() {
115        return Err(GeomError::InvalidInput(
116            "split parameter must be finite".to_owned(),
117        ));
118    }
119    let expanded = expand(curve);
120    let lo = expanded[usize::from(curve.degree)];
121    let hi = expanded[curve.control_points.len()];
122    if parameter <= lo || parameter >= hi {
123        return Err(GeomError::InvalidInput(
124            "split parameter must lie strictly inside the active domain".to_owned(),
125        ));
126    }
127    Ok(())
128}
129
130fn expand<P>(curve: &BSplineCurve<P>) -> Vec<Scalar> {
131    let mut expanded =
132        Vec::with_capacity(curve.control_points.len() + usize::from(curve.degree) + 1);
133    for (&knot, &multiplicity) in curve.knots.iter().zip(&curve.multiplicities) {
134        expanded.extend(core::iter::repeat_n(knot, multiplicity as usize));
135    }
136    expanded
137}
138
139fn multiplicity<P>(curve: &BSplineCurve<P>, parameter: Scalar) -> usize {
140    curve
141        .knots
142        .iter()
143        .position(|&k| k == parameter)
144        .map_or(0, |i| curve.multiplicities[i] as usize)
145}
146fn split_ready<P: Clone>(
147    curve: &BSplineCurve<P>,
148    parameter: Scalar,
149) -> GeomResult<(BSplineCurve<P>, BSplineCurve<P>)> {
150    let expanded = expand(curve);
151    let p = usize::from(curve.degree);
152    let n = curve.control_points.len() - 1;
153    let k = find_span(&expanded, n, p, parameter);
154    let shared = k
155        .checked_sub(p)
156        .ok_or_else(|| GeomError::InvalidInput("split control index underflows".to_owned()))?;
157    if shared == 0 || shared >= curve.control_points.len() - 1 {
158        return Err(GeomError::InvalidInput(
159            "split would create an empty segment".to_owned(),
160        ));
161    }
162    let ki = curve
163        .knots
164        .iter()
165        .position(|&value| value == parameter)
166        .ok_or_else(|| {
167            GeomError::InvalidInput("split knot is absent after refinement".to_owned())
168        })?;
169    let mut lm = curve.multiplicities[..=ki].to_vec();
170    let mut rm = curve.multiplicities[ki..].to_vec();
171    lm[ki] = u32::from(curve.degree) + 1;
172    rm[0] = u32::from(curve.degree) + 1;
173    let si = if curve.self_intersect == Some(false) {
174        Some(false)
175    } else {
176        None
177    };
178    let make = |control_points: Vec<P>,
179                knots: Vec<Scalar>,
180                multiplicities: Vec<u32>,
181                weights: Option<Vec<Scalar>>| BSplineCurve {
182        degree: curve.degree,
183        control_points,
184        knots,
185        multiplicities,
186        weights,
187        knot_spec: curve.knot_spec,
188        closed: false,
189        self_intersect: si,
190    };
191    let lw = curve.weights.as_ref().map(|w| w[..=shared].to_vec());
192    let rw = curve.weights.as_ref().map(|w| w[shared..].to_vec());
193    Ok((
194        make(
195            curve.control_points[..=shared].to_vec(),
196            curve.knots[..=ki].to_vec(),
197            lm,
198            lw,
199        ),
200        make(
201            curve.control_points[shared..].to_vec(),
202            curve.knots[ki..].to_vec(),
203            rm,
204            rw,
205        ),
206    ))
207}
208type SplitFn<P> = fn(&BSplineCurve<P>, Scalar) -> GeomResult<(BSplineCurve<P>, BSplineCurve<P>)>;
209
210fn decompose<P: Clone>(
211    curve: &BSplineCurve<P>,
212    split: SplitFn<P>,
213) -> GeomResult<Vec<BSplineCurve<P>>> {
214    let expanded = expand(curve);
215    let lo = expanded[usize::from(curve.degree)];
216    let hi = expanded[curve.control_points.len()];
217    let internal: Vec<_> = curve
218        .knots
219        .iter()
220        .copied()
221        .filter(|&k| k > lo && k < hi)
222        .collect();
223    let mut result = Vec::with_capacity(internal.len() + 1);
224    let mut remainder = curve.clone();
225    for parameter in internal {
226        let (left, right) = split(&remainder, parameter)?;
227        result.push(left);
228        remainder = right;
229    }
230    result.push(remainder);
231    Ok(result)
232}
233
234pub(crate) fn insert<const N: usize, P: Clone>(
235    curve: &BSplineCurve<P>,
236    parameter: Scalar,
237    coordinates: impl Fn(&P) -> [Scalar; N],
238    point: impl Fn([Scalar; N]) -> P,
239) -> GeomResult<BSplineCurve<P>> {
240    if !parameter.is_finite() {
241        return Err(GeomError::InvalidInput(
242            "inserted knot must be finite".to_owned(),
243        ));
244    }
245    let expanded = expand_knots(curve);
246    let p = usize::from(curve.degree);
247    let n = curve.control_points.len() - 1;
248    let lo = expanded[p];
249    let hi = expanded[n + 1];
250    if parameter <= lo || parameter >= hi {
251        return Err(GeomError::InvalidInput(format!(
252            "inserted knot {parameter} must be strictly inside ({lo}, {hi})"
253        )));
254    }
255    let k = find_span(&expanded, n, p, parameter);
256    let s = expanded.iter().filter(|&&knot| knot == parameter).count();
257    if s >= p {
258        return Err(GeomError::InvalidInput(format!(
259            "knot multiplicity {s} already reaches degree {p}"
260        )));
261    }
262    let weights = curve.weights.clone().unwrap_or_else(|| vec![1.0; n + 1]);
263    let homogeneous: Vec<_> = curve
264        .control_points
265        .iter()
266        .zip(&weights)
267        .map(|(control, &weight)| {
268            let mut h = coordinates(control);
269            for value in &mut h {
270                *value *= weight;
271            }
272            (h, weight)
273        })
274        .collect();
275    let mut output = vec![([0.0; N], 0.0); n + 2];
276    output[..=k - p].clone_from_slice(&homogeneous[..=k - p]);
277    output[k - s + 1..n + 2].copy_from_slice(&homogeneous[k - s..n + 1]);
278    for i in k - p + 1..=k - s {
279        let denominator = expanded[i + p] - expanded[i];
280        if denominator == 0.0 {
281            return Err(GeomError::Degenerate(
282                "knot insertion denominator is zero".to_owned(),
283            ));
284        }
285        let alpha = (parameter - expanded[i]) / denominator;
286        let mut h = [0.0; N];
287        for (d, value) in h.iter_mut().enumerate() {
288            *value = alpha * homogeneous[i].0[d] + (1.0 - alpha) * homogeneous[i - 1].0[d];
289        }
290        output[i] = (
291            h,
292            alpha * homogeneous[i].1 + (1.0 - alpha) * homogeneous[i - 1].1,
293        );
294    }
295    let mut new_expanded = expanded;
296    new_expanded.insert(k + 1, parameter);
297    let (knots, multiplicities) = compact(&new_expanded)?;
298    let mut controls = Vec::with_capacity(output.len());
299    let mut new_weights = Vec::with_capacity(output.len());
300    for (mut h, weight) in output {
301        if !weight.is_finite() || weight <= 0.0 {
302            return Err(GeomError::Degenerate(
303                "inserted homogeneous weight is not positive and finite".to_owned(),
304            ));
305        }
306        for value in &mut h {
307            *value /= weight;
308        }
309        controls.push(point(h));
310        new_weights.push(weight);
311    }
312    Ok(BSplineCurve {
313        degree: curve.degree,
314        control_points: controls,
315        knots,
316        multiplicities,
317        weights: curve.weights.as_ref().map(|_| new_weights),
318        knot_spec: curve.knot_spec,
319        closed: curve.closed,
320        self_intersect: curve.self_intersect,
321    })
322}
323
324fn expand_knots<P>(curve: &BSplineCurve<P>) -> Vec<Scalar> {
325    let expected = curve.control_points.len() + usize::from(curve.degree) + 1;
326    let mut expanded = Vec::with_capacity(expected);
327    for (&knot, &multiplicity) in curve.knots.iter().zip(&curve.multiplicities) {
328        expanded.extend(core::iter::repeat_n(knot, multiplicity as usize));
329    }
330    expanded
331}
332
333fn find_span(knots: &[Scalar], n: usize, degree: usize, parameter: Scalar) -> usize {
334    if parameter >= knots[n + 1] {
335        return n;
336    }
337    let mut low = degree;
338    let mut high = n + 1;
339    let mut mid = (low + high) / 2;
340    while parameter < knots[mid] || parameter >= knots[mid + 1] {
341        if parameter < knots[mid] {
342            high = mid;
343        } else {
344            low = mid;
345        }
346        mid = (low + high) / 2;
347    }
348    mid
349}
350
351fn compact(expanded: &[Scalar]) -> GeomResult<(Vec<Scalar>, Vec<u32>)> {
352    let mut knots = Vec::new();
353    let mut multiplicities = Vec::new();
354    for &knot in expanded {
355        if knots.last().copied() == Some(knot) {
356            *multiplicities
357                .last_mut()
358                .expect("knot and multiplicity stay parallel") += 1;
359        } else {
360            knots.push(knot);
361            multiplicities.push(1);
362        }
363    }
364    Ok((knots, multiplicities))
365}