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