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