Bound mesh conversion nesting certification by loop count
certify_nesting runs two planar booleans per pair of boundary loops, but its work budget counted segments, so a mesh with many small holes could trigger ~1M booleans. Refuse more than 50 loops (1225 pairs) with a "too complex to convert" error and test a 400-hole plate. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
df4c8a2994
commit
ab72c5ad56
1 file changed
+38
@@ -9,6 +9,10 @@ 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}"))
|
||||
@@ -289,6 +293,12 @@ fn extrusion(mesh: &crate::meshbody::TriMesh, axis: Vec3, tolerance: f64) -> Res
|
||||
}
|
||||
|
||||
fn certify_nesting(raw: &[Loop2], fitted: &[Loop2], epsilon: f64, size: f64) -> Result<()> {
|
||||
if raw.len() > MAX_LOOPS {
|
||||
return Err(KernelError::Invalid(format!(
|
||||
"mesh conversion: too complex to convert ({} boundary loops; at most {MAX_LOOPS} are supported)",
|
||||
raw.len()
|
||||
)));
|
||||
}
|
||||
let mut work = 0usize;
|
||||
for (i, a) in raw.iter().enumerate() {
|
||||
for (j, b) in raw.iter().enumerate().skip(i + 1) {
|
||||
@@ -468,4 +478,32 @@ mod tests {
|
||||
assert_eq!(fitted.segs.len(), 2);
|
||||
assert!(certify_nesting(&[outer.clone(), inner], &[outer, fitted], 1e-6, 3.).is_err());
|
||||
}
|
||||
|
||||
fn perforated_plate(rows: usize, cols: usize) -> Body {
|
||||
let w = cols as f64 * 4. + 2.;
|
||||
let h = rows as f64 * 4. + 2.;
|
||||
let holes = (0..rows)
|
||||
.flat_map(|r| (0..cols).map(move |c| (r, c)))
|
||||
.map(|(r, c)| {
|
||||
let (x, y) = (c as f64 * 4. + 2., r as f64 * 4. + 2.);
|
||||
Loop2::polygon(&[Vec2::new(x, y), Vec2::new(x + 2., y), Vec2::new(x + 2., y + 2.), Vec2::new(x, y + 2.)]).reversed()
|
||||
})
|
||||
.collect();
|
||||
let outer = Loop2::polygon(&[Vec2::new(0., 0.), Vec2::new(w, 0.), Vec2::new(w, h), Vec2::new(0., h)]);
|
||||
crate::extrude_planar_regions(&Plane::XY, &[Region2 { outer, holes }], 0., 3.).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_holes_are_refused_promptly_instead_of_stalling() {
|
||||
// 400 holes would be ~80k loop pairs and ~160k planar booleans.
|
||||
let mesh = mesh_of(&perforated_plate(20, 20));
|
||||
let start = std::time::Instant::now();
|
||||
let err = convert_prismatic_mesh(&mesh, 0.05).unwrap_err().to_string();
|
||||
assert!(err.contains("too complex to convert"), "{err}");
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(10), "took {:?}", start.elapsed());
|
||||
// At the limit, a plate still converts in bounded time.
|
||||
let mesh = mesh_of(&perforated_plate(7, 7));
|
||||
let result = convert_prismatic_mesh(&mesh, 0.05).unwrap();
|
||||
assert!((crate::measure(&result).unwrap().volume - (30. * 30. - 49. * 4.) * 3.).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user