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}