1use anyhow::Result;
4use indexmap::IndexMap;
5use kcl_derive_docs::stdlib;
6use kcmc::shared::Point2d as KPoint2d; use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, websocket::ModelingCmdReq, ModelingCmd};
8use kittycad_modeling_cmds as kcmc;
9use kittycad_modeling_cmds::shared::PathSegment;
10use parse_display::{Display, FromStr};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15 errors::{KclError, KclErrorDetails},
16 execution::{
17 kcl_value::RuntimeType, Artifact, ArtifactId, BasePath, CodeRef, ExecState, Face, GeoMeta, KclValue, Path,
18 Plane, Point2d, Point3d, PrimitiveType, Sketch, SketchSurface, Solid, StartSketchOnFace, StartSketchOnPlane,
19 TagEngineInfo, TagIdentifier,
20 },
21 parsing::ast::types::TagNode,
22 std::{
23 args::{Args, TyF64},
24 utils::{
25 arc_angles, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
26 intersection_with_parallel_line, TangentialArcInfoInput,
27 },
28 },
29};
30
31#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
33#[ts(export)]
34#[serde(rename_all = "snake_case", untagged)]
35pub enum FaceTag {
36 StartOrEnd(StartOrEnd),
37 Tag(Box<TagIdentifier>),
39}
40
41impl std::fmt::Display for FaceTag {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 FaceTag::Tag(t) => write!(f, "{}", t),
45 FaceTag::StartOrEnd(StartOrEnd::Start) => write!(f, "start"),
46 FaceTag::StartOrEnd(StartOrEnd::End) => write!(f, "end"),
47 }
48 }
49}
50
51impl FaceTag {
52 pub async fn get_face_id(
54 &self,
55 solid: &Solid,
56 exec_state: &mut ExecState,
57 args: &Args,
58 must_be_planar: bool,
59 ) -> Result<uuid::Uuid, KclError> {
60 match self {
61 FaceTag::Tag(ref t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
62 FaceTag::StartOrEnd(StartOrEnd::Start) => solid.start_cap_id.ok_or_else(|| {
63 KclError::Type(KclErrorDetails {
64 message: "Expected a start face".to_string(),
65 source_ranges: vec![args.source_range],
66 })
67 }),
68 FaceTag::StartOrEnd(StartOrEnd::End) => solid.end_cap_id.ok_or_else(|| {
69 KclError::Type(KclErrorDetails {
70 message: "Expected an end face".to_string(),
71 source_ranges: vec![args.source_range],
72 })
73 }),
74 }
75 }
76}
77
78#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
79#[ts(export)]
80#[serde(rename_all = "snake_case")]
81#[display(style = "snake_case")]
82pub enum StartOrEnd {
83 #[serde(rename = "start", alias = "START")]
87 Start,
88 #[serde(rename = "end", alias = "END")]
92 End,
93}
94
95pub const NEW_TAG_KW: &str = "tag";
96
97pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
99 let sketch =
101 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
102 let end = args.get_kw_arg_opt("end")?;
103 let end_absolute = args.get_kw_arg_opt("endAbsolute")?;
104 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
105
106 let new_sketch = inner_line(sketch, end_absolute, end, tag, exec_state, args).await?;
107 Ok(KclValue::Sketch {
108 value: Box::new(new_sketch),
109 })
110}
111
112#[stdlib {
137 name = "line",
138 keywords = true,
139 unlabeled_first = true,
140 args = {
141 sketch = { docs = "Which sketch should this path be added to?"},
142 end_absolute = { docs = "Which absolute point should this line go to? Incompatible with `end`."},
143 end = { docs = "How far away (along the X and Y axes) should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
144 tag = { docs = "Create a new tag which refers to this line"},
145 }
146}]
147async fn inner_line(
148 sketch: Sketch,
149 end_absolute: Option<[f64; 2]>,
150 end: Option<[f64; 2]>,
151 tag: Option<TagNode>,
152 exec_state: &mut ExecState,
153 args: Args,
154) -> Result<Sketch, KclError> {
155 straight_line(
156 StraightLineParams {
157 sketch,
158 end_absolute,
159 end,
160 tag,
161 },
162 exec_state,
163 args,
164 )
165 .await
166}
167
168struct StraightLineParams {
169 sketch: Sketch,
170 end_absolute: Option<[f64; 2]>,
171 end: Option<[f64; 2]>,
172 tag: Option<TagNode>,
173}
174
175impl StraightLineParams {
176 fn relative(p: [f64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
177 Self {
178 sketch,
179 tag,
180 end: Some(p),
181 end_absolute: None,
182 }
183 }
184 fn absolute(p: [f64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
185 Self {
186 sketch,
187 tag,
188 end: None,
189 end_absolute: Some(p),
190 }
191 }
192}
193
194async fn straight_line(
195 StraightLineParams {
196 sketch,
197 end,
198 end_absolute,
199 tag,
200 }: StraightLineParams,
201 exec_state: &mut ExecState,
202 args: Args,
203) -> Result<Sketch, KclError> {
204 let from = sketch.current_pen_position()?;
205 let (point, is_absolute) = match (end_absolute, end) {
206 (Some(_), Some(_)) => {
207 return Err(KclError::Semantic(KclErrorDetails {
208 source_ranges: vec![args.source_range],
209 message: "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other"
210 .to_owned(),
211 }));
212 }
213 (Some(end_absolute), None) => (end_absolute, true),
214 (None, Some(end)) => (end, false),
215 (None, None) => {
216 return Err(KclError::Semantic(KclErrorDetails {
217 source_ranges: vec![args.source_range],
218 message: "You must supply either `end` or `endAbsolute` arguments".to_owned(),
219 }));
220 }
221 };
222
223 let id = exec_state.next_uuid();
224 args.batch_modeling_cmd(
225 id,
226 ModelingCmd::from(mcmd::ExtendPath {
227 path: sketch.id.into(),
228 segment: PathSegment::Line {
229 end: KPoint2d::from(point).with_z(0.0).map(LengthUnit),
230 relative: !is_absolute,
231 },
232 }),
233 )
234 .await?;
235
236 let end = if is_absolute {
237 point
238 } else {
239 let from = sketch.current_pen_position()?;
240 [from.x + point[0], from.y + point[1]]
241 };
242
243 let current_path = Path::ToPoint {
244 base: BasePath {
245 from: from.into(),
246 to: end,
247 tag: tag.clone(),
248 units: sketch.units,
249 geo_meta: GeoMeta {
250 id,
251 metadata: args.source_range.into(),
252 },
253 },
254 };
255
256 let mut new_sketch = sketch.clone();
257 if let Some(tag) = &tag {
258 new_sketch.add_tag(tag, ¤t_path, exec_state);
259 }
260
261 new_sketch.paths.push(current_path);
262
263 Ok(new_sketch)
264}
265
266pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
268 let sketch =
269 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
270 let length = args.get_kw_arg_opt("length")?;
271 let end_absolute = args.get_kw_arg_opt("endAbsolute")?;
272 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
273
274 let new_sketch = inner_x_line(sketch, length, end_absolute, tag, exec_state, args).await?;
275 Ok(KclValue::Sketch {
276 value: Box::new(new_sketch),
277 })
278}
279
280#[stdlib {
303 name = "xLine",
304 keywords = true,
305 unlabeled_first = true,
306 args = {
307 sketch = { docs = "Which sketch should this path be added to?"},
308 length = { docs = "How far away along the X axis should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
309 end_absolute = { docs = "Which absolute X value should this line go to? Incompatible with `length`."},
310 tag = { docs = "Create a new tag which refers to this line"},
311 }
312}]
313async fn inner_x_line(
314 sketch: Sketch,
315 length: Option<f64>,
316 end_absolute: Option<f64>,
317 tag: Option<TagNode>,
318 exec_state: &mut ExecState,
319 args: Args,
320) -> Result<Sketch, KclError> {
321 let from = sketch.current_pen_position()?;
322 straight_line(
323 StraightLineParams {
324 sketch,
325 end_absolute: end_absolute.map(|x| [x, from.y]),
326 end: length.map(|x| [x, 0.0]),
327 tag,
328 },
329 exec_state,
330 args,
331 )
332 .await
333}
334
335pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
337 let sketch =
338 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
339 let length = args.get_kw_arg_opt("length")?;
340 let end_absolute = args.get_kw_arg_opt("endAbsolute")?;
341 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
342
343 let new_sketch = inner_y_line(sketch, length, end_absolute, tag, exec_state, args).await?;
344 Ok(KclValue::Sketch {
345 value: Box::new(new_sketch),
346 })
347}
348
349#[stdlib {
367 name = "yLine",
368 keywords = true,
369 unlabeled_first = true,
370 args = {
371 sketch = { docs = "Which sketch should this path be added to?"},
372 length = { docs = "How far away along the Y axis should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
373 end_absolute = { docs = "Which absolute Y value should this line go to? Incompatible with `length`."},
374 tag = { docs = "Create a new tag which refers to this line"},
375 }
376}]
377async fn inner_y_line(
378 sketch: Sketch,
379 length: Option<f64>,
380 end_absolute: Option<f64>,
381 tag: Option<TagNode>,
382 exec_state: &mut ExecState,
383 args: Args,
384) -> Result<Sketch, KclError> {
385 let from = sketch.current_pen_position()?;
386 straight_line(
387 StraightLineParams {
388 sketch,
389 end_absolute: end_absolute.map(|y| [from.x, y]),
390 end: length.map(|y| [0.0, y]),
391 tag,
392 },
393 exec_state,
394 args,
395 )
396 .await
397}
398
399#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
401#[ts(export)]
402#[serde(rename_all = "camelCase", untagged)]
403pub enum AngledLineData {
404 AngleAndLengthNamed {
406 angle: f64,
408 length: f64,
410 },
411 AngleAndLengthPair([f64; 2]),
413}
414
415pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
417 let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) =
418 args.get_data_and_sketch_and_tag(exec_state)?;
419
420 let new_sketch = inner_angled_line(data, sketch, tag, exec_state, args).await?;
421 Ok(KclValue::Sketch {
422 value: Box::new(new_sketch),
423 })
424}
425
426#[stdlib {
444 name = "angledLine",
445}]
446async fn inner_angled_line(
447 data: AngledLineData,
448 sketch: Sketch,
449 tag: Option<TagNode>,
450 exec_state: &mut ExecState,
451 args: Args,
452) -> Result<Sketch, KclError> {
453 let from = sketch.current_pen_position()?;
454 let (angle, length) = match data {
455 AngledLineData::AngleAndLengthNamed { angle, length } => (angle, length),
456 AngledLineData::AngleAndLengthPair(pair) => (pair[0], pair[1]),
457 };
458
459 let delta: [f64; 2] = [
461 length * f64::cos(angle.to_radians()),
462 length * f64::sin(angle.to_radians()),
463 ];
464 let relative = true;
465
466 let to: [f64; 2] = [from.x + delta[0], from.y + delta[1]];
467
468 let id = exec_state.next_uuid();
469
470 args.batch_modeling_cmd(
471 id,
472 ModelingCmd::from(mcmd::ExtendPath {
473 path: sketch.id.into(),
474 segment: PathSegment::Line {
475 end: KPoint2d::from(delta).with_z(0.0).map(LengthUnit),
476 relative,
477 },
478 }),
479 )
480 .await?;
481
482 let current_path = Path::ToPoint {
483 base: BasePath {
484 from: from.into(),
485 to,
486 tag: tag.clone(),
487 units: sketch.units,
488 geo_meta: GeoMeta {
489 id,
490 metadata: args.source_range.into(),
491 },
492 },
493 };
494
495 let mut new_sketch = sketch.clone();
496 if let Some(tag) = &tag {
497 new_sketch.add_tag(tag, ¤t_path, exec_state);
498 }
499
500 new_sketch.paths.push(current_path);
501 Ok(new_sketch)
502}
503
504pub async fn angled_line_of_x_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
506 let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) =
507 args.get_data_and_sketch_and_tag(exec_state)?;
508
509 let new_sketch = inner_angled_line_of_x_length(data, sketch, tag, exec_state, args).await?;
510 Ok(KclValue::Sketch {
511 value: Box::new(new_sketch),
512 })
513}
514
515#[stdlib {
529 name = "angledLineOfXLength",
530}]
531async fn inner_angled_line_of_x_length(
532 data: AngledLineData,
533 sketch: Sketch,
534 tag: Option<TagNode>,
535 exec_state: &mut ExecState,
536 args: Args,
537) -> Result<Sketch, KclError> {
538 let (angle, length) = match data {
539 AngledLineData::AngleAndLengthNamed { angle, length } => (angle, length),
540 AngledLineData::AngleAndLengthPair(pair) => (pair[0], pair[1]),
541 };
542
543 if angle.abs() == 270.0 {
544 return Err(KclError::Type(KclErrorDetails {
545 message: "Cannot have an x constrained angle of 270 degrees".to_string(),
546 source_ranges: vec![args.source_range],
547 }));
548 }
549
550 if angle.abs() == 90.0 {
551 return Err(KclError::Type(KclErrorDetails {
552 message: "Cannot have an x constrained angle of 90 degrees".to_string(),
553 source_ranges: vec![args.source_range],
554 }));
555 }
556
557 let to = get_y_component(Angle::from_degrees(angle), length);
558
559 let new_sketch = straight_line(StraightLineParams::relative(to.into(), sketch, tag), exec_state, args).await?;
560
561 Ok(new_sketch)
562}
563
564#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
566#[ts(export)]
567#[serde(rename_all = "camelCase")]
568pub struct AngledLineToData {
569 pub angle: f64,
571 pub to: f64,
573}
574
575pub async fn angled_line_to_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
577 let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) =
578 args.get_data_and_sketch_and_tag(exec_state)?;
579
580 let new_sketch = inner_angled_line_to_x(data, sketch, tag, exec_state, args).await?;
581 Ok(KclValue::Sketch {
582 value: Box::new(new_sketch),
583 })
584}
585
586#[stdlib {
601 name = "angledLineToX",
602}]
603async fn inner_angled_line_to_x(
604 data: AngledLineToData,
605 sketch: Sketch,
606 tag: Option<TagNode>,
607 exec_state: &mut ExecState,
608 args: Args,
609) -> Result<Sketch, KclError> {
610 let from = sketch.current_pen_position()?;
611 let AngledLineToData { angle, to: x_to } = data;
612
613 if angle.abs() == 270.0 {
614 return Err(KclError::Type(KclErrorDetails {
615 message: "Cannot have an x constrained angle of 270 degrees".to_string(),
616 source_ranges: vec![args.source_range],
617 }));
618 }
619
620 if angle.abs() == 90.0 {
621 return Err(KclError::Type(KclErrorDetails {
622 message: "Cannot have an x constrained angle of 90 degrees".to_string(),
623 source_ranges: vec![args.source_range],
624 }));
625 }
626
627 let x_component = x_to - from.x;
628 let y_component = x_component * f64::tan(angle.to_radians());
629 let y_to = from.y + y_component;
630
631 let new_sketch = straight_line(
632 StraightLineParams::absolute([x_to, y_to], sketch, tag),
633 exec_state,
634 args,
635 )
636 .await?;
637 Ok(new_sketch)
638}
639
640pub async fn angled_line_of_y_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
642 let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) =
643 args.get_data_and_sketch_and_tag(exec_state)?;
644
645 let new_sketch = inner_angled_line_of_y_length(data, sketch, tag, exec_state, args).await?;
646
647 Ok(KclValue::Sketch {
648 value: Box::new(new_sketch),
649 })
650}
651
652#[stdlib {
668 name = "angledLineOfYLength",
669}]
670async fn inner_angled_line_of_y_length(
671 data: AngledLineData,
672 sketch: Sketch,
673 tag: Option<TagNode>,
674 exec_state: &mut ExecState,
675 args: Args,
676) -> Result<Sketch, KclError> {
677 let (angle, length) = match data {
678 AngledLineData::AngleAndLengthNamed { angle, length } => (angle, length),
679 AngledLineData::AngleAndLengthPair(pair) => (pair[0], pair[1]),
680 };
681
682 if angle.abs() == 0.0 {
683 return Err(KclError::Type(KclErrorDetails {
684 message: "Cannot have a y constrained angle of 0 degrees".to_string(),
685 source_ranges: vec![args.source_range],
686 }));
687 }
688
689 if angle.abs() == 180.0 {
690 return Err(KclError::Type(KclErrorDetails {
691 message: "Cannot have a y constrained angle of 180 degrees".to_string(),
692 source_ranges: vec![args.source_range],
693 }));
694 }
695
696 let to = get_x_component(Angle::from_degrees(angle), length);
697
698 let new_sketch = straight_line(StraightLineParams::relative(to.into(), sketch, tag), exec_state, args).await?;
699
700 Ok(new_sketch)
701}
702
703pub async fn angled_line_to_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
705 let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) =
706 args.get_data_and_sketch_and_tag(exec_state)?;
707
708 let new_sketch = inner_angled_line_to_y(data, sketch, tag, exec_state, args).await?;
709 Ok(KclValue::Sketch {
710 value: Box::new(new_sketch),
711 })
712}
713
714#[stdlib {
729 name = "angledLineToY",
730}]
731async fn inner_angled_line_to_y(
732 data: AngledLineToData,
733 sketch: Sketch,
734 tag: Option<TagNode>,
735 exec_state: &mut ExecState,
736 args: Args,
737) -> Result<Sketch, KclError> {
738 let from = sketch.current_pen_position()?;
739 let AngledLineToData { angle, to: y_to } = data;
740
741 if angle.abs() == 0.0 {
742 return Err(KclError::Type(KclErrorDetails {
743 message: "Cannot have a y constrained angle of 0 degrees".to_string(),
744 source_ranges: vec![args.source_range],
745 }));
746 }
747
748 if angle.abs() == 180.0 {
749 return Err(KclError::Type(KclErrorDetails {
750 message: "Cannot have a y constrained angle of 180 degrees".to_string(),
751 source_ranges: vec![args.source_range],
752 }));
753 }
754
755 let y_component = y_to - from.y;
756 let x_component = y_component / f64::tan(angle.to_radians());
757 let x_to = from.x + x_component;
758
759 let new_sketch = straight_line(
760 StraightLineParams::absolute([x_to, y_to], sketch, tag),
761 exec_state,
762 args,
763 )
764 .await?;
765 Ok(new_sketch)
766}
767
768#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
770#[ts(export)]
771#[serde(rename_all = "camelCase")]
772pub struct AngledLineThatIntersectsData {
774 pub angle: f64,
776 pub intersect_tag: TagIdentifier,
778 pub offset: Option<f64>,
780}
781
782pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
784 let (data, sketch, tag): (AngledLineThatIntersectsData, Sketch, Option<TagNode>) =
785 args.get_data_and_sketch_and_tag(exec_state)?;
786 let new_sketch = inner_angled_line_that_intersects(data, sketch, tag, exec_state, args).await?;
787 Ok(KclValue::Sketch {
788 value: Box::new(new_sketch),
789 })
790}
791
792#[stdlib {
812 name = "angledLineThatIntersects",
813}]
814async fn inner_angled_line_that_intersects(
815 data: AngledLineThatIntersectsData,
816 sketch: Sketch,
817 tag: Option<TagNode>,
818 exec_state: &mut ExecState,
819 args: Args,
820) -> Result<Sketch, KclError> {
821 let intersect_path = args.get_tag_engine_info(exec_state, &data.intersect_tag)?;
822 let path = intersect_path.path.clone().ok_or_else(|| {
823 KclError::Type(KclErrorDetails {
824 message: format!("Expected an intersect path with a path, found `{:?}`", intersect_path),
825 source_ranges: vec![args.source_range],
826 })
827 })?;
828
829 let from = sketch.current_pen_position()?;
830 let to = intersection_with_parallel_line(
831 &[path.get_from().into(), path.get_to().into()],
832 data.offset.unwrap_or_default(),
833 data.angle,
834 from,
835 );
836
837 let new_sketch = straight_line(StraightLineParams::absolute(to.into(), sketch, tag), exec_state, args).await?;
838 Ok(new_sketch)
839}
840
841#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
844#[ts(export)]
845#[serde(rename_all = "camelCase", untagged)]
846#[allow(clippy::large_enum_variant)]
847pub enum SketchData {
848 PlaneOrientation(PlaneData),
849 Plane(Box<Plane>),
850 Solid(Box<Solid>),
851}
852
853#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
855#[ts(export)]
856#[serde(rename_all = "camelCase")]
857#[allow(clippy::large_enum_variant)]
858pub enum PlaneData {
859 #[serde(rename = "XY", alias = "xy")]
861 XY,
862 #[serde(rename = "-XY", alias = "-xy")]
864 NegXY,
865 #[serde(rename = "XZ", alias = "xz")]
867 XZ,
868 #[serde(rename = "-XZ", alias = "-xz")]
870 NegXZ,
871 #[serde(rename = "YZ", alias = "yz")]
873 YZ,
874 #[serde(rename = "-YZ", alias = "-yz")]
876 NegYZ,
877 Plane {
879 origin: Point3d,
881 #[serde(rename = "xAxis")]
883 x_axis: Point3d,
884 #[serde(rename = "yAxis")]
886 y_axis: Point3d,
887 #[serde(rename = "zAxis")]
889 z_axis: Point3d,
890 },
891}
892
893pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
895 let (data, tag): (SketchData, Option<FaceTag>) = args.get_data_and_optional_tag()?;
896
897 match inner_start_sketch_on(data, tag, exec_state, &args).await? {
898 SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
899 SketchSurface::Face(value) => Ok(KclValue::Face { value }),
900 }
901}
902
903#[stdlib {
1080 name = "startSketchOn",
1081 feature_tree_operation = true,
1082}]
1083async fn inner_start_sketch_on(
1084 data: SketchData,
1085 tag: Option<FaceTag>,
1086 exec_state: &mut ExecState,
1087 args: &Args,
1088) -> Result<SketchSurface, KclError> {
1089 match data {
1090 SketchData::PlaneOrientation(plane_data) => {
1091 let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
1092 Ok(SketchSurface::Plane(plane))
1093 }
1094 SketchData::Plane(plane) => {
1095 if plane.value == crate::exec::PlaneType::Uninit {
1096 let plane = make_sketch_plane_from_orientation(plane.into_plane_data(), exec_state, args).await?;
1097 Ok(SketchSurface::Plane(plane))
1098 } else {
1099 let id = exec_state.next_uuid();
1101 exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
1102 id: ArtifactId::from(id),
1103 plane_id: plane.artifact_id,
1104 code_ref: CodeRef::placeholder(args.source_range),
1105 }));
1106
1107 Ok(SketchSurface::Plane(plane))
1108 }
1109 }
1110 SketchData::Solid(solid) => {
1111 let Some(tag) = tag else {
1112 return Err(KclError::Type(KclErrorDetails {
1113 message: "Expected a tag for the face to sketch on".to_string(),
1114 source_ranges: vec![args.source_range],
1115 }));
1116 };
1117 let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
1118
1119 let id = exec_state.next_uuid();
1121 exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
1122 id: ArtifactId::from(id),
1123 face_id: face.artifact_id,
1124 code_ref: CodeRef::placeholder(args.source_range),
1125 }));
1126
1127 Ok(SketchSurface::Face(face))
1128 }
1129 }
1130}
1131
1132async fn start_sketch_on_face(
1133 solid: Box<Solid>,
1134 tag: FaceTag,
1135 exec_state: &mut ExecState,
1136 args: &Args,
1137) -> Result<Box<Face>, KclError> {
1138 let extrude_plane_id = tag.get_face_id(&solid, exec_state, args, true).await?;
1139
1140 Ok(Box::new(Face {
1141 id: extrude_plane_id,
1142 artifact_id: extrude_plane_id.into(),
1143 value: tag.to_string(),
1144 x_axis: solid.sketch.on.x_axis(),
1146 y_axis: solid.sketch.on.y_axis(),
1147 z_axis: solid.sketch.on.z_axis(),
1148 units: solid.units,
1149 solid,
1150 meta: vec![args.source_range.into()],
1151 }))
1152}
1153
1154async fn make_sketch_plane_from_orientation(
1155 data: PlaneData,
1156 exec_state: &mut ExecState,
1157 args: &Args,
1158) -> Result<Box<Plane>, KclError> {
1159 let plane = Plane::from_plane_data(data.clone(), exec_state);
1160
1161 let clobber = false;
1163 let size = LengthUnit(60.0);
1164 let hide = Some(true);
1165 match data {
1166 PlaneData::XY | PlaneData::NegXY | PlaneData::XZ | PlaneData::NegXZ | PlaneData::YZ | PlaneData::NegYZ => {
1167 let x_axis = match data {
1170 PlaneData::NegXY => Point3d::new(-1.0, 0.0, 0.0),
1171 PlaneData::NegXZ => Point3d::new(-1.0, 0.0, 0.0),
1172 PlaneData::NegYZ => Point3d::new(0.0, -1.0, 0.0),
1173 _ => plane.x_axis,
1174 };
1175 args.batch_modeling_cmd(
1176 plane.id,
1177 ModelingCmd::from(mcmd::MakePlane {
1178 clobber,
1179 origin: plane.origin.into(),
1180 size,
1181 x_axis: x_axis.into(),
1182 y_axis: plane.y_axis.into(),
1183 hide,
1184 }),
1185 )
1186 .await?;
1187 }
1188 PlaneData::Plane {
1189 origin,
1190 x_axis,
1191 y_axis,
1192 z_axis: _,
1193 } => {
1194 args.batch_modeling_cmd(
1195 plane.id,
1196 ModelingCmd::from(mcmd::MakePlane {
1197 clobber,
1198 origin: origin.into(),
1199 size,
1200 x_axis: x_axis.into(),
1201 y_axis: y_axis.into(),
1202 hide,
1203 }),
1204 )
1205 .await?;
1206 }
1207 }
1208
1209 Ok(Box::new(plane))
1210}
1211
1212pub async fn start_profile_at(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1214 let (start, sketch_surface, tag): ([f64; 2], SketchSurface, Option<TagNode>) =
1215 args.get_data_and_sketch_surface()?;
1216
1217 let sketch = inner_start_profile_at(start, sketch_surface, tag, exec_state, args).await?;
1218 Ok(KclValue::Sketch {
1219 value: Box::new(sketch),
1220 })
1221}
1222
1223#[stdlib {
1258 name = "startProfileAt",
1259}]
1260pub(crate) async fn inner_start_profile_at(
1261 to: [f64; 2],
1262 sketch_surface: SketchSurface,
1263 tag: Option<TagNode>,
1264 exec_state: &mut ExecState,
1265 args: Args,
1266) -> Result<Sketch, KclError> {
1267 match &sketch_surface {
1268 SketchSurface::Face(face) => {
1269 args.flush_batch_for_solids(exec_state, &[(*face.solid).clone()])
1272 .await?;
1273 }
1274 SketchSurface::Plane(plane) if !plane.is_standard() => {
1275 args.batch_end_cmd(
1278 exec_state.next_uuid(),
1279 ModelingCmd::from(mcmd::ObjectVisible {
1280 object_id: plane.id,
1281 hidden: true,
1282 }),
1283 )
1284 .await?;
1285 }
1286 _ => {}
1287 }
1288
1289 let enable_sketch_id = exec_state.next_uuid();
1290 let path_id = exec_state.next_uuid();
1291 let move_pen_id = exec_state.next_uuid();
1292 args.batch_modeling_cmds(&[
1293 ModelingCmdReq {
1296 cmd: ModelingCmd::from(mcmd::EnableSketchMode {
1297 animated: false,
1298 ortho: false,
1299 entity_id: sketch_surface.id(),
1300 adjust_camera: false,
1301 planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
1302 Some(plane.z_axis.into())
1304 } else {
1305 None
1306 },
1307 }),
1308 cmd_id: enable_sketch_id.into(),
1309 },
1310 ModelingCmdReq {
1311 cmd: ModelingCmd::from(mcmd::StartPath::default()),
1312 cmd_id: path_id.into(),
1313 },
1314 ModelingCmdReq {
1315 cmd: ModelingCmd::from(mcmd::MovePathPen {
1316 path: path_id.into(),
1317 to: KPoint2d::from(to).with_z(0.0).map(LengthUnit),
1318 }),
1319 cmd_id: move_pen_id.into(),
1320 },
1321 ModelingCmdReq {
1322 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1323 cmd_id: exec_state.next_uuid().into(),
1324 },
1325 ])
1326 .await?;
1327
1328 let current_path = BasePath {
1329 from: to,
1330 to,
1331 tag: tag.clone(),
1332 units: sketch_surface.units(),
1333 geo_meta: GeoMeta {
1334 id: move_pen_id,
1335 metadata: args.source_range.into(),
1336 },
1337 };
1338
1339 let sketch = Sketch {
1340 id: path_id,
1341 original_id: path_id,
1342 artifact_id: path_id.into(),
1343 on: sketch_surface.clone(),
1344 paths: vec![],
1345 units: sketch_surface.units(),
1346 meta: vec![args.source_range.into()],
1347 tags: if let Some(tag) = &tag {
1348 let mut tag_identifier: TagIdentifier = tag.into();
1349 tag_identifier.info = vec![(
1350 exec_state.stack().current_epoch(),
1351 TagEngineInfo {
1352 id: current_path.geo_meta.id,
1353 sketch: path_id,
1354 path: Some(Path::Base {
1355 base: current_path.clone(),
1356 }),
1357 surface: None,
1358 },
1359 )];
1360 IndexMap::from([(tag.name.to_string(), tag_identifier)])
1361 } else {
1362 Default::default()
1363 },
1364 start: current_path,
1365 };
1366 Ok(sketch)
1367}
1368
1369pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1371 let sketch: Sketch = args.get_sketch(exec_state)?;
1372 let ty = sketch.units.into();
1373 let x = inner_profile_start_x(sketch)?;
1374 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1375}
1376
1377#[stdlib {
1388 name = "profileStartX"
1389}]
1390pub(crate) fn inner_profile_start_x(sketch: Sketch) -> Result<f64, KclError> {
1391 Ok(sketch.start.to[0])
1392}
1393
1394pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1396 let sketch: Sketch = args.get_sketch(exec_state)?;
1397 let ty = sketch.units.into();
1398 let x = inner_profile_start_y(sketch)?;
1399 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1400}
1401
1402#[stdlib {
1412 name = "profileStartY"
1413}]
1414pub(crate) fn inner_profile_start_y(sketch: Sketch) -> Result<f64, KclError> {
1415 Ok(sketch.start.to[1])
1416}
1417
1418pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1420 let sketch: Sketch = args.get_sketch(exec_state)?;
1421 let ty = sketch.units.into();
1422 let point = inner_profile_start(sketch)?;
1423 Ok(KclValue::from_point2d(point, ty, args.into()))
1424}
1425
1426#[stdlib {
1439 name = "profileStart"
1440}]
1441pub(crate) fn inner_profile_start(sketch: Sketch) -> Result<[f64; 2], KclError> {
1442 Ok(sketch.start.to)
1443}
1444
1445pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1447 let sketch =
1448 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1449 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1450 let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1451 Ok(KclValue::Sketch {
1452 value: Box::new(new_sketch),
1453 })
1454}
1455
1456#[stdlib {
1478 name = "close",
1479 keywords = true,
1480 unlabeled_first = true,
1481 args = {
1482 sketch = { docs = "The sketch you want to close"},
1483 tag = { docs = "Create a new tag which refers to this line"},
1484 }
1485}]
1486pub(crate) async fn inner_close(
1487 sketch: Sketch,
1488 tag: Option<TagNode>,
1489 exec_state: &mut ExecState,
1490 args: Args,
1491) -> Result<Sketch, KclError> {
1492 let from = sketch.current_pen_position()?;
1493 let to: Point2d = sketch.start.from.into();
1494
1495 let id = exec_state.next_uuid();
1496
1497 args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }))
1498 .await?;
1499
1500 let current_path = Path::ToPoint {
1501 base: BasePath {
1502 from: from.into(),
1503 to: to.into(),
1504 tag: tag.clone(),
1505 units: sketch.units,
1506 geo_meta: GeoMeta {
1507 id,
1508 metadata: args.source_range.into(),
1509 },
1510 },
1511 };
1512
1513 let mut new_sketch = sketch.clone();
1514 if let Some(tag) = &tag {
1515 new_sketch.add_tag(tag, ¤t_path, exec_state);
1516 }
1517
1518 new_sketch.paths.push(current_path);
1519
1520 Ok(new_sketch)
1521}
1522
1523#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1525#[ts(export)]
1526#[serde(rename_all = "camelCase", untagged)]
1527pub enum ArcData {
1528 AnglesAndRadius {
1530 #[serde(rename = "angleStart")]
1532 #[schemars(range(min = -360.0, max = 360.0))]
1533 angle_start: f64,
1534 #[serde(rename = "angleEnd")]
1536 #[schemars(range(min = -360.0, max = 360.0))]
1537 angle_end: f64,
1538 radius: f64,
1540 },
1541 CenterToRadius {
1543 center: [f64; 2],
1545 to: [f64; 2],
1547 radius: f64,
1549 },
1550}
1551
1552#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
1554#[ts(export)]
1555#[serde(rename_all = "camelCase")]
1556pub struct ArcToData {
1557 pub end: [f64; 2],
1559 pub interior: [f64; 2],
1561}
1562
1563pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1565 let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
1566
1567 let new_sketch = inner_arc(data, sketch, tag, exec_state, args).await?;
1568 Ok(KclValue::Sketch {
1569 value: Box::new(new_sketch),
1570 })
1571}
1572
1573#[stdlib {
1597 name = "arc",
1598}]
1599pub(crate) async fn inner_arc(
1600 data: ArcData,
1601 sketch: Sketch,
1602 tag: Option<TagNode>,
1603 exec_state: &mut ExecState,
1604 args: Args,
1605) -> Result<Sketch, KclError> {
1606 let from: Point2d = sketch.current_pen_position()?;
1607
1608 let (center, angle_start, angle_end, radius, end) = match &data {
1609 ArcData::AnglesAndRadius {
1610 angle_start,
1611 angle_end,
1612 radius,
1613 } => {
1614 let a_start = Angle::from_degrees(*angle_start);
1615 let a_end = Angle::from_degrees(*angle_end);
1616 let (center, end) = arc_center_and_end(from, a_start, a_end, *radius);
1617 (center, a_start, a_end, *radius, end)
1618 }
1619 ArcData::CenterToRadius { center, to, radius } => {
1620 let (angle_start, angle_end) = arc_angles(from, to.into(), center.into(), *radius, args.source_range)?;
1621 (center.into(), angle_start, angle_end, *radius, to.into())
1622 }
1623 };
1624
1625 if angle_start == angle_end {
1626 return Err(KclError::Type(KclErrorDetails {
1627 message: "Arc start and end angles must be different".to_string(),
1628 source_ranges: vec![args.source_range],
1629 }));
1630 }
1631 let ccw = angle_start < angle_end;
1632
1633 let id = exec_state.next_uuid();
1634
1635 args.batch_modeling_cmd(
1636 id,
1637 ModelingCmd::from(mcmd::ExtendPath {
1638 path: sketch.id.into(),
1639 segment: PathSegment::Arc {
1640 start: angle_start,
1641 end: angle_end,
1642 center: KPoint2d::from(center).map(LengthUnit),
1643 radius: LengthUnit(radius),
1644 relative: false,
1645 },
1646 }),
1647 )
1648 .await?;
1649
1650 let current_path = Path::Arc {
1651 base: BasePath {
1652 from: from.into(),
1653 to: end.into(),
1654 tag: tag.clone(),
1655 units: sketch.units,
1656 geo_meta: GeoMeta {
1657 id,
1658 metadata: args.source_range.into(),
1659 },
1660 },
1661 center: center.into(),
1662 radius,
1663 ccw,
1664 };
1665
1666 let mut new_sketch = sketch.clone();
1667 if let Some(tag) = &tag {
1668 new_sketch.add_tag(tag, ¤t_path, exec_state);
1669 }
1670
1671 new_sketch.paths.push(current_path);
1672
1673 Ok(new_sketch)
1674}
1675
1676pub async fn arc_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1678 let (data, sketch, tag): (ArcToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
1679
1680 let new_sketch = inner_arc_to(data, sketch, tag, exec_state, args).await?;
1681 Ok(KclValue::Sketch {
1682 value: Box::new(new_sketch),
1683 })
1684}
1685
1686#[stdlib {
1703 name = "arcTo",
1704}]
1705pub(crate) async fn inner_arc_to(
1706 data: ArcToData,
1707 sketch: Sketch,
1708 tag: Option<TagNode>,
1709 exec_state: &mut ExecState,
1710 args: Args,
1711) -> Result<Sketch, KclError> {
1712 let from: Point2d = sketch.current_pen_position()?;
1713 let id = exec_state.next_uuid();
1714
1715 args.batch_modeling_cmd(
1717 id,
1718 ModelingCmd::from(mcmd::ExtendPath {
1719 path: sketch.id.into(),
1720 segment: PathSegment::ArcTo {
1721 end: kcmc::shared::Point3d {
1722 x: LengthUnit(data.end[0]),
1723 y: LengthUnit(data.end[1]),
1724 z: LengthUnit(0.0),
1725 },
1726 interior: kcmc::shared::Point3d {
1727 x: LengthUnit(data.interior[0]),
1728 y: LengthUnit(data.interior[1]),
1729 z: LengthUnit(0.0),
1730 },
1731 relative: false,
1732 },
1733 }),
1734 )
1735 .await?;
1736
1737 let start = [from.x, from.y];
1738 let interior = data.interior;
1739 let end = data.end;
1740
1741 let current_path = Path::ArcThreePoint {
1742 base: BasePath {
1743 from: from.into(),
1744 to: data.end,
1745 tag: tag.clone(),
1746 units: sketch.units,
1747 geo_meta: GeoMeta {
1748 id,
1749 metadata: args.source_range.into(),
1750 },
1751 },
1752 p1: start,
1753 p2: interior,
1754 p3: end,
1755 };
1756
1757 let mut new_sketch = sketch.clone();
1758 if let Some(tag) = &tag {
1759 new_sketch.add_tag(tag, ¤t_path, exec_state);
1760 }
1761
1762 new_sketch.paths.push(current_path);
1763
1764 Ok(new_sketch)
1765}
1766
1767#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
1769#[ts(export)]
1770#[serde(rename_all = "camelCase", untagged)]
1771pub enum TangentialArcData {
1772 RadiusAndOffset {
1773 radius: f64,
1776 offset: f64,
1778 },
1779}
1780
1781pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1783 let (data, sketch, tag): (TangentialArcData, Sketch, Option<TagNode>) =
1784 args.get_data_and_sketch_and_tag(exec_state)?;
1785
1786 let new_sketch = inner_tangential_arc(data, sketch, tag, exec_state, args).await?;
1787 Ok(KclValue::Sketch {
1788 value: Box::new(new_sketch),
1789 })
1790}
1791
1792#[stdlib {
1816 name = "tangentialArc",
1817}]
1818async fn inner_tangential_arc(
1819 data: TangentialArcData,
1820 sketch: Sketch,
1821 tag: Option<TagNode>,
1822 exec_state: &mut ExecState,
1823 args: Args,
1824) -> Result<Sketch, KclError> {
1825 let from: Point2d = sketch.current_pen_position()?;
1826 let tangent_info = sketch.get_tangential_info_from_paths(); let tan_previous_point = tangent_info.tan_previous_point(from.into());
1829
1830 let id = exec_state.next_uuid();
1831
1832 let (center, to, ccw) = match data {
1833 TangentialArcData::RadiusAndOffset { radius, offset } => {
1834 let offset = Angle::from_degrees(offset);
1836
1837 let previous_end_tangent = Angle::from_radians(f64::atan2(
1840 from.y - tan_previous_point[1],
1841 from.x - tan_previous_point[0],
1842 ));
1843 let ccw = offset.to_degrees() > 0.0;
1846 let tangent_to_arc_start_angle = if ccw {
1847 Angle::from_degrees(-90.0)
1849 } else {
1850 Angle::from_degrees(90.0)
1852 };
1853 let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1856 let end_angle = start_angle + offset;
1857 let (center, to) = arc_center_and_end(from, start_angle, end_angle, radius);
1858
1859 args.batch_modeling_cmd(
1860 id,
1861 ModelingCmd::from(mcmd::ExtendPath {
1862 path: sketch.id.into(),
1863 segment: PathSegment::TangentialArc {
1864 radius: LengthUnit(radius),
1865 offset,
1866 },
1867 }),
1868 )
1869 .await?;
1870 (center, to.into(), ccw)
1871 }
1872 };
1873
1874 let current_path = Path::TangentialArc {
1875 ccw,
1876 center: center.into(),
1877 base: BasePath {
1878 from: from.into(),
1879 to,
1880 tag: tag.clone(),
1881 units: sketch.units,
1882 geo_meta: GeoMeta {
1883 id,
1884 metadata: args.source_range.into(),
1885 },
1886 },
1887 };
1888
1889 let mut new_sketch = sketch.clone();
1890 if let Some(tag) = &tag {
1891 new_sketch.add_tag(tag, ¤t_path, exec_state);
1892 }
1893
1894 new_sketch.paths.push(current_path);
1895
1896 Ok(new_sketch)
1897}
1898
1899fn tan_arc_to(sketch: &Sketch, to: &[f64; 2]) -> ModelingCmd {
1900 ModelingCmd::from(mcmd::ExtendPath {
1901 path: sketch.id.into(),
1902 segment: PathSegment::TangentialArcTo {
1903 angle_snap_increment: None,
1904 to: KPoint2d::from(*to).with_z(0.0).map(LengthUnit),
1905 },
1906 })
1907}
1908
1909pub async fn tangential_arc_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1911 let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
1912
1913 let new_sketch = inner_tangential_arc_to(to, sketch, tag, exec_state, args).await?;
1914 Ok(KclValue::Sketch {
1915 value: Box::new(new_sketch),
1916 })
1917}
1918
1919pub async fn tangential_arc_to_relative(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1921 let (delta, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
1922
1923 let new_sketch = inner_tangential_arc_to_relative(delta, sketch, tag, exec_state, args).await?;
1924 Ok(KclValue::Sketch {
1925 value: Box::new(new_sketch),
1926 })
1927}
1928
1929#[stdlib {
1947 name = "tangentialArcTo",
1948}]
1949async fn inner_tangential_arc_to(
1950 to: [f64; 2],
1951 sketch: Sketch,
1952 tag: Option<TagNode>,
1953 exec_state: &mut ExecState,
1954 args: Args,
1955) -> Result<Sketch, KclError> {
1956 let from: Point2d = sketch.current_pen_position()?;
1957 let tangent_info = sketch.get_tangential_info_from_paths();
1958 let tan_previous_point = tangent_info.tan_previous_point(from.into());
1959 let [to_x, to_y] = to;
1960 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1961 arc_start_point: [from.x, from.y],
1962 arc_end_point: to,
1963 tan_previous_point,
1964 obtuse: true,
1965 });
1966
1967 let delta = [to_x - from.x, to_y - from.y];
1968 let id = exec_state.next_uuid();
1969 args.batch_modeling_cmd(id, tan_arc_to(&sketch, &delta)).await?;
1970
1971 let current_path = Path::TangentialArcTo {
1972 base: BasePath {
1973 from: from.into(),
1974 to,
1975 tag: tag.clone(),
1976 units: sketch.units,
1977 geo_meta: GeoMeta {
1978 id,
1979 metadata: args.source_range.into(),
1980 },
1981 },
1982 center: result.center,
1983 ccw: result.ccw > 0,
1984 };
1985
1986 let mut new_sketch = sketch.clone();
1987 if let Some(tag) = &tag {
1988 new_sketch.add_tag(tag, ¤t_path, exec_state);
1989 }
1990
1991 new_sketch.paths.push(current_path);
1992
1993 Ok(new_sketch)
1994}
1995
1996#[stdlib {
2014 name = "tangentialArcToRelative",
2015}]
2016async fn inner_tangential_arc_to_relative(
2017 delta: [f64; 2],
2018 sketch: Sketch,
2019 tag: Option<TagNode>,
2020 exec_state: &mut ExecState,
2021 args: Args,
2022) -> Result<Sketch, KclError> {
2023 let from: Point2d = sketch.current_pen_position()?;
2024 let to = [from.x + delta[0], from.y + delta[1]];
2025 let tangent_info = sketch.get_tangential_info_from_paths();
2026 let tan_previous_point = tangent_info.tan_previous_point(from.into());
2027
2028 let [dx, dy] = delta;
2029 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
2030 arc_start_point: [from.x, from.y],
2031 arc_end_point: [from.x + dx, from.y + dy],
2032 tan_previous_point,
2033 obtuse: true,
2034 });
2035
2036 if result.center[0].is_infinite() {
2037 return Err(KclError::Semantic(KclErrorDetails {
2038 source_ranges: vec![args.source_range],
2039 message:
2040 "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
2041 .to_owned(),
2042 }));
2043 } else if result.center[1].is_infinite() {
2044 return Err(KclError::Semantic(KclErrorDetails {
2045 source_ranges: vec![args.source_range],
2046 message:
2047 "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
2048 .to_owned(),
2049 }));
2050 }
2051
2052 let id = exec_state.next_uuid();
2053 args.batch_modeling_cmd(id, tan_arc_to(&sketch, &delta)).await?;
2054
2055 let current_path = Path::TangentialArcTo {
2056 base: BasePath {
2057 from: from.into(),
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 center: result.center,
2067 ccw: result.ccw > 0,
2068 };
2069
2070 let mut new_sketch = sketch.clone();
2071 if let Some(tag) = &tag {
2072 new_sketch.add_tag(tag, ¤t_path, exec_state);
2073 }
2074
2075 new_sketch.paths.push(current_path);
2076
2077 Ok(new_sketch)
2078}
2079
2080#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
2082#[ts(export)]
2083#[serde(rename_all = "camelCase")]
2084pub struct BezierData {
2085 pub to: [f64; 2],
2087 pub control1: [f64; 2],
2089 pub control2: [f64; 2],
2091}
2092
2093pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2095 let (data, sketch, tag): (BezierData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
2096
2097 let new_sketch = inner_bezier_curve(data, sketch, tag, exec_state, args).await?;
2098 Ok(KclValue::Sketch {
2099 value: Box::new(new_sketch),
2100 })
2101}
2102
2103#[stdlib {
2122 name = "bezierCurve",
2123}]
2124async fn inner_bezier_curve(
2125 data: BezierData,
2126 sketch: Sketch,
2127 tag: Option<TagNode>,
2128 exec_state: &mut ExecState,
2129 args: Args,
2130) -> Result<Sketch, KclError> {
2131 let from = sketch.current_pen_position()?;
2132
2133 let relative = true;
2134 let delta = data.to;
2135 let to = [from.x + data.to[0], from.y + data.to[1]];
2136
2137 let id = exec_state.next_uuid();
2138
2139 args.batch_modeling_cmd(
2140 id,
2141 ModelingCmd::from(mcmd::ExtendPath {
2142 path: sketch.id.into(),
2143 segment: PathSegment::Bezier {
2144 control1: KPoint2d::from(data.control1).with_z(0.0).map(LengthUnit),
2145 control2: KPoint2d::from(data.control2).with_z(0.0).map(LengthUnit),
2146 end: KPoint2d::from(delta).with_z(0.0).map(LengthUnit),
2147 relative,
2148 },
2149 }),
2150 )
2151 .await?;
2152
2153 let current_path = Path::ToPoint {
2154 base: BasePath {
2155 from: from.into(),
2156 to,
2157 tag: tag.clone(),
2158 units: sketch.units,
2159 geo_meta: GeoMeta {
2160 id,
2161 metadata: args.source_range.into(),
2162 },
2163 },
2164 };
2165
2166 let mut new_sketch = sketch.clone();
2167 if let Some(tag) = &tag {
2168 new_sketch.add_tag(tag, ¤t_path, exec_state);
2169 }
2170
2171 new_sketch.paths.push(current_path);
2172
2173 Ok(new_sketch)
2174}
2175
2176pub async fn hole(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2178 let (hole_sketch, sketch): (Vec<Sketch>, Sketch) = args.get_sketches(exec_state)?;
2179
2180 let new_sketch = inner_hole(hole_sketch, sketch, exec_state, args).await?;
2181 Ok(KclValue::Sketch {
2182 value: Box::new(new_sketch),
2183 })
2184}
2185
2186#[stdlib {
2218 name = "hole",
2219 feature_tree_operation = true,
2220}]
2221async fn inner_hole(
2222 hole_sketch: Vec<Sketch>,
2223 sketch: Sketch,
2224 exec_state: &mut ExecState,
2225 args: Args,
2226) -> Result<Sketch, KclError> {
2227 for hole_sketch in hole_sketch {
2228 args.batch_modeling_cmd(
2229 exec_state.next_uuid(),
2230 ModelingCmd::from(mcmd::Solid2dAddHole {
2231 object_id: sketch.id,
2232 hole_id: hole_sketch.id,
2233 }),
2234 )
2235 .await?;
2236
2237 args.batch_modeling_cmd(
2240 exec_state.next_uuid(),
2241 ModelingCmd::from(mcmd::ObjectVisible {
2242 object_id: hole_sketch.id,
2243 hidden: true,
2244 }),
2245 )
2246 .await?;
2247 }
2248
2249 Ok(sketch)
2250}
2251
2252#[cfg(test)]
2253mod tests {
2254
2255 use pretty_assertions::assert_eq;
2256
2257 use crate::{
2258 execution::TagIdentifier,
2259 std::{sketch::PlaneData, utils::calculate_circle_center},
2260 };
2261
2262 #[test]
2263 fn test_deserialize_plane_data() {
2264 let data = PlaneData::XY;
2265 let mut str_json = serde_json::to_string(&data).unwrap();
2266 assert_eq!(str_json, "\"XY\"");
2267
2268 str_json = "\"YZ\"".to_string();
2269 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2270 assert_eq!(data, PlaneData::YZ);
2271
2272 str_json = "\"-YZ\"".to_string();
2273 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2274 assert_eq!(data, PlaneData::NegYZ);
2275
2276 str_json = "\"-xz\"".to_string();
2277 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2278 assert_eq!(data, PlaneData::NegXZ);
2279 }
2280
2281 #[test]
2282 fn test_deserialize_sketch_on_face_tag() {
2283 let data = "start";
2284 let mut str_json = serde_json::to_string(&data).unwrap();
2285 assert_eq!(str_json, "\"start\"");
2286
2287 str_json = "\"end\"".to_string();
2288 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2289 assert_eq!(
2290 data,
2291 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2292 );
2293
2294 str_json = serde_json::to_string(&TagIdentifier {
2295 value: "thing".to_string(),
2296 info: Vec::new(),
2297 meta: Default::default(),
2298 })
2299 .unwrap();
2300 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2301 assert_eq!(
2302 data,
2303 crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2304 value: "thing".to_string(),
2305 info: Vec::new(),
2306 meta: Default::default()
2307 }))
2308 );
2309
2310 str_json = "\"END\"".to_string();
2311 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2312 assert_eq!(
2313 data,
2314 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2315 );
2316
2317 str_json = "\"start\"".to_string();
2318 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2319 assert_eq!(
2320 data,
2321 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2322 );
2323
2324 str_json = "\"START\"".to_string();
2325 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2326 assert_eq!(
2327 data,
2328 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2329 );
2330 }
2331
2332 #[test]
2333 fn test_circle_center() {
2334 let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2335 assert_eq!(actual[0], 5.0);
2336 assert_eq!(actual[1], 0.0);
2337 }
2338}