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