1use geop_core_editor::{
5 edit_form::{EditForm, FormState},
6 editable::{Context, OperationGroup, OperationInfo, OperationTier},
7 editing::{Choice, Number, Unit},
8 part_operation,
9 part_operation::PartOperation,
10};
11use geop_core_math::{
12 geop_error::{GeopError, GeopResult},
13 scalars::Scalar,
14};
15use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
16use geop_core_topology::Body;
17use geop_lib_shell::{offset_faces, thicken};
18use geop_ops_model::{Face, ModelOperation, NamedModel, Namer, build_model, model, read_model};
19use serde::{Deserialize, Serialize};
20
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum ThickenSide {
26 #[default]
28 Against,
29 Along,
31 Both,
33}
34
35impl ThickenSide {
36 const ALL: [ThickenSide; 3] = [ThickenSide::Against, ThickenSide::Along, ThickenSide::Both];
37
38 fn key(self) -> &'static str {
39 match self {
40 ThickenSide::Against => "against",
41 ThickenSide::Along => "along",
42 ThickenSide::Both => "both",
43 }
44 }
45
46 fn label(self) -> &'static str {
47 match self {
48 ThickenSide::Against => "behind the normal",
49 ThickenSide::Along => "along the normal",
50 ThickenSide::Both => "both sides",
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
68pub struct Thicken;
69
70#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
71pub struct ThickenArgs {
72 pub face: ElementReference<NamedModel, Face>,
74 pub thickness: f64,
75 #[serde(default)]
76 pub side: ThickenSide,
77}
78
79part_operation!(Thicken(ThickenArgs));
80
81impl PartOperation for Thicken {
82 const KIND: &'static str = "thicken";
83 const INFO: OperationInfo = OperationInfo::new(
84 "Thicken",
85 "Make a solid of a face standing on its own, a thickness on either side of it or on both.",
86 OperationGroup::Surface,
87 OperationTier::Menu,
88 )
89 .experimental()
90 .icon(crate::icons::THICKEN);
91 type Args = ThickenArgs;
92 type Session = ();
93
94 fn new_args(context: &Context) -> ThickenArgs {
96 let sheet = read_model(&context.before)
97 .ok()
98 .and_then(|model| model.sheet_face_names().pop());
99 ThickenArgs {
100 face: ElementReference::new(model(), sheet.unwrap_or_default()),
101 thickness: 0.1,
102 side: ThickenSide::Against,
103 }
104 }
105
106 fn run(ctx: &mut StepContext, args: &ThickenArgs) -> GeopResult<()> {
107 let _step = ctx.step();
108 build_model::<Self>(ctx, args)
109 }
110
111 fn form<'a>(
113 context: &'a Context<'a>,
114 args: &ThickenArgs,
115 _: &FormState<()>,
116 ) -> EditForm<'a, ThickenArgs> {
117 let mut f = EditForm::<ThickenArgs>::default();
118 f.pick_one(
119 "face",
120 "sheet",
121 &context.before,
122 Some(&args.face),
123 |edit, face| edit.args.face = face,
124 );
125 let thickness = Number::new("thickness", args.thickness, Unit::Length).range(0.0, 1.0);
126 f.number("thickness", thickness, |edit, t| edit.args.thickness = t);
127 let sides = (ThickenSide::ALL.iter())
128 .map(|s| Choice::new(s.key(), s.label()))
129 .collect();
130 f.select(
131 "side",
132 "side",
133 args.side.key(),
134 sides,
135 false,
136 |edit, key| {
137 if let Some(side) = ThickenSide::ALL.into_iter().find(|s| s.key() == key) {
138 edit.args.side = side;
139 }
140 },
141 );
142 f
143 }
144}
145
146impl ModelOperation for Thicken {
147 fn build(model: &mut NamedModel, _: &Reader, step: &str, args: &ThickenArgs) -> GeopResult<()> {
148 let namer = Namer::new("thicken", step)?;
149 if !args.thickness.is_finite() {
150 return Err(GeopError::new("the thickness is not a number"));
151 }
152 let sheet =
153 model.sheet_of_face(model.face_id(args.face.name())?, "a sheet is thickened")?;
154 let thickness = Scalar::from_f64(args.thickness);
155 match args.side {
156 ThickenSide::Against | ThickenSide::Along => {
157 let along = args.side == ThickenSide::Along;
158 thicken(model, &namer, sheet, thickness, along)?;
159 }
160 ThickenSide::Both => {
161 let faces = model.topology().body_faces(Body::Sheet(sheet))?;
162 let half = thickness.div(Scalar::TWO)?;
163 let copy = offset_faces(model, &namer.scoped("both"), &faces, half)?;
164 model.delete_bodies(&[Body::Sheet(sheet)])?;
165 thicken(model, &namer, copy.shells[0], thickness, false)?;
166 }
167 }
168 Ok(())
169 }
170}
171
172#[cfg(test)]
173pub(crate) mod tests;