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