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, CreatorFace, ExecState, Extrudable, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta, Path,
26        ProfileClosed, Sketch, SketchSurface, Solid, SolidCreator, annotations,
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(face_tag) => {
351                let face_id = sketch_or_face_id;
352                let solid_id = match face_tag.geometry() {
353                    Some(crate::execution::Geometry::Solid(solid)) => solid.id,
354                    Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
355                        SketchSurface::Face(face) => face.solid.id,
356                        SketchSurface::Plane(_) => sketch.id,
357                    },
358                    None => face_id,
359                };
360                BeingExtruded::Face { face_id, solid_id }
361            }
362        };
363        if let Some(post_extr_sketch) = extrudable.as_sketch() {
364            let cmds = post_extr_sketch.build_sketch_mode_cmds(
365                exec_state,
366                ModelingCmdReq {
367                    cmd_id: extrude_cmd_id.into(),
368                    cmd,
369                },
370            );
371            exec_state
372                .batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
373                .await?;
374            solids.push(
375                do_post_extrude(
376                    &post_extr_sketch,
377                    extrude_cmd_id.into(),
378                    false,
379                    &NamedCapTags {
380                        start: tag_start.as_ref(),
381                        end: tag_end.as_ref(),
382                    },
383                    extrude_method,
384                    exec_state,
385                    &args,
386                    None,
387                    None,
388                    body_type,
389                    being_extruded,
390                )
391                .await?,
392            );
393        } else {
394            return Err(KclError::new_type(KclErrorDetails::new(
395                "Expected a sketch for extrusion".to_owned(),
396                vec![args.source_range],
397            )));
398        }
399    }
400
401    Ok(solids)
402}
403
404#[derive(Debug, Default)]
405pub(crate) struct NamedCapTags<'a> {
406    pub start: Option<&'a TagNode>,
407    pub end: Option<&'a TagNode>,
408}
409
410#[derive(Debug, Clone, Copy)]
411pub enum BeingExtruded {
412    Sketch,
413    Face { face_id: Uuid, solid_id: Uuid },
414}
415
416#[allow(clippy::too_many_arguments)]
417pub(crate) async fn do_post_extrude<'a>(
418    sketch: &Sketch,
419    extrude_cmd_id: ArtifactId,
420    sectional: bool,
421    named_cap_tags: &'a NamedCapTags<'a>,
422    extrude_method: ExtrudeMethod,
423    exec_state: &mut ExecState,
424    args: &Args,
425    edge_id: Option<Uuid>,
426    clone_id_map: Option<&HashMap<Uuid, Uuid>>, // old sketch id -> new sketch id
427    body_type: BodyType,
428    being_extruded: BeingExtruded,
429) -> Result<Solid, KclError> {
430    // Bring the object to the front of the scene.
431    // See: https://github.com/KittyCAD/modeling-app/issues/806
432
433    exec_state
434        .batch_modeling_cmd(
435            ModelingCmdMeta::from_args(exec_state, args),
436            ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
437        )
438        .await?;
439
440    let any_edge_id = if let Some(edge_id) = sketch.mirror {
441        edge_id
442    } else if let Some(id) = edge_id {
443        id
444    } else {
445        // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
446        // So, let's just use the first one.
447        let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
448            return Err(KclError::new_type(KclErrorDetails::new(
449                "Expected a non-empty sketch".to_owned(),
450                vec![args.source_range],
451            )));
452        };
453        any_edge_id
454    };
455
456    // If the sketch is a clone, we will use the original info to get the extrusion face info.
457    let mut extrusion_info_edge_id = any_edge_id;
458    if sketch.clone.is_some() && clone_id_map.is_some() {
459        extrusion_info_edge_id = if let Some(clone_map) = clone_id_map {
460            if let Some(new_edge_id) = clone_map.get(&extrusion_info_edge_id) {
461                *new_edge_id
462            } else {
463                extrusion_info_edge_id
464            }
465        } else {
466            any_edge_id
467        };
468    }
469
470    let mut sketch = sketch.clone();
471    match body_type {
472        BodyType::Solid => {
473            sketch.is_closed = ProfileClosed::Explicitly;
474        }
475        BodyType::Surface => {}
476        _other => {
477            // At some point in the future we'll add sheet metal or something.
478            // Figure this out then.
479        }
480    }
481
482    match (extrude_method, being_extruded) {
483        (ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
484            // Merge the IDs.
485            // If we were sketching on a face, we need the original face id.
486            if let SketchSurface::Face(ref face) = sketch.on {
487                // If we're merging into an existing body, then assign the existing body's ID,
488                // because the variable binding for this solid won't be its own object, it's just modifying the original one.
489                sketch.id = face.solid.sketch_id().unwrap_or(face.solid.id);
490            }
491        }
492        (ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
493            // We're creating a new solid, it's not based on any existing sketch (it's based on a face).
494            // So we need a new ID, the extrude command ID.
495            sketch.id = extrude_cmd_id.into();
496        }
497        (ExtrudeMethod::New, BeingExtruded::Sketch) => {
498            // If we are creating a new body we need to preserve its new id.
499            // The sketch's ID is already correct here, it should be the ID of the sketch.
500        }
501        (ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
502            if let SketchSurface::Face(ref face) = sketch.on {
503                // If we're merging into an existing body, then assign the existing body's ID,
504                // because the variable binding for this solid won't be its own object, it's just modifying the original one.
505                sketch.id = face.solid.sketch_id().unwrap_or(face.solid.id);
506            }
507        }
508        (other, _) => {
509            // If you ever hit this, you should add a new arm to the match expression, and implement support for the new ExtrudeMethod variant.
510            return Err(KclError::new_internal(KclErrorDetails::new(
511                format!("Zoo does not yet support creating bodies via {other:?}"),
512                vec![args.source_range],
513            )));
514        }
515    }
516
517    // Similarly, if the sketch is a clone, we need to use the original sketch id to get the extrusion face info.
518    let sketch_id = if let Some(cloned_from) = sketch.clone
519        && clone_id_map.is_some()
520    {
521        cloned_from
522    } else {
523        sketch.id
524    };
525
526    let solid3d_info = exec_state
527        .send_modeling_cmd(
528            ModelingCmdMeta::from_args(exec_state, args),
529            ModelingCmd::from(
530                mcmd::Solid3dGetExtrusionFaceInfo::builder()
531                    .edge_id(extrusion_info_edge_id)
532                    .object_id(sketch_id)
533                    .build(),
534            ),
535        )
536        .await?;
537
538    let face_infos = if let OkWebSocketResponseData::Modeling {
539        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
540    } = solid3d_info
541    {
542        data.faces
543    } else {
544        vec![]
545    };
546
547    // Only do this if we need the artifact graph.
548    #[cfg(feature = "artifact-graph")]
549    {
550        // Getting the ids of a sectional sweep does not work well and we cannot guarantee that
551        // any of these call will not just fail.
552        if !sectional {
553            exec_state
554                .batch_modeling_cmd(
555                    ModelingCmdMeta::from_args(exec_state, args),
556                    ModelingCmd::from(
557                        mcmd::Solid3dGetAdjacencyInfo::builder()
558                            .object_id(sketch.id)
559                            .edge_id(any_edge_id)
560                            .build(),
561                    ),
562                )
563                .await?;
564        }
565    }
566
567    let Faces {
568        sides: mut face_id_map,
569        start_cap_id,
570        end_cap_id,
571    } = analyze_faces(exec_state, args, face_infos).await;
572
573    // 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.
574    if sketch.clone.is_some()
575        && let Some(clone_id_map) = clone_id_map
576    {
577        face_id_map = face_id_map
578            .into_iter()
579            .filter_map(|(k, v)| {
580                let fe_key = clone_id_map.get(&k)?;
581                let fe_value = clone_id_map.get(&(v?)).copied();
582                Some((*fe_key, fe_value))
583            })
584            .collect::<HashMap<Uuid, Option<Uuid>>>();
585    }
586
587    // Iterate over the sketch.value array and add face_id to GeoMeta
588    let no_engine_commands = args.ctx.no_engine_commands().await;
589    let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
590    let outer_surfaces = sketch.paths.iter().flat_map(|path| {
591        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
592            surface_of(path, *actual_face_id)
593        } else if no_engine_commands {
594            crate::log::logln!(
595                "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
596                path.get_base().geo_meta.id
597            );
598            // Only pre-populate the extrude surface if we are in mock mode.
599            fake_extrude_surface(exec_state, path)
600        } else if sketch.clone.is_some()
601            && let Some(clone_map) = clone_id_map
602        {
603            let new_path = clone_map.get(&(path.get_base().geo_meta.id));
604
605            if let Some(new_path) = new_path {
606                match face_id_map.get(new_path) {
607                    Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
608                    _ => {
609                        let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
610                            if let Some(value) = value {
611                                if value == new_path { Some(key) } else { None }
612                            } else {
613                                None
614                            }
615                        });
616                        match actual_face_id {
617                            Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
618                            None => {
619                                crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
620                                None
621                            }
622                        }
623                    }
624                }
625            } else {
626                None
627            }
628        } else {
629            crate::log::logln!(
630                "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
631                path.get_base().geo_meta.id
632            );
633            None
634        }
635    });
636
637    new_value.extend(outer_surfaces);
638    let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
639        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
640            surface_of(path, *actual_face_id)
641        } else if no_engine_commands {
642            // Only pre-populate the extrude surface if we are in mock mode.
643            fake_extrude_surface(exec_state, path)
644        } else {
645            None
646        }
647    });
648    new_value.extend(inner_surfaces);
649
650    // Add the tags for the start or end caps.
651    if let Some(tag_start) = named_cap_tags.start {
652        let Some(start_cap_id) = start_cap_id else {
653            return Err(KclError::new_type(KclErrorDetails::new(
654                format!(
655                    "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
656                    tag_start.name, sketch.id
657                ),
658                vec![args.source_range],
659            )));
660        };
661
662        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
663            face_id: start_cap_id,
664            tag: Some(tag_start.clone()),
665            geo_meta: GeoMeta {
666                id: start_cap_id,
667                metadata: args.source_range.into(),
668            },
669        }));
670    }
671    if let Some(tag_end) = named_cap_tags.end {
672        let Some(end_cap_id) = end_cap_id else {
673            return Err(KclError::new_type(KclErrorDetails::new(
674                format!(
675                    "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
676                    tag_end.name, sketch.id
677                ),
678                vec![args.source_range],
679            )));
680        };
681
682        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
683            face_id: end_cap_id,
684            tag: Some(tag_end.clone()),
685            geo_meta: GeoMeta {
686                id: end_cap_id,
687                metadata: args.source_range.into(),
688            },
689        }));
690    }
691
692    let meta = sketch.meta.clone();
693    let units = sketch.units;
694    let id = sketch.id;
695    // let creator = match &sketch.on {
696    //     SketchSurface::Plane(_) => SolidCreator::Sketch(sketch),
697    //     SketchSurface::Face(face) => SolidCreator::Face(CreatorFace {
698    //         face_id: face.id,
699    //         solid_id: face.solid.id,
700    //         sketch,
701    //     }),
702    // };
703    let creator = match being_extruded {
704        BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
705        BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
706            face_id,
707            solid_id,
708            sketch,
709        }),
710    };
711
712    Ok(Solid {
713        id,
714        artifact_id: extrude_cmd_id,
715        value: new_value,
716        meta,
717        units,
718        sectional,
719        creator,
720        start_cap_id,
721        end_cap_id,
722        edge_cuts: vec![],
723    })
724}
725
726#[derive(Default)]
727struct Faces {
728    /// Maps curve ID to face ID for each side.
729    sides: HashMap<Uuid, Option<Uuid>>,
730    /// Top face ID.
731    end_cap_id: Option<Uuid>,
732    /// Bottom face ID.
733    start_cap_id: Option<Uuid>,
734}
735
736async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
737    let mut faces = Faces {
738        sides: HashMap::with_capacity(face_infos.len()),
739        ..Default::default()
740    };
741    if args.ctx.no_engine_commands().await {
742        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
743        faces.start_cap_id = Some(exec_state.next_uuid());
744        faces.end_cap_id = Some(exec_state.next_uuid());
745    }
746    for face_info in face_infos {
747        match face_info.cap {
748            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
749            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
750            ExtrusionFaceCapType::Both => {
751                faces.end_cap_id = face_info.face_id;
752                faces.start_cap_id = face_info.face_id;
753            }
754            ExtrusionFaceCapType::None => {
755                if let Some(curve_id) = face_info.curve_id {
756                    faces.sides.insert(curve_id, face_info.face_id);
757                }
758            }
759            other => {
760                exec_state.warn(
761                    crate::CompilationError {
762                        source_range: args.source_range,
763                        message: format!("unknown extrusion face type {other:?}"),
764                        suggestion: None,
765                        severity: crate::errors::Severity::Warning,
766                        tag: crate::errors::Tag::Unnecessary,
767                    },
768                    annotations::WARN_NOT_YET_SUPPORTED,
769                );
770            }
771        }
772    }
773    faces
774}
775fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
776    match path {
777        Path::Arc { .. }
778        | Path::TangentialArc { .. }
779        | Path::TangentialArcTo { .. }
780        // TODO: (bc) fix me
781        | Path::Ellipse { .. }
782        | Path::Conic {.. }
783        | Path::Circle { .. }
784        | Path::CircleThreePoint { .. } => {
785            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
786                face_id: actual_face_id,
787                tag: path.get_base().tag.clone(),
788                geo_meta: GeoMeta {
789                    id: path.get_base().geo_meta.id,
790                    metadata: path.get_base().geo_meta.metadata,
791                },
792            });
793            Some(extrude_surface)
794        }
795        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
796            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
797                face_id: actual_face_id,
798                tag: path.get_base().tag.clone(),
799                geo_meta: GeoMeta {
800                    id: path.get_base().geo_meta.id,
801                    metadata: path.get_base().geo_meta.metadata,
802                },
803            });
804            Some(extrude_surface)
805        }
806        Path::ArcThreePoint { .. } => {
807            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
808                face_id: actual_face_id,
809                tag: path.get_base().tag.clone(),
810                geo_meta: GeoMeta {
811                    id: path.get_base().geo_meta.id,
812                    metadata: path.get_base().geo_meta.metadata,
813                },
814            });
815            Some(extrude_surface)
816        }
817    }
818}
819
820fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
821    match path {
822        Path::Arc { .. }
823        | Path::TangentialArc { .. }
824        | Path::TangentialArcTo { .. }
825        // TODO: (gserena) fix me
826        | Path::Ellipse { .. }
827        | Path::Conic {.. }
828        | Path::Circle { .. }
829        | Path::CircleThreePoint { .. } => {
830            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
831                face_id: actual_face_id,
832                tag: path.get_base().tag.clone(),
833                geo_meta: GeoMeta {
834                    id: clone_path_id,
835                    metadata: path.get_base().geo_meta.metadata,
836                },
837            });
838            Some(extrude_surface)
839        }
840        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
841            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
842                face_id: actual_face_id,
843                tag: path.get_base().tag.clone(),
844                geo_meta: GeoMeta {
845                    id: clone_path_id,
846                    metadata: path.get_base().geo_meta.metadata,
847                },
848            });
849            Some(extrude_surface)
850        }
851        Path::ArcThreePoint { .. } => {
852            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
853                face_id: actual_face_id,
854                tag: path.get_base().tag.clone(),
855                geo_meta: GeoMeta {
856                    id: clone_path_id,
857                    metadata: path.get_base().geo_meta.metadata,
858                },
859            });
860            Some(extrude_surface)
861        }
862    }
863}
864
865/// Create a fake extrude surface to report for mock execution, when there's no engine response.
866fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
867    let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
868        // pushing this values with a fake face_id to make extrudes mock-execute safe
869        face_id: exec_state.next_uuid(),
870        tag: path.get_base().tag.clone(),
871        geo_meta: GeoMeta {
872            id: path.get_base().geo_meta.id,
873            metadata: path.get_base().geo_meta.metadata,
874        },
875    });
876    Some(extrude_surface)
877}