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).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    args: &Args,
1104) -> Result<(), KclError> {
1105    if plane.is_initialized() {
1106        return Ok(());
1107    }
1108    #[cfg(feature = "artifact-graph")]
1109    {
1110        if let Some(existing_object_id) = exec_state.scene_object_id_by_artifact_id(ArtifactId::new(plane.id)) {
1111            plane.object_id = Some(existing_object_id);
1112            return Ok(());
1113        }
1114    }
1115
1116    let clobber = false;
1117    let size = LengthUnit(60.0);
1118    let hide = Some(true);
1119    let cmd = if let Some(hide) = hide {
1120        mcmd::MakePlane::builder()
1121            .clobber(clobber)
1122            .origin(plane.info.origin.into())
1123            .size(size)
1124            .x_axis(plane.info.x_axis.into())
1125            .y_axis(plane.info.y_axis.into())
1126            .hide(hide)
1127            .build()
1128    } else {
1129        mcmd::MakePlane::builder()
1130            .clobber(clobber)
1131            .origin(plane.info.origin.into())
1132            .size(size)
1133            .x_axis(plane.info.x_axis.into())
1134            .y_axis(plane.info.y_axis.into())
1135            .build()
1136    };
1137    exec_state
1138        .batch_modeling_cmd(
1139            ModelingCmdMeta::from_args_id(exec_state, args, plane.id),
1140            ModelingCmd::from(cmd),
1141        )
1142        .await?;
1143    let plane_object_id = exec_state.next_object_id();
1144    #[cfg(feature = "artifact-graph")]
1145    {
1146        let plane_object = crate::front::Object {
1147            id: plane_object_id,
1148            kind: crate::front::ObjectKind::Plane(crate::front::Plane::Object(plane_object_id)),
1149            label: Default::default(),
1150            comments: Default::default(),
1151            artifact_id: ArtifactId::new(plane.id),
1152            source: args.source_range.into(),
1153        };
1154        exec_state.add_scene_object(plane_object, args.source_range);
1155    }
1156    plane.object_id = Some(plane_object_id);
1157
1158    Ok(())
1159}
1160
1161/// Start a new profile at a given point.
1162pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1163    let sketch_surface = args.get_unlabeled_kw_arg(
1164        "startProfileOn",
1165        &RuntimeType::Union(vec![RuntimeType::plane(), RuntimeType::face()]),
1166        exec_state,
1167    )?;
1168    let start: [TyF64; 2] = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
1169    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1170
1171    let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, &args.ctx, args.source_range).await?;
1172    Ok(KclValue::Sketch {
1173        value: Box::new(sketch),
1174    })
1175}
1176
1177pub(crate) async fn inner_start_profile(
1178    sketch_surface: SketchSurface,
1179    at: [TyF64; 2],
1180    tag: Option<TagNode>,
1181    exec_state: &mut ExecState,
1182    ctx: &ExecutorContext,
1183    source_range: SourceRange,
1184) -> Result<Sketch, KclError> {
1185    let id = exec_state.next_uuid();
1186    create_sketch(id, sketch_surface, at, tag, true, exec_state, ctx, source_range).await
1187}
1188
1189#[expect(clippy::too_many_arguments)]
1190pub(crate) async fn create_sketch(
1191    id: Uuid,
1192    sketch_surface: SketchSurface,
1193    at: [TyF64; 2],
1194    tag: Option<TagNode>,
1195    send_to_engine: bool,
1196    exec_state: &mut ExecState,
1197    ctx: &ExecutorContext,
1198    source_range: SourceRange,
1199) -> Result<Sketch, KclError> {
1200    match &sketch_surface {
1201        SketchSurface::Face(face) => {
1202            // Flush the batch for our fillets/chamfers if there are any.
1203            // If we do not do these for sketch on face, things will fail with face does not exist.
1204            exec_state
1205                .flush_batch_for_solids(
1206                    ModelingCmdMeta::new(exec_state, ctx, source_range),
1207                    &[(*face.solid).clone()],
1208                )
1209                .await?;
1210        }
1211        SketchSurface::Plane(plane) if !plane.is_standard() => {
1212            // Hide whatever plane we are sketching on.
1213            // This is especially helpful for offset planes, which would be visible otherwise.
1214            exec_state
1215                .batch_end_cmd(
1216                    ModelingCmdMeta::new(exec_state, ctx, source_range),
1217                    ModelingCmd::from(mcmd::ObjectVisible::builder().object_id(plane.id).hidden(true).build()),
1218                )
1219                .await?;
1220        }
1221        _ => {}
1222    }
1223
1224    let path_id = id;
1225    let enable_sketch_id = exec_state.next_uuid();
1226    let move_pen_id = exec_state.next_uuid();
1227    let disable_sketch_id = exec_state.next_uuid();
1228    if send_to_engine {
1229        exec_state
1230            .batch_modeling_cmds(
1231                ModelingCmdMeta::new(exec_state, ctx, source_range),
1232                &[
1233                    // Enter sketch mode on the surface.
1234                    // We call this here so you can reuse the sketch surface for multiple sketches.
1235                    ModelingCmdReq {
1236                        cmd: ModelingCmd::from(if let SketchSurface::Plane(plane) = &sketch_surface {
1237                            // We pass in the normal for the plane here.
1238                            let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
1239                            mcmd::EnableSketchMode::builder()
1240                                .animated(false)
1241                                .ortho(false)
1242                                .entity_id(sketch_surface.id())
1243                                .adjust_camera(false)
1244                                .planar_normal(normal.into())
1245                                .build()
1246                        } else {
1247                            mcmd::EnableSketchMode::builder()
1248                                .animated(false)
1249                                .ortho(false)
1250                                .entity_id(sketch_surface.id())
1251                                .adjust_camera(false)
1252                                .build()
1253                        }),
1254                        cmd_id: enable_sketch_id.into(),
1255                    },
1256                    ModelingCmdReq {
1257                        cmd: ModelingCmd::from(mcmd::StartPath::default()),
1258                        cmd_id: path_id.into(),
1259                    },
1260                    ModelingCmdReq {
1261                        cmd: ModelingCmd::from(
1262                            mcmd::MovePathPen::builder()
1263                                .path(path_id.into())
1264                                .to(KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit))
1265                                .build(),
1266                        ),
1267                        cmd_id: move_pen_id.into(),
1268                    },
1269                    ModelingCmdReq {
1270                        cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1271                        cmd_id: disable_sketch_id.into(),
1272                    },
1273                ],
1274            )
1275            .await?;
1276    }
1277
1278    // Convert to the units of the module.  This is what the frontend expects.
1279    let units = exec_state.length_unit();
1280    let to = point_to_len_unit(at, units);
1281    let current_path = BasePath {
1282        from: to,
1283        to,
1284        tag: tag.clone(),
1285        units,
1286        geo_meta: GeoMeta {
1287            id: move_pen_id,
1288            metadata: source_range.into(),
1289        },
1290    };
1291
1292    let mut sketch = Sketch {
1293        id: path_id,
1294        original_id: path_id,
1295        artifact_id: path_id.into(),
1296        on: sketch_surface,
1297        paths: vec![],
1298        inner_paths: vec![],
1299        units,
1300        mirror: Default::default(),
1301        clone: Default::default(),
1302        meta: vec![source_range.into()],
1303        tags: Default::default(),
1304        start: current_path.clone(),
1305        is_closed: ProfileClosed::No,
1306    };
1307    if let Some(tag) = &tag {
1308        let path = Path::Base { base: current_path };
1309        sketch.add_tag(tag, &path, exec_state, None);
1310    }
1311
1312    Ok(sketch)
1313}
1314
1315/// Returns the X component of the sketch profile start point.
1316pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1317    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1318    let ty = sketch.units.into();
1319    let x = inner_profile_start_x(sketch)?;
1320    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1321}
1322
1323pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1324    Ok(profile.start.to[0])
1325}
1326
1327/// Returns the Y component of the sketch profile start point.
1328pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1329    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1330    let ty = sketch.units.into();
1331    let x = inner_profile_start_y(sketch)?;
1332    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1333}
1334
1335pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1336    Ok(profile.start.to[1])
1337}
1338
1339/// Returns the sketch profile start point.
1340pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1341    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1342    let ty = sketch.units.into();
1343    let point = inner_profile_start(sketch)?;
1344    Ok(KclValue::from_point2d(point, ty, args.into()))
1345}
1346
1347pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1348    Ok(profile.start.to)
1349}
1350
1351/// Close the current sketch.
1352pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1353    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1354    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1355    let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1356    Ok(KclValue::Sketch {
1357        value: Box::new(new_sketch),
1358    })
1359}
1360
1361pub(crate) async fn inner_close(
1362    sketch: Sketch,
1363    tag: Option<TagNode>,
1364    exec_state: &mut ExecState,
1365    args: Args,
1366) -> Result<Sketch, KclError> {
1367    if matches!(sketch.is_closed, ProfileClosed::Explicitly) {
1368        exec_state.warn(
1369            crate::CompilationError {
1370                source_range: args.source_range,
1371                message: "This sketch is already closed. Remove this unnecessary `close()` call".to_string(),
1372                suggestion: None,
1373                severity: crate::errors::Severity::Warning,
1374                tag: crate::errors::Tag::Unnecessary,
1375            },
1376            annotations::WARN_UNNECESSARY_CLOSE,
1377        );
1378        return Ok(sketch);
1379    }
1380    let from = sketch.current_pen_position()?;
1381    let to = point_to_len_unit(sketch.start.get_from(), from.units);
1382
1383    let id = exec_state.next_uuid();
1384
1385    exec_state
1386        .batch_modeling_cmd(
1387            ModelingCmdMeta::from_args_id(exec_state, &args, id),
1388            ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
1389        )
1390        .await?;
1391
1392    let mut new_sketch = sketch;
1393
1394    let distance = ((from.x - to[0]).powi(2) + (from.y - to[1]).powi(2)).sqrt();
1395    if distance > super::EQUAL_POINTS_DIST_EPSILON {
1396        // These will NOT be the same point in the engine, and an additional segment will be created.
1397        let current_path = Path::ToPoint {
1398            base: BasePath {
1399                from: from.ignore_units(),
1400                to,
1401                tag: tag.clone(),
1402                units: new_sketch.units,
1403                geo_meta: GeoMeta {
1404                    id,
1405                    metadata: args.source_range.into(),
1406                },
1407            },
1408        };
1409
1410        if let Some(tag) = &tag {
1411            new_sketch.add_tag(tag, &current_path, exec_state, None);
1412        }
1413        new_sketch.paths.push(current_path);
1414    } else if tag.is_some() {
1415        exec_state.warn(
1416            crate::CompilationError {
1417                source_range: args.source_range,
1418                message: "A tag declarator was specified, but no segment was created".to_string(),
1419                suggestion: None,
1420                severity: crate::errors::Severity::Warning,
1421                tag: crate::errors::Tag::Unnecessary,
1422            },
1423            annotations::WARN_UNUSED_TAGS,
1424        );
1425    }
1426
1427    new_sketch.is_closed = ProfileClosed::Explicitly;
1428
1429    Ok(new_sketch)
1430}
1431
1432/// Draw an arc.
1433pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1434    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1435
1436    let angle_start: Option<TyF64> = args.get_kw_arg_opt("angleStart", &RuntimeType::degrees(), exec_state)?;
1437    let angle_end: Option<TyF64> = args.get_kw_arg_opt("angleEnd", &RuntimeType::degrees(), exec_state)?;
1438    let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1439    let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1440    let end_absolute: Option<[TyF64; 2]> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1441    let interior_absolute: Option<[TyF64; 2]> =
1442        args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1443    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1444    let new_sketch = inner_arc(
1445        sketch,
1446        angle_start,
1447        angle_end,
1448        radius,
1449        diameter,
1450        interior_absolute,
1451        end_absolute,
1452        tag,
1453        exec_state,
1454        args,
1455    )
1456    .await?;
1457    Ok(KclValue::Sketch {
1458        value: Box::new(new_sketch),
1459    })
1460}
1461
1462#[allow(clippy::too_many_arguments)]
1463pub(crate) async fn inner_arc(
1464    sketch: Sketch,
1465    angle_start: Option<TyF64>,
1466    angle_end: Option<TyF64>,
1467    radius: Option<TyF64>,
1468    diameter: Option<TyF64>,
1469    interior_absolute: Option<[TyF64; 2]>,
1470    end_absolute: Option<[TyF64; 2]>,
1471    tag: Option<TagNode>,
1472    exec_state: &mut ExecState,
1473    args: Args,
1474) -> Result<Sketch, KclError> {
1475    let from: Point2d = sketch.current_pen_position()?;
1476    let id = exec_state.next_uuid();
1477
1478    match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1479        (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1480            let radius = get_radius(radius, diameter, args.source_range)?;
1481            relative_arc(id, exec_state, sketch, from, angle_start, angle_end, radius, tag, true, &args.ctx, args.source_range).await
1482        }
1483        (None, None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1484            absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1485        }
1486        _ => {
1487            Err(KclError::new_type(KclErrorDetails::new(
1488                "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)".to_owned(),
1489                vec![args.source_range],
1490            )))
1491        }
1492    }
1493}
1494
1495#[allow(clippy::too_many_arguments)]
1496pub async fn absolute_arc(
1497    args: &Args,
1498    id: uuid::Uuid,
1499    exec_state: &mut ExecState,
1500    sketch: Sketch,
1501    from: Point2d,
1502    interior_absolute: [TyF64; 2],
1503    end_absolute: [TyF64; 2],
1504    tag: Option<TagNode>,
1505) -> Result<Sketch, KclError> {
1506    // The start point is taken from the path you are extending.
1507    exec_state
1508        .batch_modeling_cmd(
1509            ModelingCmdMeta::from_args_id(exec_state, args, id),
1510            ModelingCmd::from(
1511                mcmd::ExtendPath::builder()
1512                    .path(sketch.id.into())
1513                    .segment(PathSegment::ArcTo {
1514                        end: kcmc::shared::Point3d {
1515                            x: LengthUnit(end_absolute[0].to_mm()),
1516                            y: LengthUnit(end_absolute[1].to_mm()),
1517                            z: LengthUnit(0.0),
1518                        },
1519                        interior: kcmc::shared::Point3d {
1520                            x: LengthUnit(interior_absolute[0].to_mm()),
1521                            y: LengthUnit(interior_absolute[1].to_mm()),
1522                            z: LengthUnit(0.0),
1523                        },
1524                        relative: false,
1525                    })
1526                    .build(),
1527            ),
1528        )
1529        .await?;
1530
1531    let start = [from.x, from.y];
1532    let end = point_to_len_unit(end_absolute, from.units);
1533    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1534
1535    let current_path = Path::ArcThreePoint {
1536        base: BasePath {
1537            from: from.ignore_units(),
1538            to: end,
1539            tag: tag.clone(),
1540            units: sketch.units,
1541            geo_meta: GeoMeta {
1542                id,
1543                metadata: args.source_range.into(),
1544            },
1545        },
1546        p1: start,
1547        p2: point_to_len_unit(interior_absolute, from.units),
1548        p3: end,
1549    };
1550
1551    let mut new_sketch = sketch;
1552    if let Some(tag) = &tag {
1553        new_sketch.add_tag(tag, &current_path, exec_state, None);
1554    }
1555    if loops_back_to_start {
1556        new_sketch.is_closed = ProfileClosed::Implicitly;
1557    }
1558
1559    new_sketch.paths.push(current_path);
1560
1561    Ok(new_sketch)
1562}
1563
1564#[allow(clippy::too_many_arguments)]
1565pub async fn relative_arc(
1566    id: uuid::Uuid,
1567    exec_state: &mut ExecState,
1568    sketch: Sketch,
1569    from: Point2d,
1570    angle_start: TyF64,
1571    angle_end: TyF64,
1572    radius: TyF64,
1573    tag: Option<TagNode>,
1574    send_to_engine: bool,
1575    ctx: &ExecutorContext,
1576    source_range: SourceRange,
1577) -> Result<Sketch, KclError> {
1578    let a_start = Angle::from_degrees(angle_start.to_degrees(exec_state, source_range));
1579    let a_end = Angle::from_degrees(angle_end.to_degrees(exec_state, source_range));
1580    let radius = radius.to_length_units(from.units);
1581    let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1582    if a_start == a_end {
1583        return Err(KclError::new_type(KclErrorDetails::new(
1584            "Arc start and end angles must be different".to_string(),
1585            vec![source_range],
1586        )));
1587    }
1588    let ccw = a_start < a_end;
1589
1590    if send_to_engine {
1591        exec_state
1592            .batch_modeling_cmd(
1593                ModelingCmdMeta::with_id(exec_state, ctx, source_range, id),
1594                ModelingCmd::from(
1595                    mcmd::ExtendPath::builder()
1596                        .path(sketch.id.into())
1597                        .segment(PathSegment::Arc {
1598                            start: a_start,
1599                            end: a_end,
1600                            center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1601                            radius: LengthUnit(
1602                                crate::execution::types::adjust_length(from.units, radius, UnitLength::Millimeters).0,
1603                            ),
1604                            relative: false,
1605                        })
1606                        .build(),
1607                ),
1608            )
1609            .await?;
1610    }
1611
1612    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1613    let current_path = Path::Arc {
1614        base: BasePath {
1615            from: from.ignore_units(),
1616            to: end,
1617            tag: tag.clone(),
1618            units: from.units,
1619            geo_meta: GeoMeta {
1620                id,
1621                metadata: source_range.into(),
1622            },
1623        },
1624        center,
1625        radius,
1626        ccw,
1627    };
1628
1629    let mut new_sketch = sketch;
1630    if let Some(tag) = &tag {
1631        new_sketch.add_tag(tag, &current_path, exec_state, None);
1632    }
1633    if loops_back_to_start {
1634        new_sketch.is_closed = ProfileClosed::Implicitly;
1635    }
1636
1637    new_sketch.paths.push(current_path);
1638
1639    Ok(new_sketch)
1640}
1641
1642/// Draw a tangential arc to a specific point.
1643pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1644    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1645    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1646    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1647    let radius = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1648    let diameter = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1649    let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
1650    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1651
1652    let new_sketch = inner_tangential_arc(
1653        sketch,
1654        end_absolute,
1655        end,
1656        radius,
1657        diameter,
1658        angle,
1659        tag,
1660        exec_state,
1661        args,
1662    )
1663    .await?;
1664    Ok(KclValue::Sketch {
1665        value: Box::new(new_sketch),
1666    })
1667}
1668
1669#[allow(clippy::too_many_arguments)]
1670async fn inner_tangential_arc(
1671    sketch: Sketch,
1672    end_absolute: Option<[TyF64; 2]>,
1673    end: Option<[TyF64; 2]>,
1674    radius: Option<TyF64>,
1675    diameter: Option<TyF64>,
1676    angle: Option<TyF64>,
1677    tag: Option<TagNode>,
1678    exec_state: &mut ExecState,
1679    args: Args,
1680) -> Result<Sketch, KclError> {
1681    match (end_absolute, end, radius, diameter, angle) {
1682        (Some(point), None, None, None, None) => {
1683            inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1684        }
1685        (None, Some(point), None, None, None) => {
1686            inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1687        }
1688        (None, None, radius, diameter, Some(angle)) => {
1689            let radius = get_radius(radius, diameter, args.source_range)?;
1690            let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1691            inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1692        }
1693        (Some(_), Some(_), None, None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
1694            "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1695            vec![args.source_range],
1696        ))),
1697        (_, _, _, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1698            "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1699            vec![args.source_range],
1700        ))),
1701    }
1702}
1703
1704/// Data to draw a tangential arc.
1705#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1706#[ts(export)]
1707#[serde(rename_all = "camelCase", untagged)]
1708pub enum TangentialArcData {
1709    RadiusAndOffset {
1710        /// Radius of the arc.
1711        /// Not to be confused with Raiders of the Lost Ark.
1712        radius: TyF64,
1713        /// Offset of the arc, in degrees.
1714        offset: TyF64,
1715    },
1716}
1717
1718/// Draw a curved line segment along part of an imaginary circle.
1719///
1720/// The arc is constructed such that the last line segment is placed tangent
1721/// to the imaginary circle of the specified radius. The resulting arc is the
1722/// segment of the imaginary circle from that tangent point for 'angle'
1723/// degrees along the imaginary circle.
1724async fn inner_tangential_arc_radius_angle(
1725    data: TangentialArcData,
1726    sketch: Sketch,
1727    tag: Option<TagNode>,
1728    exec_state: &mut ExecState,
1729    args: Args,
1730) -> Result<Sketch, KclError> {
1731    let from: Point2d = sketch.current_pen_position()?;
1732    // next set of lines is some undocumented voodoo from get_tangential_arc_to_info
1733    let tangent_info = sketch.get_tangential_info_from_paths(); //this function desperately needs some documentation
1734    let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1735
1736    let id = exec_state.next_uuid();
1737
1738    let (center, to, ccw) = match data {
1739        TangentialArcData::RadiusAndOffset { radius, offset } => {
1740            // KCL stdlib types use degrees.
1741            let offset = Angle::from_degrees(offset.to_degrees(exec_state, args.source_range));
1742
1743            // Calculate the end point from the angle and radius.
1744            // atan2 outputs radians.
1745            let previous_end_tangent = Angle::from_radians(libm::atan2(
1746                from.y - tan_previous_point[1],
1747                from.x - tan_previous_point[0],
1748            ));
1749            // make sure the arc center is on the correct side to guarantee deterministic behavior
1750            // note the engine automatically rejects an offset of zero, if we want to flag that at KCL too to avoid engine errors
1751            let ccw = offset.to_degrees() > 0.0;
1752            let tangent_to_arc_start_angle = if ccw {
1753                // CCW turn
1754                Angle::from_degrees(-90.0)
1755            } else {
1756                // CW turn
1757                Angle::from_degrees(90.0)
1758            };
1759            // may need some logic and / or modulo on the various angle values to prevent them from going "backwards"
1760            // but the above logic *should* capture that behavior
1761            let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1762            let end_angle = start_angle + offset;
1763            let (center, to) = arc_center_and_end(
1764                from.ignore_units(),
1765                start_angle,
1766                end_angle,
1767                radius.to_length_units(from.units),
1768            );
1769
1770            exec_state
1771                .batch_modeling_cmd(
1772                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
1773                    ModelingCmd::from(
1774                        mcmd::ExtendPath::builder()
1775                            .path(sketch.id.into())
1776                            .segment(PathSegment::TangentialArc {
1777                                radius: LengthUnit(radius.to_mm()),
1778                                offset,
1779                            })
1780                            .build(),
1781                    ),
1782                )
1783                .await?;
1784            (center, to, ccw)
1785        }
1786    };
1787    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1788
1789    let current_path = Path::TangentialArc {
1790        ccw,
1791        center,
1792        base: BasePath {
1793            from: from.ignore_units(),
1794            to,
1795            tag: tag.clone(),
1796            units: sketch.units,
1797            geo_meta: GeoMeta {
1798                id,
1799                metadata: args.source_range.into(),
1800            },
1801        },
1802    };
1803
1804    let mut new_sketch = sketch;
1805    if let Some(tag) = &tag {
1806        new_sketch.add_tag(tag, &current_path, exec_state, None);
1807    }
1808    if loops_back_to_start {
1809        new_sketch.is_closed = ProfileClosed::Implicitly;
1810    }
1811
1812    new_sketch.paths.push(current_path);
1813
1814    Ok(new_sketch)
1815}
1816
1817// `to` must be in sketch.units
1818fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
1819    ModelingCmd::from(
1820        mcmd::ExtendPath::builder()
1821            .path(sketch.id.into())
1822            .segment(PathSegment::TangentialArcTo {
1823                angle_snap_increment: None,
1824                to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
1825                    .with_z(0.0)
1826                    .map(LengthUnit),
1827            })
1828            .build(),
1829    )
1830}
1831
1832async fn inner_tangential_arc_to_point(
1833    sketch: Sketch,
1834    point: [TyF64; 2],
1835    is_absolute: bool,
1836    tag: Option<TagNode>,
1837    exec_state: &mut ExecState,
1838    args: Args,
1839) -> Result<Sketch, KclError> {
1840    let from: Point2d = sketch.current_pen_position()?;
1841    let tangent_info = sketch.get_tangential_info_from_paths();
1842    let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1843
1844    let point = point_to_len_unit(point, from.units);
1845
1846    let to = if is_absolute {
1847        point
1848    } else {
1849        [from.x + point[0], from.y + point[1]]
1850    };
1851    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1852    let [to_x, to_y] = to;
1853    let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1854        arc_start_point: [from.x, from.y],
1855        arc_end_point: [to_x, to_y],
1856        tan_previous_point,
1857        obtuse: true,
1858    });
1859
1860    if result.center[0].is_infinite() {
1861        return Err(KclError::new_semantic(KclErrorDetails::new(
1862            "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
1863                .to_owned(),
1864            vec![args.source_range],
1865        )));
1866    } else if result.center[1].is_infinite() {
1867        return Err(KclError::new_semantic(KclErrorDetails::new(
1868            "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
1869                .to_owned(),
1870            vec![args.source_range],
1871        )));
1872    }
1873
1874    let delta = if is_absolute {
1875        [to_x - from.x, to_y - from.y]
1876    } else {
1877        point
1878    };
1879    let id = exec_state.next_uuid();
1880    exec_state
1881        .batch_modeling_cmd(
1882            ModelingCmdMeta::from_args_id(exec_state, &args, id),
1883            tan_arc_to(&sketch, delta),
1884        )
1885        .await?;
1886
1887    let current_path = Path::TangentialArcTo {
1888        base: BasePath {
1889            from: from.ignore_units(),
1890            to,
1891            tag: tag.clone(),
1892            units: sketch.units,
1893            geo_meta: GeoMeta {
1894                id,
1895                metadata: args.source_range.into(),
1896            },
1897        },
1898        center: result.center,
1899        ccw: result.ccw > 0,
1900    };
1901
1902    let mut new_sketch = sketch;
1903    if let Some(tag) = &tag {
1904        new_sketch.add_tag(tag, &current_path, exec_state, None);
1905    }
1906    if loops_back_to_start {
1907        new_sketch.is_closed = ProfileClosed::Implicitly;
1908    }
1909
1910    new_sketch.paths.push(current_path);
1911
1912    Ok(new_sketch)
1913}
1914
1915/// Draw a bezier curve.
1916pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1917    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1918    let control1 = args.get_kw_arg_opt("control1", &RuntimeType::point2d(), exec_state)?;
1919    let control2 = args.get_kw_arg_opt("control2", &RuntimeType::point2d(), exec_state)?;
1920    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1921    let control1_absolute = args.get_kw_arg_opt("control1Absolute", &RuntimeType::point2d(), exec_state)?;
1922    let control2_absolute = args.get_kw_arg_opt("control2Absolute", &RuntimeType::point2d(), exec_state)?;
1923    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1924    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1925
1926    let new_sketch = inner_bezier_curve(
1927        sketch,
1928        control1,
1929        control2,
1930        end,
1931        control1_absolute,
1932        control2_absolute,
1933        end_absolute,
1934        tag,
1935        exec_state,
1936        args,
1937    )
1938    .await?;
1939    Ok(KclValue::Sketch {
1940        value: Box::new(new_sketch),
1941    })
1942}
1943
1944#[allow(clippy::too_many_arguments)]
1945async fn inner_bezier_curve(
1946    sketch: Sketch,
1947    control1: Option<[TyF64; 2]>,
1948    control2: Option<[TyF64; 2]>,
1949    end: Option<[TyF64; 2]>,
1950    control1_absolute: Option<[TyF64; 2]>,
1951    control2_absolute: Option<[TyF64; 2]>,
1952    end_absolute: Option<[TyF64; 2]>,
1953    tag: Option<TagNode>,
1954    exec_state: &mut ExecState,
1955    args: Args,
1956) -> Result<Sketch, KclError> {
1957    let from = sketch.current_pen_position()?;
1958    let id = exec_state.next_uuid();
1959
1960    let (to, control1_abs, control2_abs) = match (
1961        control1,
1962        control2,
1963        end,
1964        control1_absolute,
1965        control2_absolute,
1966        end_absolute,
1967    ) {
1968        // Relative
1969        (Some(control1), Some(control2), Some(end), None, None, None) => {
1970            let delta = end.clone();
1971            let to = [
1972                from.x + end[0].to_length_units(from.units),
1973                from.y + end[1].to_length_units(from.units),
1974            ];
1975            // Calculate absolute control points
1976            let control1_abs = [
1977                from.x + control1[0].to_length_units(from.units),
1978                from.y + control1[1].to_length_units(from.units),
1979            ];
1980            let control2_abs = [
1981                from.x + control2[0].to_length_units(from.units),
1982                from.y + control2[1].to_length_units(from.units),
1983            ];
1984
1985            exec_state
1986                .batch_modeling_cmd(
1987                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
1988                    ModelingCmd::from(
1989                        mcmd::ExtendPath::builder()
1990                            .path(sketch.id.into())
1991                            .segment(PathSegment::Bezier {
1992                                control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1993                                control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1994                                end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
1995                                relative: true,
1996                            })
1997                            .build(),
1998                    ),
1999                )
2000                .await?;
2001            (to, control1_abs, control2_abs)
2002        }
2003        // Absolute
2004        (None, None, None, Some(control1), Some(control2), Some(end)) => {
2005            let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
2006            let control1_abs = control1.clone().map(|v| v.to_length_units(from.units));
2007            let control2_abs = control2.clone().map(|v| v.to_length_units(from.units));
2008            exec_state
2009                .batch_modeling_cmd(
2010                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
2011                    ModelingCmd::from(
2012                        mcmd::ExtendPath::builder()
2013                            .path(sketch.id.into())
2014                            .segment(PathSegment::Bezier {
2015                                control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2016                                control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2017                                end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
2018                                relative: false,
2019                            })
2020                            .build(),
2021                    ),
2022                )
2023                .await?;
2024            (to, control1_abs, control2_abs)
2025        }
2026        _ => {
2027            return Err(KclError::new_semantic(KclErrorDetails::new(
2028                "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
2029                vec![args.source_range],
2030            )));
2031        }
2032    };
2033
2034    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2035
2036    let current_path = Path::Bezier {
2037        base: BasePath {
2038            from: from.ignore_units(),
2039            to,
2040            tag: tag.clone(),
2041            units: sketch.units,
2042            geo_meta: GeoMeta {
2043                id,
2044                metadata: args.source_range.into(),
2045            },
2046        },
2047        control1: control1_abs,
2048        control2: control2_abs,
2049    };
2050
2051    let mut new_sketch = sketch;
2052    if let Some(tag) = &tag {
2053        new_sketch.add_tag(tag, &current_path, exec_state, None);
2054    }
2055    if loops_back_to_start {
2056        new_sketch.is_closed = ProfileClosed::Implicitly;
2057    }
2058
2059    new_sketch.paths.push(current_path);
2060
2061    Ok(new_sketch)
2062}
2063
2064/// Use a sketch to cut a hole in another sketch.
2065pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2066    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2067
2068    let tool: Vec<Sketch> = args.get_kw_arg(
2069        "tool",
2070        &RuntimeType::Array(
2071            Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
2072            ArrayLen::Minimum(1),
2073        ),
2074        exec_state,
2075    )?;
2076
2077    let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
2078    Ok(KclValue::Sketch {
2079        value: Box::new(new_sketch),
2080    })
2081}
2082
2083async fn inner_subtract_2d(
2084    mut sketch: Sketch,
2085    tool: Vec<Sketch>,
2086    exec_state: &mut ExecState,
2087    args: Args,
2088) -> Result<Sketch, KclError> {
2089    for hole_sketch in tool {
2090        exec_state
2091            .batch_modeling_cmd(
2092                ModelingCmdMeta::from_args(exec_state, &args),
2093                ModelingCmd::from(
2094                    mcmd::Solid2dAddHole::builder()
2095                        .object_id(sketch.id)
2096                        .hole_id(hole_sketch.id)
2097                        .build(),
2098                ),
2099            )
2100            .await?;
2101
2102        // Hide the source hole since it's no longer its own profile,
2103        // it's just used to modify some other profile.
2104        exec_state
2105            .batch_modeling_cmd(
2106                ModelingCmdMeta::from_args(exec_state, &args),
2107                ModelingCmd::from(
2108                    mcmd::ObjectVisible::builder()
2109                        .object_id(hole_sketch.id)
2110                        .hidden(true)
2111                        .build(),
2112                ),
2113            )
2114            .await?;
2115
2116        // NOTE: We don't look at the inner paths of the hole/tool sketch.
2117        // So if you have circle A, and it has a circular hole cut out (B),
2118        // then you cut A out of an even bigger circle C, we will lose that info.
2119        // Not really sure what to do about this.
2120        sketch.inner_paths.extend_from_slice(&hole_sketch.paths);
2121    }
2122
2123    // Returns the input sketch, exactly as it was, zero modifications.
2124    // This means the edges from `tool` are basically ignored, they're not in the output.
2125    Ok(sketch)
2126}
2127
2128/// Calculate the (x, y) point on an ellipse given x or y and the major/minor radii of the ellipse.
2129pub async fn elliptic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2130    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2131    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2132    let major_radius = args.get_kw_arg("majorRadius", &RuntimeType::num_any(), exec_state)?;
2133    let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::num_any(), exec_state)?;
2134
2135    let elliptic_point = inner_elliptic_point(x, y, major_radius, minor_radius, &args).await?;
2136
2137    args.make_kcl_val_from_point(elliptic_point, exec_state.length_unit().into())
2138}
2139
2140async fn inner_elliptic_point(
2141    x: Option<TyF64>,
2142    y: Option<TyF64>,
2143    major_radius: TyF64,
2144    minor_radius: TyF64,
2145    args: &Args,
2146) -> Result<[f64; 2], KclError> {
2147    let major_radius = major_radius.n;
2148    let minor_radius = minor_radius.n;
2149    if let Some(x) = x {
2150        if x.n.abs() > major_radius {
2151            Err(KclError::Type {
2152                details: KclErrorDetails::new(
2153                    format!(
2154                        "Invalid input. The x value, {}, cannot be larger than the major radius {}.",
2155                        x.n, major_radius
2156                    ),
2157                    vec![args.source_range],
2158                ),
2159            })
2160        } else {
2161            Ok((
2162                x.n,
2163                minor_radius * (1.0 - x.n.powf(2.0) / major_radius.powf(2.0)).sqrt(),
2164            )
2165                .into())
2166        }
2167    } else if let Some(y) = y {
2168        if y.n > minor_radius {
2169            Err(KclError::Type {
2170                details: KclErrorDetails::new(
2171                    format!(
2172                        "Invalid input. The y value, {}, cannot be larger than the minor radius {}.",
2173                        y.n, minor_radius
2174                    ),
2175                    vec![args.source_range],
2176                ),
2177            })
2178        } else {
2179            Ok((
2180                major_radius * (1.0 - y.n.powf(2.0) / minor_radius.powf(2.0)).sqrt(),
2181                y.n,
2182            )
2183                .into())
2184        }
2185    } else {
2186        Err(KclError::Type {
2187            details: KclErrorDetails::new(
2188                "Invalid input. Must have either x or y, you cannot have both or neither.".to_owned(),
2189                vec![args.source_range],
2190            ),
2191        })
2192    }
2193}
2194
2195/// Draw an elliptical arc.
2196pub async fn elliptic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2197    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2198
2199    let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
2200    let angle_start = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
2201    let angle_end = args.get_kw_arg("angleEnd", &RuntimeType::degrees(), exec_state)?;
2202    let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
2203    let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
2204    let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
2205    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2206
2207    let new_sketch = inner_elliptic(
2208        sketch,
2209        center,
2210        angle_start,
2211        angle_end,
2212        major_radius,
2213        major_axis,
2214        minor_radius,
2215        tag,
2216        exec_state,
2217        args,
2218    )
2219    .await?;
2220    Ok(KclValue::Sketch {
2221        value: Box::new(new_sketch),
2222    })
2223}
2224
2225#[allow(clippy::too_many_arguments)]
2226pub(crate) async fn inner_elliptic(
2227    sketch: Sketch,
2228    center: [TyF64; 2],
2229    angle_start: TyF64,
2230    angle_end: TyF64,
2231    major_radius: Option<TyF64>,
2232    major_axis: Option<[TyF64; 2]>,
2233    minor_radius: TyF64,
2234    tag: Option<TagNode>,
2235    exec_state: &mut ExecState,
2236    args: Args,
2237) -> Result<Sketch, KclError> {
2238    let from: Point2d = sketch.current_pen_position()?;
2239    let id = exec_state.next_uuid();
2240
2241    let center_u = point_to_len_unit(center, from.units);
2242
2243    let major_axis = match (major_axis, major_radius) {
2244        (Some(_), Some(_)) | (None, None) => {
2245            return Err(KclError::new_type(KclErrorDetails::new(
2246                "Provide either `majorAxis` or `majorRadius`.".to_string(),
2247                vec![args.source_range],
2248            )));
2249        }
2250        (Some(major_axis), None) => major_axis,
2251        (None, Some(major_radius)) => [
2252            major_radius.clone(),
2253            TyF64 {
2254                n: 0.0,
2255                ty: major_radius.ty,
2256            },
2257        ],
2258    };
2259    let start_angle = Angle::from_degrees(angle_start.to_degrees(exec_state, args.source_range));
2260    let end_angle = Angle::from_degrees(angle_end.to_degrees(exec_state, args.source_range));
2261    let major_axis_magnitude = (major_axis[0].to_length_units(from.units) * major_axis[0].to_length_units(from.units)
2262        + major_axis[1].to_length_units(from.units) * major_axis[1].to_length_units(from.units))
2263    .sqrt();
2264    let to = [
2265        major_axis_magnitude * libm::cos(end_angle.to_radians()),
2266        minor_radius.to_length_units(from.units) * libm::sin(end_angle.to_radians()),
2267    ];
2268    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2269    let major_axis_angle = libm::atan2(major_axis[1].n, major_axis[0].n);
2270
2271    let point = [
2272        center_u[0] + to[0] * libm::cos(major_axis_angle) - to[1] * libm::sin(major_axis_angle),
2273        center_u[1] + to[0] * libm::sin(major_axis_angle) + to[1] * libm::cos(major_axis_angle),
2274    ];
2275
2276    let axis = major_axis.map(|x| x.to_mm());
2277    exec_state
2278        .batch_modeling_cmd(
2279            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2280            ModelingCmd::from(
2281                mcmd::ExtendPath::builder()
2282                    .path(sketch.id.into())
2283                    .segment(PathSegment::Ellipse {
2284                        center: KPoint2d::from(untyped_point_to_mm(center_u, from.units)).map(LengthUnit),
2285                        major_axis: axis.map(LengthUnit).into(),
2286                        minor_radius: LengthUnit(minor_radius.to_mm()),
2287                        start_angle,
2288                        end_angle,
2289                    })
2290                    .build(),
2291            ),
2292        )
2293        .await?;
2294
2295    let current_path = Path::Ellipse {
2296        ccw: start_angle < end_angle,
2297        center: center_u,
2298        major_axis: axis,
2299        minor_radius: minor_radius.to_mm(),
2300        base: BasePath {
2301            from: from.ignore_units(),
2302            to: point,
2303            tag: tag.clone(),
2304            units: sketch.units,
2305            geo_meta: GeoMeta {
2306                id,
2307                metadata: args.source_range.into(),
2308            },
2309        },
2310    };
2311    let mut new_sketch = sketch;
2312    if let Some(tag) = &tag {
2313        new_sketch.add_tag(tag, &current_path, exec_state, None);
2314    }
2315    if loops_back_to_start {
2316        new_sketch.is_closed = ProfileClosed::Implicitly;
2317    }
2318
2319    new_sketch.paths.push(current_path);
2320
2321    Ok(new_sketch)
2322}
2323
2324/// Calculate the (x, y) point on an hyperbola given x or y and the semi major/minor of the ellipse.
2325pub async fn hyperbolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2326    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2327    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2328    let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::num_any(), exec_state)?;
2329    let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::num_any(), exec_state)?;
2330
2331    let hyperbolic_point = inner_hyperbolic_point(x, y, semi_major, semi_minor, &args).await?;
2332
2333    args.make_kcl_val_from_point(hyperbolic_point, exec_state.length_unit().into())
2334}
2335
2336async fn inner_hyperbolic_point(
2337    x: Option<TyF64>,
2338    y: Option<TyF64>,
2339    semi_major: TyF64,
2340    semi_minor: TyF64,
2341    args: &Args,
2342) -> Result<[f64; 2], KclError> {
2343    let semi_major = semi_major.n;
2344    let semi_minor = semi_minor.n;
2345    if let Some(x) = x {
2346        if x.n.abs() < semi_major {
2347            Err(KclError::Type {
2348                details: KclErrorDetails::new(
2349                    format!(
2350                        "Invalid input. The x value, {}, cannot be less than the semi major value, {}.",
2351                        x.n, semi_major
2352                    ),
2353                    vec![args.source_range],
2354                ),
2355            })
2356        } else {
2357            Ok((x.n, semi_minor * (x.n.powf(2.0) / semi_major.powf(2.0) - 1.0).sqrt()).into())
2358        }
2359    } else if let Some(y) = y {
2360        Ok((semi_major * (y.n.powf(2.0) / semi_minor.powf(2.0) + 1.0).sqrt(), y.n).into())
2361    } else {
2362        Err(KclError::Type {
2363            details: KclErrorDetails::new(
2364                "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2365                vec![args.source_range],
2366            ),
2367        })
2368    }
2369}
2370
2371/// Draw a hyperbolic arc.
2372pub async fn hyperbolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2373    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2374
2375    let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::length(), exec_state)?;
2376    let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::length(), exec_state)?;
2377    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2378    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2379    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2380    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2381    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2382
2383    let new_sketch = inner_hyperbolic(
2384        sketch,
2385        semi_major,
2386        semi_minor,
2387        interior,
2388        end,
2389        interior_absolute,
2390        end_absolute,
2391        tag,
2392        exec_state,
2393        args,
2394    )
2395    .await?;
2396    Ok(KclValue::Sketch {
2397        value: Box::new(new_sketch),
2398    })
2399}
2400
2401/// Calculate the tangent of a hyperbolic given a point on the curve
2402fn hyperbolic_tangent(point: Point2d, semi_major: f64, semi_minor: f64) -> [f64; 2] {
2403    (point.y * semi_major.powf(2.0), point.x * semi_minor.powf(2.0)).into()
2404}
2405
2406#[allow(clippy::too_many_arguments)]
2407pub(crate) async fn inner_hyperbolic(
2408    sketch: Sketch,
2409    semi_major: TyF64,
2410    semi_minor: TyF64,
2411    interior: Option<[TyF64; 2]>,
2412    end: Option<[TyF64; 2]>,
2413    interior_absolute: Option<[TyF64; 2]>,
2414    end_absolute: Option<[TyF64; 2]>,
2415    tag: Option<TagNode>,
2416    exec_state: &mut ExecState,
2417    args: Args,
2418) -> Result<Sketch, KclError> {
2419    let from = sketch.current_pen_position()?;
2420    let id = exec_state.next_uuid();
2421
2422    let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2423        (Some(interior), Some(end), None, None) => (interior, end, true),
2424        (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2425        _ => return Err(KclError::Type {
2426            details: KclErrorDetails::new(
2427                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2428                    .to_owned(),
2429                vec![args.source_range],
2430            ),
2431        }),
2432    };
2433
2434    let interior = point_to_len_unit(interior, from.units);
2435    let end = point_to_len_unit(end, from.units);
2436    let end_point = Point2d {
2437        x: end[0],
2438        y: end[1],
2439        units: from.units,
2440    };
2441    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
2442
2443    let semi_major_u = semi_major.to_length_units(from.units);
2444    let semi_minor_u = semi_minor.to_length_units(from.units);
2445
2446    let start_tangent = hyperbolic_tangent(from, semi_major_u, semi_minor_u);
2447    let end_tangent = hyperbolic_tangent(end_point, semi_major_u, semi_minor_u);
2448
2449    exec_state
2450        .batch_modeling_cmd(
2451            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2452            ModelingCmd::from(
2453                mcmd::ExtendPath::builder()
2454                    .path(sketch.id.into())
2455                    .segment(PathSegment::ConicTo {
2456                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2457                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2458                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2459                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2460                        relative,
2461                    })
2462                    .build(),
2463            ),
2464        )
2465        .await?;
2466
2467    let current_path = Path::Conic {
2468        base: BasePath {
2469            from: from.ignore_units(),
2470            to: end,
2471            tag: tag.clone(),
2472            units: sketch.units,
2473            geo_meta: GeoMeta {
2474                id,
2475                metadata: args.source_range.into(),
2476            },
2477        },
2478    };
2479
2480    let mut new_sketch = sketch;
2481    if let Some(tag) = &tag {
2482        new_sketch.add_tag(tag, &current_path, exec_state, None);
2483    }
2484    if loops_back_to_start {
2485        new_sketch.is_closed = ProfileClosed::Implicitly;
2486    }
2487
2488    new_sketch.paths.push(current_path);
2489
2490    Ok(new_sketch)
2491}
2492
2493/// Calculate the point on a parabola given the coefficient of the parabola and either x or y
2494pub async fn parabolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2495    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2496    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2497    let coefficients = args.get_kw_arg(
2498        "coefficients",
2499        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2500        exec_state,
2501    )?;
2502
2503    let parabolic_point = inner_parabolic_point(x, y, &coefficients, &args).await?;
2504
2505    args.make_kcl_val_from_point(parabolic_point, exec_state.length_unit().into())
2506}
2507
2508async fn inner_parabolic_point(
2509    x: Option<TyF64>,
2510    y: Option<TyF64>,
2511    coefficients: &[TyF64; 3],
2512    args: &Args,
2513) -> Result<[f64; 2], KclError> {
2514    let a = coefficients[0].n;
2515    let b = coefficients[1].n;
2516    let c = coefficients[2].n;
2517    if let Some(x) = x {
2518        Ok((x.n, a * x.n.powf(2.0) + b * x.n + c).into())
2519    } else if let Some(y) = y {
2520        let det = (b.powf(2.0) - 4.0 * a * (c - y.n)).sqrt();
2521        Ok(((-b + det) / (2.0 * a), y.n).into())
2522    } else {
2523        Err(KclError::Type {
2524            details: KclErrorDetails::new(
2525                "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2526                vec![args.source_range],
2527            ),
2528        })
2529    }
2530}
2531
2532/// Draw a parabolic arc.
2533pub async fn parabolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2534    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2535
2536    let coefficients = args.get_kw_arg_opt(
2537        "coefficients",
2538        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2539        exec_state,
2540    )?;
2541    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2542    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2543    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2544    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2545    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2546
2547    let new_sketch = inner_parabolic(
2548        sketch,
2549        coefficients,
2550        interior,
2551        end,
2552        interior_absolute,
2553        end_absolute,
2554        tag,
2555        exec_state,
2556        args,
2557    )
2558    .await?;
2559    Ok(KclValue::Sketch {
2560        value: Box::new(new_sketch),
2561    })
2562}
2563
2564fn parabolic_tangent(point: Point2d, a: f64, b: f64) -> [f64; 2] {
2565    //f(x) = ax^2 + bx + c
2566    //f'(x) = 2ax + b
2567    (1.0, 2.0 * a * point.x + b).into()
2568}
2569
2570#[allow(clippy::too_many_arguments)]
2571pub(crate) async fn inner_parabolic(
2572    sketch: Sketch,
2573    coefficients: Option<[TyF64; 3]>,
2574    interior: Option<[TyF64; 2]>,
2575    end: Option<[TyF64; 2]>,
2576    interior_absolute: Option<[TyF64; 2]>,
2577    end_absolute: Option<[TyF64; 2]>,
2578    tag: Option<TagNode>,
2579    exec_state: &mut ExecState,
2580    args: Args,
2581) -> Result<Sketch, KclError> {
2582    let from = sketch.current_pen_position()?;
2583    let id = exec_state.next_uuid();
2584
2585    if (coefficients.is_some() && interior.is_some()) || (coefficients.is_none() && interior.is_none()) {
2586        return Err(KclError::Type {
2587            details: KclErrorDetails::new(
2588                "Invalid combination of arguments. Either provide (a, b, c) or (interior)".to_owned(),
2589                vec![args.source_range],
2590            ),
2591        });
2592    }
2593
2594    let (interior, end, relative) = match (coefficients.clone(), interior, end, interior_absolute, end_absolute) {
2595        (None, Some(interior), Some(end), None, None) => {
2596            let interior = point_to_len_unit(interior, from.units);
2597            let end = point_to_len_unit(end, from.units);
2598            (interior,end, true)
2599        },
2600        (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
2601            let interior_absolute = point_to_len_unit(interior_absolute, from.units);
2602            let end_absolute = point_to_len_unit(end_absolute, from.units);
2603            (interior_absolute, end_absolute, false)
2604        }
2605        (Some(coefficients), _, Some(end), _, _) => {
2606            let end = point_to_len_unit(end, from.units);
2607            let interior =
2608            inner_parabolic_point(
2609                Some(TyF64::count(0.5 * (from.x + end[0]))),
2610                None,
2611                &coefficients,
2612                &args,
2613            )
2614            .await?;
2615            (interior, end, true)
2616        }
2617        (Some(coefficients), _, _, _, Some(end)) => {
2618            let end = point_to_len_unit(end, from.units);
2619            let interior =
2620            inner_parabolic_point(
2621                Some(TyF64::count(0.5 * (from.x + end[0]))),
2622                None,
2623                &coefficients,
2624                &args,
2625            )
2626            .await?;
2627            (interior, end, false)
2628        }
2629        _ => return
2630            Err(KclError::Type{details: KclErrorDetails::new(
2631                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute) if coefficients are not provided."
2632                    .to_owned(),
2633                vec![args.source_range],
2634            )}),
2635    };
2636
2637    let end_point = Point2d {
2638        x: end[0],
2639        y: end[1],
2640        units: from.units,
2641    };
2642
2643    let (a, b, _c) = if let Some([a, b, c]) = coefficients {
2644        (a.n, b.n, c.n)
2645    } else {
2646        // Any three points is enough to uniquely define a parabola
2647        let denom = (from.x - interior[0]) * (from.x - end_point.x) * (interior[0] - end_point.x);
2648        let a = (end_point.x * (interior[1] - from.y)
2649            + interior[0] * (from.y - end_point.y)
2650            + from.x * (end_point.y - interior[1]))
2651            / denom;
2652        let b = (end_point.x.powf(2.0) * (from.y - interior[1])
2653            + interior[0].powf(2.0) * (end_point.y - from.y)
2654            + from.x.powf(2.0) * (interior[1] - end_point.y))
2655            / denom;
2656        let c = (interior[0] * end_point.x * (interior[0] - end_point.x) * from.y
2657            + end_point.x * from.x * (end_point.x - from.x) * interior[1]
2658            + from.x * interior[0] * (from.x - interior[0]) * end_point.y)
2659            / denom;
2660
2661        (a, b, c)
2662    };
2663
2664    let start_tangent = parabolic_tangent(from, a, b);
2665    let end_tangent = parabolic_tangent(end_point, a, b);
2666
2667    exec_state
2668        .batch_modeling_cmd(
2669            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2670            ModelingCmd::from(
2671                mcmd::ExtendPath::builder()
2672                    .path(sketch.id.into())
2673                    .segment(PathSegment::ConicTo {
2674                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2675                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2676                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2677                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2678                        relative,
2679                    })
2680                    .build(),
2681            ),
2682        )
2683        .await?;
2684
2685    let current_path = Path::Conic {
2686        base: BasePath {
2687            from: from.ignore_units(),
2688            to: end,
2689            tag: tag.clone(),
2690            units: sketch.units,
2691            geo_meta: GeoMeta {
2692                id,
2693                metadata: args.source_range.into(),
2694            },
2695        },
2696    };
2697
2698    let mut new_sketch = sketch;
2699    if let Some(tag) = &tag {
2700        new_sketch.add_tag(tag, &current_path, exec_state, None);
2701    }
2702
2703    new_sketch.paths.push(current_path);
2704
2705    Ok(new_sketch)
2706}
2707
2708fn conic_tangent(coefficients: [f64; 6], point: [f64; 2]) -> [f64; 2] {
2709    let [a, b, c, d, e, _] = coefficients;
2710
2711    (
2712        c * point[0] + 2.0 * b * point[1] + e,
2713        -(2.0 * a * point[0] + c * point[1] + d),
2714    )
2715        .into()
2716}
2717
2718/// Draw a conic section
2719pub async fn conic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2720    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2721
2722    let start_tangent = args.get_kw_arg_opt("startTangent", &RuntimeType::point2d(), exec_state)?;
2723    let end_tangent = args.get_kw_arg_opt("endTangent", &RuntimeType::point2d(), exec_state)?;
2724    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2725    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2726    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2727    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2728    let coefficients = args.get_kw_arg_opt(
2729        "coefficients",
2730        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(6)),
2731        exec_state,
2732    )?;
2733    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2734
2735    let new_sketch = inner_conic(
2736        sketch,
2737        start_tangent,
2738        end,
2739        end_tangent,
2740        interior,
2741        coefficients,
2742        interior_absolute,
2743        end_absolute,
2744        tag,
2745        exec_state,
2746        args,
2747    )
2748    .await?;
2749    Ok(KclValue::Sketch {
2750        value: Box::new(new_sketch),
2751    })
2752}
2753
2754#[allow(clippy::too_many_arguments)]
2755pub(crate) async fn inner_conic(
2756    sketch: Sketch,
2757    start_tangent: Option<[TyF64; 2]>,
2758    end: Option<[TyF64; 2]>,
2759    end_tangent: Option<[TyF64; 2]>,
2760    interior: Option<[TyF64; 2]>,
2761    coefficients: Option<[TyF64; 6]>,
2762    interior_absolute: Option<[TyF64; 2]>,
2763    end_absolute: Option<[TyF64; 2]>,
2764    tag: Option<TagNode>,
2765    exec_state: &mut ExecState,
2766    args: Args,
2767) -> Result<Sketch, KclError> {
2768    let from: Point2d = sketch.current_pen_position()?;
2769    let id = exec_state.next_uuid();
2770
2771    if (coefficients.is_some() && (start_tangent.is_some() || end_tangent.is_some()))
2772        || (coefficients.is_none() && (start_tangent.is_none() && end_tangent.is_none()))
2773    {
2774        return Err(KclError::Type {
2775            details: KclErrorDetails::new(
2776                "Invalid combination of arguments. Either provide coefficients or (startTangent, endTangent)"
2777                    .to_owned(),
2778                vec![args.source_range],
2779            ),
2780        });
2781    }
2782
2783    let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2784        (Some(interior), Some(end), None, None) => (interior, end, true),
2785        (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2786        _ => return Err(KclError::Type {
2787            details: KclErrorDetails::new(
2788                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2789                    .to_owned(),
2790                vec![args.source_range],
2791            ),
2792        }),
2793    };
2794
2795    let end = point_to_len_unit(end, from.units);
2796    let interior = point_to_len_unit(interior, from.units);
2797
2798    let (start_tangent, end_tangent) = if let Some(coeffs) = coefficients {
2799        let (coeffs, _) = untype_array(coeffs);
2800        (conic_tangent(coeffs, [from.x, from.y]), conic_tangent(coeffs, end))
2801    } else {
2802        let start = if let Some(start_tangent) = start_tangent {
2803            point_to_len_unit(start_tangent, from.units)
2804        } else {
2805            let previous_point = sketch
2806                .get_tangential_info_from_paths()
2807                .tan_previous_point(from.ignore_units());
2808            let from = from.ignore_units();
2809            [from[0] - previous_point[0], from[1] - previous_point[1]]
2810        };
2811
2812        let Some(end_tangent) = end_tangent else {
2813            return Err(KclError::new_semantic(KclErrorDetails::new(
2814                "You must either provide either `coefficients` or `endTangent`.".to_owned(),
2815                vec![args.source_range],
2816            )));
2817        };
2818        let end_tan = point_to_len_unit(end_tangent, from.units);
2819        (start, end_tan)
2820    };
2821
2822    exec_state
2823        .batch_modeling_cmd(
2824            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2825            ModelingCmd::from(
2826                mcmd::ExtendPath::builder()
2827                    .path(sketch.id.into())
2828                    .segment(PathSegment::ConicTo {
2829                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2830                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2831                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2832                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2833                        relative,
2834                    })
2835                    .build(),
2836            ),
2837        )
2838        .await?;
2839
2840    let current_path = Path::Conic {
2841        base: BasePath {
2842            from: from.ignore_units(),
2843            to: end,
2844            tag: tag.clone(),
2845            units: sketch.units,
2846            geo_meta: GeoMeta {
2847                id,
2848                metadata: args.source_range.into(),
2849            },
2850        },
2851    };
2852
2853    let mut new_sketch = sketch;
2854    if let Some(tag) = &tag {
2855        new_sketch.add_tag(tag, &current_path, exec_state, None);
2856    }
2857
2858    new_sketch.paths.push(current_path);
2859
2860    Ok(new_sketch)
2861}
2862#[cfg(test)]
2863mod tests {
2864
2865    use pretty_assertions::assert_eq;
2866
2867    use crate::{
2868        execution::TagIdentifier,
2869        std::{sketch::PlaneData, utils::calculate_circle_center},
2870    };
2871
2872    #[test]
2873    fn test_deserialize_plane_data() {
2874        let data = PlaneData::XY;
2875        let mut str_json = serde_json::to_string(&data).unwrap();
2876        assert_eq!(str_json, "\"XY\"");
2877
2878        str_json = "\"YZ\"".to_string();
2879        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2880        assert_eq!(data, PlaneData::YZ);
2881
2882        str_json = "\"-YZ\"".to_string();
2883        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2884        assert_eq!(data, PlaneData::NegYZ);
2885
2886        str_json = "\"-xz\"".to_string();
2887        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2888        assert_eq!(data, PlaneData::NegXZ);
2889    }
2890
2891    #[test]
2892    fn test_deserialize_sketch_on_face_tag() {
2893        let data = "start";
2894        let mut str_json = serde_json::to_string(&data).unwrap();
2895        assert_eq!(str_json, "\"start\"");
2896
2897        str_json = "\"end\"".to_string();
2898        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2899        assert_eq!(
2900            data,
2901            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2902        );
2903
2904        str_json = serde_json::to_string(&TagIdentifier {
2905            value: "thing".to_string(),
2906            info: Vec::new(),
2907            meta: Default::default(),
2908        })
2909        .unwrap();
2910        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2911        assert_eq!(
2912            data,
2913            crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2914                value: "thing".to_string(),
2915                info: Vec::new(),
2916                meta: Default::default()
2917            }))
2918        );
2919
2920        str_json = "\"END\"".to_string();
2921        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2922        assert_eq!(
2923            data,
2924            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2925        );
2926
2927        str_json = "\"start\"".to_string();
2928        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2929        assert_eq!(
2930            data,
2931            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2932        );
2933
2934        str_json = "\"START\"".to_string();
2935        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2936        assert_eq!(
2937            data,
2938            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2939        );
2940    }
2941
2942    #[test]
2943    fn test_circle_center() {
2944        let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2945        assert_eq!(actual[0], 5.0);
2946        assert_eq!(actual[1], 0.0);
2947    }
2948}