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