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