Skip to main content

kcl_lib/std/
extrude.rs

1//! Functions related to extruding.
2
3use std::collections::HashMap;
4
5use anyhow::Result;
6use indexmap::IndexMap;
7use kcmc::ModelingCmd;
8use kcmc::each_cmd as mcmd;
9use kcmc::length_unit::LengthUnit;
10use kcmc::ok_response::OkModelingCmdResponse;
11use kcmc::output::ExtrusionFaceInfo;
12use kcmc::shared::ExtrudeReference;
13use kcmc::shared::ExtrusionFaceCapType;
14use kcmc::shared::Opposite;
15use kcmc::shared::Point3d as KPoint3d; // Point3d is already defined in this pkg, to impl ts_rs traits.
16use kcmc::websocket::ModelingCmdReq;
17use kcmc::websocket::OkWebSocketResponseData;
18use kittycad_modeling_cmds::shared::Angle;
19use kittycad_modeling_cmds::shared::BodyType;
20use kittycad_modeling_cmds::shared::DirectionType;
21use kittycad_modeling_cmds::shared::EntityReference;
22use kittycad_modeling_cmds::shared::ExtrudeMethod;
23use kittycad_modeling_cmds::shared::Point2d;
24use kittycad_modeling_cmds::{self as kcmc};
25use uuid::Uuid;
26
27use super::DEFAULT_TOLERANCE_MM;
28use super::args::FromKclValue;
29use super::args::TyF64;
30use super::utils::point_to_mm;
31use crate::errors::KclError;
32use crate::errors::KclErrorDetails;
33use crate::execution::ArtifactId;
34use crate::execution::CreatorEdge;
35use crate::execution::CreatorFace;
36use crate::execution::ExecState;
37use crate::execution::ExecutorContext;
38use crate::execution::Extrudable;
39use crate::execution::ExtrudePlane;
40use crate::execution::ExtrudeSurface;
41use crate::execution::GeoMeta;
42use crate::execution::KclValue;
43use crate::execution::ModelingCmdMeta;
44use crate::execution::Path;
45use crate::execution::ProfileClosed;
46use crate::execution::Segment;
47use crate::execution::SegmentKind;
48use crate::execution::Sketch;
49use crate::execution::SketchSurface;
50use crate::execution::Solid;
51use crate::execution::SolidCreator;
52use crate::execution::annotations;
53use crate::execution::types::ArrayLen;
54use crate::execution::types::PrimitiveType;
55use crate::execution::types::RuntimeType;
56use crate::parsing::ast::types::TagDeclarator;
57use crate::parsing::ast::types::TagNode;
58use crate::std::Args;
59use crate::std::axis_or_reference::Point3dAxis3dOrGeometryReference;
60use crate::std::axis_or_reference::Point3dOrEdgeReference;
61use crate::std::edge::{self};
62use crate::std::solver::create_segments_in_engine;
63
64/// Extrudes by a given amount.
65pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
66    let sketch_values: Vec<KclValue> = args.get_unlabeled_kw_arg(
67        "sketches",
68        &RuntimeType::Array(
69            Box::new(RuntimeType::Union(vec![
70                RuntimeType::sketch(),
71                RuntimeType::face(),
72                RuntimeType::tagged_face(),
73                RuntimeType::tagged_edge(),
74                RuntimeType::Primitive(PrimitiveType::Edge),
75                RuntimeType::segment(),
76            ])),
77            ArrayLen::Minimum(1),
78        ),
79        exec_state,
80    )?;
81
82    let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
83    let to_raw = args.get_kw_arg_opt(
84        "to",
85        &RuntimeType::Union(vec![
86            RuntimeType::point3d(),
87            RuntimeType::Primitive(PrimitiveType::Axis3d),
88            RuntimeType::Primitive(PrimitiveType::Edge),
89            RuntimeType::plane(),
90            RuntimeType::Primitive(PrimitiveType::Face),
91            RuntimeType::sketch(),
92            RuntimeType::Primitive(PrimitiveType::Solid),
93            RuntimeType::tagged_edge(),
94            RuntimeType::tagged_face(),
95            RuntimeType::Primitive(PrimitiveType::Any),
96        ]),
97        exec_state,
98    )?;
99    let to = match to_raw {
100        None => None,
101        Some(v) => {
102            let inner = if let KclValue::Object { value: ref obj, .. } = v {
103                if edge::is_edge_specifier_object(&v) {
104                    Point3dAxis3dOrGeometryReference::EdgeToReference(edge::parse_edge_specifier_object(obj, &args)?)
105                } else {
106                    Point3dAxis3dOrGeometryReference::from_kcl_val(&v).ok_or_else(|| {
107                        KclError::new_type(KclErrorDetails::new(
108                            "Invalid value for `to`".to_owned(),
109                            vec![args.source_range],
110                        ))
111                    })?
112                }
113            } else {
114                Point3dAxis3dOrGeometryReference::from_kcl_val(&v).ok_or_else(|| {
115                    KclError::new_type(KclErrorDetails::new(
116                        "Invalid value for `to`".to_owned(),
117                        vec![args.source_range],
118                    ))
119                })?
120            };
121            Some(inner)
122        }
123    };
124    let symmetric = args.get_kw_arg_opt("symmetric", &RuntimeType::bool(), exec_state)?;
125    let bidirectional_length: Option<TyF64> =
126        args.get_kw_arg_opt("bidirectionalLength", &RuntimeType::length(), exec_state)?;
127    let direction = args.get_kw_arg_opt(
128        "direction",
129        &RuntimeType::Union(vec![
130            RuntimeType::point3d(),
131            RuntimeType::Primitive(PrimitiveType::Edge),
132            RuntimeType::tagged_edge(),
133            RuntimeType::segment(),
134        ]),
135        exec_state,
136    )?;
137    let tag_start = args.get_kw_arg_opt("tagStart", &RuntimeType::tag_decl(), exec_state)?;
138    let tag_end = args.get_kw_arg_opt("tagEnd", &RuntimeType::tag_decl(), exec_state)?;
139    let draft_angle: Option<TyF64> = args.get_kw_arg_opt("draftAngle", &RuntimeType::degrees(), exec_state)?;
140    let twist_angle: Option<TyF64> = args.get_kw_arg_opt("twistAngle", &RuntimeType::degrees(), exec_state)?;
141    let twist_angle_step: Option<TyF64> = args.get_kw_arg_opt("twistAngleStep", &RuntimeType::degrees(), exec_state)?;
142    let twist_center: Option<[TyF64; 2]> = args.get_kw_arg_opt("twistCenter", &RuntimeType::point2d(), exec_state)?;
143    let tolerance: Option<TyF64> = args.get_kw_arg_opt("tolerance", &RuntimeType::length(), exec_state)?;
144    let method: Option<String> = args.get_kw_arg_opt("method", &RuntimeType::string(), exec_state)?;
145    let hide_seams: Option<bool> = args.get_kw_arg_opt("hideSeams", &RuntimeType::bool(), exec_state)?;
146    let body_type: Option<BodyType> = args.get_kw_arg_opt("bodyType", &RuntimeType::string(), exec_state)?;
147    let sketches = coerce_extrude_targets(
148        sketch_values,
149        body_type.unwrap_or_default(),
150        tag_start.as_ref(),
151        tag_end.as_ref(),
152        exec_state,
153        &args.ctx,
154        args.source_range,
155    )
156    .await?;
157
158    let result = inner_extrude(
159        sketches,
160        length,
161        to,
162        symmetric,
163        direction,
164        bidirectional_length,
165        tag_start,
166        tag_end,
167        draft_angle,
168        twist_angle,
169        twist_angle_step,
170        twist_center,
171        tolerance,
172        method,
173        hide_seams,
174        body_type,
175        exec_state,
176        args,
177    )
178    .await?;
179
180    Ok(result.into())
181}
182
183pub async fn coerce_extrude_targets(
184    sketch_values: Vec<KclValue>,
185    body_type: BodyType,
186    tag_start: Option<&TagNode>,
187    tag_end: Option<&TagNode>,
188    exec_state: &mut ExecState,
189    ctx: &ExecutorContext,
190    source_range: crate::SourceRange,
191) -> Result<Vec<Extrudable>, KclError> {
192    let mut extrudables = Vec::new();
193    let mut segments = Vec::new();
194
195    for value in sketch_values {
196        if let Some(segment) = value.clone().into_segment() {
197            segments.push(segment);
198            continue;
199        }
200
201        let Some(extrudable) = Extrudable::from_kcl_val(&value) else {
202            return Err(KclError::new_type(KclErrorDetails::new(
203                "Expected sketches, faces, tagged faces, or solved sketch segments for extrusion.".to_owned(),
204                vec![source_range],
205            )));
206        };
207        extrudables.push(extrudable);
208    }
209
210    if !segments.is_empty() && !extrudables.is_empty() {
211        return Err(KclError::new_semantic(KclErrorDetails::new(
212            "Cannot extrude sketch segments together with sketches or faces in the same call. Use separate `extrude()` calls.".to_owned(),
213            vec![source_range],
214        )));
215    }
216
217    if !segments.is_empty() {
218        if !matches!(body_type, BodyType::Surface) {
219            let kind_of_extrude = match body_type {
220                BodyType::Solid => "solid extrude",
221                BodyType::Surface => "surface extrude",
222                _ => "non-surface extrude",
223            };
224            return Err(KclError::new_semantic(KclErrorDetails::new(
225                format!(
226                    "You're trying to perform a {kind_of_extrude} on an edge, but edges can only be extruded with surface extrudes. To do a solid extrude, select a closed sketch region instead. To extrude these edges, do a surface extrude by using `bodyType = SURFACE` instead."
227                ),
228                vec![source_range],
229            )));
230        }
231
232        if tag_start.is_some() || tag_end.is_some() {
233            return Err(KclError::new_semantic(KclErrorDetails::new(
234                "`tagStart` and `tagEnd` are not supported when extruding sketch segments. Segment surface extrudes do not create start or end caps."
235                    .to_owned(),
236                vec![source_range],
237            )));
238        }
239
240        let synthetic_sketch = build_segment_surface_sketch(segments, exec_state, ctx, source_range).await?;
241        return Ok(vec![Extrudable::from(synthetic_sketch)]);
242    }
243
244    // Edges behave like sketch segments: they can only be surface-extruded, they
245    // don't create caps, and they can't be mixed with sketches or faces. Enforce
246    // the same rules so these cases fail loudly instead of silently producing a
247    // surface (or silently ignoring `tagStart`/`tagEnd`).
248    let has_edge = extrudables
249        .iter()
250        .any(|e| matches!(e, Extrudable::Edge(_) | Extrudable::EdgeTag(_)));
251    if has_edge {
252        let has_non_edge = extrudables
253            .iter()
254            .any(|e| !matches!(e, Extrudable::Edge(_) | Extrudable::EdgeTag(_)));
255        if has_non_edge {
256            return Err(KclError::new_semantic(KclErrorDetails::new(
257                "Cannot extrude edges together with sketches or faces in the same call. Use separate `extrude()` calls.".to_owned(),
258                vec![source_range],
259            )));
260        }
261
262        if !matches!(body_type, BodyType::Surface) {
263            let kind_of_extrude = match body_type {
264                BodyType::Solid => "solid extrude",
265                BodyType::Surface => "surface extrude",
266                _ => "non-surface extrude",
267            };
268            return Err(KclError::new_semantic(KclErrorDetails::new(
269                format!(
270                    "You're trying to perform a {kind_of_extrude} on an edge, but edges can only be extruded with surface extrudes. To do a solid extrude, select a closed sketch region instead. To extrude these edges, do a surface extrude by using `bodyType = SURFACE` instead."
271                ),
272                vec![source_range],
273            )));
274        }
275
276        if tag_start.is_some() || tag_end.is_some() {
277            return Err(KclError::new_semantic(KclErrorDetails::new(
278                "`tagStart` and `tagEnd` are not supported when extruding edges. Edge surface extrudes do not create start or end caps."
279                    .to_owned(),
280                vec![source_range],
281            )));
282        }
283    }
284
285    Ok(extrudables)
286}
287
288pub(crate) async fn build_segment_surface_sketch(
289    mut segments: Vec<Segment>,
290    exec_state: &mut ExecState,
291    ctx: &ExecutorContext,
292    source_range: crate::SourceRange,
293) -> Result<Sketch, KclError> {
294    let Some(first_segment) = segments.first() else {
295        return Err(KclError::new_semantic(KclErrorDetails::new(
296            "Expected at least one sketch segment.".to_owned(),
297            vec![source_range],
298        )));
299    };
300
301    let sketch_id = first_segment.sketch_id;
302    let sketch_surface = first_segment.surface.clone();
303    for segment in &segments {
304        if segment.sketch_id != sketch_id {
305            return Err(KclError::new_semantic(KclErrorDetails::new(
306                "All sketch segments passed to this operation must come from the same sketch.".to_owned(),
307                vec![source_range],
308            )));
309        }
310
311        if segment.surface != sketch_surface {
312            return Err(KclError::new_semantic(KclErrorDetails::new(
313                "All sketch segments passed to this operation must lie on the same sketch surface.".to_owned(),
314                vec![source_range],
315            )));
316        }
317
318        if matches!(segment.kind, SegmentKind::Point { .. }) {
319            return Err(KclError::new_semantic(KclErrorDetails::new(
320                "Point segments cannot be used here. Select line, arc, or circle segments instead.".to_owned(),
321                vec![source_range],
322            )));
323        }
324
325        if segment.is_construction() {
326            return Err(KclError::new_semantic(KclErrorDetails::new(
327                "Construction segments cannot be used here. Select non-construction sketch segments instead."
328                    .to_owned(),
329                vec![source_range],
330            )));
331        }
332    }
333
334    let synthetic_sketch_id = exec_state.next_uuid();
335    let segment_tags = IndexMap::from_iter(segments.iter().filter_map(|segment| {
336        segment
337            .tag
338            .as_ref()
339            .map(|tag| (segment.object_id, TagDeclarator::new(&tag.value)))
340    }));
341
342    for segment in &mut segments {
343        segment.id = exec_state.next_uuid();
344        segment.sketch_id = synthetic_sketch_id;
345        segment.sketch = None;
346    }
347
348    create_segments_in_engine(
349        &sketch_surface,
350        synthetic_sketch_id,
351        &mut segments,
352        &segment_tags,
353        ctx,
354        exec_state,
355        source_range,
356    )
357    .await?
358    .ok_or_else(|| {
359        KclError::new_semantic(KclErrorDetails::new(
360            "Expected at least one usable sketch segment.".to_owned(),
361            vec![source_range],
362        ))
363    })
364}
365
366#[allow(clippy::too_many_arguments)]
367async fn inner_extrude(
368    extrudables: Vec<Extrudable>,
369    length: Option<TyF64>,
370    to: Option<Point3dAxis3dOrGeometryReference>,
371    symmetric: Option<bool>,
372    direction: Option<Point3dOrEdgeReference>,
373    bidirectional_length: Option<TyF64>,
374    tag_start: Option<TagNode>,
375    tag_end: Option<TagNode>,
376    draft_angle: Option<TyF64>,
377    twist_angle: Option<TyF64>,
378    twist_angle_step: Option<TyF64>,
379    twist_center: Option<[TyF64; 2]>,
380    tolerance: Option<TyF64>,
381    method: Option<String>,
382    hide_seams: Option<bool>,
383    body_type: Option<BodyType>,
384    exec_state: &mut ExecState,
385    args: Args,
386) -> Result<Vec<Solid>, KclError> {
387    let body_type = body_type.unwrap_or_default();
388
389    if matches!(body_type, BodyType::Solid) && extrudables.iter().any(|sk| matches!(sk.is_closed(), ProfileClosed::No))
390    {
391        return Err(KclError::new_semantic(KclErrorDetails::new(
392            "Cannot solid extrude an open profile. Either close the profile, or use a surface extrude.".to_owned(),
393            vec![args.source_range],
394        )));
395    }
396
397    if draft_angle.is_some() && twist_angle.is_some() {
398        return Err(KclError::new_semantic(KclErrorDetails::new(
399            "Zoo currently does not support adding both draft angle and twist angle to an extrude simultaneously"
400                .to_owned(),
401            vec![args.source_range],
402        )));
403    }
404
405    if direction.is_some() && twist_angle.is_some() {
406        return Err(KclError::new_semantic(KclErrorDetails::new(
407            "Zoo currently does not support adding both direction and twist angle to an extrude simultaneously"
408                .to_owned(),
409            vec![args.source_range],
410        )));
411    }
412
413    // Extrude the element(s).
414    let mut solids = Vec::new();
415    let tolerance = LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE_MM));
416
417    let extrude_method = match method.as_deref() {
418        Some("new" | "NEW") => ExtrudeMethod::New,
419        Some("merge" | "MERGE") => ExtrudeMethod::Merge,
420        None => ExtrudeMethod::default(),
421        Some(other) => {
422            return Err(KclError::new_semantic(KclErrorDetails::new(
423                format!("Unknown merge method {other}, try using `MERGE` or `NEW`"),
424                vec![args.source_range],
425            )));
426        }
427    };
428
429    if symmetric.unwrap_or(false) && bidirectional_length.is_some() {
430        return Err(KclError::new_semantic(KclErrorDetails::new(
431            "You cannot give both `symmetric` and `bidirectional` params, you have to choose one or the other"
432                .to_owned(),
433            vec![args.source_range],
434        )));
435    }
436
437    if let Some(bidirectional_length) = &bidirectional_length
438        && bidirectional_length.to_mm() < 0.0
439    {
440        return Err(KclError::new_semantic(KclErrorDetails::new(
441            "`bidirectionalLength` must be greater than or equal to 0".to_owned(),
442            vec![args.source_range],
443        )));
444    }
445
446    if (length.is_some() || twist_angle.is_some()) && to.is_some() {
447        return Err(KclError::new_semantic(KclErrorDetails::new(
448            "You cannot give `length` or `twist` params with the `to` param, you have to choose one or the other"
449                .to_owned(),
450            vec![args.source_range],
451        )));
452    }
453
454    let bidirection = bidirectional_length.map(|l| LengthUnit(l.to_mm()));
455
456    let opposite = match (symmetric, bidirection) {
457        (Some(true), _) => Opposite::Symmetric,
458        (None, None) => Opposite::None,
459        (Some(false), None) => Opposite::None,
460        (None, Some(length)) => Opposite::Other(length),
461        (Some(false), Some(length)) => Opposite::Other(length),
462    };
463
464    for extrudable in &extrudables {
465        let is_edge = match extrudable {
466            Extrudable::Sketch(..) => false,
467            Extrudable::FaceTag(_) => false,
468            Extrudable::Face(_) => false,
469            Extrudable::EdgeTag(_) => true,
470            Extrudable::Edge(_) => true,
471        };
472        let extrude_cmd_id = exec_state.next_uuid();
473        let sketch_or_face_id = extrudable.id_to_extrude(exec_state, &args, false).await?;
474        let cmd = match (
475            &twist_angle,
476            &twist_angle_step,
477            &twist_center,
478            length.clone(),
479            &to,
480            &direction,
481        ) {
482            (Some(angle), angle_step, center, Some(length), None, None) => {
483                let center = center.clone().map(point_to_mm).map(Point2d::from).unwrap_or_default();
484                let total_rotation_angle = Angle::from_degrees(angle.to_degrees(exec_state, args.source_range));
485                let angle_step_size = Angle::from_degrees(
486                    angle_step
487                        .clone()
488                        .map(|a| a.to_degrees(exec_state, args.source_range))
489                        .unwrap_or(15.0),
490                );
491                ModelingCmd::from(
492                    mcmd::TwistExtrude::builder()
493                        .target(sketch_or_face_id.into())
494                        .distance(LengthUnit(length.to_mm()))
495                        .center_2d(center)
496                        .total_rotation_angle(total_rotation_angle)
497                        .angle_step_size(angle_step_size)
498                        .tolerance(tolerance)
499                        .body_type(body_type)
500                        .build(),
501                )
502            }
503            (None, None, None, Some(length), None, None) => ModelingCmd::from(
504                mcmd::Extrude::builder()
505                    .target(sketch_or_face_id.into())
506                    .distance(LengthUnit(length.to_mm()))
507                    .opposite(opposite.clone())
508                    .maybe_draft_angle(
509                        draft_angle
510                            .clone()
511                            .map(|a| Angle::from_degrees(a.to_degrees(exec_state, args.source_range))),
512                    )
513                    .extrude_method(extrude_method)
514                    .body_type(body_type)
515                    .maybe_merge_coplanar_faces(hide_seams)
516                    .build(),
517            ),
518            (None, None, None, Some(length), None, Some(dir)) => {
519                let direction3d = match dir {
520                    Point3dOrEdgeReference::Point(p) => DirectionType::Axis {
521                        direction: KPoint3d {
522                            x: p[0].n,
523                            y: p[1].n,
524                            z: p[2].n,
525                        },
526                    },
527                    Point3dOrEdgeReference::Edge(edge) => match edge {
528                        crate::std::fillet::EdgeReference::Uuid(uuid) => DirectionType::Edge { id: *uuid },
529                        crate::std::fillet::EdgeReference::Tag(tag) => DirectionType::Edge {
530                            id: match tag.get_cur_info() {
531                                Some(info) => info.id,
532                                None => {
533                                    return Err(KclError::new_semantic(KclErrorDetails::new(
534                                        "Failed to get current info for tag".to_string(),
535                                        vec![args.source_range],
536                                    )));
537                                }
538                            },
539                        },
540                    },
541                };
542                ModelingCmd::from(
543                    mcmd::Extrude::builder()
544                        .target(sketch_or_face_id.into())
545                        .distance(LengthUnit(length.to_mm()))
546                        .opposite(opposite.clone())
547                        .maybe_draft_angle(
548                            draft_angle
549                                .clone()
550                                .map(|a| Angle::from_degrees(a.to_degrees(exec_state, args.source_range))),
551                        )
552                        .extrude_method(extrude_method)
553                        .body_type(body_type)
554                        .maybe_merge_coplanar_faces(hide_seams)
555                        .direction(direction3d)
556                        .build(),
557                )
558            }
559            (None, None, None, None, Some(to), None) => match to {
560                Point3dAxis3dOrGeometryReference::Point(point) => ModelingCmd::from(
561                    mcmd::ExtrudeToReference::builder()
562                        .target(sketch_or_face_id.into())
563                        .reference(ExtrudeReference::Point {
564                            point: KPoint3d {
565                                x: LengthUnit(point[0].to_mm()),
566                                y: LengthUnit(point[1].to_mm()),
567                                z: LengthUnit(point[2].to_mm()),
568                            },
569                        })
570                        .extrude_method(extrude_method)
571                        .body_type(body_type)
572                        .build(),
573                ),
574                Point3dAxis3dOrGeometryReference::Axis { direction, origin } => ModelingCmd::from(
575                    mcmd::ExtrudeToReference::builder()
576                        .target(sketch_or_face_id.into())
577                        .reference(ExtrudeReference::Axis {
578                            axis: KPoint3d {
579                                x: direction[0].to_mm(),
580                                y: direction[1].to_mm(),
581                                z: direction[2].to_mm(),
582                            },
583                            point: KPoint3d {
584                                x: LengthUnit(origin[0].to_mm()),
585                                y: LengthUnit(origin[1].to_mm()),
586                                z: LengthUnit(origin[2].to_mm()),
587                            },
588                        })
589                        .extrude_method(extrude_method)
590                        .body_type(body_type)
591                        .build(),
592                ),
593                Point3dAxis3dOrGeometryReference::Plane(plane) => {
594                    let plane_id = if plane.is_uninitialized() {
595                        if plane.info.origin.units.is_none() {
596                            return Err(KclError::new_semantic(KclErrorDetails::new(
597                                "Origin of plane has unknown units".to_string(),
598                                vec![args.source_range],
599                            )));
600                        }
601                        let sketch_plane = crate::std::sketch::make_sketch_plane_from_orientation(
602                            plane.clone().info.into_plane_data(),
603                            exec_state,
604                            &args,
605                        )
606                        .await?;
607                        sketch_plane.id
608                    } else {
609                        plane.id
610                    };
611                    ModelingCmd::from(
612                        mcmd::ExtrudeToReference::builder()
613                            .target(sketch_or_face_id.into())
614                            .reference(ExtrudeReference::EntityReference {
615                                entity_id: Some(plane_id),
616                                entity_reference: None,
617                            })
618                            .extrude_method(extrude_method)
619                            .body_type(body_type)
620                            .build(),
621                    )
622                }
623                Point3dAxis3dOrGeometryReference::Edge(edge_ref) => {
624                    let edge_id = edge_ref.get_engine_id(exec_state, &args)?;
625                    ModelingCmd::from(
626                        mcmd::ExtrudeToReference::builder()
627                            .target(sketch_or_face_id.into())
628                            .reference(ExtrudeReference::EntityReference {
629                                entity_id: Some(edge_id),
630                                entity_reference: None,
631                            })
632                            .extrude_method(extrude_method)
633                            .body_type(body_type)
634                            .build(),
635                    )
636                }
637                Point3dAxis3dOrGeometryReference::Face(face_tag) => {
638                    let face_id = face_tag.get_face_id_from_tag(exec_state, &args, false).await?;
639                    ModelingCmd::from(
640                        mcmd::ExtrudeToReference::builder()
641                            .target(sketch_or_face_id.into())
642                            .reference(ExtrudeReference::EntityReference {
643                                entity_id: Some(face_id),
644                                entity_reference: None,
645                            })
646                            .extrude_method(extrude_method)
647                            .body_type(body_type)
648                            .build(),
649                    )
650                }
651                Point3dAxis3dOrGeometryReference::Sketch(sketch_ref) => ModelingCmd::from(
652                    mcmd::ExtrudeToReference::builder()
653                        .target(sketch_or_face_id.into())
654                        .reference(ExtrudeReference::EntityReference {
655                            entity_id: Some(sketch_ref.id),
656                            entity_reference: None,
657                        })
658                        .extrude_method(extrude_method)
659                        .body_type(body_type)
660                        .build(),
661                ),
662                Point3dAxis3dOrGeometryReference::Solid(solid) => ModelingCmd::from(
663                    mcmd::ExtrudeToReference::builder()
664                        .target(sketch_or_face_id.into())
665                        .reference(ExtrudeReference::EntityReference {
666                            entity_id: Some(solid.id),
667                            entity_reference: None,
668                        })
669                        .extrude_method(extrude_method)
670                        .body_type(body_type)
671                        .build(),
672                ),
673                Point3dAxis3dOrGeometryReference::TaggedEdgeOrFace(tag) => {
674                    let tagged_edge_or_face = args.get_tag_engine_info(exec_state, tag)?;
675                    let tagged_edge_or_face_id = tagged_edge_or_face.id;
676                    ModelingCmd::from(
677                        mcmd::ExtrudeToReference::builder()
678                            .target(sketch_or_face_id.into())
679                            .reference(ExtrudeReference::EntityReference {
680                                entity_id: Some(tagged_edge_or_face_id),
681                                entity_reference: None,
682                            })
683                            .extrude_method(extrude_method)
684                            .body_type(body_type)
685                            .build(),
686                    )
687                }
688                Point3dAxis3dOrGeometryReference::EdgeToReference(spec) => {
689                    let inner = edge::resolve_edge_specifier_with_face_tags(spec, None, exec_state, &args).await?;
690                    ModelingCmd::from(
691                        mcmd::ExtrudeToReference::builder()
692                            .target(sketch_or_face_id.into())
693                            .reference(ExtrudeReference::EntityReference {
694                                entity_id: None,
695                                entity_reference: Some(EntityReference::Edge {
696                                    inner,
697                                    topology_fallback: None,
698                                }),
699                            })
700                            .extrude_method(extrude_method)
701                            .body_type(body_type)
702                            .build(),
703                    )
704                }
705            },
706            (Some(_), _, _, None, None, None) => {
707                return Err(KclError::new_semantic(KclErrorDetails::new(
708                    "The `length` parameter must be provided when using twist angle for extrusion.".to_owned(),
709                    vec![args.source_range],
710                )));
711            }
712            (_, _, _, None, None, None) => {
713                return Err(KclError::new_semantic(KclErrorDetails::new(
714                    "Either `length` or `to` parameter must be provided for extrusion.".to_owned(),
715                    vec![args.source_range],
716                )));
717            }
718            (_, _, _, Some(_), Some(_), None) => {
719                return Err(KclError::new_semantic(KclErrorDetails::new(
720                    "You cannot give both `length` and `to` params, you have to choose one or the other".to_owned(),
721                    vec![args.source_range],
722                )));
723            }
724            (_, _, _, _, _, _) => {
725                return Err(KclError::new_semantic(KclErrorDetails::new(
726                    "Invalid combination of parameters for extrusion.".to_owned(),
727                    vec![args.source_range],
728                )));
729            }
730        };
731
732        let being_extruded = match extrudable {
733            Extrudable::Sketch(..) => BeingExtruded::Sketch,
734            Extrudable::FaceTag(face_tag) => {
735                let face_id = sketch_or_face_id;
736                let solid_id = match face_tag.geometry() {
737                    Some(crate::execution::Geometry::Solid(solid)) => solid.id,
738                    Some(crate::execution::Geometry::Sketch(sketch)) => match sketch.on {
739                        SketchSurface::Face(face) => face.parent_solid.solid_id,
740                        SketchSurface::Plane(_) => sketch.id,
741                    },
742                    None => face_id,
743                };
744                BeingExtruded::Face { face_id, solid_id }
745            }
746            Extrudable::Face(face) => BeingExtruded::Face {
747                face_id: face.id,
748                solid_id: face.parent_solid.solid_id,
749            },
750            Extrudable::EdgeTag(_) => BeingExtruded::Edge,
751            Extrudable::Edge(_) => BeingExtruded::Edge,
752        };
753        if let Some(post_extr_sketch) = extrudable.as_sketch() {
754            let cmds = post_extr_sketch.build_sketch_mode_cmds(
755                exec_state,
756                ModelingCmdReq {
757                    cmd_id: extrude_cmd_id.into(),
758                    cmd,
759                },
760            );
761            exec_state
762                .batch_modeling_cmds(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), &cmds)
763                .await?;
764            solids.push(
765                do_post_extrude(
766                    &post_extr_sketch,
767                    extrude_cmd_id.into(),
768                    false,
769                    &NamedCapTags {
770                        start: tag_start.as_ref(),
771                        end: tag_end.as_ref(),
772                    },
773                    extrude_method,
774                    exec_state,
775                    &args,
776                    None,
777                    None,
778                    body_type,
779                    being_extruded,
780                )
781                .await?,
782            );
783        } else if is_edge {
784            // Ensure that edges do not use the MERGE method.
785            match extrude_method {
786                ExtrudeMethod::New => {
787                    // This is expected.
788                }
789                ExtrudeMethod::Merge => {
790                    return Err(KclError::new_semantic(KclErrorDetails::new(
791                        "Cannot use method MERGE with surface extrude of an edge".to_owned(),
792                        vec![args.source_range],
793                    )));
794                }
795                _ => {
796                    return Err(KclError::new_internal(KclErrorDetails::new(
797                        format!("Unknown extrude method: {extrude_method:?}"),
798                        vec![args.source_range],
799                    )));
800                }
801            }
802
803            // Surface-extrude an edge.
804            exec_state
805                .batch_modeling_cmd(ModelingCmdMeta::from_args_id(exec_state, &args, extrude_cmd_id), cmd)
806                .await?;
807            // Extract the edge tag.
808            let edge_tag = match extrudable {
809                Extrudable::Sketch(_) => None,
810                Extrudable::FaceTag(_) => None,
811                Extrudable::Face(_) => None,
812                Extrudable::EdgeTag(tag) => Some(TagDeclarator::new(&tag.value)),
813                Extrudable::Edge(_) => None,
814            };
815            solids.push(after_surface_creation(extrude_cmd_id.into(), edge_tag, exec_state, &args).await?);
816        } else {
817            return Err(KclError::new_type(KclErrorDetails::new(
818                "Expected a sketch for extrusion".to_owned(),
819                vec![args.source_range],
820            )));
821        }
822    }
823
824    Ok(solids)
825}
826
827#[derive(Debug, Default)]
828pub(crate) struct NamedCapTags<'a> {
829    pub start: Option<&'a TagNode>,
830    pub end: Option<&'a TagNode>,
831}
832
833#[derive(Debug, Clone, Copy)]
834pub enum BeingExtruded {
835    Sketch,
836    Face { face_id: Uuid, solid_id: Uuid },
837    Edge,
838}
839
840/// Which edge should we use for querying Solid3dGetExtrusionInfo and GetAdjacencyInfo?
841/// It can be any edge of the body, but if our body is a clone, we should use an edge of
842/// the original body, not the new cloned body.
843fn get_extrusion_info_edge_id(sketch: &Sketch, any_edge_id: Uuid, clone_id_map: Option<&HashMap<Uuid, Uuid>>) -> Uuid {
844    // If this isn't a clone, there's no old/new body distinction.
845    // So just use the edge.
846    if sketch.clone.is_none() {
847        return any_edge_id;
848    }
849    let Some(clone_map) = clone_id_map else {
850        return any_edge_id;
851    };
852
853    // clone_map maps old IDs -> new IDs.
854    // If the `any_edge_id` is an ID of the OLD body
855    // (we know this if it's a _key_ of the map)
856    // we should use it (because that's the old body we're querying).
857    if clone_map.contains_key(&any_edge_id) {
858        return any_edge_id;
859    }
860
861    // Otherwise, if the `any_edge_id` is an ID of the NEW body
862    // (we know this if it's a _value_ of the map),
863    // we should query the corresponding ID in the OLD body.
864    // i.e. if it's a hashmap value, find the corresponding key.
865    if let Some((old_edge_id, _)) = clone_map.iter().find(|(_, new_edge_id)| **new_edge_id == any_edge_id) {
866        return *old_edge_id;
867    }
868
869    // Fall back to this if the clone_map doesn't have the data we expect.
870    // Probably will fail in the engine because it means the clone map was built wrong,
871    // or KCL and the engine disagree about what geometry exists.
872    any_edge_id
873}
874
875/// This is similar to [`do_post_extrude()`], but for surfaces where a sketch
876/// isn't available.
877pub(crate) async fn after_surface_creation(
878    extrude_cmd_id: ArtifactId,
879    edge_tag: Option<crate::parsing::ast::types::Node<TagDeclarator>>,
880    exec_state: &mut ExecState,
881    args: &Args,
882) -> Result<Solid, KclError> {
883    let body_id = extrude_cmd_id.into();
884
885    // Bring the object to the front of the scene.
886    // See: https://github.com/KittyCAD/modeling-app/issues/806
887
888    exec_state
889        .batch_modeling_cmd(
890            ModelingCmdMeta::from_args(exec_state, args),
891            ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(body_id).build()),
892        )
893        .await?;
894
895    let (face_id, edge_id) = if args.ctx.no_engine_commands().await {
896        (exec_state.next_uuid(), exec_state.next_uuid())
897    } else {
898        // Get the body entity ids.
899        let response = exec_state
900            .send_modeling_cmd(
901                ModelingCmdMeta::from_args(exec_state, args),
902                ModelingCmd::from(mcmd::EntityGetAllChildUuids::builder().entity_id(body_id).build()),
903            )
904            .await?;
905        let OkWebSocketResponseData::Modeling {
906            modeling_response: OkModelingCmdResponse::EntityGetAllChildUuids(ref all_child_ids_resp),
907        } = response
908        else {
909            return Err(KclError::new_engine(KclErrorDetails::new(
910                format!("EntityGetAllChildUuids response was not as expected: {response:?}"),
911                vec![args.source_range],
912            )));
913        };
914        let entity_ids = &all_child_ids_resp.entity_ids;
915        let Some(face_id) = entity_ids.first().copied() else {
916            return Err(KclError::new_internal(KclErrorDetails::new(
917                format!("Expected EntityGetAllChildUuids response to have at least 1 ID: {response:?}",),
918                vec![args.source_range],
919            )));
920        };
921        let Some(edge_id) = entity_ids.get(1).copied() else {
922            return Err(KclError::new_internal(KclErrorDetails::new(
923                format!("Expected EntityGetAllChildUuids response to have at least 2 IDs: {response:?}"),
924                vec![args.source_range],
925            )));
926        };
927        (face_id, edge_id)
928    };
929
930    // TODO: Do we need to use ExtrudeArc?
931    let extrude_surface = ExtrudeSurface::ExtrudePlane(ExtrudePlane {
932        face_id,
933        tag: edge_tag,
934        geo_meta: GeoMeta {
935            id: face_id,
936            metadata: args.source_range.into(),
937        },
938    });
939    let new_value = vec![extrude_surface];
940
941    Ok(Solid {
942        id: body_id,
943        value_id: body_id,
944        artifact_id: extrude_cmd_id,
945        value: new_value,
946        faces: Default::default(),
947        meta: vec![args.source_range.into()],
948        // Normally, we would propagate the units of the sketch. But an edge
949        // doesn't have units. We also don't seem to use this field anywhere.
950        units: kcl_api::UnitLength::Millimeters,
951        sectional: false,
952        creator: SolidCreator::Edge(CreatorEdge { edge_id, body_id }),
953        start_cap_id: None,
954        end_cap_id: None,
955        edge_cuts: Vec::new(),
956        pending_edge_cut_ids: Vec::new(),
957    })
958}
959
960#[allow(clippy::too_many_arguments)]
961pub(crate) async fn do_post_extrude<'a>(
962    sketch: &Sketch,
963    extrude_cmd_id: ArtifactId,
964    sectional: bool,
965    named_cap_tags: &'a NamedCapTags<'a>,
966    extrude_method: ExtrudeMethod,
967    exec_state: &mut ExecState,
968    args: &Args,
969    edge_id: Option<Uuid>,
970    clone_id_map: Option<&HashMap<Uuid, Uuid>>, // old sketch id -> new sketch id
971    body_type: BodyType,
972    being_extruded: BeingExtruded,
973) -> Result<Solid, KclError> {
974    // Bring the object to the front of the scene.
975    // See: https://github.com/KittyCAD/modeling-app/issues/806
976
977    exec_state
978        .batch_modeling_cmd(
979            ModelingCmdMeta::from_args(exec_state, args),
980            ModelingCmd::from(mcmd::ObjectBringToFront::builder().object_id(sketch.id).build()),
981        )
982        .await?;
983
984    let any_edge_id = if let Some(edge_id) = sketch.mirror {
985        edge_id
986    } else if let Some(id) = edge_id {
987        id
988    } else {
989        // The "get extrusion face info" API call requires *any* edge on the sketch being extruded.
990        // So, let's just use the first one.
991        let Some(any_edge_id) = sketch.paths.first().map(|edge| edge.get_base().geo_meta.id) else {
992            return Err(KclError::new_type(KclErrorDetails::new(
993                "Expected a non-empty sketch".to_owned(),
994                vec![args.source_range],
995            )));
996        };
997        any_edge_id
998    };
999
1000    // If the sketch is a clone, we will use the original info to get the extrusion face info.
1001    // So let's find an edge of the old body.
1002    let extrusion_info_edge_id = get_extrusion_info_edge_id(sketch, any_edge_id, clone_id_map);
1003    let mut sketch = sketch.clone();
1004    match body_type {
1005        BodyType::Solid => {
1006            sketch.is_closed = ProfileClosed::Explicitly;
1007        }
1008        BodyType::Surface => {}
1009        _other => {
1010            // At some point in the future we'll add sheet metal or something.
1011            // Figure this out then.
1012        }
1013    }
1014
1015    match (extrude_method, being_extruded) {
1016        (ExtrudeMethod::Merge, BeingExtruded::Face { .. }) => {
1017            // Merge the IDs.
1018            // If we were sketching on a face, we need the original face id.
1019            if let SketchSurface::Face(ref face) = sketch.on {
1020                // If we're merging into an existing body, then assign the existing body's ID,
1021                // because the variable binding for this solid won't be its own object, it's just modifying the original one.
1022                sketch.id = face.parent_solid.sketch_or_solid_id();
1023            }
1024        }
1025        (ExtrudeMethod::New, BeingExtruded::Face { .. }) => {
1026            // We're creating a new solid, it's not based on any existing sketch (it's based on a face).
1027            // So we need a new ID, the extrude command ID.
1028            sketch.id = extrude_cmd_id.into();
1029        }
1030        (ExtrudeMethod::New, BeingExtruded::Sketch) => {
1031            // If we are creating a new body we need to preserve its new id.
1032            // The sketch's ID is already correct here, it should be the ID of the sketch.
1033        }
1034        (ExtrudeMethod::Merge, BeingExtruded::Sketch) => {
1035            if let SketchSurface::Face(ref face) = sketch.on {
1036                // If we're merging into an existing body, then assign the existing body's ID,
1037                // because the variable binding for this solid won't be its own object, it's just modifying the original one.
1038                sketch.id = face.parent_solid.sketch_or_solid_id();
1039            }
1040        }
1041        (other, _) => {
1042            // If you ever hit this, you should add a new arm to the match expression, and implement support for the new ExtrudeMethod variant.
1043            return Err(KclError::new_internal(KclErrorDetails::new(
1044                format!("Zoo does not yet support creating bodies via {other:?}"),
1045                vec![args.source_range],
1046            )));
1047        }
1048    }
1049
1050    // Similarly, if the sketch is a clone, we need to use the original sketch id to get the extrusion face info.
1051    let sketch_id = if let Some(cloned_from) = sketch.clone
1052        && clone_id_map.is_some()
1053    {
1054        cloned_from
1055    } else {
1056        sketch.id
1057    };
1058
1059    let solid3d_info = exec_state
1060        .send_modeling_cmd(
1061            ModelingCmdMeta::from_args(exec_state, args),
1062            ModelingCmd::from(
1063                mcmd::Solid3dGetExtrusionFaceInfo::builder()
1064                    .edge_id(extrusion_info_edge_id)
1065                    .object_id(sketch_id)
1066                    .build(),
1067            ),
1068        )
1069        .await?;
1070
1071    let face_infos = if let OkWebSocketResponseData::Modeling {
1072        modeling_response: OkModelingCmdResponse::Solid3dGetExtrusionFaceInfo(data),
1073    } = solid3d_info
1074    {
1075        data.faces
1076    } else {
1077        vec![]
1078    };
1079
1080    // Only do this if we need the artifact graph.
1081    if !args.ctx.settings.skip_artifact_graph {
1082        // Getting the ids of a sectional sweep does not work well and we cannot guarantee that
1083        // any of these call will not just fail.
1084        if !sectional {
1085            exec_state
1086                .batch_modeling_cmd(
1087                    ModelingCmdMeta::from_args(exec_state, args),
1088                    ModelingCmd::from(
1089                        mcmd::Solid3dGetAdjacencyInfo::builder()
1090                            .object_id(sketch_id)
1091                            .edge_id(extrusion_info_edge_id)
1092                            .build(),
1093                    ),
1094                )
1095                .await?;
1096        }
1097    }
1098
1099    let Faces {
1100        sides: mut face_id_map,
1101        mut start_cap_id,
1102        mut end_cap_id,
1103    } = analyze_faces(exec_state, args, face_infos).await;
1104
1105    // 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.
1106    if sketch.clone.is_some()
1107        && let Some(clone_id_map) = clone_id_map
1108    {
1109        face_id_map = face_id_map
1110            .into_iter()
1111            .filter_map(|(k, v)| {
1112                let fe_key = clone_id_map.get(&k)?;
1113                let fe_value = clone_id_map.get(&(v?)).copied();
1114                Some((*fe_key, fe_value))
1115            })
1116            .collect::<HashMap<Uuid, Option<Uuid>>>();
1117        // The face info above was queried using the original solid's id, so
1118        // the cap ids belong to the original. Map them to the clone's ids.
1119        start_cap_id = start_cap_id.and_then(|id| clone_id_map.get(&id).copied());
1120        end_cap_id = end_cap_id.and_then(|id| clone_id_map.get(&id).copied());
1121    }
1122
1123    // Iterate over the sketch.value array and add face_id to GeoMeta
1124    let no_engine_commands = args.ctx.no_engine_commands().await;
1125    let mut new_value: Vec<ExtrudeSurface> = Vec::with_capacity(sketch.paths.len() + sketch.inner_paths.len() + 2);
1126    let outer_surfaces = sketch.paths.iter().flat_map(|path| {
1127        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
1128            surface_of(path, *actual_face_id)
1129        } else if no_engine_commands {
1130            crate::log::logln!(
1131                "No face ID found for path ID {:?}, but in no-engine-commands mode, so faking it",
1132                path.get_base().geo_meta.id
1133            );
1134            // Only pre-populate the extrude surface if we are in mock mode.
1135            fake_extrude_surface(exec_state, path)
1136        } else if sketch.clone.is_some()
1137            && let Some(clone_map) = clone_id_map
1138        {
1139            let new_path = clone_map.get(&(path.get_base().geo_meta.id));
1140
1141            if let Some(new_path) = new_path {
1142                match face_id_map.get(new_path) {
1143                    Some(Some(actual_face_id)) => clone_surface_of(path, *new_path, *actual_face_id),
1144                    _ => {
1145                        let actual_face_id = face_id_map.iter().find_map(|(key, value)| {
1146                            if let Some(value) = value {
1147                                if value == new_path { Some(key) } else { None }
1148                            } else {
1149                                None
1150                            }
1151                        });
1152                        match actual_face_id {
1153                            Some(actual_face_id) => clone_surface_of(path, *new_path, *actual_face_id),
1154                            None => {
1155                                crate::log::logln!("No face ID found for clone path ID {:?}, so skipping it", new_path);
1156                                None
1157                            }
1158                        }
1159                    }
1160                }
1161            } else {
1162                None
1163            }
1164        } else {
1165            crate::log::logln!(
1166                "No face ID found for path ID {:?}, and not in no-engine-commands mode, so skipping it",
1167                path.get_base().geo_meta.id
1168            );
1169            None
1170        }
1171    });
1172
1173    new_value.extend(outer_surfaces);
1174    let inner_surfaces = sketch.inner_paths.iter().flat_map(|path| {
1175        if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
1176            surface_of(path, *actual_face_id)
1177        } else if no_engine_commands {
1178            // Only pre-populate the extrude surface if we are in mock mode.
1179            fake_extrude_surface(exec_state, path)
1180        } else {
1181            None
1182        }
1183    });
1184    new_value.extend(inner_surfaces);
1185
1186    // Add the tags for the start or end caps.
1187    if let Some(tag_start) = named_cap_tags.start {
1188        let Some(start_cap_id) = start_cap_id else {
1189            return Err(KclError::new_type(KclErrorDetails::new(
1190                format!(
1191                    "Expected a start cap ID for tag `{}` for extrusion of sketch {:?}",
1192                    tag_start.name, sketch.id
1193                ),
1194                vec![args.source_range],
1195            )));
1196        };
1197
1198        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1199            face_id: start_cap_id,
1200            tag: Some(tag_start.clone()),
1201            geo_meta: GeoMeta {
1202                id: start_cap_id,
1203                metadata: args.source_range.into(),
1204            },
1205        }));
1206    }
1207    if let Some(tag_end) = named_cap_tags.end {
1208        let Some(end_cap_id) = end_cap_id else {
1209            return Err(KclError::new_type(KclErrorDetails::new(
1210                format!(
1211                    "Expected an end cap ID for tag `{}` for extrusion of sketch {:?}",
1212                    tag_end.name, sketch.id
1213                ),
1214                vec![args.source_range],
1215            )));
1216        };
1217
1218        new_value.push(ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1219            face_id: end_cap_id,
1220            tag: Some(tag_end.clone()),
1221            geo_meta: GeoMeta {
1222                id: end_cap_id,
1223                metadata: args.source_range.into(),
1224            },
1225        }));
1226    }
1227
1228    let meta = sketch.meta.clone();
1229    let units = sketch.units;
1230    let id = sketch.id;
1231    let creator = match being_extruded {
1232        BeingExtruded::Sketch => SolidCreator::Sketch(sketch),
1233        BeingExtruded::Face { face_id, solid_id } => SolidCreator::Face(CreatorFace {
1234            face_id,
1235            solid_id,
1236            sketch,
1237        }),
1238        BeingExtruded::Edge => {
1239            let message = "Expected an edge to have been extruded via another code path";
1240            debug_assert!(false, "{message}");
1241            return Err(KclError::new_internal(KclErrorDetails::new(
1242                message.to_owned(),
1243                vec![args.source_range],
1244            )));
1245        }
1246    };
1247
1248    Ok(Solid {
1249        id,
1250        value_id: extrude_cmd_id.into(),
1251        artifact_id: extrude_cmd_id,
1252        value: new_value,
1253        faces: Default::default(),
1254        meta,
1255        units,
1256        sectional,
1257        creator,
1258        start_cap_id,
1259        end_cap_id,
1260        edge_cuts: vec![],
1261        pending_edge_cut_ids: vec![],
1262    })
1263}
1264
1265#[derive(Debug, Default)]
1266struct Faces {
1267    /// Maps curve ID to face ID for each side.
1268    sides: HashMap<Uuid, Option<Uuid>>,
1269    /// Top face ID.
1270    end_cap_id: Option<Uuid>,
1271    /// Bottom face ID.
1272    start_cap_id: Option<Uuid>,
1273}
1274
1275async fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
1276    let mut faces = Faces {
1277        sides: HashMap::with_capacity(face_infos.len()),
1278        ..Default::default()
1279    };
1280    if args.ctx.no_engine_commands().await {
1281        // Create fake IDs for start and end caps, to make extrudes mock-execute safe
1282        faces.start_cap_id = Some(exec_state.next_uuid());
1283        faces.end_cap_id = Some(exec_state.next_uuid());
1284    }
1285    for face_info in face_infos {
1286        match face_info.cap {
1287            ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
1288            ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
1289            ExtrusionFaceCapType::Both => {
1290                faces.end_cap_id = face_info.face_id;
1291                faces.start_cap_id = face_info.face_id;
1292            }
1293            ExtrusionFaceCapType::None => {
1294                if let Some(curve_id) = face_info.curve_id {
1295                    faces.sides.insert(curve_id, face_info.face_id);
1296                }
1297            }
1298            other => {
1299                exec_state.warn(
1300                    crate::CompilationIssue {
1301                        source_range: args.source_range,
1302                        message: format!("unknown extrusion face type {other:?}"),
1303                        suggestion: None,
1304                        severity: crate::errors::Severity::Warning,
1305                        tag: crate::errors::Tag::Unnecessary,
1306                    },
1307                    annotations::WARN_NOT_YET_SUPPORTED,
1308                );
1309            }
1310        }
1311    }
1312    faces
1313}
1314fn surface_of(path: &Path, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
1315    match path {
1316        Path::Arc { .. }
1317        | Path::TangentialArc { .. }
1318        | Path::TangentialArcTo { .. }
1319        // TODO: (bc) fix me
1320        | Path::Ellipse { .. }
1321        | Path::Conic {.. }
1322        | Path::Circle { .. }
1323        | Path::CircleThreePoint { .. } => {
1324            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1325                face_id: actual_face_id,
1326                tag: path.get_base().tag.clone(),
1327                geo_meta: GeoMeta {
1328                    id: path.get_base().geo_meta.id,
1329                    metadata: path.get_base().geo_meta.metadata,
1330                },
1331            });
1332            Some(extrude_surface)
1333        }
1334        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1335            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1336                face_id: actual_face_id,
1337                tag: path.get_base().tag.clone(),
1338                geo_meta: GeoMeta {
1339                    id: path.get_base().geo_meta.id,
1340                    metadata: path.get_base().geo_meta.metadata,
1341                },
1342            });
1343            Some(extrude_surface)
1344        }
1345        Path::ArcThreePoint { .. } => {
1346            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1347                face_id: actual_face_id,
1348                tag: path.get_base().tag.clone(),
1349                geo_meta: GeoMeta {
1350                    id: path.get_base().geo_meta.id,
1351                    metadata: path.get_base().geo_meta.metadata,
1352                },
1353            });
1354            Some(extrude_surface)
1355        }
1356    }
1357}
1358
1359fn clone_surface_of(path: &Path, clone_path_id: Uuid, actual_face_id: Uuid) -> Option<ExtrudeSurface> {
1360    match path {
1361        Path::Arc { .. }
1362        | Path::TangentialArc { .. }
1363        | Path::TangentialArcTo { .. }
1364        // TODO: (gserena) fix me
1365        | Path::Ellipse { .. }
1366        | Path::Conic {.. }
1367        | Path::Circle { .. }
1368        | Path::CircleThreePoint { .. } => {
1369            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1370                face_id: actual_face_id,
1371                tag: path.get_base().tag.clone(),
1372                geo_meta: GeoMeta {
1373                    id: clone_path_id,
1374                    metadata: path.get_base().geo_meta.metadata,
1375                },
1376            });
1377            Some(extrude_surface)
1378        }
1379        Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } | Path::Bezier { .. } => {
1380            let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1381                face_id: actual_face_id,
1382                tag: path.get_base().tag.clone(),
1383                geo_meta: GeoMeta {
1384                    id: clone_path_id,
1385                    metadata: path.get_base().geo_meta.metadata,
1386                },
1387            });
1388            Some(extrude_surface)
1389        }
1390        Path::ArcThreePoint { .. } => {
1391            let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
1392                face_id: actual_face_id,
1393                tag: path.get_base().tag.clone(),
1394                geo_meta: GeoMeta {
1395                    id: clone_path_id,
1396                    metadata: path.get_base().geo_meta.metadata,
1397                },
1398            });
1399            Some(extrude_surface)
1400        }
1401    }
1402}
1403
1404/// Create a fake extrude surface to report for mock execution, when there's no engine response.
1405fn fake_extrude_surface(exec_state: &mut ExecState, path: &Path) -> Option<ExtrudeSurface> {
1406    let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
1407        // pushing this values with a fake face_id to make extrudes mock-execute safe
1408        face_id: exec_state.next_uuid(),
1409        tag: path.get_base().tag.clone(),
1410        geo_meta: GeoMeta {
1411            id: path.get_base().geo_meta.id,
1412            metadata: path.get_base().geo_meta.metadata,
1413        },
1414    });
1415    Some(extrude_surface)
1416}
1417
1418#[cfg(test)]
1419mod tests {
1420    use kcl_api::UnitLength;
1421
1422    use super::*;
1423    use crate::execution::AbstractSegment;
1424    use crate::execution::Plane;
1425    use crate::execution::SegmentRepr;
1426    use crate::execution::parse_execute;
1427    use crate::execution::types::NumericType;
1428    use crate::execution::types::NumericTypeExt;
1429    use crate::front::Expr;
1430    use crate::front::Number;
1431    use crate::front::ObjectId;
1432    use crate::front::Point2d;
1433    use crate::front::PointCtor;
1434    use crate::std::sketch::PlaneData;
1435
1436    fn point_expr(x: f64, y: f64) -> Point2d<Expr> {
1437        Point2d {
1438            x: Expr::Var(Number::from((x, UnitLength::Millimeters))),
1439            y: Expr::Var(Number::from((y, UnitLength::Millimeters))),
1440        }
1441    }
1442
1443    fn segment_value(exec_state: &mut ExecState) -> KclValue {
1444        let plane = Plane::from_plane_data_skipping_engine(PlaneData::XY, exec_state).unwrap();
1445        let segment = Segment {
1446            id: exec_state.next_uuid(),
1447            object_id: ObjectId(1),
1448            kind: SegmentKind::Point {
1449                position: [TyF64::new(0.0, NumericType::mm()), TyF64::new(0.0, NumericType::mm())],
1450                ctor: Box::new(PointCtor {
1451                    position: point_expr(0.0, 0.0),
1452                }),
1453                freedom: None,
1454            },
1455            surface: SketchSurface::Plane(Box::new(plane)),
1456            sketch_id: exec_state.next_uuid(),
1457            sketch: None,
1458            tag: None,
1459            node_path: None,
1460            meta: vec![],
1461        };
1462        KclValue::Segment {
1463            value: Box::new(AbstractSegment {
1464                repr: SegmentRepr::Solved {
1465                    segment: Box::new(segment),
1466                },
1467                meta: vec![],
1468            }),
1469        }
1470    }
1471
1472    #[tokio::test(flavor = "multi_thread")]
1473    async fn extrude_rejects_negative_bidirectional_length_in_mock_exec() {
1474        let code = r#"
1475profile001 = startSketchOn(XY)
1476  |> startProfile(at = [0, 0])
1477  |> line(end = [1, 0])
1478  |> line(end = [0, 1])
1479  |> close()
1480
1481extrude(profile001, length = 1, bidirectionalLength = -1)
1482"#;
1483
1484        let err = parse_execute(code).await.unwrap_err();
1485
1486        assert!(matches!(err, KclError::Semantic { .. }), "{err:?}");
1487        assert!(
1488            err.message()
1489                .contains("`bidirectionalLength` must be greater than or equal to 0"),
1490            "{err:?}"
1491        );
1492    }
1493
1494    #[tokio::test(flavor = "multi_thread")]
1495    async fn edge_extrude_succeeds_in_mock_exec() {
1496        let code = r#"
1497@settings(kclVersion = 2.0)
1498
1499sketch001 = sketch(on = XZ) {
1500  line1 = line(start = [0mm, 0mm], end = [1mm, 0mm])
1501}
1502extrude001 = extrude(sketch001.line1, length = 5, bodyType = SURFACE)
1503extrude(
1504  getOppositeEdge(extrude001.sketch.tags.line1),
1505  length = 1,
1506  method = NEW,
1507  bodyType = SURFACE,
1508)
1509"#;
1510
1511        parse_execute(code).await.unwrap();
1512    }
1513
1514    #[tokio::test(flavor = "multi_thread")]
1515    async fn segment_extrude_rejects_cap_tags() {
1516        let ctx = ExecutorContext::new_mock(None).await;
1517        let mut exec_state = ExecState::new(&ctx);
1518        let err = coerce_extrude_targets(
1519            vec![segment_value(&mut exec_state)],
1520            BodyType::Surface,
1521            Some(&TagDeclarator::new("cap_start")),
1522            None,
1523            &mut exec_state,
1524            &ctx,
1525            crate::SourceRange::default(),
1526        )
1527        .await
1528        .unwrap_err();
1529
1530        assert!(
1531            err.message()
1532                .contains("`tagStart` and `tagEnd` are not supported when extruding sketch segments"),
1533            "{err:?}"
1534        );
1535        ctx.close().await;
1536    }
1537
1538    /// `getOppositeEdge()` and the other edge getters return a raw edge as
1539    /// `KclValue::Uuid`, which coerces to `Extrudable::Edge`.
1540    fn edge_value(exec_state: &mut ExecState) -> KclValue {
1541        KclValue::Uuid {
1542            value: exec_state.next_uuid(),
1543            meta: vec![],
1544        }
1545    }
1546
1547    #[tokio::test(flavor = "multi_thread")]
1548    async fn edge_extrude_rejects_solid_body_type() {
1549        let ctx = ExecutorContext::new_mock(None).await;
1550        let mut exec_state = ExecState::new(&ctx);
1551        let edge = edge_value(&mut exec_state);
1552        let err = coerce_extrude_targets(
1553            vec![edge],
1554            BodyType::Solid,
1555            None,
1556            None,
1557            &mut exec_state,
1558            &ctx,
1559            crate::SourceRange::default(),
1560        )
1561        .await
1562        .unwrap_err();
1563
1564        assert!(
1565            err.message()
1566                .contains("edges can only be extruded with surface extrudes"),
1567            "{err:?}"
1568        );
1569        ctx.close().await;
1570    }
1571
1572    #[tokio::test(flavor = "multi_thread")]
1573    async fn edge_extrude_rejects_cap_tags() {
1574        let ctx = ExecutorContext::new_mock(None).await;
1575        let mut exec_state = ExecState::new(&ctx);
1576        let edge = edge_value(&mut exec_state);
1577        let err = coerce_extrude_targets(
1578            vec![edge],
1579            BodyType::Surface,
1580            Some(&TagDeclarator::new("cap_start")),
1581            None,
1582            &mut exec_state,
1583            &ctx,
1584            crate::SourceRange::default(),
1585        )
1586        .await
1587        .unwrap_err();
1588
1589        assert!(
1590            err.message()
1591                .contains("`tagStart` and `tagEnd` are not supported when extruding edges"),
1592            "{err:?}"
1593        );
1594        ctx.close().await;
1595    }
1596
1597    #[tokio::test(flavor = "multi_thread")]
1598    async fn edge_extrude_rejects_mixing_with_face() {
1599        let ctx = ExecutorContext::new_mock(None).await;
1600        let mut exec_state = ExecState::new(&ctx);
1601        let edge = edge_value(&mut exec_state);
1602        // The string "START" coerces to a `FaceTag`, i.e. a non-edge extrudable.
1603        let face = KclValue::String {
1604            value: "START".to_owned(),
1605            meta: vec![],
1606        };
1607        let err = coerce_extrude_targets(
1608            vec![edge, face],
1609            BodyType::Surface,
1610            None,
1611            None,
1612            &mut exec_state,
1613            &ctx,
1614            crate::SourceRange::default(),
1615        )
1616        .await
1617        .unwrap_err();
1618
1619        assert!(
1620            err.message()
1621                .contains("Cannot extrude edges together with sketches or faces"),
1622            "{err:?}"
1623        );
1624        ctx.close().await;
1625    }
1626}