1use super::{
17 Intersections,
18 coincidence::{self, Hit, Overlap},
19};
20use crate::{
21 aabb::{aabb_could_overlap, curve_could_meet_aabb},
22 contains::curve::curve_could_contain,
23 fat_line::{
24 Stalled, carried_width, chord, clip_tensor, extent, greville_abscissae, restriction,
25 spanning, stalled,
26 },
27 nurb_curve::{NurbCurve, ParameterRefinable},
28};
29use geop_core_math::{
30 disjoint_set::DisjointSet,
31 geop_error::{GeopError, GeopResult, WithContext},
32 matrix::Matrix,
33 scalars::Scalar,
34 solvers::{
35 linear::solve_linear_system,
36 newton,
37 subdivide::{Order, Searched, Split, subdivide},
38 },
39 vector::Vector,
40};
41
42fn clip<const D: usize, const C: usize>(
55 a: &NurbCurve<D>,
56 b: &NurbCurve<D>,
57) -> GeopResult<Option<[Scalar; 2]>> {
58 let mut hats = [a.domain_as_scalar(), b.domain_as_scalar()];
59 let (na, nb) = (a.control_points.len(), b.control_points.len());
60 let greville = [
61 greville_abscissae(&a.knot_vector, a.degree, na)?,
62 greville_abscissae(&b.knot_vector, b.degree, nb)?,
63 ];
64
65 let mut dirs = chord::<D, C>(&b.control_points)
66 .orthonormal_complement()
67 .unwrap_or_default();
68 dirs.extend(
69 chord::<D, C>(&a.control_points)
70 .orthonormal_complement()
71 .unwrap_or_default(),
72 );
73
74 let w = D - 1;
75 let mut d = Vec::with_capacity(na * nb);
76 for n in spanning(dirs) {
77 let nb_pts: Vec<Scalar> = b
78 .control_points
79 .iter()
80 .map(|q| n.prod_dot(&q.head()))
81 .collect();
82 d.clear();
83 for p in &a.control_points {
84 let np = n.prod_dot(&p.head());
85 d.extend(
86 b.control_points
87 .iter()
88 .zip(&nb_pts)
89 .map(|(q, &nq)| np.mul(q[w]).sub(p[w].mul(nq))),
90 );
91 }
92 if !clip_tensor(&d, &[na, nb], &greville, &mut hats) {
93 return Ok(None);
94 }
95 }
96 Ok(Some(hats))
97}
98
99pub fn curve_curve_crossings<const D: usize, const C: usize>(
123 curve_a: &NurbCurve<D>,
124 curve_b: &NurbCurve<D>,
125 max_nodes: usize,
126 min_subdivision_size: Scalar,
127) -> GeopResult<Vec<(Scalar, Scalar)>>
128where
129 NurbCurve<D>: ParameterRefinable<C>,
130{
131 let mut solutions: DisjointSet<(Scalar, Scalar)> = DisjointSet::new();
132 let root = (curve_a.clone(), curve_b.clone());
133 let searched = subdivide(
134 [root],
135 Order::BreadthFirst,
136 max_nodes,
137 |(a, b), budget| -> GeopResult<_> {
138 if !aabb_could_overlap(&a.aabb, &b.aabb, C) {
139 return Ok(Split::Done);
140 }
141 let Some(hats) = clip::<D, C>(&a, &b)? else {
142 return Ok(Split::Done);
143 };
144
145 let pinned = (a.pinned_end::<C>(hats[0]).map(|p| (p, true)))
150 .or_else(|| b.pinned_end::<C>(hats[1]).map(|p| (p, false)));
151 if let Some((point, on_a)) = pinned {
152 let (other, free) = if on_a { (&b, hats[1]) } else { (&a, hats[0]) };
153 if let Some(t) = curve_could_contain(other, &point, budget, min_subdivision_size)?
154 && t.could_be_equal(free)
155 {
156 let free = free.intersect(t);
157 solutions.insert(if on_a {
158 (hats[0], free)
159 } else {
160 (free, hats[1])
161 });
162 }
163 return Ok(Split::Done);
164 }
165
166 let ranges = [a.domain(), b.domain()];
167 let sizes = [
168 extent([a.control_points.clone()]),
169 extent([b.control_points.clone()]),
170 ];
171 let carried = carried_width(&a.control_points).max(carried_width(&b.control_points));
172 if let Some(bounds) = restriction(&hats, &ranges, &sizes, carried)?
173 && let (Ok(ra), Ok(rb)) = (
174 a.sub_curve(bounds[0].0, bounds[0].1),
175 b.sub_curve(bounds[1].0, bounds[1].1),
176 )
177 {
178 return Ok(Split::Restrict((ra, rb)));
179 }
180
181 let order = match stalled(&ranges, &sizes, carried, min_subdivision_size) {
182 Stalled::Converged => {
183 solutions.insert((a.domain_as_scalar(), b.domain_as_scalar()));
190 return Ok(Split::Done);
191 }
192 Stalled::Bisect(order) => order,
193 };
194 let children = order.iter().find_map(|&dir| {
195 if dir == 0 {
196 let (l, r) = a.split_mid().ok()?;
197 Some(((l, b.clone()), (r, b.clone())))
198 } else {
199 let (l, r) = b.split_mid().ok()?;
200 Some(((a.clone(), l), (a.clone(), r)))
201 }
202 });
203 Ok(match children {
204 Some((first, second)) => Split::Halves(first, second),
205 None => {
207 solutions.insert((hats[0], hats[1]));
208 Split::Done
209 }
210 })
211 },
212 )?;
213 if let Searched::Exhausted { pending } = searched {
214 return Err(GeopError::new(format!(
215 "curve_curve_crossings: exhausted max_nodes={max_nodes} with {pending} \
216 pairs pending; the result would be incomplete"
217 )));
218 }
219
220 Ok(solutions.into_vec())
221}
222
223fn point_on<const D: usize, const C: usize>(
227 curve: &NurbCurve<D>,
228 other: &NurbCurve<D>,
229 t: Scalar,
230 reach: Scalar,
231 max_nodes: usize,
232 min_subdivision_size: Scalar,
233) -> GeopResult<Option<Scalar>>
234where
235 NurbCurve<D>: ParameterRefinable<C>,
236{
237 let mut point = other.evaluate_cartesian(t)?;
238 if reach.definitely_greater(Scalar::ZERO) {
239 let around = reach.neg().union(reach);
240 point = point.map(|x| x.add(around));
241 }
242 curve_could_contain(curve, &point, max_nodes, min_subdivision_size)
243}
244
245pub(crate) fn curve_curve_overlaps<const D: usize, const C: usize>(
249 a: &NurbCurve<D>,
250 b: &NurbCurve<D>,
251 reach: Scalar,
252 max_nodes: usize,
253 min_subdivision_size: Scalar,
254) -> GeopResult<Vec<Overlap<Scalar>>>
255where
256 NurbCurve<D>: ParameterRefinable<C>,
257{
258 let on = |curve: &NurbCurve<D>, other: &NurbCurve<D>, t: Scalar| {
259 point_on(curve, other, t, reach, max_nodes, min_subdivision_size)
260 };
261 let (a0, a1) = a.domain();
262 let (b0, b1) = b.domain();
263 let mut candidates = Vec::new();
264 for s in [a0, a1] {
265 if let Some(t) = on(b, a, s)? {
266 candidates.push(Hit { t: s, partner: t });
267 }
268 }
269 for t in [b0, b1] {
270 if let Some(s) = on(a, b, t)? {
271 candidates.push(Hit { t: s, partner: t });
272 }
273 }
274 let same_way = |s: Scalar, t: Scalar| -> GeopResult<bool> {
280 if reach.definitely_greater(Scalar::ZERO) {
281 return Ok(true);
282 }
283 let (along_a, along_b) = (a.tangent_cartesian(s)?, b.tangent_cartesian(t)?);
284 Ok((0..C).all(|i| {
285 (i + 1..C).all(|j| {
286 (along_a[i].mul(along_b[j]))
287 .sub(along_a[j].mul(along_b[i]))
288 .could_be_equal(Scalar::ZERO)
289 })
290 }))
291 };
292 coincidence::find_overlaps(candidates, |s| {
293 Ok(on(b, a, s)?.filter(|&t| same_way(s, t).unwrap_or(true)))
294 })
295}
296
297pub type OverlapsAndCrossings = (Vec<Overlap<Scalar>>, Vec<(Scalar, Scalar)>);
300
301pub fn curve_curve_overlaps_and_crossings<const D: usize, const C: usize>(
311 a: &NurbCurve<D>,
312 b: &NurbCurve<D>,
313 reach: Scalar,
314 max_nodes: usize,
315 min_subdivision_size: Scalar,
316) -> GeopResult<OverlapsAndCrossings>
317where
318 NurbCurve<D>: ParameterRefinable<C>,
319{
320 let overlaps = curve_curve_overlaps(a, b, reach, max_nodes, min_subdivision_size)?;
321 let crossings = coincidence::crossings_between(a, &overlaps, |piece| {
322 curve_curve_crossings(piece, b, max_nodes, min_subdivision_size)
323 })?;
324 Ok((overlaps, crossings))
325}
326
327pub fn curve_curve_intersect<const D: usize, const C: usize>(
343 curve_a: &NurbCurve<D>,
344 curve_b: &NurbCurve<D>,
345 max_solutions: usize,
346 max_nodes: usize,
347 min_subdivision_size: Scalar,
348) -> GeopResult<Intersections<(Scalar, Scalar)>>
349where
350 NurbCurve<D>: ParameterRefinable<C>,
351{
352 if max_solutions == 0
356 || !curve_could_meet_aabb(curve_a, &curve_b.aabb, C)
357 || !curve_could_meet_aabb(curve_b, &curve_a.aabb, C)
358 {
359 return Ok(Intersections::Found(vec![]));
360 }
361 let ctx = |e: GeopError| {
362 e.with_context(format!(
363 "curve_curve_intersect(curve_a={curve_a:?}, curve_b={curve_b:?}, max_nodes={max_nodes}, \
364 min_subdivision_size={min_subdivision_size:?})"
365 ))
366 };
367 let (overlaps, crossings) = curve_curve_overlaps_and_crossings(
368 curve_a,
369 curve_b,
370 Scalar::ZERO,
371 max_nodes,
372 min_subdivision_size,
373 )
374 .with_context(&ctx)?;
375 let samples = coincidence::samples(&overlaps, crossings.len(), max_solutions, |s| {
376 point_on(
377 curve_b,
378 curve_a,
379 s,
380 Scalar::ZERO,
381 max_nodes,
382 min_subdivision_size,
383 )
384 })?;
385 Ok(coincidence::assemble(
386 &overlaps,
387 crossings,
388 samples,
389 max_solutions,
390 ))
391}
392
393pub fn refine_crossing<const D: usize, const C: usize>(
402 curve_a: &NurbCurve<D>,
403 curve_b: &NurbCurve<D>,
404 t_a: Scalar,
405 t_b: Scalar,
406) -> (Scalar, Scalar)
407where
408 NurbCurve<D>: ParameterRefinable<C>,
409{
410 let refined = newton::refine(
411 [t_a, t_b],
412 [curve_a.domain(), curve_b.domain()],
413 |[ta, tb]| {
414 let pa = curve_a.evaluate_cartesian(ta).ok()?;
415 let pb = curve_b.evaluate_cartesian(tb).ok()?;
416 let da = curve_a.tangent_cartesian(ta).ok()?;
417 let db = curve_b.tangent_cartesian(tb).ok()?;
418 let f = pa.sub(&pb);
419 let m = Matrix::from_rows([
420 [da.prod_dot(&da), da.prod_dot(&db).neg()],
421 [da.prod_dot(&db).neg(), db.prod_dot(&db)],
422 ]);
423 let rhs = Vector::from_array([da.prod_dot(&f).neg(), db.prod_dot(&f)]);
424 let delta = solve_linear_system(&m, &rhs).ok()?;
425 Some([ta.add(delta[0]), tb.add(delta[1])])
426 },
427 );
428 let [a_ref, b_ref] = refined;
429 let holds_a_crossing = match (
430 curve_a.evaluate_cartesian(a_ref),
431 curve_b.evaluate_cartesian(b_ref),
432 ) {
433 (Ok(pa), Ok(pb)) => pa.could_be_equal(&pb),
434 _ => false,
435 };
436 if !holds_a_crossing {
437 return (t_a, t_b);
438 }
439 (a_ref, b_ref)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::{curve_curve_crossings, curve_curve_intersect};
445 use crate::nurb_curve::NurbCurve;
446 use crate::test_fixtures::{knots, line, weighted};
447 use geop_core_math::{scalars::Scalar, vector::Vector4};
448
449 const MAX: usize = 5000;
450 const MIN_SUBDIVISION_SIZE: f64 = 1e-6;
451
452 #[test]
460 fn edges_meeting_at_a_shallow_angle_do_not_overlap() {
461 let width = 1e-10;
462 let edge = |from: [f64; 3], to: [f64; 3]| {
463 let wide = |p: [f64; 3]| {
464 let mut at = p.map(|x| Scalar::new(x - width, x + width)).to_vec();
465 at.push(Scalar::ONE);
466 Vector4::from_array([at[0], at[1], at[2], at[3]])
467 };
468 NurbCurve::try_new(1, vec![wide(from), wide(to)], knots(&[0.0, 0.0, 1.0, 1.0])).unwrap()
469 };
470 let (sin, cos) = 3f64.to_radians().sin_cos();
471 let a = edge([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
472 let b = edge([1.0 - 5e-10, 0.0, 0.0], [1.0 + cos, sin, 0.0]);
473 let found =
474 curve_curve_intersect::<4, 3>(&a, &b, 8, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
475 .unwrap();
476 assert!(!found.is_coincident(), "{found:?}");
477 assert!(!found.into_vec().is_empty(), "they do meet");
478
479 let c = edge([0.5, 0.0, 0.0], [2.0, 0.0, 0.0]);
481 let found =
482 curve_curve_intersect::<4, 3>(&a, &c, 8, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
483 .unwrap();
484 assert!(found.is_coincident(), "{found:?}");
485 }
486
487 fn quarter_circle() -> NurbCurve<4> {
489 let w = std::f64::consts::FRAC_1_SQRT_2;
490 NurbCurve::try_new(
491 2,
492 vec![
493 weighted(1., 0., 0., 1.),
494 weighted(1., 1., 0., w),
495 weighted(0., 1., 0., 1.),
496 ],
497 knots(&[0., 0., 0., 1., 1., 1.]),
498 )
499 .unwrap()
500 }
501
502 fn solve(a: &NurbCurve<4>, b: &NurbCurve<4>) -> Vec<(Scalar, Scalar)> {
503 curve_curve_crossings::<4, 3>(a, b, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE)).unwrap()
504 }
505
506 fn assert_solutions(a: &NurbCurve<4>, b: &NurbCurve<4>, n: usize) -> Vec<(Scalar, Scalar)> {
508 let sols = solve(a, b);
509 assert_eq!(sols.len(), n, "{sols:?}");
510 sols
511 }
512
513 #[test]
515 fn crossing_diagonals() {
516 let a = line([0., 0., 0.], [1., 1., 0.]);
517 let b = line([0., 1., 0.], [1., 0., 0.]);
518 let sols = assert_solutions(&a, &b, 1);
519 let half = Scalar::from_f64(0.5);
520 assert!(
521 sols[0].0.could_be_equal(half) && sols[0].1.could_be_equal(half),
522 "{sols:?}"
523 );
524 }
525
526 #[test]
527 fn circle_meets_line_once() {
528 let sols = assert_solutions(&quarter_circle(), &line([0., 0., 0.], [1., 1., 0.]), 1);
530 assert!(sols[0].0.could_be_equal(Scalar::from_f64(0.5)), "{sols:?}");
531 }
532
533 #[test]
534 fn circle_meets_chord_twice() {
535 let chord = line([1., 0.15, 0.], [0.15, 1., 0.]);
538 assert_solutions(&quarter_circle(), &chord, 2);
539 }
540
541 #[test]
544 fn shared_endpoint() {
545 let a = line([0., 0., 0.], [1., 0., 0.]);
546 let b = line([1., 0., 0.], [1., 1., 1.]);
547 let sols = assert_solutions(&a, &b, 1);
548 assert!(
549 sols[0].0.could_be_equal(Scalar::ONE) && sols[0].1.could_be_equal(Scalar::ZERO),
550 "{sols:?}"
551 );
552 }
553
554 #[test]
555 fn skew_lines_miss() {
556 let a = line([0., 0., 0.], [1., 1., 0.]);
557 let b = line([0., 1., 0.01], [1., 0., 0.01]);
558 assert_solutions(&a, &b, 0);
559 assert_solutions(&quarter_circle(), &line([0., 0., 0.], [0.5, 0.5, 0.]), 0);
560 }
561
562 #[test]
563 fn budget_exhaustion_is_an_error() {
564 let a = line([0., 0., 0.], [1., 1., 0.]);
565 let b = line([0., 1., 0.], [1., 0., 0.]);
566 assert!(
567 curve_curve_crossings::<4, 3>(&a, &b, 1, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
568 .is_err()
569 );
570 }
571
572 use crate::intersection::Intersections;
575
576 fn wrap(a: &NurbCurve<4>, b: &NurbCurve<4>) -> Intersections<(Scalar, Scalar)> {
577 curve_curve_intersect::<4, 3>(a, b, 5, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE)).unwrap()
578 }
579
580 #[test]
583 fn arc_with_ends_on_chord_is_not_coincident() {
584 let r = wrap(&quarter_circle(), &line([1., 0., 0.], [0., 1., 0.]));
585 assert!(!r.is_coincident(), "{r:?}");
586 assert_eq!(r.len(), 2, "{r:?}");
587 }
588
589 #[test]
590 fn shared_vertex_is_not_coincident() {
591 let r = wrap(
592 &line([0., 0., 0.], [1., 0., 0.]),
593 &line([1., 0., 0.], [1., 1., 1.]),
594 );
595 assert!(!r.is_coincident() && r.len() == 1, "{r:?}");
596 }
597
598 #[test]
601 fn partial_overlap_reports_its_ends() {
602 let a = line([0., 0., 0.], [1., 0., 0.]);
603 let b = line([0.5, 0., 0.], [1.5, 0., 0.]);
604 let r = wrap(&a, &b);
605 assert!(r.is_coincident(), "{r:?}");
606 let v = r.as_slice();
607 let half = Scalar::from_f64(0.5);
608 assert!(
609 v[0].0.could_be_equal(half) && v[0].1.could_be_equal(Scalar::ZERO),
610 "{v:?}"
611 );
612 assert!(
613 v[1].0.could_be_equal(Scalar::ONE) && v[1].1.could_be_equal(half),
614 "{v:?}"
615 );
616 for (s, _) in v {
617 assert!(!s.definitely_less(half), "{v:?}");
618 }
619 }
620
621 #[test]
623 fn arc_pieces_overlap() {
624 let arc = quarter_circle();
625 let (left, _) = arc.split(Scalar::from_f64(0.75)).unwrap();
626 let (_, right) = arc.split(Scalar::from_f64(0.25)).unwrap();
627 let r = wrap(&left, &right);
628 assert!(r.is_coincident(), "{r:?}");
629 for (s, t) in r.as_slice() {
630 assert!(s.could_be_equal(*t), "same curve, same parameter: {r:?}");
631 }
632 }
633
634 mod old_suite {
637 use super::super::curve_curve_intersect;
638 use crate::intersection::curve_curve::refine_crossing;
639 use crate::nurb_curve::NurbCurve;
640 use geop_core_math::{
641 scalars::Scalar,
642 vector::{Vector3, Vector4},
643 };
644
645 const MAX_NODES: usize = 2000;
646
647 fn ptc(x: f64, y: f64, z: f64, w: f64) -> Vector4 {
648 Vector4::from_array([
649 Scalar::from_f64(x),
650 Scalar::from_f64(y),
651 Scalar::from_f64(z),
652 Scalar::from_f64(w),
653 ])
654 }
655
656 fn horizontal_line() -> NurbCurve<4> {
658 let f = Scalar::from_f64;
659 NurbCurve::try_new(
660 1,
661 vec![ptc(0., 0.3, 0., 1.), ptc(1., 0.3, 0., 1.)],
662 vec![f(0.), f(0.), f(1.), f(1.)],
663 )
664 .unwrap()
665 }
666
667 fn vertical_crossing_line() -> NurbCurve<4> {
670 let f = Scalar::from_f64;
671 NurbCurve::try_new(
672 1,
673 vec![ptc(0.5, -1., 0., 1.), ptc(0.5, 1., 0., 1.)],
674 vec![f(0.), f(0.), f(1.), f(1.)],
675 )
676 .unwrap()
677 }
678
679 fn vertical_missing_line() -> NurbCurve<4> {
682 let f = Scalar::from_f64;
683 NurbCurve::try_new(
684 1,
685 vec![ptc(2.0, -1., 0., 1.), ptc(2.0, 1., 0., 1.)],
686 vec![f(0.), f(0.), f(1.), f(1.)],
687 )
688 .unwrap()
689 }
690
691 fn double_dip_curve() -> NurbCurve<4> {
696 let f = Scalar::from_f64;
697 NurbCurve::try_new(
698 2,
699 vec![
700 ptc(0.3, 1.0, 0., 1.),
701 ptc(0.5, -2.0, 0., 1.),
702 ptc(0.7, 1.0, 0., 1.),
703 ],
704 vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
705 )
706 .unwrap()
707 }
708
709 fn coincident_overlap_line() -> NurbCurve<4> {
713 let f = Scalar::from_f64;
714 NurbCurve::try_new(
715 1,
716 vec![ptc(0.5, 0.3, 0., 1.), ptc(1.5, 0.3, 0., 1.)],
717 vec![f(0.), f(0.), f(1.), f(1.)],
718 )
719 .unwrap()
720 }
721
722 fn full_coincident_line() -> NurbCurve<4> {
725 let f = Scalar::from_f64;
726 NurbCurve::try_new(
727 1,
728 vec![ptc(0., 0.3, 0., 1.), ptc(1., 0.3, 0., 1.)],
729 vec![f(0.), f(0.), f(1.), f(1.)],
730 )
731 .unwrap()
732 }
733
734 const MIN_SUBDIVISION_SIZE: f64 = 1e-6;
735
736 #[test]
739 fn single_crossing_curves_have_one_solution() {
740 let a = horizontal_line();
741 let b = vertical_crossing_line();
742 let result =
743 curve_curve_intersect(&a, &b, 5, MAX_NODES, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
744 .unwrap();
745 assert_eq!(result.len(), 1);
746 }
747
748 #[test]
751 fn curves_missing_each_other_have_no_solution() {
752 let a = horizontal_line();
753 let b = vertical_missing_line();
754 let result =
755 curve_curve_intersect(&a, &b, 5, MAX_NODES, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
756 .unwrap();
757 assert!(result.is_empty());
758 }
759
760 #[test]
763 fn max_solutions_zero_returns_empty() {
764 let a = horizontal_line();
765 let b = vertical_crossing_line();
766 let result =
767 curve_curve_intersect(&a, &b, 0, MAX_NODES, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
768 .unwrap();
769 assert!(result.is_empty());
770 }
771
772 #[test]
773 fn max_nodes_exhausted_errors() {
774 let a = horizontal_line();
778 let b = double_dip_curve();
779 let result =
780 curve_curve_intersect(&a, &b, 1000, 1, Scalar::from_f64(MIN_SUBDIVISION_SIZE));
781 assert!(result.is_err());
782 }
783
784 #[test]
787 fn two_crossings_found_when_budget_allows() {
788 let a = horizontal_line();
789 let b = double_dip_curve();
790 let result =
791 curve_curve_intersect(&a, &b, 2, MAX_NODES, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
792 .unwrap()
793 .into_vec();
794 assert_eq!(result.len(), 2);
795 assert!(
796 !result[0].0.could_be_equal(result[1].0),
797 "the two crossings should remain distinct"
798 );
799 }
800
801 #[test]
802 fn max_solutions_one_caps_at_one_even_with_two_crossings() {
803 let a = horizontal_line();
804 let b = double_dip_curve();
805 let result =
806 curve_curve_intersect(&a, &b, 1, MAX_NODES, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
807 .unwrap();
808 assert_eq!(result.len(), 1);
809 }
810
811 #[test]
814 fn min_subdivision_size_controls_precision() {
815 let a = horizontal_line();
816 let b = vertical_crossing_line();
817 let result = curve_curve_intersect(&a, &b, 5, MAX_NODES, Scalar::from_f64(1e-3))
818 .unwrap()
819 .into_vec();
820 assert_eq!(result.len(), 1);
821
822 let (t_a, _) = result[0];
823 assert!(
824 t_a.sub(Scalar::from_f64(0.5))
825 .abs()
826 .could_be_less(Scalar::from_f64(1e-2))
827 );
828 }
829
830 #[test]
833 fn coincident_overlap_terminates() {
834 let a = horizontal_line();
835 let b = coincident_overlap_line();
836
837 let result_one =
839 curve_curve_intersect(&a, &b, 1, MAX_NODES, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
840 .unwrap();
841 assert_eq!(result_one.len(), 1);
842
843 let result_many =
847 curve_curve_intersect(&a, &b, 5, MAX_NODES, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
848 .unwrap();
849 assert!(!result_many.is_empty());
850 assert!(result_many.len() <= 5);
851
852 let lower_bound = Scalar::from_f64(0.5 - MIN_SUBDIVISION_SIZE);
853 for &(t_a, _) in result_many.as_slice() {
854 assert!(t_a.could_be_greater(lower_bound));
855 }
856 }
857
858 #[test]
861 fn full_coincidence_reaches_max_solutions() {
862 let a = horizontal_line();
863 let b = full_coincident_line();
864 let result = curve_curve_intersect(&a, &b, 5, 5000, Scalar::from_f64(1e-3)).unwrap();
870 assert!(result.is_coincident());
871 assert_eq!(result.len(), 5);
872 }
873
874 fn pt2(x: f64, y: f64) -> Vector3 {
877 Vector3::from_array([Scalar::from_f64(x), Scalar::from_f64(y), Scalar::ONE])
878 }
879
880 fn horizontal_line_2d() -> crate::nurb_curve::NurbCurve2D {
882 let f = Scalar::from_f64;
883 NurbCurve::try_new(
884 1,
885 vec![pt2(0., 0.3), pt2(1., 0.3)],
886 vec![f(0.), f(0.), f(1.), f(1.)],
887 )
888 .unwrap()
889 }
890
891 fn vertical_crossing_line_2d() -> crate::nurb_curve::NurbCurve2D {
894 let f = Scalar::from_f64;
895 NurbCurve::try_new(
896 1,
897 vec![pt2(0.5, -1.), pt2(0.5, 1.)],
898 vec![f(0.), f(0.), f(1.), f(1.)],
899 )
900 .unwrap()
901 }
902
903 #[test]
904 fn single_crossing_2d() {
905 let a = horizontal_line_2d();
906 let b = vertical_crossing_line_2d();
907 let result =
908 curve_curve_intersect(&a, &b, 5, MAX_NODES, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
909 .unwrap()
910 .into_vec();
911 assert_eq!(result.len(), 1);
912 let (t_a, _) = result[0];
913 let hit = a.evaluate(t_a).unwrap();
914 assert!(
915 hit[0]
916 .sub(Scalar::from_f64(0.5))
917 .abs()
918 .could_be_less(Scalar::from_f64(1e-3))
919 );
920 assert!(
921 hit[1]
922 .sub(Scalar::from_f64(0.3))
923 .abs()
924 .could_be_less(Scalar::from_f64(1e-3))
925 );
926 }
927
928 fn quadratic_tangent_to_x_axis() -> NurbCurve<4> {
941 let f = Scalar::from_f64;
942 NurbCurve::try_new(
943 2,
944 vec![
945 ptc(-1., 1., 0., 1.),
946 ptc(0., -1., 0., 1.),
947 ptc(1., 1., 0., 1.),
948 ],
949 vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
950 )
951 .unwrap()
952 }
953
954 fn quartic_tangent_to_x_axis() -> NurbCurve<4> {
963 let f = Scalar::from_f64;
964 NurbCurve::try_new(
965 4,
966 vec![
967 ptc(-1., 1., 0., 1.),
968 ptc(-0.5, -1., 0., 1.),
969 ptc(0., 1., 0., 1.),
970 ptc(0.5, -1., 0., 1.),
971 ptc(1., 1., 0., 1.),
972 ],
973 vec![
974 f(0.),
975 f(0.),
976 f(0.),
977 f(0.),
978 f(0.),
979 f(1.),
980 f(1.),
981 f(1.),
982 f(1.),
983 f(1.),
984 ],
985 )
986 .unwrap()
987 }
988
989 fn x_axis_line() -> NurbCurve<4> {
992 let f = Scalar::from_f64;
993 NurbCurve::try_new(
994 1,
995 vec![ptc(-1., 0., 0., 1.), ptc(1., 0., 0., 1.)],
996 vec![f(0.), f(0.), f(1.), f(1.)],
997 )
998 .unwrap()
999 }
1000
1001 #[test]
1006 fn refine_crossing_stays_sound_for_order_2_tangency() {
1007 let a = quadratic_tangent_to_x_axis();
1008 let b = x_axis_line();
1009
1010 let found = curve_curve_intersect(&a, &b, 5, MAX_NODES, Scalar::from_f64(1e-3))
1011 .unwrap()
1012 .into_vec();
1013 assert_eq!(
1014 found.len(),
1015 1,
1016 "a single tangential touch, not a crossing pair"
1017 );
1018 let (t_a, t_b) = found[0];
1019
1020 assert!(t_a.could_be_equal(Scalar::from_f64(0.5)));
1023 assert!(t_b.could_be_equal(Scalar::from_f64(0.5)));
1024
1025 let (ra, rb) = refine_crossing(&a, &b, t_a, t_b);
1026
1027 assert!(ra.could_be_equal(Scalar::from_f64(0.5)));
1029 assert!(rb.could_be_equal(Scalar::from_f64(0.5)));
1030 assert!(ra.is_subset_of(t_a));
1034 assert!(rb.is_subset_of(t_b));
1035 }
1036
1037 #[test]
1043 fn refine_crossing_stays_sound_for_order_4_tangency() {
1044 let a = quartic_tangent_to_x_axis();
1045 let b = x_axis_line();
1046
1047 let found = curve_curve_intersect(&a, &b, 5, MAX_NODES, Scalar::from_f64(1e-3))
1048 .unwrap()
1049 .into_vec();
1050 assert!(!found.is_empty(), "the touch point must still be found");
1051
1052 for (t_a, t_b) in found {
1053 assert!(t_a.could_be_equal(Scalar::from_f64(0.5)));
1055 assert!(t_b.could_be_equal(Scalar::from_f64(0.5)));
1056
1057 let (ra, rb) = refine_crossing(&a, &b, t_a, t_b);
1058
1059 assert!(
1065 ra.could_be_equal(Scalar::from_f64(0.5)),
1066 "refine_crossing must never lose the true root: got {ra:?}"
1067 );
1068 assert!(rb.could_be_equal(Scalar::from_f64(0.5)));
1069 assert!(ra.is_subset_of(t_a));
1071 assert!(rb.is_subset_of(t_b));
1072 }
1073 }
1074
1075 fn quartic_minus_epsilon() -> NurbCurve<4> {
1082 let f = Scalar::from_f64;
1083 let dy = 0.0001;
1089 NurbCurve::try_new(
1090 4,
1091 vec![
1092 ptc(-1., 1. - dy, 0., 1.),
1093 ptc(-0.5, -1. - dy, 0., 1.),
1094 ptc(0., 1. - dy, 0., 1.),
1095 ptc(0.5, -1. - dy, 0., 1.),
1096 ptc(1., 1. - dy, 0., 1.),
1097 ],
1098 vec![
1099 f(0.),
1100 f(0.),
1101 f(0.),
1102 f(0.),
1103 f(0.),
1104 f(1.),
1105 f(1.),
1106 f(1.),
1107 f(1.),
1108 f(1.),
1109 ],
1110 )
1111 .unwrap()
1112 }
1113
1114 #[test]
1115 fn close_transversal_crossings_near_quartic_flat_spot() {
1116 let a = quartic_minus_epsilon();
1117 let b = x_axis_line();
1118
1119 let found = curve_curve_intersect(&a, &b, 4, MAX_NODES, Scalar::from_f64(1e-4))
1120 .unwrap()
1121 .into_vec();
1122 assert_eq!(
1123 found.len(),
1124 2,
1125 "two distinct, separated crossings near x=±0.1"
1126 );
1127 assert!(
1128 !found[0].0.could_be_equal(found[1].0),
1129 "the two nearby crossings must remain distinct"
1130 );
1131
1132 for &(t_a, t_b) in &found {
1134 let near_left = t_a
1135 .sub(Scalar::from_f64(0.45))
1136 .abs()
1137 .could_be_less(Scalar::from_f64(1e-2));
1138 let near_right = t_a
1139 .sub(Scalar::from_f64(0.55))
1140 .abs()
1141 .could_be_less(Scalar::from_f64(1e-2));
1142 assert!(near_left || near_right, "crossing at unexpected t={t_a:?}");
1143
1144 let (ra, _) = refine_crossing(&a, &b, t_a, t_b);
1145 assert!(ra.is_subset_of(t_a), "refinement must only ever tighten");
1146 }
1147 }
1148 }
1149}