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