kcl_lib/std/
sketch.rs

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