kcl_lib/std/
shapes.rs

1//! Standard library shapes.
2
3use anyhow::Result;
4use kcmc::{
5    each_cmd as mcmd,
6    length_unit::LengthUnit,
7    shared::{Angle, Point2d as KPoint2d},
8    ModelingCmd,
9};
10use kittycad_modeling_cmds as kcmc;
11use kittycad_modeling_cmds::shared::PathSegment;
12use schemars::JsonSchema;
13use serde::Serialize;
14
15use super::{
16    args::TyF64,
17    utils::{point_to_len_unit, point_to_mm, point_to_typed, untype_point, untyped_point_to_mm},
18};
19use crate::{
20    errors::{KclError, KclErrorDetails},
21    execution::{
22        types::{RuntimeType, UnitLen},
23        BasePath, ExecState, GeoMeta, KclValue, Path, Sketch, SketchSurface,
24    },
25    parsing::ast::types::TagNode,
26    std::{
27        sketch::NEW_TAG_KW,
28        utils::{calculate_circle_center, distance},
29        Args,
30    },
31    SourceRange,
32};
33
34/// A sketch surface or a sketch.
35#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
36#[ts(export)]
37#[serde(untagged)]
38pub enum SketchOrSurface {
39    SketchSurface(SketchSurface),
40    Sketch(Box<Sketch>),
41}
42
43/// Sketch a circle.
44pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
45    let sketch_or_surface =
46        args.get_unlabeled_kw_arg_typed("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
47    let center = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
48    let radius: Option<TyF64> = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
49    let diameter: Option<TyF64> = args.get_kw_arg_opt_typed("diameter", &RuntimeType::length(), exec_state)?;
50    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
51
52    let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
53    Ok(KclValue::Sketch {
54        value: Box::new(sketch),
55    })
56}
57
58async fn inner_circle(
59    sketch_or_surface: SketchOrSurface,
60    center: [TyF64; 2],
61    radius: Option<TyF64>,
62    diameter: Option<TyF64>,
63    tag: Option<TagNode>,
64    exec_state: &mut ExecState,
65    args: Args,
66) -> Result<Sketch, KclError> {
67    let sketch_surface = match sketch_or_surface {
68        SketchOrSurface::SketchSurface(surface) => surface,
69        SketchOrSurface::Sketch(s) => s.on,
70    };
71    let (center_u, ty) = untype_point(center.clone());
72    let units = ty.expect_length();
73
74    let radius = get_radius(radius, diameter, args.source_range)?;
75    let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
76    let from_t = [TyF64::new(from[0], ty.clone()), TyF64::new(from[1], ty)];
77
78    let sketch =
79        crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
80
81    let angle_start = Angle::zero();
82    let angle_end = Angle::turn();
83
84    let id = exec_state.next_uuid();
85
86    args.batch_modeling_cmd(
87        id,
88        ModelingCmd::from(mcmd::ExtendPath {
89            path: sketch.id.into(),
90            segment: PathSegment::Arc {
91                start: angle_start,
92                end: angle_end,
93                center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
94                radius: LengthUnit(radius.to_mm()),
95                relative: false,
96            },
97        }),
98    )
99    .await?;
100
101    let current_path = Path::Circle {
102        base: BasePath {
103            from,
104            to: from,
105            tag: tag.clone(),
106            units,
107            geo_meta: GeoMeta {
108                id,
109                metadata: args.source_range.into(),
110            },
111        },
112        radius: radius.to_length_units(units),
113        center: center_u,
114        ccw: angle_start < angle_end,
115    };
116
117    let mut new_sketch = sketch.clone();
118    if let Some(tag) = &tag {
119        new_sketch.add_tag(tag, &current_path, exec_state);
120    }
121
122    new_sketch.paths.push(current_path);
123
124    args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
125        .await?;
126
127    Ok(new_sketch)
128}
129
130/// Sketch a 3-point circle.
131pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
132    let sketch_or_surface =
133        args.get_unlabeled_kw_arg_typed("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
134    let p1 = args.get_kw_arg_typed("p1", &RuntimeType::point2d(), exec_state)?;
135    let p2 = args.get_kw_arg_typed("p2", &RuntimeType::point2d(), exec_state)?;
136    let p3 = args.get_kw_arg_typed("p3", &RuntimeType::point2d(), exec_state)?;
137    let tag = args.get_kw_arg_opt("tag")?;
138
139    let sketch = inner_circle_three_point(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
140    Ok(KclValue::Sketch {
141        value: Box::new(sketch),
142    })
143}
144
145// Similar to inner_circle, but needs to retain 3-point information in the
146// path so it can be used for other features, otherwise it's lost.
147async fn inner_circle_three_point(
148    sketch_surface_or_group: SketchOrSurface,
149    p1: [TyF64; 2],
150    p2: [TyF64; 2],
151    p3: [TyF64; 2],
152    tag: Option<TagNode>,
153    exec_state: &mut ExecState,
154    args: Args,
155) -> Result<Sketch, KclError> {
156    let ty = p1[0].ty.clone();
157    let units = ty.expect_length();
158
159    let p1 = point_to_len_unit(p1, units);
160    let p2 = point_to_len_unit(p2, units);
161    let p3 = point_to_len_unit(p3, units);
162
163    let center = calculate_circle_center(p1, p2, p3);
164    // It can be the distance to any of the 3 points - they all lay on the circumference.
165    let radius = distance(center, p2);
166
167    let sketch_surface = match sketch_surface_or_group {
168        SketchOrSurface::SketchSurface(surface) => surface,
169        SketchOrSurface::Sketch(group) => group.on,
170    };
171
172    let from = [
173        TyF64::new(center[0] + radius, ty.clone()),
174        TyF64::new(center[1], ty.clone()),
175    ];
176    let sketch =
177        crate::std::sketch::inner_start_profile(sketch_surface, from.clone(), None, exec_state, args.clone()).await?;
178
179    let angle_start = Angle::zero();
180    let angle_end = Angle::turn();
181
182    let id = exec_state.next_uuid();
183
184    args.batch_modeling_cmd(
185        id,
186        ModelingCmd::from(mcmd::ExtendPath {
187            path: sketch.id.into(),
188            segment: PathSegment::Arc {
189                start: angle_start,
190                end: angle_end,
191                center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
192                radius: units.adjust_to(radius, UnitLen::Mm).0.into(),
193                relative: false,
194            },
195        }),
196    )
197    .await?;
198
199    let current_path = Path::CircleThreePoint {
200        base: BasePath {
201            // It's fine to untype here because we know `from` has units as its units.
202            from: untype_point(from.clone()).0,
203            to: untype_point(from).0,
204            tag: tag.clone(),
205            units,
206            geo_meta: GeoMeta {
207                id,
208                metadata: args.source_range.into(),
209            },
210        },
211        p1,
212        p2,
213        p3,
214    };
215
216    let mut new_sketch = sketch.clone();
217    if let Some(tag) = &tag {
218        new_sketch.add_tag(tag, &current_path, exec_state);
219    }
220
221    new_sketch.paths.push(current_path);
222
223    args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
224        .await?;
225
226    Ok(new_sketch)
227}
228
229/// Type of the polygon
230#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
231#[ts(export)]
232#[serde(rename_all = "lowercase")]
233pub enum PolygonType {
234    #[default]
235    Inscribed,
236    Circumscribed,
237}
238
239/// Create a regular polygon with the specified number of sides and radius.
240pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
241    let sketch_or_surface =
242        args.get_unlabeled_kw_arg_typed("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
243    let radius: TyF64 = args.get_kw_arg_typed("radius", &RuntimeType::length(), exec_state)?;
244    let num_sides: TyF64 = args.get_kw_arg_typed("numSides", &RuntimeType::count(), exec_state)?;
245    let center = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
246    let inscribed = args.get_kw_arg_opt_typed("inscribed", &RuntimeType::bool(), exec_state)?;
247
248    let sketch = inner_polygon(
249        sketch_or_surface,
250        radius,
251        num_sides.n as u64,
252        center,
253        inscribed,
254        exec_state,
255        args,
256    )
257    .await?;
258    Ok(KclValue::Sketch {
259        value: Box::new(sketch),
260    })
261}
262
263#[allow(clippy::too_many_arguments)]
264async fn inner_polygon(
265    sketch_surface_or_group: SketchOrSurface,
266    radius: TyF64,
267    num_sides: u64,
268    center: [TyF64; 2],
269    inscribed: Option<bool>,
270    exec_state: &mut ExecState,
271    args: Args,
272) -> Result<Sketch, KclError> {
273    if num_sides < 3 {
274        return Err(KclError::new_type(KclErrorDetails::new(
275            "Polygon must have at least 3 sides".to_string(),
276            vec![args.source_range],
277        )));
278    }
279
280    if radius.n <= 0.0 {
281        return Err(KclError::new_type(KclErrorDetails::new(
282            "Radius must be greater than 0".to_string(),
283            vec![args.source_range],
284        )));
285    }
286
287    let (sketch_surface, units) = match sketch_surface_or_group {
288        SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.expect_length()),
289        SketchOrSurface::Sketch(group) => (group.on, group.units),
290    };
291
292    let half_angle = std::f64::consts::PI / num_sides as f64;
293
294    let radius_to_vertices = if inscribed.unwrap_or(true) {
295        // inscribed
296        radius.n
297    } else {
298        // circumscribed
299        radius.n / half_angle.cos()
300    };
301
302    let angle_step = std::f64::consts::TAU / num_sides as f64;
303
304    let center_u = point_to_len_unit(center, units);
305
306    let vertices: Vec<[f64; 2]> = (0..num_sides)
307        .map(|i| {
308            let angle = angle_step * i as f64;
309            [
310                center_u[0] + radius_to_vertices * angle.cos(),
311                center_u[1] + radius_to_vertices * angle.sin(),
312            ]
313        })
314        .collect();
315
316    let mut sketch = crate::std::sketch::inner_start_profile(
317        sketch_surface,
318        point_to_typed(vertices[0], units),
319        None,
320        exec_state,
321        args.clone(),
322    )
323    .await?;
324
325    // Draw all the lines with unique IDs and modified tags
326    for vertex in vertices.iter().skip(1) {
327        let from = sketch.current_pen_position()?;
328        let id = exec_state.next_uuid();
329
330        args.batch_modeling_cmd(
331            id,
332            ModelingCmd::from(mcmd::ExtendPath {
333                path: sketch.id.into(),
334                segment: PathSegment::Line {
335                    end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
336                        .with_z(0.0)
337                        .map(LengthUnit),
338                    relative: false,
339                },
340            }),
341        )
342        .await?;
343
344        let current_path = Path::ToPoint {
345            base: BasePath {
346                from: from.ignore_units(),
347                to: *vertex,
348                tag: None,
349                units: sketch.units,
350                geo_meta: GeoMeta {
351                    id,
352                    metadata: args.source_range.into(),
353                },
354            },
355        };
356
357        sketch.paths.push(current_path);
358    }
359
360    // Close the polygon by connecting back to the first vertex with a new ID
361    let from = sketch.current_pen_position()?;
362    let close_id = exec_state.next_uuid();
363
364    args.batch_modeling_cmd(
365        close_id,
366        ModelingCmd::from(mcmd::ExtendPath {
367            path: sketch.id.into(),
368            segment: PathSegment::Line {
369                end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
370                    .with_z(0.0)
371                    .map(LengthUnit),
372                relative: false,
373            },
374        }),
375    )
376    .await?;
377
378    let current_path = Path::ToPoint {
379        base: BasePath {
380            from: from.ignore_units(),
381            to: vertices[0],
382            tag: None,
383            units: sketch.units,
384            geo_meta: GeoMeta {
385                id: close_id,
386                metadata: args.source_range.into(),
387            },
388        },
389    };
390
391    sketch.paths.push(current_path);
392
393    args.batch_modeling_cmd(
394        exec_state.next_uuid(),
395        ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
396    )
397    .await?;
398
399    Ok(sketch)
400}
401
402pub(crate) fn get_radius(
403    radius: Option<TyF64>,
404    diameter: Option<TyF64>,
405    source_range: SourceRange,
406) -> Result<TyF64, KclError> {
407    match (radius, diameter) {
408        (Some(radius), None) => Ok(radius),
409        (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
410        (None, None) => Err(KclError::new_type(KclErrorDetails::new(
411            "This function needs either `diameter` or `radius`".to_string(),
412            vec![source_range],
413        ))),
414        (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
415            "You cannot specify both `diameter` and `radius`, please remove one".to_string(),
416            vec![source_range],
417        ))),
418    }
419}