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