1use anyhow::Result;
4use kcl_api::UnitLength;
5use kcmc::ModelingCmd;
6use kcmc::each_cmd as mcmd;
7use kcmc::length_unit::LengthUnit;
8use kcmc::shared::Angle;
9use kcmc::shared::Point2d as KPoint2d;
10use kittycad_modeling_cmds::shared::PathSegment;
11use kittycad_modeling_cmds::{self as kcmc};
12use serde::Serialize;
13
14use super::args::TyF64;
15use super::utils::point_to_len_unit;
16use super::utils::point_to_mm;
17use super::utils::point_to_typed;
18use super::utils::untype_point;
19use super::utils::untyped_point_to_mm;
20use crate::SourceRange;
21use crate::errors::KclError;
22use crate::errors::KclErrorDetails;
23use crate::execution::BasePath;
24use crate::execution::ExecState;
25use crate::execution::GeoMeta;
26use crate::execution::KclValue;
27use crate::execution::ModelingCmdMeta;
28use crate::execution::Path;
29use crate::execution::ProfileClosed;
30use crate::execution::Sketch;
31use crate::execution::SketchSurface;
32use crate::execution::types::NumericTypeExt;
33use crate::execution::types::RuntimeType;
34use crate::execution::types::adjust_length;
35use crate::parsing::ast::types::TagNode;
36use crate::std::Args;
37use crate::std::utils::calculate_circle_center;
38use crate::std::utils::distance;
39
40#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
42#[ts(export)]
43#[serde(untagged)]
44pub enum SketchOrSurface {
45 SketchSurface(SketchSurface),
46 Sketch(Box<Sketch>),
47}
48
49impl SketchOrSurface {
50 pub fn into_sketch_surface(self) -> SketchSurface {
51 match self {
52 SketchOrSurface::SketchSurface(surface) => surface,
53 SketchOrSurface::Sketch(sketch) => sketch.on,
54 }
55 }
56}
57
58pub async fn rectangle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
60 let sketch_or_surface =
61 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
62 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
63 let corner = args.get_kw_arg_opt("corner", &RuntimeType::point2d(), exec_state)?;
64 let width: TyF64 = args.get_kw_arg("width", &RuntimeType::length(), exec_state)?;
65 let height: TyF64 = args.get_kw_arg("height", &RuntimeType::length(), exec_state)?;
66
67 inner_rectangle(sketch_or_surface, center, corner, width, height, exec_state, args)
68 .await
69 .map(Box::new)
70 .map(|value| KclValue::Sketch { value })
71}
72
73async fn inner_rectangle(
74 sketch_or_surface: SketchOrSurface,
75 center: Option<[TyF64; 2]>,
76 corner: Option<[TyF64; 2]>,
77 width: TyF64,
78 height: TyF64,
79 exec_state: &mut ExecState,
80 args: Args,
81) -> Result<Sketch, KclError> {
82 let sketch_surface = sketch_or_surface.into_sketch_surface();
83
84 let (ty, corner) = match (center, corner) {
86 (Some(center), None) => {
87 let units = center[0].ty.as_length().unwrap_or(UnitLength::Millimeters);
88 (
89 center[0].ty,
90 [
91 center[0].n - width.to_length_units(units) / 2.0,
92 center[1].n - height.to_length_units(units) / 2.0,
93 ],
94 )
95 }
96 (None, Some(corner)) => (corner[0].ty, [corner[0].n, corner[1].n]),
97 (None, None) => {
98 return Err(KclError::new_semantic(KclErrorDetails::new(
99 "You must supply either `corner` or `center` arguments, but not both".to_string(),
100 vec![args.source_range],
101 )));
102 }
103 (Some(_), Some(_)) => {
104 return Err(KclError::new_semantic(KclErrorDetails::new(
105 "You must supply either `corner` or `center` arguments, but not both".to_string(),
106 vec![args.source_range],
107 )));
108 }
109 };
110 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
111 let corner_t = [TyF64::new(corner[0], ty), TyF64::new(corner[1], ty)];
112 let width = width.to_length_units(units);
115 let height = height.to_length_units(units);
116
117 let sketch = crate::std::sketch::inner_start_profile(
119 sketch_surface,
120 corner_t,
121 None,
122 exec_state,
123 &args.ctx,
124 args.source_range,
125 )
126 .await?;
127 let sketch_id = sketch.id;
128 let deltas = [[width, 0.0], [0.0, height], [-width, 0.0], [0.0, -height]];
129 let ids = [
130 exec_state.next_uuid(),
131 exec_state.next_uuid(),
132 exec_state.next_uuid(),
133 exec_state.next_uuid(),
134 ];
135 for (id, delta) in ids.iter().copied().zip(deltas) {
136 exec_state
137 .batch_modeling_cmd(
138 ModelingCmdMeta::from_args_id(exec_state, &args, id),
139 ModelingCmd::from(
140 mcmd::ExtendPath::builder()
141 .path(sketch.id.into())
142 .segment(PathSegment::Line {
143 end: KPoint2d::from(untyped_point_to_mm(delta, units))
144 .with_z(0.0)
145 .map(LengthUnit),
146 relative: true,
147 })
148 .build(),
149 ),
150 )
151 .await?;
152 }
153 exec_state
154 .batch_modeling_cmd(
155 ModelingCmdMeta::from_args_id(exec_state, &args, sketch_id),
156 ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
157 )
158 .await?;
159
160 let mut new_sketch = sketch;
162 new_sketch.is_closed = ProfileClosed::Explicitly;
163 fn add(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
164 [a[0] + b[0], a[1] + b[1]]
165 }
166 let a = (corner, add(corner, deltas[0]));
167 let b = (a.1, add(a.1, deltas[1]));
168 let c = (b.1, add(b.1, deltas[2]));
169 let d = (c.1, add(c.1, deltas[3]));
170 for (id, (from, to)) in ids.into_iter().zip([a, b, c, d]) {
171 let current_path = Path::ToPoint {
172 base: BasePath {
173 from,
174 to,
175 tag: None,
176 units,
177 geo_meta: GeoMeta {
178 id,
179 metadata: args.source_range.into(),
180 },
181 },
182 };
183 new_sketch.paths.push_back(current_path);
184 }
185 Ok(new_sketch)
186}
187
188pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
190 let sketch_or_surface =
191 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
192 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
193 let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
194 let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
195 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
196
197 let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
198 Ok(KclValue::Sketch {
199 value: Box::new(sketch),
200 })
201}
202
203pub const POINT_ZERO_ZERO: [TyF64; 2] = [
204 TyF64::new(
205 0.0,
206 crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
207 ),
208 TyF64::new(
209 0.0,
210 crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
211 ),
212];
213
214pub(super) async fn inner_circle(
215 sketch_or_surface: SketchOrSurface,
216 center: Option<[TyF64; 2]>,
217 radius: Option<TyF64>,
218 diameter: Option<TyF64>,
219 tag: Option<TagNode>,
220 exec_state: &mut ExecState,
221 args: Args,
222) -> Result<Sketch, KclError> {
223 let sketch_surface = sketch_or_surface.into_sketch_surface();
224 let center = center.unwrap_or(POINT_ZERO_ZERO);
225 let (center_u, ty) = untype_point(center.clone());
226 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
227
228 let radius = get_radius(radius, diameter, args.source_range)?;
229 let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
230 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
231
232 let sketch =
233 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
234 .await?;
235
236 let angle_start = Angle::zero();
237 let angle_end = Angle::turn();
238
239 let id = exec_state.next_uuid();
240
241 exec_state
242 .batch_modeling_cmd(
243 ModelingCmdMeta::from_args_id(exec_state, &args, id),
244 ModelingCmd::from(
245 mcmd::ExtendPath::builder()
246 .path(sketch.id.into())
247 .segment(PathSegment::Arc {
248 start: angle_start,
249 end: angle_end,
250 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
251 radius: LengthUnit(radius.to_mm()),
252 relative: false,
253 })
254 .build(),
255 ),
256 )
257 .await?;
258
259 let current_path = Path::Circle {
260 base: BasePath {
261 from,
262 to: from,
263 tag: tag.clone(),
264 units,
265 geo_meta: GeoMeta {
266 id,
267 metadata: args.source_range.into(),
268 },
269 },
270 radius: radius.to_length_units(units),
271 center: center_u,
272 ccw: angle_start < angle_end,
273 };
274
275 let mut new_sketch = sketch;
276 new_sketch.is_closed = ProfileClosed::Explicitly;
277 if let Some(tag) = &tag {
278 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
279 }
280
281 new_sketch.paths.push_back(current_path);
282
283 exec_state
284 .batch_modeling_cmd(
285 ModelingCmdMeta::from_args_id(exec_state, &args, id),
286 ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
287 )
288 .await?;
289
290 Ok(new_sketch)
291}
292
293pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
295 let sketch_or_surface =
296 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
297 let p1 = args.get_kw_arg("p1", &RuntimeType::point2d(), exec_state)?;
298 let p2 = args.get_kw_arg("p2", &RuntimeType::point2d(), exec_state)?;
299 let p3 = args.get_kw_arg("p3", &RuntimeType::point2d(), exec_state)?;
300 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
301
302 let sketch = inner_circle_three_point(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
303 Ok(KclValue::Sketch {
304 value: Box::new(sketch),
305 })
306}
307
308async fn inner_circle_three_point(
311 sketch_surface_or_group: SketchOrSurface,
312 p1: [TyF64; 2],
313 p2: [TyF64; 2],
314 p3: [TyF64; 2],
315 tag: Option<TagNode>,
316 exec_state: &mut ExecState,
317 args: Args,
318) -> Result<Sketch, KclError> {
319 let ty = p1[0].ty;
320 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
321
322 let p1 = point_to_len_unit(p1, units);
323 let p2 = point_to_len_unit(p2, units);
324 let p3 = point_to_len_unit(p3, units);
325
326 let center = calculate_circle_center(p1, p2, p3);
327 let radius = distance(center, p2);
329
330 let sketch_surface = sketch_surface_or_group.into_sketch_surface();
331
332 let from = [TyF64::new(center[0] + radius, ty), TyF64::new(center[1], ty)];
333 let sketch = crate::std::sketch::inner_start_profile(
334 sketch_surface,
335 from.clone(),
336 None,
337 exec_state,
338 &args.ctx,
339 args.source_range,
340 )
341 .await?;
342
343 let angle_start = Angle::zero();
344 let angle_end = Angle::turn();
345
346 let id = exec_state.next_uuid();
347
348 exec_state
349 .batch_modeling_cmd(
350 ModelingCmdMeta::from_args_id(exec_state, &args, id),
351 ModelingCmd::from(
352 mcmd::ExtendPath::builder()
353 .path(sketch.id.into())
354 .segment(PathSegment::Arc {
355 start: angle_start,
356 end: angle_end,
357 center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
358 radius: adjust_length(units, radius, UnitLength::Millimeters).0.into(),
359 relative: false,
360 })
361 .build(),
362 ),
363 )
364 .await?;
365
366 let current_path = Path::CircleThreePoint {
367 base: BasePath {
368 from: untype_point(from.clone()).0,
370 to: untype_point(from).0,
371 tag: tag.clone(),
372 units,
373 geo_meta: GeoMeta {
374 id,
375 metadata: args.source_range.into(),
376 },
377 },
378 p1,
379 p2,
380 p3,
381 };
382
383 let mut new_sketch = sketch;
384 new_sketch.is_closed = ProfileClosed::Explicitly;
385 if let Some(tag) = &tag {
386 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
387 }
388
389 new_sketch.paths.push_back(current_path);
390
391 exec_state
392 .batch_modeling_cmd(
393 ModelingCmdMeta::from_args_id(exec_state, &args, id),
394 ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
395 )
396 .await?;
397
398 Ok(new_sketch)
399}
400
401#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, Default)]
403#[ts(export)]
404#[serde(rename_all = "lowercase")]
405pub enum PolygonType {
406 #[default]
407 Inscribed,
408 Circumscribed,
409}
410
411pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
413 let sketch_or_surface =
414 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
415 let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
416 let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
417 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
418 let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
419
420 let sketch = inner_polygon(
421 sketch_or_surface,
422 radius,
423 num_sides.n as u64,
424 center,
425 inscribed,
426 exec_state,
427 args,
428 )
429 .await?;
430 Ok(KclValue::Sketch {
431 value: Box::new(sketch),
432 })
433}
434
435#[allow(clippy::too_many_arguments)]
436async fn inner_polygon(
437 sketch_surface_or_group: SketchOrSurface,
438 radius: TyF64,
439 num_sides: u64,
440 center: Option<[TyF64; 2]>,
441 inscribed: Option<bool>,
442 exec_state: &mut ExecState,
443 args: Args,
444) -> Result<Sketch, KclError> {
445 let center = center.unwrap_or(POINT_ZERO_ZERO);
446 if num_sides < 3 {
447 return Err(KclError::new_type(KclErrorDetails::new(
448 "Polygon must have at least 3 sides".to_string(),
449 vec![args.source_range],
450 )));
451 }
452
453 if radius.n <= 0.0 {
454 return Err(KclError::new_type(KclErrorDetails::new(
455 "Radius must be greater than 0".to_string(),
456 vec![args.source_range],
457 )));
458 }
459
460 let (sketch_surface, units) = match sketch_surface_or_group {
461 SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.as_length().unwrap_or(UnitLength::Millimeters)),
462 SketchOrSurface::Sketch(group) => (group.on, group.units),
463 };
464
465 let half_angle = std::f64::consts::PI / num_sides as f64;
466
467 let radius_to_vertices = if inscribed.unwrap_or(true) {
468 radius.n
470 } else {
471 radius.n / libm::cos(half_angle)
473 };
474
475 let angle_step = std::f64::consts::TAU / num_sides as f64;
476
477 let center_u = point_to_len_unit(center, units);
478
479 let vertices: Vec<[f64; 2]> = (0..num_sides)
480 .map(|i| {
481 let angle = angle_step * i as f64;
482 [
483 center_u[0] + radius_to_vertices * libm::cos(angle),
484 center_u[1] + radius_to_vertices * libm::sin(angle),
485 ]
486 })
487 .collect();
488
489 let mut sketch = crate::std::sketch::inner_start_profile(
490 sketch_surface,
491 point_to_typed(vertices[0], units),
492 None,
493 exec_state,
494 &args.ctx,
495 args.source_range,
496 )
497 .await?;
498
499 for vertex in vertices.iter().skip(1) {
501 let from = sketch.current_pen_position()?;
502 let id = exec_state.next_uuid();
503
504 exec_state
505 .batch_modeling_cmd(
506 ModelingCmdMeta::from_args_id(exec_state, &args, id),
507 ModelingCmd::from(
508 mcmd::ExtendPath::builder()
509 .path(sketch.id.into())
510 .segment(PathSegment::Line {
511 end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
512 .with_z(0.0)
513 .map(LengthUnit),
514 relative: false,
515 })
516 .build(),
517 ),
518 )
519 .await?;
520
521 let current_path = Path::ToPoint {
522 base: BasePath {
523 from: from.ignore_units(),
524 to: *vertex,
525 tag: None,
526 units: sketch.units,
527 geo_meta: GeoMeta {
528 id,
529 metadata: args.source_range.into(),
530 },
531 },
532 };
533
534 sketch.paths.push_back(current_path);
535 }
536
537 let from = sketch.current_pen_position()?;
539 let close_id = exec_state.next_uuid();
540
541 exec_state
542 .batch_modeling_cmd(
543 ModelingCmdMeta::from_args_id(exec_state, &args, close_id),
544 ModelingCmd::from(
545 mcmd::ExtendPath::builder()
546 .path(sketch.id.into())
547 .segment(PathSegment::Line {
548 end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
549 .with_z(0.0)
550 .map(LengthUnit),
551 relative: false,
552 })
553 .build(),
554 ),
555 )
556 .await?;
557
558 let current_path = Path::ToPoint {
559 base: BasePath {
560 from: from.ignore_units(),
561 to: vertices[0],
562 tag: None,
563 units: sketch.units,
564 geo_meta: GeoMeta {
565 id: close_id,
566 metadata: args.source_range.into(),
567 },
568 },
569 };
570
571 sketch.paths.push_back(current_path);
572 sketch.is_closed = ProfileClosed::Explicitly;
573
574 exec_state
575 .batch_modeling_cmd(
576 ModelingCmdMeta::from_args(exec_state, &args),
577 ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
578 )
579 .await?;
580
581 Ok(sketch)
582}
583
584pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
586 let sketch_or_surface =
587 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
588 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
589 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
590 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
591 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
592 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
593
594 let sketch = inner_ellipse(
595 sketch_or_surface,
596 center,
597 major_radius,
598 major_axis,
599 minor_radius,
600 tag,
601 exec_state,
602 args,
603 )
604 .await?;
605 Ok(KclValue::Sketch {
606 value: Box::new(sketch),
607 })
608}
609
610#[allow(clippy::too_many_arguments)]
611async fn inner_ellipse(
612 sketch_surface_or_group: SketchOrSurface,
613 center: Option<[TyF64; 2]>,
614 major_radius: Option<TyF64>,
615 major_axis: Option<[TyF64; 2]>,
616 minor_radius: TyF64,
617 tag: Option<TagNode>,
618 exec_state: &mut ExecState,
619 args: Args,
620) -> Result<Sketch, KclError> {
621 let sketch_surface = sketch_surface_or_group.into_sketch_surface();
622 let center = center.unwrap_or(POINT_ZERO_ZERO);
623 let (center_u, ty) = untype_point(center.clone());
624 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
625
626 let major_axis = match (major_axis, major_radius) {
627 (Some(_), Some(_)) | (None, None) => {
628 return Err(KclError::new_type(KclErrorDetails::new(
629 "Provide either `majorAxis` or `majorRadius`.".to_string(),
630 vec![args.source_range],
631 )));
632 }
633 (Some(major_axis), None) => major_axis,
634 (None, Some(major_radius)) => [
635 major_radius.clone(),
636 TyF64 {
637 n: 0.0,
638 ty: major_radius.ty,
639 },
640 ],
641 };
642
643 let from = [
644 center_u[0] + major_axis[0].to_length_units(units),
645 center_u[1] + major_axis[1].to_length_units(units),
646 ];
647 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
648
649 let sketch =
650 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, &args.ctx, args.source_range)
651 .await?;
652
653 let angle_start = Angle::zero();
654 let angle_end = Angle::turn();
655
656 let id = exec_state.next_uuid();
657
658 let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
659 exec_state
660 .batch_modeling_cmd(
661 ModelingCmdMeta::from_args_id(exec_state, &args, id),
662 ModelingCmd::from(
663 mcmd::ExtendPath::builder()
664 .path(sketch.id.into())
665 .segment(PathSegment::Ellipse {
666 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
667 major_axis: axis,
668 minor_radius: LengthUnit(minor_radius.to_mm()),
669 start_angle: Angle::from_degrees(angle_start.to_degrees()),
670 end_angle: Angle::from_degrees(angle_end.to_degrees()),
671 })
672 .build(),
673 ),
674 )
675 .await?;
676
677 let current_path = Path::Ellipse {
678 base: BasePath {
679 from,
680 to: from,
681 tag: tag.clone(),
682 units,
683 geo_meta: GeoMeta {
684 id,
685 metadata: args.source_range.into(),
686 },
687 },
688 major_axis: major_axis.map(|x| x.to_length_units(units)),
689 minor_radius: minor_radius.to_length_units(units),
690 center: center_u,
691 ccw: angle_start < angle_end,
692 };
693
694 let mut new_sketch = sketch;
695 new_sketch.is_closed = ProfileClosed::Explicitly;
696 if let Some(tag) = &tag {
697 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
698 }
699
700 new_sketch.paths.push_back(current_path);
701
702 exec_state
703 .batch_modeling_cmd(
704 ModelingCmdMeta::from_args_id(exec_state, &args, id),
705 ModelingCmd::from(mcmd::ClosePath::builder().path_id(new_sketch.id).build()),
706 )
707 .await?;
708
709 Ok(new_sketch)
710}
711
712pub(crate) fn get_radius(
713 radius: Option<TyF64>,
714 diameter: Option<TyF64>,
715 source_range: SourceRange,
716) -> Result<TyF64, KclError> {
717 get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
718}
719
720pub(crate) fn get_radius_labelled(
721 radius: Option<TyF64>,
722 diameter: Option<TyF64>,
723 source_range: SourceRange,
724 label_radius: &'static str,
725 label_diameter: &'static str,
726) -> Result<TyF64, KclError> {
727 match (radius, diameter) {
728 (Some(radius), None) => Ok(radius),
729 (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
730 (None, None) => Err(KclError::new_type(KclErrorDetails::new(
731 format!("This function needs either `{label_diameter}` or `{label_radius}`"),
732 vec![source_range],
733 ))),
734 (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
735 format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
736 vec![source_range],
737 ))),
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use kittycad_modeling_cmds::ModelingCmd;
744 use kittycad_modeling_cmds::shared::PathSegment;
745
746 use crate::execution::KclValue;
747 use crate::execution::parse_execute;
748
749 async fn rectangle_in_mm(code: &str) -> ([f64; 2], Vec<[f64; 2]>, Vec<[f64; 2]>) {
753 let result = parse_execute(code).await.unwrap();
754
755 let mut start = None;
756 let mut segments = Vec::new();
757 for command in result.root_module_artifact_commands() {
758 match &command.command {
759 ModelingCmd::MovePathPen(move_pen) => start = Some([move_pen.to.x.0, move_pen.to.y.0]),
760 ModelingCmd::ExtendPath(extend) => match &extend.segment {
761 PathSegment::Line { end, relative: true } => segments.push([end.x.0, end.y.0]),
762 other => panic!("expected a relative line, got {other:?}"),
763 },
764 _ => {}
765 }
766 }
767
768 let KclValue::Sketch { value: sketch } = result.variable("r") else {
769 panic!("expected `r` to be a sketch");
770 };
771 let corners = sketch
772 .paths
773 .iter()
774 .map(|path| {
775 let [x, y] = path.get_to();
776 [x.to_mm(), y.to_mm()]
777 })
778 .collect();
779
780 (
781 start.expect("expected the profile to start somewhere"),
782 segments,
783 corners,
784 )
785 }
786
787 fn assert_close(actual: &[[f64; 2]], expected: &[[f64; 2]], code: &str) {
788 assert_eq!(actual.len(), expected.len(), "{code}");
789 for (a, e) in actual.iter().zip(expected) {
790 assert!(
791 (a[0] - e[0]).abs() < 1e-9 && (a[1] - e[1]).abs() < 1e-9,
792 "expected {expected:?}, got {actual:?} for:\n{code}"
793 );
794 }
795 }
796
797 #[tokio::test(flavor = "multi_thread")]
798 async fn rectangle_converts_width_and_height_to_the_units_of_the_center_or_corner() {
799 let centered = [
803 "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(center = [0, 0], width = 1in, height = 2in)",
804 "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(center = [0, 0], width = 25.4, height = 50.8)",
805 "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(center = [0in, 0in], width = 1in, height = 2in)",
806 "@settings(kclVersion = 2.0, defaultLengthUnit = in)\nr = startSketchOn(XY) |> rectangle(center = [0, 0], width = 25.4mm, height = 50.8mm)",
807 "@settings(kclVersion = 2.0, defaultLengthUnit = in)\nr = startSketchOn(XY) |> rectangle(center = [0, 0], width = 1, height = 2)",
808 ];
809 let cornered = [
810 "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(corner = [0, 0], width = 1in, height = 2in)",
811 "@settings(kclVersion = 2.0, defaultLengthUnit = mm)\nr = startSketchOn(XY) |> rectangle(corner = [0, 0], width = 25.4, height = 50.8)",
812 "@settings(kclVersion = 2.0, defaultLengthUnit = in)\nr = startSketchOn(XY) |> rectangle(corner = [0, 0], width = 25.4mm, height = 50.8mm)",
813 ];
814 let segments = [[25.4, 0.0], [0.0, 50.8], [-25.4, 0.0], [0.0, -50.8]];
815
816 for (codes, start, corners) in [
817 (
818 ¢ered[..],
819 [-12.7, -25.4],
820 [[12.7, -25.4], [12.7, 25.4], [-12.7, 25.4], [-12.7, -25.4]],
821 ),
822 (
823 &cornered[..],
824 [0.0, 0.0],
825 [[25.4, 0.0], [25.4, 50.8], [0.0, 50.8], [0.0, 0.0]],
826 ),
827 ] {
828 for code in codes {
829 let (actual_start, actual_segments, actual_corners) = rectangle_in_mm(code).await;
830 assert_close(&[actual_start], &[start], code);
831 assert_close(&actual_segments, &segments, code);
832 assert_close(&actual_corners, &corners, code);
833 }
834 }
835 }
836}