kcl_lib/std/
shapes.rs

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