1use 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#[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
36pub 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, ¤t_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
121pub 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#[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
155async 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 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, ¤t_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#[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#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
245#[ts(export)]
246#[serde(rename_all = "camelCase")]
247pub struct PolygonData {
248 pub radius: f64,
250 pub num_sides: u64,
252 pub center: [f64; 2],
254 #[serde(skip)]
256 pub polygon_type: PolygonType,
257 #[serde(default = "default_inscribed")]
259 pub inscribed: bool,
260}
261
262fn default_inscribed() -> bool {
263 true
264}
265
266pub 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#[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 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, ¤t_path, exec_state);
386 }
387
388 sketch.paths.push(current_path);
389 }
390
391 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, ¤t_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}