Skip to main content

kcl_lib/std/
sketch.rs

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