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("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, ¤t_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
248pub 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
263async 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 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 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, ¤t_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#[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
361pub 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 radius.n
419 } else {
420 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 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 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
529pub 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, ¤t_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}