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