kcl_lib/std/
sketch.rs

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