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.clone();
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.clone();
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.expect_length();
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: units.adjust_to(radius, UnitLen::Mm).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.clone();
331    if let Some(tag) = &tag {
332        new_sketch.add_tag(tag, &current_path, exec_state);
333    }
334
335    new_sketch.paths.push(current_path);
336
337    exec_state
338        .batch_modeling_cmd(
339            ModelingCmdMeta::from_args_id(&args, id),
340            ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
341        )
342        .await?;
343
344    Ok(new_sketch)
345}
346
347/// Type of the polygon
348#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
349#[ts(export)]
350#[serde(rename_all = "lowercase")]
351pub enum PolygonType {
352    #[default]
353    Inscribed,
354    Circumscribed,
355}
356
357/// Create a regular polygon with the specified number of sides and radius.
358pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
359    let sketch_or_surface =
360        args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
361    let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
362    let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
363    let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
364    let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
365
366    let sketch = inner_polygon(
367        sketch_or_surface,
368        radius,
369        num_sides.n as u64,
370        center,
371        inscribed,
372        exec_state,
373        args,
374    )
375    .await?;
376    Ok(KclValue::Sketch {
377        value: Box::new(sketch),
378    })
379}
380
381#[allow(clippy::too_many_arguments)]
382async fn inner_polygon(
383    sketch_surface_or_group: SketchOrSurface,
384    radius: TyF64,
385    num_sides: u64,
386    center: [TyF64; 2],
387    inscribed: Option<bool>,
388    exec_state: &mut ExecState,
389    args: Args,
390) -> Result<Sketch, KclError> {
391    if num_sides < 3 {
392        return Err(KclError::new_type(KclErrorDetails::new(
393            "Polygon must have at least 3 sides".to_string(),
394            vec![args.source_range],
395        )));
396    }
397
398    if radius.n <= 0.0 {
399        return Err(KclError::new_type(KclErrorDetails::new(
400            "Radius must be greater than 0".to_string(),
401            vec![args.source_range],
402        )));
403    }
404
405    let (sketch_surface, units) = match sketch_surface_or_group {
406        SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.expect_length()),
407        SketchOrSurface::Sketch(group) => (group.on, group.units),
408    };
409
410    let half_angle = std::f64::consts::PI / num_sides as f64;
411
412    let radius_to_vertices = if inscribed.unwrap_or(true) {
413        // inscribed
414        radius.n
415    } else {
416        // circumscribed
417        radius.n / libm::cos(half_angle)
418    };
419
420    let angle_step = std::f64::consts::TAU / num_sides as f64;
421
422    let center_u = point_to_len_unit(center, units);
423
424    let vertices: Vec<[f64; 2]> = (0..num_sides)
425        .map(|i| {
426            let angle = angle_step * i as f64;
427            [
428                center_u[0] + radius_to_vertices * libm::cos(angle),
429                center_u[1] + radius_to_vertices * libm::sin(angle),
430            ]
431        })
432        .collect();
433
434    let mut sketch = crate::std::sketch::inner_start_profile(
435        sketch_surface,
436        point_to_typed(vertices[0], units),
437        None,
438        exec_state,
439        args.clone(),
440    )
441    .await?;
442
443    // Draw all the lines with unique IDs and modified tags
444    for vertex in vertices.iter().skip(1) {
445        let from = sketch.current_pen_position()?;
446        let id = exec_state.next_uuid();
447
448        exec_state
449            .batch_modeling_cmd(
450                ModelingCmdMeta::from_args_id(&args, id),
451                ModelingCmd::from(mcmd::ExtendPath {
452                    path: sketch.id.into(),
453                    segment: PathSegment::Line {
454                        end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
455                            .with_z(0.0)
456                            .map(LengthUnit),
457                        relative: false,
458                    },
459                }),
460            )
461            .await?;
462
463        let current_path = Path::ToPoint {
464            base: BasePath {
465                from: from.ignore_units(),
466                to: *vertex,
467                tag: None,
468                units: sketch.units,
469                geo_meta: GeoMeta {
470                    id,
471                    metadata: args.source_range.into(),
472                },
473            },
474        };
475
476        sketch.paths.push(current_path);
477    }
478
479    // Close the polygon by connecting back to the first vertex with a new ID
480    let from = sketch.current_pen_position()?;
481    let close_id = exec_state.next_uuid();
482
483    exec_state
484        .batch_modeling_cmd(
485            ModelingCmdMeta::from_args_id(&args, close_id),
486            ModelingCmd::from(mcmd::ExtendPath {
487                path: sketch.id.into(),
488                segment: PathSegment::Line {
489                    end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
490                        .with_z(0.0)
491                        .map(LengthUnit),
492                    relative: false,
493                },
494            }),
495        )
496        .await?;
497
498    let current_path = Path::ToPoint {
499        base: BasePath {
500            from: from.ignore_units(),
501            to: vertices[0],
502            tag: None,
503            units: sketch.units,
504            geo_meta: GeoMeta {
505                id: close_id,
506                metadata: args.source_range.into(),
507            },
508        },
509    };
510
511    sketch.paths.push(current_path);
512
513    exec_state
514        .batch_modeling_cmd(
515            (&args).into(),
516            ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
517        )
518        .await?;
519
520    Ok(sketch)
521}
522
523pub(crate) fn get_radius(
524    radius: Option<TyF64>,
525    diameter: Option<TyF64>,
526    source_range: SourceRange,
527) -> Result<TyF64, KclError> {
528    get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
529}
530
531pub(crate) fn get_radius_labelled(
532    radius: Option<TyF64>,
533    diameter: Option<TyF64>,
534    source_range: SourceRange,
535    label_radius: &'static str,
536    label_diameter: &'static str,
537) -> Result<TyF64, KclError> {
538    match (radius, diameter) {
539        (Some(radius), None) => Ok(radius),
540        (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
541        (None, None) => Err(KclError::new_type(KclErrorDetails::new(
542            format!("This function needs either `{label_diameter}` or `{label_radius}`"),
543            vec![source_range],
544        ))),
545        (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
546            format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
547            vec![source_range],
548        ))),
549    }
550}