use crate::core::{Cell, CellKind, Dataset, Error, Mesh, Point, Result};
use std::io::Write;
#[derive(Debug, Clone, PartialEq)]
pub struct Projection {
pub dataset: Dataset,
pub normals: usize,
pub attributed_facets: usize,
}
fn err(message: impl Into<String>) -> Error {
Error::new("E_STL", message)
}
fn add_facet(mesh: &mut Mesh, vertices: [[f64; 3]; 3]) -> Result<()> {
let cell_id = u64::try_from(mesh.cells.len() + 1).map_err(|_| err("too many STL facets"))?;
let start = mesh.points.len();
let end = start
.checked_add(3)
.ok_or_else(|| err("too many STL vertices"))?;
for (offset, position) in vertices.into_iter().enumerate() {
if !position.iter().all(|value| value.is_finite()) {
return Err(err("nonfinite STL coordinate"));
}
let id = u64::try_from(start + offset + 1).map_err(|_| err("too many STL vertices"))?;
mesh.points.push(Point { id, position });
}
mesh.cells.push(Cell {
id: cell_id,
kind: CellKind::Triangle3,
connectivity: (start..end).collect(),
property_id: None,
});
Ok(())
}
fn float(bytes: &[u8]) -> Result<f64> {
let array: [u8; 4] = bytes.try_into().map_err(|_| err("truncated STL facet"))?;
let value = f32::from_le_bytes(array);
if !value.is_finite() {
return Err(err("nonfinite STL value"));
}
Ok(f64::from(value))
}
fn read_binary(bytes: &[u8], count: usize) -> Result<Projection> {
let mut mesh = Mesh {
points: Vec::with_capacity(
count
.checked_mul(3)
.ok_or_else(|| err("STL vertex count overflows"))?,
),
cells: Vec::with_capacity(count),
..Mesh::default()
};
let mut attributed_facets = 0;
for chunk in bytes[84..].chunks_exact(50) {
for component in chunk[..12].chunks_exact(4) {
let _ = float(component)?;
}
let mut vertices = [[0.0; 3]; 3];
for (vertex, encoded) in vertices.iter_mut().zip(chunk[12..48].chunks_exact(12)) {
for (component, word) in vertex.iter_mut().zip(encoded.chunks_exact(4)) {
*component = float(word)?;
}
}
attributed_facets += usize::from(chunk[48] != 0 || chunk[49] != 0);
add_facet(&mut mesh, vertices)?;
}
mesh.validate()?;
Ok(Projection {
dataset: Dataset {
mesh,
fields: vec![],
},
normals: count,
attributed_facets,
})
}
fn triple<'a>(words: impl Iterator<Item = &'a str>, what: &str) -> Result<[f64; 3]> {
let values = words
.map(|word| {
word.parse::<f64>()
.map_err(|_| err(format!("invalid {what}")))
})
.collect::<Result<Vec<_>>>()?;
let values: [f64; 3] = values
.try_into()
.map_err(|_| err(format!("{what} requires three values")))?;
if !values.iter().all(|value| value.is_finite()) {
return Err(err(format!("nonfinite {what}")));
}
Ok(values)
}
fn read_ascii(bytes: &[u8]) -> Result<Projection> {
let source =
std::str::from_utf8(bytes).map_err(|_| err("STL is neither binary nor UTF-8 ASCII"))?;
let mut lines = source
.lines()
.map(str::trim)
.filter(|line| !line.is_empty());
let header = lines
.next()
.ok_or_else(|| err("missing ASCII STL solid header"))?;
if !header.eq_ignore_ascii_case("solid") && !header.to_ascii_lowercase().starts_with("solid ") {
return Err(err("missing ASCII STL solid header"));
}
let mut mesh = Mesh::default();
let mut closed = false;
while let Some(line) = lines.next() {
if line.eq_ignore_ascii_case("endsolid")
|| line.to_ascii_lowercase().starts_with("endsolid ")
{
closed = true;
break;
}
let mut parts = line.split_whitespace();
if !parts
.next()
.is_some_and(|word| word.eq_ignore_ascii_case("facet"))
|| !parts
.next()
.is_some_and(|word| word.eq_ignore_ascii_case("normal"))
{
return Err(err("expected facet normal"));
}
let _normal = triple(parts, "facet normal")?;
if !lines
.next()
.is_some_and(|line| line.eq_ignore_ascii_case("outer loop"))
{
return Err(err("expected outer loop"));
}
let mut vertices = [[0.0; 3]; 3];
for vertex in &mut vertices {
let line = lines
.next()
.ok_or_else(|| err("truncated ASCII STL facet"))?;
let mut words = line.split_whitespace();
if !words
.next()
.is_some_and(|word| word.eq_ignore_ascii_case("vertex"))
{
return Err(err("expected vertex"));
}
*vertex = triple(words, "vertex")?;
}
if !lines
.next()
.is_some_and(|line| line.eq_ignore_ascii_case("endloop"))
|| !lines
.next()
.is_some_and(|line| line.eq_ignore_ascii_case("endfacet"))
{
return Err(err("expected endloop and endfacet"));
}
add_facet(&mut mesh, vertices)?;
}
if !closed || lines.next().is_some() {
return Err(err("missing endsolid or trailing STL data"));
}
mesh.validate()?;
let normals = mesh.cells.len();
Ok(Projection {
dataset: Dataset {
mesh,
fields: vec![],
},
normals,
attributed_facets: 0,
})
}
pub fn read_projection(bytes: &[u8]) -> Result<Projection> {
if bytes.len() >= 84 {
let count_bytes: [u8; 4] = bytes[80..84]
.try_into()
.map_err(|_| err("truncated STL facet count"))?;
let count = u32::from_le_bytes(count_bytes);
let count =
usize::try_from(count).map_err(|_| err("STL facet count exceeds platform size"))?;
if count.checked_mul(50).and_then(|size| size.checked_add(84)) == Some(bytes.len()) {
return read_binary(bytes, count);
}
}
read_ascii(bytes)
}
#[allow(clippy::cast_possible_truncation)] fn triangles(dataset: &Dataset) -> Result<Vec<[[f32; 3]; 3]>> {
dataset.validate()?;
dataset.mesh.require_no_sets("E_STL")?;
if !dataset.fields.is_empty() {
return Err(err("STL cannot encode numeric fields"));
}
if dataset
.mesh
.cells
.iter()
.any(|cell| cell.property_id.is_some())
{
return Err(err("STL cannot encode property IDs"));
}
let mut output = Vec::with_capacity(dataset.mesh.cells.len());
for cell in &dataset.mesh.cells {
if cell.kind != CellKind::Triangle3 {
return Err(err("STL output requires triangle surface cells"));
}
let mut vertices = [[0.0; 3]; 3];
for (position, &index) in vertices.iter_mut().zip(&cell.connectivity) {
for (component, &value) in position
.iter_mut()
.zip(&dataset.mesh.points[index].position)
{
let rounded = value as f32;
if !rounded.is_finite() || (value != 0.0 && rounded == 0.0) {
return Err(err("STL float32 coordinate overflows or underflows"));
}
*component = rounded;
}
}
output.push(vertices);
}
Ok(output)
}
fn normal64(vertices: &[[f64; 3]; 3]) -> Result<[f64; 3]> {
let [first, second, third] = *vertices;
let edge_one = [
second[0] - first[0],
second[1] - first[1],
second[2] - first[2],
];
let edge_two = [
third[0] - first[0],
third[1] - first[1],
third[2] - first[2],
];
let cross = [
edge_one[1] * edge_two[2] - edge_one[2] * edge_two[1],
edge_one[2] * edge_two[0] - edge_one[0] * edge_two[2],
edge_one[0] * edge_two[1] - edge_one[1] * edge_two[0],
];
let length = cross[0].hypot(cross[1]).hypot(cross[2]);
if !length.is_finite() || length == 0.0 {
return Err(err("degenerate STL triangle has no finite normal"));
}
Ok(cross.map(|component| component / length))
}
#[allow(clippy::cast_possible_truncation)] fn normal(vertices: &[[f32; 3]; 3]) -> Result<[f32; 3]> {
Ok(normal64(&vertices.map(|vertex| vertex.map(f64::from)))?.map(|value| value as f32))
}
pub fn write_data(dataset: &Dataset, mut writer: impl Write) -> Result<()> {
let facets = triangles(dataset)?;
let count = u32::try_from(facets.len()).map_err(|_| err("too many STL facets"))?;
let mut header = [0_u8; 80];
header[..18].copy_from_slice(b"caexfer binary STL");
writer.write_all(&header)?;
writer.write_all(&count.to_le_bytes())?;
for facet in &facets {
for value in normal(facet)? {
writer.write_all(&value.to_le_bytes())?;
}
for vertex in facet {
for value in vertex {
writer.write_all(&value.to_le_bytes())?;
}
}
writer.write_all(&[0, 0])?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_facets_do_not_infer_shared_nodes() {
let source = b"solid sample\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nfacet normal 0 0 1\nouter loop\nvertex 1 0 0\nvertex 1 1 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid sample\n";
let read = read_projection(source).unwrap();
assert_eq!(read.dataset.mesh.points.len(), 6);
assert_eq!(read.dataset.mesh.cells.len(), 2);
assert_eq!(read.dataset.mesh.cells[1].connectivity, [3, 4, 5]);
assert_eq!(read.normals, 2);
let mut binary = Vec::new();
write_data(&read.dataset, &mut binary).unwrap();
assert_eq!(binary.len(), 184);
assert_eq!(read_projection(&binary).unwrap().dataset, read.dataset);
}
#[test]
fn binary_header_does_not_determine_dialect() {
let dataset = read_projection(b"solid sample\nendsolid sample\n")
.unwrap()
.dataset;
let mut bytes = Vec::new();
write_data(&dataset, &mut bytes).unwrap();
bytes[..5].copy_from_slice(b"solid");
assert_eq!(read_projection(&bytes).unwrap().dataset, dataset);
}
#[test]
fn invalid_count_and_malformed_ascii_fail() {
let mut bytes = [0_u8; 84];
bytes[80..].copy_from_slice(&u32::MAX.to_le_bytes());
assert_eq!(read_projection(&bytes).unwrap_err().code, "E_STL");
assert_eq!(
read_projection(b"solid s\nfacet normal 0 0 1\n")
.unwrap_err()
.code,
"E_STL"
);
}
#[test]
fn unsupported_geometry_fails() {
let mut dataset = read_projection(b"solid s\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid s\n").unwrap().dataset;
dataset.mesh.cells[0].kind = CellKind::Tet4;
dataset.mesh.cells[0].connectivity.push(0);
assert!(write_data(&dataset, Vec::new()).is_err());
dataset.mesh.cells[0].kind = CellKind::Triangle3;
dataset.mesh.cells[0].connectivity.pop();
dataset.mesh.points[0].position[0] = f64::MAX;
assert_eq!(write_data(&dataset, Vec::new()).unwrap_err().code, "E_STL");
}
}