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");
}
fn plus_translated(m: &Mesh, d: [f32; 3]) -> Mesh {
let mut out = m.clone();
let base = (out.positions.len() / 3) as u32;
for v in m.positions.chunks_exact(3) {
out.positions
.extend_from_slice(&[v[0] + d[0], v[1] + d[1], v[2] + d[2]]);
}
out.normals.extend_from_slice(&m.normals);
out.indices.extend(m.indices.iter().map(|i| i + base));
out
}
#[test]
fn closed_cube_reports_one_closed_orientable_component() {
let mut m = cube(&[]);
let v = orient_mesh_outward_verdict(&mut m);
assert_eq!(
v,
OrientVerdict {
flipped: false,
all_closed: true,
all_orientable: true,
components: 1,
}
);
assert!(v.is_single_closed_solid());
}
#[test]
fn a_repaired_cube_is_still_a_single_closed_solid() {
let mut m = cube(&[3, 7, 10]);
let v = orient_mesh_outward_verdict(&mut m);
assert!(v.flipped, "the fixture must actually have been re-wound");
assert!(
v.is_single_closed_solid(),
"repairing the winding does not make the surface less closed: {v:?}"
);
}
#[test]
fn open_sheet_reports_not_closed() {
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 v = orient_mesh_outward_verdict(&mut m);
assert!(!v.all_closed, "a quad with boundary edges is not closed");
assert!(!v.is_single_closed_solid());
}
#[test]
fn two_disjoint_cubes_report_two_components_and_no_single_solid() {
let mut m = plus_translated(&cube(&[]), [10.0, 0.0, 0.0]);
let v = orient_mesh_outward_verdict(&mut m);
assert_eq!(v.components, 2, "two disjoint bodies are two components");
assert!(v.all_closed && v.all_orientable, "both bodies are closed: {v:?}");
assert!(
!v.is_single_closed_solid(),
"closed-but-two-pieces must NOT license a volume"
);
}
#[test]
fn a_shell_inside_a_shell_is_refused_not_summed() {
let inner = {
let mut m = cube(&[]);
for v in m.positions.chunks_exact_mut(3) {
v[0] = v[0] * 0.5 + 0.25;
v[1] = v[1] * 0.5 + 0.25;
v[2] = v[2] * 0.5 + 0.25;
}
m
};
let mut m = cube(&[]);
let base = (m.positions.len() / 3) as u32;
m.positions.extend_from_slice(&inner.positions);
m.normals.extend_from_slice(&inner.normals);
m.indices.extend(inner.indices.iter().map(|i| i + base));
let v = orient_mesh_outward_verdict(&mut m);
assert_eq!(v.components, 2);
assert!(
!v.is_single_closed_solid(),
"a cavity shell is indistinguishable from a second solid here, so neither may pass"
);
}
#[test]
fn unanalysable_meshes_report_indeterminate() {
let mut malformed = cube(&[]);
malformed.indices[0] = 9999;
assert_eq!(
orient_mesh_outward_verdict(&mut malformed),
OrientVerdict::INDETERMINATE
);
let mut tiny = Mesh::new();
tiny.positions.extend_from_slice(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
tiny.normals.extend_from_slice(&[0.0; 9]);
tiny.indices.extend_from_slice(&[0, 1, 2]);
assert_eq!(
orient_mesh_outward_verdict(&mut tiny),
OrientVerdict::INDETERMINATE,
"one triangle cannot enclose a volume, so it must not claim to be closed"
);
const { assert!(!OrientVerdict::INDETERMINATE.all_closed) };
assert!(!OrientVerdict::INDETERMINATE.is_single_closed_solid());
}
#[test]
fn reporting_the_verdict_does_not_change_a_single_index() {
let fixtures = [
cube(&[]),
cube(&[3, 7, 10]),
cube(&(0..12).collect::<Vec<_>>()),
plus_translated(&cube(&[]), [10.0, 0.0, 0.0]),
plus_translated(&cube(&[1, 4]), [0.0, 10.0, 0.0]),
];
for (i, base) in fixtures.iter().enumerate() {
let mut legacy = base.clone();
let mut with_verdict = base.clone();
let flipped = orient_mesh_outward(&mut legacy);
let verdict = orient_mesh_outward_verdict(&mut with_verdict);
assert_eq!(
legacy.indices, with_verdict.indices,
"fixture {i}: the two entry points must produce the SAME index buffer"
);
assert_eq!(
legacy.positions, with_verdict.positions,
"fixture {i}: neither may touch positions at all"
);
assert_eq!(
flipped, verdict.flipped,
"fixture {i}: the legacy bool must remain exactly `verdict.flipped`"
);
}
}