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 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/// A sketch surface or a sketch.
33#[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
41/// Sketch a rectangle.
42pub 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    // Find the corner in the negative quadrant
71    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    // Start the sketch then draw the 4 lines.
94    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    // Update the sketch in KCL memory.
128    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
155/// Sketch a circle.
156pub 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, &current_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
248/// Sketch a 3-point circle.
249pub 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
263// Similar to inner_circle, but needs to retain 3-point information in the
264// path so it can be used for other features, otherwise it's lost.
265async 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    // It can be the distance to any of the 3 points - they all lay on the circumference.
283    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            // It's fine to untype here because we know `from` has units as its units.
318            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, &current_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/// Type of the polygon
351#[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
360/// Create a regular polygon with the specified number of sides and radius.
361pub 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        // inscribed
417        radius.n
418    } else {
419        // circumscribed
420        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    // Draw all the lines with unique IDs and modified tags
447    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    // Close the polygon by connecting back to the first vertex with a new ID
483    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
526/// Sketch an ellipse.
527pub 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, &current_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}