use super::*;
use bevy_app::App;
use bevy_app::TaskPoolPlugin;
use bevy_asset::AssetPlugin;
use isomesh::chunk::ChunkLayout;
use isomesh::fields::Sphere;
struct SlowSphere {
inner: Sphere<f32>,
spin: u32,
}
impl Sdf for SlowSphere {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
let mut accumulator = 0.0f32;
for i in 0..self.spin {
accumulator += (i as f32).sin();
}
self.inner.sample(p) + accumulator * 0.0
}
}
const SPIN: u32 = 4_000;
fn app(spin: u32, chunks: i32) -> App {
let mut app = App::new();
app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
.add_plugins(IsomeshPlugin);
let layout = ChunkLayout::new(8, 0.25, [0.0; 3]).expect("a valid layout");
let volume = app
.world_mut()
.spawn(VoxelVolume::new(
layout,
SlowSphere {
inner: Sphere::<f32>::canonical(),
spin,
},
))
.id();
for x in 0..chunks {
app.world_mut().spawn((
VoxelChunk {
id: ChunkId::new([x, 0, 0]),
volume,
},
NeedsRemesh,
));
}
app
}
fn meshed(app: &mut App) -> usize {
app.world_mut()
.query::<&ChunkMesh>()
.iter(app.world())
.count()
}
fn drain_until(app: &mut App, mut done: impl FnMut(&mut App) -> bool) -> usize {
const DEADLINE: Duration = Duration::from_secs(10);
const SLICE: Duration = Duration::from_micros(250);
let started = Instant::now();
let mut frames = 0;
while started.elapsed() < DEADLINE {
app.update();
frames += 1;
if done(app) {
break;
}
std::thread::sleep(SLICE);
}
frames
}
#[test]
fn every_chunk_eventually_gets_a_mesh() {
let mut app = app(0, 12);
drain_until(&mut app, |app| meshed(app) == 12);
assert_eq!(meshed(&mut app), 12, "the queue never drained");
let waiting = app
.world_mut()
.query_filtered::<(), With<NeedsRemesh>>()
.iter(app.world())
.count();
let running = app
.world_mut()
.query_filtered::<(), With<MeshingTask>>()
.iter(app.world())
.count();
assert_eq!((waiting, running), (0, 0));
}
#[test]
fn spawning_the_work_does_not_do_the_work() {
let mut app = app(SPIN, 12);
app.update();
let running = app
.world_mut()
.query_filtered::<(), With<MeshingTask>>()
.iter(app.world())
.count();
assert_eq!(
(meshed(&mut app), running),
(0, 12),
"the update that queued twelve extractions returned with some of them \
already done: that is work that ran on the main thread"
);
drain_until(&mut app, |app| meshed(app) == 12);
assert_eq!(meshed(&mut app), 12, "the queue never drained");
}
#[test]
fn the_budget_bounds_what_lands_per_frame() {
let mut app = app(0, 12);
app.insert_resource(MeshBudget {
per_frame: Duration::ZERO,
max_in_flight: 64,
});
let mut worst = 0usize;
let mut before = 0usize;
let frames = drain_until(&mut app, |app| {
let after = meshed(app);
worst = worst.max(after - before);
before = after;
after == 12
});
assert_eq!(meshed(&mut app), 12);
assert_eq!(
worst, 1,
"a zero budget applied {worst} meshes in one frame"
);
assert!(frames >= 12, "twelve meshes landed in {frames} frames");
}
#[test]
fn a_generous_budget_is_not_bounded_the_same_way() {
let mut app = app(0, 12);
app.insert_resource(MeshBudget {
per_frame: Duration::from_secs(1),
max_in_flight: 64,
});
let mut best = 0usize;
let mut before = 0usize;
drain_until(&mut app, |app| {
let after = meshed(app);
best = best.max(after - before);
before = after;
after == 12
});
assert!(
best > 1,
"a one-second budget still applied only {best} mesh per frame, so the \
zero-budget test is not measuring the budget"
);
}
#[test]
fn queueing_is_capped_so_memory_does_not_run_ahead_of_the_frame() {
let mut app = app(2_000, 40);
app.insert_resource(MeshBudget {
per_frame: Duration::from_micros(1),
max_in_flight: 3,
});
app.update();
let running = app
.world_mut()
.query_filtered::<(), With<MeshingTask>>()
.iter(app.world())
.count();
assert!(running <= 3, "{running} tasks in flight against a cap of 3");
let stats = *app.world().resource::<MeshStats>();
assert!(stats.waiting > 0, "nothing was held back by the cap");
}
#[test]
fn an_edit_during_extraction_is_requeued_rather_than_swallowed() {
let mut app = app(2_000, 1);
app.update();
let chunk = app
.world_mut()
.query_filtered::<Entity, With<VoxelChunk>>()
.iter(app.world())
.next()
.expect("one chunk");
assert!(
app.world().entity(chunk).contains::<MeshingTask>(),
"the chunk should be extracting"
);
assert!(
!app.world().entity(chunk).contains::<NeedsRemesh>(),
"NeedsRemesh should come off at spawn"
);
app.world_mut().entity_mut(chunk).insert(NeedsRemesh);
drain_until(&mut app, |app| {
!app.world().entity(chunk).contains::<NeedsRemesh>()
&& !app.world().entity(chunk).contains::<MeshingTask>()
});
assert!(
app.world().entity(chunk).contains::<ChunkMesh>(),
"the chunk never finished"
);
assert!(
!app.world().entity(chunk).contains::<NeedsRemesh>(),
"the re-mark was never serviced"
);
}
#[test]
fn a_chunk_whose_volume_is_gone_stops_asking() {
let mut app = app(0, 4);
let volume = app
.world_mut()
.query_filtered::<Entity, With<VoxelVolume>>()
.iter(app.world())
.next()
.expect("one volume");
app.world_mut().entity_mut(volume).despawn();
app.update();
let still_marked = app
.world_mut()
.query_filtered::<(), With<NeedsRemesh>>()
.iter(app.world())
.count();
assert_eq!(still_marked, 0, "orphaned chunks kept requesting a mesh");
}
#[test]
fn the_subgrid_extractor_is_reachable_through_the_component() {
let mut app = App::new();
app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
.add_plugins(IsomeshPlugin);
let layout = ChunkLayout::new(6, 0.25, [0.0; 3]).expect("a valid layout");
let volume = app
.world_mut()
.spawn(
VoxelVolume::new(layout, Sphere::<f32>::canonical())
.with_extractor(Extractor::Subgrid { samples: 4 }),
)
.id();
app.world_mut().spawn((
VoxelChunk {
id: ChunkId::new([0, 0, 0]),
volume,
},
NeedsRemesh,
));
drain_until(&mut app, |app| meshed(app) == 1);
assert_eq!(meshed(&mut app), 1);
}
fn seam_open_edges<S: VolumeField>(field: &S, extractor: Extractor) -> usize {
let layout = ChunkLayout::new(16, 0.25, [0.0; 3]).expect("a valid layout");
let mut all = isomesh::MeshBuffer::<f32>::new();
for id in [ChunkId::new([0, 0, 0]), ChunkId::new([1, 0, 0])] {
let built =
extract_chunk(field, &layout, id, extractor).expect("a chunk extraction failed");
let mut buf = isomesh::MeshBuffer::<f32>::new();
buf.positions.extend_from_slice(built.positions());
buf.normals.extend_from_slice(built.normals());
buf.indices.extend_from_slice(built.indices());
all.append(&buf)
.expect("two chunks fit the u32 index space");
}
let h = layout.cell_size();
isomesh::weld::Welder::<f32>::new()
.weld(&mut all, isomesh::weld::epsilon_for(h))
.expect("weld");
let cfg = isomesh::validate::ValidateConfig::from_cell_size(f64::from(h)).expect("cfg");
let (_report, features) =
isomesh::validate::validate_features(&all.positions, &all.indices, &cfg);
let span = layout.cell_size() * layout.cells() as f32;
let hi = [span * 2.0, span, span];
let (seam, tol) = (span, h * 0.25);
let outer = |v: [f32; 3]| (0..3).any(|a| v[a] < tol || v[a] > hi[a] - tol);
features
.boundary_edges
.iter()
.filter(|e| {
let (p, q) = (all.positions[e[0] as usize], all.positions[e[1] as usize]);
!(outer(p) && outer(q)) && (p[0] - seam).abs() < tol && (q[0] - seam).abs() < tol
})
.count()
}
struct Waves;
impl Sdf for Waves {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
p[1] - 0.9 * (p[0] * 0.7).sin() * (p[2] * 0.6).cos() - 2.0
}
}
#[test]
fn the_seam_counts_are_pinned() {
assert_eq!(
seam_open_edges(&Waves, Extractor::MarchingCubes),
0,
"marching cubes stopped tiling across a chunk boundary"
);
assert_eq!(
seam_open_edges(&Waves, Extractor::SurfaceNets),
5,
"surface nets' chunk-seam gap changed"
);
assert_eq!(
seam_open_edges(&Waves, Extractor::DualContouring),
4,
"dual contouring's chunk-seam gap changed"
);
assert_eq!(
seam_open_edges(&Waves, Extractor::Subgrid { samples: 4 }),
0,
"subgrid stopped tiling across a chunk boundary"
);
}
#[test]
fn chunk_seams_reports_what_was_measured() {
for (extractor, expected) in [
(Extractor::MarchingCubes, ChunkSeams::Closed),
(Extractor::SurfaceNets, ChunkSeams::Gapped),
(Extractor::DualContouring, ChunkSeams::Gapped),
(Extractor::Subgrid { samples: 4 }, ChunkSeams::Closed),
] {
let open = seam_open_edges(&Waves, extractor);
let says = extractor.chunk_seams();
assert_eq!(says, expected, "{extractor:?} reported {says:?}");
match says {
ChunkSeams::Closed => assert_eq!(open, 0, "{extractor:?} says Closed with {open} open"),
ChunkSeams::Gapped => assert!(open > 0, "{extractor:?} says Gapped with none open"),
ChunkSeams::Unverified => {}
}
}
assert_eq!(
Extractor::Subgrid { samples: 4 }.chunk_seams(),
ChunkSeams::Closed
);
}
#[test]
fn the_quickstart_layout_meshes_every_chunk() {
let mut app = App::new();
app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
.add_plugins(IsomeshPlugin);
let layout = ChunkLayout::new(16, 1.0 / 16.0, [0.0; 3]).expect("a valid layout");
let volume = app
.world_mut()
.spawn(VoxelVolume::new(layout, Sphere::<f32>::canonical()))
.id();
for z in -1..1 {
for y in -1..1 {
for x in -1..1 {
app.world_mut().spawn((
VoxelChunk {
id: ChunkId::new([x, y, z]),
volume,
},
NeedsRemesh,
));
}
}
}
drain_until(&mut app, |app| meshed(app) == 8);
assert_eq!(meshed(&mut app), 8, "the quickstart's queue never drained");
let handles: Vec<_> = app
.world_mut()
.query::<&ChunkMesh>()
.iter(app.world())
.map(|m| m.0.clone())
.collect();
let meshes = app.world().resource::<Assets<Mesh>>();
let counts: Vec<usize> = handles
.iter()
.map(|h| {
meshes
.get(h)
.and_then(|m| m.indices().map(|i| i.len() / 3))
.unwrap_or(0)
})
.collect();
assert!(
counts.iter().all(|&n| n > 0),
"a quickstart chunk meshed to nothing, so the sphere does not cross all \
eight: {counts:?}"
);
std::println!("measured: quickstart chunks meshed to {counts:?} triangles");
}
use bevy_ecs::system::RunSystemOnce;
fn edit_app() -> (App, Entity) {
let mut app = App::new();
app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
.add_plugins(IsomeshPlugin);
let layout = ChunkLayout::new(8, 0.25, [0.0; 3]).expect("a valid layout");
let volume = app
.world_mut()
.spawn(VoxelVolume::new(
layout,
Sphere::<f32> {
center: [1.0, 1.0, 1.0],
radius: 0.5,
},
))
.id();
for x in 0..4 {
app.world_mut().spawn(VoxelChunk {
id: ChunkId::new([x, 0, 0]),
volume,
});
}
(app, volume)
}
fn marked(app: &mut App) -> Vec<ChunkId> {
let mut ids: Vec<ChunkId> = app
.world_mut()
.query_filtered::<&VoxelChunk, With<NeedsRemesh>>()
.iter(app.world())
.map(|c| c.id)
.collect();
ids.sort();
ids
}
fn edit(
app: &mut App,
volume: Entity,
field: impl VolumeField,
) -> isomesh::chunk::dirty::EditReport {
let old = app
.world_mut()
.get_mut::<VoxelVolume>(volume)
.expect("the volume exists")
.replace_field(field);
app.world_mut()
.run_system_once(
move |mut commands: Commands,
volumes: Query<&VoxelVolume>,
chunks: Query<(Entity, &VoxelChunk)>| {
let v = volumes.get(volume).expect("the volume exists");
mark_changed_chunks(&mut commands, volume, v, &chunks, &*old, &*v.field)
},
)
.expect("the system ran")
.expect("the region is far below the sample-space bound")
}
#[test]
fn replace_field_returns_the_old_field_and_installs_the_new_one() {
let layout = ChunkLayout::new(8, 0.25, [0.0; 3]).expect("a valid layout");
let mut volume = VoxelVolume::new(layout, Sphere::<f32>::canonical());
let probe = [0.0, 0.0, 0.5];
let before = volume.field.sample(probe);
assert!(before < 0.0, "the probe should be inside the old field");
let old = volume.replace_field(Sphere::<f32> {
center: [0.0; 3],
radius: 0.1,
});
assert_eq!(old.sample(probe), before, "the old field came back intact");
assert!(
volume.field.sample(probe) > 0.0,
"the volume now holds the new field"
);
assert_eq!(volume.layout, layout);
assert_eq!(volume.extractor, Extractor::default());
}
#[test]
fn mark_changed_chunks_marks_exactly_the_chunks_the_edit_touched() {
let (mut app, volume) = edit_app();
assert!(marked(&mut app).is_empty());
let report = edit(
&mut app,
volume,
Sphere::<f32> {
center: [1.0, 1.0, 1.0],
radius: 0.6,
},
);
assert_eq!(marked(&mut app), vec![ChunkId::new([0, 0, 0])]);
assert_eq!(report.dirty_chunks, 1);
assert!(report.output_changed_cells > 0);
}
#[test]
fn mark_changed_chunks_marks_the_chunk_a_surface_left_and_the_one_it_entered() {
let (mut app, volume) = edit_app();
edit(
&mut app,
volume,
Sphere::<f32> {
center: [5.0, 1.0, 1.0],
radius: 0.5,
},
);
assert_eq!(
marked(&mut app),
vec![ChunkId::new([0, 0, 0]), ChunkId::new([2, 0, 0])]
);
}
#[test]
fn mark_changed_chunks_marks_nothing_for_an_identical_field() {
let (mut app, volume) = edit_app();
let report = edit(
&mut app,
volume,
Sphere::<f32> {
center: [1.0, 1.0, 1.0],
radius: 0.5,
},
);
assert!(marked(&mut app).is_empty());
assert_eq!(report.dirty_chunks, 0);
assert_eq!(report.value_changed_cells, 0);
}
struct SphereAndPlate;
impl Sdf for SphereAndPlate {
type Scalar = f32;
fn sample(&self, p: [f32; 3]) -> f32 {
let sphere = Sphere::<f32> {
center: [1.0, 1.0, 1.0],
radius: 0.5,
}
.sample(p);
sphere.min((p[0] - 4.125).abs() - 0.075)
}
}
fn the_edit_app_with(extractor: Extractor) -> (App, Entity) {
let (mut app, volume) = edit_app();
app.world_mut()
.get_mut::<VoxelVolume>(volume)
.expect("the volume exists")
.extractor = extractor;
(app, volume)
}
#[test]
fn mark_changed_chunks_marks_a_subgrid_feature_thinner_than_a_cell() {
let extractor = Extractor::Subgrid { samples: 4 };
let (mut app, volume) = the_edit_app_with(extractor);
let layout = app
.world()
.get::<VoxelVolume>(volume)
.expect("the volume exists")
.layout;
let chunk_2 = ChunkId::new([2, 0, 0]);
let built = extract_chunk(&SphereAndPlate, &layout, chunk_2, extractor).expect("extracts");
assert!(built.triangle_count() > 0, "the plate is not in the mesh");
edit(&mut app, volume, SphereAndPlate);
assert!(
marked(&mut app).contains(&chunk_2),
"chunk 2's Subgrid mesh changed and the chunk was not marked"
);
for chunk in marked(&mut app) {
let entity = app
.world_mut()
.query::<(Entity, &VoxelChunk)>()
.iter(app.world())
.find(|(_, c)| c.id == chunk)
.map(|(e, _)| e)
.expect("the chunk exists");
app.world_mut().entity_mut(entity).remove::<NeedsRemesh>();
}
assert!(marked(&mut app).is_empty());
edit(
&mut app,
volume,
Sphere::<f32> {
center: [1.0, 1.0, 1.0],
radius: 0.5,
},
);
assert!(
marked(&mut app).contains(&chunk_2),
"the plate left chunk 2's mesh and the chunk was not marked"
);
}
#[test]
fn mark_changed_chunks_leaves_a_marching_cubes_volume_on_the_corner_rule() {
let (mut app, volume) = edit_app();
edit(&mut app, volume, SphereAndPlate);
assert!(
marked(&mut app).is_empty(),
"marching cubes meshes nothing of the plate: {:?}",
marked(&mut app)
);
}
fn edit_in(
app: &mut App,
volume: Entity,
field: impl VolumeField,
region: ([i64; 3], [i64; 3]),
) -> isomesh::chunk::dirty::EditReport {
let old = app
.world_mut()
.get_mut::<VoxelVolume>(volume)
.expect("the volume exists")
.replace_field(field);
app.world_mut()
.run_system_once(
move |mut commands: Commands,
volumes: Query<&VoxelVolume>,
chunks: Query<(Entity, &VoxelChunk)>| {
let v = volumes.get(volume).expect("the volume exists");
mark_changed_chunks_in(&mut commands, volume, v, &chunks, &*old, &*v.field, region)
},
)
.expect("the system ran")
.expect("the region is far below the sample-space bound")
}
#[test]
fn mark_changed_chunks_in_compares_only_its_box() {
let moved = || Sphere::<f32> {
center: [5.0, 1.0, 1.0],
radius: 0.5,
};
let (mut app, volume) = edit_app();
let report = edit_in(&mut app, volume, moved(), ([16, 0, 0], [23, 7, 7]));
assert_eq!(marked(&mut app), vec![ChunkId::new([2, 0, 0])]);
assert_eq!(report.region_cells, 8 * 8 * 8, "the cost follows the box");
let (mut app, volume) = edit_app();
let report = edit_in(&mut app, volume, moved(), ([24, 0, 0], [31, 7, 7]));
assert!(marked(&mut app).is_empty());
assert_eq!(report.dirty_chunks, 0);
let (mut app, volume) = edit_app();
let report = edit_in(&mut app, volume, moved(), ([5, 5, 5], [4, 5, 5]));
assert!(marked(&mut app).is_empty());
assert_eq!(report, isomesh::chunk::dirty::EditReport::default());
}
#[test]
fn mark_changed_chunks_in_bounds_a_subgrid_volumes_cost() {
let (mut app, volume) = the_edit_app_with(Extractor::Subgrid { samples: 4 });
let report = edit_in(&mut app, volume, SphereAndPlate, ([16, 0, 0], [23, 7, 7]));
assert_eq!(marked(&mut app), vec![ChunkId::new([2, 0, 0])]);
assert_eq!(report.region_cells, 8 * 8 * 8);
}
#[test]
fn mark_changed_chunks_leaves_another_volumes_chunks_alone() {
let (mut app, volume) = edit_app();
let layout = ChunkLayout::new(8, 0.25, [0.0; 3]).expect("a valid layout");
let other = app
.world_mut()
.spawn(VoxelVolume::new(layout, Sphere::<f32>::canonical()))
.id();
let bystander = app
.world_mut()
.spawn(VoxelChunk {
id: ChunkId::new([0, 0, 0]),
volume: other,
})
.id();
edit(
&mut app,
volume,
Sphere::<f32> {
center: [1.0, 1.0, 1.0],
radius: 0.6,
},
);
assert_eq!(marked(&mut app), vec![ChunkId::new([0, 0, 0])]);
assert!(
app.world().get::<NeedsRemesh>(bystander).is_none(),
"the other volume's chunk was marked"
);
}
#[test]
fn mark_changed_chunks_on_a_volume_with_no_chunks_reports_nothing() {
let mut app = App::new();
app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
.add_plugins(IsomeshPlugin);
let layout = ChunkLayout::new(8, 0.25, [0.0; 3]).expect("a valid layout");
let volume = app
.world_mut()
.spawn(VoxelVolume::new(layout, Sphere::<f32>::canonical()))
.id();
let report = edit(&mut app, volume, Sphere::<f32>::default());
assert_eq!(report, isomesh::chunk::dirty::EditReport::default());
}
fn mark_every_chunk(mut commands: Commands, chunks: Query<Entity, With<VoxelChunk>>) {
for entity in &chunks {
commands.entity(entity).insert(NeedsRemesh);
}
}
#[test]
fn a_system_before_isomesh_systems_is_meshed_the_same_frame() {
let (mut app, _) = edit_app();
app.add_systems(Update, mark_every_chunk.before(IsomeshSystems));
app.update();
let stats = *app.world().resource::<MeshStats>();
assert_eq!(stats.spawned, 4, "the marks were not seen this frame");
assert!(marked(&mut app).is_empty(), "NeedsRemesh was not consumed");
}
#[test]
fn a_system_after_isomesh_systems_waits_for_the_next_frame() {
let (mut app, _) = edit_app();
app.add_systems(Update, mark_every_chunk.after(IsomeshSystems));
app.update();
let stats = *app.world().resource::<MeshStats>();
assert_eq!(stats.spawned, 0, "the marks were seen the same frame");
assert_eq!(marked(&mut app).len(), 4);
app.update();
let stats = *app.world().resource::<MeshStats>();
assert_eq!(stats.spawned, 4, "and the next frame picks them up");
}
use bevy_asset::{AssetId, RenderAssetUsages};
type Published = (ChunkId, AssetId<Mesh>, usize, RenderAssetUsages);
fn published(app: &mut App) -> Vec<Published> {
let handles: Vec<(ChunkId, Handle<Mesh>)> = app
.world_mut()
.query::<(&VoxelChunk, &ChunkMesh)>()
.iter(app.world())
.map(|(chunk, mesh)| (chunk.id, mesh.0.clone()))
.collect();
let meshes = app.world().resource::<Assets<Mesh>>();
let mut rows: Vec<Published> = handles
.iter()
.map(|(id, handle)| {
let mesh = meshes
.get(handle)
.expect("a published handle names a live asset");
(*id, handle.id(), mesh.count_vertices(), mesh.asset_usage)
})
.collect();
rows.sort_by_key(|row| row.0);
rows
}
fn meshed_edit_app() -> (App, Entity) {
let (mut app, volume) = edit_app();
let chunks: Vec<Entity> = app
.world_mut()
.query_filtered::<Entity, With<VoxelChunk>>()
.iter(app.world())
.collect();
for chunk in chunks {
app.world_mut().entity_mut(chunk).insert(NeedsRemesh);
}
drain_until(&mut app, |app| meshed(app) == 4);
assert_eq!(meshed(&mut app), 4, "the queue never drained");
(app, volume)
}
#[test]
fn an_empty_chunk_keeps_its_chunk_mesh_but_not_the_render_world() {
let (mut app, _) = meshed_edit_app();
let rows = published(&mut app);
let ids: Vec<ChunkId> = rows.iter().map(|row| row.0).collect();
assert_eq!(
ids,
(0..4).map(|x| ChunkId::new([x, 0, 0])).collect::<Vec<_>>(),
"every finished chunk has a ChunkMesh, the empty ones included"
);
let (_, _, vertices, usage) = rows[0];
assert!(vertices > 0, "chunk 0 holds the sphere");
assert_eq!(
usage,
RenderAssetUsages::default(),
"a chunk with a surface is still drawn and still readable"
);
for &(id, _, vertices, usage) in &rows[1..] {
assert_eq!(vertices, 0, "{id:?} should be empty");
assert_eq!(
usage,
RenderAssetUsages::MAIN_WORLD,
"{id:?} has no vertices, so the render world must not get it: Bevy's \
mesh allocator warns `contains no vertices` for every one it is handed"
);
}
}
#[test]
fn a_chunk_that_gains_or_loses_its_surface_gets_a_fresh_mesh_with_the_matching_usage() {
let (mut app, volume) = meshed_edit_app();
let before = published(&mut app);
edit(
&mut app,
volume,
Sphere::<f32> {
center: [5.0, 1.0, 1.0],
radius: 0.5,
},
);
drain_until(&mut app, |app| {
let rows = published(app);
rows[0].1 != before[0].1 && rows[2].1 != before[2].1
});
let after = published(&mut app);
assert_ne!(after[0].1, before[0].1, "chunk 0 was never re-published");
assert_ne!(after[2].1, before[2].1, "chunk 2 was never re-published");
assert_eq!(
(after[0].2, after[0].3),
(0, RenderAssetUsages::MAIN_WORLD),
"the chunk the surface left"
);
assert!(after[2].2 > 0, "the chunk the surface entered has vertices");
assert_eq!(
after[2].3,
RenderAssetUsages::default(),
"the chunk the surface entered"
);
assert_eq!(
(after[1].1, after[3].1),
(before[1].1, before[3].1),
"chunks the edit never touched keep their assets"
);
let replaced = [before[0].1, before[2].1];
let live = |app: &App| {
let meshes = app.world().resource::<Assets<Mesh>>();
replaced
.iter()
.filter(|&&id| meshes.get(id).is_some())
.count()
};
drain_until(&mut app, |app| live(app) == 0);
assert_eq!(live(&app), 0, "a replaced mesh outlived its handle");
}