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::{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/// A sketch surface or a sketch.
31#[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
39/// Sketch a rectangle.
40pub 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    // Find the corner in the negative quadrant
69    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    // Start the sketch then draw the 4 lines.
92    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    // Update the sketch in KCL memory.
126    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
153/// Sketch a circle.
154pub 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, &current_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
246/// Sketch a 3-point circle.
247pub 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
261// Similar to inner_circle, but needs to retain 3-point information in the
262// path so it can be used for other features, otherwise it's lost.
263async 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    // It can be the distance to any of the 3 points - they all lay on the circumference.
281    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            // It's fine to untype here because we know `from` has units as its units.
316            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, &current_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/// Type of the polygon
349#[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
358/// Create a regular polygon with the specified number of sides and radius.
359pub 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        // inscribed
415        radius.n
416    } else {
417        // circumscribed
418        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    // Draw all the lines with unique IDs and modified tags
445    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    // Close the polygon by connecting back to the first vertex with a new ID
481    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
524/// Sketch an ellipse.
525pub 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, &current_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}