use crate::{Mesh, Vec3};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
pub const DEFAULT_RELATIVE_WELD_TOLERANCE: f64 = 1.0e-9;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct StlReadOptions {
pub weld_tolerance: Option<f64>,
}
pub use brep_kernel::StlFormat;
#[derive(Clone, Debug, PartialEq)]
pub struct StlImport {
pub mesh: Mesh,
pub format: StlFormat,
pub source_triangle_count: usize,
pub source_vertex_count: usize,
pub welded_vertex_count: usize,
pub weld_tolerance: f64,
pub dropped_facets: Vec<DroppedFacet>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DroppedFacet {
pub facet: usize,
pub reason: DroppedFacetReason,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DroppedFacetReason {
ZeroArea,
CollapsedByWeld,
}
impl Display for DroppedFacet {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.reason {
DroppedFacetReason::ZeroArea => {
write!(f, "facet {} has zero area (collinear or coincident corners)", self.facet)
}
DroppedFacetReason::CollapsedByWeld => {
write!(f, "facet {} has zero area after its corners weld together", self.facet)
}
}
}
}
#[derive(Debug)]
pub enum StlError {
Io {
path: PathBuf,
source: io::Error,
},
InvalidFormat(String),
Truncated {
expected: usize,
actual: usize,
},
InvalidGeometry(String),
InvalidOptions(String),
}
impl Display for StlError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, source } => {
write!(f, "could not read STL '{}': {source}", path.display())
}
Self::InvalidFormat(message) => write!(f, "invalid STL format: {message}"),
Self::Truncated { expected, actual } => write!(
f,
"truncated binary STL: expected {expected} bytes, found {actual}"
),
Self::InvalidGeometry(message) => write!(f, "invalid STL geometry: {message}"),
Self::InvalidOptions(message) => write!(f, "invalid STL options: {message}"),
}
}
}
impl From<brep_kernel::StlError> for StlError {
fn from(error: brep_kernel::StlError) -> Self {
match error {
brep_kernel::StlError::InvalidFormat(message) => Self::InvalidFormat(message),
brep_kernel::StlError::Truncated { expected, actual } => {
Self::Truncated { expected, actual }
}
brep_kernel::StlError::InvalidGeometry(message) => Self::InvalidGeometry(message),
}
}
}
impl std::error::Error for StlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
_ => None,
}
}
}
pub fn read_stl(path: impl AsRef<Path>) -> Result<StlImport, StlError> {
read_stl_with_options(path, &StlReadOptions::default())
}
pub fn read_stl_with_options(
path: impl AsRef<Path>,
options: &StlReadOptions,
) -> Result<StlImport, StlError> {
let path = path.as_ref();
let bytes = fs::read(path).map_err(|source| StlError::Io {
path: path.to_owned(),
source,
})?;
parse_stl_bytes(&bytes, options)
}
pub fn parse_stl_bytes(bytes: &[u8], options: &StlReadOptions) -> Result<StlImport, StlError> {
validate_options(options)?;
let parsed = brep_kernel::parse_stl_triangles(bytes)?;
let triangles = parsed
.triangles
.iter()
.map(|triangle| triangle.map(|[x, y, z]| Vec3::new(x, y, z)))
.collect();
build_import(triangles, parsed.format, options)
}
fn validate_options(options: &StlReadOptions) -> Result<(), StlError> {
if let Some(tolerance) = options.weld_tolerance {
if !tolerance.is_finite() || tolerance < 0.0 {
return Err(StlError::InvalidOptions(
"weld_tolerance must be finite and non-negative".into(),
));
}
}
Ok(())
}
fn build_import(
source: Vec<[Vec3; 3]>,
format: StlFormat,
options: &StlReadOptions,
) -> Result<StlImport, StlError> {
let source_triangle_count = source.len();
let source_vertex_count = source_triangle_count.checked_mul(3).ok_or_else(|| {
StlError::InvalidGeometry("source vertex count exceeds platform limits".into())
})?;
let mut bbox_min = source[0][0];
let mut bbox_max = source[0][0];
let mut dropped_facets = Vec::new();
let mut kept = Vec::with_capacity(source.len());
for (facet, triangle) in source.iter().enumerate() {
for &point in triangle {
if !point.is_finite() {
return Err(StlError::InvalidGeometry(format!(
"facet {facet} has a non-finite vertex"
)));
}
bbox_min.x = bbox_min.x.min(point.x);
bbox_min.y = bbox_min.y.min(point.y);
bbox_min.z = bbox_min.z.min(point.z);
bbox_max.x = bbox_max.x.max(point.x);
bbox_max.y = bbox_max.y.max(point.y);
bbox_max.z = bbox_max.z.max(point.z);
}
if facet_has_area(*triangle, facet)? {
kept.push((facet, *triangle));
} else {
dropped_facets.push(DroppedFacet {
facet,
reason: DroppedFacetReason::ZeroArea,
});
}
}
if kept.is_empty() {
return Err(StlError::InvalidGeometry(format!(
"every one of the {source_triangle_count} facet(s) has zero area"
)));
}
let extent = bbox_max - bbox_min;
let diagonal = extent.length();
if !extent.is_finite() || !diagonal.is_finite() || diagonal <= 0.0 {
return Err(StlError::InvalidGeometry(
"the bounding box is zero-sized or outside the supported numeric range".into(),
));
}
let weld_tolerance = options
.weld_tolerance
.unwrap_or(diagonal * DEFAULT_RELATIVE_WELD_TOLERANCE);
if !weld_tolerance.is_finite() {
return Err(StlError::InvalidGeometry(
"the derived weld tolerance is outside the supported numeric range".into(),
));
}
let (vertices, triangles) =
weld_vertices(&kept, bbox_min, diagonal, weld_tolerance, &mut dropped_facets)?;
if triangles.is_empty() {
return Err(StlError::InvalidGeometry(format!(
"every one of the {source_triangle_count} facet(s) has zero area after welding"
)));
}
dropped_facets.sort_by_key(|dropped| dropped.facet);
let welded_vertex_count = vertices.len();
Ok(StlImport {
mesh: Mesh::new(vertices, triangles),
format,
source_triangle_count,
source_vertex_count,
welded_vertex_count,
weld_tolerance,
dropped_facets,
})
}
fn facet_has_area(points: [Vec3; 3], facet: usize) -> Result<bool, StlError> {
let ab = points[1] - points[0];
let ac = points[2] - points[0];
if !ab.is_finite() || !ac.is_finite() {
return Err(StlError::InvalidGeometry(format!(
"facet {facet} exceeds the supported numeric range"
)));
}
let scale = [ab.x, ab.y, ab.z, ac.x, ac.y, ac.z]
.into_iter()
.fold(0.0_f64, |largest, value| largest.max(value.abs()));
let cross = if scale > 0.0 {
(ab / scale).cross(ac / scale)
} else {
Vec3::ZERO
};
if !cross.is_finite() {
return Err(StlError::InvalidGeometry(format!(
"facet {facet} exceeds the supported numeric range"
)));
}
Ok(cross.length_squared() != 0.0)
}
fn weld_vertices(
source: &[(usize, [Vec3; 3])],
bbox_min: Vec3,
diagonal: f64,
tolerance: f64,
dropped: &mut Vec<DroppedFacet>,
) -> Result<(Vec<Vec3>, Vec<[u32; 3]>), StlError> {
if tolerance == 0.0 {
return weld_exact(source, dropped);
}
let reach = diagonal + bbox_min.x.abs().max(bbox_min.y.abs()).max(bbox_min.z.abs());
if !(reach / tolerance <= (i64::MAX - 2) as f64) {
return Err(StlError::InvalidOptions(
"weld_tolerance is too small relative to the model size; use zero for exact welding"
.into(),
));
}
let mut weld = brep_kernel::VertexWeld::new(tolerance);
let mut vertices = Vec::<Vec3>::new();
let mut triangles = Vec::with_capacity(source.len());
for &(facet, points) in source {
let mut triangle = [0_u32; 3];
for (corner, &point) in points.iter().enumerate() {
let index = weld.weld([point.x, point.y, point.z]);
if index == vertices.len() {
vertices.push(point);
}
triangle[corner] = u32::try_from(index).map_err(|_| {
StlError::InvalidGeometry(
"the welded mesh exceeds the u32 vertex-index limit".into(),
)
})?;
}
if triangle[0] == triangle[1]
|| triangle[1] == triangle[2]
|| triangle[2] == triangle[0]
|| !facet_has_area(triangle.map(|index| vertices[index as usize]), facet)?
{
dropped.push(DroppedFacet {
facet,
reason: DroppedFacetReason::CollapsedByWeld,
});
continue;
}
triangles.push(triangle);
}
Ok((vertices, triangles))
}
fn weld_exact(
source: &[(usize, [Vec3; 3])],
dropped: &mut Vec<DroppedFacet>,
) -> Result<(Vec<Vec3>, Vec<[u32; 3]>), StlError> {
let mut vertices = Vec::new();
let mut indices = HashMap::<[u64; 3], u32>::new();
let mut triangles = Vec::with_capacity(source.len());
for &(facet, points) in source {
let mut triangle = [0_u32; 3];
for (corner, &point) in points.iter().enumerate() {
let canonical_bits = brep_kernel::canonical_bits;
let key = [
canonical_bits(point.x),
canonical_bits(point.y),
canonical_bits(point.z),
];
let index = match indices.get(&key) {
Some(&index) => index,
None => {
let index = u32::try_from(vertices.len()).map_err(|_| {
StlError::InvalidGeometry(
"the welded mesh exceeds the u32 vertex-index limit".into(),
)
})?;
vertices.push(point);
indices.insert(key, index);
index
}
};
triangle[corner] = index;
}
if triangle[0] == triangle[1]
|| triangle[1] == triangle[2]
|| triangle[2] == triangle[0]
|| !facet_has_area(triangle.map(|index| vertices[index as usize]), facet)?
{
dropped.push(DroppedFacet {
facet,
reason: DroppedFacetReason::CollapsedByWeld,
});
continue;
}
triangles.push(triangle);
}
Ok((vertices, triangles))
}