rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
//! Model instances and placement on the map.

use rustial_math::GeoCoord;

/// How a model's altitude is interpreted relative to the terrain.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AltitudeMode {
    /// Absolute meters above the WGS-84 ellipsoid.
    #[default]
    Absolute,
    /// Meters above the terrain surface.
    RelativeToGround,
    /// Placed directly on the terrain surface (altitude ignored).
    ClampToGround,
}

/// A lightweight mesh representation loaded from a 3D model file.
#[derive(Debug, Clone)]
pub struct ModelMesh {
    /// Vertex positions `[x, y, z]`.
    pub positions: Vec<[f32; 3]>,
    /// Vertex normals `[nx, ny, nz]`.
    pub normals: Vec<[f32; 3]>,
    /// Texture coordinates `[u, v]`.
    pub uvs: Vec<[f32; 2]>,
    /// Triangle indices.
    pub indices: Vec<u32>,
}

/// A placed instance of a 3D model on the map.
#[derive(Debug, Clone)]
pub struct ModelInstance {
    /// Geographic position of the model origin.
    pub position: GeoCoord,
    /// Altitude mode.
    pub altitude_mode: AltitudeMode,
    /// Rotation around the up axis in radians (0 = north).
    pub heading: f64,
    /// Pitch rotation in radians.
    pub pitch: f64,
    /// Roll rotation in radians.
    pub roll: f64,
    /// Uniform scale factor.
    pub scale: f64,
    /// The mesh data for this model.
    pub mesh: ModelMesh,
}

impl ModelInstance {
    /// Create a new model instance at a geographic position.
    pub fn new(position: GeoCoord, mesh: ModelMesh) -> Self {
        Self {
            position,
            altitude_mode: AltitudeMode::default(),
            heading: 0.0,
            pitch: 0.0,
            roll: 0.0,
            scale: 1.0,
            mesh,
        }
    }

    /// Resolve the final altitude in meters given terrain elevation at the position.
    pub fn resolve_altitude(&self, terrain_elevation: Option<f64>) -> f64 {
        match self.altitude_mode {
            AltitudeMode::Absolute => self.position.alt,
            AltitudeMode::RelativeToGround => {
                let ground = terrain_elevation.unwrap_or(0.0);
                ground + self.position.alt
            }
            AltitudeMode::ClampToGround => terrain_elevation.unwrap_or(0.0),
        }
    }
}

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

    fn dummy_mesh() -> ModelMesh {
        ModelMesh {
            positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
            normals: vec![[0.0, 0.0, 1.0]; 3],
            uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
            indices: vec![0, 1, 2],
        }
    }

    #[test]
    fn altitude_absolute() {
        let inst = ModelInstance {
            position: GeoCoord::new(0.0, 0.0, 500.0),
            altitude_mode: AltitudeMode::Absolute,
            ..ModelInstance::new(GeoCoord::default(), dummy_mesh())
        };
        assert!((inst.resolve_altitude(Some(100.0)) - 500.0).abs() < 1e-6);
    }

    #[test]
    fn altitude_relative() {
        let inst = ModelInstance {
            position: GeoCoord::new(0.0, 0.0, 50.0),
            altitude_mode: AltitudeMode::RelativeToGround,
            ..ModelInstance::new(GeoCoord::default(), dummy_mesh())
        };
        assert!((inst.resolve_altitude(Some(100.0)) - 150.0).abs() < 1e-6);
    }

    #[test]
    fn altitude_clamp() {
        let inst = ModelInstance {
            position: GeoCoord::new(0.0, 0.0, 999.0),
            altitude_mode: AltitudeMode::ClampToGround,
            ..ModelInstance::new(GeoCoord::default(), dummy_mesh())
        };
        assert!((inst.resolve_altitude(Some(200.0)) - 200.0).abs() < 1e-6);
    }
}