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