Skip to main content

kcl_lib/std/
sketch.rs

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