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/stl.rs (5K)

  1 //! STL, binary and ASCII.
  2 //!
  3 //! Binary is told from ASCII by its size, not by its first word: the format
  4 //! leaves the 80-byte header free, and plenty of binary files begin it with
  5 //! `solid`, the word that opens an ASCII one. A binary file is exactly
  6 //! 84 + 50 × its triangle count bytes long.
  7 //!
  8 //! Points are returned as written, with [`UpAxis::Z`], STL's convention;
  9 //! turning them upright is the caller's call. The file's own facet normals
 10 //! are ignored: normals come from the winding. Nor is the attribute word
 11 //! read as a colour — the two vendor schemes for that disagree.
 12 
 13 use glam::Vec3;
 14 
 15 use crate::mesh::{Material, Mesh};
 16 use crate::{Part, Scene, Unit, UpAxis};
 17 
 18 pub fn read(bytes: &[u8]) -> Result<Scene, String> {
 19     let corners = if is_binary(bytes) { binary(bytes)? } else { ascii(bytes)? };
 20     let n = corners.len() / 3;
 21     let mesh = Mesh {
 22         positions: corners,
 23         triangles: (0..n as u32).map(|t| [t * 3, t * 3 + 1, t * 3 + 2]).collect(),
 24         tri_material: vec![0; n],
 25         materials: vec![Material::default()],
 26         corner_colors: None,
 27         corner_uvs: None,
 28         file_normals: None,
 29     };
 30     Ok(Scene { parts: vec![Part { name: String::new(), mesh }], up: UpAxis::Z, unit: Unit::Millimetre, textures: Vec::new() })
 31 }
 32 
 33 fn is_binary(bytes: &[u8]) -> bool {
 34     if bytes.len() < 84 {
 35         return false;
 36     }
 37     let count = u32::from_le_bytes(bytes[80..84].try_into().unwrap()) as u64;
 38     84 + 50 * count == bytes.len() as u64 || !bytes.starts_with(b"solid")
 39 }
 40 
 41 fn binary(bytes: &[u8]) -> Result<Vec<Vec3>, String> {
 42     let count = u32::from_le_bytes(bytes[80..84].try_into().unwrap()) as usize;
 43     let need = 84 + 50 * count;
 44     if bytes.len() < need {
 45         return Err(format!(
 46             "binary STL says {count} triangles ({need} bytes) but the file is {} bytes; it is cut short",
 47             bytes.len()
 48         ));
 49     }
 50     let f = |o: usize| f32::from_le_bytes(bytes[o..o + 4].try_into().unwrap());
 51     let mut out = Vec::with_capacity(count * 3);
 52     for t in 0..count {
 53         // 12 bytes of facet normal, three 12-byte corners, 2 bytes attribute.
 54         let base = 84 + 50 * t + 12;
 55         for k in 0..3 {
 56             let o = base + 12 * k;
 57             out.push(Vec3::new(f(o), f(o + 4), f(o + 8)));
 58         }
 59     }
 60     Ok(out)
 61 }
 62 
 63 fn ascii(bytes: &[u8]) -> Result<Vec<Vec3>, String> {
 64     let text = std::str::from_utf8(bytes).map_err(|_| "not a binary STL, and not text either".to_string())?;
 65     let mut out = Vec::new();
 66     for (line_no, line) in text.lines().enumerate() {
 67         let mut words = line.split_whitespace();
 68         if words.next() != Some("vertex") {
 69             continue;
 70         }
 71         let mut xyz = [0f32; 3];
 72         for v in &mut xyz {
 73             *v = words
 74                 .next()
 75                 .and_then(|w| w.parse().ok())
 76                 .ok_or_else(|| format!("line {}: a vertex needs three numbers", line_no + 1))?;
 77         }
 78         out.push(Vec3::from(xyz));
 79     }
 80     if out.len() % 3 != 0 {
 81         return Err(format!("{} vertices is not a whole number of triangles", out.len()));
 82     }
 83     Ok(out)
 84 }
 85 
 86 #[cfg(test)]
 87 mod tests {
 88     use super::*;
 89 
 90     fn binary_of(tris: &[[[f32; 3]; 3]]) -> Vec<u8> {
 91         let mut b = vec![0u8; 80];
 92         b[..5].copy_from_slice(b"solid"); // the header may say anything, this included
 93         b.extend((tris.len() as u32).to_le_bytes());
 94         for t in tris {
 95             b.extend([0u8; 12]);
 96             for p in t {
 97                 for c in p {
 98                     b.extend(c.to_le_bytes());
 99                 }
100             }
101             b.extend([0u8; 2]);
102         }
103         b
104     }
105 
106     fn mesh(bytes: &[u8]) -> Mesh {
107         read(bytes).unwrap().parts.remove(0).mesh
108     }
109 
110     #[test]
111     fn a_binary_file_headed_solid_is_read_as_binary() {
112         let m = mesh(&binary_of(&[[[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]]]));
113         assert_eq!(m.triangles.len(), 1);
114         assert_eq!(m.positions[1], Vec3::new(1.0, 0.0, 0.0));
115     }
116 
117     #[test]
118     fn points_are_as_written_and_the_scene_says_z_is_up() {
119         let s = read(&binary_of(&[[[0., 0., 2.], [1., 0., 0.], [0., 3., 0.]]])).unwrap();
120         assert_eq!(s.up, UpAxis::Z);
121         assert_eq!(s.unit, Unit::Millimetre);
122         assert_eq!(s.parts[0].mesh.positions[0], Vec3::new(0.0, 0.0, 2.0));
123     }
124 
125     #[test]
126     fn ascii_is_read() {
127         let text = "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";
128         assert_eq!(mesh(text.as_bytes()).triangles.len(), 1);
129     }
130 
131     #[test]
132     fn a_short_binary_file_is_an_error() {
133         let mut b = binary_of(&[[[0.; 3], [1., 0., 0.], [0., 1., 0.]]; 2]);
134         b.truncate(b.len() - 30);
135         b[..5].copy_from_slice(b"model"); // not "solid": nothing to try as text
136         let e = read(&b).unwrap_err();
137         assert!(e.contains("cut short"), "{e}");
138     }
139 
140     #[test]
141     fn a_bad_ascii_vertex_is_an_error() {
142         let e = read(b"solid t\nvertex 0 0\n").unwrap_err();
143         assert!(e.contains("line 2"), "{e}");
144     }
145 }