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 keep_tools = args
236        .get_kw_arg_opt("keepTools", &RuntimeType::bool(), exec_state)?
237        .unwrap_or_default();
238    let merge = args
239        .get_kw_arg_opt("merge", &RuntimeType::bool(), exec_state)?
240        .unwrap_or_default();
241
242    if targets.is_empty() {
243        return Err(KclError::new_semantic(KclErrorDetails::new(
244            "At least one target body is required.".to_string(),
245            vec![args.source_range],
246        )));
247    }
248
249    let body = inner_imprint(targets, tools, keep_tools, merge, tolerance, exec_state, args).await?;
250    Ok(body.into())
251}
252
253pub(crate) async fn inner_imprint(
254    targets: Vec<Solid>,
255    tools: Option<Vec<Solid>>,
256    keep_tools: bool,
257    merge: bool,
258    tolerance: Option<TyF64>,
259    exec_state: &mut ExecState,
260    args: Args,
261) -> Result<Vec<Solid>, KclError> {
262    let body_out_id = exec_state.next_uuid();
263
264    let mut body = targets[0].clone();
265    body.set_id(body_out_id);
266    let mut new_solids = vec![body.clone()];
267
268    if args.ctx.no_engine_commands().await {
269        return Ok(new_solids);
270    }
271
272    let separate_bodies = !merge;
273
274    // Flush pending edge-cut operations for any solids consumed by imprint.
275    let mut imprint_solids = targets.clone();
276    if let Some(tool_solids) = tools.as_ref() {
277        imprint_solids.extend_from_slice(tool_solids);
278    }
279    exec_state
280        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &imprint_solids)
281        .await?;
282
283    let body_ids = targets.iter().map(|body| body.id).collect();
284    let tool_ids = tools.as_ref().map(|tools| tools.iter().map(|tool| tool.id).collect());
285    let tolerance = LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
286    let imprint_cmd = mcmd::BooleanImprint::builder()
287        .body_ids(body_ids)
288        .tolerance(tolerance)
289        .separate_bodies(separate_bodies)
290        .keep_tools(keep_tools)
291        .maybe_tool_ids(tool_ids)
292        .build();
293    let result = exec_state
294        .send_modeling_cmd(
295            ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
296            ModelingCmd::from(imprint_cmd),
297        )
298        .await?;
299
300    let OkWebSocketResponseData::Modeling {
301        modeling_response: OkModelingCmdResponse::BooleanImprint(boolean_resp),
302    } = result
303    else {
304        return Err(KclError::new_internal(KclErrorDetails::new(
305            "Failed to get the result of the Imprint operation.".to_string(),
306            vec![args.source_range],
307        )));
308    };
309
310    // If we have more solids, set those as well.
311    for extra_solid_id in boolean_resp.extra_solid_ids {
312        if extra_solid_id == body_out_id {
313            continue;
314        }
315        let mut new_solid = body.clone();
316        new_solid.set_id(extra_solid_id);
317        new_solids.push(new_solid);
318    }
319
320    Ok(new_solids)
321}