use num_rational::BigRational;
use crate::linalg::Vec3;
use super::exact::predicates::orient3d_r;
use super::exact::rational::{rat, rat_to_f64, R3};
use super::exact::Sign;
const DIRS: [[i32; 3]; 12] = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
[1, 1, 1],
[1, 2, 3],
[3, 1, 7],
[5, 11, 2],
[7, 3, 13],
[2, 9, 5],
[11, 4, 1],
[3, 17, 8],
[13, 6, 5],
];
fn dir_r3(d: [i32; 3]) -> R3 {
R3::new(
rat(d[0] as f64),
rat(d[1] as f64),
rat(d[2] as f64),
)
}
struct RayPrefilter {
origin: crate::linalg::Vec3,
eps: f64,
}
impl RayPrefilter {
fn new(point: &R3) -> Self {
let origin = point.to_vec3_rounded();
let mag = origin.x.abs().max(origin.y.abs()).max(origin.z.abs());
RayPrefilter {
origin,
eps: 1e-6 * (1.0 + mag),
}
}
fn may_hit(&self, d: [i32; 3], bbox: &crate::types::Box) -> bool {
let mut t0 = f64::NEG_INFINITY;
let mut t1 = f64::INFINITY;
for k in 0..3 {
let (lo, hi) = (bbox.min[k] - self.eps, bbox.max[k] + self.eps);
let dk = d[k] as f64;
let pk = self.origin[k];
if dk == 0.0 {
if pk < lo || pk > hi {
return false;
}
continue;
}
let (mut ta, mut tb) = ((lo - pk) / dk, (hi - pk) / dk);
if ta > tb {
std::mem::swap(&mut ta, &mut tb);
}
t0 = t0.max(ta);
t1 = t1.min(tb);
}
t0 <= t1 + self.eps && t1 >= -self.eps
}
}
fn tri_box_f64(t: &[Vec3; 3]) -> crate::types::Box {
let mut b = crate::types::Box::from_points(t[0], t[1]);
b.union_point(t[2]);
b
}
pub fn winding_number(point: &R3, tris: &[[Vec3; 3]]) -> i32 {
let boxes: Vec<crate::types::Box> = tris.iter().map(tri_box_f64).collect();
winding_number_boxed(point, tris, &boxes)
}
pub struct WindingIndex {
boxes: Vec<crate::types::Box>,
collider: crate::collider::Collider,
order: Vec<usize>,
}
impl WindingIndex {
pub fn new(tris: &[[Vec3; 3]]) -> Self {
let boxes: Vec<crate::types::Box> = tris.iter().map(tri_box_f64).collect();
let scene = boxes
.iter()
.fold(crate::types::Box::new(), |acc, b| acc.union_box(b));
let mut order: Vec<usize> = (0..tris.len()).collect();
order.sort_by_key(|&i| crate::sort::morton_code(boxes[i].center(), &scene));
let collider = crate::collider::Collider::new(
order.iter().map(|&i| boxes[i]).collect(),
order
.iter()
.map(|&i| crate::sort::morton_code(boxes[i].center(), &scene))
.collect(),
);
WindingIndex {
boxes,
collider,
order,
}
}
fn candidates(&self, prefilter: &RayPrefilter, d: [i32; 3]) -> Vec<usize> {
let maxd = d.iter().map(|v| v.abs()).max().unwrap_or(1) as f64;
let slack = prefilter.eps * (2.0 + maxd);
let mut lo = crate::linalg::Vec3::new(0.0, 0.0, 0.0);
let mut hi = crate::linalg::Vec3::new(0.0, 0.0, 0.0);
for k in 0..3 {
let o = prefilter.origin[k];
if d[k] > 0 {
lo[k] = o - slack;
hi[k] = f64::INFINITY;
} else if d[k] < 0 {
lo[k] = f64::NEG_INFINITY;
hi[k] = o + slack;
} else {
lo[k] = o - slack;
hi[k] = o + slack;
}
}
let query = crate::types::Box { min: lo, max: hi };
let mut out = Vec::new();
self.collider.collisions_one(&query, usize::MAX, |_, leaf| {
out.push(self.order[leaf]);
});
out
}
}
pub fn winding_number_indexed(point: &R3, tris: &[[Vec3; 3]], index: &WindingIndex) -> i32 {
let prefilter = RayPrefilter::new(point);
let ap = [
rat_to_f64(&point.x),
rat_to_f64(&point.y),
rat_to_f64(&point.z),
];
for d in DIRS {
let mut cand = index.candidates(&prefilter, d);
cand.sort_unstable(); if let Some(w) = winding_one_dir(
point,
&ap,
d,
&prefilter,
cand.iter().map(|&i| (&tris[i], &index.boxes[i])),
) {
return w;
}
}
unreachable!("all candidate ray directions degenerate — malformed input");
}
pub fn winding_number_boxed(
point: &R3,
tris: &[[Vec3; 3]],
boxes: &[crate::types::Box],
) -> i32 {
let prefilter = RayPrefilter::new(point);
let ap = [
rat_to_f64(&point.x),
rat_to_f64(&point.y),
rat_to_f64(&point.z),
];
for d in DIRS {
if let Some(w) = winding_one_dir(point, &ap, d, &prefilter, tris.iter().zip(boxes.iter()))
{
return w;
}
}
unreachable!("all candidate ray directions degenerate — malformed input");
}
fn winding_one_dir<'a, I: Iterator<Item = (&'a [Vec3; 3], &'a crate::types::Box)>>(
point: &R3,
ap: &[f64; 3],
d: [i32; 3],
prefilter: &RayPrefilter,
tris_boxes: I,
) -> Option<i32> {
use super::exact::approx::orient3d_a;
let dir = dir_r3(d);
let o2 = point.add(&dir);
let ao2 = [
ap[0] + d[0] as f64,
ap[1] + d[1] as f64,
ap[2] + d[2] as f64,
];
let mut winding = 0i32;
for (t, bbox) in tris_boxes {
if !prefilter.may_hit(d, bbox) {
continue;
}
let fa = [t[0].x, t[0].y, t[0].z];
let fb = [t[1].x, t[1].y, t[1].z];
let fc = [t[2].x, t[2].y, t[2].z];
let sides = [
orient3d_a(*ap, ao2, fa, fb),
orient3d_a(*ap, ao2, fb, fc),
orient3d_a(*ap, ao2, fc, fa),
];
if matches!(
(sides[0], sides[1]),
(Some(Sign::Pos), Some(Sign::Neg)) | (Some(Sign::Neg), Some(Sign::Pos))
) || matches!(
(sides[1], sides[2]),
(Some(Sign::Pos), Some(Sign::Neg)) | (Some(Sign::Neg), Some(Sign::Pos))
) || matches!(
(sides[0], sides[2]),
(Some(Sign::Pos), Some(Sign::Neg)) | (Some(Sign::Neg), Some(Sign::Pos))
) {
continue;
}
let (s_ab, s_bc, s_ca) = match (sides[0], sides[1], sides[2]) {
(Some(x), Some(y), Some(z)) => (x, y, z),
_ => {
let a = R3::from_vec3(t[0]);
let b = R3::from_vec3(t[1]);
let c = R3::from_vec3(t[2]);
let s_ab = sides[0].unwrap_or_else(|| orient3d_r(point, &o2, &a, &b));
let s_bc = sides[1].unwrap_or_else(|| orient3d_r(point, &o2, &b, &c));
let s_ca = sides[2].unwrap_or_else(|| orient3d_r(point, &o2, &c, &a));
if s_ab == Sign::Zero || s_bc == Sign::Zero || s_ca == Sign::Zero {
if could_graze(point, &o2, &a, &b, &c) {
return None;
}
continue;
}
(s_ab, s_bc, s_ca)
}
};
if s_ab != s_bc || s_bc != s_ca {
continue; }
let h = orient3d_a(fa, fb, fc, *ap).unwrap_or_else(|| {
orient3d_r(
&R3::from_vec3(t[0]),
&R3::from_vec3(t[1]),
&R3::from_vec3(t[2]),
point,
)
});
if h == Sign::Zero {
return None;
}
let n_dot_dir = s_ab; if h != n_dot_dir.flip() {
continue; }
winding += match n_dot_dir {
Sign::Pos => 1, _ => -1,
};
}
Some(winding)
}
pub fn winding_off_surface(
point: &R3,
outward: &R3,
tris: &[[R3; 3]],
tris_f64: &[[Vec3; 3]],
boxes: &[crate::types::Box],
) -> i32 {
use super::exact::approx::orient3d_a;
debug_assert_eq!(tris.len(), boxes.len());
debug_assert_eq!(tris.len(), tris_f64.len());
let prefilter = RayPrefilter::new(point);
let ap = [
rat_to_f64(&point.x),
rat_to_f64(&point.y),
rat_to_f64(&point.z),
];
'dirs: for d in DIRS
.iter()
.flat_map(|d| [*d, [-d[0], -d[1], -d[2]]])
{
let dir = dir_r3(d);
if Sign::of_rat(&dir.dot(outward)) != Sign::Pos {
continue;
}
let o2 = point.add(&dir);
let ao2 = [
ap[0] + d[0] as f64,
ap[1] + d[1] as f64,
ap[2] + d[2] as f64,
];
let mut winding = 0i32;
for ((t, tf), bbox) in tris.iter().zip(tris_f64.iter()).zip(boxes) {
if !prefilter.may_hit(d, bbox) {
continue;
}
let fa = [tf[0].x, tf[0].y, tf[0].z];
let fb = [tf[1].x, tf[1].y, tf[1].z];
let fc = [tf[2].x, tf[2].y, tf[2].z];
let sides = [
orient3d_a(ap, ao2, fa, fb),
orient3d_a(ap, ao2, fb, fc),
orient3d_a(ap, ao2, fc, fa),
];
if matches!(
(sides[0], sides[1]),
(Some(Sign::Pos), Some(Sign::Neg)) | (Some(Sign::Neg), Some(Sign::Pos))
) || matches!(
(sides[1], sides[2]),
(Some(Sign::Pos), Some(Sign::Neg)) | (Some(Sign::Neg), Some(Sign::Pos))
) || matches!(
(sides[0], sides[2]),
(Some(Sign::Pos), Some(Sign::Neg)) | (Some(Sign::Neg), Some(Sign::Pos))
) {
continue;
}
let (a, b, c) = (&t[0], &t[1], &t[2]);
let s_ab = sides[0].unwrap_or_else(|| orient3d_r(point, &o2, a, b));
let s_bc = sides[1].unwrap_or_else(|| orient3d_r(point, &o2, b, c));
let s_ca = sides[2].unwrap_or_else(|| orient3d_r(point, &o2, c, a));
if s_ab == Sign::Zero || s_bc == Sign::Zero || s_ca == Sign::Zero {
if could_graze(point, &o2, a, b, c) {
continue 'dirs;
}
continue;
}
if s_ab != s_bc || s_bc != s_ca {
continue; }
let n_dot_dir = s_ab; let h = orient3d_a(fa, fb, fc, ap).unwrap_or_else(|| orient3d_r(a, b, c, point));
if h == Sign::Zero {
let n = super::exact::predicates::tri_normal_r(a, b, c);
let s_out = Sign::of_rat(&n.dot(outward));
if s_out == n_dot_dir.flip() {
winding += match n_dot_dir {
Sign::Pos => 1,
_ => -1,
};
}
continue;
}
if h != n_dot_dir.flip() {
continue; }
winding += match n_dot_dir {
Sign::Pos => 1,
_ => -1,
};
}
return winding;
}
unreachable!("all candidate ray directions degenerate — malformed input");
}
fn could_graze(o: &R3, o2: &R3, a: &R3, b: &R3, c: &R3) -> bool {
let s_ab = orient3d_r(o, o2, a, b);
let s_bc = orient3d_r(o, o2, b, c);
let s_ca = orient3d_r(o, o2, c, a);
!(matches!((s_ab, s_bc), (Sign::Pos, Sign::Neg) | (Sign::Neg, Sign::Pos))
|| matches!((s_bc, s_ca), (Sign::Pos, Sign::Neg) | (Sign::Neg, Sign::Pos))
|| matches!((s_ab, s_ca), (Sign::Pos, Sign::Neg) | (Sign::Neg, Sign::Pos)))
}
pub fn piece_centroid(v: [&R3; 3]) -> R3 {
let third = BigRational::new(1.into(), 3.into());
v[0].add(v[1]).add(v[2]).scale(&third)
}
pub fn point_inside(point: &R3, tris: &[[Vec3; 3]], complement: bool) -> bool {
let w = winding_number(point, tris);
if complement {
w == 0
} else {
w != 0
}
}
#[cfg(test)]
mod tests {
use super::*;
use num_traits::Zero as _;
fn v(x: f64, y: f64, z: f64) -> Vec3 {
Vec3::new(x, y, z)
}
pub(crate) fn cube_tris(lo: f64, hi: f64) -> Vec<[Vec3; 3]> {
let p = |x, y, z| v(x, y, z);
let quads: [([f64; 3], [f64; 3], [f64; 3], [f64; 3]); 6] = [
([0., 0., 0.], [0., 1., 0.], [1., 1., 0.], [1., 0., 0.]),
([0., 0., 1.], [1., 0., 1.], [1., 1., 1.], [0., 1., 1.]),
([0., 0., 0.], [1., 0., 0.], [1., 0., 1.], [0., 0., 1.]),
([0., 1., 0.], [0., 1., 1.], [1., 1., 1.], [1., 1., 0.]),
([0., 0., 0.], [0., 0., 1.], [0., 1., 1.], [0., 1., 0.]),
([1., 0., 0.], [1., 1., 0.], [1., 1., 1.], [1., 0., 1.]),
];
let s = hi - lo;
let m = |q: [f64; 3]| p(lo + q[0] * s, lo + q[1] * s, lo + q[2] * s);
let mut out = Vec::new();
for (a, b, c, d) in quads {
out.push([m(a), m(b), m(c)]);
out.push([m(a), m(c), m(d)]);
}
out
}
#[test]
fn winding_of_cube() {
let cube = cube_tris(0.0, 2.0);
let inside = R3::from_vec3(v(1.0, 1.0, 1.0));
let outside = R3::from_vec3(v(5.0, 0.5, 0.5));
let near_out = R3::from_vec3(v(-0.25, 1.0, 1.0));
assert_eq!(winding_number(&inside, &cube), 1);
assert_eq!(winding_number(&outside, &cube), 0);
assert_eq!(winding_number(&near_out, &cube), 0);
assert!(point_inside(&inside, &cube, false));
assert!(!point_inside(&outside, &cube, false));
let flipped: Vec<[Vec3; 3]> = cube.iter().map(|t| [t[0], t[2], t[1]]).collect();
assert!(!point_inside(&inside, &flipped, true));
assert!(point_inside(&outside, &flipped, true));
assert_eq!(winding_number(&inside, &flipped), -1);
}
#[test]
fn winding_survives_degenerate_axis_rays() {
let cube = cube_tris(0.0, 2.0);
let tricky_in = R3::from_vec3(v(1.0, 1.0, 0.5)); assert_eq!(winding_number(&tricky_in, &cube), 1);
let tricky_out = R3::from_vec3(v(0.0, 0.0, 5.0)); assert_eq!(winding_number(&tricky_out, &cube), 0);
}
#[test]
fn nested_void_winding() {
let mut solid = cube_tris(0.0, 6.0);
let inner: Vec<[Vec3; 3]> = cube_tris(2.0, 4.0)
.iter()
.map(|t| [t[0], t[2], t[1]])
.collect();
solid.extend(inner);
let in_wall = R3::from_vec3(v(1.0, 1.0, 1.0));
let in_void = R3::from_vec3(v(3.0, 3.0, 3.0));
let outside = R3::from_vec3(v(7.0, 3.0, 3.0));
assert_eq!(winding_number(&in_wall, &solid), 1);
assert_eq!(winding_number(&in_void, &solid), 0);
assert_eq!(winding_number(&outside, &solid), 0);
}
#[test]
fn centroid_is_exact() {
let p = [
R3::from_vec3(v(0.0, 0.0, 0.0)),
R3::from_vec3(v(1.0, 0.0, 0.0)),
R3::from_vec3(v(0.0, 1.0, 0.0)),
];
let c = piece_centroid([&p[0], &p[1], &p[2]]);
assert_eq!(c.x, BigRational::new(1.into(), 3.into()));
assert!(c.z.is_zero());
}
}