use crate::components::PointCloudEntity;
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::points_intersect_scene_viewport;
use bevy::math::primitives::Cuboid;
use bevy::prelude::*;
use rustial_engine::{LayerId, PointInstance, VisualizationOverlay};
use std::collections::{HashMap, HashSet};
#[derive(Resource, Default)]
pub struct CachedPointCloudAssets {
mesh: Option<Handle<Mesh>>,
materials: HashMap<[u32; 4], Handle<StandardMaterial>>,
}
#[derive(Resource, Default)]
pub struct PointCloudSyncState {
pub stats: VisualizationSyncStats,
}
pub fn sync_point_clouds(
mut commands: Commands,
state: Res<MapStateResource>,
existing: Query<(Entity, &PointCloudEntity)>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut cache: ResMut<CachedPointCloudAssets>,
mut sync_state: ResMut<PointCloudSyncState>,
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::Points { layer_id, points, ramp } => Some((*layer_id, points, ramp)),
_ => None,
})
.collect();
if overlays.is_empty() {
for (entity, _) in &existing {
commands.entity(entity).despawn();
sync_state.stats.despawned_entities += 1;
}
return;
}
let mesh_handle = cache
.mesh
.get_or_insert_with(|| meshes.add(Mesh::from(Cuboid::new(1.0, 1.0, 1.0))))
.clone();
let camera_origin = state.0.scene_world_origin();
let visible_overlay_ids: HashSet<LayerId> = overlays
.iter()
.filter_map(|(layer_id, points, _)| {
points_intersect_scene_viewport(points, &state.0, camera_origin).then_some(*layer_id)
})
.collect();
let mut existing_map = HashMap::new();
for (entity, marker) in &existing {
existing_map.insert((marker.layer_id, marker.point_index), entity);
}
for (layer_id, points, ramp) in overlays {
if !visible_overlay_ids.contains(&layer_id) {
continue;
}
for (point_index, point) in points.points.iter().enumerate() {
let key = (layer_id, point_index);
let transform = build_point_transform(point, &state.0, camera_origin);
let color = point.color.unwrap_or_else(|| ramp.evaluate(point.intensity.clamp(0.0, 1.0)));
let material_handle = cached_material_handle(&mut materials, &mut cache, color);
if let Some(entity) = existing_map.remove(&key) {
commands.entity(entity).insert((
transform,
MeshMaterial3d(material_handle),
Visibility::Visible,
));
sync_state.stats.updated_entities += 1;
} else {
commands.spawn((
Mesh3d(mesh_handle.clone()),
MeshMaterial3d(material_handle),
transform,
Visibility::Visible,
PointCloudEntity {
layer_id,
point_index,
},
));
sync_state.stats.spawned_entities += 1;
}
}
}
for ((layer_id, _), entity) in existing_map {
if visible_overlay_ids.contains(&layer_id) {
commands.entity(entity).despawn();
sync_state.stats.despawned_entities += 1;
} else {
commands.entity(entity).insert(Visibility::Hidden);
sync_state.stats.hidden_entities += 1;
}
}
}
fn cached_material_handle(
materials: &mut Assets<StandardMaterial>,
cache: &mut CachedPointCloudAssets,
color: [f32; 4],
) -> Handle<StandardMaterial> {
let key = [
color[0].to_bits(),
color[1].to_bits(),
color[2].to_bits(),
color[3].to_bits(),
];
if let Some(handle) = cache.materials.get(&key) {
return handle.clone();
}
let handle = materials.add(StandardMaterial {
base_color: Color::srgba(color[0], color[1], color[2], color[3]),
alpha_mode: AlphaMode::Blend,
perceptual_roughness: 0.9,
metallic: 0.0,
..Default::default()
});
cache.materials.insert(key, handle.clone());
handle
}
fn build_point_transform(
point: &PointInstance,
state: &rustial_engine::MapState,
camera_origin: glam::DVec3,
) -> Transform {
let projected = state.camera().projection().project(&point.position);
let z = resolve_point_altitude(point, state);
let diameter = point.radius * 2.0;
Transform {
translation: Vec3::new(
(projected.position.x - camera_origin.x) as f32,
(projected.position.y - camera_origin.y) as f32,
(z - camera_origin.z) as f32,
),
scale: Vec3::splat(diameter as f32),
..Default::default()
}
}
fn resolve_point_altitude(
point: &PointInstance,
state: &rustial_engine::MapState,
) -> f64 {
let terrain = state.elevation_at(&point.position).unwrap_or(0.0);
match point.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => terrain,
rustial_engine::AltitudeMode::RelativeToGround => terrain + point.position.alt,
rustial_engine::AltitudeMode::Absolute => point.position.alt,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::RustialBevyPlugin;
use rustial_engine::{GeoCoord, MapState, PointCloudLayer, PointInstanceSet};
fn test_app() -> App {
let mut app = App::new();
app.add_plugins(bevy::app::TaskPoolPlugin::default());
app.add_plugins(bevy::time::TimePlugin::default());
app.init_resource::<Assets<Mesh>>();
app.init_resource::<Assets<StandardMaterial>>();
app.init_resource::<Assets<Image>>();
app.add_plugins(RustialBevyPlugin);
app
}
fn test_ramp() -> rustial_engine::ColorRamp {
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] },
])
}
#[test]
fn point_cloud_sync_spawns_point_entities() {
let mut app = test_app();
{
let mut state = app.world_mut().resource_mut::<crate::plugin::MapStateResource>();
state.0 = MapState::new();
state.0.push_layer(Box::new(PointCloudLayer::new(
"points",
PointInstanceSet::new(vec![
rustial_engine::PointInstance::new(GeoCoord::from_lat_lon(0.0, 0.0), 5.0)
.with_pick_id(1),
rustial_engine::PointInstance::new(GeoCoord::from_lat_lon(0.001, 0.001), 4.0)
.with_pick_id(2),
]),
test_ramp(),
)));
}
app.update();
app.update();
let count = {
let world = app.world_mut();
world.query::<&PointCloudEntity>().iter(world).count()
};
assert_eq!(count, 2);
}
#[test]
fn point_cloud_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 = MapState::new();
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_point_cloud(
"points",
PointInstanceSet::new(vec![
rustial_engine::PointInstance::new(GeoCoord::from_lat_lon(70.0, 120.0), 5.0)
.with_pick_id(1),
]),
test_ramp(),
);
}
app.update();
let count = {
let world = app.world_mut();
world.query::<&PointCloudEntity>().iter(world).count()
};
assert_eq!(count, 0);
}
#[test]
fn point_cloud_sync_updates_existing_entities_in_place() {
let mut app = test_app();
{
let mut state = app.world_mut().resource_mut::<crate::plugin::MapStateResource>();
state.0.set_point_cloud(
"points",
PointInstanceSet::new(vec![
rustial_engine::PointInstance::new(GeoCoord::from_lat_lon(0.0, 0.0), 5.0)
.with_pick_id(1),
]),
test_ramp(),
);
}
app.update();
app.update();
let entity_before = {
let world = app.world_mut();
let mut query = world.query::<(Entity, &PointCloudEntity)>();
let (entity, _) = query.single(world).expect("point entity");
entity
};
{
let mut state = app.world_mut().resource_mut::<crate::plugin::MapStateResource>();
state.0.set_point_cloud(
"points",
PointInstanceSet::new(vec![
rustial_engine::PointInstance::new(GeoCoord::from_lat_lon(0.0, 0.0), 8.0)
.with_pick_id(1)
.with_color([0.2, 0.8, 0.3, 0.9]),
]),
test_ramp(),
);
}
app.update();
let (entity_after, scale_after) = {
let world = app.world_mut();
let mut query = world.query::<(Entity, &PointCloudEntity, &Transform)>();
let (entity, _, transform) = query.single(world).expect("point entity");
(entity, transform.scale)
};
assert_eq!(entity_before, entity_after);
assert!((scale_after.x - 16.0).abs() < 1e-6);
}
#[test]
fn point_cloud_sync_large_update_reuses_entities_without_respawn() {
let mut app = test_app();
let count = 10_000usize;
let side = (count as f64).sqrt().ceil() as usize;
let make_points = |updated: bool| {
let mut points = Vec::with_capacity(count);
for idx in 0..count {
let row = idx / side;
let col = idx % side;
let lat = (row as f64 - side as f64 * 0.5) * 0.0005;
let lon = (col as f64 - side as f64 * 0.5) * 0.0005;
let mut point = rustial_engine::PointInstance::new(
GeoCoord::from_lat_lon(lat, lon),
4.0 + (idx % 8) as f64,
)
.with_pick_id(idx as u64 + 1)
.with_intensity(((idx % 100) as f32) / 100.0);
if updated && (count / 4..count / 4 + 512).contains(&idx) {
point.radius += 4.0;
point.intensity = 1.0 - point.intensity;
}
points.push(point);
}
PointInstanceSet::new(points)
};
{
let mut state = app.world_mut().resource_mut::<crate::plugin::MapStateResource>();
state.0.set_point_cloud("points", make_points(false), test_ramp());
}
app.update();
app.update();
let before = app.world().resource::<PointCloudSyncState>().stats;
{
let mut state = app.world_mut().resource_mut::<crate::plugin::MapStateResource>();
state.0.set_point_cloud("points", make_points(true), test_ramp());
}
app.update();
let (after_stats, entity_count) = {
let world = app.world_mut();
let count = world.query::<&PointCloudEntity>().iter(world).count() as u64;
(world.resource::<PointCloudSyncState>().stats, count)
};
assert_eq!(entity_count, count as u64);
assert_eq!(after_stats.spawned_entities, before.spawned_entities);
assert_eq!(after_stats.despawned_entities, before.despawned_entities);
assert!(after_stats.updated_entities >= before.updated_entities + count as u64);
}
}