Flat-pattern DXF for a laser shop: layers by role, dashed bend lines, bend notes, a file per body (#1324)
Closes #1284. sheet.export_dxf writes layers by role (OUTLINE, CUT, TEXT or ETCH, dashed BEND_UP/BEND_DOWN, optional BEND_NOTES, POCKET_<side>_Z…_MM and PERFORATION), inner contours before the outline; text: cut|etch, bend_notes and note_height, per_body files. Letters are derived from the timeline, nothing new is saved (format unchanged). I merged main in (the export's parameter list keeps main's pocket layer names). Checked on main + this branch: cargo check --workspace, the engine cmd::sheet tests (160) and the io DXF tests.
This commit is contained in:
1 parent
9fbbf8dc3a
commit
d7a4a29ce3
8 files changed
+887
-58
No files matched your search
@@ -768,7 +768,16 @@ pub(super) fn sheet_cut(
|
||||
}
|
||||
let Some(ss) = st.sketch(*sketch).cloned() else { return Ok(Vec::new()) };
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
sheet_prism_cut(f, st, &ss.plane, ®ions, *operation, tools, targets, constructor, extent.to.is_some())
|
||||
// The outlines of the sketch's text: the sheet remembers where its letters went.
|
||||
let text: Vec<Seg2> = ss
|
||||
.sketch
|
||||
.links
|
||||
.iter()
|
||||
.filter(|l| matches!(l.source, solidcraft_sketch::LinkSource::Text { .. }))
|
||||
.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)
|
||||
}
|
||||
|
||||
/// Whether a cut of `f` with these targets (else its participants, else its component's
|
||||
@@ -824,7 +833,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, &[])
|
||||
}
|
||||
|
||||
/// A hole's straight drill along a panel of a sheet (into its rim), kept as a bore of each
|
||||
@@ -911,6 +920,12 @@ impl PrimitivePrism {
|
||||
}
|
||||
}
|
||||
|
||||
/// The most outline segments of a sketch's text a cut tells letters by, and the most text regions
|
||||
/// a sheet keeps (see [`crate::sheet::SheetBody::labels`]).
|
||||
const MAX_TEXT_CURVES: usize = 20_000;
|
||||
const MAX_LABELS: usize = 1000;
|
||||
|
||||
/// `text` is the outlines of the text in the cut's sketch (none for a primitive's cut).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn sheet_prism_cut(
|
||||
f: &Feature,
|
||||
@@ -922,6 +937,7 @@ fn sheet_prism_cut(
|
||||
targets: &[String],
|
||||
constructor: Option<&ExtrudeConstructor>,
|
||||
footprints_bound: bool,
|
||||
text: &[Seg2],
|
||||
) -> Result<Vec<String>> {
|
||||
let n = plane.normal();
|
||||
let mut work = sheet::FlatWork::new();
|
||||
@@ -960,6 +976,12 @@ fn sheet_prism_cut(
|
||||
&& current_footprints.iter().all(|(plane, r)| plane.x == placed_basis.x && plane.y == placed_basis.y && regions.contains(r)))
|
||||
.then_some(c.interval)
|
||||
});
|
||||
// The regions of the cut that are letters, with their counters.
|
||||
let letters: Vec<&Region2> = if operation == Operation::Cut && !text.is_empty() && text.len() <= MAX_TEXT_CURVES {
|
||||
regions.iter().filter(|r| sheet::loop_lies_on(&r.outer, text)).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut plans = Vec::new();
|
||||
'sheets: for original in &st.sheets {
|
||||
if !sheet_eligible(f, st, targets, original) {
|
||||
@@ -993,6 +1015,7 @@ fn sheet_prism_cut(
|
||||
sh.material_panels(Some(plane), &mut work)?
|
||||
};
|
||||
let mut modeled = false;
|
||||
let mut labels = Vec::new();
|
||||
for (pi, panel) in candidates {
|
||||
let w = if sh.flat { sh.world(None) } else { sh.world(pi) };
|
||||
let zorigin = (apply_point(&w, Vec3::ZERO) - plane.origin).dot(n);
|
||||
@@ -1032,6 +1055,7 @@ fn sheet_prism_cut(
|
||||
let (z0, z1) = (a.min(b).max(0.0), a.max(b).min(sh.t));
|
||||
// Sub-resolution layers must not be silently erased by endpoint snapping.
|
||||
let depth_resolved = ![z0, sh.t - z1, z1 - z0].into_iter().any(|d| d > 0.0 && d < 1e-6);
|
||||
labels.extend(letters.iter().filter_map(|r| sh.region_to_flat(pi, plane, r).ok()));
|
||||
let mapped = if footprints_bound && current_footprints.len() == tools.len() {
|
||||
current_footprints.iter().map(|(plane, r)| sh.region_to_flat(pi, plane, r)).collect::<Result<Vec<_>>>()?
|
||||
} else {
|
||||
@@ -1096,6 +1120,13 @@ fn sheet_prism_cut(
|
||||
}
|
||||
}
|
||||
}
|
||||
if modeled {
|
||||
for flat in labels {
|
||||
if !sh.labels.contains(&flat) && sh.labels.len() < MAX_LABELS {
|
||||
sh.labels.push(flat);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !modeled {
|
||||
super::sheet_boolean_unchanged(&mb.body, tools, operation)?;
|
||||
plans.push((sh.body.clone(), Some((sh, mb.body.clone()))));
|
||||
|
||||
@@ -490,6 +490,12 @@ pub struct SheetBody {
|
||||
/// Perforation fields (#732), flat coordinates; their holes are computed on demand and never cut into the B-rep.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub perforations: Vec<SheetPerforation>,
|
||||
/// Regions (flat coordinates) that text cut or machined, letters with their counters as
|
||||
/// holes: a record of where the sheet's text went, so that its DXF can write the letters on
|
||||
/// layers of their own (see [`SheetBody::letters`]). The cut-outs and pockets themselves are
|
||||
/// in `holes` and `pockets`. Derived from feature history, like `pockets`.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub labels: Vec<Region2>,
|
||||
/// Unfolded.
|
||||
pub flat: bool,
|
||||
/// Gap between flanges that meet at a corner.
|
||||
@@ -1600,6 +1606,26 @@ impl SheetBody {
|
||||
Ok(through.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// The text the sheet carries: what [`SheetBody::labels`] records, ready to tell whether a
|
||||
/// loop of the flat pattern is a letter.
|
||||
pub fn letters(&self) -> Letters {
|
||||
let regions = self
|
||||
.labels
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let (mut lo, mut hi) = (Vec2::new(f64::MAX, f64::MAX), Vec2::new(f64::MIN, f64::MIN));
|
||||
for p in r.outer.polyline(1e-3) {
|
||||
lo = Vec2::new(lo.x.min(p.x), lo.y.min(p.y));
|
||||
hi = Vec2::new(hi.x.max(p.x), hi.y.max(p.y));
|
||||
}
|
||||
// A polyline of 0.001 mm chords is within 0.001 mm of the curve.
|
||||
let pad = Vec2::new(0.01, 0.01);
|
||||
(lo - pad, hi + pad, std::iter::once(&r.outer).chain(&r.holes).flat_map(|lp| lp.segs.iter().copied()).collect())
|
||||
})
|
||||
.collect();
|
||||
Letters { regions }
|
||||
}
|
||||
|
||||
/// Pocket operations clipped to the surviving flat material, with exact depth intervals.
|
||||
pub fn machining(&self) -> Result<Vec<SheetPocket>> {
|
||||
let material = self.flat_regions()?;
|
||||
@@ -3953,6 +3979,30 @@ fn polys_cross(a: &[Vec2], b: &[Vec2]) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether every segment of `lp` lies on one of `curves` (its ends and its middle, within
|
||||
/// 0.0001 mm): the loop is made of those curves, or of parts of them.
|
||||
pub fn loop_lies_on(lp: &Loop2, curves: &[Seg2]) -> bool {
|
||||
const TOL: f64 = 1e-4;
|
||||
let near = |p: Vec2| curves.iter().any(|c| c.point_at(c.closest_param(p)).dist(p) <= TOL);
|
||||
!lp.segs.is_empty() && lp.segs.iter().all(|g| near(g.start()) && near(g.mid()) && near(g.end()))
|
||||
}
|
||||
|
||||
/// Tells which loops of a sheet's flat pattern are letters (see [`SheetBody::letters`]).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Letters {
|
||||
/// Each text region's corners (lowest, highest) and the segments of its outlines.
|
||||
regions: Vec<(Vec2, Vec2, Vec<Seg2>)>,
|
||||
}
|
||||
|
||||
impl Letters {
|
||||
/// Whether `lp` is made of the outlines of text the sheet carries: a letter, or one of its
|
||||
/// counters.
|
||||
pub fn is_letter(&self, lp: &Loop2) -> bool {
|
||||
let Some(p) = lp.segs.first().map(Seg2::start) else { return false };
|
||||
self.regions.iter().any(|(lo, hi, curves)| p.x >= lo.x && p.x <= hi.x && p.y >= lo.y && p.y <= hi.y && loop_lies_on(lp, curves))
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded overlap predicate, including crossing-only cuts whose vertices lie outside a panel.
|
||||
pub(crate) fn flat_regions_overlap(a: &Region2, b: &Region2, work: &mut FlatWork) -> Result<bool> {
|
||||
let (a, b) = (planar_region(a)?, planar_region(b)?);
|
||||
@@ -3974,6 +4024,7 @@ pub fn base_flange(body: &str, rule: &str, rv: &RuleValues, plane: Plane, region
|
||||
pockets: Vec::new(),
|
||||
bores: Vec::new(),
|
||||
perforations: Vec::new(),
|
||||
labels: Vec::new(),
|
||||
flat: false,
|
||||
gap: rv.gap,
|
||||
relief: rv.corner_relief_shape,
|
||||
@@ -4031,6 +4082,7 @@ pub fn contour_flange(body: &str, rule: &str, rv: &RuleValues, plane: &Plane, pt
|
||||
pockets: Vec::new(),
|
||||
bores: Vec::new(),
|
||||
perforations: Vec::new(),
|
||||
labels: Vec::new(),
|
||||
flat: false,
|
||||
gap: rv.gap,
|
||||
relief: rv.corner_relief_shape,
|
||||
@@ -4175,6 +4227,41 @@ mod tests {
|
||||
kernel::measure(b).map(|m| m.volume).unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loops_lie_on_the_curves_they_are_made_of() {
|
||||
let circle = Loop2::circle(Vec2::new(5.0, 5.0), 3.0);
|
||||
let curves: Vec<Seg2> = circle.segs.clone();
|
||||
// The loop itself, started elsewhere and run the other way, and a loop of its halves.
|
||||
assert!(loop_lies_on(&circle, &curves));
|
||||
assert!(loop_lies_on(&Loop2::circle_from(Vec2::new(5.0, 5.0), 3.0, 1.0).reversed(), &curves));
|
||||
let halves: Vec<Seg2> = circle.segs.iter().flat_map(|g| [g.split_half().0, g.split_half().1]).collect();
|
||||
assert!(loop_lies_on(&Loop2 { segs: halves }, &curves));
|
||||
// Another circle, a square and an empty loop do not.
|
||||
assert!(!loop_lies_on(&Loop2::circle(Vec2::new(5.0, 5.0), 3.1), &curves));
|
||||
assert!(!loop_lies_on(&Loop2::circle(Vec2::new(5.0, 5.0), 2.0), &curves));
|
||||
assert!(!loop_lies_on(&rect(Vec2::ZERO, Vec2::X, Vec2::new(1.0, 1.0), Vec2::Y).outer, &curves));
|
||||
assert!(!loop_lies_on(&Loop2 { segs: Vec::new() }, &curves));
|
||||
// A loop only partly on them (a letter across the sheet's edge) is not on them.
|
||||
let notch = Loop2 { segs: vec![circle.segs[0], Seg2::Line { a: circle.segs[0].end(), b: Vec2::new(50.0, 50.0) }] };
|
||||
assert!(!loop_lies_on(¬ch, &curves));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sheet_knows_the_letters_it_carries() {
|
||||
// A recorded text region with a counter: its outline and its counter are letters, the same
|
||||
// circle elsewhere, a loop near it that is another size and a loop far away are not.
|
||||
let mut s = plate(100.0, 60.0);
|
||||
let counter = Loop2::circle(Vec2::new(20.0, 20.0), 2.0).reversed();
|
||||
s.labels.push(Region2 { outer: Loop2::circle(Vec2::new(20.0, 20.0), 5.0), holes: vec![counter.clone()] });
|
||||
let letters = s.letters();
|
||||
assert!(letters.is_letter(&Loop2::circle(Vec2::new(20.0, 20.0), 5.0)));
|
||||
assert!(letters.is_letter(&counter.reversed()));
|
||||
assert!(!letters.is_letter(&Loop2::circle(Vec2::new(20.0, 20.0), 4.0)));
|
||||
assert!(!letters.is_letter(&Loop2::circle(Vec2::new(70.0, 20.0), 5.0)));
|
||||
// A sheet that carries none has no letters.
|
||||
assert!(!plate(100.0, 60.0).letters().is_letter(&Loop2::circle(Vec2::new(20.0, 20.0), 5.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_flange_matches_fusion() {
|
||||
// Oracle 68: 100 x 60 plate, 20 mm flange on the y = 0 top edge.
|
||||
|
||||
Reference in new issue
Block a user