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: Option<TyF64> = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
48 let diameter: Option<TyF64> = args.get_kw_arg_opt_typed("diameter", &RuntimeType::length(), exec_state)?;
49 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
50
51 let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
52 Ok(KclValue::Sketch {
53 value: Box::new(sketch),
54 })
55}
56
57async fn inner_circle(
58 sketch_or_surface: SketchOrSurface,
59 center: [TyF64; 2],
60 radius: Option<TyF64>,
61 diameter: Option<TyF64>,
62 tag: Option<TagNode>,
63 exec_state: &mut ExecState,
64 args: Args,
65) -> Result<Sketch, KclError> {
66 let sketch_surface = match sketch_or_surface {
67 SketchOrSurface::SketchSurface(surface) => surface,
68 SketchOrSurface::Sketch(s) => s.on,
69 };
70 let (center_u, ty) = untype_point(center.clone());
71 let units = ty.expect_length();
72
73 let radius = match (radius, diameter) {
74 (Some(radius), None) => radius,
75 (None, Some(mut diameter)) => {
76 diameter.n /= 2.0;
77 diameter
78 }
79 (None, None) => {
80 return Err(KclError::Type(KclErrorDetails::new(
81 "This function needs either `diameter` or `radius`".to_string(),
82 vec![args.source_range],
83 )))
84 }
85 (Some(_), Some(_)) => {
86 return Err(KclError::Type(KclErrorDetails::new(
87 "You cannot specify both `diameter` and `radius`, please remove one".to_string(),
88 vec![args.source_range],
89 )))
90 }
91 };
92 let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
93 let from_t = [TyF64::new(from[0], ty.clone()), TyF64::new(from[1], ty)];
94
95 let sketch =
96 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
97
98 let angle_start = Angle::zero();
99 let angle_end = Angle::turn();
100
101 let id = exec_state.next_uuid();
102
103 args.batch_modeling_cmd(
104 id,
105 ModelingCmd::from(mcmd::ExtendPath {
106 path: sketch.id.into(),
107 segment: PathSegment::Arc {
108 start: angle_start,
109 end: angle_end,
110 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
111 radius: LengthUnit(radius.to_mm()),
112 relative: false,
113 },
114 }),
115 )
116 .await?;
117
118 let current_path = Path::Circle {
119 base: BasePath {
120 from,
121 to: from,
122 tag: tag.clone(),
123 units,
124 geo_meta: GeoMeta {
125 id,
126 metadata: args.source_range.into(),
127 },
128 },
129 radius: radius.to_length_units(units),
130 center: center_u,
131 ccw: angle_start < angle_end,
132 };
133
134 let mut new_sketch = sketch.clone();
135 if let Some(tag) = &tag {
136 new_sketch.add_tag(tag, ¤t_path, exec_state);
137 }
138
139 new_sketch.paths.push(current_path);
140
141 args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
142 .await?;
143
144 Ok(new_sketch)
145}
146
147pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
149 let sketch_surface_or_group = args.get_unlabeled_kw_arg("sketch_surface_or_group")?;
150 let p1 = args.get_kw_arg_typed("p1", &RuntimeType::point2d(), exec_state)?;
151 let p2 = args.get_kw_arg_typed("p2", &RuntimeType::point2d(), exec_state)?;
152 let p3 = args.get_kw_arg_typed("p3", &RuntimeType::point2d(), exec_state)?;
153 let tag = args.get_kw_arg_opt("tag")?;
154
155 let sketch = inner_circle_three_point(sketch_surface_or_group, p1, p2, p3, tag, exec_state, args).await?;
156 Ok(KclValue::Sketch {
157 value: Box::new(sketch),
158 })
159}
160
161#[stdlib {
169 name = "circleThreePoint",
170 unlabeled_first = true,
171 args = {
172 sketch_surface_or_group = {docs = "Plane or surface to sketch on."},
173 p1 = {docs = "1st point to derive the circle."},
174 p2 = {docs = "2nd point to derive the circle."},
175 p3 = {docs = "3rd point to derive the circle."},
176 tag = {docs = "Identifier for the circle to reference elsewhere."},
177 },
178 tags = ["sketch"]
179}]
180
181async fn inner_circle_three_point(
184 sketch_surface_or_group: SketchOrSurface,
185 p1: [TyF64; 2],
186 p2: [TyF64; 2],
187 p3: [TyF64; 2],
188 tag: Option<TagNode>,
189 exec_state: &mut ExecState,
190 args: Args,
191) -> Result<Sketch, KclError> {
192 let ty = p1[0].ty.clone();
193 let units = ty.expect_length();
194
195 let p1 = point_to_len_unit(p1, units);
196 let p2 = point_to_len_unit(p2, units);
197 let p3 = point_to_len_unit(p3, units);
198
199 let center = calculate_circle_center(p1, p2, p3);
200 let radius = distance(center, p2);
202
203 let sketch_surface = match sketch_surface_or_group {
204 SketchOrSurface::SketchSurface(surface) => surface,
205 SketchOrSurface::Sketch(group) => group.on,
206 };
207
208 let from = [
209 TyF64::new(center[0] + radius, ty.clone()),
210 TyF64::new(center[1], ty.clone()),
211 ];
212 let sketch =
213 crate::std::sketch::inner_start_profile(sketch_surface, from.clone(), None, exec_state, args.clone()).await?;
214
215 let angle_start = Angle::zero();
216 let angle_end = Angle::turn();
217
218 let id = exec_state.next_uuid();
219
220 args.batch_modeling_cmd(
221 id,
222 ModelingCmd::from(mcmd::ExtendPath {
223 path: sketch.id.into(),
224 segment: PathSegment::Arc {
225 start: angle_start,
226 end: angle_end,
227 center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
228 radius: units.adjust_to(radius, UnitLen::Mm).0.into(),
229 relative: false,
230 },
231 }),
232 )
233 .await?;
234
235 let current_path = Path::CircleThreePoint {
236 base: BasePath {
237 from: untype_point(from.clone()).0,
239 to: untype_point(from).0,
240 tag: tag.clone(),
241 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, 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
275pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
277 let sketch_surface_or_group = args.get_unlabeled_kw_arg("sketchOrSurface")?;
278 let radius: TyF64 = args.get_kw_arg_typed("radius", &RuntimeType::length(), exec_state)?;
279 let num_sides: TyF64 = args.get_kw_arg_typed("numSides", &RuntimeType::count(), exec_state)?;
280 let center = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
281 let inscribed = args.get_kw_arg_opt_typed("inscribed", &RuntimeType::bool(), exec_state)?;
282
283 let sketch = inner_polygon(
284 sketch_surface_or_group,
285 radius,
286 num_sides.n as u64,
287 center,
288 inscribed,
289 exec_state,
290 args,
291 )
292 .await?;
293 Ok(KclValue::Sketch {
294 value: Box::new(sketch),
295 })
296}
297
298#[stdlib {
325 name = "polygon",
326 unlabeled_first = true,
327 args = {
328 sketch_surface_or_group = { docs = "Plane or surface to sketch on" },
329 radius = { docs = "The radius of the polygon", include_in_snippet = true },
330 num_sides = { docs = "The number of sides in the polygon", include_in_snippet = true },
331 center = { docs = "The center point of the polygon", snippet_value_array = ["0", "0"] },
332 inscribed = { docs = "Whether the polygon is inscribed (true, the default) or circumscribed (false) about a circle with the specified radius" },
333 },
334 tags = ["sketch"]
335}]
336#[allow(clippy::too_many_arguments)]
337async fn inner_polygon(
338 sketch_surface_or_group: SketchOrSurface,
339 radius: TyF64,
340 num_sides: u64,
341 center: [TyF64; 2],
342 inscribed: Option<bool>,
343 exec_state: &mut ExecState,
344 args: Args,
345) -> Result<Sketch, KclError> {
346 if num_sides < 3 {
347 return Err(KclError::Type(KclErrorDetails::new(
348 "Polygon must have at least 3 sides".to_string(),
349 vec![args.source_range],
350 )));
351 }
352
353 if radius.n <= 0.0 {
354 return Err(KclError::Type(KclErrorDetails::new(
355 "Radius must be greater than 0".to_string(),
356 vec![args.source_range],
357 )));
358 }
359
360 let (sketch_surface, units) = match sketch_surface_or_group {
361 SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.expect_length()),
362 SketchOrSurface::Sketch(group) => (group.on, group.units),
363 };
364
365 let half_angle = std::f64::consts::PI / num_sides as f64;
366
367 let radius_to_vertices = if inscribed.unwrap_or(true) {
368 radius.n
370 } else {
371 radius.n / half_angle.cos()
373 };
374
375 let angle_step = std::f64::consts::TAU / num_sides as f64;
376
377 let center_u = point_to_len_unit(center, units);
378
379 let vertices: Vec<[f64; 2]> = (0..num_sides)
380 .map(|i| {
381 let angle = angle_step * i as f64;
382 [
383 center_u[0] + radius_to_vertices * angle.cos(),
384 center_u[1] + radius_to_vertices * angle.sin(),
385 ]
386 })
387 .collect();
388
389 let mut sketch = crate::std::sketch::inner_start_profile(
390 sketch_surface,
391 point_to_typed(vertices[0], units),
392 None,
393 exec_state,
394 args.clone(),
395 )
396 .await?;
397
398 for vertex in vertices.iter().skip(1) {
400 let from = sketch.current_pen_position()?;
401 let id = exec_state.next_uuid();
402
403 args.batch_modeling_cmd(
404 id,
405 ModelingCmd::from(mcmd::ExtendPath {
406 path: sketch.id.into(),
407 segment: PathSegment::Line {
408 end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
409 .with_z(0.0)
410 .map(LengthUnit),
411 relative: false,
412 },
413 }),
414 )
415 .await?;
416
417 let current_path = Path::ToPoint {
418 base: BasePath {
419 from: from.ignore_units(),
420 to: *vertex,
421 tag: None,
422 units: sketch.units,
423 geo_meta: GeoMeta {
424 id,
425 metadata: args.source_range.into(),
426 },
427 },
428 };
429
430 sketch.paths.push(current_path);
431 }
432
433 let from = sketch.current_pen_position()?;
435 let close_id = exec_state.next_uuid();
436
437 args.batch_modeling_cmd(
438 close_id,
439 ModelingCmd::from(mcmd::ExtendPath {
440 path: sketch.id.into(),
441 segment: PathSegment::Line {
442 end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
443 .with_z(0.0)
444 .map(LengthUnit),
445 relative: false,
446 },
447 }),
448 )
449 .await?;
450
451 let current_path = Path::ToPoint {
452 base: BasePath {
453 from: from.ignore_units(),
454 to: vertices[0],
455 tag: None,
456 units: sketch.units,
457 geo_meta: GeoMeta {
458 id: close_id,
459 metadata: args.source_range.into(),
460 },
461 },
462 };
463
464 sketch.paths.push(current_path);
465
466 args.batch_modeling_cmd(
467 exec_state.next_uuid(),
468 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
469 )
470 .await?;
471
472 Ok(sketch)
473}