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