Skip to main content

kcl_lib/std/
csg.rs

1//! Constructive Solid Geometry (CSG) operations.
2
3use 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
23/// Union two or more solids into a single solid.
24pub 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
29    if solids.len() < 2 {
30        return Err(KclError::new_semantic(KclErrorDetails::new(
31            "At least two solids are required for a union operation.".to_string(),
32            vec![args.source_range],
33        )));
34    }
35
36    let solids = inner_union(solids, tolerance, exec_state, args).await?;
37    Ok(solids.into())
38}
39
40pub(crate) async fn inner_union(
41    solids: Vec<Solid>,
42    tolerance: Option<TyF64>,
43    exec_state: &mut ExecState,
44    args: Args,
45) -> Result<Vec<Solid>, KclError> {
46    let solid_out_id = exec_state.next_uuid();
47
48    let mut solid = solids[0].clone();
49    solid.set_id(solid_out_id);
50    let mut new_solids = vec![solid.clone()];
51
52    if args.ctx.no_engine_commands().await {
53        return Ok(new_solids);
54    }
55
56    // Flush the fillets for the solids.
57    exec_state
58        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
59        .await?;
60
61    let result = exec_state
62        .send_modeling_cmd(
63            ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
64            ModelingCmd::from(
65                mcmd::BooleanUnion::builder()
66                    .solid_ids(solids.iter().map(|s| s.id).collect())
67                    .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
68                    .build(),
69            ),
70        )
71        .await?;
72
73    let OkWebSocketResponseData::Modeling {
74        modeling_response: OkModelingCmdResponse::BooleanUnion(boolean_resp),
75    } = result
76    else {
77        return Err(KclError::new_internal(KclErrorDetails::new(
78            "Failed to get the result of the union operation.".to_string(),
79            vec![args.source_range],
80        )));
81    };
82
83    // If we have more solids, set those as well.
84    for extra_solid_id in boolean_resp.extra_solid_ids {
85        if extra_solid_id == solid_out_id {
86            continue;
87        }
88        let mut new_solid = solid.clone();
89        new_solid.set_id(extra_solid_id);
90        new_solids.push(new_solid);
91    }
92
93    Ok(new_solids)
94}
95
96/// Intersect returns the shared volume between multiple solids, preserving only
97/// overlapping regions.
98pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
99    let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
100    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
101
102    if solids.len() < 2 {
103        return Err(KclError::new_semantic(KclErrorDetails::new(
104            "At least two solids are required for an intersect operation.".to_string(),
105            vec![args.source_range],
106        )));
107    }
108
109    let solids = inner_intersect(solids, tolerance, exec_state, args).await?;
110    Ok(solids.into())
111}
112
113pub(crate) async fn inner_intersect(
114    solids: Vec<Solid>,
115    tolerance: Option<TyF64>,
116    exec_state: &mut ExecState,
117    args: Args,
118) -> Result<Vec<Solid>, KclError> {
119    let solid_out_id = exec_state.next_uuid();
120
121    let mut solid = solids[0].clone();
122    solid.set_id(solid_out_id);
123    let mut new_solids = vec![solid.clone()];
124
125    if args.ctx.no_engine_commands().await {
126        return Ok(new_solids);
127    }
128
129    // Flush the fillets for the solids.
130    exec_state
131        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
132        .await?;
133
134    let result = exec_state
135        .send_modeling_cmd(
136            ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
137            ModelingCmd::from(
138                mcmd::BooleanIntersection::builder()
139                    .solid_ids(solids.iter().map(|s| s.id).collect())
140                    .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
141                    .build(),
142            ),
143        )
144        .await?;
145
146    let OkWebSocketResponseData::Modeling {
147        modeling_response: OkModelingCmdResponse::BooleanIntersection(boolean_resp),
148    } = result
149    else {
150        return Err(KclError::new_internal(KclErrorDetails::new(
151            "Failed to get the result of the intersection operation.".to_string(),
152            vec![args.source_range],
153        )));
154    };
155
156    // If we have more solids, set those as well.
157    for extra_solid_id in boolean_resp.extra_solid_ids {
158        if extra_solid_id == solid_out_id {
159            continue;
160        }
161        let mut new_solid = solid.clone();
162        new_solid.set_id(extra_solid_id);
163        new_solids.push(new_solid);
164    }
165
166    Ok(new_solids)
167}
168
169/// Subtract removes tool solids from base solids, leaving the remaining material.
170pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
171    let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
172    let tools: Vec<Solid> = args.get_kw_arg("tools", &RuntimeType::solids(), exec_state)?;
173
174    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
175
176    let solids = inner_subtract(solids, tools, tolerance, exec_state, args).await?;
177    Ok(solids.into())
178}
179
180pub(crate) async fn inner_subtract(
181    solids: Vec<Solid>,
182    tools: Vec<Solid>,
183    tolerance: Option<TyF64>,
184    exec_state: &mut ExecState,
185    args: Args,
186) -> Result<Vec<Solid>, KclError> {
187    let solid_out_id = exec_state.next_uuid();
188
189    let mut solid = solids[0].clone();
190    solid.set_id(solid_out_id);
191    let mut new_solids = vec![solid.clone()];
192
193    if args.ctx.no_engine_commands().await {
194        return Ok(new_solids);
195    }
196
197    // Flush the fillets for the solids and the tools.
198    let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
199    exec_state
200        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &combined_solids)
201        .await?;
202
203    let result = exec_state
204        .send_modeling_cmd(
205            ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
206            ModelingCmd::from(
207                mcmd::BooleanSubtract::builder()
208                    .target_ids(solids.iter().map(|s| s.id).collect())
209                    .tool_ids(tools.iter().map(|s| s.id).collect())
210                    .tolerance(LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)))
211                    .build(),
212            ),
213        )
214        .await?;
215
216    let OkWebSocketResponseData::Modeling {
217        modeling_response: OkModelingCmdResponse::BooleanSubtract(boolean_resp),
218    } = result
219    else {
220        return Err(KclError::new_internal(KclErrorDetails::new(
221            "Failed to get the result of the subtract operation.".to_string(),
222            vec![args.source_range],
223        )));
224    };
225
226    // If we have more solids, set those as well.
227    for extra_solid_id in boolean_resp.extra_solid_ids {
228        if extra_solid_id == solid_out_id {
229            continue;
230        }
231        let mut new_solid = solid.clone();
232        new_solid.set_id(extra_solid_id);
233        new_solids.push(new_solid);
234    }
235
236    Ok(new_solids)
237}
238
239/// Split a target body into two parts: the part that overlaps with the tool, and the part that doesn't.
240pub async fn split(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
241    let targets: Vec<Solid> = args.get_unlabeled_kw_arg("targets", &RuntimeType::solids(), exec_state)?;
242    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
243    let tools: Option<Vec<Solid>> = args.get_kw_arg_opt("tools", &RuntimeType::solids(), exec_state)?;
244    let keep_tools = args
245        .get_kw_arg_opt("keepTools", &RuntimeType::bool(), exec_state)?
246        .unwrap_or_default();
247    let merge = args
248        .get_kw_arg_opt("merge", &RuntimeType::bool(), exec_state)?
249        .unwrap_or_default();
250
251    if targets.is_empty() {
252        return Err(KclError::new_semantic(KclErrorDetails::new(
253            "At least one target body is required.".to_string(),
254            vec![args.source_range],
255        )));
256    }
257
258    let body = inner_imprint(targets, tools, keep_tools, merge, tolerance, exec_state, args).await?;
259    Ok(body.into())
260}
261
262pub(crate) async fn inner_imprint(
263    targets: Vec<Solid>,
264    tools: Option<Vec<Solid>>,
265    keep_tools: bool,
266    merge: bool,
267    tolerance: Option<TyF64>,
268    exec_state: &mut ExecState,
269    args: Args,
270) -> Result<Vec<Solid>, KclError> {
271    let body_out_id = exec_state.next_uuid();
272
273    let mut body = targets[0].clone();
274    body.set_id(body_out_id);
275    let mut new_solids = vec![body.clone()];
276
277    if args.ctx.no_engine_commands().await {
278        return Ok(new_solids);
279    }
280
281    let separate_bodies = !merge;
282
283    // Flush pending edge-cut operations for any solids consumed by imprint.
284    let mut imprint_solids = targets.clone();
285    if let Some(tool_solids) = tools.as_ref() {
286        imprint_solids.extend_from_slice(tool_solids);
287    }
288    exec_state
289        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &imprint_solids)
290        .await?;
291
292    let body_ids = targets.iter().map(|body| body.id).collect();
293    let tool_ids = tools.as_ref().map(|tools| tools.iter().map(|tool| tool.id).collect());
294    let tolerance = LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
295    let imprint_cmd = mcmd::BooleanImprint::builder()
296        .body_ids(body_ids)
297        .tolerance(tolerance)
298        .separate_bodies(separate_bodies)
299        .keep_tools(keep_tools)
300        .maybe_tool_ids(tool_ids)
301        .build();
302    let result = exec_state
303        .send_modeling_cmd(
304            ModelingCmdMeta::from_args_id(exec_state, &args, body_out_id),
305            ModelingCmd::from(imprint_cmd),
306        )
307        .await?;
308
309    let OkWebSocketResponseData::Modeling {
310        modeling_response: OkModelingCmdResponse::BooleanImprint(boolean_resp),
311    } = result
312    else {
313        return Err(KclError::new_internal(KclErrorDetails::new(
314            "Failed to get the result of the Imprint operation.".to_string(),
315            vec![args.source_range],
316        )));
317    };
318
319    // If we have more solids, set those as well.
320    for extra_solid_id in boolean_resp.extra_solid_ids {
321        if extra_solid_id == body_out_id {
322            continue;
323        }
324        let mut new_solid = body.clone();
325        new_solid.set_id(extra_solid_id);
326        new_solids.push(new_solid);
327    }
328
329    Ok(new_solids)
330}