Skip to main content

kcl_lib/std/
sketch.rs

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