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