kcl_lib/std/
sketch.rs

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