kcl_lib/std/
sketch.rs

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