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