use super::*;
use alloc::vec;
use alloc::vec::Vec;
use alloc::{format, string::String};
fn cfg() -> ValidateConfig {
ValidateConfig::from_cell_size(1.0).expect("valid cell size")
}
fn tetrahedron() -> (Vec<[f64; 3]>, Vec<u32>) {
let positions = vec![
[1.0, 1.0, 1.0],
[1.0, -1.0, -1.0],
[-1.0, 1.0, -1.0],
[-1.0, -1.0, 1.0],
];
let indices = vec![0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2];
(positions, indices)
}
fn torus_grid(m: u32, n: u32) -> (Vec<[f64; 3]>, Vec<u32>) {
let tau = 2.0 * core::f64::consts::PI;
let (major, minor) = (1.0f64, 0.3f64);
let mut positions = Vec::new();
for i in 0..m {
let theta = tau * f64::from(i) / f64::from(m);
for j in 0..n {
let phi = tau * f64::from(j) / f64::from(n);
let ring = major + minor * Real::cos(phi);
positions.push([
ring * Real::cos(theta),
minor * Real::sin(phi),
ring * Real::sin(theta),
]);
}
}
let at = |i: u32, j: u32| (i % m) * n + (j % n);
let mut indices = Vec::new();
for i in 0..m {
for j in 0..n {
let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
indices.extend_from_slice(&[a, b, c, a, c, d]);
}
}
(positions, indices)
}
#[test]
fn outward_winding_is_verified() {
let (p, idx) = tetrahedron();
for tri in idx.chunks_exact(3) {
let (a, b, c) = (p[tri[0] as usize], p[tri[1] as usize], p[tri[2] as usize]);
let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let n = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
let centroid = [
(a[0] + b[0] + c[0]) / 3.0,
(a[1] + b[1] + c[1]) / 3.0,
(a[2] + b[2] + c[2]) / 3.0,
];
let dot = n[0] * centroid[0] + n[1] * centroid[1] + n[2] * centroid[2];
assert!(dot > 0.0, "face {tri:?} is wound inward: {dot}");
}
}
#[test]
fn tetrahedron_is_a_clean_closed_surface() {
let (p, idx) = tetrahedron();
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.vertices, 4);
assert_eq!(r.referenced_vertices, 4);
assert_eq!(r.edges, 6);
assert_eq!(r.faces, 4);
assert_eq!(r.euler_characteristic, 2);
assert_eq!(r.components, 1);
assert_eq!(r.boundary_loops, 0);
assert_eq!(r.genus, Some(0));
assert_eq!(r.violations(), 0);
assert_eq!(r.degenerate_triangles, 0);
assert_eq!(r.duplicate_vertices, 0);
assert!(r.is_manifold() && r.is_closed() && !r.has_structural_errors());
}
#[test]
fn torus_grid_has_genus_one() {
let (p, idx) = torus_grid(8, 5);
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.vertices, 40);
assert_eq!(r.edges, 120);
assert_eq!(r.faces, 80);
assert_eq!(r.euler_characteristic, 0);
assert_eq!(r.components, 1);
assert_eq!(r.boundary_loops, 0);
assert_eq!(r.genus, Some(1)); assert_eq!(r.violations(), 0);
assert!(r.is_closed());
}
#[test]
fn open_tetra_is_a_disk_not_a_handle() {
let (p, mut idx) = tetrahedron();
idx.truncate(9);
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.faces, 3);
assert_eq!(r.edges, 6);
assert_eq!(r.euler_characteristic, 1);
assert_eq!(r.boundary_edges, 3);
assert_eq!(r.boundary_loops, 1);
assert_eq!(r.genus, Some(0));
assert_eq!(r.non_manifold_edges, 0);
assert!(
r.is_manifold(),
"a manifold with boundary is still a manifold"
);
assert!(!r.is_closed());
}
#[test]
fn non_manifold_edge_is_detected() {
let p = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[0.0, -1.0, 0.0],
];
let idx = vec![0, 1, 2, 0, 1, 3, 0, 1, 4];
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.non_manifold_edges, 1);
assert_eq!(r.boundary_edges, 6);
assert_eq!(r.non_manifold_vertices, 2);
assert!(!r.is_manifold());
}
#[test]
fn bowtie_is_detected_only_by_the_link_walk() {
let p = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[-1.0, 0.0, 0.0],
[-1.0, -1.0, 0.0],
];
let idx = vec![0, 1, 2, 0, 3, 4];
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.non_manifold_vertices, 1);
assert_eq!(
r.non_manifold_edges, 0,
"the cheap edge test cannot see this"
);
assert_eq!(r.components, 2);
assert_eq!(r.euler_characteristic, 1);
assert!(!r.is_manifold());
let (faces_at_apex, wing_edges_at_apex) = (2, 2);
assert_eq!(faces_at_apex, wing_edges_at_apex);
}
#[test]
fn degenerate_triangle_is_detected() {
let p = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
let idx = vec![0, 1, 2];
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.degenerate_triangles, 1);
assert_eq!(r.faces, 1);
assert!(!r.has_structural_errors());
assert_eq!(r.violations(), 0);
}
#[test]
fn repeated_index_triangle_is_skipped_and_counted() {
let (p, mut idx) = tetrahedron();
idx.extend_from_slice(&[0, 1, 1]);
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.repeated_index_triangles, 1);
assert_eq!(r.faces_skipped, 1);
assert_eq!(r.faces, 4, "the four good faces still count");
assert_eq!(r.euler_characteristic, 2);
assert!(r.has_structural_errors());
assert_eq!(r.genus, None, "genus is undefined once a face was skipped");
}
#[test]
fn out_of_range_index_does_not_panic() {
let (p, mut idx) = tetrahedron();
let last = idx.len() - 1;
idx[last] = 9; let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.out_of_range_indices, 1);
assert_eq!(r.faces_skipped, 1);
assert_eq!(r.faces, 3);
assert!(r.has_structural_errors());
assert_eq!(r.edges, 6);
assert_eq!(r.boundary_edges, 3);
}
#[test]
fn trailing_index_is_counted_and_ignored() {
let (p, idx) = tetrahedron();
let mut broken = idx.clone();
broken.push(0);
let clean = validate_indexed(&p, &idx, &cfg());
let r = validate_indexed(&p, &broken, &cfg());
assert_eq!(r.trailing_indices, 1);
assert!(r.has_structural_errors());
assert_eq!(r.faces, clean.faces);
assert_eq!(r.edges, clean.edges);
assert_eq!(r.euler_characteristic, clean.euler_characteristic);
}
#[test]
fn exact_duplicate_vertex_is_detected() {
let (mut p, idx) = tetrahedron();
p.push(p[0]); let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.duplicate_vertices, 1);
assert_eq!(r.unreferenced_vertices, 1);
assert_eq!(r.vertices, 5);
assert_eq!(r.referenced_vertices, 4);
assert_eq!(
r.euler_characteristic, 2,
"the stale vertex must not inflate chi"
);
}
#[test]
fn near_duplicate_is_detected_at_one_epsilon_and_not_at_a_quarter_of_it() {
let (mut p, idx) = tetrahedron();
let coarse = cfg();
let half_eps = coarse.weld_epsilon * 0.5;
p.push([p[0][0] + half_eps, p[0][1], p[0][2]]);
assert_eq!(validate_indexed(&p, &idx, &coarse).duplicate_vertices, 1);
let fine = ValidateConfig::from_cell_size(coarse.cell_size * 0.25).expect("valid cell size");
assert!(half_eps > fine.weld_epsilon);
assert_eq!(validate_indexed(&p, &idx, &fine).duplicate_vertices, 0);
}
#[test]
fn flipped_winding_passes_every_other_check() {
let (p, mut idx) = tetrahedron();
idx.swap(1, 2); let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.euler_characteristic, 2, "chi alone cannot see this");
assert_eq!(r.non_manifold_edges, 0);
assert_eq!(r.non_manifold_vertices, 0);
assert_eq!(r.boundary_edges, 0);
assert_eq!(r.inconsistently_oriented_edges, 3);
assert!(!r.is_manifold());
assert_eq!(r.genus, None);
}
#[test]
fn enclosed_cavity_has_two_components() {
let (outer, outer_idx) = tetrahedron();
let mut p = outer.clone();
let mut idx = outer_idx.clone();
for v in &outer {
p.push([v[0] * 0.3, v[1] * 0.3, v[2] * 0.3]);
}
for tri in outer_idx.chunks_exact(3) {
idx.extend_from_slice(&[tri[0] + 4, tri[2] + 4, tri[1] + 4]);
}
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.components, 2);
assert_eq!(r.faces, 8);
assert_eq!(r.edges, 12);
assert_eq!(r.euler_characteristic, 4);
assert_eq!(r.boundary_loops, 0);
assert_eq!(r.inconsistently_oriented_edges, 0);
assert_eq!(r.genus, None, "genus needs a single component");
assert!(r.is_closed());
}
#[test]
fn composite_broken_mesh_detects_every_violation_class() {
let mut p = vec![
[1.0, 1.0, 1.0],
[1.0, -1.0, -1.0],
[-1.0, 1.0, -1.0],
[-1.0, -1.0, 1.0],
];
p.push(p[0]); p.push([0.0, 0.0, 0.0]);
p.push([1.0, 0.0, 0.0]);
p.push([2.0, 0.0, 0.0]); p.push([f64::NAN, 0.0, 0.0]);
let mut idx = vec![
0, 2, 1, 0, 2, 3, 0, 3, 1, 1, 3, 2, 0, 1, 9, 0, 1, 1, 5, 6, 7, ];
idx.push(4);
let r = validate_indexed(&p, &idx, &cfg());
assert!(r.inconsistently_oriented_edges > 0);
assert!(r.degenerate_triangles > 0);
assert!(r.repeated_index_triangles > 0);
assert!(r.out_of_range_indices > 0);
assert!(r.trailing_indices > 0);
assert!(r.duplicate_vertices > 0);
assert!(r.unreferenced_vertices > 0);
assert!(r.non_finite_positions > 0);
assert!(r.faces_skipped > 0);
assert!(r.has_structural_errors());
assert!(!r.is_manifold());
assert_eq!(r.genus, None);
let block = format!("{r}");
assert!(block.contains("!! STRUCTURAL ERRORS"));
assert!(block.contains("(over the valid subset only)"));
assert!(block.contains("INVALID:"));
}
#[test]
fn validation_is_deterministic() {
let (p, idx) = torus_grid(6, 4);
let a = validate_indexed(&p, &idx, &cfg());
let b = validate_indexed(&p, &idx, &cfg());
assert_eq!(a, b);
assert_eq!(format!("{a}"), format!("{b}"));
}
#[test]
fn empty_mesh_does_not_divide_by_zero() {
let p: Vec<[f64; 3]> = Vec::new();
let idx: Vec<u32> = Vec::new();
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.vertices, 0);
assert_eq!(r.faces, 0);
assert_eq!(r.edges, 0);
assert_eq!(r.euler_characteristic, 0);
assert_eq!(r.components, 0);
assert_eq!(r.violations(), 0);
let _ = format!("{r}");
}
#[test]
fn mesh_buffer_normal_count_is_checked() {
let (p, idx) = tetrahedron();
let mut mesh = MeshBuffer::<f64> {
positions: p,
normals: Vec::new(),
indices: idx,
};
let r = validate(&mesh, &cfg());
assert!(r.normal_count_mismatch);
assert!(r.has_structural_errors());
mesh.normals = mesh.positions.clone();
let r = validate(&mesh, &cfg());
assert!(!r.normal_count_mismatch);
assert!(r.is_closed());
}
#[test]
fn config_rejects_a_meaningless_scale() {
for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
let error = ValidateConfig::from_cell_size(bad)
.expect_err("a meaningless spacing makes every threshold meaningless");
assert!(
matches!(error, crate::Error::InvalidCellSize { .. }),
"{error}"
);
}
let good = ValidateConfig::from_cell_size(0.5).expect("valid");
assert!((good.cell_size() - 0.5).abs() < f64::EPSILON);
assert!(good.weld_epsilon() > 0.0 && good.area_epsilon_rel() > 0.0);
}
#[test]
#[should_panic(expected = "MANIFOLD, WITH BOUNDARY")]
fn panic_if_invalid_reports_the_whole_block() {
let (p, mut idx) = tetrahedron();
idx.truncate(9);
validate_indexed(&p, &idx, &cfg()).panic_if_invalid(true);
}
#[test]
fn the_gate_is_chosen_by_the_field_not_by_the_test() {
use crate::fields::ReferenceField;
use crate::for_each_reference_field;
let (closed_p, closed_i) = tetrahedron();
let (open_p, mut open_i) = tetrahedron();
open_i.truncate(9);
let mut checked = 0;
for_each_reference_field!(f64, |name, field| {
let (p, i) = if field.closed_in_domain() {
(&closed_p, &closed_i)
} else {
(&open_p, &open_i)
};
let report = validate_indexed(p, i, &cfg());
if field.closed_in_domain() {
assert!(report.is_closed(), "{name}\n{report}");
} else {
assert!(report.is_manifold(), "{name}\n{report}");
assert!(
report.boundary_edges > 0,
"{name}: expected an open surface"
);
}
if let Some(chi) = field.expected_euler() {
assert!(
chi == 2 || chi == 0,
"{name}: unexpected declared chi {chi}"
);
}
checked += 1;
});
assert_eq!(checked, 8);
}
#[test]
fn display_block_is_stable() {
let (p, idx) = tetrahedron();
let block: String = format!("{}", validate_indexed(&p, &idx, &cfg()));
assert_eq!(block, EXPECTED_TETRAHEDRON_BLOCK);
}
const EXPECTED_TETRAHEDRON_BLOCK: &str = "\
mesh report
vertices 4 (4 referenced, 0 unreferenced)
edges 6
faces 4
euler characteristic 2
components 1
boundary loops 0
genus 0
------------------------------------------------------------
non-manifold edges 0
non-manifold vertices 0
boundary edges 0
inconsistently oriented 0
degenerate triangles 0 (area <= 1e-6 * h^2, h = 1)
repeated-index triangles 0
duplicate vertices 0 (within 1e-4)
out-of-range indices 0
trailing indices 0
non-finite positions 0
------------------------------------------------------------
MANIFOLD, CLOSED";
#[test]
fn the_weld_lattice_does_not_collapse_far_from_the_origin() {
use crate::fields::ReferenceField as _;
let field = crate::fields::Sphere::<f64>::canonical();
let (lo, _) = field.domain();
let cell_size = 4.0 / 32.0;
let shape = crate::RuntimeShape3::new([33; 3]).expect("valid shape");
let mut mc = crate::marching_cubes::MarchingCubes::<f64>::new();
let mut mesh = MeshBuffer::<f64>::new();
mc.extract(&field, &shape, lo, cell_size, &mut mesh)
.expect("extraction");
let cfg = ValidateConfig::from_cell_size(cell_size).expect("valid spacing");
let at = |offset: f64| {
let moved: Vec<[f64; 3]> = mesh
.positions
.iter()
.map(|p| [p[0] + offset, p[1] + offset, p[2] + offset])
.collect();
validate_indexed(&moved, &mesh.indices, &cfg)
};
let home = at(0.0);
let far = at(1.0e4);
let further = at(1.0e6);
std::println!(
"measured: T-008 weld buckets for {} vertices -- origin {}, +1e4 {}, +1e6 {}",
mesh.vertex_count(),
home.weld_buckets,
far.weld_buckets,
further.weld_buckets
);
assert_eq!(home.duplicate_vertices, far.duplicate_vertices);
assert_eq!(home.duplicate_vertices, further.duplicate_vertices);
assert_eq!(
home.weld_buckets, far.weld_buckets,
"the lattice collapsed at 1e4"
);
assert_eq!(
home.weld_buckets, further.weld_buckets,
"the lattice collapsed at 1e6"
);
assert!(
home.weld_buckets as usize > mesh.vertex_count() * 9 / 10,
"a sphere's vertices should be spread across buckets, got {} for {}",
home.weld_buckets,
mesh.vertex_count()
);
}
fn cap_fan(loop_xy: &[[f64; 2]]) -> (Vec<[f64; 3]>, Vec<u32>) {
let n = loop_xy.len();
let cx = loop_xy.iter().map(|p| p[0]).sum::<f64>() / n as f64;
let cy = loop_xy.iter().map(|p| p[1]).sum::<f64>() / n as f64;
let mut positions = vec![[cx, cy, 0.0]];
positions.extend(loop_xy.iter().map(|p| [p[0], p[1], 0.0]));
let mut indices = Vec::new();
for i in 0..n {
let (a, b) = (1 + i as u32, 1 + ((i + 1) % n) as u32);
let (u, v) = (loop_xy[i], loop_xy[(i + 1) % n]);
let twice_area = (u[0] - cx) * (v[1] - cy) - (u[1] - cy) * (v[0] - cx);
if twice_area >= 0.0 {
indices.extend_from_slice(&[0, a, b]);
} else {
indices.extend_from_slice(&[0, b, a]);
}
}
(positions, indices)
}
fn winding_turns(loop_xy: &[[f64; 2]]) -> f64 {
let n = loop_xy.len();
let cx = loop_xy.iter().map(|p| p[0]).sum::<f64>() / n as f64;
let cy = loop_xy.iter().map(|p| p[1]).sum::<f64>() / n as f64;
let tau = 2.0 * core::f64::consts::PI;
let mut total = 0.0;
for i in 0..n {
let (u, v) = (loop_xy[i], loop_xy[(i + 1) % n]);
let a = libm::atan2(u[1] - cy, u[0] - cx);
let b = libm::atan2(v[1] - cy, v[0] - cx);
let mut d = b - a;
while d > core::f64::consts::PI {
d -= tau;
}
while d < -core::f64::consts::PI {
d += tau;
}
total += d;
}
total / tau
}
#[test]
fn a_convex_cap_fan_is_consistently_oriented() {
let hexagon: Vec<[f64; 2]> = (0..6)
.map(|k| {
let t = 2.0 * core::f64::consts::PI * f64::from(k) / 6.0;
[Real::cos(t), Real::sin(t)]
})
.collect();
assert!((winding_turns(&hexagon) - 1.0).abs() < 1e-12, "winds once");
let (p, idx) = cap_fan(&hexagon);
let r = validate_indexed(&p, &idx, &cfg());
assert_eq!(r.faces, 6);
assert_eq!(r.inconsistently_oriented_edges, 0);
assert_eq!(r.non_manifold_edges, 0);
assert_eq!(r.degenerate_triangles, 0);
assert_eq!(r.boundary_edges, 6, "the rim, and only the rim");
}
#[test]
fn a_fan_that_reverses_its_sweep_is_caught_by_orientation_and_missed_by_self_intersection() {
let folded = [
[2.0, 0.0],
[0.0, 2.0],
[-2.0, 0.0],
[0.0, -2.0],
[-1.0, -1.0],
];
assert!(
(winding_turns(&folded) - 1.0).abs() < 1e-12,
"the loop still winds exactly once -- the defect is the reversal, not a double cover"
);
let (p, idx) = cap_fan(&folded);
let r = validate_indexed(&p, &idx, &cfg());
let si = self_intersections(&p, &idx, 1.0).expect("valid cell size");
std::println!(
"measured: T-020 reversing fold -- inconsistently_oriented_edges {}, \
self-intersections {} from {} tested, {} skipped for sharing the apex",
r.inconsistently_oriented_edges,
si.count(),
si.tested_pairs,
si.adjacent_pairs_skipped
);
assert_eq!(r.inconsistently_oriented_edges, 2);
assert_eq!(si.count(), 0);
assert_eq!(si.tested_pairs, 0);
assert_eq!(
si.adjacent_pairs_skipped, 10,
"5 choose 2, all sharing apex"
);
assert_eq!(r.non_manifold_edges, 0, "a fold is not a non-manifold edge");
assert_eq!(r.degenerate_triangles, 0);
}
#[test]
fn a_star_polygon_fan_double_covers_its_disk_and_both_counters_report_zero() {
let pentagram: Vec<[f64; 2]> = (0..5)
.map(|k| {
let t = 2.0 * core::f64::consts::PI * f64::from(2 * k) / 5.0;
[Real::cos(t), Real::sin(t)]
})
.collect();
let turns = winding_turns(&pentagram);
assert!(
(turns - 2.0).abs() < 1e-12,
"the boundary winds twice, so the cap covers its disk twice: {turns}"
);
let (p, idx) = cap_fan(&pentagram);
let r = validate_indexed(&p, &idx, &cfg());
let si = self_intersections(&p, &idx, 1.0).expect("valid cell size");
std::println!(
"measured: T-020 star polygon -- winding {turns}, \
inconsistently_oriented_edges {}, self-intersections {} from {} tested",
r.inconsistently_oriented_edges,
si.count(),
si.tested_pairs
);
assert_eq!(
r.inconsistently_oriented_edges, 0,
"no slice is flipped, so no spoke changes flip state"
);
assert_eq!(si.count(), 0);
assert_eq!(si.tested_pairs, 0);
}
fn clean_report() -> MeshReport {
MeshReport {
vertices: 4,
referenced_vertices: 4,
edges: 6,
faces: 4,
faces_skipped: 0,
euler_characteristic: 2,
components: 1,
boundary_loops: 0,
genus: Some(0),
non_manifold_edges: 0,
non_manifold_vertices: 0,
boundary_edges: 0,
inconsistently_oriented_edges: 0,
degenerate_triangles: 0,
repeated_index_triangles: 0,
duplicate_vertices: 0,
unreferenced_vertices: 0,
weld_buckets: 4,
out_of_range_indices: 0,
trailing_indices: 0,
non_finite_positions: 0,
normal_count_mismatch: false,
config: ValidateConfig::from_cell_size(1.0).expect("valid cell size"),
}
}
#[test]
fn a_closed_solid_satisfies_every_gate() {
let report = clean_report();
assert!(report.satisfies(SurfaceGate::Closed));
assert!(report.satisfies(SurfaceGate::Manifold));
assert!(report.satisfies(SurfaceGate::ClosedAllowingUnresolvedTopology));
}
#[test]
fn an_open_surface_fails_the_solid_gate_and_passes_the_surface_one() {
let mut report = clean_report();
report.boundary_edges = 12;
report.boundary_loops = 1;
report.euler_characteristic = 1;
assert!(
!report.satisfies(SurfaceGate::Closed),
"a surface with boundary is not a closed solid"
);
assert!(
report.satisfies(SurfaceGate::Manifold),
"and that is not a defect -- an open field is supposed to have boundary"
);
assert!(
!report.satisfies(SurfaceGate::ClosedAllowingUnresolvedTopology),
"this gate still requires the surface not to leave the grid"
);
}
#[test]
fn the_unresolved_topology_gate_waives_manifoldness_only() {
let mut report = clean_report();
report.non_manifold_edges = 3;
report.euler_characteristic = 1;
assert!(!report.satisfies(SurfaceGate::Closed));
assert!(!report.satisfies(SurfaceGate::Manifold));
assert!(
report.satisfies(SurfaceGate::ClosedAllowingUnresolvedTopology),
"a grid too coarse to resolve its field may be non-manifold"
);
let mut flipped = report;
flipped.inconsistently_oriented_edges = 1;
assert!(!flipped.satisfies(SurfaceGate::ClosedAllowingUnresolvedTopology));
let mut leaked = report;
leaked.boundary_edges = 1;
assert!(!leaked.satisfies(SurfaceGate::ClosedAllowingUnresolvedTopology));
}
#[test]
fn a_structurally_broken_mesh_satisfies_no_gate() {
let mut report = clean_report();
report.out_of_range_indices = 1;
assert!(report.has_structural_errors());
for gate in [
SurfaceGate::Closed,
SurfaceGate::Manifold,
SurfaceGate::ClosedAllowingUnresolvedTopology,
] {
assert!(
!report.satisfies(gate),
"{gate:?} must reject malformed input"
);
}
}