1use crate::{
17 aabb::{aabb_could_contain, curve_could_meet_aabb},
18 fat_line::{
19 Stalled, carried_width, extent, fat_line_zeros, greville_abscissae, restriction, stalled,
20 },
21 nurb_curve::{NurbCurve, ParameterRefinable},
22};
23use geop_core_math::{
24 geop_error::{GeopError, GeopResult},
25 scalars::Scalar,
26 solvers::subdivide::{Order, Searched, Split, subdivide},
27 vector::Vector,
28};
29
30fn clip<const D: usize, const C: usize>(
39 seg: &NurbCurve<D>,
40 xi: &[Scalar],
41 point: &Vector<C>,
42) -> Option<Scalar> {
43 let mut t_hat = seg.domain_as_scalar();
44 let mut d = Vec::with_capacity(seg.control_points.len());
45 for k in 0..C {
46 d.clear();
47 d.extend(
48 seg.control_points
49 .iter()
50 .map(|q| q[k].sub(point[k].mul(q[D - 1]))),
51 );
52 let zeros = fat_line_zeros(xi, &d)?;
53 if !zeros.could_be_equal(t_hat) {
57 return None;
58 }
59 t_hat = t_hat.intersect(zeros);
60 }
61 Some(t_hat)
62}
63
64pub fn curve_could_contain<const D: usize, const C: usize>(
86 curve: &NurbCurve<D>,
87 point: &Vector<C>,
88 max_nodes: usize,
89 min_subdivision_size: Scalar,
90) -> GeopResult<Option<Scalar>>
91where
92 NurbCurve<D>: ParameterRefinable<C>,
93{
94 debug_assert_eq!(
95 C + 1,
96 D,
97 "point dimension must match the curve's Cartesian dimension"
98 );
99 let mut point_box = [Scalar::ENTIRE; 3];
102 point_box[..C].copy_from_slice(&point.to_array());
103 if !curve_could_meet_aabb(curve, &point_box, C) {
104 return Ok(None);
105 }
106 let mut solution: Option<Scalar> = None;
107 let mut report = |t: Scalar| {
108 solution = Some(match solution {
109 Some(existing) => existing.union(t),
110 None => t,
111 });
112 };
113
114 let searched = subdivide(
115 [curve.clone()],
116 Order::BreadthFirst,
117 max_nodes,
118 |seg, _| -> GeopResult<_> {
119 if !aabb_could_contain(&seg.aabb, point) {
121 return Ok(Split::Done);
122 }
123
124 let Some(xi) =
125 greville_abscissae(&seg.knot_vector, seg.degree, seg.control_points.len())?
126 else {
127 report(seg.domain_as_scalar());
130 return Ok(Split::Done);
131 };
132 let Some(t_hat) = clip(&seg, &xi, point) else {
133 return Ok(Split::Done);
134 };
135
136 let ranges = [seg.domain()];
137 let sizes = [extent([seg.control_points.iter().copied()])];
138 let point_width = (0..C)
140 .map(|k| point[k].width().to_f64())
141 .fold(0.0, f64::max);
142 let carried = carried_width(&seg.control_points).max(point_width);
143 if let Some(bounds) = restriction(&[t_hat], &ranges, &sizes, carried)?
144 && let Ok(restricted) = seg.sub_curve(bounds[0].0, bounds[0].1)
145 {
146 return Ok(Split::Restrict(restricted));
147 }
148
149 let halves = match stalled(&ranges, &sizes, carried, min_subdivision_size) {
150 Stalled::Converged => None,
151 Stalled::Bisect(order) if order.is_empty() => None,
152 Stalled::Bisect(_) => seg.split_mid().ok(),
154 };
155 Ok(match halves {
156 Some((left, right)) => Split::Halves(left, right),
157 None => {
158 report(t_hat);
159 Split::Done
160 }
161 })
162 },
163 )?;
164 if let Searched::Exhausted { pending } = searched {
165 return Err(GeopError::new(format!(
166 "curve_could_contain (clipping): exhausted max_nodes={max_nodes} with {pending} \
167 segments pending; the result would be incomplete"
168 )));
169 }
170
171 Ok(solution)
172}
173
174#[cfg(test)]
175mod tests {
176 use super::curve_could_contain;
177 use crate::nurb_curve::NurbCurve;
178 use crate::test_fixtures::{hom, line};
179 use geop_core_math::{scalars::Scalar, vector::Vector3};
180
181 const MAX: usize = 500;
182 const MIN_SUBDIVISION_SIZE: f64 = 1e-3;
183
184 fn quarter_circle() -> NurbCurve<4> {
186 let f = Scalar::from_f64;
187 let w = std::f64::consts::FRAC_1_SQRT_2;
188 NurbCurve::try_new(
189 2,
190 vec![hom(1., 0., 0., 1.), hom(w, w, 0., w), hom(0., 1., 0., 1.)],
191 vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
192 )
193 .unwrap()
194 }
195
196 fn wiggle() -> NurbCurve<4> {
198 let f = Scalar::from_f64;
199 NurbCurve::try_new(
200 3,
201 vec![
202 hom(0., 0., 0., 1.),
203 hom(1., 2., 0., 1.),
204 hom(2., -1., 1., 1.),
205 hom(3., 2., 0., 1.),
206 hom(4., 0., -1., 1.),
207 hom(5., 1., 0., 1.),
208 ],
209 vec![
210 f(0.),
211 f(0.),
212 f(0.),
213 f(0.),
214 f(0.3),
215 f(0.6),
216 f(1.),
217 f(1.),
218 f(1.),
219 f(1.),
220 ],
221 )
222 .unwrap()
223 }
224
225 fn assert_contains_at(c: &NurbCurve<4>, t: f64) {
227 let t = Scalar::from_f64(t);
228 let p = c.evaluate(t).unwrap();
229 let found = curve_could_contain(c, &p, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
230 .unwrap()
231 .unwrap_or_else(|| panic!("point at t={t:?} not found"));
232 assert!(
233 found.could_be_equal(t),
234 "enclosure {found:?} misses t={t:?}"
235 );
236 }
237
238 #[test]
239 fn line_contains() {
240 for t in [0., 0.25, 0.5, 1.] {
241 assert_contains_at(&line([0.; 3], [1., 0., 0.]), t);
242 }
243 }
244
245 #[test]
246 fn quarter_circle_contains() {
247 for t in [0., 0.1, 0.5, 0.77, 1.] {
248 assert_contains_at(&quarter_circle(), t);
249 }
250 }
251
252 #[test]
253 fn wiggle_contains() {
254 for t in [0., 0.05, 0.3, 0.42, 0.6, 0.9, 1.] {
255 assert_contains_at(&wiggle(), t);
256 }
257 }
258
259 #[test]
262 fn result_is_tight() {
263 let c = wiggle();
264 let t = Scalar::from_f64(0.42);
265 let p = c.evaluate(t).unwrap();
266 let found = curve_could_contain(&c, &p, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
267 .unwrap()
268 .unwrap();
269 assert!(
270 found
271 .width()
272 .definitely_less(Scalar::from_f64(MIN_SUBDIVISION_SIZE)),
273 "{found:?}"
274 );
275 }
276
277 #[test]
278 fn misses_off_curve_points() {
279 let f = Scalar::from_f64;
280 let off = |x, y, z| Vector3::from_array([f(x), f(y), f(z)]);
281 assert!(
283 curve_could_contain(
284 &quarter_circle(),
285 &off(0.6, 0.6, 0.),
286 MAX,
287 f(MIN_SUBDIVISION_SIZE)
288 )
289 .unwrap()
290 .is_none()
291 );
292 assert!(
294 curve_could_contain(
295 &line([0.; 3], [1., 0., 0.]),
296 &off(1.5, 0., 0.),
297 MAX,
298 f(MIN_SUBDIVISION_SIZE)
299 )
300 .unwrap()
301 .is_none()
302 );
303 assert!(
304 curve_could_contain(
305 &line([0.; 3], [1., 0., 0.]),
306 &off(0.5, 0.1, 0.),
307 MAX,
308 f(MIN_SUBDIVISION_SIZE)
309 )
310 .unwrap()
311 .is_none()
312 );
313 assert!(
314 curve_could_contain(&wiggle(), &off(2.5, 0.5, 0.5), MAX, f(MIN_SUBDIVISION_SIZE))
315 .unwrap()
316 .is_none()
317 );
318 }
319
320 #[test]
324 fn misses_points_closer_than_min_subdivision_size() {
325 let f = Scalar::from_f64;
326 let r = 1. + 1e-7;
327 let (x, y) = (r * 0.6, r * 0.8);
328 let p = Vector3::from_array([f(x), f(y), f(0.)]);
329 let found =
330 curve_could_contain(&quarter_circle(), &p, MAX, f(MIN_SUBDIVISION_SIZE)).unwrap();
331 assert!(found.is_none(), "{found:?}");
332 }
333}