use crate::curves::{BSplineCurve3D, InterpParametrization, ParametricCurve3D};
use crate::math::{gauss_newton_2d, newton_3d};
use crate::surfaces::ParametricSurface;
use crate::{Circle3D, Ellipse3D, Hyperbola3D, Line3D, Parabola3D, Point2D, Point3D, Tolerance};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PlanePlaneIntersection {
Line(Line3D),
Parallel,
Coincident,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PlaneSphereIntersection {
Circle(Circle3D),
TangentPoint(Point3D),
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SphereSphereIntersection {
Circle(Circle3D),
TangentPoint(Point3D),
Empty,
Coincident,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PlaneCylinderIntersection {
Circle(Circle3D),
Ellipse(Ellipse3D),
TwoLines(Line3D, Line3D),
TangentLine(Line3D),
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PlaneConeIntersection {
Circle(Circle3D),
Ellipse(Ellipse3D),
Parabola(Parabola3D),
Hyperbola(Hyperbola3D),
TwoLines(Line3D, Line3D),
TangentLine(Line3D),
ApexPoint(Point3D),
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LinePlaneIntersection {
Point(f64, Point3D),
Parallel,
Coincident,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineQuadricIntersection {
TwoPoints((f64, Point3D), (f64, Point3D)),
OnePoint(f64, Point3D),
Tangent(f64, Point3D),
Empty,
Coincident,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SphereCylinderIntersection {
Circle(Circle3D),
TwoCircles(Circle3D, Circle3D),
Empty,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SphereConeIntersection {
Circle(Circle3D),
TwoCircles(Circle3D, Circle3D),
TangentCircle(Circle3D),
Empty,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CylinderCylinderIntersection {
TwoLines(Line3D, Line3D),
TangentLine(Line3D),
Empty,
Coincident,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConeCylinderIntersection {
Circle(Circle3D),
ApexPoint(Point3D),
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TorusPlaneIntersection {
TwoCircles(Circle3D, Circle3D),
TangentCircle(Circle3D),
Circle(Circle3D),
Empty,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TorusSphereIntersection {
Circle(Circle3D),
TwoCircles(Circle3D, Circle3D),
TangentCircle(Circle3D),
Empty,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TorusCylinderIntersection {
TwoCircles(Circle3D, Circle3D),
TangentCircle(Circle3D),
Empty,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TorusConeIntersection {
Circle(Circle3D),
TwoCircles(Circle3D, Circle3D),
TangentCircle(Circle3D),
Empty,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TorusTorusIntersection {
Circle(Circle3D),
TwoCircles(Circle3D, Circle3D),
TangentCircle(Circle3D),
Empty,
Coincident,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConeConeIntersection {
Circle(Circle3D),
ApexPoint(Point3D),
Empty,
Coincident,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineLine2DIntersection {
Point(f64, Point2D, f64),
Parallel,
Coincident,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineCircle2DIntersection {
Points((f64, Point2D), (f64, Point2D)),
Tangent(f64, Point2D),
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CircleCircle2DIntersection {
Points(Point2D, Point2D),
Tangent(Point2D),
Empty,
Coincident,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineLine3DIntersection {
Point(f64, Point3D, f64),
Parallel(f64),
Coincident,
Skew {
s: f64,
t: f64,
distance: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LineCircle3DIntersection {
Points((f64, Point3D), (f64, Point3D)),
Tangent(f64, Point3D),
Empty,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CircleCircle3DIntersection {
Points(Point3D, Point3D),
Tangent(Point3D),
Empty,
Coincident,
NotAnalytic,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConicSurfaceHit {
pub parameter: f64,
pub point: Point3D,
pub multiplicity: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CircleSurfaceIntersection {
Circle(Circle3D),
Hits(Vec<ConicSurfaceHit>),
Empty,
}
pub(crate) fn solve_trig_linear(
c: f64,
d: f64,
e: f64,
scale: f64,
tol: Tolerance,
) -> Vec<(f64, u32)> {
let amp = c.hypot(d);
let band = tol.confusion * scale.max(f64::MIN_POSITIVE);
if amp <= band {
return Vec::new();
}
if e.abs() > amp + band {
return Vec::new();
}
let phi = d.atan2(c);
let t0 = (-e / amp).clamp(-1.0, 1.0).acos();
if e.abs() >= amp - band {
vec![(phi + t0, 2)]
} else {
vec![(phi + t0, 1), (phi - t0, 1)]
}
}
pub(crate) fn solve_trig(
a2: f64,
b2: f64,
c1: f64,
d1: f64,
e: f64,
tol: Tolerance,
) -> Vec<(f64, u32)> {
let q = [
a2 + c1 + e,
4.0 * b2 + 2.0 * d1,
-6.0 * a2 + 2.0 * e,
-4.0 * b2 + 2.0 * d1,
a2 - c1 + e,
];
let mut out: Vec<(f64, u32)> = crate::math::real_roots(&q, tol.confusion)
.into_iter()
.map(|r| (2.0 * r.value.atan(), r.multiplicity))
.collect();
out.push((std::f64::consts::PI, 1));
out.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let mut deduped: Vec<(f64, u32)> = Vec::with_capacity(out.len());
for (t, m) in out {
match deduped.last_mut() {
Some((last_t, last_m)) if (t - *last_t).abs() <= tol.parametric * (1.0 + t.abs()) => {
*last_m = (*last_m).max(m);
}
_ => deduped.push((t, m)),
}
}
deduped
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum QuadraticSolution {
Two(f64, f64),
One(f64),
Linear(f64),
Degenerate,
Empty,
}
pub(crate) fn solve_quadratic(a: f64, b: f64, c: f64, tol: Tolerance) -> QuadraticSolution {
if a.abs() <= tol.angular {
if b.abs() <= tol.confusion {
return QuadraticSolution::Degenerate;
}
return QuadraticSolution::Linear(-c / b);
}
let disc = b * b - 4.0 * a * c;
let mid = -b / (2.0 * a);
let sep = disc.abs().sqrt() / (2.0 * a.abs());
if sep <= tol.confusion * (1.0 + mid.abs()) {
QuadraticSolution::One(mid)
} else if disc < 0.0 {
QuadraticSolution::Empty
} else {
let (t1, t2) = (mid - sep, mid + sep);
QuadraticSolution::Two(t1, t2)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CurveCurveExtremum {
pub first_param: f64,
pub second_param: f64,
pub distance: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CurveCurveHit {
pub first_param: f64,
pub point: Point3D,
pub second_param: f64,
}
fn clamp_param(t: f64, bounds: (f64, f64)) -> f64 {
if bounds.0.is_finite() || bounds.1.is_finite() {
t.clamp(bounds.0, bounds.1)
} else {
t
}
}
pub fn curve_curve_extrema<A, B>(a: &A, b: &B, tol: Tolerance) -> Vec<CurveCurveExtremum>
where
A: ParametricCurve3D,
B: ParametricCurve3D,
{
const WINDOW: f64 = 10.0;
let (ba, bb) = (a.bounds(), b.bounds());
let (la, ha) = (
if ba.0.is_finite() { ba.0 } else { -WINDOW },
if ba.1.is_finite() { ba.1 } else { WINDOW },
);
let (lb, hb) = (
if bb.0.is_finite() { bb.0 } else { -WINDOW },
if bb.1.is_finite() { bb.1 } else { WINDOW },
);
const NS: usize = 32;
const NT: usize = 32;
const KEEP: usize = 24;
let mut samples: Vec<(f64, f64, f64)> = Vec::with_capacity(NS * NT);
for i in 0..NS {
for j in 0..NT {
let s = la + (ha - la) * i as f64 / (NS - 1) as f64;
let t = lb + (hb - lb) * j as f64 / (NT - 1) as f64;
samples.push((s, t, a.eval_point(s).distance(b.eval_point(t))));
}
}
samples.sort_by(|x, y| x.2.partial_cmp(&y.2).unwrap());
let mut out: Vec<CurveCurveExtremum> = Vec::new();
for (s, t, _) in samples.into_iter().take(KEEP) {
let residual = |s: f64, t: f64| {
let d = a.eval_point(clamp_param(s, ba)) - b.eval_point(clamp_param(t, bb));
[d.x, d.y, d.z]
};
let jac_s = |s: f64, _t: f64| {
let v = a.eval_derivative(clamp_param(s, ba), 1);
[v.x, v.y, v.z]
};
let jac_t = |_s: f64, t: f64| {
let v = b.eval_derivative(clamp_param(t, bb), 1);
[-v.x, -v.y, -v.z]
};
if let Some((rs, rt)) = gauss_newton_2d(residual, jac_s, jac_t, s, t, tol.confusion, 50) {
let (cs, ct) = (clamp_param(rs, ba), clamp_param(rt, bb));
let distance = a.eval_point(cs).distance(b.eval_point(ct));
if !out.iter().any(|e: &CurveCurveExtremum| {
(a.eval_point(e.first_param).distance(a.eval_point(cs))
+ b.eval_point(e.second_param).distance(b.eval_point(ct)))
<= tol.confusion
}) {
out.push(CurveCurveExtremum {
first_param: cs,
second_param: ct,
distance,
});
}
}
}
out.sort_by(|x, y| x.distance.partial_cmp(&y.distance).unwrap());
out
}
pub fn intersect_curve_curve<A, B>(a: &A, b: &B, tol: Tolerance) -> Vec<CurveCurveHit>
where
A: ParametricCurve3D,
B: ParametricCurve3D,
{
let mut hits: Vec<CurveCurveHit> = curve_curve_extrema(a, b, tol)
.into_iter()
.filter(|e| e.distance <= tol.confusion)
.map(|e| CurveCurveHit {
first_param: e.first_param,
point: a.eval_point(e.first_param),
second_param: e.second_param,
})
.collect();
hits.sort_by(|x, y| x.first_param.partial_cmp(&y.first_param).unwrap());
hits
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CurveSurfaceHit {
pub curve_param: f64,
pub surface_params: (f64, f64),
pub point: Point3D,
}
pub fn intersect_curve_surface<C, S>(curve: &C, surface: &S, tol: Tolerance) -> Vec<CurveSurfaceHit>
where
C: ParametricCurve3D,
S: ParametricSurface,
{
let (t0, t1) = curve.bounds();
const WINDOW: f64 = 10.0;
let (lo, hi) = (
if t0.is_finite() { t0 } else { -WINDOW },
if t1.is_finite() { t1 } else { WINDOW },
);
const SEEDS: usize = 128;
const KEEP: usize = 24;
let mut samples: Vec<(f64, f64)> = (0..SEEDS)
.map(|i| {
let t = lo + (hi - lo) * i as f64 / (SEEDS - 1) as f64;
(t, surface.project_point(curve.eval_point(t), tol).distance)
})
.collect();
samples.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
let mut hits: Vec<CurveSurfaceHit> = Vec::new();
for (t, _) in samples.into_iter().take(KEEP) {
let proj = surface.project_point(curve.eval_point(t), tol);
let f = |x: [f64; 3]| {
let d = curve.eval_point(x[0]) - surface.eval_point(x[1], x[2]);
[d.x, d.y, d.z]
};
let jac = |x: [f64; 3]| {
let ct = curve.eval_derivative(x[0], 1);
let su = surface.eval_derivative(x[1], x[2], 1, 0);
let sv = surface.eval_derivative(x[1], x[2], 0, 1);
[
[ct.x, -su.x, -sv.x],
[ct.y, -su.y, -sv.y],
[ct.z, -su.z, -sv.z],
]
};
if let Some([rt, ru, rv]) = newton_3d(f, jac, [t, proj.u, proj.v], tol.confusion, 50) {
let p = curve.eval_point(rt);
let q = surface.eval_point(ru, rv);
if p.distance(q) <= tol.confusion
&& !hits
.iter()
.any(|h: &CurveSurfaceHit| h.point.distance(p) <= tol.confusion)
{
hits.push(CurveSurfaceHit {
curve_param: rt,
surface_params: (ru, rv),
point: p,
});
}
}
}
hits.sort_by(|a, b| a.curve_param.partial_cmp(&b.curve_param).unwrap());
hits
}
pub fn marching_intersection<A, B>(a: &A, b: &B, tol: Tolerance) -> Vec<BSplineCurve3D>
where
A: ParametricSurface,
B: ParametricSurface,
{
let mut seeds: Vec<(Point3D, f64)> = Vec::new();
{
let (u0, u1) = seed_window(a.u_bounds());
let (v0, v1) = seed_window(a.v_bounds());
const NU: usize = 24;
const NV: usize = 24;
for j in 0..=NV {
for i in 0..=NU {
let (u, v) = (
u0 + (u1 - u0) * i as f64 / NU as f64,
v0 + (v1 - v0) * j as f64 / NV as f64,
);
let p = a.eval_point(u, v);
let d = b.project_point(p, tol).distance;
if d <= 100.0 * tol.confusion {
seeds.push((p, d));
}
}
}
}
{
let (u0, u1) = seed_window(b.u_bounds());
let (v0, v1) = seed_window(b.v_bounds());
const NU: usize = 24;
const NV: usize = 24;
for j in 0..=NV {
for i in 0..=NU {
let (u, v) = (
u0 + (u1 - u0) * i as f64 / NU as f64,
v0 + (v1 - v0) * j as f64 / NV as f64,
);
let p = b.eval_point(u, v);
let d = a.project_point(p, tol).distance;
if d <= 100.0 * tol.confusion {
seeds.push((p, d));
}
}
}
}
let mut leaders: Vec<Point3D> = Vec::new();
if !seeds.is_empty() {
let mut lo = seeds[0].0;
let mut hi = seeds[0].0;
for &(p, _) in &seeds[1..] {
lo = Point3D::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z));
hi = Point3D::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z));
}
let radius = (0.05 * lo.distance(hi)).max(100.0 * tol.confusion);
let mut parent: Vec<usize> = (0..seeds.len()).collect();
fn find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
for i in 0..seeds.len() {
for j in (i + 1)..seeds.len() {
if seeds[i].0.distance(seeds[j].0) <= radius {
let (ri, rj) = (find(&mut parent, i), find(&mut parent, j));
parent[ri] = rj;
}
}
}
let mut best: std::collections::HashMap<usize, (f64, Point3D)> =
std::collections::HashMap::new();
for (i, &(p, d)) in seeds.iter().enumerate() {
let r = find(&mut parent, i);
best.entry(r)
.and_modify(|e| {
if d < e.0 {
*e = (d, p);
}
})
.or_insert((d, p));
}
let mut ordered: Vec<(f64, Point3D)> = best.into_values().collect();
ordered.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
leaders = ordered.into_iter().map(|(_, p)| p).collect();
}
let mut curves: Vec<BSplineCurve3D> = Vec::new();
let suppress = 1000.0 * tol.confusion;
let mut step = 0.02f64;
for seed in leaders {
if curves
.iter()
.any(|c| c.project_point(seed, tol).distance <= suppress)
{
continue;
}
if let Some(loop_pts) = trace_loop(a, b, seed, &mut step, tol)
&& loop_pts.len() >= 4
{
let mut closed = loop_pts;
closed.push(closed[0]);
let degree = 3.min(closed.len() - 1);
if let Ok(fit) =
BSplineCurve3D::interpolate(&closed, degree, InterpParametrization::Centripetal)
{
curves.push(fit);
}
}
}
curves
}
fn trace_loop<A, B>(
a: &A,
b: &B,
seed: Point3D,
step: &mut f64,
tol: Tolerance,
) -> Option<Vec<Point3D>>
where
A: ParametricSurface,
B: ParametricSurface,
{
let normal_at = |s: &dyn ParametricSurface, u: f64, v: f64| {
let su = s.eval_derivative(u, v, 1, 0);
let sv = s.eval_derivative(u, v, 0, 1);
let n = su.cross(sv);
let scale = su.magnitude() * sv.magnitude();
if n.magnitude() <= tol.angular * scale.max(f64::MIN_POSITIVE) {
None
} else {
Some(n * (1.0 / n.magnitude()))
}
};
let mut fwd: Vec<Point3D> = vec![seed];
let mut bwd: Vec<Point3D> = Vec::new();
for (chain, dir) in [(&mut fwd, 1.0), (&mut bwd, -1.0)] {
let mut p = seed;
let mut h = *step;
for _ in 0..2000 {
let qa = a.project_point(p, tol);
let qb = b.project_point(p, tol);
let (Some(na), Some(nb)) = (normal_at(a, qa.u, qa.v), normal_at(b, qb.u, qb.v)) else {
break;
};
let t = na.cross(nb);
if t.magnitude() <= tol.angular {
break;
}
let t = t * (dir / t.magnitude());
let mut accepted = false;
for _ in 0..10 {
let q = correct(a, b, p + t * h, tol);
match q {
Some(q) if b.project_point(q, tol).distance <= 10.0 * tol.confusion => {
p = q;
h = (h * 1.25).min(0.1);
accepted = true;
break;
}
_ => {
h *= 0.5;
if h < tol.confusion {
break;
}
}
}
}
if !accepted {
break;
}
if chain.len() > 10 && p.distance(chain[0]) <= 10.0 * tol.confusion {
chain.push(chain[0]);
break;
}
let ra = a.project_point(p, tol);
let rb = b.project_point(p, tol);
if off_bounds(a, ra.u, ra.v) || off_bounds(b, rb.u, rb.v) {
chain.push(p);
break;
}
chain.push(p);
}
}
*step = (*step).clamp(tol.confusion, 1.0);
bwd.reverse();
let mut pts = bwd;
pts.extend(fwd.into_iter().skip(1));
if pts.len() < 4 { None } else { Some(pts) }
}
fn correct<A, B>(a: &A, b: &B, p: Point3D, tol: Tolerance) -> Option<Point3D>
where
A: ParametricSurface,
B: ParametricSurface,
{
let mut q = p;
for _ in 0..8 {
let pa = a.project_point(q, tol);
let qa = a.eval_point(pa.u, pa.v);
let pb = b.project_point(qa, tol);
let qb = b.eval_point(pb.u, pb.v);
if qb.distance(q) <= tol.confusion {
return Some(qb);
}
q = qb;
}
let residual = a.project_point(q, tol).distance + b.project_point(q, tol).distance;
if residual <= 10.0 * tol.confusion {
Some(q)
} else {
None
}
}
fn seed_window((lo, hi): (f64, f64)) -> (f64, f64) {
const WINDOW: f64 = 10.0;
(
if lo.is_finite() { lo } else { -WINDOW },
if hi.is_finite() { hi } else { WINDOW },
)
}
fn off_bounds<S: ParametricSurface + ?Sized>(s: &S, u: f64, v: f64) -> bool {
let (u0, u1) = s.u_bounds();
let (v0, v1) = s.v_bounds();
(s.u_period().is_none() && (u < u0 || u > u1)) || (s.v_period().is_none() && (v < v0 || v > v1))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Tolerance;
use crate::{Line3D, LineCircle3DIntersection, Point3D, Sphere, Vector3D};
#[test]
fn test_solve_quadratic_two_roots_ordered() {
assert_eq!(
solve_quadratic(1.0, -5.0, 6.0, Tolerance::DEFAULT),
QuadraticSolution::Two(2.0, 3.0)
);
}
#[test]
fn test_solve_quadratic_tangent_band() {
assert_eq!(
solve_quadratic(1.0, -4.0, 4.0, Tolerance::DEFAULT),
QuadraticSolution::One(2.0)
);
assert_eq!(
solve_quadratic(1.0, 0.0, 1e-14, Tolerance::DEFAULT),
QuadraticSolution::One(0.0)
);
assert_eq!(
solve_quadratic(1.0, 0.0, 1e-9, Tolerance::DEFAULT),
QuadraticSolution::Empty
);
assert_eq!(
solve_quadratic(1.0, 0.0, 1.0, Tolerance::DEFAULT),
QuadraticSolution::Empty
);
}
#[test]
fn test_solve_quadratic_degenerate_arms() {
assert_eq!(
solve_quadratic(0.0, 2.0, 4.0, Tolerance::DEFAULT),
QuadraticSolution::Linear(-2.0)
);
assert_eq!(
solve_quadratic(0.0, 0.0, 1.0, Tolerance::DEFAULT),
QuadraticSolution::Degenerate
);
}
#[test]
fn test_generic_line_sphere_matches_analytic() {
let tol = Tolerance::DEFAULT;
let line = Line3D::new(Point3D::ORIGIN, Vector3D::X).unwrap();
let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
let hits = intersect_curve_surface(&line, &sphere, tol);
assert_eq!(hits.len(), 2);
assert!((hits[0].curve_param + 2.0).abs() < 1e-9);
assert!((hits[1].curve_param - 2.0).abs() < 1e-9);
match line.intersect_sphere(&sphere, tol) {
LineQuadricIntersection::TwoPoints((t1, _), (t2, _)) => {
assert!((hits[0].curve_param - t1).abs() < 1e-9);
assert!((hits[1].curve_param - t2).abs() < 1e-9);
}
_ => panic!("analytic expected two points"),
}
}
#[test]
fn test_generic_line_sphere_miss() {
let tol = Tolerance::DEFAULT;
let line = Line3D::new(Point3D::new(0.0, 0.0, 3.0), Vector3D::X).unwrap();
let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
assert!(intersect_curve_surface(&line, &sphere, tol).is_empty());
}
#[test]
fn test_generic_segment_plane_hit() {
use crate::{BSplineCurve3D, Plane};
let tol = Tolerance::DEFAULT;
let poles = vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(2.0, 0.0, 0.0)];
let curve = BSplineCurve3D::new(1, poles, vec![0.0, 1.0], vec![2, 2], false).unwrap();
let plane = Plane::new(Point3D::new(1.0, 0.0, 0.0), Vector3D::X).unwrap();
let hits = intersect_curve_surface(&curve, &plane, tol);
assert_eq!(hits.len(), 1);
assert!((hits[0].curve_param - 0.5).abs() < 1e-9);
assert_eq!(hits[0].point, Point3D::new(1.0, 0.0, 0.0));
}
#[test]
fn test_marching_plane_plane_line() {
use crate::Plane;
let tol = Tolerance::DEFAULT;
let xy = Plane::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
let zy = Plane::new(Point3D::ORIGIN, Vector3D::X).unwrap();
let curves = marching_intersection(&xy, &zy, tol);
assert_eq!(curves.len(), 1);
for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
let p = curves[0].eval_point(t);
assert!(p.x.abs() < 1e-6, "{p:?}");
assert!(p.z.abs() < 1e-6, "{p:?}");
}
}
#[test]
fn test_marching_plane_sphere_circle() {
use crate::{Plane, Sphere};
let tol = Tolerance::DEFAULT;
let plane = Plane::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
let curves = marching_intersection(&plane, &sphere, tol);
assert_eq!(curves.len(), 1);
for t in [0.0, 0.2, 0.4, 0.6, 0.8, 1.0] {
let p = curves[0].eval_point(t);
assert!(plane.project_point(p, tol).distance < 1e-3, "{p:?}");
assert!(sphere.project_point(p, tol).distance < 1e-3, "{p:?}");
}
}
#[test]
fn test_marching_miss_is_empty() {
use crate::{Plane, Sphere};
let tol = Tolerance::DEFAULT;
let plane = Plane::new(Point3D::new(0.0, 0.0, 5.0), Vector3D::Z).unwrap();
let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
assert!(marching_intersection(&plane, &sphere, tol).is_empty());
}
#[test]
fn test_curve_curve_extrema_skew_matches_analytic() {
use crate::{Line3D, LineLine3DIntersection};
let tol = Tolerance::DEFAULT;
let l1 = Line3D::new(Point3D::ORIGIN, Vector3D::X).unwrap();
let l2 = Line3D::new(Point3D::new(0.0, 0.0, 1.0), Vector3D::Y).unwrap();
let ext = curve_curve_extrema(&l1, &l2, tol);
assert_eq!(ext.len(), 1);
assert!((ext[0].distance - 1.0).abs() < 1e-9);
match l1.intersect_line(&l2, tol) {
LineLine3DIntersection::Skew { s, t, distance } => {
assert!((ext[0].first_param - s).abs() < 1e-9);
assert!((ext[0].second_param - t).abs() < 1e-9);
assert!((ext[0].distance - distance).abs() < 1e-12);
}
_ => panic!("analytic expected skew"),
}
}
#[test]
fn test_intersect_curve_curve_line_circle() {
use crate::{Circle3D, Line3D};
let tol = Tolerance::DEFAULT;
let line = Line3D::new(Point3D::ORIGIN, Vector3D::X).unwrap();
let circle = Circle3D::new(Point3D::ORIGIN, Vector3D::Z, 2.0).unwrap();
let hits = intersect_curve_curve(&line, &circle, tol);
assert_eq!(hits.len(), 2);
assert!((hits[0].first_param + 2.0).abs() < 1e-9);
assert!((hits[1].first_param - 2.0).abs() < 1e-9);
match line.intersect_circle(&circle, tol) {
LineCircle3DIntersection::Points((t1, _), (t2, _)) => {
assert!((hits[0].first_param - t1).abs() < 1e-9);
assert!((hits[1].first_param - t2).abs() < 1e-9);
}
_ => panic!("analytic expected two points"),
}
}
}