1use geop_core_editor::{
5 edit_form::{EditForm, FormState},
6 editable::{Context, OperationGroup, OperationInfo, OperationTier},
7 editing::{Number, Unit},
8 part_operation,
9 part_operation::PartOperation,
10};
11use geop_core_math::{
12 geop_error::{GeopError, GeopResult, WithContext},
13 scalars::Scalar,
14};
15use geop_core_program::{
16 element::ElementReference, numeric::NumericInput, part::StepContext, reader::Reader,
17};
18use geop_lib_shell::shell;
19use geop_ops_model::{
20 Face, ModelOperation, NamedModel, Namer, Solid, build_model, model, read_model,
21};
22use serde::{Deserialize, Serialize};
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
30pub struct Shell;
31
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33pub struct ShellArgs {
34 pub solid: ElementReference<NamedModel, Solid>,
36 #[serde(default)]
38 pub faces: Vec<ElementReference<NamedModel, Face>>,
39 pub thickness: NumericInput,
42 #[serde(default)]
44 pub other_faces: Vec<ElementReference<NamedModel, Face>>,
45 #[serde(default = "default_other_thickness")]
47 pub other_thickness: NumericInput,
48 #[serde(default)]
51 pub outward: bool,
52}
53
54fn default_other_thickness() -> NumericInput {
55 NumericInput::Plain(0.2)
56}
57
58part_operation!(Shell(ShellArgs));
59
60impl PartOperation for Shell {
61 const KIND: &'static str = "shell";
62 const INFO: OperationInfo = OperationInfo::new(
63 "Shell",
64 "Hollow a solid out to walls of one thickness, open where faces are picked.",
65 OperationGroup::Features,
66 OperationTier::Small,
67 )
68 .shortcut("Shift+H")
69 .icon(crate::icons::SHELL);
70 type Args = ShellArgs;
71 type Session = ();
72
73 fn new_args(context: &Context) -> ShellArgs {
75 let newest = read_model(&context.before)
76 .ok()
77 .and_then(|model| model.newest_solid());
78 ShellArgs {
79 solid: ElementReference::new(model(), newest.unwrap_or_default()),
80 faces: Vec::new(),
81 thickness: NumericInput::Plain(0.1),
82 other_faces: Vec::new(),
83 other_thickness: default_other_thickness(),
84 outward: false,
85 }
86 }
87
88 fn run(ctx: &mut StepContext, args: &ShellArgs) -> GeopResult<()> {
89 let _step = ctx.step();
90 build_model::<Self>(ctx, args)
91 }
92
93 fn form<'a>(
95 context: &'a Context<'a>,
96 args: &ShellArgs,
97 _: &FormState<()>,
98 ) -> EditForm<'a, ShellArgs> {
99 let before = &context.before;
100 let mut f = EditForm::<ShellArgs>::default();
101 f.pick_one(
102 "solid",
103 "solid",
104 before,
105 Some(&args.solid),
106 |edit, solid| {
107 if solid != edit.args.solid {
109 edit.args.faces.clear();
110 edit.args.other_faces.clear();
111 }
112 edit.args.solid = solid;
113 },
114 );
115 f.pick(
116 "faces",
117 "open faces",
118 before,
119 &args.faces,
120 true,
121 |edit, faces| edit.args.faces = faces,
122 );
123 f.form.dialog.optional("faces");
124 let thickness = Number::numeric(&args.thickness, "thickness", before, Unit::Length);
125 f.numeric("thickness", thickness.range(0.0, 1.0), |edit, thickness| {
126 edit.args.thickness = thickness
127 });
128 f.checkbox("outward", "walls outward", args.outward, |edit, b| {
129 edit.args.outward = b
130 });
131 f.pick(
132 "other_faces",
133 "faces of another thickness",
134 before,
135 &args.other_faces,
136 true,
137 |edit, faces| edit.args.other_faces = faces,
138 );
139 f.form.dialog.optional("other_faces");
140 if !args.other_faces.is_empty() {
141 let other = Number::numeric(
142 &args.other_thickness,
143 "their thickness",
144 before,
145 Unit::Length,
146 );
147 f.numeric(
148 "other_thickness",
149 other.range(0.0, 1.0),
150 |edit, thickness| edit.args.other_thickness = thickness,
151 );
152 }
153 f
154 }
155}
156
157impl ModelOperation for Shell {
158 fn build(
159 model: &mut NamedModel,
160 reader: &Reader,
161 step: &str,
162 args: &ShellArgs,
163 ) -> GeopResult<()> {
164 let namer = Namer::new("shell", step)?;
165 let thickness = args.thickness.resolve(reader).concerning("thickness")?;
166 if !thickness.is_finite() {
167 return Err(GeopError::new("the thickness is not a number").concerning("thickness"));
168 }
169 let solid = model.solid_id(args.solid.name()).concerning("solid")?;
170 let faces = (args.faces.iter())
171 .map(|face| model.face_id(face.name()))
172 .collect::<GeopResult<Vec<_>>>()
173 .concerning("faces")?;
174 let other = args.other_thickness.resolve(reader)?;
175 if !other.is_finite() {
176 return Err(GeopError::new("the other thickness is not a number"));
177 }
178 let way = |t: f64| Scalar::from_f64(if args.outward { -t } else { t });
180 let thicker = (args.other_faces.iter())
181 .map(|face| Ok((model.face_id(face.name())?, way(other))))
182 .collect::<GeopResult<Vec<_>>>()?;
183 shell(model, &namer, solid, &faces, way(thickness), &thicker)?;
184 Ok(())
185 }
186}