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    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    // Flush the fillets for the solids.
78    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    // If we have more solids, set those as well.
106    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
119/// Intersect returns the shared volume between multiple solids, preserving only
120/// overlapping regions.
121pub 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    // Flush the fillets for the solids.
157    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    // If we have more solids, set those as well.
185    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
198/// Subtract removes tool solids from base solids, leaving the remaining material.
199pub 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    // Flush the fillets for the solids and the tools.
231    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    // If we have more solids, set those as well.
261    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
274/// Split a target body into two parts: the part that overlaps with the tool, and the part that doesn't.
275pub 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    // Flush pending edge-cut operations for any solids consumed by imprint.
340    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    // If we have more solids, set those as well.
377    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}