Skip to main content

kcl_lib/std/
sketch.rs

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