falcon_library/model3d/
stl.rs1use std::collections::HashMap;
4
5use super::math::V3;
6use super::{Mesh, Model, ParseError, Result};
7
8#[derive(Default)]
11struct Builder {
12 positions: Vec<V3>,
13 indices: Vec<u32>,
14 seen: HashMap<[u32; 3], u32>,
15}
16
17impl Builder {
18 fn vertex(&mut self, p: V3) {
19 let key = p.map(|c| if c == 0.0 { 0 } else { c.to_bits() });
21 let positions = &mut self.positions;
22 let i = *self.seen.entry(key).or_insert_with(|| {
23 positions.push(p);
24 (positions.len() - 1) as u32
25 });
26 self.indices.push(i);
27 }
28
29 fn into_mesh(self, name: String) -> Mesh {
30 Mesh { name, positions: self.positions, indices: self.indices, ..Mesh::default() }
31 }
32}
33
34pub fn parse(bytes: &[u8]) -> Result<Model> {
35 let meshes = if let Some(count) = binary_count(bytes) {
36 vec![parse_binary(bytes, count)]
37 } else if starts_with_solid(bytes) {
38 parse_ascii(bytes)
39 } else if bytes.len() >= 84 {
40 let count = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]) as usize;
42 if bytes.len() - 84 < 50 * count {
43 return Err(ParseError::new("not an STL file"));
44 }
45 vec![parse_binary(bytes, count)]
46 } else {
47 return Err(ParseError::new("not an STL file"));
48 };
49 Model { meshes }.finish()
50}
51
52fn binary_count(bytes: &[u8]) -> Option<usize> {
54 if bytes.len() < 84 {
55 return None;
56 }
57 let count = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]) as usize;
58 (84 + 50 * count == bytes.len()).then_some(count)
59}
60
61fn starts_with_solid(bytes: &[u8]) -> bool {
62 let head = &bytes[..bytes.len().min(512)];
63 let head = head.trim_ascii_start();
64 head.len() >= 5 && head[..5].eq_ignore_ascii_case(b"solid")
65}
66
67fn parse_binary(bytes: &[u8], count: usize) -> Mesh {
68 let body = &bytes[84..];
69 let mut b = Builder::default();
70 for rec in body.as_chunks::<50>().0.iter().take(count) {
71 for v in 1..4 {
72 let f = |k: usize| {
73 let o = v * 12 + k * 4;
74 f32::from_le_bytes([rec[o], rec[o + 1], rec[o + 2], rec[o + 3]])
75 };
76 b.vertex([f(0), f(1), f(2)]);
77 }
78 }
79 b.into_mesh(String::new())
80}
81
82fn parse_ascii(bytes: &[u8]) -> Vec<Mesh> {
83 let text = String::from_utf8_lossy(bytes);
84 let mut meshes = Vec::new();
85 let mut cur: Option<(String, Builder)> = None;
86 let mut corners: Vec<V3> = Vec::new();
88 let mut in_loop = false;
89 for line in text.lines() {
90 let line = line.trim();
91 let mut words = line.split_whitespace();
92 let Some(first) = words.next() else { continue };
93 match first.to_ascii_lowercase().as_str() {
94 "solid" => {
95 if let Some((n, b)) = cur.take() {
96 meshes.push(b.into_mesh(n));
97 }
98 let name = line[first.len()..].trim().to_string();
99 cur = Some((name, Builder::default()));
100 }
101 "endsolid" => {
102 if let Some((n, b)) = cur.take() {
103 meshes.push(b.into_mesh(n));
104 }
105 }
106 "outer" => {
107 in_loop = true;
108 corners.clear();
109 }
110 "vertex" if in_loop => {
111 let v: Vec<f32> = words.filter_map(|w| w.parse().ok()).collect();
112 if v.len() >= 3 {
113 corners.push([v[0], v[1], v[2]]);
114 }
115 }
116 "endloop" => {
117 in_loop = false;
118 let (_, b) = cur.get_or_insert_with(|| (String::new(), Builder::default()));
119 for k in 1..corners.len().saturating_sub(1) {
120 b.vertex(corners[0]);
121 b.vertex(corners[k]);
122 b.vertex(corners[k + 1]);
123 }
124 corners.clear();
125 }
126 _ => {}
127 }
128 }
129 if let Some((n, b)) = cur.take() {
130 meshes.push(b.into_mesh(n));
131 }
132 meshes
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn binary(header: &[u8], tris: &[[V3; 3]]) -> Vec<u8> {
140 let mut out = vec![0u8; 80];
141 out[..header.len()].copy_from_slice(header);
142 out.extend((tris.len() as u32).to_le_bytes());
143 for t in tris {
144 out.extend([0u8; 12]);
145 for v in t {
146 for c in v {
147 out.extend(c.to_le_bytes());
148 }
149 }
150 out.extend([0u8; 2]);
151 }
152 out
153 }
154
155 fn tetra() -> Vec<[V3; 3]> {
156 let (a, b, c, d) = ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]);
157 vec![[a, c, b], [a, b, d], [b, c, d], [c, a, d]]
158 }
159
160 #[test]
161 fn binary_tetrahedron_is_welded() {
162 let m = parse(&binary(b"tetra", &tetra())).unwrap();
163 assert_eq!(m.meshes.len(), 1);
164 assert_eq!(m.triangle_count(), 4);
165 assert_eq!(m.vertex_count(), 4);
166 assert!(m.meshes[0].normals.is_none() && m.meshes[0].colors.is_none() && m.meshes[0].material.is_none());
167 }
168
169 #[test]
170 fn binary_header_starting_with_solid() {
171 let m = parse(&binary(b"solid exported by something", &tetra())).unwrap();
172 assert_eq!(m.triangle_count(), 4);
173 assert_eq!(m.vertex_count(), 4);
174 }
175
176 #[test]
177 fn truncated_binary_reads_whole_triangles() {
178 let mut data = binary(b"x", &tetra());
179 data.truncate(data.len() - 60);
180 assert!(parse(&data).is_err());
183 let mut data = binary(b"x", &tetra());
185 data[80] = 2;
186 let m = parse(&data).unwrap();
187 assert_eq!(m.triangle_count(), 2);
188 }
189
190 #[test]
191 fn ascii_cube_face() {
192 let src = "solid face\n facet normal 0 0 1\n outer loop\n vertex 0 0 1\n vertex 1 0 1\n vertex 1 1 1\n endloop\n endfacet\n\
193 FACET NORMAL 0 0 1\n OUTER LOOP\n VERTEX 0 0 1\n VERTEX 1 1 1\n VERTEX 0 1 1e0\n ENDLOOP\n ENDFACET\nendsolid face\n";
194 let m = parse(src.as_bytes()).unwrap();
195 assert_eq!(m.meshes.len(), 1);
196 assert_eq!(m.meshes[0].name, "face");
197 assert_eq!(m.triangle_count(), 2);
198 assert_eq!(m.vertex_count(), 4);
199 }
200
201 #[test]
202 fn ascii_two_solids() {
203 let tri = |n: &str| format!("solid {n}\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid {n}\n");
204 let m = parse(format!("{}{}", tri("a"), tri("b")).as_bytes()).unwrap();
205 assert_eq!(m.meshes.len(), 2);
206 assert_eq!(m.meshes[0].name, "a");
207 assert_eq!(m.meshes[1].name, "b");
208 }
209
210 #[test]
211 fn ascii_polygon_fans() {
212 let src = "solid q\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 1 1 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid q\n";
213 let m = parse(src.as_bytes()).unwrap();
214 assert_eq!(m.triangle_count(), 2);
215 assert_eq!(m.vertex_count(), 4);
216 }
217
218 #[test]
219 fn garbage_is_an_error() {
220 assert!(parse(b"hello world").is_err());
221 assert!(parse(&[7u8; 200]).is_err());
222 assert!(parse(b"").is_err());
223 }
224}