1use std::collections::HashMap;
4use std::fs::File;
5use std::io::{BufWriter, Write};
6use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct TriSurface {
11 pub nodes: Vec<[f64; 3]>,
12 pub triangles: Vec<[usize; 3]>,
13}
14
15fn detect_ascii(data: &[u8]) -> bool {
16 if !data.starts_with(b"solid") {
17 return false;
18 }
19 if data.len() > 5 && !data[5].is_ascii_whitespace() {
20 return false;
21 }
22 if data.len() >= 84 {
23 let num_triangles = u32::from_le_bytes([data[80], data[81], data[82], data[83]]) as usize;
24 let expected = 84 + 50 * num_triangles;
25 if expected == data.len() {
26 return false;
27 }
28 }
29 true
30}
31
32fn parse_stl_binary(data: &[u8]) -> Result<Vec<[[f32; 3]; 3]>, String> {
33 if data.len() < 84 {
34 return Err(
35 "binary STL is too short (84 bytes required for header + triangle count)".into(),
36 );
37 }
38 let num_triangles = u32::from_le_bytes([data[80], data[81], data[82], data[83]]) as usize;
39 let expected = 84 + 50 * num_triangles;
40 if data.len() < expected {
41 return Err(format!(
42 "binary STL data is truncated (expected {expected} bytes, got {})",
43 data.len()
44 ));
45 }
46
47 let mut triangles = Vec::with_capacity(num_triangles);
48 for i in 0..num_triangles {
49 let base = 84 + 50 * i;
50 let mut verts = [[0.0_f32; 3]; 3];
51 for (v, vert) in verts.iter_mut().enumerate() {
52 let vbase = base + 12 + 12 * v;
53 for (c, coord) in vert.iter_mut().enumerate() {
54 let offset = vbase + 4 * c;
55 *coord = f32::from_le_bytes([
56 data[offset],
57 data[offset + 1],
58 data[offset + 2],
59 data[offset + 3],
60 ]);
61 }
62 }
63 triangles.push(verts);
64 }
65 Ok(triangles)
66}
67
68fn parse_stl_ascii(data: &[u8]) -> Result<Vec<[[f32; 3]; 3]>, String> {
69 let text = std::str::from_utf8(data)
70 .map_err(|e| format!("failed to decode ASCII STL as UTF-8: {e}"))?;
71
72 let mut triangles = Vec::new();
73 let mut current_verts = Vec::new();
74 let mut in_facet = false;
75
76 for line in text.lines() {
77 let trimmed = line.trim();
78 if trimmed.starts_with("facet ") {
79 in_facet = true;
80 current_verts.clear();
81 } else if trimmed == "endfacet" {
82 if !in_facet {
83 return Err("encountered endfacet outside facet block".into());
84 }
85 if current_verts.len() != 3 {
86 return Err(format!(
87 "facet must contain exactly 3 vertices, got {}",
88 current_verts.len()
89 ));
90 }
91 triangles.push([current_verts[0], current_verts[1], current_verts[2]]);
92 in_facet = false;
93 } else if trimmed.starts_with("vertex ") && in_facet {
94 let parts: Vec<&str> = trimmed.split_whitespace().collect();
95 if parts.len() != 4 {
96 return Err(format!("invalid vertex line: {trimmed}"));
97 }
98 let x = parts[1]
99 .parse()
100 .map_err(|_| format!("failed to parse vertex coordinate: {}", parts[1]))?;
101 let y = parts[2]
102 .parse()
103 .map_err(|_| format!("failed to parse vertex coordinate: {}", parts[2]))?;
104 let z = parts[3]
105 .parse()
106 .map_err(|_| format!("failed to parse vertex coordinate: {}", parts[3]))?;
107 current_verts.push([x, y, z]);
108 }
109 }
110
111 Ok(triangles)
112}
113
114pub fn parse_stl(data: &[u8]) -> Result<TriSurface, String> {
116 let raw_triangles = if detect_ascii(data) {
117 parse_stl_ascii(data)?
118 } else {
119 parse_stl_binary(data)?
120 };
121
122 let mut nodes = Vec::new();
123 let mut triangles = Vec::new();
124 let mut vertex_map: HashMap<[u64; 3], usize> = HashMap::new();
125
126 let quantize = |v: f32| -> u64 { ((v as f64) * 1e6).round().to_bits() };
127
128 for tri in &raw_triangles {
129 let mut indices = [0usize; 3];
130 for (i, v) in tri.iter().enumerate() {
131 let key = [quantize(v[0]), quantize(v[1]), quantize(v[2])];
132 let idx = if let Some(&existing) = vertex_map.get(&key) {
133 existing
134 } else {
135 let idx = nodes.len();
136 nodes.push([v[0] as f64, v[1] as f64, v[2] as f64]);
137 vertex_map.insert(key, idx);
138 idx
139 };
140 indices[i] = idx;
141 }
142 if indices[0] != indices[1] && indices[1] != indices[2] && indices[2] != indices[0] {
143 triangles.push(indices);
144 }
145 }
146
147 if triangles.is_empty() {
148 return Err("STL file contains no valid triangles".into());
149 }
150
151 Ok(TriSurface { nodes, triangles })
152}
153
154fn write_f32_triple(writer: &mut dyn Write, v: [f64; 3]) -> std::io::Result<()> {
155 for &c in &v {
156 writer.write_all(&(c as f32).to_le_bytes())?;
157 }
158 Ok(())
159}
160
161fn checked_triangle_count(len: usize) -> std::io::Result<u32> {
162 u32::try_from(len).map_err(|_| {
163 std::io::Error::new(
164 std::io::ErrorKind::InvalidInput,
165 "triangle count exceeds u32::MAX for STL binary format",
166 )
167 })
168}
169
170pub fn write_stl_binary(
172 nodes: &[[f64; 3]],
173 triangles: &[[usize; 3]],
174 path: &Path,
175) -> std::io::Result<()> {
176 let file = File::create(path)?;
177 let mut writer = BufWriter::new(file);
178 writer.write_all(&[0u8; 80])?;
179 let n_tris = checked_triangle_count(triangles.len())?;
180 writer.write_all(&n_tris.to_le_bytes())?;
181
182 for tri in triangles {
183 let v0 = nodes[tri[0]];
184 let v1 = nodes[tri[1]];
185 let v2 = nodes[tri[2]];
186 let normal = triangle_normal(v0, v1, v2);
187
188 write_f32_triple(&mut writer, normal)?;
189 write_f32_triple(&mut writer, v0)?;
190 write_f32_triple(&mut writer, v1)?;
191 write_f32_triple(&mut writer, v2)?;
192 writer.write_all(&0u16.to_le_bytes())?;
193 }
194
195 writer.flush()
196}
197
198pub fn write_stl_ascii(
200 nodes: &[[f64; 3]],
201 triangles: &[[usize; 3]],
202 path: &Path,
203) -> std::io::Result<()> {
204 let file = File::create(path)?;
205 let mut writer = BufWriter::new(file);
206 writeln!(writer, "solid mesh")?;
207
208 for tri in triangles {
209 let v0 = nodes[tri[0]];
210 let v1 = nodes[tri[1]];
211 let v2 = nodes[tri[2]];
212 let normal = triangle_normal(v0, v1, v2);
213
214 writeln!(
215 writer,
216 " facet normal {} {} {}",
217 normal[0], normal[1], normal[2]
218 )?;
219 writeln!(writer, " outer loop")?;
220 writeln!(writer, " vertex {} {} {}", v0[0], v0[1], v0[2])?;
221 writeln!(writer, " vertex {} {} {}", v1[0], v1[1], v1[2])?;
222 writeln!(writer, " vertex {} {} {}", v2[0], v2[1], v2[2])?;
223 writeln!(writer, " endloop")?;
224 writeln!(writer, " endfacet")?;
225 }
226
227 writeln!(writer, "endsolid mesh")?;
228 writer.flush()
229}
230
231#[cfg(test)]
232mod write_tests {
233 use super::*;
234
235 #[test]
236 fn binary_writer_rejects_triangle_count_above_u32() {
237 let error = checked_triangle_count(usize::MAX).expect_err("usize::MAX exceeds u32");
238 assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
239 }
240}
241
242fn triangle_normal(v0: [f64; 3], v1: [f64; 3], v2: [f64; 3]) -> [f64; 3] {
243 let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
244 let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
245 let cx = e1[1] * e2[2] - e1[2] * e2[1];
246 let cy = e1[2] * e2[0] - e1[0] * e2[2];
247 let cz = e1[0] * e2[1] - e1[1] * e2[0];
248 let len = (cx * cx + cy * cy + cz * cz).sqrt();
249 if len < 1e-15 {
250 [0.0, 0.0, 0.0]
251 } else {
252 [cx / len, cy / len, cz / len]
253 }
254}
255
256impl TriSurface {
257 pub fn face_normals(&self) -> Vec<[f64; 3]> {
259 self.triangles
260 .iter()
261 .map(|tri| triangle_normal(self.nodes[tri[0]], self.nodes[tri[1]], self.nodes[tri[2]]))
262 .collect()
263 }
264
265 pub fn feature_edges(&self, angle_threshold_deg: f64) -> Vec<[usize; 2]> {
267 let normals = self.face_normals();
268 let cos_threshold = angle_threshold_deg.to_radians().cos();
269 let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
270
271 for (fi, tri) in self.triangles.iter().enumerate() {
272 for &(a, b) in &[(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
273 let key = if a < b { (a, b) } else { (b, a) };
274 edge_faces.entry(key).or_default().push(fi);
275 }
276 }
277
278 let mut result = Vec::new();
279 for (&(a, b), faces) in &edge_faces {
280 let is_feature = if faces.len() == 1 {
281 true
282 } else if faces.len() == 2 {
283 let n0 = normals[faces[0]];
284 let n1 = normals[faces[1]];
285 let dot = n0[0] * n1[0] + n0[1] * n1[1] + n0[2] * n1[2];
286 dot < cos_threshold
287 } else {
288 true
289 };
290 if is_feature {
291 result.push([a, b]);
292 }
293 }
294 result
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 fn make_binary_stl(triangles: &[[[f32; 3]; 3]]) -> Vec<u8> {
303 let mut buf = Vec::new();
304 buf.extend_from_slice(&[0u8; 80]);
305 let count = u32::try_from(triangles.len()).expect("triangle count exceeds u32");
306 buf.extend_from_slice(&count.to_le_bytes());
307 for tri in triangles {
308 buf.extend_from_slice(&[0u8; 12]);
309 for v in tri {
310 for &c in v {
311 buf.extend_from_slice(&c.to_le_bytes());
312 }
313 }
314 buf.extend_from_slice(&[0u8; 2]);
315 }
316 buf
317 }
318
319 #[test]
320 fn parse_ascii_stl() {
321 let ascii = b"solid test
322facet normal 0 0 1
323 outer loop
324 vertex 0 0 0
325 vertex 1 0 0
326 vertex 0 1 0
327 endloop
328endfacet
329facet normal 0 0 1
330 outer loop
331 vertex 1 0 0
332 vertex 1 1 0
333 vertex 0 1 0
334 endloop
335endfacet
336endsolid test";
337
338 let surface = parse_stl(ascii).unwrap();
339 assert_eq!(surface.nodes.len(), 4);
340 assert_eq!(surface.triangles.len(), 2);
341 }
342
343 #[test]
344 fn degenerate_triangle_filtered() {
345 let ascii = b"solid test
346facet normal 0 0 1
347 outer loop
348 vertex 0 0 0
349 vertex 0 0 0
350 vertex 0 1 0
351 endloop
352endfacet
353facet normal 0 0 1
354 outer loop
355 vertex 0 0 0
356 vertex 1 0 0
357 vertex 0 1 0
358 endloop
359endfacet
360endsolid test";
361
362 let surface = parse_stl(ascii).unwrap();
363 assert_eq!(surface.triangles.len(), 1);
364 }
365
366 #[test]
367 fn parse_binary_stl_single_triangle() {
368 let tri = [[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
369 let data = make_binary_stl(&[[tri[0], tri[1], tri[2]]]);
370 let surface = parse_stl(&data).unwrap();
371 assert_eq!(surface.nodes.len(), 3);
372 assert_eq!(surface.triangles.len(), 1);
373 }
374
375 #[test]
376 fn parse_binary_stl_multiple_triangles() {
377 let tris = [
378 [[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
379 [[1.0f32, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]],
380 ];
381 let data = make_binary_stl(&tris);
382 let surface = parse_stl(&data).unwrap();
383 assert_eq!(surface.nodes.len(), 4);
384 assert_eq!(surface.triangles.len(), 2);
385 }
386
387 #[test]
388 fn binary_stl_truncated_error() {
389 let data = vec![0u8; 50];
390 let result = parse_stl(&data);
391 assert!(result.is_err());
392 assert!(result.unwrap_err().contains("too short"));
393 }
394
395 #[test]
396 fn binary_stl_data_shortage_error() {
397 let mut data = vec![0u8; 84];
398 data[80] = 1;
399 let result = parse_stl(&data);
400 assert!(result.is_err());
401 assert!(result.unwrap_err().contains("truncated"));
402 }
403
404 #[test]
405 fn binary_stl_zero_triangles_error() {
406 let data = make_binary_stl(&[]);
407 let result = parse_stl(&data);
408 assert!(result.is_err());
409 assert!(result.unwrap_err().contains("no valid triangles"));
410 }
411
412 #[test]
413 fn detect_ascii_vs_binary() {
414 let ascii = b"solid test\nfacet normal 0 0 1\nendsolid test";
415 assert!(detect_ascii(ascii));
416
417 let binary = vec![0u8; 84];
418 assert!(!detect_ascii(&binary));
419
420 let mut tricky = vec![0u8; 84];
421 tricky[..5].copy_from_slice(b"solid");
422 tricky[5] = b' ';
423 assert!(!detect_ascii(&tricky));
424 }
425
426 #[test]
427 fn test_face_normals_basic() {
428 let surface = TriSurface {
429 nodes: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
430 triangles: vec![[0, 1, 2]],
431 };
432 let normals = surface.face_normals();
433 assert_eq!(normals.len(), 1);
434 assert!(normals[0][0].abs() < 1e-10);
435 assert!(normals[0][1].abs() < 1e-10);
436 assert!((normals[0][2] - 1.0).abs() < 1e-10);
437 }
438
439 #[test]
440 fn test_face_normals_degenerate() {
441 let surface = TriSurface {
442 nodes: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]],
443 triangles: vec![[0, 1, 2]],
444 };
445 let normals = surface.face_normals();
446 assert_eq!(normals.len(), 1);
447 assert!(normals[0][0].abs() < 1e-10);
448 assert!(normals[0][1].abs() < 1e-10);
449 assert!(normals[0][2].abs() < 1e-10);
450 }
451
452 #[test]
453 fn test_feature_edges_cube() {
454 let nodes = vec![
455 [0.0, 0.0, 0.0],
456 [1.0, 0.0, 0.0],
457 [1.0, 1.0, 0.0],
458 [0.0, 1.0, 0.0],
459 [0.0, 0.0, 1.0],
460 [1.0, 0.0, 1.0],
461 [1.0, 1.0, 1.0],
462 [0.0, 1.0, 1.0],
463 ];
464 let triangles = vec![
465 [0, 2, 1],
466 [0, 3, 2],
467 [4, 5, 6],
468 [4, 6, 7],
469 [0, 1, 5],
470 [0, 5, 4],
471 [3, 6, 2],
472 [3, 7, 6],
473 [0, 4, 7],
474 [0, 7, 3],
475 [1, 2, 6],
476 [1, 6, 5],
477 ];
478 let surface = TriSurface { nodes, triangles };
479 let edges = surface.feature_edges(30.0);
480 assert_eq!(edges.len(), 12);
481 }
482
483 #[test]
484 fn write_stl_files() {
485 let dir = std::env::temp_dir();
486 let suffix = format!(
487 "{}-{}",
488 std::process::id(),
489 std::time::SystemTime::now()
490 .duration_since(std::time::UNIX_EPOCH)
491 .unwrap()
492 .as_nanos()
493 );
494 let binary_path = dir.join(format!("neco-stl-{suffix}.bin.stl"));
495 let ascii_path = dir.join(format!("neco-stl-{suffix}.ascii.stl"));
496
497 let nodes = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
498 let triangles = [[0usize, 1, 2]];
499 write_stl_binary(&nodes, &triangles, &binary_path).unwrap();
500 write_stl_ascii(&nodes, &triangles, &ascii_path).unwrap();
501
502 let binary = std::fs::read(&binary_path).unwrap();
503 let ascii = std::fs::read_to_string(&ascii_path).unwrap();
504 assert_eq!(binary.len(), 134);
505 assert!(ascii.starts_with("solid mesh"));
506 assert!(ascii.contains("facet normal"));
507
508 let _ = std::fs::remove_file(binary_path);
509 let _ = std::fs::remove_file(ascii_path);
510 }
511}