Skip to main content

falcon_library/model3d/
stl.rs

1//! STL, ASCII and binary.
2
3use std::collections::HashMap;
4
5use super::math::V3;
6use super::{Mesh, Model, ParseError, Result};
7
8/// Triangles collected corner by corner, with equal positions welded into
9/// one vertex.
10#[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        // -0.0 and 0.0 are the same place.
20        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        // Not "solid" and the size is off: take what whole triangles there are.
41        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
52/// The triangle count when the size says this is a complete binary file.
53fn 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    // The corners of the loop being read.
87    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        // Count says 4 but only 2 whole triangles remain: not enough for the
181        // "at least count" fallback, so this is an error.
182        assert!(parse(&data).is_err());
183        // A count lower than the data present is truncated to the count.
184        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}