//! Conservative reconstruction of constant-section mechanical mesh parts.
//! Geometry is inferred from closed, manifold input, never from a display mesh.
use std::collections::BTreeMap;
use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
use crate::{Body, KernelError, Result};
const MAX_TRIANGLES: usize = 50_000;
const MAX_BOUNDARY: usize = 8192;
/// Nesting certification runs two planar booleans per pair of boundary loops,
/// so the loop count (not the segment count) bounds its cost: 50 loops (an
/// outline with 49 holes) is at most 1225 pairs and 2450 booleans.
const MAX_LOOPS: usize = 50;
fn invalid(message: &str) -> KernelError {
KernelError::Invalid(format!("mesh conversion: {message}"))
}
/// A mesh over the triangle budget: how big it is, the limit, and that only constant-section
/// parts convert, so a smaller freeform mesh would still be refused (#346).
fn over_budget(triangles: usize) -> KernelError {
invalid(&format!(
"the mesh has {triangles} triangles, more than the {MAX_TRIANGLES} conversion can take; only closed constant-section parts (one profile extruded straight) can be converted, so a freeform mesh is refused at any size"
))
}
/// Reconstruct a constant-section mesh as analytic planes and, where certified
/// within `tolerance` mm, circular cylinders. The source mesh is not modified.
/// Unsupported shapes are refused instead of silently returning a faceted solid.
pub fn convert_prismatic_mesh(body: &Body, tolerance: f64) -> Result
{
crate::guard("convert prismatic mesh", || {
if !tolerance.is_finite() || !(1e-6..=1.).contains(&tolerance) {
return Err(invalid("tolerance must be between 0.000001 and 1 mm"));
}
let input = body.mesh.as_ref().ok_or_else(|| invalid("select a mesh body"))?;
let welded = weld(input, tolerance)?;
let mesh = welded.mesh.as_ref().ok_or_else(|| invalid("empty mesh"))?;
if mesh.triangles.len() > MAX_TRIANGLES {
return Err(over_budget(mesh.triangles.len()));
}
let mut edges: BTreeMap<(u32, u32), (usize, i32)> = BTreeMap::new();
let mut axes: BTreeMap<(i64, i64, i64), (Vec3, f64)> = BTreeMap::new();
for triangle in &mesh.triangles {
let [a, b, c] = *triangle;
let (Some(pa), Some(pb), Some(pc)) = (mesh.positions.get(a as usize), mesh.positions.get(b as usize), mesh.positions.get(c as usize))
else {
return Err(invalid("triangle refers to a missing vertex"));
};
let cross = (*pb - *pa).cross(*pc - *pa);
let Some(mut n) = cross.normalized() else { return Err(invalid("degenerate triangle")) };
for (a, b) in [(a, b), (b, c), (c, a)] {
let entry = edges.entry((a.min(b), a.max(b))).or_default();
entry.0 += 1;
entry.1 += if a < b { 1 } else { -1 };
}
if n.x < -1e-8 || (n.x.abs() <= 1e-8 && (n.y < -1e-8 || (n.y.abs() <= 1e-8 && n.z < 0.))) {
n = -n;
}
let key = ((n.x * 1e7).round() as i64, (n.y * 1e7).round() as i64, (n.z * 1e7).round() as i64);
let entry = axes.entry(key).or_insert((n, 0.));
entry.1 += cross.len();
}
if edges.values().any(|&(count, balance)| count != 2 || balance != 0) {
return Err(invalid("mesh must be closed and consistently oriented, with two triangles per edge"));
}
let mut axes: Vec<_> = axes.into_values().collect();
axes.sort_by(|a, b| b.1.total_cmp(&a.1));
for (axis, _) in axes.into_iter().take(16) {
if let Some(result) = extrusion(mesh, axis, tolerance)? {
return Ok(result);
}
}
Err(invalid("this mesh is not a supported constant-section prism; no geometry was converted"))
})
}
fn weld(mesh: &crate::meshbody::TriMesh, tolerance: f64) -> Result {
if mesh.triangles.len() > MAX_TRIANGLES {
return Err(over_budget(mesh.triangles.len()));
}
if mesh.positions.len() > MAX_TRIANGLES * 3 {
return Err(invalid(&format!("the mesh has {} vertices, more than the {} conversion can take", mesh.positions.len(), MAX_TRIANGLES * 3)));
}
let bounds = mesh.positions.iter().fold(solidcraft_geom::Aabb3::EMPTY, |mut b, p| {
b.add(*p);
b
});
let epsilon = (bounds.size().len().max(1.) * 1e-7).min(tolerance * 0.01).max(1e-9);
if !epsilon.is_finite() || mesh.positions.iter().any(|p| [p.x, p.y, p.z].iter().any(|v| !v.is_finite() || (v / epsilon).abs() > 1e15)) {
return Err(invalid("mesh coordinates exceed the conversion budget"));
}
let key = |p: Vec3| ((p.x / epsilon).floor() as i64, (p.y / epsilon).floor() as i64, (p.z / epsilon).floor() as i64);
let mut grid = BTreeMap::<_, Vec>::new();
let mut positions: Vec = Vec::new();
let mut remap = Vec::new();
let mut comparisons = 0usize;
for &p in &mesh.positions {
let k = key(p);
let mut found = None;
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
for &i in grid.get(&(k.0 + dx, k.1 + dy, k.2 + dz)).into_iter().flatten() {
comparisons += 1;
if comparisons > 8_000_000 {
return Err(invalid("vertex welding exceeds its work budget"));
}
if positions.get(i as usize).is_some_and(|q| q.dist(p) <= epsilon) {
found = Some(i);
break;
}
}
}
}
}
let i = match found {
Some(i) => i,
None => {
let i = u32::try_from(positions.len()).map_err(|_| invalid("too many vertices"))?;
positions.push(p);
grid.entry(k).or_default().push(i);
i
}
};
remap.push(i);
}
let triangles = mesh
.triangles
.iter()
.map(|t| {
let (Some(&a), Some(&b), Some(&c)) = (remap.get(t[0] as usize), remap.get(t[1] as usize), remap.get(t[2] as usize)) else {
return Err(invalid("missing triangle vertex"));
};
Ok([a, b, c])
})
.collect::>>()?;
crate::mesh_body(&positions, &triangles)
}
fn extrusion(mesh: &crate::meshbody::TriMesh, axis: Vec3, tolerance: f64) -> Result