Skip to main content

geop_ops_parts/operation/
boolean.rs

1//! [`Boolean`]: combine two solids — and [`Combine`], the same done by an
2//! extrude or revolve with the solid it builds.
3
4use geop_core_math::{
5    geop_error::{GeopResult, WithContext},
6    scalars::Scalar,
7    with_context,
8};
9use geop_core_part::{Namer, Part};
10use geop_core_topology::SolidId;
11use geop_ops_booleans::{
12    boolean::{BooleanOp, boolean},
13    remesh::remesh::RemeshParams,
14};
15use geop_ops_parts_derive::OperationArgs;
16use serde::{Deserialize, Serialize};
17
18use super::Operation;
19
20/// Combines the solids named `a` and `b` into one solid named `boolean(B)`
21/// for the operation `B`, consuming both.
22///
23/// Every face, edge and vertex that survives keeps its name. What the
24/// boolean creates is named after what it was made from, see
25/// `geop_ops_booleans::naming` — for example `boolean(B,E1,E2,i,n)` for the
26/// `i`-th of the `n` points where edges `E1` and `E2` cross.
27///
28/// An empty result — intersecting solids that don't overlap, say — is an
29/// answer, not an error: both operands are consumed and no solid is named
30/// `boolean(B)`.
31#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
32pub struct Boolean;
33
34#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, OperationArgs)]
35pub struct BooleanArgs {
36    /// The first solid; for a difference, the one cut from.
37    #[arg(Solid)]
38    pub a: String,
39    /// The second solid; for a difference, the one cut away.
40    #[arg(Solid)]
41    pub b: String,
42    /// How to combine them.
43    #[arg(Choice { options: &["union", "intersection", "difference"], default: "difference" })]
44    pub op: BooleanOp,
45}
46
47impl<S: Scalar> Operation<S> for Boolean {
48    type Args = BooleanArgs;
49
50    fn apply(
51        &self,
52        mut part: Part<S>,
53        operation_id: &str,
54        args: &BooleanArgs,
55    ) -> GeopResult<Part<S>> {
56        let ctx = with_context!("boolean({operation_id}, {args:?})");
57        let namer = Namer::new("boolean", operation_id)?;
58        let a = part.solid_id(&args.a).with_context(ctx)?;
59        let b = part.solid_id(&args.b).with_context(ctx)?;
60        boolean(&mut part, &namer, a, b, args.op, RemeshParams::default()).with_context(ctx)?;
61        Ok(part)
62    }
63}
64
65/// What an extrude or revolve does with the solid it builds: keep it as a
66/// new body, or combine it with the solid named `target` — which that
67/// consumes, like a [`Boolean`] does. `Difference` cuts the new solid out
68/// of the target: an extruded pocket, a drilled hole.
69///
70/// Either way the step's result is named after the step — `extrude(E)` —
71/// so what comes after refers to "what step `E` left" however it was made.
72/// Combined, the built solid is only a tool, gone once the step is done,
73/// and what the combination creates is named `combine(E,...)`, as
74/// [`Boolean`] names its own.
75#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
76#[serde(tag = "mode", rename_all = "snake_case")]
77pub enum Combine {
78    #[default]
79    NewBody,
80    Union {
81        target: String,
82    },
83    Intersection {
84        target: String,
85    },
86    Difference {
87        target: String,
88    },
89}
90
91impl Combine {
92    /// The name the built solid gets: the step's own as a new body, or a
93    /// name within the step for a tool the combination consumes.
94    pub(crate) fn built_name(&self, namer: &Namer) -> String {
95        match self {
96            Combine::NewBody => namer.root(),
97            _ => namer.name(&["tool"]),
98        }
99    }
100
101    /// Combines `built` as the step `operation_id`, whose names `namer`
102    /// builds: the result is named `namer`'s root. An empty result — an
103    /// intersection with a solid the new one does not touch — leaves no
104    /// solid.
105    pub(crate) fn apply<S: Scalar>(
106        &self,
107        part: &mut Part<S>,
108        namer: &Namer,
109        operation_id: &str,
110        built: SolidId,
111    ) -> GeopResult<()> {
112        let (op, target) = match self {
113            Combine::NewBody => return Ok(()),
114            Combine::Union { target } => (BooleanOp::Union, target),
115            Combine::Intersection { target } => (BooleanOp::Intersection, target),
116            Combine::Difference { target } => (BooleanOp::Difference, target),
117        };
118        let ctx = with_context!("combining with {target:?} ({op:?})");
119        let target = part.solid_id(target).with_context(ctx)?;
120        let combine = Namer::new("combine", operation_id)?;
121        let result = boolean(part, &combine, target, built, op, RemeshParams::default())
122            .with_context(ctx)?;
123        if let Some(result) = result {
124            part.rename(result, namer.root())?;
125        }
126        Ok(())
127    }
128}