kcl_lib/std/
sketch.rs

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