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