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::{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(mcmd::BooleanUnion {
61                solid_ids: solids.iter().map(|s| s.id).collect(),
62                tolerance: LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)),
63            }),
64        )
65        .await?;
66
67    let OkWebSocketResponseData::Modeling {
68        modeling_response: OkModelingCmdResponse::BooleanUnion(BooleanUnion { extra_solid_ids }),
69    } = result
70    else {
71        return Err(KclError::new_internal(KclErrorDetails::new(
72            "Failed to get the result of the union operation.".to_string(),
73            vec![args.source_range],
74        )));
75    };
76
77    // If we have more solids, set those as well.
78    for extra_solid_id in extra_solid_ids {
79        if extra_solid_id == solid_out_id {
80            continue;
81        }
82        let mut new_solid = solid.clone();
83        new_solid.set_id(extra_solid_id);
84        new_solids.push(new_solid);
85    }
86
87    Ok(new_solids)
88}
89
90/// Intersect returns the shared volume between multiple solids, preserving only
91/// overlapping regions.
92pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
93    let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
94    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
95
96    if solids.len() < 2 {
97        return Err(KclError::new_semantic(KclErrorDetails::new(
98            "At least two solids are required for an intersect operation.".to_string(),
99            vec![args.source_range],
100        )));
101    }
102
103    let solids = inner_intersect(solids, tolerance, exec_state, args).await?;
104    Ok(solids.into())
105}
106
107pub(crate) async fn inner_intersect(
108    solids: Vec<Solid>,
109    tolerance: Option<TyF64>,
110    exec_state: &mut ExecState,
111    args: Args,
112) -> Result<Vec<Solid>, KclError> {
113    let solid_out_id = exec_state.next_uuid();
114
115    let mut solid = solids[0].clone();
116    solid.set_id(solid_out_id);
117    let mut new_solids = vec![solid.clone()];
118
119    if args.ctx.no_engine_commands().await {
120        return Ok(new_solids);
121    }
122
123    // Flush the fillets for the solids.
124    exec_state
125        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
126        .await?;
127
128    let result = exec_state
129        .send_modeling_cmd(
130            ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
131            ModelingCmd::from(mcmd::BooleanIntersection {
132                solid_ids: solids.iter().map(|s| s.id).collect(),
133                tolerance: LengthUnit(tolerance.map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM)),
134            }),
135        )
136        .await?;
137
138    let OkWebSocketResponseData::Modeling {
139        modeling_response: OkModelingCmdResponse::BooleanIntersection(BooleanIntersection { extra_solid_ids }),
140    } = result
141    else {
142        return Err(KclError::new_internal(KclErrorDetails::new(
143            "Failed to get the result of the intersection operation.".to_string(),
144            vec![args.source_range],
145        )));
146    };
147
148    // If we have more solids, set those as well.
149    for extra_solid_id in extra_solid_ids {
150        if extra_solid_id == solid_out_id {
151            continue;
152        }
153        let mut new_solid = solid.clone();
154        new_solid.set_id(extra_solid_id);
155        new_solids.push(new_solid);
156    }
157
158    Ok(new_solids)
159}
160
161/// Subtract removes tool solids from base solids, leaving the remaining material.
162pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
163    let solids: Vec<Solid> = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
164    let tools: Vec<Solid> = args.get_kw_arg("tools", &RuntimeType::solids(), exec_state)?;
165
166    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
167
168    let solids = inner_subtract(solids, tools, tolerance, exec_state, args).await?;
169    Ok(solids.into())
170}
171
172pub(crate) async fn inner_subtract(
173    solids: Vec<Solid>,
174    tools: Vec<Solid>,
175    tolerance: Option<TyF64>,
176    exec_state: &mut ExecState,
177    args: Args,
178) -> Result<Vec<Solid>, KclError> {
179    let solid_out_id = exec_state.next_uuid();
180
181    let mut solid = solids[0].clone();
182    solid.set_id(solid_out_id);
183    let mut new_solids = vec![solid.clone()];
184
185    if args.ctx.no_engine_commands().await {
186        return Ok(new_solids);
187    }
188
189    // Flush the fillets for the solids and the tools.
190    let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
191    exec_state
192        .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &combined_solids)
193        .await?;
194
195    let result = exec_state
196        .send_modeling_cmd(
197            ModelingCmdMeta::from_args_id(exec_state, &args, solid_out_id),
198            ModelingCmd::from(mcmd::BooleanSubtract {
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            }),
203        )
204        .await?;
205
206    let OkWebSocketResponseData::Modeling {
207        modeling_response: OkModelingCmdResponse::BooleanSubtract(BooleanSubtract { extra_solid_ids }),
208    } = result
209    else {
210        return Err(KclError::new_internal(KclErrorDetails::new(
211            "Failed to get the result of the subtract operation.".to_string(),
212            vec![args.source_range],
213        )));
214    };
215
216    // If we have more solids, set those as well.
217    for extra_solid_id in extra_solid_ids {
218        if extra_solid_id == solid_out_id {
219            continue;
220        }
221        let mut new_solid = solid.clone();
222        new_solid.set_id(extra_solid_id);
223        new_solids.push(new_solid);
224    }
225
226    Ok(new_solids)
227}