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