Skip to main content

kcl_lib/std/
extrude.rs

1//! Functions related to extruding.
2
3use std::collections::HashMap;
4
5use anyhow::Result;
6use kcmc::ModelingCmd;
7use kcmc::each_cmd as mcmd;
8use kcmc::length_unit::LengthUnit;
9use kcmc::ok_response::OkModelingCmdResponse;
10use kcmc::output::ExtrusionFaceInfo;
11use kcmc::shared::ExtrudeReference;
12use kcmc::shared::ExtrusionFaceCapType;
13use kcmc::shared::Opposite;
14use kcmc::shared::Point3d as KPoint3d; // Point3d is already defined in this pkg, to impl ts_rs traits.
15use kcmc::websocket::ModelingCmdReq;
16use kcmc::websocket::OkWebSocketResponseData;
17use kittycad_modeling_cmds::shared::Angle;
18use kittycad_modeling_cmds::shared::BodyType;
19use kittycad_modeling_cmds::shared::ExtrudeMethod;
20use kittycad_modeling_cmds::shared::Point2d;
21use kittycad_modeling_cmds::{self as kcmc};
22use uuid::Uuid;
23
24use super::DEFAULT_TOLERANCE_MM;
25use super::args::TyF64;
26use super::utils::point_to_mm;
27use crate::errors::KclError;
28use crate::errors::KclErrorDetails;
29use crate::execution::ArtifactId;
30use crate::execution::CreatorFace;
31use crate::execution::ExecState;
32use crate::execution::Extrudable;
33use crate::execution::ExtrudeSurface;
34use crate::execution::GeoMeta;
35use crate::execution::KclValue;
36use crate::execution::ModelingCmdMeta;
37use crate::execution::Path;
38use crate::execution::ProfileClosed;
39use crate::execution::Sketch;
40use crate::execution::SketchSurface;
41use crate::execution::Solid;
42use crate::execution::SolidCreator;
43use crate::execution::annotations;
44use crate::execution::types::ArrayLen;
45use crate::execution::types::PrimitiveType;
46use crate::execution::types::RuntimeType;
47use crate::parsing::ast::types::TagNode;
48use crate::std::Args;
49use crate::std::axis_or_reference::Point3dAxis3dOrGeometryReference;
50
51/// Extrudes by a given amount.
52pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
53    let sketches: Vec<Extrudable> = args.get_unlabeled_kw_arg(
54        "sketches",
55        &RuntimeType::Array(
56            Box::new(RuntimeType::Union(vec![
57                RuntimeType::sketch(),
58                RuntimeType::face(),
59                RuntimeType::tagged_face(),
60            ])),
61            ArrayLen::Minimum(1),
62        ),
63        exec_state,
64    )?;
65
66    let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
67    let to = args.get_kw_arg_opt(
68        "to",
69        &RuntimeType::Union(vec![
70            RuntimeType::point3d(),
71            RuntimeType::Primitive(PrimitiveType::Axis3d),
72            RuntimeType::Primitive(PrimitiveType::Edge),
73            RuntimeType::plane(),
74            RuntimeType::Primitive(PrimitiveType::Face),
75            RuntimeType::sketch(),
76            RuntimeType::Primitive(PrimitiveType::Solid),
77            RuntimeType::tagged_edge(),
78            RuntimeType::tagged_face(),
79        ]),
80        exec_state,
81    )?;
82    let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
83    let bidirectional_length: Option<TyF64> =
84        args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
85    let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
86    let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
87    let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
88    let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
89    let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
90    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
91    let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
92    let hide_seams: Option<bool> = args.get_kw_arg_opt("hideSeams", &RuntimeType::bool(), exec_state)?;
93    let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
94
95    let result = inner_extrude(
96        sketches,
97        length,
98        to,
99        symmetric,
100        bidirectional_length,
101        tag_start,
102        tag_end,
103        twist_angle,
104        twist_angle_step,
105        twist_center,
106        tolerance,
107        method,
108        hide_seams,
109        body_type,
110        exec_state,
111        args,
112    )
113    .await?;
114
115    Ok(result.into())
116}
117
118#[allow(clippy::too_many_arguments)]
119async fn inner_extrude(
120    extrudables: Vec<Extrudable>,
121    length: Option<TyF64>,
122    to: Option<Point3dAxis3dOrGeometryReference>,
123    symmetric: Option<bool>,
124    bidirectional_length: Option<TyF64>,
125    tag_start: Option<TagNode>,
126    tag_end: Option<TagNode>,
127    twist_angle: Option<TyF64>,
128    twist_angle_step: Option<TyF64>,
129    twist_center: Option<[TyF64; 2]>,
130    tolerance: Option<TyF64>,
131    method: Option<String>,
132    hide_seams: Option<bool>,
133    body_type: Option<BodyType>,
134    exec_state: &mut ExecState,
135    args: Args,
136) -> Result<Vec<Solid>, KclError> {
137    let body_type = body_type.unwrap_or_default();
138
139    if matches!(body_type, BodyType::Solid) && extrudables.iter().any(|sk| matches!(sk.is_closed(), ProfileClosed::No))
140    {
141        return Err(KclError::new_semantic(KclErrorDetails::new(
142            "Cannot solid extrude an open profile. Either close the profile, or use a surface extrude.".to_owned(),
143            vec![args.source_range],
144        )));
145    }
146
147    // Extrude the element(s).
148    let mut solids = Vec::new();
149    let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
150
151    let extrude_method = match method.as_deref() {
152        Some("new" | "NEW") => ExtrudeMethod::New,
153        Some("merge" | "MERGE") => ExtrudeMethod::Merge,
154        None => ExtrudeMethod::default(),
155        Some(other) => {
156            return Err(KclError::new_semantic(KclErrorDetails::new(
157                format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
158                vec![args.source_range],
159            )));
160        }
161    };
162
163    if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
164        return Err(KclError::new_semantic(KclErrorDetails::new(
165            "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
166                .to_owned(),
167            vec![args.source_range],
168        )));
169    }
170
171    if (length.is_some() || twist_angle.is_some()) && to.is_some() {
172        return Err(KclError::new_semantic(KclErrorDetails::new(
173            "You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
174                .to_owned(),
175            vec![args.source_range],
176        )));
177    }
178
179    let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
180
181    let opposite = match (symmetric, bidirection) {
182        (Some(true), _) => Opposite::Symmetric,
183        (None, None) => Opposite::None,
184        (Some(false), None) => Opposite::None,
185        (None, Some(length)) => Opposite::Other(length),
186        (Some(false), Some(length)) => Opposite::Other(length),
187    };
188
189    for extrudable in &extrudables {
190        let extrude_cmd_id = exec_state.next_uuid();
191        let sketch_or_face_id = extrudable.id_to_extrude(exec_state, &args, false).await?;
192        let cmd = match (&twist_angle, &twist_angle_step, &twist_center, length.clone(), &to) {
193            (Some(angle), angle_step, center, Some(length), None) => {
194                let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
195                let total_rotation_angle = Angle::from_degrees(angle.to_degrees(exec_state, args.source_range));
196                let angle_step_size = Angle::from_degrees(
197                    angle_step
198                        .clone()
199                        .map(|a| a.to_degrees(exec_state, args.source_range))
200                        .unwrap_or(15.0),
201                );
202                ModelingCmd::from(
203                    mcmd::TwistExtrude::builder()
204                        .target(sketch_or_face_id.into())
205                        .distance(LengthUnit(length.to_mm()))
206                        .center_2d(center)
207                        .total_rotation_angle(total_rotation_angle)
208                        .angle_step_size(angle_step_size)
209                        .tolerance(tolerance)
210                        .body_type(body_type)
211                        .build(),
212                )
213            }
214            (None, None, None, Some(length), None) => ModelingCmd::from(
215                mcmd::Extrude::builder()
216                    .target(sketch_or_face_id.into())
217                    .distance(LengthUnit(length.to_mm()))
218                    .opposite(opposite.clone())
219                    .extrude_method(extrude_method)
220                    .body_type(body_type)
221                    .maybe_merge_coplanar_faces(hide_seams)
222                    .build(),
223            ),
224            (None, None, None, None, Some(to)) => match to {
225                Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(
226                    mcmd::ExtrudeToReference::builder()
227                        .target(sketch_or_face_id.into())
228                        .reference(ExtrudeReference::Point {
229                            point: KPoint3d {
230                                x: LengthUnit(point[0].to_mm()),
231                                y: LengthUnit(point[1].to_mm()),
232                                z: LengthUnit(point[2].to_mm()),
233                            },
234                        })
235                        .extrude_method(extrude_method)
236                        .body_type(body_type)
237                        .build(),
238                ),
239                Point3dAxis3dOrGeometryReference::Axis { direction, origin } => ModelingCmd::from(
240                    mcmd::ExtrudeToReference::builder()
241                        .target(sketch_or_face_id.into())
242                        .reference(ExtrudeReference::Axis {
243                            axis: KPoint3d {
244                                x: direction[0].to_mm(),
245                                y: direction[1].to_mm(),
246                                z: direction[2].to_mm(),
247                            },
248                            point: KPoint3d {
249                                x: LengthUnit(origin[0].to_mm()),
250                                y: LengthUnit(origin[1].to_mm()),
251                                z: LengthUnit(origin[2].to_mm()),
252                            },
253                        })
254                        .extrude_method(extrude_method)
255                        .body_type(body_type)
256                        .build(),
257                ),
258                Point3dAxis3dOrGeometryReference::Plane(plane) => {
259                    let plane_id = if plane.is_uninitialized() {
260                        if plane.info.origin.units.is_none() {
261                            return Err(KclError::new_semantic(KclErrorDetails::new(
262                                "Origin of plane has unknown units".to_string(),
263                                vec![args.source_range],
264                            )));
265                        }
266                        let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
267                            plane.clone().info.into_plane_data(),
268                            exec_state,
269                            &args,
270                        )
271                        .await?;
272                        sketch_plane.id
273                    } else {
274                        plane.id
275                    };
276                    ModelingCmd::from(
277                        mcmd::ExtrudeToReference::builder()
278                            .target(sketch_or_face_id.into())
279                            .reference(ExtrudeReference::EntityReference { entity_id: plane_id })
280                            .extrude_method(extrude_method)
281                            .body_type(body_type)
282                            .build(),
283                    )
284                }
285                Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
286                    let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
287                    ModelingCmd::from(
288                        mcmd::ExtrudeToReference::builder()
289                            .target(sketch_or_face_id.into())
290                            .reference(ExtrudeReference::EntityReference { entity_id: edge_id })
291                            .extrude_method(extrude_method)
292                            .body_type(body_type)
293                            .build(),
294                    )
295                }
296                Point3dAxis3dOrGeometryReference::Face(face_tag) => {
297                    let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
298                    ModelingCmd::from(
299                        mcmd::ExtrudeToReference::builder()
300                            .target(sketch_or_face_id.into())
301                            .reference(ExtrudeReference::EntityReference { entity_id: face_id })
302                            .extrude_method(extrude_method)
303                            .body_type(body_type)
304                            .build(),
305                    )
306                }
307                Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(
308                    mcmd::ExtrudeToReference::builder()
309                        .target(sketch_or_face_id.into())
310                        .reference(ExtrudeReference::EntityReference {
311                            entity_id: sketch_ref.id,
312                        })
313                        .extrude_method(extrude_method)
314                        .body_type(body_type)
315                        .build(),
316                ),
317                Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(
318                    mcmd::ExtrudeToReference::builder()
319                        .target(sketch_or_face_id.into())
320                        .reference(ExtrudeReference::EntityReference { entity_id: solid.id })
321                        .extrude_method(extrude_method)
322                        .body_type(body_type)
323                        .build(),
324                ),
325                Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
326                    let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
327                    let tagged_edge_or_face_id = tagged_edge_or_face.id;
328                    ModelingCmd::from(
329                        mcmd::ExtrudeToReference::builder()
330                            .target(sketch_or_face_id.into())
331                            .reference(ExtrudeReference::EntityReference {
332                                entity_id: tagged_edge_or_face_id,
333                            })
334                            .extrude_method(extrude_method)
335                            .body_type(body_type)
336                            .build(),
337                    )
338                }
339            },
340            (Some(_), _, _, None, None) => {
341                return Err(KclError::new_semantic(KclErrorDetails::new(
342                    "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
343                    vec![args.source_range],
344                )));
345            }
346            (_, _, _, None, None) => {
347                return Err(KclError::new_semantic(KclErrorDetails::new(
348                    "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
349                    vec![args.source_range],
350                )));
351            }
352            (_, _, _, Some(_), Some(_)) => {
353                return Err(KclError::new_semantic(KclErrorDetails::new(
354                    "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
355                    vec![args.source_range],
356                )));
357            }
358            (_, _, _, _, _) => {
359                return Err(KclError::new_semantic(KclErrorDetails::new(
360                    "Invalid combination of parameters for extrusion.".to_owned(),
361                    vec![args.source_range],
362                )));
363            }
364        };
365
366        let being_extruded = match extrudable {
367            Extrudable::Sketch(..) => BeingExtruded::Sketch,
368            Extrudable::Face(face_tag) => {
369                let face_id = sketch_or_face_id;
370                let solid_id = match face_tag.geometry() {
371                    Some(crate::execution::Geometry::Solid(solid)) => solid.id,
372                    Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
373                        SketchSurface::Face(face) => face.solid.id,
374                        SketchSurface::Plane(_) => sketch.id,
375                    },
376                    None => face_id,
377                };
378                BeingExtruded::Face { face_id, solid_id }
379            }
380        };
381        if let Some(post_extr_sketch) = extrudable.as_sketch() {
382            let cmds = post_extr_sketch.build_sketch_mode_cmds(
383                exec_state,
384                ModelingCmdReq {
385                    cmd_id: extrude_cmd_id.into(),
386                    cmd,
387                },
388            );
389            exec_state
390                .batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
391                .await?;
392            solids.push(
393                do_post_extrude(
394                    &post_extr_sketch,
395                    extrude_cmd_id.into(),
396                    false,
397                    &NamedCapTags {
398                        start: tag_start.as_ref(),
399                        end: tag_end.as_ref(),
400                    },
401                    extrude_method,
402                    exec_state,
403                    &args,
404                    None,
405                    None,
406                    body_type,
407                    being_extruded,
408                )
409                .await?,
410            );
411        } else {
412            return Err(KclError::new_type(KclErrorDetails::new(
413                "Expected a sketch for extrusion".to_owned(),
414                vec![args.source_range],
415            )));
416        }
417    }
418
419    Ok(solids)
420}
421
422#[derive(Debug, Default)]
423pub(crate) struct NamedCapTags<'a> {
424    pub start: Option<&'a TagNode>,
425    pub end: Option<&'a TagNode>,
426}
427
428#[derive(Debug, Clone, Copy)]
429pub enum BeingExtruded {
430    Sketch,
431    Face { face_id: Uuid, solid_id: Uuid },
432}
433
434#[allow(clippy::too_many_arguments)]
435pub(crate) async fn do_post_extrude<'a>(
436    sketch: &Sketch,
437    extrude_cmd_id: ArtifactId,
438    sectional: bool,
439    named_cap_tags: &'a NamedCapTags<'a>,
440    extrude_method: ExtrudeMethod,
441    exec_state: &mut ExecState,
442    args: &Args,
443    edge_id: Option<Uuid>,
444    clone_id_map: Option<&HashMap<Uuid, Uuid>>, // old sketch id -> new sketch id
445    body_type: BodyType,
446    being_extruded: BeingExtruded,
447) -> Result<Solid, KclError> {
448    // Bring the object to the front of the scene.
449    // See: https://github.com/KittyCAD/modeling-app/issues/806
450
451    exec_state
452        .batch_modeling_cmd(
453            ModelingCmdMeta::from_args(exec_state, args),
454            ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
455        )
456        .await?;
457
458    let any_edge_id = if let Some(edge_id) = sketch.mirror {
459        edge_id
460    } else if let Some(id) = edge_id {
461        id
462    } else {
463        // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
464        // So, let's just use the first one.
465        let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
466            return Err(KclError::new_type(KclErrorDetails::new(
467                "Expected a non-empty sketch".to_owned(),
468                vec![args.source_range],
469            )));
470        };
471        any_edge_id
472    };
473
474    // If the sketch is a clone, we will use the original info to get the extrusion face info.
475    let mut extrusion_info_edge_id = any_edge_id;
476    if sketch.clone.is_some() && clone_id_map.is_some() {
477        extrusion_info_edge_id = if let Some(clone_map) = clone_id_map {
478            if let Some(new_edge_id) = clone_map.get(&extrusion_info_edge_id) {
479                *new_edge_id
480            } else {
481                extrusion_info_edge_id
482            }
483        } else {
484            any_edge_id
485        };
486    }
487
488    let mut sketch = sketch.clone();
489    match body_type {
490        BodyType::Solid => {
491            sketch.is_closed = ProfileClosed::Explicitly;
492        }
493        BodyType::Surface => {}
494        _other => {
495            // At some point in the future we'll add sheet metal or something.
496            // Figure this out then.
497        }
498    }
499
500    match (extrude_method, being_extruded) {
501        (ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
502            // Merge the IDs.
503            // If we were sketching on a face, we need the original face id.
504            if let SketchSurface::Face(ref face) = sketch.on {
505                // If we're merging into an existing body, then assign the existing body's ID,
506                // because the variable binding for this solid won't be its own object, it's just modifying the original one.
507                sketch.id = face.solid.sketch_id().unwrap_or(face.solid.id);
508            }
509        }
510        (ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
511            // We're creating a new solid, it's not based on any existing sketch (it's based on a face).
512            // So we need a new ID, the extrude command ID.
513            sketch.id = extrude_cmd_id.into();
514        }
515        (ExtrudeMethod::New, BeingExtruded::Sketch) => {
516            // If we are creating a new body we need to preserve its new id.
517            // The sketch's ID is already correct here, it should be the ID of the sketch.
518        }
519        (ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
520            if let SketchSurface::Face(ref face) = sketch.on {
521                // If we're merging into an existing body, then assign the existing body's ID,
522                // because the variable binding for this solid won't be its own object, it's just modifying the original one.
523                sketch.id = face.solid.sketch_id().unwrap_or(face.solid.id);
524            }
525        }
526        (other, _) => {
527            // If you ever hit this, you should add a new arm to the match expression, and implement support for the new ExtrudeMethod variant.
528            return Err(KclError::new_internal(KclErrorDetails::new(
529                format!("Zoo does not yet support creating bodies via {other:?}"),
530                vec![args.source_range],
531            )));
532        }
533    }
534
535    // Similarly, if the sketch is a clone, we need to use the original sketch id to get the extrusion face info.
536    let sketch_id = if let Some(cloned_from) = sketch.clone
537        && clone_id_map.is_some()
538    {
539        cloned_from
540    } else {
541        sketch.id
542    };
543
544    let solid3d_info = exec_state
545        .send_modeling_cmd(
546            ModelingCmdMeta::from_args(exec_state, args),
547            ModelingCmd::from(
548                mcmd::Solid3dGetExtrusionFaceInfo::builder()
549                    .edge_id(extrusion_info_edge_id)
550                    .object_id(sketch_id)
551                    .build(),
552            ),
553        )
554        .await?;
555
556    let face_infos = if let OkWebSocketResponseData::Modeling {
557        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
558    } = solid3d_info
559    {
560        data.faces
561    } else {
562        vec![]
563    };
564
565    // Only do this if we need the artifact graph.
566    #[cfg(feature = "artifact-graph")]
567    {
568        // Getting the ids of a sectional sweep does not work well and we cannot guarantee that
569        // any of these call will not just fail.
570        if !sectional {
571            exec_state
572                .batch_modeling_cmd(
573                    ModelingCmdMeta::from_args(exec_state, args),
574                    ModelingCmd::from(
575                        mcmd::Solid3dGetAdjacencyInfo::builder()
576                            .object_id(sketch.id)
577                            .edge_id(any_edge_id)
578                            .build(),
579                    ),
580                )
581                .await?;
582        }
583    }
584
585    let Faces {
586        sides: mut face_id_map,
587        start_cap_id,
588        end_cap_id,
589    } = analyze_faces(exec_state, args, face_infos).await;
590
591    // 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.
592    if sketch.clone.is_some()
593        && let Some(clone_id_map) = clone_id_map
594    {
595        face_id_map = face_id_map
596            .into_iter()
597            .filter_map(|(k, v)| {
598                let fe_key = clone_id_map.get(&k)?;
599                let fe_value = clone_id_map.get(&(v?)).copied();
600                Some((*fe_key, fe_value))
601            })
602            .collect::<HashMap<Uuid, Option<Uuid>>>();
603    }
604
605    // Iterate over the sketch.value array and add face_id to GeoMeta
606    let no_engine_commands = args.ctx.no_engine_commands().await;
607    let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
608    let outer_surfaces = sketch.paths.iter().flat_map(|path| {
609        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
610            surface_of(path, *actual_face_id)
611        } else if no_engine_commands {
612            crate::log::logln!(
613                "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
614                path.get_base().geo_meta.id
615            );
616            // Only pre-populate the extrude surface if we are in mock mode.
617            fake_extrude_surface(exec_state, path)
618        } else if sketch.clone.is_some()
619            && let Some(clone_map) = clone_id_map
620        {
621            let new_path = clone_map.get(&(path.get_base().geo_meta.id));
622
623            if let Some(new_path) = new_path {
624                match face_id_map.get(new_path) {
625                    Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
626                    _ => {
627                        let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
628                            if let Some(value) = value {
629                                if value == new_path { Some(key) } else { None }
630                            } else {
631                                None
632                            }
633                        });
634                        match actual_face_id {
635                            Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
636                            None => {
637                                crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
638                                None
639                            }
640                        }
641                    }
642                }
643            } else {
644                None
645            }
646        } else {
647            crate::log::logln!(
648                "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
649                path.get_base().geo_meta.id
650            );
651            None
652        }
653    });
654
655    new_value.extend(outer_surfaces);
656    let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
657        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
658            surface_of(path, *actual_face_id)
659        } else if no_engine_commands {
660            // Only pre-populate the extrude surface if we are in mock mode.
661            fake_extrude_surface(exec_state, path)
662        } else {
663            None
664        }
665    });
666    new_value.extend(inner_surfaces);
667
668    // Add the tags for the start or end caps.
669    if let Some(tag_start) = named_cap_tags.start {
670        let Some(start_cap_id) = start_cap_id else {
671            return Err(KclError::new_type(KclErrorDetails::new(
672                format!(
673                    "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
674                    tag_start.name, sketch.id
675                ),
676                vec![args.source_range],
677            )));
678        };
679
680        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
681            face_id: start_cap_id,
682            tag: Some(tag_start.clone()),
683            geo_meta: GeoMeta {
684                id: start_cap_id,
685                metadata: args.source_range.into(),
686            },
687        }));
688    }
689    if let Some(tag_end) = named_cap_tags.end {
690        let Some(end_cap_id) = end_cap_id else {
691            return Err(KclError::new_type(KclErrorDetails::new(
692                format!(
693                    "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
694                    tag_end.name, sketch.id
695                ),
696                vec![args.source_range],
697            )));
698        };
699
700        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
701            face_id: end_cap_id,
702            tag: Some(tag_end.clone()),
703            geo_meta: GeoMeta {
704                id: end_cap_id,
705                metadata: args.source_range.into(),
706            },
707        }));
708    }
709
710    let meta = sketch.meta.clone();
711    let units = sketch.units;
712    let id = sketch.id;
713    // let creator = match &sketch.on {
714    //     SketchSurface::Plane(_) => SolidCreator::Sketch(sketch),
715    //     SketchSurface::Face(face) => SolidCreator::Face(CreatorFace {
716    //         face_id: face.id,
717    //         solid_id: face.solid.id,
718    //         sketch,
719    //     }),
720    // };
721    let creator = match being_extruded {
722        BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
723        BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
724            face_id,
725            solid_id,
726            sketch,
727        }),
728    };
729
730    Ok(Solid {
731        id,
732        artifact_id: extrude_cmd_id,
733        value: new_value,
734        meta,
735        units,
736        sectional,
737        creator,
738        start_cap_id,
739        end_cap_id,
740        edge_cuts: vec![],
741    })
742}
743
744#[derive(Default)]
745struct Faces {
746    /// Maps curve ID to face ID for each side.
747    sides: HashMap<Uuid, Option<Uuid>>,
748    /// Top face ID.
749    end_cap_id: Option<Uuid>,
750    /// Bottom face ID.
751    start_cap_id: Option<Uuid>,
752}
753
754async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
755    let mut faces = Faces {
756        sides: HashMap::with_capacity(face_infos.len()),
757        ..Default::default()
758    };
759    if args.ctx.no_engine_commands().await {
760        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
761        faces.start_cap_id = Some(exec_state.next_uuid());
762        faces.end_cap_id = Some(exec_state.next_uuid());
763    }
764    for face_info in face_infos {
765        match face_info.cap {
766            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
767            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
768            ExtrusionFaceCapType::Both => {
769                faces.end_cap_id = face_info.face_id;
770                faces.start_cap_id = face_info.face_id;
771            }
772            ExtrusionFaceCapType::None => {
773                if let Some(curve_id) = face_info.curve_id {
774                    faces.sides.insert(curve_id, face_info.face_id);
775                }
776            }
777            other => {
778                exec_state.warn(
779                    crate::CompilationError {
780                        source_range: args.source_range,
781                        message: format!("unknown extrusion face type {other:?}"),
782                        suggestion: None,
783                        severity: crate::errors::Severity::Warning,
784                        tag: crate::errors::Tag::Unnecessary,
785                    },
786                    annotations::WARN_NOT_YET_SUPPORTED,
787                );
788            }
789        }
790    }
791    faces
792}
793fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
794    match path {
795        Path::Arc { .. }
796        | Path::TangentialArc { .. }
797        | Path::TangentialArcTo { .. }
798        // TODO: (bc) fix me
799        | Path::Ellipse { .. }
800        | Path::Conic {.. }
801        | Path::Circle { .. }
802        | Path::CircleThreePoint { .. } => {
803            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
804                face_id: actual_face_id,
805                tag: path.get_base().tag.clone(),
806                geo_meta: GeoMeta {
807                    id: path.get_base().geo_meta.id,
808                    metadata: path.get_base().geo_meta.metadata,
809                },
810            });
811            Some(extrude_surface)
812        }
813        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
814            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
815                face_id: actual_face_id,
816                tag: path.get_base().tag.clone(),
817                geo_meta: GeoMeta {
818                    id: path.get_base().geo_meta.id,
819                    metadata: path.get_base().geo_meta.metadata,
820                },
821            });
822            Some(extrude_surface)
823        }
824        Path::ArcThreePoint { .. } => {
825            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
826                face_id: actual_face_id,
827                tag: path.get_base().tag.clone(),
828                geo_meta: GeoMeta {
829                    id: path.get_base().geo_meta.id,
830                    metadata: path.get_base().geo_meta.metadata,
831                },
832            });
833            Some(extrude_surface)
834        }
835    }
836}
837
838fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
839    match path {
840        Path::Arc { .. }
841        | Path::TangentialArc { .. }
842        | Path::TangentialArcTo { .. }
843        // TODO: (gserena) fix me
844        | Path::Ellipse { .. }
845        | Path::Conic {.. }
846        | Path::Circle { .. }
847        | Path::CircleThreePoint { .. } => {
848            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
849                face_id: actual_face_id,
850                tag: path.get_base().tag.clone(),
851                geo_meta: GeoMeta {
852                    id: clone_path_id,
853                    metadata: path.get_base().geo_meta.metadata,
854                },
855            });
856            Some(extrude_surface)
857        }
858        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
859            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
860                face_id: actual_face_id,
861                tag: path.get_base().tag.clone(),
862                geo_meta: GeoMeta {
863                    id: clone_path_id,
864                    metadata: path.get_base().geo_meta.metadata,
865                },
866            });
867            Some(extrude_surface)
868        }
869        Path::ArcThreePoint { .. } => {
870            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
871                face_id: actual_face_id,
872                tag: path.get_base().tag.clone(),
873                geo_meta: GeoMeta {
874                    id: clone_path_id,
875                    metadata: path.get_base().geo_meta.metadata,
876                },
877            });
878            Some(extrude_surface)
879        }
880    }
881}
882
883/// Create a fake extrude surface to report for mock execution, when there's no engine response.
884fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
885    let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
886        // pushing this values with a fake face_id to make extrudes mock-execute safe
887        face_id: exec_state.next_uuid(),
888        tag: path.get_base().tag.clone(),
889        geo_meta: GeoMeta {
890            id: path.get_base().geo_meta.id,
891            metadata: path.get_base().geo_meta.metadata,
892        },
893    });
894    Some(extrude_surface)
895}