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