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