kcl_lib/std/shell.rs
1//! Standard library shells.
2
3use anyhow::Result;
4use kcl_derive_docs::stdlib;
5use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
6use kittycad_modeling_cmds as kcmc;
7
8use crate::{
9 errors::{KclError, KclErrorDetails},
10 execution::{ExecState, KclValue, Solid, SolidSet},
11 std::{sketch::FaceTag, Args},
12};
13
14/// Create a shell.
15pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
16 let solid_set = args.get_unlabeled_kw_arg("solidSet")?;
17 let thickness = args.get_kw_arg("thickness")?;
18 let faces = args.get_kw_arg("faces")?;
19
20 let result = inner_shell(solid_set, thickness, faces, exec_state, args).await?;
21 Ok(result.into())
22}
23
24/// Remove volume from a 3-dimensional shape such that a wall of the
25/// provided thickness remains, taking volume starting at the provided
26/// face, leaving it open in that direction.
27///
28/// ```no_run
29/// // Remove the end face for the extrusion.
30/// firstSketch = startSketchOn('XY')
31/// |> startProfileAt([-12, 12], %)
32/// |> line(end = [24, 0])
33/// |> line(end = [0, -24])
34/// |> line(end = [-24, 0])
35/// |> close()
36/// |> extrude(length = 6)
37///
38/// // Remove the end face for the extrusion.
39/// shell(
40/// firstSketch,
41/// faces = ['end'],
42/// thickness = 0.25,
43/// )
44/// ```
45///
46/// ```no_run
47/// // Remove the start face for the extrusion.
48/// firstSketch = startSketchOn('-XZ')
49/// |> startProfileAt([-12, 12], %)
50/// |> line(end = [24, 0])
51/// |> line(end = [0, -24])
52/// |> line(end = [-24, 0])
53/// |> close()
54/// |> extrude(length = 6)
55///
56/// // Remove the start face for the extrusion.
57/// shell(
58/// firstSketch,
59/// faces = ['start'],
60/// thickness = 0.25,
61/// )
62/// ```
63///
64/// ```no_run
65/// // Remove a tagged face and the end face for the extrusion.
66/// firstSketch = startSketchOn('XY')
67/// |> startProfileAt([-12, 12], %)
68/// |> line(end = [24, 0])
69/// |> line(end = [0, -24])
70/// |> line(end = [-24, 0], tag = $myTag)
71/// |> close()
72/// |> extrude(length = 6)
73///
74/// // Remove a tagged face for the extrusion.
75/// shell(
76/// firstSketch,
77/// faces = [myTag],
78/// thickness = 0.25,
79/// )
80/// ```
81///
82/// ```no_run
83/// // Remove multiple faces at once.
84/// firstSketch = startSketchOn('XY')
85/// |> startProfileAt([-12, 12], %)
86/// |> line(end = [24, 0])
87/// |> line(end = [0, -24])
88/// |> line(end = [-24, 0], tag = $myTag)
89/// |> close()
90/// |> extrude(length = 6)
91///
92/// // Remove a tagged face and the end face for the extrusion.
93/// shell(
94/// firstSketch,
95/// faces = [myTag, 'end'],
96/// thickness = 0.25,
97/// )
98/// ```
99///
100/// ```no_run
101/// // Shell a sketch on face.
102/// size = 100
103/// case = startSketchOn('-XZ')
104/// |> startProfileAt([-size, -size], %)
105/// |> line(end = [2 * size, 0])
106/// |> line(end = [0, 2 * size])
107/// |> tangentialArcTo([-size, size], %)
108/// |> close()
109/// |> extrude(length = 65)
110///
111/// thing1 = startSketchOn(case, 'end')
112/// |> circle( center = [-size / 2, -size / 2], radius = 25 )
113/// |> extrude(length = 50)
114///
115/// thing2 = startSketchOn(case, 'end')
116/// |> circle( center = [size / 2, -size / 2], radius = 25 )
117/// |> extrude(length = 50)
118///
119/// // We put "case" in the shell function to shell the entire object.
120/// shell(case, faces = ['start'], thickness = 5)
121/// ```
122///
123/// ```no_run
124/// // Shell a sketch on face object on the end face.
125/// size = 100
126/// case = startSketchOn('XY')
127/// |> startProfileAt([-size, -size], %)
128/// |> line(end = [2 * size, 0])
129/// |> line(end = [0, 2 * size])
130/// |> tangentialArcTo([-size, size], %)
131/// |> close()
132/// |> extrude(length = 65)
133///
134/// thing1 = startSketchOn(case, 'end')
135/// |> circle( center = [-size / 2, -size / 2], radius = 25 )
136/// |> extrude(length = 50)
137///
138/// thing2 = startSketchOn(case, 'end')
139/// |> circle( center = [size / 2, -size / 2], radius = 25 )
140/// |> extrude(length = 50)
141///
142/// // We put "thing1" in the shell function to shell the end face of the object.
143/// shell(thing1, faces = ['end'], thickness = 5)
144/// ```
145///
146/// ```no_run
147/// // Shell sketched on face objects on the end face, include all sketches to shell
148/// // the entire object.
149///
150/// size = 100
151/// case = startSketchOn('XY')
152/// |> startProfileAt([-size, -size], %)
153/// |> line(end = [2 * size, 0])
154/// |> line(end = [0, 2 * size])
155/// |> tangentialArcTo([-size, size], %)
156/// |> close()
157/// |> extrude(length = 65)
158///
159/// thing1 = startSketchOn(case, 'end')
160/// |> circle( center = [-size / 2, -size / 2], radius = 25 )
161/// |> extrude(length = 50)
162///
163/// thing2 = startSketchOn(case, 'end')
164/// |> circle( center = [size / 2, -size / 2], radius = 25)
165/// |> extrude(length = 50)
166///
167/// // We put "thing1" and "thing2" in the shell function to shell the end face of the object.
168/// shell([thing1, thing2], faces = ['end'], thickness = 5)
169/// ```
170#[stdlib {
171 name = "shell",
172 feature_tree_operation = true,
173 keywords = true,
174 unlabeled_first = true,
175 args = {
176 solid_set = { docs = "Which solid (or solids) to shell out"},
177 thickness = {docs = "The thickness of the shell"},
178 faces = {docs = "The faces you want removed"},
179 }
180}]
181async fn inner_shell(
182 solid_set: SolidSet,
183 thickness: f64,
184 faces: Vec<FaceTag>,
185 exec_state: &mut ExecState,
186 args: Args,
187) -> Result<SolidSet, KclError> {
188 if faces.is_empty() {
189 return Err(KclError::Type(KclErrorDetails {
190 message: "You must shell at least one face".to_string(),
191 source_ranges: vec![args.source_range],
192 }));
193 }
194
195 let solids: Vec<Box<Solid>> = solid_set.clone().into();
196 if solids.is_empty() {
197 return Err(KclError::Type(KclErrorDetails {
198 message: "You must shell at least one solid".to_string(),
199 source_ranges: vec![args.source_range],
200 }));
201 }
202
203 let mut face_ids = Vec::new();
204 for solid in &solids {
205 // Flush the batch for our fillets/chamfers if there are any.
206 // If we do not do these for sketch on face, things will fail with face does not exist.
207 args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?;
208
209 for tag in &faces {
210 let extrude_plane_id = tag.get_face_id(solid, exec_state, &args, false).await?;
211
212 face_ids.push(extrude_plane_id);
213 }
214 }
215
216 if face_ids.is_empty() {
217 return Err(KclError::Type(KclErrorDetails {
218 message: "Expected at least one valid face".to_string(),
219 source_ranges: vec![args.source_range],
220 }));
221 }
222
223 // Make sure all the solids have the same id, as we are going to shell them all at
224 // once.
225 if !solids.iter().all(|eg| eg.id == solids[0].id) {
226 return Err(KclError::Type(KclErrorDetails {
227 message: "All solids stem from the same root object, like multiple sketch on face extrusions, etc."
228 .to_string(),
229 source_ranges: vec![args.source_range],
230 }));
231 }
232
233 args.batch_modeling_cmd(
234 exec_state.next_uuid(),
235 ModelingCmd::from(mcmd::Solid3dShellFace {
236 hollow: false,
237 face_ids,
238 object_id: solids[0].id,
239 shell_thickness: LengthUnit(thickness),
240 }),
241 )
242 .await?;
243
244 Ok(solid_set)
245}
246
247/// Make the inside of a 3D object hollow.
248pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
249 let (thickness, solid): (f64, Box<Solid>) = args.get_data_and_solid()?;
250
251 let value = inner_hollow(thickness, solid, exec_state, args).await?;
252 Ok(KclValue::Solid { value })
253}
254
255/// Make the inside of a 3D object hollow.
256///
257/// Remove volume from a 3-dimensional shape such that a wall of the
258/// provided thickness remains around the exterior of the shape.
259///
260/// ```no_run
261/// // Hollow a basic sketch.
262/// firstSketch = startSketchOn('XY')
263/// |> startProfileAt([-12, 12], %)
264/// |> line(end = [24, 0])
265/// |> line(end = [0, -24])
266/// |> line(end = [-24, 0])
267/// |> close()
268/// |> extrude(length = 6)
269/// |> hollow (0.25, %)
270/// ```
271///
272/// ```no_run
273/// // Hollow a basic sketch.
274/// firstSketch = startSketchOn('-XZ')
275/// |> startProfileAt([-12, 12], %)
276/// |> line(end = [24, 0])
277/// |> line(end = [0, -24])
278/// |> line(end = [-24, 0])
279/// |> close()
280/// |> extrude(length = 6)
281/// |> hollow (0.5, %)
282/// ```
283///
284/// ```no_run
285/// // Hollow a sketch on face object.
286/// size = 100
287/// case = startSketchOn('-XZ')
288/// |> startProfileAt([-size, -size], %)
289/// |> line(end = [2 * size, 0])
290/// |> line(end = [0, 2 * size])
291/// |> tangentialArcTo([-size, size], %)
292/// |> close()
293/// |> extrude(length = 65)
294///
295/// thing1 = startSketchOn(case, 'end')
296/// |> circle( center = [-size / 2, -size / 2], radius = 25 )
297/// |> extrude(length = 50)
298///
299/// thing2 = startSketchOn(case, 'end')
300/// |> circle( center = [size / 2, -size / 2], radius = 25 )
301/// |> extrude(length = 50)
302///
303/// hollow(0.5, case)
304/// ```
305#[stdlib {
306 name = "hollow",
307 feature_tree_operation = true,
308}]
309async fn inner_hollow(
310 thickness: f64,
311 solid: Box<Solid>,
312 exec_state: &mut ExecState,
313 args: Args,
314) -> Result<Box<Solid>, KclError> {
315 // Flush the batch for our fillets/chamfers if there are any.
316 // If we do not do these for sketch on face, things will fail with face does not exist.
317 args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?;
318
319 args.batch_modeling_cmd(
320 exec_state.next_uuid(),
321 ModelingCmd::from(mcmd::Solid3dShellFace {
322 hollow: true,
323 face_ids: Vec::new(), // This is empty because we want to hollow the entire object.
324 object_id: solid.id,
325 shell_thickness: LengthUnit(thickness),
326 }),
327 )
328 .await?;
329
330 Ok(solid)
331}