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