Skip to main content

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::{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
14/// Union two or more solids into a single solid.
15pub 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    // Flush the fillets for the solids.
48    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    // If we have more solids, set those as well.
75    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
87/// Intersect returns the shared volume between multiple solids, preserving only
88/// overlapping regions.
89pub 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    // Flush the fillets for the solids.
121    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    // If we have more solids, set those as well.
148    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
160/// Subtract removes tool solids from base solids, leaving the remaining material.
161pub 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    // Flush the fillets for the solids and the tools.
189    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    // If we have more solids, set those as well.
218    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
230/// Split a target body into two parts: the part that overlaps with the tool, and the part that doesn't.
231pub 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    // Flush the fillets for the solids.
276    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    // If we have more solids, set those as well.
304    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}