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 {
28            message: "At least two solids are required for a union operation.".to_string(),
29            source_ranges: 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    keywords = true,
110    unlabeled_first = true,
111    args = {
112        solids = {docs = "The solids to union."},
113        tolerance = {docs = "The tolerance to use for the union operation."},
114    },
115    tags = ["solid"]
116}]
117pub(crate) async fn inner_union(
118    solids: Vec<Solid>,
119    tolerance: Option<TyF64>,
120    exec_state: &mut ExecState,
121    args: Args,
122) -> Result<Vec<Solid>, KclError> {
123    let solid_out_id = exec_state.next_uuid();
124
125    let mut solid = solids[0].clone();
126    solid.set_id(solid_out_id);
127    let mut new_solids = vec![solid.clone()];
128
129    if args.ctx.no_engine_commands().await {
130        return Ok(new_solids);
131    }
132
133    // Flush the fillets for the solids.
134    args.flush_batch_for_solids(exec_state, &solids).await?;
135
136    let result = args
137        .send_modeling_cmd(
138            solid_out_id,
139            ModelingCmd::from(mcmd::BooleanUnion {
140                solid_ids: solids.iter().map(|s| s.id).collect(),
141                tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
142            }),
143        )
144        .await?;
145
146    let OkWebSocketResponseData::Modeling {
147        modeling_response: OkModelingCmdResponse::BooleanUnion(BooleanUnion { extra_solid_ids }),
148    } = result
149    else {
150        return Err(KclError::Internal(KclErrorDetails {
151            message: "Failed to get the result of the union operation.".to_string(),
152            source_ranges: vec![args.source_range],
153        }));
154    };
155
156    // If we have more solids, set those as well.
157    if !extra_solid_ids.is_empty() {
158        solid.set_id(extra_solid_ids[0]);
159        new_solids.push(solid.clone());
160    }
161
162    Ok(new_solids)
163}
164
165/// Intersect returns the shared volume between multiple solids, preserving only
166/// overlapping regions.
167pub async fn intersect(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
168    let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
169    let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
170
171    if solids.len() < 2 {
172        return Err(KclError::UndefinedValue(KclErrorDetails {
173            message: "At least two solids are required for an intersect operation.".to_string(),
174            source_ranges: vec![args.source_range],
175        }));
176    }
177
178    let solids = inner_intersect(solids, tolerance, exec_state, args).await?;
179    Ok(solids.into())
180}
181
182/// Intersect returns the shared volume between multiple solids, preserving only
183/// overlapping regions.
184///
185/// Intersect computes the geometric intersection of multiple solid bodies,
186/// returning a new solid representing the volume that is common to all input
187/// solids. This operation is useful for determining shared material regions,
188/// verifying fit, and analyzing overlapping geometries in assemblies.
189///
190/// ```no_run
191/// // Intersect two cubes using the stdlib functions.
192///
193/// fn cube(center, size) {
194///     return startSketchOn(XY)
195///         |> startProfile(at = [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///         |> line(endAbsolute = [center[0] - size, center[1] + size])
199///         |> close()
200///         |> extrude(length = 10)
201/// }
202///
203/// part001 = cube(center = [0, 0], size = 10)
204/// part002 = cube(center = [7, 3], size = 5)
205///     |> translate(z = 1)
206///
207/// intersectedPart = intersect([part001, part002])
208/// ```
209///
210/// ```no_run
211/// // Intersect two cubes using operators.
212/// // NOTE: This will not work when using codemods through the UI.
213/// // Codemods will generate the stdlib function call instead.
214///
215/// fn cube(center, size) {
216///     return startSketchOn(XY)
217///         |> startProfile(at = [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///         |> line(endAbsolute = [center[0] - size, center[1] + size])
221///         |> close()
222///         |> extrude(length = 10)
223/// }
224///
225/// part001 = cube(center = [0, 0], size = 10)
226/// part002 = cube(center = [7, 3], size = 5)
227///     |> translate(z = 1)
228///
229/// // This is the equivalent of: intersect([part001, part002])
230/// intersectedPart = part001 & part002
231/// ```
232#[stdlib {
233    name = "intersect",
234    feature_tree_operation = true,
235    keywords = true,
236    unlabeled_first = true,
237    args = {
238        solids = {docs = "The solids to intersect."},
239        tolerance = {docs = "The tolerance to use for the intersection operation."},
240    },
241    tags = ["solid"]
242}]
243pub(crate) async fn inner_intersect(
244    solids: Vec<Solid>,
245    tolerance: Option<TyF64>,
246    exec_state: &mut ExecState,
247    args: Args,
248) -> Result<Vec<Solid>, KclError> {
249    let solid_out_id = exec_state.next_uuid();
250
251    let mut solid = solids[0].clone();
252    solid.set_id(solid_out_id);
253    let mut new_solids = vec![solid.clone()];
254
255    if args.ctx.no_engine_commands().await {
256        return Ok(new_solids);
257    }
258
259    // Flush the fillets for the solids.
260    args.flush_batch_for_solids(exec_state, &solids).await?;
261
262    let result = args
263        .send_modeling_cmd(
264            solid_out_id,
265            ModelingCmd::from(mcmd::BooleanIntersection {
266                solid_ids: solids.iter().map(|s| s.id).collect(),
267                tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
268            }),
269        )
270        .await?;
271
272    let OkWebSocketResponseData::Modeling {
273        modeling_response: OkModelingCmdResponse::BooleanIntersection(BooleanIntersection { extra_solid_ids }),
274    } = result
275    else {
276        return Err(KclError::Internal(KclErrorDetails {
277            message: "Failed to get the result of the intersection operation.".to_string(),
278            source_ranges: vec![args.source_range],
279        }));
280    };
281
282    // If we have more solids, set those as well.
283    if !extra_solid_ids.is_empty() {
284        solid.set_id(extra_solid_ids[0]);
285        new_solids.push(solid.clone());
286    }
287
288    Ok(new_solids)
289}
290
291/// Subtract removes tool solids from base solids, leaving the remaining material.
292pub async fn subtract(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
293    let solids: Vec<Solid> = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
294    let tools: Vec<Solid> = args.get_kw_arg_typed("tools", &RuntimeType::solids(), exec_state)?;
295
296    if solids.len() > 1 {
297        return Err(KclError::UndefinedValue(KclErrorDetails {
298            message: "Only one solid is allowed for a subtract operation, currently.".to_string(),
299            source_ranges: vec![args.source_range],
300        }));
301    }
302
303    if tools.len() > 1 {
304        return Err(KclError::UndefinedValue(KclErrorDetails {
305            message: "Only one tool is allowed for a subtract operation, currently.".to_string(),
306            source_ranges: vec![args.source_range],
307        }));
308    }
309
310    let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
311
312    let solids = inner_subtract(solids, tools, tolerance, exec_state, args).await?;
313    Ok(solids.into())
314}
315
316/// Subtract removes tool solids from base solids, leaving the remaining material.
317///
318/// Performs a boolean subtraction operation, removing the volume of one or more
319/// tool solids from one or more base solids. The result is a new solid
320/// representing the material that remains after all tool solids have been cut
321/// away. This function is essential for machining simulations, cavity creation,
322/// and complex multi-body part modeling.
323///
324/// ```no_run
325/// // Subtract a cylinder from a cube using the stdlib functions.
326///
327/// fn cube(center, size) {
328///     return startSketchOn(XY)
329///         |> startProfile(at = [center[0] - size, center[1] - size])
330///         |> line(endAbsolute = [center[0] + size, center[1] - size])
331///         |> line(endAbsolute = [center[0] + size, center[1] + size])
332///         |> line(endAbsolute = [center[0] - size, center[1] + size])
333///         |> close()
334///         |> extrude(length = 10)
335/// }
336///
337/// part001 = cube(center = [0, 0], size = 10)
338/// part002 = cube(center = [7, 3], size = 5)
339///     |> translate(z = 1)
340///
341/// subtractedPart = subtract([part001], tools=[part002])
342/// ```
343///
344/// ```no_run
345/// // Subtract a cylinder from a cube using operators.
346/// // NOTE: This will not work when using codemods through the UI.
347/// // Codemods will generate the stdlib function call instead.
348///
349/// fn cube(center, size) {
350///     return startSketchOn(XY)
351///         |> startProfile(at = [center[0] - size, center[1] - size])
352///         |> line(endAbsolute = [center[0] + size, center[1] - size])
353///         |> line(endAbsolute = [center[0] + size, center[1] + size])
354///         |> line(endAbsolute = [center[0] - size, center[1] + size])
355///         |> close()
356///         |> extrude(length = 10)
357/// }
358///
359/// part001 = cube(center = [0, 0], size = 10)
360/// part002 = cube(center = [7, 3], size = 5)
361///     |> translate(z = 1)
362///
363/// // This is the equivalent of: subtract([part001], tools=[part002])
364/// subtractedPart = part001 - part002
365/// ```
366#[stdlib {
367    name = "subtract",
368    feature_tree_operation = true,
369    keywords = true,
370    unlabeled_first = true,
371    args = {
372        solids = {docs = "The solids to use as the base to subtract from."},
373        tools = {docs = "The solids to subtract."},
374        tolerance = {docs = "The tolerance to use for the subtraction operation."},
375    },
376    tags = ["solid"]
377}]
378pub(crate) async fn inner_subtract(
379    solids: Vec<Solid>,
380    tools: Vec<Solid>,
381    tolerance: Option<TyF64>,
382    exec_state: &mut ExecState,
383    args: Args,
384) -> Result<Vec<Solid>, KclError> {
385    let solid_out_id = exec_state.next_uuid();
386
387    let mut solid = solids[0].clone();
388    solid.set_id(solid_out_id);
389    let mut new_solids = vec![solid.clone()];
390
391    if args.ctx.no_engine_commands().await {
392        return Ok(new_solids);
393    }
394
395    // Flush the fillets for the solids and the tools.
396    let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
397    args.flush_batch_for_solids(exec_state, &combined_solids).await?;
398
399    let result = args
400        .send_modeling_cmd(
401            solid_out_id,
402            ModelingCmd::from(mcmd::BooleanSubtract {
403                target_ids: solids.iter().map(|s| s.id).collect(),
404                tool_ids: tools.iter().map(|s| s.id).collect(),
405                tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
406            }),
407        )
408        .await?;
409
410    let OkWebSocketResponseData::Modeling {
411        modeling_response: OkModelingCmdResponse::BooleanSubtract(BooleanSubtract { extra_solid_ids }),
412    } = result
413    else {
414        return Err(KclError::Internal(KclErrorDetails {
415            message: "Failed to get the result of the subtract operation.".to_string(),
416            source_ranges: vec![args.source_range],
417        }));
418    };
419
420    // If we have more solids, set those as well.
421    if !extra_solid_ids.is_empty() {
422        solid.set_id(extra_solid_ids[0]);
423        new_solids.push(solid.clone());
424    }
425
426    Ok(new_solids)
427}