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        on: sketch_surface,
1365        paths: vec![],
1366        inner_paths: vec![],
1367        units,
1368        mirror: Default::default(),
1369        clone: Default::default(),
1370        synthetic_jump_path_ids: vec![],
1371        meta: vec![source_range.into()],
1372        tags: Default::default(),
1373        start: current_path.clone(),
1374        is_closed: ProfileClosed::No,
1375    };
1376    if let Some(tag) = &tag {
1377        let path = Path::Base { base: current_path };
1378        sketch.add_tag(tag, &path, exec_state, None);
1379    }
1380
1381    Ok(sketch)
1382}
1383
1384/// Returns the X component of the sketch profile start point.
1385pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1386    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1387    let ty = sketch.units.into();
1388    let x = inner_profile_start_x(sketch)?;
1389    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1390}
1391
1392pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1393    Ok(profile.start.to[0])
1394}
1395
1396/// Returns the Y component of the sketch profile start point.
1397pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1398    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1399    let ty = sketch.units.into();
1400    let x = inner_profile_start_y(sketch)?;
1401    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1402}
1403
1404pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1405    Ok(profile.start.to[1])
1406}
1407
1408/// Returns the sketch profile start point.
1409pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1410    let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1411    let ty = sketch.units.into();
1412    let point = inner_profile_start(sketch)?;
1413    Ok(KclValue::from_point2d(point, ty, args.into()))
1414}
1415
1416pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1417    Ok(profile.start.to)
1418}
1419
1420/// Close the current sketch.
1421pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1422    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1423    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1424    let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1425    Ok(KclValue::Sketch {
1426        value: Box::new(new_sketch),
1427    })
1428}
1429
1430pub(crate) async fn inner_close(
1431    sketch: Sketch,
1432    tag: Option<TagNode>,
1433    exec_state: &mut ExecState,
1434    args: Args,
1435) -> Result<Sketch, KclError> {
1436    if matches!(sketch.is_closed, ProfileClosed::Explicitly) {
1437        exec_state.warn(
1438            crate::CompilationIssue {
1439                source_range: args.source_range,
1440                message: "This sketch is already closed. Remove this unnecessary `close()` call".to_string(),
1441                suggestion: None,
1442                severity: crate::errors::Severity::Warning,
1443                tag: crate::errors::Tag::Unnecessary,
1444            },
1445            annotations::WARN_UNNECESSARY_CLOSE,
1446        );
1447        return Ok(sketch);
1448    }
1449    let from = sketch.current_pen_position()?;
1450    let to = point_to_len_unit(sketch.start.get_from(), from.units);
1451
1452    let id = exec_state.next_uuid();
1453
1454    exec_state
1455        .batch_modeling_cmd(
1456            ModelingCmdMeta::from_args_id(exec_state, &args, id),
1457            ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
1458        )
1459        .await?;
1460
1461    let mut new_sketch = sketch;
1462
1463    let distance = ((from.x - to[0]).powi(2) + (from.y - to[1]).powi(2)).sqrt();
1464    if distance > super::EQUAL_POINTS_DIST_EPSILON {
1465        // These will NOT be the same point in the engine, and an additional segment will be created.
1466        let current_path = Path::ToPoint {
1467            base: BasePath {
1468                from: from.ignore_units(),
1469                to,
1470                tag: tag.clone(),
1471                units: new_sketch.units,
1472                geo_meta: GeoMeta {
1473                    id,
1474                    metadata: args.source_range.into(),
1475                },
1476            },
1477        };
1478
1479        if let Some(tag) = &tag {
1480            new_sketch.add_tag(tag, &current_path, exec_state, None);
1481        }
1482        new_sketch.paths.push(current_path);
1483    } else if tag.is_some() {
1484        exec_state.warn(
1485            crate::CompilationIssue {
1486                source_range: args.source_range,
1487                message: "A tag declarator was specified, but no segment was created".to_string(),
1488                suggestion: None,
1489                severity: crate::errors::Severity::Warning,
1490                tag: crate::errors::Tag::Unnecessary,
1491            },
1492            annotations::WARN_UNUSED_TAGS,
1493        );
1494    }
1495
1496    new_sketch.is_closed = ProfileClosed::Explicitly;
1497
1498    Ok(new_sketch)
1499}
1500
1501/// Draw an arc.
1502pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1503    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1504
1505    let angle_start: Option<TyF64> = args.get_kw_arg_opt("angleStart", &RuntimeType::degrees(), exec_state)?;
1506    let angle_end: Option<TyF64> = args.get_kw_arg_opt("angleEnd", &RuntimeType::degrees(), exec_state)?;
1507    let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1508    let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1509    let end_absolute: Option<[TyF64; 2]> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1510    let interior_absolute: Option<[TyF64; 2]> =
1511        args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1512    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1513    let new_sketch = inner_arc(
1514        sketch,
1515        angle_start,
1516        angle_end,
1517        radius,
1518        diameter,
1519        interior_absolute,
1520        end_absolute,
1521        tag,
1522        exec_state,
1523        args,
1524    )
1525    .await?;
1526    Ok(KclValue::Sketch {
1527        value: Box::new(new_sketch),
1528    })
1529}
1530
1531#[allow(clippy::too_many_arguments)]
1532pub(crate) async fn inner_arc(
1533    sketch: Sketch,
1534    angle_start: Option<TyF64>,
1535    angle_end: Option<TyF64>,
1536    radius: Option<TyF64>,
1537    diameter: Option<TyF64>,
1538    interior_absolute: Option<[TyF64; 2]>,
1539    end_absolute: Option<[TyF64; 2]>,
1540    tag: Option<TagNode>,
1541    exec_state: &mut ExecState,
1542    args: Args,
1543) -> Result<Sketch, KclError> {
1544    let from: Point2d = sketch.current_pen_position()?;
1545    let id = exec_state.next_uuid();
1546
1547    match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1548        (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1549            let radius = get_radius(radius, diameter, args.source_range)?;
1550            relative_arc(id, exec_state, sketch, from, angle_start, angle_end, radius, tag, true, &args.ctx, args.source_range).await
1551        }
1552        (None, None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1553            absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1554        }
1555        _ => {
1556            Err(KclError::new_type(KclErrorDetails::new(
1557                "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)".to_owned(),
1558                vec![args.source_range],
1559            )))
1560        }
1561    }
1562}
1563
1564#[allow(clippy::too_many_arguments)]
1565pub async fn absolute_arc(
1566    args: &Args,
1567    id: uuid::Uuid,
1568    exec_state: &mut ExecState,
1569    sketch: Sketch,
1570    from: Point2d,
1571    interior_absolute: [TyF64; 2],
1572    end_absolute: [TyF64; 2],
1573    tag: Option<TagNode>,
1574) -> Result<Sketch, KclError> {
1575    // The start point is taken from the path you are extending.
1576    exec_state
1577        .batch_modeling_cmd(
1578            ModelingCmdMeta::from_args_id(exec_state, args, id),
1579            ModelingCmd::from(
1580                mcmd::ExtendPath::builder()
1581                    .path(sketch.id.into())
1582                    .segment(PathSegment::ArcTo {
1583                        end: kcmc::shared::Point3d {
1584                            x: LengthUnit(end_absolute[0].to_mm()),
1585                            y: LengthUnit(end_absolute[1].to_mm()),
1586                            z: LengthUnit(0.0),
1587                        },
1588                        interior: kcmc::shared::Point3d {
1589                            x: LengthUnit(interior_absolute[0].to_mm()),
1590                            y: LengthUnit(interior_absolute[1].to_mm()),
1591                            z: LengthUnit(0.0),
1592                        },
1593                        relative: false,
1594                    })
1595                    .build(),
1596            ),
1597        )
1598        .await?;
1599
1600    let start = [from.x, from.y];
1601    let end = point_to_len_unit(end_absolute, from.units);
1602    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1603
1604    let current_path = Path::ArcThreePoint {
1605        base: BasePath {
1606            from: from.ignore_units(),
1607            to: end,
1608            tag: tag.clone(),
1609            units: sketch.units,
1610            geo_meta: GeoMeta {
1611                id,
1612                metadata: args.source_range.into(),
1613            },
1614        },
1615        p1: start,
1616        p2: point_to_len_unit(interior_absolute, from.units),
1617        p3: end,
1618    };
1619
1620    let mut new_sketch = sketch;
1621    if let Some(tag) = &tag {
1622        new_sketch.add_tag(tag, &current_path, exec_state, None);
1623    }
1624    if loops_back_to_start {
1625        new_sketch.is_closed = ProfileClosed::Implicitly;
1626    }
1627
1628    new_sketch.paths.push(current_path);
1629
1630    Ok(new_sketch)
1631}
1632
1633#[allow(clippy::too_many_arguments)]
1634pub async fn relative_arc(
1635    id: uuid::Uuid,
1636    exec_state: &mut ExecState,
1637    sketch: Sketch,
1638    from: Point2d,
1639    angle_start: TyF64,
1640    angle_end: TyF64,
1641    radius: TyF64,
1642    tag: Option<TagNode>,
1643    send_to_engine: bool,
1644    ctx: &ExecutorContext,
1645    source_range: SourceRange,
1646) -> Result<Sketch, KclError> {
1647    let a_start = Angle::from_degrees(angle_start.to_degrees(exec_state, source_range));
1648    let a_end = Angle::from_degrees(angle_end.to_degrees(exec_state, source_range));
1649    let radius = radius.to_length_units(from.units);
1650    let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1651    if a_start == a_end {
1652        return Err(KclError::new_type(KclErrorDetails::new(
1653            "Arc start and end angles must be different".to_string(),
1654            vec![source_range],
1655        )));
1656    }
1657    let ccw = a_start < a_end;
1658
1659    if send_to_engine {
1660        exec_state
1661            .batch_modeling_cmd(
1662                ModelingCmdMeta::with_id(exec_state, ctx, source_range, id),
1663                ModelingCmd::from(
1664                    mcmd::ExtendPath::builder()
1665                        .path(sketch.id.into())
1666                        .segment(PathSegment::Arc {
1667                            start: a_start,
1668                            end: a_end,
1669                            center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1670                            radius: LengthUnit(
1671                                crate::execution::types::adjust_length(from.units, radius, UnitLength::Millimeters).0,
1672                            ),
1673                            relative: false,
1674                        })
1675                        .build(),
1676                ),
1677            )
1678            .await?;
1679    }
1680
1681    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1682    let current_path = Path::Arc {
1683        base: BasePath {
1684            from: from.ignore_units(),
1685            to: end,
1686            tag: tag.clone(),
1687            units: from.units,
1688            geo_meta: GeoMeta {
1689                id,
1690                metadata: source_range.into(),
1691            },
1692        },
1693        center,
1694        radius,
1695        ccw,
1696    };
1697
1698    let mut new_sketch = sketch;
1699    if let Some(tag) = &tag {
1700        new_sketch.add_tag(tag, &current_path, exec_state, None);
1701    }
1702    if loops_back_to_start {
1703        new_sketch.is_closed = ProfileClosed::Implicitly;
1704    }
1705
1706    new_sketch.paths.push(current_path);
1707
1708    Ok(new_sketch)
1709}
1710
1711/// Draw a tangential arc to a specific point.
1712pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1713    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1714    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1715    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1716    let radius = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1717    let diameter = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1718    let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
1719    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1720
1721    let new_sketch = inner_tangential_arc(
1722        sketch,
1723        end_absolute,
1724        end,
1725        radius,
1726        diameter,
1727        angle,
1728        tag,
1729        exec_state,
1730        args,
1731    )
1732    .await?;
1733    Ok(KclValue::Sketch {
1734        value: Box::new(new_sketch),
1735    })
1736}
1737
1738#[allow(clippy::too_many_arguments)]
1739async fn inner_tangential_arc(
1740    sketch: Sketch,
1741    end_absolute: Option<[TyF64; 2]>,
1742    end: Option<[TyF64; 2]>,
1743    radius: Option<TyF64>,
1744    diameter: Option<TyF64>,
1745    angle: Option<TyF64>,
1746    tag: Option<TagNode>,
1747    exec_state: &mut ExecState,
1748    args: Args,
1749) -> Result<Sketch, KclError> {
1750    match (end_absolute, end, radius, diameter, angle) {
1751        (Some(point), None, None, None, None) => {
1752            inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1753        }
1754        (None, Some(point), None, None, None) => {
1755            inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1756        }
1757        (None, None, radius, diameter, Some(angle)) => {
1758            let radius = get_radius(radius, diameter, args.source_range)?;
1759            let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1760            inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1761        }
1762        (Some(_), Some(_), None, None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
1763            "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1764            vec![args.source_range],
1765        ))),
1766        (_, _, _, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1767            "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1768            vec![args.source_range],
1769        ))),
1770    }
1771}
1772
1773/// Data to draw a tangential arc.
1774#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1775#[ts(export)]
1776#[serde(rename_all = "camelCase", untagged)]
1777pub enum TangentialArcData {
1778    RadiusAndOffset {
1779        /// Radius of the arc.
1780        /// Not to be confused with Raiders of the Lost Ark.
1781        radius: TyF64,
1782        /// Offset of the arc, in degrees.
1783        offset: TyF64,
1784    },
1785}
1786
1787/// Draw a curved line segment along part of an imaginary circle.
1788///
1789/// The arc is constructed such that the last line segment is placed tangent
1790/// to the imaginary circle of the specified radius. The resulting arc is the
1791/// segment of the imaginary circle from that tangent point for 'angle'
1792/// degrees along the imaginary circle.
1793async fn inner_tangential_arc_radius_angle(
1794    data: TangentialArcData,
1795    sketch: Sketch,
1796    tag: Option<TagNode>,
1797    exec_state: &mut ExecState,
1798    args: Args,
1799) -> Result<Sketch, KclError> {
1800    let from: Point2d = sketch.current_pen_position()?;
1801    // next set of lines is some undocumented voodoo from get_tangential_arc_to_info
1802    let tangent_info = sketch.get_tangential_info_from_paths(); //this function desperately needs some documentation
1803    let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1804
1805    let id = exec_state.next_uuid();
1806
1807    let (center, to, ccw) = match data {
1808        TangentialArcData::RadiusAndOffset { radius, offset } => {
1809            // KCL stdlib types use degrees.
1810            let offset = Angle::from_degrees(offset.to_degrees(exec_state, args.source_range));
1811
1812            // Calculate the end point from the angle and radius.
1813            // atan2 outputs radians.
1814            let previous_end_tangent = Angle::from_radians(libm::atan2(
1815                from.y - tan_previous_point[1],
1816                from.x - tan_previous_point[0],
1817            ));
1818            // make sure the arc center is on the correct side to guarantee deterministic behavior
1819            // note the engine automatically rejects an offset of zero, if we want to flag that at KCL too to avoid engine errors
1820            let ccw = offset.to_degrees() > 0.0;
1821            let tangent_to_arc_start_angle = if ccw {
1822                // CCW turn
1823                Angle::from_degrees(-90.0)
1824            } else {
1825                // CW turn
1826                Angle::from_degrees(90.0)
1827            };
1828            // may need some logic and / or modulo on the various angle values to prevent them from going "backwards"
1829            // but the above logic *should* capture that behavior
1830            let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1831            let end_angle = start_angle + offset;
1832            let (center, to) = arc_center_and_end(
1833                from.ignore_units(),
1834                start_angle,
1835                end_angle,
1836                radius.to_length_units(from.units),
1837            );
1838
1839            exec_state
1840                .batch_modeling_cmd(
1841                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
1842                    ModelingCmd::from(
1843                        mcmd::ExtendPath::builder()
1844                            .path(sketch.id.into())
1845                            .segment(PathSegment::TangentialArc {
1846                                radius: LengthUnit(radius.to_mm()),
1847                                offset,
1848                            })
1849                            .build(),
1850                    ),
1851                )
1852                .await?;
1853            (center, to, ccw)
1854        }
1855    };
1856    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1857
1858    let current_path = Path::TangentialArc {
1859        ccw,
1860        center,
1861        base: BasePath {
1862            from: from.ignore_units(),
1863            to,
1864            tag: tag.clone(),
1865            units: sketch.units,
1866            geo_meta: GeoMeta {
1867                id,
1868                metadata: args.source_range.into(),
1869            },
1870        },
1871    };
1872
1873    let mut new_sketch = sketch;
1874    if let Some(tag) = &tag {
1875        new_sketch.add_tag(tag, &current_path, exec_state, None);
1876    }
1877    if loops_back_to_start {
1878        new_sketch.is_closed = ProfileClosed::Implicitly;
1879    }
1880
1881    new_sketch.paths.push(current_path);
1882
1883    Ok(new_sketch)
1884}
1885
1886// `to` must be in sketch.units
1887fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
1888    ModelingCmd::from(
1889        mcmd::ExtendPath::builder()
1890            .path(sketch.id.into())
1891            .segment(PathSegment::TangentialArcTo {
1892                angle_snap_increment: None,
1893                to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
1894                    .with_z(0.0)
1895                    .map(LengthUnit),
1896            })
1897            .build(),
1898    )
1899}
1900
1901async fn inner_tangential_arc_to_point(
1902    sketch: Sketch,
1903    point: [TyF64; 2],
1904    is_absolute: bool,
1905    tag: Option<TagNode>,
1906    exec_state: &mut ExecState,
1907    args: Args,
1908) -> Result<Sketch, KclError> {
1909    let from: Point2d = sketch.current_pen_position()?;
1910    let tangent_info = sketch.get_tangential_info_from_paths();
1911    let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1912
1913    let point = point_to_len_unit(point, from.units);
1914
1915    let to = if is_absolute {
1916        point
1917    } else {
1918        [from.x + point[0], from.y + point[1]]
1919    };
1920    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1921    let [to_x, to_y] = to;
1922    let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1923        arc_start_point: [from.x, from.y],
1924        arc_end_point: [to_x, to_y],
1925        tan_previous_point,
1926        obtuse: true,
1927    });
1928
1929    if result.center[0].is_infinite() {
1930        return Err(KclError::new_semantic(KclErrorDetails::new(
1931            "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
1932                .to_owned(),
1933            vec![args.source_range],
1934        )));
1935    } else if result.center[1].is_infinite() {
1936        return Err(KclError::new_semantic(KclErrorDetails::new(
1937            "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
1938                .to_owned(),
1939            vec![args.source_range],
1940        )));
1941    }
1942
1943    let delta = if is_absolute {
1944        [to_x - from.x, to_y - from.y]
1945    } else {
1946        point
1947    };
1948    let id = exec_state.next_uuid();
1949    exec_state
1950        .batch_modeling_cmd(
1951            ModelingCmdMeta::from_args_id(exec_state, &args, id),
1952            tan_arc_to(&sketch, delta),
1953        )
1954        .await?;
1955
1956    let current_path = Path::TangentialArcTo {
1957        base: BasePath {
1958            from: from.ignore_units(),
1959            to,
1960            tag: tag.clone(),
1961            units: sketch.units,
1962            geo_meta: GeoMeta {
1963                id,
1964                metadata: args.source_range.into(),
1965            },
1966        },
1967        center: result.center,
1968        ccw: result.ccw > 0,
1969    };
1970
1971    let mut new_sketch = sketch;
1972    if let Some(tag) = &tag {
1973        new_sketch.add_tag(tag, &current_path, exec_state, None);
1974    }
1975    if loops_back_to_start {
1976        new_sketch.is_closed = ProfileClosed::Implicitly;
1977    }
1978
1979    new_sketch.paths.push(current_path);
1980
1981    Ok(new_sketch)
1982}
1983
1984/// Draw a bezier curve.
1985pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1986    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1987    let control1 = args.get_kw_arg_opt("control1", &RuntimeType::point2d(), exec_state)?;
1988    let control2 = args.get_kw_arg_opt("control2", &RuntimeType::point2d(), exec_state)?;
1989    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1990    let control1_absolute = args.get_kw_arg_opt("control1Absolute", &RuntimeType::point2d(), exec_state)?;
1991    let control2_absolute = args.get_kw_arg_opt("control2Absolute", &RuntimeType::point2d(), exec_state)?;
1992    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1993    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1994
1995    let new_sketch = inner_bezier_curve(
1996        sketch,
1997        control1,
1998        control2,
1999        end,
2000        control1_absolute,
2001        control2_absolute,
2002        end_absolute,
2003        tag,
2004        exec_state,
2005        args,
2006    )
2007    .await?;
2008    Ok(KclValue::Sketch {
2009        value: Box::new(new_sketch),
2010    })
2011}
2012
2013#[allow(clippy::too_many_arguments)]
2014async fn inner_bezier_curve(
2015    sketch: Sketch,
2016    control1: Option<[TyF64; 2]>,
2017    control2: Option<[TyF64; 2]>,
2018    end: Option<[TyF64; 2]>,
2019    control1_absolute: Option<[TyF64; 2]>,
2020    control2_absolute: Option<[TyF64; 2]>,
2021    end_absolute: Option<[TyF64; 2]>,
2022    tag: Option<TagNode>,
2023    exec_state: &mut ExecState,
2024    args: Args,
2025) -> Result<Sketch, KclError> {
2026    let from = sketch.current_pen_position()?;
2027    let id = exec_state.next_uuid();
2028
2029    let (to, control1_abs, control2_abs) = match (
2030        control1,
2031        control2,
2032        end,
2033        control1_absolute,
2034        control2_absolute,
2035        end_absolute,
2036    ) {
2037        // Relative
2038        (Some(control1), Some(control2), Some(end), None, None, None) => {
2039            let delta = end.clone();
2040            let to = [
2041                from.x + end[0].to_length_units(from.units),
2042                from.y + end[1].to_length_units(from.units),
2043            ];
2044            // Calculate absolute control points
2045            let control1_abs = [
2046                from.x + control1[0].to_length_units(from.units),
2047                from.y + control1[1].to_length_units(from.units),
2048            ];
2049            let control2_abs = [
2050                from.x + control2[0].to_length_units(from.units),
2051                from.y + control2[1].to_length_units(from.units),
2052            ];
2053
2054            exec_state
2055                .batch_modeling_cmd(
2056                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
2057                    ModelingCmd::from(
2058                        mcmd::ExtendPath::builder()
2059                            .path(sketch.id.into())
2060                            .segment(PathSegment::Bezier {
2061                                control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2062                                control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2063                                end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
2064                                relative: true,
2065                            })
2066                            .build(),
2067                    ),
2068                )
2069                .await?;
2070            (to, control1_abs, control2_abs)
2071        }
2072        // Absolute
2073        (None, None, None, Some(control1), Some(control2), Some(end)) => {
2074            let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
2075            let control1_abs = control1.clone().map(|v| v.to_length_units(from.units));
2076            let control2_abs = control2.clone().map(|v| v.to_length_units(from.units));
2077            exec_state
2078                .batch_modeling_cmd(
2079                    ModelingCmdMeta::from_args_id(exec_state, &args, id),
2080                    ModelingCmd::from(
2081                        mcmd::ExtendPath::builder()
2082                            .path(sketch.id.into())
2083                            .segment(PathSegment::Bezier {
2084                                control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2085                                control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2086                                end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
2087                                relative: false,
2088                            })
2089                            .build(),
2090                    ),
2091                )
2092                .await?;
2093            (to, control1_abs, control2_abs)
2094        }
2095        _ => {
2096            return Err(KclError::new_semantic(KclErrorDetails::new(
2097                "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
2098                vec![args.source_range],
2099            )));
2100        }
2101    };
2102
2103    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2104
2105    let current_path = Path::Bezier {
2106        base: BasePath {
2107            from: from.ignore_units(),
2108            to,
2109            tag: tag.clone(),
2110            units: sketch.units,
2111            geo_meta: GeoMeta {
2112                id,
2113                metadata: args.source_range.into(),
2114            },
2115        },
2116        control1: control1_abs,
2117        control2: control2_abs,
2118    };
2119
2120    let mut new_sketch = sketch;
2121    if let Some(tag) = &tag {
2122        new_sketch.add_tag(tag, &current_path, exec_state, None);
2123    }
2124    if loops_back_to_start {
2125        new_sketch.is_closed = ProfileClosed::Implicitly;
2126    }
2127
2128    new_sketch.paths.push(current_path);
2129
2130    Ok(new_sketch)
2131}
2132
2133/// Use a sketch to cut a hole in another sketch.
2134pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2135    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2136
2137    let tool: Vec<Sketch> = args.get_kw_arg(
2138        "tool",
2139        &RuntimeType::Array(
2140            Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
2141            ArrayLen::Minimum(1),
2142        ),
2143        exec_state,
2144    )?;
2145
2146    let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
2147    Ok(KclValue::Sketch {
2148        value: Box::new(new_sketch),
2149    })
2150}
2151
2152async fn inner_subtract_2d(
2153    mut sketch: Sketch,
2154    tool: Vec<Sketch>,
2155    exec_state: &mut ExecState,
2156    args: Args,
2157) -> Result<Sketch, KclError> {
2158    for hole_sketch in tool {
2159        exec_state
2160            .batch_modeling_cmd(
2161                ModelingCmdMeta::from_args(exec_state, &args),
2162                ModelingCmd::from(
2163                    mcmd::Solid2dAddHole::builder()
2164                        .object_id(sketch.id)
2165                        .hole_id(hole_sketch.id)
2166                        .build(),
2167                ),
2168            )
2169            .await?;
2170
2171        // Hide the source hole since it's no longer its own profile,
2172        // it's just used to modify some other profile.
2173        exec_state
2174            .batch_modeling_cmd(
2175                ModelingCmdMeta::from_args(exec_state, &args),
2176                ModelingCmd::from(
2177                    mcmd::ObjectVisible::builder()
2178                        .object_id(hole_sketch.id)
2179                        .hidden(true)
2180                        .build(),
2181                ),
2182            )
2183            .await?;
2184
2185        // NOTE: We don't look at the inner paths of the hole/tool sketch.
2186        // So if you have circle A, and it has a circular hole cut out (B),
2187        // then you cut A out of an even bigger circle C, we will lose that info.
2188        // Not really sure what to do about this.
2189        sketch.inner_paths.extend_from_slice(&hole_sketch.paths);
2190    }
2191
2192    // Returns the input sketch, exactly as it was, zero modifications.
2193    // This means the edges from `tool` are basically ignored, they're not in the output.
2194    Ok(sketch)
2195}
2196
2197/// Calculate the (x, y) point on an ellipse given x or y and the major/minor radii of the ellipse.
2198pub async fn elliptic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2199    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2200    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2201    let major_radius = args.get_kw_arg("majorRadius", &RuntimeType::num_any(), exec_state)?;
2202    let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::num_any(), exec_state)?;
2203
2204    let elliptic_point = inner_elliptic_point(x, y, major_radius, minor_radius, &args).await?;
2205
2206    args.make_kcl_val_from_point(elliptic_point, exec_state.length_unit().into())
2207}
2208
2209async fn inner_elliptic_point(
2210    x: Option<TyF64>,
2211    y: Option<TyF64>,
2212    major_radius: TyF64,
2213    minor_radius: TyF64,
2214    args: &Args,
2215) -> Result<[f64; 2], KclError> {
2216    let major_radius = major_radius.n;
2217    let minor_radius = minor_radius.n;
2218    if let Some(x) = x {
2219        if x.n.abs() > major_radius {
2220            Err(KclError::Type {
2221                details: KclErrorDetails::new(
2222                    format!(
2223                        "Invalid input. The x value, {}, cannot be larger than the major radius {}.",
2224                        x.n, major_radius
2225                    ),
2226                    vec![args.source_range],
2227                ),
2228            })
2229        } else {
2230            Ok((
2231                x.n,
2232                minor_radius * (1.0 - x.n.powf(2.0) / major_radius.powf(2.0)).sqrt(),
2233            )
2234                .into())
2235        }
2236    } else if let Some(y) = y {
2237        if y.n > minor_radius {
2238            Err(KclError::Type {
2239                details: KclErrorDetails::new(
2240                    format!(
2241                        "Invalid input. The y value, {}, cannot be larger than the minor radius {}.",
2242                        y.n, minor_radius
2243                    ),
2244                    vec![args.source_range],
2245                ),
2246            })
2247        } else {
2248            Ok((
2249                major_radius * (1.0 - y.n.powf(2.0) / minor_radius.powf(2.0)).sqrt(),
2250                y.n,
2251            )
2252                .into())
2253        }
2254    } else {
2255        Err(KclError::Type {
2256            details: KclErrorDetails::new(
2257                "Invalid input. Must have either x or y, you cannot have both or neither.".to_owned(),
2258                vec![args.source_range],
2259            ),
2260        })
2261    }
2262}
2263
2264/// Draw an elliptical arc.
2265pub async fn elliptic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2266    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2267
2268    let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
2269    let angle_start = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
2270    let angle_end = args.get_kw_arg("angleEnd", &RuntimeType::degrees(), exec_state)?;
2271    let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
2272    let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
2273    let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
2274    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2275
2276    let new_sketch = inner_elliptic(
2277        sketch,
2278        center,
2279        angle_start,
2280        angle_end,
2281        major_radius,
2282        major_axis,
2283        minor_radius,
2284        tag,
2285        exec_state,
2286        args,
2287    )
2288    .await?;
2289    Ok(KclValue::Sketch {
2290        value: Box::new(new_sketch),
2291    })
2292}
2293
2294#[allow(clippy::too_many_arguments)]
2295pub(crate) async fn inner_elliptic(
2296    sketch: Sketch,
2297    center: [TyF64; 2],
2298    angle_start: TyF64,
2299    angle_end: TyF64,
2300    major_radius: Option<TyF64>,
2301    major_axis: Option<[TyF64; 2]>,
2302    minor_radius: TyF64,
2303    tag: Option<TagNode>,
2304    exec_state: &mut ExecState,
2305    args: Args,
2306) -> Result<Sketch, KclError> {
2307    let from: Point2d = sketch.current_pen_position()?;
2308    let id = exec_state.next_uuid();
2309
2310    let center_u = point_to_len_unit(center, from.units);
2311
2312    let major_axis = match (major_axis, major_radius) {
2313        (Some(_), Some(_)) | (None, None) => {
2314            return Err(KclError::new_type(KclErrorDetails::new(
2315                "Provide either `majorAxis` or `majorRadius`.".to_string(),
2316                vec![args.source_range],
2317            )));
2318        }
2319        (Some(major_axis), None) => major_axis,
2320        (None, Some(major_radius)) => [
2321            major_radius.clone(),
2322            TyF64 {
2323                n: 0.0,
2324                ty: major_radius.ty,
2325            },
2326        ],
2327    };
2328    let start_angle = Angle::from_degrees(angle_start.to_degrees(exec_state, args.source_range));
2329    let end_angle = Angle::from_degrees(angle_end.to_degrees(exec_state, args.source_range));
2330    let major_axis_magnitude = (major_axis[0].to_length_units(from.units) * major_axis[0].to_length_units(from.units)
2331        + major_axis[1].to_length_units(from.units) * major_axis[1].to_length_units(from.units))
2332    .sqrt();
2333    let to = [
2334        major_axis_magnitude * libm::cos(end_angle.to_radians()),
2335        minor_radius.to_length_units(from.units) * libm::sin(end_angle.to_radians()),
2336    ];
2337    let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2338    let major_axis_angle = libm::atan2(major_axis[1].n, major_axis[0].n);
2339
2340    let point = [
2341        center_u[0] + to[0] * libm::cos(major_axis_angle) - to[1] * libm::sin(major_axis_angle),
2342        center_u[1] + to[0] * libm::sin(major_axis_angle) + to[1] * libm::cos(major_axis_angle),
2343    ];
2344
2345    let axis = major_axis.map(|x| x.to_mm());
2346    exec_state
2347        .batch_modeling_cmd(
2348            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2349            ModelingCmd::from(
2350                mcmd::ExtendPath::builder()
2351                    .path(sketch.id.into())
2352                    .segment(PathSegment::Ellipse {
2353                        center: KPoint2d::from(untyped_point_to_mm(center_u, from.units)).map(LengthUnit),
2354                        major_axis: axis.map(LengthUnit).into(),
2355                        minor_radius: LengthUnit(minor_radius.to_mm()),
2356                        start_angle,
2357                        end_angle,
2358                    })
2359                    .build(),
2360            ),
2361        )
2362        .await?;
2363
2364    let current_path = Path::Ellipse {
2365        ccw: start_angle < end_angle,
2366        center: center_u,
2367        major_axis: axis,
2368        minor_radius: minor_radius.to_mm(),
2369        base: BasePath {
2370            from: from.ignore_units(),
2371            to: point,
2372            tag: tag.clone(),
2373            units: sketch.units,
2374            geo_meta: GeoMeta {
2375                id,
2376                metadata: args.source_range.into(),
2377            },
2378        },
2379    };
2380    let mut new_sketch = sketch;
2381    if let Some(tag) = &tag {
2382        new_sketch.add_tag(tag, &current_path, exec_state, None);
2383    }
2384    if loops_back_to_start {
2385        new_sketch.is_closed = ProfileClosed::Implicitly;
2386    }
2387
2388    new_sketch.paths.push(current_path);
2389
2390    Ok(new_sketch)
2391}
2392
2393/// Calculate the (x, y) point on an hyperbola given x or y and the semi major/minor of the ellipse.
2394pub async fn hyperbolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2395    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2396    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2397    let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::num_any(), exec_state)?;
2398    let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::num_any(), exec_state)?;
2399
2400    let hyperbolic_point = inner_hyperbolic_point(x, y, semi_major, semi_minor, &args).await?;
2401
2402    args.make_kcl_val_from_point(hyperbolic_point, exec_state.length_unit().into())
2403}
2404
2405async fn inner_hyperbolic_point(
2406    x: Option<TyF64>,
2407    y: Option<TyF64>,
2408    semi_major: TyF64,
2409    semi_minor: TyF64,
2410    args: &Args,
2411) -> Result<[f64; 2], KclError> {
2412    let semi_major = semi_major.n;
2413    let semi_minor = semi_minor.n;
2414    if let Some(x) = x {
2415        if x.n.abs() < semi_major {
2416            Err(KclError::Type {
2417                details: KclErrorDetails::new(
2418                    format!(
2419                        "Invalid input. The x value, {}, cannot be less than the semi major value, {}.",
2420                        x.n, semi_major
2421                    ),
2422                    vec![args.source_range],
2423                ),
2424            })
2425        } else {
2426            Ok((x.n, semi_minor * (x.n.powf(2.0) / semi_major.powf(2.0) - 1.0).sqrt()).into())
2427        }
2428    } else if let Some(y) = y {
2429        Ok((semi_major * (y.n.powf(2.0) / semi_minor.powf(2.0) + 1.0).sqrt(), y.n).into())
2430    } else {
2431        Err(KclError::Type {
2432            details: KclErrorDetails::new(
2433                "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2434                vec![args.source_range],
2435            ),
2436        })
2437    }
2438}
2439
2440/// Draw a hyperbolic arc.
2441pub async fn hyperbolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2442    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2443
2444    let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::length(), exec_state)?;
2445    let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::length(), exec_state)?;
2446    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2447    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2448    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2449    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2450    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2451
2452    let new_sketch = inner_hyperbolic(
2453        sketch,
2454        semi_major,
2455        semi_minor,
2456        interior,
2457        end,
2458        interior_absolute,
2459        end_absolute,
2460        tag,
2461        exec_state,
2462        args,
2463    )
2464    .await?;
2465    Ok(KclValue::Sketch {
2466        value: Box::new(new_sketch),
2467    })
2468}
2469
2470/// Calculate the tangent of a hyperbolic given a point on the curve
2471fn hyperbolic_tangent(point: Point2d, semi_major: f64, semi_minor: f64) -> [f64; 2] {
2472    (point.y * semi_major.powf(2.0), point.x * semi_minor.powf(2.0)).into()
2473}
2474
2475#[allow(clippy::too_many_arguments)]
2476pub(crate) async fn inner_hyperbolic(
2477    sketch: Sketch,
2478    semi_major: TyF64,
2479    semi_minor: TyF64,
2480    interior: Option<[TyF64; 2]>,
2481    end: Option<[TyF64; 2]>,
2482    interior_absolute: Option<[TyF64; 2]>,
2483    end_absolute: Option<[TyF64; 2]>,
2484    tag: Option<TagNode>,
2485    exec_state: &mut ExecState,
2486    args: Args,
2487) -> Result<Sketch, KclError> {
2488    let from = sketch.current_pen_position()?;
2489    let id = exec_state.next_uuid();
2490
2491    let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2492        (Some(interior), Some(end), None, None) => (interior, end, true),
2493        (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2494        _ => return Err(KclError::Type {
2495            details: KclErrorDetails::new(
2496                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2497                    .to_owned(),
2498                vec![args.source_range],
2499            ),
2500        }),
2501    };
2502
2503    let interior = point_to_len_unit(interior, from.units);
2504    let end = point_to_len_unit(end, from.units);
2505    let end_point = Point2d {
2506        x: end[0],
2507        y: end[1],
2508        units: from.units,
2509    };
2510    let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
2511
2512    let semi_major_u = semi_major.to_length_units(from.units);
2513    let semi_minor_u = semi_minor.to_length_units(from.units);
2514
2515    let start_tangent = hyperbolic_tangent(from, semi_major_u, semi_minor_u);
2516    let end_tangent = hyperbolic_tangent(end_point, semi_major_u, semi_minor_u);
2517
2518    exec_state
2519        .batch_modeling_cmd(
2520            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2521            ModelingCmd::from(
2522                mcmd::ExtendPath::builder()
2523                    .path(sketch.id.into())
2524                    .segment(PathSegment::ConicTo {
2525                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2526                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2527                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2528                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2529                        relative,
2530                    })
2531                    .build(),
2532            ),
2533        )
2534        .await?;
2535
2536    let current_path = Path::Conic {
2537        base: BasePath {
2538            from: from.ignore_units(),
2539            to: end,
2540            tag: tag.clone(),
2541            units: sketch.units,
2542            geo_meta: GeoMeta {
2543                id,
2544                metadata: args.source_range.into(),
2545            },
2546        },
2547    };
2548
2549    let mut new_sketch = sketch;
2550    if let Some(tag) = &tag {
2551        new_sketch.add_tag(tag, &current_path, exec_state, None);
2552    }
2553    if loops_back_to_start {
2554        new_sketch.is_closed = ProfileClosed::Implicitly;
2555    }
2556
2557    new_sketch.paths.push(current_path);
2558
2559    Ok(new_sketch)
2560}
2561
2562/// Calculate the point on a parabola given the coefficient of the parabola and either x or y
2563pub async fn parabolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2564    let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2565    let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2566    let coefficients = args.get_kw_arg(
2567        "coefficients",
2568        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2569        exec_state,
2570    )?;
2571
2572    let parabolic_point = inner_parabolic_point(x, y, &coefficients, &args).await?;
2573
2574    args.make_kcl_val_from_point(parabolic_point, exec_state.length_unit().into())
2575}
2576
2577async fn inner_parabolic_point(
2578    x: Option<TyF64>,
2579    y: Option<TyF64>,
2580    coefficients: &[TyF64; 3],
2581    args: &Args,
2582) -> Result<[f64; 2], KclError> {
2583    let a = coefficients[0].n;
2584    let b = coefficients[1].n;
2585    let c = coefficients[2].n;
2586    if let Some(x) = x {
2587        Ok((x.n, a * x.n.powf(2.0) + b * x.n + c).into())
2588    } else if let Some(y) = y {
2589        let det = (b.powf(2.0) - 4.0 * a * (c - y.n)).sqrt();
2590        Ok(((-b + det) / (2.0 * a), y.n).into())
2591    } else {
2592        Err(KclError::Type {
2593            details: KclErrorDetails::new(
2594                "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2595                vec![args.source_range],
2596            ),
2597        })
2598    }
2599}
2600
2601/// Draw a parabolic arc.
2602pub async fn parabolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2603    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2604
2605    let coefficients = args.get_kw_arg_opt(
2606        "coefficients",
2607        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2608        exec_state,
2609    )?;
2610    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2611    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2612    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2613    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2614    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2615
2616    let new_sketch = inner_parabolic(
2617        sketch,
2618        coefficients,
2619        interior,
2620        end,
2621        interior_absolute,
2622        end_absolute,
2623        tag,
2624        exec_state,
2625        args,
2626    )
2627    .await?;
2628    Ok(KclValue::Sketch {
2629        value: Box::new(new_sketch),
2630    })
2631}
2632
2633fn parabolic_tangent(point: Point2d, a: f64, b: f64) -> [f64; 2] {
2634    //f(x) = ax^2 + bx + c
2635    //f'(x) = 2ax + b
2636    (1.0, 2.0 * a * point.x + b).into()
2637}
2638
2639#[allow(clippy::too_many_arguments)]
2640pub(crate) async fn inner_parabolic(
2641    sketch: Sketch,
2642    coefficients: Option<[TyF64; 3]>,
2643    interior: Option<[TyF64; 2]>,
2644    end: Option<[TyF64; 2]>,
2645    interior_absolute: Option<[TyF64; 2]>,
2646    end_absolute: Option<[TyF64; 2]>,
2647    tag: Option<TagNode>,
2648    exec_state: &mut ExecState,
2649    args: Args,
2650) -> Result<Sketch, KclError> {
2651    let from = sketch.current_pen_position()?;
2652    let id = exec_state.next_uuid();
2653
2654    if (coefficients.is_some() && interior.is_some()) || (coefficients.is_none() && interior.is_none()) {
2655        return Err(KclError::Type {
2656            details: KclErrorDetails::new(
2657                "Invalid combination of arguments. Either provide (a, b, c) or (interior)".to_owned(),
2658                vec![args.source_range],
2659            ),
2660        });
2661    }
2662
2663    let (interior, end, relative) = match (coefficients.clone(), interior, end, interior_absolute, end_absolute) {
2664        (None, Some(interior), Some(end), None, None) => {
2665            let interior = point_to_len_unit(interior, from.units);
2666            let end = point_to_len_unit(end, from.units);
2667            (interior,end, true)
2668        },
2669        (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
2670            let interior_absolute = point_to_len_unit(interior_absolute, from.units);
2671            let end_absolute = point_to_len_unit(end_absolute, from.units);
2672            (interior_absolute, end_absolute, false)
2673        }
2674        (Some(coefficients), _, Some(end), _, _) => {
2675            let end = point_to_len_unit(end, from.units);
2676            let interior =
2677            inner_parabolic_point(
2678                Some(TyF64::count(0.5 * (from.x + end[0]))),
2679                None,
2680                &coefficients,
2681                &args,
2682            )
2683            .await?;
2684            (interior, end, true)
2685        }
2686        (Some(coefficients), _, _, _, Some(end)) => {
2687            let end = point_to_len_unit(end, from.units);
2688            let interior =
2689            inner_parabolic_point(
2690                Some(TyF64::count(0.5 * (from.x + end[0]))),
2691                None,
2692                &coefficients,
2693                &args,
2694            )
2695            .await?;
2696            (interior, end, false)
2697        }
2698        _ => return
2699            Err(KclError::Type{details: KclErrorDetails::new(
2700                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute) if coefficients are not provided."
2701                    .to_owned(),
2702                vec![args.source_range],
2703            )}),
2704    };
2705
2706    let end_point = Point2d {
2707        x: end[0],
2708        y: end[1],
2709        units: from.units,
2710    };
2711
2712    let (a, b, _c) = if let Some([a, b, c]) = coefficients {
2713        (a.n, b.n, c.n)
2714    } else {
2715        // Any three points is enough to uniquely define a parabola
2716        let denom = (from.x - interior[0]) * (from.x - end_point.x) * (interior[0] - end_point.x);
2717        let a = (end_point.x * (interior[1] - from.y)
2718            + interior[0] * (from.y - end_point.y)
2719            + from.x * (end_point.y - interior[1]))
2720            / denom;
2721        let b = (end_point.x.powf(2.0) * (from.y - interior[1])
2722            + interior[0].powf(2.0) * (end_point.y - from.y)
2723            + from.x.powf(2.0) * (interior[1] - end_point.y))
2724            / denom;
2725        let c = (interior[0] * end_point.x * (interior[0] - end_point.x) * from.y
2726            + end_point.x * from.x * (end_point.x - from.x) * interior[1]
2727            + from.x * interior[0] * (from.x - interior[0]) * end_point.y)
2728            / denom;
2729
2730        (a, b, c)
2731    };
2732
2733    let start_tangent = parabolic_tangent(from, a, b);
2734    let end_tangent = parabolic_tangent(end_point, a, b);
2735
2736    exec_state
2737        .batch_modeling_cmd(
2738            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2739            ModelingCmd::from(
2740                mcmd::ExtendPath::builder()
2741                    .path(sketch.id.into())
2742                    .segment(PathSegment::ConicTo {
2743                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2744                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2745                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2746                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2747                        relative,
2748                    })
2749                    .build(),
2750            ),
2751        )
2752        .await?;
2753
2754    let current_path = Path::Conic {
2755        base: BasePath {
2756            from: from.ignore_units(),
2757            to: end,
2758            tag: tag.clone(),
2759            units: sketch.units,
2760            geo_meta: GeoMeta {
2761                id,
2762                metadata: args.source_range.into(),
2763            },
2764        },
2765    };
2766
2767    let mut new_sketch = sketch;
2768    if let Some(tag) = &tag {
2769        new_sketch.add_tag(tag, &current_path, exec_state, None);
2770    }
2771
2772    new_sketch.paths.push(current_path);
2773
2774    Ok(new_sketch)
2775}
2776
2777fn conic_tangent(coefficients: [f64; 6], point: [f64; 2]) -> [f64; 2] {
2778    let [a, b, c, d, e, _] = coefficients;
2779
2780    (
2781        c * point[0] + 2.0 * b * point[1] + e,
2782        -(2.0 * a * point[0] + c * point[1] + d),
2783    )
2784        .into()
2785}
2786
2787/// Draw a conic section
2788pub async fn conic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2789    let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2790
2791    let start_tangent = args.get_kw_arg_opt("startTangent", &RuntimeType::point2d(), exec_state)?;
2792    let end_tangent = args.get_kw_arg_opt("endTangent", &RuntimeType::point2d(), exec_state)?;
2793    let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2794    let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2795    let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2796    let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2797    let coefficients = args.get_kw_arg_opt(
2798        "coefficients",
2799        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(6)),
2800        exec_state,
2801    )?;
2802    let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2803
2804    let new_sketch = inner_conic(
2805        sketch,
2806        start_tangent,
2807        end,
2808        end_tangent,
2809        interior,
2810        coefficients,
2811        interior_absolute,
2812        end_absolute,
2813        tag,
2814        exec_state,
2815        args,
2816    )
2817    .await?;
2818    Ok(KclValue::Sketch {
2819        value: Box::new(new_sketch),
2820    })
2821}
2822
2823#[allow(clippy::too_many_arguments)]
2824pub(crate) async fn inner_conic(
2825    sketch: Sketch,
2826    start_tangent: Option<[TyF64; 2]>,
2827    end: Option<[TyF64; 2]>,
2828    end_tangent: Option<[TyF64; 2]>,
2829    interior: Option<[TyF64; 2]>,
2830    coefficients: Option<[TyF64; 6]>,
2831    interior_absolute: Option<[TyF64; 2]>,
2832    end_absolute: Option<[TyF64; 2]>,
2833    tag: Option<TagNode>,
2834    exec_state: &mut ExecState,
2835    args: Args,
2836) -> Result<Sketch, KclError> {
2837    let from: Point2d = sketch.current_pen_position()?;
2838    let id = exec_state.next_uuid();
2839
2840    if (coefficients.is_some() && (start_tangent.is_some() || end_tangent.is_some()))
2841        || (coefficients.is_none() && (start_tangent.is_none() && end_tangent.is_none()))
2842    {
2843        return Err(KclError::Type {
2844            details: KclErrorDetails::new(
2845                "Invalid combination of arguments. Either provide coefficients or (startTangent, endTangent)"
2846                    .to_owned(),
2847                vec![args.source_range],
2848            ),
2849        });
2850    }
2851
2852    let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2853        (Some(interior), Some(end), None, None) => (interior, end, true),
2854        (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2855        _ => return Err(KclError::Type {
2856            details: KclErrorDetails::new(
2857                "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2858                    .to_owned(),
2859                vec![args.source_range],
2860            ),
2861        }),
2862    };
2863
2864    let end = point_to_len_unit(end, from.units);
2865    let interior = point_to_len_unit(interior, from.units);
2866
2867    let (start_tangent, end_tangent) = if let Some(coeffs) = coefficients {
2868        let (coeffs, _) = untype_array(coeffs);
2869        (conic_tangent(coeffs, [from.x, from.y]), conic_tangent(coeffs, end))
2870    } else {
2871        let start = if let Some(start_tangent) = start_tangent {
2872            point_to_len_unit(start_tangent, from.units)
2873        } else {
2874            let previous_point = sketch
2875                .get_tangential_info_from_paths()
2876                .tan_previous_point(from.ignore_units());
2877            let from = from.ignore_units();
2878            [from[0] - previous_point[0], from[1] - previous_point[1]]
2879        };
2880
2881        let Some(end_tangent) = end_tangent else {
2882            return Err(KclError::new_semantic(KclErrorDetails::new(
2883                "You must either provide either `coefficients` or `endTangent`.".to_owned(),
2884                vec![args.source_range],
2885            )));
2886        };
2887        let end_tan = point_to_len_unit(end_tangent, from.units);
2888        (start, end_tan)
2889    };
2890
2891    exec_state
2892        .batch_modeling_cmd(
2893            ModelingCmdMeta::from_args_id(exec_state, &args, id),
2894            ModelingCmd::from(
2895                mcmd::ExtendPath::builder()
2896                    .path(sketch.id.into())
2897                    .segment(PathSegment::ConicTo {
2898                        start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2899                        end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2900                        end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2901                        interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2902                        relative,
2903                    })
2904                    .build(),
2905            ),
2906        )
2907        .await?;
2908
2909    let current_path = Path::Conic {
2910        base: BasePath {
2911            from: from.ignore_units(),
2912            to: end,
2913            tag: tag.clone(),
2914            units: sketch.units,
2915            geo_meta: GeoMeta {
2916                id,
2917                metadata: args.source_range.into(),
2918            },
2919        },
2920    };
2921
2922    let mut new_sketch = sketch;
2923    if let Some(tag) = &tag {
2924        new_sketch.add_tag(tag, &current_path, exec_state, None);
2925    }
2926
2927    new_sketch.paths.push(current_path);
2928
2929    Ok(new_sketch)
2930}
2931
2932pub(super) async fn region(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2933    let point = args.get_kw_arg_opt(
2934        "point",
2935        &RuntimeType::Union(vec![RuntimeType::point2d(), RuntimeType::segment()]),
2936        exec_state,
2937    )?;
2938    let segments = args.get_kw_arg_opt(
2939        "segments",
2940        &RuntimeType::Array(Box::new(RuntimeType::segment()), ArrayLen::Minimum(1)),
2941        exec_state,
2942    )?;
2943    let intersection_index = args.get_kw_arg_opt("intersectionIndex", &RuntimeType::count(), exec_state)?;
2944    let direction = args.get_kw_arg_opt("direction", &RuntimeType::string(), exec_state)?;
2945    let sketch = args.get_kw_arg_opt("sketch", &RuntimeType::any(), exec_state)?;
2946    inner_region(point, segments, intersection_index, direction, sketch, exec_state, args).await
2947}
2948
2949/// Helper enum to reduce cloning of Sketch and Segment in the two branches of
2950/// region creation.
2951#[expect(clippy::large_enum_variant)]
2952enum SketchOrSegment {
2953    Sketch(Sketch),
2954    Segment(Segment),
2955}
2956
2957impl SketchOrSegment {
2958    fn sketch(&self) -> Result<&Sketch, KclError> {
2959        match self {
2960            SketchOrSegment::Sketch(sketch) => Ok(sketch),
2961            SketchOrSegment::Segment(segment) => segment.sketch.as_ref().ok_or_else(|| {
2962                KclError::new_semantic(KclErrorDetails::new(
2963                    "Segment should have an associated sketch".to_owned(),
2964                    vec![],
2965                ))
2966            }),
2967        }
2968    }
2969}
2970
2971async fn inner_region(
2972    point: Option<KclValue>,
2973    segments: Option<Vec<KclValue>>,
2974    intersection_index: Option<TyF64>,
2975    direction: Option<CircularDirection>,
2976    sketch: Option<KclValue>,
2977    exec_state: &mut ExecState,
2978    args: Args,
2979) -> Result<KclValue, KclError> {
2980    let region_id = exec_state.next_uuid();
2981
2982    let (sketch_or_segment, region_mapping) = match (point, segments) {
2983        (Some(point), None) => {
2984            let (sketch, pt) = region_from_point(point, sketch, &args)?;
2985
2986            let meta = ModelingCmdMeta::from_args_id(exec_state, &args, region_id);
2987            let response = exec_state
2988                .send_modeling_cmd(
2989                    meta,
2990                    ModelingCmd::from(
2991                        mcmd::CreateRegionFromQueryPoint::builder()
2992                            .object_id(sketch.sketch()?.id)
2993                            .query_point(KPoint2d::from(point_to_mm(pt.clone())).map(LengthUnit))
2994                            .build(),
2995                    ),
2996                )
2997                .await?;
2998
2999            let region_mapping = if let kcmc::websocket::OkWebSocketResponseData::Modeling {
3000                modeling_response: kcmc::ok_response::OkModelingCmdResponse::CreateRegionFromQueryPoint(data),
3001            } = response
3002            {
3003                data.region_mapping
3004            } else {
3005                Default::default()
3006            };
3007
3008            (sketch, region_mapping)
3009        }
3010        (None, Some(segments)) => {
3011            if sketch.is_some() {
3012                return Err(KclError::new_semantic(KclErrorDetails::new(
3013                    "Sketch parameter must not be provided when segments parameters is provided".to_owned(),
3014                    vec![args.source_range],
3015                )));
3016            }
3017            let segments_len = segments.len();
3018            let mut segments = segments.into_iter();
3019            let Some(seg0_value) = segments.next() else {
3020                return Err(KclError::new_argument(KclErrorDetails::new(
3021                    format!("Expected at least 1 segment to create a region, but got {segments_len}"),
3022                    vec![args.source_range],
3023                )));
3024            };
3025            let seg1_value = segments.next().unwrap_or_else(|| seg0_value.clone());
3026            let Some(seg0) = seg0_value.into_segment() else {
3027                return Err(KclError::new_argument(KclErrorDetails::new(
3028                    "Expected first segment to be a Segment".to_owned(),
3029                    vec![args.source_range],
3030                )));
3031            };
3032            let Some(seg1) = seg1_value.into_segment() else {
3033                return Err(KclError::new_argument(KclErrorDetails::new(
3034                    "Expected second segment to be a Segment".to_owned(),
3035                    vec![args.source_range],
3036                )));
3037            };
3038            let intersection_index = intersection_index.map(|n| n.n as i32).unwrap_or(-1);
3039            let direction = direction.unwrap_or(CircularDirection::Counterclockwise);
3040
3041            let Some(sketch) = &seg0.sketch else {
3042                return Err(KclError::new_semantic(KclErrorDetails::new(
3043                    "Expected first segment to have an associated sketch. The sketch must be solved to create a region from it.".to_owned(),
3044                    vec![args.source_range],
3045                )));
3046            };
3047
3048            let meta = ModelingCmdMeta::from_args_id(exec_state, &args, region_id);
3049            let response = exec_state
3050                .send_modeling_cmd(
3051                    meta,
3052                    ModelingCmd::from(
3053                        mcmd::CreateRegion::builder()
3054                            .object_id(sketch.id)
3055                            .segment(seg0.id)
3056                            .intersection_segment(seg1.id)
3057                            .intersection_index(intersection_index)
3058                            .curve_clockwise(direction.is_clockwise())
3059                            .build(),
3060                    ),
3061                )
3062                .await?;
3063
3064            let region_mapping = if let kcmc::websocket::OkWebSocketResponseData::Modeling {
3065                modeling_response: kcmc::ok_response::OkModelingCmdResponse::CreateRegion(data),
3066            } = response
3067            {
3068                data.region_mapping
3069            } else {
3070                Default::default()
3071            };
3072
3073            (SketchOrSegment::Segment(seg0), region_mapping)
3074        }
3075        (Some(_), Some(_)) => {
3076            return Err(KclError::new_semantic(KclErrorDetails::new(
3077                "Cannot provide both point and segments parameters. Choose one.".to_owned(),
3078                vec![args.source_range],
3079            )));
3080        }
3081        (None, None) => {
3082            return Err(KclError::new_semantic(KclErrorDetails::new(
3083                "Either point or segments parameter must be provided".to_owned(),
3084                vec![args.source_range],
3085            )));
3086        }
3087    };
3088
3089    let units = exec_state.length_unit();
3090    let to = [0.0, 0.0];
3091    let first_path = Path::ToPoint {
3092        base: BasePath {
3093            from: to,
3094            to,
3095            units,
3096            tag: None,
3097            geo_meta: GeoMeta {
3098                id: match &sketch_or_segment {
3099                    SketchOrSegment::Sketch(sketch) => sketch.id,
3100                    SketchOrSegment::Segment(segment) => segment.id,
3101                },
3102                metadata: args.source_range.into(),
3103            },
3104        },
3105    };
3106    let start_base_path = BasePath {
3107        from: to,
3108        to,
3109        tag: None,
3110        units,
3111        geo_meta: GeoMeta {
3112            id: region_id,
3113            metadata: args.source_range.into(),
3114        },
3115    };
3116    let mut sketch = match sketch_or_segment {
3117        SketchOrSegment::Sketch(sketch) => sketch,
3118        SketchOrSegment::Segment(segment) => {
3119            if let Some(sketch) = segment.sketch {
3120                sketch
3121            } else {
3122                Sketch {
3123                    id: region_id,
3124                    original_id: region_id,
3125                    artifact_id: region_id.into(),
3126                    on: segment.surface.clone(),
3127                    paths: vec![first_path],
3128                    inner_paths: vec![],
3129                    units,
3130                    mirror: Default::default(),
3131                    clone: Default::default(),
3132                    synthetic_jump_path_ids: vec![],
3133                    meta: vec![args.source_range.into()],
3134                    tags: Default::default(),
3135                    start: start_base_path,
3136                    is_closed: ProfileClosed::Explicitly,
3137                }
3138            }
3139        }
3140    };
3141    sketch.id = region_id;
3142    sketch.original_id = region_id;
3143    sketch.artifact_id = region_id.into();
3144
3145    let mut region_mapping = region_mapping;
3146    if args.ctx.no_engine_commands().await && region_mapping.is_empty() {
3147        let mut mock_mapping = HashMap::new();
3148        for path in &sketch.paths {
3149            mock_mapping.insert(exec_state.next_uuid(), path.get_id());
3150        }
3151        region_mapping = mock_mapping;
3152    }
3153    let original_segment_ids = sketch.paths.iter().map(|p| p.get_id()).collect::<Vec<_>>();
3154    let original_seg_to_region = build_reverse_region_mapping(&region_mapping, &original_segment_ids);
3155
3156    {
3157        let mut new_paths = Vec::new();
3158        for path in &sketch.paths {
3159            let original_id = path.get_id();
3160            if let Some(region_ids) = original_seg_to_region.get(&original_id) {
3161                for region_id in region_ids {
3162                    let mut new_path = path.clone();
3163                    new_path.set_id(*region_id);
3164                    new_paths.push(new_path);
3165                }
3166            }
3167        }
3168
3169        // After mirror2d, sketch.paths still has the original (pre-mirror)
3170        // segment IDs. The region_mapping values are mirrored entity edge
3171        // IDs which don't match, so the remapping above produces no paths.
3172        // Fall back to creating paths from the region_mapping keys directly.
3173        if new_paths.is_empty() && !region_mapping.is_empty() {
3174            // Sort because the input order is undefined. We need to be
3175            // deterministic.
3176            for region_edge_id in region_mapping.keys().sorted_unstable() {
3177                // We don't know what the actual values are. We just need
3178                // something so that `do_post_extrude()` has the correct segment
3179                // IDs.
3180                new_paths.push(Path::ToPoint {
3181                    base: BasePath {
3182                        from: [0.0, 0.0],
3183                        to: [0.0, 0.0],
3184                        units,
3185                        tag: None,
3186                        geo_meta: GeoMeta {
3187                            id: *region_edge_id,
3188                            metadata: args.source_range.into(),
3189                        },
3190                    },
3191                });
3192            }
3193        }
3194
3195        sketch.paths = new_paths;
3196
3197        for (_tag_name, tag) in &mut sketch.tags {
3198            let Some(info) = tag.get_cur_info().cloned() else {
3199                continue;
3200            };
3201            let original_id = info.id;
3202            if let Some(region_ids) = original_seg_to_region.get(&original_id) {
3203                let epoch = tag.info.last().map(|(e, _)| *e).unwrap_or(0);
3204                for (i, region_id) in region_ids.iter().enumerate() {
3205                    if i == 0 {
3206                        if let Some((_, existing)) = tag.info.last_mut() {
3207                            existing.id = *region_id;
3208                        }
3209                    } else {
3210                        let mut new_info = info.clone();
3211                        new_info.id = *region_id;
3212                        tag.info.push((epoch, new_info));
3213                    }
3214                }
3215            }
3216        }
3217    }
3218
3219    // After mirror2d, sketch.mirror holds an edge from the mirrored entity
3220    // which is not valid on the region. Update it to a region edge so that
3221    // do_post_extrude can use it for Solid3dGetExtrusionFaceInfo.
3222    if sketch.mirror.is_some() {
3223        sketch.mirror = sketch.paths.first().map(|p| p.get_id());
3224    }
3225
3226    sketch.meta.push(args.source_range.into());
3227    sketch.is_closed = ProfileClosed::Explicitly;
3228
3229    Ok(KclValue::Sketch {
3230        value: Box::new(sketch),
3231    })
3232}
3233
3234/// The region mapping returned from the engine maps from region segment ID to
3235/// the original sketch segment ID. Create the reverse mapping, i.e. original
3236/// sketch segment ID to region segment IDs, where the entries are ordered by
3237/// the given original segments.
3238///
3239/// This runs in O(r + s) where r is the number of segments in the region, and s
3240/// is the number of segments in the original sketch. Technically, it's more
3241/// complicated since we also sort region segments, but in practice, there
3242/// should be very few of these.
3243pub(crate) fn build_reverse_region_mapping(
3244    region_mapping: &HashMap<Uuid, Uuid>,
3245    original_segments: &[Uuid],
3246) -> IndexMap<Uuid, Vec<Uuid>> {
3247    let mut reverse: HashMap<Uuid, Vec<Uuid>> = HashMap::default();
3248    #[expect(
3249        clippy::iter_over_hash_type,
3250        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."
3251    )]
3252    for (region_id, original_id) in region_mapping {
3253        reverse.entry(*original_id).or_default().push(*region_id);
3254    }
3255    #[expect(
3256        clippy::iter_over_hash_type,
3257        reason = "This is safe since we're just sorting values."
3258    )]
3259    for values in reverse.values_mut() {
3260        values.sort_unstable();
3261    }
3262    let mut ordered = IndexMap::with_capacity(original_segments.len());
3263    for original_id in original_segments {
3264        let mut region_ids = Vec::new();
3265        reverse.entry(*original_id).and_modify(|entry_value| {
3266            region_ids = std::mem::take(entry_value);
3267        });
3268        if !region_ids.is_empty() {
3269            ordered.insert(*original_id, region_ids);
3270        }
3271    }
3272    ordered
3273}
3274
3275fn region_from_point(
3276    point: KclValue,
3277    sketch: Option<KclValue>,
3278    args: &Args,
3279) -> Result<(SketchOrSegment, [TyF64; 2]), KclError> {
3280    match point {
3281        KclValue::HomArray { .. } | KclValue::Tuple { .. } => {
3282            let Some(pt) = <[TyF64; 2]>::from_kcl_val(&point) else {
3283                return Err(KclError::new_semantic(KclErrorDetails::new(
3284                    "Expected 2D point for point parameter".to_owned(),
3285                    vec![args.source_range],
3286                )));
3287            };
3288
3289            let Some(sketch_value) = sketch else {
3290                return Err(KclError::new_semantic(KclErrorDetails::new(
3291                    "Sketch must be provided when point is a 2D point".to_owned(),
3292                    vec![args.source_range],
3293                )));
3294            };
3295            let sketch = match sketch_value {
3296                KclValue::Sketch { value } => *value,
3297                KclValue::Object { value, .. } => {
3298                    let Some(meta_value) = value.get(SKETCH_OBJECT_META) else {
3299                        return Err(KclError::new_semantic(KclErrorDetails::new(
3300                            "Expected sketch to be of type Sketch with a meta field. Sketch must not be empty to create a region.".to_owned(),
3301                            vec![args.source_range],
3302                        )));
3303                    };
3304                    let meta_map = match meta_value {
3305                        KclValue::Object { value, .. } => value,
3306                        _ => {
3307                            return Err(KclError::new_semantic(KclErrorDetails::new(
3308                                "Expected sketch to be of type Sketch with a meta field that's an object".to_owned(),
3309                                vec![args.source_range],
3310                            )));
3311                        }
3312                    };
3313                    let Some(sketch_value) = meta_map.get(SKETCH_OBJECT_META_SKETCH) else {
3314                        return Err(KclError::new_semantic(KclErrorDetails::new(
3315                            "Expected sketch meta to have a sketch field. Sketch must not be empty to create a region."
3316                                .to_owned(),
3317                            vec![args.source_range],
3318                        )));
3319                    };
3320                    let Some(sketch) = sketch_value.as_sketch() else {
3321                        return Err(KclError::new_semantic(KclErrorDetails::new(
3322                            "Expected sketch meta to have a sketch field of type Sketch. Sketch must not be empty to create a region.".to_owned(),
3323                            vec![args.source_range],
3324                        )));
3325                    };
3326                    sketch.clone()
3327                }
3328                _ => {
3329                    return Err(KclError::new_semantic(KclErrorDetails::new(
3330                        "Expected sketch to be of type Sketch".to_owned(),
3331                        vec![args.source_range],
3332                    )));
3333                }
3334            };
3335
3336            Ok((SketchOrSegment::Sketch(sketch), pt))
3337        }
3338        KclValue::Segment { value } => match value.repr {
3339            crate::execution::SegmentRepr::Unsolved { .. } => Err(KclError::new_semantic(KclErrorDetails::new(
3340                "Segment provided to point parameter is unsolved; segments must be solved to be used as points"
3341                    .to_owned(),
3342                vec![args.source_range],
3343            ))),
3344            crate::execution::SegmentRepr::Solved { segment } => {
3345                let pt = match &segment.kind {
3346                    SegmentKind::Point { position, .. } => position.clone(),
3347                    _ => {
3348                        return Err(KclError::new_semantic(KclErrorDetails::new(
3349                            "Expected segment to be a point segment".to_owned(),
3350                            vec![args.source_range],
3351                        )));
3352                    }
3353                };
3354
3355                Ok((SketchOrSegment::Segment(*segment), pt))
3356            }
3357        },
3358        _ => Err(KclError::new_semantic(KclErrorDetails::new(
3359            "Expected point to be either a 2D point like `[0, 0]` or a point segment created from `point()`".to_owned(),
3360            vec![args.source_range],
3361        ))),
3362    }
3363}
3364#[cfg(test)]
3365mod tests {
3366
3367    use pretty_assertions::assert_eq;
3368
3369    use crate::execution::TagIdentifier;
3370    use crate::std::sketch::PlaneData;
3371    use crate::std::utils::calculate_circle_center;
3372
3373    #[test]
3374    fn test_deserialize_plane_data() {
3375        let data = PlaneData::XY;
3376        let mut str_json = serde_json::to_string(&data).unwrap();
3377        assert_eq!(str_json, "\"XY\"");
3378
3379        str_json = "\"YZ\"".to_string();
3380        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3381        assert_eq!(data, PlaneData::YZ);
3382
3383        str_json = "\"-YZ\"".to_string();
3384        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3385        assert_eq!(data, PlaneData::NegYZ);
3386
3387        str_json = "\"-xz\"".to_string();
3388        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3389        assert_eq!(data, PlaneData::NegXZ);
3390    }
3391
3392    #[test]
3393    fn test_deserialize_sketch_on_face_tag() {
3394        let data = "start";
3395        let mut str_json = serde_json::to_string(&data).unwrap();
3396        assert_eq!(str_json, "\"start\"");
3397
3398        str_json = "\"end\"".to_string();
3399        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3400        assert_eq!(
3401            data,
3402            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
3403        );
3404
3405        str_json = serde_json::to_string(&TagIdentifier {
3406            value: "thing".to_string(),
3407            info: Vec::new(),
3408            meta: Default::default(),
3409        })
3410        .unwrap();
3411        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3412        assert_eq!(
3413            data,
3414            crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
3415                value: "thing".to_string(),
3416                info: Vec::new(),
3417                meta: Default::default()
3418            }))
3419        );
3420
3421        str_json = "\"END\"".to_string();
3422        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3423        assert_eq!(
3424            data,
3425            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
3426        );
3427
3428        str_json = "\"start\"".to_string();
3429        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3430        assert_eq!(
3431            data,
3432            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
3433        );
3434
3435        str_json = "\"START\"".to_string();
3436        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3437        assert_eq!(
3438            data,
3439            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
3440        );
3441    }
3442
3443    #[test]
3444    fn test_circle_center() {
3445        let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
3446        assert_eq!(actual[0], 5.0);
3447        assert_eq!(actual[1], 0.0);
3448    }
3449}