kcl_lib/std/
sketch.rs

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