kcl_lib/std/
csg.rs

1//! Constructive Solid Geometry (CSG) operations.
2
3use anyhow::Result;
4use kcl_derive_docs::stdlib;
5use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
6use kittycad_modeling_cmds::{
7    self as kcmc,
8    ok_response::OkModelingCmdResponse,
9    output::{BooleanIntersection, BooleanSubtract, BooleanUnion},
10    websocket::OkWebSocketResponseData,
11};
12
13use super::{args::TyF64, DEFAULT_TOLERANCE};
14use crate::{
15    errors::{KclError, KclErrorDetails},
16    execution::{types::RuntimeType, ExecState, KclValue, Solid},
17    std::{patterns::GeometryTrait, Args},
18};
19
20/// Union two or more solids into a single solid.
21pub async fn union(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
22    let solids: Vec<Solid> =
23        args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::Union(vec![RuntimeType::solids()]), exec_state)?;
24    let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
25
26    if solids.len() < 2 {
27        return Err(KclError::UndefinedValue(KclErrorDetails::new(
28            "At least two solids are required for a union operation.".to_string(),
29            vec![args.source_range],
30        )));
31    }
32
33    let solids = inner_union(solids, tolerance, exec_state, args).await?;
34    Ok(solids.into())
35}
36
37/// Union two or more solids into a single solid.
38///
39/// ```no_run
40/// // Union two cubes using the stdlib functions.
41///
42/// fn cube(center, size) {
43///     return startSketchOn(XY)
44///         |> startProfile(at = [center[0] - size, center[1] - size])
45///         |> line(endAbsolute = [center[0] + size, center[1] - size])
46///         |> line(endAbsolute = [center[0] + size, center[1] + size])
47///         |> line(endAbsolute = [center[0] - size, center[1] + size])
48///         |> close()
49///         |> extrude(length = 10)
50/// }
51///
52/// part001 = cube(center = [0, 0], size = 10)
53/// part002 = cube(center = [7, 3], size = 5)
54///     |> translate(z = 1)
55///
56/// unionedPart = union([part001, part002])
57/// ```
58///
59/// ```no_run
60/// // Union two cubes using operators.
61/// // NOTE: This will not work when using codemods through the UI.
62/// // Codemods will generate the stdlib function call instead.
63///
64/// fn cube(center, size) {
65///     return startSketchOn(XY)
66///         |> startProfile(at = [center[0] - size, center[1] - size])
67///         |> line(endAbsolute = [center[0] + size, center[1] - size])
68///         |> line(endAbsolute = [center[0] + size, center[1] + size])
69///         |> line(endAbsolute = [center[0] - size, center[1] + size])
70///         |> close()
71///         |> extrude(length = 10)
72/// }
73///
74/// part001 = cube(center = [0, 0], size = 10)
75/// part002 = cube(center = [7, 3], size = 5)
76///     |> translate(z = 1)
77///
78/// // This is the equivalent of: union([part001, part002])
79/// unionedPart = part001 + part002
80/// ```
81///
82/// ```no_run
83/// // Union two cubes using the more programmer-friendly operator.
84/// // NOTE: This will not work when using codemods through the UI.
85/// // Codemods will generate the stdlib function call instead.
86///
87/// fn cube(center, size) {
88///     return startSketchOn(XY)
89///         |> startProfile(at = [center[0] - size, center[1] - size])
90///         |> line(endAbsolute = [center[0] + size, center[1] - size])
91///         |> line(endAbsolute = [center[0] + size, center[1] + size])
92///         |> line(endAbsolute = [center[0] - size, center[1] + size])
93///         |> close()
94///         |> extrude(length = 10)
95/// }
96///
97/// part001 = cube(center = [0, 0], size = 10)
98/// part002 = cube(center = [7, 3], size = 5)
99///     |> translate(z = 1)
100///
101/// // This is the equivalent of: union([part001, part002])
102/// // Programmers will understand `|` as a union operation, but mechanical engineers
103/// // will understand `+`, we made both work.
104/// unionedPart = part001 | part002
105/// ```
106#[stdlib {
107    name = "union",
108    feature_tree_operation = true,
109    unlabeled_first = true,
110    args = {
111        solids = {docs = "The solids to union."},
112        tolerance = {docs = "The tolerance to use for the union operation."},
113    },
114    tags = ["solid"]
115}]
116pub(crate) async fn inner_union(
117    solids: Vec<Solid>,
118    tolerance: Option<TyF64>,
119    exec_state: &mut ExecState,
120    args: Args,
121) -> Result<Vec<Solid>, KclError> {
122    let solid_out_id = exec_state.next_uuid();
123
124    let mut solid = solids[0].clone();
125    solid.set_id(solid_out_id);
126    let mut new_solids = vec![solid.clone()];
127
128    if args.ctx.no_engine_commands().await {
129        return Ok(new_solids);
130    }
131
132    // Flush the fillets for the solids.
133    args.flush_batch_for_solids(exec_state, &solids).await?;
134
135    let result = args
136        .send_modeling_cmd(
137            solid_out_id,
138            ModelingCmd::from(mcmd::BooleanUnion {
139                solid_ids: solids.iter().map(|s| s.id).collect(),
140                tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
141            }),
142        )
143        .await?;
144
145    let OkWebSocketResponseData::Modeling {
146        modeling_response: OkModelingCmdResponse::BooleanUnion(BooleanUnion { extra_solid_ids }),
147    } = result
148    else {
149        return Err(KclError::Internal(KclErrorDetails::new(
150            "Failed to get the result of the union operation.".to_string(),
151            vec![args.source_range],
152        )));
153    };
154
155    // If we have more solids, set those as well.
156    if !extra_solid_ids.is_empty() {
157        solid.set_id(extra_solid_ids[0]);
158        new_solids.push(solid.clone());
159    }
160
161    Ok(new_solids)
162}
163
164/// Intersect returns the shared volume between multiple solids, preserving only
165/// overlapping regions.
166pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
167    let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
168    let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
169
170    if solids.len() < 2 {
171        return Err(KclError::UndefinedValue(KclErrorDetails::new(
172            "At least two solids are required for an intersect operation.".to_string(),
173            vec![args.source_range],
174        )));
175    }
176
177    let solids = inner_intersect(solids, tolerance, exec_state, args).await?;
178    Ok(solids.into())
179}
180
181/// Intersect returns the shared volume between multiple solids, preserving only
182/// overlapping regions.
183///
184/// Intersect computes the geometric intersection of multiple solid bodies,
185/// returning a new solid representing the volume that is common to all input
186/// solids. This operation is useful for determining shared material regions,
187/// verifying fit, and analyzing overlapping geometries in assemblies.
188///
189/// ```no_run
190/// // Intersect two cubes using the stdlib functions.
191///
192/// fn cube(center, size) {
193///     return startSketchOn(XY)
194///         |> startProfile(at = [center[0] - size, center[1] - size])
195///         |> line(endAbsolute = [center[0] + size, center[1] - size])
196///         |> line(endAbsolute = [center[0] + size, center[1] + size])
197///         |> line(endAbsolute = [center[0] - size, center[1] + size])
198///         |> close()
199///         |> extrude(length = 10)
200/// }
201///
202/// part001 = cube(center = [0, 0], size = 10)
203/// part002 = cube(center = [7, 3], size = 5)
204///     |> translate(z = 1)
205///
206/// intersectedPart = intersect([part001, part002])
207/// ```
208///
209/// ```no_run
210/// // Intersect two cubes using operators.
211/// // NOTE: This will not work when using codemods through the UI.
212/// // Codemods will generate the stdlib function call instead.
213///
214/// fn cube(center, size) {
215///     return startSketchOn(XY)
216///         |> startProfile(at = [center[0] - size, center[1] - size])
217///         |> line(endAbsolute = [center[0] + size, center[1] - size])
218///         |> line(endAbsolute = [center[0] + size, center[1] + size])
219///         |> line(endAbsolute = [center[0] - size, center[1] + size])
220///         |> close()
221///         |> extrude(length = 10)
222/// }
223///
224/// part001 = cube(center = [0, 0], size = 10)
225/// part002 = cube(center = [7, 3], size = 5)
226///     |> translate(z = 1)
227///
228/// // This is the equivalent of: intersect([part001, part002])
229/// intersectedPart = part001 & part002
230/// ```
231#[stdlib {
232    name = "intersect",
233    feature_tree_operation = true,
234    unlabeled_first = true,
235    args = {
236        solids = {docs = "The solids to intersect."},
237        tolerance = {docs = "The tolerance to use for the intersection operation."},
238    },
239    tags = ["solid"]
240}]
241pub(crate) async fn inner_intersect(
242    solids: Vec<Solid>,
243    tolerance: Option<TyF64>,
244    exec_state: &mut ExecState,
245    args: Args,
246) -> Result<Vec<Solid>, KclError> {
247    let solid_out_id = exec_state.next_uuid();
248
249    let mut solid = solids[0].clone();
250    solid.set_id(solid_out_id);
251    let mut new_solids = vec![solid.clone()];
252
253    if args.ctx.no_engine_commands().await {
254        return Ok(new_solids);
255    }
256
257    // Flush the fillets for the solids.
258    args.flush_batch_for_solids(exec_state, &solids).await?;
259
260    let result = args
261        .send_modeling_cmd(
262            solid_out_id,
263            ModelingCmd::from(mcmd::BooleanIntersection {
264                solid_ids: solids.iter().map(|s| s.id).collect(),
265                tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
266            }),
267        )
268        .await?;
269
270    let OkWebSocketResponseData::Modeling {
271        modeling_response: OkModelingCmdResponse::BooleanIntersection(BooleanIntersection { extra_solid_ids }),
272    } = result
273    else {
274        return Err(KclError::Internal(KclErrorDetails::new(
275            "Failed to get the result of the intersection operation.".to_string(),
276            vec![args.source_range],
277        )));
278    };
279
280    // If we have more solids, set those as well.
281    if !extra_solid_ids.is_empty() {
282        solid.set_id(extra_solid_ids[0]);
283        new_solids.push(solid.clone());
284    }
285
286    Ok(new_solids)
287}
288
289/// Subtract removes tool solids from base solids, leaving the remaining material.
290pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
291    let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
292    let tools: Vec<Solid> = args.get_kw_arg_typed("tools", &RuntimeType::solids(), exec_state)?;
293
294    let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
295
296    let solids = inner_subtract(solids, tools, tolerance, exec_state, args).await?;
297    Ok(solids.into())
298}
299
300/// Subtract removes tool solids from base solids, leaving the remaining material.
301///
302/// Performs a boolean subtraction operation, removing the volume of one or more
303/// tool solids from one or more base solids. The result is a new solid
304/// representing the material that remains after all tool solids have been cut
305/// away. This function is essential for machining simulations, cavity creation,
306/// and complex multi-body part modeling.
307///
308/// ```no_run
309/// // Subtract a cylinder from a cube using the stdlib functions.
310///
311/// fn cube(center, size) {
312///     return startSketchOn(XY)
313///         |> startProfile(at = [center[0] - size, center[1] - size])
314///         |> line(endAbsolute = [center[0] + size, center[1] - size])
315///         |> line(endAbsolute = [center[0] + size, center[1] + size])
316///         |> line(endAbsolute = [center[0] - size, center[1] + size])
317///         |> close()
318///         |> extrude(length = 10)
319/// }
320///
321/// part001 = cube(center = [0, 0], size = 10)
322/// part002 = cube(center = [7, 3], size = 5)
323///     |> translate(z = 1)
324///
325/// subtractedPart = subtract([part001], tools=[part002])
326/// ```
327///
328/// ```no_run
329/// // Subtract a cylinder from a cube using operators.
330/// // NOTE: This will not work when using codemods through the UI.
331/// // Codemods will generate the stdlib function call instead.
332///
333/// fn cube(center, size) {
334///     return startSketchOn(XY)
335///         |> startProfile(at = [center[0] - size, center[1] - size])
336///         |> line(endAbsolute = [center[0] + size, center[1] - size])
337///         |> line(endAbsolute = [center[0] + size, center[1] + size])
338///         |> line(endAbsolute = [center[0] - size, center[1] + size])
339///         |> close()
340///         |> extrude(length = 10)
341/// }
342///
343/// part001 = cube(center = [0, 0], size = 10)
344/// part002 = cube(center = [7, 3], size = 5)
345///     |> translate(z = 1)
346///
347/// // This is the equivalent of: subtract([part001], tools=[part002])
348/// subtractedPart = part001 - part002
349/// ```
350#[stdlib {
351    name = "subtract",
352    feature_tree_operation = true,
353    unlabeled_first = true,
354    args = {
355        solids = {docs = "The solids to use as the base to subtract from."},
356        tools = {docs = "The solids to subtract."},
357        tolerance = {docs = "The tolerance to use for the subtraction operation."},
358    },
359    tags = ["solid"]
360}]
361pub(crate) async fn inner_subtract(
362    solids: Vec<Solid>,
363    tools: Vec<Solid>,
364    tolerance: Option<TyF64>,
365    exec_state: &mut ExecState,
366    args: Args,
367) -> Result<Vec<Solid>, KclError> {
368    let solid_out_id = exec_state.next_uuid();
369
370    let mut solid = solids[0].clone();
371    solid.set_id(solid_out_id);
372    let mut new_solids = vec![solid.clone()];
373
374    if args.ctx.no_engine_commands().await {
375        return Ok(new_solids);
376    }
377
378    // Flush the fillets for the solids and the tools.
379    let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
380    args.flush_batch_for_solids(exec_state, &combined_solids).await?;
381
382    let result = args
383        .send_modeling_cmd(
384            solid_out_id,
385            ModelingCmd::from(mcmd::BooleanSubtract {
386                target_ids: solids.iter().map(|s| s.id).collect(),
387                tool_ids: tools.iter().map(|s| s.id).collect(),
388                tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
389            }),
390        )
391        .await?;
392
393    let OkWebSocketResponseData::Modeling {
394        modeling_response: OkModelingCmdResponse::BooleanSubtract(BooleanSubtract { extra_solid_ids }),
395    } = result
396    else {
397        return Err(KclError::Internal(KclErrorDetails::new(
398            "Failed to get the result of the subtract operation.".to_string(),
399            vec![args.source_range],
400        )));
401    };
402
403    // If we have more solids, set those as well.
404    if !extra_solid_ids.is_empty() {
405        solid.set_id(extra_solid_ids[0]);
406        new_solids.push(solid.clone());
407    }
408
409    Ok(new_solids)
410}