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