kcl_lib/std/
csg.rs

1//! Constructive Solid Geometry (CSG) operations.
2
3use 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
19/// Union two or more solids into a single solid.
20pub 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    // Flush the fillets for the solids.
53    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    // If we have more solids, set those as well.
80    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
92/// Intersect returns the shared volume between multiple solids, preserving only
93/// overlapping regions.
94pub 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    // Flush the fillets for the solids.
126    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    // If we have more solids, set those as well.
153    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
165/// Subtract removes tool solids from base solids, leaving the remaining material.
166pub 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    // Flush the fillets for the solids and the tools.
194    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    // If we have more solids, set those as well.
223    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
235/// Split a target body into two parts: the part that overlaps with the tool, and the part that doesn't.
236pub 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    // Flush the fillets for the solids.
281    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    // If we have more solids, set those as well.
309    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}