1use crate::{MeshReader, MeshWriter};
9use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
10use std::collections::HashMap;
11use std::fs::File;
12use std::io::{BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
13use std::path::Path;
14use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
15
16const BINARY_HEADER_SIZE: usize = 80;
17const BINARY_TRIANGLE_SIZE: usize = 50;
18
19pub struct StlReader;
21
22pub struct StlWriter;
24
25#[derive(Debug, Clone)]
27pub struct StlWriteOptions {
28 pub binary: bool,
30 pub solid_name: String,
32 pub binary_header: String,
34}
35
36impl Default for StlWriteOptions {
37 fn default() -> Self {
38 Self {
39 binary: true,
40 solid_name: "threecrate".to_string(),
41 binary_header: "Binary STL generated by ThreeCrate".to_string(),
42 }
43 }
44}
45
46fn is_binary_stl(path: &Path) -> Result<bool> {
52 let mut file = File::open(path)?;
53 let file_size = file.metadata()?.len();
54
55 if file_size < (BINARY_HEADER_SIZE + 4) as u64 {
56 return Ok(false);
57 }
58
59 file.seek(SeekFrom::Start(BINARY_HEADER_SIZE as u64))?;
60 let triangle_count = file.read_u32::<LittleEndian>()? as u64;
61 let expected_size = BINARY_HEADER_SIZE as u64 + 4 + triangle_count * BINARY_TRIANGLE_SIZE as u64;
62
63 if expected_size == file_size {
64 return Ok(true);
65 }
66
67 file.seek(SeekFrom::Start(0))?;
70 let mut buf = [0u8; 256];
71 let n = file.read(&mut buf)?;
72 let head = String::from_utf8_lossy(&buf[..n]);
73 let head_trim = head.trim_start();
74 if head_trim.starts_with("solid") && head.contains("facet") {
75 return Ok(false);
76 }
77
78 Ok(true)
79}
80
81fn quantize(v: f32) -> i32 {
82 (v * 1_000_000.0).round() as i32
84}
85
86struct MeshBuilder {
87 vertices: Vec<Point3f>,
88 face_normals: Vec<Vector3f>,
89 faces: Vec<[usize; 3]>,
90 index: HashMap<(i32, i32, i32), usize>,
91}
92
93impl MeshBuilder {
94 fn new() -> Self {
95 Self {
96 vertices: Vec::new(),
97 face_normals: Vec::new(),
98 faces: Vec::new(),
99 index: HashMap::new(),
100 }
101 }
102
103 fn add_vertex(&mut self, v: Point3f) -> usize {
104 let key = (quantize(v.x), quantize(v.y), quantize(v.z));
105 if let Some(&i) = self.index.get(&key) {
106 return i;
107 }
108 let i = self.vertices.len();
109 self.vertices.push(v);
110 self.index.insert(key, i);
111 i
112 }
113
114 fn add_triangle(&mut self, normal: Vector3f, a: Point3f, b: Point3f, c: Point3f) {
115 let ia = self.add_vertex(a);
116 let ib = self.add_vertex(b);
117 let ic = self.add_vertex(c);
118 if ia == ib || ib == ic || ia == ic {
119 return; }
121 self.faces.push([ia, ib, ic]);
122 self.face_normals.push(normal);
123 }
124
125 fn into_mesh(self) -> TriangleMesh {
126 let MeshBuilder { vertices, face_normals, faces, .. } = self;
127 let mut mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
128
129 if !mesh.faces.is_empty() && !face_normals.is_empty() {
131 let mut acc = vec![Vector3f::new(0.0, 0.0, 0.0); mesh.vertices.len()];
132 for (face, n) in mesh.faces.iter().zip(face_normals.iter()) {
133 for &vi in face {
134 acc[vi] += *n;
135 }
136 }
137 for v in &mut acc {
138 let len = v.norm();
139 if len > 0.0 {
140 *v /= len;
141 }
142 }
143 mesh.set_normals(acc);
144 }
145
146 mesh
147 }
148}
149
150pub fn read_stl<P: AsRef<Path>>(path: P) -> Result<TriangleMesh> {
152 let path = path.as_ref();
153 if is_binary_stl(path)? {
154 read_binary_stl(path)
155 } else {
156 read_ascii_stl(path)
157 }
158}
159
160fn read_binary_stl(path: &Path) -> Result<TriangleMesh> {
161 let file = File::open(path)?;
162 let mut reader = BufReader::new(file);
163
164 let mut header = [0u8; BINARY_HEADER_SIZE];
165 reader.read_exact(&mut header)?;
166 let triangle_count = reader.read_u32::<LittleEndian>()?;
167
168 let mut builder = MeshBuilder::new();
169 for i in 0..triangle_count {
170 let nx = reader.read_f32::<LittleEndian>()
171 .map_err(|_| Error::InvalidData(format!("Failed to read normal of triangle {}", i)))?;
172 let ny = reader.read_f32::<LittleEndian>()?;
173 let nz = reader.read_f32::<LittleEndian>()?;
174
175 let mut verts = [Point3f::new(0.0, 0.0, 0.0); 3];
176 for v in &mut verts {
177 let x = reader.read_f32::<LittleEndian>()?;
178 let y = reader.read_f32::<LittleEndian>()?;
179 let z = reader.read_f32::<LittleEndian>()?;
180 *v = Point3f::new(x, y, z);
181 }
182 let _attr = reader.read_u16::<LittleEndian>()?;
183
184 builder.add_triangle(Vector3f::new(nx, ny, nz), verts[0], verts[1], verts[2]);
185 }
186
187 Ok(builder.into_mesh())
188}
189
190fn parse_vec3(parts: &[&str], line_num: usize, what: &str) -> Result<(f32, f32, f32)> {
191 if parts.len() < 3 {
192 return Err(Error::InvalidData(format!(
193 "Invalid {} at line {}: expected 3 numbers",
194 what, line_num
195 )));
196 }
197 let x = parts[0].parse::<f32>().map_err(|_| {
198 Error::InvalidData(format!("Invalid {} x at line {}", what, line_num))
199 })?;
200 let y = parts[1].parse::<f32>().map_err(|_| {
201 Error::InvalidData(format!("Invalid {} y at line {}", what, line_num))
202 })?;
203 let z = parts[2].parse::<f32>().map_err(|_| {
204 Error::InvalidData(format!("Invalid {} z at line {}", what, line_num))
205 })?;
206 Ok((x, y, z))
207}
208
209fn read_ascii_stl(path: &Path) -> Result<TriangleMesh> {
210 let file = File::open(path)?;
211 let reader = BufReader::new(file);
212
213 let mut builder = MeshBuilder::new();
214 let mut current_normal = Vector3f::new(0.0, 0.0, 0.0);
215 let mut current_verts: Vec<Point3f> = Vec::with_capacity(3);
216
217 for (line_num, line) in reader.lines().enumerate() {
218 let line = line?;
219 let trimmed = line.trim();
220 if trimmed.is_empty() {
221 continue;
222 }
223 let parts: Vec<&str> = trimmed.split_whitespace().collect();
224 match parts[0] {
225 "facet" => {
226 if parts.len() >= 5 && parts[1] == "normal" {
228 let (nx, ny, nz) = parse_vec3(&parts[2..5], line_num + 1, "normal")?;
229 current_normal = Vector3f::new(nx, ny, nz);
230 } else {
231 current_normal = Vector3f::new(0.0, 0.0, 0.0);
232 }
233 current_verts.clear();
234 }
235 "vertex" => {
236 if parts.len() < 4 {
237 return Err(Error::InvalidData(format!(
238 "Invalid vertex at line {}: expected 3 coordinates",
239 line_num + 1
240 )));
241 }
242 let (x, y, z) = parse_vec3(&parts[1..4], line_num + 1, "vertex")?;
243 current_verts.push(Point3f::new(x, y, z));
244 }
245 "endfacet" => {
246 if current_verts.len() == 3 {
247 builder.add_triangle(
248 current_normal,
249 current_verts[0],
250 current_verts[1],
251 current_verts[2],
252 );
253 }
254 current_verts.clear();
255 }
256 _ => {}
257 }
258 }
259
260 Ok(builder.into_mesh())
261}
262
263pub fn write_stl<P: AsRef<Path>>(
265 mesh: &TriangleMesh,
266 path: P,
267 options: &StlWriteOptions,
268) -> Result<()> {
269 if options.binary {
270 write_binary_stl(mesh, path.as_ref(), options)
271 } else {
272 write_ascii_stl(mesh, path.as_ref(), options)
273 }
274}
275
276fn compute_face_normal(a: Point3f, b: Point3f, c: Point3f) -> Vector3f {
277 let u = b - a;
278 let v = c - a;
279 let n = u.cross(&v);
280 let len = n.norm();
281 if len > 0.0 {
282 n / len
283 } else {
284 Vector3f::new(0.0, 0.0, 0.0)
285 }
286}
287
288fn write_binary_stl(mesh: &TriangleMesh, path: &Path, options: &StlWriteOptions) -> Result<()> {
289 let file = File::create(path)?;
290 let mut writer = BufWriter::new(file);
291
292 let mut header = [0u8; BINARY_HEADER_SIZE];
293 let bytes = options.binary_header.as_bytes();
294 let n = bytes.len().min(BINARY_HEADER_SIZE);
295 header[..n].copy_from_slice(&bytes[..n]);
296 writer.write_all(&header)?;
297 writer.write_u32::<LittleEndian>(mesh.faces.len() as u32)?;
298
299 for face in &mesh.faces {
300 let a = mesh.vertices[face[0]];
301 let b = mesh.vertices[face[1]];
302 let c = mesh.vertices[face[2]];
303 let normal = compute_face_normal(a, b, c);
304 writer.write_f32::<LittleEndian>(normal.x)?;
305 writer.write_f32::<LittleEndian>(normal.y)?;
306 writer.write_f32::<LittleEndian>(normal.z)?;
307 for v in [a, b, c] {
308 writer.write_f32::<LittleEndian>(v.x)?;
309 writer.write_f32::<LittleEndian>(v.y)?;
310 writer.write_f32::<LittleEndian>(v.z)?;
311 }
312 writer.write_u16::<LittleEndian>(0)?;
313 }
314
315 writer.flush()?;
316 Ok(())
317}
318
319fn write_ascii_stl(mesh: &TriangleMesh, path: &Path, options: &StlWriteOptions) -> Result<()> {
320 let file = File::create(path)?;
321 let mut writer = BufWriter::new(file);
322
323 writeln!(writer, "solid {}", options.solid_name)?;
324 for face in &mesh.faces {
325 let a = mesh.vertices[face[0]];
326 let b = mesh.vertices[face[1]];
327 let c = mesh.vertices[face[2]];
328 let n = compute_face_normal(a, b, c);
329 writeln!(writer, " facet normal {} {} {}", n.x, n.y, n.z)?;
330 writeln!(writer, " outer loop")?;
331 writeln!(writer, " vertex {} {} {}", a.x, a.y, a.z)?;
332 writeln!(writer, " vertex {} {} {}", b.x, b.y, b.z)?;
333 writeln!(writer, " vertex {} {} {}", c.x, c.y, c.z)?;
334 writeln!(writer, " endloop")?;
335 writeln!(writer, " endfacet")?;
336 }
337 writeln!(writer, "endsolid {}", options.solid_name)?;
338 Ok(())
339}
340
341impl crate::registry::MeshReader for StlReader {
342 fn read_mesh(&self, path: &Path) -> Result<TriangleMesh> {
343 read_stl(path)
344 }
345
346 fn can_read(&self, path: &Path) -> bool {
347 is_binary_stl(path).is_ok()
348 }
349
350 fn format_name(&self) -> &'static str {
351 "stl"
352 }
353}
354
355impl crate::registry::MeshWriter for StlWriter {
356 fn write_mesh(&self, mesh: &TriangleMesh, path: &Path) -> Result<()> {
357 write_stl(mesh, path, &StlWriteOptions::default())
358 }
359
360 fn format_name(&self) -> &'static str {
361 "stl"
362 }
363}
364
365impl MeshReader for StlReader {
366 fn read_mesh<P: AsRef<Path>>(path: P) -> Result<TriangleMesh> {
367 read_stl(path)
368 }
369}
370
371impl MeshWriter for StlWriter {
372 fn write_mesh<P: AsRef<Path>>(mesh: &TriangleMesh, path: P) -> Result<()> {
373 write_stl(mesh, path, &StlWriteOptions::default())
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use tempfile::tempdir;
381 use threecrate_core::Point3f;
382
383 fn sample_mesh() -> TriangleMesh {
384 let vertices = vec![
386 Point3f::new(0.0, 0.0, 0.0),
387 Point3f::new(1.0, 0.0, 0.0),
388 Point3f::new(0.0, 1.0, 0.0),
389 Point3f::new(0.0, 0.0, 1.0),
390 ];
391 let faces = vec![[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
392 TriangleMesh::from_vertices_and_faces(vertices, faces)
393 }
394
395 #[test]
396 fn binary_round_trip() {
397 let mesh = sample_mesh();
398 let dir = tempdir().unwrap();
399 let path = dir.path().join("tet.stl");
400
401 write_stl(&mesh, &path, &StlWriteOptions { binary: true, ..Default::default() }).unwrap();
402 assert!(is_binary_stl(&path).unwrap());
403
404 let read = read_stl(&path).unwrap();
405 assert_eq!(read.vertex_count(), 4);
406 assert_eq!(read.face_count(), 4);
407 }
408
409 #[test]
410 fn ascii_round_trip() {
411 let mesh = sample_mesh();
412 let dir = tempdir().unwrap();
413 let path = dir.path().join("tet_ascii.stl");
414
415 write_stl(
416 &mesh,
417 &path,
418 &StlWriteOptions { binary: false, ..Default::default() },
419 )
420 .unwrap();
421 assert!(!is_binary_stl(&path).unwrap());
422
423 let read = read_stl(&path).unwrap();
424 assert_eq!(read.vertex_count(), 4);
425 assert_eq!(read.face_count(), 4);
426 }
427
428 #[test]
429 fn ascii_fixture_parses() {
430 let dir = tempdir().unwrap();
431 let path = dir.path().join("tri.stl");
432 let content = "\
433solid foo
434 facet normal 0 0 1
435 outer loop
436 vertex 0 0 0
437 vertex 1 0 0
438 vertex 0 1 0
439 endloop
440 endfacet
441endsolid foo
442";
443 std::fs::write(&path, content).unwrap();
444 let mesh = read_stl(&path).unwrap();
445 assert_eq!(mesh.face_count(), 1);
446 assert_eq!(mesh.vertex_count(), 3);
447 }
448
449 #[test]
450 fn auto_dispatch_via_read_mesh() {
451 let mesh = sample_mesh();
452 let dir = tempdir().unwrap();
453 let path = dir.path().join("auto.stl");
454 crate::write_mesh(&mesh, &path).unwrap();
455 let read = crate::read_mesh(&path).unwrap();
456 assert_eq!(read.face_count(), 4);
457 }
458}