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