kcl_lib/std/
sketch.rs

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