1use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kcmc::length_unit::LengthUnit;
7use kittycad_modeling_cmds::ok_response::OkModelingCmdResponse;
8use kittycad_modeling_cmds::websocket::OkWebSocketResponseData;
9use kittycad_modeling_cmds::{self as kcmc};
10
11use super::DEFAULT_TOLERANCE_MM;
12use super::args::TyF64;
13use crate::errors::KclError;
14use crate::errors::KclErrorDetails;
15use crate::execution::ExecState;
16use crate::execution::KclValue;
17use crate::execution::ModelingCmdMeta;
18use crate::execution::Solid;
19use crate::execution::types::RuntimeType;
20use crate::std::Args;
21use crate::std::patterns::GeometryTrait;
22
23pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
25 let solids: Vec<Solid> =
26 args.get_unlabeled_kw_arg("solids", &RuntimeType::Union(vec![RuntimeType::solids()]), exec_state)?;
27 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
28
29 if solids.len() < 2 {
30 return Err(KclError::new_semantic(KclErrorDetails::new(
31 "At least two solids are required for a union operation.".to_string(),
32 vec![args.source_range],
33 )));
34 }
35
36 let solids = inner_union(solids, tolerance, exec_state, args).await?;
37 Ok(solids.into())
38}
39
40pub(crate) async fn inner_union(
41 solids: Vec<Solid>,
42 tolerance: Option<TyF64>,
43 exec_state: &mut ExecState,
44 args: Args,
45) -> Result<Vec<Solid>, KclError> {
46 let solid_out_id = exec_state.next_uuid();
47
48 let mut solid = solids[0].clone();
49 solid.set_id(solid_out_id);
50 let mut new_solids = vec![solid.clone()];
51
52 if args.ctx.no_engine_commands().await {
53 return Ok(new_solids);
54 }
55
56 exec_state
58 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
59 .await?;
60
61 let result = exec_state
62 .send_modeling_cmd(
63 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
64 ModelingCmd::from(
65 mcmd::BooleanUnion::builder()
66 .solid_ids(solids.iter().map(|s| s.id).collect())
67 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
68 .build(),
69 ),
70 )
71 .await?;
72
73 let OkWebSocketResponseData::Modeling {
74 modeling_response: OkModelingCmdResponse::BooleanUnion(boolean_resp),
75 } = result
76 else {
77 return Err(KclError::new_internal(KclErrorDetails::new(
78 "Failed to get the result of the union operation.".to_string(),
79 vec![args.source_range],
80 )));
81 };
82
83 for extra_solid_id in boolean_resp.extra_solid_ids {
85 if extra_solid_id == solid_out_id {
86 continue;
87 }
88 let mut new_solid = solid.clone();
89 new_solid.set_id(extra_solid_id);
90 new_solids.push(new_solid);
91 }
92
93 Ok(new_solids)
94}
95
96pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
99 let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
100 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
101
102 if solids.len() < 2 {
103 return Err(KclError::new_semantic(KclErrorDetails::new(
104 "At least two solids are required for an intersect operation.".to_string(),
105 vec![args.source_range],
106 )));
107 }
108
109 let solids = inner_intersect(solids, tolerance, exec_state, args).await?;
110 Ok(solids.into())
111}
112
113pub(crate) async fn inner_intersect(
114 solids: Vec<Solid>,
115 tolerance: Option<TyF64>,
116 exec_state: &mut ExecState,
117 args: Args,
118) -> Result<Vec<Solid>, KclError> {
119 let solid_out_id = exec_state.next_uuid();
120
121 let mut solid = solids[0].clone();
122 solid.set_id(solid_out_id);
123 let mut new_solids = vec![solid.clone()];
124
125 if args.ctx.no_engine_commands().await {
126 return Ok(new_solids);
127 }
128
129 exec_state
131 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
132 .await?;
133
134 let result = exec_state
135 .send_modeling_cmd(
136 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
137 ModelingCmd::from(
138 mcmd::BooleanIntersection::builder()
139 .solid_ids(solids.iter().map(|s| s.id).collect())
140 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
141 .build(),
142 ),
143 )
144 .await?;
145
146 let OkWebSocketResponseData::Modeling {
147 modeling_response: OkModelingCmdResponse::BooleanIntersection(boolean_resp),
148 } = result
149 else {
150 return Err(KclError::new_internal(KclErrorDetails::new(
151 "Failed to get the result of the intersection operation.".to_string(),
152 vec![args.source_range],
153 )));
154 };
155
156 for extra_solid_id in boolean_resp.extra_solid_ids {
158 if extra_solid_id == solid_out_id {
159 continue;
160 }
161 let mut new_solid = solid.clone();
162 new_solid.set_id(extra_solid_id);
163 new_solids.push(new_solid);
164 }
165
166 Ok(new_solids)
167}
168
169pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
171 let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
172 let tools: Vec<Solid> = args.get_kw_arg("tools", &RuntimeType::solids(), exec_state)?;
173
174 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
175
176 let solids = inner_subtract(solids, tools, tolerance, exec_state, args).await?;
177 Ok(solids.into())
178}
179
180pub(crate) async fn inner_subtract(
181 solids: Vec<Solid>,
182 tools: Vec<Solid>,
183 tolerance: Option<TyF64>,
184 exec_state: &mut ExecState,
185 args: Args,
186) -> Result<Vec<Solid>, KclError> {
187 let solid_out_id = exec_state.next_uuid();
188
189 let mut solid = solids[0].clone();
190 solid.set_id(solid_out_id);
191 let mut new_solids = vec![solid.clone()];
192
193 if args.ctx.no_engine_commands().await {
194 return Ok(new_solids);
195 }
196
197 let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
199 exec_state
200 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &combined_solids)
201 .await?;
202
203 let result = exec_state
204 .send_modeling_cmd(
205 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
206 ModelingCmd::from(
207 mcmd::BooleanSubtract::builder()
208 .target_ids(solids.iter().map(|s| s.id).collect())
209 .tool_ids(tools.iter().map(|s| s.id).collect())
210 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
211 .build(),
212 ),
213 )
214 .await?;
215
216 let OkWebSocketResponseData::Modeling {
217 modeling_response: OkModelingCmdResponse::BooleanSubtract(boolean_resp),
218 } = result
219 else {
220 return Err(KclError::new_internal(KclErrorDetails::new(
221 "Failed to get the result of the subtract operation.".to_string(),
222 vec![args.source_range],
223 )));
224 };
225
226 for extra_solid_id in boolean_resp.extra_solid_ids {
228 if extra_solid_id == solid_out_id {
229 continue;
230 }
231 let mut new_solid = solid.clone();
232 new_solid.set_id(extra_solid_id);
233 new_solids.push(new_solid);
234 }
235
236 Ok(new_solids)
237}
238
239pub async fn split(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
241 let targets: Vec<Solid> = args.get_unlabeled_kw_arg("targets", &RuntimeType::solids(), exec_state)?;
242 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
243 let tools: Option<Vec<Solid>> = args.get_kw_arg_opt("tools", &RuntimeType::solids(), exec_state)?;
244 let keep_tools = args
245 .get_kw_arg_opt("keepTools", &RuntimeType::bool(), exec_state)?
246 .unwrap_or_default();
247 let merge = args
248 .get_kw_arg_opt("merge", &RuntimeType::bool(), exec_state)?
249 .unwrap_or_default();
250
251 if targets.is_empty() {
252 return Err(KclError::new_semantic(KclErrorDetails::new(
253 "At least one target body is required.".to_string(),
254 vec![args.source_range],
255 )));
256 }
257
258 let body = inner_imprint(targets, tools, keep_tools, merge, tolerance, exec_state, args).await?;
259 Ok(body.into())
260}
261
262pub(crate) async fn inner_imprint(
263 targets: Vec<Solid>,
264 tools: Option<Vec<Solid>>,
265 keep_tools: bool,
266 merge: bool,
267 tolerance: Option<TyF64>,
268 exec_state: &mut ExecState,
269 args: Args,
270) -> Result<Vec<Solid>, KclError> {
271 let body_out_id = exec_state.next_uuid();
272
273 let mut body = targets[0].clone();
274 body.set_id(body_out_id);
275 let mut new_solids = vec![body.clone()];
276
277 if args.ctx.no_engine_commands().await {
278 return Ok(new_solids);
279 }
280
281 let separate_bodies = !merge;
282
283 let mut imprint_solids = targets.clone();
285 if let Some(tool_solids) = tools.as_ref() {
286 imprint_solids.extend_from_slice(tool_solids);
287 }
288 exec_state
289 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &imprint_solids)
290 .await?;
291
292 let body_ids = targets.iter().map(|body| body.id).collect();
293 let tool_ids = tools.as_ref().map(|tools| tools.iter().map(|tool| tool.id).collect());
294 let tolerance = LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
295 let imprint_cmd = mcmd::BooleanImprint::builder()
296 .body_ids(body_ids)
297 .tolerance(tolerance)
298 .separate_bodies(separate_bodies)
299 .keep_tools(keep_tools)
300 .maybe_tool_ids(tool_ids)
301 .build();
302 let result = exec_state
303 .send_modeling_cmd(
304 ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
305 ModelingCmd::from(imprint_cmd),
306 )
307 .await?;
308
309 let OkWebSocketResponseData::Modeling {
310 modeling_response: OkModelingCmdResponse::BooleanImprint(boolean_resp),
311 } = result
312 else {
313 return Err(KclError::new_internal(KclErrorDetails::new(
314 "Failed to get the result of the Imprint operation.".to_string(),
315 vec![args.source_range],
316 )));
317 };
318
319 for extra_solid_id in boolean_resp.extra_solid_ids {
321 if extra_solid_id == body_out_id {
322 continue;
323 }
324 let mut new_solid = body.clone();
325 new_solid.set_id(extra_solid_id);
326 new_solids.push(new_solid);
327 }
328
329 Ok(new_solids)
330}