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