Skip to main content

geop_ops_boolean/
operation.rs

1//! [`Boolean`]: combine two solids. [`Split`]: cut a solid into pieces with
2//! a face standing on its own. [`Imprint`]: divide two bodies where they
3//! cross. [`Section`]: the curves where they cross, as a wire.
4
5use geop_core_editor::{
6    edit_form::{EditForm, FormState},
7    editable::{Context, OperationGroup, OperationInfo, OperationTier},
8    one_of, part_operation,
9    part_operation::PartOperation,
10};
11use geop_core_math::{
12    geop_error::{GeopError, GeopResult, WithContext},
13    primitives::FrameAxis,
14};
15use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
16use geop_lib_booleans::{
17    RemeshParams, boolean, imprint, imprint_on_face, section, section_by_plane, split,
18};
19use geop_ops_model::{
20    Body, BooleanOp, Face, ModelOperation, NamedModel, Namer, Plane, SolidRef, build_model,
21    datums::{Datums, origin_plane},
22    element, model, read_model, solid,
23};
24use serde::{Deserialize, Serialize};
25
26/// Combines the solids named `a` and `b` into one solid named `boolean(B)`
27/// for the operation `B`, consuming both.
28///
29/// Every face, edge and vertex that survives keeps its name. What the
30/// boolean creates is named after what it was made from, see
31/// `geop_lib_booleans` — for example `boolean(B,E1,E2,i,n)` for the
32/// `i`-th of the `n` points where edges `E1` and `E2` cross.
33///
34/// An empty result — intersecting solids that don't overlap, say — is an
35/// answer, not an error: both operands are consumed and no solid is named
36/// `boolean(B)`.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
38pub struct Boolean;
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct BooleanArgs {
42    /// The first solid; for a difference, the one cut from.
43    pub a: SolidRef,
44    /// The second solid; for a difference, the one cut away.
45    pub b: SolidRef,
46    /// How to combine them.
47    pub op: BooleanOp,
48}
49
50part_operation!(
51    Boolean(BooleanArgs),
52    Split(SplitArgs),
53    Imprint(ImprintArgs),
54    Section(SectionArgs)
55);
56
57impl PartOperation for Boolean {
58    const KIND: &'static str = "boolean";
59    const INFO: OperationInfo = OperationInfo::new(
60        "Boolean",
61        "Unite, intersect or subtract two solids.",
62        OperationGroup::Bodies,
63        OperationTier::Big,
64    )
65    .shortcut("B")
66    .icon(crate::icons::BOOLEAN);
67    type Args = BooleanArgs;
68    type Session = ();
69
70    /// The difference of the two newest solids: the older cut by the one
71    /// just built as a tool.
72    fn new_args(context: &Context) -> BooleanArgs {
73        let [a, b] = newest_two(context);
74        BooleanArgs {
75            a: solid(a),
76            b: solid(b),
77            op: BooleanOp::Difference,
78        }
79    }
80
81    fn run(ctx: &mut StepContext, args: &BooleanArgs) -> GeopResult<()> {
82        let _step = ctx.step();
83        build_model::<Self>(ctx, args)
84    }
85
86    /// The two solids, picked, and how to combine them.
87    fn form<'a>(
88        context: &'a Context<'a>,
89        args: &BooleanArgs,
90        _: &FormState<()>,
91    ) -> EditForm<'a, BooleanArgs> {
92        let mut f = EditForm::<BooleanArgs>::default();
93        let before = &context.before;
94        f.pick_one("a", "a", before, Some(&args.a), |edit, a| edit.args.a = a)
95            .pick_one("b", "b", before, Some(&args.b), |edit, b| edit.args.b = b)
96            .select(
97                "op",
98                "op",
99                args.op.value(),
100                BooleanOp::choices(),
101                false,
102                |edit, choice| {
103                    if let Some(op) = BooleanOp::from_value(choice) {
104                        edit.args.op = op;
105                    }
106                },
107            );
108        f
109    }
110}
111
112impl ModelOperation for Boolean {
113    fn build(model: &mut NamedModel, _: &Reader, step: &str, args: &BooleanArgs) -> GeopResult<()> {
114        let namer = Namer::new("boolean", step)?;
115        let a = model.solid_id(args.a.name())?;
116        let b = model.solid_id(args.b.name())?;
117        boolean(model, &namer, a, b, args.op, RemeshParams::default())?;
118        Ok(())
119    }
120}
121
122/// Cuts the solid named `solid` into pieces with the face named `face` —
123/// a face standing on its own, such as an extrude or revolve builds as a
124/// face, and with it every face of its sheet. The pieces are named
125/// `split(Scalar,0)`, `split(Scalar,1)`, ... for the operation `Scalar`, the solid is
126/// consumed, and the face is left as it is (see [`geop_lib_booleans::split`]).
127#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
128pub struct Split;
129
130#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
131pub struct SplitArgs {
132    /// The solid to cut.
133    pub solid: SolidRef,
134    /// A face of the sheet to cut it with.
135    pub face: ElementReference<NamedModel, Face>,
136}
137
138impl PartOperation for Split {
139    const KIND: &'static str = "split";
140    const INFO: OperationInfo = OperationInfo::new(
141        "Split",
142        "Cut a solid into pieces with a face standing on its own.",
143        OperationGroup::Bodies,
144        OperationTier::Menu,
145    )
146    .shortcut("Shift+B")
147    .icon(crate::icons::SPLIT);
148    type Args = SplitArgs;
149    type Session = ();
150
151    /// The newest solid, cut by the newest face standing on its own.
152    fn new_args(context: &Context) -> SplitArgs {
153        let before = read_model(&context.before).unwrap_or_default();
154        SplitArgs {
155            solid: solid(before.newest_solid().unwrap_or_default()),
156            face: ElementReference::new(
157                model(),
158                before.sheet_face_names().pop().unwrap_or_default(),
159            ),
160        }
161    }
162
163    fn run(ctx: &mut StepContext, args: &SplitArgs) -> GeopResult<()> {
164        let _step = ctx.step();
165        build_model::<Self>(ctx, args)
166    }
167
168    /// The solid and the face, picked.
169    fn form<'a>(
170        context: &'a Context<'a>,
171        args: &SplitArgs,
172        _: &FormState<()>,
173    ) -> EditForm<'a, SplitArgs> {
174        let mut f = EditForm::<SplitArgs>::default();
175        let before = &context.before;
176        f.pick_one(
177            "solid",
178            "solid",
179            before,
180            Some(&args.solid),
181            |edit, solid| edit.args.solid = solid,
182        )
183        .pick_one("face", "face", before, Some(&args.face), |edit, face| {
184            edit.args.face = face
185        });
186        f
187    }
188}
189
190impl ModelOperation for Split {
191    fn build(model: &mut NamedModel, _: &Reader, step: &str, args: &SplitArgs) -> GeopResult<()> {
192        let namer = Namer::new("split", step)?;
193        let solid = model.solid_id(args.solid.name())?;
194        let face = model.face_id(args.face.name())?;
195        let sheet = model.sheet_of_face(
196            face,
197            "cut with a face standing on its own, as an extrude or revolve builds in face mode",
198        )?;
199        split(model, &namer, solid, sheet, RemeshParams::default())?;
200        Ok(())
201    }
202}
203
204/// Imprints the body `tool` onto the body `body`, keeping both: every face
205/// of each is divided along the curves where the other's boundary crosses
206/// it, and nothing is taken away. With `face`, only that face of `body` is
207/// divided, and `tool` stays as it is (see
208/// [`geop_lib_booleans::imprint_on_face`]).
209///
210/// What the imprint creates is named as a boolean names it, `imprint(I,..)`
211/// for the operation `I`; `body`'s edges along the curves keep those names,
212/// and `tool`'s copies of them are named `imprint(I,X)` for the name `X`
213/// they share (see [`geop_lib_booleans::imprint`]).
214#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
215pub struct Imprint;
216
217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
218pub struct ImprintArgs {
219    /// The body imprinted on: a solid, or a face of a sheet.
220    pub body: ElementReference<NamedModel, Body>,
221    /// The body imprinted with.
222    pub tool: ElementReference<NamedModel, Body>,
223    /// Only this face of `body` is divided, if given.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub face: Option<ElementReference<NamedModel, Face>>,
226}
227
228/// The newest solid but one and the newest: the solids a step combining
229/// two is most likely about. Empty where the part has fewer.
230fn newest_two(context: &Context) -> [String; 2] {
231    let solids = (read_model(&context.before).map(|model| model.solid_names())).unwrap_or_default();
232    let newest = |back: usize| {
233        (solids.len().checked_sub(back))
234            .and_then(|i| solids.get(i).cloned())
235            .unwrap_or_default()
236    };
237    [newest(2), newest(1)]
238}
239
240impl PartOperation for Imprint {
241    const KIND: &'static str = "imprint";
242    const INFO: OperationInfo = OperationInfo::new(
243        "Imprint",
244        "Divide two bodies where they cross, keeping both.",
245        OperationGroup::Bodies,
246        OperationTier::Menu,
247    )
248    .icon(crate::icons::IMPRINT);
249    type Args = ImprintArgs;
250    type Session = ();
251
252    /// The two newest solids, the newer imprinted on the older.
253    fn new_args(context: &Context) -> ImprintArgs {
254        let [body, tool] = newest_two(context);
255        ImprintArgs {
256            body: element(body),
257            tool: element(tool),
258            face: None,
259        }
260    }
261
262    fn run(ctx: &mut StepContext, args: &ImprintArgs) -> GeopResult<()> {
263        let _step = ctx.step();
264        build_model::<Self>(ctx, args)
265    }
266
267    /// The two bodies, picked, and the one face to divide, if only one.
268    fn form<'a>(
269        context: &'a Context<'a>,
270        args: &ImprintArgs,
271        _: &FormState<()>,
272    ) -> EditForm<'a, ImprintArgs> {
273        let mut f = EditForm::<ImprintArgs>::default();
274        let before = &context.before;
275        f.pick_one("body", "body", before, Some(&args.body), |edit, body| {
276            edit.args.body = body
277        })
278        .pick_one("tool", "tool", before, Some(&args.tool), |edit, tool| {
279            edit.args.tool = tool
280        })
281        .pick(
282            "face",
283            "only face",
284            before,
285            args.face.as_slice(),
286            false,
287            |edit, mut face| edit.args.face = face.pop(),
288        );
289        f
290    }
291}
292
293impl ModelOperation for Imprint {
294    fn build(
295        model: &mut NamedModel,
296        reader: &Reader,
297        step: &str,
298        args: &ImprintArgs,
299    ) -> GeopResult<()> {
300        let namer = Namer::new("imprint", step)?;
301        let body = reader.retrieve_element(&args.body).concerning("body")?;
302        let tool = reader.retrieve_element(&args.tool).concerning("tool")?;
303        if body == tool {
304            return Err(GeopError::new(format!(
305                "{:?} is imprinted on itself: pick another body as the tool",
306                args.tool.name()
307            ))
308            .concerning("tool"));
309        }
310        let params = RemeshParams::default();
311        let Some(face) = &args.face else {
312            imprint(model, &namer, body, tool, params)?;
313            return Ok(());
314        };
315        let face_id = model.face_id(face.name())?;
316        if model.topology().body_of_face(face_id)? != body {
317            return Err(GeopError::new(format!(
318                "the face {:?} is not of the body {:?}: pick a face of the body imprinted on",
319                face.name(),
320                args.body.name()
321            ))
322            .concerning("face"));
323        }
324        // The tool stays as it is: a copy of it is imprinted, and consumed.
325        let copy = model
326            .copy_body(tool, |name| namer.name(&["copy", name]))?
327            .body()?;
328        imprint_on_face(model, &namer, face_id, &[copy], params)?;
329        Ok(())
330    }
331}
332
333one_of! {
334    /// What a section cuts a body with: a datum plane, or another body.
335    pub enum SectionWith {
336        Plane(ElementReference<Datums, Plane>),
337        Body(ElementReference<NamedModel, Body>),
338    }
339}
340
341/// The curves where the boundary of the body `body` crosses a plane or
342/// another body's boundary, as a wire of edges standing on their own, both
343/// left as they are: what a sweep runs along (an edge of the wire picked as
344/// a path is the run of the wire it is in) or a 3-D sketch takes a curve
345/// from.
346///
347/// Its edges and vertices are named as a boolean of copies of the two
348/// names what it creates, `section(S,..)` for the operation `S` (see
349/// [`geop_lib_booleans::section`]).
350#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
351pub struct Section;
352
353#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
354pub struct SectionArgs {
355    /// The body cut: a solid, or a face of a sheet.
356    pub body: ElementReference<NamedModel, Body>,
357    /// What it is cut with.
358    pub with: SectionWith,
359}
360
361impl PartOperation for Section {
362    const KIND: &'static str = "section";
363    const INFO: OperationInfo = OperationInfo::new(
364        "Section",
365        "The curves where a body crosses a plane or another body.",
366        OperationGroup::Bodies,
367        OperationTier::Menu,
368    )
369    .icon(crate::icons::SECTION);
370    type Args = SectionArgs;
371    type Session = ();
372
373    /// The newest solid, cut by the origin's `xy` plane.
374    fn new_args(context: &Context) -> SectionArgs {
375        let [_, body] = newest_two(context);
376        SectionArgs {
377            body: element(body),
378            with: SectionWith::Plane(origin_plane(FrameAxis::Z)),
379        }
380    }
381
382    fn run(ctx: &mut StepContext, args: &SectionArgs) -> GeopResult<()> {
383        let _step = ctx.step();
384        build_model::<Self>(ctx, args)
385    }
386
387    /// The body and the plane or body cutting it, picked.
388    fn form<'a>(
389        context: &'a Context<'a>,
390        args: &SectionArgs,
391        _: &FormState<()>,
392    ) -> EditForm<'a, SectionArgs> {
393        let mut f = EditForm::<SectionArgs>::default();
394        let before = &context.before;
395        f.pick_one("body", "body", before, Some(&args.body), |edit, body| {
396            edit.args.body = body
397        })
398        .pick_one("with", "with", before, Some(&args.with), |edit, with| {
399            edit.args.with = with
400        });
401        f
402    }
403}
404
405impl ModelOperation for Section {
406    fn build(
407        model: &mut NamedModel,
408        reader: &Reader,
409        step: &str,
410        args: &SectionArgs,
411    ) -> GeopResult<()> {
412        let namer = Namer::new("section", step)?;
413        let body = reader.retrieve_element(&args.body)?;
414        let params = RemeshParams::default();
415        match &args.with {
416            SectionWith::Plane(plane) => {
417                let plane = reader.retrieve_element(plane)?;
418                section_by_plane(model, &namer, body, &plane, params)?;
419            }
420            SectionWith::Body(other) => {
421                let other = reader.retrieve_element(other)?;
422                if body == other {
423                    return Err(GeopError::new(format!(
424                        "{:?} is cut with itself: pick another body, or a plane",
425                        args.body.name()
426                    )));
427                }
428                section(model, &namer, body, other, params)?;
429            }
430        }
431        Ok(())
432    }
433}