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 Axis2dOrEdgeReference::EdgeSpecifier(_) => {
1075 return Err(KclError::new_semantic(KclErrorDetails::new(
1076 "Use of an edge reference here is unsupported, please specify an `Axis2d` (e.g. `X`) instead."
1077 .to_owned(),
1078 vec![args.source_range],
1079 )));
1080 }
1081 };
1082 let origin = Point3d::new(0.0, 0.0, 0.0, plane_of.info.origin.units);
1083 let plane_data = PlaneData::Plane(PlaneInfo {
1084 origin: plane_of.project(origin) + normal_offset,
1085 x_axis,
1086 y_axis,
1087 z_axis: x_axis.axes_cross_product(&y_axis),
1088 });
1089 let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
1090
1091 let id = exec_state.next_uuid();
1093 exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
1094 id: ArtifactId::from(id),
1095 plane_id: plane.artifact_id,
1096 code_ref: CodeRef::placeholder(args.source_range),
1097 }));
1098
1099 Ok(SketchSurface::Plane(plane))
1100 } else {
1101 let face = make_face(solid, tag, exec_state, args).await?;
1102
1103 let id = exec_state.next_uuid();
1105 exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
1106 id: ArtifactId::from(id),
1107 face_id: face.artifact_id,
1108 code_ref: CodeRef::placeholder(args.source_range),
1109 }));
1110
1111 Ok(SketchSurface::Face(face))
1112 }
1113 }
1114 }
1115}
1116
1117pub async fn make_sketch_plane_from_orientation(
1118 data: PlaneData,
1119 exec_state: &mut ExecState,
1120 args: &Args,
1121) -> Result<Box<Plane>, KclError> {
1122 let id = exec_state.next_uuid();
1123 let kind = PlaneKind::from(&data);
1124 let mut plane = Plane {
1125 id,
1126 artifact_id: id.into(),
1127 object_id: None,
1128 kind,
1129 info: PlaneInfo::try_from(data)?,
1130 meta: vec![args.source_range.into()],
1131 };
1132
1133 ensure_sketch_plane_in_engine(
1135 &mut plane,
1136 exec_state,
1137 &args.ctx,
1138 args.source_range,
1139 args.node_path.clone(),
1140 )
1141 .await?;
1142
1143 Ok(Box::new(plane))
1144}
1145
1146pub async fn ensure_sketch_plane_in_engine(
1148 plane: &mut Plane,
1149 exec_state: &mut ExecState,
1150 ctx: &ExecutorContext,
1151 source_range: SourceRange,
1152 node_path: Option<NodePath>,
1153) -> Result<(), KclError> {
1154 if plane.is_initialized() {
1155 return Ok(());
1156 }
1157 if let Some(existing_object_id) = exec_state.scene_object_id_by_artifact_id(ArtifactId::new(plane.id)) {
1158 plane.object_id = Some(existing_object_id);
1159 return Ok(());
1160 }
1161
1162 let id = exec_state.next_uuid();
1170 plane.id = id;
1171 plane.artifact_id = id.into();
1172
1173 let clobber = false;
1174 let size = LengthUnit(60.0);
1175 let hide = Some(true);
1176 let cmd = if let Some(hide) = hide {
1177 mcmd::MakePlane::builder()
1178 .clobber(clobber)
1179 .origin(plane.info.origin.into())
1180 .size(size)
1181 .x_axis(plane.info.x_axis.into())
1182 .y_axis(plane.info.y_axis.into())
1183 .hide(hide)
1184 .build()
1185 } else {
1186 mcmd::MakePlane::builder()
1187 .clobber(clobber)
1188 .origin(plane.info.origin.into())
1189 .size(size)
1190 .x_axis(plane.info.x_axis.into())
1191 .y_axis(plane.info.y_axis.into())
1192 .build()
1193 };
1194 exec_state
1195 .batch_modeling_cmd(
1196 ModelingCmdMeta::with_id(exec_state, ctx, source_range, plane.id),
1197 ModelingCmd::from(cmd),
1198 )
1199 .await?;
1200 let plane_object_id = exec_state.next_object_id();
1201 let plane_object = crate::front::Object {
1202 id: plane_object_id,
1203 kind: crate::front::ObjectKind::Plane(crate::front::Plane::Object(plane_object_id)),
1204 label: Default::default(),
1205 comments: Default::default(),
1206 artifact_id: ArtifactId::new(plane.id),
1207 source: SourceRef::new(source_range, node_path.clone()),
1208 };
1209 exec_state.add_scene_object(plane_object, source_range);
1210 plane.object_id = Some(plane_object_id);
1211
1212 Ok(())
1213}
1214
1215pub async fn start_profile(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1217 let sketch_surface = args.get_unlabeled_kw_arg(
1218 "startProfileOn",
1219 &RuntimeType::Union(vec![RuntimeType::plane(), RuntimeType::face()]),
1220 exec_state,
1221 )?;
1222 let start: [TyF64; 2] = args.get_kw_arg("at", &RuntimeType::point2d(), exec_state)?;
1223 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1224
1225 let sketch = inner_start_profile(sketch_surface, start, tag, exec_state, &args.ctx, args.source_range).await?;
1226 Ok(KclValue::Sketch {
1227 value: Box::new(sketch),
1228 })
1229}
1230
1231pub(crate) async fn inner_start_profile(
1232 sketch_surface: SketchSurface,
1233 at: [TyF64; 2],
1234 tag: Option<TagNode>,
1235 exec_state: &mut ExecState,
1236 ctx: &ExecutorContext,
1237 source_range: SourceRange,
1238) -> Result<Sketch, KclError> {
1239 let id = exec_state.next_uuid();
1240 create_sketch(id, sketch_surface, at, tag, true, exec_state, ctx, source_range).await
1241}
1242
1243#[expect(clippy::too_many_arguments)]
1244pub(crate) async fn create_sketch(
1245 id: Uuid,
1246 sketch_surface: SketchSurface,
1247 at: [TyF64; 2],
1248 tag: Option<TagNode>,
1249 send_to_engine: bool,
1250 exec_state: &mut ExecState,
1251 ctx: &ExecutorContext,
1252 source_range: SourceRange,
1253) -> Result<Sketch, KclError> {
1254 match &sketch_surface {
1255 SketchSurface::Face(face) => {
1256 exec_state
1259 .flush_batch_for_face_parent_solids(
1260 ModelingCmdMeta::new(exec_state, ctx, source_range),
1261 std::slice::from_ref(&face.parent_solid),
1262 )
1263 .await?;
1264 }
1265 SketchSurface::Plane(plane) if !plane.is_standard() => {
1266 exec_state
1269 .batch_end_cmd(
1270 ModelingCmdMeta::new(exec_state, ctx, source_range),
1271 ModelingCmd::from(mcmd::ObjectVisible::builder().object_id(plane.id).hidden(true).build()),
1272 )
1273 .await?;
1274 }
1275 _ => {}
1276 }
1277
1278 let path_id = id;
1279 let enable_sketch_id = exec_state.next_uuid();
1280 let move_pen_id = exec_state.next_uuid();
1281 let disable_sketch_id = exec_state.next_uuid();
1282 if send_to_engine {
1283 exec_state
1284 .batch_modeling_cmds(
1285 ModelingCmdMeta::new(exec_state, ctx, source_range),
1286 &[
1287 ModelingCmdReq {
1290 cmd: ModelingCmd::from(if let SketchSurface::Plane(plane) = &sketch_surface {
1291 let normal = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
1293 mcmd::EnableSketchMode::builder()
1294 .animated(false)
1295 .ortho(false)
1296 .entity_id(sketch_surface.id())
1297 .adjust_camera(false)
1298 .planar_normal(normal.into())
1299 .build()
1300 } else {
1301 mcmd::EnableSketchMode::builder()
1302 .animated(false)
1303 .ortho(false)
1304 .entity_id(sketch_surface.id())
1305 .adjust_camera(false)
1306 .build()
1307 }),
1308 cmd_id: enable_sketch_id.into(),
1309 },
1310 ModelingCmdReq {
1311 cmd: ModelingCmd::from(mcmd::StartPath::default()),
1312 cmd_id: path_id.into(),
1313 },
1314 ModelingCmdReq {
1315 cmd: ModelingCmd::from(
1316 mcmd::MovePathPen::builder()
1317 .path(path_id.into())
1318 .to(KPoint2d::from(point_to_mm(at.clone())).with_z(0.0).map(LengthUnit))
1319 .build(),
1320 ),
1321 cmd_id: move_pen_id.into(),
1322 },
1323 ModelingCmdReq {
1324 cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
1325 cmd_id: disable_sketch_id.into(),
1326 },
1327 ],
1328 )
1329 .await?;
1330 }
1331
1332 let units = exec_state.length_unit();
1334 let to = point_to_len_unit(at, units);
1335 let current_path = BasePath {
1336 from: to,
1337 to,
1338 tag: tag.clone(),
1339 units,
1340 geo_meta: GeoMeta {
1341 id: move_pen_id,
1342 metadata: source_range.into(),
1343 },
1344 };
1345
1346 let mut sketch = Sketch {
1347 id: path_id,
1348 original_id: path_id,
1349 artifact_id: path_id.into(),
1350 origin_sketch_id: None,
1351 on: sketch_surface,
1352 paths: vec![],
1353 inner_paths: vec![],
1354 units,
1355 mirror: Default::default(),
1356 clone: Default::default(),
1357 synthetic_jump_path_ids: vec![],
1358 meta: vec![source_range.into()],
1359 tags: Default::default(),
1360 start: current_path.clone(),
1361 is_closed: ProfileClosed::No,
1362 };
1363 if let Some(tag) = &tag {
1364 let path = Path::Base { base: current_path };
1365 sketch.add_tag(tag, &path, exec_state, None);
1366 }
1367
1368 Ok(sketch)
1369}
1370
1371pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1373 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1374 let ty = sketch.units.into();
1375 let x = inner_profile_start_x(sketch)?;
1376 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1377}
1378
1379pub(crate) fn inner_profile_start_x(profile: Sketch) -> Result<f64, KclError> {
1380 Ok(profile.start.to[0])
1381}
1382
1383pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1385 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1386 let ty = sketch.units.into();
1387 let x = inner_profile_start_y(sketch)?;
1388 Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
1389}
1390
1391pub(crate) fn inner_profile_start_y(profile: Sketch) -> Result<f64, KclError> {
1392 Ok(profile.start.to[1])
1393}
1394
1395pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1397 let sketch: Sketch = args.get_unlabeled_kw_arg("profile", &RuntimeType::sketch(), exec_state)?;
1398 let ty = sketch.units.into();
1399 let point = inner_profile_start(sketch)?;
1400 Ok(KclValue::from_point2d(point, ty, args.into()))
1401}
1402
1403pub(crate) fn inner_profile_start(profile: Sketch) -> Result<[f64; 2], KclError> {
1404 Ok(profile.start.to)
1405}
1406
1407pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1409 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1410 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1411 let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
1412 Ok(KclValue::Sketch {
1413 value: Box::new(new_sketch),
1414 })
1415}
1416
1417pub(crate) async fn inner_close(
1418 sketch: Sketch,
1419 tag: Option<TagNode>,
1420 exec_state: &mut ExecState,
1421 args: Args,
1422) -> Result<Sketch, KclError> {
1423 if matches!(sketch.is_closed, ProfileClosed::Explicitly) {
1424 exec_state.warn(
1425 crate::CompilationIssue {
1426 source_range: args.source_range,
1427 message: "This sketch is already closed. Remove this unnecessary `close()` call".to_string(),
1428 suggestion: None,
1429 severity: crate::errors::Severity::Warning,
1430 tag: crate::errors::Tag::Unnecessary,
1431 },
1432 annotations::WARN_UNNECESSARY_CLOSE,
1433 );
1434 return Ok(sketch);
1435 }
1436 let from = sketch.current_pen_position()?;
1437 let to = point_to_len_unit(sketch.start.get_from(), from.units);
1438
1439 let id = exec_state.next_uuid();
1440
1441 exec_state
1442 .batch_modeling_cmd(
1443 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1444 ModelingCmd::from(mcmd::ClosePath::builder().path_id(sketch.id).build()),
1445 )
1446 .await?;
1447
1448 let mut new_sketch = sketch;
1449
1450 let distance = ((from.x - to[0]).squared() + (from.y - to[1]).squared()).sqrt();
1451 if distance > super::EQUAL_POINTS_DIST_EPSILON {
1452 let current_path = Path::ToPoint {
1454 base: BasePath {
1455 from: from.ignore_units(),
1456 to,
1457 tag: tag.clone(),
1458 units: new_sketch.units,
1459 geo_meta: GeoMeta {
1460 id,
1461 metadata: args.source_range.into(),
1462 },
1463 },
1464 };
1465
1466 if let Some(tag) = &tag {
1467 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1468 }
1469 new_sketch.paths.push(current_path);
1470 } else if tag.is_some() {
1471 exec_state.warn(
1472 crate::CompilationIssue {
1473 source_range: args.source_range,
1474 message: "A tag declarator was specified, but no segment was created".to_string(),
1475 suggestion: None,
1476 severity: crate::errors::Severity::Warning,
1477 tag: crate::errors::Tag::Unnecessary,
1478 },
1479 annotations::WARN_UNUSED_TAGS,
1480 );
1481 }
1482
1483 new_sketch.is_closed = ProfileClosed::Explicitly;
1484
1485 Ok(new_sketch)
1486}
1487
1488pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1490 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1491
1492 let angle_start: Option<TyF64> = args.get_kw_arg_opt("angleStart", &RuntimeType::degrees(), exec_state)?;
1493 let angle_end: Option<TyF64> = args.get_kw_arg_opt("angleEnd", &RuntimeType::degrees(), exec_state)?;
1494 let radius: Option<TyF64> = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1495 let diameter: Option<TyF64> = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1496 let end_absolute: Option<[TyF64; 2]> = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1497 let interior_absolute: Option<[TyF64; 2]> =
1498 args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
1499 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1500 let new_sketch = inner_arc(
1501 sketch,
1502 angle_start,
1503 angle_end,
1504 radius,
1505 diameter,
1506 interior_absolute,
1507 end_absolute,
1508 tag,
1509 exec_state,
1510 args,
1511 )
1512 .await?;
1513 Ok(KclValue::Sketch {
1514 value: Box::new(new_sketch),
1515 })
1516}
1517
1518#[allow(clippy::too_many_arguments)]
1519pub(crate) async fn inner_arc(
1520 sketch: Sketch,
1521 angle_start: Option<TyF64>,
1522 angle_end: Option<TyF64>,
1523 radius: Option<TyF64>,
1524 diameter: Option<TyF64>,
1525 interior_absolute: Option<[TyF64; 2]>,
1526 end_absolute: Option<[TyF64; 2]>,
1527 tag: Option<TagNode>,
1528 exec_state: &mut ExecState,
1529 args: Args,
1530) -> Result<Sketch, KclError> {
1531 let from: Point2d = sketch.current_pen_position()?;
1532 let id = exec_state.next_uuid();
1533
1534 match (angle_start, angle_end, radius, diameter, interior_absolute, end_absolute) {
1535 (Some(angle_start), Some(angle_end), radius, diameter, None, None) => {
1536 let radius = get_radius(radius, diameter, args.source_range)?;
1537 relative_arc(id, exec_state, sketch, from, angle_start, angle_end, radius, tag, true, &args.ctx, args.source_range).await
1538 }
1539 (None, None, None, None, Some(interior_absolute), Some(end_absolute)) => {
1540 absolute_arc(&args, id, exec_state, sketch, from, interior_absolute, end_absolute, tag).await
1541 }
1542 _ => {
1543 Err(KclError::new_type(KclErrorDetails::new(
1544 "Invalid combination of arguments. Either provide (angleStart, angleEnd, radius) or (endAbsolute, interiorAbsolute)".to_owned(),
1545 vec![args.source_range],
1546 )))
1547 }
1548 }
1549}
1550
1551#[allow(clippy::too_many_arguments)]
1552pub async fn absolute_arc(
1553 args: &Args,
1554 id: uuid::Uuid,
1555 exec_state: &mut ExecState,
1556 sketch: Sketch,
1557 from: Point2d,
1558 interior_absolute: [TyF64; 2],
1559 end_absolute: [TyF64; 2],
1560 tag: Option<TagNode>,
1561) -> Result<Sketch, KclError> {
1562 exec_state
1564 .batch_modeling_cmd(
1565 ModelingCmdMeta::from_args_id(exec_state, args, id),
1566 ModelingCmd::from(
1567 mcmd::ExtendPath::builder()
1568 .path(sketch.id.into())
1569 .segment(PathSegment::ArcTo {
1570 end: kcmc::shared::Point3d {
1571 x: LengthUnit(end_absolute[0].to_mm()),
1572 y: LengthUnit(end_absolute[1].to_mm()),
1573 z: LengthUnit(0.0),
1574 },
1575 interior: kcmc::shared::Point3d {
1576 x: LengthUnit(interior_absolute[0].to_mm()),
1577 y: LengthUnit(interior_absolute[1].to_mm()),
1578 z: LengthUnit(0.0),
1579 },
1580 relative: false,
1581 })
1582 .build(),
1583 ),
1584 )
1585 .await?;
1586
1587 let start = [from.x, from.y];
1588 let end = point_to_len_unit(end_absolute, from.units);
1589 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1590
1591 let current_path = Path::ArcThreePoint {
1592 base: BasePath {
1593 from: from.ignore_units(),
1594 to: end,
1595 tag: tag.clone(),
1596 units: sketch.units,
1597 geo_meta: GeoMeta {
1598 id,
1599 metadata: args.source_range.into(),
1600 },
1601 },
1602 p1: start,
1603 p2: point_to_len_unit(interior_absolute, from.units),
1604 p3: end,
1605 };
1606
1607 let mut new_sketch = sketch;
1608 if let Some(tag) = &tag {
1609 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1610 }
1611 if loops_back_to_start {
1612 new_sketch.is_closed = ProfileClosed::Implicitly;
1613 }
1614
1615 new_sketch.paths.push(current_path);
1616
1617 Ok(new_sketch)
1618}
1619
1620#[allow(clippy::too_many_arguments)]
1621pub async fn relative_arc(
1622 id: uuid::Uuid,
1623 exec_state: &mut ExecState,
1624 sketch: Sketch,
1625 from: Point2d,
1626 angle_start: TyF64,
1627 angle_end: TyF64,
1628 radius: TyF64,
1629 tag: Option<TagNode>,
1630 send_to_engine: bool,
1631 ctx: &ExecutorContext,
1632 source_range: SourceRange,
1633) -> Result<Sketch, KclError> {
1634 let a_start = Angle::from_degrees(angle_start.to_degrees(exec_state, source_range));
1635 let a_end = Angle::from_degrees(angle_end.to_degrees(exec_state, source_range));
1636 let radius = radius.to_length_units(from.units);
1637 let (center, end) = arc_center_and_end(from.ignore_units(), a_start, a_end, radius);
1638 if a_start == a_end {
1639 return Err(KclError::new_type(KclErrorDetails::new(
1640 "Arc start and end angles must be different".to_string(),
1641 vec![source_range],
1642 )));
1643 }
1644 let ccw = a_start < a_end;
1645
1646 if send_to_engine {
1647 exec_state
1648 .batch_modeling_cmd(
1649 ModelingCmdMeta::with_id(exec_state, ctx, source_range, id),
1650 ModelingCmd::from(
1651 mcmd::ExtendPath::builder()
1652 .path(sketch.id.into())
1653 .segment(PathSegment::Arc {
1654 start: a_start,
1655 end: a_end,
1656 center: KPoint2d::from(untyped_point_to_mm(center, from.units)).map(LengthUnit),
1657 radius: LengthUnit(
1658 crate::execution::types::adjust_length(from.units, radius, UnitLength::Millimeters).0,
1659 ),
1660 relative: false,
1661 })
1662 .build(),
1663 ),
1664 )
1665 .await?;
1666 }
1667
1668 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
1669 let current_path = Path::Arc {
1670 base: BasePath {
1671 from: from.ignore_units(),
1672 to: end,
1673 tag: tag.clone(),
1674 units: from.units,
1675 geo_meta: GeoMeta {
1676 id,
1677 metadata: source_range.into(),
1678 },
1679 },
1680 center,
1681 radius,
1682 ccw,
1683 };
1684
1685 let mut new_sketch = sketch;
1686 if let Some(tag) = &tag {
1687 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1688 }
1689 if loops_back_to_start {
1690 new_sketch.is_closed = ProfileClosed::Implicitly;
1691 }
1692
1693 new_sketch.paths.push(current_path);
1694
1695 Ok(new_sketch)
1696}
1697
1698pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1700 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1701 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1702 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1703 let radius = args.get_kw_arg_opt("radius", &RuntimeType::length(), exec_state)?;
1704 let diameter = args.get_kw_arg_opt("diameter", &RuntimeType::length(), exec_state)?;
1705 let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
1706 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1707
1708 let new_sketch = inner_tangential_arc(
1709 sketch,
1710 end_absolute,
1711 end,
1712 radius,
1713 diameter,
1714 angle,
1715 tag,
1716 exec_state,
1717 args,
1718 )
1719 .await?;
1720 Ok(KclValue::Sketch {
1721 value: Box::new(new_sketch),
1722 })
1723}
1724
1725#[allow(clippy::too_many_arguments)]
1726async fn inner_tangential_arc(
1727 sketch: Sketch,
1728 end_absolute: Option<[TyF64; 2]>,
1729 end: Option<[TyF64; 2]>,
1730 radius: Option<TyF64>,
1731 diameter: Option<TyF64>,
1732 angle: Option<TyF64>,
1733 tag: Option<TagNode>,
1734 exec_state: &mut ExecState,
1735 args: Args,
1736) -> Result<Sketch, KclError> {
1737 match (end_absolute, end, radius, diameter, angle) {
1738 (Some(point), None, None, None, None) => {
1739 inner_tangential_arc_to_point(sketch, point, true, tag, exec_state, args).await
1740 }
1741 (None, Some(point), None, None, None) => {
1742 inner_tangential_arc_to_point(sketch, point, false, tag, exec_state, args).await
1743 }
1744 (None, None, radius, diameter, Some(angle)) => {
1745 let radius = get_radius(radius, diameter, args.source_range)?;
1746 let data = TangentialArcData::RadiusAndOffset { radius, offset: angle };
1747 inner_tangential_arc_radius_angle(data, sketch, tag, exec_state, args).await
1748 }
1749 (Some(_), Some(_), None, None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
1750 "You cannot give both `end` and `endAbsolute` params, you have to choose one or the other".to_owned(),
1751 vec![args.source_range],
1752 ))),
1753 (_, _, _, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1754 "You must supply `end`, `endAbsolute`, or both `angle` and `radius`/`diameter` arguments".to_owned(),
1755 vec![args.source_range],
1756 ))),
1757 }
1758}
1759
1760#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
1762#[ts(export)]
1763#[serde(rename_all = "camelCase", untagged)]
1764pub enum TangentialArcData {
1765 RadiusAndOffset {
1766 radius: TyF64,
1769 offset: TyF64,
1771 },
1772}
1773
1774async fn inner_tangential_arc_radius_angle(
1781 data: TangentialArcData,
1782 sketch: Sketch,
1783 tag: Option<TagNode>,
1784 exec_state: &mut ExecState,
1785 args: Args,
1786) -> Result<Sketch, KclError> {
1787 let from: Point2d = sketch.current_pen_position()?;
1788 let tangent_info = sketch.get_tangential_info_from_paths(); let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1791
1792 let id = exec_state.next_uuid();
1793
1794 let (center, to, ccw) = match data {
1795 TangentialArcData::RadiusAndOffset { radius, offset } => {
1796 let offset = Angle::from_degrees(offset.to_degrees(exec_state, args.source_range));
1798
1799 let previous_end_tangent = Angle::from_radians(libm::atan2(
1802 from.y - tan_previous_point[1],
1803 from.x - tan_previous_point[0],
1804 ));
1805 let ccw = offset.to_degrees() > 0.0;
1808 let tangent_to_arc_start_angle = if ccw {
1809 Angle::from_degrees(-90.0)
1811 } else {
1812 Angle::from_degrees(90.0)
1814 };
1815 let start_angle = previous_end_tangent + tangent_to_arc_start_angle;
1818 let end_angle = start_angle + offset;
1819 let (center, to) = arc_center_and_end(
1820 from.ignore_units(),
1821 start_angle,
1822 end_angle,
1823 radius.to_length_units(from.units),
1824 );
1825
1826 exec_state
1827 .batch_modeling_cmd(
1828 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1829 ModelingCmd::from(
1830 mcmd::ExtendPath::builder()
1831 .path(sketch.id.into())
1832 .segment(PathSegment::TangentialArc {
1833 radius: LengthUnit(radius.to_mm()),
1834 offset,
1835 })
1836 .build(),
1837 ),
1838 )
1839 .await?;
1840 (center, to, ccw)
1841 }
1842 };
1843 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1844
1845 let current_path = Path::TangentialArc {
1846 ccw,
1847 center,
1848 base: BasePath {
1849 from: from.ignore_units(),
1850 to,
1851 tag: tag.clone(),
1852 units: sketch.units,
1853 geo_meta: GeoMeta {
1854 id,
1855 metadata: args.source_range.into(),
1856 },
1857 },
1858 };
1859
1860 let mut new_sketch = sketch;
1861 if let Some(tag) = &tag {
1862 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1863 }
1864 if loops_back_to_start {
1865 new_sketch.is_closed = ProfileClosed::Implicitly;
1866 }
1867
1868 new_sketch.paths.push(current_path);
1869
1870 Ok(new_sketch)
1871}
1872
1873fn tan_arc_to(sketch: &Sketch, to: [f64; 2]) -> ModelingCmd {
1875 ModelingCmd::from(
1876 mcmd::ExtendPath::builder()
1877 .path(sketch.id.into())
1878 .segment(PathSegment::TangentialArcTo {
1879 angle_snap_increment: None,
1880 to: KPoint2d::from(untyped_point_to_mm(to, sketch.units))
1881 .with_z(0.0)
1882 .map(LengthUnit),
1883 })
1884 .build(),
1885 )
1886}
1887
1888async fn inner_tangential_arc_to_point(
1889 sketch: Sketch,
1890 point: [TyF64; 2],
1891 is_absolute: bool,
1892 tag: Option<TagNode>,
1893 exec_state: &mut ExecState,
1894 args: Args,
1895) -> Result<Sketch, KclError> {
1896 let from: Point2d = sketch.current_pen_position()?;
1897 let tangent_info = sketch.get_tangential_info_from_paths();
1898 let tan_previous_point = tangent_info.tan_previous_point(from.ignore_units());
1899
1900 let point = point_to_len_unit(point, from.units);
1901
1902 let to = if is_absolute {
1903 point
1904 } else {
1905 [from.x + point[0], from.y + point[1]]
1906 };
1907 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
1908 let [to_x, to_y] = to;
1909 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
1910 arc_start_point: [from.x, from.y],
1911 arc_end_point: [to_x, to_y],
1912 tan_previous_point,
1913 obtuse: true,
1914 });
1915
1916 if result.center[0].is_infinite() {
1917 return Err(KclError::new_semantic(KclErrorDetails::new(
1918 "could not sketch tangential arc, because its center would be infinitely far away in the X direction"
1919 .to_owned(),
1920 vec![args.source_range],
1921 )));
1922 } else if result.center[1].is_infinite() {
1923 return Err(KclError::new_semantic(KclErrorDetails::new(
1924 "could not sketch tangential arc, because its center would be infinitely far away in the Y direction"
1925 .to_owned(),
1926 vec![args.source_range],
1927 )));
1928 }
1929
1930 let delta = if is_absolute {
1931 [to_x - from.x, to_y - from.y]
1932 } else {
1933 point
1934 };
1935 let id = exec_state.next_uuid();
1936 exec_state
1937 .batch_modeling_cmd(
1938 ModelingCmdMeta::from_args_id(exec_state, &args, id),
1939 tan_arc_to(&sketch, delta),
1940 )
1941 .await?;
1942
1943 let current_path = Path::TangentialArcTo {
1944 base: BasePath {
1945 from: from.ignore_units(),
1946 to,
1947 tag: tag.clone(),
1948 units: sketch.units,
1949 geo_meta: GeoMeta {
1950 id,
1951 metadata: args.source_range.into(),
1952 },
1953 },
1954 center: result.center,
1955 ccw: result.ccw > 0,
1956 };
1957
1958 let mut new_sketch = sketch;
1959 if let Some(tag) = &tag {
1960 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
1961 }
1962 if loops_back_to_start {
1963 new_sketch.is_closed = ProfileClosed::Implicitly;
1964 }
1965
1966 new_sketch.paths.push(current_path);
1967
1968 Ok(new_sketch)
1969}
1970
1971pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1973 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
1974 let control1 = args.get_kw_arg_opt("control1", &RuntimeType::point2d(), exec_state)?;
1975 let control2 = args.get_kw_arg_opt("control2", &RuntimeType::point2d(), exec_state)?;
1976 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
1977 let control1_absolute = args.get_kw_arg_opt("control1Absolute", &RuntimeType::point2d(), exec_state)?;
1978 let control2_absolute = args.get_kw_arg_opt("control2Absolute", &RuntimeType::point2d(), exec_state)?;
1979 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
1980 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
1981
1982 let new_sketch = inner_bezier_curve(
1983 sketch,
1984 control1,
1985 control2,
1986 end,
1987 control1_absolute,
1988 control2_absolute,
1989 end_absolute,
1990 tag,
1991 exec_state,
1992 args,
1993 )
1994 .await?;
1995 Ok(KclValue::Sketch {
1996 value: Box::new(new_sketch),
1997 })
1998}
1999
2000#[allow(clippy::too_many_arguments)]
2001async fn inner_bezier_curve(
2002 sketch: Sketch,
2003 control1: Option<[TyF64; 2]>,
2004 control2: Option<[TyF64; 2]>,
2005 end: Option<[TyF64; 2]>,
2006 control1_absolute: Option<[TyF64; 2]>,
2007 control2_absolute: Option<[TyF64; 2]>,
2008 end_absolute: Option<[TyF64; 2]>,
2009 tag: Option<TagNode>,
2010 exec_state: &mut ExecState,
2011 args: Args,
2012) -> Result<Sketch, KclError> {
2013 let from = sketch.current_pen_position()?;
2014 let id = exec_state.next_uuid();
2015
2016 let (to, control1_abs, control2_abs) = match (
2017 control1,
2018 control2,
2019 end,
2020 control1_absolute,
2021 control2_absolute,
2022 end_absolute,
2023 ) {
2024 (Some(control1), Some(control2), Some(end), None, None, None) => {
2026 let delta = end.clone();
2027 let to = [
2028 from.x + end[0].to_length_units(from.units),
2029 from.y + end[1].to_length_units(from.units),
2030 ];
2031 let control1_abs = [
2033 from.x + control1[0].to_length_units(from.units),
2034 from.y + control1[1].to_length_units(from.units),
2035 ];
2036 let control2_abs = [
2037 from.x + control2[0].to_length_units(from.units),
2038 from.y + control2[1].to_length_units(from.units),
2039 ];
2040
2041 exec_state
2042 .batch_modeling_cmd(
2043 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2044 ModelingCmd::from(
2045 mcmd::ExtendPath::builder()
2046 .path(sketch.id.into())
2047 .segment(PathSegment::Bezier {
2048 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2049 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2050 end: KPoint2d::from(point_to_mm(delta)).with_z(0.0).map(LengthUnit),
2051 relative: true,
2052 })
2053 .build(),
2054 ),
2055 )
2056 .await?;
2057 (to, control1_abs, control2_abs)
2058 }
2059 (None, None, None, Some(control1), Some(control2), Some(end)) => {
2061 let to = [end[0].to_length_units(from.units), end[1].to_length_units(from.units)];
2062 let control1_abs = control1.clone().map(|v| v.to_length_units(from.units));
2063 let control2_abs = control2.clone().map(|v| v.to_length_units(from.units));
2064 exec_state
2065 .batch_modeling_cmd(
2066 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2067 ModelingCmd::from(
2068 mcmd::ExtendPath::builder()
2069 .path(sketch.id.into())
2070 .segment(PathSegment::Bezier {
2071 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2072 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2073 end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
2074 relative: false,
2075 })
2076 .build(),
2077 ),
2078 )
2079 .await?;
2080 (to, control1_abs, control2_abs)
2081 }
2082 _ => {
2083 return Err(KclError::new_semantic(KclErrorDetails::new(
2084 "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
2085 vec![args.source_range],
2086 )));
2087 }
2088 };
2089
2090 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2091
2092 let current_path = Path::Bezier {
2093 base: BasePath {
2094 from: from.ignore_units(),
2095 to,
2096 tag: tag.clone(),
2097 units: sketch.units,
2098 geo_meta: GeoMeta {
2099 id,
2100 metadata: args.source_range.into(),
2101 },
2102 },
2103 control1: control1_abs,
2104 control2: control2_abs,
2105 };
2106
2107 let mut new_sketch = sketch;
2108 if let Some(tag) = &tag {
2109 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2110 }
2111 if loops_back_to_start {
2112 new_sketch.is_closed = ProfileClosed::Implicitly;
2113 }
2114
2115 new_sketch.paths.push(current_path);
2116
2117 Ok(new_sketch)
2118}
2119
2120pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2122 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2123
2124 let tool: Vec<Sketch> = args.get_kw_arg(
2125 "tool",
2126 &RuntimeType::Array(
2127 Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
2128 ArrayLen::Minimum(1),
2129 ),
2130 exec_state,
2131 )?;
2132
2133 let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
2134 Ok(KclValue::Sketch {
2135 value: Box::new(new_sketch),
2136 })
2137}
2138
2139async fn inner_subtract_2d(
2140 mut sketch: Sketch,
2141 tool: Vec<Sketch>,
2142 exec_state: &mut ExecState,
2143 args: Args,
2144) -> Result<Sketch, KclError> {
2145 for hole_sketch in tool {
2146 exec_state
2147 .batch_modeling_cmd(
2148 ModelingCmdMeta::from_args(exec_state, &args),
2149 ModelingCmd::from(
2150 mcmd::Solid2dAddHole::builder()
2151 .object_id(sketch.id)
2152 .hole_id(hole_sketch.id)
2153 .build(),
2154 ),
2155 )
2156 .await?;
2157
2158 exec_state
2161 .batch_modeling_cmd(
2162 ModelingCmdMeta::from_args(exec_state, &args),
2163 ModelingCmd::from(
2164 mcmd::ObjectVisible::builder()
2165 .object_id(hole_sketch.id)
2166 .hidden(true)
2167 .build(),
2168 ),
2169 )
2170 .await?;
2171
2172 sketch.inner_paths.extend_from_slice(&hole_sketch.paths);
2177 }
2178
2179 Ok(sketch)
2182}
2183
2184pub async fn elliptic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2186 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2187 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2188 let major_radius = args.get_kw_arg("majorRadius", &RuntimeType::num_any(), exec_state)?;
2189 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::num_any(), exec_state)?;
2190
2191 let elliptic_point = inner_elliptic_point(x, y, major_radius, minor_radius, &args).await?;
2192
2193 args.make_kcl_val_from_point(elliptic_point, exec_state.length_unit().into())
2194}
2195
2196async fn inner_elliptic_point(
2197 x: Option<TyF64>,
2198 y: Option<TyF64>,
2199 major_radius: TyF64,
2200 minor_radius: TyF64,
2201 args: &Args,
2202) -> Result<[f64; 2], KclError> {
2203 let major_radius = major_radius.n;
2204 let minor_radius = minor_radius.n;
2205 if let Some(x) = x {
2206 if x.n.abs() > major_radius {
2207 Err(KclError::Type {
2208 details: KclErrorDetails::new(
2209 format!(
2210 "Invalid input. The x value, {}, cannot be larger than the major radius {}.",
2211 x.n, major_radius
2212 ),
2213 vec![args.source_range],
2214 ),
2215 })
2216 } else {
2217 Ok((
2218 x.n,
2219 minor_radius * (1.0 - x.n.squared() / major_radius.squared()).sqrt(),
2220 )
2221 .into())
2222 }
2223 } else if let Some(y) = y {
2224 if y.n > minor_radius {
2225 Err(KclError::Type {
2226 details: KclErrorDetails::new(
2227 format!(
2228 "Invalid input. The y value, {}, cannot be larger than the minor radius {}.",
2229 y.n, minor_radius
2230 ),
2231 vec![args.source_range],
2232 ),
2233 })
2234 } else {
2235 Ok((
2236 major_radius * (1.0 - y.n.squared() / minor_radius.squared()).sqrt(),
2237 y.n,
2238 )
2239 .into())
2240 }
2241 } else {
2242 Err(KclError::Type {
2243 details: KclErrorDetails::new(
2244 "Invalid input. Must have either x or y, you cannot have both or neither.".to_owned(),
2245 vec![args.source_range],
2246 ),
2247 })
2248 }
2249}
2250
2251pub async fn elliptic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2253 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2254
2255 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
2256 let angle_start = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
2257 let angle_end = args.get_kw_arg("angleEnd", &RuntimeType::degrees(), exec_state)?;
2258 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
2259 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
2260 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
2261 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2262
2263 let new_sketch = inner_elliptic(
2264 sketch,
2265 center,
2266 angle_start,
2267 angle_end,
2268 major_radius,
2269 major_axis,
2270 minor_radius,
2271 tag,
2272 exec_state,
2273 args,
2274 )
2275 .await?;
2276 Ok(KclValue::Sketch {
2277 value: Box::new(new_sketch),
2278 })
2279}
2280
2281#[allow(clippy::too_many_arguments)]
2282pub(crate) async fn inner_elliptic(
2283 sketch: Sketch,
2284 center: [TyF64; 2],
2285 angle_start: TyF64,
2286 angle_end: TyF64,
2287 major_radius: Option<TyF64>,
2288 major_axis: Option<[TyF64; 2]>,
2289 minor_radius: TyF64,
2290 tag: Option<TagNode>,
2291 exec_state: &mut ExecState,
2292 args: Args,
2293) -> Result<Sketch, KclError> {
2294 let from: Point2d = sketch.current_pen_position()?;
2295 let id = exec_state.next_uuid();
2296
2297 let center_u = point_to_len_unit(center, from.units);
2298
2299 let major_axis = match (major_axis, major_radius) {
2300 (Some(_), Some(_)) | (None, None) => {
2301 return Err(KclError::new_type(KclErrorDetails::new(
2302 "Provide either `majorAxis` or `majorRadius`.".to_string(),
2303 vec![args.source_range],
2304 )));
2305 }
2306 (Some(major_axis), None) => major_axis,
2307 (None, Some(major_radius)) => [
2308 major_radius.clone(),
2309 TyF64 {
2310 n: 0.0,
2311 ty: major_radius.ty,
2312 },
2313 ],
2314 };
2315 let start_angle = Angle::from_degrees(angle_start.to_degrees(exec_state, args.source_range));
2316 let end_angle = Angle::from_degrees(angle_end.to_degrees(exec_state, args.source_range));
2317 let major_axis_magnitude = (major_axis[0].to_length_units(from.units) * major_axis[0].to_length_units(from.units)
2318 + major_axis[1].to_length_units(from.units) * major_axis[1].to_length_units(from.units))
2319 .sqrt();
2320 let to = [
2321 major_axis_magnitude * libm::cos(end_angle.to_radians()),
2322 minor_radius.to_length_units(from.units) * libm::sin(end_angle.to_radians()),
2323 ];
2324 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2325 let major_axis_angle = libm::atan2(major_axis[1].n, major_axis[0].n);
2326
2327 let point = [
2328 center_u[0] + to[0] * libm::cos(major_axis_angle) - to[1] * libm::sin(major_axis_angle),
2329 center_u[1] + to[0] * libm::sin(major_axis_angle) + to[1] * libm::cos(major_axis_angle),
2330 ];
2331
2332 let axis = major_axis.map(|x| x.to_mm());
2333 exec_state
2334 .batch_modeling_cmd(
2335 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2336 ModelingCmd::from(
2337 mcmd::ExtendPath::builder()
2338 .path(sketch.id.into())
2339 .segment(PathSegment::Ellipse {
2340 center: KPoint2d::from(untyped_point_to_mm(center_u, from.units)).map(LengthUnit),
2341 major_axis: axis.map(LengthUnit).into(),
2342 minor_radius: LengthUnit(minor_radius.to_mm()),
2343 start_angle,
2344 end_angle,
2345 })
2346 .build(),
2347 ),
2348 )
2349 .await?;
2350
2351 let current_path = Path::Ellipse {
2352 ccw: start_angle < end_angle,
2353 center: center_u,
2354 major_axis: axis,
2355 minor_radius: minor_radius.to_mm(),
2356 base: BasePath {
2357 from: from.ignore_units(),
2358 to: point,
2359 tag: tag.clone(),
2360 units: sketch.units,
2361 geo_meta: GeoMeta {
2362 id,
2363 metadata: args.source_range.into(),
2364 },
2365 },
2366 };
2367 let mut new_sketch = sketch;
2368 if let Some(tag) = &tag {
2369 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2370 }
2371 if loops_back_to_start {
2372 new_sketch.is_closed = ProfileClosed::Implicitly;
2373 }
2374
2375 new_sketch.paths.push(current_path);
2376
2377 Ok(new_sketch)
2378}
2379
2380pub async fn hyperbolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2382 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2383 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2384 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::num_any(), exec_state)?;
2385 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::num_any(), exec_state)?;
2386
2387 let hyperbolic_point = inner_hyperbolic_point(x, y, semi_major, semi_minor, &args).await?;
2388
2389 args.make_kcl_val_from_point(hyperbolic_point, exec_state.length_unit().into())
2390}
2391
2392async fn inner_hyperbolic_point(
2393 x: Option<TyF64>,
2394 y: Option<TyF64>,
2395 semi_major: TyF64,
2396 semi_minor: TyF64,
2397 args: &Args,
2398) -> Result<[f64; 2], KclError> {
2399 let semi_major = semi_major.n;
2400 let semi_minor = semi_minor.n;
2401 if let Some(x) = x {
2402 if x.n.abs() < semi_major {
2403 Err(KclError::Type {
2404 details: KclErrorDetails::new(
2405 format!(
2406 "Invalid input. The x value, {}, cannot be less than the semi major value, {}.",
2407 x.n, semi_major
2408 ),
2409 vec![args.source_range],
2410 ),
2411 })
2412 } else {
2413 Ok((x.n, semi_minor * (x.n.squared() / semi_major.squared() - 1.0).sqrt()).into())
2414 }
2415 } else if let Some(y) = y {
2416 Ok((semi_major * (y.n.squared() / semi_minor.squared() + 1.0).sqrt(), y.n).into())
2417 } else {
2418 Err(KclError::Type {
2419 details: KclErrorDetails::new(
2420 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2421 vec![args.source_range],
2422 ),
2423 })
2424 }
2425}
2426
2427pub async fn hyperbolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2429 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2430
2431 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::length(), exec_state)?;
2432 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::length(), exec_state)?;
2433 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2434 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2435 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2436 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2437 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2438
2439 let new_sketch = inner_hyperbolic(
2440 sketch,
2441 semi_major,
2442 semi_minor,
2443 interior,
2444 end,
2445 interior_absolute,
2446 end_absolute,
2447 tag,
2448 exec_state,
2449 args,
2450 )
2451 .await?;
2452 Ok(KclValue::Sketch {
2453 value: Box::new(new_sketch),
2454 })
2455}
2456
2457fn hyperbolic_tangent(point: Point2d, semi_major: f64, semi_minor: f64) -> [f64; 2] {
2459 (point.y * semi_major.squared(), point.x * semi_minor.squared()).into()
2460}
2461
2462#[allow(clippy::too_many_arguments)]
2463pub(crate) async fn inner_hyperbolic(
2464 sketch: Sketch,
2465 semi_major: TyF64,
2466 semi_minor: TyF64,
2467 interior: Option<[TyF64; 2]>,
2468 end: Option<[TyF64; 2]>,
2469 interior_absolute: Option<[TyF64; 2]>,
2470 end_absolute: Option<[TyF64; 2]>,
2471 tag: Option<TagNode>,
2472 exec_state: &mut ExecState,
2473 args: Args,
2474) -> Result<Sketch, KclError> {
2475 let from = sketch.current_pen_position()?;
2476 let id = exec_state.next_uuid();
2477
2478 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2479 (Some(interior), Some(end), None, None) => (interior, end, true),
2480 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2481 _ => return Err(KclError::Type {
2482 details: KclErrorDetails::new(
2483 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2484 .to_owned(),
2485 vec![args.source_range],
2486 ),
2487 }),
2488 };
2489
2490 let interior = point_to_len_unit(interior, from.units);
2491 let end = point_to_len_unit(end, from.units);
2492 let end_point = Point2d {
2493 x: end[0],
2494 y: end[1],
2495 units: from.units,
2496 };
2497 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
2498
2499 let semi_major_u = semi_major.to_length_units(from.units);
2500 let semi_minor_u = semi_minor.to_length_units(from.units);
2501
2502 let start_tangent = hyperbolic_tangent(from, semi_major_u, semi_minor_u);
2503 let end_tangent = hyperbolic_tangent(end_point, semi_major_u, semi_minor_u);
2504
2505 exec_state
2506 .batch_modeling_cmd(
2507 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2508 ModelingCmd::from(
2509 mcmd::ExtendPath::builder()
2510 .path(sketch.id.into())
2511 .segment(PathSegment::ConicTo {
2512 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2513 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2514 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2515 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2516 relative,
2517 })
2518 .build(),
2519 ),
2520 )
2521 .await?;
2522
2523 let current_path = Path::Conic {
2524 base: BasePath {
2525 from: from.ignore_units(),
2526 to: end,
2527 tag: tag.clone(),
2528 units: sketch.units,
2529 geo_meta: GeoMeta {
2530 id,
2531 metadata: args.source_range.into(),
2532 },
2533 },
2534 };
2535
2536 let mut new_sketch = sketch;
2537 if let Some(tag) = &tag {
2538 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2539 }
2540 if loops_back_to_start {
2541 new_sketch.is_closed = ProfileClosed::Implicitly;
2542 }
2543
2544 new_sketch.paths.push(current_path);
2545
2546 Ok(new_sketch)
2547}
2548
2549pub async fn parabolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2551 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2552 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2553 let coefficients = args.get_kw_arg(
2554 "coefficients",
2555 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2556 exec_state,
2557 )?;
2558
2559 let parabolic_point = inner_parabolic_point(x, y, &coefficients, &args).await?;
2560
2561 args.make_kcl_val_from_point(parabolic_point, exec_state.length_unit().into())
2562}
2563
2564async fn inner_parabolic_point(
2565 x: Option<TyF64>,
2566 y: Option<TyF64>,
2567 coefficients: &[TyF64; 3],
2568 args: &Args,
2569) -> Result<[f64; 2], KclError> {
2570 let a = coefficients[0].n;
2571 let b = coefficients[1].n;
2572 let c = coefficients[2].n;
2573 if let Some(x) = x {
2574 Ok((x.n, a * x.n.squared() + b * x.n + c).into())
2575 } else if let Some(y) = y {
2576 let det = (b.squared() - 4.0 * a * (c - y.n)).sqrt();
2577 Ok(((-b + det) / (2.0 * a), y.n).into())
2578 } else {
2579 Err(KclError::Type {
2580 details: KclErrorDetails::new(
2581 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2582 vec![args.source_range],
2583 ),
2584 })
2585 }
2586}
2587
2588pub async fn parabolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2590 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2591
2592 let coefficients = args.get_kw_arg_opt(
2593 "coefficients",
2594 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2595 exec_state,
2596 )?;
2597 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2598 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2599 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2600 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2601 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2602
2603 let new_sketch = inner_parabolic(
2604 sketch,
2605 coefficients,
2606 interior,
2607 end,
2608 interior_absolute,
2609 end_absolute,
2610 tag,
2611 exec_state,
2612 args,
2613 )
2614 .await?;
2615 Ok(KclValue::Sketch {
2616 value: Box::new(new_sketch),
2617 })
2618}
2619
2620fn parabolic_tangent(point: Point2d, a: f64, b: f64) -> [f64; 2] {
2621 (1.0, 2.0 * a * point.x + b).into()
2624}
2625
2626#[allow(clippy::too_many_arguments)]
2627pub(crate) async fn inner_parabolic(
2628 sketch: Sketch,
2629 coefficients: Option<[TyF64; 3]>,
2630 interior: Option<[TyF64; 2]>,
2631 end: Option<[TyF64; 2]>,
2632 interior_absolute: Option<[TyF64; 2]>,
2633 end_absolute: Option<[TyF64; 2]>,
2634 tag: Option<TagNode>,
2635 exec_state: &mut ExecState,
2636 args: Args,
2637) -> Result<Sketch, KclError> {
2638 let from = sketch.current_pen_position()?;
2639 let id = exec_state.next_uuid();
2640
2641 if (coefficients.is_some() && interior.is_some()) || (coefficients.is_none() && interior.is_none()) {
2642 return Err(KclError::Type {
2643 details: KclErrorDetails::new(
2644 "Invalid combination of arguments. Either provide (a, b, c) or (interior)".to_owned(),
2645 vec![args.source_range],
2646 ),
2647 });
2648 }
2649
2650 let (interior, end, relative) = match (coefficients.clone(), interior, end, interior_absolute, end_absolute) {
2651 (None, Some(interior), Some(end), None, None) => {
2652 let interior = point_to_len_unit(interior, from.units);
2653 let end = point_to_len_unit(end, from.units);
2654 (interior,end, true)
2655 },
2656 (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
2657 let interior_absolute = point_to_len_unit(interior_absolute, from.units);
2658 let end_absolute = point_to_len_unit(end_absolute, from.units);
2659 (interior_absolute, end_absolute, false)
2660 }
2661 (Some(coefficients), _, Some(end), _, _) => {
2662 let end = point_to_len_unit(end, from.units);
2663 let interior =
2664 inner_parabolic_point(
2665 Some(TyF64::count(0.5 * (from.x + end[0]))),
2666 None,
2667 &coefficients,
2668 &args,
2669 )
2670 .await?;
2671 (interior, end, true)
2672 }
2673 (Some(coefficients), _, _, _, Some(end)) => {
2674 let end = point_to_len_unit(end, from.units);
2675 let interior =
2676 inner_parabolic_point(
2677 Some(TyF64::count(0.5 * (from.x + end[0]))),
2678 None,
2679 &coefficients,
2680 &args,
2681 )
2682 .await?;
2683 (interior, end, false)
2684 }
2685 _ => return
2686 Err(KclError::Type{details: KclErrorDetails::new(
2687 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute) if coefficients are not provided."
2688 .to_owned(),
2689 vec![args.source_range],
2690 )}),
2691 };
2692
2693 let end_point = Point2d {
2694 x: end[0],
2695 y: end[1],
2696 units: from.units,
2697 };
2698
2699 let (a, b, _c) = if let Some([a, b, c]) = coefficients {
2700 (a.n, b.n, c.n)
2701 } else {
2702 let denom = (from.x - interior[0]) * (from.x - end_point.x) * (interior[0] - end_point.x);
2704 let a = (end_point.x * (interior[1] - from.y)
2705 + interior[0] * (from.y - end_point.y)
2706 + from.x * (end_point.y - interior[1]))
2707 / denom;
2708 let b = (end_point.x.squared() * (from.y - interior[1])
2709 + interior[0].squared() * (end_point.y - from.y)
2710 + from.x.squared() * (interior[1] - end_point.y))
2711 / denom;
2712 let c = (interior[0] * end_point.x * (interior[0] - end_point.x) * from.y
2713 + end_point.x * from.x * (end_point.x - from.x) * interior[1]
2714 + from.x * interior[0] * (from.x - interior[0]) * end_point.y)
2715 / denom;
2716
2717 (a, b, c)
2718 };
2719
2720 let start_tangent = parabolic_tangent(from, a, b);
2721 let end_tangent = parabolic_tangent(end_point, a, b);
2722
2723 exec_state
2724 .batch_modeling_cmd(
2725 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2726 ModelingCmd::from(
2727 mcmd::ExtendPath::builder()
2728 .path(sketch.id.into())
2729 .segment(PathSegment::ConicTo {
2730 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2731 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2732 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2733 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2734 relative,
2735 })
2736 .build(),
2737 ),
2738 )
2739 .await?;
2740
2741 let current_path = Path::Conic {
2742 base: BasePath {
2743 from: from.ignore_units(),
2744 to: end,
2745 tag: tag.clone(),
2746 units: sketch.units,
2747 geo_meta: GeoMeta {
2748 id,
2749 metadata: args.source_range.into(),
2750 },
2751 },
2752 };
2753
2754 let mut new_sketch = sketch;
2755 if let Some(tag) = &tag {
2756 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2757 }
2758
2759 new_sketch.paths.push(current_path);
2760
2761 Ok(new_sketch)
2762}
2763
2764fn conic_tangent(coefficients: [f64; 6], point: [f64; 2]) -> [f64; 2] {
2765 let [a, b, c, d, e, _] = coefficients;
2766
2767 (
2768 c * point[0] + 2.0 * b * point[1] + e,
2769 -(2.0 * a * point[0] + c * point[1] + d),
2770 )
2771 .into()
2772}
2773
2774pub async fn conic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2776 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2777
2778 let start_tangent = args.get_kw_arg_opt("startTangent", &RuntimeType::point2d(), exec_state)?;
2779 let end_tangent = args.get_kw_arg_opt("endTangent", &RuntimeType::point2d(), exec_state)?;
2780 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2781 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2782 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2783 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2784 let coefficients = args.get_kw_arg_opt(
2785 "coefficients",
2786 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(6)),
2787 exec_state,
2788 )?;
2789 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2790
2791 let new_sketch = inner_conic(
2792 sketch,
2793 start_tangent,
2794 end,
2795 end_tangent,
2796 interior,
2797 coefficients,
2798 interior_absolute,
2799 end_absolute,
2800 tag,
2801 exec_state,
2802 args,
2803 )
2804 .await?;
2805 Ok(KclValue::Sketch {
2806 value: Box::new(new_sketch),
2807 })
2808}
2809
2810#[allow(clippy::too_many_arguments)]
2811pub(crate) async fn inner_conic(
2812 sketch: Sketch,
2813 start_tangent: Option<[TyF64; 2]>,
2814 end: Option<[TyF64; 2]>,
2815 end_tangent: Option<[TyF64; 2]>,
2816 interior: Option<[TyF64; 2]>,
2817 coefficients: Option<[TyF64; 6]>,
2818 interior_absolute: Option<[TyF64; 2]>,
2819 end_absolute: Option<[TyF64; 2]>,
2820 tag: Option<TagNode>,
2821 exec_state: &mut ExecState,
2822 args: Args,
2823) -> Result<Sketch, KclError> {
2824 let from: Point2d = sketch.current_pen_position()?;
2825 let id = exec_state.next_uuid();
2826
2827 if (coefficients.is_some() && (start_tangent.is_some() || end_tangent.is_some()))
2828 || (coefficients.is_none() && (start_tangent.is_none() && end_tangent.is_none()))
2829 {
2830 return Err(KclError::Type {
2831 details: KclErrorDetails::new(
2832 "Invalid combination of arguments. Either provide coefficients or (startTangent, endTangent)"
2833 .to_owned(),
2834 vec![args.source_range],
2835 ),
2836 });
2837 }
2838
2839 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2840 (Some(interior), Some(end), None, None) => (interior, end, true),
2841 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2842 _ => return Err(KclError::Type {
2843 details: KclErrorDetails::new(
2844 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2845 .to_owned(),
2846 vec![args.source_range],
2847 ),
2848 }),
2849 };
2850
2851 let end = point_to_len_unit(end, from.units);
2852 let interior = point_to_len_unit(interior, from.units);
2853
2854 let (start_tangent, end_tangent) = if let Some(coeffs) = coefficients {
2855 let (coeffs, _) = untype_array(coeffs);
2856 (conic_tangent(coeffs, [from.x, from.y]), conic_tangent(coeffs, end))
2857 } else {
2858 let start = if let Some(start_tangent) = start_tangent {
2859 point_to_len_unit(start_tangent, from.units)
2860 } else {
2861 let previous_point = sketch
2862 .get_tangential_info_from_paths()
2863 .tan_previous_point(from.ignore_units());
2864 let from = from.ignore_units();
2865 [from[0] - previous_point[0], from[1] - previous_point[1]]
2866 };
2867
2868 let Some(end_tangent) = end_tangent else {
2869 return Err(KclError::new_semantic(KclErrorDetails::new(
2870 "You must either provide either `coefficients` or `endTangent`.".to_owned(),
2871 vec![args.source_range],
2872 )));
2873 };
2874 let end_tan = point_to_len_unit(end_tangent, from.units);
2875 (start, end_tan)
2876 };
2877
2878 exec_state
2879 .batch_modeling_cmd(
2880 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2881 ModelingCmd::from(
2882 mcmd::ExtendPath::builder()
2883 .path(sketch.id.into())
2884 .segment(PathSegment::ConicTo {
2885 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2886 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2887 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2888 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2889 relative,
2890 })
2891 .build(),
2892 ),
2893 )
2894 .await?;
2895
2896 let current_path = Path::Conic {
2897 base: BasePath {
2898 from: from.ignore_units(),
2899 to: end,
2900 tag: tag.clone(),
2901 units: sketch.units,
2902 geo_meta: GeoMeta {
2903 id,
2904 metadata: args.source_range.into(),
2905 },
2906 },
2907 };
2908
2909 let mut new_sketch = sketch;
2910 if let Some(tag) = &tag {
2911 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2912 }
2913
2914 new_sketch.paths.push(current_path);
2915
2916 Ok(new_sketch)
2917}
2918
2919pub(super) async fn region(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2920 let point = args.get_kw_arg_opt(
2921 "point",
2922 &RuntimeType::Union(vec![RuntimeType::point2d(), RuntimeType::segment()]),
2923 exec_state,
2924 )?;
2925 let segments = args.get_kw_arg_opt(
2926 "segments",
2927 &RuntimeType::Array(Box::new(RuntimeType::segment()), ArrayLen::Minimum(1)),
2928 exec_state,
2929 )?;
2930 let intersection_index = args.get_kw_arg_opt("intersectionIndex", &RuntimeType::count(), exec_state)?;
2931 let direction = args.get_kw_arg_opt("direction", &RuntimeType::string(), exec_state)?;
2932 let sketch = args.get_kw_arg_opt("sketch", &RuntimeType::any(), exec_state)?;
2933 inner_region(point, segments, intersection_index, direction, sketch, exec_state, args).await
2934}
2935
2936#[expect(clippy::large_enum_variant)]
2939enum SketchOrSegment {
2940 Sketch(Sketch),
2941 Segment(Segment),
2942}
2943
2944impl SketchOrSegment {
2945 fn sketch(&self) -> Result<&Sketch, KclError> {
2946 match self {
2947 SketchOrSegment::Sketch(sketch) => Ok(sketch),
2948 SketchOrSegment::Segment(segment) => segment.sketch.as_deref().ok_or_else(|| {
2949 KclError::new_semantic(KclErrorDetails::new(
2950 "Segment should have an associated sketch".to_owned(),
2951 vec![],
2952 ))
2953 }),
2954 }
2955 }
2956}
2957
2958async fn inner_region(
2959 point: Option<KclValue>,
2960 segments: Option<Vec<KclValue>>,
2961 intersection_index: Option<TyF64>,
2962 direction: Option<CircularDirection>,
2963 sketch: Option<KclValue>,
2964 exec_state: &mut ExecState,
2965 args: Args,
2966) -> Result<KclValue, KclError> {
2967 let region_id = exec_state.next_uuid();
2968 let kcl_version = exec_state.kcl_version();
2969 let region_version = match kcl_version {
2970 KclVersion::V1 => RegionVersion::V0,
2971 KclVersion::V2 => RegionVersion::V1,
2972 };
2973
2974 let (sketch_or_segment, region_mapping) = match (point, segments) {
2975 (Some(point), None) => {
2976 let (sketch, pt) = region_from_point(point, sketch, &args)?;
2977
2978 let meta = ModelingCmdMeta::from_args_id(exec_state, &args, region_id);
2979 let response = exec_state
2980 .send_modeling_cmd(
2981 meta,
2982 ModelingCmd::from(
2983 mcmd::CreateRegionFromQueryPoint::builder()
2984 .object_id(sketch.sketch()?.id)
2985 .query_point(KPoint2d::from(point_to_mm(pt.clone())).map(LengthUnit))
2986 .version(region_version)
2987 .build(),
2988 ),
2989 )
2990 .await?;
2991
2992 let region_mapping = if let kcmc::websocket::OkWebSocketResponseData::Modeling {
2993 modeling_response: kcmc::ok_response::OkModelingCmdResponse::CreateRegionFromQueryPoint(data),
2994 } = response
2995 {
2996 data.region_mapping
2997 } else {
2998 Default::default()
2999 };
3000
3001 (sketch, region_mapping)
3002 }
3003 (None, Some(segments)) => {
3004 if sketch.is_some() {
3005 return Err(KclError::new_semantic(KclErrorDetails::new(
3006 "Sketch parameter must not be provided when segments parameters is provided".to_owned(),
3007 vec![args.source_range],
3008 )));
3009 }
3010 let segments_len = segments.len();
3011 let mut segments = segments.into_iter();
3012 let Some(seg0_value) = segments.next() else {
3013 return Err(KclError::new_argument(KclErrorDetails::new(
3014 format!("Expected at least 1 segment to create a region, but got {segments_len}"),
3015 vec![args.source_range],
3016 )));
3017 };
3018 let seg1_value = segments.next().unwrap_or_else(|| seg0_value.clone());
3019 let Some(seg0) = seg0_value.into_segment() else {
3020 return Err(KclError::new_argument(KclErrorDetails::new(
3021 "Expected first segment to be a Segment".to_owned(),
3022 vec![args.source_range],
3023 )));
3024 };
3025 let Some(seg1) = seg1_value.into_segment() else {
3026 return Err(KclError::new_argument(KclErrorDetails::new(
3027 "Expected second segment to be a Segment".to_owned(),
3028 vec![args.source_range],
3029 )));
3030 };
3031 let intersection_index = intersection_index.map(|n| n.n as i32).unwrap_or(-1);
3032 let direction = direction.unwrap_or(CircularDirection::Counterclockwise);
3033
3034 let Some(sketch) = &seg0.sketch else {
3035 return Err(KclError::new_semantic(KclErrorDetails::new(
3036 "Expected first segment to have an associated sketch. The sketch must be solved to create a region from it.".to_owned(),
3037 vec![args.source_range],
3038 )));
3039 };
3040
3041 let meta = ModelingCmdMeta::from_args_id(exec_state, &args, region_id);
3042 let response = exec_state
3043 .send_modeling_cmd(
3044 meta,
3045 ModelingCmd::from(
3046 mcmd::CreateRegion::builder()
3047 .object_id(sketch.id)
3048 .segment(seg0.id)
3049 .intersection_segment(seg1.id)
3050 .intersection_index(intersection_index)
3051 .curve_clockwise(direction.is_clockwise())
3052 .version(region_version)
3053 .build(),
3054 ),
3055 )
3056 .await?;
3057
3058 let region_mapping = if let kcmc::websocket::OkWebSocketResponseData::Modeling {
3059 modeling_response: kcmc::ok_response::OkModelingCmdResponse::CreateRegion(data),
3060 } = response
3061 {
3062 data.region_mapping
3063 } else {
3064 Default::default()
3065 };
3066
3067 (SketchOrSegment::Segment(seg0), region_mapping)
3068 }
3069 (Some(_), Some(_)) => {
3070 return Err(KclError::new_semantic(KclErrorDetails::new(
3071 "Cannot provide both point and segments parameters. Choose one.".to_owned(),
3072 vec![args.source_range],
3073 )));
3074 }
3075 (None, None) => {
3076 return Err(KclError::new_semantic(KclErrorDetails::new(
3077 "Either point or segments parameter must be provided".to_owned(),
3078 vec![args.source_range],
3079 )));
3080 }
3081 };
3082
3083 let units = exec_state.length_unit();
3084 let to = [0.0, 0.0];
3085 let first_path = Path::ToPoint {
3086 base: BasePath {
3087 from: to,
3088 to,
3089 units,
3090 tag: None,
3091 geo_meta: GeoMeta {
3092 id: match &sketch_or_segment {
3093 SketchOrSegment::Sketch(sketch) => sketch.id,
3094 SketchOrSegment::Segment(segment) => segment.id,
3095 },
3096 metadata: args.source_range.into(),
3097 },
3098 },
3099 };
3100 let start_base_path = BasePath {
3101 from: to,
3102 to,
3103 tag: None,
3104 units,
3105 geo_meta: GeoMeta {
3106 id: region_id,
3107 metadata: args.source_range.into(),
3108 },
3109 };
3110 let mut sketch = match sketch_or_segment {
3111 SketchOrSegment::Sketch(sketch) => sketch,
3112 SketchOrSegment::Segment(segment) => {
3113 if let Some(sketch) = segment.sketch {
3114 sketch.as_ref().clone()
3115 } else {
3116 Sketch {
3117 id: region_id,
3118 original_id: region_id,
3119 artifact_id: region_id.into(),
3120 origin_sketch_id: None,
3121 on: segment.surface.clone(),
3122 paths: vec![first_path],
3123 inner_paths: vec![],
3124 units,
3125 mirror: Default::default(),
3126 clone: Default::default(),
3127 synthetic_jump_path_ids: vec![],
3128 meta: vec![args.source_range.into()],
3129 tags: Default::default(),
3130 start: start_base_path,
3131 is_closed: ProfileClosed::Explicitly,
3132 }
3133 }
3134 }
3135 };
3136 sketch.origin_sketch_id = Some(sketch.id);
3137 sketch.id = region_id;
3138 sketch.original_id = region_id;
3139 sketch.artifact_id = region_id.into();
3140
3141 let mut region_mapping = region_mapping;
3142 if args.ctx.no_engine_commands().await && region_mapping.is_empty() {
3143 let mut mock_mapping = HashMap::new();
3144 for path in &sketch.paths {
3145 mock_mapping.insert(exec_state.next_uuid(), path.get_id());
3146 }
3147 region_mapping = mock_mapping;
3148 }
3149 let original_segment_ids = sketch.paths.iter().map(|p| p.get_id()).collect::<Vec<_>>();
3150 let original_seg_to_region = build_reverse_region_mapping(®ion_mapping, &original_segment_ids);
3151
3152 {
3153 let mut new_paths = Vec::new();
3154 for path in &sketch.paths {
3155 let original_id = path.get_id();
3156 if let Some(region_ids) = original_seg_to_region.get(&original_id) {
3157 for region_id in region_ids {
3158 let mut new_path = path.clone();
3159 new_path.set_id(*region_id);
3160 new_paths.push(new_path);
3161 }
3162 }
3163 }
3164
3165 sketch.paths = new_paths;
3166
3167 for (_tag_name, tag) in &mut sketch.tags {
3168 let Some(info) = tag.get_cur_info().cloned() else {
3169 continue;
3170 };
3171 let original_id = info.id;
3172 if let Some(region_ids) = original_seg_to_region.get(&original_id) {
3173 let epoch = tag.info.last().map(|(e, _)| *e).unwrap_or(0);
3174 for (i, region_id) in region_ids.iter().enumerate() {
3175 if i == 0 {
3176 if let Some((_, existing)) = tag.info.last_mut() {
3177 existing.id = *region_id;
3178 }
3179 } else {
3180 let mut new_info = info.clone();
3181 new_info.id = *region_id;
3182 tag.info.push((epoch, new_info));
3183 }
3184 }
3185 }
3186 }
3187 }
3188
3189 if sketch.mirror.is_some() {
3193 sketch.mirror = sketch.paths.first().map(|p| p.get_id());
3194 }
3195
3196 sketch.meta.push(args.source_range.into());
3197 sketch.is_closed = ProfileClosed::Explicitly;
3198
3199 Ok(KclValue::Sketch {
3200 value: Box::new(sketch),
3201 })
3202}
3203
3204pub(crate) fn build_reverse_region_mapping(
3214 region_mapping: &HashMap<Uuid, Uuid>,
3215 original_segments: &[Uuid],
3216) -> IndexMap<Uuid, Vec<Uuid>> {
3217 let mut reverse: HashMap<Uuid, Vec<Uuid>> = HashMap::default();
3218 #[expect(
3219 clippy::iter_over_hash_type,
3220 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."
3221 )]
3222 for (region_id, original_id) in region_mapping {
3223 reverse.entry(*original_id).or_default().push(*region_id);
3224 }
3225 #[expect(
3226 clippy::iter_over_hash_type,
3227 reason = "This is safe since we're just sorting values."
3228 )]
3229 for values in reverse.values_mut() {
3230 values.sort_unstable();
3231 }
3232 let mut ordered = IndexMap::with_capacity(original_segments.len());
3233 for original_id in original_segments {
3234 let mut region_ids = Vec::new();
3235 reverse.entry(*original_id).and_modify(|entry_value| {
3236 region_ids = std::mem::take(entry_value);
3237 });
3238 if !region_ids.is_empty() {
3239 ordered.insert(*original_id, region_ids);
3240 }
3241 }
3242 ordered
3243}
3244
3245fn region_from_point(
3246 point: KclValue,
3247 sketch: Option<KclValue>,
3248 args: &Args,
3249) -> Result<(SketchOrSegment, [TyF64; 2]), KclError> {
3250 match point {
3251 KclValue::HomArray { .. } | KclValue::Tuple { .. } => {
3252 let Some(pt) = <[TyF64; 2]>::from_kcl_val(&point) else {
3253 return Err(KclError::new_semantic(KclErrorDetails::new(
3254 "Expected 2D point for point parameter".to_owned(),
3255 vec![args.source_range],
3256 )));
3257 };
3258
3259 let Some(sketch_value) = sketch else {
3260 return Err(KclError::new_semantic(KclErrorDetails::new(
3261 "Sketch must be provided when point is a 2D point".to_owned(),
3262 vec![args.source_range],
3263 )));
3264 };
3265 let sketch = match sketch_value {
3266 KclValue::Sketch { value } => *value,
3267 KclValue::Object { value, .. } => {
3268 let Some(meta_value) = value.get(SKETCH_OBJECT_META) else {
3269 return Err(KclError::new_semantic(KclErrorDetails::new(
3270 "Expected sketch to be of type Sketch with a meta field. Sketch must not be empty to create a region.".to_owned(),
3271 vec![args.source_range],
3272 )));
3273 };
3274 let meta_map = match meta_value {
3275 KclValue::Object { value, .. } => value,
3276 _ => {
3277 return Err(KclError::new_semantic(KclErrorDetails::new(
3278 "Expected sketch to be of type Sketch with a meta field that's an object".to_owned(),
3279 vec![args.source_range],
3280 )));
3281 }
3282 };
3283 let Some(sketch_value) = meta_map.get(SKETCH_OBJECT_META_SKETCH) else {
3284 return Err(KclError::new_semantic(KclErrorDetails::new(
3285 "Expected sketch meta to have a sketch field. Sketch must not be empty to create a region."
3286 .to_owned(),
3287 vec![args.source_range],
3288 )));
3289 };
3290 let Some(sketch) = sketch_value.as_sketch() else {
3291 return Err(KclError::new_semantic(KclErrorDetails::new(
3292 "Expected sketch meta to have a sketch field of type Sketch. Sketch must not be empty to create a region.".to_owned(),
3293 vec![args.source_range],
3294 )));
3295 };
3296 sketch.clone()
3297 }
3298 _ => {
3299 return Err(KclError::new_semantic(KclErrorDetails::new(
3300 "Expected sketch to be of type Sketch".to_owned(),
3301 vec![args.source_range],
3302 )));
3303 }
3304 };
3305
3306 Ok((SketchOrSegment::Sketch(sketch), pt))
3307 }
3308 KclValue::Segment { value } => match value.repr {
3309 crate::execution::SegmentRepr::Unsolved { .. } => Err(KclError::new_semantic(KclErrorDetails::new(
3310 "Segment provided to point parameter is unsolved; segments must be solved to be used as points"
3311 .to_owned(),
3312 vec![args.source_range],
3313 ))),
3314 crate::execution::SegmentRepr::Solved { segment } => {
3315 let pt = match &segment.kind {
3316 SegmentKind::Point { position, .. } => position.clone(),
3317 _ => {
3318 return Err(KclError::new_semantic(KclErrorDetails::new(
3319 "Expected segment to be a point segment".to_owned(),
3320 vec![args.source_range],
3321 )));
3322 }
3323 };
3324
3325 Ok((SketchOrSegment::Segment(*segment), pt))
3326 }
3327 },
3328 _ => Err(KclError::new_semantic(KclErrorDetails::new(
3329 "Expected point to be either a 2D point like `[0, 0]` or a point segment created from `point()`".to_owned(),
3330 vec![args.source_range],
3331 ))),
3332 }
3333}
3334#[cfg(test)]
3335mod tests {
3336
3337 use pretty_assertions::assert_eq;
3338
3339 use crate::execution::TagIdentifier;
3340 use crate::std::sketch::PlaneData;
3341 use crate::std::utils::calculate_circle_center;
3342
3343 #[test]
3344 fn test_deserialize_plane_data() {
3345 let data = PlaneData::XY;
3346 let mut str_json = serde_json::to_string(&data).unwrap();
3347 assert_eq!(str_json, "\"XY\"");
3348
3349 str_json = "\"YZ\"".to_string();
3350 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3351 assert_eq!(data, PlaneData::YZ);
3352
3353 str_json = "\"-YZ\"".to_string();
3354 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3355 assert_eq!(data, PlaneData::NegYZ);
3356
3357 str_json = "\"-xz\"".to_string();
3358 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3359 assert_eq!(data, PlaneData::NegXZ);
3360 }
3361
3362 #[test]
3363 fn test_deserialize_sketch_on_face_tag() {
3364 let data = "start";
3365 let mut str_json = serde_json::to_string(&data).unwrap();
3366 assert_eq!(str_json, "\"start\"");
3367
3368 str_json = "\"end\"".to_string();
3369 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3370 assert_eq!(
3371 data,
3372 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
3373 );
3374
3375 str_json = serde_json::to_string(&TagIdentifier {
3376 value: "thing".to_string(),
3377 info: Vec::new(),
3378 meta: Default::default(),
3379 })
3380 .unwrap();
3381 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3382 assert_eq!(
3383 data,
3384 crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
3385 value: "thing".to_string(),
3386 info: Vec::new(),
3387 meta: Default::default()
3388 }))
3389 );
3390
3391 str_json = "\"END\"".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::End)
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 str_json = "\"START\"".to_string();
3406 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3407 assert_eq!(
3408 data,
3409 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
3410 );
3411 }
3412
3413 #[test]
3414 fn test_circle_center() {
3415 let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
3416 assert_eq!(actual[0], 5.0);
3417 assert_eq!(actual[1], 0.0);
3418 }
3419}