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
49#[stdlib {
71 name = "circle",
72 keywords = true,
73 unlabeled_first = true,
74 args = {
75 sketch_or_surface = {docs = "Plane or surface to sketch on."},
76 center = {docs = "The center of the circle."},
77 radius = {docs = "The radius of the circle."},
78 tag = { docs = "Create a new tag which refers to this circle"},
79 }
80}]
81async fn inner_circle(
82 sketch_or_surface: SketchOrSurface,
83 center: [f64; 2],
84 radius: f64,
85 tag: Option<TagNode>,
86 exec_state: &mut ExecState,
87 args: Args,
88) -> Result<Sketch, KclError> {
89 let sketch_surface = match sketch_or_surface {
90 SketchOrSurface::SketchSurface(surface) => surface,
91 SketchOrSurface::Sketch(s) => s.on,
92 };
93 let units = sketch_surface.units();
94 let sketch = crate::std::sketch::inner_start_profile_at(
95 [center[0] + radius, center[1]],
96 sketch_surface,
97 None,
98 exec_state,
99 args.clone(),
100 )
101 .await?;
102
103 let from = [center[0] + radius, center[1]];
104 let angle_start = Angle::zero();
105 let angle_end = Angle::turn();
106
107 let id = exec_state.next_uuid();
108
109 args.batch_modeling_cmd(
110 id,
111 ModelingCmd::from(mcmd::ExtendPath {
112 path: sketch.id.into(),
113 segment: PathSegment::Arc {
114 start: angle_start,
115 end: angle_end,
116 center: KPoint2d::from(center).map(LengthUnit),
117 radius: radius.into(),
118 relative: false,
119 },
120 }),
121 )
122 .await?;
123
124 let current_path = Path::Circle {
125 base: BasePath {
126 from,
127 to: from,
128 tag: tag.clone(),
129 units,
130 geo_meta: GeoMeta {
131 id,
132 metadata: args.source_range.into(),
133 },
134 },
135 radius,
136 center,
137 ccw: angle_start < angle_end,
138 };
139
140 let mut new_sketch = sketch.clone();
141 if let Some(tag) = &tag {
142 new_sketch.add_tag(tag, ¤t_path, exec_state);
143 }
144
145 new_sketch.paths.push(current_path);
146
147 args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
148 .await?;
149
150 Ok(new_sketch)
151}
152
153pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
155 let p1 = args.get_kw_arg("p1")?;
156 let p2 = args.get_kw_arg("p2")?;
157 let p3 = args.get_kw_arg("p3")?;
158 let sketch_surface_or_group = args.get_unlabeled_kw_arg("sketch_surface_or_group")?;
159 let tag = args.get_kw_arg_opt("tag")?;
160
161 let sketch = inner_circle_three_point(p1, p2, p3, sketch_surface_or_group, tag, exec_state, args).await?;
162 Ok(KclValue::Sketch {
163 value: Box::new(sketch),
164 })
165}
166
167#[stdlib {
175 name = "circleThreePoint",
176 keywords = true,
177 unlabeled_first = true,
178 args = {
179 p1 = {docs = "1st point to derive the circle."},
180 p2 = {docs = "2nd point to derive the circle."},
181 p3 = {docs = "3rd point to derive the circle."},
182 sketch_surface_or_group = {docs = "Plane or surface to sketch on."},
183 tag = {docs = "Identifier for the circle to reference elsewhere."},
184 }
185}]
186
187async fn inner_circle_three_point(
190 p1: [f64; 2],
191 p2: [f64; 2],
192 p3: [f64; 2],
193 sketch_surface_or_group: SketchOrSurface,
194 tag: Option<TagNode>,
195 exec_state: &mut ExecState,
196 args: Args,
197) -> Result<Sketch, KclError> {
198 let center = calculate_circle_center(p1, p2, p3);
199 let radius = distance(center.into(), p2.into());
201
202 let sketch_surface = match sketch_surface_or_group {
203 SketchOrSurface::SketchSurface(surface) => surface,
204 SketchOrSurface::Sketch(group) => group.on,
205 };
206 let sketch = crate::std::sketch::inner_start_profile_at(
207 [center[0] + radius, center[1]],
208 sketch_surface,
209 None,
210 exec_state,
211 args.clone(),
212 )
213 .await?;
214
215 let from = [center[0] + radius, center[1]];
216 let angle_start = Angle::zero();
217 let angle_end = Angle::turn();
218
219 let id = exec_state.next_uuid();
220
221 args.batch_modeling_cmd(
222 id,
223 ModelingCmd::from(mcmd::ExtendPath {
224 path: sketch.id.into(),
225 segment: PathSegment::Arc {
226 start: angle_start,
227 end: angle_end,
228 center: KPoint2d::from(center).map(LengthUnit),
229 radius: radius.into(),
230 relative: false,
231 },
232 }),
233 )
234 .await?;
235
236 let current_path = Path::CircleThreePoint {
237 base: BasePath {
238 from,
239 to: from,
240 tag: tag.clone(),
241 units: sketch.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, ¤t_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#[derive(Debug, Clone, Deserialize, 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#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
277#[ts(export)]
278#[serde(rename_all = "camelCase")]
279pub struct PolygonData {
280 pub radius: f64,
282 pub num_sides: u64,
284 pub center: [f64; 2],
286 #[serde(skip)]
288 pub polygon_type: PolygonType,
289 #[serde(default = "default_inscribed")]
291 pub inscribed: bool,
292}
293
294fn default_inscribed() -> bool {
295 true
296}
297
298pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
300 let (data, sketch_surface_or_group, tag): (PolygonData, SketchOrSurface, Option<TagNode>) =
301 args.get_polygon_args()?;
302
303 let sketch = inner_polygon(data, sketch_surface_or_group, tag, exec_state, args).await?;
304 Ok(KclValue::Sketch {
305 value: Box::new(sketch),
306 })
307}
308
309#[stdlib {
336 name = "polygon",
337}]
338async fn inner_polygon(
339 data: PolygonData,
340 sketch_surface_or_group: SketchOrSurface,
341 tag: Option<TagNode>,
342 exec_state: &mut ExecState,
343 args: Args,
344) -> Result<Sketch, KclError> {
345 if data.num_sides < 3 {
346 return Err(KclError::Type(KclErrorDetails {
347 message: "Polygon must have at least 3 sides".to_string(),
348 source_ranges: vec![args.source_range],
349 }));
350 }
351
352 if data.radius <= 0.0 {
353 return Err(KclError::Type(KclErrorDetails {
354 message: "Radius must be greater than 0".to_string(),
355 source_ranges: vec![args.source_range],
356 }));
357 }
358
359 let sketch_surface = match sketch_surface_or_group {
360 SketchOrSurface::SketchSurface(surface) => surface,
361 SketchOrSurface::Sketch(group) => group.on,
362 };
363
364 let half_angle = std::f64::consts::PI / data.num_sides as f64;
365
366 let radius_to_vertices = match data.polygon_type {
367 PolygonType::Inscribed => data.radius,
368 PolygonType::Circumscribed => data.radius / half_angle.cos(),
369 };
370
371 let angle_step = 2.0 * std::f64::consts::PI / data.num_sides as f64;
372
373 let vertices: Vec<[f64; 2]> = (0..data.num_sides)
374 .map(|i| {
375 let angle = angle_step * i as f64;
376 [
377 data.center[0] + radius_to_vertices * angle.cos(),
378 data.center[1] + radius_to_vertices * angle.sin(),
379 ]
380 })
381 .collect();
382
383 let mut sketch =
384 crate::std::sketch::inner_start_profile_at(vertices[0], sketch_surface, None, exec_state, args.clone()).await?;
385
386 for vertex in vertices.iter().skip(1) {
388 let from = sketch.current_pen_position()?;
389 let id = exec_state.next_uuid();
390
391 args.batch_modeling_cmd(
392 id,
393 ModelingCmd::from(mcmd::ExtendPath {
394 path: sketch.id.into(),
395 segment: PathSegment::Line {
396 end: KPoint2d::from(*vertex).with_z(0.0).map(LengthUnit),
397 relative: false,
398 },
399 }),
400 )
401 .await?;
402
403 let current_path = Path::ToPoint {
404 base: BasePath {
405 from: from.into(),
406 to: *vertex,
407 tag: tag.clone(),
408 units: sketch.units,
409 geo_meta: GeoMeta {
410 id,
411 metadata: args.source_range.into(),
412 },
413 },
414 };
415
416 if let Some(tag) = &tag {
417 sketch.add_tag(tag, ¤t_path, exec_state);
418 }
419
420 sketch.paths.push(current_path);
421 }
422
423 let from = sketch.current_pen_position()?;
425 let close_id = exec_state.next_uuid();
426
427 args.batch_modeling_cmd(
428 close_id,
429 ModelingCmd::from(mcmd::ExtendPath {
430 path: sketch.id.into(),
431 segment: PathSegment::Line {
432 end: KPoint2d::from(vertices[0]).with_z(0.0).map(LengthUnit),
433 relative: false,
434 },
435 }),
436 )
437 .await?;
438
439 let current_path = Path::ToPoint {
440 base: BasePath {
441 from: from.into(),
442 to: vertices[0],
443 tag: tag.clone(),
444 units: sketch.units,
445 geo_meta: GeoMeta {
446 id: close_id,
447 metadata: args.source_range.into(),
448 },
449 },
450 };
451
452 if let Some(tag) = &tag {
453 sketch.add_tag(tag, ¤t_path, exec_state);
454 }
455
456 sketch.paths.push(current_path);
457
458 args.batch_modeling_cmd(
459 exec_state.next_uuid(),
460 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
461 )
462 .await?;
463
464 Ok(sketch)
465}