Skip to main content

dreamwell_engine/content/
mesh.rs

1//! Imported mesh types — GPU-neutral geometry data.
2
3use serde::{Deserialize, Serialize};
4
5/// A single imported mesh with interleaved vertex data and index buffer.
6/// Vertices, normals, and UVs share the same length (one per vertex).
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ImportedMesh {
9    /// Human-readable name (from source file).
10    pub name: String,
11    /// Vertex positions `[x, y, z]`.
12    pub positions: Vec<[f32; 3]>,
13    /// Per-vertex normals `[nx, ny, nz]`. Same length as `positions`.
14    pub normals: Vec<[f32; 3]>,
15    /// Per-vertex texture coordinates `[u, v]`. Same length as `positions`.
16    pub uvs: Vec<[f32; 2]>,
17    /// Triangle indices (3 per triangle). Each index references a vertex.
18    pub indices: Vec<u32>,
19}
20
21impl ImportedMesh {
22    /// Number of vertices.
23    pub fn vertex_count(&self) -> usize {
24        self.positions.len()
25    }
26
27    /// Number of triangles.
28    pub fn triangle_count(&self) -> usize {
29        self.indices.len() / 3
30    }
31
32    /// Validate mesh integrity.
33    pub fn validate(&self) -> Result<(), String> {
34        let n = self.positions.len();
35        if self.normals.len() != n {
36            return Err(format!(
37                "content_mesh_normals_mismatch:expected {n} normals, got {}",
38                self.normals.len()
39            ));
40        }
41        if self.uvs.len() != n {
42            return Err(format!(
43                "content_mesh_uvs_mismatch:expected {n} uvs, got {}",
44                self.uvs.len()
45            ));
46        }
47        if !self.indices.len().is_multiple_of(3) {
48            return Err(format!(
49                "content_mesh_indices_not_triangles:index count {} not divisible by 3",
50                self.indices.len()
51            ));
52        }
53        let max_idx = n as u32;
54        for &idx in &self.indices {
55            if idx >= max_idx {
56                return Err(format!("content_mesh_index_oob:index {idx} >= vertex count {n}"));
57            }
58        }
59        Ok(())
60    }
61}
62
63/// A set of imported meshes from a single source file.
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65pub struct ImportedMeshSet {
66    pub meshes: Vec<ImportedMesh>,
67}
68
69impl ImportedMeshSet {
70    /// Total vertex count across all meshes.
71    pub fn total_vertices(&self) -> usize {
72        self.meshes.iter().map(|m| m.vertex_count()).sum()
73    }
74
75    /// Total triangle count across all meshes.
76    pub fn total_triangles(&self) -> usize {
77        self.meshes.iter().map(|m| m.triangle_count()).sum()
78    }
79
80    /// Validate all meshes.
81    pub fn validate(&self) -> Result<(), String> {
82        for (i, mesh) in self.meshes.iter().enumerate() {
83            mesh.validate().map_err(|e| format!("mesh[{i}] '{}': {e}", mesh.name))?;
84        }
85        Ok(())
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn triangle_mesh() -> ImportedMesh {
94        ImportedMesh {
95            name: "Triangle".into(),
96            positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
97            normals: vec![[0.0, 0.0, 1.0]; 3],
98            uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
99            indices: vec![0, 1, 2],
100        }
101    }
102
103    #[test]
104    fn valid_triangle() {
105        let m = triangle_mesh();
106        assert!(m.validate().is_ok());
107        assert_eq!(m.vertex_count(), 3);
108        assert_eq!(m.triangle_count(), 1);
109    }
110
111    #[test]
112    fn normals_mismatch() {
113        let mut m = triangle_mesh();
114        m.normals.pop();
115        assert!(m.validate().unwrap_err().contains("normals_mismatch"));
116    }
117
118    #[test]
119    fn uvs_mismatch() {
120        let mut m = triangle_mesh();
121        m.uvs.pop();
122        assert!(m.validate().unwrap_err().contains("uvs_mismatch"));
123    }
124
125    #[test]
126    fn indices_not_triangles() {
127        let mut m = triangle_mesh();
128        m.indices.push(0);
129        assert!(m.validate().unwrap_err().contains("not_triangles"));
130    }
131
132    #[test]
133    fn index_out_of_bounds() {
134        let mut m = triangle_mesh();
135        m.indices[2] = 99;
136        assert!(m.validate().unwrap_err().contains("index_oob"));
137    }
138
139    #[test]
140    fn mesh_set_totals() {
141        let set = ImportedMeshSet {
142            meshes: vec![triangle_mesh(), triangle_mesh()],
143        };
144        assert_eq!(set.total_vertices(), 6);
145        assert_eq!(set.total_triangles(), 2);
146        assert!(set.validate().is_ok());
147    }
148
149    #[test]
150    fn empty_mesh_set() {
151        let set = ImportedMeshSet::default();
152        assert_eq!(set.total_vertices(), 0);
153        assert!(set.validate().is_ok());
154    }
155}