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