rustial-renderer-bevy 0.0.1

Bevy Engine renderer for the rustial 2.5D map engine
#![warn(missing_docs)]
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
// ---------------------------------------------------------------------------
//! # rustial-renderer-bevy
//!
//! Bevy Engine renderer for the rustial 2.5D map engine.
//!
//! Provides [`RustialBevyPlugin`] -- a Bevy `Plugin` that synchronises
//! the engine's [`MapState`](rustial_engine::MapState) with Bevy's ECS
//! and rendering pipeline.  Includes built-in viewport sync, default
//! input handling (pan / rotate / zoom), camera sync, and (with the
//! `http-tiles` feature) tile fetching + decoding.
//!
//! ## Crate layout
//!
//! ```text
//! lib.rs              crate root -- re-exports, helpers, backend selection
//! plugin.rs           RustialBevyPlugin, RustialBevyConfig, MapStateResource
//! components.rs       ECS marker components (TileEntity, TerrainEntity, ...)
//! systems/
//!   camera_sync.rs    engine camera -> Bevy Transform + Projection
//!   map_input.rs      viewport sync + default input handling
//!   tile_sync.rs      spawn / despawn / reposition tile quad entities
//!   terrain_sync.rs   spawn / despawn / reposition terrain mesh entities
//!   vector_sync.rs    spawn / despawn / reposition vector geometry entities
//!   model_sync.rs     spawn / despawn / reposition 3D model entities
//!   geo_entity_sync.rs  reposition user-placed MapEntity + GeoTransform
//!   texture_upload.rs   decoded tile imagery -> Bevy Image + material
//!   tile_fetch.rs       (http-tiles) async HTTP fetch + decode -> cache
//! ```
//!
//! ## System schedule
//!
//! ```text
//! PreUpdate
//!   sync_viewport            read Bevy Window size -> engine viewport
//!   handle_default_input     mouse / keyboard -> engine InputEvent
//!   update_map_state         tick engine (after input)
//!   sync_camera              engine camera -> Bevy Transform (after update)
//!   [http-tiles]
//!     request_tiles           spawn async fetch tasks (after update)
//!     collect_tiles           poll tasks -> TileImageCache (after request)
//!     push_visible_tiles      cache -> MapState visible tiles (after collect)
//!
//! Update
//!   sync_tiles               tile quad entities
//!   sync_terrain             terrain mesh entities
//!   sync_vectors             vector geometry entities
//!   sync_models              3D model entities
//!   sync_geo_entities        user MapEntity + GeoTransform
//!
//! PostUpdate
//!   upload_textures          decoded imagery -> StandardMaterial textures
//! ```
//!
//! ## Feature flags
//!
//! | Flag | Default | Description |
//! |------|---------|-------------|
//! | `http-tiles` | **yes** | Built-in async tile fetcher (reqwest + tokio + image). Adds [`TileFetchConfig`]. |
//!
//! ## Quick start
//!
//! ```rust,no_run
//! use bevy::prelude::*;
//! use rustial_renderer_bevy::{RustialBevyPlugin, RustialBevyConfig};
//!
//! App::new()
//!     .add_plugins(DefaultPlugins)
//!     .insert_resource(RustialBevyConfig {
//!         center: (51.1, 17.0),
//!         zoom: 10,
//!         ..default()
//!     })
//!     .add_plugins(RustialBevyPlugin)
//!     .run();
//! ```
//!
//! ## Placing user entities on the map
//!
//! Attach [`components::MapEntity`] + [`components::GeoTransform`] to any
//! Bevy entity.  The plugin automatically keeps its `Transform` in sync
//! with the map camera so the entity stays at the correct lat/lon.
//!
//! For manual coordinate conversion outside the ECS use [`geo_to_vec3`].
// ---------------------------------------------------------------------------

pub mod components;
pub mod debug_regression;
pub mod grid_scalar_material;
pub mod hillshade_material;
pub mod painter;
mod plugin;
pub mod systems;
pub mod tile_fog_material;

// ---------------------------------------------------------------------------
// Top-level re-exports
// ---------------------------------------------------------------------------

pub use painter::TerrainInteractionBuffersResource;
pub use plugin::{MapStateResource, RustialBevyConfig, RustialBevyPlugin};
pub use systems::map_input::MapInputEnabled;

#[cfg(feature = "http-tiles")]
pub use systems::tile_fetch::{SharedHttpClientResource, TileFetchConfig};

// ---------------------------------------------------------------------------
// Imports
// ---------------------------------------------------------------------------

