use std::collections::{HashMap, HashSet};
use bevy::asset::AssetPath;
use bevy::prelude::*;
use crate::bore::Bore;
use crate::FractureSettings;
use crate::bond::BondGraph;
use crate::mesh::{FragmentSolid, append_mesh, geometry_from_piece, geometry_from_soup};
use crate::order::sort_total_by_key_at;
use crate::proxy::ProxyCell;
use crate::soup::{Soup, fracture};
use crate::tree::{FragmentId, FragmentTree};
#[derive(Component)]
pub struct FractureSubject(pub Handle<WorldAsset>);
#[derive(Component)]
pub struct FractureProxy(pub Vec<ProxyCell>);
#[derive(Component, Default, Clone, Debug)]
pub struct FractureBores(pub Vec<Bore>);
#[derive(Component)]
pub struct DetachedPart;
pub struct Fragment {
pub id: FragmentId,
pub outer_mesh: Option<Handle<Mesh>>,
pub cap_mesh: Option<Handle<Mesh>>,
pub cell: ProxyCell,
pub center_local: Vec3,
pub half_extents: Vec3,
}
pub struct DetachedChunk {
pub mesh: Handle<Mesh>,
pub material: Handle<StandardMaterial>,
pub center_local: Vec3,
pub half_extents: Vec3,
}
pub struct EjectaChunk {
pub outer_mesh: Option<Handle<Mesh>>,
pub cap_mesh: Option<Handle<Mesh>>,
pub cell: ProxyCell,
pub center_local: Vec3,
pub half_extents: Vec3,
pub exit: Vec3,
pub direction: Vec3,
}
#[derive(Resource, Default)]
pub struct FractureCache {
body: HashMap<AssetId<WorldAsset>, Vec<Option<Fragment>>>,
solids: HashMap<AssetId<WorldAsset>, Vec<FragmentSolid>>,
pending: HashMap<AssetId<WorldAsset>, Vec<Option<crate::soup::Piece>>>,
wanted: HashMap<AssetId<WorldAsset>, Vec<FragmentId>>,
trees: HashMap<AssetId<WorldAsset>, FragmentTree>,
graphs: HashMap<AssetId<WorldAsset>, BondGraph>,
detached: HashMap<AssetId<WorldAsset>, DetachedChunk>,
ejecta: HashMap<AssetId<WorldAsset>, Vec<EjectaChunk>>,
baked: HashSet<AssetId<WorldAsset>>,
}
impl FractureCache {
pub fn fragments(&self, source: AssetId<WorldAsset>) -> Option<&[Option<Fragment>]> {
self.body.get(&source).map(|v| v.as_slice())
}
pub fn solids(&self, source: AssetId<WorldAsset>) -> &[FragmentSolid] {
self.solids.get(&source).map_or(&[], |v| v.as_slice())
}
pub fn request(&mut self, source: AssetId<WorldAsset>, ids: &[FragmentId]) {
let Some(body) = self.body.get(&source) else { return };
let queue = self.wanted.entry(source).or_default();
for id in ids {
if body.get(id.index()).is_none_or(Option::is_some) {
continue; }
if !queue.contains(id) {
queue.push(*id);
}
}
}
pub fn ready(&self, source: AssetId<WorldAsset>, ids: &[FragmentId]) -> bool {
let Some(body) = self.body.get(&source) else { return false };
ids.iter().all(|id| body.get(id.index()).is_some_and(Option::is_some))
}
pub fn tree(&self, source: AssetId<WorldAsset>) -> Option<&FragmentTree> {
self.trees.get(&source)
}
pub fn bonds(&self, source: AssetId<WorldAsset>) -> Option<&BondGraph> {
self.graphs.get(&source)
}
pub fn leaves(&self, source: AssetId<WorldAsset>) -> Vec<&Fragment> {
self.pick(source, |t| t.leaves())
}
pub fn frontier_of(&self, source: AssetId<WorldAsset>, count: usize) -> Vec<&Fragment> {
self.pick(source, |t| t.frontier_of(count))
}
pub fn at_depth(&self, source: AssetId<WorldAsset>, depth: u16) -> Vec<&Fragment> {
self.pick(source, |t| t.at_depth(depth))
}
fn pick<F>(&self, source: AssetId<WorldAsset>, choose: F) -> Vec<&Fragment>
where
F: FnOnce(&FragmentTree) -> Vec<FragmentId>,
{
let (Some(frags), Some(tree)) = (self.body.get(&source), self.trees.get(&source)) else {
return Vec::new();
};
choose(tree).into_iter().filter_map(|id| frags.get(id.index())?.as_ref()).collect()
}
pub fn detached_chunk(&self, source: AssetId<WorldAsset>) -> Option<&DetachedChunk> {
self.detached.get(&source)
}
pub fn ejecta(&self, source: AssetId<WorldAsset>) -> &[EjectaChunk] {
self.ejecta.get(&source).map_or(&[], |v| v.as_slice())
}
pub fn is_baked(&self, source: AssetId<WorldAsset>) -> bool {
self.baked.contains(&source)
}
}
fn seed_from_path(path: &AssetPath) -> u32 {
let mut h: u32 = 0x811c_9dc5;
for b in path.to_string().as_bytes() {
h ^= u32::from(*b);
h = h.wrapping_mul(0x0100_0193);
}
h
}
fn build_fragment(id: FragmentId, piece: crate::soup::Piece, meshes: &mut Assets<Mesh>) -> Fragment {
let g = geometry_from_piece(id, piece);
Fragment {
id: g.id,
outer_mesh: g.outer.map(|m| meshes.add(m)),
cap_mesh: g.cap.map(|m| meshes.add(m)),
cell: g.cell,
center_local: g.center_local,
half_extents: g.half_extents,
}
}
fn build_ejecta(e: crate::soup::Ejected, meshes: &mut Assets<Mesh>) -> EjectaChunk {
let g = crate::mesh::ejecta_from_piece(e);
EjectaChunk {
outer_mesh: g.outer.map(|m| meshes.add(m)),
cap_mesh: g.cap.map(|m| meshes.add(m)),
cell: g.cell,
center_local: g.center_local,
half_extents: g.half_extents,
exit: g.exit,
direction: g.direction,
}
}
fn bake_detached(
part: &Soup,
material: Option<Handle<StandardMaterial>>,
meshes: &mut Assets<Mesh>,
) -> Option<DetachedChunk> {
let g = geometry_from_soup(part)?;
let material = material?;
let mesh = g.outer.or(g.cap).map(|m| meshes.add(m))?;
Some(DetachedChunk { mesh, material, center_local: g.center_local, half_extents: g.half_extents })
}
pub fn bake_fractures(
mut cache: ResMut<FractureCache>,
mut meshes: ResMut<Assets<Mesh>>,
settings: Res<FractureSettings>,
subjects: Query<(&FractureSubject, &FractureProxy, &Children, Option<&FractureBores>)>,
children_q: Query<&Children>,
transforms: Query<&Transform>,
mesh_q: Query<&Mesh3d>,
mat_q: Query<&MeshMaterial3d<StandardMaterial>>,
is_detached: Query<(), With<DetachedPart>>,
) {
for (subject, proxy, children, bores) in &subjects {
let source = subject.0.id();
if cache.baked.contains(&source) {
continue;
}
let Some(asset_path) = subject.0.path().map(|p| p.clone_owned()) else {
error!(
"carnage: a FractureSubject handle has no asset path — refusing to bake a fracture \
whose seed would depend on asset load order. No fragments for this source."
);
continue;
};
let mut body = Soup::default();
let mut part = Soup::default();
let mut part_material: Option<Handle<StandardMaterial>> = None;
let mut all_loaded = true;
let mut stack: Vec<(Entity, Mat4, bool)> = Vec::new();
for child in children.iter() {
let m = transforms.get(child).map(|t| t.to_matrix()).unwrap_or(Mat4::IDENTITY);
stack.push((child, m, is_detached.get(child).is_ok()));
}
let mut parts: Vec<(String, [u32; 16], Mat4, bool, Entity, Handle<Mesh>)> = Vec::new();
let mut unpathed_mesh = false;
while let Some((e, mat, in_part)) = stack.pop() {
if let Ok(mesh3d) = mesh_q.get(e) {
if meshes.get(&mesh3d.0).is_some() {
let mut bits = [0u32; 16];
for (i, v) in mat.to_cols_array().iter().enumerate() {
bits[i] = v.to_bits();
}
match mesh3d.0.path() {
Some(path) => {
parts.push((path.to_string(), bits, mat, in_part, e, mesh3d.0.clone()))
}
None => unpathed_mesh = true,
}
} else {
all_loaded = false; }
}
if let Ok(ch) = children_q.get(e) {
for child in ch.iter() {
let ct = transforms.get(child).map(|t| t.to_matrix()).unwrap_or(Mat4::IDENTITY);
let child_part = in_part || is_detached.get(child).is_ok();
stack.push((child, mat * ct, child_part));
}
}
}
if unpathed_mesh {
error!(
"carnage: a sub-mesh of {asset_path} has no asset path — refusing to assemble a vertex \
soup whose order would depend on asset load order. No fragments for this source."
);
continue;
}
sort_total_by_key_at(
concat!(file!(), ":", line!()),
&mut parts,
|p: &(String, [u32; 16], Mat4, bool, Entity, Handle<Mesh>)| (p.0.clone(), p.1),
);
for (_, _, mat, in_part, e, mesh_handle) in parts {
let Some(m) = meshes.get(&mesh_handle) else { continue };
if in_part {
append_mesh(&mut part, m, mat, false);
if part_material.is_none() {
part_material = mat_q.get(e).ok().map(|mm| mm.0.clone());
}
} else {
append_mesh(&mut body, m, mat, false);
}
}
if !all_loaded || body.is_empty() || part.is_empty() {
continue;
}
let ext = body.extent();
if ext <= 1.0e-5 {
warn!("carnage: source body is degenerate (zero extent); marking baked with no fragments");
cache.body.insert(source, Vec::new());
cache.baked.insert(source);
continue;
}
if proxy.0.is_empty() {
error!("carnage: {asset_path} has no FractureProxy cells; refusing to bake");
continue;
}
let ref_ext = settings.ref_extent.max(1.0e-4);
let raw = (settings.pieces_base as f32 * (ext / ref_ext)).round() as i32;
let target = raw.clamp(settings.min_pieces, settings.max_pieces).max(1) as usize;
let bores = bores.map(|b| b.0.clone()).unwrap_or_default();
let (pieces, tree, ejected) =
fracture(body, &proxy.0, &settings.cut_for(target, seed_from_path(&asset_path), bores));
let graph = crate::mesh::bond_graph(&pieces, &tree);
let solids: Vec<FragmentSolid> = pieces
.iter()
.enumerate()
.map(|(i, p)| FragmentSolid { id: FragmentId(i as u32), cell: p.cell.clone() })
.collect();
let node_count = solids.len();
let plugs: Vec<EjectaChunk> =
ejected.into_iter().map(|e| build_ejecta(e, &mut meshes)).collect();
info!(
"carnage: baked {} fragments for {asset_path} ({} in the finest frontier, {} cuts, \
{} bonds, {} ejected plug(s))",
node_count,
tree.leaves().len(),
tree.cuts(),
graph.len(),
plugs.len()
);
let leaves = tree.leaves();
cache.body.insert(source, (0..node_count).map(|_| None).collect());
cache.solids.insert(source, solids);
cache.pending.insert(source, pieces.into_iter().map(Some).collect());
cache.trees.insert(source, tree);
cache.graphs.insert(source, graph);
cache.ejecta.insert(source, plugs);
cache.request(source, &leaves);
if let Some(chunk) = bake_detached(&part, part_material, &mut meshes) {
cache.detached.insert(source, chunk);
}
cache.baked.insert(source);
}
}
pub fn materialise_fragments(mut cache: ResMut<FractureCache>, mut meshes: ResMut<Assets<Mesh>>) {
let sources: Vec<AssetId<WorldAsset>> =
cache.wanted.iter().filter(|(_, ids)| !ids.is_empty()).map(|(s, _)| *s).collect();
for source in sources {
let Some(ids) = cache.wanted.remove(&source) else { continue };
let mut pending = cache.pending.remove(&source).unwrap_or_default();
let mut body = cache.body.remove(&source).unwrap_or_default();
for id in ids {
let i = id.index();
if body.get(i).is_none_or(Option::is_some) {
continue; }
let Some(piece) = pending.get_mut(i).and_then(Option::take) else { continue };
body[i] = Some(build_fragment(id, piece, &mut meshes));
}
cache.pending.insert(source, pending);
cache.body.insert(source, body);
}
}