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 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#[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
43pub 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, ¤t_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
126pub 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#[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
161async 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 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 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, ¤t_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#[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
255pub 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#[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 radius.n
351 } else {
352 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 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 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}