rustial-renderer-bevy 1.0.0

Bevy Engine renderer for the rustial 2.5D map engine
//! Minimal painter / pass-plan model for the Bevy renderer.
//!
//! This mirrors the WGPU renderer's explicit pass planning and gives Bevy
//! systems a renderer-architecture layer closer to MapLibre's `Painter`, even
//! though execution is mapped onto Bevy schedules and material phases.

use bevy::prelude::*;

use crate::plugin::MapStateResource;

/// Ordered render-pass kinds used by the Bevy integration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PainterPass {
    /// Explicit sky / atmosphere stage.
    SkyAtmosphere,
    /// Renderer-owned terrain depth / coordinate data stage.
    TerrainData,
    /// Main opaque scene phase: tiles / terrain / vectors / models.
    OpaqueScene,
    /// Hillshade overlay phase rendered on top of the opaque scene.
    HillshadeOverlay,
}

/// Per-frame painter pass plan resource.
#[derive(Resource, Debug, Clone, Default)]
pub struct PainterPlanResource {
    passes: Vec<PainterPass>,
}

/// Bevy-side terrain buffer metadata mirroring renderer-owned terrain data state.
#[derive(Resource, Debug, Clone, Default)]
pub struct TerrainInteractionBuffersResource {
    /// Current viewport-sized interaction buffer extent.
    pub size: (u32, u32),
    /// Whether terrain depth/coord capture is active this frame.
    pub active: bool,
}

impl PainterPlanResource {
    /// Build a plan from the current engine state.
    pub fn from_state(state: &rustial_engine::MapState) -> Self {
        let mut passes = vec![PainterPass::SkyAtmosphere];
        if state.terrain().enabled() && !state.terrain_meshes().is_empty() {
            passes.push(PainterPass::TerrainData);
        }
        if state.terrain().enabled()
            && state.hillshade().is_some()
            && !state.hillshade_rasters().is_empty()
        {
            passes.push(PainterPass::OpaqueScene);
            passes.push(PainterPass::HillshadeOverlay);
        } else {
            passes.push(PainterPass::OpaqueScene);
        }
        Self { passes }
    }

    /// Iterate passes in execution order.
    pub fn iter(&self) -> impl Iterator<Item = PainterPass> + '_ {
        self.passes.iter().copied()
    }

    /// Whether the frame contains a given pass.
    pub fn contains(&self, pass: PainterPass) -> bool {
        self.passes.contains(&pass)
    }
}

/// System-set ordering that mirrors the painter pass order.
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PainterSet {
    /// Sky / atmosphere stage.
    SkyAtmosphere,
    /// Terrain depth / coordinate stage.
    TerrainData,
    /// Base opaque scene sync / upload stage.
    OpaqueScene,
    /// Hillshade overlay sync / upload stage.
    HillshadeOverlay,
}

/// Refresh the painter plan from `MapState` once per frame.
pub fn update_painter_plan(
    state: Res<MapStateResource>,
    mut plan: ResMut<PainterPlanResource>,
) {
    *plan = PainterPlanResource::from_state(&state.0);
}

/// Refresh terrain interaction-buffer metadata from the engine state.
pub fn update_terrain_interaction_buffers(
    state: Res<MapStateResource>,
    mut buffers: ResMut<TerrainInteractionBuffersResource>,
) {
    let camera = state.0.camera();
    buffers.size = (camera.viewport_width().max(1), camera.viewport_height().max(1));
    buffers.active = state.0.terrain().enabled() && !state.0.terrain_meshes().is_empty();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn opaque_is_always_present() {
        let state = rustial_engine::MapState::new();
        let plan = PainterPlanResource::from_state(&state);
        assert_eq!(plan.iter().collect::<Vec<_>>(), vec![PainterPass::SkyAtmosphere, PainterPass::OpaqueScene]);
    }
}