Skip to main content

gmac/io/
stl.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
4
5use crate::error::{Result, Error};
6
7#[cfg(feature = "rayon")]
8use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
9
10/// Enumeration of supported STL file formats.
11///
12/// - `Ascii`: Human-readable ASCII STL format.
13/// - `Binary`: Compact binary STL format, default choice for efficient storage and faster parsing.
14pub enum StlFormat {
15    Ascii,
16    Binary,
17}
18
19/// Writes a mesh to STL file. Defaults to binary format.
20///
21/// # Arguments
22/// * `nodes` - List of 3D vertex positions.
23/// * `cells` - List of triangles (indices into nodes).
24/// * `filename` - Optional file path. Defaults to `"mesh.stl"`.
25/// * `format` - Optional STL format. Defaults to `StlFormat::Binary`.
26///
27/// # Returns
28/// `Ok(())` on success, or an IO error.
29pub fn write_stl(
30    nodes: &[[f64; 3]],
31    cells: &[[usize; 3]],
32    filename: Option<&str>,
33    format: Option<StlFormat>,
34) -> Result<()> {
35    match format.unwrap_or(StlFormat::Binary) {
36        StlFormat::Ascii => write_stl_ascii(nodes, cells, filename),
37        StlFormat::Binary => write_stl_binary(nodes, cells, filename),
38    }
39}
40
41/// Writes a triangle mesh to an ASCII STL file.
42///
43/// The STL format represents a 3D surface mesh as a series of triangles.
44/// Each triangle is described with a normal vector and three vertices.
45///
46/// # Arguments
47/// * `nodes` - A list of 3D vertex positions.
48/// * `cells` - A list of triangles, each defined by indices into the `nodes` array.
49/// * `filename` - Optional file path. Defaults to `"mesh.stl"` if `None`.
50///
51/// # Returns
52/// Returns `Ok(())` on success, or an `std::io::Error` on failure.
53pub fn write_stl_ascii(
54    nodes: &[[f64; 3]],
55    cells: &[[usize; 3]],
56    filename: Option<&str>,
57) -> Result<()> {
58    // This closure defines the CPU-intensive work: calculating data and formatting the string.
59    let process_cell = |cell: &[usize; 3]| -> String {
60        let (normal, [p1, p2, p3]) = compute_facet_data(nodes, cell);
61        format!(
62            "  facet normal {} {} {}\n    outer loop\n      vertex {} {} {}\n      vertex {} {} {}\n      vertex {} {} {}\n    endloop\n  endfacet",
63            normal[0], normal[1], normal[2],
64            p1[0], p1[1], p1[2],
65            p2[0], p2[1], p2[2],
66            p3[0], p3[1], p3[2]
67        )
68    };
69
70    // Compute all triangle strings, using the appropriate iterator
71    #[cfg(feature = "rayon")]
72    let facet_strings: Vec<String> = cells.par_iter().map(process_cell).collect();
73
74    #[cfg(not(feature = "rayon"))]
75    let facet_strings: Vec<String> = cells.iter().map(process_cell).collect();
76
77    // Write the collected strings to a buffered writer sequentially
78    let file = File::create(filename.unwrap_or("mesh.stl"))?;
79    let mut writer = BufWriter::new(file);
80
81    writeln!(writer, "solid exported_grid")?;
82
83    for facet_str in facet_strings {
84        writeln!(writer, "{}", facet_str)?;
85    }
86
87    writeln!(writer, "endsolid exported_grid")?;
88
89    Ok(())
90}
91
92/// Writes a triangle mesh to a binary STL file.
93///
94/// Binary STL is a compact format used for 3D geometry exchange.
95/// Each triangle includes a normal and three vertices stored as 32-bit floats.
96/// An 80-byte header and a 4-byte triangle count are written before the triangle data.
97///
98/// # Arguments
99/// * `nodes` - A list of 3D vertex positions.
100/// * `cells` - A list of triangles, each defined by indices into the `nodes` array.
101/// * `filename` - Optional file path. Defaults to `"mesh.stl"` if `None`.
102///
103/// # Returns
104/// Returns `Ok(())` on success, or an `std::io::Error` on failure.
105pub fn write_stl_binary(
106    nodes: &[[f64; 3]],
107    cells: &[[usize; 3]],
108    filename: Option<&str>,
109) -> Result<()> {
110    // This closure defines the work to be done for a single triangle.
111    let process_cell = |cell: &[usize; 3]| -> [u8; 50] {
112        let (normal, [p1, p2, p3]) = compute_facet_data(nodes, cell);
113        let mut buffer = [0u8; 50];
114        let mut cursor = 0;
115
116        // Write normal and vertices into the in-memory buffer
117        for f in normal
118            .iter()
119            .chain(p1.iter())
120            .chain(p2.iter())
121            .chain(p3.iter())
122        {
123            let bytes = (*f as f32).to_le_bytes();
124            buffer[cursor..cursor + 4].copy_from_slice(&bytes);
125            cursor += 4;
126        }
127        buffer
128    };
129
130    // Compute all triangle byte data
131    #[cfg(feature = "rayon")]
132    let all_triangle_data: Vec<[u8; 50]> = cells.par_iter().map(process_cell).collect();
133
134    #[cfg(not(feature = "rayon"))]
135    let all_triangle_data: Vec<[u8; 50]> = cells.iter().map(process_cell).collect();
136
137    // Write the final data to the file sequentially
138    let mut file = File::create(filename.unwrap_or("mesh.stl"))?;
139
140    // 80-byte header
141    let mut header = [0u8; 80];
142    header[..30].copy_from_slice(b"Binary STL generated with GMAC");
143    file.write_all(&header)?;
144
145    // Number of triangles
146    let num_triangles = cells.len() as u32;
147    file.write_all(&num_triangles.to_le_bytes())?;
148
149    // Write all pre-computed triangle data
150    let flat_buffer: Vec<u8> = all_triangle_data.into_iter().flatten().collect();
151    file.write_all(&flat_buffer)?;
152
153    Ok(())
154}
155
156/// Computes the facet normal and vertex coordinates for a triangle cell.
157///
158/// Given a triangle defined by three indices into the `nodes` array,
159/// this function calculates the outward-facing normal using the right-hand rule,
160/// and returns the normal along with the three vertex coordinates.
161///
162/// # Arguments
163/// * `nodes` - A list of 3D vertex positions.
164/// * `cell` - A triangle defined by indices into the `nodes` array (must have length 3).
165///
166/// # Returns
167/// A tuple containing:
168/// - `[f64; 3]`: The unit normal vector of the triangle.
169/// - `[[f64; 3]; 3]`: The three vertex coordinates of the triangle.
170fn compute_facet_data(
171    nodes: &[[f64; 3]],
172    cell: &[usize; 3],
173) -> ([f64; 3], [[f64; 3]; 3]) {
174    let p1 = nodes[cell[0]];
175    let p2 = nodes[cell[1]];
176    let p3 = nodes[cell[2]];
177
178    // Vectors for normal
179    let u = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
180    let v = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
181
182    let mut normal = [
183        (u[1] * v[2]) - (u[2] * v[1]),
184        (u[2] * v[0]) - (u[0] * v[2]),
185        (u[0] * v[1]) - (u[1] * v[0]),
186    ];
187
188    // Normalize
189    let norm =
190        (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
191    if norm > f64::EPSILON {
192        normal = [normal[0] / norm, normal[1] / norm, normal[2] / norm];
193    }
194
195    (normal, [p1, p2, p3])
196}
197
198/// Reads an STL file (ASCII or binary) and extracts its mesh.
199///
200/// This function automatically detects the file format (ASCII or binary)
201/// based on the initial bytes and calls the appropriate parser.
202///
203/// # Arguments
204/// * `filename` - Path to the STL file.
205///
206/// # Returns
207/// Returns a tuple `(nodes, cells)` where:
208/// - `nodes`: Unique list of 3D points (`Vec<[f64; 3]>`)
209/// - `cells`: Triangles as indices into `nodes` (`Vec<[usize; 3]>`)
210///
211/// # Errors
212/// Returns an error if the file cannot be opened, read, or parsed.
213pub fn read_stl(filename: &str) -> Result<(Vec<[f64; 3]>, Vec<[usize; 3]>)> {
214    let file = File::open(filename)?;
215    let mut reader = BufReader::new(file);
216
217    // Peek first 512 bytes to detect ASCII or binary
218    let mut peek_buf = [0u8; 512];
219    let bytes_read = reader.read(&mut peek_buf)?;
220
221    let header_str = std::str::from_utf8(&peek_buf[..bytes_read]).unwrap_or("");
222
223    let is_ascii = header_str.trim_start().starts_with("solid")
224        && header_str.contains("facet normal");
225
226    // Reset reader to start for proper parsing
227    reader.seek(SeekFrom::Start(0))?;
228
229    if is_ascii {
230        read_stl_ascii_from_buf(reader)
231    } else {
232        // For binary, skip the 80-byte header before reading triangles
233        reader.seek(SeekFrom::Start(80))?;
234
235        read_stl_binary_from_buf(reader)
236    }
237}
238
239/// Parses an ASCII STL file and extracts the mesh data.
240///
241/// This parser reads each line looking for `vertex` entries,
242/// deduplicates vertices using a hash map, and constructs a list of triangles.
243///
244/// # Arguments
245/// * `filename` - Path to the ASCII STL file.
246///
247/// # Returns
248/// Returns a tuple `(nodes, cells)` where:
249/// - `nodes`: Unique list of 3D points (`Vec<[f64; 3]>`)
250/// - `cells`: Triangles as indices into `nodes` (`Vec<[usize; 3]>`)
251///
252/// # Errors
253/// Returns an error if the file cannot be opened or parsed correctly.
254pub fn read_stl_ascii_from_buf<R: BufRead>(
255    reader: R,
256) -> Result<(Vec<[f64; 3]>, Vec<[usize; 3]>)> {
257    let mut node_map: HashMap<(u64, u64, u64), usize> = HashMap::new();
258    let mut nodes: Vec<[f64; 3]> = Vec::new();
259    let mut cells: Vec<[usize; 3]> = Vec::new();
260
261    let mut current_triangle = [0usize; 3];
262    let mut vertex_index = 0;
263
264    for line in reader.lines() {
265        let line = line?.trim().to_string();
266        if line.starts_with("vertex") {
267            let parts: Vec<f64> = line
268                .split_whitespace()
269                .skip(1)
270                .filter_map(|s| s.parse::<f64>().ok())
271                .collect();
272            if parts.len() == 3 {
273                let point = [parts[0], parts[1], parts[2]];
274                let key = (point[0].to_bits(), point[1].to_bits(), point[2].to_bits());
275                let idx = *node_map.entry(key).or_insert_with(|| {
276                    let new_idx = nodes.len();
277                    nodes.push(point);
278                    new_idx
279                });
280                current_triangle[vertex_index] = idx;
281                vertex_index += 1;
282                if vertex_index == 3 {
283                    cells.push(current_triangle);
284                    vertex_index = 0;
285                }
286            }
287        }
288    }
289
290    Ok((nodes, cells))
291}
292
293/// Parses a binary STL file and extracts the mesh data.
294///
295/// This parser reads fixed-size 50-byte chunks for each triangle,
296/// decodes 3 vertices and deduplicates them, building the mesh.
297///
298/// # Arguments
299/// * `filename` - Path to the binary STL file.
300///
301/// # Returns
302/// Returns a tuple `(nodes, cells)` where:
303/// - `nodes`: Unique list of 3D points (`Vec<[f64; 3]>`)
304/// - `cells`: Triangles as indices into `nodes` (`Vec<[usize; 3]>`)
305///
306/// # Errors
307/// Returns an error if the file cannot be opened or parsed.
308pub fn read_stl_binary_from_buf<R: Read>(
309    mut reader: R,
310) -> Result<(Vec<[f64; 3]>, Vec<[usize; 3]>)> {
311    // Read number of triangles (4 bytes)
312    let mut count_buf = [0u8; 4];
313    reader.read_exact(&mut count_buf)?;
314    let num_triangles = u32::from_le_bytes(count_buf) as usize;
315
316    // Sanity check on number of triangles
317    if num_triangles > 10_000_000 {
318        return Err(Error::FileSystem(std::io::Error::new(
319            std::io::ErrorKind::InvalidData,
320            format!("Too many triangles: {}", num_triangles),
321        )));
322    }
323
324    let mut nodes: Vec<[f64; 3]> = Vec::with_capacity(num_triangles * 3);
325    let mut cells: Vec<[usize; 3]> = Vec::with_capacity(num_triangles);
326    let mut node_map: HashMap<(u64, u64, u64), usize> = HashMap::new();
327
328    let mut tri_buf = [0u8; 50];
329
330    for _ in 0..num_triangles {
331        reader.read_exact(&mut tri_buf)?;
332
333        let mut triangle = [0usize; 3];
334        // Skip first 12 bytes (normal vector), then read vertices
335        for (i, chunk) in tri_buf[12..48].chunks_exact(12).enumerate() {
336            let x = f32::from_le_bytes(chunk[0..4].try_into().unwrap()) as f64;
337            let y = f32::from_le_bytes(chunk[4..8].try_into().unwrap()) as f64;
338            let z = f32::from_le_bytes(chunk[8..12].try_into().unwrap()) as f64;
339
340            let key = (x.to_bits(), y.to_bits(), z.to_bits());
341            let index = *node_map.entry(key).or_insert_with(|| {
342                let idx = nodes.len();
343                nodes.push([x, y, z]);
344                idx
345            });
346
347            triangle[i] = index;
348        }
349
350        cells.push(triangle);
351    }
352
353    Ok((nodes, cells))
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use std::fs::{remove_file, read_to_string, metadata};
360    use std::path::Path;
361
362    // Sample simple mesh: one triangle
363    fn sample_mesh() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
364        (
365            vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
366            vec![[0, 1, 2]],
367        )
368    }
369
370    #[test]
371    fn test_write_stl_ascii_creates_file() {
372        let (nodes, cells) = sample_mesh();
373        let filename = "test_ascii.stl";
374        let result = write_stl(&nodes, &cells, Some(filename), Some(StlFormat::Ascii));
375        assert!(result.is_ok());
376        assert!(Path::new(filename).exists());
377
378        let content = read_to_string(filename).expect("Failed to read ASCII STL");
379        assert!(content.contains("solid"));
380        assert!(content.contains("facet normal"));
381        remove_file(filename).unwrap();
382    }
383
384    #[test]
385    fn test_write_stl_binary_creates_file() {
386        let (nodes, cells) = sample_mesh();
387        let filename = "test_binary.stl";
388        let result = write_stl(&nodes, &cells, Some(filename), Some(StlFormat::Binary));
389        assert!(result.is_ok());
390        assert!(Path::new(filename).exists());
391
392        let metadata = metadata(filename).expect("Failed to get metadata");
393        // Binary STL must be larger than minimal ASCII STL length (header + data)
394        assert!(metadata.len() > 80);
395        remove_file(filename).unwrap();
396    }
397
398    #[test]
399    fn test_write_stl_defaults_to_binary() {
400        let (nodes, cells) = sample_mesh();
401        let filename = "test_default.stl";
402        let result = write_stl(&nodes, &cells, Some(filename), None);
403        assert!(result.is_ok());
404        assert!(Path::new(filename).exists());
405
406        // Check header for binary STL: file size > 80 (header) + 4 (count) bytes
407        let metadata = metadata(filename).unwrap();
408        assert!(metadata.len() > 84);
409        remove_file(filename).unwrap();
410    }
411}