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::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#[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
38pub 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, ¤t_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
131pub 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#[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
174async 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 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, ¤t_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#[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#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
264#[ts(export)]
265#[serde(rename_all = "camelCase")]
266pub struct PolygonData {
267 pub radius: TyF64,
269 pub num_sides: u64,
271 pub center: [TyF64; 2],
273 #[serde(skip)]
275 pub polygon_type: PolygonType,
276 #[serde(default = "default_inscribed")]
278 pub inscribed: bool,
279}
280
281fn default_inscribed() -> bool {
282 true
283}
284
285pub 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#[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 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, ¤t_path, exec_state);
405 }
406
407 sketch.paths.push(current_path);
408 }
409
410 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, ¤t_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}