kcl_lib/std/
sketch.rs

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