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