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            let units = center[0].ty.as_length().unwrap_or(UnitLength::Millimeters);
88            (
89                center[0].ty,
90                [
91                    center[0].n - width.to_length_units(units) / 2.0,
92                    center[1].n - height.to_length_units(units) / 2.0,
93                ],
94            )
95        }
96        (None, Some(corner)) => (corner[0].ty, [corner[0].n, corner[1].n]),
97        (None, None) => {
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        (Some(_), Some(_)) => {
104            return Err(KclError::new_semantic(KclErrorDetails::new(
105                "You must supply either `corner` or `center` arguments, but not both".to_string(),
106                vec![args.source_range],
107            )));
108        }
109    };
110    let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
111    let corner_t = [TyF64::new(corner[0], ty), TyF64::new(corner[1], ty)];
112    // The rectangle is drawn in the units of `center` or `corner`, which can
113    // differ from the units of `width` and `height`.
114    let width = width.to_length_units(units);
115    let height = height.to_length_units(units);
116
117    // Start the sketch then draw the 4 lines.
118    let sketch = crate::std::sketch::inner_start_profile(
119        sketch_surface,
120        corner_t,
121        None,
122        exec_state,
123        &args.ctx,
124        args.source_range,
125    )
126    .await?;
127    let sketch_id = sketch.id;
128    let deltas = [[width, 0.0], [0.0, height], [-width, 0.0], [0.0, -height]];
129    let ids = [
130        exec_state.next_uuid(),
131        exec_state.next_uuid(),
132        exec_state.next_uuid(),
133        exec_state.next_uuid(),
134    ];
135    for (id, delta) in ids.iter().copied().zip(deltas) {
136        exec_state
137            .batch_modeling_cmd(
138                ModelingCmdMeta::from_args_id(exec_state, &args, id),
139                ModelingCmd::from(
140                    mcmd::ExtendPath::builder()
141                        .path(sketch.id.into())
142                        .segment(PathSegment::Line {
143                            end: KPoint2d::from(untyped_point_to_mm(delta, units))
144                                .with_z(0.0)
145                                .map(LengthUnit),
146                            relative: true,
147                        })
148                        .build(),
149                ),
150            )
151            .await?;
152    }
153    exec_state
154        .batch_modeling_cmd(
155            ModelingCmdMeta::from_args_id(exec_state, &args, sketch_id),
156            ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
157        )
158        .await?;
159
160    // Update the sketch in KCL memory.
161    let mut new_sketch = sketch;
162    new_sketch.is_closed = ProfileClosed::Explicitly;
163    fn add(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
164        [a[0] + b[0], a[1] + b[1]]
165    }
166    let a = (corner, add(corner, deltas[0]));
167    let b = (a.1, add(a.1, deltas[1]));
168    let c = (b.1, add(b.1, deltas[2]));
169    let d = (c.1, add(c.1, deltas[3]));
170    for (id, (from, to)) in ids.into_iter().zip([a, b, c, d]) {
171        let current_path = Path::ToPoint {
172            base: BasePath {
173                from,
174                to,
175                tag: None,
176                units,
177                geo_meta: GeoMeta {
178                    id,
179                    metadata: args.source_range.into(),
180                },
181            },
182        };
183        new_sketch.paths.push_back(current_path);
184    }
185    Ok(new_sketch)
186}
187
188/// Sketch a circle.
189pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
190    let sketch_or_surface =
191        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
192    let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
193    let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
194    let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
195    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
196
197    let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
198    Ok(KclValue::Sketch {
199        value: Box::new(sketch),
200    })
201}
202
203pub const POINT_ZERO_ZERO: [TyF64; 2] = [
204    TyF64::new(
205        0.0,
206        crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
207    ),
208    TyF64::new(
209        0.0,
210        crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
211    ),
212];
213
214pub(super) async fn inner_circle(
215    sketch_or_surface: SketchOrSurface,
216    center: Option<[TyF64; 2]>,
217    radius: Option<TyF64>,
218    diameter: Option<TyF64>,
219    tag: Option<TagNode>,
220    exec_state: &mut ExecState,
221    args: Args,
222) -> Result<Sketch, KclError> {
223    let sketch_surface = sketch_or_surface.into_sketch_surface();
224    let center = center.unwrap_or(POINT_ZERO_ZERO);
225    let (center_u, ty) = untype_point(center.clone());
226    let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
227
228    let radius = get_radius(radius, diameter, args.source_range)?;
229    let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
230    let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
231
232    let sketch =
233        crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
234            .await?;
235
236    let angle_start = Angle::zero();
237    let angle_end = Angle::turn();
238
239    let id = exec_state.next_uuid();
240
241    exec_state
242        .batch_modeling_cmd(
243            ModelingCmdMeta::from_args_id(exec_state, &args, id),
244            ModelingCmd::from(
245                mcmd::ExtendPath::builder()
246                    .path(sketch.id.into())
247                    .segment(PathSegment::Arc {
248                        start: angle_start,
249                        end: angle_end,
250                        center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
251                        radius: LengthUnit(radius.to_mm()),
252                        relative: false,
253                    })
254                    .build(),
255            ),
256        )
257        .await?;
258
259    let current_path = Path::Circle {
260        base: BasePath {
261            from,
262            to: from,
263            tag: tag.clone(),
264            units,
265            geo_meta: GeoMeta {
266                id,
267                metadata: args.source_range.into(),
268            },
269        },
270        radius: radius.to_length_units(units),
271        center: center_u,
272        ccw: angle_start < angle_end,
273    };
274
275    let mut new_sketch = sketch;
276    new_sketch.is_closed = ProfileClosed::Explicitly;
277    if let Some(tag) = &tag {
278        new_sketch.add_tag(tag, &current_path, exec_state, None);
279    }
280
281    new_sketch.paths.push_back(current_path);
282
283    exec_state
284        .batch_modeling_cmd(
285            ModelingCmdMeta::from_args_id(exec_state, &args, id),
286            ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
287        )
288        .await?;
289
290    Ok(new_sketch)
291}
292
293/// Sketch a 3-point circle.
294pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
295    let sketch_or_surface =
296        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
297    let p1 = args.get_kw_arg("p1", &RuntimeType::point2d(), exec_state)?;
298    let p2 = args.get_kw_arg("p2", &RuntimeType::point2d(), exec_state)?;
299    let p3 = args.get_kw_arg("p3", &RuntimeType::point2d(), exec_state)?;
300    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
301
302    let sketch = inner_circle_three_point(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
303    Ok(KclValue::Sketch {
304        value: Box::new(sketch),
305    })
306}
307
308// Similar to inner_circle, but needs to retain 3-point information in the
309// path so it can be used for other features, otherwise it's lost.
310async fn inner_circle_three_point(
311    sketch_surface_or_group: SketchOrSurface,
312    p1: [TyF64; 2],
313    p2: [TyF64; 2],
314    p3: [TyF64; 2],
315    tag: Option<TagNode>,
316    exec_state: &mut ExecState,
317    args: Args,
318) -> Result<Sketch, KclError> {
319    let ty = p1[0].ty;
320    let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
321
322    let p1 = point_to_len_unit(p1, units);
323    let p2 = point_to_len_unit(p2, units);
324    let p3 = point_to_len_unit(p3, units);
325
326    let center = calculate_circle_center(p1, p2, p3);
327    // It can be the distance to any of the 3 points - they all lay on the circumference.
328    let radius = distance(center, p2);
329
330    let sketch_surface = sketch_surface_or_group.into_sketch_surface();
331
332    let from = [TyF64::new(center[0] + radius, ty), TyF64::new(center[1], ty)];
333    let sketch = crate::std::sketch::inner_start_profile(
334        sketch_surface,
335        from.clone(),
336        None,
337        exec_state,
338        &args.ctx,
339        args.source_range,
340    )
341    .await?;
342
343    let angle_start = Angle::zero();
344    let angle_end = Angle::turn();
345
346    let id = exec_state.next_uuid();
347
348    exec_state
349        .batch_modeling_cmd(
350            ModelingCmdMeta::from_args_id(exec_state, &args, id),
351            ModelingCmd::from(
352                mcmd::ExtendPath::builder()
353                    .path(sketch.id.into())
354                    .segment(PathSegment::Arc {
355                        start: angle_start,
356                        end: angle_end,
357                        center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
358                        radius: adjust_length(units, radius, UnitLength::Millimeters).0.into(),
359                        relative: false,
360                    })
361                    .build(),
362            ),
363        )
364        .await?;
365
366    let current_path = Path::CircleThreePoint {
367        base: BasePath {
368            // It's fine to untype here because we know `from` has units as its units.
369            from: untype_point(from.clone()).0,
370            to: untype_point(from).0,
371            tag: tag.clone(),
372            units,
373            geo_meta: GeoMeta {
374                id,
375                metadata: args.source_range.into(),
376            },
377        },
378        p1,
379        p2,
380        p3,
381    };
382
383    let mut new_sketch = sketch;
384    new_sketch.is_closed = ProfileClosed::Explicitly;
385    if let Some(tag) = &tag {
386        new_sketch.add_tag(tag, &current_path, exec_state, None);
387    }
388
389    new_sketch.paths.push_back(current_path);
390
391    exec_state
392        .batch_modeling_cmd(
393            ModelingCmdMeta::from_args_id(exec_state, &args, id),
394            ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
395        )
396        .await?;
397
398    Ok(new_sketch)
399}
400
401/// Type of the polygon
402#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, Default)]
403#[ts(export)]
404#[serde(rename_all = "lowercase")]
405pub enum PolygonType {
406    #[default]
407    Inscribed,
408    Circumscribed,
409}
410
411/// Create a regular polygon with the specified number of sides and radius.
412pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
413    let sketch_or_surface =
414        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
415    let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
416    let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
417    let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
418    let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
419
420    let sketch = inner_polygon(
421        sketch_or_surface,
422        radius,
423        num_sides.n as u64,
424        center,
425        inscribed,
426        exec_state,
427        args,
428    )
429    .await?;
430    Ok(KclValue::Sketch {
431        value: Box::new(sketch),
432    })
433}
434
435#[allow(clippy::too_many_arguments)]
436async fn inner_polygon(
437    sketch_surface_or_group: SketchOrSurface,
438    radius: TyF64,
439    num_sides: u64,
440    center: Option<[TyF64; 2]>,
441    inscribed: Option<bool>,
442    exec_state: &mut ExecState,
443    args: Args,
444) -> Result<Sketch, KclError> {
445    let center = center.unwrap_or(POINT_ZERO_ZERO);
446    if num_sides < 3 {
447        return Err(KclError::new_type(KclErrorDetails::new(
448            "Polygon must have at least 3 sides".to_string(),
449            vec![args.source_range],
450        )));
451    }
452
453    if radius.n <= 0.0 {
454        return Err(KclError::new_type(KclErrorDetails::new(
455            "Radius must be greater than 0".to_string(),
456            vec![args.source_range],
457        )));
458    }
459
460    let (sketch_surface, units) = match sketch_surface_or_group {
461        SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.as_length().unwrap_or(UnitLength::Millimeters)),
462        SketchOrSurface::Sketch(group) => (group.on, group.units),
463    };
464
465    let half_angle = std::f64::consts::PI / num_sides as f64;
466
467    let radius_to_vertices = if inscribed.unwrap_or(true) {
468        // inscribed
469        radius.n
470    } else {
471        // circumscribed
472        radius.n / libm::cos(half_angle)
473    };
474
475    let angle_step = std::f64::consts::TAU / num_sides as f64;
476
477    let center_u = point_to_len_unit(center, units);
478
479    let vertices: Vec<[f64; 2]> = (0..num_sides)
480        .map(|i| {
481            let angle = angle_step * i as f64;
482            [
483                center_u[0] + radius_to_vertices * libm::cos(angle),
484                center_u[1] + radius_to_vertices * libm::sin(angle),
485            ]
486        })
487        .collect();
488
489    let mut sketch = crate::std::sketch::inner_start_profile(
490        sketch_surface,
491        point_to_typed(vertices[0], units),
492        None,
493        exec_state,
494        &args.ctx,
495        args.source_range,
496    )
497    .await?;
498
499    // Draw all the lines with unique IDs and modified tags
500    for vertex in vertices.iter().skip(1) {
501        let from = sketch.current_pen_position()?;
502        let id = exec_state.next_uuid();
503
504        exec_state
505            .batch_modeling_cmd(
506                ModelingCmdMeta::from_args_id(exec_state, &args, id),
507                ModelingCmd::from(
508                    mcmd::ExtendPath::builder()
509                        .path(sketch.id.into())
510                        .segment(PathSegment::Line {
511                            end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
512                                .with_z(0.0)
513                                .map(LengthUnit),
514                            relative: false,
515                        })
516                        .build(),
517                ),
518            )
519            .await?;
520
521        let current_path = Path::ToPoint {
522            base: BasePath {
523                from: from.ignore_units(),
524                to: *vertex,
525                tag: None,
526                units: sketch.units,
527                geo_meta: GeoMeta {
528                    id,
529                    metadata: args.source_range.into(),
530                },
531            },
532        };
533
534        sketch.paths.push_back(current_path);
535    }
536
537    // Close the polygon by connecting back to the first vertex with a new ID
538    let from = sketch.current_pen_position()?;
539    let close_id = exec_state.next_uuid();
540
541    exec_state
542        .batch_modeling_cmd(
543            ModelingCmdMeta::from_args_id(exec_state, &args, close_id),
544            ModelingCmd::from(
545                mcmd::ExtendPath::builder()
546                    .path(sketch.id.into())
547                    .segment(PathSegment::Line {
548                        end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
549                            .with_z(0.0)
550                            .map(LengthUnit),
551                        relative: false,
552                    })
553                    .build(),
554            ),
555        )
556        .await?;
557
558    let current_path = Path::ToPoint {
559        base: BasePath {
560            from: from.ignore_units(),
561            to: vertices[0],
562            tag: None,
563            units: sketch.units,
564            geo_meta: GeoMeta {
565                id: close_id,
566                metadata: args.source_range.into(),
567            },
568        },
569    };
570
571    sketch.paths.push_back(current_path);
572    sketch.is_closed = ProfileClosed::Explicitly;
573
574    exec_state
575        .batch_modeling_cmd(
576            ModelingCmdMeta::from_args(exec_state, &args),
577            ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
578        )
579        .await?;
580
581    Ok(sketch)
582}
583
584/// Sketch an ellipse.
585pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
586    let sketch_or_surface =
587        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
588    let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
589    let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
590    let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
591    let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
592    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
593
594    let sketch = inner_ellipse(
595        sketch_or_surface,
596        center,
597        major_radius,
598        major_axis,
599        minor_radius,
600        tag,
601        exec_state,
602        args,
603    )
604    .await?;
605    Ok(KclValue::Sketch {
606        value: Box::new(sketch),
607    })
608}
609
610#[allow(clippy::too_many_arguments)]
611async fn inner_ellipse(
612    sketch_surface_or_group: SketchOrSurface,
613    center: Option<[TyF64; 2]>,
614    major_radius: Option<TyF64>,
615    major_axis: Option<[TyF64; 2]>,
616    minor_radius: TyF64,
617    tag: Option<TagNode>,
618    exec_state: &mut ExecState,
619    args: Args,
620) -> Result<Sketch, KclError> {
621    let sketch_surface = sketch_surface_or_group.into_sketch_surface();
622    let center = center.unwrap_or(POINT_ZERO_ZERO);
623    let (center_u, ty) = untype_point(center.clone());
624    let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
625
626    let major_axis = match (major_axis, major_radius) {
627        (Some(_), Some(_)) | (None, None) => {
628            return Err(KclError::new_type(KclErrorDetails::new(
629                "Provide either `majorAxis` or `majorRadius`.".to_string(),
630                vec![args.source_range],
631            )));
632        }
633        (Some(major_axis), None) => major_axis,
634        (None, Some(major_radius)) => [
635            major_radius.clone(),
636            TyF64 {
637                n: 0.0,
638                ty: major_radius.ty,
639            },
640        ],
641    };
642
643    let from = [
644        center_u[0] + major_axis[0].to_length_units(units),
645        center_u[1] + major_axis[1].to_length_units(units),
646    ];
647    let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
648
649    let sketch =
650        crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
651            .await?;
652
653    let angle_start = Angle::zero();
654    let angle_end = Angle::turn();
655
656    let id = exec_state.next_uuid();
657
658    let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
659    exec_state
660        .batch_modeling_cmd(
661            ModelingCmdMeta::from_args_id(exec_state, &args, id),
662            ModelingCmd::from(
663                mcmd::ExtendPath::builder()
664                    .path(sketch.id.into())
665                    .segment(PathSegment::Ellipse {
666                        center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
667                        major_axis: axis,
668                        minor_radius: LengthUnit(minor_radius.to_mm()),
669                        start_angle: Angle::from_degrees(angle_start.to_degrees()),
670                        end_angle: Angle::from_degrees(angle_end.to_degrees()),
671                    })
672                    .build(),
673            ),
674        )
675        .await?;
676
677    let current_path = Path::Ellipse {
678        base: BasePath {
679            from,
680            to: from,
681            tag: tag.clone(),
682            units,
683            geo_meta: GeoMeta {
684                id,
685                metadata: args.source_range.into(),
686            },
687        },
688        major_axis: major_axis.map(|x| x.to_length_units(units)),
689        minor_radius: minor_radius.to_length_units(units),
690        center: center_u,
691        ccw: angle_start < angle_end,
692    };
693
694    let mut new_sketch = sketch;
695    new_sketch.is_closed = ProfileClosed::Explicitly;
696    if let Some(tag) = &tag {
697        new_sketch.add_tag(tag, &current_path, exec_state, None);
698    }
699
700    new_sketch.paths.push_back(current_path);
701
702    exec_state
703        .batch_modeling_cmd(
704            ModelingCmdMeta::from_args_id(exec_state, &args, id),
705            ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
706        )
707        .await?;
708
709    Ok(new_sketch)
710}
711
712pub(crate) fn get_radius(
713    radius: Option<TyF64>,
714    diameter: Option<TyF64>,
715    source_range: SourceRange,
716) -> Result<TyF64, KclError> {
717    get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
718}
719
720pub(crate) fn get_radius_labelled(
721    radius: Option<TyF64>,
722    diameter: Option<TyF64>,
723    source_range: SourceRange,
724    label_radius: &'static str,
725    label_diameter: &'static str,
726) -> Result<TyF64, KclError> {
727    match (radius, diameter) {
728        (Some(radius), None) => Ok(radius),
729        (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
730        (None, None) => Err(KclError::new_type(KclErrorDetails::new(
731            format!("This function needs either `{label_diameter}` or `{label_radius}`"),
732            vec![source_range],
733        ))),
734        (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
735            format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
736            vec![source_range],
737        ))),
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use kittycad_modeling_cmds::ModelingCmd;
744    use kittycad_modeling_cmds::shared::PathSegment;
745
746    use crate::execution::KclValue;
747    use crate::execution::parse_execute;
748
749    /// Runs `code`, which draws one rectangle called `r`, and returns where the
750    /// engine was told to start the profile, the relative line segments it was
751    /// sent, and the corners recorded in the sketch, all in mm.
752    async fn rectangle_in_mm(code: &str) -> ([f64; 2], Vec<[f64; 2]>, Vec<[f64; 2]>) {
753        let result = parse_execute(code).await.unwrap();
754
755        let mut start = None;
756        let mut segments = Vec::new();
757        for command in result.root_module_artifact_commands() {
758            match &command.command {
759                ModelingCmd::MovePathPen(move_pen) => start = Some([move_pen.to.x.0, move_pen.to.y.0]),
760                ModelingCmd::ExtendPath(extend) => match &extend.segment {
761                    PathSegment::Line { end, relative: true } => segments.push([end.x.0, end.y.0]),
762                    other => panic!("expected a relative line, got {other:?}"),
763                },
764                _ => {}
765            }
766        }
767
768        let KclValue::Sketch { value: sketch } = result.variable("r") else {
769            panic!("expected `r` to be a sketch");
770        };
771        let corners = sketch
772            .paths
773            .iter()
774            .map(|path| {
775                let [x, y] = path.get_to();
776                [x.to_mm(), y.to_mm()]
777            })
778            .collect();
779
780        (
781            start.expect("expected the profile to start somewhere"),
782            segments,
783            corners,
784        )
785    }
786
787    fn assert_close(actual: &[[f64; 2]], expected: &[[f64; 2]], code: &str) {
788        assert_eq!(actual.len(), expected.len(), "{code}");
789        for (a, e) in actual.iter().zip(expected) {
790            assert!(
791                (a[0] - e[0]).abs() < 1e-9 && (a[1] - e[1]).abs() < 1e-9,
792                "expected {expected:?}, got {actual:?} for:\n{code}"
793            );
794        }
795    }
796
797    #[tokio::test(flavor = "multi_thread")]
798    async fn rectangle_converts_width_and_height_to_the_units_of_the_center_or_corner() {
799        // https://github.com/KittyCAD/modeling-app/issues/14207
800        // Every case describes the same 1in x 2in rectangle, so the engine must
801        // get the same numbers in mm whichever units the arguments use.
802        let centered = [
803            "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(center = [0, 0], width = 1in, height = 2in)",
804            "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(center = [0, 0], width = 25.4, height = 50.8)",
805            "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(center = [0in, 0in], width = 1in, height = 2in)",
806            "@settings(kclVersion = 2.0, defaultLengthUnit = in)\nr = startSketchOn(XY) |> rectangle(center = [0, 0], width = 25.4mm, height = 50.8mm)",
807            "@settings(kclVersion = 2.0, defaultLengthUnit = in)\nr = startSketchOn(XY) |> rectangle(center = [0, 0], width = 1, height = 2)",
808        ];
809        let cornered = [
810            "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(corner = [0, 0], width = 1in, height = 2in)",
811            "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(corner = [0, 0], width = 25.4, height = 50.8)",
812            "@settings(kclVersion = 2.0, defaultLengthUnit = in)\nr = startSketchOn(XY) |> rectangle(corner = [0, 0], width = 25.4mm, height = 50.8mm)",
813        ];
814        let segments = [[25.4, 0.0], [0.0, 50.8], [-25.4, 0.0], [0.0, -50.8]];
815
816        for (codes, start, corners) in [
817            (
818                &centered[..],
819                [-12.7, -25.4],
820                [[12.7, -25.4], [12.7, 25.4], [-12.7, 25.4], [-12.7, -25.4]],
821            ),
822            (
823                &cornered[..],
824                [0.0, 0.0],
825                [[25.4, 0.0], [25.4, 50.8], [0.0, 50.8], [0.0, 0.0]],
826            ),
827        ] {
828            for code in codes {
829                let (actual_start, actual_segments, actual_corners) = rectangle_in_mm(code).await;
830                assert_close(&[actual_start], &[start], code);
831                assert_close(&actual_segments, &segments, code);
832                assert_close(&actual_corners, &corners, code);
833            }
834        }
835    }
836}