kcl_lib/std/
sketch.rs

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