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