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,
9    length_unit::LengthUnit,
10    ok_response::OkModelingCmdResponse,
11    output::ExtrusionFaceInfo,
12    shared::{ExtrusionFaceCapType, Opposite},
13    websocket::{ModelingCmdReq, OkWebSocketResponseData},
14    ModelingCmd,
15};
16use kittycad_modeling_cmds::{self as kcmc};
17use uuid::Uuid;
18
19use super::args::TyF64;
20use crate::{
21    errors::{KclError, KclErrorDetails},
22    execution::{
23        types::RuntimeType, ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSurface,
24        Solid,
25    },
26    parsing::ast::types::TagNode,
27    std::Args,
28};
29
30/// Extrudes by a given amount.
31pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
32    let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
33    let length: TyF64 = args.get_kw_arg_typed("length", &RuntimeType::length(), exec_state)?;
34    let symmetric = args.get_kw_arg_opt("symmetric")?;
35    let bidirectional_length: Option<TyF64> =
36        args.get_kw_arg_opt_typed("bidirectionalLength", &RuntimeType::length(), exec_state)?;
37    let tag_start = args.get_kw_arg_opt("tagStart")?;
38    let tag_end = args.get_kw_arg_opt("tagEnd")?;
39
40    let result = inner_extrude(
41        sketches,
42        length,
43        symmetric,
44        bidirectional_length,
45        tag_start,
46        tag_end,
47        exec_state,
48        args,
49    )
50    .await?;
51
52    Ok(result.into())
53}
54
55/// Extend a 2-dimensional sketch through a third dimension in order to
56/// create new 3-dimensional volume, or if extruded into an existing volume,
57/// cut into an existing solid.
58///
59/// You can provide more than one sketch to extrude, and they will all be
60/// extruded in the same direction.
61///
62/// ```no_run
63/// example = startSketchOn(XZ)
64///   |> startProfile(at = [0, 0])
65///   |> line(end = [10, 0])
66///   |> arc(
67///     angleStart = 120,
68///     angleEnd = 0,
69///     radius = 5,
70///   )
71///   |> line(end = [5, 0])
72///   |> line(end = [0, 10])
73///   |> bezierCurve(
74///        control1 = [-10, 0],
75///        control2 = [2, 10],
76///        end = [-5, 10],
77///      )
78///   |> line(end = [-5, -2])
79///   |> close()
80///   |> extrude(length = 10)
81/// ```
82///
83/// ```no_run
84/// exampleSketch = startSketchOn(XZ)
85///   |> startProfile(at = [-10, 0])
86///   |> arc(
87///     angleStart = 120,
88///     angleEnd = -60,
89///     radius = 5,
90///   )
91///   |> line(end = [10, 0])
92///   |> line(end = [5, 0])
93///   |> bezierCurve(
94///        control1 = [-3, 0],
95///        control2 = [2, 10],
96///        end = [-5, 10],
97///      )
98///   |> line(end = [-4, 10])
99///   |> line(end = [-5, -2])
100///   |> close()
101///
102/// example = extrude(exampleSketch, length = 10)
103/// ```
104///
105/// ```no_run
106/// exampleSketch = startSketchOn(XZ)
107///   |> startProfile(at = [-10, 0])
108///   |> arc(
109///     angleStart = 120,
110///     angleEnd = -60,
111///     radius = 5,
112///   )
113///   |> line(end = [10, 0])
114///   |> line(end = [5, 0])
115///   |> bezierCurve(
116///        control1 = [-3, 0],
117///        control2 = [2, 10],
118///        end = [-5, 10],
119///      )
120///   |> line(end = [-4, 10])
121///   |> line(end = [-5, -2])
122///   |> close()
123///
124/// example = extrude(exampleSketch, length = 20, symmetric = true)
125/// ```
126///
127/// ```no_run
128/// exampleSketch = startSketchOn(XZ)
129///   |> startProfile(at = [-10, 0])
130///   |> arc(
131///     angleStart = 120,
132///     angleEnd = -60,
133///     radius = 5,
134///   )
135///   |> line(end = [10, 0])
136///   |> line(end = [5, 0])
137///   |> bezierCurve(
138///        control1 = [-3, 0],
139///        control2 = [2, 10],
140///        end = [-5, 10],
141///      )
142///   |> line(end = [-4, 10])
143///   |> line(end = [-5, -2])
144///   |> close()
145///
146/// example = extrude(exampleSketch, length = 10, bidirectionalLength = 50)
147/// ```
148#[stdlib {
149    name = "extrude",
150    feature_tree_operation = true,
151    unlabeled_first = true,
152    args = {
153        sketches = { docs = "Which sketch or sketches should be extruded"},
154        length = { docs = "How far to extrude the given sketches"},
155        symmetric = { docs = "If true, the extrusion will happen symmetrically around the sketch. Otherwise, the extrusion will happen on only one side of the sketch." },
156        bidirectional_length = { docs = "If specified, will also extrude in the opposite direction to 'distance' to the specified distance. If 'symmetric' is true, this value is ignored."},
157        tag_start = { docs = "A named tag for the face at the start of the extrusion, i.e. the original sketch" },
158        tag_end = { docs = "A named tag for the face at the end of the extrusion, i.e. the new face created by extruding the original sketch" },
159    },
160    tags = ["sketch"]
161}]
162#[allow(clippy::too_many_arguments)]
163async fn inner_extrude(
164    sketches: Vec<Sketch>,
165    length: TyF64,
166    symmetric: Option<bool>,
167    bidirectional_length: Option<TyF64>,
168    tag_start: Option<TagNode>,
169    tag_end: Option<TagNode>,
170    exec_state: &mut ExecState,
171    args: Args,
172) -> Result<Vec<Solid>, KclError> {
173    // Extrude the element(s).
174    let mut solids = Vec::new();
175
176    if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
177        return Err(KclError::Semantic(KclErrorDetails::new(
178            "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
179                .to_owned(),
180            vec![args.source_range],
181        )));
182    }
183
184    let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
185
186    let opposite = match (symmetric, bidirection) {
187        (Some(true), _) => Opposite::Symmetric,
188        (None, None) => Opposite::None,
189        (Some(false), None) => Opposite::None,
190        (None, Some(length)) => Opposite::Other(length),
191        (Some(false), Some(length)) => Opposite::Other(length),
192    };
193
194    for sketch in &sketches {
195        let id = exec_state.next_uuid();
196        args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds(
197            exec_state,
198            ModelingCmdReq {
199                cmd_id: id.into(),
200                cmd: ModelingCmd::from(mcmd::Extrude {
201                    target: sketch.id.into(),
202                    distance: LengthUnit(length.to_mm()),
203                    faces: Default::default(),
204                    opposite: opposite.clone(),
205                }),
206            },
207        ))
208        .await?;
209
210        solids.push(
211            do_post_extrude(
212                sketch,
213                id.into(),
214                length.clone(),
215                false,
216                &NamedCapTags {
217                    start: tag_start.as_ref(),
218                    end: tag_end.as_ref(),
219                },
220                exec_state,
221                &args,
222                None,
223            )
224            .await?,
225        );
226    }
227
228    Ok(solids)
229}
230
231#[derive(Debug, Default)]
232pub(crate) struct NamedCapTags<'a> {
233    pub start: Option<&'a TagNode>,
234    pub end: Option<&'a TagNode>,
235}
236
237#[allow(clippy::too_many_arguments)]
238pub(crate) async fn do_post_extrude<'a>(
239    sketch: &Sketch,
240    solid_id: ArtifactId,
241    length: TyF64,
242    sectional: bool,
243    named_cap_tags: &'a NamedCapTags<'a>,
244    exec_state: &mut ExecState,
245    args: &Args,
246    edge_id: Option<Uuid>,
247) -> Result<Solid, KclError> {
248    // Bring the object to the front of the scene.
249    // See: https://github.com/KittyCAD/modeling-app/issues/806
250    args.batch_modeling_cmd(
251        exec_state.next_uuid(),
252        ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
253    )
254    .await?;
255
256    let any_edge_id = if let Some(id) = edge_id {
257        id
258    } else if let Some(edge_id) = sketch.mirror {
259        edge_id
260    } else {
261        // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
262        // So, let's just use the first one.
263        let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
264            return Err(KclError::Type(KclErrorDetails::new(
265                "Expected a non-empty sketch".to_owned(),
266                vec![args.source_range],
267            )));
268        };
269        any_edge_id
270    };
271
272    let mut sketch = sketch.clone();
273
274    // If we were sketching on a face, we need the original face id.
275    if let SketchSurface::Face(ref face) = sketch.on {
276        sketch.id = face.solid.sketch.id;
277    }
278
279    let solid3d_info = args
280        .send_modeling_cmd(
281            exec_state.next_uuid(),
282            ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
283                edge_id: any_edge_id,
284                object_id: sketch.id,
285            }),
286        )
287        .await?;
288
289    let face_infos = if let OkWebSocketResponseData::Modeling {
290        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
291    } = solid3d_info
292    {
293        data.faces
294    } else {
295        vec![]
296    };
297
298    // Only do this if we need the artifact graph.
299    #[cfg(feature = "artifact-graph")]
300    {
301        // Getting the ids of a sectional sweep does not work well and we cannot guarantee that
302        // any of these call will not just fail.
303        if !sectional {
304            args.batch_modeling_cmd(
305                exec_state.next_uuid(),
306                ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
307                    object_id: sketch.id,
308                    edge_id: any_edge_id,
309                }),
310            )
311            .await?;
312        }
313    }
314
315    let Faces {
316        sides: face_id_map,
317        start_cap_id,
318        end_cap_id,
319    } = analyze_faces(exec_state, args, face_infos).await;
320
321    // Iterate over the sketch.value array and add face_id to GeoMeta
322    let no_engine_commands = args.ctx.no_engine_commands().await;
323    let mut new_value: Vec<ExtrudeSurface> = sketch
324        .paths
325        .iter()
326        .flat_map(|path| {
327            if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
328                match path {
329                    Path::Arc { .. }
330                    | Path::TangentialArc { .. }
331                    | Path::TangentialArcTo { .. }
332                    | Path::Circle { .. }
333                    | Path::CircleThreePoint { .. } => {
334                        let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
335                            face_id: *actual_face_id,
336                            tag: path.get_base().tag.clone(),
337                            geo_meta: GeoMeta {
338                                id: path.get_base().geo_meta.id,
339                                metadata: path.get_base().geo_meta.metadata,
340                            },
341                        });
342                        Some(extrude_surface)
343                    }
344                    Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
345                        let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
346                            face_id: *actual_face_id,
347                            tag: path.get_base().tag.clone(),
348                            geo_meta: GeoMeta {
349                                id: path.get_base().geo_meta.id,
350                                metadata: path.get_base().geo_meta.metadata,
351                            },
352                        });
353                        Some(extrude_surface)
354                    }
355                    Path::ArcThreePoint { .. } => {
356                        let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
357                            face_id: *actual_face_id,
358                            tag: path.get_base().tag.clone(),
359                            geo_meta: GeoMeta {
360                                id: path.get_base().geo_meta.id,
361                                metadata: path.get_base().geo_meta.metadata,
362                            },
363                        });
364                        Some(extrude_surface)
365                    }
366                }
367            } else if no_engine_commands {
368                // Only pre-populate the extrude surface if we are in mock mode.
369
370                let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
371                    // pushing this values with a fake face_id to make extrudes mock-execute safe
372                    face_id: exec_state.next_uuid(),
373                    tag: path.get_base().tag.clone(),
374                    geo_meta: GeoMeta {
375                        id: path.get_base().geo_meta.id,
376                        metadata: path.get_base().geo_meta.metadata,
377                    },
378                });
379                Some(extrude_surface)
380            } else {
381                None
382            }
383        })
384        .collect();
385
386    // Add the tags for the start or end caps.
387    if let Some(tag_start) = named_cap_tags.start {
388        let Some(start_cap_id) = start_cap_id else {
389            return Err(KclError::Type(KclErrorDetails::new(
390                format!(
391                    "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
392                    tag_start.name, sketch.id
393                ),
394                vec![args.source_range],
395            )));
396        };
397
398        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
399            face_id: start_cap_id,
400            tag: Some(tag_start.clone()),
401            geo_meta: GeoMeta {
402                id: start_cap_id,
403                metadata: args.source_range.into(),
404            },
405        }));
406    }
407    if let Some(tag_end) = named_cap_tags.end {
408        let Some(end_cap_id) = end_cap_id else {
409            return Err(KclError::Type(KclErrorDetails::new(
410                format!(
411                    "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
412                    tag_end.name, sketch.id
413                ),
414                vec![args.source_range],
415            )));
416        };
417
418        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
419            face_id: end_cap_id,
420            tag: Some(tag_end.clone()),
421            geo_meta: GeoMeta {
422                id: end_cap_id,
423                metadata: args.source_range.into(),
424            },
425        }));
426    }
427
428    Ok(Solid {
429        // Ok so you would think that the id would be the id of the solid,
430        // that we passed in to the function, but it's actually the id of the
431        // sketch.
432        id: sketch.id,
433        artifact_id: solid_id,
434        value: new_value,
435        meta: sketch.meta.clone(),
436        units: sketch.units,
437        height: length.to_length_units(sketch.units),
438        sectional,
439        sketch,
440        start_cap_id,
441        end_cap_id,
442        edge_cuts: vec![],
443    })
444}
445
446#[derive(Default)]
447struct Faces {
448    /// Maps curve ID to face ID for each side.
449    sides: HashMap<Uuid, Option<Uuid>>,
450    /// Top face ID.
451    end_cap_id: Option<Uuid>,
452    /// Bottom face ID.
453    start_cap_id: Option<Uuid>,
454}
455
456async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
457    let mut faces = Faces {
458        sides: HashMap::with_capacity(face_infos.len()),
459        ..Default::default()
460    };
461    if args.ctx.no_engine_commands().await {
462        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
463        faces.start_cap_id = Some(exec_state.next_uuid());
464        faces.end_cap_id = Some(exec_state.next_uuid());
465    }
466    for face_info in face_infos {
467        match face_info.cap {
468            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
469            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
470            ExtrusionFaceCapType::Both => {
471                faces.end_cap_id = face_info.face_id;
472                faces.start_cap_id = face_info.face_id;
473            }
474            ExtrusionFaceCapType::None => {
475                if let Some(curve_id) = face_info.curve_id {
476                    faces.sides.insert(curve_id, face_info.face_id);
477                }
478            }
479        }
480    }
481    faces
482}