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