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