diff --git a/crates/kernel/src/mesh_convert.rs b/crates/kernel/src/mesh_convert.rs index 6ea13565..ebe19af9 100644 --- a/crates/kernel/src/mesh_convert.rs +++ b/crates/kernel/src/mesh_convert.rs @@ -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 { 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 { } fn weld(mesh: &crate::meshbody::TriMesh, tolerance: f64) -> Result { - 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 = (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.)]);