commit b97c6055a1944a494307fdaecab584eed8e7733c Author: Alex Selimov Date: Mon Feb 10 22:48:23 2025 -0500 Initial commit with STL parser diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4a319a5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "roctree" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "*" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fa3c805 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod stl; +pub mod utilities; diff --git a/src/stl/mod.rs b/src/stl/mod.rs new file mode 100644 index 0000000..67c567f --- /dev/null +++ b/src/stl/mod.rs @@ -0,0 +1 @@ +pub mod parser; diff --git a/src/stl/parser.rs b/src/stl/parser.rs new file mode 100644 index 0000000..09bc43f --- /dev/null +++ b/src/stl/parser.rs @@ -0,0 +1,295 @@ +use anyhow::Result; +use anyhow::{anyhow, Context}; +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufRead, BufReader, Read}; +use std::path::Path; + +/// Representation of an STL mesh +/// Vertices are stored in vec with repeat vertices removed. Each vertex is represented as an +/// [f32; 3]. Triangles are represented as a [usize; 3] where each entry is an index in the +/// vertices Vec +pub struct StlSolid { + vertices: Vec<[f32; 3]>, + triangles: Vec<[usize; 3]>, + normals: Vec<[f32; 3]>, +} + +pub fn parse_ascii_stl(file_path: &impl AsRef) -> Result { + let file = File::open(file_path.as_ref()) + .with_context(|| format!("Failed to open file \'{:?}\'!", file_path.as_ref()))?; + + let mut lines = BufReader::new(file).lines(); + + // the first line starts with solid + match lines.next() { + Some(Ok(line)) => { + if !line.starts_with("solid ") { + return Err(anyhow!("STL file does not start with \'solid\'!")); + } + } + Some(Err(err)) => { + return Err(anyhow!("Failed to read the first line!\nError: {}", err)); + } + None => return Err(anyhow!("Failed to read from the file!")), + } + + let mut lines = lines + .map_while(Result::ok) + .filter(|line| !line.trim().is_empty()); + + // facet normal -0.01905 -0.770147 0.637582 + // outer loop + // vertex 3.29426 -0.2921 0.067036 + // vertex 3.35026 -0.216682 0.159808 + // vertex 3.11981 -0.142593 0.242416 + // endloop + // endfacet + let mut unique_vertices: HashMap = HashMap::new(); + let mut normals: Vec<[f32; 3]> = Vec::new(); + let mut triangles: Vec<[usize; 3]> = Vec::new(); + + // Main reading loop + while let Some(line) = lines.next() { + let line = line.trim(); + + // Read the facet normal + if line.starts_with("facet normal ") { + let pts = line + .split_whitespace() + .skip(2) + .map(|num| num.parse()) + .collect::, _>>() + .map_err(|err| anyhow!("Failed to parse normal coordinates! Err: {err:?}"))?; + + normals.push([ + *pts.first() + .ok_or(anyhow!("facet normal should have 3 positions"))?, + *pts.get(1) + .ok_or(anyhow!("facet normal should have 3 positions"))?, + *pts.get(2) + .ok_or(anyhow!("facet normal should have 3 positions"))?, + ]); + + // outer loop + lines.next(); + let mut indices = [0; 3]; + for index in indices.iter_mut() { + if let Some(line) = lines.next() { + if !unique_vertices.contains_key(&line) { + unique_vertices.insert(line.clone(), unique_vertices.len()); + } + *index = unique_vertices[&line]; + } + } + triangles.push(indices); + + // endloop + lines.next(); + // endfacet + lines.next(); + } + } + + // Now convert the unique vertices to a vec + let mut vertices = vec![[0.0; 3]; unique_vertices.len()]; + for (k, v) in unique_vertices.iter() { + let pts = k + .split_whitespace() + .skip(1) + .map(|num| num.parse()) + .collect::, _>>() + .map_err(|err| anyhow!("Failed to parse vertex coordinates! Err: {err:?}"))?; + vertices[*v] = [ + *pts.first() + .ok_or(anyhow!("vertex should have 3 positions"))?, + *pts.get(1) + .ok_or(anyhow!("vertex should have 3 positions"))?, + *pts.get(2) + .ok_or(anyhow!("vertex should have 3 positions"))?, + ]; + } + + Ok(StlSolid { + vertices, + triangles, + normals, + }) +} + +/// Parse a binary stl file +/// The specification for a binary stl file can be found at: +/// https://www.loc.gov/preservation/digital/formats/fdd/fdd000505.shtml +pub fn parse_binary_stl(file_path: &impl AsRef) -> Result { + let mut file = File::open(file_path.as_ref()) + .with_context(|| format!("Failed to open file \'{:?}\'!", file_path.as_ref()))?; + + let mut header = [0u8; 80]; + file.read_exact(&mut header) + .with_context(|| "Failed to read header from binary STL")?; + + let mut num_triangles_bytes = [0u8; 4]; + file.read_exact(&mut num_triangles_bytes) + .with_context(|| "Failed to read number of triangles from binary STL")?; + let num_triangles = u32::from_le_bytes(num_triangles_bytes); + + let mut unique_vertices: HashMap = HashMap::new(); + let mut normals: Vec<[f32; 3]> = Vec::new(); + let mut vertices: Vec<[f32; 3]> = Vec::new(); + let mut triangles: Vec<[usize; 3]> = Vec::new(); + + // Main reading loop + for _i in 0..num_triangles { + let mut buffer = [0u8; 50]; + file.read_exact(&mut buffer) + .with_context(|| "Failed to read triangle from binary stl")?; + + normals.push([ + f32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]), + f32::from_le_bytes([buffer[4], buffer[5], buffer[6], buffer[7]]), + f32::from_le_bytes([buffer[8], buffer[9], buffer[10], buffer[11]]), + ]); + + let mut indices = [0; 3]; + for (i, index) in indices.iter_mut().enumerate() { + let offset = 12 + i * 12; + let mut vertex = [0.0; 3]; + vertex[0] = f32::from_le_bytes([ + buffer[offset], + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + ]); + vertex[1] = f32::from_le_bytes([ + buffer[offset + 4], + buffer[offset + 5], + buffer[offset + 6], + buffer[offset + 7], + ]); + vertex[2] = f32::from_le_bytes([ + buffer[offset + 8], + buffer[offset + 9], + buffer[offset + 10], + buffer[offset + 11], + ]); + let key = format!("{vertex:?}").to_string(); + if !unique_vertices.contains_key(&key) { + vertices.push(vertex); + unique_vertices.insert(key.clone(), unique_vertices.len()); + } + //Safe to unwrap because we validate the key is present + *index = *(unique_vertices.get(&key).unwrap()); + } + triangles.push(indices); + } + + Ok(StlSolid { + vertices, + triangles, + normals, + }) +} + +#[cfg(test)] +pub mod test { + use super::*; + use crate::assert_vec_tol; + + struct VerticesNormalsTriangles(Vec<[f32; 3]>, Vec<[f32; 3]>, Vec<[usize; 3]>); + + fn get_real_cube() -> VerticesNormalsTriangles { + let unique_vertices = vec![ + [13.0, -22.0, 20.0], + [13.0, -22.0, 0.0], + [13.0, -2.0, 0.0], + [13.0, -2.0, 20.0], + [-7.0, -2.0, 20.0], + [-7.0, -2.0, 0.0], + [-7.0, -22.0, 0.0], + [-7.0, -22.0, 20.0], + ]; + let normals = vec![ + [1.0, -0.0, 0.0], + [1.0, -0.0, 0.0], + [-1.0, -0.0, -0.0], + [-1.0, -0.0, 0.0], + [0.0, 0.0, 1.0], + [-0.0, 0.0, 1.0], + [0.0, 0.0, -1.0], + [0.0, 0.0, -1.0], + [0.0, 1.0, 0.0], + [0.0, 1.0, -0.0], + [0.0, -1.0, 0.0], + [0.0, -1.0, 0.0], + ]; + let triangles = vec![ + [0, 1, 2], + [0, 2, 3], + [4, 5, 6], + [4, 6, 7], + [3, 4, 7], + [3, 7, 0], + [1, 6, 5], + [1, 5, 2], + [3, 2, 5], + [3, 5, 4], + [7, 6, 1], + [7, 1, 0], + ]; + + VerticesNormalsTriangles(unique_vertices, normals, triangles) + } + #[test] + pub fn test_parse_ascii_stl() { + let stl_solid = parse_ascii_stl(&"test_files/ascii_cube.stl").unwrap(); + + let VerticesNormalsTriangles(unique_vertices, normals, triangles) = get_real_cube(); + // First check vertices + if stl_solid.vertices.len() != unique_vertices.len() { + panic!("Parsed incorrect number of vertices") + } + for (read, real) in stl_solid.vertices.iter().zip(unique_vertices.iter()) { + assert_vec_tol!(read, real, 1e-7); + } + + // Now check the normals + + for (read, real) in stl_solid.normals.iter().zip(normals.iter()) { + assert_vec_tol!(read, real, 1e-7); + } + + // Now check the triangle indices + + for (read, real) in stl_solid.triangles.iter().zip(triangles.iter()) { + for (idx1, idx2) in read.iter().zip(real.iter()) { + assert_eq!(idx1, idx2); + } + } + } + + #[test] + pub fn test_parse_binary_stl() { + let stl_solid = parse_binary_stl(&"test_files/binary_cube.stl").unwrap(); + + let VerticesNormalsTriangles(unique_vertices, normals, triangles) = get_real_cube(); + // First check vertices + if stl_solid.vertices.len() != unique_vertices.len() { + panic!("Parsed incorrect number of vertices") + } + for (read, real) in stl_solid.vertices.iter().zip(unique_vertices.iter()) { + assert_vec_tol!(read, real, 1e-7); + } + + // Now check the normals + for (read, real) in stl_solid.normals.iter().zip(normals.iter()) { + assert_vec_tol!(read, real, 1e-7); + } + + // Now check the triangle indices + for (read, real) in stl_solid.triangles.iter().zip(triangles.iter()) { + for (idx1, idx2) in read.iter().zip(real.iter()) { + assert_eq!(idx1, idx2); + } + } + } +} diff --git a/src/utilities/mod.rs b/src/utilities/mod.rs new file mode 100644 index 0000000..6755121 --- /dev/null +++ b/src/utilities/mod.rs @@ -0,0 +1,2 @@ +#[cfg(test)] +pub mod test_macros; diff --git a/src/utilities/test_macros.rs b/src/utilities/test_macros.rs new file mode 100644 index 0000000..aa5c0af --- /dev/null +++ b/src/utilities/test_macros.rs @@ -0,0 +1,21 @@ +#[macro_export] +macro_rules! assert_vec_tol { + ($a:expr, $b:expr, $tolerance:expr) => {{ + let a = $a; + let b = $b; + let tolerance = $tolerance; + + if a.len() != b.len() { + panic!("Vectors have different lengths: {} != {}", a.len(), b.len()); + } + + for (i, (&val_a, &val_b)) in a.iter().zip(b.iter()).enumerate() { + if (val_a - val_b).abs() >= tolerance { + panic!( + "Values at index {} differ by more than the tolerance: {} != {} (tolerance = {})", + i, val_a, val_b, tolerance + ); + } + } + }}; +} diff --git a/test_files/ascii_cube.stl b/test_files/ascii_cube.stl new file mode 100644 index 0000000..5841d56 --- /dev/null +++ b/test_files/ascii_cube.stl @@ -0,0 +1,86 @@ +solid STL generated by MeshLab + facet normal 1.000000e+00 0.000000e+00 0.000000e+00 + outer loop + vertex 1.300000e+01 -2.200000e+01 2.000000e+01 + vertex 1.300000e+01 -2.200000e+01 0.000000e+00 + vertex 1.300000e+01 -2.000000e+00 0.000000e+00 + endloop + endfacet + facet normal 1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex 1.300000e+01 -2.200000e+01 2.000000e+01 + vertex 1.300000e+01 -2.000000e+00 0.000000e+00 + vertex 1.300000e+01 -2.000000e+00 2.000000e+01 + endloop + endfacet + facet normal -1.000000e+00 0.000000e+00 -0.000000e+00 + outer loop + vertex -7.000000e+00 -2.000000e+00 2.000000e+01 + vertex -7.000000e+00 -2.000000e+00 0.000000e+00 + vertex -7.000000e+00 -2.200000e+01 0.000000e+00 + endloop + endfacet + facet normal -1.000000e+00 -0.000000e+00 0.000000e+00 + outer loop + vertex -7.000000e+00 -2.000000e+00 2.000000e+01 + vertex -7.000000e+00 -2.200000e+01 0.000000e+00 + vertex -7.000000e+00 -2.200000e+01 2.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 1.300000e+01 -2.000000e+00 2.000000e+01 + vertex -7.000000e+00 -2.000000e+00 2.000000e+01 + vertex -7.000000e+00 -2.200000e+01 2.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 1.000000e+00 + outer loop + vertex 1.300000e+01 -2.000000e+00 2.000000e+01 + vertex -7.000000e+00 -2.200000e+01 2.000000e+01 + vertex 1.300000e+01 -2.200000e+01 2.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 1.300000e+01 -2.200000e+01 0.000000e+00 + vertex -7.000000e+00 -2.200000e+01 0.000000e+00 + vertex -7.000000e+00 -2.000000e+00 0.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 0.000000e+00 -1.000000e+00 + outer loop + vertex 1.300000e+01 -2.200000e+01 0.000000e+00 + vertex -7.000000e+00 -2.000000e+00 0.000000e+00 + vertex 1.300000e+01 -2.000000e+00 0.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex 1.300000e+01 -2.000000e+00 2.000000e+01 + vertex 1.300000e+01 -2.000000e+00 0.000000e+00 + vertex -7.000000e+00 -2.000000e+00 0.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 1.000000e+00 0.000000e+00 + outer loop + vertex 1.300000e+01 -2.000000e+00 2.000000e+01 + vertex -7.000000e+00 -2.000000e+00 0.000000e+00 + vertex -7.000000e+00 -2.000000e+00 2.000000e+01 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex -7.000000e+00 -2.200000e+01 2.000000e+01 + vertex -7.000000e+00 -2.200000e+01 0.000000e+00 + vertex 1.300000e+01 -2.200000e+01 0.000000e+00 + endloop + endfacet + facet normal 0.000000e+00 -1.000000e+00 0.000000e+00 + outer loop + vertex -7.000000e+00 -2.200000e+01 2.000000e+01 + vertex 1.300000e+01 -2.200000e+01 0.000000e+00 + vertex 1.300000e+01 -2.200000e+01 2.000000e+01 + endloop + endfacet +endsolid vcg diff --git a/test_files/binary_cube.stl b/test_files/binary_cube.stl new file mode 100644 index 0000000..c3712fc Binary files /dev/null and b/test_files/binary_cube.stl differ