Skip to main content

kcl_lib/std/
shapes.rs

1//! Standard library shapes.
2
3use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kcmc::length_unit::LengthUnit;
7use kcmc::shared::Angle;
8use kcmc::shared::Point2d as KPoint2d;
9use kittycad_modeling_cmds::shared::PathSegment;
10use kittycad_modeling_cmds::units::UnitLength;
11use kittycad_modeling_cmds::{self as kcmc};
12use serde::Serialize;
13
14use super::args::TyF64;
15use super::utils::point_to_len_unit;
16use super::utils::point_to_mm;
17use super::utils::point_to_typed;
18use super::utils::untype_point;
19use super::utils::untyped_point_to_mm;
20use crate::SourceRange;
21use crate::errors::KclError;
22use crate::errors::KclErrorDetails;
23use crate::execution::BasePath;
24use crate::execution::ExecState;
25use crate::execution::GeoMeta;
26use crate::execution::KclValue;
27use crate::execution::ModelingCmdMeta;
28use crate::execution::Path;
29use crate::execution::ProfileClosed;
30use crate::execution::Sketch;
31use crate::execution::SketchSurface;
32use crate::execution::types::RuntimeType;
33use crate::execution::types::adjust_length;
34use crate::parsing::ast::types::TagNode;
35use crate::std::Args;
36use crate::std::utils::calculate_circle_center;
37use crate::std::utils::distance;
38
39/// A sketch surface or a sketch.
40#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
41#[ts(export)]
42#[serde(untagged)]
43pub enum SketchOrSurface {
44    SketchSurface(SketchSurface),
45    Sketch(Box<Sketch>),
46}
47
48impl SketchOrSurface {
49    pub fn into_sketch_surface(self) -> SketchSurface {
50        match self {
51            SketchOrSurface::SketchSurface(surface) => surface,
52            SketchOrSurface::Sketch(sketch) => sketch.on,
53        }
54    }
55}
56
57/// Sketch a rectangle.
58pub async fn rectangle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
59    let sketch_or_surface =
60        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
61    let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
62    let corner = args.get_kw_arg_opt("corner", &RuntimeType::point2d(), exec_state)?;
63    let width: TyF64 = args.get_kw_arg("width", &RuntimeType::length(), exec_state)?;
64    let height: TyF64 = args.get_kw_arg("height", &RuntimeType::length(), exec_state)?;
65
66    inner_rectangle(sketch_or_surface, center, corner, width, height, exec_state, args)
67        .await
68        .map(Box::new)
69        .map(|value| KclValue::Sketch { value })
70}
71
72async fn inner_rectangle(
73    sketch_or_surface: SketchOrSurface,
74    center: Option<[TyF64; 2]>,
75    corner: Option<[TyF64; 2]>,
76    width: TyF64,
77    height: TyF64,
78    exec_state: &mut ExecState,
79    args: Args,
80) -> Result<Sketch, KclError> {
81    let sketch_surface = sketch_or_surface.into_sketch_surface();
82
83    // Find the corner in the negative quadrant
84    let (ty, corner) = match (center, corner) {
85        (Some(center), None) => (
86            center[0].ty,
87            [center[0].n - width.n / 2.0, center[1].n - height.n / 2.0],
88        ),
89        (None, Some(corner)) => (corner[0].ty, [corner[0].n, corner[1].n]),
90        (None, None) => {
91            return Err(KclError::new_semantic(KclErrorDetails::new(
92                "You must supply either `corner` or `center` arguments, but not both".to_string(),
93                vec![args.source_range],
94            )));
95        }
96        (Some(_), Some(_)) => {
97            return Err(KclError::new_semantic(KclErrorDetails::new(
98                "You must supply either `corner` or `center` arguments, but not both".to_string(),
99                vec![args.source_range],
100            )));
101        }
102    };
103    let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
104    let corner_t = [TyF64::new(corner[0], ty), TyF64::new(corner[1], ty)];
105
106    // Start the sketch then draw the 4 lines.
107    let sketch = crate::std::sketch::inner_start_profile(
108        sketch_surface,
109        corner_t,
110        None,
111        exec_state,
112        &args.ctx,
113        args.source_range,
114    )
115    .await?;
116    let sketch_id = sketch.id;
117    let deltas = [[width.n, 0.0], [0.0, height.n], [-width.n, 0.0], [0.0, -height.n]];
118    let ids = [
119        exec_state.next_uuid(),
120        exec_state.next_uuid(),
121        exec_state.next_uuid(),
122        exec_state.next_uuid(),
123    ];
124    for (id, delta) in ids.iter().copied().zip(deltas) {
125        exec_state
126            .batch_modeling_cmd(
127                ModelingCmdMeta::from_args_id(exec_state, &args, id),
128                ModelingCmd::from(
129                    mcmd::ExtendPath::builder()
130                        .path(sketch.id.into())
131                        .segment(PathSegment::Line {
132                            end: KPoint2d::from(untyped_point_to_mm(delta, units))
133                                .with_z(0.0)
134                                .map(LengthUnit),
135                            relative: true,
136                        })
137                        .build(),
138                ),
139            )
140            .await?;
141    }
142    exec_state
143        .batch_modeling_cmd(
144            ModelingCmdMeta::from_args_id(exec_state, &args, sketch_id),
145            ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
146        )
147        .await?;
148
149    // Update the sketch in KCL memory.
150    let mut new_sketch = sketch;
151    new_sketch.is_closed = ProfileClosed::Explicitly;
152    fn add(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
153        [a[0] + b[0], a[1] + b[1]]
154    }
155    let a = (corner, add(corner, deltas[0]));
156    let b = (a.1, add(a.1, deltas[1]));
157    let c = (b.1, add(b.1, deltas[2]));
158    let d = (c.1, add(c.1, deltas[3]));
159    for (id, (from, to)) in ids.into_iter().zip([a, b, c, d]) {
160        let current_path = Path::ToPoint {
161            base: BasePath {
162                from,
163                to,
164                tag: None,
165                units,
166                geo_meta: GeoMeta {
167                    id,
168                    metadata: args.source_range.into(),
169                },
170            },
171        };
172        new_sketch.paths.push(current_path);
173    }
174    Ok(new_sketch)
175}
176
177/// Sketch a circle.
178pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
179    let sketch_or_surface =
180        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
181    let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
182    let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
183    let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
184    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
185
186    let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
187    Ok(KclValue::Sketch {
188        value: Box::new(sketch),
189    })
190}
191
192pub const POINT_ZERO_ZERO: [TyF64; 2] = [
193    TyF64::new(0.0, crate::exec::NumericType::mm()),
194    TyF64::new(0.0, crate::exec::NumericType::mm()),
195];
196
197pub(super) async fn inner_circle(
198    sketch_or_surface: SketchOrSurface,
199    center: Option<[TyF64; 2]>,
200    radius: Option<TyF64>,
201    diameter: Option<TyF64>,
202    tag: Option<TagNode>,
203    exec_state: &mut ExecState,
204    args: Args,
205) -> Result<Sketch, KclError> {
206    let sketch_surface = sketch_or_surface.into_sketch_surface();
207    let center = center.unwrap_or(POINT_ZERO_ZERO);
208    let (center_u, ty) = untype_point(center.clone());
209    let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
210
211    let radius = get_radius(radius, diameter, args.source_range)?;
212    let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
213    let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
214
215    let sketch =
216        crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
217            .await?;
218
219    let angle_start = Angle::zero();
220    let angle_end = Angle::turn();
221
222    let id = exec_state.next_uuid();
223
224    exec_state
225        .batch_modeling_cmd(
226            ModelingCmdMeta::from_args_id(exec_state, &args, id),
227            ModelingCmd::from(
228                mcmd::ExtendPath::builder()
229                    .path(sketch.id.into())
230                    .segment(PathSegment::Arc {
231                        start: angle_start,
232                        end: angle_end,
233                        center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
234                        radius: LengthUnit(radius.to_mm()),
235                        relative: false,
236                    })
237                    .build(),
238            ),
239        )
240        .await?;
241
242    let current_path = Path::Circle {
243        base: BasePath {
244            from,
245            to: from,
246            tag: tag.clone(),
247            units,
248            geo_meta: GeoMeta {
249                id,
250                metadata: args.source_range.into(),
251            },
252        },
253        radius: radius.to_length_units(units),
254        center: center_u,
255        ccw: angle_start < angle_end,
256    };
257
258    let mut new_sketch = sketch;
259    new_sketch.is_closed = ProfileClosed::Explicitly;
260    if let Some(tag) = &tag {
261        new_sketch.add_tag(tag, &current_path, exec_state, None);
262    }
263
264    new_sketch.paths.push(current_path);
265
266    exec_state
267        .batch_modeling_cmd(
268            ModelingCmdMeta::from_args_id(exec_state, &args, id),
269            ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
270        )
271        .await?;
272
273    Ok(new_sketch)
274}
275
276/// Sketch a 3-point circle.
277pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
278    let sketch_or_surface =
279        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
280    let p1 = args.get_kw_arg("p1", &RuntimeType::point2d(), exec_state)?;
281    let p2 = args.get_kw_arg("p2", &RuntimeType::point2d(), exec_state)?;
282    let p3 = args.get_kw_arg("p3", &RuntimeType::point2d(), exec_state)?;
283    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
284
285    let sketch = inner_circle_three_point(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
286    Ok(KclValue::Sketch {
287        value: Box::new(sketch),
288    })
289}
290
291// Similar to inner_circle, but needs to retain 3-point information in the
292// path so it can be used for other features, otherwise it's lost.
293async fn inner_circle_three_point(
294    sketch_surface_or_group: SketchOrSurface,
295    p1: [TyF64; 2],
296    p2: [TyF64; 2],
297    p3: [TyF64; 2],
298    tag: Option<TagNode>,
299    exec_state: &mut ExecState,
300    args: Args,
301) -> Result<Sketch, KclError> {
302    let ty = p1[0].ty;
303    let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
304
305    let p1 = point_to_len_unit(p1, units);
306    let p2 = point_to_len_unit(p2, units);
307    let p3 = point_to_len_unit(p3, units);
308
309    let center = calculate_circle_center(p1, p2, p3);
310    // It can be the distance to any of the 3 points - they all lay on the circumference.
311    let radius = distance(center, p2);
312
313    let sketch_surface = sketch_surface_or_group.into_sketch_surface();
314
315    let from = [TyF64::new(center[0] + radius, ty), TyF64::new(center[1], ty)];
316    let sketch = crate::std::sketch::inner_start_profile(
317        sketch_surface,
318        from.clone(),
319        None,
320        exec_state,
321        &args.ctx,
322        args.source_range,
323    )
324    .await?;
325
326    let angle_start = Angle::zero();
327    let angle_end = Angle::turn();
328
329    let id = exec_state.next_uuid();
330
331    exec_state
332        .batch_modeling_cmd(
333            ModelingCmdMeta::from_args_id(exec_state, &args, id),
334            ModelingCmd::from(
335                mcmd::ExtendPath::builder()
336                    .path(sketch.id.into())
337                    .segment(PathSegment::Arc {
338                        start: angle_start,
339                        end: angle_end,
340                        center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
341                        radius: adjust_length(units, radius, UnitLength::Millimeters).0.into(),
342                        relative: false,
343                    })
344                    .build(),
345            ),
346        )
347        .await?;
348
349    let current_path = Path::CircleThreePoint {
350        base: BasePath {
351            // It's fine to untype here because we know `from` has units as its units.
352            from: untype_point(from.clone()).0,
353            to: untype_point(from).0,
354            tag: tag.clone(),
355            units,
356            geo_meta: GeoMeta {
357                id,
358                metadata: args.source_range.into(),
359            },
360        },
361        p1,
362        p2,
363        p3,
364    };
365
366    let mut new_sketch = sketch;
367    new_sketch.is_closed = ProfileClosed::Explicitly;
368    if let Some(tag) = &tag {
369        new_sketch.add_tag(tag, &current_path, exec_state, None);
370    }
371
372    new_sketch.paths.push(current_path);
373
374    exec_state
375        .batch_modeling_cmd(
376            ModelingCmdMeta::from_args_id(exec_state, &args, id),
377            ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
378        )
379        .await?;
380
381    Ok(new_sketch)
382}
383
384/// Type of the polygon
385#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, Default)]
386#[ts(export)]
387#[serde(rename_all = "lowercase")]
388pub enum PolygonType {
389    #[default]
390    Inscribed,
391    Circumscribed,
392}
393
394/// Create a regular polygon with the specified number of sides and radius.
395pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
396    let sketch_or_surface =
397        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
398    let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
399    let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
400    let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
401    let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
402
403    let sketch = inner_polygon(
404        sketch_or_surface,
405        radius,
406        num_sides.n as u64,
407        center,
408        inscribed,
409        exec_state,
410        args,
411    )
412    .await?;
413    Ok(KclValue::Sketch {
414        value: Box::new(sketch),
415    })
416}
417
418#[allow(clippy::too_many_arguments)]
419async fn inner_polygon(
420    sketch_surface_or_group: SketchOrSurface,
421    radius: TyF64,
422    num_sides: u64,
423    center: Option<[TyF64; 2]>,
424    inscribed: Option<bool>,
425    exec_state: &mut ExecState,
426    args: Args,
427) -> Result<Sketch, KclError> {
428    let center = center.unwrap_or(POINT_ZERO_ZERO);
429    if num_sides < 3 {
430        return Err(KclError::new_type(KclErrorDetails::new(
431            "Polygon must have at least 3 sides".to_string(),
432            vec![args.source_range],
433        )));
434    }
435
436    if radius.n <= 0.0 {
437        return Err(KclError::new_type(KclErrorDetails::new(
438            "Radius must be greater than 0".to_string(),
439            vec![args.source_range],
440        )));
441    }
442
443    let (sketch_surface, units) = match sketch_surface_or_group {
444        SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.as_length().unwrap_or(UnitLength::Millimeters)),
445        SketchOrSurface::Sketch(group) => (group.on, group.units),
446    };
447
448    let half_angle = std::f64::consts::PI / num_sides as f64;
449
450    let radius_to_vertices = if inscribed.unwrap_or(true) {
451        // inscribed
452        radius.n
453    } else {
454        // circumscribed
455        radius.n / libm::cos(half_angle)
456    };
457
458    let angle_step = std::f64::consts::TAU / num_sides as f64;
459
460    let center_u = point_to_len_unit(center, units);
461
462    let vertices: Vec<[f64; 2]> = (0..num_sides)
463        .map(|i| {
464            let angle = angle_step * i as f64;
465            [
466                center_u[0] + radius_to_vertices * libm::cos(angle),
467                center_u[1] + radius_to_vertices * libm::sin(angle),
468            ]
469        })
470        .collect();
471
472    let mut sketch = crate::std::sketch::inner_start_profile(
473        sketch_surface,
474        point_to_typed(vertices[0], units),
475        None,
476        exec_state,
477        &args.ctx,
478        args.source_range,
479    )
480    .await?;
481
482    // Draw all the lines with unique IDs and modified tags
483    for vertex in vertices.iter().skip(1) {
484        let from = sketch.current_pen_position()?;
485        let id = exec_state.next_uuid();
486
487        exec_state
488            .batch_modeling_cmd(
489                ModelingCmdMeta::from_args_id(exec_state, &args, id),
490                ModelingCmd::from(
491                    mcmd::ExtendPath::builder()
492                        .path(sketch.id.into())
493                        .segment(PathSegment::Line {
494                            end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
495                                .with_z(0.0)
496                                .map(LengthUnit),
497                            relative: false,
498                        })
499                        .build(),
500                ),
501            )
502            .await?;
503
504        let current_path = Path::ToPoint {
505            base: BasePath {
506                from: from.ignore_units(),
507                to: *vertex,
508                tag: None,
509                units: sketch.units,
510                geo_meta: GeoMeta {
511                    id,
512                    metadata: args.source_range.into(),
513                },
514            },
515        };
516
517        sketch.paths.push(current_path);
518    }
519
520    // Close the polygon by connecting back to the first vertex with a new ID
521    let from = sketch.current_pen_position()?;
522    let close_id = exec_state.next_uuid();
523
524    exec_state
525        .batch_modeling_cmd(
526            ModelingCmdMeta::from_args_id(exec_state, &args, close_id),
527            ModelingCmd::from(
528                mcmd::ExtendPath::builder()
529                    .path(sketch.id.into())
530                    .segment(PathSegment::Line {
531                        end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
532                            .with_z(0.0)
533                            .map(LengthUnit),
534                        relative: false,
535                    })
536                    .build(),
537            ),
538        )
539        .await?;
540
541    let current_path = Path::ToPoint {
542        base: BasePath {
543            from: from.ignore_units(),
544            to: vertices[0],
545            tag: None,
546            units: sketch.units,
547            geo_meta: GeoMeta {
548                id: close_id,
549                metadata: args.source_range.into(),
550            },
551        },
552    };
553
554    sketch.paths.push(current_path);
555    sketch.is_closed = ProfileClosed::Explicitly;
556
557    exec_state
558        .batch_modeling_cmd(
559            ModelingCmdMeta::from_args(exec_state, &args),
560            ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
561        )
562        .await?;
563
564    Ok(sketch)
565}
566
567/// Sketch an ellipse.
568pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
569    let sketch_or_surface =
570        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
571    let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
572    let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
573    let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
574    let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
575    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
576
577    let sketch = inner_ellipse(
578        sketch_or_surface,
579        center,
580        major_radius,
581        major_axis,
582        minor_radius,
583        tag,
584        exec_state,
585        args,
586    )
587    .await?;
588    Ok(KclValue::Sketch {
589        value: Box::new(sketch),
590    })
591}
592
593#[allow(clippy::too_many_arguments)]
594async fn inner_ellipse(
595    sketch_surface_or_group: SketchOrSurface,
596    center: Option<[TyF64; 2]>,
597    major_radius: Option<TyF64>,
598    major_axis: Option<[TyF64; 2]>,
599    minor_radius: TyF64,
600    tag: Option<TagNode>,
601    exec_state: &mut ExecState,
602    args: Args,
603) -> Result<Sketch, KclError> {
604    let sketch_surface = sketch_surface_or_group.into_sketch_surface();
605    let center = center.unwrap_or(POINT_ZERO_ZERO);
606    let (center_u, ty) = untype_point(center.clone());
607    let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
608
609    let major_axis = match (major_axis, major_radius) {
610        (Some(_), Some(_)) | (None, None) => {
611            return Err(KclError::new_type(KclErrorDetails::new(
612                "Provide either `majorAxis` or `majorRadius`.".to_string(),
613                vec![args.source_range],
614            )));
615        }
616        (Some(major_axis), None) => major_axis,
617        (None, Some(major_radius)) => [
618            major_radius.clone(),
619            TyF64 {
620                n: 0.0,
621                ty: major_radius.ty,
622            },
623        ],
624    };
625
626    let from = [
627        center_u[0] + major_axis[0].to_length_units(units),
628        center_u[1] + major_axis[1].to_length_units(units),
629    ];
630    let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
631
632    let sketch =
633        crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
634            .await?;
635
636    let angle_start = Angle::zero();
637    let angle_end = Angle::turn();
638
639    let id = exec_state.next_uuid();
640
641    let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
642    exec_state
643        .batch_modeling_cmd(
644            ModelingCmdMeta::from_args_id(exec_state, &args, id),
645            ModelingCmd::from(
646                mcmd::ExtendPath::builder()
647                    .path(sketch.id.into())
648                    .segment(PathSegment::Ellipse {
649                        center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
650                        major_axis: axis,
651                        minor_radius: LengthUnit(minor_radius.to_mm()),
652                        start_angle: Angle::from_degrees(angle_start.to_degrees()),
653                        end_angle: Angle::from_degrees(angle_end.to_degrees()),
654                    })
655                    .build(),
656            ),
657        )
658        .await?;
659
660    let current_path = Path::Ellipse {
661        base: BasePath {
662            from,
663            to: from,
664            tag: tag.clone(),
665            units,
666            geo_meta: GeoMeta {
667                id,
668                metadata: args.source_range.into(),
669            },
670        },
671        major_axis: major_axis.map(|x| x.to_length_units(units)),
672        minor_radius: minor_radius.to_length_units(units),
673        center: center_u,
674        ccw: angle_start < angle_end,
675    };
676
677    let mut new_sketch = sketch;
678    new_sketch.is_closed = ProfileClosed::Explicitly;
679    if let Some(tag) = &tag {
680        new_sketch.add_tag(tag, &current_path, exec_state, None);
681    }
682
683    new_sketch.paths.push(current_path);
684
685    exec_state
686        .batch_modeling_cmd(
687            ModelingCmdMeta::from_args_id(exec_state, &args, id),
688            ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
689        )
690        .await?;
691
692    Ok(new_sketch)
693}
694
695pub(crate) fn get_radius(
696    radius: Option<TyF64>,
697    diameter: Option<TyF64>,
698    source_range: SourceRange,
699) -> Result<TyF64, KclError> {
700    get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
701}
702
703pub(crate) fn get_radius_labelled(
704    radius: Option<TyF64>,
705    diameter: Option<TyF64>,
706    source_range: SourceRange,
707    label_radius: &'static str,
708    label_diameter: &'static str,
709) -> Result<TyF64, KclError> {
710    match (radius, diameter) {
711        (Some(radius), None) => Ok(radius),
712        (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
713        (None, None) => Err(KclError::new_type(KclErrorDetails::new(
714            format!("This function needs either `{label_diameter}` or `{label_radius}`"),
715            vec![source_range],
716        ))),
717        (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
718            format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
719            vec![source_range],
720        ))),
721    }
722}