Skip to main content

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