alum/
obj.rs

1use crate::{
2    Handle, HasIterators, HasTopology, VH, VPropBuf,
3    error::Error,
4    mesh::{Adaptor, FloatScalarAdaptor, PolyMeshT},
5};
6use std::{cell::Ref, fmt::Display, fs::OpenOptions, io, path::Path};
7
8impl<A> PolyMeshT<3, A>
9where
10    A: Adaptor<3> + FloatScalarAdaptor<3>,
11{
12    fn load_from_models(models: Vec<tobj::Model>) -> Result<Self, Error> {
13        let (nverts, nfaces) = models
14            .iter()
15            .fold((0usize, 0usize), |(nverts, nfaces), model| {
16                let msh = &model.mesh;
17                (
18                    nverts + (msh.positions.len() / 3),
19                    nfaces + msh.face_arities.len(),
20                )
21            });
22        let nedges = nfaces * 3 / 2; // Estimate.
23        let mut outmesh = PolyMeshT::<3, A>::with_capacity(nverts, nedges, nfaces);
24        let mut positions = Vec::new();
25        let mut fvs: Vec<VH> = Vec::new();
26        for model in models {
27            let mesh = &model.mesh;
28            if mesh.positions.len() % 3 != 0 {
29                return Err(Error::IncorrectNumberOfCoordinates(mesh.positions.len()));
30            }
31            positions.clear();
32            positions.extend(mesh.positions.chunks(3).map(|triplet| {
33                A::vector([
34                    A::scalarf64(triplet[0]),
35                    A::scalarf64(triplet[1]),
36                    A::scalarf64(triplet[2]),
37                ])
38            }));
39            let nbefore = outmesh.num_vertices() as u32;
40            let vertices = outmesh.add_vertices(&positions)?;
41            assert_eq!(
42                vertices,
43                (nbefore..(nbefore + positions.len() as u32)).into(),
44                "Vertex indices are expected to be in a contiguous range."
45            );
46            // Faces.
47            let mut start = 0usize;
48            if mesh.face_arities.is_empty() {
49                // Load triangles.
50                for indices in mesh.indices.chunks(3) {
51                    fvs.clear();
52                    fvs.extend(indices.iter().map(|i| -> VH { i.into() }));
53                    outmesh.add_face(&fvs)?;
54                }
55            } else {
56                for size in &mesh.face_arities {
57                    let size = *size as usize;
58                    let indices = &mesh.indices[start..(start + size)];
59                    start += size;
60                    fvs.clear();
61                    fvs.extend(indices.iter().map(|i| -> VH { i.into() }));
62                    outmesh.add_face(&fvs)?;
63                }
64            }
65        }
66        Ok(outmesh)
67    }
68
69    /// Load a polygon mesh from an obj file.
70    pub fn load_obj<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
71        let path: &Path = path.as_ref();
72        if path
73            .extension()
74            .ok_or(Error::InvalidObjFile(path.to_path_buf()))?
75            .to_str()
76            .ok_or(Error::InvalidObjFile(path.to_path_buf()))?
77            != "obj"
78        {
79            return Err(Error::InvalidObjFile(path.to_path_buf()));
80        }
81        let options = tobj::LoadOptions::default();
82        let (models, _) =
83            tobj::load_obj(path, &options).map_err(|e| Error::ObjLoadFailed(format!("{e}")))?;
84        Self::load_from_models(models)
85    }
86
87    /// Read a polygon mesh from the stream assuming OBJ format.
88    pub fn read_obj(reader: &mut impl io::BufRead) -> Result<Self, Error> {
89        let options = tobj::LoadOptions::default();
90        let (models, _) = tobj::load_obj_buf(reader, &options, |_| {
91            tobj::MTLLoadResult::Ok(Default::default())
92        })
93        .map_err(|e| Error::ObjLoadFailed(format!("{e}")))?;
94        Self::load_from_models(models)
95    }
96
97    fn write_obj_impl(
98        &self,
99        points: Ref<VPropBuf<A::Vector>>,
100        vnormals: Option<Ref<VPropBuf<A::Vector>>>,
101        mut w: impl io::Write,
102    ) -> Result<(), io::Error>
103    where
104        A::Scalar: Display,
105    {
106        writeln!(w, "# {} vertices", self.num_vertices())?;
107        for pos in points.iter() {
108            writeln!(
109                w,
110                "v {} {} {}",
111                A::vector_coord(pos, 0),
112                A::vector_coord(pos, 1),
113                A::vector_coord(pos, 2)
114            )?;
115        }
116        if let Some(vnormals) = vnormals {
117            writeln!(w, "# {} normals", vnormals.len())?;
118            for n in vnormals.iter() {
119                writeln!(
120                    w,
121                    "vn {} {} {}",
122                    A::vector_coord(n, 0),
123                    A::vector_coord(n, 1),
124                    A::vector_coord(n, 2)
125                )?;
126            }
127        }
128        writeln!(w, "# {} faces", self.num_faces())?;
129        for f in self.faces() {
130            write!(w, "f")?;
131            for v in self.fv_ccw_iter(f) {
132                write!(w, " {}", v.index() + 1)?;
133            }
134            writeln!(w)?;
135        }
136        Ok(())
137    }
138
139    pub fn write_obj(&self, out: &mut impl io::Write) -> Result<(), Error>
140    where
141        A::Scalar: Display,
142    {
143        self.check_for_deleted()?;
144        self.check_topology()?;
145        let points = self.points();
146        let vnormals = self.vertex_normals();
147        // Deliberately not returning this directly to keep `points` alive.
148        self.write_obj_impl(
149            points.try_borrow()?,
150            match &vnormals {
151                Some(n) => Some(n.try_borrow()?),
152                None => None,
153            },
154            out,
155        )
156        .map_err(|_| Error::CannotWriteOBJ)?;
157        Ok(())
158    }
159
160    /// Write this mesh into an obj file.
161    pub fn save_obj<P: AsRef<Path>>(&self, path: P) -> Result<(), Error>
162    where
163        A::Scalar: Display,
164    {
165        let path = path.as_ref();
166        if path
167            .extension()
168            .ok_or(Error::InvalidObjFile(path.to_path_buf()))?
169            .to_str()
170            .ok_or(Error::InvalidObjFile(path.to_path_buf()))?
171            != "obj"
172        {
173            return Err(Error::InvalidObjFile(path.to_path_buf()));
174        }
175        let file = OpenOptions::new()
176            .write(true)
177            .truncate(true)
178            .create(true)
179            .open(path)
180            .map_err(|_| Error::CannotOpenFile(path.to_path_buf()))?;
181        let mut writer = io::BufWriter::new(&file);
182        self.write_obj(&mut writer)
183    }
184}
185
186#[cfg(test)]
187pub(crate) mod test {
188    use crate::{HasTopology, mesh::PolyMeshF32};
189    use core::str;
190    use std::{io, path::PathBuf};
191
192    pub(crate) fn bunny_mesh() -> PolyMeshF32 {
193        let path = {
194            let mut dirpath = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
195            dirpath.push("assets");
196            dirpath.push("bunny.obj");
197            dirpath
198        };
199        let mesh = PolyMeshF32::load_obj(&path).expect("Cannot load mesh");
200        let mut points = mesh.points();
201        let mut points = points.try_borrow_mut().expect("Cannot borrow points");
202        for p in points.iter_mut() {
203            *p *= 10.0;
204        }
205        mesh
206    }
207
208    fn large_bunny_mesh() -> PolyMeshF32 {
209        let path = {
210            let mut dirpath = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
211            dirpath.push("assets");
212            dirpath.push("bunny_large.obj");
213            dirpath
214        };
215        PolyMeshF32::load_obj(&path).expect("Cannot load mesh")
216    }
217
218    #[test]
219    fn t_bunny_topol() {
220        let mesh = bunny_mesh();
221        assert_eq!(2503, mesh.num_vertices());
222        assert_eq!(7473, mesh.num_edges());
223        assert_eq!(4968, mesh.num_faces());
224        assert_eq!(42, mesh.edges().filter(|e| e.is_boundary(&mesh)).count());
225    }
226
227    #[test]
228    fn t_bunny_area() {
229        let mesh = bunny_mesh();
230        assert_eq!(
231            mesh.try_calc_area().expect("Unable to compute the area"),
232            5.6468635
233        );
234    }
235
236    #[test]
237    fn t_bunny_volume() {
238        let mesh = bunny_mesh(); // This mesh is not closed.
239        assert_eq!(mesh.try_calc_volume().expect("Cannot compute volume"), 0.);
240    }
241
242    #[test]
243    fn t_large_bunny_volume() {
244        let mesh = large_bunny_mesh();
245        assert_eq!(
246            mesh.try_calc_volume().expect("Cannot compute volume"),
247            6.039229
248        );
249    }
250
251    #[test]
252    fn t_bunny_topology_check() {
253        bunny_mesh()
254            .check_topology()
255            .expect("Found topological errors");
256    }
257
258    #[test]
259    fn t_large_bunny_topology_check() {
260        large_bunny_mesh()
261            .check_topology()
262            .expect("Found topological errors");
263    }
264
265    #[test]
266    fn t_quad_bunny() {
267        let path = {
268            let mut dirpath = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
269            dirpath.push("assets");
270            dirpath.push("bunny_quad.obj");
271            dirpath
272        };
273        let mesh = PolyMeshF32::load_obj(path).expect("Cannot load mesh from obj");
274        mesh.check_topology().expect("Topological errors found");
275    }
276
277    #[test]
278    fn t_write_obj_to_string() {
279        let mesh = PolyMeshF32::hexahedron(1.0).expect("Cannot make a box");
280        let mut writer = io::BufWriter::new(Vec::new());
281        mesh.write_obj(&mut writer)
282            .expect("Cannot write obj to string");
283        assert_eq!(
284            "
285# 8 vertices
286v -0.57735026 -0.57735026 -0.57735026
287v 0.57735026 -0.57735026 -0.57735026
288v 0.57735026 0.57735026 -0.57735026
289v -0.57735026 0.57735026 -0.57735026
290v -0.57735026 -0.57735026 0.57735026
291v 0.57735026 -0.57735026 0.57735026
292v 0.57735026 0.57735026 0.57735026
293v -0.57735026 0.57735026 0.57735026
294# 6 faces
295f 4 3 2 1
296f 3 7 6 2
297f 6 7 8 5
298f 1 5 8 4
299f 4 8 7 3
300f 2 6 5 1
301"
302            .trim(),
303            str::from_utf8(writer.buffer())
304                .expect("Cannot decode utf8 string")
305                .trim()
306        );
307    }
308
309    #[test]
310    fn t_obj_string_round_trip() {
311        let meshin = PolyMeshF32::hexahedron(1.0).expect("Cannot make a box");
312        let mut writer = io::BufWriter::new(Vec::new());
313        meshin
314            .write_obj(&mut writer)
315            .expect("Cannot write obj to string");
316        let mut bytes = writer.buffer();
317        let meshout = PolyMeshF32::read_obj(&mut bytes).expect("Cannot read mesh from obj string");
318        assert_eq!(meshin.num_vertices(), meshout.num_vertices());
319        assert_eq!(meshin.num_edges(), meshout.num_edges());
320        assert_eq!(meshin.num_faces(), meshout.num_faces());
321        assert_eq!(
322            meshin.try_calc_area().expect("Cannot compute area"),
323            meshout.try_calc_area().expect("Cannot compute area")
324        );
325        assert_eq!(
326            meshin.try_calc_volume().expect("Cannot compute volume"),
327            meshout.try_calc_volume().expect("Cannot compute volume")
328        );
329        assert_eq!(
330            meshin.try_calc_vertex_centroid().unwrap(),
331            meshout.try_calc_vertex_centroid().unwrap()
332        );
333        assert_eq!(
334            meshin.try_calc_area_centroid().unwrap(),
335            meshout.try_calc_area_centroid().unwrap()
336        );
337    }
338}