Skip to main content

geop_core_geometry/nurb_curve/
mod.rs

1mod convex_hull;
2mod derivative;
3mod evaluate;
4mod interpolate;
5mod project;
6mod refine;
7pub use interpolate::true_point_fractions;
8pub use refine::ParameterRefinable;
9mod reverse;
10mod split;
11mod sweep;
12mod tangent;
13mod translate;
14
15use std::fmt::Display;
16
17pub use convex_hull::HasConvexHull;
18// `fat_axis` needs the same dehomogenized points `convex_hull()` builds —
19// re-exported at this level since `convex_hull` itself is a private
20// submodule (only `HasConvexHull` was public before).
21pub(crate) use convex_hull::dehomogenize;
22
23use crate::aabb::compute_aabb;
24use geop_core_math::{
25    geop_error::{GeopError, GeopResult},
26    scalars::Scalar,
27    vector::Vector,
28};
29
30/// A NURBS curve whose control points live in `D`-dimensional homogeneous space.
31///
32/// - `D = 4`: 3-D curve  — control points are `(wx, wy, wz, w)`.
33/// - `D = 3`: 2-D curve (pcurve) — control points are `(wu, wv, w)`.
34#[derive(Clone, Debug)]
35pub struct NurbCurve<S: Scalar, const D: usize> {
36    pub degree: usize,
37    pub control_points: Vec<Vector<S, D>>,
38    pub knot_vector: Vec<S>,
39    /// Cached axis-aligned bounding box of the (dehomogenized) control
40    /// points — see [`crate::aabb::compute_aabb`]. Every constructor here
41    /// fills this in once, so the intersection search's per-node prefilter
42    /// (`aabb_could_overlap`, in `intersection::curve_curve`/
43    /// `curve_surface`) never has to recompute it.
44    pub(crate) aabb: [S; 3],
45}
46
47/// 3-D NURBS curve (homogeneous control points in ℝ⁴).
48pub type NurbCurve3D<S> = NurbCurve<S, 4>;
49
50/// 2-D parameter-space NURBS curve / pcurve (homogeneous control points in ℝ³).
51pub type NurbCurve2D<S> = NurbCurve<S, 3>;
52
53impl<S: Scalar, const D: usize> NurbCurve<S, D> {
54    pub fn try_new(
55        degree: usize,
56        control_points: Vec<Vector<S, D>>,
57        knot_vector: Vec<S>,
58    ) -> GeopResult<Self> {
59        let n = control_points.len();
60        if knot_vector.len() != n + degree + 1 {
61            return Err(GeopError::new(&format!(
62                "Invalid knot vector length: expected {}, got {}",
63                n + degree + 1,
64                knot_vector.len()
65            )));
66        }
67        // Every weight must be definitely positive. That is what makes
68        // `W(t) = Σ w_i N_i(t) > 0` on the whole domain, which the convex hull
69        // property and `contains::curve`'s division-free per-axis functions
70        // `X_k(t) - p_k W(t)` rely on. Zero or negative weights have no use
71        // case here, so they are rejected at construction rather than
72        // surfacing as a division by zero far downstream. `split` forms
73        // convex combinations of existing control points and so preserves
74        // this; `derivative` does not (its last coordinate is `W'`, not a
75        // weight), so a derivative curve is a hodograph for evaluation only
76        // and must never be handed to a containment/intersection search.
77        for p in &control_points {
78            if !p[D - 1].definitely_greater(S::ZERO) {
79                return Err(GeopError::new(&format!(
80                    "NurbCurve::try_new: control point {p:?} has a weight that is not definitely positive"
81                )));
82            }
83        }
84        let aabb = compute_aabb(&control_points);
85        Ok(Self {
86            degree,
87            control_points,
88            knot_vector,
89            aabb,
90        })
91    }
92
93    /// Refresh the cached [`Self::aabb`] from the current `control_points`.
94    ///
95    /// Every constructor in this module keeps `aabb` in sync automatically,
96    /// but `control_points` is a `pub` field and at least one caller outside
97    /// this crate (`Model::reverse_face`, mirroring a pcurve's control
98    /// points in place to flip a face) legitimately mutates it directly
99    /// rather than building a new curve — call this afterwards or the
100    /// cached box silently goes stale and the intersection search's
101    /// `aabb_could_overlap` prefilter starts pruning real overlaps.
102    pub fn recompute_aabb(&mut self) {
103        self.aabb = compute_aabb(&self.control_points);
104    }
105
106    /// Valid parameter range `(start_t, end_t)` of this curve.
107    pub fn domain(&self) -> (S, S) {
108        let p = self.degree;
109        let n = self.control_points.len() - 1;
110        (self.knot_vector[p], self.knot_vector[n + 1])
111    }
112
113    pub fn domain_as_scalar(&self) -> S {
114        let (s, e) = self.domain();
115        s.union(e)
116    }
117
118    /// A degenerate, maximally-unsharp curve: a single control point whose
119    /// every coordinate is [`Scalar::ENTIRE`], over a domain that accepts
120    /// any parameter. `evaluate()` at any `t` returns `ENTIRE` in every
121    /// coordinate, so it `could_be_equal`s any point — a placeholder for
122    /// geometry that is not yet known.
123    pub fn everything() -> Self {
124        let mut cp = Vector::<S, D>::everything();
125        cp[D - 1] = S::ONE;
126        NurbCurve {
127            degree: 0,
128            control_points: vec![cp],
129            knot_vector: vec![S::ENTIRE, S::ENTIRE],
130            // Matches-anything, same as every other coordinate of this
131            // placeholder — no bounding box has been established yet.
132            aabb: [S::ENTIRE; 3],
133        }
134    }
135}
136
137impl<S: Scalar> Display for NurbCurve2D<S> {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        let start = self
140            .evaluate(self.domain().0)
141            .map(|p| p.to_string())
142            .unwrap_or_else(|_| "N/A".to_string());
143        let end = self
144            .evaluate(self.domain().1)
145            .map(|p| p.to_string())
146            .unwrap_or_else(|_| "N/A".to_string());
147        write!(f, "NurbCurve({} -> {})", start, end)
148    }
149}
150
151impl<S: Scalar> Display for NurbCurve3D<S> {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        let start = self
154            .evaluate(self.domain().0)
155            .map(|p| p.to_string())
156            .unwrap_or_else(|_| "N/A".to_string());
157        let end = self
158            .evaluate(self.domain().1)
159            .map(|p| p.to_string())
160            .unwrap_or_else(|_| "N/A".to_string());
161        write!(f, "NurbCurve({} -> {})", start, end)
162    }
163}