Skip to main content

geop_ops_shell/
operation.rs

1//! [`Shell`]: hollow a solid out to walls of one thickness — another for
2//! the faces picked for it — open where faces are picked.
3
4use 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/// Hollows the solid named `solid` out to walls `thickness` thick, open
25/// where its faces `faces` were — closed all round, with a void inside, if
26/// none is picked. The result replaces the solid and is named `shell(Scalar)`
27/// for the operation `Scalar`; what it is made of is named as
28/// [`geop_lib_shell::shell()`] says.
29#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
30pub struct Shell;
31
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
33pub struct ShellArgs {
34    /// The solid to hollow.
35    pub solid: ElementReference<NamedModel, Solid>,
36    /// The faces of it to take away, leaving openings.
37    #[serde(default)]
38    pub faces: Vec<ElementReference<NamedModel, Face>>,
39    /// How thick the walls are, measured inward from the solid's faces: a
40    /// number, or a formula of the part's parameters.
41    pub thickness: NumericInput,
42    /// Faces whose walls are `other_thickness` thick instead.
43    #[serde(default)]
44    pub other_faces: Vec<ElementReference<NamedModel, Face>>,
45    /// How thick the walls of `other_faces` are.
46    #[serde(default = "default_other_thickness")]
47    pub other_thickness: NumericInput,
48    /// Whether the walls grow outward from the faces instead: the solid
49    /// becomes the hollow.
50    #[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    /// The newest solid, closed all round, a tenth thick.
74    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    /// The solid and the faces to open it at, picked, and the thickness.
94    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                // The faces opened were of the solid before.
108                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        // Outward is a negative thickness to the shell.
179        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}