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());
148                let angle_step_size = Angle::from_degrees(angle_step.clone().map(|a| a.to_degrees()).unwrap_or(15.0));
149                ModelingCmd::from(mcmd::TwistExtrude {
150                    target: sketch.id.into(),
151                    distance: LengthUnit(length.to_mm()),
152                    faces: Default::default(),
153                    center_2d: center,
154                    total_rotation_angle,
155                    angle_step_size,
156                    tolerance,
157                })
158            }
159            (None, None, None, Some(length), None) => ModelingCmd::from(mcmd::Extrude {
160                target: sketch.id.into(),
161                distance: LengthUnit(length.to_mm()),
162                faces: Default::default(),
163                opposite: opposite.clone(),
164                extrude_method,
165            }),
166            (None, None, None, None, Some(to)) => match to {
167                Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(mcmd::ExtrudeToReference {
168                    target: sketch.id.into(),
169                    reference: ExtrudeReference::Point {
170                        point: KPoint3d {
171                            x: LengthUnit(point[0].to_mm()),
172                            y: LengthUnit(point[1].to_mm()),
173                            z: LengthUnit(point[2].to_mm()),
174                        },
175                    },
176                    faces: Default::default(),
177                    extrude_method,
178                }),
179                Point3dAxis3dOrGeometryReference::Axis { direction, origin } => {
180                    ModelingCmd::from(mcmd::ExtrudeToReference {
181                        target: sketch.id.into(),
182                        reference: ExtrudeReference::Axis {
183                            axis: KPoint3d {
184                                x: direction[0].to_mm(),
185                                y: direction[1].to_mm(),
186                                z: direction[2].to_mm(),
187                            },
188                            point: KPoint3d {
189                                x: LengthUnit(origin[0].to_mm()),
190                                y: LengthUnit(origin[1].to_mm()),
191                                z: LengthUnit(origin[2].to_mm()),
192                            },
193                        },
194                        faces: Default::default(),
195                        extrude_method,
196                    })
197                }
198                Point3dAxis3dOrGeometryReference::Plane(plane) => {
199                    let plane_id = if plane.value == crate::exec::PlaneType::Uninit {
200                        if plane.info.origin.units.is_none() {
201                            return Err(KclError::new_semantic(KclErrorDetails::new(
202                                "Origin of plane has unknown units".to_string(),
203                                vec![args.source_range],
204                            )));
205                        }
206                        let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
207                            plane.clone().info.into_plane_data(),
208                            exec_state,
209                            &args,
210                        )
211                        .await?;
212                        sketch_plane.id
213                    } else {
214                        plane.id
215                    };
216                    ModelingCmd::from(mcmd::ExtrudeToReference {
217                        target: sketch.id.into(),
218                        reference: ExtrudeReference::EntityReference { entity_id: plane_id },
219                        faces: Default::default(),
220                        extrude_method,
221                    })
222                }
223                Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
224                    let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
225                    ModelingCmd::from(mcmd::ExtrudeToReference {
226                        target: sketch.id.into(),
227                        reference: ExtrudeReference::EntityReference { entity_id: edge_id },
228                        faces: Default::default(),
229                        extrude_method,
230                    })
231                }
232                Point3dAxis3dOrGeometryReference::Face(face_tag) => {
233                    let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
234                    ModelingCmd::from(mcmd::ExtrudeToReference {
235                        target: sketch.id.into(),
236                        reference: ExtrudeReference::EntityReference { entity_id: face_id },
237                        faces: Default::default(),
238                        extrude_method,
239                    })
240                }
241                Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(mcmd::ExtrudeToReference {
242                    target: sketch.id.into(),
243                    reference: ExtrudeReference::EntityReference {
244                        entity_id: sketch_ref.id,
245                    },
246                    faces: Default::default(),
247                    extrude_method,
248                }),
249                Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(mcmd::ExtrudeToReference {
250                    target: sketch.id.into(),
251                    reference: ExtrudeReference::EntityReference { entity_id: solid.id },
252                    faces: Default::default(),
253                    extrude_method,
254                }),
255                Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
256                    let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
257                    let tagged_edge_or_face_id = tagged_edge_or_face.id;
258                    ModelingCmd::from(mcmd::ExtrudeToReference {
259                        target: sketch.id.into(),
260                        reference: ExtrudeReference::EntityReference {
261                            entity_id: tagged_edge_or_face_id,
262                        },
263                        faces: Default::default(),
264                        extrude_method,
265                    })
266                }
267            },
268            (Some(_), _, _, None, None) => {
269                return Err(KclError::new_semantic(KclErrorDetails::new(
270                    "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
271                    vec![args.source_range],
272                )));
273            }
274            (_, _, _, None, None) => {
275                return Err(KclError::new_semantic(KclErrorDetails::new(
276                    "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
277                    vec![args.source_range],
278                )));
279            }
280            (_, _, _, Some(_), Some(_)) => {
281                return Err(KclError::new_semantic(KclErrorDetails::new(
282                    "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
283                    vec![args.source_range],
284                )));
285            }
286            (_, _, _, _, _) => {
287                return Err(KclError::new_semantic(KclErrorDetails::new(
288                    "Invalid combination of parameters for extrusion.".to_owned(),
289                    vec![args.source_range],
290                )));
291            }
292        };
293        let cmds = sketch.build_sketch_mode_cmds(exec_state, ModelingCmdReq { cmd_id: id.into(), cmd });
294        exec_state
295            .batch_modeling_cmds(ModelingCmdMeta::from_args_id(&args, id), &cmds)
296            .await?;
297
298        solids.push(
299            do_post_extrude(
300                sketch,
301                id.into(),
302                false,
303                &NamedCapTags {
304                    start: tag_start.as_ref(),
305                    end: tag_end.as_ref(),
306                },
307                extrude_method,
308                exec_state,
309                &args,
310                None,
311            )
312            .await?,
313        );
314    }
315
316    Ok(solids)
317}
318
319#[derive(Debug, Default)]
320pub(crate) struct NamedCapTags<'a> {
321    pub start: Option<&'a TagNode>,
322    pub end: Option<&'a TagNode>,
323}
324
325#[allow(clippy::too_many_arguments)]
326pub(crate) async fn do_post_extrude<'a>(
327    sketch: &Sketch,
328    solid_id: ArtifactId,
329    sectional: bool,
330    named_cap_tags: &'a NamedCapTags<'a>,
331    extrude_method: ExtrudeMethod,
332    exec_state: &mut ExecState,
333    args: &Args,
334    edge_id: Option<Uuid>,
335) -> Result<Solid, KclError> {
336    // Bring the object to the front of the scene.
337    // See: https://github.com/KittyCAD/modeling-app/issues/806
338    exec_state
339        .batch_modeling_cmd(
340            args.into(),
341            ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
342        )
343        .await?;
344
345    let any_edge_id = if let Some(edge_id) = sketch.mirror {
346        edge_id
347    } else if let Some(id) = edge_id {
348        id
349    } else {
350        // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
351        // So, let's just use the first one.
352        let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
353            return Err(KclError::new_type(KclErrorDetails::new(
354                "Expected a non-empty sketch".to_owned(),
355                vec![args.source_range],
356            )));
357        };
358        any_edge_id
359    };
360
361    let mut sketch = sketch.clone();
362    sketch.is_closed = true;
363
364    // If we were sketching on a face, we need the original face id.
365    if let SketchSurface::Face(ref face) = sketch.on {
366        // If we are creating a new body we need to preserve its new id.
367        if extrude_method != ExtrudeMethod::New {
368            sketch.id = face.solid.sketch.id;
369        }
370    }
371
372    let solid3d_info = exec_state
373        .send_modeling_cmd(
374            args.into(),
375            ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
376                edge_id: any_edge_id,
377                object_id: sketch.id,
378            }),
379        )
380        .await?;
381
382    let face_infos = if let OkWebSocketResponseData::Modeling {
383        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
384    } = solid3d_info
385    {
386        data.faces
387    } else {
388        vec![]
389    };
390
391    // Only do this if we need the artifact graph.
392    #[cfg(feature = "artifact-graph")]
393    {
394        // Getting the ids of a sectional sweep does not work well and we cannot guarantee that
395        // any of these call will not just fail.
396        if !sectional {
397            exec_state
398                .batch_modeling_cmd(
399                    args.into(),
400                    ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
401                        object_id: sketch.id,
402                        edge_id: any_edge_id,
403                    }),
404                )
405                .await?;
406        }
407    }
408
409    let Faces {
410        sides: face_id_map,
411        start_cap_id,
412        end_cap_id,
413    } = analyze_faces(exec_state, args, face_infos).await;
414    // Iterate over the sketch.value array and add face_id to GeoMeta
415    let no_engine_commands = args.ctx.no_engine_commands().await;
416    let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
417    let outer_surfaces = sketch.paths.iter().flat_map(|path| {
418        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
419            surface_of(path, *actual_face_id)
420        } else if no_engine_commands {
421            // Only pre-populate the extrude surface if we are in mock mode.
422            fake_extrude_surface(exec_state, path)
423        } else {
424            None
425        }
426    });
427    new_value.extend(outer_surfaces);
428    let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
429        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
430            surface_of(path, *actual_face_id)
431        } else if no_engine_commands {
432            // Only pre-populate the extrude surface if we are in mock mode.
433            fake_extrude_surface(exec_state, path)
434        } else {
435            None
436        }
437    });
438    new_value.extend(inner_surfaces);
439
440    // Add the tags for the start or end caps.
441    if let Some(tag_start) = named_cap_tags.start {
442        let Some(start_cap_id) = start_cap_id else {
443            return Err(KclError::new_type(KclErrorDetails::new(
444                format!(
445                    "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
446                    tag_start.name, sketch.id
447                ),
448                vec![args.source_range],
449            )));
450        };
451
452        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
453            face_id: start_cap_id,
454            tag: Some(tag_start.clone()),
455            geo_meta: GeoMeta {
456                id: start_cap_id,
457                metadata: args.source_range.into(),
458            },
459        }));
460    }
461    if let Some(tag_end) = named_cap_tags.end {
462        let Some(end_cap_id) = end_cap_id else {
463            return Err(KclError::new_type(KclErrorDetails::new(
464                format!(
465                    "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
466                    tag_end.name, sketch.id
467                ),
468                vec![args.source_range],
469            )));
470        };
471
472        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
473            face_id: end_cap_id,
474            tag: Some(tag_end.clone()),
475            geo_meta: GeoMeta {
476                id: end_cap_id,
477                metadata: args.source_range.into(),
478            },
479        }));
480    }
481
482    Ok(Solid {
483        // Ok so you would think that the id would be the id of the solid,
484        // that we passed in to the function, but it's actually the id of the
485        // sketch.
486        //
487        // Why? Because when you extrude a sketch, the engine lets the solid absorb the
488        // sketch's ID. So the solid should take over the sketch's ID.
489        id: sketch.id,
490        artifact_id: solid_id,
491        value: new_value,
492        meta: sketch.meta.clone(),
493        units: sketch.units,
494        sectional,
495        sketch,
496        start_cap_id,
497        end_cap_id,
498        edge_cuts: vec![],
499    })
500}
501
502#[derive(Default)]
503struct Faces {
504    /// Maps curve ID to face ID for each side.
505    sides: HashMap<Uuid, Option<Uuid>>,
506    /// Top face ID.
507    end_cap_id: Option<Uuid>,
508    /// Bottom face ID.
509    start_cap_id: Option<Uuid>,
510}
511
512async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
513    let mut faces = Faces {
514        sides: HashMap::with_capacity(face_infos.len()),
515        ..Default::default()
516    };
517    if args.ctx.no_engine_commands().await {
518        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
519        faces.start_cap_id = Some(exec_state.next_uuid());
520        faces.end_cap_id = Some(exec_state.next_uuid());
521    }
522    for face_info in face_infos {
523        match face_info.cap {
524            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
525            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
526            ExtrusionFaceCapType::Both => {
527                faces.end_cap_id = face_info.face_id;
528                faces.start_cap_id = face_info.face_id;
529            }
530            ExtrusionFaceCapType::None => {
531                if let Some(curve_id) = face_info.curve_id {
532                    faces.sides.insert(curve_id, face_info.face_id);
533                }
534            }
535        }
536    }
537    faces
538}
539fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
540    match path {
541        Path::Arc { .. }
542        | Path::TangentialArc { .. }
543        | Path::TangentialArcTo { .. }
544        // TODO: (bc) fix me
545        | Path::Ellipse { .. }
546        | Path::Conic {.. }
547        | Path::Circle { .. }
548        | Path::CircleThreePoint { .. } => {
549            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
550                face_id: actual_face_id,
551                tag: path.get_base().tag.clone(),
552                geo_meta: GeoMeta {
553                    id: path.get_base().geo_meta.id,
554                    metadata: path.get_base().geo_meta.metadata,
555                },
556            });
557            Some(extrude_surface)
558        }
559        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
560            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
561                face_id: actual_face_id,
562                tag: path.get_base().tag.clone(),
563                geo_meta: GeoMeta {
564                    id: path.get_base().geo_meta.id,
565                    metadata: path.get_base().geo_meta.metadata,
566                },
567            });
568            Some(extrude_surface)
569        }
570        Path::ArcThreePoint { .. } => {
571            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
572                face_id: actual_face_id,
573                tag: path.get_base().tag.clone(),
574                geo_meta: GeoMeta {
575                    id: path.get_base().geo_meta.id,
576                    metadata: path.get_base().geo_meta.metadata,
577                },
578            });
579            Some(extrude_surface)
580        }
581    }
582}
583
584/// Create a fake extrude surface to report for mock execution, when there's no engine response.
585fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
586    let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
587        // pushing this values with a fake face_id to make extrudes mock-execute safe
588        face_id: exec_state.next_uuid(),
589        tag: path.get_base().tag.clone(),
590        geo_meta: GeoMeta {
591            id: path.get_base().geo_meta.id,
592            metadata: path.get_base().geo_meta.metadata,
593        },
594    });
595    Some(extrude_surface)
596}