1use geop_core_editor::{
5 edit_form::{EditForm, FormState, choices, chosen},
6 editable::{Context, OperationGroup, OperationInfo, OperationTier},
7 editing::{Number, Unit},
8 part_operation,
9 part_operation::PartOperation,
10};
11use geop_core_geometry::shape::Plane;
12use geop_core_math::{
13 geop_error::{GeopError, GeopResult, WithContext},
14 scalars::Scalar,
15};
16use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
17use geop_core_sketch::Shape;
18use geop_core_topology::{EdgeId, FaceId};
19use geop_ops_model::sketch::shape_profiles;
20use geop_ops_model::sketch::{PlacedSketch, SketchPlane, SketchRef};
21use geop_ops_model::{
22 Edge, Face, ModelOperation, NamedModel, Namer, build_model, model, read_model,
23};
24use geop_ops_model::{SolidRef, solid};
25use serde::{Deserialize, Serialize};
26
27use crate::{
28 draft::draft,
29 lip::{LipSize, groove, lip},
30 rib::{Growth, rib},
31};
32
33const fn info(label: &'static str, doc: &'static str) -> OperationInfo {
36 OperationInfo::new(label, doc, OperationGroup::Features, OperationTier::Menu).experimental()
37}
38
39type FaceRef = ElementReference<NamedModel, Face>;
41
42type EdgeRef = ElementReference<NamedModel, Edge>;
44
45fn finite(what: &str, value: f64) -> GeopResult<()> {
47 if value.is_finite() {
48 Ok(())
49 } else {
50 Err(GeopError::new(format!("the {what} is not a number")))
51 }
52}
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
62pub struct Draft;
63
64#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
65pub struct DraftArgs {
66 pub faces: Vec<FaceRef>,
68 pub neutral: Option<SketchPlane>,
71 pub angle: f64,
73 #[serde(default)]
75 pub reversed: bool,
76}
77
78part_operation!(
79 Draft(DraftArgs),
80 Lip(LipArgs),
81 Groove(GrooveArgs),
82 Rib(RibArgs)
83);
84
85impl PartOperation for Draft {
86 const KIND: &'static str = "draft";
87 const INFO: OperationInfo = info(
88 "Draft",
89 "Tilt planar faces about a neutral plane, so the part comes out of its mould.",
90 )
91 .icon(crate::icons::DRAFT);
92 type Args = DraftArgs;
93 type Session = ();
94
95 fn new_args(_: &Context) -> DraftArgs {
97 DraftArgs {
98 faces: Vec::new(),
99 neutral: None,
100 angle: 3.0,
101 reversed: false,
102 }
103 }
104
105 fn run(ctx: &mut StepContext, args: &DraftArgs) -> GeopResult<()> {
106 let _step = ctx.step();
107 build_model::<Self>(ctx, args)
108 }
109
110 fn form<'a>(
113 context: &'a Context<'a>,
114 args: &DraftArgs,
115 _: &FormState<()>,
116 ) -> EditForm<'a, DraftArgs> {
117 let before = &context.before;
118 let mut f = EditForm::<DraftArgs>::default();
119 f.pick(
120 "faces",
121 "faces",
122 before,
123 &args.faces,
124 true,
125 |edit, faces| edit.args.faces = faces,
126 );
127 f.pick_one(
128 "neutral",
129 "neutral plane",
130 before,
131 args.neutral.as_ref(),
132 |edit, neutral| edit.args.neutral = Some(neutral),
133 );
134 let angle = Number::new("angle", args.angle, Unit::Angle).range(-45.0, 45.0);
135 f.number("angle", angle, |edit, angle| edit.args.angle = angle);
136 f.checkbox(
137 "reversed",
138 "reverse pull direction",
139 args.reversed,
140 |edit, b| edit.args.reversed = b,
141 );
142 f
143 }
144}
145
146impl ModelOperation for Draft {
147 fn build(
148 model: &mut NamedModel,
149 reader: &Reader,
150 step: &str,
151 args: &DraftArgs,
152 ) -> GeopResult<()> {
153 let namer = Namer::new("draft", step)?;
154 let faces = (args.faces.iter())
155 .map(|face| model.face_id(face.name()))
156 .collect::<GeopResult<Vec<_>>>()?;
157 let Some(neutral) = &args.neutral else {
158 return Err(GeopError::new("pick the neutral plane"));
159 };
160 let frame = neutral.resolve(reader)?;
161 let into_solid = matches!(neutral, SketchPlane::Face(_));
163 let pull = if into_solid != args.reversed {
164 frame.w().neg()
165 } else {
166 *frame.w()
167 };
168 let plane = Plane::try_new(*frame.origin(), pull)?;
169 finite("angle", args.angle)?;
170 let angle = Scalar::from_f64(args.angle.to_radians());
171 draft(model, &namer, &faces, &plane, angle)?;
172 Ok(())
173 }
174}
175
176fn rim_fields<'a, A: 'a>(
181 f: &mut EditForm<'a, A>,
182 before: &Reader<'a>,
183 face: &FaceRef,
184 edges: &[EdgeRef],
185 rim: fn(&mut A) -> (&mut FaceRef, &mut Vec<EdgeRef>),
186) {
187 f.pick_one(
188 "face",
189 "rim face",
190 before,
191 Some(face),
192 move |edit, picked| {
193 let (face, edges) = rim(edit.args);
194 if picked != *face {
195 edges.clear();
196 }
197 *face = picked;
198 },
199 );
200 f.pick(
201 "edges",
202 "edges",
203 before,
204 edges,
205 true,
206 move |edit, picked| *rim(edit.args).1 = picked,
207 );
208 f.form.dialog.optional("edges");
209}
210
211fn resolve_rim(
213 model: &NamedModel,
214 face: &FaceRef,
215 edges: &[EdgeRef],
216) -> GeopResult<(FaceId, Vec<EdgeId>)> {
217 if face.name().is_empty() {
218 return Err(GeopError::new("pick the rim face"));
219 }
220 Ok((
221 model.face_id(face.name())?,
222 (edges.iter())
223 .map(|edge| model.edge_id(edge.name()))
224 .collect::<GeopResult<_>>()?,
225 ))
226}
227
228#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
236pub struct Lip;
237
238#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
239pub struct LipArgs {
240 pub face: FaceRef,
242 #[serde(default)]
244 pub edges: Vec<EdgeRef>,
245 pub width: f64,
246 pub height: f64,
247}
248
249impl PartOperation for Lip {
250 const KIND: &'static str = "lip";
251 const INFO: OperationInfo = info(
252 "Lip",
253 "Raise a lip along the rim of one half of an enclosure.",
254 )
255 .icon(crate::icons::LIP);
256 type Args = LipArgs;
257 type Session = ();
258
259 fn new_args(_: &Context) -> LipArgs {
261 LipArgs {
262 face: ElementReference::new(model(), ""),
263 edges: Vec::new(),
264 width: 0.05,
265 height: 0.1,
266 }
267 }
268
269 fn run(ctx: &mut StepContext, args: &LipArgs) -> GeopResult<()> {
270 let _step = ctx.step();
271 build_model::<Self>(ctx, args)
272 }
273
274 fn form<'a>(
276 context: &'a Context<'a>,
277 args: &LipArgs,
278 _: &FormState<()>,
279 ) -> EditForm<'a, LipArgs> {
280 let mut f = EditForm::<LipArgs>::default();
281 rim_fields(&mut f, &context.before, &args.face, &args.edges, |a| {
282 (&mut a.face, &mut a.edges)
283 });
284 let length = |label: &str, value: f64| Number::new(label, value, Unit::Length);
285 f.number(
286 "width",
287 length("width", args.width).range(0.0, 1.0),
288 |edit, w| edit.args.width = w,
289 );
290 f.number(
291 "height",
292 length("height", args.height).range(0.0, 1.0),
293 |edit, h| edit.args.height = h,
294 );
295 f
296 }
297}
298
299impl ModelOperation for Lip {
300 fn build(model: &mut NamedModel, _: &Reader, step: &str, args: &LipArgs) -> GeopResult<()> {
301 let namer = Namer::new("lip", step)?;
302 let (face, edges) = resolve_rim(model, &args.face, &args.edges)?;
303 finite("width", args.width)?;
304 finite("height", args.height)?;
305 let size = LipSize {
306 width: Scalar::from_f64(args.width),
307 height: Scalar::from_f64(args.height),
308 };
309 lip(model, &namer, step, face, &edges, size)
310 }
311}
312
313#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
319pub struct Groove;
320
321#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
322pub struct GrooveArgs {
323 pub face: FaceRef,
325 #[serde(default)]
327 pub edges: Vec<EdgeRef>,
328 pub width: f64,
330 pub height: f64,
332 pub clearance: f64,
334}
335
336impl PartOperation for Groove {
337 const KIND: &'static str = "groove";
338 const INFO: OperationInfo = info(
339 "Groove",
340 "Cut the groove that takes a lip into the rim of the other half.",
341 )
342 .icon(crate::icons::GROOVE);
343 type Args = GrooveArgs;
344 type Session = ();
345
346 fn new_args(_: &Context) -> GrooveArgs {
348 GrooveArgs {
349 face: ElementReference::new(model(), ""),
350 edges: Vec::new(),
351 width: 0.05,
352 height: 0.1,
353 clearance: 0.01,
354 }
355 }
356
357 fn run(ctx: &mut StepContext, args: &GrooveArgs) -> GeopResult<()> {
358 let _step = ctx.step();
359 build_model::<Self>(ctx, args)
360 }
361
362 fn form<'a>(
365 context: &'a Context<'a>,
366 args: &GrooveArgs,
367 _: &FormState<()>,
368 ) -> EditForm<'a, GrooveArgs> {
369 let mut f = EditForm::<GrooveArgs>::default();
370 rim_fields(&mut f, &context.before, &args.face, &args.edges, |a| {
371 (&mut a.face, &mut a.edges)
372 });
373 let length = |label: &str, value: f64| Number::new(label, value, Unit::Length);
374 f.number(
375 "width",
376 length("lip width", args.width).range(0.0, 1.0),
377 |edit, w| edit.args.width = w,
378 );
379 f.number(
380 "height",
381 length("lip height", args.height).range(0.0, 1.0),
382 |edit, h| edit.args.height = h,
383 );
384 let clearance = length("clearance", args.clearance).range(0.0, 0.1);
385 f.number("clearance", clearance, |edit, c| edit.args.clearance = c);
386 f
387 }
388}
389
390impl ModelOperation for Groove {
391 fn build(model: &mut NamedModel, _: &Reader, step: &str, args: &GrooveArgs) -> GeopResult<()> {
392 let namer = Namer::new("groove", step)?;
393 let (face, edges) = resolve_rim(model, &args.face, &args.edges)?;
394 finite("width", args.width)?;
395 finite("height", args.height)?;
396 finite("clearance", args.clearance)?;
397 let size = LipSize {
398 width: Scalar::from_f64(args.width),
399 height: Scalar::from_f64(args.height),
400 };
401 let clearance = Scalar::from_f64(args.clearance);
402 groove(model, &namer, step, face, &edges, size, clearance)
403 }
404}
405
406#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
410#[serde(rename_all = "snake_case")]
411pub enum RibSide {
412 #[default]
413 Symmetric,
414 First,
415 Second,
416}
417
418impl RibSide {
419 const ALL: [(RibSide, &'static str, &'static str); 3] = [
420 (RibSide::Symmetric, "symmetric", "Symmetric"),
421 (RibSide::First, "first", "First side"),
422 (RibSide::Second, "second", "Second side"),
423 ];
424}
425
426#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(rename_all = "snake_case")]
430pub enum RibDirection {
431 #[default]
432 Parallel,
433 Normal,
434}
435
436impl RibDirection {
437 const ALL: [(RibDirection, &'static str, &'static str); 2] = [
438 (RibDirection::Parallel, "parallel", "Parallel to sketch"),
439 (RibDirection::Normal, "normal", "Normal to sketch"),
440 ];
441}
442
443#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
452pub struct Rib;
453
454#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
455pub struct RibArgs {
456 pub sketch: SketchRef,
458 pub solid: SolidRef,
460 pub thickness: f64,
461 #[serde(default)]
462 pub side: RibSide,
463 #[serde(default)]
464 pub direction: RibDirection,
465 #[serde(default)]
467 pub flipped: bool,
468}
469
470impl PartOperation for Rib {
471 const KIND: &'static str = "rib";
472 const INFO: OperationInfo = info(
473 "Rib",
474 "Grow a thin wall from an open sketch profile up to a solid's faces, and join it.",
475 )
476 .icon(crate::icons::RIB);
477 type Args = RibArgs;
478 type Session = ();
479
480 fn new_args(context: &Context) -> RibArgs {
483 let before = &context.before;
484 let newest = before.targets_of::<PlacedSketch>().pop();
485 let solid_name = read_model(before)
486 .ok()
487 .and_then(|model| model.newest_solid());
488 RibArgs {
489 sketch: SketchRef::whole(newest.unwrap_or_else(|| geop_ops_model::sketch::sketch(""))),
490 solid: solid(solid_name.unwrap_or_default()),
491 thickness: 0.05,
492 side: RibSide::Symmetric,
493 direction: RibDirection::Parallel,
494 flipped: false,
495 }
496 }
497
498 fn run(ctx: &mut StepContext, args: &RibArgs) -> GeopResult<()> {
499 let _step = ctx.step();
500 build_model::<Self>(ctx, args)
501 }
502
503 fn form<'a>(
506 context: &'a Context<'a>,
507 args: &RibArgs,
508 _: &FormState<()>,
509 ) -> EditForm<'a, RibArgs> {
510 let before = &context.before;
511 let mut f = EditForm::<RibArgs>::default();
512 f.pick_one(
513 "sketch",
514 "sketch",
515 before,
516 Some(&args.sketch),
517 |edit, sketch| edit.args.sketch = sketch,
518 );
519 f.pick_one(
520 "solid",
521 "solid",
522 before,
523 Some(&args.solid),
524 |edit, solid| edit.args.solid = solid,
525 );
526 let thickness = Number::new("thickness", args.thickness, Unit::Length).range(0.0, 1.0);
527 f.number("thickness", thickness, |edit, t| edit.args.thickness = t);
528 let (side, sides) = choices(&RibSide::ALL, &args.side);
529 f.select(
530 "side",
531 "thickness side",
532 side,
533 sides,
534 false,
535 |edit, value| {
536 if let Some(side) = chosen(&RibSide::ALL, value) {
537 edit.args.side = side;
538 }
539 },
540 );
541 let (direction, directions) = choices(&RibDirection::ALL, &args.direction);
542 f.select(
543 "direction",
544 "direction",
545 direction,
546 directions,
547 false,
548 |edit, value| {
549 if let Some(direction) = chosen(&RibDirection::ALL, value) {
550 edit.args.direction = direction;
551 }
552 },
553 );
554 f.checkbox("flipped", "flip direction", args.flipped, |edit, b| {
555 edit.args.flipped = b
556 });
557 f
558 }
559}
560
561impl ModelOperation for Rib {
562 fn build(
563 model: &mut NamedModel,
564 reader: &Reader,
565 step: &str,
566 args: &RibArgs,
567 ) -> GeopResult<()> {
568 let namer = Namer::new("rib", step)?;
569 let placed = reader.retrieve_element(&args.sketch).concerning("sketch")?;
570 let target = model.solid_id(args.solid.name())?;
571 let sketch = &placed.sketch;
572 let geometry = sketch.enclose()?;
573 let chain = match sketch.shape()? {
574 chain @ Shape::Chain(_) => chain,
575 Shape::Regions(_) => {
576 return Err(GeopError::new(format!(
577 "sketch {:?} encloses an area: a rib grows from an open profile",
578 placed.name
579 )));
580 }
581 };
582 let profile = shape_profiles(&placed.name, sketch, &geometry, chain)?.remove(0);
583 finite("thickness", args.thickness)?;
584 let t = args.thickness;
585 let (near, far) = match args.side {
586 RibSide::Symmetric => (-t / 2.0, t / 2.0),
587 RibSide::First => (0.0, t),
588 RibSide::Second => (-t, 0.0),
589 };
590 let thickness = (Scalar::from_f64(near), Scalar::from_f64(far));
591 let growth = match args.direction {
592 RibDirection::Normal => Growth::Normal {
593 flipped: args.flipped,
594 },
595 RibDirection::Parallel => Growth::Parallel {
596 flipped: args.flipped,
597 },
598 };
599 rib(
600 model,
601 &namer,
602 step,
603 &placed.plane,
604 &profile,
605 target,
606 thickness,
607 growth,
608 )
609 }
610}