kcl_lib/std/
extrude.rs

1//! Functions related to extruding.
2
3use std::collections::HashMap;
4
5use anyhow::Result;
6use kcmc::shared::Point3d as KPoint3d; // Point3d is already defined in this pkg, to impl ts_rs traits.
7use kcmc::{
8    ModelingCmd, each_cmd as mcmd,
9    length_unit::LengthUnit,
10    ok_response::OkModelingCmdResponse,
11    output::ExtrusionFaceInfo,
12    shared::{ExtrudeReference, ExtrusionFaceCapType, Opposite},
13    websocket::{ModelingCmdReq, OkWebSocketResponseData},
14};
15use kittycad_modeling_cmds::{
16    self as kcmc,
17    shared::{Angle, ExtrudeMethod, Point2d},
18};
19use uuid::Uuid;
20
21use super::{DEFAULT_TOLERANCE_MM, args::TyF64, utils::point_to_mm};
22use crate::{
23    errors::{KclError, KclErrorDetails},
24    execution::{
25        ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta, Path, Sketch, SketchSurface, Solid,
26        types::{PrimitiveType, RuntimeType},
27    },
28    parsing::ast::types::TagNode,
29    std::{Args, axis_or_reference::Point3dAxis3dOrGeometryReference},
30};
31
32/// Extrudes by a given amount.
33pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
34    let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
35    let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
36    let to = args.get_kw_arg_opt(
37        "to",
38        &RuntimeType::Union(vec![
39            RuntimeType::point3d(),
40            RuntimeType::Primitive(PrimitiveType::Axis3d),
41            RuntimeType::Primitive(PrimitiveType::Edge),
42            RuntimeType::plane(),
43            RuntimeType::Primitive(PrimitiveType::Face),
44            RuntimeType::sketch(),
45            RuntimeType::Primitive(PrimitiveType::Solid),
46            RuntimeType::tagged_edge(),
47            RuntimeType::tagged_face(),
48        ]),
49        exec_state,
50    )?;
51    let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
52    let bidirectional_length: Option<TyF64> =
53        args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
54    let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
55    let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
56    let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
57    let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
58    let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
59    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
60    let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
61
62    let result = inner_extrude(
63        sketches,
64        length,
65        to,
66        symmetric,
67        bidirectional_length,
68        tag_start,
69        tag_end,
70        twist_angle,
71        twist_angle_step,
72        twist_center,
73        tolerance,
74        method,
75        exec_state,
76        args,
77    )
78    .await?;
79
80    Ok(result.into())
81}
82
83#[allow(clippy::too_many_arguments)]
84async fn inner_extrude(
85    sketches: Vec<Sketch>,
86    length: Option<TyF64>,
87    to: Option<Point3dAxis3dOrGeometryReference>,
88    symmetric: Option<bool>,
89    bidirectional_length: Option<TyF64>,
90    tag_start: Option<TagNode>,
91    tag_end: Option<TagNode>,
92    twist_angle: Option<TyF64>,
93    twist_angle_step: Option<TyF64>,
94    twist_center: Option<[TyF64; 2]>,
95    tolerance: Option<TyF64>,
96    method: Option<String>,
97    exec_state: &mut ExecState,
98    args: Args,
99) -> Result<Vec<Solid>, KclError> {
100    // Extrude the element(s).
101    let mut solids = Vec::new();
102    let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
103
104    let extrude_method = match method.as_deref() {
105        Some("new" | "NEW") => ExtrudeMethod::New,
106        Some("merge" | "MERGE") => ExtrudeMethod::Merge,
107        None => ExtrudeMethod::default(),
108        Some(other) => {
109            return Err(KclError::new_semantic(KclErrorDetails::new(
110                format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
111                vec![args.source_range],
112            )));
113        }
114    };
115
116    if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
117        return Err(KclError::new_semantic(KclErrorDetails::new(
118            "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
119                .to_owned(),
120            vec![args.source_range],
121        )));
122    }
123
124    if (length.is_some() || twist_angle.is_some()) && to.is_some() {
125        return Err(KclError::new_semantic(KclErrorDetails::new(
126            "You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
127                .to_owned(),
128            vec![args.source_range],
129        )));
130    }
131
132    let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
133
134    let opposite = match (symmetric, bidirection) {
135        (Some(true), _) => Opposite::Symmetric,
136        (None, None) => Opposite::None,
137        (Some(false), None) => Opposite::None,
138        (None, Some(length)) => Opposite::Other(length),
139        (Some(false), Some(length)) => Opposite::Other(length),
140    };
141
142    for sketch in &sketches {
143        let id = exec_state.next_uuid();
144        let cmd = match (&twist_angle, &twist_angle_step, &twist_center, length.clone(), &to) {
145            (Some(angle), angle_step, center, Some(length), None) => {
146                let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
147                let total_rotation_angle = Angle::from_degrees(angle.to_degrees(exec_state, args.source_range));
148                let angle_step_size = Angle::from_degrees(
149                    angle_step
150                        .clone()
151                        .map(|a| a.to_degrees(exec_state, args.source_range))
152                        .unwrap_or(15.0),
153                );
154                ModelingCmd::from(mcmd::TwistExtrude {
155                    target: sketch.id.into(),
156                    distance: LengthUnit(length.to_mm()),
157                    faces: Default::default(),
158                    center_2d: center,
159                    total_rotation_angle,
160                    angle_step_size,
161                    tolerance,
162                })
163            }
164            (None, None, None, Some(length), None) => ModelingCmd::from(mcmd::Extrude {
165                target: sketch.id.into(),
166                distance: LengthUnit(length.to_mm()),
167                faces: Default::default(),
168                opposite: opposite.clone(),
169                extrude_method,
170            }),
171            (None, None, None, None, Some(to)) => match to {
172                Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(mcmd::ExtrudeToReference {
173                    target: sketch.id.into(),
174                    reference: ExtrudeReference::Point {
175                        point: KPoint3d {
176                            x: LengthUnit(point[0].to_mm()),
177                            y: LengthUnit(point[1].to_mm()),
178                            z: LengthUnit(point[2].to_mm()),
179                        },
180                    },
181                    faces: Default::default(),
182                    extrude_method,
183                }),
184                Point3dAxis3dOrGeometryReference::Axis { direction, origin } => {
185                    ModelingCmd::from(mcmd::ExtrudeToReference {
186                        target: sketch.id.into(),
187                        reference: ExtrudeReference::Axis {
188                            axis: KPoint3d {
189                                x: direction[0].to_mm(),
190                                y: direction[1].to_mm(),
191                                z: direction[2].to_mm(),
192                            },
193                            point: KPoint3d {
194                                x: LengthUnit(origin[0].to_mm()),
195                                y: LengthUnit(origin[1].to_mm()),
196                                z: LengthUnit(origin[2].to_mm()),
197                            },
198                        },
199                        faces: Default::default(),
200                        extrude_method,
201                    })
202                }
203                Point3dAxis3dOrGeometryReference::Plane(plane) => {
204                    let plane_id = if plane.value == crate::exec::PlaneType::Uninit {
205                        if plane.info.origin.units.is_none() {
206                            return Err(KclError::new_semantic(KclErrorDetails::new(
207                                "Origin of plane has unknown units".to_string(),
208                                vec![args.source_range],
209                            )));
210                        }
211                        let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
212                            plane.clone().info.into_plane_data(),
213                            exec_state,
214                            &args,
215                        )
216                        .await?;
217                        sketch_plane.id
218                    } else {
219                        plane.id
220                    };
221                    ModelingCmd::from(mcmd::ExtrudeToReference {
222                        target: sketch.id.into(),
223                        reference: ExtrudeReference::EntityReference { entity_id: plane_id },
224                        faces: Default::default(),
225                        extrude_method,
226                    })
227                }
228                Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
229                    let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
230                    ModelingCmd::from(mcmd::ExtrudeToReference {
231                        target: sketch.id.into(),
232                        reference: ExtrudeReference::EntityReference { entity_id: edge_id },
233                        faces: Default::default(),
234                        extrude_method,
235                    })
236                }
237                Point3dAxis3dOrGeometryReference::Face(face_tag) => {
238                    let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
239                    ModelingCmd::from(mcmd::ExtrudeToReference {
240                        target: sketch.id.into(),
241                        reference: ExtrudeReference::EntityReference { entity_id: face_id },
242                        faces: Default::default(),
243                        extrude_method,
244                    })
245                }
246                Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(mcmd::ExtrudeToReference {
247                    target: sketch.id.into(),
248                    reference: ExtrudeReference::EntityReference {
249                        entity_id: sketch_ref.id,
250                    },
251                    faces: Default::default(),
252                    extrude_method,
253                }),
254                Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(mcmd::ExtrudeToReference {
255                    target: sketch.id.into(),
256                    reference: ExtrudeReference::EntityReference { entity_id: solid.id },
257                    faces: Default::default(),
258                    extrude_method,
259                }),
260                Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
261                    let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
262                    let tagged_edge_or_face_id = tagged_edge_or_face.id;
263                    ModelingCmd::from(mcmd::ExtrudeToReference {
264                        target: sketch.id.into(),
265                        reference: ExtrudeReference::EntityReference {
266                            entity_id: tagged_edge_or_face_id,
267                        },
268                        faces: Default::default(),
269                        extrude_method,
270                    })
271                }
272            },
273            (Some(_), _, _, None, None) => {
274                return Err(KclError::new_semantic(KclErrorDetails::new(
275                    "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
276                    vec![args.source_range],
277                )));
278            }
279            (_, _, _, None, None) => {
280                return Err(KclError::new_semantic(KclErrorDetails::new(
281                    "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
282                    vec![args.source_range],
283                )));
284            }
285            (_, _, _, Some(_), Some(_)) => {
286                return Err(KclError::new_semantic(KclErrorDetails::new(
287                    "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
288                    vec![args.source_range],
289                )));
290            }
291            (_, _, _, _, _) => {
292                return Err(KclError::new_semantic(KclErrorDetails::new(
293                    "Invalid combination of parameters for extrusion.".to_owned(),
294                    vec![args.source_range],
295                )));
296            }
297        };
298        let cmds = sketch.build_sketch_mode_cmds(exec_state, ModelingCmdReq { cmd_id: id.into(), cmd });
299        exec_state
300            .batch_modeling_cmds(ModelingCmdMeta::from_args_id(&args, id), &cmds)
301            .await?;
302
303        solids.push(
304            do_post_extrude(
305                sketch,
306                id.into(),
307                false,
308                &NamedCapTags {
309                    start: tag_start.as_ref(),
310                    end: tag_end.as_ref(),
311                },
312                extrude_method,
313                exec_state,
314                &args,
315                None,
316            )
317            .await?,
318        );
319    }
320
321    Ok(solids)
322}
323
324#[derive(Debug, Default)]
325pub(crate) struct NamedCapTags<'a> {
326    pub start: Option<&'a TagNode>,
327    pub end: Option<&'a TagNode>,
328}
329
330#[allow(clippy::too_many_arguments)]
331pub(crate) async fn do_post_extrude<'a>(
332    sketch: &Sketch,
333    solid_id: ArtifactId,
334    sectional: bool,
335    named_cap_tags: &'a NamedCapTags<'a>,
336    extrude_method: ExtrudeMethod,
337    exec_state: &mut ExecState,
338    args: &Args,
339    edge_id: Option<Uuid>,
340) -> Result<Solid, KclError> {
341    // Bring the object to the front of the scene.
342    // See: https://github.com/KittyCAD/modeling-app/issues/806
343    exec_state
344        .batch_modeling_cmd(
345            args.into(),
346            ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
347        )
348        .await?;
349
350    let any_edge_id = if let Some(edge_id) = sketch.mirror {
351        edge_id
352    } else if let Some(id) = edge_id {
353        id
354    } else {
355        // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
356        // So, let's just use the first one.
357        let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
358            return Err(KclError::new_type(KclErrorDetails::new(
359                "Expected a non-empty sketch".to_owned(),
360                vec![args.source_range],
361            )));
362        };
363        any_edge_id
364    };
365
366    let mut sketch = sketch.clone();
367    sketch.is_closed = true;
368
369    // If we were sketching on a face, we need the original face id.
370    if let SketchSurface::Face(ref face) = sketch.on {
371        // If we are creating a new body we need to preserve its new id.
372        if extrude_method != ExtrudeMethod::New {
373            sketch.id = face.solid.sketch.id;
374        }
375    }
376
377    let solid3d_info = exec_state
378        .send_modeling_cmd(
379            args.into(),
380            ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
381                edge_id: any_edge_id,
382                object_id: sketch.id,
383            }),
384        )
385        .await?;
386
387    let face_infos = if let OkWebSocketResponseData::Modeling {
388        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
389    } = solid3d_info
390    {
391        data.faces
392    } else {
393        vec![]
394    };
395
396    // Only do this if we need the artifact graph.
397    #[cfg(feature = "artifact-graph")]
398    {
399        // Getting the ids of a sectional sweep does not work well and we cannot guarantee that
400        // any of these call will not just fail.
401        if !sectional {
402            exec_state
403                .batch_modeling_cmd(
404                    args.into(),
405                    ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
406                        object_id: sketch.id,
407                        edge_id: any_edge_id,
408                    }),
409                )
410                .await?;
411        }
412    }
413
414    let Faces {
415        sides: face_id_map,
416        start_cap_id,
417        end_cap_id,
418    } = analyze_faces(exec_state, args, face_infos).await;
419    // Iterate over the sketch.value array and add face_id to GeoMeta
420    let no_engine_commands = args.ctx.no_engine_commands().await;
421    let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
422    let outer_surfaces = sketch.paths.iter().flat_map(|path| {
423        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
424            surface_of(path, *actual_face_id)
425        } else if no_engine_commands {
426            // Only pre-populate the extrude surface if we are in mock mode.
427            fake_extrude_surface(exec_state, path)
428        } else {
429            None
430        }
431    });
432    new_value.extend(outer_surfaces);
433    let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
434        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
435            surface_of(path, *actual_face_id)
436        } else if no_engine_commands {
437            // Only pre-populate the extrude surface if we are in mock mode.
438            fake_extrude_surface(exec_state, path)
439        } else {
440            None
441        }
442    });
443    new_value.extend(inner_surfaces);
444
445    // Add the tags for the start or end caps.
446    if let Some(tag_start) = named_cap_tags.start {
447        let Some(start_cap_id) = start_cap_id else {
448            return Err(KclError::new_type(KclErrorDetails::new(
449                format!(
450                    "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
451                    tag_start.name, sketch.id
452                ),
453                vec![args.source_range],
454            )));
455        };
456
457        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
458            face_id: start_cap_id,
459            tag: Some(tag_start.clone()),
460            geo_meta: GeoMeta {
461                id: start_cap_id,
462                metadata: args.source_range.into(),
463            },
464        }));
465    }
466    if let Some(tag_end) = named_cap_tags.end {
467        let Some(end_cap_id) = end_cap_id else {
468            return Err(KclError::new_type(KclErrorDetails::new(
469                format!(
470                    "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
471                    tag_end.name, sketch.id
472                ),
473                vec![args.source_range],
474            )));
475        };
476
477        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
478            face_id: end_cap_id,
479            tag: Some(tag_end.clone()),
480            geo_meta: GeoMeta {
481                id: end_cap_id,
482                metadata: args.source_range.into(),
483            },
484        }));
485    }
486
487    Ok(Solid {
488        // Ok so you would think that the id would be the id of the solid,
489        // that we passed in to the function, but it's actually the id of the
490        // sketch.
491        //
492        // Why? Because when you extrude a sketch, the engine lets the solid absorb the
493        // sketch's ID. So the solid should take over the sketch's ID.
494        id: sketch.id,
495        artifact_id: solid_id,
496        value: new_value,
497        meta: sketch.meta.clone(),
498        units: sketch.units,
499        sectional,
500        sketch,
501        start_cap_id,
502        end_cap_id,
503        edge_cuts: vec![],
504    })
505}
506
507#[derive(Default)]
508struct Faces {
509    /// Maps curve ID to face ID for each side.
510    sides: HashMap<Uuid, Option<Uuid>>,
511    /// Top face ID.
512    end_cap_id: Option<Uuid>,
513    /// Bottom face ID.
514    start_cap_id: Option<Uuid>,
515}
516
517async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
518    let mut faces = Faces {
519        sides: HashMap::with_capacity(face_infos.len()),
520        ..Default::default()
521    };
522    if args.ctx.no_engine_commands().await {
523        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
524        faces.start_cap_id = Some(exec_state.next_uuid());
525        faces.end_cap_id = Some(exec_state.next_uuid());
526    }
527    for face_info in face_infos {
528        match face_info.cap {
529            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
530            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
531            ExtrusionFaceCapType::Both => {
532                faces.end_cap_id = face_info.face_id;
533                faces.start_cap_id = face_info.face_id;
534            }
535            ExtrusionFaceCapType::None => {
536                if let Some(curve_id) = face_info.curve_id {
537                    faces.sides.insert(curve_id, face_info.face_id);
538                }
539            }
540        }
541    }
542    faces
543}
544fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
545    match path {
546        Path::Arc { .. }
547        | Path::TangentialArc { .. }
548        | Path::TangentialArcTo { .. }
549        // TODO: (bc) fix me
550        | Path::Ellipse { .. }
551        | Path::Conic {.. }
552        | Path::Circle { .. }
553        | Path::CircleThreePoint { .. } => {
554            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
555                face_id: actual_face_id,
556                tag: path.get_base().tag.clone(),
557                geo_meta: GeoMeta {
558                    id: path.get_base().geo_meta.id,
559                    metadata: path.get_base().geo_meta.metadata,
560                },
561            });
562            Some(extrude_surface)
563        }
564        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
565            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
566                face_id: actual_face_id,
567                tag: path.get_base().tag.clone(),
568                geo_meta: GeoMeta {
569                    id: path.get_base().geo_meta.id,
570                    metadata: path.get_base().geo_meta.metadata,
571                },
572            });
573            Some(extrude_surface)
574        }
575        Path::ArcThreePoint { .. } => {
576            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
577                face_id: actual_face_id,
578                tag: path.get_base().tag.clone(),
579                geo_meta: GeoMeta {
580                    id: path.get_base().geo_meta.id,
581                    metadata: path.get_base().geo_meta.metadata,
582                },
583            });
584            Some(extrude_surface)
585        }
586    }
587}
588
589/// Create a fake extrude surface to report for mock execution, when there's no engine response.
590fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
591    let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
592        // pushing this values with a fake face_id to make extrudes mock-execute safe
593        face_id: exec_state.next_uuid(),
594        tag: path.get_base().tag.clone(),
595        geo_meta: GeoMeta {
596            id: path.get_base().geo_meta.id,
597            metadata: path.get_base().geo_meta.metadata,
598        },
599    });
600    Some(extrude_surface)
601}