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 super::{
17    args::TyF64,
18    utils::{point_to_len_unit, point_to_mm, point_to_typed, untype_point, untyped_point_to_mm},
19};
20use crate::{
21    errors::{KclError, KclErrorDetails},
22    execution::{
23        types::{RuntimeType, UnitLen},
24        BasePath, ExecState, GeoMeta, KclValue, Path, Sketch, SketchSurface,
25    },
26    parsing::ast::types::TagNode,
27    std::{
28        sketch::NEW_TAG_KW,
29        utils::{calculate_circle_center, distance},
30        Args,
31    },
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 = args.get_unlabeled_kw_arg("sketchOrSurface")?;
46    let center = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
47    let radius: Option<TyF64> = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
48    let diameter: Option<TyF64> = args.get_kw_arg_opt_typed("diameter", &RuntimeType::length(), exec_state)?;
49    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
50
51    let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
52    Ok(KclValue::Sketch {
53        value: Box::new(sketch),
54    })
55}
56
57async fn inner_circle(
58    sketch_or_surface: SketchOrSurface,
59    center: [TyF64; 2],
60    radius: Option<TyF64>,
61    diameter: Option<TyF64>,
62    tag: Option<TagNode>,
63    exec_state: &mut ExecState,
64    args: Args,
65) -> Result<Sketch, KclError> {
66    let sketch_surface = match sketch_or_surface {
67        SketchOrSurface::SketchSurface(surface) => surface,
68        SketchOrSurface::Sketch(s) => s.on,
69    };
70    let (center_u, ty) = untype_point(center.clone());
71    let units = ty.expect_length();
72
73    let radius = match (radius, diameter) {
74        (Some(radius), None) => radius,
75        (None, Some(mut diameter)) => {
76            diameter.n /= 2.0;
77            diameter
78        }
79        (None, None) => {
80            return Err(KclError::Type(KclErrorDetails::new(
81                "This function needs either `diameter` or `radius`".to_string(),
82                vec![args.source_range],
83            )))
84        }
85        (Some(_), Some(_)) => {
86            return Err(KclError::Type(KclErrorDetails::new(
87                "You cannot specify both `diameter` and `radius`, please remove one".to_string(),
88                vec![args.source_range],
89            )))
90        }
91    };
92    let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
93    let from_t = [TyF64::new(from[0], ty.clone()), TyF64::new(from[1], ty)];
94
95    let sketch =
96        crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
97
98    let angle_start = Angle::zero();
99    let angle_end = Angle::turn();
100
101    let id = exec_state.next_uuid();
102
103    args.batch_modeling_cmd(
104        id,
105        ModelingCmd::from(mcmd::ExtendPath {
106            path: sketch.id.into(),
107            segment: PathSegment::Arc {
108                start: angle_start,
109                end: angle_end,
110                center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
111                radius: LengthUnit(radius.to_mm()),
112                relative: false,
113            },
114        }),
115    )
116    .await?;
117
118    let current_path = Path::Circle {
119        base: BasePath {
120            from,
121            to: from,
122            tag: tag.clone(),
123            units,
124            geo_meta: GeoMeta {
125                id,
126                metadata: args.source_range.into(),
127            },
128        },
129        radius: radius.to_length_units(units),
130        center: center_u,
131        ccw: angle_start < angle_end,
132    };
133
134    let mut new_sketch = sketch.clone();
135    if let Some(tag) = &tag {
136        new_sketch.add_tag(tag, &current_path, exec_state);
137    }
138
139    new_sketch.paths.push(current_path);
140
141    args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
142        .await?;
143
144    Ok(new_sketch)
145}
146
147/// Sketch a 3-point circle.
148pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
149    let sketch_surface_or_group = args.get_unlabeled_kw_arg("sketch_surface_or_group")?;
150    let p1 = args.get_kw_arg_typed("p1", &RuntimeType::point2d(), exec_state)?;
151    let p2 = args.get_kw_arg_typed("p2", &RuntimeType::point2d(), exec_state)?;
152    let p3 = args.get_kw_arg_typed("p3", &RuntimeType::point2d(), exec_state)?;
153    let tag = args.get_kw_arg_opt("tag")?;
154
155    let sketch = inner_circle_three_point(sketch_surface_or_group, p1, p2, p3, tag, exec_state, args).await?;
156    Ok(KclValue::Sketch {
157        value: Box::new(sketch),
158    })
159}
160
161/// Construct a circle derived from 3 points.
162///
163/// ```no_run
164/// exampleSketch = startSketchOn(XY)
165///   |> circleThreePoint(p1 = [10,10], p2 = [20,8], p3 = [15,5])
166///   |> extrude(length = 5)
167/// ```
168#[stdlib {
169    name = "circleThreePoint",
170    unlabeled_first = true,
171    args = {
172        sketch_surface_or_group = {docs = "Plane or surface to sketch on."},
173        p1 = {docs = "1st point to derive the circle."},
174        p2 = {docs = "2nd point to derive the circle."},
175        p3 = {docs = "3rd point to derive the circle."},
176        tag = {docs = "Identifier for the circle to reference elsewhere."},
177    },
178    tags = ["sketch"]
179}]
180
181// Similar to inner_circle, but needs to retain 3-point information in the
182// path so it can be used for other features, otherwise it's lost.
183async fn inner_circle_three_point(
184    sketch_surface_or_group: SketchOrSurface,
185    p1: [TyF64; 2],
186    p2: [TyF64; 2],
187    p3: [TyF64; 2],
188    tag: Option<TagNode>,
189    exec_state: &mut ExecState,
190    args: Args,
191) -> Result<Sketch, KclError> {
192    let ty = p1[0].ty.clone();
193    let units = ty.expect_length();
194
195    let p1 = point_to_len_unit(p1, units);
196    let p2 = point_to_len_unit(p2, units);
197    let p3 = point_to_len_unit(p3, units);
198
199    let center = calculate_circle_center(p1, p2, p3);
200    // It can be the distance to any of the 3 points - they all lay on the circumference.
201    let radius = distance(center, p2);
202
203    let sketch_surface = match sketch_surface_or_group {
204        SketchOrSurface::SketchSurface(surface) => surface,
205        SketchOrSurface::Sketch(group) => group.on,
206    };
207
208    let from = [
209        TyF64::new(center[0] + radius, ty.clone()),
210        TyF64::new(center[1], ty.clone()),
211    ];
212    let sketch =
213        crate::std::sketch::inner_start_profile(sketch_surface, from.clone(), None, exec_state, args.clone()).await?;
214
215    let angle_start = Angle::zero();
216    let angle_end = Angle::turn();
217
218    let id = exec_state.next_uuid();
219
220    args.batch_modeling_cmd(
221        id,
222        ModelingCmd::from(mcmd::ExtendPath {
223            path: sketch.id.into(),
224            segment: PathSegment::Arc {
225                start: angle_start,
226                end: angle_end,
227                center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
228                radius: units.adjust_to(radius, UnitLen::Mm).0.into(),
229                relative: false,
230            },
231        }),
232    )
233    .await?;
234
235    let current_path = Path::CircleThreePoint {
236        base: BasePath {
237            // It's fine to untype here because we know `from` has units as its units.
238            from: untype_point(from.clone()).0,
239            to: untype_point(from).0,
240            tag: tag.clone(),
241            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, 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/// Create a regular polygon with the specified number of sides and radius.
276pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
277    let sketch_surface_or_group = args.get_unlabeled_kw_arg("sketchOrSurface")?;
278    let radius: TyF64 = args.get_kw_arg_typed("radius", &RuntimeType::length(), exec_state)?;
279    let num_sides: TyF64 = args.get_kw_arg_typed("numSides", &RuntimeType::count(), exec_state)?;
280    let center = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
281    let inscribed = args.get_kw_arg_opt_typed("inscribed", &RuntimeType::bool(), exec_state)?;
282
283    let sketch = inner_polygon(
284        sketch_surface_or_group,
285        radius,
286        num_sides.n as u64,
287        center,
288        inscribed,
289        exec_state,
290        args,
291    )
292    .await?;
293    Ok(KclValue::Sketch {
294        value: Box::new(sketch),
295    })
296}
297
298/// Create a regular polygon with the specified number of sides that is either inscribed or circumscribed around a circle of the specified radius.
299///
300/// ```no_run
301/// // Create a regular hexagon inscribed in a circle of radius 10
302/// hex = startSketchOn(XY)
303///   |> polygon(
304///     radius = 10,
305///     numSides = 6,
306///     center = [0, 0],
307///     inscribed = true,
308///   )
309///
310/// example = extrude(hex, length = 5)
311/// ```
312///
313/// ```no_run
314/// // Create a square circumscribed around a circle of radius 5
315/// square = startSketchOn(XY)
316///   |> polygon(
317///     radius = 5.0,
318///     numSides = 4,
319///     center = [10, 10],
320///     inscribed = false,
321///   )
322/// example = extrude(square, length = 5)
323/// ```
324#[stdlib {
325    name = "polygon",
326    unlabeled_first = true,
327    args = {
328        sketch_surface_or_group = { docs = "Plane or surface to sketch on" },
329        radius = { docs = "The radius of the polygon", include_in_snippet = true },
330        num_sides = { docs = "The number of sides in the polygon", include_in_snippet = true },
331        center = { docs = "The center point of the polygon", snippet_value_array = ["0", "0"] },
332        inscribed = { docs = "Whether the polygon is inscribed (true, the default) or circumscribed (false) about a circle with the specified radius" },
333    },
334    tags = ["sketch"]
335}]
336#[allow(clippy::too_many_arguments)]
337async fn inner_polygon(
338    sketch_surface_or_group: SketchOrSurface,
339    radius: TyF64,
340    num_sides: u64,
341    center: [TyF64; 2],
342    inscribed: Option<bool>,
343    exec_state: &mut ExecState,
344    args: Args,
345) -> Result<Sketch, KclError> {
346    if num_sides < 3 {
347        return Err(KclError::Type(KclErrorDetails::new(
348            "Polygon must have at least 3 sides".to_string(),
349            vec![args.source_range],
350        )));
351    }
352
353    if radius.n <= 0.0 {
354        return Err(KclError::Type(KclErrorDetails::new(
355            "Radius must be greater than 0".to_string(),
356            vec![args.source_range],
357        )));
358    }
359
360    let (sketch_surface, units) = match sketch_surface_or_group {
361        SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.expect_length()),
362        SketchOrSurface::Sketch(group) => (group.on, group.units),
363    };
364
365    let half_angle = std::f64::consts::PI / num_sides as f64;
366
367    let radius_to_vertices = if inscribed.unwrap_or(true) {
368        // inscribed
369        radius.n
370    } else {
371        // circumscribed
372        radius.n / half_angle.cos()
373    };
374
375    let angle_step = std::f64::consts::TAU / num_sides as f64;
376
377    let center_u = point_to_len_unit(center, units);
378
379    let vertices: Vec<[f64; 2]> = (0..num_sides)
380        .map(|i| {
381            let angle = angle_step * i as f64;
382            [
383                center_u[0] + radius_to_vertices * angle.cos(),
384                center_u[1] + radius_to_vertices * angle.sin(),
385            ]
386        })
387        .collect();
388
389    let mut sketch = crate::std::sketch::inner_start_profile(
390        sketch_surface,
391        point_to_typed(vertices[0], units),
392        None,
393        exec_state,
394        args.clone(),
395    )
396    .await?;
397
398    // Draw all the lines with unique IDs and modified tags
399    for vertex in vertices.iter().skip(1) {
400        let from = sketch.current_pen_position()?;
401        let id = exec_state.next_uuid();
402
403        args.batch_modeling_cmd(
404            id,
405            ModelingCmd::from(mcmd::ExtendPath {
406                path: sketch.id.into(),
407                segment: PathSegment::Line {
408                    end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
409                        .with_z(0.0)
410                        .map(LengthUnit),
411                    relative: false,
412                },
413            }),
414        )
415        .await?;
416
417        let current_path = Path::ToPoint {
418            base: BasePath {
419                from: from.ignore_units(),
420                to: *vertex,
421                tag: None,
422                units: sketch.units,
423                geo_meta: GeoMeta {
424                    id,
425                    metadata: args.source_range.into(),
426                },
427            },
428        };
429
430        sketch.paths.push(current_path);
431    }
432
433    // Close the polygon by connecting back to the first vertex with a new ID
434    let from = sketch.current_pen_position()?;
435    let close_id = exec_state.next_uuid();
436
437    args.batch_modeling_cmd(
438        close_id,
439        ModelingCmd::from(mcmd::ExtendPath {
440            path: sketch.id.into(),
441            segment: PathSegment::Line {
442                end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
443                    .with_z(0.0)
444                    .map(LengthUnit),
445                relative: false,
446            },
447        }),
448    )
449    .await?;
450
451    let current_path = Path::ToPoint {
452        base: BasePath {
453            from: from.ignore_units(),
454            to: vertices[0],
455            tag: None,
456            units: sketch.units,
457            geo_meta: GeoMeta {
458                id: close_id,
459                metadata: args.source_range.into(),
460            },
461        },
462    };
463
464    sketch.paths.push(current_path);
465
466    args.batch_modeling_cmd(
467        exec_state.next_uuid(),
468        ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
469    )
470    .await?;
471
472    Ok(sketch)
473}