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