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