Skip to main content

geop_core_geometry/nurb_surface/
mod.rs

1mod convex_hull;
2mod curvature;
3mod evaluate;
4mod fit_pcurve;
5mod normal;
6mod project;
7mod reverse;
8mod split;
9mod translate;
10
11use std::fmt::Display;
12
13pub use project::clamp;
14
15use crate::aabb::compute_aabb;
16use geop_core_math::{
17    geop_error::{GeopError, GeopResult},
18    scalars::Scalar,
19    vector::Vector,
20};
21
22/// A NURBS surface patch whose control points live in `D`-dimensional homogeneous space.
23///
24/// In practice `D = 4` for all 3-D surfaces (`(wx, wy, wz, w)` control points).
25///
26/// Control points are stored row-major: `control_points[i * num_v + j]` is the
27/// point at u-index `i` (0 ≤ i < `num_u`) and v-index `j` (0 ≤ j < `num_v`).
28#[derive(Clone, Debug)]
29pub struct NurbSurface<S: Scalar, const D: usize> {
30    pub degree_u: usize,
31    pub degree_v: usize,
32    pub num_u: usize,
33    pub num_v: usize,
34    pub control_points: Vec<Vector<S, D>>,
35    pub knot_vector_u: Vec<S>,
36    pub knot_vector_v: Vec<S>,
37    /// Cached axis-aligned bounding box of the (dehomogenized) control
38    /// points — see [`crate::aabb::compute_aabb`] and `NurbCurve`'s own
39    /// `aabb` field doc comment.
40    pub(crate) aabb: [S; 3],
41}
42
43/// 3-D NURBS surface (homogeneous control points in ℝ⁴).
44pub type NurbSurface3D<S> = NurbSurface<S, 4>;
45
46impl<S: Scalar, const D: usize> NurbSurface<S, D> {
47    pub fn try_new(
48        degree_u: usize,
49        degree_v: usize,
50        control_points: Vec<Vector<S, D>>,
51        knot_vector_u: Vec<S>,
52        knot_vector_v: Vec<S>,
53    ) -> GeopResult<Self> {
54        let len_u = knot_vector_u.len();
55        let len_v = knot_vector_v.len();
56
57        if len_u < degree_u + 2 {
58            return Err(GeopError::new(
59                "NurbSurface: knot_vector_u too short for the given degree",
60            ));
61        }
62        if len_v < degree_v + 2 {
63            return Err(GeopError::new(
64                "NurbSurface: knot_vector_v too short for the given degree",
65            ));
66        }
67
68        let num_u = len_u - degree_u - 1;
69        let num_v = len_v - degree_v - 1;
70
71        if control_points.len() != num_u * num_v {
72            return Err(GeopError::new(format!(
73                "NurbSurface: expected {} control points ({}×{}), got {}",
74                num_u * num_v,
75                num_u,
76                num_v,
77                control_points.len()
78            )));
79        }
80
81        // See `NurbCurve::try_new`'s identical check: a control point whose
82        // weight *could* be zero produces a surface that's undefined
83        // somewhere in its domain, silently, wherever the first
84        // `.convex_hull()`/dehomogenization call happens to land — reject
85        // it here instead.
86        for p in &control_points {
87            if p[D - 1].could_be_equal(S::ZERO) {
88                return Err(GeopError::new(&format!(
89                    "NurbSurface::try_new: control point {p:?} has a weight that could be zero"
90                )));
91            }
92        }
93
94        let aabb = compute_aabb(&control_points);
95        Ok(Self {
96            degree_u,
97            degree_v,
98            num_u,
99            num_v,
100            control_points,
101            knot_vector_u,
102            knot_vector_v,
103            aabb,
104        })
105    }
106
107    /// Refresh the cached [`Self::aabb`] from the current `control_points`
108    /// — see [`crate::nurb_curve::NurbCurve::recompute_aabb`]'s identical
109    /// doc comment for why this is needed at all (`control_points` is
110    /// `pub`, and code outside this crate does mutate it in place).
111    pub fn recompute_aabb(&mut self) {
112        self.aabb = compute_aabb(&self.control_points);
113    }
114
115    /// Valid parameter range in the u direction: `(u_min, u_max)`.
116    pub fn domain_u(&self) -> (S, S) {
117        (
118            self.knot_vector_u[self.degree_u],
119            self.knot_vector_u[self.num_u],
120        )
121    }
122
123    /// Valid parameter range in the v direction: `(v_min, v_max)`.
124    pub fn domain_v(&self) -> (S, S) {
125        (
126            self.knot_vector_v[self.degree_v],
127            self.knot_vector_v[self.num_v],
128        )
129    }
130
131    /// Number of control points in the u direction.
132    pub fn num_u(&self) -> usize {
133        self.num_u
134    }
135
136    /// Number of control points in the v direction.
137    pub fn num_v(&self) -> usize {
138        self.num_v
139    }
140
141    /// Whether this is the [`NurbSurface::everything`] placeholder — the
142    /// unsharp stand-in a face carries before it is given real geometry.
143    ///
144    /// A finished solid must have none: a placeholder face has no position,
145    /// so nothing can be classified against it, and it will silently swallow
146    /// any containment or intersection query it is handed (every comparison
147    /// against `ENTIRE` succeeds). Construction code that splits faces off a
148    /// starting placeholder has to consume the last one rather than leave it
149    /// behind, and this is how a test says so.
150    pub fn is_everything(&self) -> bool {
151        self.num_u == 1
152            && self.num_v == 1
153            && !self.control_points[0][0].is_sharp()
154            && self.knot_vector_u.iter().all(|k| !k.is_sharp())
155    }
156
157    /// A degenerate, maximally-unsharp surface: a single 1×1 control point
158    /// whose every coordinate is [`Scalar::ENTIRE`], over a domain that
159    /// accepts any `(u, v)`. `evaluate()` anywhere returns `ENTIRE` in every
160    /// coordinate, so it `could_be_equal`s any point — a placeholder for
161    /// geometry that is not yet known.
162    pub fn everything() -> Self {
163        let mut cp = Vector::<S, D>::everything();
164        cp[D - 1] = S::ONE;
165        NurbSurface {
166            degree_u: 0,
167            degree_v: 0,
168            num_u: 1,
169            num_v: 1,
170            control_points: vec![cp],
171            knot_vector_u: vec![S::ENTIRE, S::ENTIRE],
172            knot_vector_v: vec![S::ENTIRE, S::ENTIRE],
173            // Matches-anything, same as every other coordinate of this
174            // placeholder — no bounding box has been established yet.
175            aabb: [S::ENTIRE; 3],
176        }
177    }
178}
179
180impl<S: Scalar> Display for NurbSurface3D<S> {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        let p00 = self
183            .evaluate(self.knot_vector_u[0], self.knot_vector_v[0])
184            .map(|p| p.to_string())
185            .unwrap_or_else(|_| "N/A".to_string());
186        let p01 = self
187            .evaluate(self.knot_vector_u[0], self.knot_vector_v[self.num_v])
188            .map(|p| p.to_string())
189            .unwrap_or_else(|_| "N/A".to_string());
190        let p10 = self
191            .evaluate(self.knot_vector_u[self.num_u], self.knot_vector_v[0])
192            .map(|p| p.to_string())
193            .unwrap_or_else(|_| "N/A".to_string());
194        let p11 = self
195            .evaluate(
196                self.knot_vector_u[self.num_u],
197                self.knot_vector_v[self.num_v],
198            )
199            .map(|p| p.to_string())
200            .unwrap_or_else(|_| "N/A".to_string());
201
202        write!(
203            f,
204            "NurbSurface((0, 0) -> {}, (1, 0) -> {}, (1, 1) -> {}, (0, 1) -> {})",
205            p00, p10, p11, p01
206        )
207    }
208}