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, 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, CodeRef, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d,
18        Sketch, SketchSet, SketchSurface, Solid, StartSketchOnFace, StartSketchOnPlane, 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/// Data for start sketch on.
898/// You can start a sketch on a plane or an solid.
899#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
900#[ts(export)]
901#[serde(rename_all = "camelCase", untagged)]
902#[allow(clippy::large_enum_variant)]
903pub enum SketchData {
904    PlaneOrientation(PlaneData),
905    Plane(Box<Plane>),
906    Solid(Box<Solid>),
907}
908
909/// Orientation data that can be used to construct a plane, not a plane in itself.
910#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
911#[ts(export)]
912#[serde(rename_all = "camelCase")]
913#[allow(clippy::large_enum_variant)]
914pub enum PlaneData {
915    /// The XY plane.
916    #[serde(rename = "XY", alias = "xy")]
917    XY,
918    /// The opposite side of the XY plane.
919    #[serde(rename = "-XY", alias = "-xy")]
920    NegXY,
921    /// The XZ plane.
922    #[serde(rename = "XZ", alias = "xz")]
923    XZ,
924    /// The opposite side of the XZ plane.
925    #[serde(rename = "-XZ", alias = "-xz")]
926    NegXZ,
927    /// The YZ plane.
928    #[serde(rename = "YZ", alias = "yz")]
929    YZ,
930    /// The opposite side of the YZ plane.
931    #[serde(rename = "-YZ", alias = "-yz")]
932    NegYZ,
933    /// A defined plane.
934    Plane {
935        /// Origin of the plane.
936        origin: Point3d,
937        /// What should the plane’s X axis be?
938        #[serde(rename = "xAxis")]
939        x_axis: Point3d,
940        /// What should the plane’s Y axis be?
941        #[serde(rename = "yAxis")]
942        y_axis: Point3d,
943        /// The z-axis (normal).
944        #[serde(rename = "zAxis")]
945        z_axis: Point3d,
946    },
947}
948
949/// Start a sketch on a specific plane or face.
950pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
951    let (data, tag): (SketchData, Option<FaceTag>) = args.get_data_and_optional_tag()?;
952
953    match inner_start_sketch_on(data, tag, exec_state, &args).await? {
954        SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
955        SketchSurface::Face(value) => Ok(KclValue::Face { value }),
956    }
957}
958
959/// Start a new 2-dimensional sketch on a specific plane or face.
960///
961/// ### Sketch on Face Behavior
962///
963/// There are some important behaviors to understand when sketching on a face:
964///
965/// The resulting sketch will _include_ the face and thus Solid
966/// that was sketched on. So say you were to export the resulting Sketch / Solid
967/// from a sketch on a face, you would get both the artifact of the sketch
968/// on the face and the parent face / Solid itself.
969///
970/// This is important to understand because if you were to then sketch on the
971/// resulting Solid, it would again include the face and parent Solid that was
972/// sketched on. This could go on indefinitely.
973///
974/// The point is if you want to export the result of a sketch on a face, you
975/// only need to export the final Solid that was created from the sketch on the
976/// face, since it will include all the parent faces and Solids.
977///
978///
979/// ```no_run
980/// exampleSketch = startSketchOn(XY)
981///   |> startProfileAt([0, 0], %)
982///   |> line(end = [10, 0])
983///   |> line(end = [0, 10])
984///   |> line(end = [-10, 0])
985///   |> close()
986///
987/// example = extrude(exampleSketch, length = 5)
988///
989/// exampleSketch002 = startSketchOn(example, 'end')
990///   |> startProfileAt([1, 1], %)
991///   |> line(end = [8, 0])
992///   |> line(end = [0, 8])
993///   |> line(end = [-8, 0])
994///   |> close()
995///
996/// example002 = extrude(exampleSketch002, length = 5)
997///
998/// exampleSketch003 = startSketchOn(example002, 'end')
999///   |> startProfileAt([2, 2], %)
1000///   |> line(end = [6, 0])
1001///   |> line(end = [0, 6])
1002///   |> line(end = [-6, 0])
1003///   |> close()
1004///
1005/// example003 = extrude(exampleSketch003, length = 5)
1006/// ```
1007///
1008/// ```no_run
1009/// exampleSketch = startSketchOn(XY)
1010///   |> startProfileAt([0, 0], %)
1011///   |> line(end = [10, 0])
1012///   |> line(end = [0, 10], tag = $sketchingFace)
1013///   |> line(end = [-10, 0])
1014///   |> close()
1015///
1016/// example = extrude(exampleSketch, length = 10)
1017///
1018/// exampleSketch002 = startSketchOn(example, sketchingFace)
1019///   |> startProfileAt([1, 1], %)
1020///   |> line(end = [8, 0])
1021///   |> line(end = [0, 8])
1022///   |> line(end = [-8, 0])
1023///   |> close(tag = $sketchingFace002)
1024///
1025/// example002 = extrude(exampleSketch002, length = 10)
1026///
1027/// exampleSketch003 = startSketchOn(example002, sketchingFace002)
1028///   |> startProfileAt([-8, 12], %)
1029///   |> line(end = [0, 6])
1030///   |> line(end = [6, 0])
1031///   |> line(end = [0, -6])
1032///   |> close()
1033///
1034/// example003 = extrude(exampleSketch003, length = 5)
1035/// ```
1036///
1037/// ```no_run
1038/// exampleSketch = startSketchOn(XY)
1039///   |> startProfileAt([4, 12], %)
1040///   |> line(end = [2, 0])
1041///   |> line(end = [0, -6])
1042///   |> line(end = [4, -6])
1043///   |> line(end = [0, -6])
1044///   |> line(end = [-3.75, -4.5])
1045///   |> line(end = [0, -5.5])
1046///   |> line(end = [-2, 0])
1047///   |> close()
1048///
1049/// example = revolve({ axis: 'y', angle: 180 }, exampleSketch)
1050///
1051/// exampleSketch002 = startSketchOn(example, 'end')
1052///   |> startProfileAt([4.5, -5], %)
1053///   |> line(end = [0, 5])
1054///   |> line(end = [5, 0])
1055///   |> line(end = [0, -5])
1056///   |> close()
1057///
1058/// example002 = extrude(exampleSketch002, length = 5)
1059/// ```
1060///
1061/// ```no_run
1062/// a1 = startSketchOn({
1063///       plane: {
1064///         origin = { x = 0, y = 0, z = 0 },
1065///         xAxis = { x = 1, y = 0, z = 0 },
1066///         yAxis = { x = 0, y = 1, z = 0 },
1067///         zAxis = { x = 0, y = 0, z = 1 }
1068///       }
1069///     })
1070///  |> startProfileAt([0, 0], %)
1071///  |> line(end = [100.0, 0])
1072///  |> yLine(-100.0, %)
1073///  |> xLine(-100.0, %)
1074///  |> yLine(100.0, %)
1075///  |> close()
1076///  |> extrude(length = 3.14)
1077/// ```
1078#[stdlib {
1079    name = "startSketchOn",
1080    feature_tree_operation = true,
1081}]
1082async fn inner_start_sketch_on(
1083    data: SketchData,
1084    tag: Option<FaceTag>,
1085    exec_state: &mut ExecState,
1086    args: &Args,
1087) -> Result<SketchSurface, KclError> {
1088    match data {
1089        SketchData::PlaneOrientation(plane_data) => {
1090            let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
1091            Ok(SketchSurface::Plane(plane))
1092        }
1093        SketchData::Plane(plane) => {
1094            if plane.value == crate::exec::PlaneType::Uninit {
1095                let plane = make_sketch_plane_from_orientation(plane.into_plane_data(), exec_state, args).await?;
1096                Ok(SketchSurface::Plane(plane))
1097            } else {
1098                // Create artifact used only by the UI, not the engine.
1099                let id = exec_state.next_uuid();
1100                exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
1101                    id: ArtifactId::from(id),
1102                    plane_id: plane.artifact_id,
1103                    code_ref: CodeRef::placeholder(args.source_range),
1104                }));
1105
1106                Ok(SketchSurface::Plane(plane))
1107            }
1108        }
1109        SketchData::Solid(solid) => {
1110            let Some(tag) = tag else {
1111                return Err(KclError::Type(KclErrorDetails {
1112                    message: "Expected a tag for the face to sketch on".to_string(),
1113                    source_ranges: vec![args.source_range],
1114                }));
1115            };
1116            let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
1117
1118            // Create artifact used only by the UI, not the engine.
1119            let id = exec_state.next_uuid();
1120            exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
1121                id: ArtifactId::from(id),
1122                face_id: face.artifact_id,
1123                code_ref: CodeRef::placeholder(args.source_range),
1124            }));
1125
1126            Ok(SketchSurface::Face(face))
1127        }
1128    }
1129}
1130
1131async fn start_sketch_on_face(
1132    solid: Box<Solid>,
1133    tag: FaceTag,
1134    exec_state: &mut ExecState,
1135    args: &Args,
1136) -> Result<Box<Face>, KclError> {
1137    let extrude_plane_id = tag.get_face_id(&solid, exec_state, args, true).await?;
1138
1139    Ok(Box::new(Face {
1140        id: extrude_plane_id,
1141        artifact_id: extrude_plane_id.into(),
1142        value: tag.to_string(),
1143        // TODO: get this from the extrude plane data.
1144        x_axis: solid.sketch.on.x_axis(),
1145        y_axis: solid.sketch.on.y_axis(),
1146        z_axis: solid.sketch.on.z_axis(),
1147        units: solid.units,
1148        solid,
1149        meta: vec![args.source_range.into()],
1150    }))
1151}
1152
1153async fn make_sketch_plane_from_orientation(
1154    data: PlaneData,
1155    exec_state: &mut ExecState,
1156    args: &Args,
1157) -> Result<Box<Plane>, KclError> {
1158    let plane = Plane::from_plane_data(data.clone(), exec_state);
1159
1160    // Create the plane on the fly.
1161    let clobber = false;
1162    let size = LengthUnit(60.0);
1163    let hide = Some(true);
1164    match data {
1165        PlaneData::XY | PlaneData::NegXY | PlaneData::XZ | PlaneData::NegXZ | PlaneData::YZ | PlaneData::NegYZ => {
1166            let x_axis = match data {
1167                PlaneData::NegXY => Point3d::new(-1.0, 0.0, 0.0),
1168                PlaneData::NegXZ => Point3d::new(-1.0, 0.0, 0.0),
1169                PlaneData::NegYZ => Point3d::new(0.0, -1.0, 0.0),
1170                _ => plane.x_axis,
1171            };
1172            args.batch_modeling_cmd(
1173                plane.id,
1174                ModelingCmd::from(mcmd::MakePlane {
1175                    clobber,
1176                    origin: plane.origin.into(),
1177                    size,
1178                    x_axis: x_axis.into(),
1179                    y_axis: plane.y_axis.into(),
1180                    hide,
1181                }),
1182            )
1183            .await?;
1184        }
1185        PlaneData::Plane {
1186            origin,
1187            x_axis,
1188            y_axis,
1189            z_axis: _,
1190        } => {
1191            args.batch_modeling_cmd(
1192                plane.id,
1193                ModelingCmd::from(mcmd::MakePlane {
1194                    clobber,
1195                    origin: origin.into(),
1196                    size,
1197                    x_axis: x_axis.into(),
1198                    y_axis: y_axis.into(),
1199                    hide,
1200                }),
1201            )
1202            .await?;
1203        }
1204    }
1205
1206    Ok(Box::new(plane))
1207}
1208
1209/// Start a new profile at a given point.
1210pub async fn start_profile_at(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1211    let (start, sketch_surface, tag): ([f64; 2], SketchSurface, Option<TagNode>) =
1212        args.get_data_and_sketch_surface()?;
1213
1214    let sketch = inner_start_profile_at(start, sketch_surface, tag, exec_state, args).await?;
1215    Ok(KclValue::Sketch {
1216        value: Box::new(sketch),
1217    })
1218}
1219
1220/// Start a new profile at a given point.
1221///
1222/// ```no_run
1223/// exampleSketch = startSketchOn(XZ)
1224///   |> startProfileAt([0, 0], %)
1225///   |> line(end = [10, 0])
1226///   |> line(end = [0, 10])
1227///   |> line(end = [-10, 0])
1228///   |> close()
1229///
1230/// example = extrude(exampleSketch, length = 5)
1231/// ```
1232///
1233/// ```no_run
1234/// exampleSketch = startSketchOn(-XZ)
1235///   |> startProfileAt([10, 10], %)
1236///   |> line(end = [10, 0])
1237///   |> line(end = [0, 10])
1238///   |> line(end = [-10, 0])
1239///   |> close()
1240///
1241/// example = extrude(exampleSketch, length = 5)
1242/// ```
1243///
1244/// ```no_run
1245/// exampleSketch = startSketchOn(-XZ)
1246///   |> startProfileAt([-10, 23], %)
1247///   |> line(end = [10, 0])
1248///   |> line(end = [0, 10])
1249///   |> line(end = [-10, 0])
1250///   |> close()
1251///
1252/// example = extrude(exampleSketch, length = 5)
1253/// ```
1254#[stdlib {
1255    name = "startProfileAt",
1256}]
1257pub(crate) async fn inner_start_profile_at(
1258    to: [f64; 2],
1259    sketch_surface: SketchSurface,
1260    tag: Option<TagNode>,
1261    exec_state: &mut ExecState,
1262    args: Args,
1263) -> Result<Sketch, KclError> {
1264    match &sketch_surface {
1265        SketchSurface::Face(face) => {
1266            // Flush the batch for our fillets/chamfers if there are any.
1267            // If we do not do these for sketch on face, things will fail with face does not exist.
1268            args.flush_batch_for_solid_set(exec_state, face.solid.clone().into())
1269                .await?;
1270        }
1271        SketchSurface::Plane(plane) if !plane.is_standard() => {
1272            // Hide whatever plane we are sketching on.
1273            // This is especially helpful for offset planes, which would be visible otherwise.
1274            args.batch_end_cmd(
1275                exec_state.next_uuid(),
1276                ModelingCmd::from(mcmd::ObjectVisible {
1277                    object_id: plane.id,
1278                    hidden: true,
1279                }),
1280            )
1281            .await?;
1282        }
1283        _ => {}
1284    }
1285
1286    // Enter sketch mode on the surface.
1287    // We call this here so you can reuse the sketch surface for multiple sketches.
1288    let id = exec_state.next_uuid();
1289    args.batch_modeling_cmd(
1290        id,
1291        ModelingCmd::from(mcmd::EnableSketchMode {
1292            animated: false,
1293            ortho: false,
1294            entity_id: sketch_surface.id(),
1295            adjust_camera: false,
1296            planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
1297                // We pass in the normal for the plane here.
1298                Some(plane.z_axis.into())
1299            } else {
1300                None
1301            },
1302        }),
1303    )
1304    .await?;
1305
1306    let id = exec_state.next_uuid();
1307    let path_id = exec_state.next_uuid();
1308
1309    args.batch_modeling_cmd(path_id, ModelingCmd::from(mcmd::StartPath::default()))
1310        .await?;
1311    args.batch_modeling_cmd(
1312        id,
1313        ModelingCmd::from(mcmd::MovePathPen {
1314            path: path_id.into(),
1315            to: KPoint2d::from(to).with_z(0.0).map(LengthUnit),
1316        }),
1317    )
1318    .await?;
1319
1320    let current_path = BasePath {
1321        from: to,
1322        to,
1323        tag: tag.clone(),
1324        units: sketch_surface.units(),
1325        geo_meta: GeoMeta {
1326            id,
1327            metadata: args.source_range.into(),
1328        },
1329    };
1330
1331    let sketch = Sketch {
1332        id: path_id,
1333        original_id: path_id,
1334        artifact_id: path_id.into(),
1335        on: sketch_surface.clone(),
1336        paths: vec![],
1337        units: sketch_surface.units(),
1338        meta: vec![args.source_range.into()],
1339        tags: if let Some(tag) = &tag {
1340            let mut tag_identifier: TagIdentifier = tag.into();
1341            tag_identifier.info = Some(TagEngineInfo {
1342                id: current_path.geo_meta.id,
1343                sketch: path_id,
1344                path: Some(Path::Base {
1345                    base: current_path.clone(),
1346                }),
1347                surface: None,
1348            });
1349            IndexMap::from([(tag.name.to_string(), tag_identifier)])
1350        } else {
1351            Default::default()
1352        },
1353        start: current_path,
1354    };
1355    Ok(sketch)
1356}
1357
1358/// Returns the X component of the sketch profile start point.
1359pub async fn profile_start_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1360    let sketch: Sketch = args.get_sketch()?;
1361    let ty = sketch.units.into();
1362    let x = inner_profile_start_x(sketch)?;
1363    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1364}
1365
1366/// Extract the provided 2-dimensional sketch's profile's origin's 'x'
1367/// value.
1368///
1369/// ```no_run
1370/// sketch001 = startSketchOn(XY)
1371///  |> startProfileAt([5, 2], %)
1372///  |> angledLine([-26.6, 50], %)
1373///  |> angledLine([90, 50], %)
1374///  |> angledLineToX({ angle = 30, to = profileStartX(%) }, %)
1375/// ```
1376#[stdlib {
1377    name = "profileStartX"
1378}]
1379pub(crate) fn inner_profile_start_x(sketch: Sketch) -> Result<f64, KclError> {
1380    Ok(sketch.start.to[0])
1381}
1382
1383/// Returns the Y component of the sketch profile start point.
1384pub async fn profile_start_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1385    let sketch: Sketch = args.get_sketch()?;
1386    let ty = sketch.units.into();
1387    let x = inner_profile_start_y(sketch)?;
1388    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1389}
1390
1391/// Extract the provided 2-dimensional sketch's profile's origin's 'y'
1392/// value.
1393///
1394/// ```no_run
1395/// sketch001 = startSketchOn(XY)
1396///  |> startProfileAt([5, 2], %)
1397///  |> angledLine({ angle = -60, length = 14 }, %)
1398///  |> angledLineToY({ angle = 30, to = profileStartY(%) }, %)
1399/// ```
1400#[stdlib {
1401    name = "profileStartY"
1402}]
1403pub(crate) fn inner_profile_start_y(sketch: Sketch) -> Result<f64, KclError> {
1404    Ok(sketch.start.to[1])
1405}
1406
1407/// Returns the sketch profile start point.
1408pub async fn profile_start(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1409    let sketch: Sketch = args.get_sketch()?;
1410    let ty = sketch.units.into();
1411    let point = inner_profile_start(sketch)?;
1412    Ok(KclValue::from_point2d(point, ty, args.into()))
1413}
1414
1415/// Extract the provided 2-dimensional sketch's profile's origin
1416/// value.
1417///
1418/// ```no_run
1419/// sketch001 = startSketchOn(XY)
1420///  |> startProfileAt([5, 2], %)
1421///  |> angledLine({ angle = 120, length = 50 }, %, $seg01)
1422///  |> angledLine({ angle = segAng(seg01) + 120, length = 50 }, %)
1423///  |> line(end = profileStart(%))
1424///  |> close()
1425///  |> extrude(length = 20)
1426/// ```
1427#[stdlib {
1428    name = "profileStart"
1429}]
1430pub(crate) fn inner_profile_start(sketch: Sketch) -> Result<[f64; 2], KclError> {
1431    Ok(sketch.start.to)
1432}
1433
1434/// Close the current sketch.
1435pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1436    let sketch = args.get_unlabeled_kw_arg("sketch")?;
1437    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1438    let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1439    Ok(KclValue::Sketch {
1440        value: Box::new(new_sketch),
1441    })
1442}
1443
1444/// Construct a line segment from the current origin back to the profile's
1445/// origin, ensuring the resulting 2-dimensional sketch is not open-ended.
1446///
1447/// ```no_run
1448/// startSketchOn(XZ)
1449///    |> startProfileAt([0, 0], %)
1450///    |> line(end = [10, 10])
1451///    |> line(end = [10, 0])
1452///    |> close()
1453///    |> extrude(length = 10)
1454/// ```
1455///
1456/// ```no_run
1457/// exampleSketch = startSketchOn(-XZ)
1458///   |> startProfileAt([0, 0], %)
1459///   |> line(end = [10, 0])
1460///   |> line(end = [0, 10])
1461///   |> close()
1462///
1463/// example = extrude(exampleSketch, length = 10)
1464/// ```
1465#[stdlib {
1466    name = "close",
1467    keywords = true,
1468    unlabeled_first = true,
1469    args = {
1470        sketch = { docs = "The sketch you want to close"},
1471        tag = { docs = "Create a new tag which refers to this line"},
1472    }
1473}]
1474pub(crate) async fn inner_close(
1475    sketch: Sketch,
1476    tag: Option<TagNode>,
1477    exec_state: &mut ExecState,
1478    args: Args,
1479) -> Result<Sketch, KclError> {
1480    let from = sketch.current_pen_position()?;
1481    let to: Point2d = sketch.start.from.into();
1482
1483    let id = exec_state.next_uuid();
1484
1485    args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }))
1486        .await?;
1487
1488    // If we are sketching on a plane we can close the sketch now.
1489    if let SketchSurface::Plane(_) = sketch.on {
1490        // We were on a plane, disable the sketch mode.
1491        args.batch_modeling_cmd(
1492            exec_state.next_uuid(),
1493            ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1494        )
1495        .await?;
1496    }
1497
1498    let current_path = Path::ToPoint {
1499        base: BasePath {
1500            from: from.into(),
1501            to: to.into(),
1502            tag: tag.clone(),
1503            units: sketch.units,
1504            geo_meta: GeoMeta {
1505                id,
1506                metadata: args.source_range.into(),
1507            },
1508        },
1509    };
1510
1511    let mut new_sketch = sketch.clone();
1512    if let Some(tag) = &tag {
1513        new_sketch.add_tag(tag, &current_path);
1514    }
1515
1516    new_sketch.paths.push(current_path);
1517
1518    Ok(new_sketch)
1519}
1520
1521/// Data to draw an arc.
1522#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1523#[ts(export)]
1524#[serde(rename_all = "camelCase", untagged)]
1525pub enum ArcData {
1526    /// Angles and radius with an optional tag.
1527    AnglesAndRadius {
1528        /// The start angle.
1529        #[serde(rename = "angleStart")]
1530        #[schemars(range(min = -360.0, max = 360.0))]
1531        angle_start: f64,
1532        /// The end angle.
1533        #[serde(rename = "angleEnd")]
1534        #[schemars(range(min = -360.0, max = 360.0))]
1535        angle_end: f64,
1536        /// The radius.
1537        radius: f64,
1538    },
1539    /// Center, to and radius with an optional tag.
1540    CenterToRadius {
1541        /// The center.
1542        center: [f64; 2],
1543        /// The to point.
1544        to: [f64; 2],
1545        /// The radius.
1546        radius: f64,
1547    },
1548}
1549
1550/// Data to draw a three point arc (arcTo).
1551#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1552#[ts(export)]
1553#[serde(rename_all = "camelCase")]
1554pub struct ArcToData {
1555    /// End point of the arc. A point in 3D space
1556    pub end: [f64; 2],
1557    /// Interior point of the arc. A point in 3D space
1558    pub interior: [f64; 2],
1559}
1560
1561/// Draw an arc.
1562pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1563    let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
1564
1565    let new_sketch = inner_arc(data, sketch, tag, exec_state, args).await?;
1566    Ok(KclValue::Sketch {
1567        value: Box::new(new_sketch),
1568    })
1569}
1570
1571/// Draw a curved line segment along an imaginary circle.
1572///
1573/// The arc is constructed such that the current position of the sketch is
1574/// placed along an imaginary circle of the specified radius, at angleStart
1575/// degrees. The resulting arc is the segment of the imaginary circle from
1576/// that origin point to angleEnd, radius away from the center of the imaginary
1577/// circle.
1578///
1579/// Unless this makes a lot of sense and feels like what you're looking
1580/// for to construct your shape, you're likely looking for tangentialArc.
1581///
1582/// ```no_run
1583/// exampleSketch = startSketchOn(XZ)
1584///   |> startProfileAt([0, 0], %)
1585///   |> line(end = [10, 0])
1586///   |> arc({
1587///        angleStart = 0,
1588///        angleEnd = 280,
1589///        radius = 16
1590///      }, %)
1591///   |> close()
1592/// example = extrude(exampleSketch, length = 10)
1593/// ```
1594#[stdlib {
1595    name = "arc",
1596}]
1597pub(crate) async fn inner_arc(
1598    data: ArcData,
1599    sketch: Sketch,
1600    tag: Option<TagNode>,
1601    exec_state: &mut ExecState,
1602    args: Args,
1603) -> Result<Sketch, KclError> {
1604    let from: Point2d = sketch.current_pen_position()?;
1605
1606    let (center, angle_start, angle_end, radius, end) = match &data {
1607        ArcData::AnglesAndRadius {
1608            angle_start,
1609            angle_end,
1610            radius,
1611        } => {
1612            let a_start = Angle::from_degrees(*angle_start);
1613            let a_end = Angle::from_degrees(*angle_end);
1614            let (center, end) = arc_center_and_end(from, a_start, a_end, *radius);
1615            (center, a_start, a_end, *radius, end)
1616        }
1617        ArcData::CenterToRadius { center, to, radius } => {
1618            let (angle_start, angle_end) = arc_angles(from, to.into(), center.into(), *radius, args.source_range)?;
1619            (center.into(), angle_start, angle_end, *radius, to.into())
1620        }
1621    };
1622
1623    if angle_start == angle_end {
1624        return Err(KclError::Type(KclErrorDetails {
1625            message: "Arc start and end angles must be different".to_string(),
1626            source_ranges: vec![args.source_range],
1627        }));
1628    }
1629    let ccw = angle_start < angle_end;
1630
1631    let id = exec_state.next_uuid();
1632
1633    args.batch_modeling_cmd(
1634        id,
1635        ModelingCmd::from(mcmd::ExtendPath {
1636            path: sketch.id.into(),
1637            segment: PathSegment::Arc {
1638                start: angle_start,
1639                end: angle_end,
1640                center: KPoint2d::from(center).map(LengthUnit),
1641                radius: LengthUnit(radius),
1642                relative: false,
1643            },
1644        }),
1645    )
1646    .await?;
1647
1648    let current_path = Path::Arc {
1649        base: BasePath {
1650            from: from.into(),
1651            to: end.into(),
1652            tag: tag.clone(),
1653            units: sketch.units,
1654            geo_meta: GeoMeta {
1655                id,
1656                metadata: args.source_range.into(),
1657            },
1658        },
1659        center: center.into(),
1660        radius,
1661        ccw,
1662    };
1663
1664    let mut new_sketch = sketch.clone();
1665    if let Some(tag) = &tag {
1666        new_sketch.add_tag(tag, &current_path);
1667    }
1668
1669    new_sketch.paths.push(current_path);
1670
1671    Ok(new_sketch)
1672}
1673
1674/// Draw a three point arc.
1675pub async fn arc_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1676    let (data, sketch, tag): (ArcToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
1677
1678    let new_sketch = inner_arc_to(data, sketch, tag, exec_state, args).await?;
1679    Ok(KclValue::Sketch {
1680        value: Box::new(new_sketch),
1681    })
1682}
1683
1684/// Draw a three point arc.
1685///
1686/// 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.
1687/// 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
1688/// the start and end.
1689///
1690/// ```no_run
1691/// exampleSketch = startSketchOn(XZ)
1692///   |> startProfileAt([0, 0], %)
1693///   |> arcTo({
1694///         end = [10,0],
1695///         interior = [5,5]
1696///      }, %)
1697///   |> close()
1698/// example = extrude(exampleSketch, length = 10)
1699/// ```
1700#[stdlib {
1701    name = "arcTo",
1702}]
1703pub(crate) async fn inner_arc_to(
1704    data: ArcToData,
1705    sketch: Sketch,
1706    tag: Option<TagNode>,
1707    exec_state: &mut ExecState,
1708    args: Args,
1709) -> Result<Sketch, KclError> {
1710    let from: Point2d = sketch.current_pen_position()?;
1711    let id = exec_state.next_uuid();
1712
1713    // The start point is taken from the path you are extending.
1714    args.batch_modeling_cmd(
1715        id,
1716        ModelingCmd::from(mcmd::ExtendPath {
1717            path: sketch.id.into(),
1718            segment: PathSegment::ArcTo {
1719                end: kcmc::shared::Point3d {
1720                    x: LengthUnit(data.end[0]),
1721                    y: LengthUnit(data.end[1]),
1722                    z: LengthUnit(0.0),
1723                },
1724                interior: kcmc::shared::Point3d {
1725                    x: LengthUnit(data.interior[0]),
1726                    y: LengthUnit(data.interior[1]),
1727                    z: LengthUnit(0.0),
1728                },
1729                relative: false,
1730            },
1731        }),
1732    )
1733    .await?;
1734
1735    let start = [from.x, from.y];
1736    let interior = data.interior;
1737    let end = data.end;
1738
1739    // compute the center of the circle since we do not have the value returned from the engine
1740    let center = calculate_circle_center(start, interior, end);
1741
1742    // compute the radius since we do not have the value returned from the engine
1743    // Pick any of the 3 points since they all lie along the circle
1744    let sum_of_square_differences =
1745        (center[0] - start[0] * center[0] - start[0]) + (center[1] - start[1] * center[1] - start[1]);
1746    let radius = sum_of_square_differences.sqrt();
1747
1748    let ccw = is_ccw(start, interior, end);
1749
1750    let current_path = Path::Arc {
1751        base: BasePath {
1752            from: from.into(),
1753            to: data.end,
1754            tag: tag.clone(),
1755            units: sketch.units,
1756            geo_meta: GeoMeta {
1757                id,
1758                metadata: args.source_range.into(),
1759            },
1760        },
1761        center,
1762        radius,
1763        ccw,
1764    };
1765
1766    let mut new_sketch = sketch.clone();
1767    if let Some(tag) = &tag {
1768        new_sketch.add_tag(tag, &current_path);
1769    }
1770
1771    new_sketch.paths.push(current_path);
1772
1773    Ok(new_sketch)
1774}
1775
1776/// Returns true if the three-point arc is counterclockwise.  The order of
1777/// parameters is critical.
1778///
1779/// |   end
1780/// |  /
1781/// |  |    / interior
1782/// |  /  /
1783/// | | /
1784/// |/_____________
1785/// start
1786///
1787/// If the slope of the line from start to interior is less than the slope of
1788/// the line from start to end, the arc is counterclockwise.
1789fn is_ccw(start: [f64; 2], interior: [f64; 2], end: [f64; 2]) -> bool {
1790    let t1 = (interior[0] - start[0]) * (end[1] - start[1]);
1791    let t2 = (end[0] - start[0]) * (interior[1] - start[1]);
1792    // If these terms are equal, the points are collinear.
1793    t1 > t2
1794}
1795
1796/// Data to draw a tangential arc.
1797#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
1798#[ts(export)]
1799#[serde(rename_all = "camelCase", untagged)]
1800pub enum TangentialArcData {
1801    RadiusAndOffset {
1802        /// Radius of the arc.
1803        /// Not to be confused with Raiders of the Lost Ark.
1804        radius: f64,
1805        /// Offset of the arc, in degrees.
1806        offset: f64,
1807    },
1808}
1809
1810/// Draw a tangential arc.
1811pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1812    let (data, sketch, tag): (TangentialArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
1813
1814    let new_sketch = inner_tangential_arc(data, sketch, tag, exec_state, args).await?;
1815    Ok(KclValue::Sketch {
1816        value: Box::new(new_sketch),
1817    })
1818}
1819
1820/// Draw a curved line segment along part of an imaginary circle.
1821///
1822/// The arc is constructed such that the last line segment is placed tangent
1823/// to the imaginary circle of the specified radius. The resulting arc is the
1824/// segment of the imaginary circle from that tangent point for 'offset'
1825/// degrees along the imaginary circle.
1826///
1827/// ```no_run
1828/// exampleSketch = startSketchOn(XZ)
1829///   |> startProfileAt([0, 0], %)
1830///   |> angledLine({
1831///     angle = 60,
1832///     length = 10,
1833///   }, %)
1834///   |> tangentialArc({ radius = 10, offset = -120 }, %)
1835///   |> angledLine({
1836///     angle = -60,
1837///     length = 10,
1838///   }, %)
1839///   |> close()
1840///
1841/// example = extrude(exampleSketch, length = 10)
1842/// ```
1843#[stdlib {
1844    name = "tangentialArc",
1845}]
1846async fn inner_tangential_arc(
1847    data: TangentialArcData,
1848    sketch: Sketch,
1849    tag: Option<TagNode>,
1850    exec_state: &mut ExecState,
1851    args: Args,
1852) -> Result<Sketch, KclError> {
1853    let from: Point2d = sketch.current_pen_position()?;
1854    // next set of lines is some undocumented voodoo from get_tangential_arc_to_info
1855    let tangent_info = sketch.get_tangential_info_from_paths(); //this function desperately needs some documentation
1856    let tan_previous_point = tangent_info.tan_previous_point(from.into());
1857
1858    let id = exec_state.next_uuid();
1859
1860    let (center, to, ccw) = match data {
1861        TangentialArcData::RadiusAndOffset { radius, offset } => {
1862            // KCL stdlib types use degrees.
1863            let offset = Angle::from_degrees(offset);
1864
1865            // Calculate the end point from the angle and radius.
1866            // atan2 outputs radians.
1867            let previous_end_tangent = Angle::from_radians(f64::atan2(
1868                from.y - tan_previous_point[1],
1869                from.x - tan_previous_point[0],
1870            ));
1871            // make sure the arc center is on the correct side to guarantee deterministic behavior
1872            // note the engine automatically rejects an offset of zero, if we want to flag that at KCL too to avoid engine errors
1873            let ccw = offset.to_degrees() > 0.0;
1874            let tangent_to_arc_start_angle = if ccw {
1875                // CCW turn
1876                Angle::from_degrees(-90.0)
1877            } else {
1878                // CW turn
1879                Angle::from_degrees(90.0)
1880            };
1881            // may need some logic and / or modulo on the various angle values to prevent them from going "backwards"
1882            // but the above logic *should* capture that behavior
1883            let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1884            let end_angle = start_angle + offset;
1885            let (center, to) = arc_center_and_end(from, start_angle, end_angle, radius);
1886
1887            args.batch_modeling_cmd(
1888                id,
1889                ModelingCmd::from(mcmd::ExtendPath {
1890                    path: sketch.id.into(),
1891                    segment: PathSegment::TangentialArc {
1892                        radius: LengthUnit(radius),
1893                        offset,
1894                    },
1895                }),
1896            )
1897            .await?;
1898            (center, to.into(), ccw)
1899        }
1900    };
1901
1902    let current_path = Path::TangentialArc {
1903        ccw,
1904        center: center.into(),
1905        base: BasePath {
1906            from: from.into(),
1907            to,
1908            tag: tag.clone(),
1909            units: sketch.units,
1910            geo_meta: GeoMeta {
1911                id,
1912                metadata: args.source_range.into(),
1913            },
1914        },
1915    };
1916
1917    let mut new_sketch = sketch.clone();
1918    if let Some(tag) = &tag {
1919        new_sketch.add_tag(tag, &current_path);
1920    }
1921
1922    new_sketch.paths.push(current_path);
1923
1924    Ok(new_sketch)
1925}
1926
1927fn tan_arc_to(sketch: &Sketch, to: &[f64; 2]) -> ModelingCmd {
1928    ModelingCmd::from(mcmd::ExtendPath {
1929        path: sketch.id.into(),
1930        segment: PathSegment::TangentialArcTo {
1931            angle_snap_increment: None,
1932            to: KPoint2d::from(*to).with_z(0.0).map(LengthUnit),
1933        },
1934    })
1935}
1936
1937/// Draw a tangential arc to a specific point.
1938pub async fn tangential_arc_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1939    let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = super::args::FromArgs::from_args(&args, 0)?;
1940
1941    let new_sketch = inner_tangential_arc_to(to, sketch, tag, exec_state, args).await?;
1942    Ok(KclValue::Sketch {
1943        value: Box::new(new_sketch),
1944    })
1945}
1946
1947/// Draw a tangential arc to point some distance away..
1948pub async fn tangential_arc_to_relative(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1949    let (delta, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = super::args::FromArgs::from_args(&args, 0)?;
1950
1951    let new_sketch = inner_tangential_arc_to_relative(delta, sketch, tag, exec_state, args).await?;
1952    Ok(KclValue::Sketch {
1953        value: Box::new(new_sketch),
1954    })
1955}
1956
1957/// Starting at the current sketch's origin, draw a curved line segment along
1958/// some part of an imaginary circle until it reaches the desired (x, y)
1959/// coordinates.
1960///
1961/// ```no_run
1962/// exampleSketch = startSketchOn(XZ)
1963///   |> startProfileAt([0, 0], %)
1964///   |> angledLine({
1965///     angle = 60,
1966///     length = 10,
1967///   }, %)
1968///   |> tangentialArcTo([15, 15], %)
1969///   |> line(end = [10, -15])
1970///   |> close()
1971///
1972/// example = extrude(exampleSketch, length = 10)
1973/// ```
1974#[stdlib {
1975    name = "tangentialArcTo",
1976}]
1977async fn inner_tangential_arc_to(
1978    to: [f64; 2],
1979    sketch: Sketch,
1980    tag: Option<TagNode>,
1981    exec_state: &mut ExecState,
1982    args: Args,
1983) -> Result<Sketch, KclError> {
1984    let from: Point2d = sketch.current_pen_position()?;
1985    let tangent_info = sketch.get_tangential_info_from_paths();
1986    let tan_previous_point = tangent_info.tan_previous_point(from.into());
1987    let [to_x, to_y] = to;
1988    let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1989        arc_start_point: [from.x, from.y],
1990        arc_end_point: to,
1991        tan_previous_point,
1992        obtuse: true,
1993    });
1994
1995    let delta = [to_x - from.x, to_y - from.y];
1996    let id = exec_state.next_uuid();
1997    args.batch_modeling_cmd(id, tan_arc_to(&sketch, &delta)).await?;
1998
1999    let current_path = Path::TangentialArcTo {
2000        base: BasePath {
2001            from: from.into(),
2002            to,
2003            tag: tag.clone(),
2004            units: sketch.units,
2005            geo_meta: GeoMeta {
2006                id,
2007                metadata: args.source_range.into(),
2008            },
2009        },
2010        center: result.center,
2011        ccw: result.ccw > 0,
2012    };
2013
2014    let mut new_sketch = sketch.clone();
2015    if let Some(tag) = &tag {
2016        new_sketch.add_tag(tag, &current_path);
2017    }
2018
2019    new_sketch.paths.push(current_path);
2020
2021    Ok(new_sketch)
2022}
2023
2024/// Starting at the current sketch's origin, draw a curved line segment along
2025/// some part of an imaginary circle until it reaches a point the given (x, y)
2026/// distance away.
2027///
2028/// ```no_run
2029/// exampleSketch = startSketchOn(XZ)
2030///   |> startProfileAt([0, 0], %)
2031///   |> angledLine({
2032///     angle = 45,
2033///     length = 10,
2034///   }, %)
2035///   |> tangentialArcToRelative([0, -10], %)
2036///   |> line(end = [-10, 0])
2037///   |> close()
2038///
2039/// example = extrude(exampleSketch, length = 10)
2040/// ```
2041#[stdlib {
2042    name = "tangentialArcToRelative",
2043}]
2044async fn inner_tangential_arc_to_relative(
2045    delta: [f64; 2],
2046    sketch: Sketch,
2047    tag: Option<TagNode>,
2048    exec_state: &mut ExecState,
2049    args: Args,
2050) -> Result<Sketch, KclError> {
2051    let from: Point2d = sketch.current_pen_position()?;
2052    let to = [from.x + delta[0], from.y + delta[1]];
2053    let tangent_info = sketch.get_tangential_info_from_paths();
2054    let tan_previous_point = tangent_info.tan_previous_point(from.into());
2055
2056    let [dx, dy] = delta;
2057    let result = get_tangential_arc_to_info(TangentialArcInfoInput {
2058        arc_start_point: [from.x, from.y],
2059        arc_end_point: [from.x + dx, from.y + dy],
2060        tan_previous_point,
2061        obtuse: true,
2062    });
2063
2064    if result.center[0].is_infinite() {
2065        return Err(KclError::Semantic(KclErrorDetails {
2066            source_ranges: vec![args.source_range],
2067            message:
2068                "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
2069                    .to_owned(),
2070        }));
2071    } else if result.center[1].is_infinite() {
2072        return Err(KclError::Semantic(KclErrorDetails {
2073            source_ranges: vec![args.source_range],
2074            message:
2075                "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
2076                    .to_owned(),
2077        }));
2078    }
2079
2080    let id = exec_state.next_uuid();
2081    args.batch_modeling_cmd(id, tan_arc_to(&sketch, &delta)).await?;
2082
2083    let current_path = Path::TangentialArcTo {
2084        base: BasePath {
2085            from: from.into(),
2086            to,
2087            tag: tag.clone(),
2088            units: sketch.units,
2089            geo_meta: GeoMeta {
2090                id,
2091                metadata: args.source_range.into(),
2092            },
2093        },
2094        center: result.center,
2095        ccw: result.ccw > 0,
2096    };
2097
2098    let mut new_sketch = sketch.clone();
2099    if let Some(tag) = &tag {
2100        new_sketch.add_tag(tag, &current_path);
2101    }
2102
2103    new_sketch.paths.push(current_path);
2104
2105    Ok(new_sketch)
2106}
2107
2108/// Data to draw a bezier curve.
2109#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
2110#[ts(export)]
2111#[serde(rename_all = "camelCase")]
2112pub struct BezierData {
2113    /// The to point.
2114    pub to: [f64; 2],
2115    /// The first control point.
2116    pub control1: [f64; 2],
2117    /// The second control point.
2118    pub control2: [f64; 2],
2119}
2120
2121/// Draw a bezier curve.
2122pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2123    let (data, sketch, tag): (BezierData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
2124
2125    let new_sketch = inner_bezier_curve(data, sketch, tag, exec_state, args).await?;
2126    Ok(KclValue::Sketch {
2127        value: Box::new(new_sketch),
2128    })
2129}
2130
2131/// Draw a smooth, continuous, curved line segment from the current origin to
2132/// the desired (x, y), using a number of control points to shape the curve's
2133/// shape.
2134///
2135/// ```no_run
2136/// exampleSketch = startSketchOn(XZ)
2137///   |> startProfileAt([0, 0], %)
2138///   |> line(end = [0, 10])
2139///   |> bezierCurve({
2140///        to = [10, 10],
2141///        control1 = [5, 0],
2142///        control2 = [5, 10]
2143///      }, %)
2144///   |> line(endAbsolute = [10, 0])
2145///   |> close()
2146///
2147/// example = extrude(exampleSketch, length = 10)
2148/// ```
2149#[stdlib {
2150    name = "bezierCurve",
2151}]
2152async fn inner_bezier_curve(
2153    data: BezierData,
2154    sketch: Sketch,
2155    tag: Option<TagNode>,
2156    exec_state: &mut ExecState,
2157    args: Args,
2158) -> Result<Sketch, KclError> {
2159    let from = sketch.current_pen_position()?;
2160
2161    let relative = true;
2162    let delta = data.to;
2163    let to = [from.x + data.to[0], from.y + data.to[1]];
2164
2165    let id = exec_state.next_uuid();
2166
2167    args.batch_modeling_cmd(
2168        id,
2169        ModelingCmd::from(mcmd::ExtendPath {
2170            path: sketch.id.into(),
2171            segment: PathSegment::Bezier {
2172                control1: KPoint2d::from(data.control1).with_z(0.0).map(LengthUnit),
2173                control2: KPoint2d::from(data.control2).with_z(0.0).map(LengthUnit),
2174                end: KPoint2d::from(delta).with_z(0.0).map(LengthUnit),
2175                relative,
2176            },
2177        }),
2178    )
2179    .await?;
2180
2181    let current_path = Path::ToPoint {
2182        base: BasePath {
2183            from: from.into(),
2184            to,
2185            tag: tag.clone(),
2186            units: sketch.units,
2187            geo_meta: GeoMeta {
2188                id,
2189                metadata: args.source_range.into(),
2190            },
2191        },
2192    };
2193
2194    let mut new_sketch = sketch.clone();
2195    if let Some(tag) = &tag {
2196        new_sketch.add_tag(tag, &current_path);
2197    }
2198
2199    new_sketch.paths.push(current_path);
2200
2201    Ok(new_sketch)
2202}
2203
2204/// Use a sketch to cut a hole in another sketch.
2205pub async fn hole(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2206    let (hole_sketch, sketch): (SketchSet, Sketch) = args.get_sketches()?;
2207
2208    let new_sketch = inner_hole(hole_sketch, sketch, exec_state, args).await?;
2209    Ok(KclValue::Sketch {
2210        value: Box::new(new_sketch),
2211    })
2212}
2213
2214/// Use a 2-dimensional sketch to cut a hole in another 2-dimensional sketch.
2215///
2216/// ```no_run
2217/// exampleSketch = startSketchOn(XY)
2218///   |> startProfileAt([0, 0], %)
2219///   |> line(end = [0, 5])
2220///   |> line(end = [5, 0])
2221///   |> line(end = [0, -5])
2222///   |> close()
2223///   |> hole(circle( center = [1, 1], radius = .25 ), %)
2224///   |> hole(circle( center = [1, 4], radius = .25 ), %)
2225///
2226/// example = extrude(exampleSketch, length = 1)
2227/// ```
2228///
2229/// ```no_run
2230/// fn squareHoleSketch() {
2231///   squareSketch = startSketchOn(-XZ)
2232///     |> startProfileAt([-1, -1], %)
2233///     |> line(end = [2, 0])
2234///     |> line(end = [0, 2])
2235///     |> line(end = [-2, 0])
2236///     |> close()
2237///   return squareSketch
2238/// }
2239///
2240/// exampleSketch = startSketchOn(-XZ)
2241///     |> circle( center = [0, 0], radius = 3 )
2242///     |> hole(squareHoleSketch(), %)
2243/// example = extrude(exampleSketch, length = 1)
2244/// ```
2245#[stdlib {
2246    name = "hole",
2247    feature_tree_operation = true,
2248}]
2249async fn inner_hole(
2250    hole_sketch: SketchSet,
2251    sketch: Sketch,
2252    exec_state: &mut ExecState,
2253    args: Args,
2254) -> Result<Sketch, KclError> {
2255    let hole_sketches: Vec<Sketch> = hole_sketch.into();
2256    for hole_sketch in hole_sketches {
2257        args.batch_modeling_cmd(
2258            exec_state.next_uuid(),
2259            ModelingCmd::from(mcmd::Solid2dAddHole {
2260                object_id: sketch.id,
2261                hole_id: hole_sketch.id,
2262            }),
2263        )
2264        .await?;
2265
2266        // suggestion (mike)
2267        // we also hide the source hole since its essentially "consumed" by this operation
2268        args.batch_modeling_cmd(
2269            exec_state.next_uuid(),
2270            ModelingCmd::from(mcmd::ObjectVisible {
2271                object_id: hole_sketch.id,
2272                hidden: true,
2273            }),
2274        )
2275        .await?;
2276    }
2277
2278    Ok(sketch)
2279}
2280
2281#[cfg(test)]
2282mod tests {
2283
2284    use pretty_assertions::assert_eq;
2285
2286    use crate::{
2287        execution::TagIdentifier,
2288        std::{sketch::PlaneData, utils::calculate_circle_center},
2289    };
2290
2291    #[test]
2292    fn test_deserialize_plane_data() {
2293        let data = PlaneData::XY;
2294        let mut str_json = serde_json::to_string(&data).unwrap();
2295        assert_eq!(str_json, "\"XY\"");
2296
2297        str_json = "\"YZ\"".to_string();
2298        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2299        assert_eq!(data, PlaneData::YZ);
2300
2301        str_json = "\"-YZ\"".to_string();
2302        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2303        assert_eq!(data, PlaneData::NegYZ);
2304
2305        str_json = "\"-xz\"".to_string();
2306        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2307        assert_eq!(data, PlaneData::NegXZ);
2308    }
2309
2310    #[test]
2311    fn test_deserialize_sketch_on_face_tag() {
2312        let data = "start";
2313        let mut str_json = serde_json::to_string(&data).unwrap();
2314        assert_eq!(str_json, "\"start\"");
2315
2316        str_json = "\"end\"".to_string();
2317        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2318        assert_eq!(
2319            data,
2320            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2321        );
2322
2323        str_json = serde_json::to_string(&TagIdentifier {
2324            value: "thing".to_string(),
2325            info: None,
2326            meta: Default::default(),
2327        })
2328        .unwrap();
2329        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2330        assert_eq!(
2331            data,
2332            crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2333                value: "thing".to_string(),
2334                info: None,
2335                meta: Default::default()
2336            }))
2337        );
2338
2339        str_json = "\"END\"".to_string();
2340        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2341        assert_eq!(
2342            data,
2343            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2344        );
2345
2346        str_json = "\"start\"".to_string();
2347        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2348        assert_eq!(
2349            data,
2350            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2351        );
2352
2353        str_json = "\"START\"".to_string();
2354        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2355        assert_eq!(
2356            data,
2357            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2358        );
2359    }
2360
2361    #[test]
2362    fn test_circle_center() {
2363        let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2364        assert_eq!(actual[0], 5.0);
2365        assert_eq!(actual[1], 0.0);
2366    }
2367}