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