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