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 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::new(
151 "Failed to get the result of the union operation.".to_string(),
152 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::new(
173 "At least two solids are required for an intersect operation.".to_string(),
174 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::new(
277 "Failed to get the result of the intersection operation.".to_string(),
278 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 let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
297
298 let solids = inner_subtract(solids, tools, tolerance, exec_state, args).await?;
299 Ok(solids.into())
300}
301
302/// Subtract removes tool solids from base solids, leaving the remaining material.
303///
304/// Performs a boolean subtraction operation, removing the volume of one or more
305/// tool solids from one or more base solids. The result is a new solid
306/// representing the material that remains after all tool solids have been cut
307/// away. This function is essential for machining simulations, cavity creation,
308/// and complex multi-body part modeling.
309///
310/// ```no_run
311/// // Subtract a cylinder from a cube using the stdlib functions.
312///
313/// fn cube(center, size) {
314/// return startSketchOn(XY)
315/// |> startProfile(at = [center[0] - size, center[1] - size])
316/// |> line(endAbsolute = [center[0] + size, center[1] - size])
317/// |> line(endAbsolute = [center[0] + size, center[1] + size])
318/// |> line(endAbsolute = [center[0] - size, center[1] + size])
319/// |> close()
320/// |> extrude(length = 10)
321/// }
322///
323/// part001 = cube(center = [0, 0], size = 10)
324/// part002 = cube(center = [7, 3], size = 5)
325/// |> translate(z = 1)
326///
327/// subtractedPart = subtract([part001], tools=[part002])
328/// ```
329///
330/// ```no_run
331/// // Subtract a cylinder from a cube using operators.
332/// // NOTE: This will not work when using codemods through the UI.
333/// // Codemods will generate the stdlib function call instead.
334///
335/// fn cube(center, size) {
336/// return startSketchOn(XY)
337/// |> startProfile(at = [center[0] - size, center[1] - size])
338/// |> line(endAbsolute = [center[0] + size, center[1] - size])
339/// |> line(endAbsolute = [center[0] + size, center[1] + size])
340/// |> line(endAbsolute = [center[0] - size, center[1] + size])
341/// |> close()
342/// |> extrude(length = 10)
343/// }
344///
345/// part001 = cube(center = [0, 0], size = 10)
346/// part002 = cube(center = [7, 3], size = 5)
347/// |> translate(z = 1)
348///
349/// // This is the equivalent of: subtract([part001], tools=[part002])
350/// subtractedPart = part001 - part002
351/// ```
352#[stdlib {
353 name = "subtract",
354 feature_tree_operation = true,
355 keywords = true,
356 unlabeled_first = true,
357 args = {
358 solids = {docs = "The solids to use as the base to subtract from."},
359 tools = {docs = "The solids to subtract."},
360 tolerance = {docs = "The tolerance to use for the subtraction operation."},
361 },
362 tags = ["solid"]
363}]
364pub(crate) async fn inner_subtract(
365 solids: Vec<Solid>,
366 tools: Vec<Solid>,
367 tolerance: Option<TyF64>,
368 exec_state: &mut ExecState,
369 args: Args,
370) -> Result<Vec<Solid>, KclError> {
371 let solid_out_id = exec_state.next_uuid();
372
373 let mut solid = solids[0].clone();
374 solid.set_id(solid_out_id);
375 let mut new_solids = vec![solid.clone()];
376
377 if args.ctx.no_engine_commands().await {
378 return Ok(new_solids);
379 }
380
381 // Flush the fillets for the solids and the tools.
382 let combined_solids = solids.iter().chain(tools.iter()).cloned().collect::<Vec<Solid>>();
383 args.flush_batch_for_solids(exec_state, &combined_solids).await?;
384
385 let result = args
386 .send_modeling_cmd(
387 solid_out_id,
388 ModelingCmd::from(mcmd::BooleanSubtract {
389 target_ids: solids.iter().map(|s| s.id).collect(),
390 tool_ids: tools.iter().map(|s| s.id).collect(),
391 tolerance: LengthUnit(tolerance.map(|t| t.n).unwrap_or(DEFAULT_TOLERANCE)),
392 }),
393 )
394 .await?;
395
396 let OkWebSocketResponseData::Modeling {
397 modeling_response: OkModelingCmdResponse::BooleanSubtract(BooleanSubtract { extra_solid_ids }),
398 } = result
399 else {
400 return Err(KclError::Internal(KclErrorDetails::new(
401 "Failed to get the result of the subtract operation.".to_string(),
402 vec![args.source_range],
403 )));
404 };
405
406 // If we have more solids, set those as well.
407 if !extra_solid_ids.is_empty() {
408 solid.set_id(extra_solid_ids[0]);
409 new_solids.push(solid.clone());
410 }
411
412 Ok(new_solids)
413}