Sheet metal: machine a hole feature's drills in one pass, keep cuts after a text cut (#1979) (#1980)
Thanks @NOWN-DEV.
This commit is contained in:
1 parent
c4d2ae87ee
commit
09b3bd0e01
5 files changed
+354
-10
No files matched your search
@@ -2660,6 +2660,23 @@ fn sheet_hole_cut(
|
||||
let mut handled: Vec<String> = Vec::new();
|
||||
// Noted only when every tool was machined here.
|
||||
let mut parted = None;
|
||||
// Many plain drills along one axis (a hole on each of a pattern's points): all in one pass.
|
||||
let prisms: Option<Vec<more::PrimitivePrism>> = tools.iter().map(|(_, prism)| *prism).collect();
|
||||
if let Some(prisms) = prisms {
|
||||
match more::sheet_drill_cuts(f, st, &prisms, &mut parted) {
|
||||
Ok(Some(done)) => {
|
||||
if let Some(note) = parted {
|
||||
append_warning(warning, note);
|
||||
}
|
||||
return Ok(done);
|
||||
}
|
||||
// Not all of them that way, or refused at once: one by one, from the model as it was.
|
||||
Ok(None) | Err(_) => {
|
||||
(st.sheets, st.bodies) = saved.clone();
|
||||
parted = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (tool, prism) in tools {
|
||||
let tool = &tool.body;
|
||||
let done = match prism {
|
||||
|
||||
+114
-9
@@ -5,7 +5,7 @@ use super::*;
|
||||
use crate::EmbossFace;
|
||||
use crate::apply_point;
|
||||
use crate::sheet;
|
||||
use solidcraft_geom::{Loop2, Seg2};
|
||||
use solidcraft_geom::{Aabb3, Loop2, Seg2};
|
||||
|
||||
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
|
||||
match &f.kind {
|
||||
@@ -833,11 +833,20 @@ pub(super) fn sheet_primitive_cut(
|
||||
if !matches!(operation, Operation::Cut | Operation::Intersect) || !any_eligible_sheet(f, st, &[]) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
// The first sheet the primitive meets decides the machining direction.
|
||||
// The first sheet the primitive meets decides the machining direction. Sheets clear of it
|
||||
// by their boxes are passed over; the one sheet left needs no boolean to show it is met (that
|
||||
// boolean came out empty against a sheet carrying a label's cut-outs, and the hole was then
|
||||
// cut into the solid without a place in the flat pattern).
|
||||
let tool_box = body_box(tool);
|
||||
let near: Vec<&sheet::SheetBody> = st
|
||||
.sheets
|
||||
.iter()
|
||||
.filter(|sh| sheet_eligible(f, st, &[], sh) && st.body(&sh.body).is_some_and(|mb| !boxes_apart(&mesh_box(mb), &tool_box)))
|
||||
.collect();
|
||||
let mut normal = None;
|
||||
for sh in st.sheets.iter().filter(|sh| sheet_eligible(f, st, &[], sh)) {
|
||||
for sh in &near {
|
||||
if let Some(mb) = st.body(&sh.body)
|
||||
&& kernel::boolean(&mb.body, tool, kernel::BoolOp::Intersect)?.is_some()
|
||||
&& (near.len() == 1 || kernel::boolean(&mb.body, tool, kernel::BoolOp::Intersect)?.is_some())
|
||||
{
|
||||
normal = Some(match prism {
|
||||
PrimitivePrism::Cylinder { axis, .. }
|
||||
@@ -895,6 +904,70 @@ pub(super) fn sheet_bore_cut(f: &Feature, st: &mut ModelState, prism: PrimitiveP
|
||||
Ok(Some(handled))
|
||||
}
|
||||
|
||||
/// The most hole tools machined in one pass by [`sheet_drill_cuts`].
|
||||
const MAX_DRILLS_AT_ONCE: usize = 1000;
|
||||
|
||||
/// A hole feature's straight drills (`prisms`, all along one axis), machined into the sheets in
|
||||
/// one pass for each depth they share: one rebuild of the sheet and of its flat pattern for all
|
||||
/// of them, rather than one per drill, each on a sheet machined by the ones before.
|
||||
/// The drills are the cut-outs and pockets [`sheet_primitive_cut`] makes of them one at a time.
|
||||
/// None when they can't all be taken this way (they are not all cylinders along one axis that
|
||||
/// every sheet they may machine takes square, or a sheet keeps one some other way): the caller
|
||||
/// then puts the model back and drills them one by one.
|
||||
pub(super) fn sheet_drill_cuts(
|
||||
f: &Feature,
|
||||
st: &mut ModelState,
|
||||
prisms: &[PrimitivePrism],
|
||||
warning: &mut Option<String>,
|
||||
) -> Result<Option<Vec<String>>> {
|
||||
if prisms.len() < 2 || prisms.len() > MAX_DRILLS_AT_ONCE || !any_eligible_sheet(f, st, &[]) {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(PrimitivePrism::Cylinder { base: base0, axis: axis0, .. }) = prisms.first().copied() else { return Ok(None) };
|
||||
let Some(axis) = axis0.normalized() else { return Ok(None) };
|
||||
let Some(plane) = Plane::from_normal(base0, axis) else { return Ok(None) };
|
||||
// Every sheet the drills may machine takes them square: along its normal, or (folded) square
|
||||
// to one of its panels, as a single drill would be taken.
|
||||
for sh in st.sheets.iter().filter(|sh| sheet_eligible(f, st, &[], sh)) {
|
||||
let square = sh.frame.normal().cross(axis).len() <= 1e-12 || (!sh.flat && !sh.parallel_panels(&plane).is_empty());
|
||||
if !square {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
// Each drill in the one plane: its circle, and its depth interval along the axis.
|
||||
let mut groups: Vec<((f64, f64), Vec<Region2>)> = Vec::new();
|
||||
for prism in prisms {
|
||||
let PrimitivePrism::Cylinder { base, axis: a, radius, height } = *prism else { return Ok(None) };
|
||||
let Some(a) = a.normalized() else { return Ok(None) };
|
||||
if a.cross(axis).len() > 1e-12 || ![base.x, base.y, base.z, radius, height].iter().all(|v| v.is_finite()) || !(radius > 0.0) {
|
||||
return Ok(None);
|
||||
}
|
||||
let (sense, at) = (a.dot(axis).signum(), (base - base0).dot(axis));
|
||||
let (e0, e1) = (at + sense * height.min(0.0), at + sense * height.max(0.0));
|
||||
let interval = (e0.min(e1), e0.max(e1));
|
||||
let circle = Region2 { outer: Loop2::circle(plane.to_local(base), radius), holes: vec![] };
|
||||
let same = |(lo, hi): (f64, f64)| (lo - interval.0).abs() <= 1e-9 * (1.0 + lo.abs()) && (hi - interval.1).abs() <= 1e-9 * (1.0 + hi.abs());
|
||||
match groups.iter_mut().find(|(i, _)| same(*i)) {
|
||||
Some((_, regions)) => regions.push(circle),
|
||||
None => groups.push((interval, vec![circle])),
|
||||
}
|
||||
}
|
||||
let mut handled: Vec<String> = Vec::new();
|
||||
for ((lo, hi), regions) in groups {
|
||||
let tools = kernel::extrude(&plane, ®ions, lo, hi)?;
|
||||
let done = sheet_prism_cut(f, st, &plane, ®ions, Operation::Cut, &tools, &[], None, false, &[], warning)?;
|
||||
if done.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
for name in done {
|
||||
if !handled.contains(&name) {
|
||||
handled.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(handled))
|
||||
}
|
||||
|
||||
/// A primitive tool's exact geometry, for [`sheet_primitive_cut`].
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) enum PrimitivePrism {
|
||||
@@ -1003,6 +1076,9 @@ fn sheet_prism_cut(
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
// Boxes around the tools: a tool clear of a bend by its box needs no boolean to show it (a
|
||||
// label's letters against every bend and flange of a tray).
|
||||
let tool_boxes: Vec<Option<Aabb3>> = tools.iter().map(body_box).collect();
|
||||
let mut plans = Vec::new();
|
||||
let mut notes = Vec::new();
|
||||
'sheets: for original in &st.sheets {
|
||||
@@ -1018,12 +1094,16 @@ fn sheet_prism_cut(
|
||||
plans.push((sh.body.clone(), Some((sh, mb.body.clone()))));
|
||||
continue;
|
||||
}
|
||||
if !sh.flat && !sh.flanges.is_empty() && !super::sheet_boolean_changes(&mb.body, tools, operation)? {
|
||||
plans.push((sh.body.clone(), Some((sh, mb.body.clone()))));
|
||||
continue;
|
||||
}
|
||||
// No 3D boolean of each tool with the whole folded sheet first: the flat machining below
|
||||
// is exact, and a cut that machines nothing there is checked against the solid after it.
|
||||
// (That boolean cost each label hundreds of milliseconds, and against a sheet already
|
||||
// carrying a label's cut-outs it came out empty: the next cut was dropped.)
|
||||
for bends in sh.unmodeled_solids(plane)? {
|
||||
for tool in tools {
|
||||
let bends_box = body_box(&bends);
|
||||
for (tool, tool_box) in tools.iter().zip(&tool_boxes) {
|
||||
if boxes_apart(&bends_box, tool_box) {
|
||||
continue;
|
||||
}
|
||||
if kernel::boolean(&bends, tool, kernel::BoolOp::Intersect)?.is_some() {
|
||||
return Err(DocError::Invalid(
|
||||
"this machining intersects a folded bend or a nonparallel panel; unfold the sheet before machining that material".into(),
|
||||
@@ -1190,6 +1270,31 @@ fn sheet_prism_cut(
|
||||
Ok(handled)
|
||||
}
|
||||
|
||||
/// A box around `body`, grown past its mesh's deflection from the exact faces (and a hair
|
||||
/// more): bodies whose boxes are apart do not meet. None when it can't be worked out.
|
||||
fn body_box(body: &Body) -> Option<Aabb3> {
|
||||
let tol = (body.size() * 1e-3).max(1e-4);
|
||||
padded_box(&body.tessellate(tol).ok()?, tol)
|
||||
}
|
||||
|
||||
/// [`body_box`] of a model body, from the mesh it keeps.
|
||||
fn mesh_box(mb: &ModelBody) -> Option<Aabb3> {
|
||||
padded_box(&mb.mesh(), mb.display_tol())
|
||||
}
|
||||
|
||||
fn padded_box(mesh: &solidcraft_geom::Mesh, tol: f64) -> Option<Aabb3> {
|
||||
let b = mesh.bounds();
|
||||
let pad = 2.0 * tol + 1e-6;
|
||||
let pad = Vec3::new(pad, pad, pad);
|
||||
(!b.is_empty() && b.min.is_finite() && b.max.is_finite()).then(|| Aabb3 { min: b.min - pad, max: b.max + pad })
|
||||
}
|
||||
|
||||
/// Whether two boxes are known to be apart (an unknown box is never apart).
|
||||
fn boxes_apart(a: &Option<Aabb3>, b: &Option<Aabb3>) -> bool {
|
||||
let (Some(a), Some(b)) = (a, b) else { return false };
|
||||
a.max.x < b.min.x || b.max.x < a.min.x || a.max.y < b.min.y || b.max.y < a.min.y || a.max.z < b.min.z || b.max.z < a.min.z
|
||||
}
|
||||
|
||||
/// A cut's note on the pieces it parted from the base of `sh` (see
|
||||
/// [`sheet::SheetBody::parted_from_base`]; `material` is the sheet's flat material after the cut,
|
||||
/// `before` the sheet before it): how many, their area and where they lie in the flat pattern.
|
||||
|
||||
+15
-1
@@ -1139,11 +1139,22 @@ impl SheetBody {
|
||||
cuts.push(fitted_region(&pocket)?);
|
||||
}
|
||||
let cuts = material_union(cuts, work)?;
|
||||
let cut_boxes = cuts.iter().map(|c| loop_box(&c.outer)).collect::<Result<Vec<_>>>()?;
|
||||
let mut out = Vec::new();
|
||||
let mut points = 0usize;
|
||||
for (piece, region) in pieces {
|
||||
let panel = fitted_region(&planar_region(®ion)?)?;
|
||||
let material = if cuts.is_empty() { vec![panel] } else { material_boolean(&[panel], &cuts, kernel::BoolOp::Cut, work)? };
|
||||
// Each piece meets only the cut-outs near it: arranging every cut-out of the sheet (a
|
||||
// label's letters on its base) against each flange and bend zone too would charge the
|
||||
// budget for all of them once per piece.
|
||||
let (lo, hi) = loop_box(&panel.outer)?;
|
||||
let near: Vec<Region2> = cuts
|
||||
.iter()
|
||||
.zip(&cut_boxes)
|
||||
.filter(|(_, (a, b))| a.x <= hi.x && b.x >= lo.x && a.y <= hi.y && b.y >= lo.y)
|
||||
.map(|(c, _)| c.clone())
|
||||
.collect();
|
||||
let material = if near.is_empty() { vec![panel] } else { material_boolean(&[panel], &near, kernel::BoolOp::Cut, work)? };
|
||||
for region in material {
|
||||
points = points.saturating_add(region_complexity(std::slice::from_ref(®ion))?.1);
|
||||
if points > MAX_FLAT_POINTS || out.len() >= MAX_FLAT_PIECES {
|
||||
@@ -1545,6 +1556,9 @@ impl SheetBody {
|
||||
}
|
||||
|
||||
fn machine_pockets(&self, mut body: Body, work: &mut FlatWork) -> Result<Body> {
|
||||
if self.pockets.is_empty() {
|
||||
return Ok(body);
|
||||
}
|
||||
if self.pockets.len() > 1000 {
|
||||
return Err(flat_budget_error());
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user