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