- A new cut was clipped against the panel with every one of its cut-outs, so each hole of a perforation cost the square of all the holes before it and the second cut of 500 ran out of the flat budget. A tool is now clipped against only the cut-outs whose boxes meet its own (the same result). - The planar booleans' point-in-loop test now skips loops whose box is clear of the point, before the costly on-the-loop test; with a thousand holes that test was most of the time. The 1000-hole panel now takes about 15 s instead of failing. Co-authored-by: jonaswre <3731681+jonaswre@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
5 files changed
+3654
-1
No files matched your search
@@ -968,9 +968,13 @@ fn sheet_prism_cut(
|
|||||||
}
|
}
|
||||||
modeled = true;
|
modeled = true;
|
||||||
} else if z1 > z0 {
|
} else if z1 > z0 {
|
||||||
|
// Each tool meets only the panel's cut-outs near it: arranging every one of a
|
||||||
|
// perforation's holes against every new hole would grow with their square (#732).
|
||||||
|
let near = sheet::NearHoles::new(&panel)?;
|
||||||
for flat in mapped {
|
for flat in mapped {
|
||||||
|
let local = near.around(&flat)?;
|
||||||
let clipped =
|
let clipped =
|
||||||
sheet::material_boolean(std::slice::from_ref(&panel), std::slice::from_ref(&flat), kernel::BoolOp::Intersect, &mut work)?;
|
sheet::material_boolean(std::slice::from_ref(&local), std::slice::from_ref(&flat), kernel::BoolOp::Intersect, &mut work)?;
|
||||||
if !clipped.is_empty() && !depth_resolved {
|
if !clipped.is_empty() && !depth_resolved {
|
||||||
return Err(DocError::Invalid(
|
return Err(DocError::Invalid(
|
||||||
"sheet machining depth is below the supported 0.000001 mm extrusion resolution; increase the layer depth".into(),
|
"sheet machining depth is below the supported 0.000001 mm extrusion resolution; increase the layer depth".into(),
|
||||||
|
|||||||
@@ -166,6 +166,46 @@ fn regions_complexity<'a>(regions: impl Iterator<Item = &'a Region2>) -> Result<
|
|||||||
Ok((edges, points))
|
Ok((edges, points))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A flat region's cut-outs by their bounding boxes, to clip tools against only the ones near
|
||||||
|
/// them (the result is the same: a cut-out clear of a tool's box can't change what it meets).
|
||||||
|
pub(crate) struct NearHoles<'a> {
|
||||||
|
region: &'a Region2,
|
||||||
|
boxes: Vec<(Vec2, Vec2)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A loop's bounding box, a little larger than its chords so arcs stay inside.
|
||||||
|
fn loop_box(lp: &Loop2) -> Result<(Vec2, Vec2)> {
|
||||||
|
let pts = flat_polyline(lp)?;
|
||||||
|
let (lo, hi) = pts.iter().fold((Vec2::new(f64::MAX, f64::MAX), Vec2::new(f64::MIN, f64::MIN)), |(lo, hi), p| {
|
||||||
|
(Vec2::new(lo.x.min(p.x), lo.y.min(p.y)), Vec2::new(hi.x.max(p.x), hi.y.max(p.y)))
|
||||||
|
});
|
||||||
|
let pad = 1e-3 + 1e-9 * (lo.x.abs() + lo.y.abs() + hi.x.abs() + hi.y.abs());
|
||||||
|
Ok((lo - Vec2::new(pad, pad), hi + Vec2::new(pad, pad)))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NearHoles<'a> {
|
||||||
|
pub(crate) fn new(region: &'a Region2) -> Result<Self> {
|
||||||
|
Ok(Self { region, boxes: region.holes.iter().map(loop_box).collect::<Result<_>>()? })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The region with only the cut-outs whose boxes meet `tool`'s.
|
||||||
|
pub(crate) fn around(&self, tool: &Region2) -> Result<Region2> {
|
||||||
|
if self.region.holes.is_empty() {
|
||||||
|
return Ok(self.region.clone());
|
||||||
|
}
|
||||||
|
let (lo, hi) = loop_box(&tool.outer)?;
|
||||||
|
let holes = self
|
||||||
|
.region
|
||||||
|
.holes
|
||||||
|
.iter()
|
||||||
|
.zip(&self.boxes)
|
||||||
|
.filter(|(_, (a, b))| a.x <= hi.x && b.x >= lo.x && a.y <= hi.y && b.y >= lo.y)
|
||||||
|
.map(|(h, _)| h.clone())
|
||||||
|
.collect();
|
||||||
|
Ok(Region2 { outer: self.region.outer.clone(), holes })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn material_boolean(a: &[Region2], b: &[Region2], op: kernel::BoolOp, work: &mut FlatWork) -> Result<Vec<Region2>> {
|
pub(crate) fn material_boolean(a: &[Region2], b: &[Region2], op: kernel::BoolOp, work: &mut FlatWork) -> Result<Vec<Region2>> {
|
||||||
let (ae, ap) = region_complexity(a)?;
|
let (ae, ap) = region_complexity(a)?;
|
||||||
let (be, bp) = region_complexity(b)?;
|
let (be, bp) = region_complexity(b)?;
|
||||||
|
|||||||
@@ -3570,3 +3570,36 @@ fn every_flat_face_of_a_sheet_takes_an_attached_sketch() {
|
|||||||
let f = flat(&mut t);
|
let f = flat(&mut t);
|
||||||
assert_eq!(f["cutouts"].as_array().map(Vec::len), Some(1), "{f}");
|
assert_eq!(f["cutouts"].as_array().map(Vec::len), Some(1), "{f}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #732: a perforated panel. A 400 x 470 x 0.8 sheet with Ø1 holes on a 1.5 mm 60° staggered grid (DIN 24041
|
||||||
|
/// Rv 1.0-1.5), cut in two sketches of 500 holes; 0.8.0 refuses the second cut ("sheet metal flat geometry exceeds
|
||||||
|
/// the bounded processing budget"). A real mesh panel needs ~94 000 such holes; 1000 is the first step.
|
||||||
|
#[test]
|
||||||
|
fn perforated_sheet_takes_a_thousand_round_holes() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
outline_blank(&mut s, json!([[0, 0], [400, 0], [400, 470], [0, 470]]));
|
||||||
|
let flat = volume(&mut s);
|
||||||
|
let (p, dy) = (1.5_f64, 1.5_f64 * 3f64.sqrt() / 2.0);
|
||||||
|
let mut holes = Vec::new();
|
||||||
|
'grid: for r in 0..(450.0 / dy) as usize {
|
||||||
|
for k in 0..(380.0 / p) as usize {
|
||||||
|
let x = 10.0 + k as f64 * p + if r % 2 == 1 { p / 2.0 } else { 0.0 };
|
||||||
|
holes.push([x, 10.0 + r as f64 * dy]);
|
||||||
|
if holes.len() == 1000 {
|
||||||
|
break 'grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (g, chunk) in holes.chunks(500).enumerate() {
|
||||||
|
let name = format!("Perforation {g}");
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, 2], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": name}));
|
||||||
|
for c in chunk {
|
||||||
|
run(&mut s, "sketch.circle.center", json!({"center": c, "radius": 0.5}));
|
||||||
|
}
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
run(&mut s, "solid.extrude", json!({"sketch": name, "distance": "4", "direction": "negative", "operation": "cut"}));
|
||||||
|
}
|
||||||
|
let removed = flat - volume(&mut s);
|
||||||
|
let expected = 1000.0 * std::f64::consts::PI * 0.5 * 0.5 * 0.8;
|
||||||
|
assert!((removed - expected).abs() / expected < 0.05, "removed {removed:.1} mm³, 1000 holes are {expected:.1} mm³");
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -854,6 +854,14 @@ fn inside_loop(lp: &Loop2, p: Vec2) -> bool {
|
|||||||
// counts the touch and the other misses it. Move the ray off such points: a shift far below
|
// counts the touch and the other misses it. Move the ray off such points: a shift far below
|
||||||
// any modelling tolerance cannot change the answer for a point that is not on the loop.
|
// any modelling tolerance cannot change the answer for a point that is not on the loop.
|
||||||
let scale = 1.0 + p.x.abs().max(p.y.abs());
|
let scale = 1.0 + p.x.abs().max(p.y.abs());
|
||||||
|
// A point clear of the loop's box (by more than the ray shifts below) is outside it: skip
|
||||||
|
// the costly tests, which a region with a thousand cut-outs runs for every probe (#732).
|
||||||
|
if let Some((lo, hi)) = loop_box(lp) {
|
||||||
|
let m = scale * 1e-6;
|
||||||
|
if p.x < lo.x - m || p.x > hi.x + m || p.y < lo.y - m || p.y > hi.y + m {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
// A point on the loop keeps the half-open answer.
|
// A point on the loop keeps the half-open answer.
|
||||||
if lp.segs.iter().any(|s| s.point_at(s.closest_param(p)).dist(p) < scale * 1e-9) {
|
if lp.segs.iter().any(|s| s.point_at(s.closest_param(p)).dist(p) < scale * 1e-9) {
|
||||||
return inside_loop_ray(lp, p);
|
return inside_loop_ray(lp, p);
|
||||||
@@ -872,6 +880,24 @@ fn inside_loop(lp: &Loop2, p: Vec2) -> bool {
|
|||||||
inside_loop_ray(lp, ray)
|
inside_loop_ray(lp, ray)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A box holding a loop of lines and arcs (an arc's whole circle); None for other curves.
|
||||||
|
fn loop_box(lp: &Loop2) -> Option<(Vec2, Vec2)> {
|
||||||
|
let mut lo = Vec2::new(f64::MAX, f64::MAX);
|
||||||
|
let mut hi = Vec2::new(f64::MIN, f64::MIN);
|
||||||
|
let mut add = |a: Vec2, b: Vec2| {
|
||||||
|
lo = Vec2::new(lo.x.min(a.x), lo.y.min(a.y));
|
||||||
|
hi = Vec2::new(hi.x.max(b.x), hi.y.max(b.y));
|
||||||
|
};
|
||||||
|
for seg in &lp.segs {
|
||||||
|
match *seg {
|
||||||
|
Seg2::Line { a, b } => add(Vec2::new(a.x.min(b.x), a.y.min(b.y)), Vec2::new(a.x.max(b.x), a.y.max(b.y))),
|
||||||
|
Seg2::Arc { center, radius, .. } => add(center - Vec2::new(radius.abs(), radius.abs()), center + Vec2::new(radius.abs(), radius.abs())),
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(lo.x <= hi.x && lo.y <= hi.y && [lo.x, lo.y, hi.x, hi.y].iter().all(|v| v.is_finite())).then_some((lo, hi))
|
||||||
|
}
|
||||||
|
|
||||||
/// [`inside_loop`] with the ray along +x from `p` taken as given.
|
/// [`inside_loop`] with the ray along +x from `p` taken as given.
|
||||||
fn inside_loop_ray(lp: &Loop2, p: Vec2) -> bool {
|
fn inside_loop_ray(lp: &Loop2, p: Vec2) -> bool {
|
||||||
let mut odd = false;
|
let mut odd = false;
|
||||||
|
|||||||
Reference in new issue
Block a user