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                None,
317            )
318            .await?,
319        );
320    }
321
322    Ok(solids)
323}
324
325#[derive(Debug, Default)]
326pub(crate) struct NamedCapTags<'a> {
327    pub start: Option<&'a TagNode>,
328    pub end: Option<&'a TagNode>,
329}
330
331#[allow(clippy::too_many_arguments)]
332pub(crate) async fn do_post_extrude<'a>(
333    sketch: &Sketch,
334    solid_id: ArtifactId,
335    sectional: bool,
336    named_cap_tags: &'a NamedCapTags<'a>,
337    extrude_method: ExtrudeMethod,
338    exec_state: &mut ExecState,
339    args: &Args,
340    edge_id: Option<Uuid>,
341    clone_id_map: Option<&HashMap<Uuid, Uuid>>, // old sketch id -> new sketch id
342) -> Result<Solid, KclError> {
343    // Bring the object to the front of the scene.
344    // See: https://github.com/KittyCAD/modeling-app/issues/806
345    exec_state
346        .batch_modeling_cmd(
347            args.into(),
348            ModelingCmd::from(mcmd::ObjectBringToFront { object_id: sketch.id }),
349        )
350        .await?;
351
352    let any_edge_id = if let Some(edge_id) = sketch.mirror {
353        edge_id
354    } else if let Some(id) = edge_id {
355        id
356    } else {
357        // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
358        // So, let's just use the first one.
359        let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
360            return Err(KclError::new_type(KclErrorDetails::new(
361                "Expected a non-empty sketch".to_owned(),
362                vec![args.source_range],
363            )));
364        };
365        any_edge_id
366    };
367
368    // If the sketch is a clone, we will use the original info to get the extrusion face info.
369    let mut extrusion_info_edge_id = any_edge_id;
370    if sketch.clone.is_some() && clone_id_map.is_some() {
371        extrusion_info_edge_id = if let Some(clone_map) = clone_id_map {
372            if let Some(new_edge_id) = clone_map.get(&extrusion_info_edge_id) {
373                *new_edge_id
374            } else {
375                extrusion_info_edge_id
376            }
377        } else {
378            any_edge_id
379        };
380    }
381
382    let mut sketch = sketch.clone();
383    sketch.is_closed = true;
384
385    // If we were sketching on a face, we need the original face id.
386    if let SketchSurface::Face(ref face) = sketch.on {
387        // If we are creating a new body we need to preserve its new id.
388        if extrude_method != ExtrudeMethod::New {
389            sketch.id = face.solid.sketch.id;
390        }
391    }
392
393    // Similarly, if the sketch is a clone, we need to use the original sketch id to get the extrusion face info.
394    let sketch_id = if let Some(cloned_from) = sketch.clone
395        && clone_id_map.is_some()
396    {
397        cloned_from
398    } else {
399        sketch.id
400    };
401
402    let solid3d_info = exec_state
403        .send_modeling_cmd(
404            args.into(),
405            ModelingCmd::from(mcmd::Solid3dGetExtrusionFaceInfo {
406                edge_id: extrusion_info_edge_id,
407                object_id: sketch_id,
408            }),
409        )
410        .await?;
411
412    let face_infos = if let OkWebSocketResponseData::Modeling {
413        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
414    } = solid3d_info
415    {
416        data.faces
417    } else {
418        vec![]
419    };
420
421    // Only do this if we need the artifact graph.
422    #[cfg(feature = "artifact-graph")]
423    {
424        // Getting the ids of a sectional sweep does not work well and we cannot guarantee that
425        // any of these call will not just fail.
426        if !sectional {
427            exec_state
428                .batch_modeling_cmd(
429                    args.into(),
430                    ModelingCmd::from(mcmd::Solid3dGetAdjacencyInfo {
431                        object_id: sketch.id,
432                        edge_id: any_edge_id,
433                    }),
434                )
435                .await?;
436        }
437    }
438
439    let Faces {
440        sides: mut face_id_map,
441        start_cap_id,
442        end_cap_id,
443    } = analyze_faces(exec_state, args, face_infos).await;
444
445    // If this is a clone, we will use the clone_id_map to map the face info from the original sketch to the clone sketch.
446    if sketch.clone.is_some()
447        && let Some(clone_id_map) = clone_id_map
448    {
449        face_id_map = face_id_map
450            .into_iter()
451            .filter_map(|(k, v)| {
452                let fe_key = clone_id_map.get(&k)?;
453                let fe_value = clone_id_map.get(&(v?)).copied();
454                Some((*fe_key, fe_value))
455            })
456            .collect::<HashMap<Uuid, Option<Uuid>>>();
457    }
458
459    // Iterate over the sketch.value array and add face_id to GeoMeta
460    let no_engine_commands = args.ctx.no_engine_commands().await;
461    let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
462    let outer_surfaces = sketch.paths.iter().flat_map(|path| {
463        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
464            surface_of(path, *actual_face_id)
465        } else if no_engine_commands {
466            crate::log::logln!(
467                "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
468                path.get_base().geo_meta.id
469            );
470            // Only pre-populate the extrude surface if we are in mock mode.
471            fake_extrude_surface(exec_state, path)
472        } else if sketch.clone.is_some()
473            && let Some(clone_map) = clone_id_map
474        {
475            let new_path = clone_map.get(&(path.get_base().geo_meta.id));
476
477            if let Some(new_path) = new_path {
478                match face_id_map.get(new_path) {
479                    Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
480                    _ => {
481                        let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
482                            if let Some(value) = value {
483                                if value == new_path { Some(key) } else { None }
484                            } else {
485                                None
486                            }
487                        });
488                        match actual_face_id {
489                            Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
490                            None => {
491                                crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
492                                None
493                            }
494                        }
495                    }
496                }
497            } else {
498                None
499            }
500        } else {
501            crate::log::logln!(
502                "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
503                path.get_base().geo_meta.id
504            );
505            None
506        }
507    });
508
509    new_value.extend(outer_surfaces);
510    let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
511        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
512            surface_of(path, *actual_face_id)
513        } else if no_engine_commands {
514            // Only pre-populate the extrude surface if we are in mock mode.
515            fake_extrude_surface(exec_state, path)
516        } else {
517            None
518        }
519    });
520    new_value.extend(inner_surfaces);
521
522    // Add the tags for the start or end caps.
523    if let Some(tag_start) = named_cap_tags.start {
524        let Some(start_cap_id) = start_cap_id else {
525            return Err(KclError::new_type(KclErrorDetails::new(
526                format!(
527                    "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
528                    tag_start.name, sketch.id
529                ),
530                vec![args.source_range],
531            )));
532        };
533
534        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
535            face_id: start_cap_id,
536            tag: Some(tag_start.clone()),
537            geo_meta: GeoMeta {
538                id: start_cap_id,
539                metadata: args.source_range.into(),
540            },
541        }));
542    }
543    if let Some(tag_end) = named_cap_tags.end {
544        let Some(end_cap_id) = end_cap_id else {
545            return Err(KclError::new_type(KclErrorDetails::new(
546                format!(
547                    "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
548                    tag_end.name, sketch.id
549                ),
550                vec![args.source_range],
551            )));
552        };
553
554        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
555            face_id: end_cap_id,
556            tag: Some(tag_end.clone()),
557            geo_meta: GeoMeta {
558                id: end_cap_id,
559                metadata: args.source_range.into(),
560            },
561        }));
562    }
563
564    Ok(Solid {
565        // Ok so you would think that the id would be the id of the solid,
566        // that we passed in to the function, but it's actually the id of the
567        // sketch.
568        //
569        // Why? Because when you extrude a sketch, the engine lets the solid absorb the
570        // sketch's ID. So the solid should take over the sketch's ID.
571        id: sketch.id,
572        artifact_id: solid_id,
573        value: new_value,
574        meta: sketch.meta.clone(),
575        units: sketch.units,
576        sectional,
577        sketch,
578        start_cap_id,
579        end_cap_id,
580        edge_cuts: vec![],
581    })
582}
583
584#[derive(Default)]
585struct Faces {
586    /// Maps curve ID to face ID for each side.
587    sides: HashMap<Uuid, Option<Uuid>>,
588    /// Top face ID.
589    end_cap_id: Option<Uuid>,
590    /// Bottom face ID.
591    start_cap_id: Option<Uuid>,
592}
593
594async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
595    let mut faces = Faces {
596        sides: HashMap::with_capacity(face_infos.len()),
597        ..Default::default()
598    };
599    if args.ctx.no_engine_commands().await {
600        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
601        faces.start_cap_id = Some(exec_state.next_uuid());
602        faces.end_cap_id = Some(exec_state.next_uuid());
603    }
604    for face_info in face_infos {
605        match face_info.cap {
606            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
607            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
608            ExtrusionFaceCapType::Both => {
609                faces.end_cap_id = face_info.face_id;
610                faces.start_cap_id = face_info.face_id;
611            }
612            ExtrusionFaceCapType::None => {
613                if let Some(curve_id) = face_info.curve_id {
614                    faces.sides.insert(curve_id, face_info.face_id);
615                }
616            }
617        }
618    }
619    faces
620}
621fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
622    match path {
623        Path::Arc { .. }
624        | Path::TangentialArc { .. }
625        | Path::TangentialArcTo { .. }
626        // TODO: (bc) fix me
627        | Path::Ellipse { .. }
628        | Path::Conic {.. }
629        | Path::Circle { .. }
630        | Path::CircleThreePoint { .. } => {
631            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
632                face_id: actual_face_id,
633                tag: path.get_base().tag.clone(),
634                geo_meta: GeoMeta {
635                    id: path.get_base().geo_meta.id,
636                    metadata: path.get_base().geo_meta.metadata,
637                },
638            });
639            Some(extrude_surface)
640        }
641        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
642            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
643                face_id: actual_face_id,
644                tag: path.get_base().tag.clone(),
645                geo_meta: GeoMeta {
646                    id: path.get_base().geo_meta.id,
647                    metadata: path.get_base().geo_meta.metadata,
648                },
649            });
650            Some(extrude_surface)
651        }
652        Path::ArcThreePoint { .. } => {
653            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
654                face_id: actual_face_id,
655                tag: path.get_base().tag.clone(),
656                geo_meta: GeoMeta {
657                    id: path.get_base().geo_meta.id,
658                    metadata: path.get_base().geo_meta.metadata,
659                },
660            });
661            Some(extrude_surface)
662        }
663    }
664}
665
666fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
667    match path {
668        Path::Arc { .. }
669        | Path::TangentialArc { .. }
670        | Path::TangentialArcTo { .. }
671        // TODO: (gserena) fix me
672        | Path::Ellipse { .. }
673        | Path::Conic {.. }
674        | Path::Circle { .. }
675        | Path::CircleThreePoint { .. } => {
676            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
677                face_id: actual_face_id,
678                tag: path.get_base().tag.clone(),
679                geo_meta: GeoMeta {
680                    id: clone_path_id,
681                    metadata: path.get_base().geo_meta.metadata,
682                },
683            });
684            Some(extrude_surface)
685        }
686        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => {
687            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
688                face_id: actual_face_id,
689                tag: path.get_base().tag.clone(),
690                geo_meta: GeoMeta {
691                    id: clone_path_id,
692                    metadata: path.get_base().geo_meta.metadata,
693                },
694            });
695            Some(extrude_surface)
696        }
697        Path::ArcThreePoint { .. } => {
698            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
699                face_id: actual_face_id,
700                tag: path.get_base().tag.clone(),
701                geo_meta: GeoMeta {
702                    id: clone_path_id,
703                    metadata: path.get_base().geo_meta.metadata,
704                },
705            });
706            Some(extrude_surface)
707        }
708    }
709}
710
711/// Create a fake extrude surface to report for mock execution, when there's no engine response.
712fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
713    let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
714        // pushing this values with a fake face_id to make extrudes mock-execute safe
715        face_id: exec_state.next_uuid(),
716        tag: path.get_base().tag.clone(),
717        geo_meta: GeoMeta {
718            id: path.get_base().geo_meta.id,
719            metadata: path.get_base().geo_meta.metadata,
720        },
721    });
722    Some(extrude_surface)
723}