use std::io::Write;
use brepkit_math::vec::{Point3, Vec3};
use brepkit_operations::tessellate::{self, TriangleMesh};
use brepkit_topology::Topology;
use brepkit_topology::solid::SolidId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StlFormat {
Binary,
Ascii,
}
pub fn write_stl(
topo: &Topology,
solids: &[SolidId],
deflection: f64,
format: StlFormat,
) -> Result<Vec<u8>, crate::IoError> {
let mut merged = TriangleMesh::default();
for &solid_id in solids {
let mesh = tessellate::tessellate_solid(topo, solid_id, deflection)?;
#[allow(clippy::cast_possible_truncation)]
let offset = merged.positions.len() as u32;
merged.positions.extend_from_slice(&mesh.positions);
merged.normals.extend_from_slice(&mesh.normals);
merged
.indices
.extend(mesh.indices.iter().map(|i| i + offset));
}
match format {
StlFormat::Binary => Ok(write_binary_stl(&merged)),
StlFormat::Ascii => write_ascii_stl(&merged),
}
}
fn triangle_data(mesh: &TriangleMesh, t: usize) -> (Vec3, Point3, Point3, Point3) {
let i0 = mesh.indices[t * 3] as usize;
let i1 = mesh.indices[t * 3 + 1] as usize;
let i2 = mesh.indices[t * 3 + 2] as usize;
let v0 = mesh.positions[i0];
let v1 = mesh.positions[i1];
let v2 = mesh.positions[i2];
let edge1 = v1 - v0;
let edge2 = v2 - v0;
let normal = edge1
.cross(edge2)
.normalize()
.unwrap_or(Vec3::new(0.0, 0.0, 1.0));
(normal, v0, v1, v2)
}
fn write_binary_stl(mesh: &TriangleMesh) -> Vec<u8> {
let tri_count = mesh.indices.len() / 3;
let mut buf = Vec::with_capacity(84 + tri_count * 50);
let header = b"brepkit STL export";
buf.extend_from_slice(header);
buf.resize(80, 0);
#[allow(clippy::cast_possible_truncation)]
buf.extend_from_slice(&(tri_count as u32).to_le_bytes());
for t in 0..tri_count {
let (normal, v0, v1, v2) = triangle_data(mesh, t);
write_f32_le(&mut buf, normal.x());
write_f32_le(&mut buf, normal.y());
write_f32_le(&mut buf, normal.z());
for v in [v0, v1, v2] {
write_f32_le(&mut buf, v.x());
write_f32_le(&mut buf, v.y());
write_f32_le(&mut buf, v.z());
}
buf.extend_from_slice(&[0u8, 0u8]);
}
buf
}
fn write_ascii_stl(mesh: &TriangleMesh) -> Result<Vec<u8>, crate::IoError> {
let tri_count = mesh.indices.len() / 3;
let mut buf = Vec::new();
writeln!(buf, "solid brepkit").map_err(crate::IoError::Io)?;
for t in 0..tri_count {
let (normal, v0, v1, v2) = triangle_data(mesh, t);
writeln!(
buf,
" facet normal {} {} {}",
normal.x(),
normal.y(),
normal.z()
)
.map_err(crate::IoError::Io)?;
writeln!(buf, " outer loop").map_err(crate::IoError::Io)?;
write_ascii_vertex(&mut buf, v0)?;
write_ascii_vertex(&mut buf, v1)?;
write_ascii_vertex(&mut buf, v2)?;
writeln!(buf, " endloop").map_err(crate::IoError::Io)?;
writeln!(buf, " endfacet").map_err(crate::IoError::Io)?;
}
writeln!(buf, "endsolid brepkit").map_err(crate::IoError::Io)?;
Ok(buf)
}
fn write_ascii_vertex(buf: &mut Vec<u8>, p: Point3) -> Result<(), crate::IoError> {
writeln!(buf, " vertex {} {} {}", p.x(), p.y(), p.z()).map_err(crate::IoError::Io)
}
#[allow(clippy::cast_possible_truncation)]
fn write_f32_le(buf: &mut Vec<u8>, v: f64) {
buf.extend_from_slice(&(v as f32).to_le_bytes());
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use brepkit_topology::Topology;
use brepkit_topology::test_utils::make_unit_cube_non_manifold;
use super::*;
#[test]
fn write_binary_stl_unit_cube() {
let mut topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut topo);
let bytes = write_stl(&topo, &[solid], 0.1, StlFormat::Binary).unwrap();
assert!(bytes.len() >= 84);
let tri_count = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]) as usize;
assert_eq!(tri_count, 12, "expected 12 triangles for unit cube");
assert_eq!(bytes.len(), 84 + tri_count * 50);
}
#[test]
fn write_ascii_stl_unit_cube() {
let mut topo = Topology::new();
let solid = make_unit_cube_non_manifold(&mut topo);
let bytes = write_stl(&topo, &[solid], 0.1, StlFormat::Ascii).unwrap();
let text = String::from_utf8(bytes).unwrap();
assert!(text.starts_with("solid brepkit"));
assert!(text.contains("facet normal"));
assert!(text.contains("vertex"));
assert!(text.trim().ends_with("endsolid brepkit"));
let facet_count = text.matches("facet normal").count();
assert_eq!(facet_count, 12, "expected 12 facets for unit cube");
}
#[test]
fn write_stl_box_primitive() {
let mut topo = Topology::new();
let solid = brepkit_operations::primitives::make_box(&mut topo, 2.0, 3.0, 4.0).unwrap();
let bytes = write_stl(&topo, &[solid], 0.1, StlFormat::Binary).unwrap();
let tri_count = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]) as usize;
assert_eq!(tri_count, 12, "box should have 12 triangles");
}
#[test]
fn write_stl_multiple_solids() {
let mut topo = Topology::new();
let s1 = brepkit_operations::primitives::make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
let s2 = brepkit_operations::primitives::make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
let bytes = write_stl(&topo, &[s1, s2], 0.1, StlFormat::Binary).unwrap();
let tri_count = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]) as usize;
assert_eq!(tri_count, 24, "two boxes should have 24 triangles");
}
#[test]
fn write_stl_watertight_box() {
let mut topo = Topology::new();
let solid = brepkit_operations::primitives::make_box(&mut topo, 2.0, 3.0, 4.0).unwrap();
let mesh = brepkit_operations::tessellate::tessellate_solid(&topo, solid, 0.1).unwrap();
let boundary = brepkit_operations::tessellate::boundary_edge_count(&mesh);
assert_eq!(
boundary, 0,
"STL mesh should have 0 boundary edges (watertight)"
);
}
#[test]
fn write_stl_shared_vertices_box() {
let mut topo = Topology::new();
let solid = brepkit_operations::primitives::make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
let mesh = brepkit_operations::tessellate::tessellate_solid(&topo, solid, 0.1).unwrap();
assert_eq!(
mesh.positions.len(),
8,
"box should share vertices at corners"
);
}
}