kcl_lib/std/
sketch.rs

1//! Functions related to sketching.
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kcmc::shared::Point2d as KPoint2d; // Point2d is already defined in this pkg, to impl ts_rs traits.
6use kcmc::shared::Point3d as KPoint3d; // Point3d is already defined in this pkg, to impl ts_rs traits.
7use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, websocket::ModelingCmdReq, ModelingCmd};
8use kittycad_modeling_cmds as kcmc;
9use kittycad_modeling_cmds::shared::PathSegment;
10use parse_display::{Display, FromStr};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use super::shapes::get_radius;
15#[cfg(feature = "artifact-graph")]
16use crate::execution::{Artifact, ArtifactId, CodeRef, StartSketchOnFace, StartSketchOnPlane};
17use crate::{
18    errors::{KclError, KclErrorDetails},
19    execution::{
20        types::{ArrayLen, NumericType, PrimitiveType, RuntimeType, UnitLen},
21        BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, PlaneInfo, Point2d, Sketch, SketchSurface, Solid,
22        TagEngineInfo, TagIdentifier,
23    },
24    parsing::ast::types::TagNode,
25    std::{
26        args::{Args, TyF64},
27        utils::{
28            arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
29            intersection_with_parallel_line, point_to_len_unit, point_to_mm, untyped_point_to_mm,
30            TangentialArcInfoInput,
31        },
32    },
33};
34
35/// A tag for a face.
36#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
37#[ts(export)]
38#[serde(rename_all = "snake_case", untagged)]
39pub enum FaceTag {
40    StartOrEnd(StartOrEnd),
41    /// A tag for the face.
42    Tag(Box<TagIdentifier>),
43}
44
45impl std::fmt::Display for FaceTag {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            FaceTag::Tag(t) => write!(f, "{}", t),
49            FaceTag::StartOrEnd(StartOrEnd::Start) => write!(f, "start"),
50            FaceTag::StartOrEnd(StartOrEnd::End) => write!(f, "end"),
51        }
52    }
53}
54
55impl FaceTag {
56    /// Get the face id from the tag.
57    pub async fn get_face_id(
58        &self,
59        solid: &Solid,
60        exec_state: &mut ExecState,
61        args: &Args,
62        must_be_planar: bool,
63    ) -> Result<uuid::Uuid, KclError> {
64        match self {
65            FaceTag::Tag(ref t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
66            FaceTag::StartOrEnd(StartOrEnd::Start) => solid.start_cap_id.ok_or_else(|| {
67                KclError::new_type(KclErrorDetails::new(
68                    "Expected a start face".to_string(),
69                    vec![args.source_range],
70                ))
71            }),
72            FaceTag::StartOrEnd(StartOrEnd::End) => solid.end_cap_id.ok_or_else(|| {
73                KclError::new_type(KclErrorDetails::new(
74                    "Expected an end face".to_string(),
75                    vec![args.source_range],
76                ))
77            }),
78        }
79    }
80}
81
82#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
83#[ts(export)]
84#[serde(rename_all = "snake_case")]
85#[display(style = "snake_case")]
86pub enum StartOrEnd {
87    /// The start face as in before you extruded. This could also be known as the bottom
88    /// face. But we do not call it bottom because it would be the top face if you
89    /// extruded it in the opposite direction or flipped the camera.
90    #[serde(rename = "start", alias = "START")]
91    Start,
92    /// The end face after you extruded. This could also be known as the top
93    /// face. But we do not call it top because it would be the bottom face if you
94    /// extruded it in the opposite direction or flipped the camera.
95    #[serde(rename = "end", alias = "END")]
96    End,
97}
98
99pub const NEW_TAG_KW: &str = "tag";
100
101pub async fn involute_circular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
102    let sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
103
104    let start_radius: TyF64 = args.get_kw_arg_typed("startRadius", &RuntimeType::length(), exec_state)?;
105    let end_radius: TyF64 = args.get_kw_arg_typed("endRadius", &RuntimeType::length(), exec_state)?;
106    let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::angle(), exec_state)?;
107    let reverse = args.get_kw_arg_opt_typed("reverse", &RuntimeType::bool(), exec_state)?;
108    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
109    let new_sketch =
110        inner_involute_circular(sketch, start_radius, end_radius, angle, reverse, tag, exec_state, args).await?;
111    Ok(KclValue::Sketch {
112        value: Box::new(new_sketch),
113    })
114}
115
116fn involute_curve(radius: f64, angle: f64) -> (f64, f64) {
117    (
118        radius * (angle.cos() + angle * angle.sin()),
119        radius * (angle.sin() - angle * angle.cos()),
120    )
121}
122
123#[allow(clippy::too_many_arguments)]
124async fn inner_involute_circular(
125    sketch: Sketch,
126    start_radius: TyF64,
127    end_radius: TyF64,
128    angle: TyF64,
129    reverse: Option<bool>,
130    tag: Option<TagNode>,
131    exec_state: &mut ExecState,
132    args: Args,
133) -> Result<Sketch, KclError> {
134    let id = exec_state.next_uuid();
135
136    args.batch_modeling_cmd(
137        id,
138        ModelingCmd::from(mcmd::ExtendPath {
139            path: sketch.id.into(),
140            segment: PathSegment::CircularInvolute {
141                start_radius: LengthUnit(start_radius.to_mm()),
142                end_radius: LengthUnit(end_radius.to_mm()),
143                angle: Angle::from_degrees(angle.to_degrees()),
144                reverse: reverse.unwrap_or_default(),
145            },
146        }),
147    )
148    .await?;
149
150    let from = sketch.current_pen_position()?;
151
152    let start_radius = start_radius.to_length_units(from.units);
153    let end_radius = end_radius.to_length_units(from.units);
154
155    let mut end: KPoint3d<f64> = Default::default(); // ADAM: TODO impl this below.
156    let theta = f64::sqrt(end_radius * end_radius - start_radius * start_radius) / start_radius;
157    let (x, y) = involute_curve(start_radius, theta);
158
159    end.x = x * angle.to_radians().cos() - y * angle.to_radians().sin();
160    end.y = x * angle.to_radians().sin() + y * angle.to_radians().cos();
161
162    end.x -= start_radius * angle.to_radians().cos();
163    end.y -= start_radius * angle.to_radians().sin();
164
165    if reverse.unwrap_or_default() {
166        end.x = -end.x;
167    }
168
169    end.x += from.x;
170    end.y += from.y;
171
172    let current_path = Path::ToPoint {
173        base: BasePath {
174            from: from.ignore_units(),
175            to: [end.x, end.y],
176            tag: tag.clone(),
177            units: sketch.units,
178            geo_meta: GeoMeta {
179                id,
180                metadata: args.source_range.into(),
181            },
182        },
183    };
184
185    let mut new_sketch = sketch.clone();
186    if let Some(tag) = &tag {
187        new_sketch.add_tag(tag, &current_path, exec_state);
188    }
189    new_sketch.paths.push(current_path);
190    Ok(new_sketch)
191}
192
193/// Draw a line to a point.
194pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
195    let sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
196    let end = args.get_kw_arg_opt_typed("end", &RuntimeType::point2d(), exec_state)?;
197    let end_absolute = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
198    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
199
200    let new_sketch = inner_line(sketch, end_absolute, end, tag, exec_state, args).await?;
201    Ok(KclValue::Sketch {
202        value: Box::new(new_sketch),
203    })
204}
205
206async fn inner_line(
207    sketch: Sketch,
208    end_absolute: Option<[TyF64; 2]>,
209    end: Option<[TyF64; 2]>,
210    tag: Option<TagNode>,
211    exec_state: &mut ExecState,
212    args: Args,
213) -> Result<Sketch, KclError> {
214    straight_line(
215        StraightLineParams {
216            sketch,
217            end_absolute,
218            end,
219            tag,
220            relative_name: "end",
221        },
222        exec_state,
223        args,
224    )
225    .await
226}
227
228struct StraightLineParams {
229    sketch: Sketch,
230    end_absolute: Option<[TyF64; 2]>,
231    end: Option<[TyF64; 2]>,
232    tag: Option<TagNode>,
233    relative_name: &'static str,
234}
235
236impl StraightLineParams {
237    fn relative(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
238        Self {
239            sketch,
240            tag,
241            end: Some(p),
242            end_absolute: None,
243            relative_name: "end",
244        }
245    }
246    fn absolute(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
247        Self {
248            sketch,
249            tag,
250            end: None,
251            end_absolute: Some(p),
252            relative_name: "end",
253        }
254    }
255}
256
257async fn straight_line(
258    StraightLineParams {
259        sketch,
260        end,
261        end_absolute,
262        tag,
263        relative_name,
264    }: StraightLineParams,
265    exec_state: &mut ExecState,
266    args: Args,
267) -> Result<Sketch, KclError> {
268    let from = sketch.current_pen_position()?;
269    let (point, is_absolute) = match (end_absolute, end) {
270        (Some(_), Some(_)) => {
271            return Err(KclError::new_semantic(KclErrorDetails::new(
272                "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
273                vec![args.source_range],
274            )));
275        }
276        (Some(end_absolute), None) => (end_absolute, true),
277        (None, Some(end)) => (end, false),
278        (None, None) => {
279            return Err(KclError::new_semantic(KclErrorDetails::new(
280                format!("You must supply either `{relative_name}` or `endAbsolute` arguments"),
281                vec![args.source_range],
282            )));
283        }
284    };
285
286    let id = exec_state.next_uuid();
287    args.batch_modeling_cmd(
288        id,
289        ModelingCmd::from(mcmd::ExtendPath {
290            path: sketch.id.into(),
291            segment: PathSegment::Line {
292                end: KPoint2d::from(point_to_mm(point.clone())).with_z(0.0).map(LengthUnit),
293                relative: !is_absolute,
294            },
295        }),
296    )
297    .await?;
298
299    let end = if is_absolute {
300        point_to_len_unit(point, from.units)
301    } else {
302        let from = sketch.current_pen_position()?;
303        let point = point_to_len_unit(point, from.units);
304        [from.x + point[0], from.y + point[1]]
305    };
306
307    let current_path = Path::ToPoint {
308        base: BasePath {
309            from: from.ignore_units(),
310            to: end,
311            tag: tag.clone(),
312            units: sketch.units,
313            geo_meta: GeoMeta {
314                id,
315                metadata: args.source_range.into(),
316            },
317        },
318    };
319
320    let mut new_sketch = sketch.clone();
321    if let Some(tag) = &tag {
322        new_sketch.add_tag(tag, &current_path, exec_state);
323    }
324
325    new_sketch.paths.push(current_path);
326
327    Ok(new_sketch)
328}
329
330/// Draw a line on the x-axis.
331pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
332    let sketch =
333        args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
334    let length: Option<TyF64> = args.get_kw_arg_opt_typed("length", &RuntimeType::length(), exec_state)?;
335    let end_absolute: Option<TyF64> = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::length(), exec_state)?;
336    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
337
338    let new_sketch = inner_x_line(sketch, length, end_absolute, tag, exec_state, args).await?;
339    Ok(KclValue::Sketch {
340        value: Box::new(new_sketch),
341    })
342}
343
344async fn inner_x_line(
345    sketch: Sketch,
346    length: Option<TyF64>,
347    end_absolute: Option<TyF64>,
348    tag: Option<TagNode>,
349    exec_state: &mut ExecState,
350    args: Args,
351) -> Result<Sketch, KclError> {
352    let from = sketch.current_pen_position()?;
353    straight_line(
354        StraightLineParams {
355            sketch,
356            end_absolute: end_absolute.map(|x| [x, from.into_y()]),
357            end: length.map(|x| [x, TyF64::new(0.0, NumericType::mm())]),
358            tag,
359            relative_name: "length",
360        },
361        exec_state,
362        args,
363    )
364    .await
365}
366
367/// Draw a line on the y-axis.
368pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
369    let sketch =
370        args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
371    let length: Option<TyF64> = args.get_kw_arg_opt_typed("length", &RuntimeType::length(), exec_state)?;
372    let end_absolute: Option<TyF64> = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::length(), exec_state)?;
373    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
374
375    let new_sketch = inner_y_line(sketch, length, end_absolute, tag, exec_state, args).await?;
376    Ok(KclValue::Sketch {
377        value: Box::new(new_sketch),
378    })
379}
380
381async fn inner_y_line(
382    sketch: Sketch,
383    length: Option<TyF64>,
384    end_absolute: Option<TyF64>,
385    tag: Option<TagNode>,
386    exec_state: &mut ExecState,
387    args: Args,
388) -> Result<Sketch, KclError> {
389    let from = sketch.current_pen_position()?;
390    straight_line(
391        StraightLineParams {
392            sketch,
393            end_absolute: end_absolute.map(|y| [from.into_x(), y]),
394            end: length.map(|y| [TyF64::new(0.0, NumericType::mm()), y]),
395            tag,
396            relative_name: "length",
397        },
398        exec_state,
399        args,
400    )
401    .await
402}
403
404/// Draw an angled line.
405pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
406    let sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
407    let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::degrees(), exec_state)?;
408    let length: Option<TyF64> = args.get_kw_arg_opt_typed("length", &RuntimeType::length(), exec_state)?;
409    let length_x: Option<TyF64> = args.get_kw_arg_opt_typed("lengthX", &RuntimeType::length(), exec_state)?;
410    let length_y: Option<TyF64> = args.get_kw_arg_opt_typed("lengthY", &RuntimeType::length(), exec_state)?;
411    let end_absolute_x: Option<TyF64> =
412        args.get_kw_arg_opt_typed("endAbsoluteX", &RuntimeType::length(), exec_state)?;
413    let end_absolute_y: Option<TyF64> =
414        args.get_kw_arg_opt_typed("endAbsoluteY", &RuntimeType::length(), exec_state)?;
415    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
416
417    let new_sketch = inner_angled_line(
418        sketch,
419        angle.n,
420        length,
421        length_x,
422        length_y,
423        end_absolute_x,
424        end_absolute_y,
425        tag,
426        exec_state,
427        args,
428    )
429    .await?;
430    Ok(KclValue::Sketch {
431        value: Box::new(new_sketch),
432    })
433}
434
435#[allow(clippy::too_many_arguments)]
436async fn inner_angled_line(
437    sketch: Sketch,
438    angle: f64,
439    length: Option<TyF64>,
440    length_x: Option<TyF64>,
441    length_y: Option<TyF64>,
442    end_absolute_x: Option<TyF64>,
443    end_absolute_y: Option<TyF64>,
444    tag: Option<TagNode>,
445    exec_state: &mut ExecState,
446    args: Args,
447) -> Result<Sketch, KclError> {
448    let options_given = [&length, &length_x, &length_y, &end_absolute_x, &end_absolute_y]
449        .iter()
450        .filter(|x| x.is_some())
451        .count();
452    if options_given > 1 {
453        return Err(KclError::new_type(KclErrorDetails::new(
454            " one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_string(),
455            vec![args.source_range],
456        )));
457    }
458    if let Some(length_x) = length_x {
459        return inner_angled_line_of_x_length(angle, length_x, sketch, tag, exec_state, args).await;
460    }
461    if let Some(length_y) = length_y {
462        return inner_angled_line_of_y_length(angle, length_y, sketch, tag, exec_state, args).await;
463    }
464    let angle_degrees = angle;
465    match (length, length_x, length_y, end_absolute_x, end_absolute_y) {
466        (Some(length), None, None, None, None) => {
467            inner_angled_line_length(sketch, angle_degrees, length, tag, exec_state, args).await
468        }
469        (None, Some(length_x), None, None, None) => {
470            inner_angled_line_of_x_length(angle_degrees, length_x, sketch, tag, exec_state, args).await
471        }
472        (None, None, Some(length_y), None, None) => {
473            inner_angled_line_of_y_length(angle_degrees, length_y, sketch, tag, exec_state, args).await
474        }
475        (None, None, None, Some(end_absolute_x), None) => {
476            inner_angled_line_to_x(angle_degrees, end_absolute_x, sketch, tag, exec_state, args).await
477        }
478        (None, None, None, None, Some(end_absolute_y)) => {
479            inner_angled_line_to_y(angle_degrees, end_absolute_y, sketch, tag, exec_state, args).await
480        }
481        (None, None, None, None, None) => Err(KclError::new_type(KclErrorDetails::new(
482            "One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` must be given".to_string(),
483            vec![args.source_range],
484        ))),
485        _ => Err(KclError::new_type(KclErrorDetails::new(
486            "Only One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_owned(),
487            vec![args.source_range],
488        ))),
489    }
490}
491
492async fn inner_angled_line_length(
493    sketch: Sketch,
494    angle_degrees: f64,
495    length: TyF64,
496    tag: Option<TagNode>,
497    exec_state: &mut ExecState,
498    args: Args,
499) -> Result<Sketch, KclError> {
500    let from = sketch.current_pen_position()?;
501    let length = length.to_length_units(from.units);
502
503    //double check me on this one - mike
504    let delta: [f64; 2] = [
505        length * f64::cos(angle_degrees.to_radians()),
506        length * f64::sin(angle_degrees.to_radians()),
507    ];
508    let relative = true;
509
510    let to: [f64; 2] = [from.x + delta[0], from.y + delta[1]];
511
512    let id = exec_state.next_uuid();
513
514    args.batch_modeling_cmd(
515        id,
516        ModelingCmd::from(mcmd::ExtendPath {
517            path: sketch.id.into(),
518            segment: PathSegment::Line {
519                end: KPoint2d::from(untyped_point_to_mm(delta, from.units))
520                    .with_z(0.0)
521                    .map(LengthUnit),
522                relative,
523            },
524        }),
525    )
526    .await?;
527
528    let current_path = Path::ToPoint {
529        base: BasePath {
530            from: from.ignore_units(),
531            to,
532            tag: tag.clone(),
533            units: sketch.units,
534            geo_meta: GeoMeta {
535                id,
536                metadata: args.source_range.into(),
537            },
538        },
539    };
540
541    let mut new_sketch = sketch.clone();
542    if let Some(tag) = &tag {
543        new_sketch.add_tag(tag, &current_path, exec_state);
544    }
545
546    new_sketch.paths.push(current_path);
547    Ok(new_sketch)
548}
549
550async fn inner_angled_line_of_x_length(
551    angle_degrees: f64,
552    length: TyF64,
553    sketch: Sketch,
554    tag: Option<TagNode>,
555    exec_state: &mut ExecState,
556    args: Args,
557) -> Result<Sketch, KclError> {
558    if angle_degrees.abs() == 270.0 {
559        return Err(KclError::new_type(KclErrorDetails::new(
560            "Cannot have an x constrained angle of 270 degrees".to_string(),
561            vec![args.source_range],
562        )));
563    }
564
565    if angle_degrees.abs() == 90.0 {
566        return Err(KclError::new_type(KclErrorDetails::new(
567            "Cannot have an x constrained angle of 90 degrees".to_string(),
568            vec![args.source_range],
569        )));
570    }
571
572    let to = get_y_component(Angle::from_degrees(angle_degrees), length.n);
573    let to = [TyF64::new(to[0], length.ty.clone()), TyF64::new(to[1], length.ty)];
574
575    let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
576
577    Ok(new_sketch)
578}
579
580async fn inner_angled_line_to_x(
581    angle_degrees: f64,
582    x_to: TyF64,
583    sketch: Sketch,
584    tag: Option<TagNode>,
585    exec_state: &mut ExecState,
586    args: Args,
587) -> Result<Sketch, KclError> {
588    let from = sketch.current_pen_position()?;
589
590    if angle_degrees.abs() == 270.0 {
591        return Err(KclError::new_type(KclErrorDetails::new(
592            "Cannot have an x constrained angle of 270 degrees".to_string(),
593            vec![args.source_range],
594        )));
595    }
596
597    if angle_degrees.abs() == 90.0 {
598        return Err(KclError::new_type(KclErrorDetails::new(
599            "Cannot have an x constrained angle of 90 degrees".to_string(),
600            vec![args.source_range],
601        )));
602    }
603
604    let x_component = x_to.to_length_units(from.units) - from.x;
605    let y_component = x_component * f64::tan(angle_degrees.to_radians());
606    let y_to = from.y + y_component;
607
608    let new_sketch = straight_line(
609        StraightLineParams::absolute([x_to, TyF64::new(y_to, from.units.into())], sketch, tag),
610        exec_state,
611        args,
612    )
613    .await?;
614    Ok(new_sketch)
615}
616
617async fn inner_angled_line_of_y_length(
618    angle_degrees: f64,
619    length: TyF64,
620    sketch: Sketch,
621    tag: Option<TagNode>,
622    exec_state: &mut ExecState,
623    args: Args,
624) -> Result<Sketch, KclError> {
625    if angle_degrees.abs() == 0.0 {
626        return Err(KclError::new_type(KclErrorDetails::new(
627            "Cannot have a y constrained angle of 0 degrees".to_string(),
628            vec![args.source_range],
629        )));
630    }
631
632    if angle_degrees.abs() == 180.0 {
633        return Err(KclError::new_type(KclErrorDetails::new(
634            "Cannot have a y constrained angle of 180 degrees".to_string(),
635            vec![args.source_range],
636        )));
637    }
638
639    let to = get_x_component(Angle::from_degrees(angle_degrees), length.n);
640    let to = [TyF64::new(to[0], length.ty.clone()), TyF64::new(to[1], length.ty)];
641
642    let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
643
644    Ok(new_sketch)
645}
646
647async fn inner_angled_line_to_y(
648    angle_degrees: f64,
649    y_to: TyF64,
650    sketch: Sketch,
651    tag: Option<TagNode>,
652    exec_state: &mut ExecState,
653    args: Args,
654) -> Result<Sketch, KclError> {
655    let from = sketch.current_pen_position()?;
656
657    if angle_degrees.abs() == 0.0 {
658        return Err(KclError::new_type(KclErrorDetails::new(
659            "Cannot have a y constrained angle of 0 degrees".to_string(),
660            vec![args.source_range],
661        )));
662    }
663
664    if angle_degrees.abs() == 180.0 {
665        return Err(KclError::new_type(KclErrorDetails::new(
666            "Cannot have a y constrained angle of 180 degrees".to_string(),
667            vec![args.source_range],
668        )));
669    }
670
671    let y_component = y_to.to_length_units(from.units) - from.y;
672    let x_component = y_component / f64::tan(angle_degrees.to_radians());
673    let x_to = from.x + x_component;
674
675    let new_sketch = straight_line(
676        StraightLineParams::absolute([TyF64::new(x_to, from.units.into()), y_to], sketch, tag),
677        exec_state,
678        args,
679    )
680    .await?;
681    Ok(new_sketch)
682}
683
684/// Draw an angled line that intersects with a given line.
685pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
686    let sketch =
687        args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
688    let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::angle(), exec_state)?;
689    let intersect_tag: TagIdentifier =
690        args.get_kw_arg_typed("intersectTag", &RuntimeType::tag_identifier(), exec_state)?;
691    let offset = args.get_kw_arg_opt_typed("offset", &RuntimeType::length(), exec_state)?;
692    let tag: Option<TagNode> = args.get_kw_arg_opt("tag")?;
693    let new_sketch =
694        inner_angled_line_that_intersects(sketch, angle, intersect_tag, offset, tag, exec_state, args).await?;
695    Ok(KclValue::Sketch {
696        value: Box::new(new_sketch),
697    })
698}
699
700pub async fn inner_angled_line_that_intersects(
701    sketch: Sketch,
702    angle: TyF64,
703    intersect_tag: TagIdentifier,
704    offset: Option<TyF64>,
705    tag: Option<TagNode>,
706    exec_state: &mut ExecState,
707    args: Args,
708) -> Result<Sketch, KclError> {
709    let intersect_path = args.get_tag_engine_info(exec_state, &intersect_tag)?;
710    let path = intersect_path.path.clone().ok_or_else(|| {
711        KclError::new_type(KclErrorDetails::new(
712            format!("Expected an intersect path with a path, found `{:?}`", intersect_path),
713            vec![args.source_range],
714        ))
715    })?;
716
717    let from = sketch.current_pen_position()?;
718    let to = intersection_with_parallel_line(
719        &[
720            point_to_len_unit(path.get_from(), from.units),
721            point_to_len_unit(path.get_to(), from.units),
722        ],
723        offset.map(|t| t.to_length_units(from.units)).unwrap_or_default(),
724        angle.to_degrees(),
725        from.ignore_units(),
726    );
727    let to = [
728        TyF64::new(to[0], from.units.into()),
729        TyF64::new(to[1], from.units.into()),
730    ];
731
732    straight_line(StraightLineParams::absolute(to, sketch, tag), exec_state, args).await
733}
734
735/// Data for start sketch on.
736/// You can start a sketch on a plane or an solid.
737#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
738#[ts(export)]
739#[serde(rename_all = "camelCase", untagged)]
740#[allow(clippy::large_enum_variant)]
741pub enum SketchData {
742    PlaneOrientation(PlaneData),
743    Plane(Box<Plane>),
744    Solid(Box<Solid>),
745}
746
747/// Orientation data that can be used to construct a plane, not a plane in itself.
748#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
749#[ts(export)]
750#[serde(rename_all = "camelCase")]
751#[allow(clippy::large_enum_variant)]
752pub enum PlaneData {
753    /// The XY plane.
754    #[serde(rename = "XY", alias = "xy")]
755    XY,
756    /// The opposite side of the XY plane.
757    #[serde(rename = "-XY", alias = "-xy")]
758    NegXY,
759    /// The XZ plane.
760    #[serde(rename = "XZ", alias = "xz")]
761    XZ,
762    /// The opposite side of the XZ plane.
763    #[serde(rename = "-XZ", alias = "-xz")]
764    NegXZ,
765    /// The YZ plane.
766    #[serde(rename = "YZ", alias = "yz")]
767    YZ,
768    /// The opposite side of the YZ plane.
769    #[serde(rename = "-YZ", alias = "-yz")]
770    NegYZ,
771    /// A defined plane.
772    Plane(PlaneInfo),
773}
774
775/// Start a sketch on a specific plane or face.
776pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
777    let data = args.get_unlabeled_kw_arg_typed(
778        "planeOrSolid",
779        &RuntimeType::Union(vec![RuntimeType::solid(), RuntimeType::plane()]),
780        exec_state,
781    )?;
782    let face = args.get_kw_arg_opt_typed("face", &RuntimeType::tag(), exec_state)?;
783
784    match inner_start_sketch_on(data, face, exec_state, &args).await? {
785        SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
786        SketchSurface::Face(value) => Ok(KclValue::Face { value }),
787    }
788}
789
790async fn inner_start_sketch_on(
791    plane_or_solid: SketchData,
792    face: Option<FaceTag>,
793    exec_state: &mut ExecState,
794    args: &Args,
795) -> Result<SketchSurface, KclError> {
796    match plane_or_solid {
797        SketchData::PlaneOrientation(plane_data) => {
798            let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
799            Ok(SketchSurface::Plane(plane))
800        }
801        SketchData::Plane(plane) => {
802            if plane.value == crate::exec::PlaneType::Uninit {
803                if plane.info.origin.units == UnitLen::Unknown {
804                    return Err(KclError::new_semantic(KclErrorDetails::new(
805                        "Origin of plane has unknown units".to_string(),
806                        vec![args.source_range],
807                    )));
808                }
809                let plane = make_sketch_plane_from_orientation(plane.info.into_plane_data(), exec_state, args).await?;
810                Ok(SketchSurface::Plane(plane))
811            } else {
812                // Create artifact used only by the UI, not the engine.
813                #[cfg(feature = "artifact-graph")]
814                {
815                    let id = exec_state.next_uuid();
816                    exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
817                        id: ArtifactId::from(id),
818                        plane_id: plane.artifact_id,
819                        code_ref: CodeRef::placeholder(args.source_range),
820                    }));
821                }
822
823                Ok(SketchSurface::Plane(plane))
824            }
825        }
826        SketchData::Solid(solid) => {
827            let Some(tag) = face else {
828                return Err(KclError::new_type(KclErrorDetails::new(
829                    "Expected a tag for the face to sketch on".to_string(),
830                    vec![args.source_range],
831                )));
832            };
833            let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
834
835            #[cfg(feature = "artifact-graph")]
836            {
837                // Create artifact used only by the UI, not the engine.
838                let id = exec_state.next_uuid();
839                exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
840                    id: ArtifactId::from(id),
841                    face_id: face.artifact_id,
842                    code_ref: CodeRef::placeholder(args.source_range),
843                }));
844            }
845
846            Ok(SketchSurface::Face(face))
847        }
848    }
849}
850
851async fn start_sketch_on_face(
852    solid: Box<Solid>,
853    tag: FaceTag,
854    exec_state: &mut ExecState,
855    args: &Args,
856) -> Result<Box<Face>, KclError> {
857    let extrude_plane_id = tag.get_face_id(&solid, exec_state, args, true).await?;
858
859    Ok(Box::new(Face {
860        id: extrude_plane_id,
861        artifact_id: extrude_plane_id.into(),
862        value: tag.to_string(),
863        // TODO: get this from the extrude plane data.
864        x_axis: solid.sketch.on.x_axis(),
865        y_axis: solid.sketch.on.y_axis(),
866        units: solid.units,
867        solid,
868        meta: vec![args.source_range.into()],
869    }))
870}
871
872async fn make_sketch_plane_from_orientation(
873    data: PlaneData,
874    exec_state: &mut ExecState,
875    args: &Args,
876) -> Result<Box<Plane>, KclError> {
877    let plane = Plane::from_plane_data(data.clone(), exec_state)?;
878
879    // Create the plane on the fly.
880    let clobber = false;
881    let size = LengthUnit(60.0);
882    let hide = Some(true);
883    args.batch_modeling_cmd(
884        plane.id,
885        ModelingCmd::from(mcmd::MakePlane {
886            clobber,
887            origin: plane.info.origin.into(),
888            size,
889            x_axis: plane.info.x_axis.into(),
890            y_axis: plane.info.y_axis.into(),
891            hide,
892        }),
893    )
894    .await?;
895
896    Ok(Box::new(plane))
897}
898
899/// Start a new profile at a given point.
900pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
901    let sketch_surface = args.get_unlabeled_kw_arg_typed(
902        "startProfileOn",
903        &RuntimeType::Union(vec![RuntimeType::plane(), RuntimeType::face()]),
904        exec_state,
905    )?;
906    let start: [TyF64; 2] = args.get_kw_arg_typed("at", &RuntimeType::point2d(), exec_state)?;
907    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
908
909    let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, args).await?;
910    Ok(KclValue::Sketch {
911        value: Box::new(sketch),
912    })
913}
914
915pub(crate) async fn inner_start_profile(
916    sketch_surface: SketchSurface,
917    at: [TyF64; 2],
918    tag: Option<TagNode>,
919    exec_state: &mut ExecState,
920    args: Args,
921) -> Result<Sketch, KclError> {
922    match &sketch_surface {
923        SketchSurface::Face(face) => {
924            // Flush the batch for our fillets/chamfers if there are any.
925            // If we do not do these for sketch on face, things will fail with face does not exist.
926            args.flush_batch_for_solids(exec_state, &[(*face.solid).clone()])
927                .await?;
928        }
929        SketchSurface::Plane(plane) if !plane.is_standard() => {
930            // Hide whatever plane we are sketching on.
931            // This is especially helpful for offset planes, which would be visible otherwise.
932            args.batch_end_cmd(
933                exec_state.next_uuid(),
934                ModelingCmd::from(mcmd::ObjectVisible {
935                    object_id: plane.id,
936                    hidden: true,
937                }),
938            )
939            .await?;
940        }
941        _ => {}
942    }
943
944    let enable_sketch_id = exec_state.next_uuid();
945    let path_id = exec_state.next_uuid();
946    let move_pen_id = exec_state.next_uuid();
947    args.batch_modeling_cmds(&[
948        // Enter sketch mode on the surface.
949        // We call this here so you can reuse the sketch surface for multiple sketches.
950        ModelingCmdReq {
951            cmd: ModelingCmd::from(mcmd::EnableSketchMode {
952                animated: false,
953                ortho: false,
954                entity_id: sketch_surface.id(),
955                adjust_camera: false,
956                planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
957                    // We pass in the normal for the plane here.
958                    let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
959                    Some(normal.into())
960                } else {
961                    None
962                },
963            }),
964            cmd_id: enable_sketch_id.into(),
965        },
966        ModelingCmdReq {
967            cmd: ModelingCmd::from(mcmd::StartPath::default()),
968            cmd_id: path_id.into(),
969        },
970        ModelingCmdReq {
971            cmd: ModelingCmd::from(mcmd::MovePathPen {
972                path: path_id.into(),
973                to: KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit),
974            }),
975            cmd_id: move_pen_id.into(),
976        },
977        ModelingCmdReq {
978            cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
979            cmd_id: exec_state.next_uuid().into(),
980        },
981    ])
982    .await?;
983
984    // Convert to the units of the module.  This is what the frontend expects.
985    let units = exec_state.length_unit();
986    let to = point_to_len_unit(at, units);
987    let current_path = BasePath {
988        from: to,
989        to,
990        tag: tag.clone(),
991        units,
992        geo_meta: GeoMeta {
993            id: move_pen_id,
994            metadata: args.source_range.into(),
995        },
996    };
997
998    let sketch = Sketch {
999        id: path_id,
1000        original_id: path_id,
1001        artifact_id: path_id.into(),
1002        on: sketch_surface.clone(),
1003        paths: vec![],
1004        units,
1005        mirror: Default::default(),
1006        meta: vec![args.source_range.into()],
1007        tags: if let Some(tag) = &tag {
1008            let mut tag_identifier: TagIdentifier = tag.into();
1009            tag_identifier.info = vec![(
1010                exec_state.stack().current_epoch(),
1011                TagEngineInfo {
1012                    id: current_path.geo_meta.id,
1013                    sketch: path_id,
1014                    path: Some(Path::Base {
1015                        base: current_path.clone(),
1016                    }),
1017                    surface: None,
1018                },
1019            )];
1020            IndexMap::from([(tag.name.to_string(), tag_identifier)])
1021        } else {
1022            Default::default()
1023        },
1024        start: current_path,
1025    };
1026    Ok(sketch)
1027}
1028
1029/// Returns the X component of the sketch profile start point.
1030pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1031    let sketch: Sketch = args.get_unlabeled_kw_arg_typed("profile", &RuntimeType::sketch(), exec_state)?;
1032    let ty = sketch.units.into();
1033    let x = inner_profile_start_x(sketch)?;
1034    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1035}
1036
1037pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1038    Ok(profile.start.to[0])
1039}
1040
1041/// Returns the Y component of the sketch profile start point.
1042pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1043    let sketch: Sketch = args.get_unlabeled_kw_arg_typed("profile", &RuntimeType::sketch(), exec_state)?;
1044    let ty = sketch.units.into();
1045    let x = inner_profile_start_y(sketch)?;
1046    Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1047}
1048
1049pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1050    Ok(profile.start.to[1])
1051}
1052
1053/// Returns the sketch profile start point.
1054pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1055    let sketch: Sketch = args.get_unlabeled_kw_arg_typed("profile", &RuntimeType::sketch(), exec_state)?;
1056    let ty = sketch.units.into();
1057    let point = inner_profile_start(sketch)?;
1058    Ok(KclValue::from_point2d(point, ty, args.into()))
1059}
1060
1061pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1062    Ok(profile.start.to)
1063}
1064
1065/// Close the current sketch.
1066pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1067    let sketch =
1068        args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1069    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1070    let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1071    Ok(KclValue::Sketch {
1072        value: Box::new(new_sketch),
1073    })
1074}
1075
1076pub(crate) async fn inner_close(
1077    sketch: Sketch,
1078    tag: Option<TagNode>,
1079    exec_state: &mut ExecState,
1080    args: Args,
1081) -> Result<Sketch, KclError> {
1082    let from = sketch.current_pen_position()?;
1083    let to = point_to_len_unit(sketch.start.get_from(), from.units);
1084
1085    let id = exec_state.next_uuid();
1086
1087    args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }))
1088        .await?;
1089
1090    let current_path = Path::ToPoint {
1091        base: BasePath {
1092            from: from.ignore_units(),
1093            to,
1094            tag: tag.clone(),
1095            units: sketch.units,
1096            geo_meta: GeoMeta {
1097                id,
1098                metadata: args.source_range.into(),
1099            },
1100        },
1101    };
1102
1103    let mut new_sketch = sketch.clone();
1104    if let Some(tag) = &tag {
1105        new_sketch.add_tag(tag, &current_path, exec_state);
1106    }
1107
1108    new_sketch.paths.push(current_path);
1109
1110    Ok(new_sketch)
1111}
1112
1113/// Draw an arc.
1114pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1115    let sketch =
1116        args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1117
1118    let angle_start: Option<TyF64> = args.get_kw_arg_opt_typed("angleStart", &RuntimeType::degrees(), exec_state)?;
1119    let angle_end: Option<TyF64> = args.get_kw_arg_opt_typed("angleEnd", &RuntimeType::degrees(), exec_state)?;
1120    let radius: Option<TyF64> = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
1121    let diameter: Option<TyF64> = args.get_kw_arg_opt_typed("diameter", &RuntimeType::length(), exec_state)?;
1122    let end_absolute: Option<[TyF64; 2]> =
1123        args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1124    let interior_absolute: Option<[TyF64; 2]> =
1125        args.get_kw_arg_opt_typed("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1126    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1127    let new_sketch = inner_arc(
1128        sketch,
1129        angle_start,
1130        angle_end,
1131        radius,
1132        diameter,
1133        interior_absolute,
1134        end_absolute,
1135        tag,
1136        exec_state,
1137        args,
1138    )
1139    .await?;
1140    Ok(KclValue::Sketch {
1141        value: Box::new(new_sketch),
1142    })
1143}
1144
1145#[allow(clippy::too_many_arguments)]
1146pub(crate) async fn inner_arc(
1147    sketch: Sketch,
1148    angle_start: Option<TyF64>,
1149    angle_end: Option<TyF64>,
1150    radius: Option<TyF64>,
1151    diameter: Option<TyF64>,
1152    interior_absolute: Option<[TyF64; 2]>,
1153    end_absolute: Option<[TyF64; 2]>,
1154    tag: Option<TagNode>,
1155    exec_state: &mut ExecState,
1156    args: Args,
1157) -> Result<Sketch, KclError> {
1158    let from: Point2d = sketch.current_pen_position()?;
1159    let id = exec_state.next_uuid();
1160
1161    match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1162        (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1163            let radius = get_radius(radius, diameter, args.source_range)?;
1164            relative_arc(&args, id, exec_state, sketch, from, angle_start, angle_end, radius, tag).await
1165        }
1166        (None, None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1167            absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1168        }
1169        _ => {
1170            Err(KclError::new_type(KclErrorDetails::new(
1171                "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)".to_owned(),
1172                vec![args.source_range],
1173            )))
1174        }
1175    }
1176}
1177
1178#[allow(clippy::too_many_arguments)]
1179pub async fn absolute_arc(
1180    args: &Args,
1181    id: uuid::Uuid,
1182    exec_state: &mut ExecState,
1183    sketch: Sketch,
1184    from: Point2d,
1185    interior_absolute: [TyF64; 2],
1186    end_absolute: [TyF64; 2],
1187    tag: Option<TagNode>,
1188) -> Result<Sketch, KclError> {
1189    // The start point is taken from the path you are extending.
1190    args.batch_modeling_cmd(
1191        id,
1192        ModelingCmd::from(mcmd::ExtendPath {
1193            path: sketch.id.into(),
1194            segment: PathSegment::ArcTo {
1195                end: kcmc::shared::Point3d {
1196                    x: LengthUnit(end_absolute[0].to_mm()),
1197                    y: LengthUnit(end_absolute[1].to_mm()),
1198                    z: LengthUnit(0.0),
1199                },
1200                interior: kcmc::shared::Point3d {
1201                    x: LengthUnit(interior_absolute[0].to_mm()),
1202                    y: LengthUnit(interior_absolute[1].to_mm()),
1203                    z: LengthUnit(0.0),
1204                },
1205                relative: false,
1206            },
1207        }),
1208    )
1209    .await?;
1210
1211    let start = [from.x, from.y];
1212    let end = point_to_len_unit(end_absolute, from.units);
1213
1214    let current_path = Path::ArcThreePoint {
1215        base: BasePath {
1216            from: from.ignore_units(),
1217            to: end,
1218            tag: tag.clone(),
1219            units: sketch.units,
1220            geo_meta: GeoMeta {
1221                id,
1222                metadata: args.source_range.into(),
1223            },
1224        },
1225        p1: start,
1226        p2: point_to_len_unit(interior_absolute, from.units),
1227        p3: end,
1228    };
1229
1230    let mut new_sketch = sketch.clone();
1231    if let Some(tag) = &tag {
1232        new_sketch.add_tag(tag, &current_path, exec_state);
1233    }
1234
1235    new_sketch.paths.push(current_path);
1236
1237    Ok(new_sketch)
1238}
1239
1240#[allow(clippy::too_many_arguments)]
1241pub async fn relative_arc(
1242    args: &Args,
1243    id: uuid::Uuid,
1244    exec_state: &mut ExecState,
1245    sketch: Sketch,
1246    from: Point2d,
1247    angle_start: TyF64,
1248    angle_end: TyF64,
1249    radius: TyF64,
1250    tag: Option<TagNode>,
1251) -> Result<Sketch, KclError> {
1252    let a_start = Angle::from_degrees(angle_start.to_degrees());
1253    let a_end = Angle::from_degrees(angle_end.to_degrees());
1254    let radius = radius.to_length_units(from.units);
1255    let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1256    if a_start == a_end {
1257        return Err(KclError::new_type(KclErrorDetails::new(
1258            "Arc start and end angles must be different".to_string(),
1259            vec![args.source_range],
1260        )));
1261    }
1262    let ccw = a_start < a_end;
1263
1264    args.batch_modeling_cmd(
1265        id,
1266        ModelingCmd::from(mcmd::ExtendPath {
1267            path: sketch.id.into(),
1268            segment: PathSegment::Arc {
1269                start: a_start,
1270                end: a_end,
1271                center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1272                radius: LengthUnit(from.units.adjust_to(radius, UnitLen::Mm).0),
1273                relative: false,
1274            },
1275        }),
1276    )
1277    .await?;
1278
1279    let current_path = Path::Arc {
1280        base: BasePath {
1281            from: from.ignore_units(),
1282            to: end,
1283            tag: tag.clone(),
1284            units: from.units,
1285            geo_meta: GeoMeta {
1286                id,
1287                metadata: args.source_range.into(),
1288            },
1289        },
1290        center,
1291        radius,
1292        ccw,
1293    };
1294
1295    let mut new_sketch = sketch.clone();
1296    if let Some(tag) = &tag {
1297        new_sketch.add_tag(tag, &current_path, exec_state);
1298    }
1299
1300    new_sketch.paths.push(current_path);
1301
1302    Ok(new_sketch)
1303}
1304
1305/// Draw a tangential arc to a specific point.
1306pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1307    let sketch =
1308        args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1309    let end = args.get_kw_arg_opt_typed("end", &RuntimeType::point2d(), exec_state)?;
1310    let end_absolute = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1311    let radius = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
1312    let diameter = args.get_kw_arg_opt_typed("diameter", &RuntimeType::length(), exec_state)?;
1313    let angle = args.get_kw_arg_opt_typed("angle", &RuntimeType::angle(), exec_state)?;
1314    let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1315
1316    let new_sketch = inner_tangential_arc(
1317        sketch,
1318        end_absolute,
1319        end,
1320        radius,
1321        diameter,
1322        angle,
1323        tag,
1324        exec_state,
1325        args,
1326    )
1327    .await?;
1328    Ok(KclValue::Sketch {
1329        value: Box::new(new_sketch),
1330    })
1331}
1332
1333#[allow(clippy::too_many_arguments)]
1334async fn inner_tangential_arc(
1335    sketch: Sketch,
1336    end_absolute: Option<[TyF64; 2]>,
1337    end: Option<[TyF64; 2]>,
1338    radius: Option<TyF64>,
1339    diameter: Option<TyF64>,
1340    angle: Option<TyF64>,
1341    tag: Option<TagNode>,
1342    exec_state: &mut ExecState,
1343    args: Args,
1344) -> Result<Sketch, KclError> {
1345    match (end_absolute, end, radius, diameter, angle) {
1346        (Some(point), None, None, None, None) => {
1347            inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1348        }
1349        (None, Some(point), None, None, None) => {
1350            inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1351        }
1352        (None, None, radius, diameter, Some(angle)) => {
1353            let radius = get_radius(radius, diameter, args.source_range)?;
1354            let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1355            inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1356        }
1357        (Some(_), Some(_), None, None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
1358            "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1359            vec![args.source_range],
1360        ))),
1361        (_, _, _, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1362            "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1363            vec![args.source_range],
1364        ))),
1365    }
1366}
1367
1368/// Data to draw a tangential arc.
1369#[derive(Debug, Clone, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
1370#[ts(export)]
1371#[serde(rename_all = "camelCase", untagged)]
1372pub enum TangentialArcData {
1373    RadiusAndOffset {
1374        /// Radius of the arc.
1375        /// Not to be confused with Raiders of the Lost Ark.
1376        radius: TyF64,
1377        /// Offset of the arc, in degrees.
1378        offset: TyF64,
1379    },
1380}
1381
1382/// Draw a curved line segment along part of an imaginary circle.
1383///
1384/// The arc is constructed such that the last line segment is placed tangent
1385/// to the imaginary circle of the specified radius. The resulting arc is the
1386/// segment of the imaginary circle from that tangent point for 'angle'
1387/// degrees along the imaginary circle.
1388async fn inner_tangential_arc_radius_angle(
1389    data: TangentialArcData,
1390    sketch: Sketch,
1391    tag: Option<TagNode>,
1392    exec_state: &mut ExecState,
1393    args: Args,
1394) -> Result<Sketch, KclError> {
1395    let from: Point2d = sketch.current_pen_position()?;
1396    // next set of lines is some undocumented voodoo from get_tangential_arc_to_info
1397    let tangent_info = sketch.get_tangential_info_from_paths(); //this function desperately needs some documentation
1398    let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1399
1400    let id = exec_state.next_uuid();
1401
1402    let (center, to, ccw) = match data {
1403        TangentialArcData::RadiusAndOffset { radius, offset } => {
1404            // KCL stdlib types use degrees.
1405            let offset = Angle::from_degrees(offset.to_degrees());
1406
1407            // Calculate the end point from the angle and radius.
1408            // atan2 outputs radians.
1409            let previous_end_tangent = Angle::from_radians(f64::atan2(
1410                from.y - tan_previous_point[1],
1411                from.x - tan_previous_point[0],
1412            ));
1413            // make sure the arc center is on the correct side to guarantee deterministic behavior
1414            // note the engine automatically rejects an offset of zero, if we want to flag that at KCL too to avoid engine errors
1415            let ccw = offset.to_degrees() > 0.0;
1416            let tangent_to_arc_start_angle = if ccw {
1417                // CCW turn
1418                Angle::from_degrees(-90.0)
1419            } else {
1420                // CW turn
1421                Angle::from_degrees(90.0)
1422            };
1423            // may need some logic and / or modulo on the various angle values to prevent them from going "backwards"
1424            // but the above logic *should* capture that behavior
1425            let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1426            let end_angle = start_angle + offset;
1427            let (center, to) = arc_center_and_end(
1428                from.ignore_units(),
1429                start_angle,
1430                end_angle,
1431                radius.to_length_units(from.units),
1432            );
1433
1434            args.batch_modeling_cmd(
1435                id,
1436                ModelingCmd::from(mcmd::ExtendPath {
1437                    path: sketch.id.into(),
1438                    segment: PathSegment::TangentialArc {
1439                        radius: LengthUnit(radius.to_mm()),
1440                        offset,
1441                    },
1442                }),
1443            )
1444            .await?;
1445            (center, to, ccw)
1446        }
1447    };
1448
1449    let current_path = Path::TangentialArc {
1450        ccw,
1451        center,
1452        base: BasePath {
1453            from: from.ignore_units(),
1454            to,
1455            tag: tag.clone(),
1456            units: sketch.units,
1457            geo_meta: GeoMeta {
1458                id,
1459                metadata: args.source_range.into(),
1460            },
1461        },
1462    };
1463
1464    let mut new_sketch = sketch.clone();
1465    if let Some(tag) = &tag {
1466        new_sketch.add_tag(tag, &current_path, exec_state);
1467    }
1468
1469    new_sketch.paths.push(current_path);
1470
1471    Ok(new_sketch)
1472}
1473
1474// `to` must be in sketch.units
1475fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
1476    ModelingCmd::from(mcmd::ExtendPath {
1477        path: sketch.id.into(),
1478        segment: PathSegment::TangentialArcTo {
1479            angle_snap_increment: None,
1480            to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
1481                .with_z(0.0)
1482                .map(LengthUnit),
1483        },
1484    })
1485}
1486
1487async fn inner_tangential_arc_to_point(
1488    sketch: Sketch,
1489    point: [TyF64; 2],
1490    is_absolute: bool,
1491    tag: Option<TagNode>,
1492    exec_state: &mut ExecState,
1493    args: Args,
1494) -> Result<Sketch, KclError> {
1495    let from: Point2d = sketch.current_pen_position()?;
1496    let tangent_info = sketch.get_tangential_info_from_paths();
1497    let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1498
1499    let point = point_to_len_unit(point, from.units);
1500
1501    let to = if is_absolute {
1502        point
1503    } else {
1504        [from.x + point[0], from.y + point[1]]
1505    };
1506    let [to_x, to_y] = to;
1507    let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1508        arc_start_point: [from.x, from.y],
1509        arc_end_point: [to_x, to_y],
1510        tan_previous_point,
1511        obtuse: true,
1512    });
1513
1514    if result.center[0].is_infinite() {
1515        return Err(KclError::new_semantic(KclErrorDetails::new(
1516            "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
1517                .to_owned(),
1518            vec![args.source_range],
1519        )));
1520    } else if result.center[1].is_infinite() {
1521        return Err(KclError::new_semantic(KclErrorDetails::new(
1522            "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
1523                .to_owned(),
1524            vec![args.source_range],
1525        )));
1526    }
1527
1528    let delta = if is_absolute {
1529        [to_x - from.x, to_y - from.y]
1530    } else {
1531        point
1532    };
1533    let id = exec_state.next_uuid();
1534    args.batch_modeling_cmd(id, tan_arc_to(&sketch, delta)).await?;
1535
1536    let current_path = Path::TangentialArcTo {
1537        base: BasePath {
1538            from: from.ignore_units(),
1539            to,
1540            tag: tag.clone(),
1541            units: sketch.units,
1542            geo_meta: GeoMeta {
1543                id,
1544                metadata: args.source_range.into(),
1545            },
1546        },
1547        center: result.center,
1548        ccw: result.ccw > 0,
1549    };
1550
1551    let mut new_sketch = sketch.clone();
1552    if let Some(tag) = &tag {
1553        new_sketch.add_tag(tag, &current_path, exec_state);
1554    }
1555
1556    new_sketch.paths.push(current_path);
1557
1558    Ok(new_sketch)
1559}
1560
1561/// Draw a bezier curve.
1562pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1563    let sketch =
1564        args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1565    let control1 = args.get_kw_arg_opt_typed("control1", &RuntimeType::point2d(), exec_state)?;
1566    let control2 = args.get_kw_arg_opt_typed("control2", &RuntimeType::point2d(), exec_state)?;
1567    let end = args.get_kw_arg_opt_typed("end", &RuntimeType::point2d(), exec_state)?;
1568    let control1_absolute = args.get_kw_arg_opt_typed("control1Absolute", &RuntimeType::point2d(), exec_state)?;
1569    let control2_absolute = args.get_kw_arg_opt_typed("control2Absolute", &RuntimeType::point2d(), exec_state)?;
1570    let end_absolute = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1571    let tag = args.get_kw_arg_opt("tag")?;
1572
1573    let new_sketch = inner_bezier_curve(
1574        sketch,
1575        control1,
1576        control2,
1577        end,
1578        control1_absolute,
1579        control2_absolute,
1580        end_absolute,
1581        tag,
1582        exec_state,
1583        args,
1584    )
1585    .await?;
1586    Ok(KclValue::Sketch {
1587        value: Box::new(new_sketch),
1588    })
1589}
1590
1591#[allow(clippy::too_many_arguments)]
1592async fn inner_bezier_curve(
1593    sketch: Sketch,
1594    control1: Option<[TyF64; 2]>,
1595    control2: Option<[TyF64; 2]>,
1596    end: Option<[TyF64; 2]>,
1597    control1_absolute: Option<[TyF64; 2]>,
1598    control2_absolute: Option<[TyF64; 2]>,
1599    end_absolute: Option<[TyF64; 2]>,
1600    tag: Option<TagNode>,
1601    exec_state: &mut ExecState,
1602    args: Args,
1603) -> Result<Sketch, KclError> {
1604    let from = sketch.current_pen_position()?;
1605    let id = exec_state.next_uuid();
1606
1607    let to = match (
1608        control1,
1609        control2,
1610        end,
1611        control1_absolute,
1612        control2_absolute,
1613        end_absolute,
1614    ) {
1615        // Relative
1616        (Some(control1), Some(control2), Some(end), None, None, None) => {
1617            let delta = end.clone();
1618            let to = [
1619                from.x + end[0].to_length_units(from.units),
1620                from.y + end[1].to_length_units(from.units),
1621            ];
1622
1623            args.batch_modeling_cmd(
1624                id,
1625                ModelingCmd::from(mcmd::ExtendPath {
1626                    path: sketch.id.into(),
1627                    segment: PathSegment::Bezier {
1628                        control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1629                        control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1630                        end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
1631                        relative: true,
1632                    },
1633                }),
1634            )
1635            .await?;
1636            to
1637        }
1638        // Absolute
1639        (None, None, None, Some(control1), Some(control2), Some(end)) => {
1640            let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
1641            args.batch_modeling_cmd(
1642                id,
1643                ModelingCmd::from(mcmd::ExtendPath {
1644                    path: sketch.id.into(),
1645                    segment: PathSegment::Bezier {
1646                        control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1647                        control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1648                        end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
1649                        relative: false,
1650                    },
1651                }),
1652            )
1653            .await?;
1654            to
1655        }
1656        _ => {
1657            return Err(KclError::new_semantic(KclErrorDetails::new(
1658                "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
1659                vec![args.source_range],
1660            )));
1661        }
1662    };
1663
1664    let current_path = Path::ToPoint {
1665        base: BasePath {
1666            from: from.ignore_units(),
1667            to,
1668            tag: tag.clone(),
1669            units: sketch.units,
1670            geo_meta: GeoMeta {
1671                id,
1672                metadata: args.source_range.into(),
1673            },
1674        },
1675    };
1676
1677    let mut new_sketch = sketch.clone();
1678    if let Some(tag) = &tag {
1679        new_sketch.add_tag(tag, &current_path, exec_state);
1680    }
1681
1682    new_sketch.paths.push(current_path);
1683
1684    Ok(new_sketch)
1685}
1686
1687/// Use a sketch to cut a hole in another sketch.
1688pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1689    let sketch =
1690        args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1691
1692    let tool: Vec<Sketch> = args.get_kw_arg_typed(
1693        "tool",
1694        &RuntimeType::Array(
1695            Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
1696            ArrayLen::Minimum(1),
1697        ),
1698        exec_state,
1699    )?;
1700
1701    let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
1702    Ok(KclValue::Sketch {
1703        value: Box::new(new_sketch),
1704    })
1705}
1706
1707async fn inner_subtract_2d(
1708    sketch: Sketch,
1709    tool: Vec<Sketch>,
1710    exec_state: &mut ExecState,
1711    args: Args,
1712) -> Result<Sketch, KclError> {
1713    for hole_sketch in tool {
1714        args.batch_modeling_cmd(
1715            exec_state.next_uuid(),
1716            ModelingCmd::from(mcmd::Solid2dAddHole {
1717                object_id: sketch.id,
1718                hole_id: hole_sketch.id,
1719            }),
1720        )
1721        .await?;
1722
1723        // suggestion (mike)
1724        // we also hide the source hole since its essentially "consumed" by this operation
1725        args.batch_modeling_cmd(
1726            exec_state.next_uuid(),
1727            ModelingCmd::from(mcmd::ObjectVisible {
1728                object_id: hole_sketch.id,
1729                hidden: true,
1730            }),
1731        )
1732        .await?;
1733    }
1734
1735    Ok(sketch)
1736}
1737
1738#[cfg(test)]
1739mod tests {
1740
1741    use pretty_assertions::assert_eq;
1742
1743    use crate::{
1744        execution::TagIdentifier,
1745        std::{sketch::PlaneData, utils::calculate_circle_center},
1746    };
1747
1748    #[test]
1749    fn test_deserialize_plane_data() {
1750        let data = PlaneData::XY;
1751        let mut str_json = serde_json::to_string(&data).unwrap();
1752        assert_eq!(str_json, "\"XY\"");
1753
1754        str_json = "\"YZ\"".to_string();
1755        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
1756        assert_eq!(data, PlaneData::YZ);
1757
1758        str_json = "\"-YZ\"".to_string();
1759        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
1760        assert_eq!(data, PlaneData::NegYZ);
1761
1762        str_json = "\"-xz\"".to_string();
1763        let data: PlaneData = serde_json::from_str(&str_json).unwrap();
1764        assert_eq!(data, PlaneData::NegXZ);
1765    }
1766
1767    #[test]
1768    fn test_deserialize_sketch_on_face_tag() {
1769        let data = "start";
1770        let mut str_json = serde_json::to_string(&data).unwrap();
1771        assert_eq!(str_json, "\"start\"");
1772
1773        str_json = "\"end\"".to_string();
1774        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
1775        assert_eq!(
1776            data,
1777            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
1778        );
1779
1780        str_json = serde_json::to_string(&TagIdentifier {
1781            value: "thing".to_string(),
1782            info: Vec::new(),
1783            meta: Default::default(),
1784        })
1785        .unwrap();
1786        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
1787        assert_eq!(
1788            data,
1789            crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
1790                value: "thing".to_string(),
1791                info: Vec::new(),
1792                meta: Default::default()
1793            }))
1794        );
1795
1796        str_json = "\"END\"".to_string();
1797        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
1798        assert_eq!(
1799            data,
1800            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
1801        );
1802
1803        str_json = "\"start\"".to_string();
1804        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
1805        assert_eq!(
1806            data,
1807            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
1808        );
1809
1810        str_json = "\"START\"".to_string();
1811        let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
1812        assert_eq!(
1813            data,
1814            crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
1815        );
1816    }
1817
1818    #[test]
1819    fn test_circle_center() {
1820        let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
1821        assert_eq!(actual[0], 5.0);
1822        assert_eq!(actual[1], 0.0);
1823    }
1824}