1use std::f64;
4
5use anyhow::Result;
6use indexmap::IndexMap;
7use kcmc::shared::Point2d as KPoint2d; use kcmc::shared::Point3d as KPoint3d; use kcmc::{ModelingCmd, each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, websocket::ModelingCmdReq};
10use kittycad_modeling_cmds as kcmc;
11use kittycad_modeling_cmds::{shared::PathSegment, units::UnitLength};
12use parse_display::{Display, FromStr};
13use serde::{Deserialize, Serialize};
14
15use super::{
16 shapes::{get_radius, get_radius_labelled},
17 utils::{untype_array, untype_point},
18};
19#[cfg(feature = "artifact-graph")]
20use crate::execution::{Artifact, ArtifactId, CodeRef, StartSketchOnFace, StartSketchOnPlane};
21use crate::{
22 errors::{KclError, KclErrorDetails},
23 execution::{
24 BasePath, ExecState, Face, GeoMeta, KclValue, ModelingCmdMeta, Path, Plane, PlaneInfo, Point2d, Point3d,
25 Sketch, SketchSurface, Solid, TagEngineInfo, TagIdentifier, annotations,
26 types::{ArrayLen, NumericType, PrimitiveType, RuntimeType},
27 },
28 parsing::ast::types::TagNode,
29 std::{
30 args::{Args, TyF64},
31 axis_or_reference::Axis2dOrEdgeReference,
32 planes::inner_plane_of,
33 utils::{
34 TangentialArcInfoInput, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
35 intersection_with_parallel_line, point_to_len_unit, point_to_mm, untyped_point_to_mm,
36 },
37 },
38};
39
40#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
42#[ts(export)]
43#[serde(rename_all = "snake_case", untagged)]
44pub enum FaceTag {
45 StartOrEnd(StartOrEnd),
46 Tag(Box<TagIdentifier>),
48}
49
50impl std::fmt::Display for FaceTag {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 FaceTag::Tag(t) => write!(f, "{t}"),
54 FaceTag::StartOrEnd(StartOrEnd::Start) => write!(f, "start"),
55 FaceTag::StartOrEnd(StartOrEnd::End) => write!(f, "end"),
56 }
57 }
58}
59
60impl FaceTag {
61 pub async fn get_face_id(
63 &self,
64 solid: &Solid,
65 exec_state: &mut ExecState,
66 args: &Args,
67 must_be_planar: bool,
68 ) -> Result<uuid::Uuid, KclError> {
69 match self {
70 FaceTag::Tag(t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
71 FaceTag::StartOrEnd(StartOrEnd::Start) => solid.start_cap_id.ok_or_else(|| {
72 KclError::new_type(KclErrorDetails::new(
73 "Expected a start face".to_string(),
74 vec![args.source_range],
75 ))
76 }),
77 FaceTag::StartOrEnd(StartOrEnd::End) => solid.end_cap_id.ok_or_else(|| {
78 KclError::new_type(KclErrorDetails::new(
79 "Expected an end face".to_string(),
80 vec![args.source_range],
81 ))
82 }),
83 }
84 }
85
86 pub async fn get_face_id_from_tag(
87 &self,
88 exec_state: &mut ExecState,
89 args: &Args,
90 must_be_planar: bool,
91 ) -> Result<uuid::Uuid, KclError> {
92 match self {
93 FaceTag::Tag(t) => args.get_adjacent_face_to_tag(exec_state, t, must_be_planar).await,
94 _ => Err(KclError::new_type(KclErrorDetails::new(
95 "Could not find the face corresponding to this tag".to_string(),
96 vec![args.source_range],
97 ))),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, FromStr, Display)]
103#[ts(export)]
104#[serde(rename_all = "snake_case")]
105#[display(style = "snake_case")]
106pub enum StartOrEnd {
107 #[serde(rename = "start", alias = "START")]
111 Start,
112 #[serde(rename = "end", alias = "END")]
116 End,
117}
118
119pub const NEW_TAG_KW: &str = "tag";
120
121pub async fn involute_circular(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
122 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
123
124 let start_radius: Option<TyF64> = args.get_kw_arg_opt("startRadius", &RuntimeType::length(), exec_state)?;
125 let end_radius: Option<TyF64> = args.get_kw_arg_opt("endRadius", &RuntimeType::length(), exec_state)?;
126 let start_diameter: Option<TyF64> = args.get_kw_arg_opt("startDiameter", &RuntimeType::length(), exec_state)?;
127 let end_diameter: Option<TyF64> = args.get_kw_arg_opt("endDiameter", &RuntimeType::length(), exec_state)?;
128 let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::angle(), exec_state)?;
129 let reverse = args.get_kw_arg_opt("reverse", &RuntimeType::bool(), exec_state)?;
130 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
131 let new_sketch = inner_involute_circular(
132 sketch,
133 start_radius,
134 end_radius,
135 start_diameter,
136 end_diameter,
137 angle,
138 reverse,
139 tag,
140 exec_state,
141 args,
142 )
143 .await?;
144 Ok(KclValue::Sketch {
145 value: Box::new(new_sketch),
146 })
147}
148
149fn involute_curve(radius: f64, angle: f64) -> (f64, f64) {
150 (
151 radius * (libm::cos(angle) + angle * libm::sin(angle)),
152 radius * (libm::sin(angle) - angle * libm::cos(angle)),
153 )
154}
155
156#[allow(clippy::too_many_arguments)]
157async fn inner_involute_circular(
158 sketch: Sketch,
159 start_radius: Option<TyF64>,
160 end_radius: Option<TyF64>,
161 start_diameter: Option<TyF64>,
162 end_diameter: Option<TyF64>,
163 angle: TyF64,
164 reverse: Option<bool>,
165 tag: Option<TagNode>,
166 exec_state: &mut ExecState,
167 args: Args,
168) -> Result<Sketch, KclError> {
169 let id = exec_state.next_uuid();
170
171 let longer_args_dot_source_range = args.source_range;
172 let start_radius = get_radius_labelled(
173 start_radius,
174 start_diameter,
175 args.source_range,
176 "startRadius",
177 "startDiameter",
178 )?;
179 let end_radius = get_radius_labelled(
180 end_radius,
181 end_diameter,
182 longer_args_dot_source_range,
183 "endRadius",
184 "endDiameter",
185 )?;
186
187 exec_state
188 .batch_modeling_cmd(
189 ModelingCmdMeta::from_args_id(&args, id),
190 ModelingCmd::from(mcmd::ExtendPath {
191 path: sketch.id.into(),
192 segment: PathSegment::CircularInvolute {
193 start_radius: LengthUnit(start_radius.to_mm()),
194 end_radius: LengthUnit(end_radius.to_mm()),
195 angle: Angle::from_degrees(angle.to_degrees()),
196 reverse: reverse.unwrap_or_default(),
197 },
198 }),
199 )
200 .await?;
201
202 let from = sketch.current_pen_position()?;
203
204 let start_radius = start_radius.to_length_units(from.units);
205 let end_radius = end_radius.to_length_units(from.units);
206
207 let mut end: KPoint3d<f64> = Default::default(); let theta = f64::sqrt(end_radius * end_radius - start_radius * start_radius) / start_radius;
209 let (x, y) = involute_curve(start_radius, theta);
210
211 end.x = x * libm::cos(angle.to_radians()) - y * libm::sin(angle.to_radians());
212 end.y = x * libm::sin(angle.to_radians()) + y * libm::cos(angle.to_radians());
213
214 end.x -= start_radius * libm::cos(angle.to_radians());
215 end.y -= start_radius * libm::sin(angle.to_radians());
216
217 if reverse.unwrap_or_default() {
218 end.x = -end.x;
219 }
220
221 end.x += from.x;
222 end.y += from.y;
223
224 let current_path = Path::ToPoint {
225 base: BasePath {
226 from: from.ignore_units(),
227 to: [end.x, end.y],
228 tag: tag.clone(),
229 units: sketch.units,
230 geo_meta: GeoMeta {
231 id,
232 metadata: args.source_range.into(),
233 },
234 },
235 };
236
237 let mut new_sketch = sketch;
238 if let Some(tag) = &tag {
239 new_sketch.add_tag(tag, ¤t_path, exec_state);
240 }
241 new_sketch.paths.push(current_path);
242 Ok(new_sketch)
243}
244
245pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
247 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
248 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
249 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
250 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
251
252 let new_sketch = inner_line(sketch, end_absolute, end, tag, exec_state, args).await?;
253 Ok(KclValue::Sketch {
254 value: Box::new(new_sketch),
255 })
256}
257
258async fn inner_line(
259 sketch: Sketch,
260 end_absolute: Option<[TyF64; 2]>,
261 end: Option<[TyF64; 2]>,
262 tag: Option<TagNode>,
263 exec_state: &mut ExecState,
264 args: Args,
265) -> Result<Sketch, KclError> {
266 straight_line(
267 StraightLineParams {
268 sketch,
269 end_absolute,
270 end,
271 tag,
272 relative_name: "end",
273 },
274 exec_state,
275 args,
276 )
277 .await
278}
279
280struct StraightLineParams {
281 sketch: Sketch,
282 end_absolute: Option<[TyF64; 2]>,
283 end: Option<[TyF64; 2]>,
284 tag: Option<TagNode>,
285 relative_name: &'static str,
286}
287
288impl StraightLineParams {
289 fn relative(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
290 Self {
291 sketch,
292 tag,
293 end: Some(p),
294 end_absolute: None,
295 relative_name: "end",
296 }
297 }
298 fn absolute(p: [TyF64; 2], sketch: Sketch, tag: Option<TagNode>) -> Self {
299 Self {
300 sketch,
301 tag,
302 end: None,
303 end_absolute: Some(p),
304 relative_name: "end",
305 }
306 }
307}
308
309async fn straight_line(
310 StraightLineParams {
311 sketch,
312 end,
313 end_absolute,
314 tag,
315 relative_name,
316 }: StraightLineParams,
317 exec_state: &mut ExecState,
318 args: Args,
319) -> Result<Sketch, KclError> {
320 let from = sketch.current_pen_position()?;
321 let (point, is_absolute) = match (end_absolute, end) {
322 (Some(_), Some(_)) => {
323 return Err(KclError::new_semantic(KclErrorDetails::new(
324 "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
325 vec![args.source_range],
326 )));
327 }
328 (Some(end_absolute), None) => (end_absolute, true),
329 (None, Some(end)) => (end, false),
330 (None, None) => {
331 return Err(KclError::new_semantic(KclErrorDetails::new(
332 format!("You must supply either `{relative_name}` or `endAbsolute` arguments"),
333 vec![args.source_range],
334 )));
335 }
336 };
337
338 let id = exec_state.next_uuid();
339 exec_state
340 .batch_modeling_cmd(
341 ModelingCmdMeta::from_args_id(&args, id),
342 ModelingCmd::from(mcmd::ExtendPath {
343 path: sketch.id.into(),
344 segment: PathSegment::Line {
345 end: KPoint2d::from(point_to_mm(point.clone())).with_z(0.0).map(LengthUnit),
346 relative: !is_absolute,
347 },
348 }),
349 )
350 .await?;
351
352 let end = if is_absolute {
353 point_to_len_unit(point, from.units)
354 } else {
355 let from = sketch.current_pen_position()?;
356 let point = point_to_len_unit(point, from.units);
357 [from.x + point[0], from.y + point[1]]
358 };
359
360 let current_path = Path::ToPoint {
361 base: BasePath {
362 from: from.ignore_units(),
363 to: end,
364 tag: tag.clone(),
365 units: sketch.units,
366 geo_meta: GeoMeta {
367 id,
368 metadata: args.source_range.into(),
369 },
370 },
371 };
372
373 let mut new_sketch = sketch;
374 if let Some(tag) = &tag {
375 new_sketch.add_tag(tag, ¤t_path, exec_state);
376 }
377
378 new_sketch.paths.push(current_path);
379
380 Ok(new_sketch)
381}
382
383pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
385 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
386 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
387 let end_absolute: Option<TyF64> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::length(), exec_state)?;
388 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
389
390 let new_sketch = inner_x_line(sketch, length, end_absolute, tag, exec_state, args).await?;
391 Ok(KclValue::Sketch {
392 value: Box::new(new_sketch),
393 })
394}
395
396async fn inner_x_line(
397 sketch: Sketch,
398 length: Option<TyF64>,
399 end_absolute: Option<TyF64>,
400 tag: Option<TagNode>,
401 exec_state: &mut ExecState,
402 args: Args,
403) -> Result<Sketch, KclError> {
404 let from = sketch.current_pen_position()?;
405 straight_line(
406 StraightLineParams {
407 sketch,
408 end_absolute: end_absolute.map(|x| [x, from.into_y()]),
409 end: length.map(|x| [x, TyF64::new(0.0, NumericType::mm())]),
410 tag,
411 relative_name: "length",
412 },
413 exec_state,
414 args,
415 )
416 .await
417}
418
419pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
421 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
422 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
423 let end_absolute: Option<TyF64> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::length(), exec_state)?;
424 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
425
426 let new_sketch = inner_y_line(sketch, length, end_absolute, tag, exec_state, args).await?;
427 Ok(KclValue::Sketch {
428 value: Box::new(new_sketch),
429 })
430}
431
432async fn inner_y_line(
433 sketch: Sketch,
434 length: Option<TyF64>,
435 end_absolute: Option<TyF64>,
436 tag: Option<TagNode>,
437 exec_state: &mut ExecState,
438 args: Args,
439) -> Result<Sketch, KclError> {
440 let from = sketch.current_pen_position()?;
441 straight_line(
442 StraightLineParams {
443 sketch,
444 end_absolute: end_absolute.map(|y| [from.into_x(), y]),
445 end: length.map(|y| [TyF64::new(0.0, NumericType::mm()), y]),
446 tag,
447 relative_name: "length",
448 },
449 exec_state,
450 args,
451 )
452 .await
453}
454
455pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
457 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::sketch(), exec_state)?;
458 let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::degrees(), exec_state)?;
459 let length: Option<TyF64> = args.get_kw_arg_opt("length", &RuntimeType::length(), exec_state)?;
460 let length_x: Option<TyF64> = args.get_kw_arg_opt("lengthX", &RuntimeType::length(), exec_state)?;
461 let length_y: Option<TyF64> = args.get_kw_arg_opt("lengthY", &RuntimeType::length(), exec_state)?;
462 let end_absolute_x: Option<TyF64> = args.get_kw_arg_opt("endAbsoluteX", &RuntimeType::length(), exec_state)?;
463 let end_absolute_y: Option<TyF64> = args.get_kw_arg_opt("endAbsoluteY", &RuntimeType::length(), exec_state)?;
464 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
465
466 let new_sketch = inner_angled_line(
467 sketch,
468 angle.n,
469 length,
470 length_x,
471 length_y,
472 end_absolute_x,
473 end_absolute_y,
474 tag,
475 exec_state,
476 args,
477 )
478 .await?;
479 Ok(KclValue::Sketch {
480 value: Box::new(new_sketch),
481 })
482}
483
484#[allow(clippy::too_many_arguments)]
485async fn inner_angled_line(
486 sketch: Sketch,
487 angle: f64,
488 length: Option<TyF64>,
489 length_x: Option<TyF64>,
490 length_y: Option<TyF64>,
491 end_absolute_x: Option<TyF64>,
492 end_absolute_y: Option<TyF64>,
493 tag: Option<TagNode>,
494 exec_state: &mut ExecState,
495 args: Args,
496) -> Result<Sketch, KclError> {
497 let options_given = [&length, &length_x, &length_y, &end_absolute_x, &end_absolute_y]
498 .iter()
499 .filter(|x| x.is_some())
500 .count();
501 if options_given > 1 {
502 return Err(KclError::new_type(KclErrorDetails::new(
503 " one of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_string(),
504 vec![args.source_range],
505 )));
506 }
507 if let Some(length_x) = length_x {
508 return inner_angled_line_of_x_length(angle, length_x, sketch, tag, exec_state, args).await;
509 }
510 if let Some(length_y) = length_y {
511 return inner_angled_line_of_y_length(angle, length_y, sketch, tag, exec_state, args).await;
512 }
513 let angle_degrees = angle;
514 match (length, length_x, length_y, end_absolute_x, end_absolute_y) {
515 (Some(length), None, None, None, None) => {
516 inner_angled_line_length(sketch, angle_degrees, length, tag, exec_state, args).await
517 }
518 (None, Some(length_x), None, None, None) => {
519 inner_angled_line_of_x_length(angle_degrees, length_x, sketch, tag, exec_state, args).await
520 }
521 (None, None, Some(length_y), None, None) => {
522 inner_angled_line_of_y_length(angle_degrees, length_y, sketch, tag, exec_state, args).await
523 }
524 (None, None, None, Some(end_absolute_x), None) => {
525 inner_angled_line_to_x(angle_degrees, end_absolute_x, sketch, tag, exec_state, args).await
526 }
527 (None, None, None, None, Some(end_absolute_y)) => {
528 inner_angled_line_to_y(angle_degrees, end_absolute_y, sketch, tag, exec_state, args).await
529 }
530 (None, None, None, None, None) => Err(KclError::new_type(KclErrorDetails::new(
531 "One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` must be given".to_string(),
532 vec![args.source_range],
533 ))),
534 _ => Err(KclError::new_type(KclErrorDetails::new(
535 "Only One of `length`, `lengthX`, `lengthY`, `endAbsoluteX`, `endAbsoluteY` can be given".to_owned(),
536 vec![args.source_range],
537 ))),
538 }
539}
540
541async fn inner_angled_line_length(
542 sketch: Sketch,
543 angle_degrees: f64,
544 length: TyF64,
545 tag: Option<TagNode>,
546 exec_state: &mut ExecState,
547 args: Args,
548) -> Result<Sketch, KclError> {
549 let from = sketch.current_pen_position()?;
550 let length = length.to_length_units(from.units);
551
552 let delta: [f64; 2] = [
554 length * libm::cos(angle_degrees.to_radians()),
555 length * libm::sin(angle_degrees.to_radians()),
556 ];
557 let relative = true;
558
559 let to: [f64; 2] = [from.x + delta[0], from.y + delta[1]];
560
561 let id = exec_state.next_uuid();
562
563 exec_state
564 .batch_modeling_cmd(
565 ModelingCmdMeta::from_args_id(&args, id),
566 ModelingCmd::from(mcmd::ExtendPath {
567 path: sketch.id.into(),
568 segment: PathSegment::Line {
569 end: KPoint2d::from(untyped_point_to_mm(delta, from.units))
570 .with_z(0.0)
571 .map(LengthUnit),
572 relative,
573 },
574 }),
575 )
576 .await?;
577
578 let current_path = Path::ToPoint {
579 base: BasePath {
580 from: from.ignore_units(),
581 to,
582 tag: tag.clone(),
583 units: sketch.units,
584 geo_meta: GeoMeta {
585 id,
586 metadata: args.source_range.into(),
587 },
588 },
589 };
590
591 let mut new_sketch = sketch;
592 if let Some(tag) = &tag {
593 new_sketch.add_tag(tag, ¤t_path, exec_state);
594 }
595
596 new_sketch.paths.push(current_path);
597 Ok(new_sketch)
598}
599
600async fn inner_angled_line_of_x_length(
601 angle_degrees: f64,
602 length: TyF64,
603 sketch: Sketch,
604 tag: Option<TagNode>,
605 exec_state: &mut ExecState,
606 args: Args,
607) -> Result<Sketch, KclError> {
608 if angle_degrees.abs() == 270.0 {
609 return Err(KclError::new_type(KclErrorDetails::new(
610 "Cannot have an x constrained angle of 270 degrees".to_string(),
611 vec![args.source_range],
612 )));
613 }
614
615 if angle_degrees.abs() == 90.0 {
616 return Err(KclError::new_type(KclErrorDetails::new(
617 "Cannot have an x constrained angle of 90 degrees".to_string(),
618 vec![args.source_range],
619 )));
620 }
621
622 let to = get_y_component(Angle::from_degrees(angle_degrees), length.n);
623 let to = [TyF64::new(to[0], length.ty), TyF64::new(to[1], length.ty)];
624
625 let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
626
627 Ok(new_sketch)
628}
629
630async fn inner_angled_line_to_x(
631 angle_degrees: f64,
632 x_to: TyF64,
633 sketch: Sketch,
634 tag: Option<TagNode>,
635 exec_state: &mut ExecState,
636 args: Args,
637) -> Result<Sketch, KclError> {
638 let from = sketch.current_pen_position()?;
639
640 if angle_degrees.abs() == 270.0 {
641 return Err(KclError::new_type(KclErrorDetails::new(
642 "Cannot have an x constrained angle of 270 degrees".to_string(),
643 vec![args.source_range],
644 )));
645 }
646
647 if angle_degrees.abs() == 90.0 {
648 return Err(KclError::new_type(KclErrorDetails::new(
649 "Cannot have an x constrained angle of 90 degrees".to_string(),
650 vec![args.source_range],
651 )));
652 }
653
654 let x_component = x_to.to_length_units(from.units) - from.x;
655 let y_component = x_component * libm::tan(angle_degrees.to_radians());
656 let y_to = from.y + y_component;
657
658 let new_sketch = straight_line(
659 StraightLineParams::absolute([x_to, TyF64::new(y_to, from.units.into())], sketch, tag),
660 exec_state,
661 args,
662 )
663 .await?;
664 Ok(new_sketch)
665}
666
667async fn inner_angled_line_of_y_length(
668 angle_degrees: f64,
669 length: TyF64,
670 sketch: Sketch,
671 tag: Option<TagNode>,
672 exec_state: &mut ExecState,
673 args: Args,
674) -> Result<Sketch, KclError> {
675 if angle_degrees.abs() == 0.0 {
676 return Err(KclError::new_type(KclErrorDetails::new(
677 "Cannot have a y constrained angle of 0 degrees".to_string(),
678 vec![args.source_range],
679 )));
680 }
681
682 if angle_degrees.abs() == 180.0 {
683 return Err(KclError::new_type(KclErrorDetails::new(
684 "Cannot have a y constrained angle of 180 degrees".to_string(),
685 vec![args.source_range],
686 )));
687 }
688
689 let to = get_x_component(Angle::from_degrees(angle_degrees), length.n);
690 let to = [TyF64::new(to[0], length.ty), TyF64::new(to[1], length.ty)];
691
692 let new_sketch = straight_line(StraightLineParams::relative(to, sketch, tag), exec_state, args).await?;
693
694 Ok(new_sketch)
695}
696
697async fn inner_angled_line_to_y(
698 angle_degrees: f64,
699 y_to: TyF64,
700 sketch: Sketch,
701 tag: Option<TagNode>,
702 exec_state: &mut ExecState,
703 args: Args,
704) -> Result<Sketch, KclError> {
705 let from = sketch.current_pen_position()?;
706
707 if angle_degrees.abs() == 0.0 {
708 return Err(KclError::new_type(KclErrorDetails::new(
709 "Cannot have a y constrained angle of 0 degrees".to_string(),
710 vec![args.source_range],
711 )));
712 }
713
714 if angle_degrees.abs() == 180.0 {
715 return Err(KclError::new_type(KclErrorDetails::new(
716 "Cannot have a y constrained angle of 180 degrees".to_string(),
717 vec![args.source_range],
718 )));
719 }
720
721 let y_component = y_to.to_length_units(from.units) - from.y;
722 let x_component = y_component / libm::tan(angle_degrees.to_radians());
723 let x_to = from.x + x_component;
724
725 let new_sketch = straight_line(
726 StraightLineParams::absolute([TyF64::new(x_to, from.units.into()), y_to], sketch, tag),
727 exec_state,
728 args,
729 )
730 .await?;
731 Ok(new_sketch)
732}
733
734pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
736 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
737 let angle: TyF64 = args.get_kw_arg("angle", &RuntimeType::angle(), exec_state)?;
738 let intersect_tag: TagIdentifier = args.get_kw_arg("intersectTag", &RuntimeType::tagged_edge(), exec_state)?;
739 let offset = args.get_kw_arg_opt("offset", &RuntimeType::length(), exec_state)?;
740 let tag: Option<TagNode> = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
741 let new_sketch =
742 inner_angled_line_that_intersects(sketch, angle, intersect_tag, offset, tag, exec_state, args).await?;
743 Ok(KclValue::Sketch {
744 value: Box::new(new_sketch),
745 })
746}
747
748pub async fn inner_angled_line_that_intersects(
749 sketch: Sketch,
750 angle: TyF64,
751 intersect_tag: TagIdentifier,
752 offset: Option<TyF64>,
753 tag: Option<TagNode>,
754 exec_state: &mut ExecState,
755 args: Args,
756) -> Result<Sketch, KclError> {
757 let intersect_path = args.get_tag_engine_info(exec_state, &intersect_tag)?;
758 let path = intersect_path.path.clone().ok_or_else(|| {
759 KclError::new_type(KclErrorDetails::new(
760 format!("Expected an intersect path with a path, found `{intersect_path:?}`"),
761 vec![args.source_range],
762 ))
763 })?;
764
765 let from = sketch.current_pen_position()?;
766 let to = intersection_with_parallel_line(
767 &[
768 point_to_len_unit(path.get_from(), from.units),
769 point_to_len_unit(path.get_to(), from.units),
770 ],
771 offset.map(|t| t.to_length_units(from.units)).unwrap_or_default(),
772 angle.to_degrees(),
773 from.ignore_units(),
774 );
775 let to = [
776 TyF64::new(to[0], from.units.into()),
777 TyF64::new(to[1], from.units.into()),
778 ];
779
780 straight_line(StraightLineParams::absolute(to, sketch, tag), exec_state, args).await
781}
782
783#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
786#[ts(export)]
787#[serde(rename_all = "camelCase", untagged)]
788#[allow(clippy::large_enum_variant)]
789pub enum SketchData {
790 PlaneOrientation(PlaneData),
791 Plane(Box<Plane>),
792 Solid(Box<Solid>),
793}
794
795#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
797#[ts(export)]
798#[serde(rename_all = "camelCase")]
799#[allow(clippy::large_enum_variant)]
800pub enum PlaneData {
801 #[serde(rename = "XY", alias = "xy")]
803 XY,
804 #[serde(rename = "-XY", alias = "-xy")]
806 NegXY,
807 #[serde(rename = "XZ", alias = "xz")]
809 XZ,
810 #[serde(rename = "-XZ", alias = "-xz")]
812 NegXZ,
813 #[serde(rename = "YZ", alias = "yz")]
815 YZ,
816 #[serde(rename = "-YZ", alias = "-yz")]
818 NegYZ,
819 Plane(PlaneInfo),
821}
822
823pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
825 let data = args.get_unlabeled_kw_arg(
826 "planeOrSolid",
827 &RuntimeType::Union(vec![RuntimeType::solid(), RuntimeType::plane()]),
828 exec_state,
829 )?;
830 let face = args.get_kw_arg_opt("face", &RuntimeType::tagged_face(), exec_state)?;
831 let normal_to_face = args.get_kw_arg_opt("normalToFace", &RuntimeType::tagged_face(), exec_state)?;
832 let align_axis = args.get_kw_arg_opt("alignAxis", &RuntimeType::Primitive(PrimitiveType::Axis2d), exec_state)?;
833 let normal_offset = args.get_kw_arg_opt("normalOffset", &RuntimeType::length(), exec_state)?;
834
835 match inner_start_sketch_on(data, face, normal_to_face, align_axis, normal_offset, exec_state, &args).await? {
836 SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
837 SketchSurface::Face(value) => Ok(KclValue::Face { value }),
838 }
839}
840
841async fn inner_start_sketch_on(
842 plane_or_solid: SketchData,
843 face: Option<FaceTag>,
844 normal_to_face: Option<FaceTag>,
845 align_axis: Option<Axis2dOrEdgeReference>,
846 normal_offset: Option<TyF64>,
847 exec_state: &mut ExecState,
848 args: &Args,
849) -> Result<SketchSurface, KclError> {
850 let face = match (face, normal_to_face, &align_axis, &normal_offset) {
851 (Some(_), Some(_), _, _) => {
852 return Err(KclError::new_semantic(KclErrorDetails::new(
853 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other."
854 .to_owned(),
855 vec![args.source_range],
856 )));
857 }
858 (Some(face), None, None, None) => Some(face),
859 (_, Some(_), None, _) => {
860 return Err(KclError::new_semantic(KclErrorDetails::new(
861 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
862 vec![args.source_range],
863 )));
864 }
865 (_, None, Some(_), _) => {
866 return Err(KclError::new_semantic(KclErrorDetails::new(
867 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
868 vec![args.source_range],
869 )));
870 }
871 (_, None, _, Some(_)) => {
872 return Err(KclError::new_semantic(KclErrorDetails::new(
873 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
874 vec![args.source_range],
875 )));
876 }
877 (_, Some(face), Some(_), _) => Some(face),
878 (None, None, None, None) => None,
879 };
880
881 match plane_or_solid {
882 SketchData::PlaneOrientation(plane_data) => {
883 let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
884 Ok(SketchSurface::Plane(plane))
885 }
886 SketchData::Plane(plane) => {
887 if plane.value == crate::exec::PlaneType::Uninit {
888 let plane = make_sketch_plane_from_orientation(plane.info.into_plane_data(), exec_state, args).await?;
889 Ok(SketchSurface::Plane(plane))
890 } else {
891 #[cfg(feature = "artifact-graph")]
893 {
894 let id = exec_state.next_uuid();
895 exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
896 id: ArtifactId::from(id),
897 plane_id: plane.artifact_id,
898 code_ref: CodeRef::placeholder(args.source_range),
899 }));
900 }
901
902 Ok(SketchSurface::Plane(plane))
903 }
904 }
905 SketchData::Solid(solid) => {
906 let Some(tag) = face else {
907 return Err(KclError::new_type(KclErrorDetails::new(
908 "Expected a tag for the face to sketch on".to_string(),
909 vec![args.source_range],
910 )));
911 };
912 if let Some(align_axis) = align_axis {
913 let plane_of = inner_plane_of(*solid, tag, exec_state, args).await?;
914
915 let offset = normal_offset.map_or(0.0, |x| x.n);
916 let (x_axis, y_axis, normal_offset) = match align_axis {
917 Axis2dOrEdgeReference::Axis { direction, origin: _ } => {
918 if (direction[0].n - 1.0).abs() < f64::EPSILON {
919 (
921 plane_of.info.x_axis,
922 plane_of.info.z_axis,
923 plane_of.info.y_axis * offset,
924 )
925 } else if (direction[0].n + 1.0).abs() < f64::EPSILON {
926 (
928 plane_of.info.x_axis.negated(),
929 plane_of.info.z_axis,
930 plane_of.info.y_axis * offset,
931 )
932 } else if (direction[1].n - 1.0).abs() < f64::EPSILON {
933 (
935 plane_of.info.y_axis,
936 plane_of.info.z_axis,
937 plane_of.info.x_axis * offset,
938 )
939 } else if (direction[1].n + 1.0).abs() < f64::EPSILON {
940 (
942 plane_of.info.y_axis.negated(),
943 plane_of.info.z_axis,
944 plane_of.info.x_axis * offset,
945 )
946 } else {
947 return Err(KclError::new_semantic(KclErrorDetails::new(
948 "Unsupported axis detected. This function only supports using X, -X, Y and -Y."
949 .to_owned(),
950 vec![args.source_range],
951 )));
952 }
953 }
954 Axis2dOrEdgeReference::Edge(_) => {
955 return Err(KclError::new_semantic(KclErrorDetails::new(
956 "Use of an edge here is unsupported, please specify an `Axis2d` (e.g. `X`) instead."
957 .to_owned(),
958 vec![args.source_range],
959 )));
960 }
961 };
962 let origin = Point3d::new(0.0, 0.0, 0.0, plane_of.info.origin.units);
963 let plane_data = PlaneData::Plane(PlaneInfo {
964 origin: plane_of.project(origin) + normal_offset,
965 x_axis,
966 y_axis,
967 z_axis: x_axis.axes_cross_product(&y_axis),
968 });
969 let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
970
971 #[cfg(feature = "artifact-graph")]
973 {
974 let id = exec_state.next_uuid();
975 exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
976 id: ArtifactId::from(id),
977 plane_id: plane.artifact_id,
978 code_ref: CodeRef::placeholder(args.source_range),
979 }));
980 }
981
982 Ok(SketchSurface::Plane(plane))
983 } else {
984 let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
985
986 #[cfg(feature = "artifact-graph")]
987 {
988 let id = exec_state.next_uuid();
990 exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
991 id: ArtifactId::from(id),
992 face_id: face.artifact_id,
993 code_ref: CodeRef::placeholder(args.source_range),
994 }));
995 }
996
997 Ok(SketchSurface::Face(face))
998 }
999 }
1000 }
1001}
1002
1003async fn start_sketch_on_face(
1004 solid: Box<Solid>,
1005 tag: FaceTag,
1006 exec_state: &mut ExecState,
1007 args: &Args,
1008) -> Result<Box<Face>, KclError> {
1009 let extrude_plane_id = tag.get_face_id(&solid, exec_state, args, true).await?;
1010
1011 Ok(Box::new(Face {
1012 id: extrude_plane_id,
1013 artifact_id: extrude_plane_id.into(),
1014 value: tag.to_string(),
1015 x_axis: solid.sketch.on.x_axis(),
1017 y_axis: solid.sketch.on.y_axis(),
1018 units: solid.units,
1019 solid,
1020 meta: vec![args.source_range.into()],
1021 }))
1022}
1023
1024pub async fn make_sketch_plane_from_orientation(
1025 data: PlaneData,
1026 exec_state: &mut ExecState,
1027 args: &Args,
1028) -> Result<Box<Plane>, KclError> {
1029 let plane = Plane::from_plane_data(data.clone(), exec_state)?;
1030
1031 let clobber = false;
1033 let size = LengthUnit(60.0);
1034 let hide = Some(true);
1035 exec_state
1036 .batch_modeling_cmd(
1037 ModelingCmdMeta::from_args_id(args, plane.id),
1038 ModelingCmd::from(mcmd::MakePlane {
1039 clobber,
1040 origin: plane.info.origin.into(),
1041 size,
1042 x_axis: plane.info.x_axis.into(),
1043 y_axis: plane.info.y_axis.into(),
1044 hide,
1045 }),
1046 )
1047 .await?;
1048
1049 Ok(Box::new(plane))
1050}
1051
1052pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1054 let sketch_surface = args.get_unlabeled_kw_arg(
1055 "startProfileOn",
1056 &RuntimeType::Union(vec![RuntimeType::plane(), RuntimeType::face()]),
1057 exec_state,
1058 )?;
1059 let start: [TyF64; 2] = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
1060 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1061
1062 let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, args).await?;
1063 Ok(KclValue::Sketch {
1064 value: Box::new(sketch),
1065 })
1066}
1067
1068pub(crate) async fn inner_start_profile(
1069 sketch_surface: SketchSurface,
1070 at: [TyF64; 2],
1071 tag: Option<TagNode>,
1072 exec_state: &mut ExecState,
1073 args: Args,
1074) -> Result<Sketch, KclError> {
1075 match &sketch_surface {
1076 SketchSurface::Face(face) => {
1077 exec_state
1080 .flush_batch_for_solids((&args).into(), &[(*face.solid).clone()])
1081 .await?;
1082 }
1083 SketchSurface::Plane(plane) if !plane.is_standard() => {
1084 exec_state
1087 .batch_end_cmd(
1088 (&args).into(),
1089 ModelingCmd::from(mcmd::ObjectVisible {
1090 object_id: plane.id,
1091 hidden: true,
1092 }),
1093 )
1094 .await?;
1095 }
1096 _ => {}
1097 }
1098
1099 let enable_sketch_id = exec_state.next_uuid();
1100 let path_id = exec_state.next_uuid();
1101 let move_pen_id = exec_state.next_uuid();
1102 let disable_sketch_id = exec_state.next_uuid();
1103 exec_state
1104 .batch_modeling_cmds(
1105 (&args).into(),
1106 &[
1107 ModelingCmdReq {
1110 cmd: ModelingCmd::from(mcmd::EnableSketchMode {
1111 animated: false,
1112 ortho: false,
1113 entity_id: sketch_surface.id(),
1114 adjust_camera: false,
1115 planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
1116 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
1118 Some(normal.into())
1119 } else {
1120 None
1121 },
1122 }),
1123 cmd_id: enable_sketch_id.into(),
1124 },
1125 ModelingCmdReq {
1126 cmd: ModelingCmd::from(mcmd::StartPath::default()),
1127 cmd_id: path_id.into(),
1128 },
1129 ModelingCmdReq {
1130 cmd: ModelingCmd::from(mcmd::MovePathPen {
1131 path: path_id.into(),
1132 to: KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit),
1133 }),
1134 cmd_id: move_pen_id.into(),
1135 },
1136 ModelingCmdReq {
1137 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1138 cmd_id: disable_sketch_id.into(),
1139 },
1140 ],
1141 )
1142 .await?;
1143
1144 let units = exec_state.length_unit();
1146 let to = point_to_len_unit(at, units);
1147 let current_path = BasePath {
1148 from: to,
1149 to,
1150 tag: tag.clone(),
1151 units,
1152 geo_meta: GeoMeta {
1153 id: move_pen_id,
1154 metadata: args.source_range.into(),
1155 },
1156 };
1157
1158 let sketch = Sketch {
1159 id: path_id,
1160 original_id: path_id,
1161 artifact_id: path_id.into(),
1162 on: sketch_surface.clone(),
1163 paths: vec![],
1164 inner_paths: vec![],
1165 units,
1166 mirror: Default::default(),
1167 meta: vec![args.source_range.into()],
1168 tags: if let Some(tag) = &tag {
1169 let mut tag_identifier: TagIdentifier = tag.into();
1170 tag_identifier.info = vec![(
1171 exec_state.stack().current_epoch(),
1172 TagEngineInfo {
1173 id: current_path.geo_meta.id,
1174 sketch: path_id,
1175 path: Some(Path::Base {
1176 base: current_path.clone(),
1177 }),
1178 surface: None,
1179 },
1180 )];
1181 IndexMap::from([(tag.name.to_string(), tag_identifier)])
1182 } else {
1183 Default::default()
1184 },
1185 start: current_path,
1186 is_closed: false,
1187 };
1188 Ok(sketch)
1189}
1190
1191pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1193 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1194 let ty = sketch.units.into();
1195 let x = inner_profile_start_x(sketch)?;
1196 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1197}
1198
1199pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1200 Ok(profile.start.to[0])
1201}
1202
1203pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1205 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1206 let ty = sketch.units.into();
1207 let x = inner_profile_start_y(sketch)?;
1208 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1209}
1210
1211pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1212 Ok(profile.start.to[1])
1213}
1214
1215pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1217 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1218 let ty = sketch.units.into();
1219 let point = inner_profile_start(sketch)?;
1220 Ok(KclValue::from_point2d(point, ty, args.into()))
1221}
1222
1223pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1224 Ok(profile.start.to)
1225}
1226
1227pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1229 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1230 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1231 let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1232 Ok(KclValue::Sketch {
1233 value: Box::new(new_sketch),
1234 })
1235}
1236
1237pub(crate) async fn inner_close(
1238 sketch: Sketch,
1239 tag: Option<TagNode>,
1240 exec_state: &mut ExecState,
1241 args: Args,
1242) -> Result<Sketch, KclError> {
1243 if sketch.is_closed {
1244 exec_state.warn(
1245 crate::CompilationError {
1246 source_range: args.source_range,
1247 message: "This sketch is already closed. Remove this unnecessary `close()` call".to_string(),
1248 suggestion: None,
1249 severity: crate::errors::Severity::Warning,
1250 tag: crate::errors::Tag::Unnecessary,
1251 },
1252 annotations::WARN_UNNECESSARY_CLOSE,
1253 );
1254 return Ok(sketch);
1255 }
1256 let from = sketch.current_pen_position()?;
1257 let to = point_to_len_unit(sketch.start.get_from(), from.units);
1258
1259 let id = exec_state.next_uuid();
1260
1261 exec_state
1262 .batch_modeling_cmd(
1263 ModelingCmdMeta::from_args_id(&args, id),
1264 ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }),
1265 )
1266 .await?;
1267
1268 let current_path = Path::ToPoint {
1269 base: BasePath {
1270 from: from.ignore_units(),
1271 to,
1272 tag: tag.clone(),
1273 units: sketch.units,
1274 geo_meta: GeoMeta {
1275 id,
1276 metadata: args.source_range.into(),
1277 },
1278 },
1279 };
1280
1281 let mut new_sketch = sketch;
1282 if let Some(tag) = &tag {
1283 new_sketch.add_tag(tag, ¤t_path, exec_state);
1284 }
1285 new_sketch.paths.push(current_path);
1286 new_sketch.is_closed = true;
1287
1288 Ok(new_sketch)
1289}
1290
1291pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1293 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1294
1295 let angle_start: Option<TyF64> = args.get_kw_arg_opt("angleStart", &RuntimeType::degrees(), exec_state)?;
1296 let angle_end: Option<TyF64> = args.get_kw_arg_opt("angleEnd", &RuntimeType::degrees(), exec_state)?;
1297 let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1298 let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1299 let end_absolute: Option<[TyF64; 2]> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1300 let interior_absolute: Option<[TyF64; 2]> =
1301 args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1302 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1303 let new_sketch = inner_arc(
1304 sketch,
1305 angle_start,
1306 angle_end,
1307 radius,
1308 diameter,
1309 interior_absolute,
1310 end_absolute,
1311 tag,
1312 exec_state,
1313 args,
1314 )
1315 .await?;
1316 Ok(KclValue::Sketch {
1317 value: Box::new(new_sketch),
1318 })
1319}
1320
1321#[allow(clippy::too_many_arguments)]
1322pub(crate) async fn inner_arc(
1323 sketch: Sketch,
1324 angle_start: Option<TyF64>,
1325 angle_end: Option<TyF64>,
1326 radius: Option<TyF64>,
1327 diameter: Option<TyF64>,
1328 interior_absolute: Option<[TyF64; 2]>,
1329 end_absolute: Option<[TyF64; 2]>,
1330 tag: Option<TagNode>,
1331 exec_state: &mut ExecState,
1332 args: Args,
1333) -> Result<Sketch, KclError> {
1334 let from: Point2d = sketch.current_pen_position()?;
1335 let id = exec_state.next_uuid();
1336
1337 match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1338 (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1339 let radius = get_radius(radius, diameter, args.source_range)?;
1340 relative_arc(&args, id, exec_state, sketch, from, angle_start, angle_end, radius, tag).await
1341 }
1342 (None, None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1343 absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1344 }
1345 _ => {
1346 Err(KclError::new_type(KclErrorDetails::new(
1347 "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)".to_owned(),
1348 vec![args.source_range],
1349 )))
1350 }
1351 }
1352}
1353
1354#[allow(clippy::too_many_arguments)]
1355pub async fn absolute_arc(
1356 args: &Args,
1357 id: uuid::Uuid,
1358 exec_state: &mut ExecState,
1359 sketch: Sketch,
1360 from: Point2d,
1361 interior_absolute: [TyF64; 2],
1362 end_absolute: [TyF64; 2],
1363 tag: Option<TagNode>,
1364) -> Result<Sketch, KclError> {
1365 exec_state
1367 .batch_modeling_cmd(
1368 ModelingCmdMeta::from_args_id(args, id),
1369 ModelingCmd::from(mcmd::ExtendPath {
1370 path: sketch.id.into(),
1371 segment: PathSegment::ArcTo {
1372 end: kcmc::shared::Point3d {
1373 x: LengthUnit(end_absolute[0].to_mm()),
1374 y: LengthUnit(end_absolute[1].to_mm()),
1375 z: LengthUnit(0.0),
1376 },
1377 interior: kcmc::shared::Point3d {
1378 x: LengthUnit(interior_absolute[0].to_mm()),
1379 y: LengthUnit(interior_absolute[1].to_mm()),
1380 z: LengthUnit(0.0),
1381 },
1382 relative: false,
1383 },
1384 }),
1385 )
1386 .await?;
1387
1388 let start = [from.x, from.y];
1389 let end = point_to_len_unit(end_absolute, from.units);
1390
1391 let current_path = Path::ArcThreePoint {
1392 base: BasePath {
1393 from: from.ignore_units(),
1394 to: end,
1395 tag: tag.clone(),
1396 units: sketch.units,
1397 geo_meta: GeoMeta {
1398 id,
1399 metadata: args.source_range.into(),
1400 },
1401 },
1402 p1: start,
1403 p2: point_to_len_unit(interior_absolute, from.units),
1404 p3: end,
1405 };
1406
1407 let mut new_sketch = sketch;
1408 if let Some(tag) = &tag {
1409 new_sketch.add_tag(tag, ¤t_path, exec_state);
1410 }
1411
1412 new_sketch.paths.push(current_path);
1413
1414 Ok(new_sketch)
1415}
1416
1417#[allow(clippy::too_many_arguments)]
1418pub async fn relative_arc(
1419 args: &Args,
1420 id: uuid::Uuid,
1421 exec_state: &mut ExecState,
1422 sketch: Sketch,
1423 from: Point2d,
1424 angle_start: TyF64,
1425 angle_end: TyF64,
1426 radius: TyF64,
1427 tag: Option<TagNode>,
1428) -> Result<Sketch, KclError> {
1429 let a_start = Angle::from_degrees(angle_start.to_degrees());
1430 let a_end = Angle::from_degrees(angle_end.to_degrees());
1431 let radius = radius.to_length_units(from.units);
1432 let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1433 if a_start == a_end {
1434 return Err(KclError::new_type(KclErrorDetails::new(
1435 "Arc start and end angles must be different".to_string(),
1436 vec![args.source_range],
1437 )));
1438 }
1439 let ccw = a_start < a_end;
1440
1441 exec_state
1442 .batch_modeling_cmd(
1443 ModelingCmdMeta::from_args_id(args, id),
1444 ModelingCmd::from(mcmd::ExtendPath {
1445 path: sketch.id.into(),
1446 segment: PathSegment::Arc {
1447 start: a_start,
1448 end: a_end,
1449 center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1450 radius: LengthUnit(
1451 crate::execution::types::adjust_length(from.units, radius, UnitLength::Millimeters).0,
1452 ),
1453 relative: false,
1454 },
1455 }),
1456 )
1457 .await?;
1458
1459 let current_path = Path::Arc {
1460 base: BasePath {
1461 from: from.ignore_units(),
1462 to: end,
1463 tag: tag.clone(),
1464 units: from.units,
1465 geo_meta: GeoMeta {
1466 id,
1467 metadata: args.source_range.into(),
1468 },
1469 },
1470 center,
1471 radius,
1472 ccw,
1473 };
1474
1475 let mut new_sketch = sketch;
1476 if let Some(tag) = &tag {
1477 new_sketch.add_tag(tag, ¤t_path, exec_state);
1478 }
1479
1480 new_sketch.paths.push(current_path);
1481
1482 Ok(new_sketch)
1483}
1484
1485pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1487 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1488 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1489 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1490 let radius = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1491 let diameter = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1492 let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
1493 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1494
1495 let new_sketch = inner_tangential_arc(
1496 sketch,
1497 end_absolute,
1498 end,
1499 radius,
1500 diameter,
1501 angle,
1502 tag,
1503 exec_state,
1504 args,
1505 )
1506 .await?;
1507 Ok(KclValue::Sketch {
1508 value: Box::new(new_sketch),
1509 })
1510}
1511
1512#[allow(clippy::too_many_arguments)]
1513async fn inner_tangential_arc(
1514 sketch: Sketch,
1515 end_absolute: Option<[TyF64; 2]>,
1516 end: Option<[TyF64; 2]>,
1517 radius: Option<TyF64>,
1518 diameter: Option<TyF64>,
1519 angle: Option<TyF64>,
1520 tag: Option<TagNode>,
1521 exec_state: &mut ExecState,
1522 args: Args,
1523) -> Result<Sketch, KclError> {
1524 match (end_absolute, end, radius, diameter, angle) {
1525 (Some(point), None, None, None, None) => {
1526 inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1527 }
1528 (None, Some(point), None, None, None) => {
1529 inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1530 }
1531 (None, None, radius, diameter, Some(angle)) => {
1532 let radius = get_radius(radius, diameter, args.source_range)?;
1533 let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1534 inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1535 }
1536 (Some(_), Some(_), None, None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
1537 "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1538 vec![args.source_range],
1539 ))),
1540 (_, _, _, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1541 "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1542 vec![args.source_range],
1543 ))),
1544 }
1545}
1546
1547#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1549#[ts(export)]
1550#[serde(rename_all = "camelCase", untagged)]
1551pub enum TangentialArcData {
1552 RadiusAndOffset {
1553 radius: TyF64,
1556 offset: TyF64,
1558 },
1559}
1560
1561async fn inner_tangential_arc_radius_angle(
1568 data: TangentialArcData,
1569 sketch: Sketch,
1570 tag: Option<TagNode>,
1571 exec_state: &mut ExecState,
1572 args: Args,
1573) -> Result<Sketch, KclError> {
1574 let from: Point2d = sketch.current_pen_position()?;
1575 let tangent_info = sketch.get_tangential_info_from_paths(); let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1578
1579 let id = exec_state.next_uuid();
1580
1581 let (center, to, ccw) = match data {
1582 TangentialArcData::RadiusAndOffset { radius, offset } => {
1583 let offset = Angle::from_degrees(offset.to_degrees());
1585
1586 let previous_end_tangent = Angle::from_radians(libm::atan2(
1589 from.y - tan_previous_point[1],
1590 from.x - tan_previous_point[0],
1591 ));
1592 let ccw = offset.to_degrees() > 0.0;
1595 let tangent_to_arc_start_angle = if ccw {
1596 Angle::from_degrees(-90.0)
1598 } else {
1599 Angle::from_degrees(90.0)
1601 };
1602 let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1605 let end_angle = start_angle + offset;
1606 let (center, to) = arc_center_and_end(
1607 from.ignore_units(),
1608 start_angle,
1609 end_angle,
1610 radius.to_length_units(from.units),
1611 );
1612
1613 exec_state
1614 .batch_modeling_cmd(
1615 ModelingCmdMeta::from_args_id(&args, id),
1616 ModelingCmd::from(mcmd::ExtendPath {
1617 path: sketch.id.into(),
1618 segment: PathSegment::TangentialArc {
1619 radius: LengthUnit(radius.to_mm()),
1620 offset,
1621 },
1622 }),
1623 )
1624 .await?;
1625 (center, to, ccw)
1626 }
1627 };
1628
1629 let current_path = Path::TangentialArc {
1630 ccw,
1631 center,
1632 base: BasePath {
1633 from: from.ignore_units(),
1634 to,
1635 tag: tag.clone(),
1636 units: sketch.units,
1637 geo_meta: GeoMeta {
1638 id,
1639 metadata: args.source_range.into(),
1640 },
1641 },
1642 };
1643
1644 let mut new_sketch = sketch;
1645 if let Some(tag) = &tag {
1646 new_sketch.add_tag(tag, ¤t_path, exec_state);
1647 }
1648
1649 new_sketch.paths.push(current_path);
1650
1651 Ok(new_sketch)
1652}
1653
1654fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
1656 ModelingCmd::from(mcmd::ExtendPath {
1657 path: sketch.id.into(),
1658 segment: PathSegment::TangentialArcTo {
1659 angle_snap_increment: None,
1660 to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
1661 .with_z(0.0)
1662 .map(LengthUnit),
1663 },
1664 })
1665}
1666
1667async fn inner_tangential_arc_to_point(
1668 sketch: Sketch,
1669 point: [TyF64; 2],
1670 is_absolute: bool,
1671 tag: Option<TagNode>,
1672 exec_state: &mut ExecState,
1673 args: Args,
1674) -> Result<Sketch, KclError> {
1675 let from: Point2d = sketch.current_pen_position()?;
1676 let tangent_info = sketch.get_tangential_info_from_paths();
1677 let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1678
1679 let point = point_to_len_unit(point, from.units);
1680
1681 let to = if is_absolute {
1682 point
1683 } else {
1684 [from.x + point[0], from.y + point[1]]
1685 };
1686 let [to_x, to_y] = to;
1687 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1688 arc_start_point: [from.x, from.y],
1689 arc_end_point: [to_x, to_y],
1690 tan_previous_point,
1691 obtuse: true,
1692 });
1693
1694 if result.center[0].is_infinite() {
1695 return Err(KclError::new_semantic(KclErrorDetails::new(
1696 "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
1697 .to_owned(),
1698 vec![args.source_range],
1699 )));
1700 } else if result.center[1].is_infinite() {
1701 return Err(KclError::new_semantic(KclErrorDetails::new(
1702 "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
1703 .to_owned(),
1704 vec![args.source_range],
1705 )));
1706 }
1707
1708 let delta = if is_absolute {
1709 [to_x - from.x, to_y - from.y]
1710 } else {
1711 point
1712 };
1713 let id = exec_state.next_uuid();
1714 exec_state
1715 .batch_modeling_cmd(ModelingCmdMeta::from_args_id(&args, id), tan_arc_to(&sketch, delta))
1716 .await?;
1717
1718 let current_path = Path::TangentialArcTo {
1719 base: BasePath {
1720 from: from.ignore_units(),
1721 to,
1722 tag: tag.clone(),
1723 units: sketch.units,
1724 geo_meta: GeoMeta {
1725 id,
1726 metadata: args.source_range.into(),
1727 },
1728 },
1729 center: result.center,
1730 ccw: result.ccw > 0,
1731 };
1732
1733 let mut new_sketch = sketch;
1734 if let Some(tag) = &tag {
1735 new_sketch.add_tag(tag, ¤t_path, exec_state);
1736 }
1737
1738 new_sketch.paths.push(current_path);
1739
1740 Ok(new_sketch)
1741}
1742
1743pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1745 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1746 let control1 = args.get_kw_arg_opt("control1", &RuntimeType::point2d(), exec_state)?;
1747 let control2 = args.get_kw_arg_opt("control2", &RuntimeType::point2d(), exec_state)?;
1748 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1749 let control1_absolute = args.get_kw_arg_opt("control1Absolute", &RuntimeType::point2d(), exec_state)?;
1750 let control2_absolute = args.get_kw_arg_opt("control2Absolute", &RuntimeType::point2d(), exec_state)?;
1751 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1752 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1753
1754 let new_sketch = inner_bezier_curve(
1755 sketch,
1756 control1,
1757 control2,
1758 end,
1759 control1_absolute,
1760 control2_absolute,
1761 end_absolute,
1762 tag,
1763 exec_state,
1764 args,
1765 )
1766 .await?;
1767 Ok(KclValue::Sketch {
1768 value: Box::new(new_sketch),
1769 })
1770}
1771
1772#[allow(clippy::too_many_arguments)]
1773async fn inner_bezier_curve(
1774 sketch: Sketch,
1775 control1: Option<[TyF64; 2]>,
1776 control2: Option<[TyF64; 2]>,
1777 end: Option<[TyF64; 2]>,
1778 control1_absolute: Option<[TyF64; 2]>,
1779 control2_absolute: Option<[TyF64; 2]>,
1780 end_absolute: Option<[TyF64; 2]>,
1781 tag: Option<TagNode>,
1782 exec_state: &mut ExecState,
1783 args: Args,
1784) -> Result<Sketch, KclError> {
1785 let from = sketch.current_pen_position()?;
1786 let id = exec_state.next_uuid();
1787
1788 let to = match (
1789 control1,
1790 control2,
1791 end,
1792 control1_absolute,
1793 control2_absolute,
1794 end_absolute,
1795 ) {
1796 (Some(control1), Some(control2), Some(end), None, None, None) => {
1798 let delta = end.clone();
1799 let to = [
1800 from.x + end[0].to_length_units(from.units),
1801 from.y + end[1].to_length_units(from.units),
1802 ];
1803
1804 exec_state
1805 .batch_modeling_cmd(
1806 ModelingCmdMeta::from_args_id(&args, id),
1807 ModelingCmd::from(mcmd::ExtendPath {
1808 path: sketch.id.into(),
1809 segment: PathSegment::Bezier {
1810 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1811 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1812 end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
1813 relative: true,
1814 },
1815 }),
1816 )
1817 .await?;
1818 to
1819 }
1820 (None, None, None, Some(control1), Some(control2), Some(end)) => {
1822 let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
1823 exec_state
1824 .batch_modeling_cmd(
1825 ModelingCmdMeta::from_args_id(&args, id),
1826 ModelingCmd::from(mcmd::ExtendPath {
1827 path: sketch.id.into(),
1828 segment: PathSegment::Bezier {
1829 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1830 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1831 end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
1832 relative: false,
1833 },
1834 }),
1835 )
1836 .await?;
1837 to
1838 }
1839 _ => {
1840 return Err(KclError::new_semantic(KclErrorDetails::new(
1841 "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
1842 vec![args.source_range],
1843 )));
1844 }
1845 };
1846
1847 let current_path = Path::ToPoint {
1848 base: BasePath {
1849 from: from.ignore_units(),
1850 to,
1851 tag: tag.clone(),
1852 units: sketch.units,
1853 geo_meta: GeoMeta {
1854 id,
1855 metadata: args.source_range.into(),
1856 },
1857 },
1858 };
1859
1860 let mut new_sketch = sketch;
1861 if let Some(tag) = &tag {
1862 new_sketch.add_tag(tag, ¤t_path, exec_state);
1863 }
1864
1865 new_sketch.paths.push(current_path);
1866
1867 Ok(new_sketch)
1868}
1869
1870pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1872 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1873
1874 let tool: Vec<Sketch> = args.get_kw_arg(
1875 "tool",
1876 &RuntimeType::Array(
1877 Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
1878 ArrayLen::Minimum(1),
1879 ),
1880 exec_state,
1881 )?;
1882
1883 let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
1884 Ok(KclValue::Sketch {
1885 value: Box::new(new_sketch),
1886 })
1887}
1888
1889async fn inner_subtract_2d(
1890 mut sketch: Sketch,
1891 tool: Vec<Sketch>,
1892 exec_state: &mut ExecState,
1893 args: Args,
1894) -> Result<Sketch, KclError> {
1895 for hole_sketch in tool {
1896 exec_state
1897 .batch_modeling_cmd(
1898 ModelingCmdMeta::from(&args),
1899 ModelingCmd::from(mcmd::Solid2dAddHole {
1900 object_id: sketch.id,
1901 hole_id: hole_sketch.id,
1902 }),
1903 )
1904 .await?;
1905
1906 exec_state
1909 .batch_modeling_cmd(
1910 ModelingCmdMeta::from(&args),
1911 ModelingCmd::from(mcmd::ObjectVisible {
1912 object_id: hole_sketch.id,
1913 hidden: true,
1914 }),
1915 )
1916 .await?;
1917
1918 sketch.inner_paths.extend_from_slice(&hole_sketch.paths);
1923 }
1924
1925 Ok(sketch)
1928}
1929
1930pub async fn elliptic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1932 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
1933 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
1934 let major_radius = args.get_kw_arg("majorRadius", &RuntimeType::num_any(), exec_state)?;
1935 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::num_any(), exec_state)?;
1936
1937 let elliptic_point = inner_elliptic_point(x, y, major_radius, minor_radius, &args).await?;
1938
1939 args.make_kcl_val_from_point(elliptic_point, exec_state.length_unit().into())
1940}
1941
1942async fn inner_elliptic_point(
1943 x: Option<TyF64>,
1944 y: Option<TyF64>,
1945 major_radius: TyF64,
1946 minor_radius: TyF64,
1947 args: &Args,
1948) -> Result<[f64; 2], KclError> {
1949 let major_radius = major_radius.n;
1950 let minor_radius = minor_radius.n;
1951 if let Some(x) = x {
1952 if x.n.abs() > major_radius {
1953 Err(KclError::Type {
1954 details: KclErrorDetails::new(
1955 format!(
1956 "Invalid input. The x value, {}, cannot be larger than the major radius {}.",
1957 x.n, major_radius
1958 )
1959 .to_owned(),
1960 vec![args.source_range],
1961 ),
1962 })
1963 } else {
1964 Ok((
1965 x.n,
1966 minor_radius * (1.0 - x.n.powf(2.0) / major_radius.powf(2.0)).sqrt(),
1967 )
1968 .into())
1969 }
1970 } else if let Some(y) = y {
1971 if y.n > minor_radius {
1972 Err(KclError::Type {
1973 details: KclErrorDetails::new(
1974 format!(
1975 "Invalid input. The y value, {}, cannot be larger than the minor radius {}.",
1976 y.n, minor_radius
1977 )
1978 .to_owned(),
1979 vec![args.source_range],
1980 ),
1981 })
1982 } else {
1983 Ok((
1984 major_radius * (1.0 - y.n.powf(2.0) / minor_radius.powf(2.0)).sqrt(),
1985 y.n,
1986 )
1987 .into())
1988 }
1989 } else {
1990 Err(KclError::Type {
1991 details: KclErrorDetails::new(
1992 "Invalid input. Must have either x or y, you cannot have both or neither.".to_owned(),
1993 vec![args.source_range],
1994 ),
1995 })
1996 }
1997}
1998
1999pub async fn elliptic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2001 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2002
2003 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
2004 let angle_start = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
2005 let angle_end = args.get_kw_arg("angleEnd", &RuntimeType::degrees(), exec_state)?;
2006 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
2007 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
2008 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
2009 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2010
2011 let new_sketch = inner_elliptic(
2012 sketch,
2013 center,
2014 angle_start,
2015 angle_end,
2016 major_radius,
2017 major_axis,
2018 minor_radius,
2019 tag,
2020 exec_state,
2021 args,
2022 )
2023 .await?;
2024 Ok(KclValue::Sketch {
2025 value: Box::new(new_sketch),
2026 })
2027}
2028
2029#[allow(clippy::too_many_arguments)]
2030pub(crate) async fn inner_elliptic(
2031 sketch: Sketch,
2032 center: [TyF64; 2],
2033 angle_start: TyF64,
2034 angle_end: TyF64,
2035 major_radius: Option<TyF64>,
2036 major_axis: Option<[TyF64; 2]>,
2037 minor_radius: TyF64,
2038 tag: Option<TagNode>,
2039 exec_state: &mut ExecState,
2040 args: Args,
2041) -> Result<Sketch, KclError> {
2042 let from: Point2d = sketch.current_pen_position()?;
2043 let id = exec_state.next_uuid();
2044
2045 let (center_u, _) = untype_point(center);
2046
2047 let major_axis = match (major_axis, major_radius) {
2048 (Some(_), Some(_)) | (None, None) => {
2049 return Err(KclError::new_type(KclErrorDetails::new(
2050 "Provide either `majorAxis` or `majorRadius`.".to_string(),
2051 vec![args.source_range],
2052 )));
2053 }
2054 (Some(major_axis), None) => major_axis,
2055 (None, Some(major_radius)) => [
2056 major_radius.clone(),
2057 TyF64 {
2058 n: 0.0,
2059 ty: major_radius.ty,
2060 },
2061 ],
2062 };
2063 let start_angle = Angle::from_degrees(angle_start.to_degrees());
2064 let end_angle = Angle::from_degrees(angle_end.to_degrees());
2065 let major_axis_magnitude = (major_axis[0].to_length_units(from.units) * major_axis[0].to_length_units(from.units)
2066 + major_axis[1].to_length_units(from.units) * major_axis[1].to_length_units(from.units))
2067 .sqrt();
2068 let to = [
2069 major_axis_magnitude * libm::cos(end_angle.to_radians()),
2070 minor_radius.to_length_units(from.units) * libm::sin(end_angle.to_radians()),
2071 ];
2072 let major_axis_angle = libm::atan2(major_axis[1].n, major_axis[0].n);
2073
2074 let point = [
2075 center_u[0] + to[0] * libm::cos(major_axis_angle) - to[1] * libm::sin(major_axis_angle),
2076 center_u[1] + to[0] * libm::sin(major_axis_angle) + to[1] * libm::cos(major_axis_angle),
2077 ];
2078
2079 let axis = major_axis.map(|x| x.to_mm());
2080 exec_state
2081 .batch_modeling_cmd(
2082 ModelingCmdMeta::from_args_id(&args, id),
2083 ModelingCmd::from(mcmd::ExtendPath {
2084 path: sketch.id.into(),
2085 segment: PathSegment::Ellipse {
2086 center: KPoint2d::from(untyped_point_to_mm(center_u, from.units)).map(LengthUnit),
2087 major_axis: axis.map(LengthUnit).into(),
2088 minor_radius: LengthUnit(minor_radius.to_mm()),
2089 start_angle,
2090 end_angle,
2091 },
2092 }),
2093 )
2094 .await?;
2095
2096 let current_path = Path::Ellipse {
2097 ccw: start_angle < end_angle,
2098 center: center_u,
2099 major_axis: axis,
2100 minor_radius: minor_radius.to_mm(),
2101 base: BasePath {
2102 from: from.ignore_units(),
2103 to: point,
2104 tag: tag.clone(),
2105 units: sketch.units,
2106 geo_meta: GeoMeta {
2107 id,
2108 metadata: args.source_range.into(),
2109 },
2110 },
2111 };
2112 let mut new_sketch = sketch;
2113 if let Some(tag) = &tag {
2114 new_sketch.add_tag(tag, ¤t_path, exec_state);
2115 }
2116
2117 new_sketch.paths.push(current_path);
2118
2119 Ok(new_sketch)
2120}
2121
2122pub async fn hyperbolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2124 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2125 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2126 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::num_any(), exec_state)?;
2127 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::num_any(), exec_state)?;
2128
2129 let hyperbolic_point = inner_hyperbolic_point(x, y, semi_major, semi_minor, &args).await?;
2130
2131 args.make_kcl_val_from_point(hyperbolic_point, exec_state.length_unit().into())
2132}
2133
2134async fn inner_hyperbolic_point(
2135 x: Option<TyF64>,
2136 y: Option<TyF64>,
2137 semi_major: TyF64,
2138 semi_minor: TyF64,
2139 args: &Args,
2140) -> Result<[f64; 2], KclError> {
2141 let semi_major = semi_major.n;
2142 let semi_minor = semi_minor.n;
2143 if let Some(x) = x {
2144 if x.n.abs() < semi_major {
2145 Err(KclError::Type {
2146 details: KclErrorDetails::new(
2147 format!(
2148 "Invalid input. The x value, {}, cannot be less than the semi major value, {}.",
2149 x.n, semi_major
2150 )
2151 .to_owned(),
2152 vec![args.source_range],
2153 ),
2154 })
2155 } else {
2156 Ok((x.n, semi_minor * (x.n.powf(2.0) / semi_major.powf(2.0) - 1.0).sqrt()).into())
2157 }
2158 } else if let Some(y) = y {
2159 Ok((semi_major * (y.n.powf(2.0) / semi_minor.powf(2.0) + 1.0).sqrt(), y.n).into())
2160 } else {
2161 Err(KclError::Type {
2162 details: KclErrorDetails::new(
2163 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2164 vec![args.source_range],
2165 ),
2166 })
2167 }
2168}
2169
2170pub async fn hyperbolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2172 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2173
2174 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::length(), exec_state)?;
2175 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::length(), exec_state)?;
2176 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2177 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2178 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2179 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2180 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2181
2182 let new_sketch = inner_hyperbolic(
2183 sketch,
2184 semi_major,
2185 semi_minor,
2186 interior,
2187 end,
2188 interior_absolute,
2189 end_absolute,
2190 tag,
2191 exec_state,
2192 args,
2193 )
2194 .await?;
2195 Ok(KclValue::Sketch {
2196 value: Box::new(new_sketch),
2197 })
2198}
2199
2200fn hyperbolic_tangent(point: Point2d, semi_major: f64, semi_minor: f64) -> [f64; 2] {
2202 (point.y * semi_major.powf(2.0), point.x * semi_minor.powf(2.0)).into()
2203}
2204
2205#[allow(clippy::too_many_arguments)]
2206pub(crate) async fn inner_hyperbolic(
2207 sketch: Sketch,
2208 semi_major: TyF64,
2209 semi_minor: TyF64,
2210 interior: Option<[TyF64; 2]>,
2211 end: Option<[TyF64; 2]>,
2212 interior_absolute: Option<[TyF64; 2]>,
2213 end_absolute: Option<[TyF64; 2]>,
2214 tag: Option<TagNode>,
2215 exec_state: &mut ExecState,
2216 args: Args,
2217) -> Result<Sketch, KclError> {
2218 let from = sketch.current_pen_position()?;
2219 let id = exec_state.next_uuid();
2220
2221 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2222 (Some(interior), Some(end), None, None) => (interior, end, true),
2223 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2224 _ => return Err(KclError::Type {
2225 details: KclErrorDetails::new(
2226 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2227 .to_owned(),
2228 vec![args.source_range],
2229 ),
2230 }),
2231 };
2232
2233 let (interior, _) = untype_point(interior);
2234 let (end, _) = untype_point(end);
2235 let end_point = Point2d {
2236 x: end[0],
2237 y: end[1],
2238 units: from.units,
2239 };
2240
2241 let semi_major_u = semi_major.to_length_units(from.units);
2242 let semi_minor_u = semi_minor.to_length_units(from.units);
2243
2244 let start_tangent = hyperbolic_tangent(from, semi_major_u, semi_minor_u);
2245 let end_tangent = hyperbolic_tangent(end_point, semi_major_u, semi_minor_u);
2246
2247 exec_state
2248 .batch_modeling_cmd(
2249 ModelingCmdMeta::from_args_id(&args, id),
2250 ModelingCmd::from(mcmd::ExtendPath {
2251 path: sketch.id.into(),
2252 segment: PathSegment::ConicTo {
2253 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2254 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2255 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2256 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2257 relative,
2258 },
2259 }),
2260 )
2261 .await?;
2262
2263 let current_path = Path::Conic {
2264 base: BasePath {
2265 from: from.ignore_units(),
2266 to: end,
2267 tag: tag.clone(),
2268 units: sketch.units,
2269 geo_meta: GeoMeta {
2270 id,
2271 metadata: args.source_range.into(),
2272 },
2273 },
2274 };
2275
2276 let mut new_sketch = sketch;
2277 if let Some(tag) = &tag {
2278 new_sketch.add_tag(tag, ¤t_path, exec_state);
2279 }
2280
2281 new_sketch.paths.push(current_path);
2282
2283 Ok(new_sketch)
2284}
2285
2286pub async fn parabolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2288 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2289 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2290 let coefficients = args.get_kw_arg(
2291 "coefficients",
2292 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2293 exec_state,
2294 )?;
2295
2296 let parabolic_point = inner_parabolic_point(x, y, &coefficients, &args).await?;
2297
2298 args.make_kcl_val_from_point(parabolic_point, exec_state.length_unit().into())
2299}
2300
2301async fn inner_parabolic_point(
2302 x: Option<TyF64>,
2303 y: Option<TyF64>,
2304 coefficients: &[TyF64; 3],
2305 args: &Args,
2306) -> Result<[f64; 2], KclError> {
2307 let a = coefficients[0].n;
2308 let b = coefficients[1].n;
2309 let c = coefficients[2].n;
2310 if let Some(x) = x {
2311 Ok((x.n, a * x.n.powf(2.0) + b * x.n + c).into())
2312 } else if let Some(y) = y {
2313 let det = (b.powf(2.0) - 4.0 * a * (c - y.n)).sqrt();
2314 Ok(((-b + det) / (2.0 * a), y.n).into())
2315 } else {
2316 Err(KclError::Type {
2317 details: KclErrorDetails::new(
2318 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2319 vec![args.source_range],
2320 ),
2321 })
2322 }
2323}
2324
2325pub async fn parabolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2327 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2328
2329 let coefficients = args.get_kw_arg_opt(
2330 "coefficients",
2331 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2332 exec_state,
2333 )?;
2334 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2335 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2336 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2337 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2338 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2339
2340 let new_sketch = inner_parabolic(
2341 sketch,
2342 coefficients,
2343 interior,
2344 end,
2345 interior_absolute,
2346 end_absolute,
2347 tag,
2348 exec_state,
2349 args,
2350 )
2351 .await?;
2352 Ok(KclValue::Sketch {
2353 value: Box::new(new_sketch),
2354 })
2355}
2356
2357fn parabolic_tangent(point: Point2d, a: f64, b: f64) -> [f64; 2] {
2358 (1.0, 2.0 * a * point.x + b).into()
2361}
2362
2363#[allow(clippy::too_many_arguments)]
2364pub(crate) async fn inner_parabolic(
2365 sketch: Sketch,
2366 coefficients: Option<[TyF64; 3]>,
2367 interior: Option<[TyF64; 2]>,
2368 end: Option<[TyF64; 2]>,
2369 interior_absolute: Option<[TyF64; 2]>,
2370 end_absolute: Option<[TyF64; 2]>,
2371 tag: Option<TagNode>,
2372 exec_state: &mut ExecState,
2373 args: Args,
2374) -> Result<Sketch, KclError> {
2375 let from = sketch.current_pen_position()?;
2376 let id = exec_state.next_uuid();
2377
2378 if (coefficients.is_some() && interior.is_some()) || (coefficients.is_none() && interior.is_none()) {
2379 return Err(KclError::Type {
2380 details: KclErrorDetails::new(
2381 "Invalid combination of arguments. Either provide (a, b, c) or (interior)".to_owned(),
2382 vec![args.source_range],
2383 ),
2384 });
2385 }
2386
2387 let (interior, end, relative) = match (coefficients.clone(), interior, end, interior_absolute, end_absolute) {
2388 (None, Some(interior), Some(end), None, None) => {
2389 let (interior, _) = untype_point(interior);
2390 let (end, _) = untype_point(end);
2391 (interior,end, true)
2392 },
2393 (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
2394 let (interior_absolute, _) = untype_point(interior_absolute);
2395 let (end_absolute, _) = untype_point(end_absolute);
2396 (interior_absolute, end_absolute, false)
2397 }
2398 (Some(coefficients), _, Some(end), _, _) => {
2399 let (end, _) = untype_point(end);
2400 let interior =
2401 inner_parabolic_point(
2402 Some(TyF64::count(0.5 * (from.x + end[0]))),
2403 None,
2404 &coefficients,
2405 &args,
2406 )
2407 .await?;
2408 (interior, end, true)
2409 }
2410 (Some(coefficients), _, _, _, Some(end)) => {
2411 let (end, _) = untype_point(end);
2412 let interior =
2413 inner_parabolic_point(
2414 Some(TyF64::count(0.5 * (from.x + end[0]))),
2415 None,
2416 &coefficients,
2417 &args,
2418 )
2419 .await?;
2420 (interior, end, false)
2421 }
2422 _ => return
2423 Err(KclError::Type{details: KclErrorDetails::new(
2424 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute) if coefficients are not provided."
2425 .to_owned(),
2426 vec![args.source_range],
2427 )}),
2428 };
2429
2430 let end_point = Point2d {
2431 x: end[0],
2432 y: end[1],
2433 units: from.units,
2434 };
2435
2436 let (a, b, _c) = if let Some([a, b, c]) = coefficients {
2437 (a.n, b.n, c.n)
2438 } else {
2439 let denom = (from.x - interior[0]) * (from.x - end_point.x) * (interior[0] - end_point.x);
2441 let a = (end_point.x * (interior[1] - from.y)
2442 + interior[0] * (from.y - end_point.y)
2443 + from.x * (end_point.y - interior[1]))
2444 / denom;
2445 let b = (end_point.x.powf(2.0) * (from.y - interior[1])
2446 + interior[0].powf(2.0) * (end_point.y - from.y)
2447 + from.x.powf(2.0) * (interior[1] - end_point.y))
2448 / denom;
2449 let c = (interior[0] * end_point.x * (interior[0] - end_point.x) * from.y
2450 + end_point.x * from.x * (end_point.x - from.x) * interior[1]
2451 + from.x * interior[0] * (from.x - interior[0]) * end_point.y)
2452 / denom;
2453
2454 (a, b, c)
2455 };
2456
2457 let start_tangent = parabolic_tangent(from, a, b);
2458 let end_tangent = parabolic_tangent(end_point, a, b);
2459
2460 exec_state
2461 .batch_modeling_cmd(
2462 ModelingCmdMeta::from_args_id(&args, id),
2463 ModelingCmd::from(mcmd::ExtendPath {
2464 path: sketch.id.into(),
2465 segment: PathSegment::ConicTo {
2466 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2467 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2468 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2469 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2470 relative,
2471 },
2472 }),
2473 )
2474 .await?;
2475
2476 let current_path = Path::Conic {
2477 base: BasePath {
2478 from: from.ignore_units(),
2479 to: end,
2480 tag: tag.clone(),
2481 units: sketch.units,
2482 geo_meta: GeoMeta {
2483 id,
2484 metadata: args.source_range.into(),
2485 },
2486 },
2487 };
2488
2489 let mut new_sketch = sketch;
2490 if let Some(tag) = &tag {
2491 new_sketch.add_tag(tag, ¤t_path, exec_state);
2492 }
2493
2494 new_sketch.paths.push(current_path);
2495
2496 Ok(new_sketch)
2497}
2498
2499fn conic_tangent(coefficients: [f64; 6], point: [f64; 2]) -> [f64; 2] {
2500 let [a, b, c, d, e, _] = coefficients;
2501
2502 (
2503 c * point[0] + 2.0 * b * point[1] + e,
2504 -(2.0 * a * point[0] + c * point[1] + d),
2505 )
2506 .into()
2507}
2508
2509pub async fn conic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2511 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2512
2513 let start_tangent = args.get_kw_arg_opt("startTangent", &RuntimeType::point2d(), exec_state)?;
2514 let end_tangent = args.get_kw_arg_opt("endTangent", &RuntimeType::point2d(), exec_state)?;
2515 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2516 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2517 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2518 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2519 let coefficients = args.get_kw_arg_opt(
2520 "coefficients",
2521 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(6)),
2522 exec_state,
2523 )?;
2524 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2525
2526 let new_sketch = inner_conic(
2527 sketch,
2528 start_tangent,
2529 end,
2530 end_tangent,
2531 interior,
2532 coefficients,
2533 interior_absolute,
2534 end_absolute,
2535 tag,
2536 exec_state,
2537 args,
2538 )
2539 .await?;
2540 Ok(KclValue::Sketch {
2541 value: Box::new(new_sketch),
2542 })
2543}
2544
2545#[allow(clippy::too_many_arguments)]
2546pub(crate) async fn inner_conic(
2547 sketch: Sketch,
2548 start_tangent: Option<[TyF64; 2]>,
2549 end: Option<[TyF64; 2]>,
2550 end_tangent: Option<[TyF64; 2]>,
2551 interior: Option<[TyF64; 2]>,
2552 coefficients: Option<[TyF64; 6]>,
2553 interior_absolute: Option<[TyF64; 2]>,
2554 end_absolute: Option<[TyF64; 2]>,
2555 tag: Option<TagNode>,
2556 exec_state: &mut ExecState,
2557 args: Args,
2558) -> Result<Sketch, KclError> {
2559 let from: Point2d = sketch.current_pen_position()?;
2560 let id = exec_state.next_uuid();
2561
2562 if (coefficients.is_some() && (start_tangent.is_some() || end_tangent.is_some()))
2563 || (coefficients.is_none() && (start_tangent.is_none() && end_tangent.is_none()))
2564 {
2565 return Err(KclError::Type {
2566 details: KclErrorDetails::new(
2567 "Invalid combination of arguments. Either provide coefficients or (startTangent, endTangent)"
2568 .to_owned(),
2569 vec![args.source_range],
2570 ),
2571 });
2572 }
2573
2574 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2575 (Some(interior), Some(end), None, None) => (interior, end, true),
2576 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2577 _ => return Err(KclError::Type {
2578 details: KclErrorDetails::new(
2579 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2580 .to_owned(),
2581 vec![args.source_range],
2582 ),
2583 }),
2584 };
2585
2586 let (end, _) = untype_array(end);
2587 let (interior, _) = untype_point(interior);
2588
2589 let (start_tangent, end_tangent) = if let Some(coeffs) = coefficients {
2590 let (coeffs, _) = untype_array(coeffs);
2591 (conic_tangent(coeffs, [from.x, from.y]), conic_tangent(coeffs, end))
2592 } else {
2593 let start = if let Some(start_tangent) = start_tangent {
2594 let (start, _) = untype_point(start_tangent);
2595 start
2596 } else {
2597 let previous_point = sketch
2598 .get_tangential_info_from_paths()
2599 .tan_previous_point(from.ignore_units());
2600 let from = from.ignore_units();
2601 [from[0] - previous_point[0], from[1] - previous_point[1]]
2602 };
2603
2604 let Some(end_tangent) = end_tangent else {
2605 return Err(KclError::new_semantic(KclErrorDetails::new(
2606 "You must either provide either `coefficients` or `endTangent`.".to_owned(),
2607 vec![args.source_range],
2608 )));
2609 };
2610 let (end_tan, _) = untype_point(end_tangent);
2611 (start, end_tan)
2612 };
2613
2614 exec_state
2615 .batch_modeling_cmd(
2616 ModelingCmdMeta::from_args_id(&args, id),
2617 ModelingCmd::from(mcmd::ExtendPath {
2618 path: sketch.id.into(),
2619 segment: PathSegment::ConicTo {
2620 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2621 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2622 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2623 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2624 relative,
2625 },
2626 }),
2627 )
2628 .await?;
2629
2630 let current_path = Path::Conic {
2631 base: BasePath {
2632 from: from.ignore_units(),
2633 to: end,
2634 tag: tag.clone(),
2635 units: sketch.units,
2636 geo_meta: GeoMeta {
2637 id,
2638 metadata: args.source_range.into(),
2639 },
2640 },
2641 };
2642
2643 let mut new_sketch = sketch;
2644 if let Some(tag) = &tag {
2645 new_sketch.add_tag(tag, ¤t_path, exec_state);
2646 }
2647
2648 new_sketch.paths.push(current_path);
2649
2650 Ok(new_sketch)
2651}
2652#[cfg(test)]
2653mod tests {
2654
2655 use pretty_assertions::assert_eq;
2656
2657 use crate::{
2658 execution::TagIdentifier,
2659 std::{sketch::PlaneData, utils::calculate_circle_center},
2660 };
2661
2662 #[test]
2663 fn test_deserialize_plane_data() {
2664 let data = PlaneData::XY;
2665 let mut str_json = serde_json::to_string(&data).unwrap();
2666 assert_eq!(str_json, "\"XY\"");
2667
2668 str_json = "\"YZ\"".to_string();
2669 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2670 assert_eq!(data, PlaneData::YZ);
2671
2672 str_json = "\"-YZ\"".to_string();
2673 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2674 assert_eq!(data, PlaneData::NegYZ);
2675
2676 str_json = "\"-xz\"".to_string();
2677 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2678 assert_eq!(data, PlaneData::NegXZ);
2679 }
2680
2681 #[test]
2682 fn test_deserialize_sketch_on_face_tag() {
2683 let data = "start";
2684 let mut str_json = serde_json::to_string(&data).unwrap();
2685 assert_eq!(str_json, "\"start\"");
2686
2687 str_json = "\"end\"".to_string();
2688 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2689 assert_eq!(
2690 data,
2691 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2692 );
2693
2694 str_json = serde_json::to_string(&TagIdentifier {
2695 value: "thing".to_string(),
2696 info: Vec::new(),
2697 meta: Default::default(),
2698 })
2699 .unwrap();
2700 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2701 assert_eq!(
2702 data,
2703 crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2704 value: "thing".to_string(),
2705 info: Vec::new(),
2706 meta: Default::default()
2707 }))
2708 );
2709
2710 str_json = "\"END\"".to_string();
2711 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2712 assert_eq!(
2713 data,
2714 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2715 );
2716
2717 str_json = "\"start\"".to_string();
2718 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2719 assert_eq!(
2720 data,
2721 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2722 );
2723
2724 str_json = "\"START\"".to_string();
2725 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2726 assert_eq!(
2727 data,
2728 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2729 );
2730 }
2731
2732 #[test]
2733 fn test_circle_center() {
2734 let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2735 assert_eq!(actual[0], 5.0);
2736 assert_eq!(actual[1], 0.0);
2737 }
2738}