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
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(exec_state, &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(exec_state, &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 = ProfileClosed::Explicitly;
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(exec_state, &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 = ProfileClosed::Explicitly;
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(exec_state, &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(exec_state, &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 = ProfileClosed::Explicitly;
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(exec_state, &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(exec_state, &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(exec_state, &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 sketch.is_closed = ProfileClosed::Explicitly;
525
526 exec_state
527 .batch_modeling_cmd(
528 ModelingCmdMeta::from_args(exec_state, &args),
529 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
530 )
531 .await?;
532
533 Ok(sketch)
534}
535
536pub async fn ellipse(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
538 let sketch_or_surface =
539 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
540 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
541 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
542 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
543 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
544 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
545
546 let sketch = inner_ellipse(
547 sketch_or_surface,
548 center,
549 major_radius,
550 major_axis,
551 minor_radius,
552 tag,
553 exec_state,
554 args,
555 )
556 .await?;
557 Ok(KclValue::Sketch {
558 value: Box::new(sketch),
559 })
560}
561
562#[allow(clippy::too_many_arguments)]
563async fn inner_ellipse(
564 sketch_surface_or_group: SketchOrSurface,
565 center: [TyF64; 2],
566 major_radius: Option<TyF64>,
567 major_axis: Option<[TyF64; 2]>,
568 minor_radius: TyF64,
569 tag: Option<TagNode>,
570 exec_state: &mut ExecState,
571 args: Args,
572) -> Result<Sketch, KclError> {
573 let sketch_surface = match sketch_surface_or_group {
574 SketchOrSurface::SketchSurface(surface) => surface,
575 SketchOrSurface::Sketch(group) => group.on,
576 };
577 let (center_u, ty) = untype_point(center.clone());
578 let units = ty.as_length().unwrap_or(UnitLength::Millimeters);
579
580 let major_axis = match (major_axis, major_radius) {
581 (Some(_), Some(_)) | (None, None) => {
582 return Err(KclError::new_type(KclErrorDetails::new(
583 "Provide either `majorAxis` or `majorRadius`.".to_string(),
584 vec![args.source_range],
585 )));
586 }
587 (Some(major_axis), None) => major_axis,
588 (None, Some(major_radius)) => [
589 major_radius.clone(),
590 TyF64 {
591 n: 0.0,
592 ty: major_radius.ty,
593 },
594 ],
595 };
596
597 let from = [
598 center_u[0] + major_axis[0].to_length_units(units),
599 center_u[1] + major_axis[1].to_length_units(units),
600 ];
601 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
602
603 let sketch =
604 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
605
606 let angle_start = Angle::zero();
607 let angle_end = Angle::turn();
608
609 let id = exec_state.next_uuid();
610
611 let axis = KPoint2d::from(untyped_point_to_mm([major_axis[0].n, major_axis[1].n], units)).map(LengthUnit);
612 exec_state
613 .batch_modeling_cmd(
614 ModelingCmdMeta::from_args_id(exec_state, &args, id),
615 ModelingCmd::from(mcmd::ExtendPath {
616 label: Default::default(),
617 path: sketch.id.into(),
618 segment: PathSegment::Ellipse {
619 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
620 major_axis: axis,
621 minor_radius: LengthUnit(minor_radius.to_mm()),
622 start_angle: Angle::from_degrees(angle_start.to_degrees()),
623 end_angle: Angle::from_degrees(angle_end.to_degrees()),
624 },
625 }),
626 )
627 .await?;
628
629 let current_path = Path::Ellipse {
630 base: BasePath {
631 from,
632 to: from,
633 tag: tag.clone(),
634 units,
635 geo_meta: GeoMeta {
636 id,
637 metadata: args.source_range.into(),
638 },
639 },
640 major_axis: major_axis.map(|x| x.to_length_units(units)),
641 minor_radius: minor_radius.to_length_units(units),
642 center: center_u,
643 ccw: angle_start < angle_end,
644 };
645
646 let mut new_sketch = sketch;
647 new_sketch.is_closed = ProfileClosed::Explicitly;
648 if let Some(tag) = &tag {
649 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
650 }
651
652 new_sketch.paths.push(current_path);
653
654 exec_state
655 .batch_modeling_cmd(
656 ModelingCmdMeta::from_args_id(exec_state, &args, id),
657 ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
658 )
659 .await?;
660
661 Ok(new_sketch)
662}
663
664pub(crate) fn get_radius(
665 radius: Option<TyF64>,
666 diameter: Option<TyF64>,
667 source_range: SourceRange,
668) -> Result<TyF64, KclError> {
669 get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
670}
671
672pub(crate) fn get_radius_labelled(
673 radius: Option<TyF64>,
674 diameter: Option<TyF64>,
675 source_range: SourceRange,
676 label_radius: &'static str,
677 label_diameter: &'static str,
678) -> Result<TyF64, KclError> {
679 match (radius, diameter) {
680 (Some(radius), None) => Ok(radius),
681 (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
682 (None, None) => Err(KclError::new_type(KclErrorDetails::new(
683 format!("This function needs either `{label_diameter}` or `{label_radius}`"),
684 vec![source_range],
685 ))),
686 (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
687 format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
688 vec![source_range],
689 ))),
690 }
691}