rustial-renderer-bevy 0.0.1

Bevy Engine renderer for the rustial 2.5D map engine
// ---------------------------------------------------------------------------
//! # Image overlay sync system
//!
//! Synchronises image overlay data from the engine's
//! [`FrameOutput::image_overlays`](rustial_engine::FrameOutput) to Bevy
//! textured quad entities each frame.
//!
//! ## Data flow
//!
//! ```text
//! ImageOverlayLayer::to_overlay_data()  (engine, during update_heavy_layers)
//!     |
//!     |  produces Vec<ImageOverlayData>
//!     |  corners in world-space metres (f64)
//!     |  RGBA8 pixel data
//!     v
//! sync_image_overlays (Update)            <-- this system
//!     |
//!     |  Phase 1: despawn stale entities
//!     |  Phase 2: reposition surviving entities
//!     |  Phase 3: spawn new textured quad entities
//!     v
//! Bevy render graph picks up mesh + material
//! ```
//!
//! ## Entity lifecycle
//!
//! Image overlay entities use a **count-based rebuild** strategy:
//!
//! 1. **Data empty** -- despawn all [`ImageOverlayEntity`] entities.
//! 2. **Count changed** -- full despawn + respawn.
//! 3. **Count stable** -- reposition only via `Transform.translation`.
// ---------------------------------------------------------------------------

use crate::components::ImageOverlayEntity;
use crate::plugin::MapStateResource;
use crate::systems::frame_change_detection::{frame_unchanged, FrameChangeDetection};
use bevy::asset::RenderAssetUsages;
use bevy::image::Image;
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
use glam::DVec3;

// ---------------------------------------------------------------------------
// Sync state resource
// ---------------------------------------------------------------------------

/// Tracks the last-synced overlay state so steady-state frames can
/// skip the full rebuild.
#[derive(Resource, Default)]
pub struct ImageOverlaySyncState {
    overlay_count: usize,
    last_origin: [i64; 3],
    /// Data pointer identity for each overlay (for change detection).
    data_ptrs: Vec<usize>,
}

fn quantise_origin(origin: DVec3) -> [i64; 3] {
    [
        (origin.x * 100.0) as i64,
        (origin.y * 100.0) as i64,
        (origin.z * 100.0) as i64,
    ]
}

// ---------------------------------------------------------------------------
// System
// ---------------------------------------------------------------------------

/// Synchronise engine image overlays to Bevy textured quad entities.
#[allow(clippy::too_many_arguments)]
pub fn sync_image_overlays(
    mut commands: Commands,
    state: Res<MapStateResource>,
    mut existing: Query<(Entity, &ImageOverlayEntity, &mut Transform)>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut images: ResMut<Assets<Image>>,
    mut sync_state: ResMut<ImageOverlaySyncState>,
    detection: Res<FrameChangeDetection>,
) {
    if frame_unchanged(&detection, &state.0) {
        return;
    }

    let camera_origin: DVec3 = state.0.scene_world_origin();
    let frame = state.0.frame_output();
    let overlays = &frame.image_overlays;

    // -----------------------------------------------------------------
    // Fast path: no overlays -- despawn everything.
    // -----------------------------------------------------------------
    if overlays.is_empty() {
        for (entity, _, _) in existing.iter() {
            commands.entity(entity).despawn();
        }
        sync_state.overlay_count = 0;
        sync_state.last_origin = [0; 3];
        sync_state.data_ptrs.clear();
        return;
    }

    // Check if overlay data has changed (Arc pointer identity).
    let current_ptrs: Vec<usize> = overlays
        .iter()
        .map(|o| std::sync::Arc::as_ptr(&o.data) as usize)
        .collect();
    let data_changed = current_ptrs != sync_state.data_ptrs;

    // -----------------------------------------------------------------
    // Stable path: count matches and data unchanged -- reposition only.
    // -----------------------------------------------------------------
    if overlays.len() == sync_state.overlay_count && !data_changed {
        let origin_key = quantise_origin(camera_origin);
        if origin_key == sync_state.last_origin {
            return;
        }
        for (_, overlay_entity, mut transform) in existing.iter_mut() {
            let offset = overlay_entity.spawn_origin - camera_origin;
            transform.translation = Vec3::new(offset.x as f32, offset.y as f32, offset.z as f32);
        }
        sync_state.last_origin = origin_key;
        return;
    }

    // -----------------------------------------------------------------
    // Structural or data change: full despawn + respawn.
    // -----------------------------------------------------------------
    for (entity, _, _) in existing.iter() {
        commands.entity(entity).despawn();
    }

    for overlay in overlays.iter() {
        if overlay.width == 0 || overlay.height == 0 || overlay.data.is_empty() {
            continue;
        }

        // Build a quad mesh from the 4 corners.
        let positions: Vec<[f32; 3]> = overlay
            .corners
            .iter()
            .map(|c| {
                [
                    (c[0] - camera_origin.x) as f32,
                    (c[1] - camera_origin.y) as f32,
                    (c[2] - camera_origin.z) as f32,
                ]
            })
            .collect();

        let uvs = vec![[0.0f32, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
        let normals = vec![[0.0f32, 0.0, 1.0]; 4];
        let indices = vec![0u32, 1, 2, 0, 2, 3];

        let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, Default::default());
        mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
        mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
        mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
        mesh.insert_indices(Indices::U32(indices));

        let mesh_handle = meshes.add(mesh);

        // Create Bevy Image from RGBA8 data.
        let image = Image::new(
            Extent3d {
                width: overlay.width,
                height: overlay.height,
                depth_or_array_layers: 1,
            },
            TextureDimension::D2,
            overlay.data.to_vec(),
            TextureFormat::Rgba8UnormSrgb,
            RenderAssetUsages::default(),
        );
        let image_handle = images.add(image);

        let material_handle = materials.add(StandardMaterial {
            base_color_texture: Some(image_handle),
            base_color: Color::linear_rgba(1.0, 1.0, 1.0, overlay.opacity),
            alpha_mode: AlphaMode::Blend,
            unlit: true,
            double_sided: true,
            ..Default::default()
        });

        commands.spawn((
            Mesh3d(mesh_handle),
            MeshMaterial3d(material_handle),
            Transform::default(),
            Visibility::default(),
            ImageOverlayEntity {
                layer_id: overlay.layer_id,
                spawn_origin: camera_origin,
            },
        ));
    }

    sync_state.overlay_count = overlays.len();
    sync_state.last_origin = quantise_origin(camera_origin);
    sync_state.data_ptrs = current_ptrs;
}