1use anyhow::Result;
4use indexmap::IndexMap;
5use kcl_derive_docs::stdlib;
6use kcmc::shared::Point2d as KPoint2d; use kcmc::shared::Point3d as KPoint3d; use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, websocket::ModelingCmdReq, ModelingCmd};
9use kittycad_modeling_cmds as kcmc;
10use kittycad_modeling_cmds::shared::PathSegment;
11use parse_display::{Display, FromStr};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14
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, untype_point, untyped_point_to_mm,
30 TangentialArcInfoInput,
31 },
32 },
33};
34
35#[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 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 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::Type(KclErrorDetails {
68 message: "Expected a start face".to_string(),
69 source_ranges: vec![args.source_range],
70 })
71 }),
72 FaceTag::StartOrEnd(StartOrEnd::End) => solid.end_cap_id.ok_or_else(|| {
73 KclError::Type(KclErrorDetails {
74 message: "Expected an end face".to_string(),
75 source_ranges: 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 #[serde(rename = "start", alias = "START")]
91 Start,
92 #[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 =
103 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
104
105 let start_radius: TyF64 = args.get_kw_arg_typed("startRadius", &RuntimeType::length(), exec_state)?;
106 let end_radius: TyF64 = args.get_kw_arg_typed("endRadius", &RuntimeType::length(), exec_state)?;
107 let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::angle(), exec_state)?;
108 let reverse = args.get_kw_arg_opt("reverse")?;
109 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
110 let new_sketch =
111 inner_involute_circular(sketch, start_radius, end_radius, angle, reverse, tag, exec_state, args).await?;
112 Ok(KclValue::Sketch {
113 value: Box::new(new_sketch),
114 })
115}
116
117fn involute_curve(radius: f64, angle: f64) -> (f64, f64) {
118 (
119 radius * (angle.cos() + angle * angle.sin()),
120 radius * (angle.sin() - angle * angle.cos()),
121 )
122}
123
124#[stdlib {
135 name = "involuteCircular",
136 keywords = true,
137 unlabeled_first = true,
138 args = {
139 sketch = { docs = "Which sketch should this path be added to?"},
140 start_radius = { docs = "The involute is described between two circles, start_radius is the radius of the inner circle."},
141 end_radius = { docs = "The involute is described between two circles, end_radius is the radius of the outer circle."},
142 angle = { docs = "The angle to rotate the involute by. A value of zero will produce a curve with a tangent along the x-axis at the start point of the curve."},
143 reverse = { docs = "If reverse is true, the segment will start from the end of the involute, otherwise it will start from that start. Defaults to false."},
144 tag = { docs = "Create a new tag which refers to this line"},
145 },
146 tags = ["sketch"]
147}]
148#[allow(clippy::too_many_arguments)]
149async fn inner_involute_circular(
150 sketch: Sketch,
151 start_radius: TyF64,
152 end_radius: TyF64,
153 angle: TyF64,
154 reverse: Option<bool>,
155 tag: Option<TagNode>,
156 exec_state: &mut ExecState,
157 args: Args,
158) -> Result<Sketch, KclError> {
159 let id = exec_state.next_uuid();
160
161 args.batch_modeling_cmd(
162 id,
163 ModelingCmd::from(mcmd::ExtendPath {
164 path: sketch.id.into(),
165 segment: PathSegment::CircularInvolute {
166 start_radius: LengthUnit(start_radius.to_mm()),
167 end_radius: LengthUnit(end_radius.to_mm()),
168 angle: Angle::from_degrees(angle.to_degrees()),
169 reverse: reverse.unwrap_or_default(),
170 },
171 }),
172 )
173 .await?;
174
175 let from = sketch.current_pen_position()?;
176
177 let start_radius = start_radius.to_length_units(from.units);
178 let end_radius = end_radius.to_length_units(from.units);
179
180 let mut end: KPoint3d<f64> = Default::default(); let theta = f64::sqrt(end_radius * end_radius - start_radius * start_radius) / start_radius;
182 let (x, y) = involute_curve(start_radius, theta);
183
184 end.x = x * angle.to_radians().cos() - y * angle.to_radians().sin();
185 end.y = x * angle.to_radians().sin() + y * angle.to_radians().cos();
186
187 end.x -= start_radius * angle.to_radians().cos();
188 end.y -= start_radius * angle.to_radians().sin();
189
190 if reverse.unwrap_or_default() {
191 end.x = -end.x;
192 }
193
194 end.x += from.x;
195 end.y += from.y;
196
197 let current_path = Path::ToPoint {
198 base: BasePath {
199 from: from.ignore_units(),
200 to: [end.x, end.y],
201 tag: tag.clone(),
202 units: sketch.units,
203 geo_meta: GeoMeta {
204 id,
205 metadata: args.source_range.into(),
206 },
207 },
208 };
209
210 let mut new_sketch = sketch.clone();
211 if let Some(tag) = &tag {
212 new_sketch.add_tag(tag, ¤t_path, exec_state);
213 }
214 new_sketch.paths.push(current_path);
215 Ok(new_sketch)
216}
217
218pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
220 let sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
221 let end = args.get_kw_arg_opt_typed("end", &RuntimeType::point2d(), exec_state)?;
222 let end_absolute = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
223 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
224
225 let new_sketch = inner_line(sketch, end_absolute, end, tag, exec_state, args).await?;
226 Ok(KclValue::Sketch {
227 value: Box::new(new_sketch),
228 })
229}
230
231#[stdlib {
256 name = "line",
257 keywords = true,
258 unlabeled_first = true,
259 args = {
260 sketch = { docs = "Which sketch should this path be added to?"},
261 end_absolute = { docs = "Which absolute point should this line go to? Incompatible with `end`."},
262 end = { docs = "How far away (along the X and Y axes) should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
263 tag = { docs = "Create a new tag which refers to this line"},
264 },
265 tags = ["sketch"]
266}]
267async fn inner_line(
268 sketch: Sketch,
269 end_absolute: Option<[TyF64; 2]>,
270 end: Option<[TyF64; 2]>,
271 tag: Option<TagNode>,
272 exec_state: &mut ExecState,
273 args: Args,
274) -> Result<Sketch, KclError> {
275 straight_line(
276 StraightLineParams {
277 sketch,
278 end_absolute,
279 end,
280 tag,
281 relative_name: "end",
282 },
283 exec_state,
284 args,
285 )
286 .await
287}
288
289struct StraightLineParams {
290 sketch: Sketch,
291 end_absolute: Option<[TyF64; 2]>,
292 end: Option<[TyF64; 2]>,
293 tag: Option<TagNode>,
294 relative_name: &'static str,
295}
296
297impl StraightLineParams {
298 fn relative(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
299 Self {
300 sketch,
301 tag,
302 end: Some(p),
303 end_absolute: None,
304 relative_name: "end",
305 }
306 }
307 fn absolute(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
308 Self {
309 sketch,
310 tag,
311 end: None,
312 end_absolute: Some(p),
313 relative_name: "end",
314 }
315 }
316}
317
318async fn straight_line(
319 StraightLineParams {
320 sketch,
321 end,
322 end_absolute,
323 tag,
324 relative_name,
325 }: StraightLineParams,
326 exec_state: &mut ExecState,
327 args: Args,
328) -> Result<Sketch, KclError> {
329 let from = sketch.current_pen_position()?;
330 let (point, is_absolute) = match (end_absolute, end) {
331 (Some(_), Some(_)) => {
332 return Err(KclError::Semantic(KclErrorDetails {
333 source_ranges: vec![args.source_range],
334 message: "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other"
335 .to_owned(),
336 }));
337 }
338 (Some(end_absolute), None) => (end_absolute, true),
339 (None, Some(end)) => (end, false),
340 (None, None) => {
341 return Err(KclError::Semantic(KclErrorDetails {
342 source_ranges: vec![args.source_range],
343 message: format!("You must supply either `{relative_name}` or `endAbsolute` arguments"),
344 }));
345 }
346 };
347
348 let id = exec_state.next_uuid();
349 args.batch_modeling_cmd(
350 id,
351 ModelingCmd::from(mcmd::ExtendPath {
352 path: sketch.id.into(),
353 segment: PathSegment::Line {
354 end: KPoint2d::from(point_to_mm(point.clone())).with_z(0.0).map(LengthUnit),
355 relative: !is_absolute,
356 },
357 }),
358 )
359 .await?;
360
361 let end = if is_absolute {
362 point_to_len_unit(point, from.units)
363 } else {
364 let from = sketch.current_pen_position()?;
365 let point = point_to_len_unit(point, from.units);
366 [from.x + point[0], from.y + point[1]]
367 };
368
369 let current_path = Path::ToPoint {
370 base: BasePath {
371 from: from.ignore_units(),
372 to: end,
373 tag: tag.clone(),
374 units: sketch.units,
375 geo_meta: GeoMeta {
376 id,
377 metadata: args.source_range.into(),
378 },
379 },
380 };
381
382 let mut new_sketch = sketch.clone();
383 if let Some(tag) = &tag {
384 new_sketch.add_tag(tag, ¤t_path, exec_state);
385 }
386
387 new_sketch.paths.push(current_path);
388
389 Ok(new_sketch)
390}
391
392pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
394 let sketch =
395 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
396 let length: Option<TyF64> = args.get_kw_arg_opt_typed("length", &RuntimeType::length(), exec_state)?;
397 let end_absolute: Option<TyF64> = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::length(), exec_state)?;
398 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
399
400 let new_sketch = inner_x_line(sketch, length, end_absolute, tag, exec_state, args).await?;
401 Ok(KclValue::Sketch {
402 value: Box::new(new_sketch),
403 })
404}
405
406#[stdlib {
429 name = "xLine",
430 keywords = true,
431 unlabeled_first = true,
432 args = {
433 sketch = { docs = "Which sketch should this path be added to?"},
434 length = { docs = "How far away along the X axis should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
435 end_absolute = { docs = "Which absolute X value should this line go to? Incompatible with `length`."},
436 tag = { docs = "Create a new tag which refers to this line"},
437 },
438 tags = ["sketch"]
439}]
440async fn inner_x_line(
441 sketch: Sketch,
442 length: Option<TyF64>,
443 end_absolute: Option<TyF64>,
444 tag: Option<TagNode>,
445 exec_state: &mut ExecState,
446 args: Args,
447) -> Result<Sketch, KclError> {
448 let from = sketch.current_pen_position()?;
449 straight_line(
450 StraightLineParams {
451 sketch,
452 end_absolute: end_absolute.map(|x| [x, from.into_y()]),
453 end: length.map(|x| [x, TyF64::new(0.0, NumericType::mm())]),
454 tag,
455 relative_name: "length",
456 },
457 exec_state,
458 args,
459 )
460 .await
461}
462
463pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
465 let sketch =
466 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
467 let length: Option<TyF64> = args.get_kw_arg_opt_typed("length", &RuntimeType::length(), exec_state)?;
468 let end_absolute: Option<TyF64> = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::length(), exec_state)?;
469 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
470
471 let new_sketch = inner_y_line(sketch, length, end_absolute, tag, exec_state, args).await?;
472 Ok(KclValue::Sketch {
473 value: Box::new(new_sketch),
474 })
475}
476
477#[stdlib {
495 name = "yLine",
496 keywords = true,
497 unlabeled_first = true,
498 args = {
499 sketch = { docs = "Which sketch should this path be added to?"},
500 length = { docs = "How far away along the Y axis should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
501 end_absolute = { docs = "Which absolute Y value should this line go to? Incompatible with `length`."},
502 tag = { docs = "Create a new tag which refers to this line"},
503 },
504 tags = ["sketch"]
505}]
506async fn inner_y_line(
507 sketch: Sketch,
508 length: Option<TyF64>,
509 end_absolute: Option<TyF64>,
510 tag: Option<TagNode>,
511 exec_state: &mut ExecState,
512 args: Args,
513) -> Result<Sketch, KclError> {
514 let from = sketch.current_pen_position()?;
515 straight_line(
516 StraightLineParams {
517 sketch,
518 end_absolute: end_absolute.map(|y| [from.into_x(), y]),
519 end: length.map(|y| [TyF64::new(0.0, NumericType::mm()), y]),
520 tag,
521 relative_name: "length",
522 },
523 exec_state,
524 args,
525 )
526 .await
527}
528
529pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
531 let sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
532 let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::degrees(), exec_state)?;
533 let length: Option<TyF64> = args.get_kw_arg_opt_typed("length", &RuntimeType::length(), exec_state)?;
534 let length_x: Option<TyF64> = args.get_kw_arg_opt_typed("lengthX", &RuntimeType::length(), exec_state)?;
535 let length_y: Option<TyF64> = args.get_kw_arg_opt_typed("lengthY", &RuntimeType::length(), exec_state)?;
536 let end_absolute_x: Option<TyF64> =
537 args.get_kw_arg_opt_typed("endAbsoluteX", &RuntimeType::length(), exec_state)?;
538 let end_absolute_y: Option<TyF64> =
539 args.get_kw_arg_opt_typed("endAbsoluteY", &RuntimeType::length(), exec_state)?;
540 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
541
542 let new_sketch = inner_angled_line(
543 sketch,
544 angle.n,
545 length,
546 length_x,
547 length_y,
548 end_absolute_x,
549 end_absolute_y,
550 tag,
551 exec_state,
552 args,
553 )
554 .await?;
555 Ok(KclValue::Sketch {
556 value: Box::new(new_sketch),
557 })
558}
559
560#[stdlib {
578 name = "angledLine",
579 keywords = true,
580 unlabeled_first = true,
581 args = {
582 sketch = { docs = "Which sketch should this path be added to?"},
583 angle = { docs = "Which angle should the line be drawn at?" },
584 length = { docs = "Draw the line this distance along the given angle. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
585 length_x = { docs = "Draw the line this distance along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
586 length_y = { docs = "Draw the line this distance along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
587 end_absolute_x = { docs = "Draw the line along the given angle until it reaches this point along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
588 end_absolute_y = { docs = "Draw the line along the given angle until it reaches this point along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
589 tag = { docs = "Create a new tag which refers to this line"},
590 },
591 tags = ["sketch"]
592}]
593#[allow(clippy::too_many_arguments)]
594async fn inner_angled_line(
595 sketch: Sketch,
596 angle: f64,
597 length: Option<TyF64>,
598 length_x: Option<TyF64>,
599 length_y: Option<TyF64>,
600 end_absolute_x: Option<TyF64>,
601 end_absolute_y: Option<TyF64>,
602 tag: Option<TagNode>,
603 exec_state: &mut ExecState,
604 args: Args,
605) -> Result<Sketch, KclError> {
606 let options_given = [&length, &length_x, &length_y, &end_absolute_x, &end_absolute_y]
607 .iter()
608 .filter(|x| x.is_some())
609 .count();
610 if options_given > 1 {
611 return Err(KclError::Type(KclErrorDetails {
612 message: " one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_string(),
613 source_ranges: vec![args.source_range],
614 }));
615 }
616 if let Some(length_x) = length_x {
617 return inner_angled_line_of_x_length(angle, length_x, sketch, tag, exec_state, args).await;
618 }
619 if let Some(length_y) = length_y {
620 return inner_angled_line_of_y_length(angle, length_y, sketch, tag, exec_state, args).await;
621 }
622 let angle_degrees = angle;
623 match (length, length_x, length_y, end_absolute_x, end_absolute_y) {
624 (Some(length), None, None, None, None) => {
625 inner_angled_line_length(sketch, angle_degrees, length, tag, exec_state, args).await
626 }
627 (None, Some(length_x), None, None, None) => {
628 inner_angled_line_of_x_length(angle_degrees, length_x, sketch, tag, exec_state, args).await
629 }
630 (None, None, Some(length_y), None, None) => {
631 inner_angled_line_of_y_length(angle_degrees, length_y, sketch, tag, exec_state, args).await
632 }
633 (None, None, None, Some(end_absolute_x), None) => {
634 inner_angled_line_to_x(angle_degrees, end_absolute_x, sketch, tag, exec_state, args).await
635 }
636 (None, None, None, None, Some(end_absolute_y)) => {
637 inner_angled_line_to_y(angle_degrees, end_absolute_y, sketch, tag, exec_state, args).await
638 }
639 (None, None, None, None, None) => Err(KclError::Type(KclErrorDetails {
640 message: "One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` must be given".to_string(),
641 source_ranges: vec![args.source_range],
642 })),
643 _ => Err(KclError::Type(KclErrorDetails {
644 message: "Only One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given"
645 .to_string(),
646 source_ranges: vec![args.source_range],
647 })),
648 }
649}
650
651async fn inner_angled_line_length(
652 sketch: Sketch,
653 angle_degrees: f64,
654 length: TyF64,
655 tag: Option<TagNode>,
656 exec_state: &mut ExecState,
657 args: Args,
658) -> Result<Sketch, KclError> {
659 let from = sketch.current_pen_position()?;
660 let length = length.to_length_units(from.units);
661
662 let delta: [f64; 2] = [
664 length * f64::cos(angle_degrees.to_radians()),
665 length * f64::sin(angle_degrees.to_radians()),
666 ];
667 let relative = true;
668
669 let to: [f64; 2] = [from.x + delta[0], from.y + delta[1]];
670
671 let id = exec_state.next_uuid();
672
673 args.batch_modeling_cmd(
674 id,
675 ModelingCmd::from(mcmd::ExtendPath {
676 path: sketch.id.into(),
677 segment: PathSegment::Line {
678 end: KPoint2d::from(untyped_point_to_mm(delta, from.units))
679 .with_z(0.0)
680 .map(LengthUnit),
681 relative,
682 },
683 }),
684 )
685 .await?;
686
687 let current_path = Path::ToPoint {
688 base: BasePath {
689 from: from.ignore_units(),
690 to,
691 tag: tag.clone(),
692 units: sketch.units,
693 geo_meta: GeoMeta {
694 id,
695 metadata: args.source_range.into(),
696 },
697 },
698 };
699
700 let mut new_sketch = sketch.clone();
701 if let Some(tag) = &tag {
702 new_sketch.add_tag(tag, ¤t_path, exec_state);
703 }
704
705 new_sketch.paths.push(current_path);
706 Ok(new_sketch)
707}
708
709async fn inner_angled_line_of_x_length(
710 angle_degrees: f64,
711 length: TyF64,
712 sketch: Sketch,
713 tag: Option<TagNode>,
714 exec_state: &mut ExecState,
715 args: Args,
716) -> Result<Sketch, KclError> {
717 if angle_degrees.abs() == 270.0 {
718 return Err(KclError::Type(KclErrorDetails {
719 message: "Cannot have an x constrained angle of 270 degrees".to_string(),
720 source_ranges: vec![args.source_range],
721 }));
722 }
723
724 if angle_degrees.abs() == 90.0 {
725 return Err(KclError::Type(KclErrorDetails {
726 message: "Cannot have an x constrained angle of 90 degrees".to_string(),
727 source_ranges: vec![args.source_range],
728 }));
729 }
730
731 let to = get_y_component(Angle::from_degrees(angle_degrees), length.n);
732 let to = [TyF64::new(to[0], length.ty.clone()), TyF64::new(to[1], length.ty)];
733
734 let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
735
736 Ok(new_sketch)
737}
738
739async fn inner_angled_line_to_x(
740 angle_degrees: f64,
741 x_to: TyF64,
742 sketch: Sketch,
743 tag: Option<TagNode>,
744 exec_state: &mut ExecState,
745 args: Args,
746) -> Result<Sketch, KclError> {
747 let from = sketch.current_pen_position()?;
748
749 if angle_degrees.abs() == 270.0 {
750 return Err(KclError::Type(KclErrorDetails {
751 message: "Cannot have an x constrained angle of 270 degrees".to_string(),
752 source_ranges: vec![args.source_range],
753 }));
754 }
755
756 if angle_degrees.abs() == 90.0 {
757 return Err(KclError::Type(KclErrorDetails {
758 message: "Cannot have an x constrained angle of 90 degrees".to_string(),
759 source_ranges: vec![args.source_range],
760 }));
761 }
762
763 let x_component = x_to.to_length_units(from.units) - from.x;
764 let y_component = x_component * f64::tan(angle_degrees.to_radians());
765 let y_to = from.y + y_component;
766
767 let new_sketch = straight_line(
768 StraightLineParams::absolute([x_to, TyF64::new(y_to, from.units.into())], sketch, tag),
769 exec_state,
770 args,
771 )
772 .await?;
773 Ok(new_sketch)
774}
775
776async fn inner_angled_line_of_y_length(
777 angle_degrees: f64,
778 length: TyF64,
779 sketch: Sketch,
780 tag: Option<TagNode>,
781 exec_state: &mut ExecState,
782 args: Args,
783) -> Result<Sketch, KclError> {
784 if angle_degrees.abs() == 0.0 {
785 return Err(KclError::Type(KclErrorDetails {
786 message: "Cannot have a y constrained angle of 0 degrees".to_string(),
787 source_ranges: vec![args.source_range],
788 }));
789 }
790
791 if angle_degrees.abs() == 180.0 {
792 return Err(KclError::Type(KclErrorDetails {
793 message: "Cannot have a y constrained angle of 180 degrees".to_string(),
794 source_ranges: vec![args.source_range],
795 }));
796 }
797
798 let to = get_x_component(Angle::from_degrees(angle_degrees), length.n);
799 let to = [TyF64::new(to[0], length.ty.clone()), TyF64::new(to[1], length.ty)];
800
801 let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
802
803 Ok(new_sketch)
804}
805
806async fn inner_angled_line_to_y(
807 angle_degrees: f64,
808 y_to: TyF64,
809 sketch: Sketch,
810 tag: Option<TagNode>,
811 exec_state: &mut ExecState,
812 args: Args,
813) -> Result<Sketch, KclError> {
814 let from = sketch.current_pen_position()?;
815
816 if angle_degrees.abs() == 0.0 {
817 return Err(KclError::Type(KclErrorDetails {
818 message: "Cannot have a y constrained angle of 0 degrees".to_string(),
819 source_ranges: vec![args.source_range],
820 }));
821 }
822
823 if angle_degrees.abs() == 180.0 {
824 return Err(KclError::Type(KclErrorDetails {
825 message: "Cannot have a y constrained angle of 180 degrees".to_string(),
826 source_ranges: vec![args.source_range],
827 }));
828 }
829
830 let y_component = y_to.to_length_units(from.units) - from.y;
831 let x_component = y_component / f64::tan(angle_degrees.to_radians());
832 let x_to = from.x + x_component;
833
834 let new_sketch = straight_line(
835 StraightLineParams::absolute([TyF64::new(x_to, from.units.into()), y_to], sketch, tag),
836 exec_state,
837 args,
838 )
839 .await?;
840 Ok(new_sketch)
841}
842
843pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
845 let sketch =
846 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
847 let angle: TyF64 = args.get_kw_arg("angle")?;
848 let intersect_tag: TagIdentifier = args.get_kw_arg("intersectTag")?;
849 let offset: Option<TyF64> = args.get_kw_arg_opt("offset")?;
850 let tag: Option<TagNode> = args.get_kw_arg_opt("tag")?;
851 let new_sketch =
852 inner_angled_line_that_intersects(sketch, angle, intersect_tag, offset, tag, exec_state, args).await?;
853 Ok(KclValue::Sketch {
854 value: Box::new(new_sketch),
855 })
856}
857
858#[stdlib {
878 name = "angledLineThatIntersects",
879 keywords = true,
880 unlabeled_first = true,
881 args = {
882 sketch = { docs = "Which sketch should this path be added to?"},
883 angle = { docs = "Which angle should the line be drawn at?" },
884 intersect_tag = { docs = "The tag of the line to intersect with" },
885 offset = { docs = "The offset from the intersecting line. Defaults to 0." },
886 tag = { docs = "Create a new tag which refers to this line"},
887 },
888 tags = ["sketch"]
889}]
890pub async fn inner_angled_line_that_intersects(
891 sketch: Sketch,
892 angle: TyF64,
893 intersect_tag: TagIdentifier,
894 offset: Option<TyF64>,
895 tag: Option<TagNode>,
896 exec_state: &mut ExecState,
897 args: Args,
898) -> Result<Sketch, KclError> {
899 let intersect_path = args.get_tag_engine_info(exec_state, &intersect_tag)?;
900 let path = intersect_path.path.clone().ok_or_else(|| {
901 KclError::Type(KclErrorDetails {
902 message: format!("Expected an intersect path with a path, found `{:?}`", intersect_path),
903 source_ranges: vec![args.source_range],
904 })
905 })?;
906
907 let from = sketch.current_pen_position()?;
908 let to = intersection_with_parallel_line(
909 &[
910 point_to_len_unit(path.get_from(), from.units),
911 point_to_len_unit(path.get_to(), from.units),
912 ],
913 offset.map(|t| t.to_length_units(from.units)).unwrap_or_default(),
914 angle.to_degrees(),
915 from.ignore_units(),
916 );
917 let to = [
918 TyF64::new(to[0], from.units.into()),
919 TyF64::new(to[1], from.units.into()),
920 ];
921
922 straight_line(StraightLineParams::absolute(to, sketch, tag), exec_state, args).await
923}
924
925#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
928#[ts(export)]
929#[serde(rename_all = "camelCase", untagged)]
930#[allow(clippy::large_enum_variant)]
931pub enum SketchData {
932 PlaneOrientation(PlaneData),
933 Plane(Box<Plane>),
934 Solid(Box<Solid>),
935}
936
937#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
939#[ts(export)]
940#[serde(rename_all = "camelCase")]
941#[allow(clippy::large_enum_variant)]
942pub enum PlaneData {
943 #[serde(rename = "XY", alias = "xy")]
945 XY,
946 #[serde(rename = "-XY", alias = "-xy")]
948 NegXY,
949 #[serde(rename = "XZ", alias = "xz")]
951 XZ,
952 #[serde(rename = "-XZ", alias = "-xz")]
954 NegXZ,
955 #[serde(rename = "YZ", alias = "yz")]
957 YZ,
958 #[serde(rename = "-YZ", alias = "-yz")]
960 NegYZ,
961 Plane(PlaneInfo),
963}
964
965pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
967 let data = args.get_unlabeled_kw_arg_typed(
968 "planeOrSolid",
969 &RuntimeType::Union(vec![RuntimeType::solid(), RuntimeType::plane()]),
970 exec_state,
971 )?;
972 let face = args.get_kw_arg_opt("face")?;
973
974 match inner_start_sketch_on(data, face, exec_state, &args).await? {
975 SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
976 SketchSurface::Face(value) => Ok(KclValue::Face { value }),
977 }
978}
979
980#[stdlib {
1155 name = "startSketchOn",
1156 feature_tree_operation = true,
1157 keywords = true,
1158 unlabeled_first = true,
1159 args = {
1160 plane_or_solid = { docs = "The plane or solid to sketch on"},
1161 face = { docs = "Identify a face of a solid if a solid is specified as the input argument (`plane_or_solid`)"},
1162 },
1163 tags = ["sketch"]
1164}]
1165async fn inner_start_sketch_on(
1166 plane_or_solid: SketchData,
1167 face: Option<FaceTag>,
1168 exec_state: &mut ExecState,
1169 args: &Args,
1170) -> Result<SketchSurface, KclError> {
1171 match plane_or_solid {
1172 SketchData::PlaneOrientation(plane_data) => {
1173 let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
1174 Ok(SketchSurface::Plane(plane))
1175 }
1176 SketchData::Plane(plane) => {
1177 if plane.value == crate::exec::PlaneType::Uninit {
1178 if plane.info.origin.units == UnitLen::Unknown {
1179 return Err(KclError::Semantic(KclErrorDetails {
1180 message: "Origin of plane has unknown units".to_string(),
1181 source_ranges: vec![args.source_range],
1182 }));
1183 }
1184 let plane = make_sketch_plane_from_orientation(plane.info.into_plane_data(), exec_state, args).await?;
1185 Ok(SketchSurface::Plane(plane))
1186 } else {
1187 #[cfg(feature = "artifact-graph")]
1189 {
1190 let id = exec_state.next_uuid();
1191 exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
1192 id: ArtifactId::from(id),
1193 plane_id: plane.artifact_id,
1194 code_ref: CodeRef::placeholder(args.source_range),
1195 }));
1196 }
1197
1198 Ok(SketchSurface::Plane(plane))
1199 }
1200 }
1201 SketchData::Solid(solid) => {
1202 let Some(tag) = face else {
1203 return Err(KclError::Type(KclErrorDetails {
1204 message: "Expected a tag for the face to sketch on".to_string(),
1205 source_ranges: vec![args.source_range],
1206 }));
1207 };
1208 let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
1209
1210 #[cfg(feature = "artifact-graph")]
1211 {
1212 let id = exec_state.next_uuid();
1214 exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
1215 id: ArtifactId::from(id),
1216 face_id: face.artifact_id,
1217 code_ref: CodeRef::placeholder(args.source_range),
1218 }));
1219 }
1220
1221 Ok(SketchSurface::Face(face))
1222 }
1223 }
1224}
1225
1226async fn start_sketch_on_face(
1227 solid: Box<Solid>,
1228 tag: FaceTag,
1229 exec_state: &mut ExecState,
1230 args: &Args,
1231) -> Result<Box<Face>, KclError> {
1232 let extrude_plane_id = tag.get_face_id(&solid, exec_state, args, true).await?;
1233
1234 Ok(Box::new(Face {
1235 id: extrude_plane_id,
1236 #[cfg(feature = "artifact-graph")]
1237 artifact_id: extrude_plane_id.into(),
1238 value: tag.to_string(),
1239 x_axis: solid.sketch.on.x_axis(),
1241 y_axis: solid.sketch.on.y_axis(),
1242 units: solid.units,
1243 solid,
1244 meta: vec![args.source_range.into()],
1245 }))
1246}
1247
1248async fn make_sketch_plane_from_orientation(
1249 data: PlaneData,
1250 exec_state: &mut ExecState,
1251 args: &Args,
1252) -> Result<Box<Plane>, KclError> {
1253 let plane = Plane::from_plane_data(data.clone(), exec_state)?;
1254
1255 let clobber = false;
1257 let size = LengthUnit(60.0);
1258 let hide = Some(true);
1259 args.batch_modeling_cmd(
1260 plane.id,
1261 ModelingCmd::from(mcmd::MakePlane {
1262 clobber,
1263 origin: plane.info.origin.into(),
1264 size,
1265 x_axis: plane.info.x_axis.into(),
1266 y_axis: plane.info.y_axis.into(),
1267 hide,
1268 }),
1269 )
1270 .await?;
1271
1272 Ok(Box::new(plane))
1273}
1274
1275pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1277 let sketch_surface = args.get_unlabeled_kw_arg("startProfileOn")?;
1279 let start: [TyF64; 2] = args.get_kw_arg_typed("at", &RuntimeType::point2d(), exec_state)?;
1280 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1281
1282 let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, args).await?;
1283 Ok(KclValue::Sketch {
1284 value: Box::new(sketch),
1285 })
1286}
1287
1288#[stdlib {
1323 name = "startProfile",
1324 keywords = true,
1325 unlabeled_first = true,
1326 args = {
1327 sketch_surface = { docs = "What to start the profile on" },
1328 at = { docs = "Where to start the profile. An absolute point." },
1329 tag = { docs = "Tag this first starting point" },
1330 },
1331 tags = ["sketch"]
1332}]
1333pub(crate) async fn inner_start_profile(
1334 sketch_surface: SketchSurface,
1335 at: [TyF64; 2],
1336 tag: Option<TagNode>,
1337 exec_state: &mut ExecState,
1338 args: Args,
1339) -> Result<Sketch, KclError> {
1340 match &sketch_surface {
1341 SketchSurface::Face(face) => {
1342 args.flush_batch_for_solids(exec_state, &[(*face.solid).clone()])
1345 .await?;
1346 }
1347 SketchSurface::Plane(plane) if !plane.is_standard() => {
1348 args.batch_end_cmd(
1351 exec_state.next_uuid(),
1352 ModelingCmd::from(mcmd::ObjectVisible {
1353 object_id: plane.id,
1354 hidden: true,
1355 }),
1356 )
1357 .await?;
1358 }
1359 _ => {}
1360 }
1361
1362 let enable_sketch_id = exec_state.next_uuid();
1363 let path_id = exec_state.next_uuid();
1364 let move_pen_id = exec_state.next_uuid();
1365 args.batch_modeling_cmds(&[
1366 ModelingCmdReq {
1369 cmd: ModelingCmd::from(mcmd::EnableSketchMode {
1370 animated: false,
1371 ortho: false,
1372 entity_id: sketch_surface.id(),
1373 adjust_camera: false,
1374 planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
1375 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
1377 Some(normal.into())
1378 } else {
1379 None
1380 },
1381 }),
1382 cmd_id: enable_sketch_id.into(),
1383 },
1384 ModelingCmdReq {
1385 cmd: ModelingCmd::from(mcmd::StartPath::default()),
1386 cmd_id: path_id.into(),
1387 },
1388 ModelingCmdReq {
1389 cmd: ModelingCmd::from(mcmd::MovePathPen {
1390 path: path_id.into(),
1391 to: KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit),
1392 }),
1393 cmd_id: move_pen_id.into(),
1394 },
1395 ModelingCmdReq {
1396 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1397 cmd_id: exec_state.next_uuid().into(),
1398 },
1399 ])
1400 .await?;
1401
1402 let (to, ty) = untype_point(at);
1403 let current_path = BasePath {
1404 from: to,
1405 to,
1406 tag: tag.clone(),
1407 units: ty.expect_length(),
1408 geo_meta: GeoMeta {
1409 id: move_pen_id,
1410 metadata: args.source_range.into(),
1411 },
1412 };
1413
1414 let sketch = Sketch {
1415 id: path_id,
1416 original_id: path_id,
1417 #[cfg(feature = "artifact-graph")]
1418 artifact_id: path_id.into(),
1419 on: sketch_surface.clone(),
1420 paths: vec![],
1421 units: ty.expect_length(),
1422 mirror: Default::default(),
1423 meta: vec![args.source_range.into()],
1424 tags: if let Some(tag) = &tag {
1425 let mut tag_identifier: TagIdentifier = tag.into();
1426 tag_identifier.info = vec![(
1427 exec_state.stack().current_epoch(),
1428 TagEngineInfo {
1429 id: current_path.geo_meta.id,
1430 sketch: path_id,
1431 path: Some(Path::Base {
1432 base: current_path.clone(),
1433 }),
1434 surface: None,
1435 },
1436 )];
1437 IndexMap::from([(tag.name.to_string(), tag_identifier)])
1438 } else {
1439 Default::default()
1440 },
1441 start: current_path,
1442 };
1443 Ok(sketch)
1444}
1445
1446pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1448 let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
1449 let ty = sketch.units.into();
1450 let x = inner_profile_start_x(sketch)?;
1451 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1452}
1453
1454#[stdlib {
1465 name = "profileStartX",
1466 keywords = true,
1467 unlabeled_first = true,
1468 args = {
1469 profile = {docs = "Profile whose start is being used"},
1470 },
1471 tags = ["sketch"]
1472}]
1473pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1474 Ok(profile.start.to[0])
1475}
1476
1477pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1479 let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
1480 let ty = sketch.units.into();
1481 let x = inner_profile_start_y(sketch)?;
1482 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1483}
1484
1485#[stdlib {
1495 name = "profileStartY",
1496 keywords = true,
1497 unlabeled_first = true,
1498 args = {
1499 profile = {docs = "Profile whose start is being used"},
1500 },
1501 tags = ["sketch"]
1502}]
1503pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1504 Ok(profile.start.to[1])
1505}
1506
1507pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1509 let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
1510 let ty = sketch.units.into();
1511 let point = inner_profile_start(sketch)?;
1512 Ok(KclValue::from_point2d(point, ty, args.into()))
1513}
1514
1515#[stdlib {
1528 name = "profileStart",
1529 keywords = true,
1530 unlabeled_first = true,
1531 args = {
1532 profile = {docs = "Profile whose start is being used"},
1533 },
1534 tags = ["sketch"]
1535}]
1536pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1537 Ok(profile.start.to)
1538}
1539
1540pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1542 let sketch =
1543 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1544 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1545 let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1546 Ok(KclValue::Sketch {
1547 value: Box::new(new_sketch),
1548 })
1549}
1550
1551#[stdlib {
1573 name = "close",
1574 keywords = true,
1575 unlabeled_first = true,
1576 args = {
1577 sketch = { docs = "The sketch you want to close"},
1578 tag = { docs = "Create a new tag which refers to this line"},
1579 },
1580 tags = ["sketch"]
1581}]
1582pub(crate) async fn inner_close(
1583 sketch: Sketch,
1584 tag: Option<TagNode>,
1585 exec_state: &mut ExecState,
1586 args: Args,
1587) -> Result<Sketch, KclError> {
1588 let from = sketch.current_pen_position()?;
1589 let to = point_to_len_unit(sketch.start.get_from(), from.units);
1590
1591 let id = exec_state.next_uuid();
1592
1593 args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }))
1594 .await?;
1595
1596 let current_path = Path::ToPoint {
1597 base: BasePath {
1598 from: from.ignore_units(),
1599 to,
1600 tag: tag.clone(),
1601 units: sketch.units,
1602 geo_meta: GeoMeta {
1603 id,
1604 metadata: args.source_range.into(),
1605 },
1606 },
1607 };
1608
1609 let mut new_sketch = sketch.clone();
1610 if let Some(tag) = &tag {
1611 new_sketch.add_tag(tag, ¤t_path, exec_state);
1612 }
1613
1614 new_sketch.paths.push(current_path);
1615
1616 Ok(new_sketch)
1617}
1618
1619pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1621 let sketch =
1622 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1623
1624 let angle_start: Option<TyF64> = args.get_kw_arg_opt_typed("angleStart", &RuntimeType::degrees(), exec_state)?;
1625 let angle_end: Option<TyF64> = args.get_kw_arg_opt_typed("angleEnd", &RuntimeType::degrees(), exec_state)?;
1626 let radius: Option<TyF64> = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
1627 let end_absolute: Option<[TyF64; 2]> =
1628 args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1629 let interior_absolute: Option<[TyF64; 2]> =
1630 args.get_kw_arg_opt_typed("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1631 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1632 let new_sketch = inner_arc(
1633 sketch,
1634 angle_start,
1635 angle_end,
1636 radius,
1637 interior_absolute,
1638 end_absolute,
1639 tag,
1640 exec_state,
1641 args,
1642 )
1643 .await?;
1644 Ok(KclValue::Sketch {
1645 value: Box::new(new_sketch),
1646 })
1647}
1648
1649#[stdlib {
1683 name = "arc",
1684 keywords = true,
1685 unlabeled_first = true,
1686 args = {
1687 sketch = { docs = "Which sketch should this path be added to?" },
1688 angle_start = { docs = "Where along the circle should this arc start?", include_in_snippet = true },
1689 angle_end = { docs = "Where along the circle should this arc end?", include_in_snippet = true },
1690 radius = { docs = "How large should the circle be?", include_in_snippet = true },
1691 interior_absolute = { docs = "Any point between the arc's start and end? Requires `endAbsolute`. Incompatible with `angleStart` or `angleEnd`" },
1692 end_absolute = { docs = "Where should this arc end? Requires `interiorAbsolute`. Incompatible with `angleStart` or `angleEnd`" },
1693 tag = { docs = "Create a new tag which refers to this line"},
1694 },
1695 tags = ["sketch"]
1696}]
1697#[allow(clippy::too_many_arguments)]
1698pub(crate) async fn inner_arc(
1699 sketch: Sketch,
1700 angle_start: Option<TyF64>,
1701 angle_end: Option<TyF64>,
1702 radius: Option<TyF64>,
1703 interior_absolute: Option<[TyF64; 2]>,
1704 end_absolute: Option<[TyF64; 2]>,
1705 tag: Option<TagNode>,
1706 exec_state: &mut ExecState,
1707 args: Args,
1708) -> Result<Sketch, KclError> {
1709 let from: Point2d = sketch.current_pen_position()?;
1710 let id = exec_state.next_uuid();
1711
1712 match (angle_start, angle_end, radius, interior_absolute, end_absolute) {
1713 (Some(angle_start), Some(angle_end), Some(radius), None, None) => {
1714 relative_arc(&args, id, exec_state, sketch, from, angle_start, angle_end, radius, tag).await
1715 }
1716 (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1717 absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1718 }
1719 _ => {
1720 Err(KclError::Type(KclErrorDetails {
1721 message:
1722 "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)"
1723 .to_string(),
1724 source_ranges: vec![args.source_range],
1725 }))
1726 }
1727 }
1728}
1729
1730#[allow(clippy::too_many_arguments)]
1731pub async fn absolute_arc(
1732 args: &Args,
1733 id: uuid::Uuid,
1734 exec_state: &mut ExecState,
1735 sketch: Sketch,
1736 from: Point2d,
1737 interior_absolute: [TyF64; 2],
1738 end_absolute: [TyF64; 2],
1739 tag: Option<TagNode>,
1740) -> Result<Sketch, KclError> {
1741 args.batch_modeling_cmd(
1743 id,
1744 ModelingCmd::from(mcmd::ExtendPath {
1745 path: sketch.id.into(),
1746 segment: PathSegment::ArcTo {
1747 end: kcmc::shared::Point3d {
1748 x: LengthUnit(end_absolute[0].to_mm()),
1749 y: LengthUnit(end_absolute[1].to_mm()),
1750 z: LengthUnit(0.0),
1751 },
1752 interior: kcmc::shared::Point3d {
1753 x: LengthUnit(interior_absolute[0].to_mm()),
1754 y: LengthUnit(interior_absolute[1].to_mm()),
1755 z: LengthUnit(0.0),
1756 },
1757 relative: false,
1758 },
1759 }),
1760 )
1761 .await?;
1762
1763 let start = [from.x, from.y];
1764 let end = point_to_len_unit(end_absolute, from.units);
1765
1766 let current_path = Path::ArcThreePoint {
1767 base: BasePath {
1768 from: from.ignore_units(),
1769 to: end,
1770 tag: tag.clone(),
1771 units: sketch.units,
1772 geo_meta: GeoMeta {
1773 id,
1774 metadata: args.source_range.into(),
1775 },
1776 },
1777 p1: start,
1778 p2: point_to_len_unit(interior_absolute, from.units),
1779 p3: end,
1780 };
1781
1782 let mut new_sketch = sketch.clone();
1783 if let Some(tag) = &tag {
1784 new_sketch.add_tag(tag, ¤t_path, exec_state);
1785 }
1786
1787 new_sketch.paths.push(current_path);
1788
1789 Ok(new_sketch)
1790}
1791
1792#[allow(clippy::too_many_arguments)]
1793pub async fn relative_arc(
1794 args: &Args,
1795 id: uuid::Uuid,
1796 exec_state: &mut ExecState,
1797 sketch: Sketch,
1798 from: Point2d,
1799 angle_start: TyF64,
1800 angle_end: TyF64,
1801 radius: TyF64,
1802 tag: Option<TagNode>,
1803) -> Result<Sketch, KclError> {
1804 let a_start = Angle::from_degrees(angle_start.to_degrees());
1805 let a_end = Angle::from_degrees(angle_end.to_degrees());
1806 let radius = radius.to_length_units(from.units);
1807 let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1808 if a_start == a_end {
1809 return Err(KclError::Type(KclErrorDetails {
1810 message: "Arc start and end angles must be different".to_string(),
1811 source_ranges: vec![args.source_range],
1812 }));
1813 }
1814 let ccw = a_start < a_end;
1815
1816 args.batch_modeling_cmd(
1817 id,
1818 ModelingCmd::from(mcmd::ExtendPath {
1819 path: sketch.id.into(),
1820 segment: PathSegment::Arc {
1821 start: a_start,
1822 end: a_end,
1823 center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1824 radius: LengthUnit(from.units.adjust_to(radius, UnitLen::Mm).0),
1825 relative: false,
1826 },
1827 }),
1828 )
1829 .await?;
1830
1831 let current_path = Path::Arc {
1832 base: BasePath {
1833 from: from.ignore_units(),
1834 to: end,
1835 tag: tag.clone(),
1836 units: from.units,
1837 geo_meta: GeoMeta {
1838 id,
1839 metadata: args.source_range.into(),
1840 },
1841 },
1842 center,
1843 radius,
1844 ccw,
1845 };
1846
1847 let mut new_sketch = sketch.clone();
1848 if let Some(tag) = &tag {
1849 new_sketch.add_tag(tag, ¤t_path, exec_state);
1850 }
1851
1852 new_sketch.paths.push(current_path);
1853
1854 Ok(new_sketch)
1855}
1856
1857pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1859 let sketch =
1860 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1861 let end = args.get_kw_arg_opt_typed("end", &RuntimeType::point2d(), exec_state)?;
1862 let end_absolute = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1863 let radius = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
1864 let angle = args.get_kw_arg_opt_typed("angle", &RuntimeType::angle(), exec_state)?;
1865 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1866
1867 let new_sketch = inner_tangential_arc(sketch, end_absolute, end, radius, angle, tag, exec_state, args).await?;
1868 Ok(KclValue::Sketch {
1869 value: Box::new(new_sketch),
1870 })
1871}
1872
1873#[stdlib {
1928 name = "tangentialArc",
1929 keywords = true,
1930 unlabeled_first = true,
1931 args = {
1932 sketch = { docs = "Which sketch should this path be added to?"},
1933 end_absolute = { docs = "Which absolute point should this arc go to? Incompatible with `end`, `radius`, and `offset`."},
1934 end = { docs = "How far away (along the X and Y axes) should this arc go? Incompatible with `endAbsolute`, `radius`, and `offset`.", include_in_snippet = true },
1935 radius = { docs = "Radius of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute`."},
1936 angle = { docs = "Offset of the arc in degrees. `radius` must be given. Incompatible with `end` and `endAbsolute`."},
1937 tag = { docs = "Create a new tag which refers to this arc"},
1938 },
1939 tags = ["sketch"]
1940}]
1941#[allow(clippy::too_many_arguments)]
1942async fn inner_tangential_arc(
1943 sketch: Sketch,
1944 end_absolute: Option<[TyF64; 2]>,
1945 end: Option<[TyF64; 2]>,
1946 radius: Option<TyF64>,
1947 angle: Option<TyF64>,
1948 tag: Option<TagNode>,
1949 exec_state: &mut ExecState,
1950 args: Args,
1951) -> Result<Sketch, KclError> {
1952 match (end_absolute, end, radius, angle) {
1953 (Some(point), None, None, None) => {
1954 inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1955 }
1956 (None, Some(point), None, None) => {
1957 inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1958 }
1959 (None, None, Some(radius), Some(angle)) => {
1960 let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1961 inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1962 }
1963 (Some(_), Some(_), None, None) => Err(KclError::Semantic(KclErrorDetails {
1964 source_ranges: vec![args.source_range],
1965 message: "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other"
1966 .to_owned(),
1967 })),
1968 (None, None, Some(_), None) | (None, None, None, Some(_)) => Err(KclError::Semantic(KclErrorDetails {
1969 source_ranges: vec![args.source_range],
1970 message: "You must supply both `radius` and `angle` arguments".to_owned(),
1971 })),
1972 (_, _, _, _) => Err(KclError::Semantic(KclErrorDetails {
1973 source_ranges: vec![args.source_range],
1974 message: "You must supply `end`, `endAbsolute`, or both `radius` and `angle` arguments".to_owned(),
1975 })),
1976 }
1977}
1978
1979#[derive(Debug, Clone, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
1981#[ts(export)]
1982#[serde(rename_all = "camelCase", untagged)]
1983pub enum TangentialArcData {
1984 RadiusAndOffset {
1985 radius: TyF64,
1988 offset: TyF64,
1990 },
1991}
1992
1993async fn inner_tangential_arc_radius_angle(
2000 data: TangentialArcData,
2001 sketch: Sketch,
2002 tag: Option<TagNode>,
2003 exec_state: &mut ExecState,
2004 args: Args,
2005) -> Result<Sketch, KclError> {
2006 let from: Point2d = sketch.current_pen_position()?;
2007 let tangent_info = sketch.get_tangential_info_from_paths(); let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
2010
2011 let id = exec_state.next_uuid();
2012
2013 let (center, to, ccw) = match data {
2014 TangentialArcData::RadiusAndOffset { radius, offset } => {
2015 let offset = Angle::from_degrees(offset.to_degrees());
2017
2018 let previous_end_tangent = Angle::from_radians(f64::atan2(
2021 from.y - tan_previous_point[1],
2022 from.x - tan_previous_point[0],
2023 ));
2024 let ccw = offset.to_degrees() > 0.0;
2027 let tangent_to_arc_start_angle = if ccw {
2028 Angle::from_degrees(-90.0)
2030 } else {
2031 Angle::from_degrees(90.0)
2033 };
2034 let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
2037 let end_angle = start_angle + offset;
2038 let (center, to) = arc_center_and_end(
2039 from.ignore_units(),
2040 start_angle,
2041 end_angle,
2042 radius.to_length_units(from.units),
2043 );
2044
2045 args.batch_modeling_cmd(
2046 id,
2047 ModelingCmd::from(mcmd::ExtendPath {
2048 path: sketch.id.into(),
2049 segment: PathSegment::TangentialArc {
2050 radius: LengthUnit(radius.to_mm()),
2051 offset,
2052 },
2053 }),
2054 )
2055 .await?;
2056 (center, to, ccw)
2057 }
2058 };
2059
2060 let current_path = Path::TangentialArc {
2061 ccw,
2062 center,
2063 base: BasePath {
2064 from: from.ignore_units(),
2065 to,
2066 tag: tag.clone(),
2067 units: sketch.units,
2068 geo_meta: GeoMeta {
2069 id,
2070 metadata: args.source_range.into(),
2071 },
2072 },
2073 };
2074
2075 let mut new_sketch = sketch.clone();
2076 if let Some(tag) = &tag {
2077 new_sketch.add_tag(tag, ¤t_path, exec_state);
2078 }
2079
2080 new_sketch.paths.push(current_path);
2081
2082 Ok(new_sketch)
2083}
2084
2085fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
2087 ModelingCmd::from(mcmd::ExtendPath {
2088 path: sketch.id.into(),
2089 segment: PathSegment::TangentialArcTo {
2090 angle_snap_increment: None,
2091 to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
2092 .with_z(0.0)
2093 .map(LengthUnit),
2094 },
2095 })
2096}
2097
2098async fn inner_tangential_arc_to_point(
2099 sketch: Sketch,
2100 point: [TyF64; 2],
2101 is_absolute: bool,
2102 tag: Option<TagNode>,
2103 exec_state: &mut ExecState,
2104 args: Args,
2105) -> Result<Sketch, KclError> {
2106 let from: Point2d = sketch.current_pen_position()?;
2107 let tangent_info = sketch.get_tangential_info_from_paths();
2108 let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
2109
2110 let point = point_to_len_unit(point, from.units);
2111
2112 let to = if is_absolute {
2113 point
2114 } else {
2115 [from.x + point[0], from.y + point[1]]
2116 };
2117 let [to_x, to_y] = to;
2118 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
2119 arc_start_point: [from.x, from.y],
2120 arc_end_point: [to_x, to_y],
2121 tan_previous_point,
2122 obtuse: true,
2123 });
2124
2125 if result.center[0].is_infinite() {
2126 return Err(KclError::Semantic(KclErrorDetails {
2127 source_ranges: vec![args.source_range],
2128 message:
2129 "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
2130 .to_owned(),
2131 }));
2132 } else if result.center[1].is_infinite() {
2133 return Err(KclError::Semantic(KclErrorDetails {
2134 source_ranges: vec![args.source_range],
2135 message:
2136 "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
2137 .to_owned(),
2138 }));
2139 }
2140
2141 let delta = if is_absolute {
2142 [to_x - from.x, to_y - from.y]
2143 } else {
2144 point
2145 };
2146 let id = exec_state.next_uuid();
2147 args.batch_modeling_cmd(id, tan_arc_to(&sketch, delta)).await?;
2148
2149 let current_path = Path::TangentialArcTo {
2150 base: BasePath {
2151 from: from.ignore_units(),
2152 to,
2153 tag: tag.clone(),
2154 units: sketch.units,
2155 geo_meta: GeoMeta {
2156 id,
2157 metadata: args.source_range.into(),
2158 },
2159 },
2160 center: result.center,
2161 ccw: result.ccw > 0,
2162 };
2163
2164 let mut new_sketch = sketch.clone();
2165 if let Some(tag) = &tag {
2166 new_sketch.add_tag(tag, ¤t_path, exec_state);
2167 }
2168
2169 new_sketch.paths.push(current_path);
2170
2171 Ok(new_sketch)
2172}
2173
2174pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2176 let sketch =
2177 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2178 let end: [TyF64; 2] = args.get_kw_arg_typed("end", &RuntimeType::point2d(), exec_state)?;
2179 let control1: [TyF64; 2] = args.get_kw_arg_typed("control1", &RuntimeType::point2d(), exec_state)?;
2180 let control2: [TyF64; 2] = args.get_kw_arg_typed("control2", &RuntimeType::point2d(), exec_state)?;
2181 let tag = args.get_kw_arg_opt("tag")?;
2182
2183 let new_sketch = inner_bezier_curve(sketch, control1, control2, end, tag, exec_state, args).await?;
2184 Ok(KclValue::Sketch {
2185 value: Box::new(new_sketch),
2186 })
2187}
2188
2189#[stdlib {
2208 name = "bezierCurve",
2209 keywords = true,
2210 unlabeled_first = true,
2211 args = {
2212 sketch = { docs = "Which sketch should this path be added to?"},
2213 end = { docs = "How far away (along the X and Y axes) should this line go?" },
2214 control1 = { docs = "First control point for the cubic" },
2215 control2 = { docs = "Second control point for the cubic" },
2216 tag = { docs = "Create a new tag which refers to this line"},
2217 },
2218 tags = ["sketch"]
2219}]
2220async fn inner_bezier_curve(
2221 sketch: Sketch,
2222 control1: [TyF64; 2],
2223 control2: [TyF64; 2],
2224 end: [TyF64; 2],
2225 tag: Option<TagNode>,
2226 exec_state: &mut ExecState,
2227 args: Args,
2228) -> Result<Sketch, KclError> {
2229 let from = sketch.current_pen_position()?;
2230
2231 let relative = true;
2232 let delta = end.clone();
2233 let to = [
2234 from.x + end[0].to_length_units(from.units),
2235 from.y + end[1].to_length_units(from.units),
2236 ];
2237
2238 let id = exec_state.next_uuid();
2239
2240 args.batch_modeling_cmd(
2241 id,
2242 ModelingCmd::from(mcmd::ExtendPath {
2243 path: sketch.id.into(),
2244 segment: PathSegment::Bezier {
2245 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2246 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2247 end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
2248 relative,
2249 },
2250 }),
2251 )
2252 .await?;
2253
2254 let current_path = Path::ToPoint {
2255 base: BasePath {
2256 from: from.ignore_units(),
2257 to,
2258 tag: tag.clone(),
2259 units: sketch.units,
2260 geo_meta: GeoMeta {
2261 id,
2262 metadata: args.source_range.into(),
2263 },
2264 },
2265 };
2266
2267 let mut new_sketch = sketch.clone();
2268 if let Some(tag) = &tag {
2269 new_sketch.add_tag(tag, ¤t_path, exec_state);
2270 }
2271
2272 new_sketch.paths.push(current_path);
2273
2274 Ok(new_sketch)
2275}
2276
2277pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2279 let sketch =
2280 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2281
2282 let tool: Vec<Sketch> = args.get_kw_arg_typed(
2283 "tool",
2284 &RuntimeType::Array(
2285 Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
2286 ArrayLen::NonEmpty,
2287 ),
2288 exec_state,
2289 )?;
2290
2291 let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
2292 Ok(KclValue::Sketch {
2293 value: Box::new(new_sketch),
2294 })
2295}
2296
2297#[stdlib {
2329 name = "subtract2d",
2330 feature_tree_operation = true,
2331 keywords = true,
2332 unlabeled_first = true,
2333 args = {
2334 sketch = { docs = "Which sketch should this path be added to?" },
2335 tool = { docs = "The shape(s) which should be cut out of the sketch." },
2336 },
2337 tags = ["sketch"]
2338}]
2339async fn inner_subtract_2d(
2340 sketch: Sketch,
2341 tool: Vec<Sketch>,
2342 exec_state: &mut ExecState,
2343 args: Args,
2344) -> Result<Sketch, KclError> {
2345 for hole_sketch in tool {
2346 args.batch_modeling_cmd(
2347 exec_state.next_uuid(),
2348 ModelingCmd::from(mcmd::Solid2dAddHole {
2349 object_id: sketch.id,
2350 hole_id: hole_sketch.id,
2351 }),
2352 )
2353 .await?;
2354
2355 args.batch_modeling_cmd(
2358 exec_state.next_uuid(),
2359 ModelingCmd::from(mcmd::ObjectVisible {
2360 object_id: hole_sketch.id,
2361 hidden: true,
2362 }),
2363 )
2364 .await?;
2365 }
2366
2367 Ok(sketch)
2368}
2369
2370#[cfg(test)]
2371mod tests {
2372
2373 use pretty_assertions::assert_eq;
2374
2375 use crate::{
2376 execution::TagIdentifier,
2377 std::{sketch::PlaneData, utils::calculate_circle_center},
2378 };
2379
2380 #[test]
2381 fn test_deserialize_plane_data() {
2382 let data = PlaneData::XY;
2383 let mut str_json = serde_json::to_string(&data).unwrap();
2384 assert_eq!(str_json, "\"XY\"");
2385
2386 str_json = "\"YZ\"".to_string();
2387 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2388 assert_eq!(data, PlaneData::YZ);
2389
2390 str_json = "\"-YZ\"".to_string();
2391 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2392 assert_eq!(data, PlaneData::NegYZ);
2393
2394 str_json = "\"-xz\"".to_string();
2395 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2396 assert_eq!(data, PlaneData::NegXZ);
2397 }
2398
2399 #[test]
2400 fn test_deserialize_sketch_on_face_tag() {
2401 let data = "start";
2402 let mut str_json = serde_json::to_string(&data).unwrap();
2403 assert_eq!(str_json, "\"start\"");
2404
2405 str_json = "\"end\"".to_string();
2406 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2407 assert_eq!(
2408 data,
2409 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2410 );
2411
2412 str_json = serde_json::to_string(&TagIdentifier {
2413 value: "thing".to_string(),
2414 info: Vec::new(),
2415 meta: Default::default(),
2416 })
2417 .unwrap();
2418 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2419 assert_eq!(
2420 data,
2421 crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2422 value: "thing".to_string(),
2423 info: Vec::new(),
2424 meta: Default::default()
2425 }))
2426 );
2427
2428 str_json = "\"END\"".to_string();
2429 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2430 assert_eq!(
2431 data,
2432 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2433 );
2434
2435 str_json = "\"start\"".to_string();
2436 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2437 assert_eq!(
2438 data,
2439 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2440 );
2441
2442 str_json = "\"START\"".to_string();
2443 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2444 assert_eq!(
2445 data,
2446 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2447 );
2448 }
2449
2450 #[test]
2451 fn test_circle_center() {
2452 let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2453 assert_eq!(actual[0], 5.0);
2454 assert_eq!(actual[1], 0.0);
2455 }
2456}