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 #[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 #[must_use]
38 pub fn merge_meshes(mut self, enable: bool) -> Self {
39 self.merge_meshes = enable;
40 self
41 }
42
43 #[must_use]
69 pub fn custom_reader(mut self, reader: Reader<B>) -> Self {
70 self.reader = reader;
71 self
72 }
73
74 #[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 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 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 if starts_with(s, b"<COLLADA") {
261 return FileType::Collada;
262 }
263 }
264 _ => {}
265 }
266 s = s_next;
267 }
268 FileType::Unknown
269}