Skip to main content

geop_ops_surface/
thicken.rs

1//! [`Thicken`]: a sheet made a solid of one thickness — the sheet shelled
2//! (see [`geop_lib_shell::thicken`]).
3
4use 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/// Which side of a sheet a [`Thicken`] adds its material on, as the sheet's
22/// faces' normals point.
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum ThickenSide {
26    /// Behind the sheet, against its normals.
27    #[default]
28    Against,
29    /// In front of it, along its normals.
30    Along,
31    /// Half on either side.
32    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/// Thickens the sheet the face named `face` stands in into a solid of walls
56/// `thickness` thick, on the `side` of it asked for, for the operation `T`:
57/// the sheet is consumed, the solid named `thicken(T)`.
58///
59/// On one side, the sheet's faces, edges and vertices keep their names and
60/// bound the solid on that side; the inner copy of each `X` is
61/// `thicken(T,X)`, and along a free edge `E` of the sheet the wall is
62/// `thicken(T,E,side)`, the straight edge from a vertex `V` of it to its
63/// copy `thicken(T,V,side)`. On both sides, the sheet is first copied
64/// half the thickness along its normals, each copy `X'` named
65/// `thicken(T,both,X)` — and then that is thickened behind its normals,
66/// named as above after `X'`.
67#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
68pub struct Thicken;
69
70#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
71pub struct ThickenArgs {
72    /// A face of the sheet to thicken.
73    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    /// The newest sheet, a tenth thick, behind its normals.
95    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    /// The sheet, picked, the thickness and the side.
112    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;