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