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