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