use num_rational::BigRational;
use num_traits::{Signed, Zero};
use crate::linalg::Vec3;
use super::exact::approx::orient2d_a;
use super::exact::filtered::orient3d;
use super::exact::predicates::{
homog2_of, line_line_intersect_2d, line_plane_intersect, orient2d_h, tri_normal_r,
Homog2,
};
use super::exact::rational::{r2_eq, rat_to_f64, R2, R3};
use super::exact::Sign;
#[derive(Clone, Debug, PartialEq)]
pub enum TriTriIsect {
None,
Point(R3),
Segment(R3, R3),
Coplanar {
polygon: Vec<R3>,
same_orientation: bool,
},
}
pub fn dominant_axis(n: &R3) -> usize {
let ax = n.x.abs();
let ay = n.y.abs();
let az = n.z.abs();
if az >= ax && az >= ay {
2
} else if ay >= ax {
1
} else {
0
}
}
pub use super::exact::predicates::lift_to_plane;
pub mod stats {
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
pub static PLANE_REJECT: AtomicU64 = AtomicU64::new(0);
pub static COPLANAR: AtomicU64 = AtomicU64::new(0);
pub static COPLANAR_SAT: AtomicU64 = AtomicU64::new(0);
pub static SAT_REJECT: AtomicU64 = AtomicU64::new(0);
pub static INTERVAL: AtomicU64 = AtomicU64::new(0);
pub static COPLANAR_NS: AtomicU64 = AtomicU64::new(0);
pub static COPLANAR_CLIP_NS: AtomicU64 = AtomicU64::new(0);
pub static PLANE_NS: AtomicU64 = AtomicU64::new(0);
pub static INTERVAL_NS: AtomicU64 = AtomicU64::new(0);
pub fn snapshot_and_reset() -> String {
let take = |a: &AtomicU64| a.swap(0, Relaxed);
format!(
"plane-reject {} ({:.3}s signs), coplanar {} (sat {}, {:.3}s of which clip {:.3}s), sat-reject {}, interval {} ({:.3}s)",
take(&PLANE_REJECT),
take(&PLANE_NS) as f64 * 1e-9,
take(&COPLANAR),
take(&COPLANAR_SAT),
take(&COPLANAR_NS) as f64 * 1e-9,
take(&COPLANAR_CLIP_NS) as f64 * 1e-9,
take(&SAT_REJECT),
take(&INTERVAL),
take(&INTERVAL_NS) as f64 * 1e-9,
)
}
}
pub fn tri_tri_intersect(t1: [Vec3; 3], t2: [Vec3; 3]) -> TriTriIsect {
use std::sync::atomic::Ordering::Relaxed;
let t_signs = crate::timing::Stopwatch::start();
let s2 = [
orient3d(t1[0], t1[1], t1[2], t2[0]),
orient3d(t1[0], t1[1], t1[2], t2[1]),
orient3d(t1[0], t1[1], t1[2], t2[2]),
];
if all_same_strict(&s2) {
stats::PLANE_REJECT.fetch_add(1, Relaxed);
stats::PLANE_NS.fetch_add(t_signs.elapsed_ns(), Relaxed);
return TriTriIsect::None;
}
if s2.iter().all(|s| *s == Sign::Zero) {
stats::COPLANAR.fetch_add(1, Relaxed);
let out = coplanar_overlap(t1, t2);
stats::COPLANAR_NS.fetch_add(t_signs.elapsed_ns(), Relaxed);
return out;
}
let s1 = [
orient3d(t2[0], t2[1], t2[2], t1[0]),
orient3d(t2[0], t2[1], t2[2], t1[1]),
orient3d(t2[0], t2[1], t2[2], t1[2]),
];
if all_same_strict(&s1) {
stats::PLANE_REJECT.fetch_add(1, Relaxed);
stats::PLANE_NS.fetch_add(t_signs.elapsed_ns(), Relaxed);
return TriTriIsect::None;
}
stats::PLANE_NS.fetch_add(t_signs.elapsed_ns(), Relaxed);
debug_assert!(
!s1.iter().all(|s| *s == Sign::Zero),
"t1 coplanar with t2's plane implies t2 coplanar with t1's — handled above"
);
{
let f1 = [
[t1[0].x, t1[0].y, t1[0].z],
[t1[1].x, t1[1].y, t1[1].z],
[t1[2].x, t1[2].y, t1[2].z],
];
let f2 = [
[t2[0].x, t2[0].y, t2[0].z],
[t2[1].x, t2[1].y, t2[1].z],
[t2[2].x, t2[2].y, t2[2].z],
];
if super::exact::approx::sat_edge_axes_disjoint(&f1, &f2) {
stats::SAT_REJECT.fetch_add(1, Relaxed);
return TriTriIsect::None;
}
}
stats::INTERVAL.fetch_add(1, Relaxed);
let t_interval = crate::timing::Stopwatch::start();
let out = interval_overlap(t1, t2, &s1, &s2);
stats::INTERVAL_NS.fetch_add(t_interval.elapsed_ns(), Relaxed);
out
}
#[derive(Clone, Copy)]
enum EndPt {
Vert(u8, u8),
Cross(u8, u8),
}
fn interval_overlap(t1: [Vec3; 3], t2: [Vec3; 3], s1: &[Sign; 3], s2: &[Sign; 3]) -> TriTriIsect {
use num_bigint::BigInt;
let degenerate_at = |s: &[Sign; 3]| -> Option<usize> {
(0..3).find(|&i| {
s[i] == Sign::Zero
&& s[(i + 1) % 3] != Sign::Zero
&& s[(i + 1) % 3] == s[(i + 2) % 3]
})
};
if let (Some(i), Some(j)) = (degenerate_at(s1), degenerate_at(s2)) {
return if t1[i] == t2[j] {
TriTriIsect::Point(R3::from_vec3(t1[i]))
} else {
TriTriIsect::None
};
}
let sx = super::exact::intpred::scaled_big([
t1[0].x, t1[1].x, t1[2].x, t2[0].x, t2[1].x, t2[2].x,
]);
let sy = super::exact::intpred::scaled_big([
t1[0].y, t1[1].y, t1[2].y, t2[0].y, t2[1].y, t2[2].y,
]);
let sz = super::exact::intpred::scaled_big([
t1[0].z, t1[1].z, t1[2].z, t2[0].z, t2[1].z, t2[2].z,
]);
let v = |k: usize| [&sx[k], &sy[k], &sz[k]];
let sub = |a: [&BigInt; 3], b: [&BigInt; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
let cross = |a: &[BigInt; 3], b: &[BigInt; 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],
]
};
let dot = |a: &[BigInt; 3], b: [&BigInt; 3]| &a[0] * b[0] + &a[1] * b[1] + &a[2] * b[2];
let n1 = cross(&sub(v(1), v(0)), &sub(v(2), v(0)));
let n2 = cross(&sub(v(4), v(3)), &sub(v(5), v(3)));
let dir = cross(&n1, &n2);
debug_assert!(
dir.iter().any(|c| !c.is_zero()),
"non-coplanar intersecting planes"
);
let du = |k: usize| dot(&dir, v(k));
let h = |k: usize, n: &[BigInt; 3], origin: usize| dot(n, v(k)) - dot(n, v(origin));
#[cfg(debug_assertions)]
for i in 0..3 {
debug_assert_eq!(int_sign(&h(i, &n2, 3)), s1[i], "scaled height disagrees with s1");
debug_assert_eq!(int_sign(&h(3 + i, &n1, 0)), s2[i], "scaled height disagrees with s2");
}
let endpoints = |which: u8, s: &[Sign; 3]| -> Vec<(Frac, EndPt)> {
let base = if which == 0 { 0 } else { 3 };
let (n, origin) = if which == 0 { (&n2, 3) } else { (&n1, 0) };
let mut pts = Vec::with_capacity(2);
for i in 0..3 {
if s[i] == Sign::Zero {
pts.push((
(du(base + i), BigInt::from(1)),
EndPt::Vert(which, i as u8),
));
}
}
for i in 0..3 {
let j = (i + 1) % 3;
if s[i] != Sign::Zero && s[j] != Sign::Zero && s[i] != s[j] {
let hu = h(base + i, n, origin);
let hv = h(base + j, n, origin);
let du_u = du(base + i);
let du_v = du(base + j);
let mut den = &hu - &hv;
let mut num = &den * &du_u + &hu * (&du_v - &du_u);
if den.sign() == num_bigint::Sign::Minus {
den = -den;
num = -num;
}
pts.push(((num, den), EndPt::Cross(which, i as u8)));
}
}
debug_assert!(!pts.is_empty() && pts.len() <= 2);
pts
};
let pts1 = endpoints(0, s1);
let pts2 = endpoints(1, s2);
let minmax = |pts: Vec<(Frac, EndPt)>| -> ((Frac, EndPt), (Frac, EndPt)) {
let mut lo = pts[0].clone();
let mut hi = pts[0].clone();
for p in &pts[1..] {
if cmp_frac(&p.0, &lo.0) == std::cmp::Ordering::Less {
lo = p.clone();
}
if cmp_frac(&p.0, &hi.0) == std::cmp::Ordering::Greater {
hi = p.clone();
}
}
(lo, hi)
};
let i1 = minmax(pts1);
let i2 = minmax(pts2);
let (lo, lo_pt) = if cmp_frac(&i1.0 .0, &i2.0 .0) != std::cmp::Ordering::Less { i1.0 } else { i2.0 };
let (hi, hi_pt) = if cmp_frac(&i1.1 .0, &i2.1 .0) != std::cmp::Ordering::Greater { i1.1 } else { i2.1 };
match cmp_frac(&lo, &hi) {
std::cmp::Ordering::Greater => TriTriIsect::None,
std::cmp::Ordering::Equal => TriTriIsect::Point(build_endpoint(lo_pt, &t1, &t2)),
std::cmp::Ordering::Less => TriTriIsect::Segment(
build_endpoint(lo_pt, &t1, &t2),
build_endpoint(hi_pt, &t1, &t2),
),
}
}
#[cfg(debug_assertions)]
fn int_sign(v: &num_bigint::BigInt) -> Sign {
match v.sign() {
num_bigint::Sign::Minus => Sign::Neg,
num_bigint::Sign::NoSign => Sign::Zero,
num_bigint::Sign::Plus => Sign::Pos,
}
}
fn build_endpoint(e: EndPt, t1: &[Vec3; 3], t2: &[Vec3; 3]) -> R3 {
let tri = |which: u8| if which == 0 { t1 } else { t2 };
match e {
EndPt::Vert(w, i) => R3::from_vec3(tri(w)[i as usize]),
EndPt::Cross(w, i) => {
let own = tri(w);
let other = tri(1 - w);
let a = R3::from_vec3(own[i as usize]);
let b = R3::from_vec3(own[(i as usize + 1) % 3]);
let p: [R3; 3] = [
R3::from_vec3(other[0]),
R3::from_vec3(other[1]),
R3::from_vec3(other[2]),
];
line_plane_intersect(&a, &b, &p[0], &p[1], &p[2])
.expect("strictly straddling edge cannot be parallel to the plane")
}
}
}
type Frac = (num_bigint::BigInt, num_bigint::BigInt);
fn cmp_frac(a: &Frac, b: &Frac) -> std::cmp::Ordering {
(&a.0 * &b.1).cmp(&(&b.0 * &a.1))
}
fn all_same_strict(s: &[Sign; 3]) -> bool {
s[0] != Sign::Zero && s[0] == s[1] && s[1] == s[2]
}
fn coplanar_separated_2d(t1: [Vec3; 3], t2: [Vec3; 3], axis: usize) -> bool {
use super::exact::filtered::orient2d;
let proj = |v: Vec3| match axis {
0 => crate::linalg::Vec2::new(v.y, v.z),
1 => crate::linalg::Vec2::new(v.z, v.x),
_ => crate::linalg::Vec2::new(v.x, v.y),
};
let p1 = t1.map(proj);
let p2 = t2.map(proj);
let separates = |tri: &[crate::linalg::Vec2; 3], other: &[crate::linalg::Vec2; 3]| {
(0..3).any(|i| {
let a = tri[i];
let b = tri[(i + 1) % 3];
let s_ref = orient2d(a, b, tri[(i + 2) % 3]);
s_ref != Sign::Zero
&& other.iter().all(|&q| {
let s = orient2d(a, b, q);
s != Sign::Zero && s != s_ref
})
})
};
separates(&p1, &p2) || separates(&p2, &p1)
}
fn coplanar_overlap(t1: [Vec3; 3], t2: [Vec3; 3]) -> TriTriIsect {
{
let n = crate::linalg::cross(t1[1] - t1[0], t1[2] - t1[0]);
let (ax, ay, az) = (n.x.abs(), n.y.abs(), n.z.abs());
let axis = if az >= ax && az >= ay { 2 } else if ay >= ax { 1 } else { 0 };
if coplanar_separated_2d(t1, t2, axis) {
stats::COPLANAR_SAT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return TriTriIsect::None;
}
}
let t_clip = crate::timing::Stopwatch::start();
let out = coplanar_clip(t1, t2);
stats::COPLANAR_CLIP_NS.fetch_add(t_clip.elapsed_ns(), std::sync::atomic::Ordering::Relaxed);
out
}
#[derive(Clone)]
struct ClipPt {
r: R2,
a: [f64; 2],
h: std::cell::OnceCell<Homog2>,
}
impl ClipPt {
fn new(r: R2) -> Self {
let a = [rat_to_f64(&r.x), rat_to_f64(&r.y)];
ClipPt {
r,
a,
h: std::cell::OnceCell::new(),
}
}
fn h(&self) -> &Homog2 {
self.h.get_or_init(|| homog2_of(&self.r))
}
}
#[inline]
fn o2p(a: &ClipPt, b: &ClipPt, c: &ClipPt) -> Sign {
orient2d_a(a.a, b.a, c.a).unwrap_or_else(|| orient2d_h(a.h(), b.h(), c.h()))
}
#[inline]
fn clip_pt_eq(a: &ClipPt, b: &ClipPt) -> bool {
r2_eq(&a.r, &b.r)
}
fn coplanar_clip(t1: [Vec3; 3], t2: [Vec3; 3]) -> TriTriIsect {
let r1: [R3; 3] = [
R3::from_vec3(t1[0]),
R3::from_vec3(t1[1]),
R3::from_vec3(t1[2]),
];
let r2: [R3; 3] = [
R3::from_vec3(t2[0]),
R3::from_vec3(t2[1]),
R3::from_vec3(t2[2]),
];
let n1 = tri_normal_r(&r1[0], &r1[1], &r1[2]);
debug_assert!(!n1.is_zero(), "degenerate input triangle");
let axis = dominant_axis(&n1);
let mut clip: Vec<ClipPt> = r1
.iter()
.map(|p| ClipPt::new(p.project_drop(axis)))
.collect();
if o2p(&clip[0], &clip[1], &clip[2]) == Sign::Neg {
clip.swap(1, 2);
}
let mut subject: Vec<ClipPt> = r2
.iter()
.map(|p| ClipPt::new(p.project_drop(axis)))
.collect();
if o2p(&subject[0], &subject[1], &subject[2]) == Sign::Neg {
subject.swap(1, 2);
}
let mut poly = subject;
for i in 0..3 {
if poly.is_empty() {
break;
}
let (c0, c1) = (&clip[i], &clip[(i + 1) % 3]);
let mut out: Vec<ClipPt> = Vec::with_capacity(poly.len() + 2);
let sides: Vec<bool> = poly.iter().map(|p| o2p(c0, c1, p) != Sign::Neg).collect();
for k in 0..poly.len() {
let kn = (k + 1) % poly.len();
let (s, e) = (&poly[k], &poly[kn]);
match (sides[k], sides[kn]) {
(true, true) => out.push(e.clone()),
(true, false) => {
let x = line_line_intersect_2d(&c0.r, &c1.r, &s.r, &e.r)
.expect("strictly crossing edge is not parallel to clip line");
out.push(ClipPt::new(x));
}
(false, true) => {
let x = line_line_intersect_2d(&c0.r, &c1.r, &s.r, &e.r)
.expect("strictly crossing edge is not parallel to clip line");
out.push(ClipPt::new(x));
out.push(e.clone());
}
(false, false) => {}
}
}
poly = out;
}
let poly = canonical_polygon(poly);
match poly.len() {
0 => TriTriIsect::None,
1 => TriTriIsect::Point(lift_to_plane(&poly[0].r, axis, &r1[0], &n1)),
2 => TriTriIsect::Segment(
lift_to_plane(&poly[0].r, axis, &r1[0], &n1),
lift_to_plane(&poly[1].r, axis, &r1[0], &n1),
),
_ => {
let n2 = tri_normal_r(&r2[0], &r2[1], &r2[2]);
debug_assert!(!n2.is_zero(), "degenerate input triangle");
let same_orientation = match Sign::of_rat(&n1.dot(&n2)) {
Sign::Pos => true,
Sign::Neg => false,
Sign::Zero => unreachable!("coplanar triangles have parallel normals"),
};
TriTriIsect::Coplanar {
polygon: poly
.iter()
.map(|p| lift_to_plane(&p.r, axis, &r1[0], &n1))
.collect(),
same_orientation,
}
}
}
}
fn canonical_polygon(poly: Vec<ClipPt>) -> Vec<ClipPt> {
let mut pts: Vec<ClipPt> = Vec::with_capacity(poly.len());
for p in poly {
if pts.last().map_or(true, |last| !clip_pt_eq(last, &p)) {
pts.push(p);
}
}
while pts.len() > 1 && clip_pt_eq(&pts[0], &pts[pts.len() - 1]) {
pts.pop();
}
if pts.len() <= 2 {
return pts;
}
let all_collinear = (0..pts.len()).all(|i| {
let a = &pts[i];
let b = &pts[(i + 1) % pts.len()];
let c = &pts[(i + 2) % pts.len()];
o2p(a, b, c) == Sign::Zero
});
if all_collinear {
let dir = pts
.iter()
.skip(1)
.map(|p| p.r.sub(&pts[0].r))
.find(|d| !d.is_zero())
.expect("at least two distinct points");
let param = |p: &R2| p.sub(&pts[0].r).dot(&dir);
let (mut lo, mut hi) = (0usize, 0usize);
let (mut lo_t, mut hi_t) = (BigRational::zero(), BigRational::zero());
for (i, p) in pts.iter().enumerate() {
let t = param(&p.r);
if t < lo_t {
lo_t = t.clone();
lo = i;
}
if t > hi_t {
hi_t = t;
hi = i;
}
}
if clip_pt_eq(&pts[lo], &pts[hi]) {
return vec![pts[lo].clone()];
}
return vec![pts[lo].clone(), pts[hi].clone()];
}
let n = pts.len();
let keep: Vec<ClipPt> = (0..n)
.filter(|&i| {
let prev = &pts[(i + n - 1) % n];
let next = &pts[(i + 1) % n];
o2p(prev, &pts[i], next) != Sign::Zero
})
.map(|i| pts[i].clone())
.collect();
keep
}
#[cfg(test)]
#[path = "tri_tri_tests.rs"]
mod tests;