Skip to main content

rustial_engine/models/
instance.rs

1//! Model instances and placement on the map.
2
3use rustial_math::GeoCoord;
4
5/// How a model's altitude is interpreted relative to the terrain.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum AltitudeMode {
8    /// Absolute meters above the WGS-84 ellipsoid.
9    #[default]
10    Absolute,
11    /// Meters above the terrain surface.
12    RelativeToGround,
13    /// Placed directly on the terrain surface (altitude ignored).
14    ClampToGround,
15}
16
17/// A lightweight mesh representation loaded from a 3D model file.
18#[derive(Debug, Clone)]
19pub struct ModelMesh {
20    /// Vertex positions `[x, y, z]`.
21    pub positions: Vec<[f32; 3]>,
22    /// Vertex normals `[nx, ny, nz]`.
23    pub normals: Vec<[f32; 3]>,
24    /// Texture coordinates `[u, v]`.
25    pub uvs: Vec<[f32; 2]>,
26    /// Triangle indices.
27    pub indices: Vec<u32>,
28}
29
30/// A placed instance of a 3D model on the map.
31#[derive(Debug, Clone)]
32pub struct ModelInstance {
33    /// Geographic position of the model origin.
34    pub position: GeoCoord,
35    /// Altitude mode.
36    pub altitude_mode: AltitudeMode,
37    /// Rotation around the up axis in radians (0 = north).
38    pub heading: f64,
39    /// Pitch rotation in radians.
40    pub pitch: f64,
41    /// Roll rotation in radians.
42    pub roll: f64,
43    /// Uniform scale factor.
44    pub scale: f64,
45    /// The mesh data for this model.
46    pub mesh: ModelMesh,
47}
48
49impl ModelInstance {
50    /// Create a new model instance at a geographic position.
51    pub fn new(position: GeoCoord, mesh: ModelMesh) -> Self {
52        Self {
53            position,
54            altitude_mode: AltitudeMode::default(),
55            heading: 0.0,
56            pitch: 0.0,
57            roll: 0.0,
58            scale: 1.0,
59            mesh,
60        }
61    }
62
63    /// Resolve the final altitude in meters given terrain elevation at the position.
64    pub fn resolve_altitude(&self, terrain_elevation: Option<f64>) -> f64 {
65        match self.altitude_mode {
66            AltitudeMode::Absolute => self.position.alt,
67            AltitudeMode::RelativeToGround => {
68                let ground = terrain_elevation.unwrap_or(0.0);
69                ground + self.position.alt
70            }
71            AltitudeMode::ClampToGround => terrain_elevation.unwrap_or(0.0),
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    fn dummy_mesh() -> ModelMesh {
81        ModelMesh {
82            positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
83            normals: vec![[0.0, 0.0, 1.0]; 3],
84            uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
85            indices: vec![0, 1, 2],
86        }
87    }
88
89    #[test]
90    fn altitude_absolute() {
91        let inst = ModelInstance {
92            position: GeoCoord::new(0.0, 0.0, 500.0),
93            altitude_mode: AltitudeMode::Absolute,
94            ..ModelInstance::new(GeoCoord::default(), dummy_mesh())
95        };
96        assert!((inst.resolve_altitude(Some(100.0)) - 500.0).abs() < 1e-6);
97    }
98
99    #[test]
100    fn altitude_relative() {
101        let inst = ModelInstance {
102            position: GeoCoord::new(0.0, 0.0, 50.0),
103            altitude_mode: AltitudeMode::RelativeToGround,
104            ..ModelInstance::new(GeoCoord::default(), dummy_mesh())
105        };
106        assert!((inst.resolve_altitude(Some(100.0)) - 150.0).abs() < 1e-6);
107    }
108
109    #[test]
110    fn altitude_clamp() {
111        let inst = ModelInstance {
112            position: GeoCoord::new(0.0, 0.0, 999.0),
113            altitude_mode: AltitudeMode::ClampToGround,
114            ..ModelInstance::new(GeoCoord::default(), dummy_mesh())
115        };
116        assert!((inst.resolve_altitude(Some(200.0)) - 200.0).abs() < 1e-6);
117    }
118}