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