1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
54pub struct SheetCut;
55
56pub type FlatFaceRef = ElementReference<NamedModel, Plane>;
58
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub struct SheetCutArgs {
61 pub sketch: SketchRef,
63 #[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 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 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 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
161fn 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 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
226fn 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 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}