kcl_lib/std/
extrude.rs

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