kcl_lib/std/
sketch.rs

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