Skip to main content

landmark_common/
mesh.rs

1//! Mesh utilities for triangle mesh handling
2
3use crate::MeshError;
4use glam::Vec3;
5
6#[cfg(feature = "std")]
7use std::fs::File;
8#[cfg(feature = "std")]
9use std::io::{BufRead, BufReader};
10#[cfg(feature = "std")]
11use std::path::Path;
12
13/// A simple triangle mesh
14#[derive(Debug, Clone, Default)]
15pub struct TriMesh {
16    /// Flat array of [x, y, z] coordinates
17    pub vertices: Vec<f32>,
18    /// Triangle indices, 3 per triangle
19    pub indices: Vec<i32>,
20    pub vert_count: usize,
21    pub tri_count: usize,
22}
23
24impl TriMesh {
25    /// Creates a new empty triangle mesh
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Loads a mesh from an OBJ file
31    ///
32    /// This method is only available when the `std` feature is enabled.
33    #[cfg(feature = "std")]
34    pub fn from_obj<P: AsRef<Path>>(path: P) -> Result<Self, MeshError> {
35        let file = File::open(path)?;
36        let reader = BufReader::new(file);
37
38        let mut mesh = Self::new();
39
40        for line in reader.lines() {
41            let line = line?;
42            Self::parse_obj_line(&line, &mut mesh)?;
43        }
44
45        Ok(mesh)
46    }
47
48    /// Parses OBJ content from a string
49    ///
50    /// This method is WASM-compatible and can parse OBJ data that has been
51    /// loaded into memory through other means (e.g., fetch API in browser).
52    ///
53    /// # Example
54    ///
55    /// ```
56    /// use landmark_common::TriMesh;
57    ///
58    /// let obj_content = r#"
59    /// v 0.0 0.0 0.0
60    /// v 1.0 0.0 0.0
61    /// v 0.5 1.0 0.0
62    /// f 1 2 3
63    /// "#;
64    ///
65    /// let mesh = TriMesh::from_obj_str(obj_content).unwrap();
66    /// assert_eq!(mesh.vert_count, 3);
67    /// assert_eq!(mesh.tri_count, 1);
68    /// ```
69    pub fn from_obj_str(content: &str) -> Result<Self, MeshError> {
70        let mut mesh = Self::new();
71
72        for line in content.lines() {
73            Self::parse_obj_line(line, &mut mesh)?;
74        }
75
76        Ok(mesh)
77    }
78
79    /// Parses a single line from an OBJ file
80    fn parse_obj_line(line: &str, mesh: &mut Self) -> Result<(), MeshError> {
81        let mut tokens = line.split_whitespace();
82
83        match tokens.next() {
84            Some("v") => {
85                // Vertex
86                let x = tokens
87                    .next()
88                    .ok_or_else(|| {
89                        MeshError::ObjParse("Invalid vertex: missing x coordinate".to_string())
90                    })?
91                    .parse::<f32>()
92                    .map_err(|_| {
93                        MeshError::ObjParse(
94                            "Invalid vertex: x coordinate is not a number".to_string(),
95                        )
96                    })?;
97
98                let y = tokens
99                    .next()
100                    .ok_or_else(|| {
101                        MeshError::ObjParse("Invalid vertex: missing y coordinate".to_string())
102                    })?
103                    .parse::<f32>()
104                    .map_err(|_| {
105                        MeshError::ObjParse(
106                            "Invalid vertex: y coordinate is not a number".to_string(),
107                        )
108                    })?;
109
110                let z = tokens
111                    .next()
112                    .ok_or_else(|| {
113                        MeshError::ObjParse("Invalid vertex: missing z coordinate".to_string())
114                    })?
115                    .parse::<f32>()
116                    .map_err(|_| {
117                        MeshError::ObjParse(
118                            "Invalid vertex: z coordinate is not a number".to_string(),
119                        )
120                    })?;
121
122                mesh.vertices.push(x);
123                mesh.vertices.push(y);
124                mesh.vertices.push(z);
125                mesh.vert_count += 1;
126            }
127            Some("f") => {
128                // Face
129                let mut face_indices = Vec::new();
130
131                for token in tokens {
132                    let index_str = token.split('/').next().ok_or_else(|| {
133                        MeshError::ObjParse("Invalid face: missing vertex index".to_string())
134                    })?;
135
136                    let index = index_str.parse::<i32>().map_err(|_| {
137                        MeshError::ObjParse(
138                            "Invalid face: vertex index is not a number".to_string(),
139                        )
140                    })? - 1; // OBJ indices are 1-based
141
142                    face_indices.push(index);
143                }
144
145                if face_indices.len() < 3 {
146                    return Err(MeshError::ObjParse(
147                        "Invalid face: less than 3 vertices".to_string(),
148                    ));
149                }
150
151                // Triangulate the face if it has more than 3 vertices (simple fan triangulation)
152                for i in 1..(face_indices.len() - 1) {
153                    mesh.indices.push(face_indices[0]);
154                    mesh.indices.push(face_indices[i]);
155                    mesh.indices.push(face_indices[i + 1]);
156                    mesh.tri_count += 1;
157                }
158            }
159            _ => {
160                // Skip other lines (normals, texture coordinates, comments, etc.)
161            }
162        }
163
164        Ok(())
165    }
166
167    /// Calculates the axis-aligned bounding box of the mesh
168    pub fn calculate_bounds(&self) -> (Vec3, Vec3) {
169        if self.vert_count == 0 {
170            return (Vec3::ZERO, Vec3::ZERO);
171        }
172
173        let mut bmin = Vec3::new(f32::MAX, f32::MAX, f32::MAX);
174        let mut bmax = Vec3::new(f32::MIN, f32::MIN, f32::MIN);
175
176        for i in 0..self.vert_count {
177            let x = self.vertices[i * 3];
178            let y = self.vertices[i * 3 + 1];
179            let z = self.vertices[i * 3 + 2];
180
181            bmin.x = bmin.x.min(x);
182            bmin.y = bmin.y.min(y);
183            bmin.z = bmin.z.min(z);
184
185            bmax.x = bmax.x.max(x);
186            bmax.y = bmax.y.max(y);
187            bmax.z = bmax.z.max(z);
188        }
189
190        (bmin, bmax)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_from_obj_str_simple_triangle() {
200        let obj = r#"
201v 0.0 0.0 0.0
202v 1.0 0.0 0.0
203v 0.5 1.0 0.0
204f 1 2 3
205"#;
206        let mesh = TriMesh::from_obj_str(obj).unwrap();
207        assert_eq!(mesh.vert_count, 3);
208        assert_eq!(mesh.tri_count, 1);
209        assert_eq!(mesh.vertices.len(), 9);
210        assert_eq!(mesh.indices.len(), 3);
211    }
212
213    #[test]
214    fn test_from_obj_str_quad_triangulation() {
215        let obj = r#"
216v 0.0 0.0 0.0
217v 1.0 0.0 0.0
218v 1.0 1.0 0.0
219v 0.0 1.0 0.0
220f 1 2 3 4
221"#;
222        let mesh = TriMesh::from_obj_str(obj).unwrap();
223        assert_eq!(mesh.vert_count, 4);
224        assert_eq!(mesh.tri_count, 2); // Quad triangulated into 2 triangles
225        assert_eq!(mesh.indices.len(), 6);
226    }
227
228    #[test]
229    fn test_from_obj_str_with_texture_coords() {
230        // OBJ with texture coordinates (v/vt format)
231        let obj = r#"
232v 0.0 0.0 0.0
233v 1.0 0.0 0.0
234v 0.5 1.0 0.0
235vt 0.0 0.0
236vt 1.0 0.0
237vt 0.5 1.0
238f 1/1 2/2 3/3
239"#;
240        let mesh = TriMesh::from_obj_str(obj).unwrap();
241        assert_eq!(mesh.vert_count, 3);
242        assert_eq!(mesh.tri_count, 1);
243    }
244
245    #[test]
246    fn test_from_obj_str_with_normals() {
247        // OBJ with normals (v/vt/vn format)
248        let obj = r#"
249v 0.0 0.0 0.0
250v 1.0 0.0 0.0
251v 0.5 1.0 0.0
252vn 0.0 0.0 1.0
253f 1//1 2//1 3//1
254"#;
255        let mesh = TriMesh::from_obj_str(obj).unwrap();
256        assert_eq!(mesh.vert_count, 3);
257        assert_eq!(mesh.tri_count, 1);
258    }
259
260    #[test]
261    fn test_from_obj_str_skips_comments() {
262        let obj = r#"
263# This is a comment
264v 0.0 0.0 0.0
265# Another comment
266v 1.0 0.0 0.0
267v 0.5 1.0 0.0
268f 1 2 3
269"#;
270        let mesh = TriMesh::from_obj_str(obj).unwrap();
271        assert_eq!(mesh.vert_count, 3);
272        assert_eq!(mesh.tri_count, 1);
273    }
274
275    #[test]
276    fn test_from_obj_str_invalid_vertex() {
277        let obj = "v 0.0 0.0"; // Missing z coordinate
278        let result = TriMesh::from_obj_str(obj);
279        assert!(result.is_err());
280    }
281
282    #[test]
283    fn test_from_obj_str_invalid_face() {
284        let obj = r#"
285v 0.0 0.0 0.0
286v 1.0 0.0 0.0
287f 1 2
288"#;
289        let result = TriMesh::from_obj_str(obj);
290        assert!(result.is_err());
291    }
292}