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