Skip to main content

geop_ops_sheetmetal/
cut.rs

1//! [`SheetCut`]: holes and notches cut through a sheet-metal body along a
2//! sketch, across its bends unrolled.
3
4use geop_core_editor::{
5    edit_form::{EditForm, FormState},
6    editable::{Context, OperationGroup, OperationInfo, OperationTier},
7    part_operation,
8    part_operation::PartOperation,
9};
10use geop_core_geometry::nurb_curve::{NurbCurve, NurbCurve2D};
11use geop_core_math::{
12    geop_error::{GeopError, GeopResult, WithContext},
13    primitives::CoordinateSystem,
14    scalars::Scalar,
15    vector::Vector3,
16    with_context,
17};
18use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
19use geop_core_sketch::Shape;
20use geop_ops_model::sketch::shape_profiles;
21use geop_ops_model::sketch::{PlacedSketch, SketchRef};
22use geop_ops_model::{ModelOperation, NamedModel, Namer, Plane, read_model};
23use serde::{Deserialize, Serialize};
24
25use crate::{
26    body::{self, Made, Making, SheetBody, SheetStep, sheet_body},
27    sheet::{Cut, FlatEdge, Placement, Sheet},
28};
29
30/// Cuts through a sheet-metal body along a sketch, for the operation `C`:
31/// the body is consumed and rebuilt as `sheet_cut(C)`, every face, edge and
32/// vertex it had keeping its name except those the cut splits or takes
33/// away.
34///
35/// Every area of `sketch` is projected onto the flat face `face`, along
36/// its normal, and cut out straight through the sheet: a hole where it
37/// lies inside the face, a notch where it reaches over the face's outline,
38/// and where it reaches past the face into a bend — and on into the next
39/// flat — it is cut through the bend as the bend lies unrolled, so that
40/// the flat pattern shows it exactly as drawn. The cut is recorded with the
41/// body's bends: the body still unfolds.
42///
43/// The edges the cut makes are named after the sketch's pieces: piece `X`
44/// of sketch `K` gives `sheet_cut(C,K,X)` — `...,0`, `...,1` along it where
45/// the body's edges split it — with its edges `...,a` and `...,b` and its
46/// wall as [`crate::thicken`] says. An edge of the body the cut splits
47/// becomes `E,0`, `E,1`, ... along it.
48///
49/// Refused, by name: a sketch not parallel to the face, an area with a
50/// hole in it (whose island would fall out), a cut that crosses an edge at
51/// its end, touches one or runs along one, crosses a curve that is no line
52/// or arc, splits a face in two or takes one away, or misses the sheet.
53#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
54pub struct SheetCut;
55
56/// A flat face of the part, by name.
57pub type FlatFaceRef = ElementReference<NamedModel, Plane>;
58
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub struct SheetCutArgs {
61    /// The sketch: one area or several, none with a hole.
62    pub sketch: SketchRef,
63    /// A flat face of a sheet-metal body — either side — parallel to the
64    /// sketch. None: the flat face the sketch lies on.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub face: Option<FlatFaceRef>,
67}
68
69impl SheetStep for SheetCut {
70    fn plan(reader: &Reader, step: &str, args: &SheetCutArgs) -> GeopResult<Made> {
71        let ctx = with_context!("sheet_cut({step}, {args:?})");
72        let namer = Namer::new("sheet_cut", step)?;
73        let model = read_model(reader).with_context(ctx)?;
74        let placed = reader.retrieve_element(&args.sketch).with_context(ctx)?;
75        let face = args.face.as_ref().map(ElementReference::name);
76        let (solid, mut sheet, f) =
77            target(&model, reader, face, &placed.plane).with_context(ctx)?;
78        let loops = cut_loops(&namer, placed, &sheet.flats[f].place).with_context(ctx)?;
79        sheet.cuts.push(Cut {
80            flat: f,
81            loops,
82            name: namer.clone(),
83        });
84        Ok(Made {
85            consumes: Some(solid),
86            solid: namer.root(),
87            making: Making::Bent(sheet),
88        })
89    }
90}
91
92part_operation!(SheetCut(SheetCutArgs));
93
94impl PartOperation for SheetCut {
95    const KIND: &'static str = "sheet_cut";
96    const INFO: OperationInfo = OperationInfo::new(
97        "Sheet cut",
98        "Cut holes and notches through a sheet-metal body along a sketch, across its bends unrolled.",
99        OperationGroup::SheetMetal,
100        OperationTier::Small,
101    )
102    .icon(crate::icons::SHEET_CUT);
103    type Args = SheetCutArgs;
104    type Session = ();
105
106    /// The newest sketch, on the face it lies on.
107    fn new_args(context: &Context) -> SheetCutArgs {
108        let newest = context.before.targets_of::<PlacedSketch>().pop();
109        SheetCutArgs {
110            sketch: SketchRef::whole(newest.unwrap_or_else(|| geop_ops_model::sketch::sketch(""))),
111            face: None,
112        }
113    }
114
115    fn run(ctx: &mut StepContext, args: &SheetCutArgs) -> GeopResult<()> {
116        let _step = ctx.step();
117        body::run::<Self>(ctx, args)
118    }
119
120    /// The sketch, picked, and the face, if not the one the sketch lies on.
121    fn form<'a>(
122        context: &'a Context<'a>,
123        args: &SheetCutArgs,
124        _: &FormState<()>,
125    ) -> EditForm<'a, SheetCutArgs> {
126        let before = &context.before;
127        let mut f = EditForm::<SheetCutArgs>::default();
128        f.pick_one(
129            "sketch",
130            "sketch",
131            before,
132            Some(&args.sketch),
133            |edit, sketch| edit.args.sketch = sketch,
134        );
135        let face = args.face.as_slice();
136        f.pick(
137            "face",
138            "onto face",
139            before,
140            face,
141            false,
142            |edit, mut face| edit.args.face = face.pop(),
143        );
144        // The face the sketch lies on, unless one is picked.
145        f.form.dialog.optional("face");
146        f
147    }
148}
149
150impl ModelOperation for SheetCut {
151    fn build(
152        model: &mut NamedModel,
153        reader: &Reader,
154        step: &str,
155        _: &SheetCutArgs,
156    ) -> GeopResult<()> {
157        body::build(model, reader, step)
158    }
159}
160
161/// The sheet-metal body to cut, its sheet, and the flat the sketch on the
162/// plane `plane` is projected onto: the one with the face `face`, or, if
163/// none is named, the one the plane lies on.
164fn target(
165    model: &NamedModel,
166    reader: &Reader,
167    face: Option<&str>,
168    plane: &CoordinateSystem,
169) -> GeopResult<(String, Sheet, usize)> {
170    let parallel = |place: &Placement| {
171        plane
172            .w()
173            .prod_cross(&place.n)
174            .norm()
175            .could_be_equal(Scalar::ZERO)
176    };
177    let mut found = Vec::new();
178    for solid in model.solid_names() {
179        let Some(sheet) = sheet_body(reader, &solid).and_then(SheetBody::bent) else {
180            continue;
181        };
182        for (f, flat) in sheet.flats.iter().enumerate() {
183            let Some(face) = face else {
184                // On the A side's plane or the B side's.
185                let t = Scalar::from_f64(sheet.rules.thickness);
186                let on = |place: &Placement| {
187                    parallel(place)
188                        && plane
189                            .origin()
190                            .sub(&place.origin)
191                            .prod_dot(&place.n)
192                            .could_be_equal(Scalar::ZERO)
193                };
194                if on(&flat.place) || on(&flat.place.offset(t)) {
195                    found.push((solid.clone(), sheet.clone(), f));
196                }
197                continue;
198            };
199            if flat.name.name(&["a"]) == face || flat.name.name(&["b"]) == face {
200                if !parallel(&flat.place) {
201                    return Err(GeopError::new(format!(
202                        "the sketch's plane is not parallel to face {face}: a sketch is cut through a sheet along the normal of a flat face it is parallel to"
203                    )));
204                }
205                return Ok((solid.clone(), sheet.clone(), f));
206            }
207        }
208    }
209    if let Some(face) = face {
210        return Err(GeopError::new(format!(
211            "{face:?} is no flat face of a sheet-metal body: a cut is projected onto one of those"
212        )));
213    }
214    match found.len() {
215        1 => Ok(found.remove(0)),
216        0 => Err(GeopError::new(
217            "the sketch lies on no flat face of a sheet-metal body: pick the face to project it onto",
218        )),
219        _ => Err(GeopError::new(format!(
220            "the sketch lies on {} flat faces of sheet-metal bodies: pick the face to project it onto",
221            found.len()
222        ))),
223    }
224}
225
226/// The areas of the sketch `placed` projected into the sheet coordinates of
227/// the flat at `place`: each a loop counter-clockwise there, its curves
228/// named `C,K,X`.
229fn cut_loops(
230    namer: &Namer,
231    placed: &PlacedSketch,
232    place: &Placement,
233) -> GeopResult<Vec<Vec<FlatEdge>>> {
234    let (name, sketch) = (&placed.name, &placed.sketch);
235    let geometry = sketch.enclose()?;
236    let plane = &placed.plane;
237    // Sketch coordinates to sheet coordinates: affine, so exact on the
238    // homogeneous control points.
239    let d = plane.origin().sub(&place.origin);
240    let row = |e: &Vector3| [plane.u().prod_dot(e), plane.v().prod_dot(e), d.prod_dot(e)];
241    let m = [row(&place.e1), row(&place.e2)];
242    let flips = m[0][0]
243        .mul(m[1][1])
244        .sub(m[0][1].mul(m[1][0]))
245        .definitely_less(Scalar::ZERO);
246    let map = |curve: &NurbCurve2D| -> GeopResult<NurbCurve2D> {
247        let control_points = curve
248            .control_points
249            .iter()
250            .map(|cp| {
251                let image =
252                    |r: &[Scalar; 3]| r[0].mul(cp[0]).add(r[1].mul(cp[1])).add(r[2].mul(cp[2]));
253                Vector3::from_array([image(&m[0]), image(&m[1]), cp[2]])
254            })
255            .collect();
256        let mapped = NurbCurve::try_new(curve.degree, control_points, curve.knot_vector.clone())?;
257        Ok(if flips { mapped.reverse() } else { mapped })
258    };
259    let mut loops = Vec::new();
260    for region in sketch.regions()? {
261        if !region.holes.is_empty() {
262            return Err(GeopError::new(format!(
263                "an area of sketch {name} has a hole in it: cut out, the island inside would fall out — draw the area without it"
264            )));
265        }
266        for lp in shape_profiles(name, sketch, &geometry, Shape::Regions(vec![region]))? {
267            let mut edges = lp
268                .curves
269                .iter()
270                .zip(&lp.curve_names)
271                .map(|(curve, piece)| {
272                    Ok(FlatEdge {
273                        curve: map(curve)?,
274                        name: namer.scoped(piece),
275                    })
276                })
277                .collect::<GeopResult<Vec<_>>>()?;
278            if flips {
279                edges.reverse();
280            }
281            loops.push(edges);
282        }
283    }
284    Ok(loops)
285}