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