Sheet metal: a cut names the pieces it parts from the base (#1338)
A sheet-metal cut that parts pieces from the base names them, and the guide explains it.
This commit is contained in:
1 parent
6fa9e0d2dd
commit
e95e32d72a
5 files changed
+341
-15
No files matched your search
+17
-7
@@ -2276,17 +2276,24 @@ fn hole_feature_tools(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Featu
|
||||
/// counterbore) is machined like the equivalent extrude cut, so the sheet keeps it as a cut-out
|
||||
/// or pocket of its flat pattern (#560). Returns the sheet bodies it machined; none, with the
|
||||
/// model as it was, when a tool that meets a sheet can't be kept that way (the ordinary cut then
|
||||
/// refuses it).
|
||||
fn sheet_hole_cut(st: &mut ModelState, f: &Feature, tools: &[(Traced, Option<more::PrimitivePrism>)]) -> Result<Vec<String>> {
|
||||
/// refuses it). Pieces the hole parts from a sheet's base are named in `warning`.
|
||||
fn sheet_hole_cut(
|
||||
st: &mut ModelState,
|
||||
f: &Feature,
|
||||
tools: &[(Traced, Option<more::PrimitivePrism>)],
|
||||
warning: &mut Option<String>,
|
||||
) -> Result<Vec<String>> {
|
||||
if st.sheets.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let saved = (st.sheets.clone(), st.bodies.clone());
|
||||
let mut handled: Vec<String> = Vec::new();
|
||||
// Noted only when every tool was machined here.
|
||||
let mut parted = None;
|
||||
for (tool, prism) in tools {
|
||||
let tool = &tool.body;
|
||||
let done = match prism {
|
||||
Some(prism) => match more::sheet_primitive_cut(f, st, tool, *prism, Operation::Cut, true) {
|
||||
Some(prism) => match more::sheet_primitive_cut(f, st, tool, *prism, Operation::Cut, true, &mut parted) {
|
||||
Ok(done) => done,
|
||||
// Drilled along a panel (into a rim) the hole is no flat cut-out: a bore of
|
||||
// that panel, when it stays inside it.
|
||||
@@ -2322,6 +2329,9 @@ fn sheet_hole_cut(st: &mut ModelState, f: &Feature, tools: &[(Traced, Option<mor
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(note) = parted {
|
||||
append_warning(warning, note);
|
||||
}
|
||||
Ok(handled)
|
||||
}
|
||||
|
||||
@@ -4045,7 +4055,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
// Cuts through sheet metal become cut-outs of its flat pattern.
|
||||
if matches!(operation, Operation::Cut | Operation::Intersect) && matches!(f.kind, FeatureKind::Extrude { .. }) && !st.sheets.is_empty() {
|
||||
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
|
||||
let handled = more::sheet_cut(f, st, &bodies, targets, constructor.as_ref())?;
|
||||
let handled = more::sheet_cut(f, st, &bodies, targets, constructor.as_ref(), warning)?;
|
||||
if !handled.is_empty() {
|
||||
return apply_beside_sheet_cut(st, f, tools, *operation, targets, &handled);
|
||||
}
|
||||
@@ -4055,7 +4065,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
FeatureKind::Hole { thread, diameter, .. } => {
|
||||
let (spots, dir) = hole_spots(st, f)?;
|
||||
let tools = hole_feature_tools(vals, st, f)?;
|
||||
let handled = sheet_hole_cut(st, f, &tools)?;
|
||||
let handled = sheet_hole_cut(st, f, &tools, warning)?;
|
||||
let tools: Vec<Traced> = tools.into_iter().map(|(tool, _)| tool).collect();
|
||||
if handled.is_empty() {
|
||||
apply_traced(st, f, tools, Operation::Cut, &[])?;
|
||||
@@ -4445,7 +4455,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let corner = point(vals, corner)?;
|
||||
let b = kernel::box_solid(corner, corner + s)?;
|
||||
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_box_certificate(f.id)? } else { b };
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, more::PrimitivePrism::Box { lo: corner, hi: corner + s }, *operation, false)?;
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, more::PrimitivePrism::Box { lo: corner, hi: corner + s }, *operation, false, warning)?;
|
||||
let tools = vec![crate::lineage::box_tool(b)];
|
||||
if !handled.is_empty() {
|
||||
return apply_beside_sheet_cut(st, f, tools, *operation, &[], &handled);
|
||||
@@ -4457,7 +4467,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
let b = kernel::cylinder(base, *axis, radius, height)?;
|
||||
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_cylinder_certificate(f.id)? } else { b };
|
||||
let prism = more::PrimitivePrism::Cylinder { base, axis: *axis, radius, height };
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, prism, *operation, false)?;
|
||||
let handled = more::sheet_primitive_cut(f, st, &b, prism, *operation, false, warning)?;
|
||||
let tools = vec![crate::lineage::cylinder_tool(b)];
|
||||
if !handled.is_empty() {
|
||||
return apply_beside_sheet_cut(st, f, tools, *operation, &[], &handled);
|
||||
|
||||
@@ -764,6 +764,7 @@ pub(super) fn sheet_cut(
|
||||
tools: &[Body],
|
||||
targets: &[String],
|
||||
constructor: Option<&ExtrudeConstructor>,
|
||||
warning: &mut Option<String>,
|
||||
) -> Result<Vec<String>> {
|
||||
let FeatureKind::Extrude { sketch, profiles, operation, extent, .. } = &f.kind else { return Ok(Vec::new()) };
|
||||
if extent.taper.is_some() || !any_eligible_sheet(f, st, targets) {
|
||||
@@ -780,7 +781,7 @@ pub(super) fn sheet_cut(
|
||||
.flat_map(|l| ss.sketch.link_curves(&l.id))
|
||||
.flat_map(|ci| ss.sketch.exact_segs(ci).unwrap_or_else(|| ss.sketch.segs(ci)))
|
||||
.collect();
|
||||
sheet_prism_cut(f, st, &ss.plane, ®ions, *operation, tools, targets, constructor, extent.to.is_some(), &text)
|
||||
sheet_prism_cut(f, st, &ss.plane, ®ions, *operation, tools, targets, constructor, extent.to.is_some(), &text, warning)
|
||||
}
|
||||
|
||||
/// Whether a cut of `f` with these targets (else its participants, else its component's
|
||||
@@ -812,6 +813,7 @@ pub(super) fn sheet_primitive_cut(
|
||||
prism: PrimitivePrism,
|
||||
operation: Operation,
|
||||
drill: bool,
|
||||
warning: &mut Option<String>,
|
||||
) -> Result<Vec<String>> {
|
||||
if !matches!(operation, Operation::Cut | Operation::Intersect) || !any_eligible_sheet(f, st, &[]) {
|
||||
return Ok(Vec::new());
|
||||
@@ -836,7 +838,7 @@ pub(super) fn sheet_primitive_cut(
|
||||
let Some(normal) = normal else { return Ok(Vec::new()) };
|
||||
let Some((plane, region, interval)) = prism.along(normal) else { return Ok(Vec::new()) };
|
||||
let tools = kernel::extrude(&plane, std::slice::from_ref(®ion), interval.0, interval.1)?;
|
||||
sheet_prism_cut(f, st, &plane, std::slice::from_ref(®ion), operation, &tools, &[], None, false, &[])
|
||||
sheet_prism_cut(f, st, &plane, std::slice::from_ref(®ion), operation, &tools, &[], None, false, &[], warning)
|
||||
}
|
||||
|
||||
/// A hole's straight drill along a panel of a sheet (into its rim), kept as a bore of each
|
||||
@@ -941,6 +943,7 @@ fn sheet_prism_cut(
|
||||
constructor: Option<&ExtrudeConstructor>,
|
||||
footprints_bound: bool,
|
||||
text: &[Seg2],
|
||||
warning: &mut Option<String>,
|
||||
) -> Result<Vec<String>> {
|
||||
let n = plane.normal();
|
||||
let mut work = sheet::FlatWork::new();
|
||||
@@ -986,6 +989,7 @@ fn sheet_prism_cut(
|
||||
Vec::new()
|
||||
};
|
||||
let mut plans = Vec::new();
|
||||
let mut notes = Vec::new();
|
||||
'sheets: for original in &st.sheets {
|
||||
if !sheet_eligible(f, st, targets, original) {
|
||||
continue;
|
||||
@@ -1133,16 +1137,24 @@ fn sheet_prism_cut(
|
||||
if !modeled {
|
||||
super::sheet_boolean_unchanged(&mb.body, tools, operation)?;
|
||||
plans.push((sh.body.clone(), Some((sh, mb.body.clone()))));
|
||||
} else if sh.flat_material()?.is_empty() {
|
||||
// Empty exact material removes geometry and metadata together. Budget or
|
||||
// topology errors propagate; they are never mistaken for an empty blank.
|
||||
plans.push((sh.body.clone(), None));
|
||||
} else {
|
||||
let body = sh.solid()?;
|
||||
plans.push((sh.body.clone(), Some((sh, body))));
|
||||
let material = sh.flat_material()?;
|
||||
if material.is_empty() {
|
||||
// Empty exact material removes geometry and metadata together. Budget or
|
||||
// topology errors propagate; they are never mistaken for an empty blank.
|
||||
plans.push((sh.body.clone(), None));
|
||||
} else {
|
||||
// Every piece the cut leaves stays; the ones it parts from the base are named.
|
||||
notes.extend(parted_note(&sh, &material, original));
|
||||
let body = sh.solid()?;
|
||||
plans.push((sh.body.clone(), Some((sh, body))));
|
||||
}
|
||||
}
|
||||
}
|
||||
let handled = plans.iter().map(|(name, _)| name.clone()).collect();
|
||||
if !notes.is_empty() {
|
||||
append_warning(warning, notes.join("; "));
|
||||
}
|
||||
for (name, result) in plans {
|
||||
match result {
|
||||
Some((sh, body)) => {
|
||||
@@ -1163,6 +1175,35 @@ fn sheet_prism_cut(
|
||||
Ok(handled)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// The pieces stay, as every piece a cut leaves does. None when nothing is parted, and when the
|
||||
/// pieces can't be worked out: the note is no reason for a cut to fail.
|
||||
fn parted_note(sh: &sheet::SheetBody, material: &[Region2], before: &sheet::SheetBody) -> Option<String> {
|
||||
let pieces = sh.parted_from_base(material, before).ok().filter(|p| !p.is_empty())?;
|
||||
let area: f64 = pieces.iter().map(Region2::area).sum();
|
||||
let at: Vec<String> = pieces
|
||||
.iter()
|
||||
.take(4)
|
||||
.map(|r| {
|
||||
let c = r.interior_point();
|
||||
format!("({:.1}, {:.1})", c.x, c.y)
|
||||
})
|
||||
.collect();
|
||||
let at = at.join(", ");
|
||||
let body = &sh.body;
|
||||
Some(match pieces.len() {
|
||||
1 => format!("1 piece of `{body}` is no longer joined to its base ({area:.2} mm², near {at} in the flat pattern); it is kept"),
|
||||
n => {
|
||||
let more = if n > 4 { format!(" and {} more", n - 4) } else { String::new() };
|
||||
format!(
|
||||
"{n} pieces of `{body}` are no longer joined to its base ({area:.2} mm² in all, near {at}{more} in the flat pattern); they are kept"
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Outline loops (in `plane` coordinates) of the mesh triangles lying in the plane at distance
|
||||
/// `level` from `at` along `n`: the edges used by one of them only, chained.
|
||||
fn face_loops(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<Vec2>> {
|
||||
|
||||
@@ -31,6 +31,8 @@ const MAX_FLAT_POINTS: usize = 16_384;
|
||||
const MAX_FLAT_SAMPLES: usize = 65_536;
|
||||
const MAX_FLAT_PIECES: usize = 4096;
|
||||
const MAX_FLAT_WORK: usize = 500_000_000;
|
||||
/// Pieces of flat material sharing less area than this (mm²) only touch.
|
||||
const TOUCH_AREA: f64 = 1e-6;
|
||||
|
||||
/// Starts the warning a fold gives when its `fixed` point touches no material: a recompute keeps
|
||||
/// the side the point lies on, and the `sheet.fold` command refuses such a point (#263).
|
||||
@@ -1696,6 +1698,84 @@ impl SheetBody {
|
||||
let regions = if cuts.is_empty() { regions } else { material_boolean(®ions, &cuts, kernel::BoolOp::Cut, &mut work)? };
|
||||
Ok(regions)
|
||||
}
|
||||
|
||||
/// The pieces of flat material that a cut parted from the base: `after` is this sheet's
|
||||
/// [`Self::flat_material`] after the cut, `before` the sheet before it. A piece is parted when
|
||||
/// it holds none of the base panel, lies in a piece that held some before the cut, and no
|
||||
/// other piece surrounds it: a flange cut off along its bend (with any flange on it), the
|
||||
/// part of a ring's inside that lies on a flange's edge, the tip of a flange beyond a cut
|
||||
/// through a closed corner. A ring's island and a letter's counter, inside other material,
|
||||
/// are not counted; nor are pieces an earlier cut parted, nor anything once no piece holds
|
||||
/// the base.
|
||||
pub(crate) fn parted_from_base(&self, after: &[Region2], before: &SheetBody) -> Result<Vec<Region2>> {
|
||||
if after.len() < 2 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut work = FlatWork::new();
|
||||
work.charge(after.len().saturating_mul(after.len()))?;
|
||||
let base = [planar_region(&self.base)?];
|
||||
let holds_base = |piece: &Region2, work: &mut FlatWork| -> Result<bool> {
|
||||
let common = material_boolean(std::slice::from_ref(piece), &base, kernel::BoolOp::Intersect, work)?;
|
||||
Ok(common.iter().map(Region2::area).sum::<f64>() > TOUCH_AREA)
|
||||
};
|
||||
// The cheap test first: a piece inside another's outline is an island, whatever it holds.
|
||||
let outlines: Vec<(Vec<Vec2>, Vec2, Vec2)> = after
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let pts = r.outer.polyline(1e-3);
|
||||
let lo = pts.iter().fold(Vec2::new(f64::MAX, f64::MAX), |a, p| Vec2::new(a.x.min(p.x), a.y.min(p.y)));
|
||||
let hi = pts.iter().fold(Vec2::new(f64::MIN, f64::MIN), |a, p| Vec2::new(a.x.max(p.x), a.y.max(p.y)));
|
||||
(pts, lo, hi)
|
||||
})
|
||||
.collect();
|
||||
let (mut open, mut islands) = (Vec::new(), Vec::new());
|
||||
for (i, piece) in after.iter().enumerate() {
|
||||
let p = piece.interior_point();
|
||||
let inside = |(pts, lo, hi): &(Vec<Vec2>, Vec2, Vec2)| (lo.x..=hi.x).contains(&p.x) && (lo.y..=hi.y).contains(&p.y) && point_in(pts, p);
|
||||
if outlines.iter().enumerate().any(|(j, o)| j != i && inside(o)) {
|
||||
islands.push(piece);
|
||||
} else {
|
||||
open.push(piece);
|
||||
}
|
||||
}
|
||||
let (mut joined, mut loose) = (false, Vec::new());
|
||||
for piece in open {
|
||||
if holds_base(piece, &mut work)? {
|
||||
joined = true;
|
||||
} else {
|
||||
loose.push(piece);
|
||||
}
|
||||
}
|
||||
if loose.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
// The base may be an island itself (a cut all round it).
|
||||
for piece in islands {
|
||||
if joined {
|
||||
break;
|
||||
}
|
||||
joined = holds_base(piece, &mut work)?;
|
||||
}
|
||||
if !joined {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
// A cut only takes material away: each piece lies in the piece it was part of.
|
||||
let mut held = Vec::new();
|
||||
for piece in before.flat_material()? {
|
||||
if holds_base(&piece, &mut work)? {
|
||||
held.push(piece);
|
||||
}
|
||||
}
|
||||
Ok(loose
|
||||
.into_iter()
|
||||
.filter(|piece| {
|
||||
let p = piece.interior_point();
|
||||
held.iter().any(|h| h.contains(p))
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Outline loops of the flat pattern (flat coordinates): the pieces' edges, shared ones left out.
|
||||
pub fn outline(&self) -> Result<Vec<Vec<Vec2>>> {
|
||||
if self.flanges.len().saturating_mul(2).saturating_add(1) > MAX_FLAT_PIECES {
|
||||
@@ -7389,4 +7469,49 @@ mod tests {
|
||||
holds_the_flat_material(&s, &what);
|
||||
}
|
||||
}
|
||||
|
||||
/// The 100 x 60 plate with a 20 mm flange on its y = 0 edge (2.5 mm, R 2.5, K 0.44): in the
|
||||
/// flat the base starts at y = t + R = 5, the bend's zone is (π/2)·(R + K·t) long and the
|
||||
/// flange's panel 20 − 5, so its far edge lies at y = 5 − (π/2)·3.6 − 15.
|
||||
#[test]
|
||||
fn a_cut_names_the_pieces_it_parts_from_the_base_and_no_others() {
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
let far = 5.0 - FRAC_PI_2 * (2.5 + 0.44 * 2.5) - 15.0;
|
||||
let mut s = plate(100.0, 60.0);
|
||||
s.add_flange(Vec3::new(50.0, 0.0, 2.5), 20.0, FRAC_PI_2, 2.5, "inside", false).unwrap();
|
||||
let r = |x0: f64, y0: f64, x1: f64, y1: f64| rect(Vec2::new(x0, y0), Vec2::new(x1, y0), Vec2::new(x1, y1), Vec2::new(x0, y1));
|
||||
let ring = |c: Vec2, ro: f64, ri: f64| Region2 { outer: Loop2::circle(c, ro), holes: vec![Loop2::circle(c, ri)] };
|
||||
let parted = |before: &SheetBody, holes: &[Region2]| {
|
||||
let mut after = before.clone();
|
||||
after.holes.extend_from_slice(holes);
|
||||
after.parted_from_base(&after.flat_material().unwrap(), before).unwrap()
|
||||
};
|
||||
// A slot across the flange beside its bend parts the rest of the flange: 100 × (−3 − far).
|
||||
let slot = r(-5.0, -3.0, 105.0, -1.0);
|
||||
let p = parted(&s, std::slice::from_ref(&slot));
|
||||
assert!(p.len() == 1 && (p[0].area() - 100.0 * (-3.0 - far)).abs() < 1e-6, "{p:?}");
|
||||
// So does a ring on the flange's far edge: the half of its inside on the flange, π·2²/2.
|
||||
let p = parted(&s, &[ring(Vec2::new(50.0, far), 4.0, 2.0)]);
|
||||
assert!(p.len() == 1 && (p[0].area() - 2.0 * PI).abs() < 1e-6, "{p:?}");
|
||||
// A ring's island, on the base or on the flange, has material round it; a strip across the
|
||||
// whole sheet leaves two halves that each keep their part of the base. None is named.
|
||||
for holes in [ring(Vec2::new(50.0, 30.0), 10.0, 5.0), ring(Vec2::new(50.0, -9.0), 4.0, 2.0), r(45.0, -30.0, 55.0, 70.0)] {
|
||||
assert!(parted(&s, std::slice::from_ref(&holes)).is_empty(), "{holes:?}");
|
||||
}
|
||||
// Pieces an earlier cut parted are not named again, nor when a later cut splits them.
|
||||
let mut cut = s.clone();
|
||||
cut.holes.push(slot);
|
||||
for holes in [Region2 { outer: Loop2::circle(Vec2::new(30.0, 30.0), 4.0), holes: vec![] }, r(45.0, -30.0, 55.0, -2.0)] {
|
||||
assert!(parted(&cut, std::slice::from_ref(&holes)).is_empty(), "{holes:?}");
|
||||
}
|
||||
// Nothing is named once no piece holds the base: a cut that takes the base and both bends
|
||||
// of a sheet with flanges on two sides leaves the two flanges.
|
||||
let mut two = plate(100.0, 60.0);
|
||||
two.add_flanges(&[Vec3::new(50.0, 0.0, 2.5), Vec3::new(50.0, 60.0, 2.5)], 20.0, FRAC_PI_2, 2.5, "inside", false).unwrap();
|
||||
let mut after = two.clone();
|
||||
after.holes.push(r(-5.0, -3.0, 105.0, 63.0));
|
||||
let material = after.flat_material().unwrap();
|
||||
assert_eq!(material.len(), 2);
|
||||
assert!(after.parted_from_base(&material, &two).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user