use std::io::{BufRead, BufReader, Read, Seek};
use std::sync::LazyLock;
use anyhow::Result;
use regex::Regex;
use sc_mesh_core::{MeshMetadata, Normal, Triangle, Vertex};
use crate::stl::TriangleIterator;
static ASTL_HEADER_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^solid (?P<name>([a-zA-Z0-9_-]+( [a-zA-Z0-9_-]+)*)?)$").unwrap());
static ASTL_HEADER_RE_LN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^solid (?P<name>([a-zA-Z0-9_-]+( [a-zA-Z0-9_-]+)*)?)\n$").unwrap()
});
pub struct AsciiStlReader<'a> {
lines: Box<dyn Iterator<Item = Result<Vec<String>>> + 'a>,
}
impl<'a> Iterator for AsciiStlReader<'a> {
type Item = Result<Triangle>;
fn next(&mut self) -> Option<Self::Item> {
match self.next_face() {
Ok(None) => None,
Ok(Some(t)) => Some(Ok(t)),
Err(e) => Some(Err(e)),
}
}
}
impl<'a> AsciiStlReader<'a> {
pub fn probe<F: Read + Seek>(read: &mut F) -> Result<bool> {
let mut header = String::new();
let maybe_read_error = BufReader::new(&mut *read).read_line(&mut header);
read.seek(std::io::SeekFrom::Start(0))?;
match maybe_read_error {
Ok(_) => Ok(ASTL_HEADER_RE_LN.is_match(&header)),
Err(_) => Ok(false),
}
}
pub fn extract_metadata<F: Read + Seek>(read: &mut F) -> Result<MeshMetadata> {
let mut header = String::new();
let maybe_read_error = BufReader::new(&mut *read).read_line(&mut header);
read.seek(std::io::SeekFrom::Start(0))?;
maybe_read_error?;
match ASTL_HEADER_RE_LN.captures(&header) {
Some(c) => Ok(MeshMetadata {
name: match c.name("name") {
Some(n) => {
if !n.is_empty() {
Some(String::from(n.as_str()))
} else {
None
}
}
None => None,
},
}),
None => Ok(MeshMetadata { name: None }),
}
}
pub fn create_triangles_iterator(
read: &'a mut dyn Read,
) -> Result<Box<dyn TriangleIterator<Item = Result<Triangle>> + 'a>> {
let mut lines = BufReader::new(read).lines();
match lines.next() {
Some(Err(e)) => return Err(e.into()),
Some(Ok(ref line)) if !ASTL_HEADER_RE.is_match(line) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Ascii STL does not start with \"solid \"",
)
.into());
}
None => {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"File seems to be empty",
)
.into());
}
_ => {}
}
let lines = lines
.map(|line_result| {
line_result
.map(|l| {
l.split_whitespace()
.map(|t| t.to_string())
.collect::<Vec<_>>()
})
.map_err(|err| err.into())
})
.filter(|result| !matches!(result, Ok(tokens) if tokens.is_empty()));
Ok(Box::new(AsciiStlReader {
lines: Box::new(lines),
})
as Box<dyn TriangleIterator<Item = Result<Triangle>>>)
}
fn next_face(&mut self) -> Result<Option<Triangle>> {
let Some(face_header) = self.lines.next() else {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF while expecting facet or endsolid.",
)
.into());
};
let face_header = face_header?;
if !face_header.is_empty() && face_header[0] == "endsolid" {
return Ok(None);
}
if face_header.len() != 5 || face_header[0] != "facet" || face_header[1] != "normal" {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid facet header: {face_header:?}"),
)
.into());
}
let mut result_normal = Normal::default();
AsciiStlReader::tokens_to_f32(&face_header[2..5], &mut result_normal.0[0..3])?;
self.expect_static(&["outer", "loop"])?;
let mut result_vertices = [Vertex::default(); 3];
for vertex_result in &mut result_vertices {
let Some(line) = self.lines.next() else {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF while expecting vertex",
)
.into());
};
let line = line?;
if line.len() != 4 || line[0] != "vertex" {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("vertex f32 f32 f32, got {line:?}"),
)
.into());
}
AsciiStlReader::tokens_to_f32(&line[1..4], &mut vertex_result.0[0..3])?;
}
self.expect_static(&["endloop"])?;
self.expect_static(&["endfacet"])?;
Ok(Some(Triangle {
normal: result_normal,
vertices: result_vertices,
}))
}
fn tokens_to_f32(tokens: &[String], output: &mut [f32]) -> Result<()> {
assert_eq!(tokens.len(), output.len());
for (token, out) in tokens.iter().zip(output.iter_mut()) {
let f = token
.parse::<f32>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
if !f.is_finite() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("expected finite f32, got {f} which is {:?}", f.classify()),
)
.into());
}
*out = f;
}
Ok(())
}
fn expect_static(&mut self, expectation: &[&str]) -> Result<()> {
let Some(line) = self.lines.next() else {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!("EOF while expecting {expectation:?}"),
)
.into());
};
let line = line?;
if line != expectation {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("expected {expectation:?}, got {line:?}"),
)
.into());
}
Ok(())
}
}
impl<'a> TriangleIterator for AsciiStlReader<'a> {}
#[cfg(test)]
mod test_ascii_stl_reader {
use sc_mesh_core::{IndexedMesh, IndexedTriangle, MeshMetadata, Normal, Vertex};
use super::AsciiStlReader;
static STL_1: &'static [u8; 304] = b"solid foobar
facet normal -1.000000e+000 0.000000e+000 0.000000e+000
outer loop
vertex 0.000000e+000 1.000000e+002 1.000000e+002
vertex 0.000000e+000 1.000000e+002 0.000000e+000
vertex 0.000000e+000 0.000000e+000 1.000000e+002
endloop
endfacet
endsolid foobar";
static STL_2: &'static [u8; 291] = b"solid
facet normal -1.000000e+000 0.000000e+000 0.000000e+000
outer loop
vertex 0.000000e+000 1.000000e+002 1.000000e+002
vertex 0.000000e+000 1.000000e+002 0.000000e+000
vertex 0.000000e+000 0.000000e+000 1.000000e+002
endloop
endfacet
endsolid";
#[test]
fn test_probe() {
let mut reader_1 = std::io::Cursor::new(STL_1.clone());
assert!(AsciiStlReader::probe(&mut reader_1).unwrap());
let mut reader_2 = std::io::Cursor::new(STL_2.clone());
assert!(AsciiStlReader::probe(&mut reader_2).unwrap());
let mut reader_3 = std::io::Cursor::new(
b"something
else",
);
assert!(!AsciiStlReader::probe(&mut reader_3).unwrap());
}
#[test]
fn test_extract_metadata() {
let mut reader_1 = std::io::Cursor::new(STL_1.clone());
let metadata_1 = AsciiStlReader::extract_metadata(&mut reader_1).unwrap();
assert_eq!(
MeshMetadata {
name: Some("foobar".into())
},
metadata_1
);
let mut reader_2 = std::io::Cursor::new(STL_2.clone());
let metadata_2 = AsciiStlReader::extract_metadata(&mut reader_2).unwrap();
assert_eq!(MeshMetadata { name: None }, metadata_2);
}
#[test]
fn test_as_indexed_mesh() {
let mut reader_1 = std::io::Cursor::new(STL_1.clone());
let mesh = AsciiStlReader::create_triangles_iterator(&mut reader_1)
.unwrap()
.as_indexed_mesh(None)
.unwrap();
assert_eq!(
IndexedMesh {
meta: None,
vertices: vec![
Vertex::new([0.000000e+000, 1.000000e+002, 1.000000e+002]),
Vertex::new([0.000000e+000, 1.000000e+002, 0.000000e+000]),
Vertex::new([0.000000e+000, 0.000000e+000, 1.000000e+002]),
],
faces: vec![IndexedTriangle {
normal: Normal::new([-1.000000e+000, 0.000000e+000, 0.000000e+000]),
vertices: [0, 1, 2],
}],
},
mesh
);
}
}