Skip to main content

kcl_lib/std/
shapes.rs

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