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