kcl_lib/std/
shapes.rs

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