use super::predicates::orient3d;
use super::{ImplicitPoint, Lpi, Sign};
#[inline]
fn e(p: [f64; 3]) -> ImplicitPoint {
ImplicitPoint::Explicit(p)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PlaneCross {
Disjoint,
Coplanar,
Crosses { apex: usize },
Touches,
}
pub fn classify_vs_plane(tri: &[[f64; 3]; 3], plane: &[[f64; 3]; 3]) -> PlaneCross {
let s = [
orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[0])),
orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[1])),
orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[2])),
];
let zeros = s.iter().filter(|&&x| x == Sign::Zero).count();
if zeros == 3 {
return PlaneCross::Coplanar;
}
if zeros > 0 {
return PlaneCross::Touches;
}
let pos = s.iter().filter(|&&x| x == Sign::Positive).count();
if pos == 0 || pos == 3 {
return PlaneCross::Disjoint;
}
let apex = if s[0] != s[1] && s[0] != s[2] {
0
} else if s[1] != s[0] && s[1] != s[2] {
1
} else {
2
};
PlaneCross::Crosses { apex }
}
#[inline]
pub fn edge_plane_lpi(a: [f64; 3], b: [f64; 3], plane: &[[f64; 3]; 3]) -> Lpi {
Lpi { p: a, q: b, r: plane[0], s: plane[1], t: plane[2] }
}
pub fn crossing_lpis(tri: &[[f64; 3]; 3], apex: usize, plane: &[[f64; 3]; 3]) -> [Lpi; 2] {
let o1 = (apex + 1) % 3;
let o2 = (apex + 2) % 3;
[
edge_plane_lpi(tri[apex], tri[o1], plane),
edge_plane_lpi(tri[apex], tri[o2], plane),
]
}
pub fn planes_mutually_cross(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> bool {
matches!(classify_vs_plane(t1, t2), PlaneCross::Crosses { .. })
&& matches!(classify_vs_plane(t2, t1), PlaneCross::Crosses { .. })
}
#[inline]
fn sub_f64(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn cross_f64(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
#[inline]
fn plane_normal(t: &[[f64; 3]; 3]) -> [f64; 3] {
cross_f64(sub_f64(t[1], t[0]), sub_f64(t[2], t[0]))
}
fn line_direction(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> [f64; 3] {
let n = cross_f64(plane_normal(t1), plane_normal(t2));
let m = n[0].abs().max(n[1].abs()).max(n[2].abs());
if m == 0.0 || !m.is_finite() {
return n;
}
let s = 1_048_576.0 / m; [(n[0] * s).round(), (n[1] * s).round(), (n[2] * s).round()]
}
#[derive(Clone, Debug)]
pub enum TriTri {
None,
Coplanar,
Point(ImplicitPoint),
Segment([ImplicitPoint; 2]),
}
#[inline]
fn cmp_along(a: &ImplicitPoint, b: &ImplicitPoint, u: [f64; 3]) -> Sign {
super::fixed::cmp_along(a, b, u).unwrap_or_else(|| super::rational::cmp_along(a, b, u))
}
enum PlaneInterval {
None,
Coplanar,
Point(ImplicitPoint),
Chord([ImplicitPoint; 2]),
}
fn plane_interval(tri: &[[f64; 3]; 3], plane: &[[f64; 3]; 3]) -> PlaneInterval {
let s = [
orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[0])),
orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[1])),
orient3d(&e(plane[0]), &e(plane[1]), &e(plane[2]), &e(tri[2])),
];
let zeros: Vec<usize> = (0..3).filter(|&i| s[i] == Sign::Zero).collect();
match zeros.len() {
3 => PlaneInterval::Coplanar,
2 => PlaneInterval::Chord([e(tri[zeros[0]]), e(tri[zeros[1]])]), 1 => {
let vz = zeros[0];
let (o1, o2) = ((vz + 1) % 3, (vz + 2) % 3);
if s[o1] == s[o2] {
PlaneInterval::Point(e(tri[vz])) } else {
PlaneInterval::Chord([e(tri[vz]), ImplicitPoint::Lpi(edge_plane_lpi(tri[o1], tri[o2], plane))])
}
}
_ => {
let pos = s.iter().filter(|&&x| x == Sign::Positive).count();
if pos == 0 || pos == 3 {
PlaneInterval::None
} else {
let apex = if s[0] != s[1] && s[0] != s[2] {
0
} else if s[1] != s[0] && s[1] != s[2] {
1
} else {
2
};
let [a, b] = crossing_lpis(tri, apex, plane);
PlaneInterval::Chord([ImplicitPoint::Lpi(a), ImplicitPoint::Lpi(b)])
}
}
}
}
use super::mesh_bridge::SNAP_GRID;
#[inline]
fn ti_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn ti_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
#[inline]
fn ti_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn ti_normal(t: &[[f64; 3]; 3]) -> [f64; 3] {
ti_cross(ti_sub(t[1], t[0]), ti_sub(t[2], t[0]))
}
fn near_coplanar(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> bool {
let (n1, n2) = (ti_normal(t1), ti_normal(t2));
let (nn1, nn2) = (ti_dot(n1, n1), ti_dot(n2, n2));
if nn1 <= 0.0 || nn2 <= 0.0 || !nn1.is_finite() || !nn2.is_finite() {
return false; }
let mut extent = 1.0f64;
for p in t1.iter().chain(t2.iter()) {
for &c in p {
extent = extent.max(c.abs());
}
}
let band = (8.0 * SNAP_GRID).max(extent * (1.0 / 4_194_304.0)); let band2 = band * band;
let in_slab = |t: &[[f64; 3]; 3], plane: &[[f64; 3]; 3], n: [f64; 3], nn: f64| {
t.iter().all(|&v| {
let d = ti_dot(ti_sub(v, plane[0]), n); (d * d) / nn <= band2
})
};
in_slab(t2, t1, n1, nn1) || in_slab(t1, t2, n2, nn2)
}
pub fn tri_tri_intersection(t1: &[[f64; 3]; 3], t2: &[[f64; 3]; 3]) -> TriTri {
use PlaneInterval as PI;
if near_coplanar(t1, t2) {
return TriTri::Coplanar;
}
let (i1, i2) = (plane_interval(t1, t2), plane_interval(t2, t1));
let ends = |pi: &PI| -> Option<[ImplicitPoint; 2]> {
match pi {
PI::Point(p) => Some([p.clone(), p.clone()]),
PI::Chord([a, b]) => Some([a.clone(), b.clone()]),
_ => None,
}
};
if matches!(i1, PI::Coplanar) || matches!(i2, PI::Coplanar) {
return TriTri::Coplanar;
}
let ([a1, b1], [a2, b2]) = match (ends(&i1), ends(&i2)) {
(Some(s1), Some(s2)) => (s1, s2),
_ => return TriTri::None,
};
let u = line_direction(t1, t2);
let order = |a: ImplicitPoint, b: ImplicitPoint| {
if cmp_along(&a, &b, u) == Sign::Positive {
(b, a)
} else {
(a, b)
}
};
let (lo1, hi1) = order(a1, b1);
let (lo2, hi2) = order(a2, b2);
let lo = if cmp_along(&lo1, &lo2, u) == Sign::Positive { lo1 } else { lo2 };
let hi = if cmp_along(&hi1, &hi2, u) == Sign::Negative { hi1 } else { hi2 };
match cmp_along(&lo, &hi, u) {
Sign::Positive => TriTri::None, Sign::Zero => TriTri::Point(lo), Sign::Negative => TriTri::Segment([lo, hi]),
}
}
#[cfg(test)]
mod tests {
use super::*;
const ZPLANE: [[f64; 3]; 3] = [[0., 0., 0.], [2., 0., 0.], [0., 2., 0.]];
#[test]
fn classify_above_below_crossing_coplanar() {
let above = [[0., 0., 1.], [1., 0., 1.], [0., 1., 1.]];
assert_eq!(classify_vs_plane(&above, &ZPLANE), PlaneCross::Disjoint);
let below = [[0., 0., -1.], [1., 0., -2.], [0., 1., -0.5]];
assert_eq!(classify_vs_plane(&below, &ZPLANE), PlaneCross::Disjoint);
let crossing = [[0.3, 0.3, -1.], [0.3, 0.3, 1.], [1., 1., 1.]];
assert!(matches!(classify_vs_plane(&crossing, &ZPLANE), PlaneCross::Crosses { .. }));
let coplanar = [[0.1, 0.1, 0.], [1., 0., 0.], [0., 1., 0.]];
assert_eq!(classify_vs_plane(&coplanar, &ZPLANE), PlaneCross::Coplanar);
}
#[test]
fn apex_is_the_odd_vertex_out() {
let t = [[0.5, 0.5, -1.], [0., 0., 1.], [1., 0., 1.]];
assert_eq!(classify_vs_plane(&t, &ZPLANE), PlaneCross::Crosses { apex: 0 });
let t = [[0., 0., 1.], [0.5, 0.5, -1.], [1., 0., 1.]];
assert_eq!(classify_vs_plane(&t, &ZPLANE), PlaneCross::Crosses { apex: 1 });
}
#[test]
fn edge_crossing_lpi_lies_exactly_on_the_plane() {
let lpi = edge_plane_lpi([0.5, 0.5, -1.], [0.5, 0.5, 3.], &ZPLANE);
assert_eq!(
orient3d(&ImplicitPoint::Lpi(lpi), &e(ZPLANE[0]), &e(ZPLANE[1]), &e(ZPLANE[2])),
Sign::Zero,
"edge∩plane LPI is not exactly on the plane"
);
let tilted = [[0., 0., 1.], [3., 0., 2.], [0., 3., 2.]];
let lpi2 = edge_plane_lpi([1., 1., 0.], [1.5, 0.5, 5.], &tilted);
assert_eq!(
orient3d(&ImplicitPoint::Lpi(lpi2), &e(tilted[0]), &e(tilted[1]), &e(tilted[2])),
Sign::Zero,
"tilted edge∩plane LPI is not exactly on the plane"
);
}
#[test]
fn crossing_lpis_both_on_plane_and_planes_mutually_cross() {
let t1 = [[-2., 0., -1.], [2., 0., -1.], [0., 0., 2.]]; let t2 = [[1., -2., 1.], [1., 2., 1.], [1., 0.5, -3.]]; assert!(planes_mutually_cross(&t1, &t2));
if let PlaneCross::Crosses { apex } = classify_vs_plane(&t1, &t2) {
for lpi in crossing_lpis(&t1, apex, &t2) {
assert_eq!(
orient3d(&ImplicitPoint::Lpi(lpi), &e(t2[0]), &e(t2[1]), &e(t2[2])),
Sign::Zero
);
}
} else {
panic!("expected t1 to cross t2's plane");
}
}
#[test]
fn proper_crossing_yields_segment_on_both_planes() {
let t1 = [[-2., 0., -1.], [2., 0., -1.], [0., 0., 2.]]; let t2 = [[1., -2., 1.], [1., 2., 1.], [1., 0.5, -3.]]; match tri_tri_intersection(&t1, &t2) {
TriTri::Segment([a, b]) => {
assert_ne!(
super::cmp_along(&a, &b, super::line_direction(&t1, &t2)),
Sign::Zero,
"segment collapsed to a point"
);
for ep in [&a, &b] {
assert_eq!(
orient3d(ep, &e(t1[0]), &e(t1[1]), &e(t1[2])),
Sign::Zero,
"segment endpoint off t1's plane"
);
assert_eq!(
orient3d(ep, &e(t2[0]), &e(t2[1]), &e(t2[2])),
Sign::Zero,
"segment endpoint off t2's plane"
);
}
}
other => panic!("expected a segment, got {other:?}"),
}
}
#[test]
fn touches_vertex_on_plane_yields_segment_with_explicit_endpoint() {
let t1 = [[-2., 0., -1.], [2., 0., -1.], [0., 0., 2.]]; let t2 = [[0., 0., 0.5], [0.5, -1., 0.5], [0.5, 1., 0.5]]; match tri_tri_intersection(&t1, &t2) {
TriTri::Segment([a, b]) => {
let explicits = [&a, &b]
.iter()
.filter(|p| matches!(p, ImplicitPoint::Explicit(_)))
.count();
assert_eq!(explicits, 1, "expected one Explicit (on-plane vertex) endpoint");
for ep in [&a, &b] {
assert_eq!(orient3d(ep, &e(t1[0]), &e(t1[1]), &e(t1[2])), Sign::Zero);
assert_eq!(orient3d(ep, &e(t2[0]), &e(t2[1]), &e(t2[2])), Sign::Zero);
}
}
other => panic!("Touches case should yield a Segment, got {other:?}"),
}
}
#[test]
fn planes_cross_but_intervals_disjoint_is_none() {
let t1 = [[-2., 0., -1.], [2., 0., -1.], [0., 0., 2.]]; let t2 = [[1., -2., 5.], [1., 2., 5.], [1., 0.5, 9.]]; assert!(planes_mutually_cross(&t1, &t2)); assert!(
matches!(tri_tri_intersection(&t1, &t2), TriTri::None),
"disjoint intervals along L should give no intersection"
);
}
}