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, untyped_point_to_mm,
30 TangentialArcInfoInput,
31 },
32 },
33};
34
35use super::shapes::get_radius;
36
37#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
39#[ts(export)]
40#[serde(rename_all = "snake_case", untagged)]
41pub enum FaceTag {
42 StartOrEnd(StartOrEnd),
43 Tag(Box<TagIdentifier>),
45}
46
47impl std::fmt::Display for FaceTag {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 FaceTag::Tag(t) => write!(f, "{}", t),
51 FaceTag::StartOrEnd(StartOrEnd::Start) => write!(f, "start"),
52 FaceTag::StartOrEnd(StartOrEnd::End) => write!(f, "end"),
53 }
54 }
55}
56
57impl FaceTag {
58 pub async fn get_face_id(
60 &self,
61 solid: &Solid,
62 exec_state: &mut ExecState,
63 args: &Args,
64 must_be_planar: bool,
65 ) -> Result<uuid::Uuid, KclError> {
66 match self {
67 FaceTag::Tag(ref t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
68 FaceTag::StartOrEnd(StartOrEnd::Start) => solid.start_cap_id.ok_or_else(|| {
69 KclError::Type(KclErrorDetails::new(
70 "Expected a start face".to_string(),
71 vec![args.source_range],
72 ))
73 }),
74 FaceTag::StartOrEnd(StartOrEnd::End) => solid.end_cap_id.ok_or_else(|| {
75 KclError::Type(KclErrorDetails::new(
76 "Expected an end face".to_string(),
77 vec![args.source_range],
78 ))
79 }),
80 }
81 }
82}
83
84#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
85#[ts(export)]
86#[serde(rename_all = "snake_case")]
87#[display(style = "snake_case")]
88pub enum StartOrEnd {
89 #[serde(rename = "start", alias = "START")]
93 Start,
94 #[serde(rename = "end", alias = "END")]
98 End,
99}
100
101pub const NEW_TAG_KW: &str = "tag";
102
103pub async fn involute_circular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
104 let sketch =
105 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
106
107 let start_radius: TyF64 = args.get_kw_arg_typed("startRadius", &RuntimeType::length(), exec_state)?;
108 let end_radius: TyF64 = args.get_kw_arg_typed("endRadius", &RuntimeType::length(), exec_state)?;
109 let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::angle(), exec_state)?;
110 let reverse = args.get_kw_arg_opt_typed("reverse", &RuntimeType::bool(), exec_state)?;
111 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
112 let new_sketch =
113 inner_involute_circular(sketch, start_radius, end_radius, angle, reverse, tag, exec_state, args).await?;
114 Ok(KclValue::Sketch {
115 value: Box::new(new_sketch),
116 })
117}
118
119fn involute_curve(radius: f64, angle: f64) -> (f64, f64) {
120 (
121 radius * (angle.cos() + angle * angle.sin()),
122 radius * (angle.sin() - angle * angle.cos()),
123 )
124}
125
126#[stdlib {
137 name = "involuteCircular",
138 unlabeled_first = true,
139 args = {
140 sketch = { docs = "Which sketch should this path be added to?"},
141 start_radius = { docs = "The involute is described between two circles, start_radius is the radius of the inner circle."},
142 end_radius = { docs = "The involute is described between two circles, end_radius is the radius of the outer circle."},
143 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."},
144 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."},
145 tag = { docs = "Create a new tag which refers to this line"},
146 },
147 tags = ["sketch"]
148}]
149#[allow(clippy::too_many_arguments)]
150async fn inner_involute_circular(
151 sketch: Sketch,
152 start_radius: TyF64,
153 end_radius: TyF64,
154 angle: TyF64,
155 reverse: Option<bool>,
156 tag: Option<TagNode>,
157 exec_state: &mut ExecState,
158 args: Args,
159) -> Result<Sketch, KclError> {
160 let id = exec_state.next_uuid();
161
162 args.batch_modeling_cmd(
163 id,
164 ModelingCmd::from(mcmd::ExtendPath {
165 path: sketch.id.into(),
166 segment: PathSegment::CircularInvolute {
167 start_radius: LengthUnit(start_radius.to_mm()),
168 end_radius: LengthUnit(end_radius.to_mm()),
169 angle: Angle::from_degrees(angle.to_degrees()),
170 reverse: reverse.unwrap_or_default(),
171 },
172 }),
173 )
174 .await?;
175
176 let from = sketch.current_pen_position()?;
177
178 let start_radius = start_radius.to_length_units(from.units);
179 let end_radius = end_radius.to_length_units(from.units);
180
181 let mut end: KPoint3d<f64> = Default::default(); let theta = f64::sqrt(end_radius * end_radius - start_radius * start_radius) / start_radius;
183 let (x, y) = involute_curve(start_radius, theta);
184
185 end.x = x * angle.to_radians().cos() - y * angle.to_radians().sin();
186 end.y = x * angle.to_radians().sin() + y * angle.to_radians().cos();
187
188 end.x -= start_radius * angle.to_radians().cos();
189 end.y -= start_radius * angle.to_radians().sin();
190
191 if reverse.unwrap_or_default() {
192 end.x = -end.x;
193 }
194
195 end.x += from.x;
196 end.y += from.y;
197
198 let current_path = Path::ToPoint {
199 base: BasePath {
200 from: from.ignore_units(),
201 to: [end.x, end.y],
202 tag: tag.clone(),
203 units: sketch.units,
204 geo_meta: GeoMeta {
205 id,
206 metadata: args.source_range.into(),
207 },
208 },
209 };
210
211 let mut new_sketch = sketch.clone();
212 if let Some(tag) = &tag {
213 new_sketch.add_tag(tag, ¤t_path, exec_state);
214 }
215 new_sketch.paths.push(current_path);
216 Ok(new_sketch)
217}
218
219pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
221 let sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
222 let end = args.get_kw_arg_opt_typed("end", &RuntimeType::point2d(), exec_state)?;
223 let end_absolute = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
224 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
225
226 let new_sketch = inner_line(sketch, end_absolute, end, tag, exec_state, args).await?;
227 Ok(KclValue::Sketch {
228 value: Box::new(new_sketch),
229 })
230}
231
232#[stdlib {
257 name = "line",
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 unlabeled_first = true,
430 args = {
431 sketch = { docs = "Which sketch should this path be added to?"},
432 length = { docs = "How far away along the X axis should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
433 end_absolute = { docs = "Which absolute X value should this line go to? Incompatible with `length`."},
434 tag = { docs = "Create a new tag which refers to this line"},
435 },
436 tags = ["sketch"]
437}]
438async fn inner_x_line(
439 sketch: Sketch,
440 length: Option<TyF64>,
441 end_absolute: Option<TyF64>,
442 tag: Option<TagNode>,
443 exec_state: &mut ExecState,
444 args: Args,
445) -> Result<Sketch, KclError> {
446 let from = sketch.current_pen_position()?;
447 straight_line(
448 StraightLineParams {
449 sketch,
450 end_absolute: end_absolute.map(|x| [x, from.into_y()]),
451 end: length.map(|x| [x, TyF64::new(0.0, NumericType::mm())]),
452 tag,
453 relative_name: "length",
454 },
455 exec_state,
456 args,
457 )
458 .await
459}
460
461pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
463 let sketch =
464 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
465 let length: Option<TyF64> = args.get_kw_arg_opt_typed("length", &RuntimeType::length(), exec_state)?;
466 let end_absolute: Option<TyF64> = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::length(), exec_state)?;
467 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
468
469 let new_sketch = inner_y_line(sketch, length, end_absolute, tag, exec_state, args).await?;
470 Ok(KclValue::Sketch {
471 value: Box::new(new_sketch),
472 })
473}
474
475#[stdlib {
493 name = "yLine",
494 unlabeled_first = true,
495 args = {
496 sketch = { docs = "Which sketch should this path be added to?"},
497 length = { docs = "How far away along the Y axis should this line go? Incompatible with `endAbsolute`.", include_in_snippet = true},
498 end_absolute = { docs = "Which absolute Y value should this line go to? Incompatible with `length`."},
499 tag = { docs = "Create a new tag which refers to this line"},
500 },
501 tags = ["sketch"]
502}]
503async fn inner_y_line(
504 sketch: Sketch,
505 length: Option<TyF64>,
506 end_absolute: Option<TyF64>,
507 tag: Option<TagNode>,
508 exec_state: &mut ExecState,
509 args: Args,
510) -> Result<Sketch, KclError> {
511 let from = sketch.current_pen_position()?;
512 straight_line(
513 StraightLineParams {
514 sketch,
515 end_absolute: end_absolute.map(|y| [from.into_x(), y]),
516 end: length.map(|y| [TyF64::new(0.0, NumericType::mm()), y]),
517 tag,
518 relative_name: "length",
519 },
520 exec_state,
521 args,
522 )
523 .await
524}
525
526pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
528 let sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
529 let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::degrees(), exec_state)?;
530 let length: Option<TyF64> = args.get_kw_arg_opt_typed("length", &RuntimeType::length(), exec_state)?;
531 let length_x: Option<TyF64> = args.get_kw_arg_opt_typed("lengthX", &RuntimeType::length(), exec_state)?;
532 let length_y: Option<TyF64> = args.get_kw_arg_opt_typed("lengthY", &RuntimeType::length(), exec_state)?;
533 let end_absolute_x: Option<TyF64> =
534 args.get_kw_arg_opt_typed("endAbsoluteX", &RuntimeType::length(), exec_state)?;
535 let end_absolute_y: Option<TyF64> =
536 args.get_kw_arg_opt_typed("endAbsoluteY", &RuntimeType::length(), exec_state)?;
537 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
538
539 let new_sketch = inner_angled_line(
540 sketch,
541 angle.n,
542 length,
543 length_x,
544 length_y,
545 end_absolute_x,
546 end_absolute_y,
547 tag,
548 exec_state,
549 args,
550 )
551 .await?;
552 Ok(KclValue::Sketch {
553 value: Box::new(new_sketch),
554 })
555}
556
557#[stdlib {
575 name = "angledLine",
576 unlabeled_first = true,
577 args = {
578 sketch = { docs = "Which sketch should this path be added to?"},
579 angle = { docs = "Which angle should the line be drawn at?" },
580 length = { docs = "Draw the line this distance along the given angle. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
581 length_x = { docs = "Draw the line this distance along the X axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
582 length_y = { docs = "Draw the line this distance along the Y axis. Only one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given."},
583 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."},
584 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."},
585 tag = { docs = "Create a new tag which refers to this line"},
586 },
587 tags = ["sketch"]
588}]
589#[allow(clippy::too_many_arguments)]
590async fn inner_angled_line(
591 sketch: Sketch,
592 angle: f64,
593 length: Option<TyF64>,
594 length_x: Option<TyF64>,
595 length_y: Option<TyF64>,
596 end_absolute_x: Option<TyF64>,
597 end_absolute_y: Option<TyF64>,
598 tag: Option<TagNode>,
599 exec_state: &mut ExecState,
600 args: Args,
601) -> Result<Sketch, KclError> {
602 let options_given = [&length, &length_x, &length_y, &end_absolute_x, &end_absolute_y]
603 .iter()
604 .filter(|x| x.is_some())
605 .count();
606 if options_given > 1 {
607 return Err(KclError::Type(KclErrorDetails::new(
608 " one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_string(),
609 vec![args.source_range],
610 )));
611 }
612 if let Some(length_x) = length_x {
613 return inner_angled_line_of_x_length(angle, length_x, sketch, tag, exec_state, args).await;
614 }
615 if let Some(length_y) = length_y {
616 return inner_angled_line_of_y_length(angle, length_y, sketch, tag, exec_state, args).await;
617 }
618 let angle_degrees = angle;
619 match (length, length_x, length_y, end_absolute_x, end_absolute_y) {
620 (Some(length), None, None, None, None) => {
621 inner_angled_line_length(sketch, angle_degrees, length, tag, exec_state, args).await
622 }
623 (None, Some(length_x), None, None, None) => {
624 inner_angled_line_of_x_length(angle_degrees, length_x, sketch, tag, exec_state, args).await
625 }
626 (None, None, Some(length_y), None, None) => {
627 inner_angled_line_of_y_length(angle_degrees, length_y, sketch, tag, exec_state, args).await
628 }
629 (None, None, None, Some(end_absolute_x), None) => {
630 inner_angled_line_to_x(angle_degrees, end_absolute_x, sketch, tag, exec_state, args).await
631 }
632 (None, None, None, None, Some(end_absolute_y)) => {
633 inner_angled_line_to_y(angle_degrees, end_absolute_y, sketch, tag, exec_state, args).await
634 }
635 (None, None, None, None, None) => Err(KclError::Type(KclErrorDetails::new(
636 "One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` must be given".to_string(),
637 vec![args.source_range],
638 ))),
639 _ => Err(KclError::Type(KclErrorDetails::new(
640 "Only One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_owned(),
641 vec![args.source_range],
642 ))),
643 }
644}
645
646async fn inner_angled_line_length(
647 sketch: Sketch,
648 angle_degrees: f64,
649 length: TyF64,
650 tag: Option<TagNode>,
651 exec_state: &mut ExecState,
652 args: Args,
653) -> Result<Sketch, KclError> {
654 let from = sketch.current_pen_position()?;
655 let length = length.to_length_units(from.units);
656
657 let delta: [f64; 2] = [
659 length * f64::cos(angle_degrees.to_radians()),
660 length * f64::sin(angle_degrees.to_radians()),
661 ];
662 let relative = true;
663
664 let to: [f64; 2] = [from.x + delta[0], from.y + delta[1]];
665
666 let id = exec_state.next_uuid();
667
668 args.batch_modeling_cmd(
669 id,
670 ModelingCmd::from(mcmd::ExtendPath {
671 path: sketch.id.into(),
672 segment: PathSegment::Line {
673 end: KPoint2d::from(untyped_point_to_mm(delta, from.units))
674 .with_z(0.0)
675 .map(LengthUnit),
676 relative,
677 },
678 }),
679 )
680 .await?;
681
682 let current_path = Path::ToPoint {
683 base: BasePath {
684 from: from.ignore_units(),
685 to,
686 tag: tag.clone(),
687 units: sketch.units,
688 geo_meta: GeoMeta {
689 id,
690 metadata: args.source_range.into(),
691 },
692 },
693 };
694
695 let mut new_sketch = sketch.clone();
696 if let Some(tag) = &tag {
697 new_sketch.add_tag(tag, ¤t_path, exec_state);
698 }
699
700 new_sketch.paths.push(current_path);
701 Ok(new_sketch)
702}
703
704async fn inner_angled_line_of_x_length(
705 angle_degrees: f64,
706 length: TyF64,
707 sketch: Sketch,
708 tag: Option<TagNode>,
709 exec_state: &mut ExecState,
710 args: Args,
711) -> Result<Sketch, KclError> {
712 if angle_degrees.abs() == 270.0 {
713 return Err(KclError::Type(KclErrorDetails::new(
714 "Cannot have an x constrained angle of 270 degrees".to_string(),
715 vec![args.source_range],
716 )));
717 }
718
719 if angle_degrees.abs() == 90.0 {
720 return Err(KclError::Type(KclErrorDetails::new(
721 "Cannot have an x constrained angle of 90 degrees".to_string(),
722 vec![args.source_range],
723 )));
724 }
725
726 let to = get_y_component(Angle::from_degrees(angle_degrees), length.n);
727 let to = [TyF64::new(to[0], length.ty.clone()), TyF64::new(to[1], length.ty)];
728
729 let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
730
731 Ok(new_sketch)
732}
733
734async fn inner_angled_line_to_x(
735 angle_degrees: f64,
736 x_to: TyF64,
737 sketch: Sketch,
738 tag: Option<TagNode>,
739 exec_state: &mut ExecState,
740 args: Args,
741) -> Result<Sketch, KclError> {
742 let from = sketch.current_pen_position()?;
743
744 if angle_degrees.abs() == 270.0 {
745 return Err(KclError::Type(KclErrorDetails::new(
746 "Cannot have an x constrained angle of 270 degrees".to_string(),
747 vec![args.source_range],
748 )));
749 }
750
751 if angle_degrees.abs() == 90.0 {
752 return Err(KclError::Type(KclErrorDetails::new(
753 "Cannot have an x constrained angle of 90 degrees".to_string(),
754 vec![args.source_range],
755 )));
756 }
757
758 let x_component = x_to.to_length_units(from.units) - from.x;
759 let y_component = x_component * f64::tan(angle_degrees.to_radians());
760 let y_to = from.y + y_component;
761
762 let new_sketch = straight_line(
763 StraightLineParams::absolute([x_to, TyF64::new(y_to, from.units.into())], sketch, tag),
764 exec_state,
765 args,
766 )
767 .await?;
768 Ok(new_sketch)
769}
770
771async fn inner_angled_line_of_y_length(
772 angle_degrees: f64,
773 length: TyF64,
774 sketch: Sketch,
775 tag: Option<TagNode>,
776 exec_state: &mut ExecState,
777 args: Args,
778) -> Result<Sketch, KclError> {
779 if angle_degrees.abs() == 0.0 {
780 return Err(KclError::Type(KclErrorDetails::new(
781 "Cannot have a y constrained angle of 0 degrees".to_string(),
782 vec![args.source_range],
783 )));
784 }
785
786 if angle_degrees.abs() == 180.0 {
787 return Err(KclError::Type(KclErrorDetails::new(
788 "Cannot have a y constrained angle of 180 degrees".to_string(),
789 vec![args.source_range],
790 )));
791 }
792
793 let to = get_x_component(Angle::from_degrees(angle_degrees), length.n);
794 let to = [TyF64::new(to[0], length.ty.clone()), TyF64::new(to[1], length.ty)];
795
796 let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
797
798 Ok(new_sketch)
799}
800
801async fn inner_angled_line_to_y(
802 angle_degrees: f64,
803 y_to: TyF64,
804 sketch: Sketch,
805 tag: Option<TagNode>,
806 exec_state: &mut ExecState,
807 args: Args,
808) -> Result<Sketch, KclError> {
809 let from = sketch.current_pen_position()?;
810
811 if angle_degrees.abs() == 0.0 {
812 return Err(KclError::Type(KclErrorDetails::new(
813 "Cannot have a y constrained angle of 0 degrees".to_string(),
814 vec![args.source_range],
815 )));
816 }
817
818 if angle_degrees.abs() == 180.0 {
819 return Err(KclError::Type(KclErrorDetails::new(
820 "Cannot have a y constrained angle of 180 degrees".to_string(),
821 vec![args.source_range],
822 )));
823 }
824
825 let y_component = y_to.to_length_units(from.units) - from.y;
826 let x_component = y_component / f64::tan(angle_degrees.to_radians());
827 let x_to = from.x + x_component;
828
829 let new_sketch = straight_line(
830 StraightLineParams::absolute([TyF64::new(x_to, from.units.into()), y_to], sketch, tag),
831 exec_state,
832 args,
833 )
834 .await?;
835 Ok(new_sketch)
836}
837
838pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
840 let sketch =
841 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
842 let angle: TyF64 = args.get_kw_arg_typed("angle", &RuntimeType::angle(), exec_state)?;
843 let intersect_tag: TagIdentifier =
844 args.get_kw_arg_typed("intersectTag", &RuntimeType::tag_identifier(), exec_state)?;
845 let offset = args.get_kw_arg_opt_typed("offset", &RuntimeType::length(), exec_state)?;
846 let tag: Option<TagNode> = args.get_kw_arg_opt("tag")?;
847 let new_sketch =
848 inner_angled_line_that_intersects(sketch, angle, intersect_tag, offset, tag, exec_state, args).await?;
849 Ok(KclValue::Sketch {
850 value: Box::new(new_sketch),
851 })
852}
853
854#[stdlib {
874 name = "angledLineThatIntersects",
875 unlabeled_first = true,
876 args = {
877 sketch = { docs = "Which sketch should this path be added to?"},
878 angle = { docs = "Which angle should the line be drawn at?" },
879 intersect_tag = { docs = "The tag of the line to intersect with" },
880 offset = { docs = "The offset from the intersecting line. Defaults to 0." },
881 tag = { docs = "Create a new tag which refers to this line"},
882 },
883 tags = ["sketch"]
884}]
885pub async fn inner_angled_line_that_intersects(
886 sketch: Sketch,
887 angle: TyF64,
888 intersect_tag: TagIdentifier,
889 offset: Option<TyF64>,
890 tag: Option<TagNode>,
891 exec_state: &mut ExecState,
892 args: Args,
893) -> Result<Sketch, KclError> {
894 let intersect_path = args.get_tag_engine_info(exec_state, &intersect_tag)?;
895 let path = intersect_path.path.clone().ok_or_else(|| {
896 KclError::Type(KclErrorDetails::new(
897 format!("Expected an intersect path with a path, found `{:?}`", intersect_path),
898 vec![args.source_range],
899 ))
900 })?;
901
902 let from = sketch.current_pen_position()?;
903 let to = intersection_with_parallel_line(
904 &[
905 point_to_len_unit(path.get_from(), from.units),
906 point_to_len_unit(path.get_to(), from.units),
907 ],
908 offset.map(|t| t.to_length_units(from.units)).unwrap_or_default(),
909 angle.to_degrees(),
910 from.ignore_units(),
911 );
912 let to = [
913 TyF64::new(to[0], from.units.into()),
914 TyF64::new(to[1], from.units.into()),
915 ];
916
917 straight_line(StraightLineParams::absolute(to, sketch, tag), exec_state, args).await
918}
919
920#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
923#[ts(export)]
924#[serde(rename_all = "camelCase", untagged)]
925#[allow(clippy::large_enum_variant)]
926pub enum SketchData {
927 PlaneOrientation(PlaneData),
928 Plane(Box<Plane>),
929 Solid(Box<Solid>),
930}
931
932#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
934#[ts(export)]
935#[serde(rename_all = "camelCase")]
936#[allow(clippy::large_enum_variant)]
937pub enum PlaneData {
938 #[serde(rename = "XY", alias = "xy")]
940 XY,
941 #[serde(rename = "-XY", alias = "-xy")]
943 NegXY,
944 #[serde(rename = "XZ", alias = "xz")]
946 XZ,
947 #[serde(rename = "-XZ", alias = "-xz")]
949 NegXZ,
950 #[serde(rename = "YZ", alias = "yz")]
952 YZ,
953 #[serde(rename = "-YZ", alias = "-yz")]
955 NegYZ,
956 Plane(PlaneInfo),
958}
959
960pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
962 let data = args.get_unlabeled_kw_arg_typed(
963 "planeOrSolid",
964 &RuntimeType::Union(vec![RuntimeType::solid(), RuntimeType::plane()]),
965 exec_state,
966 )?;
967 let face = args.get_kw_arg_opt_typed("face", &RuntimeType::tag(), exec_state)?;
968
969 match inner_start_sketch_on(data, face, exec_state, &args).await? {
970 SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
971 SketchSurface::Face(value) => Ok(KclValue::Face { value }),
972 }
973}
974
975#[stdlib {
1150 name = "startSketchOn",
1151 feature_tree_operation = true,
1152 unlabeled_first = true,
1153 args = {
1154 plane_or_solid = { docs = "The plane or solid to sketch on"},
1155 face = { docs = "Identify a face of a solid if a solid is specified as the input argument (`plane_or_solid`)"},
1156 },
1157 tags = ["sketch"]
1158}]
1159async fn inner_start_sketch_on(
1160 plane_or_solid: SketchData,
1161 face: Option<FaceTag>,
1162 exec_state: &mut ExecState,
1163 args: &Args,
1164) -> Result<SketchSurface, KclError> {
1165 match plane_or_solid {
1166 SketchData::PlaneOrientation(plane_data) => {
1167 let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
1168 Ok(SketchSurface::Plane(plane))
1169 }
1170 SketchData::Plane(plane) => {
1171 if plane.value == crate::exec::PlaneType::Uninit {
1172 if plane.info.origin.units == UnitLen::Unknown {
1173 return Err(KclError::Semantic(KclErrorDetails::new(
1174 "Origin of plane has unknown units".to_string(),
1175 vec![args.source_range],
1176 )));
1177 }
1178 let plane = make_sketch_plane_from_orientation(plane.info.into_plane_data(), exec_state, args).await?;
1179 Ok(SketchSurface::Plane(plane))
1180 } else {
1181 #[cfg(feature = "artifact-graph")]
1183 {
1184 let id = exec_state.next_uuid();
1185 exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
1186 id: ArtifactId::from(id),
1187 plane_id: plane.artifact_id,
1188 code_ref: CodeRef::placeholder(args.source_range),
1189 }));
1190 }
1191
1192 Ok(SketchSurface::Plane(plane))
1193 }
1194 }
1195 SketchData::Solid(solid) => {
1196 let Some(tag) = face else {
1197 return Err(KclError::Type(KclErrorDetails::new(
1198 "Expected a tag for the face to sketch on".to_string(),
1199 vec![args.source_range],
1200 )));
1201 };
1202 let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
1203
1204 #[cfg(feature = "artifact-graph")]
1205 {
1206 let id = exec_state.next_uuid();
1208 exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
1209 id: ArtifactId::from(id),
1210 face_id: face.artifact_id,
1211 code_ref: CodeRef::placeholder(args.source_range),
1212 }));
1213 }
1214
1215 Ok(SketchSurface::Face(face))
1216 }
1217 }
1218}
1219
1220async fn start_sketch_on_face(
1221 solid: Box<Solid>,
1222 tag: FaceTag,
1223 exec_state: &mut ExecState,
1224 args: &Args,
1225) -> Result<Box<Face>, KclError> {
1226 let extrude_plane_id = tag.get_face_id(&solid, exec_state, args, true).await?;
1227
1228 Ok(Box::new(Face {
1229 id: extrude_plane_id,
1230 artifact_id: extrude_plane_id.into(),
1231 value: tag.to_string(),
1232 x_axis: solid.sketch.on.x_axis(),
1234 y_axis: solid.sketch.on.y_axis(),
1235 units: solid.units,
1236 solid,
1237 meta: vec![args.source_range.into()],
1238 }))
1239}
1240
1241async fn make_sketch_plane_from_orientation(
1242 data: PlaneData,
1243 exec_state: &mut ExecState,
1244 args: &Args,
1245) -> Result<Box<Plane>, KclError> {
1246 let plane = Plane::from_plane_data(data.clone(), exec_state)?;
1247
1248 let clobber = false;
1250 let size = LengthUnit(60.0);
1251 let hide = Some(true);
1252 args.batch_modeling_cmd(
1253 plane.id,
1254 ModelingCmd::from(mcmd::MakePlane {
1255 clobber,
1256 origin: plane.info.origin.into(),
1257 size,
1258 x_axis: plane.info.x_axis.into(),
1259 y_axis: plane.info.y_axis.into(),
1260 hide,
1261 }),
1262 )
1263 .await?;
1264
1265 Ok(Box::new(plane))
1266}
1267
1268pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1270 let sketch_surface = args.get_unlabeled_kw_arg_typed(
1271 "startProfileOn",
1272 &RuntimeType::Union(vec![RuntimeType::plane(), RuntimeType::face()]),
1273 exec_state,
1274 )?;
1275 let start: [TyF64; 2] = args.get_kw_arg_typed("at", &RuntimeType::point2d(), exec_state)?;
1276 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1277
1278 let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, args).await?;
1279 Ok(KclValue::Sketch {
1280 value: Box::new(sketch),
1281 })
1282}
1283
1284#[stdlib {
1319 name = "startProfile",
1320 unlabeled_first = true,
1321 args = {
1322 sketch_surface = { docs = "What to start the profile on" },
1323 at = { docs = "Where to start the profile. An absolute point.", snippet_value_array = ["0", "0"] },
1324 tag = { docs = "Tag this first starting point" },
1325 },
1326 tags = ["sketch"]
1327}]
1328pub(crate) async fn inner_start_profile(
1329 sketch_surface: SketchSurface,
1330 at: [TyF64; 2],
1331 tag: Option<TagNode>,
1332 exec_state: &mut ExecState,
1333 args: Args,
1334) -> Result<Sketch, KclError> {
1335 match &sketch_surface {
1336 SketchSurface::Face(face) => {
1337 args.flush_batch_for_solids(exec_state, &[(*face.solid).clone()])
1340 .await?;
1341 }
1342 SketchSurface::Plane(plane) if !plane.is_standard() => {
1343 args.batch_end_cmd(
1346 exec_state.next_uuid(),
1347 ModelingCmd::from(mcmd::ObjectVisible {
1348 object_id: plane.id,
1349 hidden: true,
1350 }),
1351 )
1352 .await?;
1353 }
1354 _ => {}
1355 }
1356
1357 let enable_sketch_id = exec_state.next_uuid();
1358 let path_id = exec_state.next_uuid();
1359 let move_pen_id = exec_state.next_uuid();
1360 args.batch_modeling_cmds(&[
1361 ModelingCmdReq {
1364 cmd: ModelingCmd::from(mcmd::EnableSketchMode {
1365 animated: false,
1366 ortho: false,
1367 entity_id: sketch_surface.id(),
1368 adjust_camera: false,
1369 planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
1370 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
1372 Some(normal.into())
1373 } else {
1374 None
1375 },
1376 }),
1377 cmd_id: enable_sketch_id.into(),
1378 },
1379 ModelingCmdReq {
1380 cmd: ModelingCmd::from(mcmd::StartPath::default()),
1381 cmd_id: path_id.into(),
1382 },
1383 ModelingCmdReq {
1384 cmd: ModelingCmd::from(mcmd::MovePathPen {
1385 path: path_id.into(),
1386 to: KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit),
1387 }),
1388 cmd_id: move_pen_id.into(),
1389 },
1390 ModelingCmdReq {
1391 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1392 cmd_id: exec_state.next_uuid().into(),
1393 },
1394 ])
1395 .await?;
1396
1397 let units = exec_state.length_unit();
1399 let to = point_to_len_unit(at, units);
1400 let current_path = BasePath {
1401 from: to,
1402 to,
1403 tag: tag.clone(),
1404 units,
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,
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 unlabeled_first = true,
1463 args = {
1464 profile = {docs = "Profile whose start is being used"},
1465 },
1466 tags = ["sketch"]
1467}]
1468pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1469 Ok(profile.start.to[0])
1470}
1471
1472pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1474 let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
1475 let ty = sketch.units.into();
1476 let x = inner_profile_start_y(sketch)?;
1477 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1478}
1479
1480#[stdlib {
1490 name = "profileStartY",
1491 unlabeled_first = true,
1492 args = {
1493 profile = {docs = "Profile whose start is being used"},
1494 },
1495 tags = ["sketch"]
1496}]
1497pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1498 Ok(profile.start.to[1])
1499}
1500
1501pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1503 let sketch: Sketch = args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::sketch(), exec_state)?;
1504 let ty = sketch.units.into();
1505 let point = inner_profile_start(sketch)?;
1506 Ok(KclValue::from_point2d(point, ty, args.into()))
1507}
1508
1509#[stdlib {
1522 name = "profileStart",
1523 unlabeled_first = true,
1524 args = {
1525 profile = {docs = "Profile whose start is being used"},
1526 },
1527 tags = ["sketch"]
1528}]
1529pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1530 Ok(profile.start.to)
1531}
1532
1533pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1535 let sketch =
1536 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1537 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1538 let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1539 Ok(KclValue::Sketch {
1540 value: Box::new(new_sketch),
1541 })
1542}
1543
1544#[stdlib {
1566 name = "close",
1567 unlabeled_first = true,
1568 args = {
1569 sketch = { docs = "The sketch you want to close"},
1570 tag = { docs = "Create a new tag which refers to this line"},
1571 },
1572 tags = ["sketch"]
1573}]
1574pub(crate) async fn inner_close(
1575 sketch: Sketch,
1576 tag: Option<TagNode>,
1577 exec_state: &mut ExecState,
1578 args: Args,
1579) -> Result<Sketch, KclError> {
1580 let from = sketch.current_pen_position()?;
1581 let to = point_to_len_unit(sketch.start.get_from(), from.units);
1582
1583 let id = exec_state.next_uuid();
1584
1585 args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }))
1586 .await?;
1587
1588 let current_path = Path::ToPoint {
1589 base: BasePath {
1590 from: from.ignore_units(),
1591 to,
1592 tag: tag.clone(),
1593 units: sketch.units,
1594 geo_meta: GeoMeta {
1595 id,
1596 metadata: args.source_range.into(),
1597 },
1598 },
1599 };
1600
1601 let mut new_sketch = sketch.clone();
1602 if let Some(tag) = &tag {
1603 new_sketch.add_tag(tag, ¤t_path, exec_state);
1604 }
1605
1606 new_sketch.paths.push(current_path);
1607
1608 Ok(new_sketch)
1609}
1610
1611pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1613 let sketch =
1614 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1615
1616 let angle_start: Option<TyF64> = args.get_kw_arg_opt_typed("angleStart", &RuntimeType::degrees(), exec_state)?;
1617 let angle_end: Option<TyF64> = args.get_kw_arg_opt_typed("angleEnd", &RuntimeType::degrees(), exec_state)?;
1618 let radius: Option<TyF64> = args.get_kw_arg_opt_typed("radius", &RuntimeType::length(), exec_state)?;
1619 let diameter: Option<TyF64> = args.get_kw_arg_opt_typed("diameter", &RuntimeType::length(), exec_state)?;
1620 let end_absolute: Option<[TyF64; 2]> =
1621 args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1622 let interior_absolute: Option<[TyF64; 2]> =
1623 args.get_kw_arg_opt_typed("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1624 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1625 let new_sketch = inner_arc(
1626 sketch,
1627 angle_start,
1628 angle_end,
1629 radius,
1630 diameter,
1631 interior_absolute,
1632 end_absolute,
1633 tag,
1634 exec_state,
1635 args,
1636 )
1637 .await?;
1638 Ok(KclValue::Sketch {
1639 value: Box::new(new_sketch),
1640 })
1641}
1642
1643#[stdlib {
1677 name = "arc",
1678 unlabeled_first = true,
1679 args = {
1680 sketch = { docs = "Which sketch should this path be added to?" },
1681 angle_start = { docs = "Where along the circle should this arc start?", include_in_snippet = true },
1682 angle_end = { docs = "Where along the circle should this arc end?", include_in_snippet = true },
1683 radius = { docs = "How large should the circle be? Incompatible with `diameter`." },
1684 diameter = { docs = "How large should the circle be? Incompatible with `radius`.", include_in_snippet = true },
1685 interior_absolute = { docs = "Any point between the arc's start and end? Requires `endAbsolute`. Incompatible with `angleStart` or `angleEnd`" },
1686 end_absolute = { docs = "Where should this arc end? Requires `interiorAbsolute`. Incompatible with `angleStart` or `angleEnd`" },
1687 tag = { docs = "Create a new tag which refers to this line"},
1688 },
1689 tags = ["sketch"]
1690}]
1691#[allow(clippy::too_many_arguments)]
1692pub(crate) async fn inner_arc(
1693 sketch: Sketch,
1694 angle_start: Option<TyF64>,
1695 angle_end: Option<TyF64>,
1696 radius: Option<TyF64>,
1697 diameter: Option<TyF64>,
1698 interior_absolute: Option<[TyF64; 2]>,
1699 end_absolute: Option<[TyF64; 2]>,
1700 tag: Option<TagNode>,
1701 exec_state: &mut ExecState,
1702 args: Args,
1703) -> Result<Sketch, KclError> {
1704 let from: Point2d = sketch.current_pen_position()?;
1705 let id = exec_state.next_uuid();
1706
1707 match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1708 (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1709 let radius = get_radius(radius, diameter, args.source_range)?;
1710 relative_arc(&args, id, exec_state, sketch, from, angle_start, angle_end, radius, tag).await
1711 }
1712 (None, 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 diameter = args.get_kw_arg_opt_typed("diameter", &RuntimeType::length(), exec_state)?;
1859 let angle = args.get_kw_arg_opt_typed("angle", &RuntimeType::angle(), exec_state)?;
1860 let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
1861
1862 let new_sketch = inner_tangential_arc(
1863 sketch,
1864 end_absolute,
1865 end,
1866 radius,
1867 diameter,
1868 angle,
1869 tag,
1870 exec_state,
1871 args,
1872 )
1873 .await?;
1874 Ok(KclValue::Sketch {
1875 value: Box::new(new_sketch),
1876 })
1877}
1878
1879#[stdlib {
1934 name = "tangentialArc",
1935 unlabeled_first = true,
1936 args = {
1937 sketch = { docs = "Which sketch should this path be added to?"},
1938 end_absolute = { docs = "Which absolute point should this arc go to? Incompatible with `end`, `radius`, and `offset`."},
1939 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 },
1940 radius = { docs = "Radius of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute` and `diameter`."},
1941 diameter = { docs = "Diameter of the imaginary circle. `angle` must be given. Incompatible with `end` and `endAbsolute` and `radius`."},
1942 angle = { docs = "Offset of the arc in degrees. `radius` must be given. Incompatible with `end` and `endAbsolute`."},
1943 tag = { docs = "Create a new tag which refers to this arc"},
1944 },
1945 tags = ["sketch"]
1946}]
1947#[allow(clippy::too_many_arguments)]
1948async fn inner_tangential_arc(
1949 sketch: Sketch,
1950 end_absolute: Option<[TyF64; 2]>,
1951 end: Option<[TyF64; 2]>,
1952 radius: Option<TyF64>,
1953 diameter: Option<TyF64>,
1954 angle: Option<TyF64>,
1955 tag: Option<TagNode>,
1956 exec_state: &mut ExecState,
1957 args: Args,
1958) -> Result<Sketch, KclError> {
1959 match (end_absolute, end, radius, diameter, angle) {
1960 (Some(point), None, None, None, None) => {
1961 inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1962 }
1963 (None, Some(point), None, None, None) => {
1964 inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1965 }
1966 (None, None, radius, diameter, Some(angle)) => {
1967 let radius = get_radius(radius, diameter, args.source_range)?;
1968 let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1969 inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1970 }
1971 (Some(_), Some(_), None, None, None) => Err(KclError::Semantic(KclErrorDetails::new(
1972 "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1973 vec![args.source_range],
1974 ))),
1975 (_, _, _, _, _) => Err(KclError::Semantic(KclErrorDetails::new(
1976 "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1977 vec![args.source_range],
1978 ))),
1979 }
1980}
1981
1982#[derive(Debug, Clone, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
1984#[ts(export)]
1985#[serde(rename_all = "camelCase", untagged)]
1986pub enum TangentialArcData {
1987 RadiusAndOffset {
1988 radius: TyF64,
1991 offset: TyF64,
1993 },
1994}
1995
1996async fn inner_tangential_arc_radius_angle(
2003 data: TangentialArcData,
2004 sketch: Sketch,
2005 tag: Option<TagNode>,
2006 exec_state: &mut ExecState,
2007 args: Args,
2008) -> Result<Sketch, KclError> {
2009 let from: Point2d = sketch.current_pen_position()?;
2010 let tangent_info = sketch.get_tangential_info_from_paths(); let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
2013
2014 let id = exec_state.next_uuid();
2015
2016 let (center, to, ccw) = match data {
2017 TangentialArcData::RadiusAndOffset { radius, offset } => {
2018 let offset = Angle::from_degrees(offset.to_degrees());
2020
2021 let previous_end_tangent = Angle::from_radians(f64::atan2(
2024 from.y - tan_previous_point[1],
2025 from.x - tan_previous_point[0],
2026 ));
2027 let ccw = offset.to_degrees() > 0.0;
2030 let tangent_to_arc_start_angle = if ccw {
2031 Angle::from_degrees(-90.0)
2033 } else {
2034 Angle::from_degrees(90.0)
2036 };
2037 let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
2040 let end_angle = start_angle + offset;
2041 let (center, to) = arc_center_and_end(
2042 from.ignore_units(),
2043 start_angle,
2044 end_angle,
2045 radius.to_length_units(from.units),
2046 );
2047
2048 args.batch_modeling_cmd(
2049 id,
2050 ModelingCmd::from(mcmd::ExtendPath {
2051 path: sketch.id.into(),
2052 segment: PathSegment::TangentialArc {
2053 radius: LengthUnit(radius.to_mm()),
2054 offset,
2055 },
2056 }),
2057 )
2058 .await?;
2059 (center, to, ccw)
2060 }
2061 };
2062
2063 let current_path = Path::TangentialArc {
2064 ccw,
2065 center,
2066 base: BasePath {
2067 from: from.ignore_units(),
2068 to,
2069 tag: tag.clone(),
2070 units: sketch.units,
2071 geo_meta: GeoMeta {
2072 id,
2073 metadata: args.source_range.into(),
2074 },
2075 },
2076 };
2077
2078 let mut new_sketch = sketch.clone();
2079 if let Some(tag) = &tag {
2080 new_sketch.add_tag(tag, ¤t_path, exec_state);
2081 }
2082
2083 new_sketch.paths.push(current_path);
2084
2085 Ok(new_sketch)
2086}
2087
2088fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
2090 ModelingCmd::from(mcmd::ExtendPath {
2091 path: sketch.id.into(),
2092 segment: PathSegment::TangentialArcTo {
2093 angle_snap_increment: None,
2094 to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
2095 .with_z(0.0)
2096 .map(LengthUnit),
2097 },
2098 })
2099}
2100
2101async fn inner_tangential_arc_to_point(
2102 sketch: Sketch,
2103 point: [TyF64; 2],
2104 is_absolute: bool,
2105 tag: Option<TagNode>,
2106 exec_state: &mut ExecState,
2107 args: Args,
2108) -> Result<Sketch, KclError> {
2109 let from: Point2d = sketch.current_pen_position()?;
2110 let tangent_info = sketch.get_tangential_info_from_paths();
2111 let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
2112
2113 let point = point_to_len_unit(point, from.units);
2114
2115 let to = if is_absolute {
2116 point
2117 } else {
2118 [from.x + point[0], from.y + point[1]]
2119 };
2120 let [to_x, to_y] = to;
2121 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
2122 arc_start_point: [from.x, from.y],
2123 arc_end_point: [to_x, to_y],
2124 tan_previous_point,
2125 obtuse: true,
2126 });
2127
2128 if result.center[0].is_infinite() {
2129 return Err(KclError::Semantic(KclErrorDetails::new(
2130 "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
2131 .to_owned(),
2132 vec![args.source_range],
2133 )));
2134 } else if result.center[1].is_infinite() {
2135 return Err(KclError::Semantic(KclErrorDetails::new(
2136 "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
2137 .to_owned(),
2138 vec![args.source_range],
2139 )));
2140 }
2141
2142 let delta = if is_absolute {
2143 [to_x - from.x, to_y - from.y]
2144 } else {
2145 point
2146 };
2147 let id = exec_state.next_uuid();
2148 args.batch_modeling_cmd(id, tan_arc_to(&sketch, delta)).await?;
2149
2150 let current_path = Path::TangentialArcTo {
2151 base: BasePath {
2152 from: from.ignore_units(),
2153 to,
2154 tag: tag.clone(),
2155 units: sketch.units,
2156 geo_meta: GeoMeta {
2157 id,
2158 metadata: args.source_range.into(),
2159 },
2160 },
2161 center: result.center,
2162 ccw: result.ccw > 0,
2163 };
2164
2165 let mut new_sketch = sketch.clone();
2166 if let Some(tag) = &tag {
2167 new_sketch.add_tag(tag, ¤t_path, exec_state);
2168 }
2169
2170 new_sketch.paths.push(current_path);
2171
2172 Ok(new_sketch)
2173}
2174
2175pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2177 let sketch =
2178 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2179 let control1 = args.get_kw_arg_opt_typed("control1", &RuntimeType::point2d(), exec_state)?;
2180 let control2 = args.get_kw_arg_opt_typed("control2", &RuntimeType::point2d(), exec_state)?;
2181 let end = args.get_kw_arg_opt_typed("end", &RuntimeType::point2d(), exec_state)?;
2182 let control1_absolute = args.get_kw_arg_opt_typed("control1Absolute", &RuntimeType::point2d(), exec_state)?;
2183 let control2_absolute = args.get_kw_arg_opt_typed("control2Absolute", &RuntimeType::point2d(), exec_state)?;
2184 let end_absolute = args.get_kw_arg_opt_typed("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2185 let tag = args.get_kw_arg_opt("tag")?;
2186
2187 let new_sketch = inner_bezier_curve(
2188 sketch,
2189 control1,
2190 control2,
2191 end,
2192 control1_absolute,
2193 control2_absolute,
2194 end_absolute,
2195 tag,
2196 exec_state,
2197 args,
2198 )
2199 .await?;
2200 Ok(KclValue::Sketch {
2201 value: Box::new(new_sketch),
2202 })
2203}
2204
2205#[stdlib {
2233 name = "bezierCurve",
2234 unlabeled_first = true,
2235 args = {
2236 sketch = { docs = "Which sketch should this path be added to?"},
2237 control1 = { docs = "First control point for the cubic" },
2238 control2 = { docs = "Second control point for the cubic" },
2239 end = { docs = "How far away (along the X and Y axes) should this line go?" },
2240 control1_absolute = { docs = "First control point for the cubic. Absolute point." },
2241 control2_absolute = { docs = "Second control point for the cubic. Absolute point." },
2242 end_absolute = { docs = "Coordinate on the plane at which this line should end." },
2243 tag = { docs = "Create a new tag which refers to this line"},
2244 },
2245 tags = ["sketch"]
2246}]
2247#[allow(clippy::too_many_arguments)]
2248async fn inner_bezier_curve(
2249 sketch: Sketch,
2250 control1: Option<[TyF64; 2]>,
2251 control2: Option<[TyF64; 2]>,
2252 end: Option<[TyF64; 2]>,
2253 control1_absolute: Option<[TyF64; 2]>,
2254 control2_absolute: Option<[TyF64; 2]>,
2255 end_absolute: Option<[TyF64; 2]>,
2256 tag: Option<TagNode>,
2257 exec_state: &mut ExecState,
2258 args: Args,
2259) -> Result<Sketch, KclError> {
2260 let from = sketch.current_pen_position()?;
2261 let id = exec_state.next_uuid();
2262
2263 let to = match (
2264 control1,
2265 control2,
2266 end,
2267 control1_absolute,
2268 control2_absolute,
2269 end_absolute,
2270 ) {
2271 (Some(control1), Some(control2), Some(end), None, None, None) => {
2273 let delta = end.clone();
2274 let to = [
2275 from.x + end[0].to_length_units(from.units),
2276 from.y + end[1].to_length_units(from.units),
2277 ];
2278
2279 args.batch_modeling_cmd(
2280 id,
2281 ModelingCmd::from(mcmd::ExtendPath {
2282 path: sketch.id.into(),
2283 segment: PathSegment::Bezier {
2284 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2285 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2286 end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
2287 relative: true,
2288 },
2289 }),
2290 )
2291 .await?;
2292 to
2293 }
2294 (None, None, None, Some(control1), Some(control2), Some(end)) => {
2296 let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
2297 args.batch_modeling_cmd(
2298 id,
2299 ModelingCmd::from(mcmd::ExtendPath {
2300 path: sketch.id.into(),
2301 segment: PathSegment::Bezier {
2302 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2303 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2304 end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
2305 relative: false,
2306 },
2307 }),
2308 )
2309 .await?;
2310 to
2311 }
2312 _ => {
2313 return Err(KclError::Semantic(KclErrorDetails::new(
2314 "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
2315 vec![args.source_range],
2316 )));
2317 }
2318 };
2319
2320 let current_path = Path::ToPoint {
2321 base: BasePath {
2322 from: from.ignore_units(),
2323 to,
2324 tag: tag.clone(),
2325 units: sketch.units,
2326 geo_meta: GeoMeta {
2327 id,
2328 metadata: args.source_range.into(),
2329 },
2330 },
2331 };
2332
2333 let mut new_sketch = sketch.clone();
2334 if let Some(tag) = &tag {
2335 new_sketch.add_tag(tag, ¤t_path, exec_state);
2336 }
2337
2338 new_sketch.paths.push(current_path);
2339
2340 Ok(new_sketch)
2341}
2342
2343pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2345 let sketch =
2346 args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2347
2348 let tool: Vec<Sketch> = args.get_kw_arg_typed(
2349 "tool",
2350 &RuntimeType::Array(
2351 Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
2352 ArrayLen::Minimum(1),
2353 ),
2354 exec_state,
2355 )?;
2356
2357 let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
2358 Ok(KclValue::Sketch {
2359 value: Box::new(new_sketch),
2360 })
2361}
2362
2363#[stdlib {
2395 name = "subtract2d",
2396 feature_tree_operation = true,
2397 unlabeled_first = true,
2398 args = {
2399 sketch = { docs = "Which sketch should this path be added to?" },
2400 tool = { docs = "The shape(s) which should be cut out of the sketch." },
2401 },
2402 tags = ["sketch"]
2403}]
2404async fn inner_subtract_2d(
2405 sketch: Sketch,
2406 tool: Vec<Sketch>,
2407 exec_state: &mut ExecState,
2408 args: Args,
2409) -> Result<Sketch, KclError> {
2410 for hole_sketch in tool {
2411 args.batch_modeling_cmd(
2412 exec_state.next_uuid(),
2413 ModelingCmd::from(mcmd::Solid2dAddHole {
2414 object_id: sketch.id,
2415 hole_id: hole_sketch.id,
2416 }),
2417 )
2418 .await?;
2419
2420 args.batch_modeling_cmd(
2423 exec_state.next_uuid(),
2424 ModelingCmd::from(mcmd::ObjectVisible {
2425 object_id: hole_sketch.id,
2426 hidden: true,
2427 }),
2428 )
2429 .await?;
2430 }
2431
2432 Ok(sketch)
2433}
2434
2435#[cfg(test)]
2436mod tests {
2437
2438 use pretty_assertions::assert_eq;
2439
2440 use crate::{
2441 execution::TagIdentifier,
2442 std::{sketch::PlaneData, utils::calculate_circle_center},
2443 };
2444
2445 #[test]
2446 fn test_deserialize_plane_data() {
2447 let data = PlaneData::XY;
2448 let mut str_json = serde_json::to_string(&data).unwrap();
2449 assert_eq!(str_json, "\"XY\"");
2450
2451 str_json = "\"YZ\"".to_string();
2452 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2453 assert_eq!(data, PlaneData::YZ);
2454
2455 str_json = "\"-YZ\"".to_string();
2456 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2457 assert_eq!(data, PlaneData::NegYZ);
2458
2459 str_json = "\"-xz\"".to_string();
2460 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2461 assert_eq!(data, PlaneData::NegXZ);
2462 }
2463
2464 #[test]
2465 fn test_deserialize_sketch_on_face_tag() {
2466 let data = "start";
2467 let mut str_json = serde_json::to_string(&data).unwrap();
2468 assert_eq!(str_json, "\"start\"");
2469
2470 str_json = "\"end\"".to_string();
2471 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2472 assert_eq!(
2473 data,
2474 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2475 );
2476
2477 str_json = serde_json::to_string(&TagIdentifier {
2478 value: "thing".to_string(),
2479 info: Vec::new(),
2480 meta: Default::default(),
2481 })
2482 .unwrap();
2483 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2484 assert_eq!(
2485 data,
2486 crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2487 value: "thing".to_string(),
2488 info: Vec::new(),
2489 meta: Default::default()
2490 }))
2491 );
2492
2493 str_json = "\"END\"".to_string();
2494 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2495 assert_eq!(
2496 data,
2497 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2498 );
2499
2500 str_json = "\"start\"".to_string();
2501 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2502 assert_eq!(
2503 data,
2504 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2505 );
2506
2507 str_json = "\"START\"".to_string();
2508 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2509 assert_eq!(
2510 data,
2511 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2512 );
2513 }
2514
2515 #[test]
2516 fn test_circle_center() {
2517 let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2518 assert_eq!(actual[0], 5.0);
2519 assert_eq!(actual[1], 0.0);
2520 }
2521}