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