use super::{pieces, surface_surface};
use crate::curves::{BSplineCurve3D, PointParametrization};
use crate::math::boxtree::{Aabb, boxes_apart};
use crate::math::{solve_2x2, solve_3x3, solve_4x4};
use crate::surfaces::ParametricSurface;
use crate::{Point3D, Tolerance, Vector3D};
pub fn marching_intersection<A, B>(a: &A, b: &B, tol: Tolerance) -> Vec<BSplineCurve3D>
where
A: ParametricSurface,
B: ParametricSurface,
{
let mut traced = Traced::default();
if let (Some(sa), Some(sb)) = (a.as_bspline(), b.as_bspline()) {
let pairs = surface_surface::patch_pairs(sa, sb, tol);
for leaf in &pairs.leaves {
if leaf.open && traced.crosses(&leaf.overlap, tol) {
continue;
}
let [ra, rb] = leaf.rects;
let centres = [
0.5 * (ra[0] + ra[1]),
0.5 * (ra[2] + ra[3]),
0.5 * (rb[0] + rb[1]),
0.5 * (rb[2] + rb[3]),
];
if let Some(seed) = meet(a, b, centres, None, tol) {
traced.follow(a, b, seed, tol);
}
}
if pairs.complete {
return traced.curves;
}
}
trace_grid_seeds(a, b, tol, &mut traced);
traced.curves
}
#[cfg(test)]
pub(super) fn grid_marching<A, B>(a: &A, b: &B, tol: Tolerance) -> Vec<BSplineCurve3D>
where
A: ParametricSurface,
B: ParametricSurface,
{
let mut traced = Traced::default();
trace_grid_seeds(a, b, tol, &mut traced);
traced.curves
}
fn trace_grid_seeds<A, B>(a: &A, b: &B, tol: Tolerance, traced: &mut Traced)
where
A: ParametricSurface,
B: ParametricSurface,
{
let mut seeds = grid_seeds(a, b, tol);
seeds.extend(
grid_seeds(b, a, tol)
.into_iter()
.map(|x| [x[2], x[3], x[0], x[1]]),
);
for seed in seeds {
traced.follow(a, b, seed, tol);
}
}
#[derive(Default)]
struct Traced {
curves: Vec<BSplineCurve3D>,
traces: Vec<(Aabb, Vec<Aabb>, Vec<Point3D>)>,
}
impl Traced {
fn follow<A, B>(&mut self, a: &A, b: &B, seed: [f64; 4], tol: Tolerance)
where
A: ParametricSurface,
B: ParametricSurface,
{
let p = a.eval_point(seed[0], seed[1]);
if self.crosses(&[p.x, p.y, p.z, p.x, p.y, p.z], tol) {
return;
}
if let Some(points) = trace(a, b, seed, tol) {
let degree = 3.min(points.len() - 1);
if let Ok(fit) =
BSplineCurve3D::interpolate(&points, degree, PointParametrization::Centripetal)
{
let coords: Vec<[f64; 3]> = points.iter().map(|p| [p.x, p.y, p.z]).collect();
let runs = (1..coords.len())
.step_by(RUN)
.map(|k| pieces::bounding_box(&coords[k - 1..(k + RUN).min(coords.len())]))
.collect();
self.traces
.push((pieces::bounding_box(&coords), runs, points));
self.curves.push(fit);
}
}
}
fn crosses(&self, b: &Aabb, tol: Tolerance) -> bool {
let gap = 8.0 * CHORD_DEVIATION * tol.confusion;
let grown: Aabb = std::array::from_fn(|c| if c < 3 { b[c] - gap } else { b[c] + gap });
self.traces.iter().any(|(bounds, runs, points)| {
!boxes_apart(bounds, &grown, 0.0)
&& runs.iter().enumerate().any(|(r, run)| {
let first = r * RUN;
!boxes_apart(run, &grown, 0.0)
&& points[first..(first + RUN + 1).min(points.len())]
.windows(2)
.any(|w| segment_meets_box(w[0], w[1], &grown))
})
})
}
}
const RUN: usize = 16;
fn segment_meets_box(p: Point3D, q: Point3D, b: &Aabb) -> bool {
let (from, d) = ([p.x, p.y, p.z], [q.x - p.x, q.y - p.y, q.z - p.z]);
let (mut lo, mut hi) = (0.0f64, 1.0f64);
for c in 0..3 {
if d[c] == 0.0 {
if from[c] < b[c] || from[c] > b[c + 3] {
return false;
}
continue;
}
let (t0, t1) = ((b[c] - from[c]) / d[c], (b[c + 3] - from[c]) / d[c]);
lo = lo.max(t0.min(t1));
hi = hi.min(t0.max(t1));
if lo > hi {
return false;
}
}
true
}
fn grid_seeds<S, T>(s: &S, other: &T, tol: Tolerance) -> Vec<[f64; 4]>
where
S: ParametricSurface,
T: ParametricSurface,
{
const N: usize = 24;
let (u0, u1) = seed_window(s.u_bounds());
let (v0, v1) = seed_window(s.v_bounds());
let mut samples: Vec<([f64; 4], f64)> = Vec::with_capacity((N + 1) * (N + 1));
for j in 0..=N {
for i in 0..=N {
let (u, v) = (
u0 + (u1 - u0) * i as f64 / N as f64,
v0 + (v1 - v0) * j as f64 / N as f64,
);
let q = other.project_point(s.eval_point(u, v), tol);
samples.push(([u, v, q.u, q.v], q.distance));
}
}
let distance = |i: usize, j: usize| samples[j * (N + 1) + i].1;
let mut seeds = Vec::new();
for j in 0..=N {
for i in 0..=N {
let d = distance(i, j);
let neighbours = [
(i > 0).then(|| distance(i - 1, j)),
(i < N).then(|| distance(i + 1, j)),
(j > 0).then(|| distance(i, j - 1)),
(j < N).then(|| distance(i, j + 1)),
];
if neighbours.into_iter().flatten().all(|e| d <= e)
&& let Some(x) = meet(s, other, samples[j * (N + 1) + i].0, None, tol)
{
seeds.push(x);
}
}
}
seeds
}
const CHORD_DEVIATION: f64 = 1000.0;
fn trace<A, B>(a: &A, b: &B, seed: [f64; 4], tol: Tolerance) -> Option<Vec<Point3D>>
where
A: ParametricSurface,
B: ParametricSurface,
{
let chord_deviation = CHORD_DEVIATION * tol.confusion;
const H_MAX: f64 = 0.1;
const FREE_TURN: f64 = 0.05;
const CURVATURE_GROWTH: f64 = 4.0;
let start = a.eval_point(seed[0], seed[1]);
let t0 = tangent(a, b, seed, tol)?;
let mut halves: [Vec<Point3D>; 2] = [Vec::new(), Vec::new()];
let mut closed = false;
for (half, dir) in halves.iter_mut().zip([1.0, -1.0]) {
if closed {
break;
}
let (mut x, mut p, mut t) = (seed, start, t0 * dir);
let mut h = (8.0 * chord_deviation).sqrt().min(H_MAX);
let mut last_curvature = 0.0;
for _ in 0..2000 {
let mut step = None;
let mut edge = None;
while h >= tol.confusion {
let target = p + t * h;
let guess = predict(a, b, x, t * h);
if let Some((pin, bound)) = leaving(a, b, x, guess) {
if let Some(y) = land(a, b, guess, pin, bound, tol) {
let q = a.eval_point(y[0], y[1]);
if (q - p).dot(t) >= 0.0 && q.distance(p) <= 2.0 * h {
edge = Some(q);
break;
}
}
} else if let Some(y) = meet(a, b, guess, Some((target, t)), tol)
&& let Some(ty) = tangent(a, b, y, tol).map(|ty| ty * dir)
{
let q = a.eval_point(y[0], y[1]);
let chord = q.distance(p);
let turn = t.cross(ty).magnitude().atan2(t.dot(ty));
let smooth = turn <= FREE_TURN.max(CURVATURE_GROWTH * last_curvature * chord);
if ty.dot(t) > 0.0
&& chord <= 2.0 * h
&& chord * turn <= 32.0 * chord_deviation
&& smooth
{
step = Some((y, q, ty, turn / chord.max(f64::MIN_POSITIVE)));
break;
}
}
h *= 0.5;
}
if let Some(q) = edge {
if q.distance(p) > tol.confusion.max(crate::tol::CONFUSION) {
half.push(q);
}
break;
}
let Some((y, q, ty, curvature)) = step else {
break;
};
let along = (start - p).dot(q - p) / (q - p).dot(q - p).max(f64::MIN_POSITIVE);
if half.len() >= 3
&& (0.0..=1.0).contains(&along)
&& (p + (q - p) * along).distance(start)
<= 4.0 * chord_deviation + 10.0 * tol.confusion
{
if p.distance(start) < 0.1 * h {
half.pop();
}
half.push(start);
closed = true;
break;
}
half.push(q);
(x, p, t) = (y, q, ty);
last_curvature = curvature;
h = (2.0 * h)
.min(H_MAX)
.min((8.0 * chord_deviation / curvature.max(f64::MIN_POSITIVE)).sqrt());
}
}
let [forward, backward] = halves;
let mut points: Vec<Point3D> = backward.into_iter().rev().collect();
points.push(start);
points.extend(forward);
(points.len() >= 4).then_some(points)
}
fn tangent<A, B>(a: &A, b: &B, x: [f64; 4], tol: Tolerance) -> Option<Vector3D>
where
A: ParametricSurface,
B: ParametricSurface,
{
let (_, [au, av]) = a.eval_d1(x[0], x[1]);
let (_, [bu, bv]) = b.eval_d1(x[2], x[3]);
let (na, nb) = (au.cross(av), bu.cross(bv));
let t = na.cross(nb);
let scale = na.magnitude() * nb.magnitude();
if t.magnitude() <= tol.angular * scale.max(f64::MIN_POSITIVE) {
None
} else {
Some(t * (1.0 / t.magnitude()))
}
}
fn predict<A, B>(a: &A, b: &B, x: [f64; 4], d: Vector3D) -> [f64; 4]
where
A: ParametricSurface,
B: ParametricSurface,
{
let shift = |s: &dyn ParametricSurface, u: f64, v: f64| {
let (_, [su, sv]) = s.eval_d1(u, v);
let m = [[su.dot(su), su.dot(sv)], [su.dot(sv), sv.dot(sv)]];
solve_2x2(m, [su.dot(d), sv.dot(d)], 1e-12).unwrap_or([0.0, 0.0])
};
let [du1, dv1] = shift(a, x[0], x[1]);
let [du2, dv2] = shift(b, x[2], x[3]);
[x[0] + du1, x[1] + dv1, x[2] + du2, x[3] + dv2]
}
fn meet<A, B>(
a: &A,
b: &B,
x: [f64; 4],
plane: Option<(Point3D, Vector3D)>,
tol: Tolerance,
) -> Option<[f64; 4]>
where
A: ParametricSurface,
B: ParametricSurface,
{
let bounded = |x: [f64; 4]| within_bounds(a, b, x);
let residual = |x: [f64; 4]| {
let pa = a.eval_point(x[0], x[1]);
let f = pa - b.eval_point(x[2], x[3]);
let r = plane.map_or(0.0, |(origin, normal)| (pa - origin).dot(normal));
(f, r, f.dot(f) + r * r)
};
let mut x = bounded(x);
let (mut f, mut r, mut merit) = residual(x);
for _ in 0..32 {
if f.magnitude() <= 0.1 * tol.confusion && r.abs() <= 0.1 * tol.confusion {
return Some(x);
}
let (_, [au, av]) = a.eval_d1(x[0], x[1]);
let (_, [bu, bv]) = b.eval_d1(x[2], x[3]);
let cols = [au, av, -bu, -bv];
let delta = match plane {
Some((_, normal)) => {
let m = [
cols.map(|c| c.x),
cols.map(|c| c.y),
cols.map(|c| c.z),
[cols[0].dot(normal), cols[1].dot(normal), 0.0, 0.0],
];
solve_4x4(m, [-f.x, -f.y, -f.z, -r], 1e-12)?
}
None => {
let rows = [cols.map(|c| c.x), cols.map(|c| c.y), cols.map(|c| c.z)];
let dot = |p: [f64; 4], q: [f64; 4]| (0..4).map(|k| p[k] * q[k]).sum::<f64>();
let m = rows.map(|p| rows.map(|q| dot(p, q)));
let y = solve_3x3(m, [-f.x, -f.y, -f.z], 1e-12)?;
cols.map(|c| c.dot(Vector3D::new(y[0], y[1], y[2])))
}
};
let mut scale = 1.0;
let mut improved = false;
for _ in 0..12 {
let trial = bounded([0, 1, 2, 3].map(|k| x[k] + scale * delta[k]));
let (tf, tr, tm) = residual(trial);
if tm < merit {
(x, f, r, merit) = (trial, tf, tr, tm);
improved = true;
break;
}
scale *= 0.5;
}
if !improved {
break;
}
}
(f.magnitude() <= tol.confusion && r.abs() <= tol.confusion).then_some(x)
}
fn param_bounds<A, B>(a: &A, b: &B, k: usize) -> Option<(f64, f64)>
where
A: ParametricSurface,
B: ParametricSurface,
{
let s: &dyn ParametricSurface = if k < 2 { a } else { b };
if k.is_multiple_of(2) {
s.u_period().is_none().then(|| s.u_bounds())
} else {
s.v_period().is_none().then(|| s.v_bounds())
}
}
fn within_bounds<A, B>(a: &A, b: &B, x: [f64; 4]) -> [f64; 4]
where
A: ParametricSurface,
B: ParametricSurface,
{
std::array::from_fn(|k| param_bounds(a, b, k).map_or(x[k], |(lo, hi)| x[k].clamp(lo, hi)))
}
fn leaving<A, B>(a: &A, b: &B, x: [f64; 4], y: [f64; 4]) -> Option<(usize, f64)>
where
A: ParametricSurface,
B: ParametricSurface,
{
let mut first: Option<(usize, f64, f64)> = None;
for k in 0..4 {
let Some((lo, hi)) = param_bounds(a, b, k) else {
continue;
};
let bound = if y[k] > hi {
hi
} else if y[k] < lo {
lo
} else {
continue;
};
let fraction = (bound - x[k]) / (y[k] - x[k]);
if first.is_none_or(|(_, _, f)| fraction < f) {
first = Some((k, bound, fraction));
}
}
first.map(|(k, bound, _)| (k, bound))
}
fn land<A, B>(a: &A, b: &B, x: [f64; 4], pin: usize, value: f64, tol: Tolerance) -> Option<[f64; 4]>
where
A: ParametricSurface,
B: ParametricSurface,
{
let free: [usize; 3] = std::array::from_fn(|i| if i < pin { i } else { i + 1 });
let residual = |x: [f64; 4]| a.eval_point(x[0], x[1]) - b.eval_point(x[2], x[3]);
let mut x = within_bounds(a, b, x);
x[pin] = value;
let mut f = residual(x);
for _ in 0..32 {
if f.magnitude() <= 0.1 * tol.confusion {
return Some(x);
}
let (_, [au, av]) = a.eval_d1(x[0], x[1]);
let (_, [bu, bv]) = b.eval_d1(x[2], x[3]);
let cols = [au, av, -bu, -bv].map(|c| [c.x, c.y, c.z]);
let m = std::array::from_fn(|r| free.map(|k| cols[k][r]));
let delta = solve_3x3(m, [-f.x, -f.y, -f.z], 1e-12)?;
let mut scale = 1.0;
let mut improved = false;
for _ in 0..12 {
let mut trial = x;
for (i, &k) in free.iter().enumerate() {
trial[k] += scale * delta[i];
}
let trial = within_bounds(a, b, trial);
let tf = residual(trial);
if tf.square_magnitude() < f.square_magnitude() {
(x, f) = (trial, tf);
improved = true;
break;
}
scale *= 0.5;
}
if !improved {
break;
}
}
(f.magnitude() <= tol.confusion).then_some(x)
}
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 },
)
}
#[cfg(test)]
mod tests {
use super::*;
#[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_loop_is_traced_once() {
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);
let curve = assert_one_curve_on_both(&curves, &plane, &sphere);
let (t0, t1) = curve.bounds();
let n = 2000;
let length: f64 = (0..n)
.map(|k| {
let at = |k: usize| curve.eval_point(t0 + (t1 - t0) * k as f64 / n as f64);
at(k).distance(at(k + 1))
})
.sum();
let circumference = 4.0 * std::f64::consts::PI;
assert!(
(length - circumference).abs() < 1e-3 * circumference,
"{length}"
);
}
fn assert_one_curve_on_both<A, B>(curves: &[BSplineCurve3D], a: &A, b: &B) -> BSplineCurve3D
where
A: ParametricSurface,
B: ParametricSurface,
{
let tol = Tolerance::DEFAULT;
assert_eq!(curves.len(), 1);
let curve = curves[0].clone();
let (t0, t1) = curve.bounds();
for k in 0..=40 {
let p = curve.eval_point(t0 + (t1 - t0) * k as f64 / 40.0);
assert!(a.project_point(p, tol).distance < 1e-3, "{p:?}");
assert!(b.project_point(p, tol).distance < 1e-3, "{p:?}");
}
curve
}
#[test]
fn test_marching_plane_sphere_off_grid_heights() {
use crate::{Plane, Sphere};
let tol = Tolerance::DEFAULT;
let sphere = Sphere::new(Point3D::ORIGIN, 1.0).unwrap();
for h in [0.3, 0.8, -0.45, 0.123] {
let plane = Plane::new(Point3D::new(0.0, 0.0, h), Vector3D::Z).unwrap();
let curves = marching_intersection(&plane, &sphere, tol);
let curve = assert_one_curve_on_both(&curves, &plane, &sphere);
let r = (1.0 - h * h).sqrt();
for t in [0.0, 0.3, 0.6, 0.9] {
let p = curve.eval_point(t);
assert!((p.x.hypot(p.y) - r).abs() < 1e-3, "h = {h}: {p:?}");
}
}
}
#[test]
fn test_marching_cylinder_offset_sphere() {
use crate::{Cylinder, Sphere};
let tol = Tolerance::DEFAULT;
let cylinder = Cylinder::new(Point3D::ORIGIN, Vector3D::Z, 1.0).unwrap();
let sphere = Sphere::new(Point3D::new(0.8, 0.0, 0.0), 1.78).unwrap();
let curves = marching_intersection(&cylinder, &sphere, tol);
assert_one_curve_on_both(&curves, &cylinder, &sphere);
}
#[test]
fn test_marching_shallow_angle_spheres() {
use crate::Sphere;
let tol = Tolerance::DEFAULT;
let a = Sphere::new(Point3D::ORIGIN, 1.0).unwrap();
for d in [0.05, 0.0175] {
let b = Sphere::new(Point3D::new(0.0, 0.0, d), 1.0).unwrap();
let curves = marching_intersection(&a, &b, tol);
let curve = assert_one_curve_on_both(&curves, &a, &b);
let r = (1.0 - d * d / 4.0).sqrt();
for t in [0.0, 0.3, 0.6, 0.9] {
let p = curve.eval_point(t);
assert!((p.z - d / 2.0).abs() < 1e-3, "d = {d}: {p:?}");
assert!((p.x.hypot(p.y) - r).abs() < 1e-3, "d = {d}: {p:?}");
}
}
}
#[test]
fn test_marching_small_loop() {
use crate::{Plane, Sphere};
let tol = Tolerance::DEFAULT;
let plane = Plane::new(Point3D::new(0.0, 0.0, 0.003), Vector3D::Z).unwrap();
let sphere = Sphere::new(Point3D::ORIGIN, 0.01).unwrap();
let curves = marching_intersection(&plane, &sphere, tol);
let curve = assert_one_curve_on_both(&curves, &plane, &sphere);
let r = (1e-4f64 - 9e-6).sqrt();
for t in [0.0, 0.3, 0.6, 0.9] {
let p = curve.eval_point(t);
assert!((p.x.hypot(p.y) - r).abs() < 1e-4, "{p:?}");
}
}
#[test]
fn test_marching_open_trace_is_not_closed() {
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);
let (t0, t1) = curves[0].bounds();
let gap = curves[0].eval_point(t0).distance(curves[0].eval_point(t1));
assert!(gap > 10.0, "open line fitted as a loop: end gap {gap}");
}
fn traced_length(curve: &BSplineCurve3D) -> f64 {
let (t0, t1) = curve.bounds();
let n = 4000;
let at = |k: usize| curve.eval_point(t0 + (t1 - t0) * k as f64 / n as f64);
(0..n).map(|k| at(k).distance(at(k + 1))).sum()
}
fn villarceau_pair(d: f64) -> (crate::Torus, crate::Plane) {
let torus = crate::Torus::new(Point3D::ORIGIN, Vector3D::Z, 3.0, 1.0).unwrap();
let a = (1.0f64 / 3.0).asin() + d;
let plane =
crate::Plane::new(Point3D::ORIGIN, Vector3D::new(-a.sin(), 0.0, a.cos())).unwrap();
(torus, plane)
}
#[test]
fn test_marching_stops_at_a_self_crossing() {
let tol = Tolerance::DEFAULT;
let (torus, plane) = villarceau_pair(0.0);
let curves = marching_intersection(&torus, &plane, tol);
assert!(!curves.is_empty());
for curve in &curves {
let (t0, t1) = curve.bounds();
for k in 0..=40 {
let p = curve.eval_point(t0 + (t1 - t0) * k as f64 / 40.0);
assert!(torus.project_point(p, tol).distance < 1e-3, "{p:?}");
assert!(plane.project_point(p, tol).distance < 1e-3, "{p:?}");
}
}
let total: f64 = curves.iter().map(traced_length).sum();
let expected = 4.0 * std::f64::consts::PI * 3.0;
assert!(
(total - expected).abs() < 1e-3 * expected,
"total length {total}, expected {expected}: {:?}",
curves.iter().map(traced_length).collect::<Vec<_>>()
);
}
#[test]
fn test_marching_near_the_villarceau_tilt_gives_two_loops() {
let tol = Tolerance::DEFAULT;
for d in [-1e-3, 1e-3] {
let (torus, plane) = villarceau_pair(d);
let curves = marching_intersection(&torus, &plane, tol);
assert_eq!(curves.len(), 2, "d = {d}");
for curve in &curves {
let (t0, t1) = curve.bounds();
let gap = curve.eval_point(t0).distance(curve.eval_point(t1));
assert!(gap < 1e-6, "d = {d}: open curve, end gap {gap}");
}
}
}
#[test]
fn test_segment_meets_box() {
let unit = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let meets = |p: [f64; 3], q: [f64; 3]| {
segment_meets_box(
Point3D::new(p[0], p[1], p[2]),
Point3D::new(q[0], q[1], q[2]),
&unit,
)
};
assert!(meets([-1.0, 0.5, 0.5], [2.0, 0.5, 0.5]));
assert!(meets([0.2, 0.3, 0.4], [0.2, 0.3, 0.4]));
assert!(meets([-1.0, -1.0, 0.5], [0.0, 0.0, 0.5]));
assert!(!meets([-1.0, 0.5, 0.5], [-0.1, 0.5, 0.5]));
assert!(!meets([-1.0, 0.0, 0.5], [0.0, -1.0, 0.5]));
assert!(!meets([0.5, 0.5, 2.0], [0.5, 0.6, 2.0]));
}
#[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());
}
}