Skip to main content

kcl_lib/std/
sketch.rs

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