mesh_loader/
loader.rs

1use std::{cmp, ffi::OsStr, fmt, fs, io, path::Path};
2
3use crate::{utils::bytes::starts_with, Scene};
4
5type Reader<B> = fn(&Path) -> io::Result<B>;
6
7pub struct Loader<B = Vec<u8>> {
8    reader: Reader<B>,
9    merge_meshes: bool,
10    // STL config
11    #[cfg(feature = "stl")]
12    stl_parse_color: bool,
13}
14
15fn default_reader(path: &Path) -> io::Result<Vec<u8>> {
16    fs::read(path)
17}
18
19impl Default for Loader<Vec<u8>> {
20    fn default() -> Self {
21        Self {
22            reader: default_reader,
23            merge_meshes: false,
24            #[cfg(feature = "stl")]
25            stl_parse_color: false,
26        }
27    }
28}
29
30impl<B: AsRef<[u8]>> Loader<B> {
31    /// Sets whether or not to merge meshes at load time.
32    ///
33    /// If set to `true`, it is guaranteed that there is exactly one mesh in the
34    /// loaded `Scene` (i.e., `scene.meshes.len() == 1`).
35    ///
36    /// Default: `false`
37    #[must_use]
38    pub fn merge_meshes(mut self, enable: bool) -> Self {
39        self.merge_meshes = enable;
40        self
41    }
42
43    /// Use the given function as a file reader of this loader.
44    ///
45    /// Default: [`std::fs::read`]
46    ///
47    /// # Example
48    ///
49    /// This is useful if you want to load a mesh from a location that the
50    /// default reader does not support.
51    ///
52    /// ```
53    /// use std::fs;
54    ///
55    /// use mesh_loader::Loader;
56    ///
57    /// let loader = Loader::default().custom_reader(|path| {
58    ///     match path.to_str() {
59    ///         Some(url) if url.starts_with("https://") || url.starts_with("http://") => {
60    ///             // Fetch online file
61    ///             // ...
62    /// #           unimplemented!()
63    ///         }
64    ///         _ => fs::read(path), // Otherwise, read from a file (same as the default reader)
65    ///     }
66    /// });
67    /// ```
68    #[must_use]
69    pub fn custom_reader(mut self, reader: Reader<B>) -> Self {
70        self.reader = reader;
71        self
72    }
73
74    /// Creates a new loader with the given file reader.
75    ///
76    /// This is similar to [`Loader::default().custom_reader()`](Self::custom_reader),
77    /// but the reader can return a non-`Vec<u8>` type.
78    ///
79    /// # Example
80    ///
81    /// This is useful when using mmap.
82    ///
83    /// ```
84    /// use std::fs::File;
85    ///
86    /// use memmap2::Mmap;
87    /// use mesh_loader::Loader;
88    ///
89    /// let loader = Loader::with_custom_reader(|path| unsafe { Mmap::map(&File::open(path)?) });
90    /// ```
91    #[must_use]
92    pub fn with_custom_reader(reader: Reader<B>) -> Self {
93        Self {
94            reader,
95            merge_meshes: false,
96            #[cfg(feature = "stl")]
97            stl_parse_color: false,
98        }
99    }
100
101    pub fn load<P: AsRef<Path>>(&self, path: P) -> io::Result<Scene> {
102        self.load_with_reader(path.as_ref(), self.reader)
103    }
104    pub fn load_with_reader<P: AsRef<Path>, F: FnMut(&Path) -> io::Result<B>>(
105        &self,
106        path: P,
107        mut reader: F,
108    ) -> io::Result<Scene> {
109        let path = path.as_ref();
110        self.load_from_slice_with_reader(reader(path)?.as_ref(), path, reader)
111    }
112    pub fn load_from_slice<P: AsRef<Path>>(&self, bytes: &[u8], path: P) -> io::Result<Scene> {
113        self.load_from_slice_with_reader(bytes, path.as_ref(), self.reader)
114    }
115    pub fn load_from_slice_with_reader<P: AsRef<Path>, F: FnMut(&Path) -> io::Result<B>>(
116        &self,
117        bytes: &[u8],
118        path: P,
119        #[allow(unused_variables)] reader: F,
120    ) -> io::Result<Scene> {
121        let path = path.as_ref();
122        match detect_file_type(path, bytes) {
123            #[cfg(feature = "stl")]
124            FileType::Stl => self.load_stl_from_slice(bytes, path),
125            #[cfg(not(feature = "stl"))]
126            FileType::Stl => Err(io::Error::new(
127                io::ErrorKind::Unsupported,
128                "'stl' feature of mesh-loader must be enabled to parse STL file ({path:?})",
129            )),
130            #[cfg(feature = "collada")]
131            FileType::Collada => self.load_collada_from_slice(bytes, path),
132            #[cfg(not(feature = "collada"))]
133            FileType::Collada => Err(io::Error::new(
134                io::ErrorKind::Unsupported,
135                "'collada' feature of mesh-loader must be enabled to parse COLLADA file ({path:?})",
136            )),
137            #[cfg(feature = "obj")]
138            FileType::Obj => self.load_obj_from_slice_with_reader(bytes, path, reader),
139            #[cfg(not(feature = "obj"))]
140            FileType::Obj => Err(io::Error::new(
141                io::ErrorKind::Unsupported,
142                "'obj' feature of mesh-loader must be enabled to parse OBJ file ({path:?})",
143            )),
144            FileType::Unknown => Err(io::Error::new(
145                io::ErrorKind::Unsupported,
146                "unsupported or unrecognized file type {path:?}",
147            )),
148        }
149    }
150
151    #[cfg(feature = "stl")]
152    pub fn load_stl<P: AsRef<Path>>(&self, path: P) -> io::Result<Scene> {
153        let path = path.as_ref();
154        self.load_stl_from_slice((self.reader)(path)?.as_ref(), path)
155    }
156    #[cfg(feature = "stl")]
157    pub fn load_stl_from_slice<P: AsRef<Path>>(&self, bytes: &[u8], path: P) -> io::Result<Scene> {
158        let scene =
159            crate::stl::from_slice_internal(bytes, Some(path.as_ref()), self.stl_parse_color)?;
160        Ok(self.post_process(scene))
161    }
162    #[cfg(feature = "stl")]
163    #[must_use]
164    pub fn stl_parse_color(mut self, enable: bool) -> Self {
165        self.stl_parse_color = enable;
166        self
167    }
168
169    #[cfg(feature = "collada")]
170    pub fn load_collada<P: AsRef<Path>>(&self, path: P) -> io::Result<Scene> {
171        let path = path.as_ref();
172        self.load_collada_from_slice((self.reader)(path)?.as_ref(), path)
173    }
174    #[cfg(feature = "collada")]
175    pub fn load_collada_from_slice<P: AsRef<Path>>(
176        &self,
177        bytes: &[u8],
178        path: P,
179    ) -> io::Result<Scene> {
180        let scene = crate::collada::from_slice_internal(bytes, Some(path.as_ref()))?;
181        Ok(self.post_process(scene))
182    }
183
184    #[cfg(feature = "obj")]
185    pub fn load_obj<P: AsRef<Path>>(&self, path: P) -> io::Result<Scene> {
186        self.load_obj_with_reader(path.as_ref(), self.reader)
187    }
188    #[cfg(feature = "obj")]
189    pub fn load_obj_from_slice<P: AsRef<Path>>(&self, bytes: &[u8], path: P) -> io::Result<Scene> {
190        self.load_obj_from_slice_with_reader(bytes, path.as_ref(), self.reader)
191    }
192    #[cfg(feature = "obj")]
193    pub fn load_obj_with_reader<P: AsRef<Path>, F: FnMut(&Path) -> io::Result<B>>(
194        &self,
195        path: P,
196        mut reader: F,
197    ) -> io::Result<Scene> {
198        let path = path.as_ref();
199        self.load_obj_from_slice_with_reader(reader(path)?.as_ref(), path, reader)
200    }
201    #[cfg(feature = "obj")]
202    pub fn load_obj_from_slice_with_reader<P: AsRef<Path>, F: FnMut(&Path) -> io::Result<B>>(
203        &self,
204        bytes: &[u8],
205        path: P,
206        reader: F,
207    ) -> io::Result<Scene> {
208        let scene = crate::obj::from_slice(bytes, Some(path.as_ref()), reader)?;
209        Ok(self.post_process(scene))
210    }
211
212    #[cfg(any(feature = "collada", feature = "obj", feature = "stl"))]
213    fn post_process(&self, mut scene: Scene) -> Scene {
214        if self.merge_meshes && scene.meshes.len() != 1 {
215            scene.meshes = vec![crate::Mesh::merge(scene.meshes)];
216            // TODO
217            scene.materials = vec![crate::Material::default()];
218        }
219        scene
220    }
221}
222
223impl fmt::Debug for Loader {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        let mut d = f.debug_struct("Loader");
226        d.field("merge_meshes", &self.merge_meshes);
227        #[cfg(feature = "stl")]
228        d.field("stl_parse_color", &self.stl_parse_color);
229        d.finish_non_exhaustive()
230    }
231}
232
233enum FileType {
234    Stl,
235    Collada,
236    Obj,
237    Unknown,
238}
239
240fn detect_file_type(path: &Path, bytes: &[u8]) -> FileType {
241    match path.extension().and_then(OsStr::to_str) {
242        Some("stl" | "STL") => return FileType::Stl,
243        Some("dae" | "DAE") => return FileType::Collada,
244        Some("obj" | "OBJ") => return FileType::Obj,
245        _ => {}
246    }
247    // Fallback: If failed to detect file type from extension,
248    // read the first 1024 bytes to detect the file type.
249    // TODO: rewrite based on what assimp does.
250    let mut s = &bytes[..cmp::min(bytes.len(), 1024)];
251    while let Some((&c, s_next)) = s.split_first() {
252        match c {
253            b's' => {
254                if starts_with(s_next, &b"solid"[1..]) {
255                    return FileType::Stl;
256                }
257            }
258            b'<' => {
259                // Compare whole s instead of s_next since needle.len() == 8
260                if starts_with(s, b"<COLLADA") {
261                    return FileType::Collada;
262                }
263            }
264            _ => {}
265        }
266        s = s_next;
267    }
268    FileType::Unknown
269}