Convert Mesh says how big a mesh is, the limit, and what converts (#797)

A mesh over the conversion budget is refused with its triangle (or vertex) count, the limit, and that only constant-section parts convert.

Refs #346

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
HiroKenEzandClaude Opus 5.5 authored and GitHub committed 2026-10-13 14:43:29 -04:00
1 parent 9cabc3532d
commit 9bb18842e9
1 file changed
+30 -3
+30 -3
View File
@@ -18,6 +18,14 @@ 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.
@@ -30,7 +38,7 @@ pub fn convert_prismatic_mesh(body: &Body, tolerance: f64) -> Result<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(invalid("mesh exceeds the 50000-triangle conversion budget"));
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();
@@ -69,8 +77,11 @@ pub fn convert_prismatic_mesh(body: &Body, tolerance: f64) -> Result<Body> {
}
fn weld(mesh: &crate::meshbody::TriMesh, tolerance: f64) -> Result<Body> {
if mesh.triangles.len() > MAX_TRIANGLES || mesh.positions.len() > MAX_TRIANGLES * 3 {
return Err(invalid("mesh exceeds the conversion budget"));
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);
@@ -464,6 +475,22 @@ mod tests {
}
}
#[test]
fn a_mesh_over_the_budget_says_how_big_it_is_and_what_converts() {
// #346: a freeform mesh just over the limit (a strip of 50001 triangles) is refused with
// its size, the limit, and that only constant-section parts convert at any size.
let n = MAX_TRIANGLES + 1;
let positions: Vec<Vec3> = (0..n + 2).map(|k| Vec3::new((k / 2) as f64, (k % 2) as f64, 0.)).collect();
let triangles: Vec<[u32; 3]> = (0..n as u32).map(|i| [i, i + 1, i + 2]).collect();
let body = crate::mesh_body(&positions, &triangles).unwrap();
let err = convert_prismatic_mesh(&body, 0.05).unwrap_err().to_string();
assert!(err.contains("50001 triangles") && err.contains("50000"), "{err}");
assert!(err.contains("constant-section") && err.contains("freeform"), "{err}");
let mesh = crate::meshbody::TriMesh { positions: vec![Vec3::ZERO; MAX_TRIANGLES * 3 + 1], triangles: vec![] };
let err = weld(&mesh, 0.05).unwrap_err().to_string();
assert!(err.contains("150001 vertices") && err.contains("150000"), "{err}");
}
#[test]
fn fitting_cannot_open_a_hole_through_a_nearby_wall() {
let outer = Loop2::polygon(&[Vec2::new(-1., -1.), Vec2::new(1., -1.), Vec2::new(1., 1.), Vec2::new(-1., 1.)]);