kcl_lib/std/
shapes.rs

1//! Standard library shapes.
2
3use 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/// A sketch surface or a sketch.
32#[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
40/// Sketch a rectangle.
41pub 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    // Find the corner in the negative quadrant
70    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    // Start the sketch then draw the 4 lines.
93    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    // Update the sketch in KCL memory.
127    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
154/// Sketch a circle.
155pub 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, &current_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
247/// Sketch a 3-point circle.
248pub 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
262// Similar to inner_circle, but needs to retain 3-point information in the
263// path so it can be used for other features, otherwise it's lost.
264async 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    // It can be the distance to any of the 3 points - they all lay on the circumference.
282    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            // It's fine to untype here because we know `from` has units as its units.
317            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, &current_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/// Type of the polygon
350#[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
359/// Create a regular polygon with the specified number of sides and radius.
360pub 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        // inscribed
416        radius.n
417    } else {
418        // circumscribed
419        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    // Draw all the lines with unique IDs and modified tags
446    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    // Close the polygon by connecting back to the first vertex with a new ID
482    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
525/// Sketch an ellipse.
526pub 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, &current_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}