Skip to main content

geop_core_geometry/contains/
surface_bisect.rs

1//! The previous surface/point containment — convex hull test and
2//! bisection — kept only as the baseline for
3//! `examples/surface_contains_bench.rs`. The kernel uses [`super::surface`].
4
5use std::collections::VecDeque;
6
7use crate::{aabb::aabb_could_contain, nurb_surface::NurbSurface};
8use geop_core_math::{geop_error::GeopResult, scalars::Scalar, vector::Vector3};
9
10/// Folds `(u, v)` into the running `(u, v)` solution, unioning each
11/// component independently — see `curve::union_domain`'s own doc comment for
12/// why folding in every converged patch (instead of returning the first)
13/// matters.
14fn union_uv<S: Scalar>(solution: Option<(S, S)>, uv: (S, S)) -> (S, S) {
15    match solution {
16        Some((eu, ev)) => (eu.union(uv.0), ev.union(uv.1)),
17        None => uv,
18    }
19}
20
21/// BFS over subdivisions of `surface`, exploring every node up to the
22/// `max_nodes` budget (never stopping early at the first hit) and returning
23/// the union of every converged patch's own `(u, v)` domain (each axis as a
24/// single unsharp interval scalar spanning that patch, not a numeric
25/// midpoint — see `patch_uv`) — a patch converges once its convex hull could
26/// contain `point` and its
27/// maximum span (max of the u-edge and v-edge of its control net) is no
28/// longer definitely greater than `min_subdivision_size`. `None` if no patch
29/// converged within budget.
30///
31/// Exploring to completion (rather than returning on the first match)
32/// matters for the same reason as `curve_bisect::curve_could_contain`: more than one patch
33/// can independently converge on `point` (e.g. near a seam, a pole, or
34/// simply because the point is close to more than one subdivision
35/// boundary), and stopping early would silently narrow the answer to
36/// whichever one the BFS happened to visit first.
37///
38/// At each step the patch is split along the longer of its two parameter-domain
39/// dimensions, keeping the BFS balanced.
40pub fn surface_could_contain<S: Scalar>(
41    surface: &NurbSurface<S, 4>,
42    point: &Vector3<S>,
43    max_nodes: usize,
44    min_subdivision_size: S,
45) -> GeopResult<Option<(S, S)>> {
46    let mut queue: VecDeque<NurbSurface<S, 4>> = VecDeque::new();
47    queue.push_back(surface.clone());
48
49    let mut explored = 0usize;
50    let mut solution: Option<(S, S)> = None;
51
52    while let Some(patch) = queue.pop_front() {
53        if explored >= max_nodes {
54            break;
55        }
56        explored += 1;
57
58        // The whole patch domain, unioned into a single (necessarily
59        // unsharp) interval per axis — like `NurbCurve::domain_as_scalar` —
60        // rather than its numeric midpoint, so a converged patch's genuine
61        // remaining uncertainty (up to `min_subdivision_size`) propagates
62        // through as interval width instead of being silently collapsed
63        // into one (possibly off-curve) point.
64        let patch_uv = || -> (S, S) {
65            let (u_min, u_max) = patch.domain_u();
66            let (v_min, v_max) = patch.domain_v();
67            (u_min.union(u_max), v_min.union(v_max))
68        };
69
70        // Cheap prefilter: see `contains::curve_bisect::curve_could_contain`'s
71        // identical structure — the cached bounding box is far quicker to
72        // compare than building a convex hull and running GJK, and just as
73        // sound.
74        if !aabb_could_contain(&patch.aabb, point) {
75            continue;
76        }
77
78        let hull = match patch.convex_hull() {
79            Ok(hull) => hull,
80            Err(_) => {
81                // Degenerate patch: can't be ruled out, so its whole domain
82                // conservatively folds into the solution instead of aborting
83                // the rest of the search.
84                solution = Some(union_uv(solution, patch_uv()));
85                continue;
86            }
87        };
88
89        if !hull.could_contain(point) {
90            continue;
91        }
92
93        // Patch could contain the point — is it small enough?
94        let nu = patch.num_u();
95        let nv = patch.num_v();
96        let p00 = hull.points[0];
97        let pn0 = hull.points[(nu - 1) * nv];
98        let p0m = hull.points[nv - 1];
99        let u_size = pn0.sub(&p00).norm();
100        let v_size = p0m.sub(&p00).norm();
101        let max_size = if v_size.definitely_greater(u_size) {
102            v_size
103        } else {
104            u_size
105        };
106        if !max_size.definitely_greater(min_subdivision_size) {
107            solution = Some(union_uv(solution, patch_uv()));
108            continue;
109        }
110
111        // Split along the longer dimension.
112        let (left, right) = if v_size.definitely_greater(u_size) {
113            let (v_min, v_max) = patch.domain_v();
114            // A self-chosen subdivision point: any value in the interval cuts
115            // it equally well, so sharpening loses no accuracy and keeps
116            // repeated splits from compounding width (see AGENTS.md).
117            let mid_v = v_min.add(v_max).div(S::TWO)?.sharpen();
118            match patch.split_v(mid_v) {
119                Ok(halves) => halves,
120                Err(_) => {
121                    solution = Some(union_uv(solution, patch_uv()));
122                    continue;
123                }
124            }
125        } else {
126            let (u_min, u_max) = patch.domain_u();
127            let mid_u = u_min.add(u_max).div(S::TWO)?.sharpen();
128            match patch.split_u(mid_u) {
129                Ok(halves) => halves,
130                Err(_) => {
131                    solution = Some(union_uv(solution, patch_uv()));
132                    continue;
133                }
134            }
135        };
136
137        queue.push_back(left);
138        queue.push_back(right);
139    }
140
141    Ok(solution)
142}
143
144/// Boolean negation of [`surface_could_contain`].
145pub fn surface_definitely_not_contains<S: Scalar>(
146    surface: &NurbSurface<S, 4>,
147    point: &Vector3<S>,
148    max_nodes: usize,
149    epsilon: S,
150) -> GeopResult<bool> {
151    Ok(surface_could_contain(surface, point, max_nodes, epsilon)?.is_none())
152}
153
154#[cfg(test)]
155mod tests {
156    use super::{surface_could_contain, surface_definitely_not_contains};
157    use crate::nurb_surface::NurbSurface;
158    use geop_core_math::for_all_scalars;
159    use geop_core_math::{
160        scalars::Scalar,
161        vector::{Vector3, Vector4},
162    };
163
164    fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
165        Vector4::from_array([
166            S::from_f64(x),
167            S::from_f64(y),
168            S::from_f64(z),
169            S::from_f64(w),
170        ])
171    }
172
173    fn v3<S: Scalar>(x: f64, y: f64, z: f64) -> Vector3<S> {
174        Vector3::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(z)])
175    }
176
177    const MAX: usize = 2000;
178    const EPS: f64 = 1e-3;
179
180    fn flat_patch<S: Scalar>() -> NurbSurface<S, 4> {
181        let f = S::from_f64;
182        NurbSurface::try_new(
183            1,
184            1,
185            vec![
186                pt(0., 0., 0., 1.),
187                pt(0., 1., 0., 1.),
188                pt(1., 0., 0., 1.),
189                pt(1., 1., 0., 1.),
190            ],
191            vec![f(0.), f(0.), f(1.), f(1.)],
192            vec![f(0.), f(0.), f(1.), f(1.)],
193        )
194        .unwrap()
195    }
196
197    /// One corner lifted to z=1.
198    fn lifted_patch<S: Scalar>() -> NurbSurface<S, 4> {
199        let f = S::from_f64;
200        NurbSurface::try_new(
201            1,
202            1,
203            vec![
204                pt(0., 0., 0., 1.),
205                pt(0., 1., 0., 1.),
206                pt(1., 0., 0., 1.),
207                pt(1., 1., 1., 1.),
208            ],
209            vec![f(0.), f(0.), f(1.), f(1.)],
210            vec![f(0.), f(0.), f(1.), f(1.)],
211        )
212        .unwrap()
213    }
214
215    // ── On-surface points ─────────────────────────────────────────────────────
216
217    fn check_flat_surface_contains_corner<S: Scalar>() {
218        let s = flat_patch::<S>();
219        let p = s.evaluate(S::ZERO, S::ZERO).unwrap();
220        assert!(
221            surface_could_contain(&s, &p, MAX, S::from_f64(EPS))
222                .unwrap()
223                .is_some()
224        );
225    }
226    #[test]
227    fn flat_surface_contains_corner() {
228        for_all_scalars!(check_flat_surface_contains_corner);
229    }
230
231    fn check_flat_surface_contains_center<S: Scalar>() {
232        let s = flat_patch::<S>();
233        let p = s.evaluate(S::from_f64(0.5), S::from_f64(0.5)).unwrap();
234        assert!(
235            surface_could_contain(&s, &p, MAX, S::from_f64(EPS))
236                .unwrap()
237                .is_some()
238        );
239    }
240    #[test]
241    fn flat_surface_contains_center() {
242        for_all_scalars!(check_flat_surface_contains_center);
243    }
244
245    fn check_flat_surface_contains_midedge<S: Scalar>() {
246        let s = flat_patch::<S>();
247        let p = s.evaluate(S::from_f64(0.5), S::ZERO).unwrap();
248        assert!(
249            surface_could_contain(&s, &p, MAX, S::from_f64(EPS))
250                .unwrap()
251                .is_some()
252        );
253    }
254    #[test]
255    fn flat_surface_contains_midedge() {
256        for_all_scalars!(check_flat_surface_contains_midedge);
257    }
258
259    fn check_lifted_surface_contains_corner<S: Scalar>() {
260        let s = lifted_patch::<S>();
261        let p = s.evaluate(S::ZERO, S::ZERO).unwrap();
262        assert!(
263            surface_could_contain(&s, &p, MAX, S::from_f64(EPS))
264                .unwrap()
265                .is_some()
266        );
267    }
268    #[test]
269    fn lifted_surface_contains_corner() {
270        for_all_scalars!(check_lifted_surface_contains_corner);
271    }
272
273    fn check_lifted_surface_contains_midpoint<S: Scalar>() {
274        let s = lifted_patch::<S>();
275        let p = s.evaluate(S::from_f64(0.5), S::from_f64(0.5)).unwrap();
276        assert!(
277            surface_could_contain(&s, &p, MAX, S::from_f64(EPS))
278                .unwrap()
279                .is_some()
280        );
281    }
282    #[test]
283    fn lifted_surface_contains_midpoint() {
284        for_all_scalars!(check_lifted_surface_contains_midpoint);
285    }
286
287    // ── Off-surface points ────────────────────────────────────────────────────
288
289    fn check_flat_surface_excludes_above<S: Scalar>() {
290        let s = flat_patch::<S>();
291        assert!(
292            surface_definitely_not_contains(&s, &v3(0.5, 0.5, 5.), MAX, S::from_f64(EPS)).unwrap()
293        );
294    }
295    #[test]
296    fn flat_surface_excludes_above() {
297        for_all_scalars!(check_flat_surface_excludes_above);
298    }
299
300    fn check_flat_surface_excludes_below<S: Scalar>() {
301        let s = flat_patch::<S>();
302        assert!(
303            surface_definitely_not_contains(&s, &v3(0.5, 0.5, -5.), MAX, S::from_f64(EPS)).unwrap()
304        );
305    }
306    #[test]
307    fn flat_surface_excludes_below() {
308        for_all_scalars!(check_flat_surface_excludes_below);
309    }
310
311    fn check_flat_surface_excludes_outside_uv<S: Scalar>() {
312        let s = flat_patch::<S>();
313        assert!(
314            surface_definitely_not_contains(&s, &v3(5., 5., 0.), MAX, S::from_f64(EPS)).unwrap()
315        );
316    }
317    #[test]
318    fn flat_surface_excludes_outside_uv() {
319        for_all_scalars!(check_flat_surface_excludes_outside_uv);
320    }
321
322    fn check_lifted_surface_excludes_far_above<S: Scalar>() {
323        let s = lifted_patch::<S>();
324        assert!(
325            surface_definitely_not_contains(&s, &v3(0.5, 0.5, 10.), MAX, S::from_f64(EPS)).unwrap()
326        );
327    }
328    #[test]
329    fn lifted_surface_excludes_far_above() {
330        for_all_scalars!(check_lifted_surface_excludes_far_above);
331    }
332
333    fn check_lifted_surface_excludes_outside_uv<S: Scalar>() {
334        let s = lifted_patch::<S>();
335        assert!(
336            surface_definitely_not_contains(&s, &v3(5., 5., 0.), MAX, S::from_f64(EPS)).unwrap()
337        );
338    }
339    #[test]
340    fn lifted_surface_excludes_outside_uv() {
341        for_all_scalars!(check_lifted_surface_excludes_outside_uv);
342    }
343
344    // ── Budget ────────────────────────────────────────────────────────────────
345
346    fn check_zero_budget_always_false<S: Scalar>() {
347        let s = flat_patch::<S>();
348        let p = s.evaluate(S::from_f64(0.5), S::from_f64(0.5)).unwrap();
349        assert!(
350            surface_could_contain(&s, &p, 0, S::from_f64(EPS))
351                .unwrap()
352                .is_none()
353        );
354    }
355    #[test]
356    fn zero_budget_always_false() {
357        for_all_scalars!(check_zero_budget_always_false);
358    }
359}