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 tools = tools.unwrap_or_default();
236 let merge: bool = args.get_kw_arg("merge", &RuntimeType::bool(), exec_state)?;
237
238 if !merge {
239 return Err(KclError::new_semantic(KclErrorDetails::new(
240 "Zoo currently only supports merge = true for split".to_string(),
241 vec![args.source_range],
242 )));
243 }
244
245 let mut bodies = Vec::with_capacity(targets.len() + tools.len());
246 bodies.extend(targets);
247 bodies.extend(tools);
248 if bodies.len() < 2 {
249 return Err(KclError::new_semantic(KclErrorDetails::new(
250 "At least two bodies are required for an Imprint operation.".to_string(),
251 vec![args.source_range],
252 )));
253 }
254
255 let body = inner_imprint(bodies, tolerance, exec_state, args).await?;
256 Ok(body.into())
257}
258
259pub(crate) async fn inner_imprint(
260 bodies: Vec<Solid>,
261 tolerance: Option<TyF64>,
262 exec_state: &mut ExecState,
263 args: Args,
264) -> Result<Vec<Solid>, KclError> {
265 let body_out_id = exec_state.next_uuid();
266
267 let mut body = bodies[0].clone();
268 body.set_id(body_out_id);
269 let mut new_solids = vec![body.clone()];
270
271 if args.ctx.no_engine_commands().await {
272 return Ok(new_solids);
273 }
274
275 exec_state
277 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &bodies)
278 .await?;
279
280 let body_ids = bodies.iter().map(|body| body.id).collect();
281 let result = exec_state
282 .send_modeling_cmd(
283 ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
284 ModelingCmd::from(
285 mcmd::BooleanImprint::builder()
286 .body_ids(body_ids)
287 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
288 .build(),
289 ),
290 )
291 .await?;
292
293 let OkWebSocketResponseData::Modeling {
294 modeling_response: OkModelingCmdResponse::BooleanImprint(boolean_resp),
295 } = result
296 else {
297 return Err(KclError::new_internal(KclErrorDetails::new(
298 "Failed to get the result of the Imprint operation.".to_string(),
299 vec![args.source_range],
300 )));
301 };
302
303 for extra_solid_id in boolean_resp.extra_solid_ids {
305 if extra_solid_id == body_out_id {
306 continue;
307 }
308 let mut new_solid = body.clone();
309 new_solid.set_id(extra_solid_id);
310 new_solids.push(new_solid);
311 }
312
313 Ok(new_solids)
314}