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