1use 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#[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
48pub 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 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 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 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
168pub 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
183pub const 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, ¤t_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
267pub 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
282async 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 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 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, ¤t_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#[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
385pub 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_opt("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: Option<[TyF64; 2]>,
415 inscribed: Option<bool>,
416 exec_state: &mut ExecState,
417 args: Args,
418) -> Result<Sketch, KclError> {
419 let center = center.unwrap_or(POINT_ZERO_ZERO);
420 if num_sides < 3 {
421 return Err(KclError::new_type(KclErrorDetails::new(
422 "Polygon must have at least 3 sides".to_string(),
423 vec![args.source_range],
424 )));
425 }
426
427 if radius.n <= 0.0 {
428 return Err(KclError::new_type(KclErrorDetails::new(
429 "Radius must be greater than 0".to_string(),
430 vec![args.source_range],
431 )));
432 }
433
434 let (sketch_surface, units) = match sketch_surface_or_group {
435 SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.as_length().unwrap_or(UnitLength::Millimeters)),
436 SketchOrSurface::Sketch(group) => (group.on, group.units),
437 };
438
439 let half_angle = std::f64::consts::PI / num_sides as f64;
440
441 let radius_to_vertices = if inscribed.unwrap_or(true) {
442 radius.n
444 } else {
445 radius.n / libm::cos(half_angle)
447 };
448
449 let angle_step = std::f64::consts::TAU / num_sides as f64;
450
451 let center_u = point_to_len_unit(center, units);
452
453 let vertices: Vec<[f64; 2]> = (0..num_sides)
454 .map(|i| {
455 let angle = angle_step * i as f64;
456 [
457 center_u[0] + radius_to_vertices * libm::cos(angle),
458 center_u[1] + radius_to_vertices * libm::sin(angle),
459 ]
460 })
461 .collect();
462
463 let mut sketch = crate::std::sketch::inner_start_profile(
464 sketch_surface,
465 point_to_typed(vertices[0], units),
466 None,
467 exec_state,
468 &args.ctx,
469 args.source_range,
470 )
471 .await?;
472
473 for vertex in vertices.iter().skip(1) {
475 let from = sketch.current_pen_position()?;
476 let id = exec_state.next_uuid();
477
478 exec_state
479 .batch_modeling_cmd(
480 ModelingCmdMeta::from_args_id(exec_state, &args, id),
481 ModelingCmd::from(
482 mcmd::ExtendPath::builder()
483 .path(sketch.id.into())
484 .segment(PathSegment::Line {
485 end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
486 .with_z(0.0)
487 .map(LengthUnit),
488 relative: false,
489 })
490 .build(),
491 ),
492 )
493 .await?;
494
495 let current_path = Path::ToPoint {
496 base: BasePath {
497 from: from.ignore_units(),
498 to: *vertex,
499 tag: None,
500 units: sketch.units,
501 geo_meta: GeoMeta {
502 id,
503 metadata: args.source_range.into(),
504 },
505 },
506 };
507
508 sketch.paths.push(current_path);
509 }
510
511 let from = sketch.current_pen_position()?;
513 let close_id = exec_state.next_uuid();
514
515 exec_state
516 .batch_modeling_cmd(
517 ModelingCmdMeta::from_args_id(exec_state, &args, close_id),
518 ModelingCmd::from(
519 mcmd::ExtendPath::builder()
520 .path(sketch.id.into())
521 .segment(PathSegment::Line {
522 end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
523 .with_z(0.0)
524 .map(LengthUnit),
525 relative: false,
526 })
527 .build(),
528 ),
529 )
530 .await?;
531
532 let current_path = Path::ToPoint {
533 base: BasePath {
534 from: from.ignore_units(),
535 to: vertices[0],
536 tag: None,
537 units: sketch.units,
538 geo_meta: GeoMeta {
539 id: close_id,
540 metadata: args.source_range.into(),
541 },
542 },
543 };
544
545 sketch.paths.push(current_path);
546 sketch.is_closed = ProfileClosed::Explicitly;
547
548 exec_state
549 .batch_modeling_cmd(
550 ModelingCmdMeta::from_args(exec_state, &args),
551 ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
552 )
553 .await?;
554
555 Ok(sketch)
556}
557
558pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
560 let sketch_or_surface =
561 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
562 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
563 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
564 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
565 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
566 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
567
568 let sketch = inner_ellipse(
569 sketch_or_surface,
570 center,
571 major_radius,
572 major_axis,
573 minor_radius,
574 tag,
575 exec_state,
576 args,
577 )
578 .await?;
579 Ok(KclValue::Sketch {
580 value: Box::new(sketch),
581 })
582}
583
584#[allow(clippy::too_many_arguments)]
585async fn inner_ellipse(
586 sketch_surface_or_group: SketchOrSurface,
587 center: Option<[TyF64; 2]>,
588 major_radius: Option<TyF64>,
589 major_axis: Option<[TyF64; 2]>,
590 minor_radius: TyF64,
591 tag: Option<TagNode>,
592 exec_state: &mut ExecState,
593 args: Args,
594) -> Result<Sketch, KclError> {
595 let sketch_surface = sketch_surface_or_group.into_sketch_surface();
596 let center = center.unwrap_or(POINT_ZERO_ZERO);
597 let (center_u, ty) = untype_point(center.clone());
598 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
599
600 let major_axis = match (major_axis, major_radius) {
601 (Some(_), Some(_)) | (None, None) => {
602 return Err(KclError::new_type(KclErrorDetails::new(
603 "Provide either `majorAxis` or `majorRadius`.".to_string(),
604 vec![args.source_range],
605 )));
606 }
607 (Some(major_axis), None) => major_axis,
608 (None, Some(major_radius)) => [
609 major_radius.clone(),
610 TyF64 {
611 n: 0.0,
612 ty: major_radius.ty,
613 },
614 ],
615 };
616
617 let from = [
618 center_u[0] + major_axis[0].to_length_units(units),
619 center_u[1] + major_axis[1].to_length_units(units),
620 ];
621 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
622
623 let sketch =
624 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
625 .await?;
626
627 let angle_start = Angle::zero();
628 let angle_end = Angle::turn();
629
630 let id = exec_state.next_uuid();
631
632 let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
633 exec_state
634 .batch_modeling_cmd(
635 ModelingCmdMeta::from_args_id(exec_state, &args, id),
636 ModelingCmd::from(
637 mcmd::ExtendPath::builder()
638 .path(sketch.id.into())
639 .segment(PathSegment::Ellipse {
640 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
641 major_axis: axis,
642 minor_radius: LengthUnit(minor_radius.to_mm()),
643 start_angle: Angle::from_degrees(angle_start.to_degrees()),
644 end_angle: Angle::from_degrees(angle_end.to_degrees()),
645 })
646 .build(),
647 ),
648 )
649 .await?;
650
651 let current_path = Path::Ellipse {
652 base: BasePath {
653 from,
654 to: from,
655 tag: tag.clone(),
656 units,
657 geo_meta: GeoMeta {
658 id,
659 metadata: args.source_range.into(),
660 },
661 },
662 major_axis: major_axis.map(|x| x.to_length_units(units)),
663 minor_radius: minor_radius.to_length_units(units),
664 center: center_u,
665 ccw: angle_start < angle_end,
666 };
667
668 let mut new_sketch = sketch;
669 new_sketch.is_closed = ProfileClosed::Explicitly;
670 if let Some(tag) = &tag {
671 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
672 }
673
674 new_sketch.paths.push(current_path);
675
676 exec_state
677 .batch_modeling_cmd(
678 ModelingCmdMeta::from_args_id(exec_state, &args, id),
679 ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
680 )
681 .await?;
682
683 Ok(new_sketch)
684}
685
686pub(crate) fn get_radius(
687 radius: Option<TyF64>,
688 diameter: Option<TyF64>,
689 source_range: SourceRange,
690) -> Result<TyF64, KclError> {
691 get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
692}
693
694pub(crate) fn get_radius_labelled(
695 radius: Option<TyF64>,
696 diameter: Option<TyF64>,
697 source_range: SourceRange,
698 label_radius: &'static str,
699 label_diameter: &'static str,
700) -> Result<TyF64, KclError> {
701 match (radius, diameter) {
702 (Some(radius), None) => Ok(radius),
703 (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
704 (None, None) => Err(KclError::new_type(KclErrorDetails::new(
705 format!("This function needs either `{label_diameter}` or `{label_radius}`"),
706 vec![source_range],
707 ))),
708 (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
709 format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
710 vec![source_range],
711 ))),
712 }
713}