use super::curve_surface_hull::CurveSurfaceHits;
use super::{CurveSurfaceHit, mixed};
use crate::Tolerance;
use crate::curves::ParametricCurve3D;
use crate::math::{brent_minimum, newton_3d};
use crate::surfaces::ParametricSurface;
pub fn intersect_curve_surface<C, S>(curve: &C, surface: &S, tol: Tolerance) -> CurveSurfaceHits
where
C: ParametricCurve3D,
S: ParametricSurface,
{
if let Some(mut hits) = mixed::curve_surface(curve, surface, tol) {
if !hits.is_complete() {
hits.add_sampled(sampled_curve_surface(curve, surface, tol), tol);
}
return hits;
}
CurveSurfaceHits::sampled(sampled_curve_surface(curve, surface, tol))
}
pub(super) fn sampled_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 step = (hi - lo) / (SEEDS - 1) as f64;
let samples: Vec<(f64, f64)> = (0..SEEDS)
.map(|i| {
let t = lo + step * i as f64;
(t, surface.project_point(curve.eval_point(t), tol).distance)
})
.collect();
let local_min = |i: usize| {
(i == 0 || samples[i].1 <= samples[i - 1].1)
&& (i + 1 == SEEDS || samples[i].1 <= samples[i + 1].1)
};
let mut order: Vec<usize> = (0..SEEDS).collect();
order.sort_by(|&a, &b| samples[a].1.total_cmp(&samples[b].1));
let mut hits: Vec<CurveSurfaceHit> = Vec::new();
let merge_gap = tol.confusion.sqrt();
let push = |hits: &mut Vec<CurveSurfaceHit>, hit: CurveSurfaceHit| {
if !hits
.iter()
.any(|h| h.point.distance(hit.point) <= merge_gap)
{
hits.push(hit);
}
};
for i in order.into_iter().take(KEEP) {
let t = samples[i].0;
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_d1(x[0]);
let (_, [su, sv]) = surface.eval_d1(x[1], x[2]);
[
[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);
if p.distance(surface.eval_point(ru, rv)) <= tol.confusion {
push(
&mut hits,
CurveSurfaceHit {
curve_param: rt,
surface_params: (ru, rv),
point: p,
},
);
continue;
}
}
if local_min(i) {
let distance = |t: f64| {
let p = curve.eval_point(t);
let proj = surface.project_point(p, tol);
p.distance(surface.eval_point(proj.u, proj.v))
};
let (a, b) = ((t - step).max(lo), (t + step).min(hi));
let rt = brent_minimum(distance, a, b, 1e-13);
let p = curve.eval_point(rt);
let proj = surface.project_point(p, tol);
if proj.distance <= tol.confusion {
push(
&mut hits,
CurveSurfaceHit {
curve_param: rt,
surface_params: (proj.u, proj.v),
point: p,
},
);
}
}
}
hits.sort_by(|a, b| a.curve_param.partial_cmp(&b.curve_param).unwrap());
hits
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Intersect, Line3D, LineQuadricIntersection, Point3D, Sphere, Vector3D};
#[test]
fn test_generic_line_sphere_matches_exact() {
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, tol) {
LineQuadricIntersection::TwoPoints((t1, _), (t2, _)) => {
assert!((hits[0].curve_param - t1).abs() < 1e-9);
assert!((hits[1].curve_param - t2).abs() < 1e-9);
}
_ => panic!("exact intersection 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_curve_surface_hits_at_singular_points() {
use crate::{BSplineCurve3D, Cone};
let tol = Tolerance::DEFAULT;
let on_both = |hit: &CurveSurfaceHit, surface: &dyn ParametricSurface| {
surface.contains(hit.point, tol)
&& surface
.eval_point(hit.surface_params.0, hit.surface_params.1)
.distance(hit.point)
<= 1e-6
};
let sphere = Sphere::new(Point3D::ORIGIN, 1.0).unwrap();
let line = Line3D::new(Point3D::new(0.0, 0.0, -3.0), Vector3D::Z).unwrap();
let hits = intersect_curve_surface(&line, &sphere, tol);
assert_eq!(hits.len(), 2, "{hits:?}");
assert!((hits[0].curve_param - 2.0).abs() < 1e-9, "{hits:?}");
assert!((hits[1].curve_param - 4.0).abs() < 1e-9, "{hits:?}");
assert!(hits.iter().all(|h| on_both(h, &sphere)));
let cone = Cone::new(Point3D::new(0.0, 0.0, -1.0), Vector3D::Z, 0.5, 0.5).unwrap();
let apex = cone.apex();
let line = Line3D::new(apex - Vector3D::X * 3.0, Vector3D::X).unwrap();
let hits = intersect_curve_surface(&line, &cone, tol);
assert_eq!(hits.len(), 1, "{hits:?}");
assert!(hits[0].point.distance(apex) < 1e-6, "{hits:?}");
assert!(on_both(&hits[0], &cone));
let poles = vec![
Point3D::new(-1.0, 0.0, 3.0),
Point3D::new(0.0, 0.0, 0.0),
Point3D::new(1.0, 0.0, 1.0),
];
let curve = BSplineCurve3D::new(2, poles, vec![0.0, 1.0], vec![3, 3], false).unwrap();
assert_eq!(curve.eval_point(0.5), Point3D::new(0.0, 0.0, 1.0));
let hits = intersect_curve_surface(&curve, &sphere, tol);
assert_eq!(hits.len(), 2, "{hits:?}");
assert!((hits[0].curve_param - 0.5).abs() < 1e-9, "{hits:?}");
assert!(hits.iter().all(|h| on_both(h, &sphere)));
}
#[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));
}
}