Skip to main content

rustial_engine/layers/
hillshade_layer.rs

1//! Hillshade layer contract.
2//!
3//! This is a backend-owned style contract for terrain shading, inspired by
4//! MapLibre's `HillshadeStyleLayer`, but intentionally smaller to fit the
5//! current Rustial architecture.
6
7use crate::layer::{Layer, LayerId};
8use std::any::Any;
9
10/// Snapshot of effective hillshade parameters consumed by renderers.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct HillshadeParams {
13    /// Overall hillshade layer opacity / overlay strength.
14    pub opacity: f32,
15    /// RGBA colour used for lit slopes.
16    pub highlight_color: [f32; 4],
17    /// RGBA colour used for shadowed slopes.
18    pub shadow_color: [f32; 4],
19    /// RGBA accent colour mixed onto steeper terrain.
20    pub accent_color: [f32; 4],
21    /// Illumination direction in radians, clockwise from north.
22    pub illumination_direction: f32,
23    /// Illumination altitude in radians above the horizon.
24    pub illumination_altitude: f32,
25    /// Hillshade strength multiplier.
26    pub exaggeration: f32,
27}
28
29impl Default for HillshadeParams {
30    fn default() -> Self {
31        Self {
32            opacity: 1.0,
33            highlight_color: [1.0, 1.0, 1.0, 1.0],
34            shadow_color: [0.0, 0.0, 0.0, 1.0],
35            accent_color: [0.42, 0.48, 0.42, 1.0],
36            illumination_direction: 335.0f32.to_radians(),
37            illumination_altitude: 45.0f32.to_radians(),
38            exaggeration: 1.0,
39        }
40    }
41}
42
43/// Backend-owned hillshade styling parameters.
44///
45/// These parameters are consumed by renderers when drawing terrain. The layer
46/// itself does not fetch tiles or generate meshes; it only contributes shading
47/// state to the frame, similar to a style layer.
48#[derive(Debug, Clone)]
49pub struct HillshadeLayer {
50    id: LayerId,
51    name: String,
52    visible: bool,
53    opacity: f32,
54    highlight_color: [f32; 4],
55    shadow_color: [f32; 4],
56    accent_color: [f32; 4],
57    illumination_direction_deg: f32,
58    illumination_altitude_deg: f32,
59    exaggeration: f32,
60}
61
62impl HillshadeLayer {
63    /// Create a hillshade layer with conservative MapLibre-like defaults.
64    pub fn new(name: impl Into<String>) -> Self {
65        Self {
66            id: LayerId::next(),
67            name: name.into(),
68            visible: true,
69            opacity: 1.0,
70            highlight_color: [1.0, 1.0, 1.0, 1.0],
71            shadow_color: [0.0, 0.0, 0.0, 1.0],
72            accent_color: [0.42, 0.48, 0.42, 1.0],
73            illumination_direction_deg: 335.0,
74            illumination_altitude_deg: 45.0,
75            exaggeration: 1.0,
76        }
77    }
78
79    /// RGBA colour used for lit slopes.
80    #[inline]
81    pub fn highlight_color(&self) -> [f32; 4] {
82        self.highlight_color
83    }
84
85    /// RGBA colour used for shadowed slopes.
86    #[inline]
87    pub fn shadow_color(&self) -> [f32; 4] {
88        self.shadow_color
89    }
90
91    /// RGBA accent colour mixed onto steeper terrain.
92    #[inline]
93    pub fn accent_color(&self) -> [f32; 4] {
94        self.accent_color
95    }
96
97    /// Illumination direction in degrees clockwise from north.
98    #[inline]
99    pub fn illumination_direction_deg(&self) -> f32 {
100        self.illumination_direction_deg
101    }
102
103    /// Illumination altitude in degrees above the horizon.
104    #[inline]
105    pub fn illumination_altitude_deg(&self) -> f32 {
106        self.illumination_altitude_deg
107    }
108
109    /// Vertical emphasis applied to the hillshade effect only.
110    #[inline]
111    pub fn exaggeration(&self) -> f32 {
112        self.exaggeration
113    }
114
115    /// Set highlight colour.
116    pub fn set_highlight_color(&mut self, color: [f32; 4]) {
117        self.highlight_color = color;
118    }
119
120    /// Set shadow colour.
121    pub fn set_shadow_color(&mut self, color: [f32; 4]) {
122        self.shadow_color = color;
123    }
124
125    /// Set accent colour.
126    pub fn set_accent_color(&mut self, color: [f32; 4]) {
127        self.accent_color = color;
128    }
129
130    /// Set illumination direction in degrees clockwise from north.
131    pub fn set_illumination_direction_deg(&mut self, direction_deg: f32) {
132        if direction_deg.is_finite() {
133            self.illumination_direction_deg = direction_deg.rem_euclid(360.0);
134        }
135    }
136
137    /// Set illumination altitude in degrees above the horizon.
138    pub fn set_illumination_altitude_deg(&mut self, altitude_deg: f32) {
139        if altitude_deg.is_finite() {
140            self.illumination_altitude_deg = altitude_deg.clamp(0.0, 90.0);
141        }
142    }
143
144    /// Set hillshade exaggeration multiplier.
145    pub fn set_exaggeration(&mut self, exaggeration: f32) {
146        if exaggeration.is_finite() {
147            self.exaggeration = exaggeration.max(0.0);
148        }
149    }
150
151    /// Return the effective renderer-facing parameter snapshot.
152    pub fn effective_params(&self) -> HillshadeParams {
153        HillshadeParams {
154            opacity: self.opacity,
155            highlight_color: self.highlight_color,
156            shadow_color: self.shadow_color,
157            accent_color: self.accent_color,
158            illumination_direction: self.illumination_direction_deg.to_radians(),
159            illumination_altitude: self.illumination_altitude_deg.to_radians(),
160            exaggeration: self.exaggeration,
161        }
162    }
163}
164
165impl Layer for HillshadeLayer {
166    fn id(&self) -> LayerId {
167        self.id
168    }
169
170    fn kind(&self) -> crate::layer::LayerKind {
171        crate::layer::LayerKind::Hillshade
172    }
173
174    fn name(&self) -> &str {
175        &self.name
176    }
177
178    fn visible(&self) -> bool {
179        self.visible
180    }
181
182    fn set_visible(&mut self, visible: bool) {
183        self.visible = visible;
184    }
185
186    fn opacity(&self) -> f32 {
187        self.opacity
188    }
189
190    fn set_opacity(&mut self, opacity: f32) {
191        self.opacity = opacity.clamp(0.0, 1.0);
192    }
193
194    fn as_any(&self) -> &dyn Any {
195        self
196    }
197
198    fn as_any_mut(&mut self) -> &mut dyn Any {
199        self
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::layer::Layer;
207
208    #[test]
209    fn defaults_match_expected_contract() {
210        let layer = HillshadeLayer::new("hillshade");
211        assert_eq!(layer.kind(), crate::layer::LayerKind::Hillshade);
212        assert_eq!(layer.illumination_direction_deg(), 335.0);
213        assert_eq!(layer.illumination_altitude_deg(), 45.0);
214        assert_eq!(layer.exaggeration(), 1.0);
215    }
216
217    #[test]
218    fn setters_clamp_values() {
219        let mut layer = HillshadeLayer::new("hillshade");
220        layer.set_illumination_direction_deg(725.0);
221        layer.set_illumination_altitude_deg(120.0);
222        layer.set_exaggeration(-2.0);
223
224        assert_eq!(layer.illumination_direction_deg(), 5.0);
225        assert_eq!(layer.illumination_altitude_deg(), 90.0);
226        assert_eq!(layer.exaggeration(), 0.0);
227    }
228}