use crate::components::GridExtrusionEntity;
use crate::plugin::MapStateResource;
use crate::systems::frame_change_detection::{frame_unchanged, FrameChangeDetection};
use crate::systems::visualization_sync_stats::VisualizationSyncStats;
use crate::systems::visualization_visibility::grid_intersects_scene_viewport;
use bevy::mesh::{Indices, MeshVertexAttribute, PrimitiveTopology};
use bevy::prelude::*;
use glam::DVec3;
use rustial_engine as rustial_math;
use rustial_engine::{ExtrusionParams, LayerId, VisualizationOverlay};
use std::collections::{HashMap, HashSet};
#[derive(Resource, Default)]
pub struct GridExtrusionSyncState {
fingerprints: HashMap<LayerId, GridExtrusionFingerprint>,
pub stats: VisualizationSyncStats,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct GridExtrusionFingerprint {
generation: u64,
grid_fingerprint: u64,
params_fingerprint: u64,
ramp_fingerprint: u64,
projection: rustial_engine::CameraProjection,
}
struct GridExtrusionMeshData {
positions: Vec<[f32; 3]>,
normals: Vec<[f32; 3]>,
indices: Vec<u32>,
color: Color,
}
pub fn sync_grid_extrusions(
mut commands: Commands,
state: Res<MapStateResource>,
mut existing: Query<(
Entity,
&mut GridExtrusionEntity,
&mut Transform,
&Mesh3d,
&MeshMaterial3d<StandardMaterial>,
)>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut sync_state: ResMut<GridExtrusionSyncState>,
detection: Res<FrameChangeDetection>,
) {
if frame_unchanged(&detection, &state.0) {
sync_state.stats.skipped_frames += 1;
return;
}
let frame = state.0.frame_output();
let overlays: Vec<_> = frame
.visualization
.iter()
.filter_map(|overlay| match overlay {
VisualizationOverlay::GridExtrusion {
layer_id,
grid,
field,
ramp,
params,
} => Some((*layer_id, grid, field, ramp, params)),
_ => None,
})
.collect();
let camera_origin = state.0.scene_world_origin();
let projection = state.0.camera().projection();
let visible_overlay_ids: HashSet<LayerId> = overlays
.iter()
.filter_map(|(layer_id, grid, _, _, _)| {
grid_intersects_scene_viewport(grid, &state.0, camera_origin).then_some(*layer_id)
})
.collect();
if overlays.is_empty() {
for (entity, _, _, _, _) in &mut existing {
commands.entity(entity).despawn();
sync_state.stats.despawned_entities += 1;
}
sync_state.fingerprints.clear();
return;
}
let overlay_map: HashMap<LayerId, _> = overlays.iter().map(|entry| (entry.0, *entry)).collect();
let desired_ids: HashSet<LayerId> = overlay_map.keys().copied().collect();
let mut existing_ids = HashSet::new();
for (entity, mut extrusion_entity, mut transform, mesh3d, material3d) in &mut existing {
let Some((layer_id, grid, field, ramp, params)) =
overlay_map.get(&extrusion_entity.layer_id).copied()
else {
commands.entity(entity).despawn();
continue;
};
if !visible_overlay_ids.contains(&layer_id) {
commands.entity(entity).insert(Visibility::Hidden);
sync_state.stats.hidden_entities += 1;
existing_ids.insert(layer_id);
continue;
}
let fingerprint = GridExtrusionFingerprint {
generation: field.generation,
grid_fingerprint: grid_extrusion_grid_fingerprint(grid),
params_fingerprint: grid_extrusion_params_fingerprint(params),
ramp_fingerprint: grid_scalar_ramp_fingerprint(ramp),
projection,
};
let structural_changed = sync_state.fingerprints.get(&layer_id) != Some(&fingerprint);
let value_changed = extrusion_entity.value_generation != field.value_generation;
let origin_changed = extrusion_entity.spawn_origin != camera_origin;
let terrain_enabled = state.0.terrain().enabled();
if structural_changed || value_changed || origin_changed || terrain_enabled {
let mesh_data =
build_grid_extrusion_mesh_data(grid, field, ramp, params, &state.0, camera_origin);
let color = mesh_data.color;
if let Some(existing_mesh) = meshes.get_mut(&mesh3d.0) {
let can_preserve_topology = !structural_changed
&& existing_mesh.indices().map(|idx| idx.len())
== Some(mesh_data.indices.len())
&& mesh_attribute_len(existing_mesh, Mesh::ATTRIBUTE_POSITION)
== Some(mesh_data.positions.len());
if can_preserve_topology {
existing_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, mesh_data.positions);
existing_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, mesh_data.normals);
} else {
*existing_mesh = mesh_from_extrusion_data(mesh_data);
}
}
if let Some(material) = materials.get_mut(&material3d.0) {
material.base_color = color;
}
extrusion_entity.spawn_origin = camera_origin;
extrusion_entity.value_generation = field.value_generation;
extrusion_entity.projection = projection;
transform.translation = Vec3::ZERO;
sync_state.fingerprints.insert(layer_id, fingerprint);
} else {
let offset = extrusion_entity.spawn_origin - camera_origin;
transform.translation = Vec3::new(offset.x as f32, offset.y as f32, offset.z as f32);
}
commands.entity(entity).insert(Visibility::Visible);
sync_state.stats.updated_entities += 1;
existing_ids.insert(layer_id);
}
for (layer_id, grid, field, ramp, params) in overlays {
if existing_ids.contains(&layer_id) {
continue;
}
if !visible_overlay_ids.contains(&layer_id) {
continue;
}
let mesh_data =
build_grid_extrusion_mesh_data(grid, field, ramp, params, &state.0, camera_origin);
let color = mesh_data.color;
let mesh = mesh_from_extrusion_data(mesh_data);
let mesh_handle = meshes.add(mesh);
let material_handle = materials.add(StandardMaterial {
base_color: color,
alpha_mode: AlphaMode::Blend,
double_sided: true,
perceptual_roughness: 0.9,
metallic: 0.0,
..Default::default()
});
commands.spawn((
Mesh3d(mesh_handle),
MeshMaterial3d(material_handle),
Transform::IDENTITY,
Visibility::Visible,
GridExtrusionEntity {
layer_id,
spawn_origin: camera_origin,
value_generation: field.value_generation,
projection,
},
));
sync_state.stats.spawned_entities += 1;
sync_state.fingerprints.insert(
layer_id,
GridExtrusionFingerprint {
generation: field.generation,
grid_fingerprint: grid_extrusion_grid_fingerprint(grid),
params_fingerprint: grid_extrusion_params_fingerprint(params),
ramp_fingerprint: grid_scalar_ramp_fingerprint(ramp),
projection,
},
);
}
sync_state
.fingerprints
.retain(|layer_id, _| desired_ids.contains(layer_id));
}
fn build_grid_extrusion_mesh_data(
grid: &rustial_engine::GeoGrid,
field: &rustial_engine::ScalarField2D,
ramp: &rustial_engine::ColorRamp,
params: &ExtrusionParams,
state: &rustial_engine::MapState,
scene_origin: DVec3,
) -> GridExtrusionMeshData {
let mut positions = Vec::<[f32; 3]>::new();
let mut indices = Vec::<u32>::new();
let mut accum_color = [0.0f32; 4];
let mut color_count = 0usize;
for row in 0..grid.rows {
for col in 0..grid.cols {
let Some(value) = field.sample(row, col) else {
continue;
};
let t = field.normalized(row, col).unwrap_or(0.5);
let color = ramp.evaluate(t);
accum_color[0] += color[0];
accum_color[1] += color[1];
accum_color[2] += color[2];
accum_color[3] += color[3];
color_count += 1;
let corners = grid_cell_corners_world(grid, row, col, state, scene_origin, params);
append_extruded_cell_geometry(
&mut positions,
&mut indices,
corners,
value * params.height_scale as f32,
);
}
}
let normals = compute_vertex_normals(&positions, &indices);
let avg = if color_count > 0 {
[
accum_color[0] / color_count as f32,
accum_color[1] / color_count as f32,
accum_color[2] / color_count as f32,
accum_color[3] / color_count as f32,
]
} else {
[0.7, 0.7, 0.7, 1.0]
};
GridExtrusionMeshData {
positions,
normals,
indices,
color: Color::srgba(avg[0], avg[1], avg[2], avg[3]),
}
}
fn mesh_from_extrusion_data(data: GridExtrusionMeshData) -> Mesh {
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, Default::default());
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, data.positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, data.normals);
mesh.insert_indices(Indices::U32(data.indices));
mesh
}
fn mesh_attribute_len(mesh: &Mesh, attribute: MeshVertexAttribute) -> Option<usize> {
match mesh.attribute(attribute) {
Some(bevy::mesh::VertexAttributeValues::Float32x3(values)) => Some(values.len()),
_ => None,
}
}
fn append_extruded_cell_geometry(
positions: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
corners: [DVec3; 4],
extrusion_height: f32,
) {
let [nw, ne, sw, se] = corners;
let top = [
[nw.x as f32, nw.y as f32, nw.z as f32 + extrusion_height],
[ne.x as f32, ne.y as f32, ne.z as f32 + extrusion_height],
[sw.x as f32, sw.y as f32, sw.z as f32 + extrusion_height],
[se.x as f32, se.y as f32, se.z as f32 + extrusion_height],
];
let base = [
[nw.x as f32, nw.y as f32, nw.z as f32],
[ne.x as f32, ne.y as f32, ne.z as f32],
[sw.x as f32, sw.y as f32, sw.z as f32],
[se.x as f32, se.y as f32, se.z as f32],
];
append_quad(positions, indices, top[0], top[1], top[2], top[3]);
append_quad(positions, indices, base[0], base[1], top[0], top[1]);
append_quad(positions, indices, top[2], top[3], base[2], base[3]);
append_quad(positions, indices, base[0], top[0], base[2], top[2]);
append_quad(positions, indices, top[1], base[1], top[3], base[3]);
}
fn append_quad(
positions: &mut Vec<[f32; 3]>,
indices: &mut Vec<u32>,
a: [f32; 3],
b: [f32; 3],
c: [f32; 3],
d: [f32; 3],
) {
let base_index = positions.len() as u32;
positions.extend_from_slice(&[a, b, c, d]);
indices.extend_from_slice(&[
base_index,
base_index + 2,
base_index + 1,
base_index + 1,
base_index + 2,
base_index + 3,
]);
}
fn grid_cell_corners_world(
grid: &rustial_engine::GeoGrid,
row: usize,
col: usize,
state: &rustial_engine::MapState,
scene_origin: DVec3,
params: &ExtrusionParams,
) -> [DVec3; 4] {
let nw = project_grid_offset(
grid,
col as f64 * grid.cell_width,
row as f64 * grid.cell_height,
state,
scene_origin,
params,
);
let ne = project_grid_offset(
grid,
(col + 1) as f64 * grid.cell_width,
row as f64 * grid.cell_height,
state,
scene_origin,
params,
);
let sw = project_grid_offset(
grid,
col as f64 * grid.cell_width,
(row + 1) as f64 * grid.cell_height,
state,
scene_origin,
params,
);
let se = project_grid_offset(
grid,
(col + 1) as f64 * grid.cell_width,
(row + 1) as f64 * grid.cell_height,
state,
scene_origin,
params,
);
[nw, ne, sw, se]
}
fn project_grid_offset(
grid: &rustial_engine::GeoGrid,
dx: f64,
dy: f64,
state: &rustial_engine::MapState,
scene_origin: DVec3,
params: &ExtrusionParams,
) -> DVec3 {
let (sin_r, cos_r) = grid.rotation.sin_cos();
let rx = dx * cos_r - dy * sin_r;
let ry = dx * sin_r + dy * cos_r;
let coord = offset_geo_coord(&grid.origin, rx, ry);
let altitude = resolve_grid_base_altitude(grid, &coord, state, params);
let elevated_coord = rustial_math::GeoCoord::new(coord.lat, coord.lon, altitude);
let projected = state.camera().projection().project(&elevated_coord);
DVec3::new(
projected.position.x - scene_origin.x,
projected.position.y - scene_origin.y,
projected.position.z - scene_origin.z,
)
}
fn offset_geo_coord(
origin: &rustial_math::GeoCoord,
dx_meters: f64,
dy_meters: f64,
) -> rustial_math::GeoCoord {
const METERS_PER_DEG_LAT: f64 = 111_320.0;
let lat = origin.lat - dy_meters / METERS_PER_DEG_LAT;
let cos_lat = origin.lat.to_radians().cos().max(1e-10);
let lon = origin.lon + dx_meters / (METERS_PER_DEG_LAT * cos_lat);
rustial_math::GeoCoord::new(lat, lon, origin.alt)
}
fn resolve_grid_base_altitude(
grid: &rustial_engine::GeoGrid,
coord: &rustial_math::GeoCoord,
state: &rustial_engine::MapState,
params: &ExtrusionParams,
) -> f64 {
let terrain = state.elevation_at(coord).unwrap_or(0.0);
match grid.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => terrain + params.base_meters,
rustial_engine::AltitudeMode::RelativeToGround => {
terrain + grid.origin.alt + params.base_meters
}
rustial_engine::AltitudeMode::Absolute => grid.origin.alt + params.base_meters,
}
}
fn compute_vertex_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
let mut accum = vec![Vec3::ZERO; positions.len()];
for tri in indices.chunks_exact(3) {
let ia = tri[0] as usize;
let ib = tri[1] as usize;
let ic = tri[2] as usize;
if ia >= positions.len() || ib >= positions.len() || ic >= positions.len() {
continue;
}
let a = Vec3::from_array(positions[ia]);
let b = Vec3::from_array(positions[ib]);
let c = Vec3::from_array(positions[ic]);
let normal = (b - a).cross(c - a);
if normal.length_squared() > 1e-12 {
accum[ia] += normal;
accum[ib] += normal;
accum[ic] += normal;
}
}
accum
.into_iter()
.map(|normal| {
let normal = if normal.length_squared() > 1e-12 {
normal.normalize()
} else {
Vec3::Z
};
[normal.x, normal.y, normal.z]
})
.collect()
}
fn grid_extrusion_params_fingerprint(params: &ExtrusionParams) -> u64 {
(params.height_scale.to_bits())
.wrapping_mul(31)
.wrapping_add(params.base_meters.to_bits())
}
fn grid_extrusion_grid_fingerprint(grid: &rustial_engine::GeoGrid) -> u64 {
let mut h = 17u64;
h = h.wrapping_mul(31).wrapping_add(grid.origin.lat.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.origin.lon.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.origin.alt.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.rows as u64);
h = h.wrapping_mul(31).wrapping_add(grid.cols as u64);
h = h.wrapping_mul(31).wrapping_add(grid.cell_width.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.cell_height.to_bits());
h = h.wrapping_mul(31).wrapping_add(grid.rotation.to_bits());
h = h.wrapping_mul(31).wrapping_add(match grid.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => 0,
rustial_engine::AltitudeMode::RelativeToGround => 1,
rustial_engine::AltitudeMode::Absolute => 2,
});
h
}
fn grid_scalar_ramp_fingerprint(ramp: &rustial_engine::ColorRamp) -> u64 {
let mut h = ramp.stops.len() as u64;
for stop in &ramp.stops {
h = h
.wrapping_mul(31)
.wrapping_add(stop.value.to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(stop.color[0].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(stop.color[1].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(stop.color[2].to_bits() as u64)
.wrapping_mul(31)
.wrapping_add(stop.color[3].to_bits() as u64);
}
h
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::GridExtrusionEntity;
use crate::plugin::RustialBevyPlugin;
use rustial_engine::GeoCoord;
fn test_app() -> App {
let mut app = App::new();
app.add_plugins(bevy::app::TaskPoolPlugin::default());
app.add_plugins(bevy::time::TimePlugin);
app.init_resource::<Assets<Mesh>>();
app.init_resource::<Assets<StandardMaterial>>();
app.init_resource::<Assets<Image>>();
app.add_plugins(RustialBevyPlugin);
app
}
#[test]
fn grid_extrusion_sync_spawns_overlay_entity() {
let mut app = test_app();
{
let mut state = app
.world_mut()
.resource_mut::<crate::plugin::MapStateResource>();
state.0.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(GeoCoord::from_lat_lon(51.5, -0.12), 2, 2, 50.0, 50.0),
rustial_engine::ScalarField2D::from_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 10.0,
base_meters: 0.0,
},
);
}
app.update();
app.update();
app.update();
app.update();
let count = {
let world = app.world_mut();
world.query::<&GridExtrusionEntity>().iter(world).count()
};
assert_eq!(count, 1);
}
#[test]
fn grid_extrusion_sync_despawns_when_overlay_removed() {
let mut app = test_app();
{
let mut state = app
.world_mut()
.resource_mut::<crate::plugin::MapStateResource>();
state.0.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(GeoCoord::from_lat_lon(51.5, -0.12), 1, 1, 50.0, 50.0),
rustial_engine::ScalarField2D::from_data(1, 1, vec![1.0]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 5.0,
base_meters: 0.0,
},
);
}
app.update();
app.update();
{
let mut state = app
.world_mut()
.resource_mut::<crate::plugin::MapStateResource>();
state.0 = rustial_engine::MapState::new();
}
app.update();
let count = {
let world = app.world_mut();
world.query::<&GridExtrusionEntity>().iter(world).count()
};
assert_eq!(count, 0);
}
#[test]
fn grid_extrusion_value_only_update_preserves_mesh_handle_and_topology() {
let mut app = test_app();
let origin = GeoCoord::from_lat_lon(51.5, -0.12);
{
let mut state = app
.world_mut()
.resource_mut::<crate::plugin::MapStateResource>();
state.0.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(origin, 1, 1, 50.0, 50.0),
rustial_engine::ScalarField2D::from_data(1, 1, vec![1.0]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 10.0,
base_meters: 2.0,
},
);
}
app.update();
app.update();
let (mesh_handle_before, index_count_before, positions_before) = {
let world = app.world_mut();
let mut query = world.query::<(&GridExtrusionEntity, &Mesh3d)>();
let (_, mesh3d) = query.single(world).expect("grid extrusion entity");
let meshes = world.resource::<Assets<Mesh>>();
let mesh = meshes.get(&mesh3d.0).expect("mesh");
(
mesh3d.0.clone(),
mesh.indices().expect("indices").len(),
mesh_positions(mesh),
)
};
{
let mut field = rustial_engine::ScalarField2D::from_data(1, 1, vec![1.0]);
field.update_values(vec![5.0]);
let mut state = app
.world_mut()
.resource_mut::<crate::plugin::MapStateResource>();
state.0.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(origin, 1, 1, 50.0, 50.0),
field,
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 10.0,
base_meters: 2.0,
},
);
}
app.update();
app.update();
let (mesh_handle_after, index_count_after, positions_after) = {
let world = app.world_mut();
let mut query = world.query::<(&GridExtrusionEntity, &Mesh3d)>();
let (_, mesh3d) = query.single(world).expect("grid extrusion entity");
let meshes = world.resource::<Assets<Mesh>>();
let mesh = meshes.get(&mesh3d.0).expect("mesh");
(
mesh3d.0.clone(),
mesh.indices().expect("indices").len(),
mesh_positions(mesh),
)
};
assert_eq!(mesh_handle_before, mesh_handle_after);
assert_eq!(index_count_before, index_count_after);
assert_ne!(positions_before, positions_after);
}
#[test]
fn grid_extrusion_baseline_follows_altitude_mode_contract() {
let params = rustial_engine::ExtrusionParams {
height_scale: 5.0,
base_meters: 7.0,
};
let state = rustial_engine::MapState::new();
let coord = GeoCoord::new(0.0, 0.0, 100.0);
let clamp = rustial_engine::GeoGrid {
origin: coord,
rows: 1,
cols: 1,
cell_width: 10.0,
cell_height: 10.0,
rotation: 0.0,
altitude_mode: rustial_engine::AltitudeMode::ClampToGround,
};
let relative = rustial_engine::GeoGrid {
altitude_mode: rustial_engine::AltitudeMode::RelativeToGround,
..clamp.clone()
};
let absolute = rustial_engine::GeoGrid {
altitude_mode: rustial_engine::AltitudeMode::Absolute,
..clamp.clone()
};
assert!((resolve_grid_base_altitude(&clamp, &coord, &state, ¶ms) - 7.0).abs() < 1e-9);
assert!(
(resolve_grid_base_altitude(&relative, &coord, &state, ¶ms) - 107.0).abs() < 1e-9
);
assert!(
(resolve_grid_base_altitude(&absolute, &coord, &state, ¶ms) - 107.0).abs() < 1e-9
);
}
#[test]
fn grid_extrusion_sync_skips_far_offscreen_overlay() {
let mut app = test_app();
app.update();
{
let mut state = app
.world_mut()
.resource_mut::<crate::plugin::MapStateResource>();
state.0.set_viewport(1280, 720);
state.0.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
state.0.set_camera_distance(1_000.0);
state.0.update_camera(1.0 / 60.0);
state.0.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(GeoCoord::from_lat_lon(70.0, 120.0), 1, 1, 50.0, 50.0),
rustial_engine::ScalarField2D::from_data(1, 1, vec![1.0]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 5.0,
base_meters: 0.0,
},
);
}
app.update();
let count = {
let world = app.world_mut();
world.query::<&GridExtrusionEntity>().iter(world).count()
};
assert_eq!(count, 0);
}
#[test]
fn grid_extrusion_sync_reports_steady_state_skip_on_unchanged_frame() {
let mut app = test_app();
{
let mut state = app
.world_mut()
.resource_mut::<crate::plugin::MapStateResource>();
state.0.set_grid_extrusion(
"surface",
rustial_engine::GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 2, 2, 50.0, 50.0),
rustial_engine::ScalarField2D::from_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]),
rustial_engine::ColorRamp::new(vec![
rustial_engine::ColorStop {
value: 0.0,
color: [0.0, 0.0, 1.0, 0.5],
},
rustial_engine::ColorStop {
value: 1.0,
color: [1.0, 0.0, 0.0, 0.8],
},
]),
rustial_engine::ExtrusionParams {
height_scale: 10.0,
base_meters: 0.0,
},
);
}
app.update();
app.update();
let before = app.world().resource::<GridExtrusionSyncState>().stats;
app.update();
let after = app.world().resource::<GridExtrusionSyncState>().stats;
assert_eq!(after.skipped_frames, before.skipped_frames + 1);
assert_eq!(after.spawned_entities, before.spawned_entities);
assert_eq!(after.updated_entities, before.updated_entities);
assert_eq!(after.hidden_entities, before.hidden_entities);
assert_eq!(after.despawned_entities, before.despawned_entities);
}
fn mesh_positions(mesh: &Mesh) -> Vec<[f32; 3]> {
match mesh.attribute(Mesh::ATTRIBUTE_POSITION) {
Some(bevy::mesh::VertexAttributeValues::Float32x3(values)) => values.clone(),
other => panic!("unexpected position attribute: {other:?}"),
}
}
}