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.ctx, args.source_range).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 ctx: &ExecutorContext,
1104 source_range: SourceRange,
1105) -> Result<(), KclError> {
1106 if plane.is_initialized() {
1107 return Ok(());
1108 }
1109 #[cfg(feature = "artifact-graph")]
1110 {
1111 if let Some(existing_object_id) = exec_state.scene_object_id_by_artifact_id(ArtifactId::new(plane.id)) {
1112 plane.object_id = Some(existing_object_id);
1113 return Ok(());
1114 }
1115 }
1116
1117 let clobber = false;
1118 let size = LengthUnit(60.0);
1119 let hide = Some(true);
1120 let cmd = if let Some(hide) = hide {
1121 mcmd::MakePlane::builder()
1122 .clobber(clobber)
1123 .origin(plane.info.origin.into())
1124 .size(size)
1125 .x_axis(plane.info.x_axis.into())
1126 .y_axis(plane.info.y_axis.into())
1127 .hide(hide)
1128 .build()
1129 } else {
1130 mcmd::MakePlane::builder()
1131 .clobber(clobber)
1132 .origin(plane.info.origin.into())
1133 .size(size)
1134 .x_axis(plane.info.x_axis.into())
1135 .y_axis(plane.info.y_axis.into())
1136 .build()
1137 };
1138 exec_state
1139 .batch_modeling_cmd(
1140 ModelingCmdMeta::with_id(exec_state, ctx, source_range, plane.id),
1141 ModelingCmd::from(cmd),
1142 )
1143 .await?;
1144 let plane_object_id = exec_state.next_object_id();
1145 #[cfg(feature = "artifact-graph")]
1146 {
1147 let plane_object = crate::front::Object {
1148 id: plane_object_id,
1149 kind: crate::front::ObjectKind::Plane(crate::front::Plane::Object(plane_object_id)),
1150 label: Default::default(),
1151 comments: Default::default(),
1152 artifact_id: ArtifactId::new(plane.id),
1153 source: source_range.into(),
1154 };
1155 exec_state.add_scene_object(plane_object, source_range);
1156 }
1157 plane.object_id = Some(plane_object_id);
1158
1159 Ok(())
1160}
1161
1162pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1164 let sketch_surface = args.get_unlabeled_kw_arg(
1165 "startProfileOn",
1166 &RuntimeType::Union(vec![RuntimeType::plane(), RuntimeType::face()]),
1167 exec_state,
1168 )?;
1169 let start: [TyF64; 2] = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
1170 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1171
1172 let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, &args.ctx, args.source_range).await?;
1173 Ok(KclValue::Sketch {
1174 value: Box::new(sketch),
1175 })
1176}
1177
1178pub(crate) async fn inner_start_profile(
1179 sketch_surface: SketchSurface,
1180 at: [TyF64; 2],
1181 tag: Option<TagNode>,
1182 exec_state: &mut ExecState,
1183 ctx: &ExecutorContext,
1184 source_range: SourceRange,
1185) -> Result<Sketch, KclError> {
1186 let id = exec_state.next_uuid();
1187 create_sketch(id, sketch_surface, at, tag, true, exec_state, ctx, source_range).await
1188}
1189
1190#[expect(clippy::too_many_arguments)]
1191pub(crate) async fn create_sketch(
1192 id: Uuid,
1193 sketch_surface: SketchSurface,
1194 at: [TyF64; 2],
1195 tag: Option<TagNode>,
1196 send_to_engine: bool,
1197 exec_state: &mut ExecState,
1198 ctx: &ExecutorContext,
1199 source_range: SourceRange,
1200) -> Result<Sketch, KclError> {
1201 match &sketch_surface {
1202 SketchSurface::Face(face) => {
1203 exec_state
1206 .flush_batch_for_solids(
1207 ModelingCmdMeta::new(exec_state, ctx, source_range),
1208 &[(*face.solid).clone()],
1209 )
1210 .await?;
1211 }
1212 SketchSurface::Plane(plane) if !plane.is_standard() => {
1213 exec_state
1216 .batch_end_cmd(
1217 ModelingCmdMeta::new(exec_state, ctx, source_range),
1218 ModelingCmd::from(mcmd::ObjectVisible::builder().object_id(plane.id).hidden(true).build()),
1219 )
1220 .await?;
1221 }
1222 _ => {}
1223 }
1224
1225 let path_id = id;
1226 let enable_sketch_id = exec_state.next_uuid();
1227 let move_pen_id = exec_state.next_uuid();
1228 let disable_sketch_id = exec_state.next_uuid();
1229 if send_to_engine {
1230 exec_state
1231 .batch_modeling_cmds(
1232 ModelingCmdMeta::new(exec_state, ctx, source_range),
1233 &[
1234 ModelingCmdReq {
1237 cmd: ModelingCmd::from(if let SketchSurface::Plane(plane) = &sketch_surface {
1238 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
1240 mcmd::EnableSketchMode::builder()
1241 .animated(false)
1242 .ortho(false)
1243 .entity_id(sketch_surface.id())
1244 .adjust_camera(false)
1245 .planar_normal(normal.into())
1246 .build()
1247 } else {
1248 mcmd::EnableSketchMode::builder()
1249 .animated(false)
1250 .ortho(false)
1251 .entity_id(sketch_surface.id())
1252 .adjust_camera(false)
1253 .build()
1254 }),
1255 cmd_id: enable_sketch_id.into(),
1256 },
1257 ModelingCmdReq {
1258 cmd: ModelingCmd::from(mcmd::StartPath::default()),
1259 cmd_id: path_id.into(),
1260 },
1261 ModelingCmdReq {
1262 cmd: ModelingCmd::from(
1263 mcmd::MovePathPen::builder()
1264 .path(path_id.into())
1265 .to(KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit))
1266 .build(),
1267 ),
1268 cmd_id: move_pen_id.into(),
1269 },
1270 ModelingCmdReq {
1271 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1272 cmd_id: disable_sketch_id.into(),
1273 },
1274 ],
1275 )
1276 .await?;
1277 }
1278
1279 let units = exec_state.length_unit();
1281 let to = point_to_len_unit(at, units);
1282 let current_path = BasePath {
1283 from: to,
1284 to,
1285 tag: tag.clone(),
1286 units,
1287 geo_meta: GeoMeta {
1288 id: move_pen_id,
1289 metadata: source_range.into(),
1290 },
1291 };
1292
1293 let mut sketch = Sketch {
1294 id: path_id,
1295 original_id: path_id,
1296 artifact_id: path_id.into(),
1297 on: sketch_surface,
1298 paths: vec![],
1299 inner_paths: vec![],
1300 units,
1301 mirror: Default::default(),
1302 clone: Default::default(),
1303 meta: vec![source_range.into()],
1304 tags: Default::default(),
1305 start: current_path.clone(),
1306 is_closed: ProfileClosed::No,
1307 };
1308 if let Some(tag) = &tag {
1309 let path = Path::Base { base: current_path };
1310 sketch.add_tag(tag, &path, exec_state, None);
1311 }
1312
1313 Ok(sketch)
1314}
1315
1316pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1318 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1319 let ty = sketch.units.into();
1320 let x = inner_profile_start_x(sketch)?;
1321 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1322}
1323
1324pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1325 Ok(profile.start.to[0])
1326}
1327
1328pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1330 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1331 let ty = sketch.units.into();
1332 let x = inner_profile_start_y(sketch)?;
1333 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1334}
1335
1336pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1337 Ok(profile.start.to[1])
1338}
1339
1340pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1342 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1343 let ty = sketch.units.into();
1344 let point = inner_profile_start(sketch)?;
1345 Ok(KclValue::from_point2d(point, ty, args.into()))
1346}
1347
1348pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1349 Ok(profile.start.to)
1350}
1351
1352pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1354 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1355 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1356 let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1357 Ok(KclValue::Sketch {
1358 value: Box::new(new_sketch),
1359 })
1360}
1361
1362pub(crate) async fn inner_close(
1363 sketch: Sketch,
1364 tag: Option<TagNode>,
1365 exec_state: &mut ExecState,
1366 args: Args,
1367) -> Result<Sketch, KclError> {
1368 if matches!(sketch.is_closed, ProfileClosed::Explicitly) {
1369 exec_state.warn(
1370 crate::CompilationError {
1371 source_range: args.source_range,
1372 message: "This sketch is already closed. Remove this unnecessary `close()` call".to_string(),
1373 suggestion: None,
1374 severity: crate::errors::Severity::Warning,
1375 tag: crate::errors::Tag::Unnecessary,
1376 },
1377 annotations::WARN_UNNECESSARY_CLOSE,
1378 );
1379 return Ok(sketch);
1380 }
1381 let from = sketch.current_pen_position()?;
1382 let to = point_to_len_unit(sketch.start.get_from(), from.units);
1383
1384 let id = exec_state.next_uuid();
1385
1386 exec_state
1387 .batch_modeling_cmd(
1388 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1389 ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
1390 )
1391 .await?;
1392
1393 let mut new_sketch = sketch;
1394
1395 let distance = ((from.x - to[0]).powi(2) + (from.y - to[1]).powi(2)).sqrt();
1396 if distance > super::EQUAL_POINTS_DIST_EPSILON {
1397 let current_path = Path::ToPoint {
1399 base: BasePath {
1400 from: from.ignore_units(),
1401 to,
1402 tag: tag.clone(),
1403 units: new_sketch.units,
1404 geo_meta: GeoMeta {
1405 id,
1406 metadata: args.source_range.into(),
1407 },
1408 },
1409 };
1410
1411 if let Some(tag) = &tag {
1412 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1413 }
1414 new_sketch.paths.push(current_path);
1415 } else if tag.is_some() {
1416 exec_state.warn(
1417 crate::CompilationError {
1418 source_range: args.source_range,
1419 message: "A tag declarator was specified, but no segment was created".to_string(),
1420 suggestion: None,
1421 severity: crate::errors::Severity::Warning,
1422 tag: crate::errors::Tag::Unnecessary,
1423 },
1424 annotations::WARN_UNUSED_TAGS,
1425 );
1426 }
1427
1428 new_sketch.is_closed = ProfileClosed::Explicitly;
1429
1430 Ok(new_sketch)
1431}
1432
1433pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1435 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1436
1437 let angle_start: Option<TyF64> = args.get_kw_arg_opt("angleStart", &RuntimeType::degrees(), exec_state)?;
1438 let angle_end: Option<TyF64> = args.get_kw_arg_opt("angleEnd", &RuntimeType::degrees(), exec_state)?;
1439 let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1440 let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1441 let end_absolute: Option<[TyF64; 2]> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1442 let interior_absolute: Option<[TyF64; 2]> =
1443 args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1444 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1445 let new_sketch = inner_arc(
1446 sketch,
1447 angle_start,
1448 angle_end,
1449 radius,
1450 diameter,
1451 interior_absolute,
1452 end_absolute,
1453 tag,
1454 exec_state,
1455 args,
1456 )
1457 .await?;
1458 Ok(KclValue::Sketch {
1459 value: Box::new(new_sketch),
1460 })
1461}
1462
1463#[allow(clippy::too_many_arguments)]
1464pub(crate) async fn inner_arc(
1465 sketch: Sketch,
1466 angle_start: Option<TyF64>,
1467 angle_end: Option<TyF64>,
1468 radius: Option<TyF64>,
1469 diameter: Option<TyF64>,
1470 interior_absolute: Option<[TyF64; 2]>,
1471 end_absolute: Option<[TyF64; 2]>,
1472 tag: Option<TagNode>,
1473 exec_state: &mut ExecState,
1474 args: Args,
1475) -> Result<Sketch, KclError> {
1476 let from: Point2d = sketch.current_pen_position()?;
1477 let id = exec_state.next_uuid();
1478
1479 match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1480 (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1481 let radius = get_radius(radius, diameter, args.source_range)?;
1482 relative_arc(id, exec_state, sketch, from, angle_start, angle_end, radius, tag, true, &args.ctx, args.source_range).await
1483 }
1484 (None, None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1485 absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1486 }
1487 _ => {
1488 Err(KclError::new_type(KclErrorDetails::new(
1489 "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)".to_owned(),
1490 vec![args.source_range],
1491 )))
1492 }
1493 }
1494}
1495
1496#[allow(clippy::too_many_arguments)]
1497pub async fn absolute_arc(
1498 args: &Args,
1499 id: uuid::Uuid,
1500 exec_state: &mut ExecState,
1501 sketch: Sketch,
1502 from: Point2d,
1503 interior_absolute: [TyF64; 2],
1504 end_absolute: [TyF64; 2],
1505 tag: Option<TagNode>,
1506) -> Result<Sketch, KclError> {
1507 exec_state
1509 .batch_modeling_cmd(
1510 ModelingCmdMeta::from_args_id(exec_state, args, id),
1511 ModelingCmd::from(
1512 mcmd::ExtendPath::builder()
1513 .path(sketch.id.into())
1514 .segment(PathSegment::ArcTo {
1515 end: kcmc::shared::Point3d {
1516 x: LengthUnit(end_absolute[0].to_mm()),
1517 y: LengthUnit(end_absolute[1].to_mm()),
1518 z: LengthUnit(0.0),
1519 },
1520 interior: kcmc::shared::Point3d {
1521 x: LengthUnit(interior_absolute[0].to_mm()),
1522 y: LengthUnit(interior_absolute[1].to_mm()),
1523 z: LengthUnit(0.0),
1524 },
1525 relative: false,
1526 })
1527 .build(),
1528 ),
1529 )
1530 .await?;
1531
1532 let start = [from.x, from.y];
1533 let end = point_to_len_unit(end_absolute, from.units);
1534 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1535
1536 let current_path = Path::ArcThreePoint {
1537 base: BasePath {
1538 from: from.ignore_units(),
1539 to: end,
1540 tag: tag.clone(),
1541 units: sketch.units,
1542 geo_meta: GeoMeta {
1543 id,
1544 metadata: args.source_range.into(),
1545 },
1546 },
1547 p1: start,
1548 p2: point_to_len_unit(interior_absolute, from.units),
1549 p3: end,
1550 };
1551
1552 let mut new_sketch = sketch;
1553 if let Some(tag) = &tag {
1554 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1555 }
1556 if loops_back_to_start {
1557 new_sketch.is_closed = ProfileClosed::Implicitly;
1558 }
1559
1560 new_sketch.paths.push(current_path);
1561
1562 Ok(new_sketch)
1563}
1564
1565#[allow(clippy::too_many_arguments)]
1566pub async fn relative_arc(
1567 id: uuid::Uuid,
1568 exec_state: &mut ExecState,
1569 sketch: Sketch,
1570 from: Point2d,
1571 angle_start: TyF64,
1572 angle_end: TyF64,
1573 radius: TyF64,
1574 tag: Option<TagNode>,
1575 send_to_engine: bool,
1576 ctx: &ExecutorContext,
1577 source_range: SourceRange,
1578) -> Result<Sketch, KclError> {
1579 let a_start = Angle::from_degrees(angle_start.to_degrees(exec_state, source_range));
1580 let a_end = Angle::from_degrees(angle_end.to_degrees(exec_state, source_range));
1581 let radius = radius.to_length_units(from.units);
1582 let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1583 if a_start == a_end {
1584 return Err(KclError::new_type(KclErrorDetails::new(
1585 "Arc start and end angles must be different".to_string(),
1586 vec![source_range],
1587 )));
1588 }
1589 let ccw = a_start < a_end;
1590
1591 if send_to_engine {
1592 exec_state
1593 .batch_modeling_cmd(
1594 ModelingCmdMeta::with_id(exec_state, ctx, source_range, id),
1595 ModelingCmd::from(
1596 mcmd::ExtendPath::builder()
1597 .path(sketch.id.into())
1598 .segment(PathSegment::Arc {
1599 start: a_start,
1600 end: a_end,
1601 center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1602 radius: LengthUnit(
1603 crate::execution::types::adjust_length(from.units, radius, UnitLength::Millimeters).0,
1604 ),
1605 relative: false,
1606 })
1607 .build(),
1608 ),
1609 )
1610 .await?;
1611 }
1612
1613 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1614 let current_path = Path::Arc {
1615 base: BasePath {
1616 from: from.ignore_units(),
1617 to: end,
1618 tag: tag.clone(),
1619 units: from.units,
1620 geo_meta: GeoMeta {
1621 id,
1622 metadata: source_range.into(),
1623 },
1624 },
1625 center,
1626 radius,
1627 ccw,
1628 };
1629
1630 let mut new_sketch = sketch;
1631 if let Some(tag) = &tag {
1632 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1633 }
1634 if loops_back_to_start {
1635 new_sketch.is_closed = ProfileClosed::Implicitly;
1636 }
1637
1638 new_sketch.paths.push(current_path);
1639
1640 Ok(new_sketch)
1641}
1642
1643pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1645 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1646 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1647 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1648 let radius = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1649 let diameter = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1650 let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
1651 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1652
1653 let new_sketch = inner_tangential_arc(
1654 sketch,
1655 end_absolute,
1656 end,
1657 radius,
1658 diameter,
1659 angle,
1660 tag,
1661 exec_state,
1662 args,
1663 )
1664 .await?;
1665 Ok(KclValue::Sketch {
1666 value: Box::new(new_sketch),
1667 })
1668}
1669
1670#[allow(clippy::too_many_arguments)]
1671async fn inner_tangential_arc(
1672 sketch: Sketch,
1673 end_absolute: Option<[TyF64; 2]>,
1674 end: Option<[TyF64; 2]>,
1675 radius: Option<TyF64>,
1676 diameter: Option<TyF64>,
1677 angle: Option<TyF64>,
1678 tag: Option<TagNode>,
1679 exec_state: &mut ExecState,
1680 args: Args,
1681) -> Result<Sketch, KclError> {
1682 match (end_absolute, end, radius, diameter, angle) {
1683 (Some(point), None, None, None, None) => {
1684 inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1685 }
1686 (None, Some(point), None, None, None) => {
1687 inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1688 }
1689 (None, None, radius, diameter, Some(angle)) => {
1690 let radius = get_radius(radius, diameter, args.source_range)?;
1691 let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1692 inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1693 }
1694 (Some(_), Some(_), None, None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
1695 "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1696 vec![args.source_range],
1697 ))),
1698 (_, _, _, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1699 "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1700 vec![args.source_range],
1701 ))),
1702 }
1703}
1704
1705#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1707#[ts(export)]
1708#[serde(rename_all = "camelCase", untagged)]
1709pub enum TangentialArcData {
1710 RadiusAndOffset {
1711 radius: TyF64,
1714 offset: TyF64,
1716 },
1717}
1718
1719async fn inner_tangential_arc_radius_angle(
1726 data: TangentialArcData,
1727 sketch: Sketch,
1728 tag: Option<TagNode>,
1729 exec_state: &mut ExecState,
1730 args: Args,
1731) -> Result<Sketch, KclError> {
1732 let from: Point2d = sketch.current_pen_position()?;
1733 let tangent_info = sketch.get_tangential_info_from_paths(); let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1736
1737 let id = exec_state.next_uuid();
1738
1739 let (center, to, ccw) = match data {
1740 TangentialArcData::RadiusAndOffset { radius, offset } => {
1741 let offset = Angle::from_degrees(offset.to_degrees(exec_state, args.source_range));
1743
1744 let previous_end_tangent = Angle::from_radians(libm::atan2(
1747 from.y - tan_previous_point[1],
1748 from.x - tan_previous_point[0],
1749 ));
1750 let ccw = offset.to_degrees() > 0.0;
1753 let tangent_to_arc_start_angle = if ccw {
1754 Angle::from_degrees(-90.0)
1756 } else {
1757 Angle::from_degrees(90.0)
1759 };
1760 let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1763 let end_angle = start_angle + offset;
1764 let (center, to) = arc_center_and_end(
1765 from.ignore_units(),
1766 start_angle,
1767 end_angle,
1768 radius.to_length_units(from.units),
1769 );
1770
1771 exec_state
1772 .batch_modeling_cmd(
1773 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1774 ModelingCmd::from(
1775 mcmd::ExtendPath::builder()
1776 .path(sketch.id.into())
1777 .segment(PathSegment::TangentialArc {
1778 radius: LengthUnit(radius.to_mm()),
1779 offset,
1780 })
1781 .build(),
1782 ),
1783 )
1784 .await?;
1785 (center, to, ccw)
1786 }
1787 };
1788 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1789
1790 let current_path = Path::TangentialArc {
1791 ccw,
1792 center,
1793 base: BasePath {
1794 from: from.ignore_units(),
1795 to,
1796 tag: tag.clone(),
1797 units: sketch.units,
1798 geo_meta: GeoMeta {
1799 id,
1800 metadata: args.source_range.into(),
1801 },
1802 },
1803 };
1804
1805 let mut new_sketch = sketch;
1806 if let Some(tag) = &tag {
1807 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1808 }
1809 if loops_back_to_start {
1810 new_sketch.is_closed = ProfileClosed::Implicitly;
1811 }
1812
1813 new_sketch.paths.push(current_path);
1814
1815 Ok(new_sketch)
1816}
1817
1818fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
1820 ModelingCmd::from(
1821 mcmd::ExtendPath::builder()
1822 .path(sketch.id.into())
1823 .segment(PathSegment::TangentialArcTo {
1824 angle_snap_increment: None,
1825 to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
1826 .with_z(0.0)
1827 .map(LengthUnit),
1828 })
1829 .build(),
1830 )
1831}
1832
1833async fn inner_tangential_arc_to_point(
1834 sketch: Sketch,
1835 point: [TyF64; 2],
1836 is_absolute: bool,
1837 tag: Option<TagNode>,
1838 exec_state: &mut ExecState,
1839 args: Args,
1840) -> Result<Sketch, KclError> {
1841 let from: Point2d = sketch.current_pen_position()?;
1842 let tangent_info = sketch.get_tangential_info_from_paths();
1843 let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1844
1845 let point = point_to_len_unit(point, from.units);
1846
1847 let to = if is_absolute {
1848 point
1849 } else {
1850 [from.x + point[0], from.y + point[1]]
1851 };
1852 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1853 let [to_x, to_y] = to;
1854 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1855 arc_start_point: [from.x, from.y],
1856 arc_end_point: [to_x, to_y],
1857 tan_previous_point,
1858 obtuse: true,
1859 });
1860
1861 if result.center[0].is_infinite() {
1862 return Err(KclError::new_semantic(KclErrorDetails::new(
1863 "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
1864 .to_owned(),
1865 vec![args.source_range],
1866 )));
1867 } else if result.center[1].is_infinite() {
1868 return Err(KclError::new_semantic(KclErrorDetails::new(
1869 "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
1870 .to_owned(),
1871 vec![args.source_range],
1872 )));
1873 }
1874
1875 let delta = if is_absolute {
1876 [to_x - from.x, to_y - from.y]
1877 } else {
1878 point
1879 };
1880 let id = exec_state.next_uuid();
1881 exec_state
1882 .batch_modeling_cmd(
1883 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1884 tan_arc_to(&sketch, delta),
1885 )
1886 .await?;
1887
1888 let current_path = Path::TangentialArcTo {
1889 base: BasePath {
1890 from: from.ignore_units(),
1891 to,
1892 tag: tag.clone(),
1893 units: sketch.units,
1894 geo_meta: GeoMeta {
1895 id,
1896 metadata: args.source_range.into(),
1897 },
1898 },
1899 center: result.center,
1900 ccw: result.ccw > 0,
1901 };
1902
1903 let mut new_sketch = sketch;
1904 if let Some(tag) = &tag {
1905 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1906 }
1907 if loops_back_to_start {
1908 new_sketch.is_closed = ProfileClosed::Implicitly;
1909 }
1910
1911 new_sketch.paths.push(current_path);
1912
1913 Ok(new_sketch)
1914}
1915
1916pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1918 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1919 let control1 = args.get_kw_arg_opt("control1", &RuntimeType::point2d(), exec_state)?;
1920 let control2 = args.get_kw_arg_opt("control2", &RuntimeType::point2d(), exec_state)?;
1921 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1922 let control1_absolute = args.get_kw_arg_opt("control1Absolute", &RuntimeType::point2d(), exec_state)?;
1923 let control2_absolute = args.get_kw_arg_opt("control2Absolute", &RuntimeType::point2d(), exec_state)?;
1924 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1925 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1926
1927 let new_sketch = inner_bezier_curve(
1928 sketch,
1929 control1,
1930 control2,
1931 end,
1932 control1_absolute,
1933 control2_absolute,
1934 end_absolute,
1935 tag,
1936 exec_state,
1937 args,
1938 )
1939 .await?;
1940 Ok(KclValue::Sketch {
1941 value: Box::new(new_sketch),
1942 })
1943}
1944
1945#[allow(clippy::too_many_arguments)]
1946async fn inner_bezier_curve(
1947 sketch: Sketch,
1948 control1: Option<[TyF64; 2]>,
1949 control2: Option<[TyF64; 2]>,
1950 end: Option<[TyF64; 2]>,
1951 control1_absolute: Option<[TyF64; 2]>,
1952 control2_absolute: Option<[TyF64; 2]>,
1953 end_absolute: Option<[TyF64; 2]>,
1954 tag: Option<TagNode>,
1955 exec_state: &mut ExecState,
1956 args: Args,
1957) -> Result<Sketch, KclError> {
1958 let from = sketch.current_pen_position()?;
1959 let id = exec_state.next_uuid();
1960
1961 let (to, control1_abs, control2_abs) = match (
1962 control1,
1963 control2,
1964 end,
1965 control1_absolute,
1966 control2_absolute,
1967 end_absolute,
1968 ) {
1969 (Some(control1), Some(control2), Some(end), None, None, None) => {
1971 let delta = end.clone();
1972 let to = [
1973 from.x + end[0].to_length_units(from.units),
1974 from.y + end[1].to_length_units(from.units),
1975 ];
1976 let control1_abs = [
1978 from.x + control1[0].to_length_units(from.units),
1979 from.y + control1[1].to_length_units(from.units),
1980 ];
1981 let control2_abs = [
1982 from.x + control2[0].to_length_units(from.units),
1983 from.y + control2[1].to_length_units(from.units),
1984 ];
1985
1986 exec_state
1987 .batch_modeling_cmd(
1988 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1989 ModelingCmd::from(
1990 mcmd::ExtendPath::builder()
1991 .path(sketch.id.into())
1992 .segment(PathSegment::Bezier {
1993 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
1994 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
1995 end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
1996 relative: true,
1997 })
1998 .build(),
1999 ),
2000 )
2001 .await?;
2002 (to, control1_abs, control2_abs)
2003 }
2004 (None, None, None, Some(control1), Some(control2), Some(end)) => {
2006 let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
2007 let control1_abs = control1.clone().map(|v| v.to_length_units(from.units));
2008 let control2_abs = control2.clone().map(|v| v.to_length_units(from.units));
2009 exec_state
2010 .batch_modeling_cmd(
2011 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2012 ModelingCmd::from(
2013 mcmd::ExtendPath::builder()
2014 .path(sketch.id.into())
2015 .segment(PathSegment::Bezier {
2016 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2017 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2018 end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
2019 relative: false,
2020 })
2021 .build(),
2022 ),
2023 )
2024 .await?;
2025 (to, control1_abs, control2_abs)
2026 }
2027 _ => {
2028 return Err(KclError::new_semantic(KclErrorDetails::new(
2029 "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
2030 vec![args.source_range],
2031 )));
2032 }
2033 };
2034
2035 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2036
2037 let current_path = Path::Bezier {
2038 base: BasePath {
2039 from: from.ignore_units(),
2040 to,
2041 tag: tag.clone(),
2042 units: sketch.units,
2043 geo_meta: GeoMeta {
2044 id,
2045 metadata: args.source_range.into(),
2046 },
2047 },
2048 control1: control1_abs,
2049 control2: control2_abs,
2050 };
2051
2052 let mut new_sketch = sketch;
2053 if let Some(tag) = &tag {
2054 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2055 }
2056 if loops_back_to_start {
2057 new_sketch.is_closed = ProfileClosed::Implicitly;
2058 }
2059
2060 new_sketch.paths.push(current_path);
2061
2062 Ok(new_sketch)
2063}
2064
2065pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2067 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2068
2069 let tool: Vec<Sketch> = args.get_kw_arg(
2070 "tool",
2071 &RuntimeType::Array(
2072 Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
2073 ArrayLen::Minimum(1),
2074 ),
2075 exec_state,
2076 )?;
2077
2078 let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
2079 Ok(KclValue::Sketch {
2080 value: Box::new(new_sketch),
2081 })
2082}
2083
2084async fn inner_subtract_2d(
2085 mut sketch: Sketch,
2086 tool: Vec<Sketch>,
2087 exec_state: &mut ExecState,
2088 args: Args,
2089) -> Result<Sketch, KclError> {
2090 for hole_sketch in tool {
2091 exec_state
2092 .batch_modeling_cmd(
2093 ModelingCmdMeta::from_args(exec_state, &args),
2094 ModelingCmd::from(
2095 mcmd::Solid2dAddHole::builder()
2096 .object_id(sketch.id)
2097 .hole_id(hole_sketch.id)
2098 .build(),
2099 ),
2100 )
2101 .await?;
2102
2103 exec_state
2106 .batch_modeling_cmd(
2107 ModelingCmdMeta::from_args(exec_state, &args),
2108 ModelingCmd::from(
2109 mcmd::ObjectVisible::builder()
2110 .object_id(hole_sketch.id)
2111 .hidden(true)
2112 .build(),
2113 ),
2114 )
2115 .await?;
2116
2117 sketch.inner_paths.extend_from_slice(&hole_sketch.paths);
2122 }
2123
2124 Ok(sketch)
2127}
2128
2129pub async fn elliptic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2131 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2132 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2133 let major_radius = args.get_kw_arg("majorRadius", &RuntimeType::num_any(), exec_state)?;
2134 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::num_any(), exec_state)?;
2135
2136 let elliptic_point = inner_elliptic_point(x, y, major_radius, minor_radius, &args).await?;
2137
2138 args.make_kcl_val_from_point(elliptic_point, exec_state.length_unit().into())
2139}
2140
2141async fn inner_elliptic_point(
2142 x: Option<TyF64>,
2143 y: Option<TyF64>,
2144 major_radius: TyF64,
2145 minor_radius: TyF64,
2146 args: &Args,
2147) -> Result<[f64; 2], KclError> {
2148 let major_radius = major_radius.n;
2149 let minor_radius = minor_radius.n;
2150 if let Some(x) = x {
2151 if x.n.abs() > major_radius {
2152 Err(KclError::Type {
2153 details: KclErrorDetails::new(
2154 format!(
2155 "Invalid input. The x value, {}, cannot be larger than the major radius {}.",
2156 x.n, major_radius
2157 ),
2158 vec![args.source_range],
2159 ),
2160 })
2161 } else {
2162 Ok((
2163 x.n,
2164 minor_radius * (1.0 - x.n.powf(2.0) / major_radius.powf(2.0)).sqrt(),
2165 )
2166 .into())
2167 }
2168 } else if let Some(y) = y {
2169 if y.n > minor_radius {
2170 Err(KclError::Type {
2171 details: KclErrorDetails::new(
2172 format!(
2173 "Invalid input. The y value, {}, cannot be larger than the minor radius {}.",
2174 y.n, minor_radius
2175 ),
2176 vec![args.source_range],
2177 ),
2178 })
2179 } else {
2180 Ok((
2181 major_radius * (1.0 - y.n.powf(2.0) / minor_radius.powf(2.0)).sqrt(),
2182 y.n,
2183 )
2184 .into())
2185 }
2186 } else {
2187 Err(KclError::Type {
2188 details: KclErrorDetails::new(
2189 "Invalid input. Must have either x or y, you cannot have both or neither.".to_owned(),
2190 vec![args.source_range],
2191 ),
2192 })
2193 }
2194}
2195
2196pub async fn elliptic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2198 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2199
2200 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
2201 let angle_start = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
2202 let angle_end = args.get_kw_arg("angleEnd", &RuntimeType::degrees(), exec_state)?;
2203 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
2204 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
2205 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
2206 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2207
2208 let new_sketch = inner_elliptic(
2209 sketch,
2210 center,
2211 angle_start,
2212 angle_end,
2213 major_radius,
2214 major_axis,
2215 minor_radius,
2216 tag,
2217 exec_state,
2218 args,
2219 )
2220 .await?;
2221 Ok(KclValue::Sketch {
2222 value: Box::new(new_sketch),
2223 })
2224}
2225
2226#[allow(clippy::too_many_arguments)]
2227pub(crate) async fn inner_elliptic(
2228 sketch: Sketch,
2229 center: [TyF64; 2],
2230 angle_start: TyF64,
2231 angle_end: TyF64,
2232 major_radius: Option<TyF64>,
2233 major_axis: Option<[TyF64; 2]>,
2234 minor_radius: TyF64,
2235 tag: Option<TagNode>,
2236 exec_state: &mut ExecState,
2237 args: Args,
2238) -> Result<Sketch, KclError> {
2239 let from: Point2d = sketch.current_pen_position()?;
2240 let id = exec_state.next_uuid();
2241
2242 let center_u = point_to_len_unit(center, from.units);
2243
2244 let major_axis = match (major_axis, major_radius) {
2245 (Some(_), Some(_)) | (None, None) => {
2246 return Err(KclError::new_type(KclErrorDetails::new(
2247 "Provide either `majorAxis` or `majorRadius`.".to_string(),
2248 vec![args.source_range],
2249 )));
2250 }
2251 (Some(major_axis), None) => major_axis,
2252 (None, Some(major_radius)) => [
2253 major_radius.clone(),
2254 TyF64 {
2255 n: 0.0,
2256 ty: major_radius.ty,
2257 },
2258 ],
2259 };
2260 let start_angle = Angle::from_degrees(angle_start.to_degrees(exec_state, args.source_range));
2261 let end_angle = Angle::from_degrees(angle_end.to_degrees(exec_state, args.source_range));
2262 let major_axis_magnitude = (major_axis[0].to_length_units(from.units) * major_axis[0].to_length_units(from.units)
2263 + major_axis[1].to_length_units(from.units) * major_axis[1].to_length_units(from.units))
2264 .sqrt();
2265 let to = [
2266 major_axis_magnitude * libm::cos(end_angle.to_radians()),
2267 minor_radius.to_length_units(from.units) * libm::sin(end_angle.to_radians()),
2268 ];
2269 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2270 let major_axis_angle = libm::atan2(major_axis[1].n, major_axis[0].n);
2271
2272 let point = [
2273 center_u[0] + to[0] * libm::cos(major_axis_angle) - to[1] * libm::sin(major_axis_angle),
2274 center_u[1] + to[0] * libm::sin(major_axis_angle) + to[1] * libm::cos(major_axis_angle),
2275 ];
2276
2277 let axis = major_axis.map(|x| x.to_mm());
2278 exec_state
2279 .batch_modeling_cmd(
2280 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2281 ModelingCmd::from(
2282 mcmd::ExtendPath::builder()
2283 .path(sketch.id.into())
2284 .segment(PathSegment::Ellipse {
2285 center: KPoint2d::from(untyped_point_to_mm(center_u, from.units)).map(LengthUnit),
2286 major_axis: axis.map(LengthUnit).into(),
2287 minor_radius: LengthUnit(minor_radius.to_mm()),
2288 start_angle,
2289 end_angle,
2290 })
2291 .build(),
2292 ),
2293 )
2294 .await?;
2295
2296 let current_path = Path::Ellipse {
2297 ccw: start_angle < end_angle,
2298 center: center_u,
2299 major_axis: axis,
2300 minor_radius: minor_radius.to_mm(),
2301 base: BasePath {
2302 from: from.ignore_units(),
2303 to: point,
2304 tag: tag.clone(),
2305 units: sketch.units,
2306 geo_meta: GeoMeta {
2307 id,
2308 metadata: args.source_range.into(),
2309 },
2310 },
2311 };
2312 let mut new_sketch = sketch;
2313 if let Some(tag) = &tag {
2314 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2315 }
2316 if loops_back_to_start {
2317 new_sketch.is_closed = ProfileClosed::Implicitly;
2318 }
2319
2320 new_sketch.paths.push(current_path);
2321
2322 Ok(new_sketch)
2323}
2324
2325pub async fn hyperbolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2327 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2328 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2329 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::num_any(), exec_state)?;
2330 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::num_any(), exec_state)?;
2331
2332 let hyperbolic_point = inner_hyperbolic_point(x, y, semi_major, semi_minor, &args).await?;
2333
2334 args.make_kcl_val_from_point(hyperbolic_point, exec_state.length_unit().into())
2335}
2336
2337async fn inner_hyperbolic_point(
2338 x: Option<TyF64>,
2339 y: Option<TyF64>,
2340 semi_major: TyF64,
2341 semi_minor: TyF64,
2342 args: &Args,
2343) -> Result<[f64; 2], KclError> {
2344 let semi_major = semi_major.n;
2345 let semi_minor = semi_minor.n;
2346 if let Some(x) = x {
2347 if x.n.abs() < semi_major {
2348 Err(KclError::Type {
2349 details: KclErrorDetails::new(
2350 format!(
2351 "Invalid input. The x value, {}, cannot be less than the semi major value, {}.",
2352 x.n, semi_major
2353 ),
2354 vec![args.source_range],
2355 ),
2356 })
2357 } else {
2358 Ok((x.n, semi_minor * (x.n.powf(2.0) / semi_major.powf(2.0) - 1.0).sqrt()).into())
2359 }
2360 } else if let Some(y) = y {
2361 Ok((semi_major * (y.n.powf(2.0) / semi_minor.powf(2.0) + 1.0).sqrt(), y.n).into())
2362 } else {
2363 Err(KclError::Type {
2364 details: KclErrorDetails::new(
2365 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2366 vec![args.source_range],
2367 ),
2368 })
2369 }
2370}
2371
2372pub async fn hyperbolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2374 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2375
2376 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::length(), exec_state)?;
2377 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::length(), exec_state)?;
2378 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2379 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2380 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2381 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2382 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2383
2384 let new_sketch = inner_hyperbolic(
2385 sketch,
2386 semi_major,
2387 semi_minor,
2388 interior,
2389 end,
2390 interior_absolute,
2391 end_absolute,
2392 tag,
2393 exec_state,
2394 args,
2395 )
2396 .await?;
2397 Ok(KclValue::Sketch {
2398 value: Box::new(new_sketch),
2399 })
2400}
2401
2402fn hyperbolic_tangent(point: Point2d, semi_major: f64, semi_minor: f64) -> [f64; 2] {
2404 (point.y * semi_major.powf(2.0), point.x * semi_minor.powf(2.0)).into()
2405}
2406
2407#[allow(clippy::too_many_arguments)]
2408pub(crate) async fn inner_hyperbolic(
2409 sketch: Sketch,
2410 semi_major: TyF64,
2411 semi_minor: TyF64,
2412 interior: Option<[TyF64; 2]>,
2413 end: Option<[TyF64; 2]>,
2414 interior_absolute: Option<[TyF64; 2]>,
2415 end_absolute: Option<[TyF64; 2]>,
2416 tag: Option<TagNode>,
2417 exec_state: &mut ExecState,
2418 args: Args,
2419) -> Result<Sketch, KclError> {
2420 let from = sketch.current_pen_position()?;
2421 let id = exec_state.next_uuid();
2422
2423 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2424 (Some(interior), Some(end), None, None) => (interior, end, true),
2425 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2426 _ => return Err(KclError::Type {
2427 details: KclErrorDetails::new(
2428 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2429 .to_owned(),
2430 vec![args.source_range],
2431 ),
2432 }),
2433 };
2434
2435 let interior = point_to_len_unit(interior, from.units);
2436 let end = point_to_len_unit(end, from.units);
2437 let end_point = Point2d {
2438 x: end[0],
2439 y: end[1],
2440 units: from.units,
2441 };
2442 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
2443
2444 let semi_major_u = semi_major.to_length_units(from.units);
2445 let semi_minor_u = semi_minor.to_length_units(from.units);
2446
2447 let start_tangent = hyperbolic_tangent(from, semi_major_u, semi_minor_u);
2448 let end_tangent = hyperbolic_tangent(end_point, semi_major_u, semi_minor_u);
2449
2450 exec_state
2451 .batch_modeling_cmd(
2452 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2453 ModelingCmd::from(
2454 mcmd::ExtendPath::builder()
2455 .path(sketch.id.into())
2456 .segment(PathSegment::ConicTo {
2457 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2458 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2459 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2460 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2461 relative,
2462 })
2463 .build(),
2464 ),
2465 )
2466 .await?;
2467
2468 let current_path = Path::Conic {
2469 base: BasePath {
2470 from: from.ignore_units(),
2471 to: end,
2472 tag: tag.clone(),
2473 units: sketch.units,
2474 geo_meta: GeoMeta {
2475 id,
2476 metadata: args.source_range.into(),
2477 },
2478 },
2479 };
2480
2481 let mut new_sketch = sketch;
2482 if let Some(tag) = &tag {
2483 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2484 }
2485 if loops_back_to_start {
2486 new_sketch.is_closed = ProfileClosed::Implicitly;
2487 }
2488
2489 new_sketch.paths.push(current_path);
2490
2491 Ok(new_sketch)
2492}
2493
2494pub async fn parabolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2496 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2497 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2498 let coefficients = args.get_kw_arg(
2499 "coefficients",
2500 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2501 exec_state,
2502 )?;
2503
2504 let parabolic_point = inner_parabolic_point(x, y, &coefficients, &args).await?;
2505
2506 args.make_kcl_val_from_point(parabolic_point, exec_state.length_unit().into())
2507}
2508
2509async fn inner_parabolic_point(
2510 x: Option<TyF64>,
2511 y: Option<TyF64>,
2512 coefficients: &[TyF64; 3],
2513 args: &Args,
2514) -> Result<[f64; 2], KclError> {
2515 let a = coefficients[0].n;
2516 let b = coefficients[1].n;
2517 let c = coefficients[2].n;
2518 if let Some(x) = x {
2519 Ok((x.n, a * x.n.powf(2.0) + b * x.n + c).into())
2520 } else if let Some(y) = y {
2521 let det = (b.powf(2.0) - 4.0 * a * (c - y.n)).sqrt();
2522 Ok(((-b + det) / (2.0 * a), y.n).into())
2523 } else {
2524 Err(KclError::Type {
2525 details: KclErrorDetails::new(
2526 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2527 vec![args.source_range],
2528 ),
2529 })
2530 }
2531}
2532
2533pub async fn parabolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2535 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2536
2537 let coefficients = args.get_kw_arg_opt(
2538 "coefficients",
2539 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2540 exec_state,
2541 )?;
2542 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2543 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2544 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2545 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2546 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2547
2548 let new_sketch = inner_parabolic(
2549 sketch,
2550 coefficients,
2551 interior,
2552 end,
2553 interior_absolute,
2554 end_absolute,
2555 tag,
2556 exec_state,
2557 args,
2558 )
2559 .await?;
2560 Ok(KclValue::Sketch {
2561 value: Box::new(new_sketch),
2562 })
2563}
2564
2565fn parabolic_tangent(point: Point2d, a: f64, b: f64) -> [f64; 2] {
2566 (1.0, 2.0 * a * point.x + b).into()
2569}
2570
2571#[allow(clippy::too_many_arguments)]
2572pub(crate) async fn inner_parabolic(
2573 sketch: Sketch,
2574 coefficients: Option<[TyF64; 3]>,
2575 interior: Option<[TyF64; 2]>,
2576 end: Option<[TyF64; 2]>,
2577 interior_absolute: Option<[TyF64; 2]>,
2578 end_absolute: Option<[TyF64; 2]>,
2579 tag: Option<TagNode>,
2580 exec_state: &mut ExecState,
2581 args: Args,
2582) -> Result<Sketch, KclError> {
2583 let from = sketch.current_pen_position()?;
2584 let id = exec_state.next_uuid();
2585
2586 if (coefficients.is_some() && interior.is_some()) || (coefficients.is_none() && interior.is_none()) {
2587 return Err(KclError::Type {
2588 details: KclErrorDetails::new(
2589 "Invalid combination of arguments. Either provide (a, b, c) or (interior)".to_owned(),
2590 vec![args.source_range],
2591 ),
2592 });
2593 }
2594
2595 let (interior, end, relative) = match (coefficients.clone(), interior, end, interior_absolute, end_absolute) {
2596 (None, Some(interior), Some(end), None, None) => {
2597 let interior = point_to_len_unit(interior, from.units);
2598 let end = point_to_len_unit(end, from.units);
2599 (interior,end, true)
2600 },
2601 (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
2602 let interior_absolute = point_to_len_unit(interior_absolute, from.units);
2603 let end_absolute = point_to_len_unit(end_absolute, from.units);
2604 (interior_absolute, end_absolute, false)
2605 }
2606 (Some(coefficients), _, Some(end), _, _) => {
2607 let end = point_to_len_unit(end, from.units);
2608 let interior =
2609 inner_parabolic_point(
2610 Some(TyF64::count(0.5 * (from.x + end[0]))),
2611 None,
2612 &coefficients,
2613 &args,
2614 )
2615 .await?;
2616 (interior, end, true)
2617 }
2618 (Some(coefficients), _, _, _, Some(end)) => {
2619 let end = point_to_len_unit(end, from.units);
2620 let interior =
2621 inner_parabolic_point(
2622 Some(TyF64::count(0.5 * (from.x + end[0]))),
2623 None,
2624 &coefficients,
2625 &args,
2626 )
2627 .await?;
2628 (interior, end, false)
2629 }
2630 _ => return
2631 Err(KclError::Type{details: KclErrorDetails::new(
2632 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute) if coefficients are not provided."
2633 .to_owned(),
2634 vec![args.source_range],
2635 )}),
2636 };
2637
2638 let end_point = Point2d {
2639 x: end[0],
2640 y: end[1],
2641 units: from.units,
2642 };
2643
2644 let (a, b, _c) = if let Some([a, b, c]) = coefficients {
2645 (a.n, b.n, c.n)
2646 } else {
2647 let denom = (from.x - interior[0]) * (from.x - end_point.x) * (interior[0] - end_point.x);
2649 let a = (end_point.x * (interior[1] - from.y)
2650 + interior[0] * (from.y - end_point.y)
2651 + from.x * (end_point.y - interior[1]))
2652 / denom;
2653 let b = (end_point.x.powf(2.0) * (from.y - interior[1])
2654 + interior[0].powf(2.0) * (end_point.y - from.y)
2655 + from.x.powf(2.0) * (interior[1] - end_point.y))
2656 / denom;
2657 let c = (interior[0] * end_point.x * (interior[0] - end_point.x) * from.y
2658 + end_point.x * from.x * (end_point.x - from.x) * interior[1]
2659 + from.x * interior[0] * (from.x - interior[0]) * end_point.y)
2660 / denom;
2661
2662 (a, b, c)
2663 };
2664
2665 let start_tangent = parabolic_tangent(from, a, b);
2666 let end_tangent = parabolic_tangent(end_point, a, b);
2667
2668 exec_state
2669 .batch_modeling_cmd(
2670 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2671 ModelingCmd::from(
2672 mcmd::ExtendPath::builder()
2673 .path(sketch.id.into())
2674 .segment(PathSegment::ConicTo {
2675 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2676 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2677 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2678 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2679 relative,
2680 })
2681 .build(),
2682 ),
2683 )
2684 .await?;
2685
2686 let current_path = Path::Conic {
2687 base: BasePath {
2688 from: from.ignore_units(),
2689 to: end,
2690 tag: tag.clone(),
2691 units: sketch.units,
2692 geo_meta: GeoMeta {
2693 id,
2694 metadata: args.source_range.into(),
2695 },
2696 },
2697 };
2698
2699 let mut new_sketch = sketch;
2700 if let Some(tag) = &tag {
2701 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2702 }
2703
2704 new_sketch.paths.push(current_path);
2705
2706 Ok(new_sketch)
2707}
2708
2709fn conic_tangent(coefficients: [f64; 6], point: [f64; 2]) -> [f64; 2] {
2710 let [a, b, c, d, e, _] = coefficients;
2711
2712 (
2713 c * point[0] + 2.0 * b * point[1] + e,
2714 -(2.0 * a * point[0] + c * point[1] + d),
2715 )
2716 .into()
2717}
2718
2719pub async fn conic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2721 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2722
2723 let start_tangent = args.get_kw_arg_opt("startTangent", &RuntimeType::point2d(), exec_state)?;
2724 let end_tangent = args.get_kw_arg_opt("endTangent", &RuntimeType::point2d(), exec_state)?;
2725 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2726 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2727 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2728 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2729 let coefficients = args.get_kw_arg_opt(
2730 "coefficients",
2731 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(6)),
2732 exec_state,
2733 )?;
2734 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2735
2736 let new_sketch = inner_conic(
2737 sketch,
2738 start_tangent,
2739 end,
2740 end_tangent,
2741 interior,
2742 coefficients,
2743 interior_absolute,
2744 end_absolute,
2745 tag,
2746 exec_state,
2747 args,
2748 )
2749 .await?;
2750 Ok(KclValue::Sketch {
2751 value: Box::new(new_sketch),
2752 })
2753}
2754
2755#[allow(clippy::too_many_arguments)]
2756pub(crate) async fn inner_conic(
2757 sketch: Sketch,
2758 start_tangent: Option<[TyF64; 2]>,
2759 end: Option<[TyF64; 2]>,
2760 end_tangent: Option<[TyF64; 2]>,
2761 interior: Option<[TyF64; 2]>,
2762 coefficients: Option<[TyF64; 6]>,
2763 interior_absolute: Option<[TyF64; 2]>,
2764 end_absolute: Option<[TyF64; 2]>,
2765 tag: Option<TagNode>,
2766 exec_state: &mut ExecState,
2767 args: Args,
2768) -> Result<Sketch, KclError> {
2769 let from: Point2d = sketch.current_pen_position()?;
2770 let id = exec_state.next_uuid();
2771
2772 if (coefficients.is_some() && (start_tangent.is_some() || end_tangent.is_some()))
2773 || (coefficients.is_none() && (start_tangent.is_none() && end_tangent.is_none()))
2774 {
2775 return Err(KclError::Type {
2776 details: KclErrorDetails::new(
2777 "Invalid combination of arguments. Either provide coefficients or (startTangent, endTangent)"
2778 .to_owned(),
2779 vec![args.source_range],
2780 ),
2781 });
2782 }
2783
2784 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2785 (Some(interior), Some(end), None, None) => (interior, end, true),
2786 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2787 _ => return Err(KclError::Type {
2788 details: KclErrorDetails::new(
2789 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2790 .to_owned(),
2791 vec![args.source_range],
2792 ),
2793 }),
2794 };
2795
2796 let end = point_to_len_unit(end, from.units);
2797 let interior = point_to_len_unit(interior, from.units);
2798
2799 let (start_tangent, end_tangent) = if let Some(coeffs) = coefficients {
2800 let (coeffs, _) = untype_array(coeffs);
2801 (conic_tangent(coeffs, [from.x, from.y]), conic_tangent(coeffs, end))
2802 } else {
2803 let start = if let Some(start_tangent) = start_tangent {
2804 point_to_len_unit(start_tangent, from.units)
2805 } else {
2806 let previous_point = sketch
2807 .get_tangential_info_from_paths()
2808 .tan_previous_point(from.ignore_units());
2809 let from = from.ignore_units();
2810 [from[0] - previous_point[0], from[1] - previous_point[1]]
2811 };
2812
2813 let Some(end_tangent) = end_tangent else {
2814 return Err(KclError::new_semantic(KclErrorDetails::new(
2815 "You must either provide either `coefficients` or `endTangent`.".to_owned(),
2816 vec![args.source_range],
2817 )));
2818 };
2819 let end_tan = point_to_len_unit(end_tangent, from.units);
2820 (start, end_tan)
2821 };
2822
2823 exec_state
2824 .batch_modeling_cmd(
2825 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2826 ModelingCmd::from(
2827 mcmd::ExtendPath::builder()
2828 .path(sketch.id.into())
2829 .segment(PathSegment::ConicTo {
2830 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2831 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2832 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2833 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2834 relative,
2835 })
2836 .build(),
2837 ),
2838 )
2839 .await?;
2840
2841 let current_path = Path::Conic {
2842 base: BasePath {
2843 from: from.ignore_units(),
2844 to: end,
2845 tag: tag.clone(),
2846 units: sketch.units,
2847 geo_meta: GeoMeta {
2848 id,
2849 metadata: args.source_range.into(),
2850 },
2851 },
2852 };
2853
2854 let mut new_sketch = sketch;
2855 if let Some(tag) = &tag {
2856 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2857 }
2858
2859 new_sketch.paths.push(current_path);
2860
2861 Ok(new_sketch)
2862}
2863#[cfg(test)]
2864mod tests {
2865
2866 use pretty_assertions::assert_eq;
2867
2868 use crate::{
2869 execution::TagIdentifier,
2870 std::{sketch::PlaneData, utils::calculate_circle_center},
2871 };
2872
2873 #[test]
2874 fn test_deserialize_plane_data() {
2875 let data = PlaneData::XY;
2876 let mut str_json = serde_json::to_string(&data).unwrap();
2877 assert_eq!(str_json, "\"XY\"");
2878
2879 str_json = "\"YZ\"".to_string();
2880 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2881 assert_eq!(data, PlaneData::YZ);
2882
2883 str_json = "\"-YZ\"".to_string();
2884 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2885 assert_eq!(data, PlaneData::NegYZ);
2886
2887 str_json = "\"-xz\"".to_string();
2888 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
2889 assert_eq!(data, PlaneData::NegXZ);
2890 }
2891
2892 #[test]
2893 fn test_deserialize_sketch_on_face_tag() {
2894 let data = "start";
2895 let mut str_json = serde_json::to_string(&data).unwrap();
2896 assert_eq!(str_json, "\"start\"");
2897
2898 str_json = "\"end\"".to_string();
2899 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2900 assert_eq!(
2901 data,
2902 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2903 );
2904
2905 str_json = serde_json::to_string(&TagIdentifier {
2906 value: "thing".to_string(),
2907 info: Vec::new(),
2908 meta: Default::default(),
2909 })
2910 .unwrap();
2911 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2912 assert_eq!(
2913 data,
2914 crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
2915 value: "thing".to_string(),
2916 info: Vec::new(),
2917 meta: Default::default()
2918 }))
2919 );
2920
2921 str_json = "\"END\"".to_string();
2922 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2923 assert_eq!(
2924 data,
2925 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
2926 );
2927
2928 str_json = "\"start\"".to_string();
2929 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2930 assert_eq!(
2931 data,
2932 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2933 );
2934
2935 str_json = "\"START\"".to_string();
2936 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
2937 assert_eq!(
2938 data,
2939 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
2940 );
2941 }
2942
2943 #[test]
2944 fn test_circle_center() {
2945 let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
2946 assert_eq!(actual[0], 5.0);
2947 assert_eq!(actual[1], 0.0);
2948 }
2949}