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 schemars::JsonSchema;
12use serde::Serialize;
13
14use super::{
15 args::TyF64,
16 utils::{point_to_len_unit, point_to_mm, point_to_typed, untype_point, untyped_point_to_mm},
17};
18use crate::{
19 SourceRange,
20 errors::{KclError, KclErrorDetails},
21 execution::{
22 BasePath, ExecState, GeoMeta, KclValue, ModelingCmdMeta, Path, Sketch, SketchSurface,
23 types::{RuntimeType, UnitLen},
24 },
25 parsing::ast::types::TagNode,
26 std::{
27 Args,
28 utils::{calculate_circle_center, distance},
29 },
30};
31
32#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
34#[ts(export)]
35#[serde(untagged)]
36pub enum SketchOrSurface {
37 SketchSurface(SketchSurface),
38 Sketch(Box<Sketch>),
39}
40
41pub async fn rectangle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
43 let sketch_or_surface =
44 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
45 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
46 let corner = args.get_kw_arg_opt("corner", &RuntimeType::point2d(), exec_state)?;
47 let width: TyF64 = args.get_kw_arg("width", &RuntimeType::length(), exec_state)?;
48 let height: TyF64 = args.get_kw_arg("height", &RuntimeType::length(), exec_state)?;
49
50 inner_rectangle(sketch_or_surface, center, corner, width, height, exec_state, args)
51 .await
52 .map(Box::new)
53 .map(|value| KclValue::Sketch { value })
54}
55
56async fn inner_rectangle(
57 sketch_or_surface: SketchOrSurface,
58 center: Option<[TyF64; 2]>,
59 corner: Option<[TyF64; 2]>,
60 width: TyF64,
61 height: TyF64,
62 exec_state: &mut ExecState,
63 args: Args,
64) -> Result<Sketch, KclError> {
65 let sketch_surface = match sketch_or_surface {
66 SketchOrSurface::SketchSurface(surface) => surface,
67 SketchOrSurface::Sketch(s) => s.on,
68 };
69
70 let (ty, corner) = match (center, corner) {
72 (Some(center), None) => (
73 center[0].ty,
74 [center[0].n - width.n / 2.0, center[1].n - height.n / 2.0],
75 ),
76 (None, Some(corner)) => (corner[0].ty, [corner[0].n, corner[1].n]),
77 (None, None) => {
78 return Err(KclError::new_semantic(KclErrorDetails::new(
79 "You must supply either `corner` or `center` arguments, but not both".to_string(),
80 vec![args.source_range],
81 )));
82 }
83 (Some(_), Some(_)) => {
84 return Err(KclError::new_semantic(KclErrorDetails::new(
85 "You must supply either `corner` or `center` arguments, but not both".to_string(),
86 vec![args.source_range],
87 )));
88 }
89 };
90 let units = ty.expect_length();
91 let corner_t = [TyF64::new(corner[0], ty), TyF64::new(corner[1], ty)];
92
93 let sketch =
95 crate::std::sketch::inner_start_profile(sketch_surface, corner_t, None, exec_state, args.clone()).await?;
96 let sketch_id = sketch.id;
97 let deltas = [[width.n, 0.0], [0.0, height.n], [-width.n, 0.0], [0.0, -height.n]];
98 let ids = [
99 exec_state.next_uuid(),
100 exec_state.next_uuid(),
101 exec_state.next_uuid(),
102 exec_state.next_uuid(),
103 ];
104 for (id, delta) in ids.iter().copied().zip(deltas) {
105 exec_state
106 .batch_modeling_cmd(
107 ModelingCmdMeta::from_args_id(&args, id),
108 ModelingCmd::from(mcmd::ExtendPath {
109 path: sketch.id.into(),
110 segment: PathSegment::Line {
111 end: KPoint2d::from(untyped_point_to_mm(delta, units))
112 .with_z(0.0)
113 .map(LengthUnit),
114 relative: true,
115 },
116 }),
117 )
118 .await?;
119 }
120 exec_state
121 .batch_modeling_cmd(
122 ModelingCmdMeta::from_args_id(&args, sketch_id),
123 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
124 )
125 .await?;
126
127 let mut new_sketch = sketch;
129 new_sketch.is_closed = true;
130 fn add(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
131 [a[0] + b[0], a[1] + b[1]]
132 }
133 let a = (corner, add(corner, deltas[0]));
134 let b = (a.1, add(a.1, deltas[1]));
135 let c = (b.1, add(b.1, deltas[2]));
136 let d = (c.1, add(c.1, deltas[3]));
137 for (id, (from, to)) in ids.into_iter().zip([a, b, c, d]) {
138 let current_path = Path::ToPoint {
139 base: BasePath {
140 from,
141 to,
142 tag: None,
143 units,
144 geo_meta: GeoMeta {
145 id,
146 metadata: args.source_range.into(),
147 },
148 },
149 };
150 new_sketch.paths.push(current_path);
151 }
152 Ok(new_sketch)
153}
154
155pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
157 let sketch_or_surface =
158 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
159 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
160 let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
161 let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
162 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
163
164 let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
165 Ok(KclValue::Sketch {
166 value: Box::new(sketch),
167 })
168}
169
170async fn inner_circle(
171 sketch_or_surface: SketchOrSurface,
172 center: [TyF64; 2],
173 radius: Option<TyF64>,
174 diameter: Option<TyF64>,
175 tag: Option<TagNode>,
176 exec_state: &mut ExecState,
177 args: Args,
178) -> Result<Sketch, KclError> {
179 let sketch_surface = match sketch_or_surface {
180 SketchOrSurface::SketchSurface(surface) => surface,
181 SketchOrSurface::Sketch(s) => s.on,
182 };
183 let (center_u, ty) = untype_point(center.clone());
184 let units = ty.expect_length();
185
186 let radius = get_radius(radius, diameter, args.source_range)?;
187 let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
188 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
189
190 let sketch =
191 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
192
193 let angle_start = Angle::zero();
194 let angle_end = Angle::turn();
195
196 let id = exec_state.next_uuid();
197
198 exec_state
199 .batch_modeling_cmd(
200 ModelingCmdMeta::from_args_id(&args, id),
201 ModelingCmd::from(mcmd::ExtendPath {
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);
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.expect_length();
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 path: sketch.id.into(),
304 segment: PathSegment::Arc {
305 start: angle_start,
306 end: angle_end,
307 center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
308 radius: units.adjust_to(radius, UnitLen::Mm).0.into(),
309 relative: false,
310 },
311 }),
312 )
313 .await?;
314
315 let current_path = Path::CircleThreePoint {
316 base: BasePath {
317 from: untype_point(from.clone()).0,
319 to: untype_point(from).0,
320 tag: tag.clone(),
321 units,
322 geo_meta: GeoMeta {
323 id,
324 metadata: args.source_range.into(),
325 },
326 },
327 p1,
328 p2,
329 p3,
330 };
331
332 let mut new_sketch = sketch;
333 new_sketch.is_closed = true;
334 if let Some(tag) = &tag {
335 new_sketch.add_tag(tag, ¤t_path, exec_state);
336 }
337
338 new_sketch.paths.push(current_path);
339
340 exec_state
341 .batch_modeling_cmd(
342 ModelingCmdMeta::from_args_id(&args, id),
343 ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
344 )
345 .await?;
346
347 Ok(new_sketch)
348}
349
350#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
352#[ts(export)]
353#[serde(rename_all = "lowercase")]
354pub enum PolygonType {
355 #[default]
356 Inscribed,
357 Circumscribed,
358}
359
360pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
362 let sketch_or_surface =
363 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
364 let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
365 let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
366 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
367 let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
368
369 let sketch = inner_polygon(
370 sketch_or_surface,
371 radius,
372 num_sides.n as u64,
373 center,
374 inscribed,
375 exec_state,
376 args,
377 )
378 .await?;
379 Ok(KclValue::Sketch {
380 value: Box::new(sketch),
381 })
382}
383
384#[allow(clippy::too_many_arguments)]
385async fn inner_polygon(
386 sketch_surface_or_group: SketchOrSurface,
387 radius: TyF64,
388 num_sides: u64,
389 center: [TyF64; 2],
390 inscribed: Option<bool>,
391 exec_state: &mut ExecState,
392 args: Args,
393) -> Result<Sketch, KclError> {
394 if num_sides < 3 {
395 return Err(KclError::new_type(KclErrorDetails::new(
396 "Polygon must have at least 3 sides".to_string(),
397 vec![args.source_range],
398 )));
399 }
400
401 if radius.n <= 0.0 {
402 return Err(KclError::new_type(KclErrorDetails::new(
403 "Radius must be greater than 0".to_string(),
404 vec![args.source_range],
405 )));
406 }
407
408 let (sketch_surface, units) = match sketch_surface_or_group {
409 SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.expect_length()),
410 SketchOrSurface::Sketch(group) => (group.on, group.units),
411 };
412
413 let half_angle = std::f64::consts::PI / num_sides as f64;
414
415 let radius_to_vertices = if inscribed.unwrap_or(true) {
416 radius.n
418 } else {
419 radius.n / libm::cos(half_angle)
421 };
422
423 let angle_step = std::f64::consts::TAU / num_sides as f64;
424
425 let center_u = point_to_len_unit(center, units);
426
427 let vertices: Vec<[f64; 2]> = (0..num_sides)
428 .map(|i| {
429 let angle = angle_step * i as f64;
430 [
431 center_u[0] + radius_to_vertices * libm::cos(angle),
432 center_u[1] + radius_to_vertices * libm::sin(angle),
433 ]
434 })
435 .collect();
436
437 let mut sketch = crate::std::sketch::inner_start_profile(
438 sketch_surface,
439 point_to_typed(vertices[0], units),
440 None,
441 exec_state,
442 args.clone(),
443 )
444 .await?;
445
446 for vertex in vertices.iter().skip(1) {
448 let from = sketch.current_pen_position()?;
449 let id = exec_state.next_uuid();
450
451 exec_state
452 .batch_modeling_cmd(
453 ModelingCmdMeta::from_args_id(&args, id),
454 ModelingCmd::from(mcmd::ExtendPath {
455 path: sketch.id.into(),
456 segment: PathSegment::Line {
457 end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
458 .with_z(0.0)
459 .map(LengthUnit),
460 relative: false,
461 },
462 }),
463 )
464 .await?;
465
466 let current_path = Path::ToPoint {
467 base: BasePath {
468 from: from.ignore_units(),
469 to: *vertex,
470 tag: None,
471 units: sketch.units,
472 geo_meta: GeoMeta {
473 id,
474 metadata: args.source_range.into(),
475 },
476 },
477 };
478
479 sketch.paths.push(current_path);
480 }
481
482 let from = sketch.current_pen_position()?;
484 let close_id = exec_state.next_uuid();
485
486 exec_state
487 .batch_modeling_cmd(
488 ModelingCmdMeta::from_args_id(&args, close_id),
489 ModelingCmd::from(mcmd::ExtendPath {
490 path: sketch.id.into(),
491 segment: PathSegment::Line {
492 end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
493 .with_z(0.0)
494 .map(LengthUnit),
495 relative: false,
496 },
497 }),
498 )
499 .await?;
500
501 let current_path = Path::ToPoint {
502 base: BasePath {
503 from: from.ignore_units(),
504 to: vertices[0],
505 tag: None,
506 units: sketch.units,
507 geo_meta: GeoMeta {
508 id: close_id,
509 metadata: args.source_range.into(),
510 },
511 },
512 };
513
514 sketch.paths.push(current_path);
515
516 exec_state
517 .batch_modeling_cmd(
518 (&args).into(),
519 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
520 )
521 .await?;
522
523 Ok(sketch)
524}
525
526pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
528 let sketch_or_surface =
529 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
530 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
531 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
532 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
533 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
534 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
535
536 let sketch = inner_ellipse(
537 sketch_or_surface,
538 center,
539 major_radius,
540 major_axis,
541 minor_radius,
542 tag,
543 exec_state,
544 args,
545 )
546 .await?;
547 Ok(KclValue::Sketch {
548 value: Box::new(sketch),
549 })
550}
551
552#[allow(clippy::too_many_arguments)]
553async fn inner_ellipse(
554 sketch_surface_or_group: SketchOrSurface,
555 center: [TyF64; 2],
556 major_radius: Option<TyF64>,
557 major_axis: Option<[TyF64; 2]>,
558 minor_radius: TyF64,
559 tag: Option<TagNode>,
560 exec_state: &mut ExecState,
561 args: Args,
562) -> Result<Sketch, KclError> {
563 let sketch_surface = match sketch_surface_or_group {
564 SketchOrSurface::SketchSurface(surface) => surface,
565 SketchOrSurface::Sketch(group) => group.on,
566 };
567 let (center_u, ty) = untype_point(center.clone());
568 let units = ty.expect_length();
569
570 let major_axis = match (major_axis, major_radius) {
571 (Some(_), Some(_)) | (None, None) => {
572 return Err(KclError::new_type(KclErrorDetails::new(
573 "Provide either `majorAxis` or `majorRadius`.".to_string(),
574 vec![args.source_range],
575 )));
576 }
577 (Some(major_axis), None) => major_axis,
578 (None, Some(major_radius)) => [
579 major_radius.clone(),
580 TyF64 {
581 n: 0.0,
582 ty: major_radius.ty,
583 },
584 ],
585 };
586
587 let from = [
588 center_u[0] + major_axis[0].to_length_units(units),
589 center_u[1] + major_axis[1].to_length_units(units),
590 ];
591 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
592
593 let sketch =
594 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
595
596 let angle_start = Angle::zero();
597 let angle_end = Angle::turn();
598
599 let id = exec_state.next_uuid();
600
601 let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
602 exec_state
603 .batch_modeling_cmd(
604 ModelingCmdMeta::from_args_id(&args, id),
605 ModelingCmd::from(mcmd::ExtendPath {
606 path: sketch.id.into(),
607 segment: PathSegment::Ellipse {
608 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
609 major_axis: axis,
610 minor_radius: LengthUnit(minor_radius.to_mm()),
611 start_angle: Angle::from_degrees(angle_start.to_degrees()),
612 end_angle: Angle::from_degrees(angle_end.to_degrees()),
613 },
614 }),
615 )
616 .await?;
617
618 let current_path = Path::Ellipse {
619 base: BasePath {
620 from,
621 to: from,
622 tag: tag.clone(),
623 units,
624 geo_meta: GeoMeta {
625 id,
626 metadata: args.source_range.into(),
627 },
628 },
629 major_axis: major_axis.map(|x| x.to_length_units(units)),
630 minor_radius: minor_radius.to_length_units(units),
631 center: center_u,
632 ccw: angle_start < angle_end,
633 };
634
635 let mut new_sketch = sketch;
636 new_sketch.is_closed = true;
637 if let Some(tag) = &tag {
638 new_sketch.add_tag(tag, ¤t_path, exec_state);
639 }
640
641 new_sketch.paths.push(current_path);
642
643 exec_state
644 .batch_modeling_cmd(
645 ModelingCmdMeta::from_args_id(&args, id),
646 ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
647 )
648 .await?;
649
650 Ok(new_sketch)
651}
652
653pub(crate) fn get_radius(
654 radius: Option<TyF64>,
655 diameter: Option<TyF64>,
656 source_range: SourceRange,
657) -> Result<TyF64, KclError> {
658 get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
659}
660
661pub(crate) fn get_radius_labelled(
662 radius: Option<TyF64>,
663 diameter: Option<TyF64>,
664 source_range: SourceRange,
665 label_radius: &'static str,
666 label_diameter: &'static str,
667) -> Result<TyF64, KclError> {
668 match (radius, diameter) {
669 (Some(radius), None) => Ok(radius),
670 (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
671 (None, None) => Err(KclError::new_type(KclErrorDetails::new(
672 format!("This function needs either `{label_diameter}` or `{label_radius}`"),
673 vec![source_range],
674 ))),
675 (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
676 format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
677 vec![source_range],
678 ))),
679 }
680}