Skip to main content

geop_ops_edit/
delete.rs

1//! [`DeleteBody`]: delete whole bodies — solids, and sheets of faces
2//! standing on their own — from the part.
3
4use geop_core_editor::{
5    edit_form::{EditForm, FormState},
6    editable::{Context, OperationGroup, OperationInfo, OperationTier},
7    part_operation,
8    part_operation::PartOperation,
9};
10use geop_core_math::geop_error::GeopResult;
11use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
12use geop_ops_model::{Body, ModelOperation, NamedModel, build_model, resolve_bodies};
13use serde::{Deserialize, Serialize};
14
15/// Deletes the bodies `bodies` refers to, and everything of them: a solid
16/// by its name, a sheet — faces standing on their own, which have no name
17/// as a whole — by one of its faces. A face of a solid deletes the whole
18/// solid.
19///
20/// Creates nothing, so names nothing; the names of everything deleted are
21/// free again.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
23pub struct DeleteBody;
24
25#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
26pub struct DeleteBodyArgs {
27    /// The bodies to delete: solids, or a face of each sheet.
28    pub bodies: Vec<ElementReference<NamedModel, Body>>,
29}
30
31part_operation!(DeleteBody(DeleteBodyArgs));
32
33impl PartOperation for DeleteBody {
34    const KIND: &'static str = "delete_body";
35    const INFO: OperationInfo = OperationInfo::new(
36        "Delete body",
37        "Delete solids, and faces standing on their own.",
38        OperationGroup::Bodies,
39        OperationTier::Menu,
40    )
41    .icon(crate::icons::DELETE_BODY);
42    type Args = DeleteBodyArgs;
43    type Session = ();
44
45    /// Nothing picked yet: what to delete is never a guess.
46    fn new_args(_: &Context) -> DeleteBodyArgs {
47        DeleteBodyArgs::default()
48    }
49
50    fn run(ctx: &mut StepContext, args: &DeleteBodyArgs) -> GeopResult<()> {
51        let _step = ctx.step();
52        build_model::<Self>(ctx, args)
53    }
54
55    /// The bodies, picked.
56    fn form<'a>(
57        context: &'a Context<'a>,
58        args: &DeleteBodyArgs,
59        _: &FormState<()>,
60    ) -> EditForm<'a, DeleteBodyArgs> {
61        let mut f = EditForm::<DeleteBodyArgs>::default();
62        f.pick(
63            "bodies",
64            "bodies",
65            &context.before,
66            &args.bodies,
67            true,
68            |edit, picked| edit.args.bodies = picked,
69        );
70        f
71    }
72}
73
74impl ModelOperation for DeleteBody {
75    /// The bodies are resolved in the model as the steps before left it,
76    /// which `model` is a copy of.
77    fn build(
78        model: &mut NamedModel,
79        reader: &Reader,
80        _: &str,
81        args: &DeleteBodyArgs,
82    ) -> GeopResult<()> {
83        let bodies = resolve_bodies(reader, &args.bodies)?;
84        model.delete_bodies(&bodies)?;
85        Ok(())
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use geop_lib_sweep::shapes::{block, block_part};
93    use geop_ops_model::{model, testing::apply_to_model};
94
95    fn body(name: impl Into<String>) -> ElementReference<NamedModel, Body> {
96        ElementReference::new(model(), name)
97    }
98
99    fn two_cubes() -> NamedModel {
100        let mut part = block_part("a", [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
101        block(&mut part, "b", [2.0, 0.0, 0.0], [3.0, 1.0, 1.0]);
102        part
103    }
104
105    /// Deleting one of two solids leaves the other exactly as it was, and
106    /// no name of the deleted one behind.
107    #[test]
108    fn deletes_one_solid_and_keeps_the_other() {
109        let part = two_cubes();
110        let b = part.solid_names()[1].clone();
111        let kept_faces = part
112            .topology()
113            .solid_faces(part.solid_id(&part.solid_names()[0]).unwrap())
114            .unwrap()
115            .len();
116        let args = DeleteBodyArgs {
117            bodies: vec![body(b)],
118        };
119        let part = apply_to_model::<DeleteBody>(part, "del", &args).unwrap();
120        assert_eq!(part.solid_names().len(), 1);
121        assert_eq!(part.topology().faces.len(), kept_faces);
122        assert_eq!(part.topology().vertices.len(), 8);
123        part.check_names().unwrap();
124    }
125
126    /// A face of a solid stands for the whole solid, and two references
127    /// to one body delete it once.
128    #[test]
129    fn a_face_deletes_its_whole_body() {
130        let part = two_cubes();
131        let a = part.solid_id(&part.solid_names()[0]).unwrap();
132        let face = part.topology().solid_faces(a).unwrap()[0];
133        let face = part.name_of(face).unwrap().to_string();
134        let name = part.name_of(a).unwrap().to_string();
135        let args = DeleteBodyArgs {
136            bodies: vec![body(face), body(name)],
137        };
138        let part = apply_to_model::<DeleteBody>(part, "del", &args).unwrap();
139        assert_eq!(part.solid_names().len(), 1);
140        assert_eq!(part.topology().vertices.len(), 8);
141        part.check_names().unwrap();
142    }
143
144    /// Nothing picked, or a name of something that is no body — an edge —
145    /// is refused.
146    #[test]
147    fn refuses_what_is_no_body() {
148        let error = apply_to_model::<DeleteBody>(two_cubes(), "del", &DeleteBodyArgs::default())
149            .expect_err("refused");
150        assert!(error.root_message().contains("pick"), "{error:?}");
151        let part = two_cubes();
152        let edge = part
153            .topology()
154            .edges
155            .keys()
156            .copied()
157            .min_by_key(|e| e.0)
158            .unwrap();
159        let args = DeleteBodyArgs {
160            bodies: vec![body(part.name_of(edge).unwrap())],
161        };
162        let error = apply_to_model::<DeleteBody>(part, "del", &args).expect_err("refused");
163        assert!(error.root_message().contains("no body"), "{error:?}");
164    }
165}