use bevy::prelude::{DefaultPlugins, PluginGroup, Vec3, Window, WindowPlugin};
use bevy::render::settings::Backends;
use rustial_engine::GeoCoord;

// ---------------------------------------------------------------------------
// Coordinate helpers
// ---------------------------------------------------------------------------

/// Convert a geographic coordinate to a camera-relative Bevy [`Vec3`].
///
/// The returned position is in the same coordinate space used by all
/// map tile, terrain, vector, and model entities: **Web Mercator
/// metres relative to the current camera origin**
/// (`state.0.camera().target_world()`).
///
/// This is the function to call when you need to position your own
/// Bevy entities on the map from a system without using the
/// [`MapEntity`](components::MapEntity) + [`GeoTransform`](components::GeoTransform)
/// approach:
///
/// ```rust,ignore
/// use rustial_renderer_bevy::{geo_to_vec3, MapStateResource};
/// use rustial_engine::GeoCoord;
///
/// fn spawn_marker(
///     mut commands: Commands,
///     state: Res<MapStateResource>,
/// ) {
///     let pos = geo_to_vec3(
///         &state,
///         &GeoCoord::from_lat_lon(48.8566, 2.3522),
///     );
///     commands.spawn(Transform::from_translation(pos));
/// }
/// ```
///
/// # Note
///
/// The Z component is `GeoCoord.alt` converted to f32 (absolute
/// altitude, not camera-relative).  For terrain-aware placement use
/// [`MapEntity`](components::MapEntity) with
/// [`GeoAltitudeMode`](components::GeoAltitudeMode) instead.
pub fn geo_to_vec3(state: &MapStateResource, coord: &GeoCoord) -> Vec3 {
    let world = state.0.camera().projection().project(coord);
    let origin = state.0.scene_world_origin();
    Vec3::new(
        (world.position.x - origin.x) as f32,
        (world.position.y - origin.y) as f32,
        world.position.z as f32,
    )
}

// ---------------------------------------------------------------------------
// GPU backend selection
// ---------------------------------------------------------------------------

/// Read the `RUSTIAL_BACKEND` environment variable and return the
/// corresponding [`Backends`] flags.
///
/// Supported values include common WGPU backend names such as:
///
/// - `vulkan`
/// - `metal`
/// - `dx12`
/// - `gl`
/// - `browser-webgpu`
/// - `all`
///
/// Unknown or missing values fall back to [`Backends::PRIMARY`].
pub fn backends_from_env() -> Backends {
    let Ok(value) = std::env::var("RUSTIAL_BACKEND") else {
        return Backends::PRIMARY;
    };

    match value.trim().to_ascii_lowercase().as_str() {
        "vulkan" => Backends::VULKAN,
        "metal" => Backends::METAL,
        "dx12" | "d3d12" => Backends::DX12,
        "gl" => Backends::GL,
        "browser-webgpu" | "webgpu" => Backends::BROWSER_WEBGPU,
        "all" => Backends::all(),
        _ => Backends::PRIMARY,
    }
}

/// Construct a convenient Bevy plugin group for rustial examples.
///
/// Uses [`backends_from_env`] so examples can switch graphics backend via the
/// `RUSTIAL_BACKEND` environment variable.
pub fn default_plugins(title: impl Into<String>, size: (u32, u32)) -> impl PluginGroup {
    DefaultPlugins
        .set(WindowPlugin {
            primary_window: Some(Window {
                title: title.into(),
                resolution: (size.0, size.1).into(),
                ..Default::default()
            }),
            ..Default::default()
        })
        .set(bevy::render::RenderPlugin {
            render_creation: bevy::render::settings::RenderCreation::Automatic(
                bevy::render::settings::WgpuSettings {
                    backends: Some(backends_from_env()),
                    ..Default::default()
                },
            ),
            ..Default::default()
        })
}

#[cfg(test)]
mod tests {
    use super::*;
    use rustial_engine::rustial_math::GeoCoord;
    use rustial_engine::{CameraProjection, MapState};

    #[test]
    fn geo_to_vec3_uses_renderer_origin_for_non_mercator_camera() {
        let mut state = MapState::new();
        state.set_camera_projection(CameraProjection::Equirectangular);
        state.set_camera_target(GeoCoord::from_lat_lon(30.0, 20.0));

        let state = MapStateResource(state);
        let pos = geo_to_vec3(&state, &GeoCoord::from_lat_lon(30.0, 20.0));

        assert!(pos.x.abs() < 1e-3);
        assert!(pos.y.abs() < 1e-3);
    }
}