Sheet metal: a perforation field bounded by a sketch's outline (#732) (#10754)

`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:
Christian Glombek
2026-11-02 00:14:41 +00:00
committed by GitHub
parent a33619fd2b
commit d7a17de3d9
13 changed files with 238 additions and 26 deletions
+11 -3
View File
@@ -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`.
+12 -2
View File
@@ -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, &regions)?)
}
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
View File
@@ -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));
+9
View File
@@ -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 { .. });
+9 -2
View File
@@ -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> {
+96
View File
@@ -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,
+17 -5
View File
@@ -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,
}
+1 -1
View File
@@ -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"]));
+8 -7
View File
@@ -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,
})
}