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