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