use std::collections::HashMap;
use bevy::math::Vec3;
use crate::proxy::ProxyCell;
use crate::soup::plane_basis;
use crate::tree::FragmentId;
const PLANE_EPS: f32 = crate::soup::WELD;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BondId(pub u32);
impl BondId {
pub fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bond {
pub a: FragmentId,
pub b: FragmentId,
pub centroid: Vec3,
pub normal: Vec3,
pub area: f32,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BondGraph {
bonds: Vec<Bond>,
incident: Vec<Vec<BondId>>,
centers: Vec<Option<Vec3>>,
members: Vec<FragmentId>,
}
impl BondGraph {
pub fn of(members: &[(FragmentId, &ProxyCell)], capacity: usize) -> BondGraph {
struct Face {
member: usize,
face: usize,
normal: Vec3,
d: f32,
}
let mut faces: Vec<Face> = Vec::new();
for (m, (_, cell)) in members.iter().enumerate() {
for fi in 0..cell.face_count() {
let Some((origin, normal)) = cell.face_plane(fi) else { continue };
faces.push(Face { member: m, face: fi, normal, d: -normal.dot(origin) });
}
}
faces.sort_unstable_by(|x, y| {
x.d.abs().total_cmp(&y.d.abs()).then((x.member, x.face).cmp(&(y.member, y.face)))
});
struct Patch {
centroid: Vec3,
normal: Vec3,
area: f32,
}
let mut found: HashMap<(usize, usize), Patch> = HashMap::new();
for i in 0..faces.len() {
for j in i + 1..faces.len() {
if (faces[j].d.abs() - faces[i].d.abs()).abs() > PLANE_EPS {
break;
}
let (x, y) = (&faces[i], &faces[j]);
if x.member == y.member {
continue;
}
if x.normal.dot(y.normal) > -1.0 + 1.0e-4 || (x.d + y.d).abs() > PLANE_EPS {
continue;
}
let ring_x = members[x.member].1.face_ring(x.face);
let ring_y = members[y.member].1.face_ring(y.face);
let Some((area, centroid)) = overlap(&ring_x, &ring_y, x.normal) else { continue };
if area <= 1.0e-9 {
continue;
}
let key = (x.member.min(y.member), x.member.max(y.member));
let toward = if x.member < y.member { x.normal } else { y.normal };
found
.entry(key)
.and_modify(|p| {
p.centroid = (p.centroid * p.area + centroid * area) / (p.area + area);
p.area += area;
})
.or_insert(Patch { centroid, normal: toward, area });
}
}
let mut pairs: Vec<((usize, usize), Patch)> = found.into_iter().collect();
pairs.sort_unstable_by_key(|&((m, n), _)| (m, n));
let mut bonds = Vec::with_capacity(pairs.len());
let mut incident = vec![Vec::new(); capacity];
for ((m, n), Patch { centroid, normal, area }) in pairs {
let (a, b) = (members[m].0, members[n].0);
let id = BondId(bonds.len() as u32);
bonds.push(Bond { a, b, centroid, normal, area });
for end in [a, b] {
if let Some(slot) = incident.get_mut(end.index()) {
slot.push(id);
}
}
}
let mut centers = vec![None; capacity];
for (id, cell) in members {
if let Some(slot) = centers.get_mut(id.index()) {
*slot = Some(cell.center());
}
}
let mut ids: Vec<FragmentId> = members.iter().map(|(id, _)| *id).collect();
ids.sort_unstable();
BondGraph { bonds, incident, centers, members: ids }
}
pub fn members(&self) -> &[FragmentId] {
&self.members
}
pub fn center(&self, fragment: FragmentId) -> Option<Vec3> {
self.centers.get(fragment.index()).copied().flatten()
}
pub fn bonds(&self) -> &[Bond] {
&self.bonds
}
pub fn len(&self) -> usize {
self.bonds.len()
}
pub fn is_empty(&self) -> bool {
self.bonds.is_empty()
}
pub fn bond(&self, id: BondId) -> Option<&Bond> {
self.bonds.get(id.index())
}
pub fn incident(&self, fragment: FragmentId) -> &[BondId] {
self.incident.get(fragment.index()).map_or(&[], |v| v.as_slice())
}
pub fn across(&self, bond: BondId, fragment: FragmentId) -> Option<FragmentId> {
let b = self.bond(bond)?;
if b.a == fragment {
Some(b.b)
} else if b.b == fragment {
Some(b.a)
} else {
None
}
}
pub fn islands(&self, members: &[FragmentId], broken: &BondSet) -> Vec<Vec<FragmentId>> {
let present: HashMap<FragmentId, usize> =
members.iter().enumerate().map(|(i, &id)| (id, i)).collect();
let mut seen = vec![false; members.len()];
let mut out: Vec<Vec<FragmentId>> = Vec::new();
for start in 0..members.len() {
if seen[start] {
continue;
}
seen[start] = true;
let mut island = vec![members[start]];
let mut stack = vec![members[start]];
while let Some(cur) = stack.pop() {
for &bid in self.incident(cur) {
if broken.is_broken(bid) {
continue;
}
let Some(next) = self.across(bid, cur) else { continue };
let Some(&slot) = present.get(&next) else { continue };
if seen[slot] {
continue;
}
seen[slot] = true;
island.push(next);
stack.push(next);
}
}
island.sort_unstable();
out.push(island);
}
out.sort_unstable_by_key(|i| i.first().copied());
out
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BondSet {
broken: Vec<bool>,
}
impl BondSet {
pub fn new(graph: &BondGraph) -> Self {
BondSet { broken: vec![false; graph.len()] }
}
pub fn sever(&mut self, id: BondId) -> bool {
match self.broken.get_mut(id.index()) {
Some(slot) if !*slot => {
*slot = true;
true
}
_ => false,
}
}
pub fn sever_all(&mut self, ids: &[BondId]) -> usize {
ids.iter().filter(|&&id| self.sever(id)).count()
}
pub fn is_broken(&self, id: BondId) -> bool {
self.broken.get(id.index()).copied().unwrap_or(false)
}
pub fn severed(&self) -> usize {
self.broken.iter().filter(|b| **b).count()
}
pub fn is_intact(&self) -> bool {
!self.broken.iter().any(|b| *b)
}
pub fn iter(&self) -> impl Iterator<Item = BondId> + '_ {
self.broken.iter().enumerate().filter(|(_, b)| **b).map(|(i, _)| BondId(i as u32))
}
}
fn overlap(ring_a: &[Vec3], ring_b: &[Vec3], normal: Vec3) -> Option<(f32, Vec3)> {
if ring_a.len() < 3 || ring_b.len() < 3 {
return None;
}
let origin = ring_a[0];
let (u, v) = plane_basis(normal);
let to_2d = |p: Vec3| {
let q = p - origin;
(q.dot(u), q.dot(v))
};
let mut a: Vec<(f32, f32)> = ring_a.iter().map(|p| to_2d(*p)).collect();
let mut b: Vec<(f32, f32)> = ring_b.iter().map(|p| to_2d(*p)).collect();
if signed_area(&a) < 0.0 {
a.reverse();
}
if signed_area(&b) < 0.0 {
b.reverse();
}
let clipped = clip_convex(&a, &b);
if clipped.len() < 3 {
return None;
}
let area = signed_area(&clipped).abs();
let (cx, cy) = centroid_2d(&clipped);
Some((area, origin + u * cx + v * cy))
}
fn signed_area(p: &[(f32, f32)]) -> f32 {
let mut s = 0.0;
for i in 0..p.len() {
let (x0, y0) = p[i];
let (x1, y1) = p[(i + 1) % p.len()];
s += x0 * y1 - x1 * y0;
}
s * 0.5
}
fn centroid_2d(p: &[(f32, f32)]) -> (f32, f32) {
let mut a = 0.0;
let (mut cx, mut cy) = (0.0, 0.0);
for i in 0..p.len() {
let (x0, y0) = p[i];
let (x1, y1) = p[(i + 1) % p.len()];
let cross = x0 * y1 - x1 * y0;
a += cross;
cx += (x0 + x1) * cross;
cy += (y0 + y1) * cross;
}
if a.abs() < 1.0e-12 {
let n = p.len().max(1) as f32;
return (p.iter().map(|q| q.0).sum::<f32>() / n, p.iter().map(|q| q.1).sum::<f32>() / n);
}
(cx / (3.0 * a), cy / (3.0 * a))
}
fn clip_convex(subject: &[(f32, f32)], window: &[(f32, f32)]) -> Vec<(f32, f32)> {
let mut out: Vec<(f32, f32)> = subject.to_vec();
for i in 0..window.len() {
if out.is_empty() {
break;
}
let (ax, ay) = window[i];
let (bx, by) = window[(i + 1) % window.len()];
let side = |(px, py): (f32, f32)| (bx - ax) * (py - ay) - (by - ay) * (px - ax);
let input = std::mem::take(&mut out);
for k in 0..input.len() {
let cur = input[k];
let prev = input[(k + input.len() - 1) % input.len()];
let (sc, sp) = (side(cur), side(prev));
if (sc >= 0.0) != (sp >= 0.0)
&& let Some(x) = intersect(prev, cur, sp, sc)
{
out.push(x);
}
if sc >= 0.0 {
out.push(cur);
}
}
}
out
}
fn intersect(p: (f32, f32), q: (f32, f32), sp: f32, sq: f32) -> Option<(f32, f32)> {
let denom = sp - sq;
if denom.abs() < 1.0e-20 {
return None;
}
let t = sp / denom;
Some((p.0 + (q.0 - p.0) * t, p.1 + (q.1 - p.1) * t))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CutSettings;
use crate::proxy::FaceKind;
use crate::soup::{Plane, fracture};
fn unit_cube_cells() -> Vec<ProxyCell> {
vec![ProxyCell::from_box(Vec3::ZERO, Vec3::splat(0.5))]
}
#[test]
fn a_single_cut_bonds_its_two_halves_over_the_full_cut_face() {
let cell = ProxyCell::from_box(Vec3::ZERO, Vec3::splat(0.5));
let (above, below) = cell.clip(&Plane { point: Vec3::ZERO, normal: Vec3::Y }, FaceKind::Cut);
let (above, below) = (above.expect("cuts"), below.expect("cuts"));
let members = [(FragmentId(0), &above), (FragmentId(1), &below)];
let g = BondGraph::of(&members, 2);
assert_eq!(g.len(), 1, "one cut, one bond");
let b = &g.bonds()[0];
assert_eq!((b.a, b.b), (FragmentId(0), FragmentId(1)));
assert!((b.area - 1.0).abs() < 1.0e-4, "the shared face is the unit square, got {}", b.area);
assert!(b.centroid.length() < 1.0e-4, "it is centred on the cut, got {:?}", b.centroid);
assert!(b.normal.dot(Vec3::Y).abs() > 0.99, "and its normal is the cut plane's");
}
#[test]
fn a_partial_face_overlap_reports_only_the_shared_area() {
let slab = ProxyCell::from_box(Vec3::new(0.0, -0.5, 0.0), Vec3::new(0.5, 0.5, 0.5));
let post = ProxyCell::from_box(Vec3::new(0.0, 0.2, 0.0), Vec3::new(0.2, 0.2, 0.2));
let members = [(FragmentId(0), &slab), (FragmentId(1), &post)];
let g = BondGraph::of(&members, 2);
assert_eq!(g.len(), 1);
let b = &g.bonds()[0];
assert!((b.area - 0.16).abs() < 1.0e-5, "expected the post's 0.4x0.4 footprint, got {}", b.area);
assert!(b.centroid.y.abs() < 1.0e-5, "the shared face is at y = 0, got {:?}", b.centroid);
}
#[test]
fn cells_that_do_not_share_a_face_are_not_bonded() {
let a = ProxyCell::from_box(Vec3::ZERO, Vec3::splat(0.5));
let b = ProxyCell::from_box(Vec3::new(0.4, 0.0, 0.0), Vec3::splat(0.5));
let c = ProxyCell::from_box(Vec3::new(9.0, 0.0, 0.0), Vec3::splat(0.5));
let members = [(FragmentId(0), &a), (FragmentId(1), &b), (FragmentId(2), &c)];
assert_eq!(BondGraph::of(&members, 3).len(), 0);
}
#[test]
fn a_baked_graph_is_symmetric_and_connected() {
let (pieces, tree, _) = fracture(crate::soup::Soup::default(), &unit_cube_cells(), &CutSettings::new(8, 0.05, 0x1234));
let leaves = tree.leaves();
let members: Vec<_> =
leaves.iter().filter_map(|&id| pieces.get(id.index()).map(|p| (id, &p.cell))).collect();
let g = BondGraph::of(&members, tree.len());
assert!(g.len() >= leaves.len() - 1, "a connected graph needs at least n-1 bonds");
for (i, b) in g.bonds().iter().enumerate() {
let id = BondId(i as u32);
assert!(b.a < b.b, "each pair is stored once, lower id first");
assert!(g.incident(b.a).contains(&id) && g.incident(b.b).contains(&id), "symmetric");
assert_eq!(g.across(id, b.a), Some(b.b));
assert_eq!(g.across(id, b.b), Some(b.a));
assert!(b.area > 0.0 && b.area.is_finite());
}
let intact = BondSet::new(&g);
assert_eq!(g.islands(&leaves, &intact).len(), 1, "nothing broken is one island");
}
#[test]
fn every_frontier_has_its_own_connected_graph() {
let (pieces, tree, _) =
fracture(crate::soup::Soup::default(), &unit_cube_cells(), &CutSettings::new(12, 0.03, 0x0FF1_CE));
assert!(tree.cuts() >= 6, "need a deep enough bake to have coarse frontiers");
for want in 2..=tree.leaves().len() {
let ids = tree.frontier_of(want);
let members: Vec<_> =
ids.iter().filter_map(|&id| pieces.get(id.index()).map(|p| (id, &p.cell))).collect();
let g = BondGraph::of(&members, tree.len());
let islands = g.islands(&ids, &BondSet::new(&g));
assert_eq!(
islands.len(),
1,
"the {want}-piece frontier came back as {} islands — it is one solid",
islands.len()
);
}
let leaf_graph = crate::mesh::bond_graph(&pieces, &tree);
let coarse = tree.frontier_of(4);
assert!(
leaf_graph.islands(&coarse, &BondSet::new(&leaf_graph)).len() > 1,
"a coarse frontier read against the leaf graph should look disconnected"
);
}
#[test]
fn severing_one_fragments_bonds_detaches_only_it() {
let (pieces, tree, _) = fracture(crate::soup::Soup::default(), &unit_cube_cells(), &CutSettings::new(8, 0.05, 0x1234));
let leaves = tree.leaves();
let members: Vec<_> =
leaves.iter().filter_map(|&id| pieces.get(id.index()).map(|p| (id, &p.cell))).collect();
let g = BondGraph::of(&members, tree.len());
let victim = *leaves
.iter()
.min_by_key(|&&id| g.incident(id).len())
.expect("the bake produced fragments");
let mut broken = BondSet::new(&g);
assert_eq!(broken.severed(), 0, "and it starts intact");
broken.sever_all(g.incident(victim));
let islands = g.islands(&leaves, &broken);
assert!(islands.len() >= 2, "the victim left, so there is more than one island");
assert!(
islands.iter().any(|i| i == &vec![victim]),
"and it left alone, not dragging neighbours with it"
);
let total: usize = islands.iter().map(|i| i.len()).sum();
assert_eq!(total, leaves.len(), "every fragment lands in exactly one island");
}
#[test]
fn repeated_severing_only_ever_breaks_things_further() {
let (pieces, tree, _) = fracture(crate::soup::Soup::default(), &unit_cube_cells(), &CutSettings::new(8, 0.05, 0xBEEF));
let leaves = tree.leaves();
let members: Vec<_> =
leaves.iter().filter_map(|&id| pieces.get(id.index()).map(|p| (id, &p.cell))).collect();
let g = BondGraph::of(&members, tree.len());
let mut broken = BondSet::new(&g);
let mut last = g.islands(&leaves, &broken).len();
for i in 0..g.len() {
assert!(broken.sever(BondId(i as u32)), "the first severing of a bond reports true");
assert!(!broken.sever(BondId(i as u32)), "and the second reports false");
let now = g.islands(&leaves, &broken).len();
assert!(now >= last, "severing never re-joins anything: {last} -> {now}");
last = now;
}
assert_eq!(last, leaves.len(), "every bond gone is every fragment alone");
assert_eq!(broken.severed(), g.len());
assert_eq!(broken.iter().count(), g.len());
}
#[test]
fn an_out_of_range_id_is_refused() {
let g = BondGraph::default();
assert!(g.bond(BondId(7)).is_none());
assert!(g.incident(FragmentId(7)).is_empty());
assert!(g.across(BondId(7), FragmentId(0)).is_none());
let mut s = BondSet::new(&g);
assert!(!s.sever(BondId(7)), "severing a bond that does not exist changes nothing");
assert!(!s.is_broken(BondId(7)));
assert!(s.is_intact());
}
}