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 as kcmc;
10use kittycad_modeling_cmds::shared::PathSegment;
11use serde::Serialize;
12
13use super::{
14 args::TyF64,
15 utils::{point_to_len_unit, point_to_mm, point_to_typed, untype_point, untyped_point_to_mm},
16};
17use crate::{
18 SourceRange,
19 errors::{KclError, KclErrorDetails},
20 execution::{
21 BasePath, ExecState, GeoMeta, KclValue, ModelingCmdMeta, Path, Sketch, SketchSurface,
22 types::{RuntimeType, UnitLen},
23 },
24 parsing::ast::types::TagNode,
25 std::{
26 Args,
27 utils::{calculate_circle_center, distance},
28 },
29};
30
31#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
33#[ts(export)]
34#[serde(untagged)]
35pub enum SketchOrSurface {
36 SketchSurface(SketchSurface),
37 Sketch(Box<Sketch>),
38}
39
40pub async fn rectangle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
42 let sketch_or_surface =
43 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
44 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
45 let corner = args.get_kw_arg_opt("corner", &RuntimeType::point2d(), exec_state)?;
46 let width: TyF64 = args.get_kw_arg("width", &RuntimeType::length(), exec_state)?;
47 let height: TyF64 = args.get_kw_arg("height", &RuntimeType::length(), exec_state)?;
48
49 inner_rectangle(sketch_or_surface, center, corner, width, height, exec_state, args)
50 .await
51 .map(Box::new)
52 .map(|value| KclValue::Sketch { value })
53}
54
55async fn inner_rectangle(
56 sketch_or_surface: SketchOrSurface,
57 center: Option<[TyF64; 2]>,
58 corner: Option<[TyF64; 2]>,
59 width: TyF64,
60 height: TyF64,
61 exec_state: &mut ExecState,
62 args: Args,
63) -> Result<Sketch, KclError> {
64 let sketch_surface = match sketch_or_surface {
65 SketchOrSurface::SketchSurface(surface) => surface,
66 SketchOrSurface::Sketch(s) => s.on,
67 };
68
69 let (ty, corner) = match (center, corner) {
71 (Some(center), None) => (
72 center[0].ty,
73 [center[0].n - width.n / 2.0, center[1].n - height.n / 2.0],
74 ),
75 (None, Some(corner)) => (corner[0].ty, [corner[0].n, corner[1].n]),
76 (None, None) => {
77 return Err(KclError::new_semantic(KclErrorDetails::new(
78 "You must supply either `corner` or `center` arguments, but not both".to_string(),
79 vec![args.source_range],
80 )));
81 }
82 (Some(_), Some(_)) => {
83 return Err(KclError::new_semantic(KclErrorDetails::new(
84 "You must supply either `corner` or `center` arguments, but not both".to_string(),
85 vec![args.source_range],
86 )));
87 }
88 };
89 let units = ty.expect_length();
90 let corner_t = [TyF64::new(corner[0], ty), TyF64::new(corner[1], ty)];
91
92 let sketch =
94 crate::std::sketch::inner_start_profile(sketch_surface, corner_t, None, exec_state, args.clone()).await?;
95 let sketch_id = sketch.id;
96 let deltas = [[width.n, 0.0], [0.0, height.n], [-width.n, 0.0], [0.0, -height.n]];
97 let ids = [
98 exec_state.next_uuid(),
99 exec_state.next_uuid(),
100 exec_state.next_uuid(),
101 exec_state.next_uuid(),
102 ];
103 for (id, delta) in ids.iter().copied().zip(deltas) {
104 exec_state
105 .batch_modeling_cmd(
106 ModelingCmdMeta::from_args_id(&args, id),
107 ModelingCmd::from(mcmd::ExtendPath {
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.expect_length();
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 path: sketch.id.into(),
202 segment: PathSegment::Arc {
203 start: angle_start,
204 end: angle_end,
205 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
206 radius: LengthUnit(radius.to_mm()),
207 relative: false,
208 },
209 }),
210 )
211 .await?;
212
213 let current_path = Path::Circle {
214 base: BasePath {
215 from,
216 to: from,
217 tag: tag.clone(),
218 units,
219 geo_meta: GeoMeta {
220 id,
221 metadata: args.source_range.into(),
222 },
223 },
224 radius: radius.to_length_units(units),
225 center: center_u,
226 ccw: angle_start < angle_end,
227 };
228
229 let mut new_sketch = sketch;
230 new_sketch.is_closed = true;
231 if let Some(tag) = &tag {
232 new_sketch.add_tag(tag, ¤t_path, exec_state);
233 }
234
235 new_sketch.paths.push(current_path);
236
237 exec_state
238 .batch_modeling_cmd(
239 ModelingCmdMeta::from_args_id(&args, id),
240 ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
241 )
242 .await?;
243
244 Ok(new_sketch)
245}
246
247pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
249 let sketch_or_surface =
250 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
251 let p1 = args.get_kw_arg("p1", &RuntimeType::point2d(), exec_state)?;
252 let p2 = args.get_kw_arg("p2", &RuntimeType::point2d(), exec_state)?;
253 let p3 = args.get_kw_arg("p3", &RuntimeType::point2d(), exec_state)?;
254 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
255
256 let sketch = inner_circle_three_point(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
257 Ok(KclValue::Sketch {
258 value: Box::new(sketch),
259 })
260}
261
262async fn inner_circle_three_point(
265 sketch_surface_or_group: SketchOrSurface,
266 p1: [TyF64; 2],
267 p2: [TyF64; 2],
268 p3: [TyF64; 2],
269 tag: Option<TagNode>,
270 exec_state: &mut ExecState,
271 args: Args,
272) -> Result<Sketch, KclError> {
273 let ty = p1[0].ty;
274 let units = ty.expect_length();
275
276 let p1 = point_to_len_unit(p1, units);
277 let p2 = point_to_len_unit(p2, units);
278 let p3 = point_to_len_unit(p3, units);
279
280 let center = calculate_circle_center(p1, p2, p3);
281 let radius = distance(center, p2);
283
284 let sketch_surface = match sketch_surface_or_group {
285 SketchOrSurface::SketchSurface(surface) => surface,
286 SketchOrSurface::Sketch(group) => group.on,
287 };
288
289 let from = [TyF64::new(center[0] + radius, ty), TyF64::new(center[1], ty)];
290 let sketch =
291 crate::std::sketch::inner_start_profile(sketch_surface, from.clone(), None, exec_state, args.clone()).await?;
292
293 let angle_start = Angle::zero();
294 let angle_end = Angle::turn();
295
296 let id = exec_state.next_uuid();
297
298 exec_state
299 .batch_modeling_cmd(
300 ModelingCmdMeta::from_args_id(&args, id),
301 ModelingCmd::from(mcmd::ExtendPath {
302 path: sketch.id.into(),
303 segment: PathSegment::Arc {
304 start: angle_start,
305 end: angle_end,
306 center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
307 radius: units.adjust_to(radius, UnitLen::Mm).0.into(),
308 relative: false,
309 },
310 }),
311 )
312 .await?;
313
314 let current_path = Path::CircleThreePoint {
315 base: BasePath {
316 from: untype_point(from.clone()).0,
318 to: untype_point(from).0,
319 tag: tag.clone(),
320 units,
321 geo_meta: GeoMeta {
322 id,
323 metadata: args.source_range.into(),
324 },
325 },
326 p1,
327 p2,
328 p3,
329 };
330
331 let mut new_sketch = sketch;
332 new_sketch.is_closed = true;
333 if let Some(tag) = &tag {
334 new_sketch.add_tag(tag, ¤t_path, exec_state);
335 }
336
337 new_sketch.paths.push(current_path);
338
339 exec_state
340 .batch_modeling_cmd(
341 ModelingCmdMeta::from_args_id(&args, id),
342 ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
343 )
344 .await?;
345
346 Ok(new_sketch)
347}
348
349#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, Default)]
351#[ts(export)]
352#[serde(rename_all = "lowercase")]
353pub enum PolygonType {
354 #[default]
355 Inscribed,
356 Circumscribed,
357}
358
359pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
361 let sketch_or_surface =
362 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
363 let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
364 let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
365 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
366 let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
367
368 let sketch = inner_polygon(
369 sketch_or_surface,
370 radius,
371 num_sides.n as u64,
372 center,
373 inscribed,
374 exec_state,
375 args,
376 )
377 .await?;
378 Ok(KclValue::Sketch {
379 value: Box::new(sketch),
380 })
381}
382
383#[allow(clippy::too_many_arguments)]
384async fn inner_polygon(
385 sketch_surface_or_group: SketchOrSurface,
386 radius: TyF64,
387 num_sides: u64,
388 center: [TyF64; 2],
389 inscribed: Option<bool>,
390 exec_state: &mut ExecState,
391 args: Args,
392) -> Result<Sketch, KclError> {
393 if num_sides < 3 {
394 return Err(KclError::new_type(KclErrorDetails::new(
395 "Polygon must have at least 3 sides".to_string(),
396 vec![args.source_range],
397 )));
398 }
399
400 if radius.n <= 0.0 {
401 return Err(KclError::new_type(KclErrorDetails::new(
402 "Radius must be greater than 0".to_string(),
403 vec![args.source_range],
404 )));
405 }
406
407 let (sketch_surface, units) = match sketch_surface_or_group {
408 SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.expect_length()),
409 SketchOrSurface::Sketch(group) => (group.on, group.units),
410 };
411
412 let half_angle = std::f64::consts::PI / num_sides as f64;
413
414 let radius_to_vertices = if inscribed.unwrap_or(true) {
415 radius.n
417 } else {
418 radius.n / libm::cos(half_angle)
420 };
421
422 let angle_step = std::f64::consts::TAU / num_sides as f64;
423
424 let center_u = point_to_len_unit(center, units);
425
426 let vertices: Vec<[f64; 2]> = (0..num_sides)
427 .map(|i| {
428 let angle = angle_step * i as f64;
429 [
430 center_u[0] + radius_to_vertices * libm::cos(angle),
431 center_u[1] + radius_to_vertices * libm::sin(angle),
432 ]
433 })
434 .collect();
435
436 let mut sketch = crate::std::sketch::inner_start_profile(
437 sketch_surface,
438 point_to_typed(vertices[0], units),
439 None,
440 exec_state,
441 args.clone(),
442 )
443 .await?;
444
445 for vertex in vertices.iter().skip(1) {
447 let from = sketch.current_pen_position()?;
448 let id = exec_state.next_uuid();
449
450 exec_state
451 .batch_modeling_cmd(
452 ModelingCmdMeta::from_args_id(&args, id),
453 ModelingCmd::from(mcmd::ExtendPath {
454 path: sketch.id.into(),
455 segment: PathSegment::Line {
456 end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
457 .with_z(0.0)
458 .map(LengthUnit),
459 relative: false,
460 },
461 }),
462 )
463 .await?;
464
465 let current_path = Path::ToPoint {
466 base: BasePath {
467 from: from.ignore_units(),
468 to: *vertex,
469 tag: None,
470 units: sketch.units,
471 geo_meta: GeoMeta {
472 id,
473 metadata: args.source_range.into(),
474 },
475 },
476 };
477
478 sketch.paths.push(current_path);
479 }
480
481 let from = sketch.current_pen_position()?;
483 let close_id = exec_state.next_uuid();
484
485 exec_state
486 .batch_modeling_cmd(
487 ModelingCmdMeta::from_args_id(&args, close_id),
488 ModelingCmd::from(mcmd::ExtendPath {
489 path: sketch.id.into(),
490 segment: PathSegment::Line {
491 end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
492 .with_z(0.0)
493 .map(LengthUnit),
494 relative: false,
495 },
496 }),
497 )
498 .await?;
499
500 let current_path = Path::ToPoint {
501 base: BasePath {
502 from: from.ignore_units(),
503 to: vertices[0],
504 tag: None,
505 units: sketch.units,
506 geo_meta: GeoMeta {
507 id: close_id,
508 metadata: args.source_range.into(),
509 },
510 },
511 };
512
513 sketch.paths.push(current_path);
514
515 exec_state
516 .batch_modeling_cmd(
517 (&args).into(),
518 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
519 )
520 .await?;
521
522 Ok(sketch)
523}
524
525pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
527 let sketch_or_surface =
528 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
529 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
530 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
531 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
532 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
533 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
534
535 let sketch = inner_ellipse(
536 sketch_or_surface,
537 center,
538 major_radius,
539 major_axis,
540 minor_radius,
541 tag,
542 exec_state,
543 args,
544 )
545 .await?;
546 Ok(KclValue::Sketch {
547 value: Box::new(sketch),
548 })
549}
550
551#[allow(clippy::too_many_arguments)]
552async fn inner_ellipse(
553 sketch_surface_or_group: SketchOrSurface,
554 center: [TyF64; 2],
555 major_radius: Option<TyF64>,
556 major_axis: Option<[TyF64; 2]>,
557 minor_radius: TyF64,
558 tag: Option<TagNode>,
559 exec_state: &mut ExecState,
560 args: Args,
561) -> Result<Sketch, KclError> {
562 let sketch_surface = match sketch_surface_or_group {
563 SketchOrSurface::SketchSurface(surface) => surface,
564 SketchOrSurface::Sketch(group) => group.on,
565 };
566 let (center_u, ty) = untype_point(center.clone());
567 let units = ty.expect_length();
568
569 let major_axis = match (major_axis, major_radius) {
570 (Some(_), Some(_)) | (None, None) => {
571 return Err(KclError::new_type(KclErrorDetails::new(
572 "Provide either `majorAxis` or `majorRadius`.".to_string(),
573 vec![args.source_range],
574 )));
575 }
576 (Some(major_axis), None) => major_axis,
577 (None, Some(major_radius)) => [
578 major_radius.clone(),
579 TyF64 {
580 n: 0.0,
581 ty: major_radius.ty,
582 },
583 ],
584 };
585
586 let from = [
587 center_u[0] + major_axis[0].to_length_units(units),
588 center_u[1] + major_axis[1].to_length_units(units),
589 ];
590 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
591
592 let sketch =
593 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
594
595 let angle_start = Angle::zero();
596 let angle_end = Angle::turn();
597
598 let id = exec_state.next_uuid();
599
600 let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
601 exec_state
602 .batch_modeling_cmd(
603 ModelingCmdMeta::from_args_id(&args, id),
604 ModelingCmd::from(mcmd::ExtendPath {
605 path: sketch.id.into(),
606 segment: PathSegment::Ellipse {
607 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
608 major_axis: axis,
609 minor_radius: LengthUnit(minor_radius.to_mm()),
610 start_angle: Angle::from_degrees(angle_start.to_degrees()),
611 end_angle: Angle::from_degrees(angle_end.to_degrees()),
612 },
613 }),
614 )
615 .await?;
616
617 let current_path = Path::Ellipse {
618 base: BasePath {
619 from,
620 to: from,
621 tag: tag.clone(),
622 units,
623 geo_meta: GeoMeta {
624 id,
625 metadata: args.source_range.into(),
626 },
627 },
628 major_axis: major_axis.map(|x| x.to_length_units(units)),
629 minor_radius: minor_radius.to_length_units(units),
630 center: center_u,
631 ccw: angle_start < angle_end,
632 };
633
634 let mut new_sketch = sketch;
635 new_sketch.is_closed = true;
636 if let Some(tag) = &tag {
637 new_sketch.add_tag(tag, ¤t_path, exec_state);
638 }
639
640 new_sketch.paths.push(current_path);
641
642 exec_state
643 .batch_modeling_cmd(
644 ModelingCmdMeta::from_args_id(&args, id),
645 ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
646 )
647 .await?;
648
649 Ok(new_sketch)
650}
651
652pub(crate) fn get_radius(
653 radius: Option<TyF64>,
654 diameter: Option<TyF64>,
655 source_range: SourceRange,
656) -> Result<TyF64, KclError> {
657 get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
658}
659
660pub(crate) fn get_radius_labelled(
661 radius: Option<TyF64>,
662 diameter: Option<TyF64>,
663 source_range: SourceRange,
664 label_radius: &'static str,
665 label_diameter: &'static str,
666) -> Result<TyF64, KclError> {
667 match (radius, diameter) {
668 (Some(radius), None) => Ok(radius),
669 (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
670 (None, None) => Err(KclError::new_type(KclErrorDetails::new(
671 format!("This function needs either `{label_diameter}` or `{label_radius}`"),
672 vec![source_range],
673 ))),
674 (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
675 format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
676 vec![source_range],
677 ))),
678 }
679}