diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index 73ba17464e..70a46feec1 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -1217,9 +1217,10 @@ pub enum FeatureKind { body: Option, }, /// 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, #[serde(default, skip_serializing_if = "Option::is_none")] body: Option, + /// 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, + /// 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, }, /// Fold a sheet along a line on its base face: a sketch line (followed when the sketch /// changes), else the points `a`, `b`. diff --git a/crates/doc/src/eval_more.rs b/crates/doc/src/eval_more.rs index 9cc8952423..1b0f2e17d2 100644 --- a/crates/doc/src/eval_more.rs +++ b/crates/doc/src/eval_more.rs @@ -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 } => { diff --git a/crates/doc/src/feature_walk_tests/fixtures.rs b/crates/doc/src/feature_walk_tests/fixtures.rs index 27037f33e2..6e148707ff 100644 --- a/crates/doc/src/feature_walk_tests/fixtures.rs +++ b/crates/doc/src/feature_walk_tests/fixtures.rs @@ -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", diff --git a/crates/doc/src/feature_walk_tests/golden_geometry.txt b/crates/doc/src/feature_walk_tests/golden_geometry.txt index ce58eadb27..4196cf1333 100644 --- a/crates/doc/src/feature_walk_tests/golden_geometry.txt +++ b/crates/doc/src/feature_walk_tests/golden_geometry.txt @@ -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 diff --git a/crates/doc/src/feature_walk_tests/golden_refs.txt b/crates/doc/src/feature_walk_tests/golden_refs.txt index 303007b9d2..7e61b3ed10 100644 --- a/crates/doc/src/feature_walk_tests/golden_refs.txt +++ b/crates/doc/src/feature_walk_tests/golden_refs.txt @@ -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] diff --git a/crates/doc/src/format/pending/sheet-perforation-outline.rs b/crates/doc/src/format/pending/sheet-perforation-outline.rs new file mode 100644 index 0000000000..423133a3bd --- /dev/null +++ b/crates/doc/src/format/pending/sheet-perforation-outline.rs @@ -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). diff --git a/crates/doc/src/sheet.rs b/crates/doc/src/sheet.rs index d529700f8a..269e24419e 100644 --- a/crates/doc/src/sheet.rs +++ b/crates/doc/src/sheet.rs @@ -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>, } #[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> { + let on = |pi: Option| { + 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::>>()?; + 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> { 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::()); + 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)); diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index b2e38945be..5b309f42c2 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -1767,6 +1767,15 @@ fn redefine(s: &mut Session, p: &Value) -> Result { { *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 { .. }); diff --git a/crates/engine/src/cmd/sheet.rs b/crates/engine/src/cmd/sheet.rs index dcb9862579..ce74fff2c3 100644 --- a/crates/engine/src/cmd/sheet.rs +++ b/crates/engine/src/cmd/sheet.rs @@ -56,7 +56,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .contract(&PERFORATE_CONTRACT) .output(output_schema_for::) .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 { 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 { diff --git a/crates/engine/src/cmd/sheet_tests.rs b/crates/engine/src/cmd/sheet_tests.rs index adc6e3f109..b3afc4a233 100644 --- a/crates/engine/src/cmd/sheet_tests.rs +++ b/crates/engine/src/cmd/sheet_tests.rs @@ -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, diff --git a/crates/engine/src/schema/sheet.rs b/crates/engine/src/schema/sheet.rs index 8e3cbf5731..da76fcba10 100644 --- a/crates/engine/src/schema/sheet.rs +++ b/crates/engine/src/schema/sheet.rs @@ -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, /// 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, + /// 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, + /// 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, #[serde(flatten, deserialize_with = "feature_common")] pub common: FeatureCommon, } diff --git a/crates/engine/src/schema/sheet_tests.rs b/crates/engine/src/schema/sheet_tests.rs index 069904a051..1e1d50d3a8 100644 --- a/crates/engine/src/schema/sheet_tests.rs +++ b/crates/engine/src/schema/sheet_tests.rs @@ -64,7 +64,7 @@ fn schemas_and_the_key_check_agree() { "type", ]), ); - assert_keys(input_schema_for::(), &keys(&["body", "diameter", "margin", "pattern", "pitch"])); + assert_keys(input_schema_for::(), &keys(&["body", "diameter", "margin", "pattern", "pitch", "profiles", "sketch"])); assert_keys(input_schema_for::(), &keys(&["body", "edges", "flip", "gap", "length"])); assert_keys(input_schema_for::(), &keys(&["angle", "body", "curve", "fixed", "flip", "points", "position", "radius", "sketch"])); assert_keys(input_schema_for::(), &keys(&["body"])); diff --git a/crates/engine/src/view.rs b/crates/engine/src/view.rs index 31aa716585..ba35ea7189 100644 --- a/crates/engine/src/view.rs +++ b/crates/engine/src/view.rs @@ -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 Some(sketch.0), + | FeatureKind::Hole { points: Some(SketchPoints { sketch, single: false, .. }), .. } + | FeatureKind::SheetPerforate { sketch: Some(sketch), .. } => Some(sketch.0), _ => None, }) }