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