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 }