1use crate::curves::{BSplineCurve3D, InterpParametrization, ParametricCurve3D};
11use crate::math::{gauss_newton_2d, newton_3d};
12use crate::surfaces::ParametricSurface;
13use crate::{Circle3D, Ellipse3D, Hyperbola3D, Line3D, Parabola3D, Point2D, Point3D, Tolerance};
14
15#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum PlanePlaneIntersection {
18 Line(Line3D),
20 Parallel,
22 Coincident,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum PlaneSphereIntersection {
29 Circle(Circle3D),
31 TangentPoint(Point3D),
33 Empty,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum SphereSphereIntersection {
40 Circle(Circle3D),
42 TangentPoint(Point3D),
44 Empty,
47 Coincident,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
53pub enum PlaneCylinderIntersection {
54 Circle(Circle3D),
56 Ellipse(Ellipse3D),
58 TwoLines(Line3D, Line3D),
60 TangentLine(Line3D),
62 Empty,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq)]
68pub enum PlaneConeIntersection {
69 Circle(Circle3D),
71 Ellipse(Ellipse3D),
73 Parabola(Parabola3D),
75 Hyperbola(Hyperbola3D),
77 TwoLines(Line3D, Line3D),
79 TangentLine(Line3D),
81 ApexPoint(Point3D),
83 Empty,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq)]
89pub enum LinePlaneIntersection {
90 Point(f64, Point3D),
92 Parallel,
94 Coincident,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq)]
100pub enum LineQuadricIntersection {
101 TwoPoints((f64, Point3D), (f64, Point3D)),
103 OnePoint(f64, Point3D),
106 Tangent(f64, Point3D),
108 Empty,
110 Coincident,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq)]
123pub enum SphereCylinderIntersection {
124 Circle(Circle3D),
126 TwoCircles(Circle3D, Circle3D),
128 Empty,
130 NotAnalytic,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq)]
141pub enum SphereConeIntersection {
142 Circle(Circle3D),
144 TwoCircles(Circle3D, Circle3D),
146 TangentCircle(Circle3D),
148 Empty,
150 NotAnalytic,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq)]
161pub enum CylinderCylinderIntersection {
162 TwoLines(Line3D, Line3D),
164 TangentLine(Line3D),
166 Empty,
168 Coincident,
170 NotAnalytic,
172}
173
174#[derive(Debug, Clone, Copy, PartialEq)]
183pub enum ConeCylinderIntersection {
184 Circle(Circle3D),
186 ApexPoint(Point3D),
188 NotAnalytic,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq)]
199pub enum TorusPlaneIntersection {
200 TwoCircles(Circle3D, Circle3D),
202 TangentCircle(Circle3D),
204 Circle(Circle3D),
206 Empty,
208 NotAnalytic,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq)]
218pub enum TorusSphereIntersection {
219 Circle(Circle3D),
221 TwoCircles(Circle3D, Circle3D),
223 TangentCircle(Circle3D),
225 Empty,
227 NotAnalytic,
229}
230
231#[derive(Debug, Clone, Copy, PartialEq)]
236pub enum TorusCylinderIntersection {
237 TwoCircles(Circle3D, Circle3D),
239 TangentCircle(Circle3D),
241 Empty,
243 NotAnalytic,
245}
246
247#[derive(Debug, Clone, Copy, PartialEq)]
253pub enum TorusConeIntersection {
254 Circle(Circle3D),
256 TwoCircles(Circle3D, Circle3D),
258 TangentCircle(Circle3D),
260 Empty,
262 NotAnalytic,
264}
265
266#[derive(Debug, Clone, Copy, PartialEq)]
272pub enum TorusTorusIntersection {
273 Circle(Circle3D),
275 TwoCircles(Circle3D, Circle3D),
277 TangentCircle(Circle3D),
279 Empty,
281 Coincident,
283 NotAnalytic,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq)]
293pub enum ConeConeIntersection {
294 Circle(Circle3D),
296 ApexPoint(Point3D),
298 Empty,
301 Coincident,
303 NotAnalytic,
305}
306
307#[derive(Debug, Clone, Copy, PartialEq)]
309pub enum LineLine2DIntersection {
310 Point(f64, Point2D, f64),
312 Parallel,
314 Coincident,
316}
317
318#[derive(Debug, Clone, Copy, PartialEq)]
320pub enum LineCircle2DIntersection {
321 Points((f64, Point2D), (f64, Point2D)),
323 Tangent(f64, Point2D),
325 Empty,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq)]
334pub enum CircleCircle2DIntersection {
335 Points(Point2D, Point2D),
337 Tangent(Point2D),
339 Empty,
341 Coincident,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq)]
347pub enum LineLine3DIntersection {
348 Point(f64, Point3D, f64),
350 Parallel(f64),
352 Coincident,
354 Skew {
356 s: f64,
358 t: f64,
360 distance: f64,
362 },
363}
364
365#[derive(Debug, Clone, Copy, PartialEq)]
370pub enum LineCircle3DIntersection {
371 Points((f64, Point3D), (f64, Point3D)),
373 Tangent(f64, Point3D),
375 Empty,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq)]
386pub enum CircleCircle3DIntersection {
387 Points(Point3D, Point3D),
389 Tangent(Point3D),
391 Empty,
393 Coincident,
395 NotAnalytic,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq)]
401pub(crate) enum QuadraticSolution {
402 Two(f64, f64),
404 One(f64),
406 Linear(f64),
408 Degenerate,
410 Empty,
412}
413
414pub(crate) fn solve_quadratic(a: f64, b: f64, c: f64, tol: Tolerance) -> QuadraticSolution {
423 if a.abs() <= tol.angular {
424 if b.abs() <= tol.confusion {
425 return QuadraticSolution::Degenerate;
426 }
427 return QuadraticSolution::Linear(-c / b);
428 }
429 let disc = b * b - 4.0 * a * c;
430 let mid = -b / (2.0 * a);
431 let sep = disc.abs().sqrt() / (2.0 * a.abs());
432 if sep <= tol.confusion * (1.0 + mid.abs()) {
433 QuadraticSolution::One(mid)
434 } else if disc < 0.0 {
435 QuadraticSolution::Empty
436 } else {
437 let (t1, t2) = (mid - sep, mid + sep);
438 QuadraticSolution::Two(t1, t2)
439 }
440}
441
442#[derive(Debug, Clone, Copy, PartialEq)]
444pub struct CurveCurveExtremum {
445 pub first_param: f64,
447 pub second_param: f64,
449 pub distance: f64,
451}
452
453#[derive(Debug, Clone, Copy, PartialEq)]
455pub struct CurveCurveHit {
456 pub first_param: f64,
458 pub point: Point3D,
460 pub second_param: f64,
462}
463
464fn clamp_param(t: f64, bounds: (f64, f64)) -> f64 {
467 if bounds.0.is_finite() || bounds.1.is_finite() {
468 t.clamp(bounds.0, bounds.1)
469 } else {
470 t
471 }
472}
473
474pub fn curve_curve_extrema<A, B>(a: &A, b: &B, tol: Tolerance) -> Vec<CurveCurveExtremum>
493where
494 A: ParametricCurve3D,
495 B: ParametricCurve3D,
496{
497 const WINDOW: f64 = 10.0;
498 let (ba, bb) = (a.bounds(), b.bounds());
499 let (la, ha) = (
500 if ba.0.is_finite() { ba.0 } else { -WINDOW },
501 if ba.1.is_finite() { ba.1 } else { WINDOW },
502 );
503 let (lb, hb) = (
504 if bb.0.is_finite() { bb.0 } else { -WINDOW },
505 if bb.1.is_finite() { bb.1 } else { WINDOW },
506 );
507 const NS: usize = 32;
508 const NT: usize = 32;
509 const KEEP: usize = 24;
510 let mut samples: Vec<(f64, f64, f64)> = Vec::with_capacity(NS * NT);
511 for i in 0..NS {
512 for j in 0..NT {
513 let s = la + (ha - la) * i as f64 / (NS - 1) as f64;
514 let t = lb + (hb - lb) * j as f64 / (NT - 1) as f64;
515 samples.push((s, t, a.eval_point(s).distance(b.eval_point(t))));
516 }
517 }
518 samples.sort_by(|x, y| x.2.partial_cmp(&y.2).unwrap());
519 let mut out: Vec<CurveCurveExtremum> = Vec::new();
520 for (s, t, _) in samples.into_iter().take(KEEP) {
521 let residual = |s: f64, t: f64| {
522 let d = a.eval_point(clamp_param(s, ba)) - b.eval_point(clamp_param(t, bb));
523 [d.x, d.y, d.z]
524 };
525 let jac_s = |s: f64, _t: f64| {
526 let v = a.eval_derivative(clamp_param(s, ba), 1);
527 [v.x, v.y, v.z]
528 };
529 let jac_t = |_s: f64, t: f64| {
530 let v = b.eval_derivative(clamp_param(t, bb), 1);
531 [-v.x, -v.y, -v.z]
532 };
533 if let Some((rs, rt)) = gauss_newton_2d(residual, jac_s, jac_t, s, t, tol.confusion, 50) {
534 let (cs, ct) = (clamp_param(rs, ba), clamp_param(rt, bb));
535 let distance = a.eval_point(cs).distance(b.eval_point(ct));
536 if !out.iter().any(|e: &CurveCurveExtremum| {
537 (a.eval_point(e.first_param).distance(a.eval_point(cs))
538 + b.eval_point(e.second_param).distance(b.eval_point(ct)))
539 <= tol.confusion
540 }) {
541 out.push(CurveCurveExtremum {
542 first_param: cs,
543 second_param: ct,
544 distance,
545 });
546 }
547 }
548 }
549 out.sort_by(|x, y| x.distance.partial_cmp(&y.distance).unwrap());
550 out
551}
552
553pub fn intersect_curve_curve<A, B>(a: &A, b: &B, tol: Tolerance) -> Vec<CurveCurveHit>
566where
567 A: ParametricCurve3D,
568 B: ParametricCurve3D,
569{
570 let mut hits: Vec<CurveCurveHit> = curve_curve_extrema(a, b, tol)
571 .into_iter()
572 .filter(|e| e.distance <= tol.confusion)
573 .map(|e| CurveCurveHit {
574 first_param: e.first_param,
575 point: a.eval_point(e.first_param),
576 second_param: e.second_param,
577 })
578 .collect();
579 hits.sort_by(|x, y| x.first_param.partial_cmp(&y.first_param).unwrap());
580 hits
581}
582
583#[derive(Debug, Clone, Copy, PartialEq)]
585pub struct CurveSurfaceHit {
586 pub curve_param: f64,
588 pub surface_params: (f64, f64),
590 pub point: Point3D,
592}
593
594pub fn intersect_curve_surface<C, S>(curve: &C, surface: &S, tol: Tolerance) -> Vec<CurveSurfaceHit>
615where
616 C: ParametricCurve3D,
617 S: ParametricSurface,
618{
619 let (t0, t1) = curve.bounds();
620 const WINDOW: f64 = 10.0;
622 let (lo, hi) = (
623 if t0.is_finite() { t0 } else { -WINDOW },
624 if t1.is_finite() { t1 } else { WINDOW },
625 );
626 const SEEDS: usize = 128;
627 const KEEP: usize = 24;
628 let mut samples: Vec<(f64, f64)> = (0..SEEDS)
629 .map(|i| {
630 let t = lo + (hi - lo) * i as f64 / (SEEDS - 1) as f64;
631 (t, surface.project_point(curve.eval_point(t), tol).distance)
632 })
633 .collect();
634 samples.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
635 let mut hits: Vec<CurveSurfaceHit> = Vec::new();
636 for (t, _) in samples.into_iter().take(KEEP) {
637 let proj = surface.project_point(curve.eval_point(t), tol);
638 let f = |x: [f64; 3]| {
639 let d = curve.eval_point(x[0]) - surface.eval_point(x[1], x[2]);
640 [d.x, d.y, d.z]
641 };
642 let jac = |x: [f64; 3]| {
643 let ct = curve.eval_derivative(x[0], 1);
644 let su = surface.eval_derivative(x[1], x[2], 1, 0);
645 let sv = surface.eval_derivative(x[1], x[2], 0, 1);
646 [
647 [ct.x, -su.x, -sv.x],
648 [ct.y, -su.y, -sv.y],
649 [ct.z, -su.z, -sv.z],
650 ]
651 };
652 if let Some([rt, ru, rv]) = newton_3d(f, jac, [t, proj.u, proj.v], tol.confusion, 50) {
653 let p = curve.eval_point(rt);
654 let q = surface.eval_point(ru, rv);
655 if p.distance(q) <= tol.confusion
656 && !hits
657 .iter()
658 .any(|h: &CurveSurfaceHit| h.point.distance(p) <= tol.confusion)
659 {
660 hits.push(CurveSurfaceHit {
661 curve_param: rt,
662 surface_params: (ru, rv),
663 point: p,
664 });
665 }
666 }
667 }
668 hits.sort_by(|a, b| a.curve_param.partial_cmp(&b.curve_param).unwrap());
669 hits
670}
671
672pub fn marching_intersection<A, B>(a: &A, b: &B, tol: Tolerance) -> Vec<BSplineCurve3D>
698where
699 A: ParametricSurface,
700 B: ParametricSurface,
701{
702 let mut seeds: Vec<(Point3D, f64)> = Vec::new();
705 {
706 let (u0, u1) = seed_window(a.u_bounds());
707 let (v0, v1) = seed_window(a.v_bounds());
708 const NU: usize = 24;
709 const NV: usize = 24;
710 for j in 0..=NV {
711 for i in 0..=NU {
712 let (u, v) = (
713 u0 + (u1 - u0) * i as f64 / NU as f64,
714 v0 + (v1 - v0) * j as f64 / NV as f64,
715 );
716 let p = a.eval_point(u, v);
717 let d = b.project_point(p, tol).distance;
718 if d <= 100.0 * tol.confusion {
719 seeds.push((p, d));
720 }
721 }
722 }
723 }
724 {
725 let (u0, u1) = seed_window(b.u_bounds());
726 let (v0, v1) = seed_window(b.v_bounds());
727 const NU: usize = 24;
728 const NV: usize = 24;
729 for j in 0..=NV {
730 for i in 0..=NU {
731 let (u, v) = (
732 u0 + (u1 - u0) * i as f64 / NU as f64,
733 v0 + (v1 - v0) * j as f64 / NV as f64,
734 );
735 let p = b.eval_point(u, v);
736 let d = a.project_point(p, tol).distance;
737 if d <= 100.0 * tol.confusion {
738 seeds.push((p, d));
739 }
740 }
741 }
742 }
743 let mut leaders: Vec<Point3D> = Vec::new();
747 if !seeds.is_empty() {
748 let mut lo = seeds[0].0;
749 let mut hi = seeds[0].0;
750 for &(p, _) in &seeds[1..] {
751 lo = Point3D::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
752 hi = Point3D::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
753 }
754 let radius = (0.05 * lo.distance(hi)).max(100.0 * tol.confusion);
755 let mut parent: Vec<usize> = (0..seeds.len()).collect();
756 fn find(parent: &mut [usize], mut x: usize) -> usize {
757 while parent[x] != x {
758 parent[x] = parent[parent[x]];
759 x = parent[x];
760 }
761 x
762 }
763 for i in 0..seeds.len() {
764 for j in (i + 1)..seeds.len() {
765 if seeds[i].0.distance(seeds[j].0) <= radius {
766 let (ri, rj) = (find(&mut parent, i), find(&mut parent, j));
767 parent[ri] = rj;
768 }
769 }
770 }
771 let mut best: std::collections::HashMap<usize, (f64, Point3D)> =
773 std::collections::HashMap::new();
774 for (i, &(p, d)) in seeds.iter().enumerate() {
775 let r = find(&mut parent, i);
776 best.entry(r)
777 .and_modify(|e| {
778 if d < e.0 {
779 *e = (d, p);
780 }
781 })
782 .or_insert((d, p));
783 }
784 let mut ordered: Vec<(f64, Point3D)> = best.into_values().collect();
785 ordered.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
786 leaders = ordered.into_iter().map(|(_, p)| p).collect();
787 }
788 let mut curves: Vec<BSplineCurve3D> = Vec::new();
789 let suppress = 1000.0 * tol.confusion;
794 let mut step = 0.02f64;
795 for seed in leaders {
796 if curves
797 .iter()
798 .any(|c| c.project_point(seed, tol).distance <= suppress)
799 {
800 continue;
801 }
802 if let Some(loop_pts) = trace_loop(a, b, seed, &mut step, tol)
803 && loop_pts.len() >= 4
804 {
805 let mut closed = loop_pts;
806 closed.push(closed[0]);
807 let degree = 3.min(closed.len() - 1);
808 if let Ok(fit) =
809 BSplineCurve3D::interpolate(&closed, degree, InterpParametrization::Centripetal)
810 {
811 curves.push(fit);
812 }
813 }
814 }
815 curves
816}
817
818fn trace_loop<A, B>(
820 a: &A,
821 b: &B,
822 seed: Point3D,
823 step: &mut f64,
824 tol: Tolerance,
825) -> Option<Vec<Point3D>>
826where
827 A: ParametricSurface,
828 B: ParametricSurface,
829{
830 let normal_at = |s: &dyn ParametricSurface, u: f64, v: f64| {
831 let su = s.eval_derivative(u, v, 1, 0);
832 let sv = s.eval_derivative(u, v, 0, 1);
833 let n = su.cross(sv);
834 let scale = su.magnitude() * sv.magnitude();
835 if n.magnitude() <= tol.angular * scale.max(f64::MIN_POSITIVE) {
836 None
837 } else {
838 Some(n * (1.0 / n.magnitude()))
839 }
840 };
841 let mut fwd: Vec<Point3D> = vec![seed];
843 let mut bwd: Vec<Point3D> = Vec::new();
844 for (chain, dir) in [(&mut fwd, 1.0), (&mut bwd, -1.0)] {
846 let mut p = seed;
847 let mut h = *step;
848 for _ in 0..2000 {
849 let qa = a.project_point(p, tol);
850 let qb = b.project_point(p, tol);
851 let (Some(na), Some(nb)) = (normal_at(a, qa.u, qa.v), normal_at(b, qb.u, qb.v)) else {
852 break;
853 };
854 let t = na.cross(nb);
855 if t.magnitude() <= tol.angular {
856 break;
857 }
858 let t = t * (dir / t.magnitude());
859 let mut accepted = false;
861 for _ in 0..10 {
862 let q = correct(a, b, p + t * h, tol);
863 match q {
864 Some(q) if b.project_point(q, tol).distance <= 10.0 * tol.confusion => {
865 p = q;
866 h = (h * 1.25).min(0.1);
867 accepted = true;
868 break;
869 }
870 _ => {
871 h *= 0.5;
872 if h < tol.confusion {
873 break;
874 }
875 }
876 }
877 }
878 if !accepted {
879 break;
880 }
881 if chain.len() > 10 && p.distance(chain[0]) <= 10.0 * tol.confusion {
883 chain.push(chain[0]);
884 break;
885 }
886 let ra = a.project_point(p, tol);
888 let rb = b.project_point(p, tol);
889 if off_bounds(a, ra.u, ra.v) || off_bounds(b, rb.u, rb.v) {
890 chain.push(p);
891 break;
892 }
893 chain.push(p);
894 }
895 }
896 *step = (*step).clamp(tol.confusion, 1.0);
897 bwd.reverse();
899 let mut pts = bwd;
900 pts.extend(fwd.into_iter().skip(1));
901 if pts.len() < 4 { None } else { Some(pts) }
902}
903
904fn correct<A, B>(a: &A, b: &B, p: Point3D, tol: Tolerance) -> Option<Point3D>
906where
907 A: ParametricSurface,
908 B: ParametricSurface,
909{
910 let mut q = p;
911 for _ in 0..8 {
912 let pa = a.project_point(q, tol);
913 let qa = a.eval_point(pa.u, pa.v);
914 let pb = b.project_point(qa, tol);
915 let qb = b.eval_point(pb.u, pb.v);
916 if qb.distance(q) <= tol.confusion {
917 return Some(qb);
918 }
919 q = qb;
920 }
921 let residual = a.project_point(q, tol).distance + b.project_point(q, tol).distance;
922 if residual <= 10.0 * tol.confusion {
923 Some(q)
924 } else {
925 None
926 }
927}
928
929fn seed_window((lo, hi): (f64, f64)) -> (f64, f64) {
931 const WINDOW: f64 = 10.0;
932 (
933 if lo.is_finite() { lo } else { -WINDOW },
934 if hi.is_finite() { hi } else { WINDOW },
935 )
936}
937
938fn off_bounds<S: ParametricSurface + ?Sized>(s: &S, u: f64, v: f64) -> bool {
940 let (u0, u1) = s.u_bounds();
941 let (v0, v1) = s.v_bounds();
942 (s.u_period().is_none() && (u < u0 || u > u1)) || (s.v_period().is_none() && (v < v0 || v > v1))
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948 use crate::Tolerance;
949 use crate::{Line3D, LineCircle3DIntersection, Point3D, Sphere, Vector3D};
950
951 #[test]
952 fn test_solve_quadratic_two_roots_ordered() {
953 assert_eq!(
955 solve_quadratic(1.0, -5.0, 6.0, Tolerance::DEFAULT),
956 QuadraticSolution::Two(2.0, 3.0)
957 );
958 }
959
960 #[test]
961 fn test_solve_quadratic_tangent_band() {
962 assert_eq!(
964 solve_quadratic(1.0, -4.0, 4.0, Tolerance::DEFAULT),
965 QuadraticSolution::One(2.0)
966 );
967 assert_eq!(
969 solve_quadratic(1.0, 0.0, 1e-14, Tolerance::DEFAULT),
970 QuadraticSolution::One(0.0)
971 );
972 assert_eq!(
974 solve_quadratic(1.0, 0.0, 1e-9, Tolerance::DEFAULT),
975 QuadraticSolution::Empty
976 );
977 assert_eq!(
979 solve_quadratic(1.0, 0.0, 1.0, Tolerance::DEFAULT),
980 QuadraticSolution::Empty
981 );
982 }
983
984 #[test]
985 fn test_solve_quadratic_degenerate_arms() {
986 assert_eq!(
988 solve_quadratic(0.0, 2.0, 4.0, Tolerance::DEFAULT),
989 QuadraticSolution::Linear(-2.0)
990 );
991 assert_eq!(
993 solve_quadratic(0.0, 0.0, 1.0, Tolerance::DEFAULT),
994 QuadraticSolution::Degenerate
995 );
996 }
997
998 #[test]
999 fn test_generic_line_sphere_matches_analytic() {
1000 let tol = Tolerance::DEFAULT;
1001 let line = Line3D::new(Point3D::ORIGIN, Vector3D::X).unwrap();
1002 let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
1003 let hits = intersect_curve_surface(&line, &sphere, tol);
1004 assert_eq!(hits.len(), 2);
1005 assert!((hits[0].curve_param + 2.0).abs() < 1e-9);
1006 assert!((hits[1].curve_param - 2.0).abs() < 1e-9);
1007 match line.intersect_sphere(&sphere, tol) {
1009 LineQuadricIntersection::TwoPoints((t1, _), (t2, _)) => {
1010 assert!((hits[0].curve_param - t1).abs() < 1e-9);
1011 assert!((hits[1].curve_param - t2).abs() < 1e-9);
1012 }
1013 _ => panic!("analytic expected two points"),
1014 }
1015 }
1016
1017 #[test]
1018 fn test_generic_line_sphere_miss() {
1019 let tol = Tolerance::DEFAULT;
1020 let line = Line3D::new(Point3D::new(0.0, 0.0, 3.0), Vector3D::X).unwrap();
1021 let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
1022 assert!(intersect_curve_surface(&line, &sphere, tol).is_empty());
1023 }
1024
1025 #[test]
1026 fn test_generic_segment_plane_hit() {
1027 use crate::{BSplineCurve3D, Plane};
1028 let tol = Tolerance::DEFAULT;
1029 let poles = vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(2.0, 0.0, 0.0)];
1030 let curve = BSplineCurve3D::new(1, poles, vec![0.0, 1.0], vec![2, 2], false).unwrap();
1031 let plane = Plane::new(Point3D::new(1.0, 0.0, 0.0), Vector3D::X).unwrap();
1032 let hits = intersect_curve_surface(&curve, &plane, tol);
1033 assert_eq!(hits.len(), 1);
1034 assert!((hits[0].curve_param - 0.5).abs() < 1e-9);
1035 assert_eq!(hits[0].point, Point3D::new(1.0, 0.0, 0.0));
1036 }
1037
1038 #[test]
1039 fn test_marching_plane_plane_line() {
1040 use crate::Plane;
1041 let tol = Tolerance::DEFAULT;
1042 let xy = Plane::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
1043 let zy = Plane::new(Point3D::ORIGIN, Vector3D::X).unwrap();
1044 let curves = marching_intersection(&xy, &zy, tol);
1045 assert_eq!(curves.len(), 1);
1046 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
1048 let p = curves[0].eval_point(t);
1049 assert!(p.x.abs() < 1e-6, "{p:?}");
1050 assert!(p.z.abs() < 1e-6, "{p:?}");
1051 }
1052 }
1053
1054 #[test]
1055 fn test_marching_plane_sphere_circle() {
1056 use crate::{Plane, Sphere};
1057 let tol = Tolerance::DEFAULT;
1058 let plane = Plane::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
1059 let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
1060 let curves = marching_intersection(&plane, &sphere, tol);
1061 assert_eq!(curves.len(), 1);
1062 for t in [0.0, 0.2, 0.4, 0.6, 0.8, 1.0] {
1064 let p = curves[0].eval_point(t);
1065 assert!(plane.project_point(p, tol).distance < 1e-3, "{p:?}");
1066 assert!(sphere.project_point(p, tol).distance < 1e-3, "{p:?}");
1067 }
1068 }
1069
1070 #[test]
1071 fn test_marching_miss_is_empty() {
1072 use crate::{Plane, Sphere};
1073 let tol = Tolerance::DEFAULT;
1074 let plane = Plane::new(Point3D::new(0.0, 0.0, 5.0), Vector3D::Z).unwrap();
1075 let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
1076 assert!(marching_intersection(&plane, &sphere, tol).is_empty());
1077 }
1078
1079 #[test]
1080 fn test_curve_curve_extrema_skew_matches_analytic() {
1081 use crate::{Line3D, LineLine3DIntersection};
1082 let tol = Tolerance::DEFAULT;
1083 let l1 = Line3D::new(Point3D::ORIGIN, Vector3D::X).unwrap();
1084 let l2 = Line3D::new(Point3D::new(0.0, 0.0, 1.0), Vector3D::Y).unwrap();
1085 let ext = curve_curve_extrema(&l1, &l2, tol);
1086 assert_eq!(ext.len(), 1);
1087 assert!((ext[0].distance - 1.0).abs() < 1e-9);
1088 match l1.intersect_line(&l2, tol) {
1089 LineLine3DIntersection::Skew { s, t, distance } => {
1090 assert!((ext[0].first_param - s).abs() < 1e-9);
1091 assert!((ext[0].second_param - t).abs() < 1e-9);
1092 assert!((ext[0].distance - distance).abs() < 1e-12);
1093 }
1094 _ => panic!("analytic expected skew"),
1095 }
1096 }
1097
1098 #[test]
1099 fn test_intersect_curve_curve_line_circle() {
1100 use crate::{Circle3D, Line3D};
1101 let tol = Tolerance::DEFAULT;
1102 let line = Line3D::new(Point3D::ORIGIN, Vector3D::X).unwrap();
1103 let circle = Circle3D::new(Point3D::ORIGIN, Vector3D::Z, 2.0).unwrap();
1104 let hits = intersect_curve_curve(&line, &circle, tol);
1105 assert_eq!(hits.len(), 2);
1106 assert!((hits[0].first_param + 2.0).abs() < 1e-9);
1107 assert!((hits[1].first_param - 2.0).abs() < 1e-9);
1108 match line.intersect_circle(&circle, tol) {
1110 LineCircle3DIntersection::Points((t1, _), (t2, _)) => {
1111 assert!((hits[0].first_param - t1).abs() < 1e-9);
1112 assert!((hits[1].first_param - t2).abs() < 1e-9);
1113 }
1114 _ => panic!("analytic expected two points"),
1115 }
1116 }
1117}