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