1use anyhow::Result;
4use kcmc::{
5 each_cmd as mcmd,
6 length_unit::LengthUnit,
7 shared::{Angle, Point2d as KPoint2d},
8 ModelingCmd,
9};
10use kittycad_modeling_cmds as kcmc;
11use kittycad_modeling_cmds::shared::PathSegment;
12use schemars::JsonSchema;
13use serde::Serialize;
14
15use super::{
16 args::TyF64,
17 utils::{point_to_len_unit, point_to_mm, point_to_typed, untype_point, untyped_point_to_mm},
18};
19use crate::{
20 errors::{KclError, KclErrorDetails},
21 execution::{
22 types::{RuntimeType, UnitLen},
23 BasePath, ExecState, GeoMeta, KclValue, Path, Sketch, SketchSurface,
24 },
25 parsing::ast::types::TagNode,
26 std::{
27 sketch::NEW_TAG_KW,
28 utils::{calculate_circle_center, distance},
29 Args,
30 },
31 SourceRange,
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 =
46 args.get_unlabeled_kw_arg_typed("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
47 let center = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
48 let radius: Option<TyF64> = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
49 let diameter: Option<TyF64> = args.get_kw_arg_opt_typed("diameter", &RuntimeType::length(), exec_state)?;
50 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
51
52 let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
53 Ok(KclValue::Sketch {
54 value: Box::new(sketch),
55 })
56}
57
58async fn inner_circle(
59 sketch_or_surface: SketchOrSurface,
60 center: [TyF64; 2],
61 radius: Option<TyF64>,
62 diameter: Option<TyF64>,
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 (center_u, ty) = untype_point(center.clone());
72 let units = ty.expect_length();
73
74 let radius = get_radius(radius, diameter, args.source_range)?;
75 let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
76 let from_t = [TyF64::new(from[0], ty.clone()), TyF64::new(from[1], ty)];
77
78 let sketch =
79 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
80
81 let angle_start = Angle::zero();
82 let angle_end = Angle::turn();
83
84 let id = exec_state.next_uuid();
85
86 args.batch_modeling_cmd(
87 id,
88 ModelingCmd::from(mcmd::ExtendPath {
89 path: sketch.id.into(),
90 segment: PathSegment::Arc {
91 start: angle_start,
92 end: angle_end,
93 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
94 radius: LengthUnit(radius.to_mm()),
95 relative: false,
96 },
97 }),
98 )
99 .await?;
100
101 let current_path = Path::Circle {
102 base: BasePath {
103 from,
104 to: from,
105 tag: tag.clone(),
106 units,
107 geo_meta: GeoMeta {
108 id,
109 metadata: args.source_range.into(),
110 },
111 },
112 radius: radius.to_length_units(units),
113 center: center_u,
114 ccw: angle_start < angle_end,
115 };
116
117 let mut new_sketch = sketch.clone();
118 if let Some(tag) = &tag {
119 new_sketch.add_tag(tag, ¤t_path, exec_state);
120 }
121
122 new_sketch.paths.push(current_path);
123
124 args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
125 .await?;
126
127 Ok(new_sketch)
128}
129
130pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
132 let sketch_or_surface =
133 args.get_unlabeled_kw_arg_typed("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
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(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
140 Ok(KclValue::Sketch {
141 value: Box::new(sketch),
142 })
143}
144
145async fn inner_circle_three_point(
148 sketch_surface_or_group: SketchOrSurface,
149 p1: [TyF64; 2],
150 p2: [TyF64; 2],
151 p3: [TyF64; 2],
152 tag: Option<TagNode>,
153 exec_state: &mut ExecState,
154 args: Args,
155) -> Result<Sketch, KclError> {
156 let ty = p1[0].ty.clone();
157 let units = ty.expect_length();
158
159 let p1 = point_to_len_unit(p1, units);
160 let p2 = point_to_len_unit(p2, units);
161 let p3 = point_to_len_unit(p3, units);
162
163 let center = calculate_circle_center(p1, p2, p3);
164 let radius = distance(center, p2);
166
167 let sketch_surface = match sketch_surface_or_group {
168 SketchOrSurface::SketchSurface(surface) => surface,
169 SketchOrSurface::Sketch(group) => group.on,
170 };
171
172 let from = [
173 TyF64::new(center[0] + radius, ty.clone()),
174 TyF64::new(center[1], ty.clone()),
175 ];
176 let sketch =
177 crate::std::sketch::inner_start_profile(sketch_surface, from.clone(), None, exec_state, args.clone()).await?;
178
179 let angle_start = Angle::zero();
180 let angle_end = Angle::turn();
181
182 let id = exec_state.next_uuid();
183
184 args.batch_modeling_cmd(
185 id,
186 ModelingCmd::from(mcmd::ExtendPath {
187 path: sketch.id.into(),
188 segment: PathSegment::Arc {
189 start: angle_start,
190 end: angle_end,
191 center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
192 radius: units.adjust_to(radius, UnitLen::Mm).0.into(),
193 relative: false,
194 },
195 }),
196 )
197 .await?;
198
199 let current_path = Path::CircleThreePoint {
200 base: BasePath {
201 from: untype_point(from.clone()).0,
203 to: untype_point(from).0,
204 tag: tag.clone(),
205 units,
206 geo_meta: GeoMeta {
207 id,
208 metadata: args.source_range.into(),
209 },
210 },
211 p1,
212 p2,
213 p3,
214 };
215
216 let mut new_sketch = sketch.clone();
217 if let Some(tag) = &tag {
218 new_sketch.add_tag(tag, ¤t_path, exec_state);
219 }
220
221 new_sketch.paths.push(current_path);
222
223 args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }))
224 .await?;
225
226 Ok(new_sketch)
227}
228
229#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
231#[ts(export)]
232#[serde(rename_all = "lowercase")]
233pub enum PolygonType {
234 #[default]
235 Inscribed,
236 Circumscribed,
237}
238
239pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
241 let sketch_or_surface =
242 args.get_unlabeled_kw_arg_typed("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
243 let radius: TyF64 = args.get_kw_arg_typed("radius", &RuntimeType::length(), exec_state)?;
244 let num_sides: TyF64 = args.get_kw_arg_typed("numSides", &RuntimeType::count(), exec_state)?;
245 let center = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
246 let inscribed = args.get_kw_arg_opt_typed("inscribed", &RuntimeType::bool(), exec_state)?;
247
248 let sketch = inner_polygon(
249 sketch_or_surface,
250 radius,
251 num_sides.n as u64,
252 center,
253 inscribed,
254 exec_state,
255 args,
256 )
257 .await?;
258 Ok(KclValue::Sketch {
259 value: Box::new(sketch),
260 })
261}
262
263#[allow(clippy::too_many_arguments)]
264async fn inner_polygon(
265 sketch_surface_or_group: SketchOrSurface,
266 radius: TyF64,
267 num_sides: u64,
268 center: [TyF64; 2],
269 inscribed: Option<bool>,
270 exec_state: &mut ExecState,
271 args: Args,
272) -> Result<Sketch, KclError> {
273 if num_sides < 3 {
274 return Err(KclError::new_type(KclErrorDetails::new(
275 "Polygon must have at least 3 sides".to_string(),
276 vec![args.source_range],
277 )));
278 }
279
280 if radius.n <= 0.0 {
281 return Err(KclError::new_type(KclErrorDetails::new(
282 "Radius must be greater than 0".to_string(),
283 vec![args.source_range],
284 )));
285 }
286
287 let (sketch_surface, units) = match sketch_surface_or_group {
288 SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.expect_length()),
289 SketchOrSurface::Sketch(group) => (group.on, group.units),
290 };
291
292 let half_angle = std::f64::consts::PI / num_sides as f64;
293
294 let radius_to_vertices = if inscribed.unwrap_or(true) {
295 radius.n
297 } else {
298 radius.n / half_angle.cos()
300 };
301
302 let angle_step = std::f64::consts::TAU / num_sides as f64;
303
304 let center_u = point_to_len_unit(center, units);
305
306 let vertices: Vec<[f64; 2]> = (0..num_sides)
307 .map(|i| {
308 let angle = angle_step * i as f64;
309 [
310 center_u[0] + radius_to_vertices * angle.cos(),
311 center_u[1] + radius_to_vertices * angle.sin(),
312 ]
313 })
314 .collect();
315
316 let mut sketch = crate::std::sketch::inner_start_profile(
317 sketch_surface,
318 point_to_typed(vertices[0], units),
319 None,
320 exec_state,
321 args.clone(),
322 )
323 .await?;
324
325 for vertex in vertices.iter().skip(1) {
327 let from = sketch.current_pen_position()?;
328 let id = exec_state.next_uuid();
329
330 args.batch_modeling_cmd(
331 id,
332 ModelingCmd::from(mcmd::ExtendPath {
333 path: sketch.id.into(),
334 segment: PathSegment::Line {
335 end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
336 .with_z(0.0)
337 .map(LengthUnit),
338 relative: false,
339 },
340 }),
341 )
342 .await?;
343
344 let current_path = Path::ToPoint {
345 base: BasePath {
346 from: from.ignore_units(),
347 to: *vertex,
348 tag: None,
349 units: sketch.units,
350 geo_meta: GeoMeta {
351 id,
352 metadata: args.source_range.into(),
353 },
354 },
355 };
356
357 sketch.paths.push(current_path);
358 }
359
360 let from = sketch.current_pen_position()?;
362 let close_id = exec_state.next_uuid();
363
364 args.batch_modeling_cmd(
365 close_id,
366 ModelingCmd::from(mcmd::ExtendPath {
367 path: sketch.id.into(),
368 segment: PathSegment::Line {
369 end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
370 .with_z(0.0)
371 .map(LengthUnit),
372 relative: false,
373 },
374 }),
375 )
376 .await?;
377
378 let current_path = Path::ToPoint {
379 base: BasePath {
380 from: from.ignore_units(),
381 to: vertices[0],
382 tag: None,
383 units: sketch.units,
384 geo_meta: GeoMeta {
385 id: close_id,
386 metadata: args.source_range.into(),
387 },
388 },
389 };
390
391 sketch.paths.push(current_path);
392
393 args.batch_modeling_cmd(
394 exec_state.next_uuid(),
395 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
396 )
397 .await?;
398
399 Ok(sketch)
400}
401
402pub(crate) fn get_radius(
403 radius: Option<TyF64>,
404 diameter: Option<TyF64>,
405 source_range: SourceRange,
406) -> Result<TyF64, KclError> {
407 match (radius, diameter) {
408 (Some(radius), None) => Ok(radius),
409 (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
410 (None, None) => Err(KclError::new_type(KclErrorDetails::new(
411 "This function needs either `diameter` or `radius`".to_string(),
412 vec![source_range],
413 ))),
414 (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
415 "You cannot specify both `diameter` and `radius`, please remove one".to_string(),
416 vec![source_range],
417 ))),
418 }
419}