1pub mod dae;
12pub mod earclip;
13pub mod export;
14pub mod fbx;
15pub mod geom;
16pub mod gltf;
17pub mod ifc;
18mod ifc_native;
19pub mod math;
20pub mod obj;
21pub mod off;
22pub mod ply;
23pub mod raster;
24pub mod step;
25pub mod stl;
26pub mod texture;
27pub mod threemf;
28pub mod threeds;
29
30use std::path::Path;
31
32use math::V3;
33
34pub const EXTENSIONS: &[&str] = &["obj", "stl", "ply", "glb", "3mf", "fbx", "dae", "3ds", "ifc"];
37
38pub fn is_supported(ext: &str) -> bool {
40 EXTENSIONS.iter().any(|e| e.eq_ignore_ascii_case(ext))
41}
42
43pub const DEFAULT_COLOR: V3 = [200.0 / 255.0, 200.0 / 255.0, 200.0 / 255.0];
46
47#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
49#[error("{0}")]
50pub struct ParseError(pub String);
51
52impl ParseError {
53 pub fn new(e: impl std::fmt::Display) -> Self {
54 ParseError(e.to_string())
55 }
56}
57
58pub type Result<T> = std::result::Result<T, ParseError>;
59
60#[derive(Debug, Clone, PartialEq)]
62pub struct Material {
63 pub name: String,
64 pub color: V3,
67 pub opacity: f32,
69}
70
71#[derive(Debug, Clone, Default, PartialEq)]
74pub struct Mesh {
75 pub name: String,
76 pub positions: Vec<V3>,
77 pub normals: Option<Vec<V3>>,
79 pub colors: Option<Vec<V3>>,
82 pub indices: Vec<u32>,
84 pub material: Option<Material>,
85 pub uvs: Option<Vec<[f32; 2]>>,
88 pub texture: Option<texture::Texture>,
91 pub surface: Surface,
93}
94
95#[derive(Debug, Clone, Default, PartialEq)]
100pub struct Surface {
101 pub normal_map: Option<texture::Texture>,
103 pub bump_map: Option<texture::Texture>,
105 pub emissive_map: Option<texture::Texture>,
107 pub emissive: V3,
109 pub blend: bool,
112}
113
114impl Mesh {
115 pub fn triangle_count(&self) -> usize {
116 self.indices.len() / 3
117 }
118
119 pub fn sanitize(&mut self) {
122 let n = self.positions.len();
123 let whole = self.indices.len() / 3 * 3;
124 self.indices.truncate(whole);
125 if self.indices.iter().any(|&i| i as usize >= n) {
126 self.indices = self.indices.as_chunks::<3>().0.iter().filter(|t| t.iter().all(|&i| (i as usize) < n)).flatten().copied().collect();
127 }
128 if self.normals.as_ref().is_some_and(|v| v.len() != n) {
129 self.normals = None;
130 }
131 if self.colors.as_ref().is_some_and(|v| v.len() != n) {
132 self.colors = None;
133 }
134 if self.uvs.as_ref().is_some_and(|v| v.len() != n) {
135 self.uvs = None;
136 }
137 if self.uvs.is_none() {
138 let s = &mut self.surface;
139 (s.normal_map, s.bump_map, s.emissive_map) = (None, None, None);
140 }
141 if !self.surface.emissive.iter().all(|c| c.is_finite()) {
142 self.surface.emissive = [0.0; 3];
143 }
144 }
145
146 pub fn textured(&self) -> Option<(&texture::Texture, &[[f32; 2]])> {
148 Some((self.texture.as_ref()?, self.uvs.as_deref()?))
149 }
150
151 pub fn opacity(&self) -> f32 {
154 if self.surface.blend { self.material.as_ref().map_or(1.0, |m| m.opacity.clamp(0.0, 1.0)) } else { 1.0 }
155 }
156
157 pub fn has_look(&self) -> bool {
160 let s = &self.surface;
161 self.textured().is_some() || s.blend || s.emissive != [0.0; 3] || (self.uvs.is_some() && (s.normal_map.is_some() || s.bump_map.is_some()))
162 }
163}
164
165#[derive(Debug, Clone, Default, PartialEq)]
167pub struct Model {
168 pub meshes: Vec<Mesh>,
169}
170
171impl Model {
172 pub fn vertex_count(&self) -> usize {
173 self.meshes.iter().map(|m| m.positions.len()).sum()
174 }
175
176 pub fn triangle_count(&self) -> usize {
177 self.meshes.iter().map(Mesh::triangle_count).sum()
178 }
179
180 pub fn bounds(&self) -> Option<(V3, V3)> {
183 let mut b: Option<(V3, V3)> = None;
184 for m in &self.meshes {
185 for &i in &m.indices {
186 let p = m.positions[i as usize];
187 if !p.iter().all(|c| c.is_finite()) {
188 continue;
189 }
190 let (lo, hi) = b.get_or_insert((p, p));
191 for k in 0..3 {
192 lo[k] = lo[k].min(p[k]);
193 hi[k] = hi[k].max(p[k]);
194 }
195 }
196 }
197 b
198 }
199
200 pub fn bounding_sphere(&self) -> Option<(V3, f32)> {
202 let (lo, hi) = self.bounds()?;
203 let c = math::scale(math::add(lo, hi), 0.5);
204 Some((c, math::length(math::sub(hi, c))))
205 }
206
207 pub fn finish(mut self) -> Result<Model> {
210 for m in &mut self.meshes {
211 m.sanitize();
212 }
213 self.meshes.retain(|m| !m.indices.is_empty());
214 if self.meshes.is_empty() {
215 return Err(ParseError::new("the file has no triangles"));
216 }
217 Ok(self)
218 }
219}
220
221pub fn load(path: &Path, ext: &str) -> Result<Model> {
224 let bytes = std::fs::read(path).map_err(ParseError::new)?;
225 let dir = path.parent().map(Path::to_path_buf).unwrap_or_default();
226 let side = move |name: &str| -> Option<Vec<u8>> {
227 let rel = Path::new(name);
229 if rel.is_absolute() || rel.components().any(|c| matches!(c, std::path::Component::ParentDir)) {
230 return None;
231 }
232 std::fs::read(dir.join(rel)).ok()
233 };
234 parse(&bytes, ext, &side)
235}
236
237pub fn parse(bytes: &[u8], ext: &str, side: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<Model> {
240 match ext.to_ascii_lowercase().as_str() {
241 "obj" => obj::parse(bytes, side),
242 "stl" => stl::parse(bytes),
243 "ply" => ply::parse(bytes),
244 "glb" => gltf::parse_glb(bytes, side),
245 "3mf" => threemf::parse(bytes),
246 "fbx" => fbx::parse(bytes, side),
247 "dae" => dae::parse(bytes, side),
248 "3ds" => threeds::parse(bytes, side),
249 "ifc" => ifc::parse(bytes),
250 other => Err(ParseError(format!("cannot read .{other} models"))),
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn load_reads_files_and_their_neighbours() {
260 let dir = tempfile::tempdir().unwrap();
261 let sub = dir.path().join("models");
262 std::fs::create_dir(&sub).unwrap();
263 std::fs::write(sub.join("cube.mtl"), "newmtl red\nKd 1 0 0\n").unwrap();
264 std::fs::write(dir.path().join("outside.mtl"), "newmtl blue\nKd 0 0 1\n").unwrap();
266 let obj = "mtllib cube.mtl\nmtllib ../outside.mtl\nv 0 0 0\nv 1 0 0\nv 0 1 0\nusemtl red\nf 1 2 3\nusemtl blue\nf 1 3 2\n";
267 std::fs::write(sub.join("Cube.OBJ"), obj).unwrap();
268 let m = load(&sub.join("Cube.OBJ"), "OBJ").unwrap();
269 assert_eq!(m.meshes.len(), 2);
270 assert_eq!(m.meshes[0].material.as_ref().unwrap().color, [1.0, 0.0, 0.0]);
271 assert_eq!(m.meshes[1].material.as_ref().unwrap().color, DEFAULT_COLOR, "../ is refused");
272 std::fs::write(sub.join("t.stl"), "solid t\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid t\n").unwrap();
274 assert_eq!(load(&sub.join("t.stl"), "stl").unwrap().triangle_count(), 1);
275 assert!(load(&sub.join("missing.ply"), "ply").is_err());
276 assert!(parse(b"", "fbx", &|_| None).is_err());
277 assert!(parse(b"", "dae", &|_| None).is_err());
278 assert!(is_supported("GLB") && is_supported("FBX") && is_supported("Dae") && is_supported("3DS"));
279 }
280}