1use anyhow::Result;
4use kcmc::{
5 ModelingCmd, each_cmd as mcmd,
6 length_unit::LengthUnit,
7 shared::{Angle, Point2d as KPoint2d},
8};
9use kittycad_modeling_cmds as kcmc;
10use kittycad_modeling_cmds::shared::PathSegment;
11use schemars::JsonSchema;
12use serde::Serialize;
13
14use super::{
15 args::TyF64,
16 utils::{point_to_len_unit, point_to_mm, point_to_typed, untype_point, untyped_point_to_mm},
17};
18use crate::{
19 SourceRange,
20 errors::{KclError, KclErrorDetails},
21 execution::{
22 BasePath, ExecState, GeoMeta, KclValue, ModelingCmdMeta, Path, Sketch, SketchSurface,
23 types::{RuntimeType, UnitLen},
24 },
25 parsing::ast::types::TagNode,
26 std::{
27 Args,
28 utils::{calculate_circle_center, distance},
29 },
30};
31
32#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
34#[ts(export)]
35#[serde(untagged)]
36pub enum SketchOrSurface {
37 SketchSurface(SketchSurface),
38 Sketch(Box<Sketch>),
39}
40
41pub async fn rectangle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
43 let sketch_or_surface =
44 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
45 let center = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
46 let corner = args.get_kw_arg_opt("corner", &RuntimeType::point2d(), exec_state)?;
47 let width: TyF64 = args.get_kw_arg("width", &RuntimeType::length(), exec_state)?;
48 let height: TyF64 = args.get_kw_arg("height", &RuntimeType::length(), exec_state)?;
49
50 inner_rectangle(sketch_or_surface, center, corner, width, height, exec_state, args)
51 .await
52 .map(Box::new)
53 .map(|value| KclValue::Sketch { value })
54}
55
56async fn inner_rectangle(
57 sketch_or_surface: SketchOrSurface,
58 center: Option<[TyF64; 2]>,
59 corner: Option<[TyF64; 2]>,
60 width: TyF64,
61 height: TyF64,
62 exec_state: &mut ExecState,
63 args: Args,
64) -> Result<Sketch, KclError> {
65 let sketch_surface = match sketch_or_surface {
66 SketchOrSurface::SketchSurface(surface) => surface,
67 SketchOrSurface::Sketch(s) => s.on,
68 };
69
70 let (ty, corner) = match (center, corner) {
72 (Some(center), None) => (
73 center[0].ty,
74 [center[0].n - width.n / 2.0, center[1].n - height.n / 2.0],
75 ),
76 (None, Some(corner)) => (corner[0].ty, [corner[0].n, corner[1].n]),
77 (None, None) => {
78 return Err(KclError::new_semantic(KclErrorDetails::new(
79 "You must supply either `corner` or `center` arguments, but not both".to_string(),
80 vec![args.source_range],
81 )));
82 }
83 (Some(_), Some(_)) => {
84 return Err(KclError::new_semantic(KclErrorDetails::new(
85 "You must supply either `corner` or `center` arguments, but not both".to_string(),
86 vec![args.source_range],
87 )));
88 }
89 };
90 let units = ty.expect_length();
91 let corner_t = [TyF64::new(corner[0], ty), TyF64::new(corner[1], ty)];
92
93 let sketch =
95 crate::std::sketch::inner_start_profile(sketch_surface, corner_t, None, exec_state, args.clone()).await?;
96 let sketch_id = sketch.id;
97 let deltas = [[width.n, 0.0], [0.0, height.n], [-width.n, 0.0], [0.0, -height.n]];
98 let ids = [
99 exec_state.next_uuid(),
100 exec_state.next_uuid(),
101 exec_state.next_uuid(),
102 exec_state.next_uuid(),
103 ];
104 for (id, delta) in ids.iter().copied().zip(deltas) {
105 exec_state
106 .batch_modeling_cmd(
107 ModelingCmdMeta::from_args_id(&args, id),
108 ModelingCmd::from(mcmd::ExtendPath {
109 path: sketch.id.into(),
110 segment: PathSegment::Line {
111 end: KPoint2d::from(untyped_point_to_mm(delta, units))
112 .with_z(0.0)
113 .map(LengthUnit),
114 relative: true,
115 },
116 }),
117 )
118 .await?;
119 }
120 exec_state
121 .batch_modeling_cmd(
122 ModelingCmdMeta::from_args_id(&args, sketch_id),
123 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
124 )
125 .await?;
126
127 let mut new_sketch = sketch.clone();
129 fn add(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
130 [a[0] + b[0], a[1] + b[1]]
131 }
132 let a = (corner, add(corner, deltas[0]));
133 let b = (a.1, add(a.1, deltas[1]));
134 let c = (b.1, add(b.1, deltas[2]));
135 let d = (c.1, add(c.1, deltas[3]));
136 for (id, (from, to)) in ids.into_iter().zip([a, b, c, d]) {
137 let current_path = Path::ToPoint {
138 base: BasePath {
139 from,
140 to,
141 tag: None,
142 units,
143 geo_meta: GeoMeta {
144 id,
145 metadata: args.source_range.into(),
146 },
147 },
148 };
149 new_sketch.paths.push(current_path);
150 }
151 Ok(new_sketch)
152}
153
154pub async fn circle(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
156 let sketch_or_surface =
157 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
158 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
159 let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
160 let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
161 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
162
163 let sketch = inner_circle(sketch_or_surface, center, radius, diameter, tag, exec_state, args).await?;
164 Ok(KclValue::Sketch {
165 value: Box::new(sketch),
166 })
167}
168
169async fn inner_circle(
170 sketch_or_surface: SketchOrSurface,
171 center: [TyF64; 2],
172 radius: Option<TyF64>,
173 diameter: Option<TyF64>,
174 tag: Option<TagNode>,
175 exec_state: &mut ExecState,
176 args: Args,
177) -> Result<Sketch, KclError> {
178 let sketch_surface = match sketch_or_surface {
179 SketchOrSurface::SketchSurface(surface) => surface,
180 SketchOrSurface::Sketch(s) => s.on,
181 };
182 let (center_u, ty) = untype_point(center.clone());
183 let units = ty.expect_length();
184
185 let radius = get_radius(radius, diameter, args.source_range)?;
186 let from = [center_u[0] + radius.to_length_units(units), center_u[1]];
187 let from_t = [TyF64::new(from[0], ty), TyF64::new(from[1], ty)];
188
189 let sketch =
190 crate::std::sketch::inner_start_profile(sketch_surface, from_t, None, exec_state, args.clone()).await?;
191
192 let angle_start = Angle::zero();
193 let angle_end = Angle::turn();
194
195 let id = exec_state.next_uuid();
196
197 exec_state
198 .batch_modeling_cmd(
199 ModelingCmdMeta::from_args_id(&args, id),
200 ModelingCmd::from(mcmd::ExtendPath {
201 path: sketch.id.into(),
202 segment: PathSegment::Arc {
203 start: angle_start,
204 end: angle_end,
205 center: KPoint2d::from(point_to_mm(center)).map(LengthUnit),
206 radius: LengthUnit(radius.to_mm()),
207 relative: false,
208 },
209 }),
210 )
211 .await?;
212
213 let current_path = Path::Circle {
214 base: BasePath {
215 from,
216 to: from,
217 tag: tag.clone(),
218 units,
219 geo_meta: GeoMeta {
220 id,
221 metadata: args.source_range.into(),
222 },
223 },
224 radius: radius.to_length_units(units),
225 center: center_u,
226 ccw: angle_start < angle_end,
227 };
228
229 let mut new_sketch = sketch.clone();
230 if let Some(tag) = &tag {
231 new_sketch.add_tag(tag, ¤t_path, exec_state);
232 }
233
234 new_sketch.paths.push(current_path);
235
236 exec_state
237 .batch_modeling_cmd(
238 ModelingCmdMeta::from_args_id(&args, id),
239 ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
240 )
241 .await?;
242
243 Ok(new_sketch)
244}
245
246pub async fn circle_three_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
248 let sketch_or_surface =
249 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
250 let p1 = args.get_kw_arg("p1", &RuntimeType::point2d(), exec_state)?;
251 let p2 = args.get_kw_arg("p2", &RuntimeType::point2d(), exec_state)?;
252 let p3 = args.get_kw_arg("p3", &RuntimeType::point2d(), exec_state)?;
253 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
254
255 let sketch = inner_circle_three_point(sketch_or_surface, p1, p2, p3, tag, exec_state, args).await?;
256 Ok(KclValue::Sketch {
257 value: Box::new(sketch),
258 })
259}
260
261async fn inner_circle_three_point(
264 sketch_surface_or_group: SketchOrSurface,
265 p1: [TyF64; 2],
266 p2: [TyF64; 2],
267 p3: [TyF64; 2],
268 tag: Option<TagNode>,
269 exec_state: &mut ExecState,
270 args: Args,
271) -> Result<Sketch, KclError> {
272 let ty = p1[0].ty;
273 let units = ty.expect_length();
274
275 let p1 = point_to_len_unit(p1, units);
276 let p2 = point_to_len_unit(p2, units);
277 let p3 = point_to_len_unit(p3, units);
278
279 let center = calculate_circle_center(p1, p2, p3);
280 let radius = distance(center, p2);
282
283 let sketch_surface = match sketch_surface_or_group {
284 SketchOrSurface::SketchSurface(surface) => surface,
285 SketchOrSurface::Sketch(group) => group.on,
286 };
287
288 let from = [TyF64::new(center[0] + radius, ty), TyF64::new(center[1], ty)];
289 let sketch =
290 crate::std::sketch::inner_start_profile(sketch_surface, from.clone(), None, exec_state, args.clone()).await?;
291
292 let angle_start = Angle::zero();
293 let angle_end = Angle::turn();
294
295 let id = exec_state.next_uuid();
296
297 exec_state
298 .batch_modeling_cmd(
299 ModelingCmdMeta::from_args_id(&args, id),
300 ModelingCmd::from(mcmd::ExtendPath {
301 path: sketch.id.into(),
302 segment: PathSegment::Arc {
303 start: angle_start,
304 end: angle_end,
305 center: KPoint2d::from(untyped_point_to_mm(center, units)).map(LengthUnit),
306 radius: units.adjust_to(radius, UnitLen::Mm).0.into(),
307 relative: false,
308 },
309 }),
310 )
311 .await?;
312
313 let current_path = Path::CircleThreePoint {
314 base: BasePath {
315 from: untype_point(from.clone()).0,
317 to: untype_point(from).0,
318 tag: tag.clone(),
319 units,
320 geo_meta: GeoMeta {
321 id,
322 metadata: args.source_range.into(),
323 },
324 },
325 p1,
326 p2,
327 p3,
328 };
329
330 let mut new_sketch = sketch.clone();
331 if let Some(tag) = &tag {
332 new_sketch.add_tag(tag, ¤t_path, exec_state);
333 }
334
335 new_sketch.paths.push(current_path);
336
337 exec_state
338 .batch_modeling_cmd(
339 ModelingCmdMeta::from_args_id(&args, id),
340 ModelingCmd::from(mcmd::ClosePath { path_id: new_sketch.id }),
341 )
342 .await?;
343
344 Ok(new_sketch)
345}
346
347#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, Default)]
349#[ts(export)]
350#[serde(rename_all = "lowercase")]
351pub enum PolygonType {
352 #[default]
353 Inscribed,
354 Circumscribed,
355}
356
357pub async fn polygon(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
359 let sketch_or_surface =
360 args.get_unlabeled_kw_arg("sketchOrSurface", &RuntimeType::sketch_or_surface(), exec_state)?;
361 let radius: TyF64 = args.get_kw_arg("radius", &RuntimeType::length(), exec_state)?;
362 let num_sides: TyF64 = args.get_kw_arg("numSides", &RuntimeType::count(), exec_state)?;
363 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
364 let inscribed = args.get_kw_arg_opt("inscribed", &RuntimeType::bool(), exec_state)?;
365
366 let sketch = inner_polygon(
367 sketch_or_surface,
368 radius,
369 num_sides.n as u64,
370 center,
371 inscribed,
372 exec_state,
373 args,
374 )
375 .await?;
376 Ok(KclValue::Sketch {
377 value: Box::new(sketch),
378 })
379}
380
381#[allow(clippy::too_many_arguments)]
382async fn inner_polygon(
383 sketch_surface_or_group: SketchOrSurface,
384 radius: TyF64,
385 num_sides: u64,
386 center: [TyF64; 2],
387 inscribed: Option<bool>,
388 exec_state: &mut ExecState,
389 args: Args,
390) -> Result<Sketch, KclError> {
391 if num_sides < 3 {
392 return Err(KclError::new_type(KclErrorDetails::new(
393 "Polygon must have at least 3 sides".to_string(),
394 vec![args.source_range],
395 )));
396 }
397
398 if radius.n <= 0.0 {
399 return Err(KclError::new_type(KclErrorDetails::new(
400 "Radius must be greater than 0".to_string(),
401 vec![args.source_range],
402 )));
403 }
404
405 let (sketch_surface, units) = match sketch_surface_or_group {
406 SketchOrSurface::SketchSurface(surface) => (surface, radius.ty.expect_length()),
407 SketchOrSurface::Sketch(group) => (group.on, group.units),
408 };
409
410 let half_angle = std::f64::consts::PI / num_sides as f64;
411
412 let radius_to_vertices = if inscribed.unwrap_or(true) {
413 radius.n
415 } else {
416 radius.n / libm::cos(half_angle)
418 };
419
420 let angle_step = std::f64::consts::TAU / num_sides as f64;
421
422 let center_u = point_to_len_unit(center, units);
423
424 let vertices: Vec<[f64; 2]> = (0..num_sides)
425 .map(|i| {
426 let angle = angle_step * i as f64;
427 [
428 center_u[0] + radius_to_vertices * libm::cos(angle),
429 center_u[1] + radius_to_vertices * libm::sin(angle),
430 ]
431 })
432 .collect();
433
434 let mut sketch = crate::std::sketch::inner_start_profile(
435 sketch_surface,
436 point_to_typed(vertices[0], units),
437 None,
438 exec_state,
439 args.clone(),
440 )
441 .await?;
442
443 for vertex in vertices.iter().skip(1) {
445 let from = sketch.current_pen_position()?;
446 let id = exec_state.next_uuid();
447
448 exec_state
449 .batch_modeling_cmd(
450 ModelingCmdMeta::from_args_id(&args, id),
451 ModelingCmd::from(mcmd::ExtendPath {
452 path: sketch.id.into(),
453 segment: PathSegment::Line {
454 end: KPoint2d::from(untyped_point_to_mm(*vertex, units))
455 .with_z(0.0)
456 .map(LengthUnit),
457 relative: false,
458 },
459 }),
460 )
461 .await?;
462
463 let current_path = Path::ToPoint {
464 base: BasePath {
465 from: from.ignore_units(),
466 to: *vertex,
467 tag: None,
468 units: sketch.units,
469 geo_meta: GeoMeta {
470 id,
471 metadata: args.source_range.into(),
472 },
473 },
474 };
475
476 sketch.paths.push(current_path);
477 }
478
479 let from = sketch.current_pen_position()?;
481 let close_id = exec_state.next_uuid();
482
483 exec_state
484 .batch_modeling_cmd(
485 ModelingCmdMeta::from_args_id(&args, close_id),
486 ModelingCmd::from(mcmd::ExtendPath {
487 path: sketch.id.into(),
488 segment: PathSegment::Line {
489 end: KPoint2d::from(untyped_point_to_mm(vertices[0], units))
490 .with_z(0.0)
491 .map(LengthUnit),
492 relative: false,
493 },
494 }),
495 )
496 .await?;
497
498 let current_path = Path::ToPoint {
499 base: BasePath {
500 from: from.ignore_units(),
501 to: vertices[0],
502 tag: None,
503 units: sketch.units,
504 geo_meta: GeoMeta {
505 id: close_id,
506 metadata: args.source_range.into(),
507 },
508 },
509 };
510
511 sketch.paths.push(current_path);
512
513 exec_state
514 .batch_modeling_cmd(
515 (&args).into(),
516 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
517 )
518 .await?;
519
520 Ok(sketch)
521}
522
523pub(crate) fn get_radius(
524 radius: Option<TyF64>,
525 diameter: Option<TyF64>,
526 source_range: SourceRange,
527) -> Result<TyF64, KclError> {
528 get_radius_labelled(radius, diameter, source_range, "radius", "diameter")
529}
530
531pub(crate) fn get_radius_labelled(
532 radius: Option<TyF64>,
533 diameter: Option<TyF64>,
534 source_range: SourceRange,
535 label_radius: &'static str,
536 label_diameter: &'static str,
537) -> Result<TyF64, KclError> {
538 match (radius, diameter) {
539 (Some(radius), None) => Ok(radius),
540 (None, Some(diameter)) => Ok(TyF64::new(diameter.n / 2.0, diameter.ty)),
541 (None, None) => Err(KclError::new_type(KclErrorDetails::new(
542 format!("This function needs either `{label_diameter}` or `{label_radius}`"),
543 vec![source_range],
544 ))),
545 (Some(_), Some(_)) => Err(KclError::new_type(KclErrorDetails::new(
546 format!("You cannot specify both `{label_diameter}` and `{label_radius}`, please remove one"),
547 vec![source_range],
548 ))),
549 }
550}