Skip to main content

geop_ops_model/
combine.rs

1//! How a step meets the solid it builds a tool for ([`Combine`]): kept as a
2//! new body, or joined to, cut from or intersected with a solid of the part
3//! ([`SolidRef`]) — the vocabulary every feature that combines shares. The
4//! boolean that applies it is `geop_lib_booleans::combine_tools`, which takes the
5//! [`Tool`]s a step built.
6
7use geop_core_editor::{
8    edit_form::{Edit, EditForm},
9    editing::Choice,
10};
11use geop_core_program::{element::ElementReference, reader::Reader};
12use geop_core_topology::SolidId;
13use serde::{Deserialize, Serialize};
14
15use crate::{BooleanOp, NamedModel, Namer, Solid, ToolReach, model, read_model};
16
17/// A solid of the part, by name.
18pub type SolidRef = ElementReference<NamedModel, Solid>;
19
20/// The solid named `name`.
21pub fn solid(name: impl Into<String>) -> SolidRef {
22    ElementReference::new(model(), name)
23}
24
25/// What an extrude or revolve does with the solid it builds: keep it as a
26/// new body, or combine it by `op` with the solid named `target` — which
27/// that consumes, as a boolean step does. A [`BooleanOp::Difference`] cuts
28/// the new solid out of the target: an extruded pocket, a drilled hole.
29///
30/// A step may build its solid in several parts, each combined in turn — the
31/// two sides of an extrude, one of which goes up to the next face of the
32/// target (see [`Tool`]).
33///
34/// Either way the step's result is named after the step — `extrude(E)` —
35/// so what comes after refers to "what step `E` left" however it was made.
36/// Combined, the built solid is only a tool, gone once the step is done,
37/// and what the combination creates is named `combine(E,...)`, as a
38/// boolean step names its own.
39#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
40#[serde(tag = "mode", rename_all = "snake_case")]
41pub enum Combine {
42    #[default]
43    NewBody,
44    With {
45        op: BooleanOp,
46        target: SolidRef,
47    },
48}
49
50impl Combine {
51    /// Cutting the tools out of the solid named `target`.
52    pub fn cut(target: impl Into<String>) -> Self {
53        Combine::With {
54            op: BooleanOp::Difference,
55            target: solid(target),
56        }
57    }
58
59    /// Joining the tools onto the solid named `target`.
60    pub fn join(target: impl Into<String>) -> Self {
61        Combine::With {
62            op: BooleanOp::Union,
63            target: solid(target),
64        }
65    }
66
67    /// Keeping what the tools and the solid named `target` have in common.
68    pub fn intersect(target: impl Into<String>) -> Self {
69        Combine::With {
70            op: BooleanOp::Intersection,
71            target: solid(target),
72        }
73    }
74
75    /// The solid it combines with, if any.
76    pub fn target(&self) -> Option<&str> {
77        match self {
78            Combine::NewBody => None,
79            Combine::With { target, .. } => Some(target.name()),
80        }
81    }
82
83    /// The default for a step after `before`: joining the newest solid, if
84    /// there is one — building onto what is there is the common case.
85    pub fn new_for(before: &NamedModel) -> Self {
86        match before.newest_solid() {
87            Some(target) => Combine::join(target),
88            None => Combine::NewBody,
89        }
90    }
91
92    /// A join for a positive `value`, a cut for a negative one — what an
93    /// extrude up out of a face, or down into it, means — when `value`
94    /// crosses to the other side of zero from `from`. On the same side, a
95    /// join or a cut stays what was chosen: an extrude may cut upwards into
96    /// a solid above its sketch. A new body and an intersection stay what
97    /// they are.
98    pub fn follow_sign(&mut self, from: f64, value: f64) {
99        let crossed = (from > 0.0) != (value > 0.0) || (from < 0.0) != (value < 0.0);
100        if let Combine::With { op, .. } = self
101            && crossed
102            && *op != BooleanOp::Intersection
103        {
104            if value > 0.0 {
105                *op = BooleanOp::Union;
106            } else if value < 0.0 {
107                *op = BooleanOp::Difference;
108            }
109        }
110    }
111
112    /// The field that picks the solid to combine with — or to go as far
113    /// as — labelled `label`, holding `target`, in the part `reader` reads;
114    /// `set` gets the solid picked.
115    pub fn target_field<'a, A, T>(
116        f: &mut EditForm<'a, A, T>,
117        reader: &Reader<'a>,
118        label: &str,
119        target: Option<&SolidRef>,
120        set: impl Fn(Edit<'_, A, T>, SolidRef) + 'a,
121    ) {
122        f.pick_one(TARGET, label, reader, target, set);
123    }
124
125    /// Its fields in `f`: the mode — chosen without a target, it takes the
126    /// newest solid of the part `reader` reads — and the target to pick.
127    /// `set` puts the combination set where the arguments keep it.
128    pub fn fields<'a, A, T>(
129        &self,
130        f: &mut EditForm<'a, A, T>,
131        reader: &Reader<'a>,
132        set: impl Fn(&mut A, Combine) + Clone + 'a,
133    ) {
134        let mode = match self {
135            Combine::NewBody => NEW_BODY,
136            Combine::With { op, .. } => op.value(),
137        };
138        let modes =
139            std::iter::once(Choice::new(NEW_BODY, "New body")).chain(BooleanOp::combine_choices());
140        let newest = read_model(reader)
141            .ok()
142            .and_then(|model| model.newest_solid());
143        let target = (self.target().map(str::to_string))
144            .or(newest)
145            .unwrap_or_default();
146        let set_mode = set.clone();
147        f.select(
148            "combine",
149            "combine",
150            mode,
151            modes.collect(),
152            false,
153            move |edit, mode| {
154                if mode == NEW_BODY {
155                    set_mode(edit.args, Combine::NewBody);
156                } else if let Some(op) = BooleanOp::from_value(mode) {
157                    let target = solid(&target);
158                    set_mode(edit.args, Combine::With { op, target });
159                }
160            },
161        );
162        if let Combine::With { op, target } = self {
163            let op = *op;
164            Self::target_field(f, reader, "target", Some(target), move |edit, target| {
165                set(edit.args, Combine::With { op, target })
166            });
167        }
168    }
169
170    /// The name the built solid gets: the step's own as a new body, or a
171    /// name within the step for a tool the combination consumes.
172    pub fn built_name(&self, namer: &Namer) -> String {
173        match self {
174            Combine::NewBody => namer.root(),
175            Combine::With { .. } => namer.name(&["tool"]),
176        }
177    }
178}
179
180/// The key of the field that picks the solid a step combines with (see
181/// [`Combine::target_field`]).
182const TARGET: &str = "combine_target";
183
184/// How a new body is offered, among the ways to combine.
185const NEW_BODY: &str = "new_body";
186
187impl BooleanOp {
188    /// Every boolean operation, in the order a dialog offers them.
189    const ALL: [BooleanOp; 4] = [
190        BooleanOp::Union,
191        BooleanOp::Difference,
192        BooleanOp::Intersection,
193        BooleanOp::SymmetricDifference,
194    ];
195
196    /// How it is offered, as a boolean or as a way to combine.
197    pub fn value(self) -> &'static str {
198        match self {
199            BooleanOp::Union => "union",
200            BooleanOp::Difference => "difference",
201            BooleanOp::Intersection => "intersection",
202            BooleanOp::SymmetricDifference => "symmetric_difference",
203        }
204    }
205
206    /// The op offered as `value`.
207    pub fn from_value(value: &str) -> Option<BooleanOp> {
208        Self::ALL.into_iter().find(|op| op.value() == value)
209    }
210
211    /// The ops as a boolean offers them: union, difference, intersection,
212    /// symmetric difference.
213    pub fn choices() -> Vec<Choice> {
214        let label = |op| match op {
215            BooleanOp::Union => "Union",
216            BooleanOp::Difference => "Difference",
217            BooleanOp::Intersection => "Intersection",
218            BooleanOp::SymmetricDifference => "Symmetric difference",
219        };
220        (Self::ALL.into_iter())
221            .map(|op| Choice::new(op.value(), label(op)))
222            .collect()
223    }
224
225    /// The ops as a way to combine a tool: join, cut, intersect, exclude.
226    fn combine_choices() -> impl Iterator<Item = Choice> {
227        let label = |op| match op {
228            BooleanOp::Union => "Join",
229            BooleanOp::Difference => "Cut",
230            BooleanOp::Intersection => "Intersect",
231            BooleanOp::SymmetricDifference => "Exclude",
232        };
233        (Self::ALL.into_iter()).map(move |op| Choice::new(op.value(), label(op)))
234    }
235}
236
237/// One solid an extrude or revolve built, for `geop_lib_booleans::combine_tools`
238/// to combine: the whole of it, or, with an `up_to_next` reach, only up to
239/// the next face of the target.
240#[derive(Clone, Debug)]
241pub struct Tool {
242    pub solid: SolidId,
243    pub reach: ToolReach,
244}
245
246impl Tool {
247    /// All of `solid`, the one tool of its step.
248    pub fn whole(solid: SolidId) -> Self {
249        Tool {
250            solid,
251            reach: ToolReach::default(),
252        }
253    }
254}