mesh files in: STL, OBJ, glTF and PLY
git clone https://git.lucas.co/cce-mesh-io.git
src/lib.rs (5.2K)
1 //! Mesh files in, one scene type out, for every cce app that reads geometry:
2 //! cce-model shows it, cce-designer imports it.
3 //!
4 //! Each reader returns a [`Scene`] as the file has it: named parts, in the
5 //! file's own coordinates and units, with the up axis its format conventionally
6 //! uses, and the length [`Unit`] its format conventionally means. Nothing is
7 //! rotated, scaled or welded by a reader; [`load`] welds, and what to do about
8 //! the up axis is the caller's choice ([`Mesh::z_up_to_y_up`]).
9 //! A viewer turns a Z-up STL upright; an editor importing its own Y-up export
10 //! must not.
11 //!
12 //! This crate draws nothing and knows no renderer, so a headless tool can use
13 //! it as freely as an app.
14
15 mod gltf;
16 mod mesh;
17 mod obj;
18 mod ply;
19 mod stl;
20
21 use std::path::Path;
22
23 pub use mesh::{srgb_to_linear, Material, Mesh, CLAY};
24
25 /// File extensions [`load`] reads, lowercase.
26 pub const EXTENSIONS: &[&str] = &["stl", "obj", "gltf", "glb", "ply"];
27
28 /// Which way is up in a file's coordinates, by its format's convention.
29 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
30 pub enum UpAxis {
31 /// OBJ, glTF (by its spec) and PLY.
32 Y,
33 /// STL: it comes from CAD and goes to printers, which build along Z.
34 Z,
35 }
36
37 /// What one unit of a file's coordinates is, by its format's convention.
38 /// None of these formats can be trusted to say: it is what a file of that
39 /// kind usually means, and a caller showing a size should say which it is.
40 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
41 pub enum Unit {
42 /// STL: what every slicer assumes.
43 Millimetre,
44 /// glTF: metres, by its spec.
45 Metre,
46 /// OBJ and PLY say nothing.
47 Unspecified,
48 }
49
50 impl Unit {
51 /// Millimetres per file unit, or `None` when the format does not say.
52 pub fn millimetres(self) -> Option<f32> {
53 match self {
54 Unit::Millimetre => Some(1.0),
55 Unit::Metre => Some(1000.0),
56 Unit::Unspecified => None,
57 }
58 }
59 }
60
61 /// An image a material names: decoded, 8-bit RGBA, rows top to bottom,
62 /// sRGB-encoded as the file had it (a renderer that samples it through an
63 /// sRGB format gets linear values).
64 #[derive(Clone)]
65 pub struct Texture {
66 pub width: u32,
67 pub height: u32,
68 pub rgba: Vec<u8>,
69 }
70
71 impl std::fmt::Debug for Texture {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 write!(f, "Texture({}x{})", self.width, self.height)
74 }
75 }
76
77 impl Texture {
78 /// Decode a PNG or JPEG.
79 pub fn decode(bytes: &[u8]) -> Result<Texture, String> {
80 let image = image::load_from_memory(bytes).map_err(|e| format!("an image that will not decode: {e}"))?;
81 let rgba = image.to_rgba8();
82 Ok(Texture { width: rgba.width(), height: rgba.height(), rgba: rgba.into_raw() })
83 }
84
85 /// The texel nearest `uv` (image convention, repeating), sRGB-encoded.
86 pub fn sample(&self, uv: [f32; 2]) -> [u8; 4] {
87 let wrap = |v: f32, n: u32| ((v - v.floor()) * n as f32).floor().clamp(0.0, n as f32 - 1.0) as usize;
88 let (x, y) = (wrap(uv[0], self.width), wrap(uv[1], self.height));
89 let i = (y * self.width as usize + x) * 4;
90 [self.rgba[i], self.rgba[i + 1], self.rgba[i + 2], self.rgba[i + 3]]
91 }
92 }
93
94 /// One named piece of a scene: a glTF node's mesh, a whole STL.
95 #[derive(Debug, Clone, Default)]
96 pub struct Part {
97 pub name: String,
98 pub mesh: Mesh,
99 }
100
101 /// What a file holds: its parts, already placed where the file's node tree
102 /// puts them, its up axis and its unit, and the images its materials name.
103 #[derive(Debug, Clone)]
104 pub struct Scene {
105 pub parts: Vec<Part>,
106 pub up: UpAxis,
107 pub unit: Unit,
108 /// Indexed by [`Material::texture`].
109 pub textures: Vec<Texture>,
110 }
111
112 impl Scene {
113 pub fn triangle_count(&self) -> usize {
114 self.parts.iter().map(|p| p.mesh.triangles.len()).sum()
115 }
116
117 /// Every part in one mesh. Parts are not welded to each other, so two
118 /// parts that touch keep separate normals where they meet.
119 pub fn merged(&self) -> Mesh {
120 let mut out = Mesh::default();
121 for part in &self.parts {
122 out.append(&part.mesh);
123 }
124 out
125 }
126 }
127
128 /// Read `path` by its extension, weld each part, and name an unnamed part
129 /// after the file.
130 pub fn load(path: &Path) -> Result<Scene, String> {
131 let ext = path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default();
132 let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
133 let dir = path.parent();
134 let mut scene = match ext.as_str() {
135 "stl" => stl::read(&bytes)?,
136 "obj" => obj::read(&bytes, dir)?,
137 "gltf" | "glb" => gltf::read(&bytes, dir)?,
138 "ply" => ply::read(&bytes)?,
139 "" => return Err("no file extension, so no way to tell the format".into()),
140 other => return Err(format!("cannot read .{other} files")),
141 };
142 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("model");
143 for part in &mut scene.parts {
144 part.mesh.weld();
145 if part.name.is_empty() {
146 part.name = stem.to_string();
147 }
148 }
149 scene.parts.retain(|p| !p.mesh.triangles.is_empty());
150 if scene.parts.is_empty() {
151 return Err("the file holds no triangles".into());
152 }
153 Ok(scene)
154 }