Skip to main content

kcl_lib/std/
sketch.rs

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