1use std::f64;
4
5use anyhow::Result;
6use indexmap::IndexMap;
7use kcmc::shared::Point2d as KPoint2d; use kcmc::shared::Point3d as KPoint3d; use kcmc::{ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, websocket::ModelingCmdReq};
10use kittycad_modeling_cmds as kcmc;
11use kittycad_modeling_cmds::{shared::PathSegment, units::UnitLength};
12use parse_display::{Display, FromStr};
13use serde::{Deserialize, Serialize};
14
15use super::{
16 shapes::{get_radius, get_radius_labelled},
17 utils::untype_array,
18};
19#[cfg(feature = "artifact-graph")]
20use crate::execution::{Artifact, ArtifactId, CodeRef, StartSketchOnFace, StartSketchOnPlane};
21use crate::{
22 errors::{KclError, KclErrorDetails},
23 exec::PlaneKind,
24 execution::{
25 BasePath, ExecState, GeoMeta, KclValue, ModelingCmdMeta, Path, Plane, PlaneInfo, Point2d, Point3d,
26 ProfileClosed, Sketch, SketchSurface, Solid, TagEngineInfo, TagIdentifier, annotations,
27 types::{ArrayLen, NumericType, PrimitiveType, RuntimeType},
28 },
29 parsing::ast::types::TagNode,
30 std::{
31 EQUAL_POINTS_DIST_EPSILON,
32 args::{Args, TyF64},
33 axis_or_reference::Axis2dOrEdgeReference,
34 faces::make_face,
35 planes::inner_plane_of,
36 utils::{
37 TangentialArcInfoInput, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
38 intersection_with_parallel_line, point_to_len_unit, point_to_mm, untyped_point_to_mm,
39 },
40 },
41};
42
43#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
45#[ts(export)]
46#[serde(rename_all = "snake_case", untagged)]
47pub enum FaceTag {
48 StartOrEnd(StartOrEnd),
49 Tag(Box<TagIdentifier>),
51}
52
53impl std::fmt::Display for FaceTag {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 FaceTag::Tag(t) => write!(f, "{t}"),
57 FaceTag::StartOrEnd(StartOrEnd::Start) => write!(f, "start"),
58 FaceTag::StartOrEnd(StartOrEnd::End) => write!(f, "end"),
59 }
60 }
61}
62
63impl FaceTag {
64 pub async fn get_face_id(
66 &self,
67 solid: &Solid,
68 exec_state: &mut ExecState,
69 args: &Args,
70 must_be_planar: bool,
71 ) -> Result<uuid::Uuid, KclError> {
72 match self {
73 FaceTag::Tag(t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
74 FaceTag::StartOrEnd(StartOrEnd::Start) => solid.start_cap_id.ok_or_else(|| {
75 KclError::new_type(KclErrorDetails::new(
76 "Expected a start face".to_string(),
77 vec![args.source_range],
78 ))
79 }),
80 FaceTag::StartOrEnd(StartOrEnd::End) => solid.end_cap_id.ok_or_else(|| {
81 KclError::new_type(KclErrorDetails::new(
82 "Expected an end face".to_string(),
83 vec![args.source_range],
84 ))
85 }),
86 }
87 }
88
89 pub async fn get_face_id_from_tag(
90 &self,
91 exec_state: &mut ExecState,
92 args: &Args,
93 must_be_planar: bool,
94 ) -> Result<uuid::Uuid, KclError> {
95 match self {
96 FaceTag::Tag(t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
97 _ => Err(KclError::new_type(KclErrorDetails::new(
98 "Could not find the face corresponding to this tag".to_string(),
99 vec![args.source_range],
100 ))),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, FromStr, Display)]
106#[ts(export)]
107#[serde(rename_all = "snake_case")]
108#[display(style = "snake_case")]
109pub enum StartOrEnd {
110 #[serde(rename = "start", alias = "START")]
114 Start,
115 #[serde(rename = "end", alias = "END")]
119 End,
120}
121
122pub const NEW_TAG_KW: &str = "tag";
123
124pub async fn involute_circular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
125 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
126
127 let start_radius: Option<TyF64> = args.get_kw_arg_opt("startRadius", &RuntimeType::length(), exec_state)?;
128 let end_radius: Option<TyF64> = args.get_kw_arg_opt("endRadius", &RuntimeType::length(), exec_state)?;
129 let start_diameter: Option<TyF64> = args.get_kw_arg_opt("startDiameter", &RuntimeType::length(), exec_state)?;
130 let end_diameter: Option<TyF64> = args.get_kw_arg_opt("endDiameter", &RuntimeType::length(), exec_state)?;
131 let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::angle(), exec_state)?;
132 let reverse = args.get_kw_arg_opt("reverse", &RuntimeType::bool(), exec_state)?;
133 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
134 let new_sketch = inner_involute_circular(
135 sketch,
136 start_radius,
137 end_radius,
138 start_diameter,
139 end_diameter,
140 angle,
141 reverse,
142 tag,
143 exec_state,
144 args,
145 )
146 .await?;
147 Ok(KclValue::Sketch {
148 value: Box::new(new_sketch),
149 })
150}
151
152fn involute_curve(radius: f64, angle: f64) -> (f64, f64) {
153 (
154 radius * (libm::cos(angle) + angle * libm::sin(angle)),
155 radius * (libm::sin(angle) - angle * libm::cos(angle)),
156 )
157}
158
159#[allow(clippy::too_many_arguments)]
160async fn inner_involute_circular(
161 sketch: Sketch,
162 start_radius: Option<TyF64>,
163 end_radius: Option<TyF64>,
164 start_diameter: Option<TyF64>,
165 end_diameter: Option<TyF64>,
166 angle: TyF64,
167 reverse: Option<bool>,
168 tag: Option<TagNode>,
169 exec_state: &mut ExecState,
170 args: Args,
171) -> Result<Sketch, KclError> {
172 let id = exec_state.next_uuid();
173 let angle_deg = angle.to_degrees(exec_state, args.source_range);
174 let angle_rad = angle.to_radians(exec_state, args.source_range);
175
176 let longer_args_dot_source_range = args.source_range;
177 let start_radius = get_radius_labelled(
178 start_radius,
179 start_diameter,
180 args.source_range,
181 "startRadius",
182 "startDiameter",
183 )?;
184 let end_radius = get_radius_labelled(
185 end_radius,
186 end_diameter,
187 longer_args_dot_source_range,
188 "endRadius",
189 "endDiameter",
190 )?;
191
192 exec_state
193 .batch_modeling_cmd(
194 ModelingCmdMeta::from_args_id(exec_state, &args, id),
195 ModelingCmd::from(
196 mcmd::ExtendPath::builder()
197 .path(sketch.id.into())
198 .segment(PathSegment::CircularInvolute {
199 start_radius: LengthUnit(start_radius.to_mm()),
200 end_radius: LengthUnit(end_radius.to_mm()),
201 angle: Angle::from_degrees(angle_deg),
202 reverse: reverse.unwrap_or_default(),
203 })
204 .build(),
205 ),
206 )
207 .await?;
208
209 let from = sketch.current_pen_position()?;
210
211 let start_radius = start_radius.to_length_units(from.units);
212 let end_radius = end_radius.to_length_units(from.units);
213
214 let mut end: KPoint3d<f64> = Default::default(); let theta = f64::sqrt(end_radius * end_radius - start_radius * start_radius) / start_radius;
216 let (x, y) = involute_curve(start_radius, theta);
217
218 end.x = x * libm::cos(angle_rad) - y * libm::sin(angle_rad);
219 end.y = x * libm::sin(angle_rad) + y * libm::cos(angle_rad);
220
221 end.x -= start_radius * libm::cos(angle_rad);
222 end.y -= start_radius * libm::sin(angle_rad);
223
224 if reverse.unwrap_or_default() {
225 end.x = -end.x;
226 }
227
228 end.x += from.x;
229 end.y += from.y;
230
231 let current_path = Path::ToPoint {
232 base: BasePath {
233 from: from.ignore_units(),
234 to: [end.x, end.y],
235 tag: tag.clone(),
236 units: sketch.units,
237 geo_meta: GeoMeta {
238 id,
239 metadata: args.source_range.into(),
240 },
241 },
242 };
243
244 let mut new_sketch = sketch;
245 if let Some(tag) = &tag {
246 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
247 }
248 new_sketch.paths.push(current_path);
249 Ok(new_sketch)
250}
251
252pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
254 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
255 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
256 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
257 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
258
259 let new_sketch = inner_line(sketch, end_absolute, end, tag, exec_state, args).await?;
260 Ok(KclValue::Sketch {
261 value: Box::new(new_sketch),
262 })
263}
264
265async fn inner_line(
266 sketch: Sketch,
267 end_absolute: Option<[TyF64; 2]>,
268 end: Option<[TyF64; 2]>,
269 tag: Option<TagNode>,
270 exec_state: &mut ExecState,
271 args: Args,
272) -> Result<Sketch, KclError> {
273 straight_line(
274 StraightLineParams {
275 sketch,
276 end_absolute,
277 end,
278 tag,
279 relative_name: "end",
280 },
281 exec_state,
282 args,
283 )
284 .await
285}
286
287struct StraightLineParams {
288 sketch: Sketch,
289 end_absolute: Option<[TyF64; 2]>,
290 end: Option<[TyF64; 2]>,
291 tag: Option<TagNode>,
292 relative_name: &'static str,
293}
294
295impl StraightLineParams {
296 fn relative(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
297 Self {
298 sketch,
299 tag,
300 end: Some(p),
301 end_absolute: None,
302 relative_name: "end",
303 }
304 }
305 fn absolute(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
306 Self {
307 sketch,
308 tag,
309 end: None,
310 end_absolute: Some(p),
311 relative_name: "end",
312 }
313 }
314}
315
316async fn straight_line(
317 StraightLineParams {
318 sketch,
319 end,
320 end_absolute,
321 tag,
322 relative_name,
323 }: StraightLineParams,
324 exec_state: &mut ExecState,
325 args: Args,
326) -> Result<Sketch, KclError> {
327 let from = sketch.current_pen_position()?;
328 let (point, is_absolute) = match (end_absolute, end) {
329 (Some(_), Some(_)) => {
330 return Err(KclError::new_semantic(KclErrorDetails::new(
331 "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
332 vec![args.source_range],
333 )));
334 }
335 (Some(end_absolute), None) => (end_absolute, true),
336 (None, Some(end)) => (end, false),
337 (None, None) => {
338 return Err(KclError::new_semantic(KclErrorDetails::new(
339 format!("You must supply either `{relative_name}` or `endAbsolute` arguments"),
340 vec![args.source_range],
341 )));
342 }
343 };
344
345 let id = exec_state.next_uuid();
346 exec_state
347 .batch_modeling_cmd(
348 ModelingCmdMeta::from_args_id(exec_state, &args, id),
349 ModelingCmd::from(
350 mcmd::ExtendPath::builder()
351 .path(sketch.id.into())
352 .segment(PathSegment::Line {
353 end: KPoint2d::from(point_to_mm(point.clone())).with_z(0.0).map(LengthUnit),
354 relative: !is_absolute,
355 })
356 .build(),
357 ),
358 )
359 .await?;
360
361 let end = if is_absolute {
362 point_to_len_unit(point, from.units)
363 } else {
364 let from = sketch.current_pen_position()?;
365 let point = point_to_len_unit(point, from.units);
366 [from.x + point[0], from.y + point[1]]
367 };
368
369 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
371
372 let current_path = Path::ToPoint {
373 base: BasePath {
374 from: from.ignore_units(),
375 to: end,
376 tag: tag.clone(),
377 units: sketch.units,
378 geo_meta: GeoMeta {
379 id,
380 metadata: args.source_range.into(),
381 },
382 },
383 };
384
385 let mut new_sketch = sketch;
386 if let Some(tag) = &tag {
387 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
388 }
389 if loops_back_to_start {
390 new_sketch.is_closed = ProfileClosed::Implicitly;
391 }
392
393 new_sketch.paths.push(current_path);
394
395 Ok(new_sketch)
396}
397
398fn does_segment_close_sketch(end: [f64; 2], from: [f64; 2]) -> bool {
399 let same_x = (end[0] - from[0]).abs() < EQUAL_POINTS_DIST_EPSILON;
400 let same_y = (end[1] - from[1]).abs() < EQUAL_POINTS_DIST_EPSILON;
401 same_x && same_y
402}
403
404pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
406 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
407 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
408 let end_absolute: Option<TyF64> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::length(), exec_state)?;
409 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
410
411 let new_sketch = inner_x_line(sketch, length, end_absolute, tag, exec_state, args).await?;
412 Ok(KclValue::Sketch {
413 value: Box::new(new_sketch),
414 })
415}
416
417async fn inner_x_line(
418 sketch: Sketch,
419 length: Option<TyF64>,
420 end_absolute: Option<TyF64>,
421 tag: Option<TagNode>,
422 exec_state: &mut ExecState,
423 args: Args,
424) -> Result<Sketch, KclError> {
425 let from = sketch.current_pen_position()?;
426 straight_line(
427 StraightLineParams {
428 sketch,
429 end_absolute: end_absolute.map(|x| [x, from.into_y()]),
430 end: length.map(|x| [x, TyF64::new(0.0, NumericType::mm())]),
431 tag,
432 relative_name: "length",
433 },
434 exec_state,
435 args,
436 )
437 .await
438}
439
440pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
442 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
443 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
444 let end_absolute: Option<TyF64> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::length(), exec_state)?;
445 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
446
447 let new_sketch = inner_y_line(sketch, length, end_absolute, tag, exec_state, args).await?;
448 Ok(KclValue::Sketch {
449 value: Box::new(new_sketch),
450 })
451}
452
453async fn inner_y_line(
454 sketch: Sketch,
455 length: Option<TyF64>,
456 end_absolute: Option<TyF64>,
457 tag: Option<TagNode>,
458 exec_state: &mut ExecState,
459 args: Args,
460) -> Result<Sketch, KclError> {
461 let from = sketch.current_pen_position()?;
462 straight_line(
463 StraightLineParams {
464 sketch,
465 end_absolute: end_absolute.map(|y| [from.into_x(), y]),
466 end: length.map(|y| [TyF64::new(0.0, NumericType::mm()), y]),
467 tag,
468 relative_name: "length",
469 },
470 exec_state,
471 args,
472 )
473 .await
474}
475
476pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
478 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
479 let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::degrees(), exec_state)?;
480 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
481 let length_x: Option<TyF64> = args.get_kw_arg_opt("lengthX", &RuntimeType::length(), exec_state)?;
482 let length_y: Option<TyF64> = args.get_kw_arg_opt("lengthY", &RuntimeType::length(), exec_state)?;
483 let end_absolute_x: Option<TyF64> = args.get_kw_arg_opt("endAbsoluteX", &RuntimeType::length(), exec_state)?;
484 let end_absolute_y: Option<TyF64> = args.get_kw_arg_opt("endAbsoluteY", &RuntimeType::length(), exec_state)?;
485 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
486
487 let new_sketch = inner_angled_line(
488 sketch,
489 angle.n,
490 length,
491 length_x,
492 length_y,
493 end_absolute_x,
494 end_absolute_y,
495 tag,
496 exec_state,
497 args,
498 )
499 .await?;
500 Ok(KclValue::Sketch {
501 value: Box::new(new_sketch),
502 })
503}
504
505#[allow(clippy::too_many_arguments)]
506async fn inner_angled_line(
507 sketch: Sketch,
508 angle: f64,
509 length: Option<TyF64>,
510 length_x: Option<TyF64>,
511 length_y: Option<TyF64>,
512 end_absolute_x: Option<TyF64>,
513 end_absolute_y: Option<TyF64>,
514 tag: Option<TagNode>,
515 exec_state: &mut ExecState,
516 args: Args,
517) -> Result<Sketch, KclError> {
518 let options_given = [&length, &length_x, &length_y, &end_absolute_x, &end_absolute_y]
519 .iter()
520 .filter(|x| x.is_some())
521 .count();
522 if options_given > 1 {
523 return Err(KclError::new_type(KclErrorDetails::new(
524 " one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_string(),
525 vec![args.source_range],
526 )));
527 }
528 if let Some(length_x) = length_x {
529 return inner_angled_line_of_x_length(angle, length_x, sketch, tag, exec_state, args).await;
530 }
531 if let Some(length_y) = length_y {
532 return inner_angled_line_of_y_length(angle, length_y, sketch, tag, exec_state, args).await;
533 }
534 let angle_degrees = angle;
535 match (length, length_x, length_y, end_absolute_x, end_absolute_y) {
536 (Some(length), None, None, None, None) => {
537 inner_angled_line_length(sketch, angle_degrees, length, tag, exec_state, args).await
538 }
539 (None, Some(length_x), None, None, None) => {
540 inner_angled_line_of_x_length(angle_degrees, length_x, sketch, tag, exec_state, args).await
541 }
542 (None, None, Some(length_y), None, None) => {
543 inner_angled_line_of_y_length(angle_degrees, length_y, sketch, tag, exec_state, args).await
544 }
545 (None, None, None, Some(end_absolute_x), None) => {
546 inner_angled_line_to_x(angle_degrees, end_absolute_x, sketch, tag, exec_state, args).await
547 }
548 (None, None, None, None, Some(end_absolute_y)) => {
549 inner_angled_line_to_y(angle_degrees, end_absolute_y, sketch, tag, exec_state, args).await
550 }
551 (None, None, None, None, None) => Err(KclError::new_type(KclErrorDetails::new(
552 "One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` must be given".to_string(),
553 vec![args.source_range],
554 ))),
555 _ => Err(KclError::new_type(KclErrorDetails::new(
556 "Only One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_owned(),
557 vec![args.source_range],
558 ))),
559 }
560}
561
562async fn inner_angled_line_length(
563 sketch: Sketch,
564 angle_degrees: f64,
565 length: TyF64,
566 tag: Option<TagNode>,
567 exec_state: &mut ExecState,
568 args: Args,
569) -> Result<Sketch, KclError> {
570 let from = sketch.current_pen_position()?;
571 let length = length.to_length_units(from.units);
572
573 let delta: [f64; 2] = [
575 length * libm::cos(angle_degrees.to_radians()),
576 length * libm::sin(angle_degrees.to_radians()),
577 ];
578 let relative = true;
579
580 let to: [f64; 2] = [from.x + delta[0], from.y + delta[1]];
581 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
582
583 let id = exec_state.next_uuid();
584
585 exec_state
586 .batch_modeling_cmd(
587 ModelingCmdMeta::from_args_id(exec_state, &args, id),
588 ModelingCmd::from(
589 mcmd::ExtendPath::builder()
590 .path(sketch.id.into())
591 .segment(PathSegment::Line {
592 end: KPoint2d::from(untyped_point_to_mm(delta, from.units))
593 .with_z(0.0)
594 .map(LengthUnit),
595 relative,
596 })
597 .build(),
598 ),
599 )
600 .await?;
601
602 let current_path = Path::ToPoint {
603 base: BasePath {
604 from: from.ignore_units(),
605 to,
606 tag: tag.clone(),
607 units: sketch.units,
608 geo_meta: GeoMeta {
609 id,
610 metadata: args.source_range.into(),
611 },
612 },
613 };
614
615 let mut new_sketch = sketch;
616 if let Some(tag) = &tag {
617 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
618 }
619 if loops_back_to_start {
620 new_sketch.is_closed = ProfileClosed::Implicitly;
621 }
622
623 new_sketch.paths.push(current_path);
624 Ok(new_sketch)
625}
626
627async fn inner_angled_line_of_x_length(
628 angle_degrees: f64,
629 length: TyF64,
630 sketch: Sketch,
631 tag: Option<TagNode>,
632 exec_state: &mut ExecState,
633 args: Args,
634) -> Result<Sketch, KclError> {
635 if angle_degrees.abs() == 270.0 {
636 return Err(KclError::new_type(KclErrorDetails::new(
637 "Cannot have an x constrained angle of 270 degrees".to_string(),
638 vec![args.source_range],
639 )));
640 }
641
642 if angle_degrees.abs() == 90.0 {
643 return Err(KclError::new_type(KclErrorDetails::new(
644 "Cannot have an x constrained angle of 90 degrees".to_string(),
645 vec![args.source_range],
646 )));
647 }
648
649 let to = get_y_component(Angle::from_degrees(angle_degrees), length.n);
650 let to = [TyF64::new(to[0], length.ty), TyF64::new(to[1], length.ty)];
651
652 let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
653
654 Ok(new_sketch)
655}
656
657async fn inner_angled_line_to_x(
658 angle_degrees: f64,
659 x_to: TyF64,
660 sketch: Sketch,
661 tag: Option<TagNode>,
662 exec_state: &mut ExecState,
663 args: Args,
664) -> Result<Sketch, KclError> {
665 let from = sketch.current_pen_position()?;
666
667 if angle_degrees.abs() == 270.0 {
668 return Err(KclError::new_type(KclErrorDetails::new(
669 "Cannot have an x constrained angle of 270 degrees".to_string(),
670 vec![args.source_range],
671 )));
672 }
673
674 if angle_degrees.abs() == 90.0 {
675 return Err(KclError::new_type(KclErrorDetails::new(
676 "Cannot have an x constrained angle of 90 degrees".to_string(),
677 vec![args.source_range],
678 )));
679 }
680
681 let x_component = x_to.to_length_units(from.units) - from.x;
682 let y_component = x_component * libm::tan(angle_degrees.to_radians());
683 let y_to = from.y + y_component;
684
685 let new_sketch = straight_line(
686 StraightLineParams::absolute([x_to, TyF64::new(y_to, from.units.into())], sketch, tag),
687 exec_state,
688 args,
689 )
690 .await?;
691 Ok(new_sketch)
692}
693
694async fn inner_angled_line_of_y_length(
695 angle_degrees: f64,
696 length: TyF64,
697 sketch: Sketch,
698 tag: Option<TagNode>,
699 exec_state: &mut ExecState,
700 args: Args,
701) -> Result<Sketch, KclError> {
702 if angle_degrees.abs() == 0.0 {
703 return Err(KclError::new_type(KclErrorDetails::new(
704 "Cannot have a y constrained angle of 0 degrees".to_string(),
705 vec![args.source_range],
706 )));
707 }
708
709 if angle_degrees.abs() == 180.0 {
710 return Err(KclError::new_type(KclErrorDetails::new(
711 "Cannot have a y constrained angle of 180 degrees".to_string(),
712 vec![args.source_range],
713 )));
714 }
715
716 let to = get_x_component(Angle::from_degrees(angle_degrees), length.n);
717 let to = [TyF64::new(to[0], length.ty), TyF64::new(to[1], length.ty)];
718
719 let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
720
721 Ok(new_sketch)
722}
723
724async fn inner_angled_line_to_y(
725 angle_degrees: f64,
726 y_to: TyF64,
727 sketch: Sketch,
728 tag: Option<TagNode>,
729 exec_state: &mut ExecState,
730 args: Args,
731) -> Result<Sketch, KclError> {
732 let from = sketch.current_pen_position()?;
733
734 if angle_degrees.abs() == 0.0 {
735 return Err(KclError::new_type(KclErrorDetails::new(
736 "Cannot have a y constrained angle of 0 degrees".to_string(),
737 vec![args.source_range],
738 )));
739 }
740
741 if angle_degrees.abs() == 180.0 {
742 return Err(KclError::new_type(KclErrorDetails::new(
743 "Cannot have a y constrained angle of 180 degrees".to_string(),
744 vec![args.source_range],
745 )));
746 }
747
748 let y_component = y_to.to_length_units(from.units) - from.y;
749 let x_component = y_component / libm::tan(angle_degrees.to_radians());
750 let x_to = from.x + x_component;
751
752 let new_sketch = straight_line(
753 StraightLineParams::absolute([TyF64::new(x_to, from.units.into()), y_to], sketch, tag),
754 exec_state,
755 args,
756 )
757 .await?;
758 Ok(new_sketch)
759}
760
761pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
763 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
764 let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::angle(), exec_state)?;
765 let intersect_tag: TagIdentifier = args.get_kw_arg("intersectTag", &RuntimeType::tagged_edge(), exec_state)?;
766 let offset = args.get_kw_arg_opt("offset", &RuntimeType::length(), exec_state)?;
767 let tag: Option<TagNode> = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
768 let new_sketch =
769 inner_angled_line_that_intersects(sketch, angle, intersect_tag, offset, tag, exec_state, args).await?;
770 Ok(KclValue::Sketch {
771 value: Box::new(new_sketch),
772 })
773}
774
775pub async fn inner_angled_line_that_intersects(
776 sketch: Sketch,
777 angle: TyF64,
778 intersect_tag: TagIdentifier,
779 offset: Option<TyF64>,
780 tag: Option<TagNode>,
781 exec_state: &mut ExecState,
782 args: Args,
783) -> Result<Sketch, KclError> {
784 let intersect_path = args.get_tag_engine_info(exec_state, &intersect_tag)?;
785 let path = intersect_path.path.clone().ok_or_else(|| {
786 KclError::new_type(KclErrorDetails::new(
787 format!("Expected an intersect path with a path, found `{intersect_path:?}`"),
788 vec![args.source_range],
789 ))
790 })?;
791
792 let from = sketch.current_pen_position()?;
793 let to = intersection_with_parallel_line(
794 &[
795 point_to_len_unit(path.get_from(), from.units),
796 point_to_len_unit(path.get_to(), from.units),
797 ],
798 offset.map(|t| t.to_length_units(from.units)).unwrap_or_default(),
799 angle.to_degrees(exec_state, args.source_range),
800 from.ignore_units(),
801 );
802 let to = [
803 TyF64::new(to[0], from.units.into()),
804 TyF64::new(to[1], from.units.into()),
805 ];
806
807 straight_line(StraightLineParams::absolute(to, sketch, tag), exec_state, args).await
808}
809
810#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
813#[ts(export)]
814#[serde(rename_all = "camelCase", untagged)]
815#[allow(clippy::large_enum_variant)]
816pub enum SketchData {
817 PlaneOrientation(PlaneData),
818 Plane(Box<Plane>),
819 Solid(Box<Solid>),
820}
821
822#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
824#[ts(export)]
825#[serde(rename_all = "camelCase")]
826#[allow(clippy::large_enum_variant)]
827pub enum PlaneData {
828 #[serde(rename = "XY", alias = "xy")]
830 XY,
831 #[serde(rename = "-XY", alias = "-xy")]
833 NegXY,
834 #[serde(rename = "XZ", alias = "xz")]
836 XZ,
837 #[serde(rename = "-XZ", alias = "-xz")]
839 NegXZ,
840 #[serde(rename = "YZ", alias = "yz")]
842 YZ,
843 #[serde(rename = "-YZ", alias = "-yz")]
845 NegYZ,
846 Plane(PlaneInfo),
848}
849
850pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
852 let data = args.get_unlabeled_kw_arg(
853 "planeOrSolid",
854 &RuntimeType::Union(vec![RuntimeType::solid(), RuntimeType::plane()]),
855 exec_state,
856 )?;
857 let face = args.get_kw_arg_opt("face", &RuntimeType::tagged_face(), exec_state)?;
858 let normal_to_face = args.get_kw_arg_opt("normalToFace", &RuntimeType::tagged_face(), exec_state)?;
859 let align_axis = args.get_kw_arg_opt("alignAxis", &RuntimeType::Primitive(PrimitiveType::Axis2d), exec_state)?;
860 let normal_offset = args.get_kw_arg_opt("normalOffset", &RuntimeType::length(), exec_state)?;
861
862 match inner_start_sketch_on(data, face, normal_to_face, align_axis, normal_offset, exec_state, &args).await? {
863 SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
864 SketchSurface::Face(value) => Ok(KclValue::Face { value }),
865 }
866}
867
868async fn inner_start_sketch_on(
869 plane_or_solid: SketchData,
870 face: Option<FaceTag>,
871 normal_to_face: Option<FaceTag>,
872 align_axis: Option<Axis2dOrEdgeReference>,
873 normal_offset: Option<TyF64>,
874 exec_state: &mut ExecState,
875 args: &Args,
876) -> Result<SketchSurface, KclError> {
877 let face = match (face, normal_to_face, &align_axis, &normal_offset) {
878 (Some(_), Some(_), _, _) => {
879 return Err(KclError::new_semantic(KclErrorDetails::new(
880 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other."
881 .to_owned(),
882 vec![args.source_range],
883 )));
884 }
885 (Some(face), None, None, None) => Some(face),
886 (_, Some(_), None, _) => {
887 return Err(KclError::new_semantic(KclErrorDetails::new(
888 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
889 vec![args.source_range],
890 )));
891 }
892 (_, None, Some(_), _) => {
893 return Err(KclError::new_semantic(KclErrorDetails::new(
894 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
895 vec![args.source_range],
896 )));
897 }
898 (_, None, _, Some(_)) => {
899 return Err(KclError::new_semantic(KclErrorDetails::new(
900 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
901 vec![args.source_range],
902 )));
903 }
904 (_, Some(face), Some(_), _) => Some(face),
905 (None, None, None, None) => None,
906 };
907
908 match plane_or_solid {
909 SketchData::PlaneOrientation(plane_data) => {
910 let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
911 Ok(SketchSurface::Plane(plane))
912 }
913 SketchData::Plane(plane) => {
914 if plane.is_uninitialized() {
915 let plane = make_sketch_plane_from_orientation(plane.info.into_plane_data(), exec_state, args).await?;
916 Ok(SketchSurface::Plane(plane))
917 } else {
918 #[cfg(feature = "artifact-graph")]
920 {
921 let id = exec_state.next_uuid();
922 exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
923 id: ArtifactId::from(id),
924 plane_id: plane.artifact_id,
925 code_ref: CodeRef::placeholder(args.source_range),
926 }));
927 }
928
929 Ok(SketchSurface::Plane(plane))
930 }
931 }
932 SketchData::Solid(solid) => {
933 let Some(tag) = face else {
934 return Err(KclError::new_type(KclErrorDetails::new(
935 "Expected a tag for the face to sketch on".to_string(),
936 vec![args.source_range],
937 )));
938 };
939 if let Some(align_axis) = align_axis {
940 let plane_of = inner_plane_of(*solid, tag, exec_state, args).await?;
941
942 let offset = normal_offset.map_or(0.0, |x| x.to_mm());
944 let (x_axis, y_axis, normal_offset) = match align_axis {
945 Axis2dOrEdgeReference::Axis { direction, origin: _ } => {
946 if (direction[0].n - 1.0).abs() < f64::EPSILON {
947 (
949 plane_of.info.x_axis,
950 plane_of.info.z_axis,
951 plane_of.info.y_axis * offset,
952 )
953 } else if (direction[0].n + 1.0).abs() < f64::EPSILON {
954 (
956 plane_of.info.x_axis.negated(),
957 plane_of.info.z_axis,
958 plane_of.info.y_axis * offset,
959 )
960 } else if (direction[1].n - 1.0).abs() < f64::EPSILON {
961 (
963 plane_of.info.y_axis,
964 plane_of.info.z_axis,
965 plane_of.info.x_axis * offset,
966 )
967 } else if (direction[1].n + 1.0).abs() < f64::EPSILON {
968 (
970 plane_of.info.y_axis.negated(),
971 plane_of.info.z_axis,
972 plane_of.info.x_axis * offset,
973 )
974 } else {
975 return Err(KclError::new_semantic(KclErrorDetails::new(
976 "Unsupported axis detected. This function only supports using X, -X, Y and -Y."
977 .to_owned(),
978 vec![args.source_range],
979 )));
980 }
981 }
982 Axis2dOrEdgeReference::Edge(_) => {
983 return Err(KclError::new_semantic(KclErrorDetails::new(
984 "Use of an edge here is unsupported, please specify an `Axis2d` (e.g. `X`) instead."
985 .to_owned(),
986 vec![args.source_range],
987 )));
988 }
989 };
990 let origin = Point3d::new(0.0, 0.0, 0.0, plane_of.info.origin.units);
991 let plane_data = PlaneData::Plane(PlaneInfo {
992 origin: plane_of.project(origin) + normal_offset,
993 x_axis,
994 y_axis,
995 z_axis: x_axis.axes_cross_product(&y_axis),
996 });
997 let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
998
999 #[cfg(feature = "artifact-graph")]
1001 {
1002 let id = exec_state.next_uuid();
1003 exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
1004 id: ArtifactId::from(id),
1005 plane_id: plane.artifact_id,
1006 code_ref: CodeRef::placeholder(args.source_range),
1007 }));
1008 }
1009
1010 Ok(SketchSurface::Plane(plane))
1011 } else {
1012 let face = make_face(solid, tag, exec_state, args).await?;
1013
1014 #[cfg(feature = "artifact-graph")]
1015 {
1016 let id = exec_state.next_uuid();
1018 exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
1019 id: ArtifactId::from(id),
1020 face_id: face.artifact_id,
1021 code_ref: CodeRef::placeholder(args.source_range),
1022 }));
1023 }
1024
1025 Ok(SketchSurface::Face(face))
1026 }
1027 }
1028 }
1029}
1030
1031pub async fn make_sketch_plane_from_orientation(
1032 data: PlaneData,
1033 exec_state: &mut ExecState,
1034 args: &Args,
1035) -> Result<Box<Plane>, KclError> {
1036 let id = exec_state.next_uuid();
1037 let kind = PlaneKind::from(&data);
1038 let mut plane = Plane {
1039 id,
1040 artifact_id: id.into(),
1041 object_id: None,
1042 kind,
1043 info: PlaneInfo::try_from(data)?,
1044 meta: vec![args.source_range.into()],
1045 };
1046
1047 ensure_sketch_plane_in_engine(&mut plane, exec_state, args).await?;
1049
1050 Ok(Box::new(plane))
1051}
1052
1053pub async fn ensure_sketch_plane_in_engine(
1055 plane: &mut Plane,
1056 exec_state: &mut ExecState,
1057 args: &Args,
1058) -> Result<(), KclError> {
1059 if plane.is_initialized() {
1060 return Ok(());
1061 }
1062
1063 let clobber = false;
1064 let size = LengthUnit(60.0);
1065 let hide = Some(true);
1066 let cmd = if let Some(hide) = hide {
1067 mcmd::MakePlane::builder()
1068 .clobber(clobber)
1069 .origin(plane.info.origin.into())
1070 .size(size)
1071 .x_axis(plane.info.x_axis.into())
1072 .y_axis(plane.info.y_axis.into())
1073 .hide(hide)
1074 .build()
1075 } else {
1076 mcmd::MakePlane::builder()
1077 .clobber(clobber)
1078 .origin(plane.info.origin.into())
1079 .size(size)
1080 .x_axis(plane.info.x_axis.into())
1081 .y_axis(plane.info.y_axis.into())
1082 .build()
1083 };
1084 exec_state
1085 .batch_modeling_cmd(
1086 ModelingCmdMeta::from_args_id(exec_state, args, plane.id),
1087 ModelingCmd::from(cmd),
1088 )
1089 .await?;
1090 let plane_object_id = exec_state.next_object_id();
1091 #[cfg(feature = "artifact-graph")]
1092 {
1093 let plane_object = crate::front::Object {
1094 id: plane_object_id,
1095 kind: crate::front::ObjectKind::Plane(crate::front::Plane::Object(plane_object_id)),
1096 label: Default::default(),
1097 comments: Default::default(),
1098 artifact_id: ArtifactId::new(plane.id),
1099 source: args.source_range.into(),
1100 };
1101 exec_state.add_scene_object(plane_object, args.source_range);
1102 }
1103 plane.object_id = Some(plane_object_id);
1104
1105 Ok(())
1106}
1107
1108pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1110 let sketch_surface = args.get_unlabeled_kw_arg(
1111 "startProfileOn",
1112 &RuntimeType::Union(vec![RuntimeType::plane(), RuntimeType::face()]),
1113 exec_state,
1114 )?;
1115 let start: [TyF64; 2] = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
1116 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1117
1118 let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, args).await?;
1119 Ok(KclValue::Sketch {
1120 value: Box::new(sketch),
1121 })
1122}
1123
1124pub(crate) async fn inner_start_profile(
1125 sketch_surface: SketchSurface,
1126 at: [TyF64; 2],
1127 tag: Option<TagNode>,
1128 exec_state: &mut ExecState,
1129 args: Args,
1130) -> Result<Sketch, KclError> {
1131 match &sketch_surface {
1132 SketchSurface::Face(face) => {
1133 exec_state
1136 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &[(*face.solid).clone()])
1137 .await?;
1138 }
1139 SketchSurface::Plane(plane) if !plane.is_standard() => {
1140 exec_state
1143 .batch_end_cmd(
1144 ModelingCmdMeta::from_args(exec_state, &args),
1145 ModelingCmd::from(mcmd::ObjectVisible::builder().object_id(plane.id).hidden(true).build()),
1146 )
1147 .await?;
1148 }
1149 _ => {}
1150 }
1151
1152 let enable_sketch_id = exec_state.next_uuid();
1153 let path_id = exec_state.next_uuid();
1154 let move_pen_id = exec_state.next_uuid();
1155 let disable_sketch_id = exec_state.next_uuid();
1156 exec_state
1157 .batch_modeling_cmds(
1158 ModelingCmdMeta::from_args(exec_state, &args),
1159 &[
1160 ModelingCmdReq {
1163 cmd: ModelingCmd::from(if let SketchSurface::Plane(plane) = &sketch_surface {
1164 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
1166 mcmd::EnableSketchMode::builder()
1167 .animated(false)
1168 .ortho(false)
1169 .entity_id(sketch_surface.id())
1170 .adjust_camera(false)
1171 .planar_normal(normal.into())
1172 .build()
1173 } else {
1174 mcmd::EnableSketchMode::builder()
1175 .animated(false)
1176 .ortho(false)
1177 .entity_id(sketch_surface.id())
1178 .adjust_camera(false)
1179 .build()
1180 }),
1181 cmd_id: enable_sketch_id.into(),
1182 },
1183 ModelingCmdReq {
1184 cmd: ModelingCmd::from(mcmd::StartPath::default()),
1185 cmd_id: path_id.into(),
1186 },
1187 ModelingCmdReq {
1188 cmd: ModelingCmd::from(
1189 mcmd::MovePathPen::builder()
1190 .path(path_id.into())
1191 .to(KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit))
1192 .build(),
1193 ),
1194 cmd_id: move_pen_id.into(),
1195 },
1196 ModelingCmdReq {
1197 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1198 cmd_id: disable_sketch_id.into(),
1199 },
1200 ],
1201 )
1202 .await?;
1203
1204 let units = exec_state.length_unit();
1206 let to = point_to_len_unit(at, units);
1207 let current_path = BasePath {
1208 from: to,
1209 to,
1210 tag: tag.clone(),
1211 units,
1212 geo_meta: GeoMeta {
1213 id: move_pen_id,
1214 metadata: args.source_range.into(),
1215 },
1216 };
1217
1218 let sketch = Sketch {
1219 id: path_id,
1220 original_id: path_id,
1221 artifact_id: path_id.into(),
1222 on: sketch_surface.clone(),
1223 paths: vec![],
1224 inner_paths: vec![],
1225 units,
1226 mirror: Default::default(),
1227 clone: Default::default(),
1228 meta: vec![args.source_range.into()],
1229 tags: if let Some(tag) = &tag {
1230 let mut tag_identifier: TagIdentifier = tag.into();
1231 tag_identifier.info = vec![(
1232 exec_state.stack().current_epoch(),
1233 TagEngineInfo {
1234 id: current_path.geo_meta.id,
1235 sketch: path_id,
1236 path: Some(Path::Base {
1237 base: current_path.clone(),
1238 }),
1239 surface: None,
1240 },
1241 )];
1242 IndexMap::from([(tag.name.to_string(), tag_identifier)])
1243 } else {
1244 Default::default()
1245 },
1246 start: current_path,
1247 is_closed: ProfileClosed::No,
1248 };
1249 Ok(sketch)
1250}
1251
1252pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1254 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1255 let ty = sketch.units.into();
1256 let x = inner_profile_start_x(sketch)?;
1257 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1258}
1259
1260pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1261 Ok(profile.start.to[0])
1262}
1263
1264pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1266 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1267 let ty = sketch.units.into();
1268 let x = inner_profile_start_y(sketch)?;
1269 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1270}
1271
1272pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1273 Ok(profile.start.to[1])
1274}
1275
1276pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1278 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1279 let ty = sketch.units.into();
1280 let point = inner_profile_start(sketch)?;
1281 Ok(KclValue::from_point2d(point, ty, args.into()))
1282}
1283
1284pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1285 Ok(profile.start.to)
1286}
1287
1288pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1290 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1291 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1292 let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1293 Ok(KclValue::Sketch {
1294 value: Box::new(new_sketch),
1295 })
1296}
1297
1298pub(crate) async fn inner_close(
1299 sketch: Sketch,
1300 tag: Option<TagNode>,
1301 exec_state: &mut ExecState,
1302 args: Args,
1303) -> Result<Sketch, KclError> {
1304 if matches!(sketch.is_closed, ProfileClosed::Explicitly) {
1305 exec_state.warn(
1306 crate::CompilationError {
1307 source_range: args.source_range,
1308 message: "This sketch is already closed. Remove this unnecessary `close()` call".to_string(),
1309 suggestion: None,
1310 severity: crate::errors::Severity::Warning,
1311 tag: crate::errors::Tag::Unnecessary,
1312 },
1313 annotations::WARN_UNNECESSARY_CLOSE,
1314 );
1315 return Ok(sketch);
1316 }
1317 let from = sketch.current_pen_position()?;
1318 let to = point_to_len_unit(sketch.start.get_from(), from.units);
1319
1320 let id = exec_state.next_uuid();
1321
1322 exec_state
1323 .batch_modeling_cmd(
1324 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1325 ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
1326 )
1327 .await?;
1328
1329 let mut new_sketch = sketch;
1330
1331 let distance = ((from.x - to[0]).powi(2) + (from.y - to[1]).powi(2)).sqrt();
1332 if distance > super::EQUAL_POINTS_DIST_EPSILON {
1333 let current_path = Path::ToPoint {
1335 base: BasePath {
1336 from: from.ignore_units(),
1337 to,
1338 tag: tag.clone(),
1339 units: new_sketch.units,
1340 geo_meta: GeoMeta {
1341 id,
1342 metadata: args.source_range.into(),
1343 },
1344 },
1345 };
1346
1347 if let Some(tag) = &tag {
1348 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1349 }
1350 new_sketch.paths.push(current_path);
1351 } else if tag.is_some() {
1352 exec_state.warn(
1353 crate::CompilationError {
1354 source_range: args.source_range,
1355 message: "A tag declarator was specified, but no segment was created".to_string(),
1356 suggestion: None,
1357 severity: crate::errors::Severity::Warning,
1358 tag: crate::errors::Tag::Unnecessary,
1359 },
1360 annotations::WARN_UNUSED_TAGS,
1361 );
1362 }
1363
1364 new_sketch.is_closed = ProfileClosed::Explicitly;
1365
1366 Ok(new_sketch)
1367}
1368
1369pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1371 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1372
1373 let angle_start: Option<TyF64> = args.get_kw_arg_opt("angleStart", &RuntimeType::degrees(), exec_state)?;
1374 let angle_end: Option<TyF64> = args.get_kw_arg_opt("angleEnd", &RuntimeType::degrees(), exec_state)?;
1375 let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1376 let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1377 let end_absolute: Option<[TyF64; 2]> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1378 let interior_absolute: Option<[TyF64; 2]> =
1379 args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1380 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1381 let new_sketch = inner_arc(
1382 sketch,
1383 angle_start,
1384 angle_end,
1385 radius,
1386 diameter,
1387 interior_absolute,
1388 end_absolute,
1389 tag,
1390 exec_state,
1391 args,
1392 )
1393 .await?;
1394 Ok(KclValue::Sketch {
1395 value: Box::new(new_sketch),
1396 })
1397}
1398
1399#[allow(clippy::too_many_arguments)]
1400pub(crate) async fn inner_arc(
1401 sketch: Sketch,
1402 angle_start: Option<TyF64>,
1403 angle_end: Option<TyF64>,
1404 radius: Option<TyF64>,
1405 diameter: Option<TyF64>,
1406 interior_absolute: Option<[TyF64; 2]>,
1407 end_absolute: Option<[TyF64; 2]>,
1408 tag: Option<TagNode>,
1409 exec_state: &mut ExecState,
1410 args: Args,
1411) -> Result<Sketch, KclError> {
1412 let from: Point2d = sketch.current_pen_position()?;
1413 let id = exec_state.next_uuid();
1414
1415 match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1416 (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1417 let radius = get_radius(radius, diameter, args.source_range)?;
1418 relative_arc(&args, id, exec_state, sketch, from, angle_start, angle_end, radius, tag).await
1419 }
1420 (None, None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1421 absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1422 }
1423 _ => {
1424 Err(KclError::new_type(KclErrorDetails::new(
1425 "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)".to_owned(),
1426 vec![args.source_range],
1427 )))
1428 }
1429 }
1430}
1431
1432#[allow(clippy::too_many_arguments)]
1433pub async fn absolute_arc(
1434 args: &Args,
1435 id: uuid::Uuid,
1436 exec_state: &mut ExecState,
1437 sketch: Sketch,
1438 from: Point2d,
1439 interior_absolute: [TyF64; 2],
1440 end_absolute: [TyF64; 2],
1441 tag: Option<TagNode>,
1442) -> Result<Sketch, KclError> {
1443 exec_state
1445 .batch_modeling_cmd(
1446 ModelingCmdMeta::from_args_id(exec_state, args, id),
1447 ModelingCmd::from(
1448 mcmd::ExtendPath::builder()
1449 .path(sketch.id.into())
1450 .segment(PathSegment::ArcTo {
1451 end: kcmc::shared::Point3d {
1452 x: LengthUnit(end_absolute[0].to_mm()),
1453 y: LengthUnit(end_absolute[1].to_mm()),
1454 z: LengthUnit(0.0),
1455 },
1456 interior: kcmc::shared::Point3d {
1457 x: LengthUnit(interior_absolute[0].to_mm()),
1458 y: LengthUnit(interior_absolute[1].to_mm()),
1459 z: LengthUnit(0.0),
1460 },
1461 relative: false,
1462 })
1463 .build(),
1464 ),
1465 )
1466 .await?;
1467
1468 let start = [from.x, from.y];
1469 let end = point_to_len_unit(end_absolute, from.units);
1470 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1471
1472 let current_path = Path::ArcThreePoint {
1473 base: BasePath {
1474 from: from.ignore_units(),
1475 to: end,
1476 tag: tag.clone(),
1477 units: sketch.units,
1478 geo_meta: GeoMeta {
1479 id,
1480 metadata: args.source_range.into(),
1481 },
1482 },
1483 p1: start,
1484 p2: point_to_len_unit(interior_absolute, from.units),
1485 p3: end,
1486 };
1487
1488 let mut new_sketch = sketch;
1489 if let Some(tag) = &tag {
1490 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1491 }
1492 if loops_back_to_start {
1493 new_sketch.is_closed = ProfileClosed::Implicitly;
1494 }
1495
1496 new_sketch.paths.push(current_path);
1497
1498 Ok(new_sketch)
1499}
1500
1501#[allow(clippy::too_many_arguments)]
1502pub async fn relative_arc(
1503 args: &Args,
1504 id: uuid::Uuid,
1505 exec_state: &mut ExecState,
1506 sketch: Sketch,
1507 from: Point2d,
1508 angle_start: TyF64,
1509 angle_end: TyF64,
1510 radius: TyF64,
1511 tag: Option<TagNode>,
1512) -> Result<Sketch, KclError> {
1513 let a_start = Angle::from_degrees(angle_start.to_degrees(exec_state, args.source_range));
1514 let a_end = Angle::from_degrees(angle_end.to_degrees(exec_state, args.source_range));
1515 let radius = radius.to_length_units(from.units);
1516 let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1517 if a_start == a_end {
1518 return Err(KclError::new_type(KclErrorDetails::new(
1519 "Arc start and end angles must be different".to_string(),
1520 vec![args.source_range],
1521 )));
1522 }
1523 let ccw = a_start < a_end;
1524
1525 exec_state
1526 .batch_modeling_cmd(
1527 ModelingCmdMeta::from_args_id(exec_state, args, id),
1528 ModelingCmd::from(
1529 mcmd::ExtendPath::builder()
1530 .path(sketch.id.into())
1531 .segment(PathSegment::Arc {
1532 start: a_start,
1533 end: a_end,
1534 center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1535 radius: LengthUnit(
1536 crate::execution::types::adjust_length(from.units, radius, UnitLength::Millimeters).0,
1537 ),
1538 relative: false,
1539 })
1540 .build(),
1541 ),
1542 )
1543 .await?;
1544
1545 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1546 let current_path = Path::Arc {
1547 base: BasePath {
1548 from: from.ignore_units(),
1549 to: end,
1550 tag: tag.clone(),
1551 units: from.units,
1552 geo_meta: GeoMeta {
1553 id,
1554 metadata: args.source_range.into(),
1555 },
1556 },
1557 center,
1558 radius,
1559 ccw,
1560 };
1561
1562 let mut new_sketch = sketch;
1563 if let Some(tag) = &tag {
1564 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1565 }
1566 if loops_back_to_start {
1567 new_sketch.is_closed = ProfileClosed::Implicitly;
1568 }
1569
1570 new_sketch.paths.push(current_path);
1571
1572 Ok(new_sketch)
1573}
1574
1575pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1577 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1578 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1579 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1580 let radius = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1581 let diameter = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1582 let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
1583 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1584
1585 let new_sketch = inner_tangential_arc(
1586 sketch,
1587 end_absolute,
1588 end,
1589 radius,
1590 diameter,
1591 angle,
1592 tag,
1593 exec_state,
1594 args,
1595 )
1596 .await?;
1597 Ok(KclValue::Sketch {
1598 value: Box::new(new_sketch),
1599 })
1600}
1601
1602#[allow(clippy::too_many_arguments)]
1603async fn inner_tangential_arc(
1604 sketch: Sketch,
1605 end_absolute: Option<[TyF64; 2]>,
1606 end: Option<[TyF64; 2]>,
1607 radius: Option<TyF64>,
1608 diameter: Option<TyF64>,
1609 angle: Option<TyF64>,
1610 tag: Option<TagNode>,
1611 exec_state: &mut ExecState,
1612 args: Args,
1613) -> Result<Sketch, KclError> {
1614 match (end_absolute, end, radius, diameter, angle) {
1615 (Some(point), None, None, None, None) => {
1616 inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1617 }
1618 (None, Some(point), None, None, None) => {
1619 inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1620 }
1621 (None, None, radius, diameter, Some(angle)) => {
1622 let radius = get_radius(radius, diameter, args.source_range)?;
1623 let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1624 inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1625 }
1626 (Some(_), Some(_), None, None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
1627 "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1628 vec![args.source_range],
1629 ))),
1630 (_, _, _, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1631 "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1632 vec![args.source_range],
1633 ))),
1634 }
1635}
1636
1637#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1639#[ts(export)]
1640#[serde(rename_all = "camelCase", untagged)]
1641pub enum TangentialArcData {
1642 RadiusAndOffset {
1643 radius: TyF64,
1646 offset: TyF64,
1648 },
1649}
1650
1651async fn inner_tangential_arc_radius_angle(
1658 data: TangentialArcData,
1659 sketch: Sketch,
1660 tag: Option<TagNode>,
1661 exec_state: &mut ExecState,
1662 args: Args,
1663) -> Result<Sketch, KclError> {
1664 let from: Point2d = sketch.current_pen_position()?;
1665 let tangent_info = sketch.get_tangential_info_from_paths(); let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1668
1669 let id = exec_state.next_uuid();
1670
1671 let (center, to, ccw) = match data {
1672 TangentialArcData::RadiusAndOffset { radius, offset } => {
1673 let offset = Angle::from_degrees(offset.to_degrees(exec_state, args.source_range));
1675
1676 let previous_end_tangent = Angle::from_radians(libm::atan2(
1679 from.y - tan_previous_point[1],
1680 from.x - tan_previous_point[0],
1681 ));
1682 let ccw = offset.to_degrees() > 0.0;
1685 let tangent_to_arc_start_angle = if ccw {
1686 Angle::from_degrees(-90.0)
1688 } else {
1689 Angle::from_degrees(90.0)
1691 };
1692 let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1695 let end_angle = start_angle + offset;
1696 let (center, to) = arc_center_and_end(
1697 from.ignore_units(),
1698 start_angle,
1699 end_angle,
1700 radius.to_length_units(from.units),
1701 );
1702
1703 exec_state
1704 .batch_modeling_cmd(
1705 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1706 ModelingCmd::from(
1707 mcmd::ExtendPath::builder()
1708 .path(sketch.id.into())
1709 .segment(PathSegment::TangentialArc {
1710 radius: LengthUnit(radius.to_mm()),
1711 offset,
1712 })
1713 .build(),
1714 ),
1715 )
1716 .await?;
1717 (center, to, ccw)
1718 }
1719 };
1720 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1721
1722 let current_path = Path::TangentialArc {
1723 ccw,
1724 center,
1725 base: BasePath {
1726 from: from.ignore_units(),
1727 to,
1728 tag: tag.clone(),
1729 units: sketch.units,
1730 geo_meta: GeoMeta {
1731 id,
1732 metadata: args.source_range.into(),
1733 },
1734 },
1735 };
1736
1737 let mut new_sketch = sketch;
1738 if let Some(tag) = &tag {
1739 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1740 }
1741 if loops_back_to_start {
1742 new_sketch.is_closed = ProfileClosed::Implicitly;
1743 }
1744
1745 new_sketch.paths.push(current_path);
1746
1747 Ok(new_sketch)
1748}
1749
1750fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
1752 ModelingCmd::from(
1753 mcmd::ExtendPath::builder()
1754 .path(sketch.id.into())
1755 .segment(PathSegment::TangentialArcTo {
1756 angle_snap_increment: None,
1757 to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
1758 .with_z(0.0)
1759 .map(LengthUnit),
1760 })
1761 .build(),
1762 )
1763}
1764
1765async fn inner_tangential_arc_to_point(
1766 sketch: Sketch,
1767 point: [TyF64; 2],
1768 is_absolute: bool,
1769 tag: Option<TagNode>,
1770 exec_state: &mut ExecState,
1771 args: Args,
1772) -> Result<Sketch, KclError> {
1773 let from: Point2d = sketch.current_pen_position()?;
1774 let tangent_info = sketch.get_tangential_info_from_paths();
1775 let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1776
1777 let point = point_to_len_unit(point, from.units);
1778
1779 let to = if is_absolute {
1780 point
1781 } else {
1782 [from.x + point[0], from.y + point[1]]
1783 };
1784 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1785 let [to_x, to_y] = to;
1786 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1787 arc_start_point: [from.x, from.y],
1788 arc_end_point: [to_x, to_y],
1789 tan_previous_point,
1790 obtuse: true,
1791 });
1792
1793 if result.center[0].is_infinite() {
1794 return Err(KclError::new_semantic(KclErrorDetails::new(
1795 "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
1796 .to_owned(),
1797 vec![args.source_range],
1798 )));
1799 } else if result.center[1].is_infinite() {
1800 return Err(KclError::new_semantic(KclErrorDetails::new(
1801 "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
1802 .to_owned(),
1803 vec![args.source_range],
1804 )));
1805 }
1806
1807 let delta = if is_absolute {
1808 [to_x - from.x, to_y - from.y]
1809 } else {
1810 point
1811 };
1812 let id = exec_state.next_uuid();
1813 exec_state
1814 .batch_modeling_cmd(
1815 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1816 tan_arc_to(&sketch, delta),
1817 )
1818 .await?;
1819
1820 let current_path = Path::TangentialArcTo {
1821 base: BasePath {
1822 from: from.ignore_units(),
1823 to,
1824 tag: tag.clone(),
1825 units: sketch.units,
1826 geo_meta: GeoMeta {
1827 id,
1828 metadata: args.source_range.into(),
1829 },
1830 },
1831 center: result.center,
1832 ccw: result.ccw > 0,
1833 };
1834
1835 let mut new_sketch = sketch;
1836 if let Some(tag) = &tag {
1837 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1838 }
1839 if loops_back_to_start {
1840 new_sketch.is_closed = ProfileClosed::Implicitly;
1841 }
1842
1843 new_sketch.paths.push(current_path);
1844
1845 Ok(new_sketch)
1846}
1847
1848pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1850 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1851 let control1 = args.get_kw_arg_opt("control1", &RuntimeType::point2d(), exec_state)?;
1852 let control2 = args.get_kw_arg_opt("control2", &RuntimeType::point2d(), exec_state)?;
1853 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1854 let control1_absolute = args.get_kw_arg_opt("control1Absolute", &RuntimeType::point2d(), exec_state)?;
1855 let control2_absolute = args.get_kw_arg_opt("control2Absolute", &RuntimeType::point2d(), exec_state)?;
1856 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1857 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1858
1859 let new_sketch = inner_bezier_curve(
1860 sketch,
1861 control1,
1862 control2,
1863 end,
1864 control1_absolute,
1865 control2_absolute,
1866 end_absolute,
1867 tag,
1868 exec_state,
1869 args,
1870 )
1871 .await?;
1872 Ok(KclValue::Sketch {
1873 value: Box::new(new_sketch),
1874 })
1875}
1876
1877#[allow(clippy::too_many_arguments)]
1878async fn inner_bezier_curve(
1879 sketch: Sketch,
1880 control1: Option<[TyF64; 2]>,
1881 control2: Option<[TyF64; 2]>,
1882 end: Option<[TyF64; 2]>,
1883 control1_absolute: Option<[TyF64; 2]>,
1884 control2_absolute: Option<[TyF64; 2]>,
1885 end_absolute: Option<[TyF64; 2]>,
1886 tag: Option<TagNode>,
1887 exec_state: &mut ExecState,
1888 args: Args,
1889) -> Result<Sketch, KclError> {
1890 let from = sketch.current_pen_position()?;
1891 let id = exec_state.next_uuid();
1892
1893 let to = match (
1894 control1,
1895 control2,
1896 end,
1897 control1_absolute,
1898 control2_absolute,
1899 end_absolute,
1900 ) {
1901 (Some(control1), Some(control2), Some(end), None, None, None) => {
1903 let delta = end.clone();
1904 let to = [
1905 from.x + end[0].to_length_units(from.units),
1906 from.y + end[1].to_length_units(from.units),
1907 ];
1908
1909 exec_state
1910 .batch_modeling_cmd(
1911 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1912 ModelingCmd::from(
1913 mcmd::ExtendPath::builder()
1914 .path(sketch.id.into())
1915 .segment(PathSegment::Bezier {
1916 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1917 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1918 end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
1919 relative: true,
1920 })
1921 .build(),
1922 ),
1923 )
1924 .await?;
1925 to
1926 }
1927 (None, None, None, Some(control1), Some(control2), Some(end)) => {
1929 let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
1930 exec_state
1931 .batch_modeling_cmd(
1932 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1933 ModelingCmd::from(
1934 mcmd::ExtendPath::builder()
1935 .path(sketch.id.into())
1936 .segment(PathSegment::Bezier {
1937 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1938 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1939 end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
1940 relative: false,
1941 })
1942 .build(),
1943 ),
1944 )
1945 .await?;
1946 to
1947 }
1948 _ => {
1949 return Err(KclError::new_semantic(KclErrorDetails::new(
1950 "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
1951 vec![args.source_range],
1952 )));
1953 }
1954 };
1955
1956 let current_path = Path::ToPoint {
1957 base: BasePath {
1958 from: from.ignore_units(),
1959 to,
1960 tag: tag.clone(),
1961 units: sketch.units,
1962 geo_meta: GeoMeta {
1963 id,
1964 metadata: args.source_range.into(),
1965 },
1966 },
1967 };
1968
1969 let mut new_sketch = sketch;
1970 if let Some(tag) = &tag {
1971 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1972 }
1973
1974 new_sketch.paths.push(current_path);
1975
1976 Ok(new_sketch)
1977}
1978
1979pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1981 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1982
1983 let tool: Vec<Sketch> = args.get_kw_arg(
1984 "tool",
1985 &RuntimeType::Array(
1986 Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
1987 ArrayLen::Minimum(1),
1988 ),
1989 exec_state,
1990 )?;
1991
1992 let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
1993 Ok(KclValue::Sketch {
1994 value: Box::new(new_sketch),
1995 })
1996}
1997
1998async fn inner_subtract_2d(
1999 mut sketch: Sketch,
2000 tool: Vec<Sketch>,
2001 exec_state: &mut ExecState,
2002 args: Args,
2003) -> Result<Sketch, KclError> {
2004 for hole_sketch in tool {
2005 exec_state
2006 .batch_modeling_cmd(
2007 ModelingCmdMeta::from_args(exec_state, &args),
2008 ModelingCmd::from(
2009 mcmd::Solid2dAddHole::builder()
2010 .object_id(sketch.id)
2011 .hole_id(hole_sketch.id)
2012 .build(),
2013 ),
2014 )
2015 .await?;
2016
2017 exec_state
2020 .batch_modeling_cmd(
2021 ModelingCmdMeta::from_args(exec_state, &args),
2022 ModelingCmd::from(
2023 mcmd::ObjectVisible::builder()
2024 .object_id(hole_sketch.id)
2025 .hidden(true)
2026 .build(),
2027 ),
2028 )
2029 .await?;
2030
2031 sketch.inner_paths.extend_from_slice(&hole_sketch.paths);
2036 }
2037
2038 Ok(sketch)
2041}
2042
2043pub async fn elliptic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2045 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2046 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2047 let major_radius = args.get_kw_arg("majorRadius", &RuntimeType::num_any(), exec_state)?;
2048 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::num_any(), exec_state)?;
2049
2050 let elliptic_point = inner_elliptic_point(x, y, major_radius, minor_radius, &args).await?;
2051
2052 args.make_kcl_val_from_point(elliptic_point, exec_state.length_unit().into())
2053}
2054
2055async fn inner_elliptic_point(
2056 x: Option<TyF64>,
2057 y: Option<TyF64>,
2058 major_radius: TyF64,
2059 minor_radius: TyF64,
2060 args: &Args,
2061) -> Result<[f64; 2], KclError> {
2062 let major_radius = major_radius.n;
2063 let minor_radius = minor_radius.n;
2064 if let Some(x) = x {
2065 if x.n.abs() > major_radius {
2066 Err(KclError::Type {
2067 details: KclErrorDetails::new(
2068 format!(
2069 "Invalid input. The x value, {}, cannot be larger than the major radius {}.",
2070 x.n, major_radius
2071 ),
2072 vec![args.source_range],
2073 ),
2074 })
2075 } else {
2076 Ok((
2077 x.n,
2078 minor_radius * (1.0 - x.n.powf(2.0) / major_radius.powf(2.0)).sqrt(),
2079 )
2080 .into())
2081 }
2082 } else if let Some(y) = y {
2083 if y.n > minor_radius {
2084 Err(KclError::Type {
2085 details: KclErrorDetails::new(
2086 format!(
2087 "Invalid input. The y value, {}, cannot be larger than the minor radius {}.",
2088 y.n, minor_radius
2089 ),
2090 vec![args.source_range],
2091 ),
2092 })
2093 } else {
2094 Ok((
2095 major_radius * (1.0 - y.n.powf(2.0) / minor_radius.powf(2.0)).sqrt(),
2096 y.n,
2097 )
2098 .into())
2099 }
2100 } else {
2101 Err(KclError::Type {
2102 details: KclErrorDetails::new(
2103 "Invalid input. Must have either x or y, you cannot have both or neither.".to_owned(),
2104 vec![args.source_range],
2105 ),
2106 })
2107 }
2108}
2109
2110pub async fn elliptic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2112 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2113
2114 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
2115 let angle_start = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
2116 let angle_end = args.get_kw_arg("angleEnd", &RuntimeType::degrees(), exec_state)?;
2117 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
2118 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
2119 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
2120 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2121
2122 let new_sketch = inner_elliptic(
2123 sketch,
2124 center,
2125 angle_start,
2126 angle_end,
2127 major_radius,
2128 major_axis,
2129 minor_radius,
2130 tag,
2131 exec_state,
2132 args,
2133 )
2134 .await?;
2135 Ok(KclValue::Sketch {
2136 value: Box::new(new_sketch),
2137 })
2138}
2139
2140#[allow(clippy::too_many_arguments)]
2141pub(crate) async fn inner_elliptic(
2142 sketch: Sketch,
2143 center: [TyF64; 2],
2144 angle_start: TyF64,
2145 angle_end: TyF64,
2146 major_radius: Option<TyF64>,
2147 major_axis: Option<[TyF64; 2]>,
2148 minor_radius: TyF64,
2149 tag: Option<TagNode>,
2150 exec_state: &mut ExecState,
2151 args: Args,
2152) -> Result<Sketch, KclError> {
2153 let from: Point2d = sketch.current_pen_position()?;
2154 let id = exec_state.next_uuid();
2155
2156 let center_u = point_to_len_unit(center, from.units);
2157
2158 let major_axis = match (major_axis, major_radius) {
2159 (Some(_), Some(_)) | (None, None) => {
2160 return Err(KclError::new_type(KclErrorDetails::new(
2161 "Provide either `majorAxis` or `majorRadius`.".to_string(),
2162 vec![args.source_range],
2163 )));
2164 }
2165 (Some(major_axis), None) => major_axis,
2166 (None, Some(major_radius)) => [
2167 major_radius.clone(),
2168 TyF64 {
2169 n: 0.0,
2170 ty: major_radius.ty,
2171 },
2172 ],
2173 };
2174 let start_angle = Angle::from_degrees(angle_start.to_degrees(exec_state, args.source_range));
2175 let end_angle = Angle::from_degrees(angle_end.to_degrees(exec_state, args.source_range));
2176 let major_axis_magnitude = (major_axis[0].to_length_units(from.units) * major_axis[0].to_length_units(from.units)
2177 + major_axis[1].to_length_units(from.units) * major_axis[1].to_length_units(from.units))
2178 .sqrt();
2179 let to = [
2180 major_axis_magnitude * libm::cos(end_angle.to_radians()),
2181 minor_radius.to_length_units(from.units) * libm::sin(end_angle.to_radians()),
2182 ];
2183 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2184 let major_axis_angle = libm::atan2(major_axis[1].n, major_axis[0].n);
2185
2186 let point = [
2187 center_u[0] + to[0] * libm::cos(major_axis_angle) - to[1] * libm::sin(major_axis_angle),
2188 center_u[1] + to[0] * libm::sin(major_axis_angle) + to[1] * libm::cos(major_axis_angle),
2189 ];
2190
2191 let axis = major_axis.map(|x| x.to_mm());
2192 exec_state
2193 .batch_modeling_cmd(
2194 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2195 ModelingCmd::from(
2196 mcmd::ExtendPath::builder()
2197 .path(sketch.id.into())
2198 .segment(PathSegment::Ellipse {
2199 center: KPoint2d::from(untyped_point_to_mm(center_u, from.units)).map(LengthUnit),
2200 major_axis: axis.map(LengthUnit).into(),
2201 minor_radius: LengthUnit(minor_radius.to_mm()),
2202 start_angle,
2203 end_angle,
2204 })
2205 .build(),
2206 ),
2207 )
2208 .await?;
2209
2210 let current_path = Path::Ellipse {
2211 ccw: start_angle < end_angle,
2212 center: center_u,
2213 major_axis: axis,
2214 minor_radius: minor_radius.to_mm(),
2215 base: BasePath {
2216 from: from.ignore_units(),
2217 to: point,
2218 tag: tag.clone(),
2219 units: sketch.units,
2220 geo_meta: GeoMeta {
2221 id,
2222 metadata: args.source_range.into(),
2223 },
2224 },
2225 };
2226 let mut new_sketch = sketch;
2227 if let Some(tag) = &tag {
2228 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2229 }
2230 if loops_back_to_start {
2231 new_sketch.is_closed = ProfileClosed::Implicitly;
2232 }
2233
2234 new_sketch.paths.push(current_path);
2235
2236 Ok(new_sketch)
2237}
2238
2239pub async fn hyperbolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2241 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2242 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2243 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::num_any(), exec_state)?;
2244 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::num_any(), exec_state)?;
2245
2246 let hyperbolic_point = inner_hyperbolic_point(x, y, semi_major, semi_minor, &args).await?;
2247
2248 args.make_kcl_val_from_point(hyperbolic_point, exec_state.length_unit().into())
2249}
2250
2251async fn inner_hyperbolic_point(
2252 x: Option<TyF64>,
2253 y: Option<TyF64>,
2254 semi_major: TyF64,
2255 semi_minor: TyF64,
2256 args: &Args,
2257) -> Result<[f64; 2], KclError> {
2258 let semi_major = semi_major.n;
2259 let semi_minor = semi_minor.n;
2260 if let Some(x) = x {
2261 if x.n.abs() < semi_major {
2262 Err(KclError::Type {
2263 details: KclErrorDetails::new(
2264 format!(
2265 "Invalid input. The x value, {}, cannot be less than the semi major value, {}.",
2266 x.n, semi_major
2267 ),
2268 vec![args.source_range],
2269 ),
2270 })
2271 } else {
2272 Ok((x.n, semi_minor * (x.n.powf(2.0) / semi_major.powf(2.0) - 1.0).sqrt()).into())
2273 }
2274 } else if let Some(y) = y {
2275 Ok((semi_major * (y.n.powf(2.0) / semi_minor.powf(2.0) + 1.0).sqrt(), y.n).into())
2276 } else {
2277 Err(KclError::Type {
2278 details: KclErrorDetails::new(
2279 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2280 vec![args.source_range],
2281 ),
2282 })
2283 }
2284}
2285
2286pub async fn hyperbolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2288 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2289
2290 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::length(), exec_state)?;
2291 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::length(), exec_state)?;
2292 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2293 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2294 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2295 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2296 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2297
2298 let new_sketch = inner_hyperbolic(
2299 sketch,
2300 semi_major,
2301 semi_minor,
2302 interior,
2303 end,
2304 interior_absolute,
2305 end_absolute,
2306 tag,
2307 exec_state,
2308 args,
2309 )
2310 .await?;
2311 Ok(KclValue::Sketch {
2312 value: Box::new(new_sketch),
2313 })
2314}
2315
2316fn hyperbolic_tangent(point: Point2d, semi_major: f64, semi_minor: f64) -> [f64; 2] {
2318 (point.y * semi_major.powf(2.0), point.x * semi_minor.powf(2.0)).into()
2319}
2320
2321#[allow(clippy::too_many_arguments)]
2322pub(crate) async fn inner_hyperbolic(
2323 sketch: Sketch,
2324 semi_major: TyF64,
2325 semi_minor: TyF64,
2326 interior: Option<[TyF64; 2]>,
2327 end: Option<[TyF64; 2]>,
2328 interior_absolute: Option<[TyF64; 2]>,
2329 end_absolute: Option<[TyF64; 2]>,
2330 tag: Option<TagNode>,
2331 exec_state: &mut ExecState,
2332 args: Args,
2333) -> Result<Sketch, KclError> {
2334 let from = sketch.current_pen_position()?;
2335 let id = exec_state.next_uuid();
2336
2337 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2338 (Some(interior), Some(end), None, None) => (interior, end, true),
2339 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2340 _ => return Err(KclError::Type {
2341 details: KclErrorDetails::new(
2342 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2343 .to_owned(),
2344 vec![args.source_range],
2345 ),
2346 }),
2347 };
2348
2349 let interior = point_to_len_unit(interior, from.units);
2350 let end = point_to_len_unit(end, from.units);
2351 let end_point = Point2d {
2352 x: end[0],
2353 y: end[1],
2354 units: from.units,
2355 };
2356 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
2357
2358 let semi_major_u = semi_major.to_length_units(from.units);
2359 let semi_minor_u = semi_minor.to_length_units(from.units);
2360
2361 let start_tangent = hyperbolic_tangent(from, semi_major_u, semi_minor_u);
2362 let end_tangent = hyperbolic_tangent(end_point, semi_major_u, semi_minor_u);
2363
2364 exec_state
2365 .batch_modeling_cmd(
2366 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2367 ModelingCmd::from(
2368 mcmd::ExtendPath::builder()
2369 .path(sketch.id.into())
2370 .segment(PathSegment::ConicTo {
2371 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2372 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2373 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2374 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2375 relative,
2376 })
2377 .build(),
2378 ),
2379 )
2380 .await?;
2381
2382 let current_path = Path::Conic {
2383 base: BasePath {
2384 from: from.ignore_units(),
2385 to: end,
2386 tag: tag.clone(),
2387 units: sketch.units,
2388 geo_meta: GeoMeta {
2389 id,
2390 metadata: args.source_range.into(),
2391 },
2392 },
2393 };
2394
2395 let mut new_sketch = sketch;
2396 if let Some(tag) = &tag {
2397 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2398 }
2399 if loops_back_to_start {
2400 new_sketch.is_closed = ProfileClosed::Implicitly;
2401 }
2402
2403 new_sketch.paths.push(current_path);
2404
2405 Ok(new_sketch)
2406}
2407
2408pub async fn parabolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2410 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2411 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2412 let coefficients = args.get_kw_arg(
2413 "coefficients",
2414 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2415 exec_state,
2416 )?;
2417
2418 let parabolic_point = inner_parabolic_point(x, y, &coefficients, &args).await?;
2419
2420 args.make_kcl_val_from_point(parabolic_point, exec_state.length_unit().into())
2421}
2422
2423async fn inner_parabolic_point(
2424 x: Option<TyF64>,
2425 y: Option<TyF64>,
2426 coefficients: &[TyF64; 3],
2427 args: &Args,
2428) -> Result<[f64; 2], KclError> {
2429 let a = coefficients[0].n;
2430 let b = coefficients[1].n;
2431 let c = coefficients[2].n;
2432 if let Some(x) = x {
2433 Ok((x.n, a * x.n.powf(2.0) + b * x.n + c).into())
2434 } else if let Some(y) = y {
2435 let det = (b.powf(2.0) - 4.0 * a * (c - y.n)).sqrt();
2436 Ok(((-b + det) / (2.0 * a), y.n).into())
2437 } else {
2438 Err(KclError::Type {
2439 details: KclErrorDetails::new(
2440 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2441 vec![args.source_range],
2442 ),
2443 })
2444 }
2445}
2446
2447pub async fn parabolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2449 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2450
2451 let coefficients = args.get_kw_arg_opt(
2452 "coefficients",
2453 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2454 exec_state,
2455 )?;
2456 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2457 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2458 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2459 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2460 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2461
2462 let new_sketch = inner_parabolic(
2463 sketch,
2464 coefficients,
2465 interior,
2466 end,
2467 interior_absolute,
2468 end_absolute,
2469 tag,
2470 exec_state,
2471 args,
2472 )
2473 .await?;
2474 Ok(KclValue::Sketch {
2475 value: Box::new(new_sketch),
2476 })
2477}
2478
2479fn parabolic_tangent(point: Point2d, a: f64, b: f64) -> [f64; 2] {
2480 (1.0, 2.0 * a * point.x + b).into()
2483}
2484
2485#[allow(clippy::too_many_arguments)]
2486pub(crate) async fn inner_parabolic(
2487 sketch: Sketch,
2488 coefficients: Option<[TyF64; 3]>,
2489 interior: Option<[TyF64; 2]>,
2490 end: Option<[TyF64; 2]>,
2491 interior_absolute: Option<[TyF64; 2]>,
2492 end_absolute: Option<[TyF64; 2]>,
2493 tag: Option<TagNode>,
2494 exec_state: &mut ExecState,
2495 args: Args,
2496) -> Result<Sketch, KclError> {
2497 let from = sketch.current_pen_position()?;
2498 let id = exec_state.next_uuid();
2499
2500 if (coefficients.is_some() && interior.is_some()) || (coefficients.is_none() && interior.is_none()) {
2501 return Err(KclError::Type {
2502 details: KclErrorDetails::new(
2503 "Invalid combination of arguments. Either provide (a, b, c) or (interior)".to_owned(),
2504 vec![args.source_range],
2505 ),
2506 });
2507 }
2508
2509 let (interior, end, relative) = match (coefficients.clone(), interior, end, interior_absolute, end_absolute) {
2510 (None, Some(interior), Some(end), None, None) => {
2511 let interior = point_to_len_unit(interior, from.units);
2512 let end = point_to_len_unit(end, from.units);
2513 (interior,end, true)
2514 },
2515 (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
2516 let interior_absolute = point_to_len_unit(interior_absolute, from.units);
2517 let end_absolute = point_to_len_unit(end_absolute, from.units);
2518 (interior_absolute, end_absolute, false)
2519 }
2520 (Some(coefficients), _, Some(end), _, _) => {
2521 let end = point_to_len_unit(end, from.units);
2522 let interior =
2523 inner_parabolic_point(
2524 Some(TyF64::count(0.5 * (from.x + end[0]))),
2525 None,
2526 &coefficients,
2527 &args,
2528 )
2529 .await?;
2530 (interior, end, true)
2531 }
2532 (Some(coefficients), _, _, _, Some(end)) => {
2533 let end = point_to_len_unit(end, from.units);
2534 let interior =
2535 inner_parabolic_point(
2536 Some(TyF64::count(0.5 * (from.x + end[0]))),
2537 None,
2538 &coefficients,
2539 &args,
2540 )
2541 .await?;
2542 (interior, end, false)
2543 }
2544 _ => return
2545 Err(KclError::Type{details: KclErrorDetails::new(
2546 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute) if coefficients are not provided."
2547 .to_owned(),
2548 vec![args.source_range],
2549 )}),
2550 };
2551
2552 let end_point = Point2d {
2553 x: end[0],
2554 y: end[1],
2555 units: from.units,
2556 };
2557
2558 let (a, b, _c) = if let Some([a, b, c]) = coefficients {
2559 (a.n, b.n, c.n)
2560 } else {
2561 let denom = (from.x - interior[0]) * (from.x - end_point.x) * (interior[0] - end_point.x);
2563 let a = (end_point.x * (interior[1] - from.y)
2564 + interior[0] * (from.y - end_point.y)
2565 + from.x * (end_point.y - interior[1]))
2566 / denom;
2567 let b = (end_point.x.powf(2.0) * (from.y - interior[1])
2568 + interior[0].powf(2.0) * (end_point.y - from.y)
2569 + from.x.powf(2.0) * (interior[1] - end_point.y))
2570 / denom;
2571 let c = (interior[0] * end_point.x * (interior[0] - end_point.x) * from.y
2572 + end_point.x * from.x * (end_point.x - from.x) * interior[1]
2573 + from.x * interior[0] * (from.x - interior[0]) * end_point.y)
2574 / denom;
2575
2576 (a, b, c)
2577 };
2578
2579 let start_tangent = parabolic_tangent(from, a, b);
2580 let end_tangent = parabolic_tangent(end_point, a, b);
2581
2582 exec_state
2583 .batch_modeling_cmd(
2584 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2585 ModelingCmd::from(
2586 mcmd::ExtendPath::builder()
2587 .path(sketch.id.into())
2588 .segment(PathSegment::ConicTo {
2589 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2590 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2591 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2592 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2593 relative,
2594 })
2595 .build(),
2596 ),
2597 )
2598 .await?;
2599
2600 let current_path = Path::Conic {
2601 base: BasePath {
2602 from: from.ignore_units(),
2603 to: end,
2604 tag: tag.clone(),
2605 units: sketch.units,
2606 geo_meta: GeoMeta {
2607 id,
2608 metadata: args.source_range.into(),
2609 },
2610 },
2611 };
2612
2613 let mut new_sketch = sketch;
2614 if let Some(tag) = &tag {
2615 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2616 }
2617
2618 new_sketch.paths.push(current_path);
2619
2620 Ok(new_sketch)
2621}
2622
2623fn conic_tangent(coefficients: [f64; 6], point: [f64; 2]) -> [f64; 2] {
2624 let [a, b, c, d, e, _] = coefficients;
2625
2626 (
2627 c * point[0] + 2.0 * b * point[1] + e,
2628 -(2.0 * a * point[0] + c * point[1] + d),
2629 )
2630 .into()
2631}
2632
2633pub async fn conic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2635 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2636
2637 let start_tangent = args.get_kw_arg_opt("startTangent", &RuntimeType::point2d(), exec_state)?;
2638 let end_tangent = args.get_kw_arg_opt("endTangent", &RuntimeType::point2d(), exec_state)?;
2639 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2640 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2641 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2642 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2643 let coefficients = args.get_kw_arg_opt(
2644 "coefficients",
2645 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(6)),
2646 exec_state,
2647 )?;
2648 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2649
2650 let new_sketch = inner_conic(
2651 sketch,
2652 start_tangent,
2653 end,
2654 end_tangent,
2655 interior,
2656 coefficients,
2657 interior_absolute,
2658 end_absolute,
2659 tag,
2660 exec_state,
2661 args,
2662 )
2663 .await?;
2664 Ok(KclValue::Sketch {
2665 value: Box::new(new_sketch),
2666 })
2667}
2668
2669#[allow(clippy::too_many_arguments)]
2670pub(crate) async fn inner_conic(
2671 sketch: Sketch,
2672 start_tangent: Option<[TyF64; 2]>,
2673 end: Option<[TyF64; 2]>,
2674 end_tangent: Option<[TyF64; 2]>,
2675 interior: Option<[TyF64; 2]>,
2676 coefficients: Option<[TyF64; 6]>,
2677 interior_absolute: Option<[TyF64; 2]>,
2678 end_absolute: Option<[TyF64; 2]>,
2679 tag: Option<TagNode>,
2680 exec_state: &mut ExecState,
2681 args: Args,
2682) -> Result<Sketch, KclError> {
2683 let from: Point2d = sketch.current_pen_position()?;
2684 let id = exec_state.next_uuid();
2685
2686 if (coefficients.is_some() && (start_tangent.is_some() || end_tangent.is_some()))
2687 || (coefficients.is_none() && (start_tangent.is_none() && end_tangent.is_none()))
2688 {
2689 return Err(KclError::Type {
2690 details: KclErrorDetails::new(
2691 "Invalid combination of arguments. Either provide coefficients or (startTangent, endTangent)"
2692 .to_owned(),
2693 vec![args.source_range],
2694 ),
2695 });
2696 }
2697
2698 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2699 (Some(interior), Some(end), None, None) => (interior, end, true),
2700 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2701 _ => return Err(KclError::Type {
2702 details: KclErrorDetails::new(
2703 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2704 .to_owned(),
2705 vec![args.source_range],
2706 ),
2707 }),
2708 };
2709
2710 let end = point_to_len_unit(end, from.units);
2711 let interior = point_to_len_unit(interior, from.units);
2712
2713 let (start_tangent, end_tangent) = if let Some(coeffs) = coefficients {
2714 let (coeffs, _) = untype_array(coeffs);
2715 (conic_tangent(coeffs, [from.x, from.y]), conic_tangent(coeffs, end))
2716 } else {
2717 let start = if let Some(start_tangent) = start_tangent {
2718 point_to_len_unit(start_tangent, from.units)
2719 } else {
2720 let previous_point = sketch
2721 .get_tangential_info_from_paths()
2722 .tan_previous_point(from.ignore_units());
2723 let from = from.ignore_units();
2724 [from[0] - previous_point[0], from[1] - previous_point[1]]
2725 };
2726
2727 let Some(end_tangent) = end_tangent else {
2728 return Err(KclError::new_semantic(KclErrorDetails::new(
2729 "You must either provide either `coefficients` or `endTangent`.".to_owned(),
2730 vec![args.source_range],
2731 )));
2732 };
2733 let end_tan = point_to_len_unit(end_tangent, from.units);
2734 (start, end_tan)
2735 };
2736
2737 exec_state
2738 .batch_modeling_cmd(
2739 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2740 ModelingCmd::from(
2741 mcmd::ExtendPath::builder()
2742 .path(sketch.id.into())
2743 .segment(PathSegment::ConicTo {
2744 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2745 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2746 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2747 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2748 relative,
2749 })
2750 .build(),
2751 ),
2752 )
2753 .await?;
2754
2755 let current_path = Path::Conic {
2756 base: BasePath {
2757 from: from.ignore_units(),
2758 to: end,
2759 tag: tag.clone(),
2760 units: sketch.units,
2761 geo_meta: GeoMeta {
2762 id,
2763 metadata: args.source_range.into(),
2764 },
2765 },
2766 };
2767
2768 let mut new_sketch = sketch;
2769 if let Some(tag) = &tag {
2770 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2771 }
2772
2773 new_sketch.paths.push(current_path);
2774
2775 Ok(new_sketch)
2776}
2777#[cfg(test)]
2778mod tests {
2779
2780 use pretty_assertions::assert_eq;
2781
2782 use crate::{
2783 execution::TagIdentifier,
2784 std::{sketch::PlaneData, utils::calculate_circle_center},
2785 };
2786
2787 #[test]
2788 fn test_deserialize_plane_data() {
2789 let data = PlaneData::XY;
2790 let mut str_json = serde_json::to_string(&data).unwrap();
2791 assert_eq!(str_json, "\"XY\"");
2792
2793 str_json = "\"YZ\"".to_string();
2794 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2795 assert_eq!(data, PlaneData::YZ);
2796
2797 str_json = "\"-YZ\"".to_string();
2798 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2799 assert_eq!(data, PlaneData::NegYZ);
2800
2801 str_json = "\"-xz\"".to_string();
2802 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2803 assert_eq!(data, PlaneData::NegXZ);
2804 }
2805
2806 #[test]
2807 fn test_deserialize_sketch_on_face_tag() {
2808 let data = "start";
2809 let mut str_json = serde_json::to_string(&data).unwrap();
2810 assert_eq!(str_json, "\"start\"");
2811
2812 str_json = "\"end\"".to_string();
2813 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2814 assert_eq!(
2815 data,
2816 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2817 );
2818
2819 str_json = serde_json::to_string(&TagIdentifier {
2820 value: "thing".to_string(),
2821 info: Vec::new(),
2822 meta: Default::default(),
2823 })
2824 .unwrap();
2825 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2826 assert_eq!(
2827 data,
2828 crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2829 value: "thing".to_string(),
2830 info: Vec::new(),
2831 meta: Default::default()
2832 }))
2833 );
2834
2835 str_json = "\"END\"".to_string();
2836 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2837 assert_eq!(
2838 data,
2839 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2840 );
2841
2842 str_json = "\"start\"".to_string();
2843 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2844 assert_eq!(
2845 data,
2846 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2847 );
2848
2849 str_json = "\"START\"".to_string();
2850 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2851 assert_eq!(
2852 data,
2853 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2854 );
2855 }
2856
2857 #[test]
2858 fn test_circle_center() {
2859 let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2860 assert_eq!(actual[0], 5.0);
2861 assert_eq!(actual[1], 0.0);
2862 }
2863}