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