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;
20#[cfg(feature = "artifact-graph")]
21use crate::execution::ArtifactId;
22use crate::{
23    errors::{KclError, KclErrorDetails},
24    execution::{types::RuntimeType, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSurface, Solid},
25    parsing::ast::types::TagNode,
26    std::Args,
27};
28
29/// Extrudes by a given amount.
30pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
31    let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
32    let length: TyF64 = args.get_kw_arg_typed("length", &RuntimeType::length(), exec_state)?;
33    let symmetric = args.get_kw_arg_opt("symmetric")?;
34    let bidirectional_length: Option<TyF64> =
35        args.get_kw_arg_opt_typed("bidirectionalLength", &RuntimeType::length(), exec_state)?;
36    let tag_start = args.get_kw_arg_opt("tagStart")?;
37    let tag_end = args.get_kw_arg_opt("tagEnd")?;
38
39    let result = inner_extrude(
40        sketches,
41        length,
42        symmetric,
43        bidirectional_length,
44        tag_start,
45        tag_end,
46        exec_state,
47        args,
48    )
49    .await?;
50
51    Ok(result.into())
52}
53
54/// Extend a 2-dimensional sketch through a third dimension in order to
55/// create new 3-dimensional volume, or if extruded into an existing volume,
56/// cut into an existing solid.
57///
58/// You can provide more than one sketch to extrude, and they will all be
59/// extruded in the same direction.
60///
61/// ```no_run
62/// example = startSketchOn(XZ)
63///   |> startProfile(at = [0, 0])
64///   |> line(end = [10, 0])
65///   |> arc(
66///     angleStart = 120,
67///     angleEnd = 0,
68///     radius = 5,
69///   )
70///   |> line(end = [5, 0])
71///   |> line(end = [0, 10])
72///   |> bezierCurve(
73///        control1 = [-10, 0],
74///        control2 = [2, 10],
75///        end = [-5, 10],
76///      )
77///   |> line(end = [-5, -2])
78///   |> close()
79///   |> extrude(length = 10)
80/// ```
81///
82/// ```no_run
83/// exampleSketch = startSketchOn(XZ)
84///   |> startProfile(at = [-10, 0])
85///   |> arc(
86///     angleStart = 120,
87///     angleEnd = -60,
88///     radius = 5,
89///   )
90///   |> line(end = [10, 0])
91///   |> line(end = [5, 0])
92///   |> bezierCurve(
93///        control1 = [-3, 0],
94///        control2 = [2, 10],
95///        end = [-5, 10],
96///      )
97///   |> line(end = [-4, 10])
98///   |> line(end = [-5, -2])
99///   |> close()
100///
101/// example = extrude(exampleSketch, length = 10)
102/// ```
103///
104/// ```no_run
105/// exampleSketch = startSketchOn(XZ)
106///   |> startProfile(at = [-10, 0])
107///   |> arc(
108///     angleStart = 120,
109///     angleEnd = -60,
110///     radius = 5,
111///   )
112///   |> line(end = [10, 0])
113///   |> line(end = [5, 0])
114///   |> bezierCurve(
115///        control1 = [-3, 0],
116///        control2 = [2, 10],
117///        end = [-5, 10],
118///      )
119///   |> line(end = [-4, 10])
120///   |> line(end = [-5, -2])
121///   |> close()
122///
123/// example = extrude(exampleSketch, length = 20, symmetric = true)
124/// ```
125///
126/// ```no_run
127/// exampleSketch = startSketchOn(XZ)
128///   |> startProfile(at = [-10, 0])
129///   |> arc(
130///     angleStart = 120,
131///     angleEnd = -60,
132///     radius = 5,
133///   )
134///   |> line(end = [10, 0])
135///   |> line(end = [5, 0])
136///   |> bezierCurve(
137///        control1 = [-3, 0],
138///        control2 = [2, 10],
139///        end = [-5, 10],
140///      )
141///   |> line(end = [-4, 10])
142///   |> line(end = [-5, -2])
143///   |> close()
144///
145/// example = extrude(exampleSketch, length = 10, bidirectionalLength = 50)
146/// ```
147#[stdlib {
148    name = "extrude",
149    feature_tree_operation = true,
150    keywords = 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
156            extrusion will happen on only one side of the sketch." },
157        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."},
158        tag_start = { docs = "A named tag for the face at the start of the extrusion, i.e. the original sketch" },
159        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" },
160    },
161    tags = ["sketch"]
162}]
163#[allow(clippy::too_many_arguments)]
164async fn inner_extrude(
165    sketches: Vec<Sketch>,
166    length: TyF64,
167    symmetric: Option<bool>,
168    bidirectional_length: Option<TyF64>,
169    tag_start: Option<TagNode>,
170    tag_end: Option<TagNode>,
171    exec_state: &mut ExecState,
172    args: Args,
173) -> Result<Vec<Solid>, KclError> {
174    // Extrude the element(s).
175    let mut solids = Vec::new();
176
177    if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
178        return Err(KclError::Semantic(KclErrorDetails {
179            source_ranges: vec![args.source_range],
180            message: "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
181                .to_owned(),
182        }));
183    }
184
185    let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
186
187    let opposite = match (symmetric, bidirection) {
188        (Some(true), _) => Opposite::Symmetric,
189        (None, None) => Opposite::None,
190        (Some(false), None) => Opposite::None,
191        (None, Some(length)) => Opposite::Other(length),
192        (Some(false), Some(length)) => Opposite::Other(length),
193    };
194
195    for sketch in &sketches {
196        let id = exec_state.next_uuid();
197        args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds(
198            exec_state,
199            ModelingCmdReq {
200                cmd_id: id.into(),
201                cmd: ModelingCmd::from(mcmd::Extrude {
202                    target: sketch.id.into(),
203                    distance: LengthUnit(length.to_mm()),
204                    faces: Default::default(),
205                    opposite: opposite.clone(),
206                }),
207            },
208        ))
209        .await?;
210
211        solids.push(
212            do_post_extrude(
213                sketch,
214                #[cfg(feature = "artifact-graph")]
215                id.into(),
216                length.clone(),
217                false,
218                &NamedCapTags {
219                    start: tag_start.as_ref(),
220                    end: tag_end.as_ref(),
221                },
222                exec_state,
223                &args,
224            )
225            .await?,
226        );
227    }
228
229    Ok(solids)
230}
231
232#[derive(Debug, Default)]
233pub(crate) struct NamedCapTags<'a> {
234    pub start: Option<&'a TagNode>,
235    pub end: Option<&'a TagNode>,
236}
237
238pub(crate) async fn do_post_extrude<'a>(
239    sketch: &Sketch,
240    #[cfg(feature = "artifact-graph")] 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) -> Result<Solid, KclError> {
247    // Bring the object to the front of the scene.
248    // See: https://github.com/KittyCAD/modeling-app/issues/806
249    args.batch_modeling_cmd(
250        exec_state.next_uuid(),
251        ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
252    )
253    .await?;
254
255    let any_edge_id = if let Some(edge_id) = sketch.mirror {
256        edge_id
257    } else {
258        // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
259        // So, let's just use the first one.
260        let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
261            return Err(KclError::Type(KclErrorDetails {
262                message: "Expected a non-empty sketch".to_string(),
263                source_ranges: vec![args.source_range],
264            }));
265        };
266        any_edge_id
267    };
268
269    let mut sketch = sketch.clone();
270
271    // If we were sketching on a face, we need the original face id.
272    if let SketchSurface::Face(ref face) = sketch.on {
273        sketch.id = face.solid.sketch.id;
274    }
275
276    let solid3d_info = args
277        .send_modeling_cmd(
278            exec_state.next_uuid(),
279            ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
280                edge_id: any_edge_id,
281                object_id: sketch.id,
282            }),
283        )
284        .await?;
285
286    let face_infos = if let OkWebSocketResponseData::Modeling {
287        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
288    } = solid3d_info
289    {
290        data.faces
291    } else {
292        vec![]
293    };
294
295    // Face filtering attempt in order to resolve https://github.com/KittyCAD/modeling-app/issues/5328
296    // In case of a sectional sweep, empirically it looks that the first n faces that are yielded from the sweep
297    // are the ones that work with GetOppositeEdge and GetNextAdjacentEdge, aka the n sides in the sweep.
298    // So here we're figuring out that n number as yielded_sides_count here,
299    // making sure that circle() calls count but close() don't (no length)
300    #[cfg(feature = "artifact-graph")]
301    let count_of_first_set_of_faces_if_sectional = if sectional {
302        sketch
303            .paths
304            .iter()
305            .filter(|p| {
306                let is_circle = matches!(p, Path::Circle { .. });
307                let has_length = p.get_base().from != p.get_base().to;
308                is_circle || has_length
309            })
310            .count()
311    } else {
312        usize::MAX
313    };
314
315    // Only do this if we need the artifact graph.
316    #[cfg(feature = "artifact-graph")]
317    for (curve_id, face_id) in face_infos
318        .iter()
319        .filter(|face_info| face_info.cap == ExtrusionFaceCapType::None)
320        .filter_map(|face_info| {
321            if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) {
322                Some((curve_id, face_id))
323            } else {
324                None
325            }
326        })
327        .take(count_of_first_set_of_faces_if_sectional)
328    {
329        // Batch these commands, because the Rust code doesn't actually care about the outcome.
330        // So, there's no need to await them.
331        // Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm)
332        // uses this to build the artifact graph, which the UI needs.
333        //
334        // Spawn this in the background, because we don't care about the result.
335        // Only the artifact graph needs at the end.
336        let args_cloned = args.clone();
337        let opposite_edge_uuid = exec_state.next_uuid();
338        let next_adjacent_edge_uuid = exec_state.next_uuid();
339        let get_all_edge_faces_opposite_uuid = exec_state.next_uuid();
340        let get_all_edge_faces_next_uuid = exec_state.next_uuid();
341
342        // Get faces for original edge
343        // Since this one is batched we can just run it.
344        args.batch_modeling_cmd(
345            exec_state.next_uuid(),
346            ModelingCmd::from(mcmd::Solid3dGetAllEdgeFaces {
347                edge_id: curve_id,
348                object_id: sketch.id,
349            }),
350        )
351        .await?;
352
353        get_bg_edge_info_opposite(
354            args_cloned.clone(),
355            curve_id,
356            sketch.id,
357            face_id,
358            opposite_edge_uuid,
359            get_all_edge_faces_opposite_uuid,
360            true,
361        )
362        .await?;
363
364        get_bg_edge_info_next(
365            args_cloned,
366            curve_id,
367            sketch.id,
368            face_id,
369            next_adjacent_edge_uuid,
370            get_all_edge_faces_next_uuid,
371            true,
372        )
373        .await?;
374    }
375
376    let Faces {
377        sides: face_id_map,
378        start_cap_id,
379        end_cap_id,
380    } = analyze_faces(exec_state, args, face_infos).await;
381
382    // Iterate over the sketch.value array and add face_id to GeoMeta
383    let no_engine_commands = args.ctx.no_engine_commands().await;
384    let mut new_value: Vec<ExtrudeSurface> = sketch
385        .paths
386        .iter()
387        .flat_map(|path| {
388            if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
389                match path {
390                    Path::Arc { .. }
391                    | Path::TangentialArc { .. }
392                    | Path::TangentialArcTo { .. }
393                    | Path::Circle { .. }
394                    | Path::CircleThreePoint { .. } => {
395                        let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
396                            face_id: *actual_face_id,
397                            tag: path.get_base().tag.clone(),
398                            geo_meta: GeoMeta {
399                                id: path.get_base().geo_meta.id,
400                                metadata: path.get_base().geo_meta.metadata,
401                            },
402                        });
403                        Some(extrude_surface)
404                    }
405                    Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
406                        let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
407                            face_id: *actual_face_id,
408                            tag: path.get_base().tag.clone(),
409                            geo_meta: GeoMeta {
410                                id: path.get_base().geo_meta.id,
411                                metadata: path.get_base().geo_meta.metadata,
412                            },
413                        });
414                        Some(extrude_surface)
415                    }
416                    Path::ArcThreePoint { .. } => {
417                        let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
418                            face_id: *actual_face_id,
419                            tag: path.get_base().tag.clone(),
420                            geo_meta: GeoMeta {
421                                id: path.get_base().geo_meta.id,
422                                metadata: path.get_base().geo_meta.metadata,
423                            },
424                        });
425                        Some(extrude_surface)
426                    }
427                }
428            } else if no_engine_commands {
429                // Only pre-populate the extrude surface if we are in mock mode.
430
431                let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
432                    // pushing this values with a fake face_id to make extrudes mock-execute safe
433                    face_id: exec_state.next_uuid(),
434                    tag: path.get_base().tag.clone(),
435                    geo_meta: GeoMeta {
436                        id: path.get_base().geo_meta.id,
437                        metadata: path.get_base().geo_meta.metadata,
438                    },
439                });
440                Some(extrude_surface)
441            } else {
442                None
443            }
444        })
445        .collect();
446
447    // Add the tags for the start or end caps.
448    if let Some(tag_start) = named_cap_tags.start {
449        let Some(start_cap_id) = start_cap_id else {
450            return Err(KclError::Type(KclErrorDetails {
451                message: format!(
452                    "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
453                    tag_start.name, sketch.id
454                ),
455                source_ranges: vec![args.source_range],
456            }));
457        };
458
459        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
460            face_id: start_cap_id,
461            tag: Some(tag_start.clone()),
462            geo_meta: GeoMeta {
463                id: start_cap_id,
464                metadata: args.source_range.into(),
465            },
466        }));
467    }
468    if let Some(tag_end) = named_cap_tags.end {
469        let Some(end_cap_id) = end_cap_id else {
470            return Err(KclError::Type(KclErrorDetails {
471                message: format!(
472                    "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
473                    tag_end.name, sketch.id
474                ),
475                source_ranges: vec![args.source_range],
476            }));
477        };
478
479        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
480            face_id: end_cap_id,
481            tag: Some(tag_end.clone()),
482            geo_meta: GeoMeta {
483                id: end_cap_id,
484                metadata: args.source_range.into(),
485            },
486        }));
487    }
488
489    Ok(Solid {
490        // Ok so you would think that the id would be the id of the solid,
491        // that we passed in to the function, but it's actually the id of the
492        // sketch.
493        id: sketch.id,
494        #[cfg(feature = "artifact-graph")]
495        artifact_id: solid_id,
496        value: new_value,
497        meta: sketch.meta.clone(),
498        units: sketch.units,
499        height: length.to_length_units(sketch.units),
500        sectional,
501        sketch,
502        start_cap_id,
503        end_cap_id,
504        edge_cuts: vec![],
505    })
506}
507
508#[derive(Default)]
509struct Faces {
510    /// Maps curve ID to face ID for each side.
511    sides: HashMap<Uuid, Option<Uuid>>,
512    /// Top face ID.
513    end_cap_id: Option<Uuid>,
514    /// Bottom face ID.
515    start_cap_id: Option<Uuid>,
516}
517
518async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
519    let mut faces = Faces {
520        sides: HashMap::with_capacity(face_infos.len()),
521        ..Default::default()
522    };
523    if args.ctx.no_engine_commands().await {
524        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
525        faces.start_cap_id = Some(exec_state.next_uuid());
526        faces.end_cap_id = Some(exec_state.next_uuid());
527    }
528    for face_info in face_infos {
529        match face_info.cap {
530            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
531            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
532            ExtrusionFaceCapType::Both => {
533                faces.end_cap_id = face_info.face_id;
534                faces.start_cap_id = face_info.face_id;
535            }
536            ExtrusionFaceCapType::None => {
537                if let Some(curve_id) = face_info.curve_id {
538                    faces.sides.insert(curve_id, face_info.face_id);
539                }
540            }
541        }
542    }
543    faces
544}
545
546#[cfg(feature = "artifact-graph")]
547async fn send_fn(args: &Args, id: uuid::Uuid, cmd: ModelingCmd, single_threaded: bool) -> Result<(), KclError> {
548    if single_threaded {
549        // In single threaded mode, we can safely batch the command.
550        args.batch_modeling_cmd(id, cmd).await
551    } else {
552        // We cannot batch this call, because otherwise it might batch after say
553        // a shell that makes this edge no longer relevant.
554        args.send_modeling_cmd(id, cmd).await.map(|_| ())
555    }
556}
557
558#[cfg(feature = "artifact-graph")]
559#[allow(clippy::too_many_arguments)]
560async fn get_bg_edge_info_next(
561    args: Args,
562    curve_id: uuid::Uuid,
563    sketch_id: uuid::Uuid,
564    face_id: uuid::Uuid,
565    edge_uuid: uuid::Uuid,
566    get_all_edge_faces_uuid: uuid::Uuid,
567    single_threaded: bool,
568) -> Result<(), KclError> {
569    let next_adjacent_edge_id = args
570        .send_modeling_cmd(
571            edge_uuid,
572            ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
573                edge_id: curve_id,
574                object_id: sketch_id,
575                face_id,
576            }),
577        )
578        .await?;
579
580    // Get faces for next adjacent edge
581    if let OkWebSocketResponseData::Modeling {
582        modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(next_adjacent_edge),
583    } = next_adjacent_edge_id
584    {
585        if let Some(edge_id) = next_adjacent_edge.edge {
586            send_fn(
587                &args,
588                get_all_edge_faces_uuid,
589                ModelingCmd::from(mcmd::Solid3dGetAllEdgeFaces {
590                    edge_id,
591                    object_id: sketch_id,
592                }),
593                single_threaded,
594            )
595            .await?;
596        }
597    }
598
599    Ok(())
600}
601
602#[cfg(feature = "artifact-graph")]
603#[allow(clippy::too_many_arguments)]
604async fn get_bg_edge_info_opposite(
605    args: Args,
606    curve_id: uuid::Uuid,
607    sketch_id: uuid::Uuid,
608    face_id: uuid::Uuid,
609    edge_uuid: uuid::Uuid,
610    get_all_edge_faces_uuid: uuid::Uuid,
611    single_threaded: bool,
612) -> Result<(), KclError> {
613    let opposite_edge_id = args
614        .send_modeling_cmd(
615            edge_uuid,
616            ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
617                edge_id: curve_id,
618                object_id: sketch_id,
619                face_id,
620            }),
621        )
622        .await?;
623
624    // Get faces for opposite edge
625    if let OkWebSocketResponseData::Modeling {
626        modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
627    } = opposite_edge_id
628    {
629        send_fn(
630            &args,
631            get_all_edge_faces_uuid,
632            ModelingCmd::from(mcmd::Solid3dGetAllEdgeFaces {
633                edge_id: opposite_edge.edge,
634                object_id: sketch_id,
635            }),
636            single_threaded,
637        )
638        .await?;
639    }
640
641    Ok(())
642}