use bevy::log::warn;
use bevy::mesh::{Indices, Mesh, VertexAttributeValues};
use isomesh::MeshBuffer;
use isomesh::collider::{self, ColliderReadiness};
use isomesh::validate::{MeshReport, ValidateConfig};
use isomesh::weld::Welder;
use crate::mesh::FragmentGeometry;
use crate::proxy::ProxyCell;
const WELD_EPSILON: f32 = crate::soup::WELD;
const CELL_SIZE: f64 = WELD_EPSILON as f64 / ValidateConfig::WELD_EPSILON_REL;
#[derive(Clone, Debug, PartialEq)]
pub struct SolidAudit {
pub triangles: u64,
pub vertices_before_weld: u64,
pub vertices_after_weld: u64,
pub boundary_edges: u64,
pub non_manifold_edges: u64,
pub non_manifold_vertices: u64,
pub inconsistently_oriented_edges: u64,
pub euler_characteristic: i64,
pub genus: Option<i64>,
pub usable_as_trimesh: bool,
pub supports_inside_outside: bool,
pub signed_volume: f32,
}
fn signed_volume(positions: &[[f32; 3]], indices: &[u32]) -> f32 {
let mut v6 = 0.0f32;
for t in indices.chunks_exact(3) {
let (Some(a), Some(b), Some(c)) = (
positions.get(t[0] as usize),
positions.get(t[1] as usize),
positions.get(t[2] as usize),
) else {
continue;
};
let cross = [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
v6 += cross[0] * c[0] + cross[1] * c[1] + cross[2] * c[2];
}
v6 / 6.0
}
impl SolidAudit {
#[must_use]
pub fn is_closed(&self) -> bool {
self.boundary_edges == 0
}
#[must_use]
pub fn is_manifold(&self) -> bool {
self.non_manifold_edges == 0 && self.non_manifold_vertices == 0
}
#[must_use]
pub fn violations(&self) -> u64 {
self.boundary_edges
+ self.non_manifold_edges
+ self.non_manifold_vertices
+ self.inconsistently_oriented_edges
}
}
fn append(buf: &mut MeshBuffer<f32>, mesh: &Mesh) -> bool {
let Some(VertexAttributeValues::Float32x3(pos)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) else {
warn!("carnage: audit skipped a fragment mesh with no Float32x3 POSITION");
return false;
};
let Some(VertexAttributeValues::Float32x3(nrm)) = mesh.attribute(Mesh::ATTRIBUTE_NORMAL) else {
warn!("carnage: audit skipped a fragment mesh with no Float32x3 NORMAL");
return false;
};
if nrm.len() != pos.len() {
warn!(
"carnage: audit skipped a fragment mesh whose NORMAL count ({}) differs from its POSITION \
count ({})",
nrm.len(),
pos.len()
);
return false;
}
let base = buf.positions.len() as u32;
buf.positions.extend_from_slice(pos);
buf.normals.extend_from_slice(nrm);
match mesh.indices() {
Some(Indices::U32(v)) => buf.indices.extend(v.iter().map(|i| i + base)),
Some(Indices::U16(v)) => buf.indices.extend(v.iter().map(|i| u32::from(*i) + base)),
None => {
warn!("carnage: audit skipped a non-indexed fragment mesh");
return false;
}
}
true
}
pub fn audit_render(frag: &FragmentGeometry) -> Result<SurfaceReport, String> {
let mut buf: MeshBuffer<f32> = MeshBuffer::new();
if let Some(outer) = frag.outer.as_ref() {
append(&mut buf, outer);
}
if let Some(cap) = frag.cap.as_ref() {
append(&mut buf, cap);
}
if buf.indices.is_empty() {
return Err("fragment has no drawable triangles to audit".to_string());
}
let vertices_before_weld = buf.positions.len() as u64;
let report = weld_then_validate(&mut buf)?;
Ok(SurfaceReport {
triangles: report.faces,
vertices_before_weld,
vertices_after_weld: buf.positions.len() as u64,
open_edges: report.boundary_edges,
non_manifold_edges: report.non_manifold_edges,
non_manifold_vertices: report.non_manifold_vertices,
inconsistently_oriented_edges: report.inconsistently_oriented_edges,
})
}
#[derive(Clone, Debug, PartialEq)]
pub struct SurfaceReport {
pub triangles: u64,
pub vertices_before_weld: u64,
pub vertices_after_weld: u64,
pub open_edges: u64,
pub non_manifold_edges: u64,
pub non_manifold_vertices: u64,
pub inconsistently_oriented_edges: u64,
}
pub(crate) fn audit_buffer(mut buf: MeshBuffer<f32>) -> Result<SolidAudit, String> {
let vertices_before_weld = buf.positions.len() as u64;
let report = weld_then_validate(&mut buf)?;
let readiness = collider::from_report(&report);
Ok(SolidAudit {
triangles: report.faces,
vertices_before_weld,
vertices_after_weld: buf.positions.len() as u64,
boundary_edges: report.boundary_edges,
non_manifold_edges: report.non_manifold_edges,
non_manifold_vertices: report.non_manifold_vertices,
inconsistently_oriented_edges: report.inconsistently_oriented_edges,
euler_characteristic: report.euler_characteristic,
genus: report.genus,
usable_as_trimesh: readiness.is_usable(),
supports_inside_outside: ColliderReadiness::supports_inside_outside(&readiness),
signed_volume: signed_volume(&buf.positions, &buf.indices),
})
}
fn weld_then_validate(buf: &mut MeshBuffer<f32>) -> Result<MeshReport, String> {
let mut welder = Welder::<f32>::new();
welder
.weld(buf, WELD_EPSILON)
.map_err(|e| format!("weld rejected epsilon {WELD_EPSILON}: {e}"))?;
let cfg = ValidateConfig::from_cell_size(CELL_SIZE)
.map_err(|e| format!("audit cell size {CELL_SIZE} is not a usable length scale: {e}"))?;
Ok(isomesh::validate::validate_indexed(&buf.positions, &buf.indices, &cfg))
}
pub fn audit_proxy(frag: &FragmentGeometry) -> Result<SolidAudit, String> {
audit_cell(&frag.cell)
}
pub fn audit_cell(cell: &ProxyCell) -> Result<SolidAudit, String> {
let soup = crate::mesh::proxy_soup(cell);
let mesh = crate::mesh::soup_to_mesh_all_faces(&soup)?;
let mut buf: MeshBuffer<f32> = MeshBuffer::new();
if !append(&mut buf, &mesh) {
return Err("the proxy cell produced no auditable triangles".to_string());
}
audit_buffer(buf)
}
#[must_use]
pub fn audit_proxies(frags: &[FragmentGeometry]) -> Vec<SolidAudit> {
frags
.iter()
.enumerate()
.filter_map(|(i, f)| match audit_proxy(f) {
Ok(a) => Some(a),
Err(e) => {
warn!("carnage: fragment {i} could not be audited: {e}");
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CutSettings;
use crate::mesh::fracture_mesh;
use crate::proxy::ProxyCell;
use bevy::math::{Mat4, Vec3, primitives::Cuboid};
use isomesh::validate::check_determinism;
fn cube_parts() -> (Mesh, Vec<ProxyCell>) {
(
Mesh::from(Cuboid::new(1.0, 2.0, 1.0)),
vec![ProxyCell::from_box(Vec3::ZERO, Vec3::new(0.5, 1.0, 0.5))],
)
}
fn torso_and_head() -> ([Mesh; 2], Vec<ProxyCell>) {
(
[Mesh::from(Cuboid::new(0.6, 1.0, 0.35)), Mesh::from(Cuboid::new(0.34, 0.34, 0.34))],
vec![
ProxyCell::from_box(Vec3::ZERO, Vec3::new(0.3, 0.5, 0.175)),
ProxyCell::from_box(Vec3::new(0.0, 0.67, 0.0), Vec3::splat(0.17)),
],
)
}
fn torso_and_head_fracture(parts: &[Mesh; 2], proxy: &[ProxyCell]) -> Vec<FragmentGeometry> {
let placed = [
(&parts[0], Mat4::IDENTITY),
(&parts[1], Mat4::from_translation(Vec3::new(0.0, 0.67, 0.0))),
];
fracture_mesh(&placed, proxy, &CutSettings::new(12, 0.15, 0x00C0_FFEE)).into_leaves()
}
#[test]
fn every_fragment_is_closed_at_every_seed_and_jitter() {
let (cube, proxy) = cube_parts();
for (jitter, size_spread) in [(0.0f32, 0.0f32), (0.35, 0.5), (0.6, 1.0)] {
for seed in 0..60u32 {
let cut = CutSettings {
plane_jitter: jitter,
size_spread,
..CutSettings::new(10, 0.04, seed.wrapping_mul(2_654_435_761))
};
let pieces = fracture_mesh(&[(&cube, Mat4::IDENTITY)], &proxy, &cut).into_leaves();
assert!(!pieces.is_empty(), "jitter {jitter}, seed {seed}: produced nothing");
for (i, a) in audit_proxies(&pieces).into_iter().enumerate() {
assert_eq!(
a.boundary_edges, 0,
"jitter {jitter}, seed {seed}, fragment {i}: open cut — {a:?}"
);
assert!(
a.supports_inside_outside,
"jitter {jitter}, seed {seed}, fragment {i}: not a solid — {a:?}"
);
}
}
}
}
#[test]
fn the_shape_dials_widen_the_fragment_size_spread() {
let (cube, proxy) = cube_parts();
let median_ratio = |jitter: f32, size_spread: f32| -> f32 {
let mut ratios: Vec<f32> = Vec::new();
for seed in 0..60u32 {
let cut = CutSettings {
plane_jitter: jitter,
size_spread,
..CutSettings::new(12, 0.04, seed.wrapping_mul(2_654_435_761))
};
let pieces = fracture_mesh(&[(&cube, Mat4::IDENTITY)], &proxy, &cut).into_leaves();
let mut v: Vec<f32> = pieces.iter().map(|f| f.cell.volume()).collect();
v.sort_by(|a, b| a.total_cmp(b));
if v.len() >= 4 && v[0] > 0.0 {
ratios.push(v[v.len() - 1] / v[0]);
}
}
ratios.sort_by(|a, b| a.total_cmp(b));
ratios[ratios.len() / 2]
};
let flat = median_ratio(0.0, 0.0);
let default = median_ratio(0.35, 0.5);
let wide = median_ratio(0.6, 0.8);
assert!(
default > flat * 1.3,
"the shipped defaults must visibly widen the spread: {flat:.2} -> {default:.2}"
);
assert!(wide > default, "and turning them up must widen it further: {default:.2} -> {wide:.2}");
}
#[test]
fn the_shipped_mesh_shares_vertices_without_smearing_creases() {
let (parts, proxy) = torso_and_head();
let pieces = torso_and_head_fracture(&parts, &proxy);
let reports: Vec<_> = pieces.iter().filter_map(|p| audit_render(p).ok()).collect();
assert_eq!(reports.len(), 12, "every fragment should be measurable");
let shipped: u64 = reports.iter().map(|r| r.vertices_before_weld).sum();
let position_only: u64 = reports.iter().map(|r| r.vertices_after_weld).sum();
let triangles: u64 = reports.iter().map(|r| r.triangles).sum();
let per_tri = shipped as f64 / triangles as f64;
assert!(
per_tri < 2.0,
"shipped {per_tri:.2} vertices per triangle over {triangles} triangles — the weld merged \
little or nothing. It was 3.00 before AG-005; anything at or near 3 means the composite \
key stopped matching."
);
assert!(
shipped > position_only,
"the composite key merged as hard as a position-only weld ({shipped} vs {position_only}), \
which means creases are being smeared — exactly what the normal and UV terms exist to stop"
);
}
fn cape() -> Mesh {
let mut m = Mesh::new(
bevy::mesh::PrimitiveTopology::TriangleList,
bevy::asset::RenderAssetUsages::default(),
);
let z = -0.16f32;
m.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[-0.25, -0.4, z], [0.25, -0.4, z], [0.25, 0.4, z], [-0.25, 0.4, z]],
);
m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, -1.0]; 4]);
m.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
m.insert_indices(bevy::mesh::Indices::U32(vec![0, 1, 2, 0, 2, 3]));
m
}
#[test]
fn an_open_shell_survives_the_fracture_whole() {
let (parts, proxy) = torso_and_head();
let cape = cape();
let placed = [
(&parts[0], Mat4::IDENTITY),
(&parts[1], Mat4::from_translation(Vec3::new(0.0, 0.67, 0.0))),
(&cape, Mat4::IDENTITY),
];
let pieces = fracture_mesh(&placed, &proxy, &CutSettings::new(12, 0.15, 0x00C0_FFEE)).into_leaves();
assert!(!pieces.is_empty(), "the subject did not fracture");
let mut holders = 0usize;
let mut cape_tris = 0usize;
for p in &pieces {
let Some(outer) = p.outer.as_ref() else { continue };
let n = cape_triangles(outer, p.center_local);
if n > 0 {
holders += 1;
cape_tris += n;
}
}
assert_eq!(holders, 1, "the cape was split across {holders} fragments; it must ride on exactly one");
assert_eq!(cape_tris, 2, "the cape should arrive with both its triangles, got {cape_tris}");
}
fn cape_triangles(mesh: &Mesh, recenter: Vec3) -> usize {
let Some(bevy::mesh::VertexAttributeValues::Float32x3(pos)) =
mesh.attribute(Mesh::ATTRIBUTE_POSITION)
else {
return 0;
};
let Some(idx) = mesh.indices() else { return 0 };
let v: Vec<u32> = idx.iter().map(|i| i as u32).collect();
v.chunks_exact(3)
.filter(|t| {
t.iter().all(|&i| {
let p = pos[i as usize];
(p[2] + recenter.z + 0.16).abs() < 1.0e-4
})
})
.count()
}
#[test]
fn fracture_output_is_bit_identical_across_runs() {
let (cube, proxy) = cube_parts();
let report = check_determinism(|out: &mut MeshBuffer<f32>| {
let pieces = fracture_mesh(&[(&cube, Mat4::IDENTITY)], &proxy, &CutSettings::new(8, 0.05, 0xC0FF_EE00)).into_leaves();
for p in &pieces {
if let Some(m) = p.outer.as_ref() {
append(out, m);
}
if let Some(m) = p.cap.as_ref() {
append(out, m);
}
}
});
assert!(
report.is_deterministic(),
"the fracture moved between two runs of the same build: {:?}",
report.divergence
);
assert!(report.vertices > 0, "the determinism check ran on an empty mesh, so it proved nothing");
}
#[test]
fn every_proxy_fragment_of_a_closed_solid_is_closed() {
let (cube, proxy) = cube_parts();
let pieces = fracture_mesh(&[(&cube, Mat4::IDENTITY)], &proxy, &CutSettings::new(8, 0.05, 0x5EED)).into_leaves();
assert!(pieces.len() >= 2, "expected the cube to break, got {}", pieces.len());
for (i, p) in pieces.iter().enumerate() {
let a = crate::audit::audit_proxy(p).unwrap_or_else(|e| panic!("proxy {i} could not be audited: {e}"));
assert_eq!(a.boundary_edges, 0, "proxy {i} has an open cut: {a:?}");
assert!(a.is_manifold(), "proxy {i} is not a manifold: {a:?}");
assert_eq!(a.inconsistently_oriented_edges, 0, "proxy {i} has an inside-out face: {a:?}");
assert_eq!(a.euler_characteristic, 2, "proxy {i} is not a topological sphere: {a:?}");
assert!(a.supports_inside_outside, "proxy {i} is not solid enough for a collider: {a:?}");
}
}
#[test]
fn fracture_conserves_volume() {
let (cube, proxy) = cube_parts();
let pieces = fracture_mesh(&[(&cube, Mat4::IDENTITY)], &proxy, &CutSettings::new(8, 0.05, 0x5EED)).into_leaves();
let total: f32 = pieces.iter().filter_map(|p| crate::audit::audit_proxy(p).ok()).map(|a| a.signed_volume).sum();
assert!(
(total - 2.0).abs() < 1.0e-3,
"fragments enclose {total}, but the source cube encloses 2.0 — the fracture gained or lost solid"
);
}
#[test]
fn every_proxy_fragment_of_the_two_shell_subject_is_closed() {
let (parts, proxy) = torso_and_head();
let pieces = torso_and_head_fracture(&parts, &proxy);
assert_eq!(pieces.len(), 12, "expected 12 fragments, got {}", pieces.len());
let mut volume = 0.0f32;
for (i, p) in pieces.iter().enumerate() {
let a = crate::audit::audit_proxy(p).unwrap_or_else(|e| panic!("proxy {i} could not be audited: {e}"));
assert_eq!(a.boundary_edges, 0, "proxy {i} has an open cut: {a:?}");
assert!(a.is_manifold(), "proxy {i} is not a manifold: {a:?}");
assert_eq!(a.euler_characteristic, 2, "proxy {i} is not a topological sphere: {a:?}");
assert_eq!(a.inconsistently_oriented_edges, 0, "proxy {i} has an inside-out face: {a:?}");
assert!(a.supports_inside_outside, "proxy {i} is not collider-ready: {a:?}");
volume += a.signed_volume;
}
let expected = 0.6 * 1.0 * 0.35 + 0.34 * 0.34 * 0.34;
assert!(
(volume - expected).abs() < 1.0e-3,
"proxy fragments enclose {volume}, the two cells enclose {expected}"
);
}
#[test]
fn the_audit_welds_before_it_measures() {
let (cube, proxy) = cube_parts();
let pieces = fracture_mesh(&[(&cube, Mat4::IDENTITY)], &proxy, &CutSettings::new(4, 0.05, 1)).into_leaves();
let a = audit_render(&pieces[0]).expect("the first fragment can be audited");
assert!(
a.vertices_after_weld < a.vertices_before_weld,
"the weld merged nothing ({} -> {}), so the topology counts describe an unwelded soup and mean nothing",
a.vertices_before_weld,
a.vertices_after_weld
);
}
}