rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
//! Hillshade layer contract.
//!
//! This is a backend-owned style contract for terrain shading, inspired by
//! MapLibre's `HillshadeStyleLayer`, but intentionally smaller to fit the
//! current Rustial architecture.

use crate::layer::{Layer, LayerId};
use std::any::Any;

/// Snapshot of effective hillshade parameters consumed by renderers.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HillshadeParams {
    /// Overall hillshade layer opacity / overlay strength.
    pub opacity: f32,
    /// RGBA colour used for lit slopes.
    pub highlight_color: [f32; 4],
    /// RGBA colour used for shadowed slopes.
    pub shadow_color: [f32; 4],
    /// RGBA accent colour mixed onto steeper terrain.
    pub accent_color: [f32; 4],
    /// Illumination direction in radians, clockwise from north.
    pub illumination_direction: f32,
    /// Illumination altitude in radians above the horizon.
    pub illumination_altitude: f32,
    /// Hillshade strength multiplier.
    pub exaggeration: f32,
}

impl Default for HillshadeParams {
    fn default() -> Self {
        Self {
            opacity: 1.0,
            highlight_color: [1.0, 1.0, 1.0, 1.0],
            shadow_color: [0.0, 0.0, 0.0, 1.0],
            accent_color: [0.42, 0.48, 0.42, 1.0],
            illumination_direction: 335.0f32.to_radians(),
            illumination_altitude: 45.0f32.to_radians(),
            exaggeration: 1.0,
        }
    }
}

/// Backend-owned hillshade styling parameters.
///
/// These parameters are consumed by renderers when drawing terrain. The layer
/// itself does not fetch tiles or generate meshes; it only contributes shading
/// state to the frame, similar to a style layer.
#[derive(Debug, Clone)]
pub struct HillshadeLayer {
    id: LayerId,
    name: String,
    visible: bool,
    opacity: f32,
    highlight_color: [f32; 4],
    shadow_color: [f32; 4],
    accent_color: [f32; 4],
    illumination_direction_deg: f32,
    illumination_altitude_deg: f32,
    exaggeration: f32,
}

impl HillshadeLayer {
    /// Create a hillshade layer with conservative MapLibre-like defaults.
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            id: LayerId::next(),
            name: name.into(),
            visible: true,
            opacity: 1.0,
            highlight_color: [1.0, 1.0, 1.0, 1.0],
            shadow_color: [0.0, 0.0, 0.0, 1.0],
            accent_color: [0.42, 0.48, 0.42, 1.0],
            illumination_direction_deg: 335.0,
            illumination_altitude_deg: 45.0,
            exaggeration: 1.0,
        }
    }

    /// RGBA colour used for lit slopes.
    #[inline]
    pub fn highlight_color(&self) -> [f32; 4] {
        self.highlight_color
    }

    /// RGBA colour used for shadowed slopes.
    #[inline]
    pub fn shadow_color(&self) -> [f32; 4] {
        self.shadow_color
    }

    /// RGBA accent colour mixed onto steeper terrain.
    #[inline]
    pub fn accent_color(&self) -> [f32; 4] {
        self.accent_color
    }

    /// Illumination direction in degrees clockwise from north.
    #[inline]
    pub fn illumination_direction_deg(&self) -> f32 {
        self.illumination_direction_deg
    }

    /// Illumination altitude in degrees above the horizon.
    #[inline]
    pub fn illumination_altitude_deg(&self) -> f32 {
        self.illumination_altitude_deg
    }

    /// Vertical emphasis applied to the hillshade effect only.
    #[inline]
    pub fn exaggeration(&self) -> f32 {
        self.exaggeration
    }

    /// Set highlight colour.
    pub fn set_highlight_color(&mut self, color: [f32; 4]) {
        self.highlight_color = color;
    }

    /// Set shadow colour.
    pub fn set_shadow_color(&mut self, color: [f32; 4]) {
        self.shadow_color = color;
    }

    /// Set accent colour.
    pub fn set_accent_color(&mut self, color: [f32; 4]) {
        self.accent_color = color;
    }

    /// Set illumination direction in degrees clockwise from north.
    pub fn set_illumination_direction_deg(&mut self, direction_deg: f32) {
        if direction_deg.is_finite() {
            self.illumination_direction_deg = direction_deg.rem_euclid(360.0);
        }
    }

    /// Set illumination altitude in degrees above the horizon.
    pub fn set_illumination_altitude_deg(&mut self, altitude_deg: f32) {
        if altitude_deg.is_finite() {
            self.illumination_altitude_deg = altitude_deg.clamp(0.0, 90.0);
        }
    }

    /// Set hillshade exaggeration multiplier.
    pub fn set_exaggeration(&mut self, exaggeration: f32) {
        if exaggeration.is_finite() {
            self.exaggeration = exaggeration.max(0.0);
        }
    }

    /// Return the effective renderer-facing parameter snapshot.
    pub fn effective_params(&self) -> HillshadeParams {
        HillshadeParams {
            opacity: self.opacity,
            highlight_color: self.highlight_color,
            shadow_color: self.shadow_color,
            accent_color: self.accent_color,
            illumination_direction: self.illumination_direction_deg.to_radians(),
            illumination_altitude: self.illumination_altitude_deg.to_radians(),
            exaggeration: self.exaggeration,
        }
    }
}

impl Layer for HillshadeLayer {
    fn id(&self) -> LayerId {
        self.id
    }

    fn kind(&self) -> crate::layer::LayerKind {
        crate::layer::LayerKind::Hillshade
    }

    fn name(&self) -> &str {
        &self.name
    }

    fn visible(&self) -> bool {
        self.visible
    }

    fn set_visible(&mut self, visible: bool) {
        self.visible = visible;
    }

    fn opacity(&self) -> f32 {
        self.opacity
    }

    fn set_opacity(&mut self, opacity: f32) {
        self.opacity = opacity.clamp(0.0, 1.0);
    }

    fn as_any(&self) -> &dyn Any {
        self
    }

    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
}

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

    #[test]
    fn defaults_match_expected_contract() {
        let layer = HillshadeLayer::new("hillshade");
        assert_eq!(layer.kind(), crate::layer::LayerKind::Hillshade);
        assert_eq!(layer.illumination_direction_deg(), 335.0);
        assert_eq!(layer.illumination_altitude_deg(), 45.0);
        assert_eq!(layer.exaggeration(), 1.0);
    }

    #[test]
    fn setters_clamp_values() {
        let mut layer = HillshadeLayer::new("hillshade");
        layer.set_illumination_direction_deg(725.0);
        layer.set_illumination_altitude_deg(120.0);
        layer.set_exaggeration(-2.0);

        assert_eq!(layer.illumination_direction_deg(), 5.0);
        assert_eq!(layer.illumination_altitude_deg(), 90.0);
        assert_eq!(layer.exaggeration(), 0.0);
    }
}