Skip to main content

geop_core_geometry/nurb_surface/
convex_hull.rs

1use geop_core_math::{
2    convex_hull::ConvexHull,
3    geop_error::{GeopError, GeopResult},
4    scalars::Scalar,
5    vector::Vector3,
6};
7
8use super::NurbSurface;
9
10impl<S: Scalar> NurbSurface<S, 4> {
11    /// Convex hull of the surface patch's Cartesian (dehomogenized) control
12    /// points, stored row-major (matching [`Self::control_points`]).
13    ///
14    /// By the convex-hull property of the NURBS basis, every point on the
15    /// patch lies within this hull.
16    pub fn convex_hull(&self) -> GeopResult<ConvexHull<S, 3>> {
17        let mut points: Vec<Vector3<S>> = Vec::with_capacity(self.control_points.len());
18        for p in &self.control_points {
19            let w = p[3];
20            if w.could_be_equal(S::ZERO) {
21                return Err(GeopError::new(
22                    "convex_hull: control point has zero or near-zero weight",
23                ));
24            }
25            let inv_w = S::ONE.div(w)?;
26            let mut pt = Vector3::zero();
27            for c in 0..3 {
28                pt[c] = p[c].mul(inv_w);
29            }
30            points.push(pt);
31        }
32        Ok(ConvexHull::new(points))
33    }
34
35    /// `(u_size, v_size)`: the control net's own extent along each
36    /// dimension, each the *larger* of the two corner-to-corner edges
37    /// running that direction (u: the `v=0` row's own span *and* the
38    /// `v=max` row's own span; v: the `u=0` column's *and* the `u=max`
39    /// column's). Sampling only one row/column (as an earlier version of
40    /// this did) badly underestimates that dimension's true extent for any
41    /// patch that includes a coordinate-singular pole as one of its own
42    /// `u`/`v` boundaries (e.g. a `revolve`d disk cap row very close to its
43    /// own apex): every point along that *particular* row collapses to
44    /// nearly the same 3-D point regardless of how wide its own parameter
45    /// range still is, while the *other* row does still spread out — so a
46    /// single-row/column measurement can report a dimension as
47    /// already-converged when it isn't, and a subdivision search relying on
48    /// that (see `intersection::curve_surface_intersect`) never picks that
49    /// dimension to split, degrading into combinatorial blowup along
50    /// whatever it splits instead.
51    fn extents(&self) -> GeopResult<(S, S)> {
52        let hull = self.convex_hull()?;
53        let (nu, nv) = (self.num_u(), self.num_v());
54        let (u0v0, u0vn, unv0, unvn) = (
55            hull.points[0],
56            hull.points[nv - 1],
57            hull.points[(nu - 1) * nv],
58            hull.points[(nu - 1) * nv + (nv - 1)],
59        );
60        let u_size_at_v0 = unv0.sub(&u0v0).norm();
61        let u_size_at_vmax = unvn.sub(&u0vn).norm();
62        let v_size_at_u0 = u0vn.sub(&u0v0).norm();
63        let v_size_at_umax = unvn.sub(&unv0).norm();
64        let u_size = if u_size_at_vmax.definitely_greater(u_size_at_v0) {
65            u_size_at_vmax
66        } else {
67            u_size_at_v0
68        };
69        let v_size = if v_size_at_umax.definitely_greater(v_size_at_u0) {
70            v_size_at_umax
71        } else {
72            v_size_at_u0
73        };
74        Ok((u_size, v_size))
75    }
76
77    /// `max(u_size, v_size)`, measured via the convex hull's control-net
78    /// edges (see [`Self::extents`]), used as a convergence measure for
79    /// subdivision algorithms.
80    pub fn size(&self) -> GeopResult<S> {
81        let (u_size, v_size) = self.extents()?;
82        Ok(if v_size.definitely_greater(u_size) {
83            v_size
84        } else {
85            u_size
86        })
87    }
88
89    /// Split along the longer of the u/v dimensions (measured via
90    /// [`Self::extents`]) at that dimension's domain midpoint.
91    pub fn split_mid(&self) -> GeopResult<(NurbSurface<S, 4>, NurbSurface<S, 4>)> {
92        let (u_size, v_size) = self.extents()?;
93        let along_u = u_size.definitely_greater(v_size);
94        if along_u {
95            self.split_u_mid()
96        } else {
97            self.split_v_mid()
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::super::NurbSurface;
105    use geop_core_math::for_all_scalars;
106    use geop_core_math::{
107        scalars::Scalar,
108        vector::{Vector3, Vector4},
109    };
110
111    fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
112        Vector4::from_array([
113            S::from_f64(x),
114            S::from_f64(y),
115            S::from_f64(z),
116            S::from_f64(w),
117        ])
118    }
119
120    fn v3<S: Scalar>(x: f64, y: f64, z: f64) -> Vector3<S> {
121        Vector3::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(z)])
122    }
123
124    fn flat_patch<S: Scalar>() -> NurbSurface<S, 4> {
125        let f = S::from_f64;
126        NurbSurface::try_new(
127            1,
128            1,
129            vec![
130                pt(0., 0., 0., 1.),
131                pt(0., 1., 0., 1.),
132                pt(1., 0., 0., 1.),
133                pt(1., 1., 0., 1.),
134            ],
135            vec![f(0.), f(0.), f(1.), f(1.)],
136            vec![f(0.), f(0.), f(1.), f(1.)],
137        )
138        .unwrap()
139    }
140
141    fn lifted_patch<S: Scalar>() -> NurbSurface<S, 4> {
142        let f = S::from_f64;
143        NurbSurface::try_new(
144            1,
145            1,
146            vec![
147                pt(0., 0., 0., 1.),
148                pt(0., 1., 0., 1.),
149                pt(1., 0., 0., 1.),
150                pt(1., 1., 1., 1.),
151            ],
152            vec![f(0.), f(0.), f(1.), f(1.)],
153            vec![f(0.), f(0.), f(1.), f(1.)],
154        )
155        .unwrap()
156    }
157
158    fn check_flat_patch_contains_surface_points<S: Scalar>() {
159        let s = flat_patch::<S>();
160        let hull = s.convex_hull().unwrap();
161        let p = s.evaluate(S::from_f64(0.5), S::from_f64(0.5)).unwrap();
162        assert!(hull.could_contain(&p));
163    }
164    #[test]
165    fn flat_patch_contains_surface_points() {
166        for_all_scalars!(check_flat_patch_contains_surface_points);
167    }
168
169    fn check_flat_patch_excludes_elevated_point<S: Scalar>() {
170        let hull = flat_patch::<S>().convex_hull().unwrap();
171        assert!(hull.definitely_not_contains(&v3(0.5, 0.5, 5.)));
172    }
173    #[test]
174    fn flat_patch_excludes_elevated_point() {
175        for_all_scalars!(check_flat_patch_excludes_elevated_point);
176    }
177
178    fn check_flat_patch_excludes_point_outside_bounds<S: Scalar>() {
179        let hull = flat_patch::<S>().convex_hull().unwrap();
180        assert!(hull.definitely_not_contains(&v3(2., 0.5, 0.)));
181    }
182    #[test]
183    fn flat_patch_excludes_point_outside_bounds() {
184        for_all_scalars!(check_flat_patch_excludes_point_outside_bounds);
185    }
186
187    fn check_lifted_patch_contains_midheight_point<S: Scalar>() {
188        let hull = lifted_patch::<S>().convex_hull().unwrap();
189        // Centroid of the lifted patch's control points lies at height 0.25.
190        assert!(hull.could_contain(&v3(0.5, 0.5, 0.25)));
191    }
192    #[test]
193    fn lifted_patch_contains_midheight_point() {
194        for_all_scalars!(check_lifted_patch_contains_midheight_point);
195    }
196
197    fn check_lifted_patch_excludes_far_above<S: Scalar>() {
198        let hull = lifted_patch::<S>().convex_hull().unwrap();
199        assert!(hull.definitely_not_contains(&v3(0.5, 0.5, 5.)));
200    }
201    #[test]
202    fn lifted_patch_excludes_far_above() {
203        for_all_scalars!(check_lifted_patch_excludes_far_above);
204    }
205
206    fn check_zero_weight_returns_err<S: Scalar>() {
207        let f = S::from_f64;
208        // A zero-weight control point is now rejected at construction (see
209        // `NurbSurface::try_new`), not merely when the resulting degenerate
210        // surface is later queried.
211        let result = NurbSurface::<S, 4>::try_new(
212            1,
213            1,
214            vec![
215                pt(0., 0., 0., 0.),
216                pt(0., 1., 0., 1.),
217                pt(1., 0., 0., 1.),
218                pt(1., 1., 0., 1.),
219            ],
220            vec![f(0.), f(0.), f(1.), f(1.)],
221            vec![f(0.), f(0.), f(1.), f(1.)],
222        );
223        assert!(result.is_err());
224    }
225    #[test]
226    fn zero_weight_returns_err() {
227        for_all_scalars!(check_zero_weight_returns_err);
228    }
229}