use crate::Mesh;
use rustc_hash::FxHashMap;
use std::cell::RefCell;
#[derive(Clone, Copy, Default)]
struct EdgeInc {
tris: [usize; 2],
count: u32,
}
impl EdgeInc {
#[inline]
fn push(&mut self, t: usize) {
if (self.count as usize) < 2 {
self.tris[self.count as usize] = t;
}
self.count += 1;
}
#[inline]
fn incident(&self) -> &[usize] {
&self.tris[..(self.count as usize).min(2)]
}
}
const WELD_SCALE: f64 = 1.0e5;
#[derive(Default)]
struct OrientScratch {
vid_of: FxHashMap<(i64, i64, i64), u32>,
vpos: Vec<[f64; 3]>,
corner: Vec<u32>,
edge_tris: FxHashMap<(u32, u32), EdgeInc>,
flip: Vec<bool>,
visited: Vec<bool>,
comp: Vec<usize>,
stack: Vec<usize>,
}
thread_local! {
static ORIENT_SCRATCH: RefCell<Option<OrientScratch>> = const { RefCell::new(None) };
}
pub fn orient_mesh_outward(mesh: &mut Mesh) -> bool {
let ntri = mesh.indices.len() / 3;
if ntri < 2 {
return false;
}
let vertex_count = mesh.positions.len() / 3;
if !mesh.positions.len().is_multiple_of(3)
|| mesh.indices.iter().any(|&idx| idx as usize >= vertex_count)
{
return false;
}
let mut scratch = ORIENT_SCRATCH.with(|c| c.borrow_mut().take()).unwrap_or_default();
let OrientScratch { vid_of, vpos, corner, edge_tris, flip, visited, comp, stack } =
&mut scratch;
vid_of.clear();
vid_of.reserve(vertex_count);
vpos.clear();
corner.clear();
corner.reserve(mesh.indices.len());
let q = |v: f32| (v as f64 * WELD_SCALE).round() as i64;
for &idx in &mesh.indices {
let b = idx as usize * 3;
let key = (
q(mesh.positions[b]),
q(mesh.positions[b + 1]),
q(mesh.positions[b + 2]),
);
let vid = *vid_of.entry(key).or_insert_with(|| {
let id = vpos.len() as u32;
vpos.push([
key.0 as f64 / WELD_SCALE,
key.1 as f64 / WELD_SCALE,
key.2 as f64 / WELD_SCALE,
]);
id
});
corner.push(vid);
}
let tv = |t: usize| [corner[3 * t], corner[3 * t + 1], corner[3 * t + 2]];
edge_tris.clear();
edge_tris.reserve(ntri * 2);
for t in 0..ntri {
let v = tv(t);
for &(a, b) in &[(v[0], v[1]), (v[1], v[2]), (v[2], v[0])] {
if a == b {
continue; }
let key = if a < b { (a, b) } else { (b, a) };
edge_tris.entry(key).or_default().push(t);
}
}
flip.clear();
flip.resize(ntri, false);
visited.clear();
visited.resize(ntri, false);
let mut any_flip = false;
for seed in 0..ntri {
if visited[seed] {
continue;
}
comp.clear();
stack.clear();
stack.push(seed);
visited[seed] = true;
let mut orientable = true;
let mut closed = true;
while let Some(t) = stack.pop() {
comp.push(t);
let v = tv(t);
let dirs = if flip[t] {
[(v[0], v[2]), (v[2], v[1]), (v[1], v[0])]
} else {
[(v[0], v[1]), (v[1], v[2]), (v[2], v[0])]
};
for &(a, b) in &dirs {
if a == b {
continue;
}
let key = if a < b { (a, b) } else { (b, a) };
let inc = &edge_tris[&key];
if inc.count != 2 {
closed = false; }
if inc.count > 2 {
continue; }
for &nb in inc.incident() {
if nb == t {
continue;
}
let nv = tv(nb);
let need_flip =
[(nv[0], nv[1]), (nv[1], nv[2]), (nv[2], nv[0])].contains(&(a, b));
if !visited[nb] {
visited[nb] = true;
flip[nb] = need_flip;
stack.push(nb);
} else if flip[nb] != need_flip {
orientable = false; }
}
}
}
if !orientable || !closed {
for &t in comp.iter() {
flip[t] = false; }
continue;
}
let mut vol6 = 0.0f64;
for &t in comp.iter() {
let v = tv(t);
let (i0, i1, i2) = if flip[t] {
(v[0], v[2], v[1])
} else {
(v[0], v[1], v[2])
};
let (a, b, c) = (vpos[i0 as usize], vpos[i1 as usize], vpos[i2 as usize]);
vol6 += a[0] * (b[1] * c[2] - b[2] * c[1]) + a[1] * (b[2] * c[0] - b[0] * c[2])
+ a[2] * (b[0] * c[1] - b[1] * c[0]);
}
if vol6 < 0.0 {
for &t in comp.iter() {
flip[t] = !flip[t];
}
}
}
for t in 0..ntri {
if flip[t] {
mesh.indices.swap(3 * t + 1, 3 * t + 2);
any_flip = true;
}
}
ORIENT_SCRATCH.with(|c| *c.borrow_mut() = Some(scratch));
any_flip
}
#[cfg(test)]
mod tests {
use super::*;
fn cube(flipped: &[usize]) -> Mesh {
let c = [
[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0],
[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 faces: [[usize; 3]; 12] = [
[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7], [0, 1, 5], [0, 5, 4], [2, 3, 7], [2, 7, 6], [1, 2, 6], [1, 6, 5], [0, 4, 7], [0, 7, 3], ];
let mut m = Mesh::new();
for (t, f) in faces.iter().enumerate() {
let mut tri = *f;
if flipped.contains(&t) {
tri.swap(1, 2);
}
for &vi in &tri {
m.positions.extend_from_slice(&c[vi]);
m.normals.extend_from_slice(&[0.0, 0.0, 0.0]);
}
let base = (m.indices.len()) as u32;
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
m
}
fn bad_edges(m: &Mesh) -> usize {
let q = |v: f32| (v as f64 * WELD_SCALE).round() as i64;
let key = |i: u32| {
let b = i as usize * 3;
(q(m.positions[b]), q(m.positions[b + 1]), q(m.positions[b + 2]))
};
let mut dir: FxHashMap<((i64, i64, i64), (i64, i64, i64)), u32> = FxHashMap::default();
for t in m.indices.chunks_exact(3) {
let (a, b, c) = (key(t[0]), key(t[1]), key(t[2]));
for e in [(a, b), (b, c), (c, a)] {
*dir.entry(e).or_insert(0) += 1;
}
}
dir.values().filter(|&&c| c >= 2).count()
}
#[test]
fn fixes_mixed_winding_to_consistent_outward() {
let mut m = cube(&[3, 7, 10]); assert!(bad_edges(&m) > 0, "fixture must start winding-inconsistent");
let flipped = orient_mesh_outward(&mut m);
assert!(flipped, "the mixed-winding cube must be re-oriented");
assert_eq!(bad_edges(&m), 0, "winding must be consistent after orient");
}
#[test]
fn already_outward_is_untouched() {
let mut m = cube(&[]);
let before = m.indices.clone();
let flipped = orient_mesh_outward(&mut m);
assert!(!flipped, "a clean outward cube must not be touched");
assert_eq!(m.indices, before, "index buffer must be byte-identical");
}
#[test]
fn fully_inward_cube_is_flipped_outward() {
let all: Vec<usize> = (0..12).collect();
let mut m = cube(&all);
assert_eq!(bad_edges(&m), 0, "a fully-inward cube is still consistent");
let flipped = orient_mesh_outward(&mut m);
assert!(flipped, "an inward-wound cube must be flipped outward");
assert_eq!(bad_edges(&m), 0);
}
#[test]
fn open_sheet_is_left_untouched() {
let p = [
[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0],
];
let faces: [[usize; 3]; 2] = [[0, 1, 2], [0, 3, 2]]; let mut m = Mesh::new();
for f in &faces {
for &vi in f {
m.positions.extend_from_slice(&p[vi]);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
let base = m.indices.len() as u32;
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
let before = m.indices.clone();
let flipped = orient_mesh_outward(&mut m);
assert!(!flipped, "an open sheet must not be re-oriented");
assert_eq!(m.indices, before, "open-sheet index buffer must be untouched");
}
#[test]
fn malformed_indices_bail_without_panic() {
let mut m = cube(&[]);
m.indices[0] = 9999; let flipped = orient_mesh_outward(&mut m);
assert!(!flipped, "malformed input must be a no-op");
}
}