Skip to main content

geop_core_geometry/contains/
curve_bisect.rs

1//! The previous curve/point containment — convex hull test and bisection —
2//! kept only as the baseline for `examples/curve_contains_bench.rs`. The
3//! kernel uses [`super::curve`].
4
5use std::collections::VecDeque;
6
7use crate::{
8    aabb::aabb_could_contain,
9    nurb_curve::{HasConvexHull, NurbCurve},
10};
11use geop_core_math::{geop_error::GeopResult, scalars::Scalar, vector::Vector};
12
13/// Folds `domain` into the running solution: the first one found stands
14/// as-is, every subsequent one widens it via [`Scalar::union`] — so the
15/// final result covers every converged segment found, not just whichever
16/// one the BFS happened to reach first.
17fn union_domain<S: Scalar>(solution: Option<S>, domain: S) -> S {
18    match solution {
19        Some(existing) => existing.union(domain),
20        None => domain,
21    }
22}
23
24/// BFS over subdivisions of `curve`, exploring every node up to the
25/// `max_nodes` budget (never stopping early at the first hit) and returning
26/// the [`Scalar::union`] of every converged segment's own domain — a segment
27/// converges once its convex hull could contain `point` and its chord length
28/// is no longer definitely greater than `min_subdivision_size`. `None` if no
29/// segment converged within budget.
30///
31/// Exploring to completion (rather than returning on the first match)
32/// matters because more than one segment can independently converge on
33/// `point` — e.g. near a curve self-intersection, or simply because more
34/// than one leaf of the subdivision tree ends up within tolerance of it —
35/// and stopping early would silently narrow the answer to whichever one the
36/// BFS happened to visit first instead of the true (possibly wider) set of
37/// parameters that could contain it.
38///
39/// `epsilon` is compared against the Euclidean length of each segment's
40/// chord (first to last control point), so it should be in the same units as
41/// the control points.
42///
43/// Generic over the curve's homogeneous dimension `D` (e.g. `D=4` for 3-D
44/// curves, `D=3` for 2-D pcurves); `point` is in the matching Cartesian
45/// dimension `C = D - 1`.
46pub fn curve_could_contain<S: Scalar, const D: usize, const C: usize>(
47    curve: &NurbCurve<S, D>,
48    point: &Vector<S, C>,
49    max_nodes: usize,
50    min_subdivision_size: S,
51) -> GeopResult<Option<S>>
52where
53    NurbCurve<S, D>: HasConvexHull<S, C>,
54{
55    let mut queue: VecDeque<NurbCurve<S, D>> = VecDeque::new();
56    queue.push_back(curve.clone());
57
58    let mut explored = 0usize;
59    let mut solution: Option<S> = None;
60
61    while let Some(seg) = queue.pop_front() {
62        if explored >= max_nodes {
63            break;
64        }
65        explored += 1;
66
67        // Cheap prefilter: see `intersection::curve_curve::dfs`'s identical
68        // check — the cached bounding box is far quicker to compare than
69        // building a convex hull and running GJK, and just as sound.
70        if !aabb_could_contain(&seg.aabb, point) {
71            continue;
72        }
73
74        let hull = match seg.convex_hull() {
75            Ok(hull) => hull,
76            // Degenerate segment (zero weight): can't be ruled out, so its
77            // whole domain conservatively folds into the solution instead of
78            // aborting the rest of the search.
79            Err(_) => {
80                solution = Some(union_domain(solution, seg.domain_as_scalar()));
81                continue;
82            }
83        };
84
85        if !hull.could_contain(point) {
86            continue;
87        }
88
89        // Segment could contain the point — is the chord short enough?
90        let chord_len = hull.points[hull.points.len() - 1]
91            .sub(&hull.points[0])
92            .norm();
93        if !chord_len.definitely_greater(min_subdivision_size) {
94            solution = Some(union_domain(solution, seg.domain_as_scalar()));
95            continue;
96        }
97
98        // Subdivide at the parameter-domain midpoint.
99        let (start_t, end_t) = seg.domain();
100        // A self-chosen subdivision point: any value in the interval cuts
101        // it equally well, so sharpening loses no accuracy and keeps
102        // repeated splits from compounding width (see AGENTS.md).
103        let mid_t = start_t.add(end_t).div(S::TWO)?.sharpen();
104
105        match seg.split(mid_t) {
106            Ok((left, right)) => {
107                queue.push_back(left);
108                queue.push_back(right);
109            }
110            // Cannot split (e.g. midpoint already at multiplicity p+1).
111            Err(_) => solution = Some(union_domain(solution, seg.domain_as_scalar())),
112        }
113    }
114
115    Ok(solution)
116}
117
118#[cfg(test)]
119mod tests {
120    use super::curve_could_contain;
121    use crate::nurb_curve::NurbCurve;
122    use geop_core_math::for_all_scalars;
123    use geop_core_math::{scalars::Scalar, vector::Vector4};
124
125    fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
126        Vector4::from_array([
127            S::from_f64(x),
128            S::from_f64(y),
129            S::from_f64(z),
130            S::from_f64(w),
131        ])
132    }
133
134    const MAX: usize = 500;
135    const EPS: f64 = 1e-3;
136
137    // ── Curve constructors ────────────────────────────────────────────────────
138
139    /// Degree-1 line from (0,0,0) to (1,0,0).
140    fn line<S: Scalar>() -> NurbCurve<S, 4> {
141        let f = S::from_f64;
142        NurbCurve::try_new(
143            1,
144            vec![pt(0., 0., 0., 1.), pt(1., 0., 0., 1.)],
145            vec![f(0.), f(0.), f(1.), f(1.)],
146        )
147        .unwrap()
148    }
149
150    // ── Line: points on the curve ─────────────────────────────────────────────
151
152    fn check_line_contains_start<S: Scalar>() {
153        let c = line::<S>();
154        let p = c.evaluate(S::ZERO).unwrap();
155        assert!(
156            curve_could_contain(&c, &p, MAX, S::from_f64(EPS))
157                .unwrap()
158                .is_some()
159        );
160    }
161    #[test]
162    fn line_contains_start() {
163        for_all_scalars!(check_line_contains_start);
164    }
165
166    fn check_line_contains_midpoint<S: Scalar>() {
167        let c = line::<S>();
168        let p = c.evaluate(S::from_f64(0.5)).unwrap();
169        assert!(
170            curve_could_contain(&c, &p, MAX, S::from_f64(EPS))
171                .unwrap()
172                .is_some()
173        );
174    }
175    #[test]
176    fn line_contains_midpoint() {
177        for_all_scalars!(check_line_contains_midpoint);
178    }
179
180    fn check_line_contains_end<S: Scalar>() {
181        let c = line::<S>();
182        let p = c.evaluate(S::ONE).unwrap();
183        assert!(
184            curve_could_contain(&c, &p, MAX, S::from_f64(EPS))
185                .unwrap()
186                .is_some()
187        );
188    }
189    #[test]
190    fn line_contains_end() {
191        for_all_scalars!(check_line_contains_end);
192    }
193}