Skip to main content

geop_core_geometry/nurb_curve/
convex_hull.rs

1use geop_core_math::{
2    convex_hull::ConvexHull,
3    geop_error::{GeopError, GeopResult},
4    scalars::Scalar,
5    vector::Vector,
6};
7
8use super::NurbCurve;
9
10/// Dehomogenize `control_points` (`D`-dimensional homogeneous, last component
11/// the weight) into `C`-dimensional Cartesian points (`C` = `D - 1`, passed
12/// explicitly since Rust's stable const generics can't express `D - 1` in a
13/// single generic parameter's bound).
14///
15/// `pub(crate)`, not private: `fat_axis` needs the exact same dehomogenized
16/// points this module already builds for `convex_hull()`, and duplicating
17/// the loop there would just be two copies of the same zero-weight
18/// handling to keep in sync.
19pub(crate) fn dehomogenize<S: Scalar, const D: usize, const C: usize>(
20    control_points: &[Vector<S, D>],
21) -> GeopResult<Vec<Vector<S, C>>> {
22    let mut points = Vec::with_capacity(control_points.len());
23    for p in control_points {
24        let w = p[D - 1];
25        if w.could_be_equal(S::ZERO) {
26            return Err(GeopError::new(
27                "convex_hull: control point has zero or near-zero weight",
28            ));
29        }
30        let inv_w = S::ONE.div(w)?;
31        let mut pt = Vector::<S, C>::zero();
32        for c in 0..C {
33            pt[c] = p[c].mul(inv_w);
34        }
35        points.push(pt);
36    }
37    Ok(points)
38}
39
40fn hull_size<S: Scalar, const N: usize>(hull: &ConvexHull<S, N>) -> S {
41    hull.points[hull.points.len() - 1]
42        .sub(&hull.points[0])
43        .norm()
44}
45
46/// Bridges `NurbCurve<S, D>`'s pair of concrete `convex_hull()`/`size()`
47/// impls (`D=4` → `ConvexHull<S,3>`, `D=3` → `ConvexHull<S,2>`) so code
48/// generic over `D` (e.g. `curve_curve_intersect`, `curve_could_contain`) can
49/// call them via a `where NurbCurve<S, D>: HasConvexHull<S, C>` bound —
50/// Rust's stable const generics can't express `C = D - 1` directly in a
51/// single generic function.
52pub trait HasConvexHull<S: Scalar, const C: usize> {
53    fn convex_hull(&self) -> GeopResult<ConvexHull<S, C>>;
54    fn size(&self) -> GeopResult<S>;
55}
56
57impl<S: Scalar> NurbCurve<S, 4> {
58    /// Convex hull of the curve's Cartesian (dehomogenized) control points.
59    ///
60    /// By the convex-hull property of the NURBS basis, every point on the
61    /// curve lies within this hull.
62    pub fn convex_hull(&self) -> GeopResult<ConvexHull<S, 3>> {
63        Ok(ConvexHull::new(dehomogenize(&self.control_points)?))
64    }
65
66    /// Chord length of the curve's convex hull, used as a convergence
67    /// measure for subdivision algorithms.
68    pub fn size(&self) -> GeopResult<S> {
69        Ok(hull_size(&self.convex_hull()?))
70    }
71}
72
73impl<S: Scalar> HasConvexHull<S, 3> for NurbCurve<S, 4> {
74    fn convex_hull(&self) -> GeopResult<ConvexHull<S, 3>> {
75        self.convex_hull()
76    }
77    fn size(&self) -> GeopResult<S> {
78        self.size()
79    }
80}
81
82impl<S: Scalar> NurbCurve<S, 3> {
83    /// Convex hull of the pcurve's Cartesian (dehomogenized) control points.
84    ///
85    /// By the convex-hull property of the NURBS basis, every point on the
86    /// curve lies within this hull.
87    pub fn convex_hull(&self) -> GeopResult<ConvexHull<S, 2>> {
88        Ok(ConvexHull::new(dehomogenize(&self.control_points)?))
89    }
90
91    /// Chord length of the pcurve's convex hull, used as a convergence
92    /// measure for subdivision algorithms.
93    pub fn size(&self) -> GeopResult<S> {
94        Ok(hull_size(&self.convex_hull()?))
95    }
96}
97
98impl<S: Scalar> HasConvexHull<S, 2> for NurbCurve<S, 3> {
99    fn convex_hull(&self) -> GeopResult<ConvexHull<S, 2>> {
100        self.convex_hull()
101    }
102    fn size(&self) -> GeopResult<S> {
103        self.size()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use crate::nurb_curve::NurbCurve;
110    use geop_core_math::for_all_scalars;
111    use geop_core_math::{scalars::Scalar, vector::Vector4};
112
113    fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
114        Vector4::from_array([
115            S::from_f64(x),
116            S::from_f64(y),
117            S::from_f64(z),
118            S::from_f64(w),
119        ])
120    }
121
122    fn check_hull_contains_curve_points<S: Scalar>() {
123        let f = S::from_f64;
124        let c = NurbCurve::try_new(
125            2,
126            vec![pt(0., 0., 0., 1.), pt(0.5, 1., 0., 1.), pt(1., 0., 0., 1.)],
127            vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
128        )
129        .unwrap();
130        let hull = c.convex_hull().unwrap();
131        let p = c.evaluate(S::from_f64(0.5)).unwrap();
132        assert!(hull.could_contain(&p));
133    }
134    #[test]
135    fn hull_contains_curve_points() {
136        for_all_scalars!(check_hull_contains_curve_points);
137    }
138
139    fn check_hull_points_match_dehomogenized_control_points<S: Scalar>() {
140        let f = S::from_f64;
141        let c = NurbCurve::try_new(
142            2,
143            vec![pt(0., 0., 0., 1.), pt(0.5, 2., 0., 2.), pt(1., 0., 0., 1.)],
144            vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
145        )
146        .unwrap();
147        let hull = c.convex_hull().unwrap();
148        assert!(hull.points[0][0].could_be_equal(S::ZERO));
149        // (0.5, 2, 0, 2) dehomogenizes to (0.25, 1, 0)
150        assert!(hull.points[1][0].could_be_equal(S::from_f64(0.25)));
151        assert!(hull.points[1][1].could_be_equal(S::ONE));
152        assert!(hull.points[2][0].could_be_equal(S::ONE));
153    }
154    #[test]
155    fn hull_points_match_dehomogenized_control_points() {
156        for_all_scalars!(check_hull_points_match_dehomogenized_control_points);
157    }
158
159    fn check_hull_excludes_far_point<S: Scalar>() {
160        let f = S::from_f64;
161        let c = NurbCurve::try_new(
162            1,
163            vec![pt(0., 0., 0., 1.), pt(1., 0., 0., 1.)],
164            vec![f(0.), f(0.), f(1.), f(1.)],
165        )
166        .unwrap();
167        let hull = c.convex_hull().unwrap();
168        assert!(
169            hull.definitely_not_contains(&geop_core_math::vector::Vector3::from_array([
170                f(0.5),
171                f(5.),
172                f(0.),
173            ]))
174        );
175    }
176    #[test]
177    fn hull_excludes_far_point() {
178        for_all_scalars!(check_hull_excludes_far_point);
179    }
180
181    fn check_zero_weight_returns_err<S: Scalar>() {
182        let f = S::from_f64;
183        // A zero-weight control point is now rejected at construction (see
184        // `NurbCurve::try_new`), not merely when the resulting degenerate
185        // curve is later queried.
186        let result = NurbCurve::<S, 4>::try_new(
187            1,
188            vec![pt(0., 0., 0., 0.), pt(1., 0., 0., 1.)],
189            vec![f(0.), f(0.), f(1.), f(1.)],
190        );
191        assert!(result.is_err());
192    }
193    #[test]
194    fn zero_weight_returns_err() {
195        for_all_scalars!(check_zero_weight_returns_err);
196    }
197
198    fn check_2d_hull_contains_pcurve_points<S: Scalar>() {
199        let f = S::from_f64;
200        let c: crate::nurb_curve::NurbCurve2D<S> = NurbCurve::try_new(
201            1,
202            vec![
203                geop_core_math::vector::Vector3::from_array([f(0.), f(0.), f(1.)]),
204                geop_core_math::vector::Vector3::from_array([f(1.), f(1.), f(1.)]),
205            ],
206            vec![f(0.), f(0.), f(1.), f(1.)],
207        )
208        .unwrap();
209        let hull = c.convex_hull().unwrap();
210        assert!(
211            hull.could_contain(&geop_core_math::vector::Vector2::from_array([
212                f(0.5),
213                f(0.5)
214            ]))
215        );
216        assert!(
217            hull.definitely_not_contains(&geop_core_math::vector::Vector2::from_array([
218                f(5.),
219                f(5.)
220            ]))
221        );
222    }
223    #[test]
224    fn hull_2d_contains_pcurve_points() {
225        for_all_scalars!(check_2d_hull_contains_pcurve_points);
226    }
227}