Skip to main content

geop_lib_booleans/
combine.rs

1//! [`combine_tools`]: what an extrude, a revolve, a hole or a pattern does with
2//! the tools it built, as the [`Combine`] of its step says — kept as a new
3//! body, or combined with a solid of the part by the boolean.
4
5use geop_core_math::{
6    geop_error::{GeopError, GeopResult, WithContext},
7    with_context,
8};
9use geop_core_topology::{Body, SolidId};
10use geop_ops_model::{BooleanOp, Combine, FeatureTool, NamedModel, Namer, Tool};
11
12use crate::{
13    boolean::{Reach, boolean, boolean_up_to_next, piece_up_to_next},
14    remesh::RemeshParams,
15};
16
17/// Combines the `tools` built, one after the other, as `how` says, as the
18/// step `step`, whose names `namer` builds: the result is named `namer`'s
19/// root, and what the `k`-th tool's combination creates is named after
20/// `combine(E)` — scoped by the tool's own scope, if it has one. An
21/// empty result — an intersection with a solid the new one does not
22/// touch — leaves no solid, and no tool. A tool going up to the next
23/// face that nothing stops all round [`Reach::RunsOn`], and leaves
24/// `model` part way through: a caller going another way tries on a copy.
25///
26/// A new body is the tools as built — a tool going up to the next face
27/// of whichever other solid it meets first (see [`piece_up_to_next`]),
28/// those solids left as they are — joined into one.
29///
30/// Combined with a target, each tool is recorded, as built, as the
31/// feature of the step (see [`geop_ops_model::Feature`]): what a pattern of
32/// the feature copies and combines the same way. A new body is no
33/// feature: it is a body, patterned as one.
34pub fn combine_tools(
35    model: &mut NamedModel,
36    namer: &Namer,
37    step: &str,
38    tools: &[Tool],
39    how: &Combine,
40) -> GeopResult<Reach<()>> {
41    let (op, target) = match how {
42        Combine::NewBody => return new_body(model, namer, step, tools),
43        Combine::With { op, target } => (*op, target),
44    };
45    let ctx = with_context!("combining with {:?} ({op:?})", target.name());
46    let mut target = model.solid_id(target.name()).with_context(ctx)?;
47    for tool in tools {
48        let (spec, names) = model.body_record(tool.solid.into()).with_context(ctx)?;
49        model.record_feature_tool(
50            step,
51            FeatureTool {
52                spec,
53                names,
54                op,
55                reach: tool.reach.clone(),
56            },
57        );
58    }
59    for (k, tool) in tools.iter().enumerate() {
60        let combine = tool_namer(tool, step)?;
61        let params = RemeshParams::default();
62        let result = match &tool.reach.up_to_next {
63            None => boolean(model, &combine, target, tool.solid, op, params),
64            Some((start, end)) => match boolean_up_to_next(
65                model,
66                &combine,
67                target,
68                tool.solid,
69                op,
70                (start, end),
71                params,
72            )
73            .with_context(ctx)?
74            {
75                Reach::Stopped(result) => Ok(result),
76                Reach::RunsOn(e) => return Ok(Reach::RunsOn(e)),
77            },
78        }
79        .with_context(ctx)?;
80        let Some(result) = result else {
81            // Nothing left to combine the other tools with.
82            let rest: Vec<Body> = tools[k + 1..].iter().map(|t| t.solid.into()).collect();
83            model.assemble_solid(&rest, &[], namer.root())?;
84            return Ok(Reach::Stopped(()));
85        };
86        model.rename(result, namer.root())?;
87        target = result;
88    }
89    Ok(Reach::Stopped(()))
90}
91
92/// The new body `tools` make, named `namer`'s root (see [`combine_tools`]).
93fn new_body(
94    model: &mut NamedModel,
95    namer: &Namer,
96    step: &str,
97    tools: &[Tool],
98) -> GeopResult<Reach<()>> {
99    let built: Vec<SolidId> = tools.iter().map(|t| t.solid).collect();
100    let mut stops: Vec<SolidId> = (model.topology().solids.keys().copied())
101        .filter(|s| !built.contains(s))
102        .collect();
103    stops.sort_by_key(|s| s.0);
104    let params = RemeshParams::default();
105    let mut body: Option<SolidId> = None;
106    for tool in tools {
107        let combine = tool_namer(tool, step)?;
108        let piece = match &tool.reach.up_to_next {
109            None => tool.solid,
110            Some(_) if stops.is_empty() => {
111                return Err(GeopError::new(
112                    "up to next: there is no other solid to go up to the next face of",
113                ));
114            }
115            Some((start, end)) => {
116                match piece_up_to_next(model, &combine, tool.solid, &stops, (start, end), params)? {
117                    Reach::Stopped(piece) => piece,
118                    Reach::RunsOn(e) => return Ok(Reach::RunsOn(e)),
119                }
120            }
121        };
122        body = Some(match body {
123            None => piece,
124            Some(body) => boolean(
125                model,
126                &combine.scoped("join"),
127                body,
128                piece,
129                BooleanOp::Union,
130                params,
131            )?
132            .ok_or_else(|| GeopError::new("joining the sides of a new body left nothing"))?,
133        });
134    }
135    if let Some(body) = body
136        && model.name_of(body) != Some(namer.root().as_str())
137    {
138        model.rename(body, namer.root())?;
139    }
140    Ok(Reach::Stopped(()))
141}
142
143/// The names of what combining `tool` as the step `step` creates:
144/// `combine(step)`, scoped by its reach's scope, if it has one.
145fn tool_namer(tool: &Tool, step: &str) -> GeopResult<Namer> {
146    let combine = Namer::new("combine", step)?;
147    Ok(match &tool.reach.scope {
148        Some(scope) => combine.scoped(scope),
149        None => combine,
150    })
151}