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