1use 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
17pub 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 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
92fn 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
143fn 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}