use crate::compiled::real::LawMinMax;
use crate::eval::eval;
use crate::interval::{eval_interval, Vec3Interval};
use crate::mesh::{validate_mesh, Mesh, MeshValidation};
use crate::types::SdfNode;
use glam::Vec3;
const MARCH_EPS: f32 = 1e-4;
#[derive(Debug, Clone, Copy)]
pub struct PrintRequirements {
pub min_wall: f32,
pub max_overhang: f32,
pub build_direction: Vec3,
pub erosion_depth: u32,
}
impl PrintRequirements {
#[must_use]
pub const fn fdm_0_4_nozzle() -> Self {
Self {
min_wall: 0.8,
max_overhang: std::f32::consts::FRAC_PI_4,
build_direction: Vec3::Z,
erosion_depth: 6,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ErosionVerdict {
HasThickEnoughRegion {
witness: Vec3,
},
EntirelyTooThin,
Undecided,
}
impl ErosionVerdict {
#[must_use]
pub const fn has_thick_region(&self) -> bool {
matches!(self, Self::HasThickEnoughRegion { .. })
}
}
#[derive(Debug, Clone)]
pub struct ValidityReport {
pub mesh: MeshValidation,
pub erosion: ErosionVerdict,
pub min_local_thickness: Option<f32>,
pub thin_triangles: usize,
pub max_overhang: f32,
pub overhang_triangles: usize,
pub requirements: PrintRequirements,
}
impl ValidityReport {
#[must_use]
pub const fn is_printable(&self) -> bool {
self.mesh.is_clean()
&& self.erosion.has_thick_region()
&& self.thin_triangles == 0
&& self.overhang_triangles == 0
}
}
#[must_use]
pub fn overhang_stats(mesh: &Mesh, build_direction: Vec3, threshold: f32) -> (f32, usize) {
let dir = build_direction.normalize_or_zero();
let mut max_angle = 0.0_f32;
let mut over = 0_usize;
for idx in mesh.indices.chunks_exact(3) {
let Some(normal) = face_normal(mesh, idx) else {
continue; };
let downward = (-normal.dot(dir)).clamp(-1.0, 1.0);
let angle = if downward <= 0.0 {
0.0
} else {
alice_det_math::asin(downward)
};
if angle > max_angle {
max_angle = angle;
}
if angle > threshold {
over += 1;
}
}
(max_angle, over)
}
fn face_normal(mesh: &Mesh, idx: &[u32]) -> Option<Vec3> {
let p = |i: u32| mesh.vertices.get(i as usize).map(|v| v.position);
let (a, b, c) = (p(idx[0])?, p(idx[1])?, p(idx[2])?);
let n = (b - a).cross(c - a);
let len = n.length();
if len <= f32::EPSILON {
return None;
}
Some(n / len)
}
#[must_use]
pub fn local_thickness(node: &SdfNode, origin: Vec3, inward: Vec3, max_march: f32) -> Option<f32> {
let dir = inward.normalize_or_zero();
if dir == Vec3::ZERO {
return None;
}
let mut t = MARCH_EPS;
loop {
let d = eval(node, origin + dir * t);
if d < 0.0 {
break; }
t += d.law_max(MARCH_EPS);
if t > max_march {
return None; }
}
loop {
let d = eval(node, origin + dir * t);
if d >= 0.0 {
return Some(t);
}
t += (-d).law_max(MARCH_EPS);
if t > max_march {
return None; }
}
}
#[must_use]
pub fn prove_erosion(
node: &SdfNode,
min_wall: f32,
bmin: Vec3,
bmax: Vec3,
max_depth: u32,
) -> ErosionVerdict {
let eroded = node.clone().round(-min_wall * 0.5);
let mut undecided = false;
let mut stack: Vec<(Vec3, Vec3, u32)> = vec![(bmin, bmax, 0)];
while let Some((lo, hi, depth)) = stack.pop() {
let iv = eval_interval(&eroded, Vec3Interval::from_bounds(lo, hi));
if iv.is_positive() {
continue; }
if iv.is_negative() {
return ErosionVerdict::HasThickEnoughRegion {
witness: (lo + hi) * 0.5,
};
}
if depth >= max_depth {
undecided = true; continue;
}
let mid = (lo + hi) * 0.5;
for i in 0..8_usize {
let (x0, x1) = if i & 1 == 0 {
(lo.x, mid.x)
} else {
(mid.x, hi.x)
};
let (y0, y1) = if i & 2 == 0 {
(lo.y, mid.y)
} else {
(mid.y, hi.y)
};
let (z0, z1) = if i & 4 == 0 {
(lo.z, mid.z)
} else {
(mid.z, hi.z)
};
stack.push((Vec3::new(x0, y0, z0), Vec3::new(x1, y1, z1), depth + 1));
}
}
if undecided {
ErosionVerdict::Undecided
} else {
ErosionVerdict::EntirelyTooThin
}
}
#[must_use]
pub fn validate_for_printing(
node: &SdfNode,
mesh: &Mesh,
bmin: Vec3,
bmax: Vec3,
req: PrintRequirements,
) -> ValidityReport {
let erosion = prove_erosion(node, req.min_wall, bmin, bmax, req.erosion_depth);
let (max_overhang, overhang_triangles) =
overhang_stats(mesh, req.build_direction, req.max_overhang);
let span = (bmax - bmin).length();
let mut min_local: Option<f32> = None;
let mut thin_triangles = 0_usize;
for idx in mesh.indices.chunks_exact(3) {
let Some(normal) = face_normal(mesh, idx) else {
continue;
};
let p = |i: u32| mesh.vertices[i as usize].position;
let centroid = (p(idx[0]) + p(idx[1]) + p(idx[2])) / 3.0;
if let Some(thickness) = local_thickness(node, centroid, -normal, span) {
min_local = Some(min_local.map_or(thickness, |m| m.law_min(thickness)));
if thickness < req.min_wall {
thin_triangles += 1;
}
}
}
ValidityReport {
mesh: validate_mesh(mesh),
erosion,
min_local_thickness: min_local,
thin_triangles,
max_overhang,
overhang_triangles,
requirements: req,
}
}
#[derive(Debug)]
pub enum ValidatedExportError {
Io(std::io::Error),
NotPrintable(Box<ValidityReport>),
}
impl std::fmt::Display for ValidatedExportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "STEP export failed: {e}"),
Self::NotPrintable(r) => write!(
f,
"not printable, nothing written (erosion {:?}, thin triangles {}, \
overhang triangles {}, min local thickness {:?})",
r.erosion, r.thin_triangles, r.overhang_triangles, r.min_local_thickness
),
}
}
}
impl std::error::Error for ValidatedExportError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::NotPrintable(_) => None,
}
}
}
impl From<std::io::Error> for ValidatedExportError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
pub fn export_step_validated(
path: impl AsRef<std::path::Path>,
node: &SdfNode,
cfg: &crate::io::step::StepConfig,
req: PrintRequirements,
) -> Result<ValidityReport, ValidatedExportError> {
let (lo, hi) = cfg.bounds;
let (bmin, bmax) = (Vec3::splat(lo), Vec3::splat(hi));
let mc = crate::mesh::MarchingCubesConfig {
resolution: usize::try_from(cfg.resolution).unwrap_or(usize::MAX),
..Default::default()
};
let mesh = crate::mesh::sdf_to_mesh(node, bmin, bmax, &mc);
let report = validate_for_printing(node, &mesh, bmin, bmax, req);
if !report.is_printable() {
return Err(ValidatedExportError::NotPrintable(Box::new(report)));
}
crate::io::step::export_step(path, node, cfg)?;
Ok(report)
}