use crate::bvh::Bvh;
use crate::distance::{Point, has_area};
use crate::intersect::edge;
fn side(a: [f64; 2], b: [f64; 2]) -> i8 {
let sign = |x: f64| {
if x > 0.0 {
1
} else if x < 0.0 {
-1
} else {
0
}
};
let e = edge(a[0], b[1], a[1], b[0]);
if e != 0.0 {
return sign(e);
}
let dy = b[1] - a[1];
if dy != 0.0 {
return -sign(dy);
}
sign(b[0] - a[0])
}
fn height(p: Point, k: usize, t: [Point; 3]) -> Option<f64> {
if !has_area(t) {
return None;
}
let (kx, ky) = ((k + 1) % 3, (k + 2) % 3);
let q = t.map(|c| [c[kx] - p[kx], c[ky] - p[ky], c[k] - p[k]]);
let flat = q.map(|c| [c[0], c[1]]);
let [a, b, c] = flat;
let s = [side(a, b), side(b, c), side(c, a)];
if !(s[0] == s[1] && s[1] == s[2]) || s[0] == 0 {
return None;
}
let area = |u: [f64; 2], v: [f64; 2]| edge(u[0], v[1], u[1], v[0]);
let w = [area(b, c), area(c, a), area(a, b)];
let det = w[0] + w[1] + w[2];
if det == 0.0 {
return None;
}
Some((w[0] * q[0][2] + w[1] * q[1][2] + w[2] * q[2][2]) / det)
}
pub(crate) fn inside(bvh: &Bvh, p: Point) -> bool {
let Some((lo, hi)) = bvh.bounds() else { return false };
let mut votes = 0_i32;
for k in 0..3 {
let (mut a, mut b) = (p, p);
(a[k], b[k]) = (lo[k].min(p[k]), hi[k].max(p[k]));
let (mut all, mut above) = (0_u32, 0_u32);
for i in bvh.near_segment(a, b) {
let Some(t) = bvh.corners(i) else { continue };
if let Some(z) = height(p, k, t) {
all += 1;
above += u32::from(z >= 0.0);
}
}
if all % 2 == 0 {
votes += if above % 2 == 1 { 1 } else { -1 };
}
}
votes > 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::Mesh;
#[test]
fn an_edge_through_the_line_takes_the_moved_lines_sign() {
assert_eq!(side([0.0, -1.0], [0.0, 1.0]), -1);
assert_eq!(side([0.0, 1.0], [0.0, -1.0]), 1);
assert_eq!(side([-1.0, 0.0], [1.0, 0.0]), 1);
assert_eq!(side([1.0, 0.0], [-1.0, 0.0]), -1);
assert_eq!(side([2.0, 2.0], [2.0, 2.0]), 0);
assert_eq!(side([1.0, 0.0], [1.0, 1.0]), 1);
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(4096))]
#[test]
fn whatever_an_axis_line_crosses_has_area(
a in proptest::array::uniform3(-1.0..1.0_f64),
b in proptest::array::uniform3(-1.0..1.0_f64),
t in -1.0..2.0_f64,
s in 0.0..1.0_f64,
k in 0..3_usize,
) {
let c = [0, 1, 2].map(|i| a[i] + t * (b[i] - a[i]));
let p = [0, 1, 2].map(|i| a[i] + s * (b[i] - a[i]));
if height(p, k, [a, b, c]).is_some() {
proptest::prop_assert!(has_area([a, b, c]), "{:?} crossed along {}", [a, b, c], k);
}
}
}
#[test]
fn tied_votes_are_outside_and_an_odd_line_does_not_vote() {
let quad = |k: usize, at: f64, v: &mut Vec<Point>, t: &mut Vec<[u32; 3]>| {
let base = u32::try_from(v.len()).unwrap();
for (u, w) in [(-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5)] {
let mut p = [0.0; 3];
(p[k], p[(k + 1) % 3], p[(k + 2) % 3]) = (at, u, w);
v.push(p);
}
t.extend([[base, base + 1, base + 2], [base, base + 2, base + 3]]);
};
let (mut v, mut t) = (Vec::new(), Vec::new());
quad(0, -1.0, &mut v, &mut t);
quad(0, 1.0, &mut v, &mut t);
quad(1, 1.0, &mut v, &mut t);
let bvh = Bvh::new(&Mesh::new(v, t)).unwrap();
assert!(!inside(&bvh, [0.0, 0.0, 0.0]));
}
#[test]
fn a_line_on_an_edge_or_a_corner_counts_once_and_a_fold_none_or_twice() {
let (o, x, d, y) = ([0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0]);
let on_edge = [0.5, 0.5, 0.0];
for (first, second) in
[([o, x, d], [o, d, y]), ([o, d, x], [o, d, y]), ([x, o, d], [y, o, d])]
{
let hits = [first, second].iter().filter(|t| height(on_edge, 2, **t).is_some()).count();
assert_eq!(hits, 1, "{first:?} {second:?}");
}
let ring = [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [-1.0, 0.0, 1.0], [0.0, -1.0, 1.0]];
let fan: Vec<[Point; 3]> =
(0..4).map(|i| [[0.0, 0.0, 1.0], ring[i], ring[(i + 1) % 4]]).collect();
let centre = [0.0, 0.0, 0.0];
for turned in 0..16 {
let hits = fan
.iter()
.enumerate()
.filter(|(i, t)| {
let t = if turned >> i & 1 == 1 { [t[0], t[2], t[1]] } else { **t };
height(centre, 2, t).is_some()
})
.count();
assert_eq!(hits, 1, "turned {turned:04b}");
}
let ridge = ([0.5, 0.0, 2.0], [0.5, 1.0, 2.0]);
let left = [ridge.0, ridge.1, [0.0, 0.5, 1.0]];
let right = [ridge.0, ridge.1, [0.1, 0.5, 3.0]];
let hits =
[left, right].iter().filter(|t| height([0.5, 0.5, 0.0], 2, **t).is_some()).count();
assert!(hits == 0 || hits == 2, "{hits}");
}
#[test]
fn a_cube_is_inside_along_its_split_edges_whatever_its_faces() {
let corner = |i: u32| [0, 1, 2].map(|k| if i >> k & 1 == 1 { 1.0 } else { -1.0 });
let faces: [[u32; 3]; 12] = [
[0, 2, 3],
[0, 3, 1],
[4, 5, 7],
[4, 7, 6],
[0, 1, 5],
[0, 5, 4],
[2, 6, 7],
[2, 7, 3],
[0, 4, 6],
[0, 6, 2],
[1, 3, 7],
[1, 7, 5],
];
for turned in [0_u32, 0b1100, 0xfff, 0b1010_0101_0011] {
let triangles = faces
.iter()
.enumerate()
.map(|(i, t)| if turned >> i & 1 == 1 { [t[0], t[2], t[1]] } else { *t })
.collect();
let m = Mesh::new((0..8).map(corner).collect(), triangles);
let bvh = Bvh::new(&m).unwrap();
for p in [[0.0, 0.0, 0.0], [0.25, 0.25, 0.25], [-0.5, -0.5, 0.3], [0.9, -0.9, 0.9]] {
assert!(inside(&bvh, p), "{turned:b} {p:?}");
}
for p in [[1.5, 0.0, 0.0], [1.5, 1.5, 1.5], [0.5, 0.5, -1.25]] {
assert!(!inside(&bvh, p), "{turned:b} {p:?}");
}
}
}
}