Skip to main content

kcl_lib/std/
sketch.rs

1//! Functions related to sketching.
2
3use std::f64;
4
5use anyhow::Result;
6use kcl_error::SourceRange;
7use kcmc::shared::Point2d as KPoint2d; // Point2d is already defined in this pkg, to impl ts_rs traits.
8use kcmc::shared::Point3d as KPoint3d; // Point3d is already defined in this pkg, to impl ts_rs traits.
9use kcmc::{ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, websocket::ModelingCmdReq};
10use kittycad_modeling_cmds as kcmc;
11use kittycad_modeling_cmds::{shared::PathSegment, units::UnitLength};
12use parse_display::{Display, FromStr};
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16use super::{
17    shapes::{get_radius, get_radius_labelled},
18    utils::untype_array,
19};
20#[cfg(feature = "artifact-graph")]
21use crate::execution::{Artifact, ArtifactId, CodeRef, StartSketchOnFace, StartSketchOnPlane};
22use crate::{
23    ExecutorContext,
24    errors::{KclError, KclErrorDetails},
25    exec::PlaneKind,
26    execution::{
27        BasePath, ExecState, GeoMeta, Geometry, KclValue, ModelingCmdMeta, Path, Plane, PlaneInfo, Point2d, Point3d,
28        ProfileClosed, Sketch, SketchSurface, Solid, TagIdentifier, annotations,
29        types::{ArrayLen, NumericType, PrimitiveType, RuntimeType},
30    },
31    parsing::ast::types::TagNode,
32    std::{
33        EQUAL_POINTS_DIST_EPSILON,
34        args::{Args, TyF64},
35        axis_or_reference::Axis2dOrEdgeReference,
36        faces::{FaceSpecifier, make_face},
37        planes::inner_plane_of,
38        utils::{
39            TangentialArcInfoInput, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
40            intersection_with_parallel_line, point_to_len_unit, point_to_mm, untyped_point_to_mm,
41        },
42    },
43};
44
45/// A tag for a face.
46#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
47#[ts(export)]
48#[serde(rename_all = "snake_case", untagged)]
49pub enum FaceTag {
50    StartOrEnd(StartOrEnd),
51    /// A tag for the face.
52    Tag(Box<TagIdentifier>),
53}
54
55impl std::fmt::Display for FaceTag {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            FaceTag::Tag(t) => write!(f, "{t}"),
59            FaceTag::StartOrEnd(StartOrEnd::Start) => write!(f, "start"),
60            FaceTag::StartOrEnd(StartOrEnd::End) => write!(f, "end"),
61        }
62    }
63}
64
65impl FaceTag {
66    /// Get the face id from the tag.
67    pub async fn get_face_id(
68        &self,
69        solid: &Solid,
70        exec_state: &mut ExecState,
71        args: &Args,
72        must_be_planar: bool,
73    ) -> Result<uuid::Uuid, KclError> {
74        match self {
75            FaceTag::Tag(t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
76            FaceTag::StartOrEnd(StartOrEnd::Start) => solid.start_cap_id.ok_or_else(|| {
77                KclError::new_type(KclErrorDetails::new(
78                    "Expected a start face".to_string(),
79                    vec![args.source_range],
80                ))
81            }),
82            FaceTag::StartOrEnd(StartOrEnd::End) => solid.end_cap_id.ok_or_else(|| {
83                KclError::new_type(KclErrorDetails::new(
84                    "Expected an end face".to_string(),
85                    vec![args.source_range],
86                ))
87            }),
88        }
89    }
90
91    pub async fn get_face_id_from_tag(
92        &self,
93        exec_state: &mut ExecState,
94        args: &Args,
95        must_be_planar: bool,
96    ) -> Result<uuid::Uuid, KclError> {
97        match self {
98            FaceTag::Tag(t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
99            _ => Err(KclError::new_type(KclErrorDetails::new(
100                "Could not find the face corresponding to this tag".to_string(),
101                vec![args.source_range],
102            ))),
103        }
104    }
105
106    pub fn geometry(&self) -> Option<Geometry> {
107        match self {
108            FaceTag::Tag(t) => t.geometry(),
109            _ => None,
110        }
111    }
112}
113
114#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, FromStr, Display)]
115#[ts(export)]
116#[serde(rename_all = "snake_case")]
117#[display(style = "snake_case")]
118pub enum StartOrEnd {
119    /// The start face as in before you extruded. This could also be known as the bottom
120    /// face. But we do not call it bottom because it would be the top face if you
121    /// extruded it in the opposite direction or flipped the camera.
122    #[serde(rename = "start", alias = "START")]
123    Start,
124    /// The end face after you extruded. This could also be known as the top
125    /// face. But we do not call it top because it would be the bottom face if you
126    /// extruded it in the opposite direction or flipped the camera.
127    #[serde(rename = "end", alias = "END")]
128    End,
129}
130
131pub const NEW_TAG_KW: &str = "tag";
132
133pub async fn involute_circular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
134    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
135
136    let start_radius: Option<TyF64> = args.get_kw_arg_opt("startRadius", &RuntimeType::length(), exec_state)?;
137    let end_radius: Option<TyF64> = args.get_kw_arg_opt("endRadius", &RuntimeType::length(), exec_state)?;
138    let start_diameter: Option<TyF64> = args.get_kw_arg_opt("startDiameter", &RuntimeType::length(), exec_state)?;
139    let end_diameter: Option<TyF64> = args.get_kw_arg_opt("endDiameter", &RuntimeType::length(), exec_state)?;
140    let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::angle(), exec_state)?;
141    let reverse = args.get_kw_arg_opt("reverse", &RuntimeType::bool(), exec_state)?;
142    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
143    let new_sketch = inner_involute_circular(
144        sketch,
145        start_radius,
146        end_radius,
147        start_diameter,
148        end_diameter,
149        angle,
150        reverse,
151        tag,
152        exec_state,
153        args,
154    )
155    .await?;
156    Ok(KclValue::Sketch {
157        value: Box::new(new_sketch),
158    })
159}
160
161fn involute_curve(radius: f64, angle: f64) -> (f64, f64) {
162    (
163        radius * (libm::cos(angle) + angle * libm::sin(angle)),
164        radius * (libm::sin(angle) - angle * libm::cos(angle)),
165    )
166}
167
168#[allow(clippy::too_many_arguments)]
169async fn inner_involute_circular(
170    sketch: Sketch,
171    start_radius: Option<TyF64>,
172    end_radius: Option<TyF64>,
173    start_diameter: Option<TyF64>,
174    end_diameter: Option<TyF64>,
175    angle: TyF64,
176    reverse: Option<bool>,
177    tag: Option<TagNode>,
178    exec_state: &mut ExecState,
179    args: Args,
180) -> Result<Sketch, KclError> {
181    let id = exec_state.next_uuid();
182    let angle_deg = angle.to_degrees(exec_state, args.source_range);
183    let angle_rad = angle.to_radians(exec_state, args.source_range);
184
185    let longer_args_dot_source_range = args.source_range;
186    let start_radius = get_radius_labelled(
187        start_radius,
188        start_diameter,
189        args.source_range,
190        "startRadius",
191        "startDiameter",
192    )?;
193    let end_radius = get_radius_labelled(
194        end_radius,
195        end_diameter,
196        longer_args_dot_source_range,
197        "endRadius",
198        "endDiameter",
199    )?;
200
201    exec_state
202        .batch_modeling_cmd(
203            ModelingCmdMeta::from_args_id(exec_state, &args, id),
204            ModelingCmd::from(
205                mcmd::ExtendPath::builder()
206                    .path(sketch.id.into())
207                    .segment(PathSegment::CircularInvolute {
208                        start_radius: LengthUnit(start_radius.to_mm()),
209                        end_radius: LengthUnit(end_radius.to_mm()),
210                        angle: Angle::from_degrees(angle_deg),
211                        reverse: reverse.unwrap_or_default(),
212                    })
213                    .build(),
214            ),
215        )
216        .await?;
217
218    let from = sketch.current_pen_position()?;
219
220    let start_radius = start_radius.to_length_units(from.units);
221    let end_radius = end_radius.to_length_units(from.units);
222
223    let mut end: KPoint3d<f64> = Default::default(); // ADAM: TODO impl this below.
224    let theta = f64::sqrt(end_radius * end_radius - start_radius * start_radius) / start_radius;
225    let (x, y) = involute_curve(start_radius, theta);
226
227    end.x = x * libm::cos(angle_rad) - y * libm::sin(angle_rad);
228    end.y = x * libm::sin(angle_rad) + y * libm::cos(angle_rad);
229
230    end.x -= start_radius * libm::cos(angle_rad);
231    end.y -= start_radius * libm::sin(angle_rad);
232
233    if reverse.unwrap_or_default() {
234        end.x = -end.x;
235    }
236
237    end.x += from.x;
238    end.y += from.y;
239
240    let current_path = Path::ToPoint {
241        base: BasePath {
242            from: from.ignore_units(),
243            to: [end.x, end.y],
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;
254    if let Some(tag) = &tag {
255        new_sketch.add_tag(tag, &current_path, exec_state, None);
256    }
257    new_sketch.paths.push(current_path);
258    Ok(new_sketch)
259}
260
261/// Draw a line to a point.
262pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
263    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
264    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
265    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
266    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
267
268    let new_sketch = inner_line(sketch, end_absolute, end, tag, exec_state, args).await?;
269    Ok(KclValue::Sketch {
270        value: Box::new(new_sketch),
271    })
272}
273
274async fn inner_line(
275    sketch: Sketch,
276    end_absolute: Option<[TyF64; 2]>,
277    end: Option<[TyF64; 2]>,
278    tag: Option<TagNode>,
279    exec_state: &mut ExecState,
280    args: Args,
281) -> Result<Sketch, KclError> {
282    straight_line_with_new_id(
283        StraightLineParams {
284            sketch,
285            end_absolute,
286            end,
287            tag,
288            relative_name: "end",
289        },
290        exec_state,
291        &args.ctx,
292        args.source_range,
293    )
294    .await
295}
296
297pub(super) struct StraightLineParams {
298    sketch: Sketch,
299    end_absolute: Option<[TyF64; 2]>,
300    end: Option<[TyF64; 2]>,
301    tag: Option<TagNode>,
302    relative_name: &'static str,
303}
304
305impl StraightLineParams {
306    fn relative(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
307        Self {
308            sketch,
309            tag,
310            end: Some(p),
311            end_absolute: None,
312            relative_name: "end",
313        }
314    }
315    pub(super) fn absolute(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
316        Self {
317            sketch,
318            tag,
319            end: None,
320            end_absolute: Some(p),
321            relative_name: "end",
322        }
323    }
324}
325
326pub(super) async fn straight_line_with_new_id(
327    straight_line_params: StraightLineParams,
328    exec_state: &mut ExecState,
329    ctx: &ExecutorContext,
330    source_range: SourceRange,
331) -> Result<Sketch, KclError> {
332    let id = exec_state.next_uuid();
333    straight_line(id, straight_line_params, true, exec_state, ctx, source_range).await
334}
335
336pub(super) async fn straight_line(
337    id: Uuid,
338    StraightLineParams {
339        sketch,
340        end,
341        end_absolute,
342        tag,
343        relative_name,
344    }: StraightLineParams,
345    send_to_engine: bool,
346    exec_state: &mut ExecState,
347    ctx: &ExecutorContext,
348    source_range: SourceRange,
349) -> Result<Sketch, KclError> {
350    let from = sketch.current_pen_position()?;
351    let (point, is_absolute) = match (end_absolute, end) {
352        (Some(_), Some(_)) => {
353            return Err(KclError::new_semantic(KclErrorDetails::new(
354                "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
355                vec![source_range],
356            )));
357        }
358        (Some(end_absolute), None) => (end_absolute, true),
359        (None, Some(end)) => (end, false),
360        (None, None) => {
361            return Err(KclError::new_semantic(KclErrorDetails::new(
362                format!("You must supply either `{relative_name}` or `endAbsolute` arguments"),
363                vec![source_range],
364            )));
365        }
366    };
367
368    if send_to_engine {
369        exec_state
370            .batch_modeling_cmd(
371                ModelingCmdMeta::with_id(exec_state, ctx, source_range, id),
372                ModelingCmd::from(
373                    mcmd::ExtendPath::builder()
374                        .path(sketch.id.into())
375                        .segment(PathSegment::Line {
376                            end: KPoint2d::from(point_to_mm(point.clone())).with_z(0.0).map(LengthUnit),
377                            relative: !is_absolute,
378                        })
379                        .build(),
380                ),
381            )
382            .await?;
383    }
384
385    let end = if is_absolute {
386        point_to_len_unit(point, from.units)
387    } else {
388        let from = sketch.current_pen_position()?;
389        let point = point_to_len_unit(point, from.units);
390        [from.x + point[0], from.y + point[1]]
391    };
392
393    // Does it loop back on itself?
394    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
395
396    let current_path = Path::ToPoint {
397        base: BasePath {
398            from: from.ignore_units(),
399            to: end,
400            tag: tag.clone(),
401            units: sketch.units,
402            geo_meta: GeoMeta {
403                id,
404                metadata: source_range.into(),
405            },
406        },
407    };
408
409    let mut new_sketch = sketch;
410    if let Some(tag) = &tag {
411        new_sketch.add_tag(tag, &current_path, exec_state, None);
412    }
413    if loops_back_to_start {
414        new_sketch.is_closed = ProfileClosed::Implicitly;
415    }
416
417    new_sketch.paths.push(current_path);
418
419    Ok(new_sketch)
420}
421
422fn does_segment_close_sketch(end: [f64; 2], from: [f64; 2]) -> bool {
423    let same_x = (end[0] - from[0]).abs() < EQUAL_POINTS_DIST_EPSILON;
424    let same_y = (end[1] - from[1]).abs() < EQUAL_POINTS_DIST_EPSILON;
425    same_x && same_y
426}
427
428/// Draw a line on the x-axis.
429pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
430    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
431    let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
432    let end_absolute: Option<TyF64> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::length(), exec_state)?;
433    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
434
435    let new_sketch = inner_x_line(sketch, length, end_absolute, tag, exec_state, args).await?;
436    Ok(KclValue::Sketch {
437        value: Box::new(new_sketch),
438    })
439}
440
441async fn inner_x_line(
442    sketch: Sketch,
443    length: Option<TyF64>,
444    end_absolute: Option<TyF64>,
445    tag: Option<TagNode>,
446    exec_state: &mut ExecState,
447    args: Args,
448) -> Result<Sketch, KclError> {
449    let from = sketch.current_pen_position()?;
450    straight_line_with_new_id(
451        StraightLineParams {
452            sketch,
453            end_absolute: end_absolute.map(|x| [x, from.into_y()]),
454            end: length.map(|x| [x, TyF64::new(0.0, NumericType::mm())]),
455            tag,
456            relative_name: "length",
457        },
458        exec_state,
459        &args.ctx,
460        args.source_range,
461    )
462    .await
463}
464
465/// Draw a line on the y-axis.
466pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
467    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
468    let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
469    let end_absolute: Option<TyF64> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::length(), exec_state)?;
470    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
471
472    let new_sketch = inner_y_line(sketch, length, end_absolute, tag, exec_state, args).await?;
473    Ok(KclValue::Sketch {
474        value: Box::new(new_sketch),
475    })
476}
477
478async fn inner_y_line(
479    sketch: Sketch,
480    length: Option<TyF64>,
481    end_absolute: Option<TyF64>,
482    tag: Option<TagNode>,
483    exec_state: &mut ExecState,
484    args: Args,
485) -> Result<Sketch, KclError> {
486    let from = sketch.current_pen_position()?;
487    straight_line_with_new_id(
488        StraightLineParams {
489            sketch,
490            end_absolute: end_absolute.map(|y| [from.into_x(), y]),
491            end: length.map(|y| [TyF64::new(0.0, NumericType::mm()), y]),
492            tag,
493            relative_name: "length",
494        },
495        exec_state,
496        &args.ctx,
497        args.source_range,
498    )
499    .await
500}
501
502/// Draw an angled line.
503pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
504    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
505    let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::degrees(), exec_state)?;
506    let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
507    let length_x: Option<TyF64> = args.get_kw_arg_opt("lengthX", &RuntimeType::length(), exec_state)?;
508    let length_y: Option<TyF64> = args.get_kw_arg_opt("lengthY", &RuntimeType::length(), exec_state)?;
509    let end_absolute_x: Option<TyF64> = args.get_kw_arg_opt("endAbsoluteX", &RuntimeType::length(), exec_state)?;
510    let end_absolute_y: Option<TyF64> = args.get_kw_arg_opt("endAbsoluteY", &RuntimeType::length(), exec_state)?;
511    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
512
513    let new_sketch = inner_angled_line(
514        sketch,
515        angle.n,
516        length,
517        length_x,
518        length_y,
519        end_absolute_x,
520        end_absolute_y,
521        tag,
522        exec_state,
523        args,
524    )
525    .await?;
526    Ok(KclValue::Sketch {
527        value: Box::new(new_sketch),
528    })
529}
530
531#[allow(clippy::too_many_arguments)]
532async fn inner_angled_line(
533    sketch: Sketch,
534    angle: f64,
535    length: Option<TyF64>,
536    length_x: Option<TyF64>,
537    length_y: Option<TyF64>,
538    end_absolute_x: Option<TyF64>,
539    end_absolute_y: Option<TyF64>,
540    tag: Option<TagNode>,
541    exec_state: &mut ExecState,
542    args: Args,
543) -> Result<Sketch, KclError> {
544    let options_given = [&length, &length_x, &length_y, &end_absolute_x, &end_absolute_y]
545        .iter()
546        .filter(|x| x.is_some())
547        .count();
548    if options_given > 1 {
549        return Err(KclError::new_type(KclErrorDetails::new(
550            " one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_string(),
551            vec![args.source_range],
552        )));
553    }
554    if let Some(length_x) = length_x {
555        return inner_angled_line_of_x_length(angle, length_x, sketch, tag, exec_state, args).await;
556    }
557    if let Some(length_y) = length_y {
558        return inner_angled_line_of_y_length(angle, length_y, sketch, tag, exec_state, args).await;
559    }
560    let angle_degrees = angle;
561    match (length, length_x, length_y, end_absolute_x, end_absolute_y) {
562        (Some(length), None, None, None, None) => {
563            inner_angled_line_length(sketch, angle_degrees, length, tag, exec_state, args).await
564        }
565        (None, Some(length_x), None, None, None) => {
566            inner_angled_line_of_x_length(angle_degrees, length_x, sketch, tag, exec_state, args).await
567        }
568        (None, None, Some(length_y), None, None) => {
569            inner_angled_line_of_y_length(angle_degrees, length_y, sketch, tag, exec_state, args).await
570        }
571        (None, None, None, Some(end_absolute_x), None) => {
572            inner_angled_line_to_x(angle_degrees, end_absolute_x, sketch, tag, exec_state, args).await
573        }
574        (None, None, None, None, Some(end_absolute_y)) => {
575            inner_angled_line_to_y(angle_degrees, end_absolute_y, sketch, tag, exec_state, args).await
576        }
577        (None, None, None, None, None) => Err(KclError::new_type(KclErrorDetails::new(
578            "One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` must be given".to_string(),
579            vec![args.source_range],
580        ))),
581        _ => Err(KclError::new_type(KclErrorDetails::new(
582            "Only One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_owned(),
583            vec![args.source_range],
584        ))),
585    }
586}
587
588async fn inner_angled_line_length(
589    sketch: Sketch,
590    angle_degrees: f64,
591    length: TyF64,
592    tag: Option<TagNode>,
593    exec_state: &mut ExecState,
594    args: Args,
595) -> Result<Sketch, KclError> {
596    let from = sketch.current_pen_position()?;
597    let length = length.to_length_units(from.units);
598
599    //double check me on this one - mike
600    let delta: [f64; 2] = [
601        length * libm::cos(angle_degrees.to_radians()),
602        length * libm::sin(angle_degrees.to_radians()),
603    ];
604    let relative = true;
605
606    let to: [f64; 2] = [from.x + delta[0], from.y + delta[1]];
607    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
608
609    let id = exec_state.next_uuid();
610
611    exec_state
612        .batch_modeling_cmd(
613            ModelingCmdMeta::from_args_id(exec_state, &args, id),
614            ModelingCmd::from(
615                mcmd::ExtendPath::builder()
616                    .path(sketch.id.into())
617                    .segment(PathSegment::Line {
618                        end: KPoint2d::from(untyped_point_to_mm(delta, from.units))
619                            .with_z(0.0)
620                            .map(LengthUnit),
621                        relative,
622                    })
623                    .build(),
624            ),
625        )
626        .await?;
627
628    let current_path = Path::ToPoint {
629        base: BasePath {
630            from: from.ignore_units(),
631            to,
632            tag: tag.clone(),
633            units: sketch.units,
634            geo_meta: GeoMeta {
635                id,
636                metadata: args.source_range.into(),
637            },
638        },
639    };
640
641    let mut new_sketch = sketch;
642    if let Some(tag) = &tag {
643        new_sketch.add_tag(tag, &current_path, exec_state, None);
644    }
645    if loops_back_to_start {
646        new_sketch.is_closed = ProfileClosed::Implicitly;
647    }
648
649    new_sketch.paths.push(current_path);
650    Ok(new_sketch)
651}
652
653async fn inner_angled_line_of_x_length(
654    angle_degrees: f64,
655    length: TyF64,
656    sketch: Sketch,
657    tag: Option<TagNode>,
658    exec_state: &mut ExecState,
659    args: Args,
660) -> Result<Sketch, KclError> {
661    if angle_degrees.abs() == 270.0 {
662        return Err(KclError::new_type(KclErrorDetails::new(
663            "Cannot have an x constrained angle of 270 degrees".to_string(),
664            vec![args.source_range],
665        )));
666    }
667
668    if angle_degrees.abs() == 90.0 {
669        return Err(KclError::new_type(KclErrorDetails::new(
670            "Cannot have an x constrained angle of 90 degrees".to_string(),
671            vec![args.source_range],
672        )));
673    }
674
675    let to = get_y_component(Angle::from_degrees(angle_degrees), length.n);
676    let to = [TyF64::new(to[0], length.ty), TyF64::new(to[1], length.ty)];
677
678    let new_sketch = straight_line_with_new_id(
679        StraightLineParams::relative(to, sketch, tag),
680        exec_state,
681        &args.ctx,
682        args.source_range,
683    )
684    .await?;
685
686    Ok(new_sketch)
687}
688
689async fn inner_angled_line_to_x(
690    angle_degrees: f64,
691    x_to: TyF64,
692    sketch: Sketch,
693    tag: Option<TagNode>,
694    exec_state: &mut ExecState,
695    args: Args,
696) -> Result<Sketch, KclError> {
697    let from = sketch.current_pen_position()?;
698
699    if angle_degrees.abs() == 270.0 {
700        return Err(KclError::new_type(KclErrorDetails::new(
701            "Cannot have an x constrained angle of 270 degrees".to_string(),
702            vec![args.source_range],
703        )));
704    }
705
706    if angle_degrees.abs() == 90.0 {
707        return Err(KclError::new_type(KclErrorDetails::new(
708            "Cannot have an x constrained angle of 90 degrees".to_string(),
709            vec![args.source_range],
710        )));
711    }
712
713    let x_component = x_to.to_length_units(from.units) - from.x;
714    let y_component = x_component * libm::tan(angle_degrees.to_radians());
715    let y_to = from.y + y_component;
716
717    let new_sketch = straight_line_with_new_id(
718        StraightLineParams::absolute([x_to, TyF64::new(y_to, from.units.into())], sketch, tag),
719        exec_state,
720        &args.ctx,
721        args.source_range,
722    )
723    .await?;
724    Ok(new_sketch)
725}
726
727async fn inner_angled_line_of_y_length(
728    angle_degrees: f64,
729    length: TyF64,
730    sketch: Sketch,
731    tag: Option<TagNode>,
732    exec_state: &mut ExecState,
733    args: Args,
734) -> Result<Sketch, KclError> {
735    if angle_degrees.abs() == 0.0 {
736        return Err(KclError::new_type(KclErrorDetails::new(
737            "Cannot have a y constrained angle of 0 degrees".to_string(),
738            vec![args.source_range],
739        )));
740    }
741
742    if angle_degrees.abs() == 180.0 {
743        return Err(KclError::new_type(KclErrorDetails::new(
744            "Cannot have a y constrained angle of 180 degrees".to_string(),
745            vec![args.source_range],
746        )));
747    }
748
749    let to = get_x_component(Angle::from_degrees(angle_degrees), length.n);
750    let to = [TyF64::new(to[0], length.ty), TyF64::new(to[1], length.ty)];
751
752    let new_sketch = straight_line_with_new_id(
753        StraightLineParams::relative(to, sketch, tag),
754        exec_state,
755        &args.ctx,
756        args.source_range,
757    )
758    .await?;
759
760    Ok(new_sketch)
761}
762
763async fn inner_angled_line_to_y(
764    angle_degrees: f64,
765    y_to: TyF64,
766    sketch: Sketch,
767    tag: Option<TagNode>,
768    exec_state: &mut ExecState,
769    args: Args,
770) -> Result<Sketch, KclError> {
771    let from = sketch.current_pen_position()?;
772
773    if angle_degrees.abs() == 0.0 {
774        return Err(KclError::new_type(KclErrorDetails::new(
775            "Cannot have a y constrained angle of 0 degrees".to_string(),
776            vec![args.source_range],
777        )));
778    }
779
780    if angle_degrees.abs() == 180.0 {
781        return Err(KclError::new_type(KclErrorDetails::new(
782            "Cannot have a y constrained angle of 180 degrees".to_string(),
783            vec![args.source_range],
784        )));
785    }
786
787    let y_component = y_to.to_length_units(from.units) - from.y;
788    let x_component = y_component / libm::tan(angle_degrees.to_radians());
789    let x_to = from.x + x_component;
790
791    let new_sketch = straight_line_with_new_id(
792        StraightLineParams::absolute([TyF64::new(x_to, from.units.into()), y_to], sketch, tag),
793        exec_state,
794        &args.ctx,
795        args.source_range,
796    )
797    .await?;
798    Ok(new_sketch)
799}
800
801/// Draw an angled line that intersects with a given line.
802pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
803    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
804    let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::angle(), exec_state)?;
805    let intersect_tag: TagIdentifier = args.get_kw_arg("intersectTag", &RuntimeType::tagged_edge(), exec_state)?;
806    let offset = args.get_kw_arg_opt("offset", &RuntimeType::length(), exec_state)?;
807    let tag: Option<TagNode> = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
808    let new_sketch =
809        inner_angled_line_that_intersects(sketch, angle, intersect_tag, offset, tag, exec_state, args).await?;
810    Ok(KclValue::Sketch {
811        value: Box::new(new_sketch),
812    })
813}
814
815pub async fn inner_angled_line_that_intersects(
816    sketch: Sketch,
817    angle: TyF64,
818    intersect_tag: TagIdentifier,
819    offset: Option<TyF64>,
820    tag: Option<TagNode>,
821    exec_state: &mut ExecState,
822    args: Args,
823) -> Result<Sketch, KclError> {
824    let intersect_path = args.get_tag_engine_info(exec_state, &intersect_tag)?;
825    let path = intersect_path.path.clone().ok_or_else(|| {
826        KclError::new_type(KclErrorDetails::new(
827            format!("Expected an intersect path with a path, found `{intersect_path:?}`"),
828            vec![args.source_range],
829        ))
830    })?;
831
832    let from = sketch.current_pen_position()?;
833    let to = intersection_with_parallel_line(
834        &[
835            point_to_len_unit(path.get_from(), from.units),
836            point_to_len_unit(path.get_to(), from.units),
837        ],
838        offset.map(|t| t.to_length_units(from.units)).unwrap_or_default(),
839        angle.to_degrees(exec_state, args.source_range),
840        from.ignore_units(),
841    );
842    let to = [
843        TyF64::new(to[0], from.units.into()),
844        TyF64::new(to[1], from.units.into()),
845    ];
846
847    straight_line_with_new_id(
848        StraightLineParams::absolute(to, sketch, tag),
849        exec_state,
850        &args.ctx,
851        args.source_range,
852    )
853    .await
854}
855
856/// Data for start sketch on.
857/// You can start a sketch on a plane or an solid.
858#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
859#[ts(export)]
860#[serde(rename_all = "camelCase", untagged)]
861#[allow(clippy::large_enum_variant)]
862pub enum SketchData {
863    PlaneOrientation(PlaneData),
864    Plane(Box<Plane>),
865    Solid(Box<Solid>),
866}
867
868/// Orientation data that can be used to construct a plane, not a plane in itself.
869#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
870#[ts(export)]
871#[serde(rename_all = "camelCase")]
872#[allow(clippy::large_enum_variant)]
873pub enum PlaneData {
874    /// The XY plane.
875    #[serde(rename = "XY", alias = "xy")]
876    XY,
877    /// The opposite side of the XY plane.
878    #[serde(rename = "-XY", alias = "-xy")]
879    NegXY,
880    /// The XZ plane.
881    #[serde(rename = "XZ", alias = "xz")]
882    XZ,
883    /// The opposite side of the XZ plane.
884    #[serde(rename = "-XZ", alias = "-xz")]
885    NegXZ,
886    /// The YZ plane.
887    #[serde(rename = "YZ", alias = "yz")]
888    YZ,
889    /// The opposite side of the YZ plane.
890    #[serde(rename = "-YZ", alias = "-yz")]
891    NegYZ,
892    /// A defined plane.
893    Plane(PlaneInfo),
894}
895
896/// Start a sketch on a specific plane or face.
897pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
898    let data = args.get_unlabeled_kw_arg(
899        "planeOrSolid",
900        &RuntimeType::Union(vec![RuntimeType::solid(), RuntimeType::plane()]),
901        exec_state,
902    )?;
903    let face = args.get_kw_arg_opt("face", &RuntimeType::tagged_face_or_segment(), exec_state)?;
904    let normal_to_face = args.get_kw_arg_opt("normalToFace", &RuntimeType::tagged_face(), exec_state)?;
905    let align_axis = args.get_kw_arg_opt("alignAxis", &RuntimeType::Primitive(PrimitiveType::Axis2d), exec_state)?;
906    let normal_offset = args.get_kw_arg_opt("normalOffset", &RuntimeType::length(), exec_state)?;
907
908    match inner_start_sketch_on(data, face, normal_to_face, align_axis, normal_offset, exec_state, &args).await? {
909        SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
910        SketchSurface::Face(value) => Ok(KclValue::Face { value }),
911    }
912}
913
914async fn inner_start_sketch_on(
915    plane_or_solid: SketchData,
916    face: Option<FaceSpecifier>,
917    normal_to_face: Option<FaceSpecifier>,
918    align_axis: Option<Axis2dOrEdgeReference>,
919    normal_offset: Option<TyF64>,
920    exec_state: &mut ExecState,
921    args: &Args,
922) -> Result<SketchSurface, KclError> {
923    let face = match (face, normal_to_face, &align_axis, &normal_offset) {
924        (Some(_), Some(_), _, _) => {
925            return Err(KclError::new_semantic(KclErrorDetails::new(
926                "You cannot give both `face` and `normalToFace` params, you have to choose one or the other."
927                    .to_owned(),
928                vec![args.source_range],
929            )));
930        }
931        (Some(face), None, None, None) => Some(face),
932        (_, Some(_), None, _) => {
933            return Err(KclError::new_semantic(KclErrorDetails::new(
934                "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
935                vec![args.source_range],
936            )));
937        }
938        (_, None, Some(_), _) => {
939            return Err(KclError::new_semantic(KclErrorDetails::new(
940                "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
941                vec![args.source_range],
942            )));
943        }
944        (_, None, _, Some(_)) => {
945            return Err(KclError::new_semantic(KclErrorDetails::new(
946                "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
947                vec![args.source_range],
948            )));
949        }
950        (_, Some(face), Some(_), _) => Some(face),
951        (None, None, None, None) => None,
952    };
953
954    match plane_or_solid {
955        SketchData::PlaneOrientation(plane_data) => {
956            let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
957            Ok(SketchSurface::Plane(plane))
958        }
959        SketchData::Plane(plane) => {
960            if plane.is_uninitialized() {
961                let plane = make_sketch_plane_from_orientation(plane.info.into_plane_data(), exec_state, args).await?;
962                Ok(SketchSurface::Plane(plane))
963            } else {
964                // Create artifact used only by the UI, not the engine.
965                #[cfg(feature = "artifact-graph")]
966                {
967                    let id = exec_state.next_uuid();
968                    exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
969                        id: ArtifactId::from(id),
970                        plane_id: plane.artifact_id,
971                        code_ref: CodeRef::placeholder(args.source_range),
972                    }));
973                }
974
975                Ok(SketchSurface::Plane(plane))
976            }
977        }
978        SketchData::Solid(solid) => {
979            let Some(tag) = face else {
980                return Err(KclError::new_type(KclErrorDetails::new(
981                    "Expected a tag for the face to sketch on".to_string(),
982                    vec![args.source_range],
983                )));
984            };
985            if let Some(align_axis) = align_axis {
986                let plane_of = inner_plane_of(*solid, tag, exec_state, args).await?;
987
988                // plane_of info axis units are Some(UnitLength::Millimeters), see inner_plane_of and PlaneInfo
989                let offset = normal_offset.map_or(0.0, |x| x.to_mm());
990                let (x_axis, y_axis, normal_offset) = match align_axis {
991                    Axis2dOrEdgeReference::Axis { direction, origin: _ } => {
992                        if (direction[0].n - 1.0).abs() < f64::EPSILON {
993                            //X axis chosen
994                            (
995                                plane_of.info.x_axis,
996                                plane_of.info.z_axis,
997                                plane_of.info.y_axis * offset,
998                            )
999                        } else if (direction[0].n + 1.0).abs() < f64::EPSILON {
1000                            // -X axis chosen
1001                            (
1002                                plane_of.info.x_axis.negated(),
1003                                plane_of.info.z_axis,
1004                                plane_of.info.y_axis * offset,
1005                            )
1006                        } else if (direction[1].n - 1.0).abs() < f64::EPSILON {
1007                            // Y axis chosen
1008                            (
1009                                plane_of.info.y_axis,
1010                                plane_of.info.z_axis,
1011                                plane_of.info.x_axis * offset,
1012                            )
1013                        } else if (direction[1].n + 1.0).abs() < f64::EPSILON {
1014                            // -Y axis chosen
1015                            (
1016                                plane_of.info.y_axis.negated(),
1017                                plane_of.info.z_axis,
1018                                plane_of.info.x_axis * offset,
1019                            )
1020                        } else {
1021                            return Err(KclError::new_semantic(KclErrorDetails::new(
1022                                "Unsupported axis detected. This function only supports using X, -X, Y and -Y."
1023                                    .to_owned(),
1024                                vec![args.source_range],
1025                            )));
1026                        }
1027                    }
1028                    Axis2dOrEdgeReference::Edge(_) => {
1029                        return Err(KclError::new_semantic(KclErrorDetails::new(
1030                            "Use of an edge here is unsupported, please specify an `Axis2d` (e.g. `X`) instead."
1031                                .to_owned(),
1032                            vec![args.source_range],
1033                        )));
1034                    }
1035                };
1036                let origin = Point3d::new(0.0, 0.0, 0.0, plane_of.info.origin.units);
1037                let plane_data = PlaneData::Plane(PlaneInfo {
1038                    origin: plane_of.project(origin) + normal_offset,
1039                    x_axis,
1040                    y_axis,
1041                    z_axis: x_axis.axes_cross_product(&y_axis),
1042                });
1043                let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
1044
1045                // Create artifact used only by the UI, not the engine.
1046                #[cfg(feature = "artifact-graph")]
1047                {
1048                    let id = exec_state.next_uuid();
1049                    exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
1050                        id: ArtifactId::from(id),
1051                        plane_id: plane.artifact_id,
1052                        code_ref: CodeRef::placeholder(args.source_range),
1053                    }));
1054                }
1055
1056                Ok(SketchSurface::Plane(plane))
1057            } else {
1058                let face = make_face(solid, tag, exec_state, args).await?;
1059
1060                #[cfg(feature = "artifact-graph")]
1061                {
1062                    // Create artifact used only by the UI, not the engine.
1063                    let id = exec_state.next_uuid();
1064                    exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
1065                        id: ArtifactId::from(id),
1066                        face_id: face.artifact_id,
1067                        code_ref: CodeRef::placeholder(args.source_range),
1068                    }));
1069                }
1070
1071                Ok(SketchSurface::Face(face))
1072            }
1073        }
1074    }
1075}
1076
1077pub async fn make_sketch_plane_from_orientation(
1078    data: PlaneData,
1079    exec_state: &mut ExecState,
1080    args: &Args,
1081) -> Result<Box<Plane>, KclError> {
1082    let id = exec_state.next_uuid();
1083    let kind = PlaneKind::from(&data);
1084    let mut plane = Plane {
1085        id,
1086        artifact_id: id.into(),
1087        object_id: None,
1088        kind,
1089        info: PlaneInfo::try_from(data)?,
1090        meta: vec![args.source_range.into()],
1091    };
1092
1093    // Create the plane on the fly.
1094    ensure_sketch_plane_in_engine(&mut plane, exec_state, &args.ctx, args.source_range).await?;
1095
1096    Ok(Box::new(plane))
1097}
1098
1099/// Ensure that the plane exists in the engine.
1100pub async fn ensure_sketch_plane_in_engine(
1101    plane: &mut Plane,
1102    exec_state: &mut ExecState,
1103    ctx: &ExecutorContext,
1104    source_range: SourceRange,
1105) -> Result<(), KclError> {
1106    if plane.is_initialized() {
1107        return Ok(());
1108    }
1109    #[cfg(feature = "artifact-graph")]
1110    {
1111        if let Some(existing_object_id) = exec_state.scene_object_id_by_artifact_id(ArtifactId::new(plane.id)) {
1112            plane.object_id = Some(existing_object_id);
1113            return Ok(());
1114        }
1115    }
1116
1117    let clobber = false;
1118    let size = LengthUnit(60.0);
1119    let hide = Some(true);
1120    let cmd = if let Some(hide) = hide {
1121        mcmd::MakePlane::builder()
1122            .clobber(clobber)
1123            .origin(plane.info.origin.into())
1124            .size(size)
1125            .x_axis(plane.info.x_axis.into())
1126            .y_axis(plane.info.y_axis.into())
1127            .hide(hide)
1128            .build()
1129    } else {
1130        mcmd::MakePlane::builder()
1131            .clobber(clobber)
1132            .origin(plane.info.origin.into())
1133            .size(size)
1134            .x_axis(plane.info.x_axis.into())
1135            .y_axis(plane.info.y_axis.into())
1136            .build()
1137    };
1138    exec_state
1139        .batch_modeling_cmd(
1140            ModelingCmdMeta::with_id(exec_state, ctx, source_range, plane.id),
1141            ModelingCmd::from(cmd),
1142        )
1143        .await?;
1144    let plane_object_id = exec_state.next_object_id();
1145    #[cfg(feature = "artifact-graph")]
1146    {
1147        let plane_object = crate::front::Object {
1148            id: plane_object_id,
1149            kind: crate::front::ObjectKind::Plane(crate::front::Plane::Object(plane_object_id)),
1150            label: Default::default(),
1151            comments: Default::default(),
1152            artifact_id: ArtifactId::new(plane.id),
1153            source: source_range.into(),
1154        };
1155        exec_state.add_scene_object(plane_object, source_range);
1156    }
1157    plane.object_id = Some(plane_object_id);
1158
1159    Ok(())
1160}
1161
1162/// Start a new profile at a given point.
1163pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1164    let sketch_surface = args.get_unlabeled_kw_arg(
1165        "startProfileOn",
1166        &RuntimeType::Union(vec![RuntimeType::plane(), RuntimeType::face()]),
1167        exec_state,
1168    )?;
1169    let start: [TyF64; 2] = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
1170    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1171
1172    let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, &args.ctx, args.source_range).await?;
1173    Ok(KclValue::Sketch {
1174        value: Box::new(sketch),
1175    })
1176}
1177
1178pub(crate) async fn inner_start_profile(
1179    sketch_surface: SketchSurface,
1180    at: [TyF64; 2],
1181    tag: Option<TagNode>,
1182    exec_state: &mut ExecState,
1183    ctx: &ExecutorContext,
1184    source_range: SourceRange,
1185) -> Result<Sketch, KclError> {
1186    let id = exec_state.next_uuid();
1187    create_sketch(id, sketch_surface, at, tag, true, exec_state, ctx, source_range).await
1188}
1189
1190#[expect(clippy::too_many_arguments)]
1191pub(crate) async fn create_sketch(
1192    id: Uuid,
1193    sketch_surface: SketchSurface,
1194    at: [TyF64; 2],
1195    tag: Option<TagNode>,
1196    send_to_engine: bool,
1197    exec_state: &mut ExecState,
1198    ctx: &ExecutorContext,
1199    source_range: SourceRange,
1200) -> Result<Sketch, KclError> {
1201    match &sketch_surface {
1202        SketchSurface::Face(face) => {
1203            // Flush the batch for our fillets/chamfers if there are any.
1204            // If we do not do these for sketch on face, things will fail with face does not exist.
1205            exec_state
1206                .flush_batch_for_solids(
1207                    ModelingCmdMeta::new(exec_state, ctx, source_range),
1208                    &[(*face.solid).clone()],
1209                )
1210                .await?;
1211        }
1212        SketchSurface::Plane(plane) if !plane.is_standard() => {
1213            // Hide whatever plane we are sketching on.
1214            // This is especially helpful for offset planes, which would be visible otherwise.
1215            exec_state
1216                .batch_end_cmd(
1217                    ModelingCmdMeta::new(exec_state, ctx, source_range),
1218                    ModelingCmd::from(mcmd::ObjectVisible::builder().object_id(plane.id).hidden(true).build()),
1219                )
1220                .await?;
1221        }
1222        _ => {}
1223    }
1224
1225    let path_id = id;
1226    let enable_sketch_id = exec_state.next_uuid();
1227    let move_pen_id = exec_state.next_uuid();
1228    let disable_sketch_id = exec_state.next_uuid();
1229    if send_to_engine {
1230        exec_state
1231            .batch_modeling_cmds(
1232                ModelingCmdMeta::new(exec_state, ctx, source_range),
1233                &[
1234                    // Enter sketch mode on the surface.
1235                    // We call this here so you can reuse the sketch surface for multiple sketches.
1236                    ModelingCmdReq {
1237                        cmd: ModelingCmd::from(if let SketchSurface::Plane(plane) = &sketch_surface {
1238                            // We pass in the normal for the plane here.
1239                            let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
1240                            mcmd::EnableSketchMode::builder()
1241                                .animated(false)
1242                                .ortho(false)
1243                                .entity_id(sketch_surface.id())
1244                                .adjust_camera(false)
1245                                .planar_normal(normal.into())
1246                                .build()
1247                        } else {
1248                            mcmd::EnableSketchMode::builder()
1249                                .animated(false)
1250                                .ortho(false)
1251                                .entity_id(sketch_surface.id())
1252                                .adjust_camera(false)
1253                                .build()
1254                        }),
1255                        cmd_id: enable_sketch_id.into(),
1256                    },
1257                    ModelingCmdReq {
1258                        cmd: ModelingCmd::from(mcmd::StartPath::default()),
1259                        cmd_id: path_id.into(),
1260                    },
1261                    ModelingCmdReq {
1262                        cmd: ModelingCmd::from(
1263                            mcmd::MovePathPen::builder()
1264                                .path(path_id.into())
1265                                .to(KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit))
1266                                .build(),
1267                        ),
1268                        cmd_id: move_pen_id.into(),
1269                    },
1270                    ModelingCmdReq {
1271                        cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1272                        cmd_id: disable_sketch_id.into(),
1273                    },
1274                ],
1275            )
1276            .await?;
1277    }
1278
1279    // Convert to the units of the module.  This is what the frontend expects.
1280    let units = exec_state.length_unit();
1281    let to = point_to_len_unit(at, units);
1282    let current_path = BasePath {
1283        from: to,
1284        to,
1285        tag: tag.clone(),
1286        units,
1287        geo_meta: GeoMeta {
1288            id: move_pen_id,
1289            metadata: source_range.into(),
1290        },
1291    };
1292
1293    let mut sketch = Sketch {
1294        id: path_id,
1295        original_id: path_id,
1296        artifact_id: path_id.into(),
1297        on: sketch_surface,
1298        paths: vec![],
1299        inner_paths: vec![],
1300        units,
1301        mirror: Default::default(),
1302        clone: Default::default(),
1303        meta: vec![source_range.into()],
1304        tags: Default::default(),
1305        start: current_path.clone(),
1306        is_closed: ProfileClosed::No,
1307    };
1308    if let Some(tag) = &tag {
1309        let path = Path::Base { base: current_path };
1310        sketch.add_tag(tag, &path, exec_state, None);
1311    }
1312
1313    Ok(sketch)
1314}
1315
1316/// Returns the X component of the sketch profile start point.
1317pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1318    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1319    let ty = sketch.units.into();
1320    let x = inner_profile_start_x(sketch)?;
1321    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1322}
1323
1324pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1325    Ok(profile.start.to[0])
1326}
1327
1328/// Returns the Y component of the sketch profile start point.
1329pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1330    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1331    let ty = sketch.units.into();
1332    let x = inner_profile_start_y(sketch)?;
1333    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1334}
1335
1336pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1337    Ok(profile.start.to[1])
1338}
1339
1340/// Returns the sketch profile start point.
1341pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1342    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1343    let ty = sketch.units.into();
1344    let point = inner_profile_start(sketch)?;
1345    Ok(KclValue::from_point2d(point, ty, args.into()))
1346}
1347
1348pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1349    Ok(profile.start.to)
1350}
1351
1352/// Close the current sketch.
1353pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1354    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1355    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1356    let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1357    Ok(KclValue::Sketch {
1358        value: Box::new(new_sketch),
1359    })
1360}
1361
1362pub(crate) async fn inner_close(
1363    sketch: Sketch,
1364    tag: Option<TagNode>,
1365    exec_state: &mut ExecState,
1366    args: Args,
1367) -> Result<Sketch, KclError> {
1368    if matches!(sketch.is_closed, ProfileClosed::Explicitly) {
1369        exec_state.warn(
1370            crate::CompilationError {
1371                source_range: args.source_range,
1372                message: "This sketch is already closed. Remove this unnecessary `close()` call".to_string(),
1373                suggestion: None,
1374                severity: crate::errors::Severity::Warning,
1375                tag: crate::errors::Tag::Unnecessary,
1376            },
1377            annotations::WARN_UNNECESSARY_CLOSE,
1378        );
1379        return Ok(sketch);
1380    }
1381    let from = sketch.current_pen_position()?;
1382    let to = point_to_len_unit(sketch.start.get_from(), from.units);
1383
1384    let id = exec_state.next_uuid();
1385
1386    exec_state
1387        .batch_modeling_cmd(
1388            ModelingCmdMeta::from_args_id(exec_state, &args, id),
1389            ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
1390        )
1391        .await?;
1392
1393    let mut new_sketch = sketch;
1394
1395    let distance = ((from.x - to[0]).powi(2) + (from.y - to[1]).powi(2)).sqrt();
1396    if distance > super::EQUAL_POINTS_DIST_EPSILON {
1397        // These will NOT be the same point in the engine, and an additional segment will be created.
1398        let current_path = Path::ToPoint {
1399            base: BasePath {
1400                from: from.ignore_units(),
1401                to,
1402                tag: tag.clone(),
1403                units: new_sketch.units,
1404                geo_meta: GeoMeta {
1405                    id,
1406                    metadata: args.source_range.into(),
1407                },
1408            },
1409        };
1410
1411        if let Some(tag) = &tag {
1412            new_sketch.add_tag(tag, &current_path, exec_state, None);
1413        }
1414        new_sketch.paths.push(current_path);
1415    } else if tag.is_some() {
1416        exec_state.warn(
1417            crate::CompilationError {
1418                source_range: args.source_range,
1419                message: "A tag declarator was specified, but no segment was created".to_string(),
1420                suggestion: None,
1421                severity: crate::errors::Severity::Warning,
1422                tag: crate::errors::Tag::Unnecessary,
1423            },
1424            annotations::WARN_UNUSED_TAGS,
1425        );
1426    }
1427
1428    new_sketch.is_closed = ProfileClosed::Explicitly;
1429
1430    Ok(new_sketch)
1431}
1432
1433/// Draw an arc.
1434pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1435    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1436
1437    let angle_start: Option<TyF64> = args.get_kw_arg_opt("angleStart", &RuntimeType::degrees(), exec_state)?;
1438    let angle_end: Option<TyF64> = args.get_kw_arg_opt("angleEnd", &RuntimeType::degrees(), exec_state)?;
1439    let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1440    let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1441    let end_absolute: Option<[TyF64; 2]> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1442    let interior_absolute: Option<[TyF64; 2]> =
1443        args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1444    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1445    let new_sketch = inner_arc(
1446        sketch,
1447        angle_start,
1448        angle_end,
1449        radius,
1450        diameter,
1451        interior_absolute,
1452        end_absolute,
1453        tag,
1454        exec_state,
1455        args,
1456    )
1457    .await?;
1458    Ok(KclValue::Sketch {
1459        value: Box::new(new_sketch),
1460    })
1461}
1462
1463#[allow(clippy::too_many_arguments)]
1464pub(crate) async fn inner_arc(
1465    sketch: Sketch,
1466    angle_start: Option<TyF64>,
1467    angle_end: Option<TyF64>,
1468    radius: Option<TyF64>,
1469    diameter: Option<TyF64>,
1470    interior_absolute: Option<[TyF64; 2]>,
1471    end_absolute: Option<[TyF64; 2]>,
1472    tag: Option<TagNode>,
1473    exec_state: &mut ExecState,
1474    args: Args,
1475) -> Result<Sketch, KclError> {
1476    let from: Point2d = sketch.current_pen_position()?;
1477    let id = exec_state.next_uuid();
1478
1479    match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1480        (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1481            let radius = get_radius(radius, diameter, args.source_range)?;
1482            relative_arc(id, exec_state, sketch, from, angle_start, angle_end, radius, tag, true, &args.ctx, args.source_range).await
1483        }
1484        (None, None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1485            absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1486        }
1487        _ => {
1488            Err(KclError::new_type(KclErrorDetails::new(
1489                "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)".to_owned(),
1490                vec![args.source_range],
1491            )))
1492        }
1493    }
1494}
1495
1496#[allow(clippy::too_many_arguments)]
1497pub async fn absolute_arc(
1498    args: &Args,
1499    id: uuid::Uuid,
1500    exec_state: &mut ExecState,
1501    sketch: Sketch,
1502    from: Point2d,
1503    interior_absolute: [TyF64; 2],
1504    end_absolute: [TyF64; 2],
1505    tag: Option<TagNode>,
1506) -> Result<Sketch, KclError> {
1507    // The start point is taken from the path you are extending.
1508    exec_state
1509        .batch_modeling_cmd(
1510            ModelingCmdMeta::from_args_id(exec_state, args, id),
1511            ModelingCmd::from(
1512                mcmd::ExtendPath::builder()
1513                    .path(sketch.id.into())
1514                    .segment(PathSegment::ArcTo {
1515                        end: kcmc::shared::Point3d {
1516                            x: LengthUnit(end_absolute[0].to_mm()),
1517                            y: LengthUnit(end_absolute[1].to_mm()),
1518                            z: LengthUnit(0.0),
1519                        },
1520                        interior: kcmc::shared::Point3d {
1521                            x: LengthUnit(interior_absolute[0].to_mm()),
1522                            y: LengthUnit(interior_absolute[1].to_mm()),
1523                            z: LengthUnit(0.0),
1524                        },
1525                        relative: false,
1526                    })
1527                    .build(),
1528            ),
1529        )
1530        .await?;
1531
1532    let start = [from.x, from.y];
1533    let end = point_to_len_unit(end_absolute, from.units);
1534    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1535
1536    let current_path = Path::ArcThreePoint {
1537        base: BasePath {
1538            from: from.ignore_units(),
1539            to: end,
1540            tag: tag.clone(),
1541            units: sketch.units,
1542            geo_meta: GeoMeta {
1543                id,
1544                metadata: args.source_range.into(),
1545            },
1546        },
1547        p1: start,
1548        p2: point_to_len_unit(interior_absolute, from.units),
1549        p3: end,
1550    };
1551
1552    let mut new_sketch = sketch;
1553    if let Some(tag) = &tag {
1554        new_sketch.add_tag(tag, &current_path, exec_state, None);
1555    }
1556    if loops_back_to_start {
1557        new_sketch.is_closed = ProfileClosed::Implicitly;
1558    }
1559
1560    new_sketch.paths.push(current_path);
1561
1562    Ok(new_sketch)
1563}
1564
1565#[allow(clippy::too_many_arguments)]
1566pub async fn relative_arc(
1567    id: uuid::Uuid,
1568    exec_state: &mut ExecState,
1569    sketch: Sketch,
1570    from: Point2d,
1571    angle_start: TyF64,
1572    angle_end: TyF64,
1573    radius: TyF64,
1574    tag: Option<TagNode>,
1575    send_to_engine: bool,
1576    ctx: &ExecutorContext,
1577    source_range: SourceRange,
1578) -> Result<Sketch, KclError> {
1579    let a_start = Angle::from_degrees(angle_start.to_degrees(exec_state, source_range));
1580    let a_end = Angle::from_degrees(angle_end.to_degrees(exec_state, source_range));
1581    let radius = radius.to_length_units(from.units);
1582    let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1583    if a_start == a_end {
1584        return Err(KclError::new_type(KclErrorDetails::new(
1585            "Arc start and end angles must be different".to_string(),
1586            vec![source_range],
1587        )));
1588    }
1589    let ccw = a_start < a_end;
1590
1591    if send_to_engine {
1592        exec_state
1593            .batch_modeling_cmd(
1594                ModelingCmdMeta::with_id(exec_state, ctx, source_range, id),
1595                ModelingCmd::from(
1596                    mcmd::ExtendPath::builder()
1597                        .path(sketch.id.into())
1598                        .segment(PathSegment::Arc {
1599                            start: a_start,
1600                            end: a_end,
1601                            center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1602                            radius: LengthUnit(
1603                                crate::execution::types::adjust_length(from.units, radius, UnitLength::Millimeters).0,
1604                            ),
1605                            relative: false,
1606                        })
1607                        .build(),
1608                ),
1609            )
1610            .await?;
1611    }
1612
1613    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1614    let current_path = Path::Arc {
1615        base: BasePath {
1616            from: from.ignore_units(),
1617            to: end,
1618            tag: tag.clone(),
1619            units: from.units,
1620            geo_meta: GeoMeta {
1621                id,
1622                metadata: source_range.into(),
1623            },
1624        },
1625        center,
1626        radius,
1627        ccw,
1628    };
1629
1630    let mut new_sketch = sketch;
1631    if let Some(tag) = &tag {
1632        new_sketch.add_tag(tag, &current_path, exec_state, None);
1633    }
1634    if loops_back_to_start {
1635        new_sketch.is_closed = ProfileClosed::Implicitly;
1636    }
1637
1638    new_sketch.paths.push(current_path);
1639
1640    Ok(new_sketch)
1641}
1642
1643/// Draw a tangential arc to a specific point.
1644pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1645    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1646    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1647    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1648    let radius = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1649    let diameter = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1650    let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
1651    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1652
1653    let new_sketch = inner_tangential_arc(
1654        sketch,
1655        end_absolute,
1656        end,
1657        radius,
1658        diameter,
1659        angle,
1660        tag,
1661        exec_state,
1662        args,
1663    )
1664    .await?;
1665    Ok(KclValue::Sketch {
1666        value: Box::new(new_sketch),
1667    })
1668}
1669
1670#[allow(clippy::too_many_arguments)]
1671async fn inner_tangential_arc(
1672    sketch: Sketch,
1673    end_absolute: Option<[TyF64; 2]>,
1674    end: Option<[TyF64; 2]>,
1675    radius: Option<TyF64>,
1676    diameter: Option<TyF64>,
1677    angle: Option<TyF64>,
1678    tag: Option<TagNode>,
1679    exec_state: &mut ExecState,
1680    args: Args,
1681) -> Result<Sketch, KclError> {
1682    match (end_absolute, end, radius, diameter, angle) {
1683        (Some(point), None, None, None, None) => {
1684            inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1685        }
1686        (None, Some(point), None, None, None) => {
1687            inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1688        }
1689        (None, None, radius, diameter, Some(angle)) => {
1690            let radius = get_radius(radius, diameter, args.source_range)?;
1691            let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1692            inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1693        }
1694        (Some(_), Some(_), None, None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
1695            "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1696            vec![args.source_range],
1697        ))),
1698        (_, _, _, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1699            "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1700            vec![args.source_range],
1701        ))),
1702    }
1703}
1704
1705/// Data to draw a tangential arc.
1706#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1707#[ts(export)]
1708#[serde(rename_all = "camelCase", untagged)]
1709pub enum TangentialArcData {
1710    RadiusAndOffset {
1711        /// Radius of the arc.
1712        /// Not to be confused with Raiders of the Lost Ark.
1713        radius: TyF64,
1714        /// Offset of the arc, in degrees.
1715        offset: TyF64,
1716    },
1717}
1718
1719/// Draw a curved line segment along part of an imaginary circle.
1720///
1721/// The arc is constructed such that the last line segment is placed tangent
1722/// to the imaginary circle of the specified radius. The resulting arc is the
1723/// segment of the imaginary circle from that tangent point for 'angle'
1724/// degrees along the imaginary circle.
1725async fn inner_tangential_arc_radius_angle(
1726    data: TangentialArcData,
1727    sketch: Sketch,
1728    tag: Option<TagNode>,
1729    exec_state: &mut ExecState,
1730    args: Args,
1731) -> Result<Sketch, KclError> {
1732    let from: Point2d = sketch.current_pen_position()?;
1733    // next set of lines is some undocumented voodoo from get_tangential_arc_to_info
1734    let tangent_info = sketch.get_tangential_info_from_paths(); //this function desperately needs some documentation
1735    let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1736
1737    let id = exec_state.next_uuid();
1738
1739    let (center, to, ccw) = match data {
1740        TangentialArcData::RadiusAndOffset { radius, offset } => {
1741            // KCL stdlib types use degrees.
1742            let offset = Angle::from_degrees(offset.to_degrees(exec_state, args.source_range));
1743
1744            // Calculate the end point from the angle and radius.
1745            // atan2 outputs radians.
1746            let previous_end_tangent = Angle::from_radians(libm::atan2(
1747                from.y - tan_previous_point[1],
1748                from.x - tan_previous_point[0],
1749            ));
1750            // make sure the arc center is on the correct side to guarantee deterministic behavior
1751            // note the engine automatically rejects an offset of zero, if we want to flag that at KCL too to avoid engine errors
1752            let ccw = offset.to_degrees() > 0.0;
1753            let tangent_to_arc_start_angle = if ccw {
1754                // CCW turn
1755                Angle::from_degrees(-90.0)
1756            } else {
1757                // CW turn
1758                Angle::from_degrees(90.0)
1759            };
1760            // may need some logic and / or modulo on the various angle values to prevent them from going "backwards"
1761            // but the above logic *should* capture that behavior
1762            let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1763            let end_angle = start_angle + offset;
1764            let (center, to) = arc_center_and_end(
1765                from.ignore_units(),
1766                start_angle,
1767                end_angle,
1768                radius.to_length_units(from.units),
1769            );
1770
1771            exec_state
1772                .batch_modeling_cmd(
1773                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
1774                    ModelingCmd::from(
1775                        mcmd::ExtendPath::builder()
1776                            .path(sketch.id.into())
1777                            .segment(PathSegment::TangentialArc {
1778                                radius: LengthUnit(radius.to_mm()),
1779                                offset,
1780                            })
1781                            .build(),
1782                    ),
1783                )
1784                .await?;
1785            (center, to, ccw)
1786        }
1787    };
1788    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1789
1790    let current_path = Path::TangentialArc {
1791        ccw,
1792        center,
1793        base: BasePath {
1794            from: from.ignore_units(),
1795            to,
1796            tag: tag.clone(),
1797            units: sketch.units,
1798            geo_meta: GeoMeta {
1799                id,
1800                metadata: args.source_range.into(),
1801            },
1802        },
1803    };
1804
1805    let mut new_sketch = sketch;
1806    if let Some(tag) = &tag {
1807        new_sketch.add_tag(tag, &current_path, exec_state, None);
1808    }
1809    if loops_back_to_start {
1810        new_sketch.is_closed = ProfileClosed::Implicitly;
1811    }
1812
1813    new_sketch.paths.push(current_path);
1814
1815    Ok(new_sketch)
1816}
1817
1818// `to` must be in sketch.units
1819fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
1820    ModelingCmd::from(
1821        mcmd::ExtendPath::builder()
1822            .path(sketch.id.into())
1823            .segment(PathSegment::TangentialArcTo {
1824                angle_snap_increment: None,
1825                to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
1826                    .with_z(0.0)
1827                    .map(LengthUnit),
1828            })
1829            .build(),
1830    )
1831}
1832
1833async fn inner_tangential_arc_to_point(
1834    sketch: Sketch,
1835    point: [TyF64; 2],
1836    is_absolute: bool,
1837    tag: Option<TagNode>,
1838    exec_state: &mut ExecState,
1839    args: Args,
1840) -> Result<Sketch, KclError> {
1841    let from: Point2d = sketch.current_pen_position()?;
1842    let tangent_info = sketch.get_tangential_info_from_paths();
1843    let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1844
1845    let point = point_to_len_unit(point, from.units);
1846
1847    let to = if is_absolute {
1848        point
1849    } else {
1850        [from.x + point[0], from.y + point[1]]
1851    };
1852    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1853    let [to_x, to_y] = to;
1854    let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1855        arc_start_point: [from.x, from.y],
1856        arc_end_point: [to_x, to_y],
1857        tan_previous_point,
1858        obtuse: true,
1859    });
1860
1861    if result.center[0].is_infinite() {
1862        return Err(KclError::new_semantic(KclErrorDetails::new(
1863            "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
1864                .to_owned(),
1865            vec![args.source_range],
1866        )));
1867    } else if result.center[1].is_infinite() {
1868        return Err(KclError::new_semantic(KclErrorDetails::new(
1869            "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
1870                .to_owned(),
1871            vec![args.source_range],
1872        )));
1873    }
1874
1875    let delta = if is_absolute {
1876        [to_x - from.x, to_y - from.y]
1877    } else {
1878        point
1879    };
1880    let id = exec_state.next_uuid();
1881    exec_state
1882        .batch_modeling_cmd(
1883            ModelingCmdMeta::from_args_id(exec_state, &args, id),
1884            tan_arc_to(&sketch, delta),
1885        )
1886        .await?;
1887
1888    let current_path = Path::TangentialArcTo {
1889        base: BasePath {
1890            from: from.ignore_units(),
1891            to,
1892            tag: tag.clone(),
1893            units: sketch.units,
1894            geo_meta: GeoMeta {
1895                id,
1896                metadata: args.source_range.into(),
1897            },
1898        },
1899        center: result.center,
1900        ccw: result.ccw > 0,
1901    };
1902
1903    let mut new_sketch = sketch;
1904    if let Some(tag) = &tag {
1905        new_sketch.add_tag(tag, &current_path, exec_state, None);
1906    }
1907    if loops_back_to_start {
1908        new_sketch.is_closed = ProfileClosed::Implicitly;
1909    }
1910
1911    new_sketch.paths.push(current_path);
1912
1913    Ok(new_sketch)
1914}
1915
1916/// Draw a bezier curve.
1917pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1918    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1919    let control1 = args.get_kw_arg_opt("control1", &RuntimeType::point2d(), exec_state)?;
1920    let control2 = args.get_kw_arg_opt("control2", &RuntimeType::point2d(), exec_state)?;
1921    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1922    let control1_absolute = args.get_kw_arg_opt("control1Absolute", &RuntimeType::point2d(), exec_state)?;
1923    let control2_absolute = args.get_kw_arg_opt("control2Absolute", &RuntimeType::point2d(), exec_state)?;
1924    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1925    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1926
1927    let new_sketch = inner_bezier_curve(
1928        sketch,
1929        control1,
1930        control2,
1931        end,
1932        control1_absolute,
1933        control2_absolute,
1934        end_absolute,
1935        tag,
1936        exec_state,
1937        args,
1938    )
1939    .await?;
1940    Ok(KclValue::Sketch {
1941        value: Box::new(new_sketch),
1942    })
1943}
1944
1945#[allow(clippy::too_many_arguments)]
1946async fn inner_bezier_curve(
1947    sketch: Sketch,
1948    control1: Option<[TyF64; 2]>,
1949    control2: Option<[TyF64; 2]>,
1950    end: Option<[TyF64; 2]>,
1951    control1_absolute: Option<[TyF64; 2]>,
1952    control2_absolute: Option<[TyF64; 2]>,
1953    end_absolute: Option<[TyF64; 2]>,
1954    tag: Option<TagNode>,
1955    exec_state: &mut ExecState,
1956    args: Args,
1957) -> Result<Sketch, KclError> {
1958    let from = sketch.current_pen_position()?;
1959    let id = exec_state.next_uuid();
1960
1961    let (to, control1_abs, control2_abs) = match (
1962        control1,
1963        control2,
1964        end,
1965        control1_absolute,
1966        control2_absolute,
1967        end_absolute,
1968    ) {
1969        // Relative
1970        (Some(control1), Some(control2), Some(end), None, None, None) => {
1971            let delta = end.clone();
1972            let to = [
1973                from.x + end[0].to_length_units(from.units),
1974                from.y + end[1].to_length_units(from.units),
1975            ];
1976            // Calculate absolute control points
1977            let control1_abs = [
1978                from.x + control1[0].to_length_units(from.units),
1979                from.y + control1[1].to_length_units(from.units),
1980            ];
1981            let control2_abs = [
1982                from.x + control2[0].to_length_units(from.units),
1983                from.y + control2[1].to_length_units(from.units),
1984            ];
1985
1986            exec_state
1987                .batch_modeling_cmd(
1988                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
1989                    ModelingCmd::from(
1990                        mcmd::ExtendPath::builder()
1991                            .path(sketch.id.into())
1992                            .segment(PathSegment::Bezier {
1993                                control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1994                                control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1995                                end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
1996                                relative: true,
1997                            })
1998                            .build(),
1999                    ),
2000                )
2001                .await?;
2002            (to, control1_abs, control2_abs)
2003        }
2004        // Absolute
2005        (None, None, None, Some(control1), Some(control2), Some(end)) => {
2006            let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
2007            let control1_abs = control1.clone().map(|v| v.to_length_units(from.units));
2008            let control2_abs = control2.clone().map(|v| v.to_length_units(from.units));
2009            exec_state
2010                .batch_modeling_cmd(
2011                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
2012                    ModelingCmd::from(
2013                        mcmd::ExtendPath::builder()
2014                            .path(sketch.id.into())
2015                            .segment(PathSegment::Bezier {
2016                                control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2017                                control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2018                                end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
2019                                relative: false,
2020                            })
2021                            .build(),
2022                    ),
2023                )
2024                .await?;
2025            (to, control1_abs, control2_abs)
2026        }
2027        _ => {
2028            return Err(KclError::new_semantic(KclErrorDetails::new(
2029                "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
2030                vec![args.source_range],
2031            )));
2032        }
2033    };
2034
2035    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2036
2037    let current_path = Path::Bezier {
2038        base: BasePath {
2039            from: from.ignore_units(),
2040            to,
2041            tag: tag.clone(),
2042            units: sketch.units,
2043            geo_meta: GeoMeta {
2044                id,
2045                metadata: args.source_range.into(),
2046            },
2047        },
2048        control1: control1_abs,
2049        control2: control2_abs,
2050    };
2051
2052    let mut new_sketch = sketch;
2053    if let Some(tag) = &tag {
2054        new_sketch.add_tag(tag, &current_path, exec_state, None);
2055    }
2056    if loops_back_to_start {
2057        new_sketch.is_closed = ProfileClosed::Implicitly;
2058    }
2059
2060    new_sketch.paths.push(current_path);
2061
2062    Ok(new_sketch)
2063}
2064
2065/// Use a sketch to cut a hole in another sketch.
2066pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2067    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2068
2069    let tool: Vec<Sketch> = args.get_kw_arg(
2070        "tool",
2071        &RuntimeType::Array(
2072            Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
2073            ArrayLen::Minimum(1),
2074        ),
2075        exec_state,
2076    )?;
2077
2078    let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
2079    Ok(KclValue::Sketch {
2080        value: Box::new(new_sketch),
2081    })
2082}
2083
2084async fn inner_subtract_2d(
2085    mut sketch: Sketch,
2086    tool: Vec<Sketch>,
2087    exec_state: &mut ExecState,
2088    args: Args,
2089) -> Result<Sketch, KclError> {
2090    for hole_sketch in tool {
2091        exec_state
2092            .batch_modeling_cmd(
2093                ModelingCmdMeta::from_args(exec_state, &args),
2094                ModelingCmd::from(
2095                    mcmd::Solid2dAddHole::builder()
2096                        .object_id(sketch.id)
2097                        .hole_id(hole_sketch.id)
2098                        .build(),
2099                ),
2100            )
2101            .await?;
2102
2103        // Hide the source hole since it's no longer its own profile,
2104        // it's just used to modify some other profile.
2105        exec_state
2106            .batch_modeling_cmd(
2107                ModelingCmdMeta::from_args(exec_state, &args),
2108                ModelingCmd::from(
2109                    mcmd::ObjectVisible::builder()
2110                        .object_id(hole_sketch.id)
2111                        .hidden(true)
2112                        .build(),
2113                ),
2114            )
2115            .await?;
2116
2117        // NOTE: We don't look at the inner paths of the hole/tool sketch.
2118        // So if you have circle A, and it has a circular hole cut out (B),
2119        // then you cut A out of an even bigger circle C, we will lose that info.
2120        // Not really sure what to do about this.
2121        sketch.inner_paths.extend_from_slice(&hole_sketch.paths);
2122    }
2123
2124    // Returns the input sketch, exactly as it was, zero modifications.
2125    // This means the edges from `tool` are basically ignored, they're not in the output.
2126    Ok(sketch)
2127}
2128
2129/// Calculate the (x, y) point on an ellipse given x or y and the major/minor radii of the ellipse.
2130pub async fn elliptic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2131    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2132    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2133    let major_radius = args.get_kw_arg("majorRadius", &RuntimeType::num_any(), exec_state)?;
2134    let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::num_any(), exec_state)?;
2135
2136    let elliptic_point = inner_elliptic_point(x, y, major_radius, minor_radius, &args).await?;
2137
2138    args.make_kcl_val_from_point(elliptic_point, exec_state.length_unit().into())
2139}
2140
2141async fn inner_elliptic_point(
2142    x: Option<TyF64>,
2143    y: Option<TyF64>,
2144    major_radius: TyF64,
2145    minor_radius: TyF64,
2146    args: &Args,
2147) -> Result<[f64; 2], KclError> {
2148    let major_radius = major_radius.n;
2149    let minor_radius = minor_radius.n;
2150    if let Some(x) = x {
2151        if x.n.abs() > major_radius {
2152            Err(KclError::Type {
2153                details: KclErrorDetails::new(
2154                    format!(
2155                        "Invalid input. The x value, {}, cannot be larger than the major radius {}.",
2156                        x.n, major_radius
2157                    ),
2158                    vec![args.source_range],
2159                ),
2160            })
2161        } else {
2162            Ok((
2163                x.n,
2164                minor_radius * (1.0 - x.n.powf(2.0) / major_radius.powf(2.0)).sqrt(),
2165            )
2166                .into())
2167        }
2168    } else if let Some(y) = y {
2169        if y.n > minor_radius {
2170            Err(KclError::Type {
2171                details: KclErrorDetails::new(
2172                    format!(
2173                        "Invalid input. The y value, {}, cannot be larger than the minor radius {}.",
2174                        y.n, minor_radius
2175                    ),
2176                    vec![args.source_range],
2177                ),
2178            })
2179        } else {
2180            Ok((
2181                major_radius * (1.0 - y.n.powf(2.0) / minor_radius.powf(2.0)).sqrt(),
2182                y.n,
2183            )
2184                .into())
2185        }
2186    } else {
2187        Err(KclError::Type {
2188            details: KclErrorDetails::new(
2189                "Invalid input. Must have either x or y, you cannot have both or neither.".to_owned(),
2190                vec![args.source_range],
2191            ),
2192        })
2193    }
2194}
2195
2196/// Draw an elliptical arc.
2197pub async fn elliptic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2198    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2199
2200    let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
2201    let angle_start = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
2202    let angle_end = args.get_kw_arg("angleEnd", &RuntimeType::degrees(), exec_state)?;
2203    let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
2204    let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
2205    let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
2206    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2207
2208    let new_sketch = inner_elliptic(
2209        sketch,
2210        center,
2211        angle_start,
2212        angle_end,
2213        major_radius,
2214        major_axis,
2215        minor_radius,
2216        tag,
2217        exec_state,
2218        args,
2219    )
2220    .await?;
2221    Ok(KclValue::Sketch {
2222        value: Box::new(new_sketch),
2223    })
2224}
2225
2226#[allow(clippy::too_many_arguments)]
2227pub(crate) async fn inner_elliptic(
2228    sketch: Sketch,
2229    center: [TyF64; 2],
2230    angle_start: TyF64,
2231    angle_end: TyF64,
2232    major_radius: Option<TyF64>,
2233    major_axis: Option<[TyF64; 2]>,
2234    minor_radius: TyF64,
2235    tag: Option<TagNode>,
2236    exec_state: &mut ExecState,
2237    args: Args,
2238) -> Result<Sketch, KclError> {
2239    let from: Point2d = sketch.current_pen_position()?;
2240    let id = exec_state.next_uuid();
2241
2242    let center_u = point_to_len_unit(center, from.units);
2243
2244    let major_axis = match (major_axis, major_radius) {
2245        (Some(_), Some(_)) | (None, None) => {
2246            return Err(KclError::new_type(KclErrorDetails::new(
2247                "Provide either `majorAxis` or `majorRadius`.".to_string(),
2248                vec![args.source_range],
2249            )));
2250        }
2251        (Some(major_axis), None) => major_axis,
2252        (None, Some(major_radius)) => [
2253            major_radius.clone(),
2254            TyF64 {
2255                n: 0.0,
2256                ty: major_radius.ty,
2257            },
2258        ],
2259    };
2260    let start_angle = Angle::from_degrees(angle_start.to_degrees(exec_state, args.source_range));
2261    let end_angle = Angle::from_degrees(angle_end.to_degrees(exec_state, args.source_range));
2262    let major_axis_magnitude = (major_axis[0].to_length_units(from.units) * major_axis[0].to_length_units(from.units)
2263        + major_axis[1].to_length_units(from.units) * major_axis[1].to_length_units(from.units))
2264    .sqrt();
2265    let to = [
2266        major_axis_magnitude * libm::cos(end_angle.to_radians()),
2267        minor_radius.to_length_units(from.units) * libm::sin(end_angle.to_radians()),
2268    ];
2269    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2270    let major_axis_angle = libm::atan2(major_axis[1].n, major_axis[0].n);
2271
2272    let point = [
2273        center_u[0] + to[0] * libm::cos(major_axis_angle) - to[1] * libm::sin(major_axis_angle),
2274        center_u[1] + to[0] * libm::sin(major_axis_angle) + to[1] * libm::cos(major_axis_angle),
2275    ];
2276
2277    let axis = major_axis.map(|x| x.to_mm());
2278    exec_state
2279        .batch_modeling_cmd(
2280            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2281            ModelingCmd::from(
2282                mcmd::ExtendPath::builder()
2283                    .path(sketch.id.into())
2284                    .segment(PathSegment::Ellipse {
2285                        center: KPoint2d::from(untyped_point_to_mm(center_u, from.units)).map(LengthUnit),
2286                        major_axis: axis.map(LengthUnit).into(),
2287                        minor_radius: LengthUnit(minor_radius.to_mm()),
2288                        start_angle,
2289                        end_angle,
2290                    })
2291                    .build(),
2292            ),
2293        )
2294        .await?;
2295
2296    let current_path = Path::Ellipse {
2297        ccw: start_angle < end_angle,
2298        center: center_u,
2299        major_axis: axis,
2300        minor_radius: minor_radius.to_mm(),
2301        base: BasePath {
2302            from: from.ignore_units(),
2303            to: point,
2304            tag: tag.clone(),
2305            units: sketch.units,
2306            geo_meta: GeoMeta {
2307                id,
2308                metadata: args.source_range.into(),
2309            },
2310        },
2311    };
2312    let mut new_sketch = sketch;
2313    if let Some(tag) = &tag {
2314        new_sketch.add_tag(tag, &current_path, exec_state, None);
2315    }
2316    if loops_back_to_start {
2317        new_sketch.is_closed = ProfileClosed::Implicitly;
2318    }
2319
2320    new_sketch.paths.push(current_path);
2321
2322    Ok(new_sketch)
2323}
2324
2325/// Calculate the (x, y) point on an hyperbola given x or y and the semi major/minor of the ellipse.
2326pub async fn hyperbolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2327    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2328    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2329    let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::num_any(), exec_state)?;
2330    let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::num_any(), exec_state)?;
2331
2332    let hyperbolic_point = inner_hyperbolic_point(x, y, semi_major, semi_minor, &args).await?;
2333
2334    args.make_kcl_val_from_point(hyperbolic_point, exec_state.length_unit().into())
2335}
2336
2337async fn inner_hyperbolic_point(
2338    x: Option<TyF64>,
2339    y: Option<TyF64>,
2340    semi_major: TyF64,
2341    semi_minor: TyF64,
2342    args: &Args,
2343) -> Result<[f64; 2], KclError> {
2344    let semi_major = semi_major.n;
2345    let semi_minor = semi_minor.n;
2346    if let Some(x) = x {
2347        if x.n.abs() < semi_major {
2348            Err(KclError::Type {
2349                details: KclErrorDetails::new(
2350                    format!(
2351                        "Invalid input. The x value, {}, cannot be less than the semi major value, {}.",
2352                        x.n, semi_major
2353                    ),
2354                    vec![args.source_range],
2355                ),
2356            })
2357        } else {
2358            Ok((x.n, semi_minor * (x.n.powf(2.0) / semi_major.powf(2.0) - 1.0).sqrt()).into())
2359        }
2360    } else if let Some(y) = y {
2361        Ok((semi_major * (y.n.powf(2.0) / semi_minor.powf(2.0) + 1.0).sqrt(), y.n).into())
2362    } else {
2363        Err(KclError::Type {
2364            details: KclErrorDetails::new(
2365                "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2366                vec![args.source_range],
2367            ),
2368        })
2369    }
2370}
2371
2372/// Draw a hyperbolic arc.
2373pub async fn hyperbolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2374    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2375
2376    let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::length(), exec_state)?;
2377    let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::length(), exec_state)?;
2378    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2379    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2380    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2381    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2382    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2383
2384    let new_sketch = inner_hyperbolic(
2385        sketch,
2386        semi_major,
2387        semi_minor,
2388        interior,
2389        end,
2390        interior_absolute,
2391        end_absolute,
2392        tag,
2393        exec_state,
2394        args,
2395    )
2396    .await?;
2397    Ok(KclValue::Sketch {
2398        value: Box::new(new_sketch),
2399    })
2400}
2401
2402/// Calculate the tangent of a hyperbolic given a point on the curve
2403fn hyperbolic_tangent(point: Point2d, semi_major: f64, semi_minor: f64) -> [f64; 2] {
2404    (point.y * semi_major.powf(2.0), point.x * semi_minor.powf(2.0)).into()
2405}
2406
2407#[allow(clippy::too_many_arguments)]
2408pub(crate) async fn inner_hyperbolic(
2409    sketch: Sketch,
2410    semi_major: TyF64,
2411    semi_minor: TyF64,
2412    interior: Option<[TyF64; 2]>,
2413    end: Option<[TyF64; 2]>,
2414    interior_absolute: Option<[TyF64; 2]>,
2415    end_absolute: Option<[TyF64; 2]>,
2416    tag: Option<TagNode>,
2417    exec_state: &mut ExecState,
2418    args: Args,
2419) -> Result<Sketch, KclError> {
2420    let from = sketch.current_pen_position()?;
2421    let id = exec_state.next_uuid();
2422
2423    let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2424        (Some(interior), Some(end), None, None) => (interior, end, true),
2425        (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2426        _ => return Err(KclError::Type {
2427            details: KclErrorDetails::new(
2428                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2429                    .to_owned(),
2430                vec![args.source_range],
2431            ),
2432        }),
2433    };
2434
2435    let interior = point_to_len_unit(interior, from.units);
2436    let end = point_to_len_unit(end, from.units);
2437    let end_point = Point2d {
2438        x: end[0],
2439        y: end[1],
2440        units: from.units,
2441    };
2442    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
2443
2444    let semi_major_u = semi_major.to_length_units(from.units);
2445    let semi_minor_u = semi_minor.to_length_units(from.units);
2446
2447    let start_tangent = hyperbolic_tangent(from, semi_major_u, semi_minor_u);
2448    let end_tangent = hyperbolic_tangent(end_point, semi_major_u, semi_minor_u);
2449
2450    exec_state
2451        .batch_modeling_cmd(
2452            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2453            ModelingCmd::from(
2454                mcmd::ExtendPath::builder()
2455                    .path(sketch.id.into())
2456                    .segment(PathSegment::ConicTo {
2457                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2458                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2459                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2460                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2461                        relative,
2462                    })
2463                    .build(),
2464            ),
2465        )
2466        .await?;
2467
2468    let current_path = Path::Conic {
2469        base: BasePath {
2470            from: from.ignore_units(),
2471            to: end,
2472            tag: tag.clone(),
2473            units: sketch.units,
2474            geo_meta: GeoMeta {
2475                id,
2476                metadata: args.source_range.into(),
2477            },
2478        },
2479    };
2480
2481    let mut new_sketch = sketch;
2482    if let Some(tag) = &tag {
2483        new_sketch.add_tag(tag, &current_path, exec_state, None);
2484    }
2485    if loops_back_to_start {
2486        new_sketch.is_closed = ProfileClosed::Implicitly;
2487    }
2488
2489    new_sketch.paths.push(current_path);
2490
2491    Ok(new_sketch)
2492}
2493
2494/// Calculate the point on a parabola given the coefficient of the parabola and either x or y
2495pub async fn parabolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2496    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2497    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2498    let coefficients = args.get_kw_arg(
2499        "coefficients",
2500        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2501        exec_state,
2502    )?;
2503
2504    let parabolic_point = inner_parabolic_point(x, y, &coefficients, &args).await?;
2505
2506    args.make_kcl_val_from_point(parabolic_point, exec_state.length_unit().into())
2507}
2508
2509async fn inner_parabolic_point(
2510    x: Option<TyF64>,
2511    y: Option<TyF64>,
2512    coefficients: &[TyF64; 3],
2513    args: &Args,
2514) -> Result<[f64; 2], KclError> {
2515    let a = coefficients[0].n;
2516    let b = coefficients[1].n;
2517    let c = coefficients[2].n;
2518    if let Some(x) = x {
2519        Ok((x.n, a * x.n.powf(2.0) + b * x.n + c).into())
2520    } else if let Some(y) = y {
2521        let det = (b.powf(2.0) - 4.0 * a * (c - y.n)).sqrt();
2522        Ok(((-b + det) / (2.0 * a), y.n).into())
2523    } else {
2524        Err(KclError::Type {
2525            details: KclErrorDetails::new(
2526                "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2527                vec![args.source_range],
2528            ),
2529        })
2530    }
2531}
2532
2533/// Draw a parabolic arc.
2534pub async fn parabolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2535    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2536
2537    let coefficients = args.get_kw_arg_opt(
2538        "coefficients",
2539        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2540        exec_state,
2541    )?;
2542    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2543    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2544    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2545    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2546    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2547
2548    let new_sketch = inner_parabolic(
2549        sketch,
2550        coefficients,
2551        interior,
2552        end,
2553        interior_absolute,
2554        end_absolute,
2555        tag,
2556        exec_state,
2557        args,
2558    )
2559    .await?;
2560    Ok(KclValue::Sketch {
2561        value: Box::new(new_sketch),
2562    })
2563}
2564
2565fn parabolic_tangent(point: Point2d, a: f64, b: f64) -> [f64; 2] {
2566    //f(x) = ax^2 + bx + c
2567    //f'(x) = 2ax + b
2568    (1.0, 2.0 * a * point.x + b).into()
2569}
2570
2571#[allow(clippy::too_many_arguments)]
2572pub(crate) async fn inner_parabolic(
2573    sketch: Sketch,
2574    coefficients: Option<[TyF64; 3]>,
2575    interior: Option<[TyF64; 2]>,
2576    end: Option<[TyF64; 2]>,
2577    interior_absolute: Option<[TyF64; 2]>,
2578    end_absolute: Option<[TyF64; 2]>,
2579    tag: Option<TagNode>,
2580    exec_state: &mut ExecState,
2581    args: Args,
2582) -> Result<Sketch, KclError> {
2583    let from = sketch.current_pen_position()?;
2584    let id = exec_state.next_uuid();
2585
2586    if (coefficients.is_some() && interior.is_some()) || (coefficients.is_none() && interior.is_none()) {
2587        return Err(KclError::Type {
2588            details: KclErrorDetails::new(
2589                "Invalid combination of arguments. Either provide (a, b, c) or (interior)".to_owned(),
2590                vec![args.source_range],
2591            ),
2592        });
2593    }
2594
2595    let (interior, end, relative) = match (coefficients.clone(), interior, end, interior_absolute, end_absolute) {
2596        (None, Some(interior), Some(end), None, None) => {
2597            let interior = point_to_len_unit(interior, from.units);
2598            let end = point_to_len_unit(end, from.units);
2599            (interior,end, true)
2600        },
2601        (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
2602            let interior_absolute = point_to_len_unit(interior_absolute, from.units);
2603            let end_absolute = point_to_len_unit(end_absolute, from.units);
2604            (interior_absolute, end_absolute, false)
2605        }
2606        (Some(coefficients), _, Some(end), _, _) => {
2607            let end = point_to_len_unit(end, from.units);
2608            let interior =
2609            inner_parabolic_point(
2610                Some(TyF64::count(0.5 * (from.x + end[0]))),
2611                None,
2612                &coefficients,
2613                &args,
2614            )
2615            .await?;
2616            (interior, end, true)
2617        }
2618        (Some(coefficients), _, _, _, Some(end)) => {
2619            let end = point_to_len_unit(end, from.units);
2620            let interior =
2621            inner_parabolic_point(
2622                Some(TyF64::count(0.5 * (from.x + end[0]))),
2623                None,
2624                &coefficients,
2625                &args,
2626            )
2627            .await?;
2628            (interior, end, false)
2629        }
2630        _ => return
2631            Err(KclError::Type{details: KclErrorDetails::new(
2632                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute) if coefficients are not provided."
2633                    .to_owned(),
2634                vec![args.source_range],
2635            )}),
2636    };
2637
2638    let end_point = Point2d {
2639        x: end[0],
2640        y: end[1],
2641        units: from.units,
2642    };
2643
2644    let (a, b, _c) = if let Some([a, b, c]) = coefficients {
2645        (a.n, b.n, c.n)
2646    } else {
2647        // Any three points is enough to uniquely define a parabola
2648        let denom = (from.x - interior[0]) * (from.x - end_point.x) * (interior[0] - end_point.x);
2649        let a = (end_point.x * (interior[1] - from.y)
2650            + interior[0] * (from.y - end_point.y)
2651            + from.x * (end_point.y - interior[1]))
2652            / denom;
2653        let b = (end_point.x.powf(2.0) * (from.y - interior[1])
2654            + interior[0].powf(2.0) * (end_point.y - from.y)
2655            + from.x.powf(2.0) * (interior[1] - end_point.y))
2656            / denom;
2657        let c = (interior[0] * end_point.x * (interior[0] - end_point.x) * from.y
2658            + end_point.x * from.x * (end_point.x - from.x) * interior[1]
2659            + from.x * interior[0] * (from.x - interior[0]) * end_point.y)
2660            / denom;
2661
2662        (a, b, c)
2663    };
2664
2665    let start_tangent = parabolic_tangent(from, a, b);
2666    let end_tangent = parabolic_tangent(end_point, a, b);
2667
2668    exec_state
2669        .batch_modeling_cmd(
2670            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2671            ModelingCmd::from(
2672                mcmd::ExtendPath::builder()
2673                    .path(sketch.id.into())
2674                    .segment(PathSegment::ConicTo {
2675                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2676                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2677                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2678                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2679                        relative,
2680                    })
2681                    .build(),
2682            ),
2683        )
2684        .await?;
2685
2686    let current_path = Path::Conic {
2687        base: BasePath {
2688            from: from.ignore_units(),
2689            to: end,
2690            tag: tag.clone(),
2691            units: sketch.units,
2692            geo_meta: GeoMeta {
2693                id,
2694                metadata: args.source_range.into(),
2695            },
2696        },
2697    };
2698
2699    let mut new_sketch = sketch;
2700    if let Some(tag) = &tag {
2701        new_sketch.add_tag(tag, &current_path, exec_state, None);
2702    }
2703
2704    new_sketch.paths.push(current_path);
2705
2706    Ok(new_sketch)
2707}
2708
2709fn conic_tangent(coefficients: [f64; 6], point: [f64; 2]) -> [f64; 2] {
2710    let [a, b, c, d, e, _] = coefficients;
2711
2712    (
2713        c * point[0] + 2.0 * b * point[1] + e,
2714        -(2.0 * a * point[0] + c * point[1] + d),
2715    )
2716        .into()
2717}
2718
2719/// Draw a conic section
2720pub async fn conic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2721    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2722
2723    let start_tangent = args.get_kw_arg_opt("startTangent", &RuntimeType::point2d(), exec_state)?;
2724    let end_tangent = args.get_kw_arg_opt("endTangent", &RuntimeType::point2d(), exec_state)?;
2725    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2726    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2727    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2728    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2729    let coefficients = args.get_kw_arg_opt(
2730        "coefficients",
2731        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(6)),
2732        exec_state,
2733    )?;
2734    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2735
2736    let new_sketch = inner_conic(
2737        sketch,
2738        start_tangent,
2739        end,
2740        end_tangent,
2741        interior,
2742        coefficients,
2743        interior_absolute,
2744        end_absolute,
2745        tag,
2746        exec_state,
2747        args,
2748    )
2749    .await?;
2750    Ok(KclValue::Sketch {
2751        value: Box::new(new_sketch),
2752    })
2753}
2754
2755#[allow(clippy::too_many_arguments)]
2756pub(crate) async fn inner_conic(
2757    sketch: Sketch,
2758    start_tangent: Option<[TyF64; 2]>,
2759    end: Option<[TyF64; 2]>,
2760    end_tangent: Option<[TyF64; 2]>,
2761    interior: Option<[TyF64; 2]>,
2762    coefficients: Option<[TyF64; 6]>,
2763    interior_absolute: Option<[TyF64; 2]>,
2764    end_absolute: Option<[TyF64; 2]>,
2765    tag: Option<TagNode>,
2766    exec_state: &mut ExecState,
2767    args: Args,
2768) -> Result<Sketch, KclError> {
2769    let from: Point2d = sketch.current_pen_position()?;
2770    let id = exec_state.next_uuid();
2771
2772    if (coefficients.is_some() && (start_tangent.is_some() || end_tangent.is_some()))
2773        || (coefficients.is_none() && (start_tangent.is_none() && end_tangent.is_none()))
2774    {
2775        return Err(KclError::Type {
2776            details: KclErrorDetails::new(
2777                "Invalid combination of arguments. Either provide coefficients or (startTangent, endTangent)"
2778                    .to_owned(),
2779                vec![args.source_range],
2780            ),
2781        });
2782    }
2783
2784    let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2785        (Some(interior), Some(end), None, None) => (interior, end, true),
2786        (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2787        _ => return Err(KclError::Type {
2788            details: KclErrorDetails::new(
2789                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2790                    .to_owned(),
2791                vec![args.source_range],
2792            ),
2793        }),
2794    };
2795
2796    let end = point_to_len_unit(end, from.units);
2797    let interior = point_to_len_unit(interior, from.units);
2798
2799    let (start_tangent, end_tangent) = if let Some(coeffs) = coefficients {
2800        let (coeffs, _) = untype_array(coeffs);
2801        (conic_tangent(coeffs, [from.x, from.y]), conic_tangent(coeffs, end))
2802    } else {
2803        let start = if let Some(start_tangent) = start_tangent {
2804            point_to_len_unit(start_tangent, from.units)
2805        } else {
2806            let previous_point = sketch
2807                .get_tangential_info_from_paths()
2808                .tan_previous_point(from.ignore_units());
2809            let from = from.ignore_units();
2810            [from[0] - previous_point[0], from[1] - previous_point[1]]
2811        };
2812
2813        let Some(end_tangent) = end_tangent else {
2814            return Err(KclError::new_semantic(KclErrorDetails::new(
2815                "You must either provide either `coefficients` or `endTangent`.".to_owned(),
2816                vec![args.source_range],
2817            )));
2818        };
2819        let end_tan = point_to_len_unit(end_tangent, from.units);
2820        (start, end_tan)
2821    };
2822
2823    exec_state
2824        .batch_modeling_cmd(
2825            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2826            ModelingCmd::from(
2827                mcmd::ExtendPath::builder()
2828                    .path(sketch.id.into())
2829                    .segment(PathSegment::ConicTo {
2830                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2831                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2832                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2833                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2834                        relative,
2835                    })
2836                    .build(),
2837            ),
2838        )
2839        .await?;
2840
2841    let current_path = Path::Conic {
2842        base: BasePath {
2843            from: from.ignore_units(),
2844            to: end,
2845            tag: tag.clone(),
2846            units: sketch.units,
2847            geo_meta: GeoMeta {
2848                id,
2849                metadata: args.source_range.into(),
2850            },
2851        },
2852    };
2853
2854    let mut new_sketch = sketch;
2855    if let Some(tag) = &tag {
2856        new_sketch.add_tag(tag, &current_path, exec_state, None);
2857    }
2858
2859    new_sketch.paths.push(current_path);
2860
2861    Ok(new_sketch)
2862}
2863#[cfg(test)]
2864mod tests {
2865
2866    use pretty_assertions::assert_eq;
2867
2868    use crate::{
2869        execution::TagIdentifier,
2870        std::{sketch::PlaneData, utils::calculate_circle_center},
2871    };
2872
2873    #[test]
2874    fn test_deserialize_plane_data() {
2875        let data = PlaneData::XY;
2876        let mut str_json = serde_json::to_string(&data).unwrap();
2877        assert_eq!(str_json, "\"XY\"");
2878
2879        str_json = "\"YZ\"".to_string();
2880        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2881        assert_eq!(data, PlaneData::YZ);
2882
2883        str_json = "\"-YZ\"".to_string();
2884        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2885        assert_eq!(data, PlaneData::NegYZ);
2886
2887        str_json = "\"-xz\"".to_string();
2888        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2889        assert_eq!(data, PlaneData::NegXZ);
2890    }
2891
2892    #[test]
2893    fn test_deserialize_sketch_on_face_tag() {
2894        let data = "start";
2895        let mut str_json = serde_json::to_string(&data).unwrap();
2896        assert_eq!(str_json, "\"start\"");
2897
2898        str_json = "\"end\"".to_string();
2899        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2900        assert_eq!(
2901            data,
2902            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2903        );
2904
2905        str_json = serde_json::to_string(&TagIdentifier {
2906            value: "thing".to_string(),
2907            info: Vec::new(),
2908            meta: Default::default(),
2909        })
2910        .unwrap();
2911        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2912        assert_eq!(
2913            data,
2914            crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2915                value: "thing".to_string(),
2916                info: Vec::new(),
2917                meta: Default::default()
2918            }))
2919        );
2920
2921        str_json = "\"END\"".to_string();
2922        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2923        assert_eq!(
2924            data,
2925            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2926        );
2927
2928        str_json = "\"start\"".to_string();
2929        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2930        assert_eq!(
2931            data,
2932            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2933        );
2934
2935        str_json = "\"START\"".to_string();
2936        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2937        assert_eq!(
2938            data,
2939            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2940        );
2941    }
2942
2943    #[test]
2944    fn test_circle_center() {
2945        let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2946        assert_eq!(actual[0], 5.0);
2947        assert_eq!(actual[1], 0.0);
2948    }
2949}