use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Aabb, Point2, Point3, Scalar, Tolerance};
use axiolid_measure::WindingMesh;
use axiolid_mesh::TriMesh;
use crate::orient2d;
use crate::triangle_triangle::{triangle_triangle_relation, TriangleTriangleRelation};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Interference {
Clear,
Touching,
Penetrating,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct InterferenceReport {
pub kind: Interference,
pub penetrating_pairs: Vec<(usize, usize)>,
pub touching_pairs: Vec<(usize, usize)>,
pub narrow_phase_tests: usize,
pub broad_phase_rejections: usize,
pub degenerate_skips: usize,
pub containment: bool,
}
impl InterferenceReport {
pub fn is_penetrating(&self) -> bool {
self.kind == Interference::Penetrating
}
pub fn is_clear(&self) -> bool {
self.kind == Interference::Clear
}
}
pub fn interference(
a: &TriMesh,
b: &TriMesh,
tolerance: Tolerance,
) -> GeomResult<InterferenceReport> {
let pad = tolerance.linear();
if !(pad.is_finite() && pad >= 0.0) {
return Err(GeomError::InvalidInput(format!(
"tolerance must be finite and non-negative, got {pad}"
)));
}
let boxes_a = triangle_boxes(a, pad);
let boxes_b = triangle_boxes(b, pad);
let mut report = InterferenceReport {
kind: Interference::Clear,
penetrating_pairs: Vec::new(),
touching_pairs: Vec::new(),
narrow_phase_tests: 0,
broad_phase_rejections: 0,
degenerate_skips: 0,
containment: false,
};
let tree = axiolid_spatial::Bvh::build(
boxes_b
.iter()
.enumerate()
.map(|(j, bounds)| axiolid_spatial::SpatialItem::new(j, *bounds)),
);
let mut hits: Vec<usize> = Vec::new();
for (i, box_a) in boxes_a.iter().enumerate() {
tree.query_aabb(box_a, &mut hits);
report.broad_phase_rejections += boxes_b.len() - hits.len();
for &j in &hits {
let box_b = &boxes_b[j];
if !box_a.intersects(box_b) {
report.broad_phase_rejections += 1;
continue;
}
report.narrow_phase_tests += 1;
let ta = triangle(a, i);
let tb = triangle(b, j);
match triangle_triangle_relation(ta, tb) {
TriangleTriangleRelation::Proper => {
report.penetrating_pairs.push((i, j));
report.kind = Interference::Penetrating;
}
TriangleTriangleRelation::Touching => {
report.touching_pairs.push((i, j));
if report.kind == Interference::Clear {
report.kind = Interference::Touching;
}
}
TriangleTriangleRelation::Coplanar => {
if coplanar_pair_overlaps(ta, tb) {
report.touching_pairs.push((i, j));
if report.kind == Interference::Clear {
report.kind = Interference::Touching;
}
}
}
TriangleTriangleRelation::DegenerateTriangle => {
report.degenerate_skips += 1;
}
_ => {
report.degenerate_skips += 1;
}
}
}
}
let bounds_a = mesh_bounds(a);
let bounds_b = mesh_bounds(b);
if report.kind != Interference::Penetrating
&& !a.indices.is_empty()
&& !b.indices.is_empty()
&& bounds_a.intersects(&bounds_b)
{
let a_in_b = WindingMesh::prepare(b, tolerance)
.ok()
.is_some_and(|w| interior_probes(a).any(|p| inside(&w, p)));
let b_in_a = WindingMesh::prepare(a, tolerance)
.ok()
.is_some_and(|w| interior_probes(b).any(|p| inside(&w, p)));
if a_in_b || b_in_a {
report.kind = Interference::Penetrating;
report.containment = true;
}
}
Ok(report)
}
fn triangle_boxes(mesh: &TriMesh, pad: Scalar) -> Vec<Aabb> {
(0..mesh.indices.len() / 3)
.map(|i| {
let [a, b, c] = triangle(mesh, i);
let lo = Point3::new(
a.x.min(b.x).min(c.x) - pad,
a.y.min(b.y).min(c.y) - pad,
a.z.min(b.z).min(c.z) - pad,
);
let hi = Point3::new(
a.x.max(b.x).max(c.x) + pad,
a.y.max(b.y).max(c.y) + pad,
a.z.max(b.z).max(c.z) + pad,
);
Aabb { min: lo, max: hi }
})
.collect()
}
fn triangle(mesh: &TriMesh, i: usize) -> [Point3; 3] {
let base = i * 3;
[
mesh.positions[mesh.indices[base] as usize],
mesh.positions[mesh.indices[base + 1] as usize],
mesh.positions[mesh.indices[base + 2] as usize],
]
}
fn coplanar_pair_overlaps(a: [Point3; 3], b: [Point3; 3]) -> bool {
let normal = (a[1] - a[0]).cross(a[2] - a[0]);
let drop = dominant_axis(normal);
let pa = a.map(|p| flatten(p, drop));
let pb = b.map(|p| flatten(p, drop));
if pb.iter().any(|p| point_in_triangle(*p, pa)) || pa.iter().any(|p| point_in_triangle(*p, pb))
{
return true;
}
let edges = |t: [Point2; 3]| [[t[0], t[1]], [t[1], t[2]], [t[2], t[0]]];
edges(pa)
.iter()
.any(|ea| edges(pb).iter().any(|eb| segments_cross(*ea, *eb)))
}
fn dominant_axis(n: axiolid_core::Vec3) -> usize {
let (x, y, z) = (n.x.abs(), n.y.abs(), n.z.abs());
if x >= y && x >= z {
0
} else if y >= z {
1
} else {
2
}
}
fn flatten(p: Point3, drop: usize) -> Point2 {
match drop {
0 => Point2::new(p.y, p.z),
1 => Point2::new(p.x, p.z),
_ => Point2::new(p.x, p.y),
}
}
fn point_in_triangle(p: Point2, t: [Point2; 3]) -> bool {
let s = |i: usize, j: usize| sign_of(orient2d(t[i], t[j], p));
let (a, b, c) = (s(0, 1), s(1, 2), s(2, 0));
let non_negative = a >= 0 && b >= 0 && c >= 0;
let non_positive = a <= 0 && b <= 0 && c <= 0;
non_negative || non_positive
}
fn segments_cross(u: [Point2; 2], v: [Point2; 2]) -> bool {
let d1 = sign_of(orient2d(u[0], u[1], v[0]));
let d2 = sign_of(orient2d(u[0], u[1], v[1]));
let d3 = sign_of(orient2d(v[0], v[1], u[0]));
let d4 = sign_of(orient2d(v[0], v[1], u[1]));
if d1 * d2 < 0 && d3 * d4 < 0 {
return true;
}
(d1 == 0 && between(u[0], v[0], u[1]))
|| (d2 == 0 && between(u[0], v[1], u[1]))
|| (d3 == 0 && between(v[0], u[0], v[1]))
|| (d4 == 0 && between(v[0], u[1], v[1]))
}
fn between(p: Point2, q: Point2, r: Point2) -> bool {
q.x >= p.x.min(r.x) && q.x <= p.x.max(r.x) && q.y >= p.y.min(r.y) && q.y <= p.y.max(r.y)
}
fn sign_of(c: axiolid_contracts::Certified) -> i32 {
match c.sign() {
Some(axiolid_contracts::Sign::Positive) => 1,
Some(axiolid_contracts::Sign::Negative) => -1,
_ => 0,
}
}
pub fn point_inside(point: Point3, solid: &TriMesh, tolerance: Tolerance) -> Option<bool> {
let winding = WindingMesh::prepare(solid, tolerance).ok()?;
let w = winding.winding_number(point).ok()?.value;
Some(w > 0.75)
}
fn interior_probes(mesh: &TriMesh) -> impl Iterator<Item = Point3> + '_ {
let mut lo = Point3::splat(Scalar::INFINITY);
let mut hi = Point3::splat(Scalar::NEG_INFINITY);
for p in &mesh.positions {
lo = lo.min(*p);
hi = hi.max(*p);
}
let centre = (lo + hi) * 0.5;
let span = (hi - lo).length().max(1.0);
let nudge = span * 1e-12;
core::iter::once(centre).chain(mesh.indices.chunks_exact(3).map(move |t| {
let a = mesh.positions[t[0] as usize];
let b = mesh.positions[t[1] as usize];
let c = mesh.positions[t[2] as usize];
let m = (a + b + c) / 3.0;
let toward = centre - m;
let len = toward.length();
if len > 0.0 {
m + toward * (nudge / len)
} else {
m
}
}))
}
fn inside(winding: &WindingMesh<'_, TriMesh>, point: Point3) -> bool {
winding
.winding_number(point)
.map(|w| w.value > 0.75)
.unwrap_or(false)
}
fn mesh_bounds(mesh: &TriMesh) -> Aabb {
let mut bounds = Aabb::empty();
for p in &mesh.positions {
bounds.extend(*p);
}
bounds
}