axiolid_nurbs/
surface_ops.rs

1//! Knot removal, degree change and iso-curves of tensor-product surfaces
2//! (#141).
3//!
4//! Each operation along `u` applies the curve operation to every column of
5//! the control net (every row, along `v`), in homogeneous coordinates, so
6//! rational surfaces are handled like polynomial ones. The `v` operations
7//! transpose the surface, work along `u`, and transpose back.
8//!
9//! Degree elevation and iso-curves are exact. Knot removal and degree
10//! reduction are not in general, so each bounds the deviation it introduced
11//! and keeps it within the caller's tolerance. The bound is not sampled: the
12//! result is refined back onto the original's knot vector (by knot insertion
13//! or degree elevation, both exact), and the two control nets bound the
14//! distance between the surfaces everywhere, by the convex-hull property of
15//! the B-spline basis.
16
17use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
18use axiolid_core::{Point3, Scalar};
19use axiolid_curve::{BSplineCurve, BSplineCurve3};
20use axiolid_evaluate::surface::bspline_jet;
21use axiolid_surface::BSplineSurface;
22
23use crate::surface_transform::u_curve;
24
25/// A homogeneous control point: `[w x, w y, w z, w]`.
26type Homogeneous = [Scalar; 4];
27
28/// A surface changed within a stated bound.
29#[derive(Debug, Clone, PartialEq)]
30pub struct BoundedSurface {
31    /// The transformed surface.
32    pub surface: BSplineSurface,
33    /// Upper bound on the distance between the original and transformed
34    /// surfaces at every parameter, in model units (computed in `f64`).
35    pub deviation_upper_bound: Scalar,
36}
37
38/// The outcome of removing one knot value from a surface up to some number
39/// of times.
40#[derive(Debug, Clone, PartialEq)]
41pub struct SurfaceKnotRemoval {
42    /// The surface with `removed` copies of the knot taken out; the input
43    /// surface unchanged when `removed` is zero.
44    pub surface: BSplineSurface,
45    /// How many copies were removed.
46    pub removed: u32,
47    /// Upper bound on the distance between the input and `surface` at every
48    /// parameter, in model units (computed in `f64`): the sum of the bounds
49    /// of the single removals.
50    pub deviation_upper_bound: Scalar,
51}
52
53/// Remove the `u` knot `parameter` up to `times` times, keeping the surface
54/// within `tolerance` of the input.
55///
56/// A copy is removed only when it can be removed from every column of the
57/// control net with the accumulated deviation bound still within
58/// `tolerance`; removal stops at the first copy that cannot. A knot that
59/// carries shape is therefore left in place (`removed == 0`), never
60/// approximated away.
61///
62/// # Errors
63///
64/// An invalid surface, a tolerance that is negative or not finite, or a
65/// `parameter` that is not an interior `u` knot.
66pub fn remove_surface_knot_u(
67    surface: &BSplineSurface,
68    parameter: Scalar,
69    times: u32,
70    tolerance: Scalar,
71) -> GeomResult<SurfaceKnotRemoval> {
72    bspline_jet(surface, 0.0, 0.0)?;
73    if !tolerance.is_finite() || tolerance < 0.0 {
74        return Err(GeomError::InvalidInput(
75            "tolerance must be finite and non-negative".to_owned(),
76        ));
77    }
78    let index = surface
79        .u_knots
80        .iter()
81        .position(|&k| k == parameter)
82        .ok_or_else(|| GeomError::InvalidInput(format!("{parameter} is not a u knot")))?;
83    if index == 0 || index + 1 == surface.u_knots.len() {
84        return Err(GeomError::InvalidInput(
85            "end knots bound the domain and cannot be removed".to_owned(),
86        ));
87    }
88    let rational = surface.weights.is_some();
89    let mut columns = homogeneous_columns(surface);
90    let mut removed = 0;
91    let mut bound = 0.0;
92    while removed < times && columns[0].knots.contains(&parameter) {
93        let Some((candidate, step)) = remove_once(&columns, parameter, rational) else {
94            break;
95        };
96        if bound + step > tolerance {
97            break;
98        }
99        columns = candidate;
100        bound += step;
101        removed += 1;
102    }
103    let surface = if removed == 0 {
104        surface.clone()
105    } else {
106        from_homogeneous_columns(surface, &columns)?
107    };
108    Ok(SurfaceKnotRemoval {
109        surface,
110        removed,
111        deviation_upper_bound: bound,
112    })
113}
114
115/// Remove the `v` knot `parameter` up to `times` times; as
116/// [`remove_surface_knot_u`].
117///
118/// # Errors
119///
120/// As [`remove_surface_knot_u`].
121pub fn remove_surface_knot_v(
122    surface: &BSplineSurface,
123    parameter: Scalar,
124    times: u32,
125    tolerance: Scalar,
126) -> GeomResult<SurfaceKnotRemoval> {
127    let mut result = remove_surface_knot_u(&transpose(surface), parameter, times, tolerance)?;
128    result.surface = transpose(&result.surface);
129    Ok(result)
130}
131
132/// Raise the surface's `u` degree by one without changing it.
133///
134/// Exact. Interior `u` knots come out with multiplicity equal to the new
135/// degree (Bezier form), as for curves: collapsing them would be lossy, and
136/// is [`remove_surface_knot_u`]'s job.
137///
138/// # Errors
139///
140/// An invalid surface.
141pub fn elevate_surface_degree_u(surface: &BSplineSurface) -> GeomResult<BSplineSurface> {
142    bspline_jet(surface, 0.0, 0.0)?;
143    let columns = (0..surface.control_points[0].len())
144        .map(|v| crate::degree::elevate_degree3(&u_curve(surface, v)))
145        .collect::<GeomResult<Vec<_>>>()?;
146    assemble_columns(surface, &columns)
147}
148
149/// Raise the surface's `v` degree by one; as [`elevate_surface_degree_u`].
150///
151/// # Errors
152///
153/// An invalid surface.
154pub fn elevate_surface_degree_v(surface: &BSplineSurface) -> GeomResult<BSplineSurface> {
155    Ok(transpose(&elevate_surface_degree_u(&transpose(surface))?))
156}
157
158/// Lower the surface's `u` degree by one, within `tolerance`, or refuse.
159///
160/// Only a surface already representable at the lower degree reduces
161/// cleanly; one that needs its degree is refused. The result is in Bezier
162/// form along `u`. Rational surfaces are refused, as for curves.
163///
164/// # Errors
165///
166/// An invalid or rational surface, a `u` degree below 2, a tolerance that is
167/// negative or not finite, or a deviation bound above `tolerance`
168/// (`GeomError::Degenerate`).
169pub fn reduce_surface_degree_u(
170    surface: &BSplineSurface,
171    tolerance: Scalar,
172) -> GeomResult<BoundedSurface> {
173    bspline_jet(surface, 0.0, 0.0)?;
174    if !tolerance.is_finite() || tolerance < 0.0 {
175        return Err(GeomError::InvalidInput(
176            "tolerance must be finite and non-negative".to_owned(),
177        ));
178    }
179    if surface.weights.is_some() {
180        return Err(GeomError::Unsupported {
181            backend: BackendId::new("nurbs"),
182            operation: Operation::SurfaceEvaluation,
183        });
184    }
185    if surface.u_degree < 2 {
186        return Err(GeomError::InvalidInput(
187            "a u degree of 1 cannot be reduced".to_owned(),
188        ));
189    }
190    let mut reduced = Vec::with_capacity(surface.control_points[0].len());
191    let mut bound: Scalar = 0.0;
192    for v in 0..surface.control_points[0].len() {
193        let column = u_curve(surface, v);
194        // The per-column curve check samples; the bound used here is the
195        // control-net one below, so the curve step accepts any deviation.
196        let candidate = crate::degree::reduce_degree3(&column, Scalar::MAX)?.curve;
197        // Elevating back is exact and gives Bezier form at the original
198        // degree; refining the original to Bezier form is exact too. The
199        // nets then share a knot vector, and their largest difference
200        // bounds the curves' distance everywhere.
201        let back = crate::degree::elevate_degree3(&candidate)?;
202        let original = bezier_form(&column)?;
203        if back.knots != original.knots
204            || back.multiplicities != original.multiplicities
205            || back.control_points.len() != original.control_points.len()
206        {
207            return Err(GeomError::Degenerate(
208                "reduced surface does not refine onto the original knots".to_owned(),
209            ));
210        }
211        for (a, b) in back.control_points.iter().zip(&original.control_points) {
212            bound = bound.max(a.distance(*b));
213        }
214        reduced.push(candidate);
215    }
216    if bound.is_nan() || bound > tolerance {
217        return Err(GeomError::Degenerate(format!(
218            "u degree is not reducible within tolerance: deviation {bound:.3e} exceeds {tolerance:.3e}"
219        )));
220    }
221    Ok(BoundedSurface {
222        surface: assemble_columns(surface, &reduced)?,
223        deviation_upper_bound: bound,
224    })
225}
226
227/// Lower the surface's `v` degree by one; as [`reduce_surface_degree_u`].
228///
229/// # Errors
230///
231/// As [`reduce_surface_degree_u`].
232pub fn reduce_surface_degree_v(
233    surface: &BSplineSurface,
234    tolerance: Scalar,
235) -> GeomResult<BoundedSurface> {
236    let mut result = reduce_surface_degree_u(&transpose(surface), tolerance)?;
237    result.surface = transpose(&result.surface);
238    Ok(result)
239}
240
241/// The iso-curve `u = parameter`, parametrised by `v`.
242///
243/// Exact: its control points are the columns' homogeneous points blended by
244/// the `u` basis at `parameter`, so it has the surface's `v` degree and
245/// knots, and is rational exactly when the surface is.
246///
247/// # Errors
248///
249/// An invalid surface, or a `parameter` outside the `u` domain.
250pub fn iso_curve_at_u(surface: &BSplineSurface, parameter: Scalar) -> GeomResult<BSplineCurve3> {
251    bspline_jet(surface, 0.0, 0.0)?;
252    let degree = usize::from(surface.u_degree);
253    let knots = expand(&surface.u_knots, &surface.u_multiplicities);
254    let count = surface.control_points.len();
255    let (lo, hi) = (knots[degree], knots[count]);
256    if !(parameter >= lo && parameter <= hi) {
257        return Err(GeomError::InvalidInput(format!(
258            "u = {parameter} is outside the domain [{lo}, {hi}]"
259        )));
260    }
261    let span = find_span(&knots, count, degree, parameter);
262    let basis = basis_functions(&knots, span, degree, parameter);
263    let rows = surface.control_points[0].len();
264    let mut points = Vec::with_capacity(rows);
265    let mut weights = Vec::with_capacity(rows);
266    for v in 0..rows {
267        let mut h = [0.0; 4];
268        for (k, &n) in basis.iter().enumerate() {
269            let i = span - degree + k;
270            let p = surface.control_points[i][v];
271            let w = surface.weights.as_ref().map_or(1.0, |rows| rows[i][v]);
272            h[0] += n * w * p.x;
273            h[1] += n * w * p.y;
274            h[2] += n * w * p.z;
275            h[3] += n * w;
276        }
277        if surface.weights.is_some() {
278            if !(h[3].is_finite() && h[3] > 0.0) {
279                return Err(GeomError::Degenerate(
280                    "iso-curve weight is not positive and finite".to_owned(),
281                ));
282            }
283            points.push(Point3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]));
284            weights.push(h[3]);
285        } else {
286            points.push(Point3::new(h[0], h[1], h[2]));
287        }
288    }
289    Ok(BSplineCurve {
290        degree: surface.v_degree,
291        control_points: points,
292        knots: surface.v_knots.clone(),
293        multiplicities: surface.v_multiplicities.clone(),
294        weights: surface.weights.as_ref().map(|_| weights),
295        knot_spec: surface.knot_spec,
296        closed: surface.v_closed,
297        self_intersect: surface.self_intersect,
298    })
299}
300
301/// The iso-curve `v = parameter`, parametrised by `u`; as
302/// [`iso_curve_at_u`].
303///
304/// # Errors
305///
306/// As [`iso_curve_at_u`].
307pub fn iso_curve_at_v(surface: &BSplineSurface, parameter: Scalar) -> GeomResult<BSplineCurve3> {
308    iso_curve_at_u(&transpose(surface), parameter)
309}
310
311/// Remove one copy of `parameter` from every column, and bound the
312/// deviation; `None` when some column cannot take the removal.
313fn remove_once(
314    columns: &[BSplineCurve<Homogeneous>],
315    parameter: Scalar,
316    rational: bool,
317) -> Option<(Vec<BSplineCurve<Homogeneous>>, Scalar)> {
318    let mut candidate = Vec::with_capacity(columns.len());
319    let mut refined = Vec::with_capacity(columns.len());
320    for column in columns {
321        let removed = crate::degree::remove(column, parameter, |h| *h, |h| h).ok()?;
322        // A removal that drives a weight to zero or below leaves no valid
323        // rational surface: the knot is not removable.
324        if rational
325            && removed
326                .control_points
327                .iter()
328                .any(|h| !(h[3].is_finite() && h[3] > 0.0))
329        {
330            return None;
331        }
332        // Inserting the knot back is exact and restores the original knot
333        // vector, so the two nets can be compared point for point.
334        let back = crate::transform::insert(&removed, parameter, |h| *h, |h| h).ok()?;
335        if back.control_points.len() != column.control_points.len() {
336            return None;
337        }
338        candidate.push(removed);
339        refined.push(back);
340    }
341    let bound = net_deviation(columns, &refined, rational)?;
342    Some((candidate, bound))
343}
344
345/// A bound on the distance between two surfaces on one knot vector, from
346/// their homogeneous nets.
347///
348/// With `H`, `h` the homogeneous points and `W`, `w` the weight sums of the
349/// first and second surface, and any centre `c`:
350/// `S1 - S2 = sum N (H - c W1 - (h - c w2) - (S2 - c)(W1 - w2)) / W1`, so
351/// `|S1 - S2| <= max (|dH_c| + R |dw|) / min W1`, with `R` the radius about
352/// `c` of the second net, which contains `S2`. Polynomial nets reduce to the
353/// largest control-point difference.
354fn net_deviation(
355    first: &[BSplineCurve<Homogeneous>],
356    second: &[BSplineCurve<Homogeneous>],
357    rational: bool,
358) -> Option<Scalar> {
359    let pairs = || {
360        first
361            .iter()
362            .zip(second)
363            .flat_map(|(a, b)| a.control_points.iter().zip(&b.control_points))
364    };
365    if !rational {
366        return Some(
367            pairs()
368                .map(|(a, b)| {
369                    let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
370                    (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
371                })
372                .fold(0.0, Scalar::max),
373        );
374    }
375    let euclid = |h: &Homogeneous| [h[0] / h[3], h[1] / h[3], h[2] / h[3]];
376    let mut min = [Scalar::INFINITY; 3];
377    let mut max = [Scalar::NEG_INFINITY; 3];
378    for (_, b) in pairs() {
379        let p = euclid(b);
380        for k in 0..3 {
381            min[k] = min[k].min(p[k]);
382            max[k] = max[k].max(p[k]);
383        }
384    }
385    let c = [0, 1, 2].map(|k| 0.5 * (min[k] + max[k]));
386    let radius = pairs()
387        .map(|(_, b)| {
388            let p = euclid(b);
389            ((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2) + (p[2] - c[2]).powi(2)).sqrt()
390        })
391        .fold(0.0, Scalar::max);
392    let mut worst: Scalar = 0.0;
393    let mut least_weight = Scalar::INFINITY;
394    for (a, b) in pairs() {
395        least_weight = least_weight.min(a[3]);
396        let d = [0, 1, 2].map(|k| (a[k] - c[k] * a[3]) - (b[k] - c[k] * b[3]));
397        let term = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() + radius * (a[3] - b[3]).abs();
398        worst = worst.max(term);
399    }
400    if least_weight.is_nan() || least_weight <= 0.0 {
401        return None;
402    }
403    let bound = worst / least_weight;
404    bound.is_finite().then_some(bound)
405}
406
407/// Refine a curve to Bezier form: every interior knot at multiplicity equal
408/// to the degree.
409fn bezier_form(curve: &BSplineCurve3) -> GeomResult<BSplineCurve3> {
410    let degree = u32::from(curve.degree);
411    let mut refined = curve.clone();
412    let interior = &curve.knots[1..curve.knots.len() - 1];
413    for &knot in interior {
414        loop {
415            let index = refined
416                .knots
417                .iter()
418                .position(|&k| k == knot)
419                .expect("insertion keeps the knot");
420            if refined.multiplicities[index] >= degree {
421                break;
422            }
423            refined = crate::transform::insert_knot3(&refined, knot)?;
424        }
425    }
426    Ok(refined)
427}
428
429/// Every column of the net, along `u`, in homogeneous coordinates.
430fn homogeneous_columns(surface: &BSplineSurface) -> Vec<BSplineCurve<Homogeneous>> {
431    (0..surface.control_points[0].len())
432        .map(|v| BSplineCurve {
433            degree: surface.u_degree,
434            control_points: surface
435                .control_points
436                .iter()
437                .enumerate()
438                .map(|(u, row)| {
439                    let p = row[v];
440                    let w = surface.weights.as_ref().map_or(1.0, |rows| rows[u][v]);
441                    [w * p.x, w * p.y, w * p.z, w]
442                })
443                .collect(),
444            knots: surface.u_knots.clone(),
445            multiplicities: surface.u_multiplicities.clone(),
446            weights: None,
447            knot_spec: surface.knot_spec,
448            closed: surface.u_closed,
449            self_intersect: surface.self_intersect,
450        })
451        .collect()
452}
453
454/// The surface from homogeneous columns sharing one knot vector.
455fn from_homogeneous_columns(
456    template: &BSplineSurface,
457    columns: &[BSplineCurve<Homogeneous>],
458) -> GeomResult<BSplineSurface> {
459    let rational = template.weights.is_some();
460    let u_count = columns[0].control_points.len();
461    let mut control_points = vec![Vec::with_capacity(columns.len()); u_count];
462    let mut weights = vec![Vec::with_capacity(columns.len()); u_count];
463    for column in columns {
464        for (u, h) in column.control_points.iter().enumerate() {
465            if rational {
466                if !(h[3].is_finite() && h[3] > 0.0) {
467                    return Err(GeomError::Degenerate(
468                        "a weight is not positive and finite".to_owned(),
469                    ));
470                }
471                control_points[u].push(Point3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]));
472                weights[u].push(h[3]);
473            } else {
474                // Polynomial: the weight coordinate is one, up to rounding.
475                control_points[u].push(Point3::new(h[0], h[1], h[2]));
476            }
477        }
478    }
479    Ok(BSplineSurface {
480        u_degree: columns[0].degree,
481        v_degree: template.v_degree,
482        control_points,
483        u_knots: columns[0].knots.clone(),
484        u_multiplicities: columns[0].multiplicities.clone(),
485        v_knots: template.v_knots.clone(),
486        v_multiplicities: template.v_multiplicities.clone(),
487        weights: rational.then_some(weights),
488        knot_spec: template.knot_spec,
489        u_closed: template.u_closed,
490        v_closed: template.v_closed,
491        self_intersect: template.self_intersect,
492    })
493}
494
495/// The surface from Euclidean (possibly rational) columns sharing one knot
496/// vector and degree.
497fn assemble_columns(
498    template: &BSplineSurface,
499    columns: &[BSplineCurve3],
500) -> GeomResult<BSplineSurface> {
501    let first = &columns[0];
502    if columns.iter().any(|c| {
503        c.degree != first.degree
504            || c.knots != first.knots
505            || c.multiplicities != first.multiplicities
506            || c.control_points.len() != first.control_points.len()
507    }) {
508        return Err(GeomError::Degenerate(
509            "columns disagree on their knot vector".to_owned(),
510        ));
511    }
512    let u_count = first.control_points.len();
513    let control_points = (0..u_count)
514        .map(|u| columns.iter().map(|c| c.control_points[u]).collect())
515        .collect();
516    let weights = template.weights.as_ref().map(|_| {
517        (0..u_count)
518            .map(|u| {
519                columns
520                    .iter()
521                    .map(|c| c.weights.as_ref().map_or(1.0, |w| w[u]))
522                    .collect()
523            })
524            .collect()
525    });
526    Ok(BSplineSurface {
527        u_degree: first.degree,
528        v_degree: template.v_degree,
529        control_points,
530        u_knots: first.knots.clone(),
531        u_multiplicities: first.multiplicities.clone(),
532        v_knots: template.v_knots.clone(),
533        v_multiplicities: template.v_multiplicities.clone(),
534        weights,
535        knot_spec: template.knot_spec,
536        u_closed: template.u_closed,
537        v_closed: template.v_closed,
538        self_intersect: template.self_intersect,
539    })
540}
541
542/// The same surface with `u` and `v` exchanged.
543fn transpose(surface: &BSplineSurface) -> BSplineSurface {
544    BSplineSurface {
545        u_degree: surface.v_degree,
546        v_degree: surface.u_degree,
547        control_points: flip(&surface.control_points),
548        u_knots: surface.v_knots.clone(),
549        u_multiplicities: surface.v_multiplicities.clone(),
550        v_knots: surface.u_knots.clone(),
551        v_multiplicities: surface.u_multiplicities.clone(),
552        weights: surface.weights.as_deref().map(flip),
553        knot_spec: surface.knot_spec,
554        u_closed: surface.v_closed,
555        v_closed: surface.u_closed,
556        self_intersect: surface.self_intersect,
557    }
558}
559
560fn flip<T: Copy>(net: &[Vec<T>]) -> Vec<Vec<T>> {
561    (0..net[0].len())
562        .map(|v| net.iter().map(|row| row[v]).collect())
563        .collect()
564}
565
566fn expand(knots: &[Scalar], multiplicities: &[u32]) -> Vec<Scalar> {
567    let mut out = Vec::new();
568    for (&k, &m) in knots.iter().zip(multiplicities) {
569        out.extend(core::iter::repeat_n(k, m as usize));
570    }
571    out
572}
573
574/// The span `i` with `knots[i] <= t < knots[i + 1]`, the last non-empty
575/// one at the domain's end.
576fn find_span(knots: &[Scalar], count: usize, degree: usize, t: Scalar) -> usize {
577    if t >= knots[count] {
578        let mut span = count - 1;
579        while span > degree && knots[span] == knots[count] {
580            span -= 1;
581        }
582        return span;
583    }
584    let mut span = degree;
585    while span + 1 < count && knots[span + 1] <= t {
586        span += 1;
587    }
588    span
589}
590
591/// The `degree + 1` non-zero basis functions on `span` at `t` (Piegl and
592/// Tiller A2.2).
593fn basis_functions(knots: &[Scalar], span: usize, degree: usize, t: Scalar) -> Vec<Scalar> {
594    let mut n = vec![0.0; degree + 1];
595    let mut left = vec![0.0; degree + 1];
596    let mut right = vec![0.0; degree + 1];
597    n[0] = 1.0;
598    for j in 1..=degree {
599        left[j] = t - knots[span + 1 - j];
600        right[j] = knots[span + j] - t;
601        let mut saved = 0.0;
602        for r in 0..j {
603            let temp = n[r] / (right[r + 1] + left[j - r]);
604            n[r] = saved + right[r + 1] * temp;
605            saved = left[j - r] * temp;
606        }
607        n[j] = saved;
608    }
609    n
610}