git.lucas.co / cce-mesh-io
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 }