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 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
29 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
30
31 if solids.len() < 2 {
32 return Err(KclError::new_semantic(KclErrorDetails::new(
33 "At least two solids are required for a union operation.".to_string(),
34 vec![args.source_range],
35 )));
36 }
37
38 let solids = inner_union(solids, tolerance, csg_algorithm, exec_state, args).await?;
39 Ok(solids.into())
40}
41
42pub enum CsgAlgorithm {
43 Latest,
44 Legacy,
45}
46
47impl CsgAlgorithm {
48 pub fn legacy(is_legacy: bool) -> Self {
49 if is_legacy { Self::Legacy } else { Self::Latest }
50 }
51 pub fn is_legacy(&self) -> bool {
52 match self {
53 CsgAlgorithm::Latest => false,
54 CsgAlgorithm::Legacy => true,
55 }
56 }
57}
58
59pub(crate) async fn inner_union(
60 solids: Vec<Solid>,
61 tolerance: Option<TyF64>,
62 csg_algorithm: CsgAlgorithm,
63 exec_state: &mut ExecState,
64 args: Args,
65) -> Result<Vec<Solid>, KclError> {
66 let solid_out_id = exec_state.next_uuid();
67
68 let mut solid = solids[0].clone();
69 solid.set_id(solid_out_id);
70 let mut new_solids = vec![solid.clone()];
71
72 if args.ctx.no_engine_commands().await {
73 return Ok(new_solids);
74 }
75
76 exec_state
78 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
79 .await?;
80
81 let result = exec_state
82 .send_modeling_cmd(
83 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
84 ModelingCmd::from(
85 mcmd::BooleanUnion::builder()
86 .use_legacy(csg_algorithm.is_legacy())
87 .solid_ids(solids.iter().map(|s| s.id).collect())
88 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
89 .build(),
90 ),
91 )
92 .await?;
93
94 let OkWebSocketResponseData::Modeling {
95 modeling_response: OkModelingCmdResponse::BooleanUnion(boolean_resp),
96 } = result
97 else {
98 return Err(KclError::new_internal(KclErrorDetails::new(
99 "Failed to get the result of the union operation.".to_string(),
100 vec![args.source_range],
101 )));
102 };
103
104 for extra_solid_id in boolean_resp.extra_solid_ids {
106 if extra_solid_id == solid_out_id {
107 continue;
108 }
109 let mut new_solid = solid.clone();
110 new_solid.set_id(extra_solid_id);
111 new_solids.push(new_solid);
112 }
113
114 Ok(new_solids)
115}
116
117pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
120 let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
121 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
122 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
123 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
124
125 if solids.len() < 2 {
126 return Err(KclError::new_semantic(KclErrorDetails::new(
127 "At least two solids are required for an intersect operation.".to_string(),
128 vec![args.source_range],
129 )));
130 }
131
132 let solids = inner_intersect(solids, tolerance, csg_algorithm, exec_state, args).await?;
133 Ok(solids.into())
134}
135
136pub(crate) async fn inner_intersect(
137 solids: Vec<Solid>,
138 tolerance: Option<TyF64>,
139 csg_algorithm: CsgAlgorithm,
140 exec_state: &mut ExecState,
141 args: Args,
142) -> Result<Vec<Solid>, KclError> {
143 let solid_out_id = exec_state.next_uuid();
144
145 let mut solid = solids[0].clone();
146 solid.set_id(solid_out_id);
147 let mut new_solids = vec![solid.clone()];
148
149 if args.ctx.no_engine_commands().await {
150 return Ok(new_solids);
151 }
152
153 exec_state
155 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
156 .await?;
157
158 let result = exec_state
159 .send_modeling_cmd(
160 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
161 ModelingCmd::from(
162 mcmd::BooleanIntersection::builder()
163 .use_legacy(csg_algorithm.is_legacy())
164 .solid_ids(solids.iter().map(|s| s.id).collect())
165 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
166 .build(),
167 ),
168 )
169 .await?;
170
171 let OkWebSocketResponseData::Modeling {
172 modeling_response: OkModelingCmdResponse::BooleanIntersection(boolean_resp),
173 } = result
174 else {
175 return Err(KclError::new_internal(KclErrorDetails::new(
176 "Failed to get the result of the intersection operation.".to_string(),
177 vec![args.source_range],
178 )));
179 };
180
181 for extra_solid_id in boolean_resp.extra_solid_ids {
183 if extra_solid_id == solid_out_id {
184 continue;
185 }
186 let mut new_solid = solid.clone();
187 new_solid.set_id(extra_solid_id);
188 new_solids.push(new_solid);
189 }
190
191 Ok(new_solids)
192}
193
194pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
196 let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
197 let tools: Vec<Solid> = args.get_kw_arg("tools", &RuntimeType::solids(), exec_state)?;
198
199 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
200 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
201 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
202
203 let solids = inner_subtract(solids, tools, tolerance, csg_algorithm, exec_state, args).await?;
204 Ok(solids.into())
205}
206
207pub(crate) async fn inner_subtract(
208 solids: Vec<Solid>,
209 tools: Vec<Solid>,
210 tolerance: Option<TyF64>,
211 csg_algorithm: CsgAlgorithm,
212 exec_state: &mut ExecState,
213 args: Args,
214) -> Result<Vec<Solid>, KclError> {
215 let solid_out_id = exec_state.next_uuid();
216
217 let mut solid = solids[0].clone();
218 solid.set_id(solid_out_id);
219 let mut new_solids = vec![solid.clone()];
220
221 if args.ctx.no_engine_commands().await {
222 return Ok(new_solids);
223 }
224
225 let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
227 exec_state
228 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &combined_solids)
229 .await?;
230
231 let result = exec_state
232 .send_modeling_cmd(
233 ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
234 ModelingCmd::from(
235 mcmd::BooleanSubtract::builder()
236 .use_legacy(csg_algorithm.is_legacy())
237 .target_ids(solids.iter().map(|s| s.id).collect())
238 .tool_ids(tools.iter().map(|s| s.id).collect())
239 .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
240 .build(),
241 ),
242 )
243 .await?;
244
245 let OkWebSocketResponseData::Modeling {
246 modeling_response: OkModelingCmdResponse::BooleanSubtract(boolean_resp),
247 } = result
248 else {
249 return Err(KclError::new_internal(KclErrorDetails::new(
250 "Failed to get the result of the subtract operation.".to_string(),
251 vec![args.source_range],
252 )));
253 };
254
255 for extra_solid_id in boolean_resp.extra_solid_ids {
257 if extra_solid_id == solid_out_id {
258 continue;
259 }
260 let mut new_solid = solid.clone();
261 new_solid.set_id(extra_solid_id);
262 new_solids.push(new_solid);
263 }
264
265 Ok(new_solids)
266}
267
268pub async fn split(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
270 let targets: Vec<Solid> = args.get_unlabeled_kw_arg("targets", &RuntimeType::solids(), exec_state)?;
271 let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
272 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
273 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
274 let tools: Option<Vec<Solid>> = args.get_kw_arg_opt("tools", &RuntimeType::solids(), exec_state)?;
275 let keep_tools = args
276 .get_kw_arg_opt("keepTools", &RuntimeType::bool(), exec_state)?
277 .unwrap_or_default();
278 let merge = args
279 .get_kw_arg_opt("merge", &RuntimeType::bool(), exec_state)?
280 .unwrap_or_default();
281
282 if targets.is_empty() {
283 return Err(KclError::new_semantic(KclErrorDetails::new(
284 "At least one target body is required.".to_string(),
285 vec![args.source_range],
286 )));
287 }
288
289 let body = inner_imprint(
290 targets,
291 tools,
292 keep_tools,
293 merge,
294 tolerance,
295 csg_algorithm,
296 exec_state,
297 args,
298 )
299 .await?;
300 Ok(body.into())
301}
302
303#[allow(clippy::too_many_arguments)]
304pub(crate) async fn inner_imprint(
305 targets: Vec<Solid>,
306 tools: Option<Vec<Solid>>,
307 keep_tools: bool,
308 merge: bool,
309 tolerance: Option<TyF64>,
310 csg_algorithm: CsgAlgorithm,
311 exec_state: &mut ExecState,
312 args: Args,
313) -> Result<Vec<Solid>, KclError> {
314 let body_out_id = exec_state.next_uuid();
315
316 let mut body = targets[0].clone();
317 body.set_id(body_out_id);
318 let mut new_solids = vec![body.clone()];
319
320 if args.ctx.no_engine_commands().await {
321 return Ok(new_solids);
322 }
323
324 let separate_bodies = !merge;
325
326 let mut imprint_solids = targets.clone();
328 if let Some(tool_solids) = tools.as_ref() {
329 imprint_solids.extend_from_slice(tool_solids);
330 }
331 exec_state
332 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &imprint_solids)
333 .await?;
334
335 let body_ids = targets.iter().map(|body| body.id).collect();
336 let tool_ids = tools.as_ref().map(|tools| tools.iter().map(|tool| tool.id).collect());
337 let tolerance = LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
338 let imprint_cmd = mcmd::BooleanImprint::builder()
339 .use_legacy(csg_algorithm.is_legacy())
340 .body_ids(body_ids)
341 .tolerance(tolerance)
342 .separate_bodies(separate_bodies)
343 .keep_tools(keep_tools)
344 .maybe_tool_ids(tool_ids)
345 .build();
346 let result = exec_state
347 .send_modeling_cmd(
348 ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
349 ModelingCmd::from(imprint_cmd),
350 )
351 .await?;
352
353 let OkWebSocketResponseData::Modeling {
354 modeling_response: OkModelingCmdResponse::BooleanImprint(boolean_resp),
355 } = result
356 else {
357 return Err(KclError::new_internal(KclErrorDetails::new(
358 "Failed to get the result of the Imprint operation.".to_string(),
359 vec![args.source_range],
360 )));
361 };
362
363 for extra_solid_id in boolean_resp.extra_solid_ids {
365 if extra_solid_id == body_out_id {
366 continue;
367 }
368 let mut new_solid = body.clone();
369 new_solid.set_id(extra_solid_id);
370 new_solids.push(new_solid);
371 }
372
373 Ok(new_solids)
374}