use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use crate::TextureLayers;
use crate::asset::Asset;
use crate::clip::ClipBinding;
use crate::components::{Instance, Mesh, Texture, TextureFile, Topology};
use crate::pass::PassLog;
use crate::world::{Entity, RebuildLog, Rebuilt, World};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Measure {
Placements,
Primitives,
Triangles,
Textures,
CollisionTriangles,
}
impl Measure {
pub const ALL: [Measure; 5] = [
Measure::Placements,
Measure::Primitives,
Measure::Triangles,
Measure::Textures,
Measure::CollisionTriangles,
];
pub const fn measured(self) -> bool {
!matches!(self, Measure::CollisionTriangles)
}
}
impl fmt::Display for Measure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Measure::Placements => "placements",
Measure::Primitives => "primitives",
Measure::Triangles => "triangles",
Measure::Textures => "textures",
Measure::CollisionTriangles => "collision triangles",
})
}
}
pub fn held(w: &World, e: Entity) -> Vec<(Measure, u64)> {
let mut out = Vec::new();
if w.has::<Instance>(e) {
out.push((Measure::Placements, 1));
}
if w.has::<Texture>(e) || w.has::<TextureLayers>(e) || w.has::<TextureFile>(e) {
out.push((Measure::Textures, 1));
}
if let Some(m) = w.get::<Mesh>(e) {
out.push((Measure::Primitives, m.primitives().len() as u64));
}
out
}
pub fn despawn(w: &mut World, e: Entity, log: &mut PassLog, why: &str) -> bool {
if !w.is_alive(e) {
return false;
}
for (m, n) in held(w, e) {
log.dropped(m, why, n);
}
let gone = w.despawn(e);
unlist(w, e, log);
unbind(w, e, log);
gone
}
pub fn unbind(w: &mut World, e: Entity, log: &mut PassLog) {
let naming: Vec<Entity> = w
.iter::<ClipBinding>()
.filter(|(_, b)| b.meshes.contains(&e))
.map(|(b, _)| b)
.collect();
for b in naming {
if let Some(binding) = w.get_mut::<ClipBinding>(b) {
let before = binding.meshes.len();
binding.meshes.retain(|m| *m != e);
log.count(
"clip binding meshes dropped with the entity they named",
(before - binding.meshes.len()) as u64,
);
}
}
}
pub fn unlist(w: &mut World, e: Entity, log: &mut PassLog) {
let listing: Vec<Entity> = w
.iter::<Asset>()
.filter(|(_, a)| a.members.iter().any(|(m, _)| *m == e))
.map(|(a, _)| a)
.collect();
for a in listing {
if let Some(asset) = w.get_mut::<Asset>(a) {
let before = asset.members.len();
asset.members.retain(|(m, _)| *m != e);
log.count(
"asset members dropped with the entity they listed",
(before - asset.members.len()) as u64,
);
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Conservation {
#[default]
Off,
Report,
Enforce,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Gap {
pub measure: Measure,
pub dropped: u64,
pub declared: u64,
}
impl fmt::Display for Gap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} left out {} but declared {}",
self.measure, self.dropped, self.declared
)
}
}
fn triangles(mesh: &Mesh) -> Vec<u64> {
mesh.primitives()
.iter()
.map(|p| match p.topology {
Topology::Triangles => (p.indices.as_ref().map_or(p.vertex_count(), Vec::len) / 3) as u64,
_ => 0,
})
.collect()
}
fn exact(
before: &[u64],
id: u64,
rebuild: Option<&Option<Rebuilt>>,
now: Option<&(u64, Vec<u64>)>,
) -> Option<(u64, u64)> {
let (now_id, now) = now?;
match rebuild {
Some(Some(r)) if r.source == id && r.made == *now_id => {
let kept: BTreeSet<usize> = r.from.iter().copied().collect();
let mut tris = 0;
for &j in &kept {
let pieces: u64 = r
.from
.iter()
.zip(now)
.filter(|(i, _)| **i == j)
.map(|(_, t)| t)
.sum();
tris += before[j].saturating_sub(pieces);
}
Some(((r.first - kept.len()) as u64, tris))
}
None if now_id == &id => Some((0, before.iter().zip(now).map(|(b, a)| b.saturating_sub(*a)).sum())),
_ => None,
}
}
#[derive(Debug)]
pub(crate) struct Before {
placements: BTreeSet<Entity>,
textures: BTreeSet<Entity>,
meshes: BTreeMap<Entity, (u64, Vec<u64>)>,
}
impl Before {
pub(crate) fn take(w: &World) -> Self {
let mut textures: BTreeSet<Entity> = w.iter::<Texture>().map(|(e, _)| e).collect();
textures.extend(w.iter::<TextureLayers>().map(|(e, _)| e));
textures.extend(w.iter::<TextureFile>().map(|(e, _)| e));
Before {
placements: w.iter::<Instance>().map(|(e, _)| e).collect(),
textures,
meshes: w
.iter::<Mesh>()
.map(|(e, m)| (e, (m.id(), triangles(m))))
.collect(),
}
}
pub(crate) fn dropped(&self, w: &World, rebuilds: &RebuildLog) -> BTreeMap<Measure, u64> {
let placements = self.placements.iter().filter(|&&e| !w.has::<Instance>(e)).count() as u64;
let textures = self
.textures
.iter()
.filter(|&&e| !w.has::<Texture>(e) && !w.has::<TextureLayers>(e) && !w.has::<TextureFile>(e))
.count() as u64;
let (mut primitives, mut tris) = (0u64, 0u64);
let after: BTreeMap<Entity, (u64, Vec<u64>)> = w
.iter::<Mesh>()
.map(|(e, m)| (e, (m.id(), triangles(m))))
.collect();
let mut gone: Vec<(u64, &Vec<u64>)> = Vec::new();
let mut matched: BTreeSet<Entity> = BTreeSet::new();
for (e, (id, before)) in &self.meshes {
match exact(before, *id, rebuilds.get(e), after.get(e)) {
Some((p, t)) => {
primitives += p;
tris += t;
matched.insert(*e);
}
None => gone.push((*id, before)),
}
}
let (mut net_before, mut net_after) = (0u64, 0u64);
let mut unmatched: Vec<(Entity, &(u64, Vec<u64>))> = after
.iter()
.filter(|(e, _)| !matched.contains(*e))
.map(|(e, a)| (*e, a))
.collect();
for (id, before) in gone {
let found = unmatched.iter().position(|(e, (now_id, _))| {
*now_id == id
|| rebuilds
.get(e)
.is_some_and(|r| r.as_ref().is_some_and(|r| r.source == id))
});
match found.and_then(|i| {
let (e, now) = unmatched[i];
exact(before, id, rebuilds.get(&e), Some(now)).map(|c| (i, c))
}) {
Some((i, (p, t))) => {
primitives += p;
tris += t;
unmatched.remove(i);
}
None => net_before += before.len() as u64,
}
}
net_after += unmatched
.iter()
.map(|(_, (_, now))| now.len() as u64)
.sum::<u64>();
primitives += net_before.saturating_sub(net_after);
BTreeMap::from([
(Measure::Placements, placements),
(Measure::Primitives, primitives),
(Measure::Triangles, tris),
(Measure::Textures, textures),
])
}
pub(crate) fn gaps(&self, w: &World, rebuilds: &RebuildLog, log: &PassLog) -> Vec<Gap> {
self.dropped(w, rebuilds)
.into_iter()
.filter(|(measure, _)| measure.measured())
.map(|(measure, dropped)| Gap {
measure,
dropped,
declared: log.declared(measure),
})
.filter(|g| g.dropped != g.declared)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::Primitive;
use crate::components::{ColorSpace, Semantic, Stream, TexRole};
use crate::pass::{Error, Pass, PassManager, PassRegistry, Pipeline, RunReport};
use crate::texformat::TexFormat;
use std::collections::BTreeMap as Map;
fn set(m: &mut Mesh, i: usize, p: Primitive) {
let mut q = m.primitive_mut(i).unwrap();
let f = q.fields();
(*f.topology, *f.streams, *f.indices, *f.material) = (p.topology, p.streams, p.indices, p.material);
}
fn prim(n: u32) -> Primitive {
Primitive {
topology: Topology::Triangles,
streams: Map::from([(Semantic::Position, Stream::F32x3(vec![[0.; 3]; 3]))]),
indices: Some((0..n).flat_map(|_| [0, 1, 2]).collect()),
material: None,
}
}
fn texture() -> Texture {
Texture {
width: 1,
height: 1,
format: TexFormat::Rgba8Unorm,
levels: vec![vec![0; 4]],
color_space: ColorSpace::Srgb,
role: TexRole::Color,
normal: None,
}
}
fn world() -> World {
let mut w = World::new();
let p = w.spawn();
w.insert(p, Mesh::new(vec![prim(1), prim(2), prim(3)]));
for _ in 0..2 {
let e = w.spawn();
w.insert(
e,
Instance {
prototype: p,
variant: 0,
},
);
w.insert(e, crate::Transform::IDENTITY);
}
let t = w.spawn();
w.insert(t, TextureFile { path: "a.png".into() });
w
}
struct Do {
f: fn(&mut World),
drops: Vec<(Measure, u64)>,
}
impl Pass for Do {
fn name(&self) -> &'static str {
"do"
}
fn removes(&self) -> &[&'static str] {
&["core.instance", "core.texture_file", "core.mesh"]
}
fn provides(&self) -> &[&'static str] {
&["core.texture", "core.mesh"]
}
fn modifies(&self) -> &[&'static str] {
&["core.mesh"]
}
fn despawns_entities(&self) -> bool {
true
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
(self.f)(w);
for &(m, n) in &self.drops {
log.dropped(m, "on purpose", n);
}
Ok(())
}
}
fn run_with(c: Conservation, f: fn(&mut World), drops: Vec<(Measure, u64)>) -> Result<RunReport, Error> {
let mut report = RunReport::default();
PassManager::new(PassRegistry::new()).with_conservation(c).run(
&mut world(),
&Pipeline {
passes: vec![Box::new(Do { f, drops })],
},
&mut report,
)?;
Ok(report)
}
fn run(f: fn(&mut World), drops: Vec<(Measure, u64)>) -> Result<RunReport, Error> {
run_with(Conservation::Enforce, f, drops)
}
fn left_out(f: fn(&mut World), drops: Vec<(Measure, u64)>) -> (Measure, u64, u64) {
match run(f, drops) {
Err(Error::Conservation {
measure,
dropped,
declared,
..
}) => (measure, dropped, declared),
other => panic!("{other:?}"),
}
}
#[test]
fn the_check_is_off_unless_asked_and_reports_or_fails_as_asked() {
let despawn = |w: &mut World| {
w.despawn(Entity::from_index(1));
};
let r = run_with(Conservation::Off, despawn, vec![]).unwrap();
assert!(r.steps[0].conservation.is_empty());
let r = run_with(Conservation::Report, despawn, vec![]).unwrap();
assert_eq!(
r.steps[0].conservation,
vec![Gap {
measure: Measure::Placements,
dropped: 1,
declared: 0
}]
);
assert!(
r.to_string().contains("!placements left out 1 but declared 0"),
"{r}"
);
assert_eq!(left_out(despawn, vec![]), (Measure::Placements, 1, 0));
}
#[test]
fn collision_drops_are_reported_not_measured() {
let nothing = |_: &mut World| {};
let r = run(nothing, vec![(Measure::CollisionTriangles, 3)]).unwrap();
assert!(r.steps[0].conservation.is_empty());
assert_eq!(r.steps[0].log.declared(Measure::CollisionTriangles), 3);
}
#[test]
fn placements_and_textures_are_counted_by_entity() {
let despawn = |w: &mut World| {
w.despawn(Entity::from_index(1));
};
run(despawn, vec![(Measure::Placements, 1)]).unwrap();
assert_eq!(
left_out(despawn, vec![(Measure::Placements, 2)]),
(Measure::Placements, 1, 2)
);
let swap = |w: &mut World| {
w.despawn(Entity::from_index(1));
let e = w.spawn();
w.insert(
e,
Instance {
prototype: Entity::from_index(0),
variant: 0,
},
);
w.insert(e, crate::Transform::IDENTITY);
};
assert_eq!(left_out(swap, vec![]), (Measure::Placements, 1, 0));
let load = |w: &mut World| {
let t = Entity::from_index(3);
w.remove::<TextureFile>(t);
w.insert(t, texture());
};
run(load, vec![]).unwrap();
let lose = |w: &mut World| {
w.remove::<TextureFile>(Entity::from_index(3));
};
assert_eq!(left_out(lose, vec![]), (Measure::Textures, 1, 0));
}
#[test]
fn a_rebuild_is_exact_so_a_split_cannot_hide_a_drop() {
let split = |w: &mut World| {
w.rebuild_primitives(Entity::from_index(0), |_| {
Ok((vec![prim(2), prim(1), prim(2)], vec![1, 2, 2]))
})
.unwrap();
};
assert_eq!(left_out(split, vec![]), (Measure::Primitives, 1, 0));
run(split, vec![(Measure::Primitives, 1)]).unwrap();
let twice = |w: &mut World| {
let e = Entity::from_index(0);
w.rebuild_primitives(e, |_| Ok((vec![prim(2), prim(1), prim(2)], vec![1, 2, 2])))
.unwrap();
w.rebuild_primitives(e, |_| Ok((vec![prim(2), prim(2)], vec![0, 2])))
.unwrap();
};
assert_eq!(
left_out(twice, vec![(Measure::Primitives, 1)]),
(Measure::Triangles, 1, 0)
);
run(twice, vec![(Measure::Primitives, 1), (Measure::Triangles, 1)]).unwrap();
}
#[test]
fn triangles_are_counted_inside_kept_primitives() {
let decimate = |w: &mut World| {
set(w.get_mut::<Mesh>(Entity::from_index(0)).unwrap(), 2, prim(1));
};
assert_eq!(left_out(decimate, vec![]), (Measure::Triangles, 2, 0));
run(decimate, vec![(Measure::Triangles, 2)]).unwrap();
}
#[test]
fn a_replaced_mesh_is_counted_net_whatever_its_primitive_count() {
let insert = |w: &mut World| {
w.insert(Entity::from_index(0), Mesh::new(vec![prim(1), prim(1), prim(1)]));
};
run(insert, vec![]).unwrap();
let assign = |w: &mut World| {
*w.get_mut::<Mesh>(Entity::from_index(0)).unwrap() = Mesh::new(vec![prim(1), prim(1), prim(1)]);
};
run(assign, vec![]).unwrap();
let between = |w: &mut World| {
let e = Entity::from_index(0);
w.rebuild_primitives(e, |_| Ok((vec![prim(2), prim(3), prim(1)], vec![1, 2, 0])))
.unwrap();
w.insert(e, Mesh::new(vec![prim(1), prim(1), prim(1)]));
w.rebuild_primitives(e, |_| Ok((vec![prim(1), prim(1)], vec![1, 2])))
.unwrap();
};
assert_eq!(left_out(between, vec![]), (Measure::Primitives, 1, 0));
run(between, vec![(Measure::Primitives, 1)]).unwrap();
let before = |w: &mut World| {
let e = Entity::from_index(0);
w.insert(e, Mesh::new(vec![prim(1), prim(1), prim(1)]));
w.rebuild_primitives(e, |_| Ok((vec![prim(1), prim(1)], vec![1, 2])))
.unwrap();
};
run(before, vec![(Measure::Primitives, 1)]).unwrap();
}
#[test]
fn an_edited_mesh_is_counted_exactly() {
let split_and_drop = |w: &mut World| {
w.edit_mesh(Entity::from_index(0), |m| {
let p = m.primitives().to_vec();
*m = Mesh::new(vec![p[1].clone(), prim(1), prim(2)]);
Ok(Some(vec![1, 2, 2]))
})
.unwrap();
};
assert_eq!(left_out(split_and_drop, vec![]), (Measure::Primitives, 1, 0));
run(split_and_drop, vec![(Measure::Primitives, 1)]).unwrap();
let in_place = |w: &mut World| {
w.edit_mesh(Entity::from_index(0), |m| {
set(m, 0, prim(0));
Ok(None)
})
.unwrap();
};
assert_eq!(left_out(in_place, vec![]), (Measure::Triangles, 1, 0));
let renewed_then_split = |w: &mut World| {
let e = Entity::from_index(0);
w.edit_mesh(e, |m| {
*m = Mesh::new(m.primitives().to_vec());
Ok(None)
})
.unwrap();
w.edit_mesh(e, |m| {
let p = m.primitives().to_vec();
*m = Mesh::new(vec![p[1].clone(), prim(1), prim(2)]);
Ok(Some(vec![1, 2, 2]))
})
.unwrap();
};
assert_eq!(left_out(renewed_then_split, vec![]), (Measure::Primitives, 1, 0));
let mut w = world();
let e = Entity::from_index(0);
assert!(w.edit_mesh(e, |_| Err("no".into())).is_err());
assert_eq!(w.get::<Mesh>(e).map(|m| m.primitives().len()), Some(3));
assert!(w.edit_mesh(e, |_| Ok(Some(vec![5]))).is_err());
assert_eq!(w.get::<Mesh>(e).map(|m| m.primitives().len()), Some(3));
assert_eq!(w.edit_mesh(Entity::from_index(1), |_| Ok(None)), Ok(false));
}
#[test]
fn clone_move_duplicate_and_linear_maps_count_as_they_should() {
let clone_back = |w: &mut World| {
let e = Entity::from_index(0);
let mut m = w.get::<Mesh>(e).unwrap().clone();
set(&mut m, 1, prim(1));
w.insert(e, m);
};
assert_eq!(left_out(clone_back, vec![]), (Measure::Triangles, 1, 0));
let moved = |w: &mut World| {
let form = w.spawn();
w.insert(form, Mesh::new(vec![prim(1)]));
w.move_components(form, Entity::from_index(0)).unwrap();
};
assert_eq!(left_out(moved, vec![]), (Measure::Primitives, 2, 0));
run(moved, vec![(Measure::Primitives, 2)]).unwrap();
let duplicated = |w: &mut World| {
let p = Entity::from_index(0);
let copy = w.duplicate(p).unwrap();
for e in [Entity::from_index(1), Entity::from_index(2)] {
w.get_mut::<Instance>(e).unwrap().prototype = copy;
}
w.despawn(p);
};
run(duplicated, vec![]).unwrap();
let mirrored = |w: &mut World| {
let l = crate::Linear::mirror_x();
w.apply_linear(Entity::from_index(0), &l).unwrap();
};
run(mirrored, vec![]).unwrap();
let mut w = world();
w.start_rebuild_log();
w.rebuild_primitives(Entity::from_index(0), |_| Ok((vec![prim(1)], vec![0])))
.unwrap();
assert!(w.clone().take_rebuild_log().is_empty());
assert!(w.subset(&w.entities().collect()).take_rebuild_log().is_empty());
assert_eq!(w.take_rebuild_log().len(), 1);
}
#[test]
fn a_mesh_is_followed_by_its_identity_across_entities() {
fn stand_in(w: &mut World) -> Entity {
let p = Entity::from_index(0);
let copy = w.duplicate(p).unwrap();
for e in [Entity::from_index(1), Entity::from_index(2)] {
w.get_mut::<Instance>(e).unwrap().prototype = copy;
}
w.despawn(p);
copy
}
fn moved(w: &mut World) -> Entity {
let to = w.spawn();
w.move_components(Entity::from_index(0), to).unwrap();
for e in [Entity::from_index(1), Entity::from_index(2)] {
w.get_mut::<Instance>(e).unwrap().prototype = to;
}
to
}
let stand_in_edited = |w: &mut World| {
let copy = stand_in(w);
set(w.get_mut::<Mesh>(copy).unwrap(), 2, prim(1));
};
assert_eq!(left_out(stand_in_edited, vec![]), (Measure::Triangles, 2, 0));
run(stand_in_edited, vec![(Measure::Triangles, 2)]).unwrap();
let stand_in_rebuilt = |w: &mut World| {
let copy = stand_in(w);
w.rebuild_primitives(copy, |_| Ok((vec![prim(1), prim(2)], vec![0, 1])))
.unwrap();
};
assert_eq!(left_out(stand_in_rebuilt, vec![]), (Measure::Primitives, 1, 0));
let moved_edited = |w: &mut World| {
let to = moved(w);
set(w.get_mut::<Mesh>(to).unwrap(), 2, prim(1));
};
assert_eq!(left_out(moved_edited, vec![]), (Measure::Triangles, 2, 0));
let moved_split = |w: &mut World| {
let to = moved(w);
w.rebuild_primitives(to, |_| Ok((vec![prim(2), prim(1), prim(2)], vec![1, 2, 2])))
.unwrap();
};
assert_eq!(left_out(moved_split, vec![]), (Measure::Primitives, 1, 0));
let copy_edited = |w: &mut World| {
let copy = w.duplicate(Entity::from_index(0)).unwrap();
set(w.get_mut::<Mesh>(copy).unwrap(), 2, prim(1));
};
run(copy_edited, vec![]).unwrap();
let copy_rebuilt = |w: &mut World| {
let to = w.spawn();
w.copy_components(Entity::from_index(0), to);
w.rebuild_primitives(to, |_| Ok((vec![prim(1)], vec![0])))
.unwrap();
};
run(copy_rebuilt, vec![]).unwrap();
let clone_elsewhere = |w: &mut World| {
let m = w.get::<Mesh>(Entity::from_index(0)).unwrap().clone();
let to = w.spawn();
w.insert(to, m);
set(w.get_mut::<Mesh>(to).unwrap(), 0, prim(0));
};
run(clone_elsewhere, vec![]).unwrap();
let source_rebuilt = |w: &mut World| {
w.duplicate(Entity::from_index(0)).unwrap();
w.rebuild_primitives(Entity::from_index(0), |_| {
Ok((vec![prim(1), prim(2)], vec![0, 1]))
})
.unwrap();
};
assert_eq!(left_out(source_rebuilt, vec![]), (Measure::Primitives, 1, 0));
}
#[test]
fn despawn_declares_what_the_entity_held() {
let mut w = World::new();
let proto = w.spawn();
let e = w.spawn();
w.insert(
e,
Instance {
prototype: proto,
variant: 0,
},
);
w.insert(e, crate::Transform::IDENTITY);
let prim = Primitive {
topology: Topology::Triangles,
streams: Default::default(),
indices: None,
material: None,
};
w.insert(e, Mesh::new(vec![prim.clone(), prim]));
let mut log = PassLog::default();
assert!(despawn(&mut w, e, &mut log, "left out"));
assert!(!despawn(&mut w, e, &mut log, "left out"));
assert_eq!(log.declared(Measure::Placements), 1);
assert_eq!(log.declared(Measure::Primitives), 2);
assert_eq!(log.declared(Measure::Textures), 0);
assert_eq!(
log.drops.get(&(Measure::Primitives, "left out".to_owned())),
Some(&2)
);
}
#[test]
fn despawn_drops_the_entity_from_the_assets_that_list_it() {
use crate::asset::Role;
let mut w = World::new();
let (part, rig, other) = (w.spawn(), w.spawn(), w.spawn());
let (a, b) = (w.spawn(), w.spawn());
w.insert(
a,
Asset {
members: vec![(rig, Role::Rig), (part, Role::Part)],
},
);
w.insert(
b,
Asset {
members: vec![(part, Role::Part), (other, Role::Part)],
},
);
let mut log = PassLog::default();
assert!(despawn(&mut w, part, &mut log, "left out"));
assert_eq!(w.get::<Asset>(a).unwrap().members, [(rig, Role::Rig)]);
assert_eq!(w.get::<Asset>(b).unwrap().members, [(other, Role::Part)]);
assert_eq!(log.counts["asset members dropped with the entity they listed"], 2);
let mut log = PassLog::default();
assert!(despawn(&mut w, other, &mut log, "left out"));
assert_eq!(log.counts["asset members dropped with the entity they listed"], 1);
let loose = w.spawn();
let mut log = PassLog::default();
assert!(despawn(&mut w, loose, &mut log, "left out"));
assert!(
!log.counts
.contains_key("asset members dropped with the entity they listed")
);
}
#[test]
fn despawn_drops_the_entity_from_the_clip_bindings_that_name_it() {
let mut w = World::new();
let (clip, a, b) = (w.spawn(), w.spawn(), w.spawn());
let (first, second) = (w.spawn(), w.spawn());
for (e, meshes) in [(first, vec![a, b]), (second, vec![a])] {
w.insert(
e,
ClipBinding {
clip,
rig: None,
meshes,
},
);
}
let mut log = PassLog::default();
assert!(despawn(&mut w, a, &mut log, "left out"));
assert_eq!(w.get::<ClipBinding>(first).unwrap().meshes, [b]);
assert!(w.get::<ClipBinding>(second).unwrap().meshes.is_empty());
assert_eq!(w.get::<ClipBinding>(first).unwrap().clip, clip);
assert_eq!(
log.counts["clip binding meshes dropped with the entity they named"],
2
);
let loose = w.spawn();
let mut log = PassLog::default();
assert!(despawn(&mut w, loose, &mut log, "left out"));
assert!(
!log.counts
.contains_key("clip binding meshes dropped with the entity they named")
);
}
}