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