kcl_lib/std/
extrude.rs

1//! Functions related to extruding.
2
3use std::collections::HashMap;
4
5use anyhow::Result;
6use kcl_derive_docs::stdlib;
7use kcmc::{
8    each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, output::ExtrusionFaceInfo,
9    shared::ExtrusionFaceCapType, websocket::OkWebSocketResponseData, ModelingCmd,
10};
11use kittycad_modeling_cmds as kcmc;
12use uuid::Uuid;
13
14use crate::{
15    errors::{KclError, KclErrorDetails},
16    execution::{
17        ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSet, SketchSurface, Solid,
18        SolidSet,
19    },
20    std::Args,
21};
22
23/// Extrudes by a given amount.
24pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
25    let sketch_set = args.get_unlabeled_kw_arg("sketch_set")?;
26    let length = args.get_kw_arg("length")?;
27
28    let result = inner_extrude(sketch_set, length, exec_state, args).await?;
29
30    Ok(result.into())
31}
32
33/// Extend a 2-dimensional sketch through a third dimension in order to
34/// create new 3-dimensional volume, or if extruded into an existing volume,
35/// cut into an existing solid.
36///
37/// ```no_run
38/// example = startSketchOn('XZ')
39///   |> startProfileAt([0, 0], %)
40///   |> line(end = [10, 0])
41///   |> arc({
42///     angleStart = 120,
43///     angleEnd = 0,
44///     radius = 5,
45///   }, %)
46///   |> line(end = [5, 0])
47///   |> line(end = [0, 10])
48///   |> bezierCurve({
49///     control1 = [-10, 0],
50///     control2 = [2, 10],
51///     to = [-5, 10],
52///   }, %)
53///   |> line(end = [-5, -2])
54///   |> close()
55///   |> extrude(length = 10)
56/// ```
57///
58/// ```no_run
59/// exampleSketch = startSketchOn('XZ')
60///   |> startProfileAt([-10, 0], %)
61///   |> arc({
62///     angleStart = 120,
63///     angleEnd = -60,
64///     radius = 5,
65///   }, %)
66///   |> line(end = [10, 0])
67///   |> line(end = [5, 0])
68///   |> bezierCurve({
69///     control1 = [-3, 0],
70///     control2 = [2, 10],
71///     to = [-5, 10],
72///   }, %)
73///   |> line(end = [-4, 10])
74///   |> line(end = [-5, -2])
75///   |> close()
76///
77/// example = extrude(exampleSketch, length = 10)
78/// ```
79#[stdlib {
80    name = "extrude",
81    feature_tree_operation = true,
82    keywords = true,
83    unlabeled_first = true,
84    args = {
85        sketch_set = { docs = "Which sketches should be extruded"},
86        length = { docs = "How far to extrude the given sketches"},
87    }
88}]
89async fn inner_extrude(
90    sketch_set: SketchSet,
91    length: f64,
92    exec_state: &mut ExecState,
93    args: Args,
94) -> Result<SolidSet, KclError> {
95    let id = exec_state.next_uuid();
96
97    // Extrude the element(s).
98    let sketches: Vec<Sketch> = sketch_set.into();
99    let mut solids = Vec::new();
100    for sketch in &sketches {
101        // Before we extrude, we need to enable the sketch mode.
102        // We do this here in case extrude is called out of order.
103        args.batch_modeling_cmd(
104            exec_state.next_uuid(),
105            ModelingCmd::from(mcmd::EnableSketchMode {
106                animated: false,
107                ortho: false,
108                entity_id: sketch.on.id(),
109                adjust_camera: false,
110                planar_normal: if let SketchSurface::Plane(plane) = &sketch.on {
111                    // We pass in the normal for the plane here.
112                    Some(plane.z_axis.into())
113                } else {
114                    None
115                },
116            }),
117        )
118        .await?;
119
120        // TODO: We're reusing the same UUID for multiple commands.  This seems
121        // like the artifact graph would never be able to find all the
122        // responses.
123        args.batch_modeling_cmd(
124            id,
125            ModelingCmd::from(mcmd::Extrude {
126                target: sketch.id.into(),
127                distance: LengthUnit(length),
128                faces: Default::default(),
129            }),
130        )
131        .await?;
132
133        // Disable the sketch mode.
134        args.batch_modeling_cmd(
135            exec_state.next_uuid(),
136            ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
137        )
138        .await?;
139        solids.push(do_post_extrude(sketch.clone(), id.into(), length, exec_state, args.clone()).await?);
140    }
141
142    Ok(solids.into())
143}
144
145pub(crate) async fn do_post_extrude(
146    sketch: Sketch,
147    solid_id: ArtifactId,
148    length: f64,
149    exec_state: &mut ExecState,
150    args: Args,
151) -> Result<Box<Solid>, KclError> {
152    // Bring the object to the front of the scene.
153    // See: https://github.com/KittyCAD/modeling-app/issues/806
154    args.batch_modeling_cmd(
155        exec_state.next_uuid(),
156        ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
157    )
158    .await?;
159
160    // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
161    // So, let's just use the first one.
162    let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
163        return Err(KclError::Type(KclErrorDetails {
164            message: "Expected a non-empty sketch".to_string(),
165            source_ranges: vec![args.source_range],
166        }));
167    };
168
169    let mut sketch = sketch.clone();
170
171    // If we were sketching on a face, we need the original face id.
172    if let SketchSurface::Face(ref face) = sketch.on {
173        sketch.id = face.solid.sketch.id;
174    }
175
176    let solid3d_info = args
177        .send_modeling_cmd(
178            exec_state.next_uuid(),
179            ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
180                edge_id: any_edge_id,
181                object_id: sketch.id,
182            }),
183        )
184        .await?;
185
186    let face_infos = if let OkWebSocketResponseData::Modeling {
187        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
188    } = solid3d_info
189    {
190        data.faces
191    } else {
192        vec![]
193    };
194
195    for (curve_id, face_id) in face_infos
196        .iter()
197        .filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
198        .filter_map(|face_info| {
199            if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
200                Some((curve_id, face_id))
201            } else {
202                None
203            }
204        })
205    {
206        // Batch these commands, because the Rust code doesn't actually care about the outcome.
207        // So, there's no need to await them.
208        // Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm)
209        // uses this to build the artifact graph, which the UI needs.
210        args.batch_modeling_cmd(
211            exec_state.next_uuid(),
212            ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
213                edge_id: curve_id,
214                object_id: sketch.id,
215                face_id,
216            }),
217        )
218        .await?;
219
220        args.batch_modeling_cmd(
221            exec_state.next_uuid(),
222            ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
223                edge_id: curve_id,
224                object_id: sketch.id,
225                face_id,
226            }),
227        )
228        .await?;
229    }
230
231    let Faces {
232        sides: face_id_map,
233        start_cap_id,
234        end_cap_id,
235    } = analyze_faces(exec_state, &args, face_infos).await;
236    // Iterate over the sketch.value array and add face_id to GeoMeta
237    let no_engine_commands = args.ctx.no_engine_commands().await;
238    let new_value = sketch
239        .paths
240        .iter()
241        .flat_map(|path| {
242            if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
243                match path {
244                    Path::Arc { .. }
245                    | Path::TangentialArc { .. }
246                    | Path::TangentialArcTo { .. }
247                    | Path::Circle { .. }
248                    | Path::CircleThreePoint { .. } => {
249                        let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
250                            face_id: *actual_face_id,
251                            tag: path.get_base().tag.clone(),
252                            geo_meta: GeoMeta {
253                                id: path.get_base().geo_meta.id,
254                                metadata: path.get_base().geo_meta.metadata,
255                            },
256                        });
257                        Some(extrude_surface)
258                    }
259                    Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
260                        let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
261                            face_id: *actual_face_id,
262                            tag: path.get_base().tag.clone(),
263                            geo_meta: GeoMeta {
264                                id: path.get_base().geo_meta.id,
265                                metadata: path.get_base().geo_meta.metadata,
266                            },
267                        });
268                        Some(extrude_surface)
269                    }
270                }
271            } else if no_engine_commands {
272                // Only pre-populate the extrude surface if we are in mock mode.
273
274                let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
275                    // pushing this values with a fake face_id to make extrudes mock-execute safe
276                    face_id: exec_state.next_uuid(),
277                    tag: path.get_base().tag.clone(),
278                    geo_meta: GeoMeta {
279                        id: path.get_base().geo_meta.id,
280                        metadata: path.get_base().geo_meta.metadata,
281                    },
282                });
283                Some(extrude_surface)
284            } else {
285                None
286            }
287        })
288        .collect();
289
290    Ok(Box::new(Solid {
291        // Ok so you would think that the id would be the id of the solid,
292        // that we passed in to the function, but it's actually the id of the
293        // sketch.
294        id: sketch.id,
295        artifact_id: solid_id,
296        value: new_value,
297        meta: sketch.meta.clone(),
298        units: sketch.units,
299        sketch,
300        height: length,
301        start_cap_id,
302        end_cap_id,
303        edge_cuts: vec![],
304    }))
305}
306
307#[derive(Default)]
308struct Faces {
309    /// Maps curve ID to face ID for each side.
310    sides: HashMap<Uuid, Option<Uuid>>,
311    /// Top face ID.
312    end_cap_id: Option<Uuid>,
313    /// Bottom face ID.
314    start_cap_id: Option<Uuid>,
315}
316
317async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
318    let mut faces = Faces {
319        sides: HashMap::with_capacity(face_infos.len()),
320        ..Default::default()
321    };
322    if args.ctx.no_engine_commands().await {
323        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
324        faces.start_cap_id = Some(exec_state.next_uuid());
325        faces.end_cap_id = Some(exec_state.next_uuid());
326    }
327    for face_info in face_infos {
328        match face_info.cap {
329            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
330            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
331            ExtrusionFaceCapType::Both => {
332                faces.end_cap_id = face_info.face_id;
333                faces.start_cap_id = face_info.face_id;
334            }
335            ExtrusionFaceCapType::None => {
336                if let Some(curve_id) = face_info.curve_id {
337                    faces.sides.insert(curve_id, face_info.face_id);
338                }
339            }
340        }
341    }
342    faces
343}