use alloc::vec::Vec;
use parry3d::math::Vector;
use parry3d::shape::{TriMesh, TriMeshFlags};
use super::*;
use crate::chunk::{ChunkId, ChunkLayout};
use crate::fields::{ReferenceField, Sphere, Torus, csg_difference};
use crate::marching_cubes::MarchingCubes;
use crate::validate::ValidateConfig;
use crate::weld::Welder;
use crate::{MeshBuffer, RuntimeShape3, Sdf};
fn mesh<F: Sdf<Scalar = f32> + ReferenceField>(field: &F, samples: u32) -> (MeshBuffer<f32>, f32) {
let (lo, hi) = field.domain();
let h = (hi[0] - lo[0]) / (samples - 1) as f32;
let shape = RuntimeShape3::new([samples; 3]).expect("valid shape");
let mut out = MeshBuffer::<f32>::new();
MarchingCubes::<f32>::new()
.extract(field, &shape, lo, h, &mut out)
.expect("extraction");
(out, h)
}
fn config(h: f32) -> ValidateConfig {
ValidateConfig::from_cell_size(f64::from(h)).expect("valid cell size")
}
fn to_parry(mesh: &MeshBuffer<f32>) -> Result<TriMesh, parry3d::shape::TriMeshBuilderError> {
let vertices: Vec<Vector> = mesh
.positions
.iter()
.map(|p| Vector::new(p[0], p[1], p[2]))
.collect();
TriMesh::new(vertices, triangle_indices(mesh))
}
#[test]
fn a_carved_shape_builds_a_parry_trimesh() {
let field = csg_difference::<f32>();
let (lo, hi) = field.domain();
let samples = 41u32;
let h = (hi[0] - lo[0]) / (samples - 1) as f32;
let shape = RuntimeShape3::new([samples; 3]).expect("valid shape");
let mut buffer = MeshBuffer::<f32>::new();
MarchingCubes::<f32>::new()
.extract(&field, &shape, lo, h, &mut buffer)
.expect("extraction");
let ready = readiness(&buffer, &config(h));
std::println!(
"carved csg_difference at {samples}^3: {} triangles, {} degenerate, {} duplicate vertices, \
usable {}, inside/outside {}",
ready.triangles,
ready.degenerate_triangles,
ready.duplicate_vertices,
ready.is_usable(),
ready.supports_inside_outside(),
);
assert!(ready.is_usable(), "{ready:?}");
let mut trimesh = to_parry(&buffer).expect("parry accepts the mesh");
assert_eq!(trimesh.num_triangles() as u64, ready.triangles);
trimesh
.set_flags(TriMeshFlags::HALF_EDGE_TOPOLOGY)
.expect("parry builds a half-edge topology");
assert!(trimesh.topology().is_some());
assert!(ready.supports_inside_outside(), "{ready:?}");
trimesh
.set_flags(TriMeshFlags::ORIENTED)
.expect("parry orients the mesh");
assert!(trimesh.pseudo_normals_if_oriented().is_some());
}
#[test]
fn parry_only_refuses_an_empty_index_buffer() {
let empty = MeshBuffer::<f32>::new();
assert!(to_parry(&empty).is_err(), "parry accepted an empty mesh");
let mut degenerate = MeshBuffer::<f32>::new();
for p in [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]] {
degenerate.positions.push(p);
degenerate.normals.push([0.0, 0.0, 1.0]);
}
degenerate.indices.extend_from_slice(&[0, 1, 2]);
assert!(
to_parry(°enerate).is_ok(),
"parry refused a degenerate triangle, so this module's premise is wrong"
);
let ready = readiness(°enerate, &config(1.0));
assert_eq!(ready.degenerate_triangles, 1, "{ready:?}");
std::println!(
"parry accepted a zero-area triangle; readiness reports {} degenerate",
ready.degenerate_triangles
);
}
#[test]
fn an_unwelded_seam_is_reported_and_welding_fixes_it() {
let field = Torus::<f32>::canonical();
let layout = ChunkLayout::<f32>::new(16, 0.125, [-1.0, -1.0, -1.0]).expect("valid layout");
let shape = layout.sample_shape().expect("valid shape");
let mut joined = MeshBuffer::<f32>::new();
for id in [ChunkId::new([0, 0, 0]), ChunkId::new([1, 0, 0])] {
let mut chunk = MeshBuffer::<f32>::new();
MarchingCubes::<f32>::new()
.extract(
&field,
&shape,
layout.sample_origin(id),
layout.cell_size(),
&mut chunk,
)
.expect("extraction");
joined
.append(&chunk)
.expect("the meshes fit the u32 index space");
}
let cfg = config(layout.cell_size());
let before = readiness(&joined, &cfg);
std::println!(
"two chunks unwelded: {} duplicate vertices, {} boundary edges, seam-free {}",
before.duplicate_vertices,
before.boundary_edges,
before.is_seam_free()
);
assert!(
before.is_usable(),
"an unwelded seam is still structurally fine"
);
assert!(
!before.is_seam_free(),
"two chunks met with no duplicate vertices at all — the fixture is not exercising a seam"
);
let taken = to_parry(&joined);
assert!(
taken.is_ok(),
"parry refused the unwelded mesh, which would make this a non-issue"
);
let mut welder = Welder::<f32>::new();
welder
.weld(&mut joined, crate::weld::epsilon_for(layout.cell_size()))
.expect("weld");
let after = readiness(&joined, &cfg);
std::println!(
"after welding: {} duplicate vertices, {} boundary edges, seam-free {}",
after.duplicate_vertices,
after.boundary_edges,
after.is_seam_free()
);
assert!(after.is_seam_free(), "{after:?}");
assert!(
after.boundary_edges < before.boundary_edges,
"welding did not close any boundary: {before:?} -> {after:?}"
);
assert!(to_parry(&joined).is_ok());
}
#[test]
fn only_a_closed_surface_supports_an_inside_outside_query() {
let (whole, h) = mesh(&Sphere::<f32>::canonical(), 33);
let ready = readiness(&whole, &config(h));
assert!(ready.is_usable(), "{ready:?}");
assert!(ready.supports_inside_outside(), "{ready:?}");
assert_eq!(ready.boundary_edges, 0);
let field = Torus::<f32>::canonical();
let layout = ChunkLayout::<f32>::new(16, 0.125, [-1.0, -1.0, -1.0]).expect("valid layout");
let shape = layout.sample_shape().expect("valid shape");
let mut chunk = MeshBuffer::<f32>::new();
MarchingCubes::<f32>::new()
.extract(
&field,
&shape,
layout.sample_origin(ChunkId::new([0, 0, 0])),
layout.cell_size(),
&mut chunk,
)
.expect("extraction");
let ready = readiness(&chunk, &config(layout.cell_size()));
std::println!(
"one chunk: {} triangles, {} boundary edges, usable {}, inside/outside {}",
ready.triangles,
ready.boundary_edges,
ready.is_usable(),
ready.supports_inside_outside()
);
assert!(ready.is_usable(), "a chunk is still a usable collider");
assert!(
ready.boundary_edges > 0 && !ready.supports_inside_outside(),
"a chunk of a larger field must be open: {ready:?}"
);
}
#[test]
fn triangle_indices_regroups_without_changing_anything() {
let (buffer, _) = mesh(&Sphere::<f32>::canonical(), 17);
let triples = triangle_indices(&buffer);
assert_eq!(triples.len(), buffer.triangle_count());
let flat: Vec<u32> = triples.iter().flat_map(|t| t.iter().copied()).collect();
assert_eq!(flat, buffer.indices);
}
#[test]
fn a_trailing_partial_triangle_is_dropped_and_reported() {
let (mut buffer, h) = mesh(&Sphere::<f32>::canonical(), 17);
let complete = buffer.triangle_count();
buffer.indices.push(0);
buffer.indices.push(1);
assert_eq!(triangle_indices(&buffer).len(), complete);
let ready = readiness(&buffer, &config(h));
assert_eq!(ready.trailing_indices, 2, "{ready:?}");
assert!(
!ready.is_usable(),
"a trailing index means the caller built the buffer wrong: {ready:?}"
);
}
#[test]
fn from_report_agrees_with_readiness() {
let (buffer, h) = mesh(&Torus::<f32>::canonical(), 25);
let cfg = config(h);
let direct = readiness(&buffer, &cfg);
let report = crate::validate::validate_indexed(&buffer.positions, &buffer.indices, &cfg);
assert_eq!(direct, from_report(&report));
}
#[test]
fn a_bowtie_vertex_fails_readiness_that_every_edge_counter_passes() {
let tetra = [
[1.0f32, 1.0, 1.0],
[1.0, -1.0, -1.0],
[-1.0, 1.0, -1.0],
[-1.0, -1.0, 1.0],
];
let mut positions = Vec::from(tetra);
for v in &tetra[1..] {
positions.push([
2.0 * tetra[0][0] - v[0],
2.0 * tetra[0][1] - v[1],
2.0 * tetra[0][2] - v[2],
]);
}
let indices = alloc::vec![
0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2, 0, 5, 4, 0, 6, 5, 0, 4, 6, 4, 5, 6, ];
let report = crate::validate::validate_indexed(&positions, &indices, &config(1.0));
let r = from_report(&report);
std::println!(
"measured: T-021 bowtie -- chi {}, boundary {}, nm_edges {}, nm_vertices {}, oriented {}",
report.euler_characteristic,
r.boundary_edges,
r.non_manifold_edges,
r.non_manifold_vertices,
r.inconsistently_oriented_edges
);
assert_eq!(r.boundary_edges, 0, "the surface is closed");
assert_eq!(r.non_manifold_edges, 0, "every edge has exactly two faces");
assert_eq!(
r.inconsistently_oriented_edges, 0,
"both cones face outward"
);
assert!(r.is_usable());
assert_eq!(r.non_manifold_vertices, 1, "the shared apex, and only it");
assert!(
!r.supports_inside_outside(),
"a bowtie has no single normal at the apex"
);
}