mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
`sheet.perforate` takes an optional `sketch` (and `profiles`): the field's outline. Holes go only inside its closed profiles, out of their inner loops, `margin` clear of its edges, on a grid from the outline's lower-left corner. On a folded sheet the outline maps through each panel the sketch lies on and is clipped to it, so it never reaches a flange in the flat; a sketch on no face of the sheet is refused. A mirrored copy mirrors the outline, and an edit that names no sketch (the Perforate dialog has no outline) keeps it. Without a sketch the field covers the whole sheet as before. The feature saves `sketch` and `profiles`, a sheet's perforation its outline in flat coordinates: one pending format note, no migration. Assisted-by: LLM
This commit is contained in:
committed by
GitHub
parent
a33619fd2b
commit
d7a17de3d9
@@ -1217,9 +1217,10 @@ pub enum FeatureKind {
|
||||
body: Option<BodyName>,
|
||||
},
|
||||
/// A perforation field (#732): round holes of `diameter` on a staggered (60°) or square grid of `pitch` over the
|
||||
/// whole flat sheet, kept `margin` (default: the diameter) clear of its edges, cut-outs and bend zones. Stored like
|
||||
/// a cosmetic thread: the holes go to the flat pattern, the DXF and the measured volume, not into the B-rep, so a
|
||||
/// panel can carry tens of thousands of them.
|
||||
/// whole flat sheet, or inside the closed profiles of `sketch` when it is given, kept `margin` (default: the
|
||||
/// diameter) clear of its edges, cut-outs, bend zones and the outline. Stored like a cosmetic thread: the holes go
|
||||
/// to the flat pattern, the DXF and the measured volume, not into the B-rep, so a panel can carry tens of thousands
|
||||
/// of them.
|
||||
SheetPerforate {
|
||||
#[walk(length = "Diameter")]
|
||||
diameter: Expr,
|
||||
@@ -1232,6 +1233,13 @@ pub enum FeatureKind {
|
||||
pattern: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
body: Option<BodyName>,
|
||||
/// The field's outline: a sketch on a face of the sheet. None: the whole sheet.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
sketch: Option<SketchId>,
|
||||
/// Which of the outline sketch's profiles bound the field. None: all of them.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[walk(profiles_of = sketch)]
|
||||
profiles: Option<ProfileSel>,
|
||||
},
|
||||
/// Fold a sheet along a line on its base face: a sketch line (followed when the sketch
|
||||
/// changes), else the points `a`, `b`.
|
||||
|
||||
@@ -684,7 +684,7 @@ pub(super) fn sheet_eval(
|
||||
tag_flanges(sh, n0, f.id);
|
||||
rebuild_sheet(st, i)
|
||||
}
|
||||
FeatureKind::SheetPerforate { diameter, pitch, margin, pattern, body } => {
|
||||
FeatureKind::SheetPerforate { diameter, pitch, margin, pattern, body, sketch, profiles } => {
|
||||
// Without a body name the field goes on the newest sheet, as the command's own body lookup does.
|
||||
let i = match body {
|
||||
Some(_) => sheet_for(st, body, None)?,
|
||||
@@ -706,8 +706,18 @@ pub(super) fn sheet_eval(
|
||||
"perforation needs diameter > 0, pitch > diameter and margin >= 0 (diameter {d}, pitch {pitch}, margin {margin})"
|
||||
)));
|
||||
}
|
||||
// The outline sketch's profiles, where they lie on the sheet now.
|
||||
let outline = match sketch {
|
||||
Some(sk) => {
|
||||
let ss = st.sketch(sk.0).ok_or_else(|| DocError::Unknown(format!("sketch {sk}")))?;
|
||||
let regions = select_profiles(ss, profiles.as_ref().unwrap_or(&ProfileSel::All))?;
|
||||
let sh = st.sheets.get(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
Some(sh.perforation_outline(&ss.plane, ®ions)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
|
||||
sh.perforations.push(crate::sheet::SheetPerforation { feature: f.id, diameter: d, pitch, margin, square, from_right: false });
|
||||
sh.perforations.push(crate::sheet::SheetPerforation { feature: f.id, diameter: d, pitch, margin, square, from_right: false, outline });
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SheetHem { edges, length, gap, flip, body } => {
|
||||
|
||||
@@ -148,7 +148,10 @@ pub(super) fn fixtures() -> Vec<(&'static str, Value)> {
|
||||
("sheet_hem", json!({"type": "sheet_hem", "edges": [o], "length": "hem.length", "gap": "hem.gap"})),
|
||||
(
|
||||
"sheet_perforate",
|
||||
json!({"type": "sheet_perforate", "diameter": "perforate.diameter", "pitch": "perforate.pitch", "margin": "perforate.margin"}),
|
||||
json!({
|
||||
"type": "sheet_perforate", "diameter": "perforate.diameter", "pitch": "perforate.pitch", "margin": "perforate.margin",
|
||||
"sketch": 1, "profiles": {"type": "all"}
|
||||
}),
|
||||
),
|
||||
(
|
||||
"sheet_fold",
|
||||
|
||||
@@ -133,6 +133,7 @@ sheet_flange
|
||||
sheet_hem
|
||||
moves: edges[0][0], edges[0][1], edges[0][2]
|
||||
sheet_perforate
|
||||
profiles: 1 all
|
||||
sheet_fold
|
||||
moves: a[0], a[1], a[2], b[1], b[2], fixed[0], fixed[1], fixed[2]
|
||||
sheet_path
|
||||
|
||||
@@ -143,6 +143,8 @@ sheet_flange
|
||||
sheet_hem
|
||||
remap_bodies: body
|
||||
sheet_perforate
|
||||
sketches: [1]
|
||||
remap_refs: sketch
|
||||
remap_bodies: body
|
||||
sheet_fold
|
||||
sketches: [7]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
//! A perforation field can be bounded by a sketch: a `sheet_perforate` feature saves an optional `sketch` and
|
||||
//! `profiles` (its outline), and a sheet's perforation the outline in flat coordinates (`outline`). Holes go only
|
||||
//! inside the outline. Older designs need no transformation: a field without an outline covers the whole sheet as
|
||||
//! before. The last release refuses a design saved with this (the newer format number).
|
||||
+64
-5
@@ -688,6 +688,10 @@ pub struct SheetPerforation {
|
||||
/// copy, whose holes are the mirror image of its source's.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub from_right: bool,
|
||||
/// The field's outline in flat coordinates (see [`SheetBody::perforation_outline`]): holes only inside these
|
||||
/// regions, margin clear of their edges, on a grid that starts from their bounding box. None: the whole flat.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub outline: Option<Vec<Region2>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -2104,9 +2108,45 @@ impl SheetBody {
|
||||
self.flanges.iter().map(|f| self.zone(f)).collect()
|
||||
}
|
||||
|
||||
/// A perforation field's outline (#732) in flat coordinates: closed regions of a sketch in `plane` (world). An
|
||||
/// unfolded sheet takes them as drawn on the flat; a folded one through each panel whose material the plane lies
|
||||
/// in, clipped to that panel, so an outline on one face never reaches a flange in the flat. Refused when the
|
||||
/// plane lies in no panel.
|
||||
pub fn perforation_outline(&self, plane: &Plane, regions: &[Region2]) -> Result<Vec<Region2>> {
|
||||
let on = |pi: Option<usize>| {
|
||||
let w = self.world(pi);
|
||||
let n = apply_vector(&w, Vec3::Z);
|
||||
let z = (plane.origin - apply_point(&w, Vec3::ZERO)).dot(n);
|
||||
n.cross(plane.normal()).len() < 1e-6 && (-LINEAR_TOL..=self.t + LINEAR_TOL).contains(&z)
|
||||
};
|
||||
if self.flat {
|
||||
if !on(None) {
|
||||
return Err(DocError::Invalid("the perforation's outline sketch must lie on the flat sheet".into()));
|
||||
}
|
||||
return regions.iter().map(|r| self.region_to_flat(None, plane, r)).collect();
|
||||
}
|
||||
let mut work = FlatWork::new();
|
||||
let mut out = Vec::new();
|
||||
let mut found = false;
|
||||
for (pi, panel) in self.parallel_panels(plane) {
|
||||
if !on(pi) {
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
let mapped = regions.iter().map(|r| self.region_to_flat(pi, plane, r)).collect::<Result<Vec<_>>>()?;
|
||||
out.extend(material_boolean(std::slice::from_ref(&panel), &mapped, kernel::BoolOp::Intersect, &mut work)?);
|
||||
}
|
||||
if !found {
|
||||
return Err(DocError::Invalid("the perforation's outline sketch must lie on a face of the sheet".into()));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Hole centres of a perforation field (#732), flat coordinates: a staggered (60°, odd rows shifted half a pitch)
|
||||
/// or square grid from the flat bounding box's lower-left corner plus margin + radius; a hole is kept where it is
|
||||
/// at least margin clear of the material's edges and cut-outs and of every bend zone.
|
||||
/// at least margin clear of the material's edges and cut-outs and of every bend zone. A field with an outline
|
||||
/// keeps its holes inside the outline and margin clear of its edges too, on a grid from the lower-left corner of
|
||||
/// the outline's bounding box (within the flat's).
|
||||
pub fn perforation_holes(&self, p: &SheetPerforation) -> Result<Vec<Vec2>> {
|
||||
let regions = self.flat_regions()?;
|
||||
let zones = self.bend_zones();
|
||||
@@ -2127,6 +2167,20 @@ impl SheetBody {
|
||||
if !(lo.x <= hi.x) || !(p.pitch > 0.0) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
// The rows and columns span the flat, or the outline's part of it; the bucket grid below stays the flat's.
|
||||
let mut field = Aabb2 { min: lo, max: hi };
|
||||
if let Some(outline) = &p.outline {
|
||||
let n = segs.len();
|
||||
for r in outline {
|
||||
for l in std::iter::once(&r.outer).chain(&r.holes) {
|
||||
segs.extend(l.polyline(1e-3).edges().map(|(&a, &b)| (a, b)));
|
||||
}
|
||||
}
|
||||
field = field.intersection(&segs.get(n..).unwrap_or(&[]).iter().map(|(a, _)| *a).collect::<Aabb2>());
|
||||
if !(field.min.x <= field.max.x && field.min.y <= field.max.y) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
}
|
||||
// Segments bucketed in a grid, so each hole only checks the edges near it.
|
||||
let cell = (2.0 * clear).max(p.pitch).max(1.0);
|
||||
let (nx, ny) = (((hi.x - lo.x) / cell) as usize + 1, ((hi.y - lo.y) / cell) as usize + 1);
|
||||
@@ -2153,20 +2207,22 @@ impl SheetBody {
|
||||
let dy = if p.square { p.pitch } else { p.pitch * 3f64.sqrt() / 2.0 };
|
||||
let mut out = Vec::new();
|
||||
let mut row = 0usize;
|
||||
let Aabb2 { min: flo, max: fhi } = field;
|
||||
loop {
|
||||
let y = lo.y + clear + row as f64 * dy;
|
||||
if y > hi.y - clear + 1e-9 {
|
||||
let y = flo.y + clear + row as f64 * dy;
|
||||
if y > fhi.y - clear + 1e-9 {
|
||||
break;
|
||||
}
|
||||
let shift = if !p.square && row % 2 == 1 { p.pitch / 2.0 } else { 0.0 };
|
||||
// Steps across the row from its first hole; a mirrored field counts from the right.
|
||||
let mut step = 0.0;
|
||||
while lo.x + clear + shift + step <= hi.x - clear + 1e-9 {
|
||||
let x = if p.from_right { hi.x - clear - shift - step } else { lo.x + clear + shift + step };
|
||||
while flo.x + clear + shift + step <= fhi.x - clear + 1e-9 {
|
||||
let x = if p.from_right { fhi.x - clear - shift - step } else { flo.x + clear + shift + step };
|
||||
let c = Vec2::new(x, y);
|
||||
let (i, j) = idx(x, y);
|
||||
let near = grid.get(j * nx + i).map(Vec::as_slice).unwrap_or(&[]);
|
||||
if regions.iter().any(|r| r.contains(c))
|
||||
&& p.outline.as_ref().is_none_or(|o| o.iter().any(|r| r.contains(c)))
|
||||
&& !zones.iter().any(|z| z.contains(c))
|
||||
&& near.iter().all(|&k| segs.get(k).is_none_or(|&s| dist(c, s) >= clear - 1e-9))
|
||||
{
|
||||
@@ -4733,6 +4789,9 @@ impl SheetBody {
|
||||
}
|
||||
for p in &mut self.perforations {
|
||||
p.from_right = !p.from_right;
|
||||
for r in p.outline.iter_mut().flatten() {
|
||||
*r = flip_x_region(r);
|
||||
}
|
||||
}
|
||||
for (_, a, b) in &mut self.base_curves {
|
||||
(*a, *b) = (flip_x(*b), flip_x(*a));
|
||||
|
||||
@@ -1767,6 +1767,15 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
{
|
||||
*after = before.clone();
|
||||
}
|
||||
// A perforation keeps its outline through an edit that names no sketch (the Perforate dialog has no outline).
|
||||
if let (FeatureKind::SheetPerforate { sketch: Some(before), profiles: picked, .. }, FeatureKind::SheetPerforate { sketch, profiles, .. }) =
|
||||
(&old.kind, &mut new.kind)
|
||||
&& sketch.is_none()
|
||||
&& params.get("sketch").is_none()
|
||||
{
|
||||
*sketch = Some(*before);
|
||||
*profiles = picked.clone();
|
||||
}
|
||||
// A boundary surface's loop of sketch curves and its curve network are one command's two
|
||||
// forms: an edit may turn one into the other.
|
||||
let boundary = |k: &FeatureKind| matches!(k, FeatureKind::SurfaceBoundary { .. } | FeatureKind::SurfaceNetwork { .. });
|
||||
|
||||
@@ -56,7 +56,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.contract(&PERFORATE_CONTRACT)
|
||||
.output(output_schema_for::<FeatureResult>)
|
||||
.params(
|
||||
"diameter, pitch: expr; margin?: expr (clear of edges, cut-outs and bends; default: the diameter); pattern?: staggered (60°, default) | square; body? - a perforation field over the whole flat sheet: holes in the flat pattern (perforations), the DXF (layer PERFORATION) and the measured volume, not cut into the B-rep (#732)",
|
||||
"diameter, pitch: expr; margin?: expr (clear of edges, cut-outs, bends and the outline; default: the diameter); pattern?: staggered (60°, default) | square; body?; sketch? + profiles? (the outline: closed profiles of a sketch on a face of the sheet; holes only inside them, out of their inner loops) - a perforation field over the whole flat sheet or inside the outline: holes in the flat pattern (perforations), the DXF (layer PERFORATION) and the measured volume, not cut into the B-rep (#732)",
|
||||
),
|
||||
CommandSpec::new("sheet.hem", "Hem", crate::checked!("sheet.hem", hem))
|
||||
.at("SHEET METAL", "CREATE")
|
||||
@@ -472,7 +472,14 @@ fn perforate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
check_expr(s, m, Kind::Length, cmd, "margin")?;
|
||||
}
|
||||
let pattern = str_(p, "pattern").map(|t| perforation_pattern(t).map(str::to_string).map_err(|why| bad(cmd, why))).transpose()?;
|
||||
add_feature(s, p, FeatureKind::SheetPerforate { diameter, pitch, margin, pattern, body: str_(p, "body").map(BodyName::from) })
|
||||
// The outline: only a sketch named here, never the active one.
|
||||
let sketch = match p.get("sketch") {
|
||||
Some(_) => Some(SketchId(feature_sketch(s, p, cmd)?)),
|
||||
None if p.get("profiles").is_some() => return Err(bad(cmd, "`profiles` picks the outline's profiles: it needs `sketch`")),
|
||||
None => None,
|
||||
};
|
||||
let profiles = p.get("profiles").map(|_| profiles(p, cmd)).transpose()?;
|
||||
add_feature(s, p, FeatureKind::SheetPerforate { diameter, pitch, margin, pattern, body: str_(p, "body").map(BodyName::from), sketch, profiles })
|
||||
}
|
||||
|
||||
fn hem(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -5215,6 +5215,102 @@ fn perforate_supports_a_square_pattern_and_refuses_impossible_values() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The hole centres `staggered_count` counts, over the box (x0, y0)..(x1, y1).
|
||||
fn staggered_centres(x0: f64, y0: f64, x1: f64, y1: f64, d: f64, pitch: f64, margin: f64) -> Vec<(f64, f64)> {
|
||||
let (clear, dy) = (d / 2.0 + margin, pitch * 3f64.sqrt() / 2.0);
|
||||
let mut out = Vec::new();
|
||||
let mut row = 0;
|
||||
while y0 + clear + row as f64 * dy <= y1 - clear + 1e-9 {
|
||||
let mut x = x0 + clear + if row % 2 == 1 { pitch / 2.0 } else { 0.0 };
|
||||
while x <= x1 - clear + 1e-9 {
|
||||
out.push((x, y0 + clear + row as f64 * dy));
|
||||
x += pitch;
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A 100 x 60 x 0.8 sheet with a sketch on its face: a 50 x 30 rectangle at (10, 10) with a Ø10 circle at (35, 25).
|
||||
fn sheet_with_a_field_outline(s: &mut Session) {
|
||||
outline_blank(s, json!([[0, 0], [100, 0], [100, 60], [0, 60]]));
|
||||
run(s, "sketch.create", json!({"plane": "XY", "name": "Field"}));
|
||||
run(s, "sketch.rectangle.two_point", json!({"p0": [10, 10], "p1": [60, 40]}));
|
||||
run(s, "sketch.circle.center", json!({"center": [35, 25], "radius": 5}));
|
||||
run(s, "sketch.finish", json!({}));
|
||||
}
|
||||
|
||||
/// #732: a sketch bounds the field. The ring of the rectangle less the circle takes Rv 1.0-1.5 only inside it, margin
|
||||
/// clear of both its loops, on the grid from the outline's lower-left corner; an edit without a sketch keeps it.
|
||||
#[test]
|
||||
fn perforation_outline_bounds_the_field_and_keeps_out_its_inner_loops() {
|
||||
let mut s = Session::default();
|
||||
sheet_with_a_field_outline(&mut s);
|
||||
let made =
|
||||
run(&mut s, "sheet.perforate", json!({"diameter": "1", "pitch": "1.5", "margin": "1", "sketch": "Field", "profiles": [{"point": [15, 15]}]}));
|
||||
let expected: Vec<(f64, f64)> =
|
||||
staggered_centres(10.0, 10.0, 60.0, 40.0, 1.0, 1.5, 1.0).into_iter().filter(|&(x, y)| (x - 35.0).hypot(y - 25.0) >= 6.5).collect();
|
||||
assert_eq!(flat(&mut s)["perforations"][0]["count"].as_u64(), Some(expected.len() as u64));
|
||||
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap_or_default().to_string();
|
||||
let holes = perforation_circles(&dxf);
|
||||
assert_eq!(holes.len(), expected.len());
|
||||
assert!((holes[0].0 - 11.5).abs() < 1e-9 && (holes[0].1 - 11.5).abs() < 1e-9, "first hole at the outline's corner: {:?}", holes[0]);
|
||||
assert!(holes.iter().all(|&(x, y, _)| (11.5 - 1e-9..=58.5 + 1e-9).contains(&x) && (11.5 - 1e-9..=38.5 + 1e-9).contains(&y)));
|
||||
assert!(holes.iter().all(|&(x, y, _)| (x - 35.0).hypot(y - 25.0) >= 6.5 - 1e-9), "no hole in or near the inner loop");
|
||||
let n = holes.len();
|
||||
run(
|
||||
&mut s,
|
||||
"timeline.redefine",
|
||||
json!({"feature": made["feature"], "command": "sheet.perforate", "params": {"diameter": "1", "pitch": "1.5", "margin": "1"}}),
|
||||
);
|
||||
assert_eq!(flat(&mut s)["perforations"][0]["count"].as_u64(), Some(n as u64), "the edit keeps the outline");
|
||||
}
|
||||
|
||||
/// #732: on a folded sheet the outline goes through the panel its sketch lies on and stops there: an outline that
|
||||
/// reaches past the bend puts no hole in the flange. A sketch on no face of the sheet is refused.
|
||||
#[test]
|
||||
fn perforation_outline_on_a_folded_sheet_stays_in_its_panel() {
|
||||
let mut s = Session::default();
|
||||
outline_blank(&mut s, json!([[0, 0], [100, 0], [100, 60], [0, 60]]));
|
||||
run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 0.8]], "height": 20}));
|
||||
for (name, z) in [("Top", 0.8), ("Above", 10.0)] {
|
||||
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, z], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": name}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-50, -50], "p1": [150, 110]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
}
|
||||
run(&mut s, "sheet.perforate", json!({"diameter": "1", "pitch": "1.5", "margin": "1", "sketch": "Top"}));
|
||||
let zone = &flat(&mut s)["bend_zones"][0]; // [min x, max x, min y, max y] of the bend zone, flat coordinates
|
||||
let top = zone[3].as_f64().unwrap_or(f64::NAN);
|
||||
let dxf = run(&mut s, "sheet.export_dxf", json!({}))["dxf"].as_str().unwrap_or_default().to_string();
|
||||
let holes = perforation_circles(&dxf);
|
||||
assert!(!holes.is_empty());
|
||||
assert!(holes.iter().all(|&(_, y, r)| y - r - 1.0 >= top - 1e-9), "every hole is in the base, beyond the bend zone's y {top}");
|
||||
let refused = s.execute("sheet.perforate", &json!({"diameter": "1", "pitch": "1.5", "sketch": "Above"}));
|
||||
assert!(refused.is_err_and(|e| e.to_string().contains("must lie on a face of the sheet")));
|
||||
}
|
||||
|
||||
/// #732: a mirrored copy's bounded field is the mirror image of its source's.
|
||||
#[test]
|
||||
fn a_mirrored_sheet_copy_mirrors_its_perforation_outline() {
|
||||
let mut s = Session::default();
|
||||
sheet_with_a_field_outline(&mut s);
|
||||
run(&mut s, "sheet.perforate", json!({"diameter": "1", "pitch": "1.5", "margin": "1", "sketch": "Field", "profiles": [{"point": [15, 15]}]}));
|
||||
run(&mut s, "solid.mirror", json!({"bodies": ["Body1"], "plane": "YZ"}));
|
||||
let holes = |s: &mut Session, body: &str, sx: f64| -> Vec<(i64, i64)> {
|
||||
let dxf = run(s, "sheet.export_dxf", json!({"body": body}))["dxf"].as_str().unwrap_or_default().to_string();
|
||||
let c = perforation_circles(&dxf);
|
||||
// Relative to the field's own lower-left hole, x scaled by sx, rounded to 1e-6 mm.
|
||||
let x0 = c.iter().map(|h| h.0 * sx).fold(f64::INFINITY, f64::min);
|
||||
let y0 = c.iter().map(|h| h.1).fold(f64::INFINITY, f64::min);
|
||||
let mut v: Vec<(i64, i64)> = c.iter().map(|h| (((h.0 * sx - x0) * 1e6).round() as i64, ((h.1 - y0) * 1e6).round() as i64)).collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
};
|
||||
let original = holes(&mut s, "Body1", -1.0);
|
||||
assert!(!original.is_empty());
|
||||
assert_eq!(holes(&mut s, "Body1 (2)", 1.0), original);
|
||||
}
|
||||
|
||||
/// A 100 x 60 tray with one 45° flange 40 high on its y = 0 edge (2.5 mm, R 2.5, K 0.44), and on
|
||||
/// it a return 10 high when asked. Returns the far edge of the last panel in the flat pattern:
|
||||
/// the base starts at the setback s = (t + R)·tan 22.5°, each bend's zone is (π/4)·(R + K·t) long,
|
||||
|
||||
@@ -417,9 +417,9 @@ pub struct FlangeParams {
|
||||
pub common: FeatureCommon,
|
||||
}
|
||||
|
||||
/// Add a field of round perforations over a whole flat sheet: holes in the flat pattern, the DXF
|
||||
/// (layer PERFORATION) and the measured volume, not cut into the solid. `participants` changes
|
||||
/// nothing.
|
||||
/// Add a field of round perforations over a whole flat sheet, or inside a sketch's outline: holes
|
||||
/// in the flat pattern, the DXF (layer PERFORATION) and the measured volume, not cut into the
|
||||
/// solid. `participants` changes nothing.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct PerforateParams {
|
||||
@@ -427,8 +427,8 @@ pub struct PerforateParams {
|
||||
pub diameter: LengthExpr,
|
||||
/// The distance between hole centres, more than the diameter.
|
||||
pub pitch: LengthExpr,
|
||||
/// How far the holes stay clear of the sheet's edges, cut-outs and bends, at least 0.
|
||||
/// Default: the diameter.
|
||||
/// How far the holes stay clear of the sheet's edges, cut-outs and bends, and of the outline,
|
||||
/// at least 0. Default: the diameter.
|
||||
#[serde(default)]
|
||||
pub margin: Option<LengthExpr>,
|
||||
/// How the holes are laid out. Default: staggered.
|
||||
@@ -437,6 +437,18 @@ pub struct PerforateParams {
|
||||
/// The sheet metal body. Default: the newest sheet.
|
||||
#[serde(default)]
|
||||
pub body: Option<String>,
|
||||
/// The field's outline: a sketch, by id or name, on a face of the sheet (or on the flat sheet
|
||||
/// while it is unfolded). Holes go only inside its closed profiles, out of their inner loops,
|
||||
/// on a grid from the outline's lower-left corner; on a folded sheet only in the panels the
|
||||
/// sketch lies on. Default: the whole sheet.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "IdOrName")]
|
||||
pub sketch: Option<IdOrName>,
|
||||
/// Which of the outline sketch's profiles bound the field. Needs `sketch`. Default: all of
|
||||
/// them.
|
||||
#[serde(default, deserialize_with = "present", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "ProfilesParam")]
|
||||
pub profiles: Option<ProfilesParam>,
|
||||
#[serde(flatten, deserialize_with = "feature_common")]
|
||||
pub common: FeatureCommon,
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ fn schemas_and_the_key_check_agree() {
|
||||
"type",
|
||||
]),
|
||||
);
|
||||
assert_keys(input_schema_for::<PerforateParams>(), &keys(&["body", "diameter", "margin", "pattern", "pitch"]));
|
||||
assert_keys(input_schema_for::<PerforateParams>(), &keys(&["body", "diameter", "margin", "pattern", "pitch", "profiles", "sketch"]));
|
||||
assert_keys(input_schema_for::<HemParams>(), &keys(&["body", "edges", "flip", "gap", "length"]));
|
||||
assert_keys(input_schema_for::<FoldParams>(), &keys(&["angle", "body", "curve", "fixed", "flip", "points", "position", "radius", "sketch"]));
|
||||
assert_keys(input_schema_for::<UnfoldParams>(), &keys(&["body"]));
|
||||
|
||||
@@ -1030,12 +1030,12 @@ pub fn sketch_shown(s: &Session, id: u64) -> bool {
|
||||
v.shown_sketches.contains(&id) || !sketch_consumed(s, id)
|
||||
}
|
||||
|
||||
/// Is a sketch used by a later feature? A sheet metal base or contour flange uses its sketch as
|
||||
/// an Extrude does: shown, its lines lie on the sheet's edges and take the clicks meant for
|
||||
/// them (#6804). So does a Hole at sketch points: shown, its points are drawn in the holes
|
||||
/// (#9082). A Single hole snapped to one point (#2110) does not: it was placed on a face, not
|
||||
/// built from the sketch, and hiding the sketch would take its other points from the next
|
||||
/// Single hole's snap.
|
||||
/// Is a sketch used by a later feature? A sheet metal base or contour flange, or a perforation's
|
||||
/// outline, uses its sketch as an Extrude does: shown, its lines lie on the sheet's edges and
|
||||
/// faces and take the clicks meant for them (#6804). So does a Hole at sketch points: shown, its
|
||||
/// points are drawn in the holes (#9082). A Single hole snapped to one point (#2110) does not: it
|
||||
/// was placed on a face, not built from the sketch, and hiding the sketch would take its other
|
||||
/// points from the next Single hole's snap.
|
||||
pub fn sketch_consumed(s: &Session, id: u64) -> bool {
|
||||
if let Ok(mut cache) = s.consumed_sketches_cache.lock() {
|
||||
if cache.as_ref().is_none_or(|c| !std::ptr::eq(c.doc.as_ptr(), Arc::as_ptr(&s.doc))) {
|
||||
@@ -1065,7 +1065,8 @@ fn consumed_sketches(doc: &solidcraft_doc::Document) -> impl Iterator<Item = u64
|
||||
| FeatureKind::Revolve { sketch, .. }
|
||||
| FeatureKind::SheetBase { sketch, .. }
|
||||
| FeatureKind::SheetContour { sketch, .. }
|
||||
| FeatureKind::Hole { points: Some(SketchPoints { sketch, single: false, .. }), .. } => Some(sketch.0),
|
||||
| FeatureKind::Hole { points: Some(SketchPoints { sketch, single: false, .. }), .. }
|
||||
| FeatureKind::SheetPerforate { sketch: Some(sketch), .. } => Some(sketch.0),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user