1use std::collections::HashMap;
4
5use anyhow::Result;
6use indexmap::IndexMap;
7use kcl_api::SourceRef;
8use kcl_api::UnitLength;
9use kcl_error::SourceRange;
10use kcmc::ModelingCmd;
11use kcmc::each_cmd as mcmd;
12use kcmc::length_unit::LengthUnit;
13use kcmc::shared::Angle;
14use kcmc::shared::Point2d as KPoint2d; use kcmc::shared::Point3d as KPoint3d; use kcmc::websocket::ModelingCmdReq;
17use kittycad_modeling_cmds as kcmc;
18use kittycad_modeling_cmds::shared::PathSegment;
19use kittycad_modeling_cmds::shared::RegionVersion;
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::NumericTypeExt;
64use crate::execution::types::PrimitiveType;
65use crate::execution::types::RuntimeType;
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.unwrap_to_degrees(exec_state, args.source_range);
225 let angle_rad = angle.unwrap_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.unwrap_to_mm()),
251 end_radius: LengthUnit(end_radius.unwrap_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.unwrap_to_length_units(from.units);
263 let end_radius = end_radius.unwrap_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_back(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_back(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.unwrap_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_back(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.unwrap_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, NumericType::length(from.units))], 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.unwrap_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, NumericType::length(from.units)), 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.unwrap_to_length_units(from.units)).unwrap_or_default(),
881 angle.unwrap_to_degrees(exec_state, args.source_range),
882 from.ignore_units(),
883 );
884 let to = [
885 TyF64::new(to[0], NumericType::length(from.units)),
886 TyF64::new(to[1], NumericType::length(from.units)),
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.unwrap_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 = kcl_api::Object {
1202 id: plane_object_id,
1203 kind: kcl_api::ObjectKind::Plane(kcl_api::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_modeling_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: Default::default(),
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 = NumericType::length(sketch.units);
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 = NumericType::length(sketch.units);
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 = NumericType::length(sketch.units);
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_back(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].unwrap_to_mm()),
1572 y: LengthUnit(end_absolute[1].unwrap_to_mm()),
1573 z: LengthUnit(0.0),
1574 },
1575 interior: kcmc::shared::Point3d {
1576 x: LengthUnit(interior_absolute[0].unwrap_to_mm()),
1577 y: LengthUnit(interior_absolute[1].unwrap_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_back(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.unwrap_to_degrees(exec_state, source_range));
1635 let a_end = Angle::from_degrees(angle_end.unwrap_to_degrees(exec_state, source_range));
1636 let radius = radius.unwrap_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_back(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.unwrap_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.unwrap_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.unwrap_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_back(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_back(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].unwrap_to_length_units(from.units),
2029 from.y + end[1].unwrap_to_length_units(from.units),
2030 ];
2031 let control1_abs = [
2033 from.x + control1[0].unwrap_to_length_units(from.units),
2034 from.y + control1[1].unwrap_to_length_units(from.units),
2035 ];
2036 let control2_abs = [
2037 from.x + control2[0].unwrap_to_length_units(from.units),
2038 from.y + control2[1].unwrap_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 = [
2062 end[0].unwrap_to_length_units(from.units),
2063 end[1].unwrap_to_length_units(from.units),
2064 ];
2065 let control1_abs = control1.clone().map(|v| v.unwrap_to_length_units(from.units));
2066 let control2_abs = control2.clone().map(|v| v.unwrap_to_length_units(from.units));
2067 exec_state
2068 .batch_modeling_cmd(
2069 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2070 ModelingCmd::from(
2071 mcmd::ExtendPath::builder()
2072 .path(sketch.id.into())
2073 .segment(PathSegment::Bezier {
2074 control1: KPoint2d::from(point_to_mm(control1)).with_z(0.0).map(LengthUnit),
2075 control2: KPoint2d::from(point_to_mm(control2)).with_z(0.0).map(LengthUnit),
2076 end: KPoint2d::from(point_to_mm(end)).with_z(0.0).map(LengthUnit),
2077 relative: false,
2078 })
2079 .build(),
2080 ),
2081 )
2082 .await?;
2083 (to, control1_abs, control2_abs)
2084 }
2085 _ => {
2086 return Err(KclError::new_semantic(KclErrorDetails::new(
2087 "You must either give `control1`, `control2` and `end`, or `control1Absolute`, `control2Absolute` and `endAbsolute`.".to_owned(),
2088 vec![args.source_range],
2089 )));
2090 }
2091 };
2092
2093 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2094
2095 let current_path = Path::Bezier {
2096 base: BasePath {
2097 from: from.ignore_units(),
2098 to,
2099 tag: tag.clone(),
2100 units: sketch.units,
2101 geo_meta: GeoMeta {
2102 id,
2103 metadata: args.source_range.into(),
2104 },
2105 },
2106 control1: control1_abs,
2107 control2: control2_abs,
2108 };
2109
2110 let mut new_sketch = sketch;
2111 if let Some(tag) = &tag {
2112 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2113 }
2114 if loops_back_to_start {
2115 new_sketch.is_closed = ProfileClosed::Implicitly;
2116 }
2117
2118 new_sketch.paths.push_back(current_path);
2119
2120 Ok(new_sketch)
2121}
2122
2123pub async fn subtract_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2125 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2126
2127 let tool: Vec<Sketch> = args.get_kw_arg(
2128 "tool",
2129 &RuntimeType::Array(
2130 Box::new(RuntimeType::Primitive(PrimitiveType::Sketch)),
2131 ArrayLen::Minimum(1),
2132 ),
2133 exec_state,
2134 )?;
2135
2136 let new_sketch = inner_subtract_2d(sketch, tool, exec_state, args).await?;
2137 Ok(KclValue::Sketch {
2138 value: Box::new(new_sketch),
2139 })
2140}
2141
2142async fn inner_subtract_2d(
2143 mut sketch: Sketch,
2144 tool: Vec<Sketch>,
2145 exec_state: &mut ExecState,
2146 args: Args,
2147) -> Result<Sketch, KclError> {
2148 for hole_sketch in tool {
2149 exec_state
2150 .batch_modeling_cmd(
2151 ModelingCmdMeta::from_args(exec_state, &args),
2152 ModelingCmd::from(
2153 mcmd::Solid2dAddHole::builder()
2154 .object_id(sketch.id)
2155 .hole_id(hole_sketch.id)
2156 .build(),
2157 ),
2158 )
2159 .await?;
2160
2161 exec_state
2164 .batch_modeling_cmd(
2165 ModelingCmdMeta::from_args(exec_state, &args),
2166 ModelingCmd::from(
2167 mcmd::ObjectVisible::builder()
2168 .object_id(hole_sketch.id)
2169 .hidden(true)
2170 .build(),
2171 ),
2172 )
2173 .await?;
2174
2175 sketch.inner_paths.extend(hole_sketch.paths.iter().cloned());
2180 }
2181
2182 Ok(sketch)
2185}
2186
2187pub async fn elliptic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2189 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2190 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2191 let major_radius = args.get_kw_arg("majorRadius", &RuntimeType::num_any(), exec_state)?;
2192 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::num_any(), exec_state)?;
2193
2194 let elliptic_point = inner_elliptic_point(x, y, major_radius, minor_radius, &args).await?;
2195
2196 args.make_kcl_val_from_point(elliptic_point, NumericType::length(exec_state.length_unit()))
2197}
2198
2199async fn inner_elliptic_point(
2200 x: Option<TyF64>,
2201 y: Option<TyF64>,
2202 major_radius: TyF64,
2203 minor_radius: TyF64,
2204 args: &Args,
2205) -> Result<[f64; 2], KclError> {
2206 let major_radius = major_radius.n;
2207 let minor_radius = minor_radius.n;
2208 if let Some(x) = x {
2209 if x.n.abs() > major_radius {
2210 Err(KclError::Type {
2211 details: KclErrorDetails::new(
2212 format!(
2213 "Invalid input. The x value, {}, cannot be larger than the major radius {}.",
2214 x.n, major_radius
2215 ),
2216 vec![args.source_range],
2217 ),
2218 })
2219 } else {
2220 Ok((
2221 x.n,
2222 minor_radius * (1.0 - x.n.squared() / major_radius.squared()).sqrt(),
2223 )
2224 .into())
2225 }
2226 } else if let Some(y) = y {
2227 if y.n > minor_radius {
2228 Err(KclError::Type {
2229 details: KclErrorDetails::new(
2230 format!(
2231 "Invalid input. The y value, {}, cannot be larger than the minor radius {}.",
2232 y.n, minor_radius
2233 ),
2234 vec![args.source_range],
2235 ),
2236 })
2237 } else {
2238 Ok((
2239 major_radius * (1.0 - y.n.squared() / minor_radius.squared()).sqrt(),
2240 y.n,
2241 )
2242 .into())
2243 }
2244 } else {
2245 Err(KclError::Type {
2246 details: KclErrorDetails::new(
2247 "Invalid input. Must have either x or y, you cannot have both or neither.".to_owned(),
2248 vec![args.source_range],
2249 ),
2250 })
2251 }
2252}
2253
2254pub async fn elliptic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2256 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2257
2258 let center = args.get_kw_arg("center", &RuntimeType::point2d(), exec_state)?;
2259 let angle_start = args.get_kw_arg("angleStart", &RuntimeType::degrees(), exec_state)?;
2260 let angle_end = args.get_kw_arg("angleEnd", &RuntimeType::degrees(), exec_state)?;
2261 let major_radius = args.get_kw_arg_opt("majorRadius", &RuntimeType::length(), exec_state)?;
2262 let major_axis = args.get_kw_arg_opt("majorAxis", &RuntimeType::point2d(), exec_state)?;
2263 let minor_radius = args.get_kw_arg("minorRadius", &RuntimeType::length(), exec_state)?;
2264 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2265
2266 let new_sketch = inner_elliptic(
2267 sketch,
2268 center,
2269 angle_start,
2270 angle_end,
2271 major_radius,
2272 major_axis,
2273 minor_radius,
2274 tag,
2275 exec_state,
2276 args,
2277 )
2278 .await?;
2279 Ok(KclValue::Sketch {
2280 value: Box::new(new_sketch),
2281 })
2282}
2283
2284#[allow(clippy::too_many_arguments)]
2285pub(crate) async fn inner_elliptic(
2286 sketch: Sketch,
2287 center: [TyF64; 2],
2288 angle_start: TyF64,
2289 angle_end: TyF64,
2290 major_radius: Option<TyF64>,
2291 major_axis: Option<[TyF64; 2]>,
2292 minor_radius: TyF64,
2293 tag: Option<TagNode>,
2294 exec_state: &mut ExecState,
2295 args: Args,
2296) -> Result<Sketch, KclError> {
2297 let from: Point2d = sketch.current_pen_position()?;
2298 let id = exec_state.next_uuid();
2299
2300 let center_u = point_to_len_unit(center, from.units);
2301
2302 let major_axis = match (major_axis, major_radius) {
2303 (Some(_), Some(_)) | (None, None) => {
2304 return Err(KclError::new_type(KclErrorDetails::new(
2305 "Provide either `majorAxis` or `majorRadius`.".to_string(),
2306 vec![args.source_range],
2307 )));
2308 }
2309 (Some(major_axis), None) => major_axis,
2310 (None, Some(major_radius)) => [
2311 major_radius.clone(),
2312 TyF64 {
2313 n: 0.0,
2314 ty: major_radius.ty,
2315 },
2316 ],
2317 };
2318 let start_angle = Angle::from_degrees(angle_start.unwrap_to_degrees(exec_state, args.source_range));
2319 let end_angle = Angle::from_degrees(angle_end.unwrap_to_degrees(exec_state, args.source_range));
2320 let major_axis_magnitude = (major_axis[0].unwrap_to_length_units(from.units)
2321 * major_axis[0].unwrap_to_length_units(from.units)
2322 + major_axis[1].unwrap_to_length_units(from.units) * major_axis[1].unwrap_to_length_units(from.units))
2323 .sqrt();
2324 let to = [
2325 major_axis_magnitude * libm::cos(end_angle.to_radians()),
2326 minor_radius.unwrap_to_length_units(from.units) * libm::sin(end_angle.to_radians()),
2327 ];
2328 let loops_back_to_start = does_segment_close_sketch(to, sketch.start.from);
2329 let major_axis_angle = libm::atan2(major_axis[1].n, major_axis[0].n);
2330
2331 let point = [
2332 center_u[0] + to[0] * libm::cos(major_axis_angle) - to[1] * libm::sin(major_axis_angle),
2333 center_u[1] + to[0] * libm::sin(major_axis_angle) + to[1] * libm::cos(major_axis_angle),
2334 ];
2335
2336 let axis = major_axis.map(|x| x.unwrap_to_mm());
2337 exec_state
2338 .batch_modeling_cmd(
2339 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2340 ModelingCmd::from(
2341 mcmd::ExtendPath::builder()
2342 .path(sketch.id.into())
2343 .segment(PathSegment::Ellipse {
2344 center: KPoint2d::from(untyped_point_to_mm(center_u, from.units)).map(LengthUnit),
2345 major_axis: axis.map(LengthUnit).into(),
2346 minor_radius: LengthUnit(minor_radius.unwrap_to_mm()),
2347 start_angle,
2348 end_angle,
2349 })
2350 .build(),
2351 ),
2352 )
2353 .await?;
2354
2355 let current_path = Path::Ellipse {
2356 ccw: start_angle < end_angle,
2357 center: center_u,
2358 major_axis: axis,
2359 minor_radius: minor_radius.unwrap_to_mm(),
2360 base: BasePath {
2361 from: from.ignore_units(),
2362 to: point,
2363 tag: tag.clone(),
2364 units: sketch.units,
2365 geo_meta: GeoMeta {
2366 id,
2367 metadata: args.source_range.into(),
2368 },
2369 },
2370 };
2371 let mut new_sketch = sketch;
2372 if let Some(tag) = &tag {
2373 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2374 }
2375 if loops_back_to_start {
2376 new_sketch.is_closed = ProfileClosed::Implicitly;
2377 }
2378
2379 new_sketch.paths.push_back(current_path);
2380
2381 Ok(new_sketch)
2382}
2383
2384pub async fn hyperbolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2386 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2387 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2388 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::num_any(), exec_state)?;
2389 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::num_any(), exec_state)?;
2390
2391 let hyperbolic_point = inner_hyperbolic_point(x, y, semi_major, semi_minor, &args).await?;
2392
2393 args.make_kcl_val_from_point(hyperbolic_point, NumericType::length(exec_state.length_unit()))
2394}
2395
2396async fn inner_hyperbolic_point(
2397 x: Option<TyF64>,
2398 y: Option<TyF64>,
2399 semi_major: TyF64,
2400 semi_minor: TyF64,
2401 args: &Args,
2402) -> Result<[f64; 2], KclError> {
2403 let semi_major = semi_major.n;
2404 let semi_minor = semi_minor.n;
2405 if let Some(x) = x {
2406 if x.n.abs() < semi_major {
2407 Err(KclError::Type {
2408 details: KclErrorDetails::new(
2409 format!(
2410 "Invalid input. The x value, {}, cannot be less than the semi major value, {}.",
2411 x.n, semi_major
2412 ),
2413 vec![args.source_range],
2414 ),
2415 })
2416 } else {
2417 Ok((x.n, semi_minor * (x.n.squared() / semi_major.squared() - 1.0).sqrt()).into())
2418 }
2419 } else if let Some(y) = y {
2420 Ok((semi_major * (y.n.squared() / semi_minor.squared() + 1.0).sqrt(), y.n).into())
2421 } else {
2422 Err(KclError::Type {
2423 details: KclErrorDetails::new(
2424 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2425 vec![args.source_range],
2426 ),
2427 })
2428 }
2429}
2430
2431pub async fn hyperbolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2433 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2434
2435 let semi_major = args.get_kw_arg("semiMajor", &RuntimeType::length(), exec_state)?;
2436 let semi_minor = args.get_kw_arg("semiMinor", &RuntimeType::length(), exec_state)?;
2437 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2438 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2439 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2440 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2441 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2442
2443 let new_sketch = inner_hyperbolic(
2444 sketch,
2445 semi_major,
2446 semi_minor,
2447 interior,
2448 end,
2449 interior_absolute,
2450 end_absolute,
2451 tag,
2452 exec_state,
2453 args,
2454 )
2455 .await?;
2456 Ok(KclValue::Sketch {
2457 value: Box::new(new_sketch),
2458 })
2459}
2460
2461fn hyperbolic_tangent(point: Point2d, semi_major: f64, semi_minor: f64) -> [f64; 2] {
2463 (point.y * semi_major.squared(), point.x * semi_minor.squared()).into()
2464}
2465
2466#[allow(clippy::too_many_arguments)]
2467pub(crate) async fn inner_hyperbolic(
2468 sketch: Sketch,
2469 semi_major: TyF64,
2470 semi_minor: TyF64,
2471 interior: Option<[TyF64; 2]>,
2472 end: Option<[TyF64; 2]>,
2473 interior_absolute: Option<[TyF64; 2]>,
2474 end_absolute: Option<[TyF64; 2]>,
2475 tag: Option<TagNode>,
2476 exec_state: &mut ExecState,
2477 args: Args,
2478) -> Result<Sketch, KclError> {
2479 let from = sketch.current_pen_position()?;
2480 let id = exec_state.next_uuid();
2481
2482 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2483 (Some(interior), Some(end), None, None) => (interior, end, true),
2484 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2485 _ => return Err(KclError::Type {
2486 details: KclErrorDetails::new(
2487 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2488 .to_owned(),
2489 vec![args.source_range],
2490 ),
2491 }),
2492 };
2493
2494 let interior = point_to_len_unit(interior, from.units);
2495 let end = point_to_len_unit(end, from.units);
2496 let end_point = Point2d {
2497 x: end[0],
2498 y: end[1],
2499 units: from.units,
2500 };
2501 let loops_back_to_start = does_segment_close_sketch(end, sketch.start.from);
2502
2503 let semi_major_u = semi_major.unwrap_to_length_units(from.units);
2504 let semi_minor_u = semi_minor.unwrap_to_length_units(from.units);
2505
2506 let start_tangent = hyperbolic_tangent(from, semi_major_u, semi_minor_u);
2507 let end_tangent = hyperbolic_tangent(end_point, semi_major_u, semi_minor_u);
2508
2509 exec_state
2510 .batch_modeling_cmd(
2511 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2512 ModelingCmd::from(
2513 mcmd::ExtendPath::builder()
2514 .path(sketch.id.into())
2515 .segment(PathSegment::ConicTo {
2516 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2517 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2518 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2519 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2520 relative,
2521 })
2522 .build(),
2523 ),
2524 )
2525 .await?;
2526
2527 let current_path = Path::Conic {
2528 base: BasePath {
2529 from: from.ignore_units(),
2530 to: end,
2531 tag: tag.clone(),
2532 units: sketch.units,
2533 geo_meta: GeoMeta {
2534 id,
2535 metadata: args.source_range.into(),
2536 },
2537 },
2538 };
2539
2540 let mut new_sketch = sketch;
2541 if let Some(tag) = &tag {
2542 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2543 }
2544 if loops_back_to_start {
2545 new_sketch.is_closed = ProfileClosed::Implicitly;
2546 }
2547
2548 new_sketch.paths.push_back(current_path);
2549
2550 Ok(new_sketch)
2551}
2552
2553pub async fn parabolic_point(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2555 let x = args.get_kw_arg_opt("x", &RuntimeType::length(), exec_state)?;
2556 let y = args.get_kw_arg_opt("y", &RuntimeType::length(), exec_state)?;
2557 let coefficients = args.get_kw_arg(
2558 "coefficients",
2559 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2560 exec_state,
2561 )?;
2562
2563 let parabolic_point = inner_parabolic_point(x, y, &coefficients, &args).await?;
2564
2565 args.make_kcl_val_from_point(parabolic_point, NumericType::length(exec_state.length_unit()))
2566}
2567
2568async fn inner_parabolic_point(
2569 x: Option<TyF64>,
2570 y: Option<TyF64>,
2571 coefficients: &[TyF64; 3],
2572 args: &Args,
2573) -> Result<[f64; 2], KclError> {
2574 let a = coefficients[0].n;
2575 let b = coefficients[1].n;
2576 let c = coefficients[2].n;
2577 if let Some(x) = x {
2578 Ok((x.n, a * x.n.squared() + b * x.n + c).into())
2579 } else if let Some(y) = y {
2580 let det = (b.squared() - 4.0 * a * (c - y.n)).sqrt();
2581 Ok(((-b + det) / (2.0 * a), y.n).into())
2582 } else {
2583 Err(KclError::Type {
2584 details: KclErrorDetails::new(
2585 "Invalid input. Must have either x or y, cannot have both or neither.".to_owned(),
2586 vec![args.source_range],
2587 ),
2588 })
2589 }
2590}
2591
2592pub async fn parabolic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2594 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2595
2596 let coefficients = args.get_kw_arg_opt(
2597 "coefficients",
2598 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(3)),
2599 exec_state,
2600 )?;
2601 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2602 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2603 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2604 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2605 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2606
2607 let new_sketch = inner_parabolic(
2608 sketch,
2609 coefficients,
2610 interior,
2611 end,
2612 interior_absolute,
2613 end_absolute,
2614 tag,
2615 exec_state,
2616 args,
2617 )
2618 .await?;
2619 Ok(KclValue::Sketch {
2620 value: Box::new(new_sketch),
2621 })
2622}
2623
2624fn parabolic_tangent(point: Point2d, a: f64, b: f64) -> [f64; 2] {
2625 (1.0, 2.0 * a * point.x + b).into()
2628}
2629
2630#[allow(clippy::too_many_arguments)]
2631pub(crate) async fn inner_parabolic(
2632 sketch: Sketch,
2633 coefficients: Option<[TyF64; 3]>,
2634 interior: Option<[TyF64; 2]>,
2635 end: Option<[TyF64; 2]>,
2636 interior_absolute: Option<[TyF64; 2]>,
2637 end_absolute: Option<[TyF64; 2]>,
2638 tag: Option<TagNode>,
2639 exec_state: &mut ExecState,
2640 args: Args,
2641) -> Result<Sketch, KclError> {
2642 let from = sketch.current_pen_position()?;
2643 let id = exec_state.next_uuid();
2644
2645 if (coefficients.is_some() && interior.is_some()) || (coefficients.is_none() && interior.is_none()) {
2646 return Err(KclError::Type {
2647 details: KclErrorDetails::new(
2648 "Invalid combination of arguments. Either provide (a, b, c) or (interior)".to_owned(),
2649 vec![args.source_range],
2650 ),
2651 });
2652 }
2653
2654 let (interior, end, relative) = match (coefficients.clone(), interior, end, interior_absolute, end_absolute) {
2655 (None, Some(interior), Some(end), None, None) => {
2656 let interior = point_to_len_unit(interior, from.units);
2657 let end = point_to_len_unit(end, from.units);
2658 (interior,end, true)
2659 },
2660 (None, None, None, Some(interior_absolute), Some(end_absolute)) => {
2661 let interior_absolute = point_to_len_unit(interior_absolute, from.units);
2662 let end_absolute = point_to_len_unit(end_absolute, from.units);
2663 (interior_absolute, end_absolute, false)
2664 }
2665 (Some(coefficients), _, Some(end), _, _) => {
2666 let end = point_to_len_unit(end, from.units);
2667 let interior =
2668 inner_parabolic_point(
2669 Some(TyF64::count(0.5 * (from.x + end[0]))),
2670 None,
2671 &coefficients,
2672 &args,
2673 )
2674 .await?;
2675 (interior, end, true)
2676 }
2677 (Some(coefficients), _, _, _, Some(end)) => {
2678 let end = point_to_len_unit(end, from.units);
2679 let interior =
2680 inner_parabolic_point(
2681 Some(TyF64::count(0.5 * (from.x + end[0]))),
2682 None,
2683 &coefficients,
2684 &args,
2685 )
2686 .await?;
2687 (interior, end, false)
2688 }
2689 _ => return
2690 Err(KclError::Type{details: KclErrorDetails::new(
2691 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute) if coefficients are not provided."
2692 .to_owned(),
2693 vec![args.source_range],
2694 )}),
2695 };
2696
2697 let end_point = Point2d {
2698 x: end[0],
2699 y: end[1],
2700 units: from.units,
2701 };
2702
2703 let (a, b, _c) = if let Some([a, b, c]) = coefficients {
2704 (a.n, b.n, c.n)
2705 } else {
2706 let denom = (from.x - interior[0]) * (from.x - end_point.x) * (interior[0] - end_point.x);
2708 let a = (end_point.x * (interior[1] - from.y)
2709 + interior[0] * (from.y - end_point.y)
2710 + from.x * (end_point.y - interior[1]))
2711 / denom;
2712 let b = (end_point.x.squared() * (from.y - interior[1])
2713 + interior[0].squared() * (end_point.y - from.y)
2714 + from.x.squared() * (interior[1] - end_point.y))
2715 / denom;
2716 let c = (interior[0] * end_point.x * (interior[0] - end_point.x) * from.y
2717 + end_point.x * from.x * (end_point.x - from.x) * interior[1]
2718 + from.x * interior[0] * (from.x - interior[0]) * end_point.y)
2719 / denom;
2720
2721 (a, b, c)
2722 };
2723
2724 let start_tangent = parabolic_tangent(from, a, b);
2725 let end_tangent = parabolic_tangent(end_point, a, b);
2726
2727 exec_state
2728 .batch_modeling_cmd(
2729 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2730 ModelingCmd::from(
2731 mcmd::ExtendPath::builder()
2732 .path(sketch.id.into())
2733 .segment(PathSegment::ConicTo {
2734 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2735 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2736 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2737 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2738 relative,
2739 })
2740 .build(),
2741 ),
2742 )
2743 .await?;
2744
2745 let current_path = Path::Conic {
2746 base: BasePath {
2747 from: from.ignore_units(),
2748 to: end,
2749 tag: tag.clone(),
2750 units: sketch.units,
2751 geo_meta: GeoMeta {
2752 id,
2753 metadata: args.source_range.into(),
2754 },
2755 },
2756 };
2757
2758 let mut new_sketch = sketch;
2759 if let Some(tag) = &tag {
2760 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2761 }
2762
2763 new_sketch.paths.push_back(current_path);
2764
2765 Ok(new_sketch)
2766}
2767
2768fn conic_tangent(coefficients: [f64; 6], point: [f64; 2]) -> [f64; 2] {
2769 let [a, b, c, d, e, _] = coefficients;
2770
2771 (
2772 c * point[0] + 2.0 * b * point[1] + e,
2773 -(2.0 * a * point[0] + c * point[1] + d),
2774 )
2775 .into()
2776}
2777
2778pub async fn conic(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2780 let sketch = args.get_unlabeled_kw_arg("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
2781
2782 let start_tangent = args.get_kw_arg_opt("startTangent", &RuntimeType::point2d(), exec_state)?;
2783 let end_tangent = args.get_kw_arg_opt("endTangent", &RuntimeType::point2d(), exec_state)?;
2784 let end = args.get_kw_arg_opt("end", &RuntimeType::point2d(), exec_state)?;
2785 let interior = args.get_kw_arg_opt("interior", &RuntimeType::point2d(), exec_state)?;
2786 let end_absolute = args.get_kw_arg_opt("endAbsolute", &RuntimeType::point2d(), exec_state)?;
2787 let interior_absolute = args.get_kw_arg_opt("interiorAbsolute", &RuntimeType::point2d(), exec_state)?;
2788 let coefficients = args.get_kw_arg_opt(
2789 "coefficients",
2790 &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Known(6)),
2791 exec_state,
2792 )?;
2793 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
2794
2795 let new_sketch = inner_conic(
2796 sketch,
2797 start_tangent,
2798 end,
2799 end_tangent,
2800 interior,
2801 coefficients,
2802 interior_absolute,
2803 end_absolute,
2804 tag,
2805 exec_state,
2806 args,
2807 )
2808 .await?;
2809 Ok(KclValue::Sketch {
2810 value: Box::new(new_sketch),
2811 })
2812}
2813
2814#[allow(clippy::too_many_arguments)]
2815pub(crate) async fn inner_conic(
2816 sketch: Sketch,
2817 start_tangent: Option<[TyF64; 2]>,
2818 end: Option<[TyF64; 2]>,
2819 end_tangent: Option<[TyF64; 2]>,
2820 interior: Option<[TyF64; 2]>,
2821 coefficients: Option<[TyF64; 6]>,
2822 interior_absolute: Option<[TyF64; 2]>,
2823 end_absolute: Option<[TyF64; 2]>,
2824 tag: Option<TagNode>,
2825 exec_state: &mut ExecState,
2826 args: Args,
2827) -> Result<Sketch, KclError> {
2828 let from: Point2d = sketch.current_pen_position()?;
2829 let id = exec_state.next_uuid();
2830
2831 if (coefficients.is_some() && (start_tangent.is_some() || end_tangent.is_some()))
2832 || (coefficients.is_none() && (start_tangent.is_none() && end_tangent.is_none()))
2833 {
2834 return Err(KclError::Type {
2835 details: KclErrorDetails::new(
2836 "Invalid combination of arguments. Either provide coefficients or (startTangent, endTangent)"
2837 .to_owned(),
2838 vec![args.source_range],
2839 ),
2840 });
2841 }
2842
2843 let (interior, end, relative) = match (interior, end, interior_absolute, end_absolute) {
2844 (Some(interior), Some(end), None, None) => (interior, end, true),
2845 (None, None, Some(interior_absolute), Some(end_absolute)) => (interior_absolute, end_absolute, false),
2846 _ => return Err(KclError::Type {
2847 details: KclErrorDetails::new(
2848 "Invalid combination of arguments. Either provide (end, interior) or (endAbsolute, interiorAbsolute)"
2849 .to_owned(),
2850 vec![args.source_range],
2851 ),
2852 }),
2853 };
2854
2855 let end = point_to_len_unit(end, from.units);
2856 let interior = point_to_len_unit(interior, from.units);
2857
2858 let (start_tangent, end_tangent) = if let Some(coeffs) = coefficients {
2859 let (coeffs, _) = untype_array(coeffs);
2860 (conic_tangent(coeffs, [from.x, from.y]), conic_tangent(coeffs, end))
2861 } else {
2862 let start = if let Some(start_tangent) = start_tangent {
2863 point_to_len_unit(start_tangent, from.units)
2864 } else {
2865 let previous_point = sketch
2866 .get_tangential_info_from_paths()
2867 .tan_previous_point(from.ignore_units());
2868 let from = from.ignore_units();
2869 [from[0] - previous_point[0], from[1] - previous_point[1]]
2870 };
2871
2872 let Some(end_tangent) = end_tangent else {
2873 return Err(KclError::new_semantic(KclErrorDetails::new(
2874 "You must either provide either `coefficients` or `endTangent`.".to_owned(),
2875 vec![args.source_range],
2876 )));
2877 };
2878 let end_tan = point_to_len_unit(end_tangent, from.units);
2879 (start, end_tan)
2880 };
2881
2882 exec_state
2883 .batch_modeling_cmd(
2884 ModelingCmdMeta::from_args_id(exec_state, &args, id),
2885 ModelingCmd::from(
2886 mcmd::ExtendPath::builder()
2887 .path(sketch.id.into())
2888 .segment(PathSegment::ConicTo {
2889 start_tangent: KPoint2d::from(untyped_point_to_mm(start_tangent, from.units)).map(LengthUnit),
2890 end_tangent: KPoint2d::from(untyped_point_to_mm(end_tangent, from.units)).map(LengthUnit),
2891 end: KPoint2d::from(untyped_point_to_mm(end, from.units)).map(LengthUnit),
2892 interior: KPoint2d::from(untyped_point_to_mm(interior, from.units)).map(LengthUnit),
2893 relative,
2894 })
2895 .build(),
2896 ),
2897 )
2898 .await?;
2899
2900 let current_path = Path::Conic {
2901 base: BasePath {
2902 from: from.ignore_units(),
2903 to: end,
2904 tag: tag.clone(),
2905 units: sketch.units,
2906 geo_meta: GeoMeta {
2907 id,
2908 metadata: args.source_range.into(),
2909 },
2910 },
2911 };
2912
2913 let mut new_sketch = sketch;
2914 if let Some(tag) = &tag {
2915 new_sketch.add_tag(tag, ¤t_path, exec_state, None);
2916 }
2917
2918 new_sketch.paths.push_back(current_path);
2919
2920 Ok(new_sketch)
2921}
2922
2923pub(super) async fn region(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2924 let point = args.get_kw_arg_opt(
2925 "point",
2926 &RuntimeType::Union(vec![RuntimeType::point2d(), RuntimeType::segment()]),
2927 exec_state,
2928 )?;
2929 let segments = args.get_kw_arg_opt(
2930 "segments",
2931 &RuntimeType::Array(Box::new(RuntimeType::segment()), ArrayLen::Minimum(1)),
2932 exec_state,
2933 )?;
2934 let intersection_index = args.get_kw_arg_opt("intersectionIndex", &RuntimeType::count(), exec_state)?;
2935 let direction = args.get_kw_arg_opt("direction", &RuntimeType::string(), exec_state)?;
2936 let sketch = args.get_kw_arg_opt("sketch", &RuntimeType::any(), exec_state)?;
2937 inner_region(point, segments, intersection_index, direction, sketch, exec_state, args).await
2938}
2939
2940#[expect(clippy::large_enum_variant)]
2943enum SketchOrSegment {
2944 Sketch(Sketch),
2945 Segment(Segment),
2946}
2947
2948impl SketchOrSegment {
2949 fn sketch(&self) -> Result<&Sketch, KclError> {
2950 match self {
2951 SketchOrSegment::Sketch(sketch) => Ok(sketch),
2952 SketchOrSegment::Segment(segment) => segment.sketch.as_deref().ok_or_else(|| {
2953 KclError::new_semantic(KclErrorDetails::new(
2954 "Segment should have an associated sketch".to_owned(),
2955 vec![],
2956 ))
2957 }),
2958 }
2959 }
2960}
2961
2962const REGION_DOCS_URL: &str = "https://zoo.dev/docs/kcl-std/functions/std-sketch-region";
2963
2964const REGION_FROM_POINT_ENGINE_HELP: &str = "The engine found no closed boundary containing that point. Check that the point lies inside a fully closed boundary of the sketch, and that `sketch` names the sketch the point belongs to. Prefer `segments = [...]`, which traces the boundary from sketch segments instead of a point.";
2969
2970const REGION_FROM_SEGMENTS_ENGINE_HELP: &str = "The engine could not trace a closed boundary from those segments. Check that the segments belong to the same solved sketch and enclose an area. Use `intersectionIndex` and `direction` to choose between boundaries when more than one is possible.";
2972
2973fn with_region_help(mut error: KclError, help: &str) -> KclError {
2979 if !matches!(error, KclError::Engine { .. }) {
2980 return error;
2981 }
2982 let details = error.details_mut();
2983 let separator = if details.message.ends_with(['.', '!', '?']) {
2984 ""
2985 } else {
2986 "."
2987 };
2988 details.message = format!("{}{separator} {help} See {REGION_DOCS_URL}", details.message);
2989 error
2990}
2991
2992async fn inner_region(
2993 point: Option<KclValue>,
2994 segments: Option<Vec<KclValue>>,
2995 intersection_index: Option<TyF64>,
2996 direction: Option<CircularDirection>,
2997 sketch: Option<KclValue>,
2998 exec_state: &mut ExecState,
2999 args: Args,
3000) -> Result<KclValue, KclError> {
3001 let region_id = exec_state.next_uuid();
3002 let kcl_version = exec_state.kcl_version();
3003 let region_version = if kcl_version <= KclVersion::V1 {
3004 RegionVersion::V0
3005 } else {
3006 RegionVersion::V1
3007 };
3008
3009 let (sketch_or_segment, region_mapping) = match (point, segments) {
3010 (Some(point), None) => {
3011 let (sketch, pt) = region_from_point(point, sketch, &args)?;
3012
3013 let meta = ModelingCmdMeta::from_args_id(exec_state, &args, region_id);
3014 let response = exec_state
3015 .send_modeling_cmd(
3016 meta,
3017 ModelingCmd::from(
3018 mcmd::CreateRegionFromQueryPoint::builder()
3019 .object_id(sketch.sketch()?.id)
3020 .query_point(KPoint2d::from(point_to_mm(pt.clone())).map(LengthUnit))
3021 .version(region_version)
3022 .build(),
3023 ),
3024 )
3025 .await
3026 .map_err(|error| with_region_help(error, REGION_FROM_POINT_ENGINE_HELP))?;
3027
3028 let region_mapping = if let kcmc::websocket::OkWebSocketResponseData::Modeling {
3029 modeling_response: kcmc::ok_response::OkModelingCmdResponse::CreateRegionFromQueryPoint(data),
3030 } = response
3031 {
3032 data.region_mapping
3033 } else {
3034 Default::default()
3035 };
3036
3037 (sketch, region_mapping)
3038 }
3039 (None, Some(segments)) => {
3040 if sketch.is_some() {
3041 return Err(KclError::new_semantic(KclErrorDetails::new(
3042 "Sketch parameter must not be provided when segments parameters is provided".to_owned(),
3043 vec![args.source_range],
3044 )));
3045 }
3046 let segments_len = segments.len();
3047 let mut segments = segments.into_iter();
3048 let Some(seg0_value) = segments.next() else {
3049 return Err(KclError::new_argument(KclErrorDetails::new(
3050 format!("Expected at least 1 segment to create a region, but got {segments_len}"),
3051 vec![args.source_range],
3052 )));
3053 };
3054 let seg1_value = segments.next().unwrap_or_else(|| seg0_value.clone());
3055 let Some(seg0) = seg0_value.into_segment() else {
3056 return Err(KclError::new_argument(KclErrorDetails::new(
3057 "Expected first segment to be a Segment".to_owned(),
3058 vec![args.source_range],
3059 )));
3060 };
3061 let Some(seg1) = seg1_value.into_segment() else {
3062 return Err(KclError::new_argument(KclErrorDetails::new(
3063 "Expected second segment to be a Segment".to_owned(),
3064 vec![args.source_range],
3065 )));
3066 };
3067 let intersection_index = intersection_index.map(|n| n.n as i32).unwrap_or(-1);
3068 let direction = direction.unwrap_or(CircularDirection::Counterclockwise);
3069
3070 let Some(sketch) = &seg0.sketch else {
3071 return Err(KclError::new_semantic(KclErrorDetails::new(
3072 "Expected first segment to have an associated sketch. The sketch must be solved to create a region from it.".to_owned(),
3073 vec![args.source_range],
3074 )));
3075 };
3076
3077 let meta = ModelingCmdMeta::from_args_id(exec_state, &args, region_id);
3078 let response = exec_state
3079 .send_modeling_cmd(
3080 meta,
3081 ModelingCmd::from(
3082 mcmd::CreateRegion::builder()
3083 .object_id(sketch.id)
3084 .segment(seg0.id)
3085 .intersection_segment(seg1.id)
3086 .intersection_index(intersection_index)
3087 .curve_clockwise(direction.is_clockwise())
3088 .version(region_version)
3089 .build(),
3090 ),
3091 )
3092 .await
3093 .map_err(|error| with_region_help(error, REGION_FROM_SEGMENTS_ENGINE_HELP))?;
3094
3095 let region_mapping = if let kcmc::websocket::OkWebSocketResponseData::Modeling {
3096 modeling_response: kcmc::ok_response::OkModelingCmdResponse::CreateRegion(data),
3097 } = response
3098 {
3099 data.region_mapping
3100 } else {
3101 Default::default()
3102 };
3103
3104 (SketchOrSegment::Segment(seg0), region_mapping)
3105 }
3106 (Some(_), Some(_)) => {
3107 return Err(KclError::new_semantic(KclErrorDetails::new(
3108 "Cannot provide both point and segments parameters. Choose one.".to_owned(),
3109 vec![args.source_range],
3110 )));
3111 }
3112 (None, None) => {
3113 return Err(KclError::new_semantic(KclErrorDetails::new(
3114 "Either point or segments parameter must be provided".to_owned(),
3115 vec![args.source_range],
3116 )));
3117 }
3118 };
3119
3120 let units = exec_state.length_unit();
3121 let to = [0.0, 0.0];
3122 let first_path = Path::ToPoint {
3123 base: BasePath {
3124 from: to,
3125 to,
3126 units,
3127 tag: None,
3128 geo_meta: GeoMeta {
3129 id: match &sketch_or_segment {
3130 SketchOrSegment::Sketch(sketch) => sketch.id,
3131 SketchOrSegment::Segment(segment) => segment.id,
3132 },
3133 metadata: args.source_range.into(),
3134 },
3135 },
3136 };
3137 let start_base_path = BasePath {
3138 from: to,
3139 to,
3140 tag: None,
3141 units,
3142 geo_meta: GeoMeta {
3143 id: region_id,
3144 metadata: args.source_range.into(),
3145 },
3146 };
3147 let mut sketch = match sketch_or_segment {
3148 SketchOrSegment::Sketch(sketch) => sketch,
3149 SketchOrSegment::Segment(segment) => {
3150 if let Some(sketch) = segment.sketch {
3151 sketch.as_ref().clone()
3152 } else {
3153 Sketch {
3154 id: region_id,
3155 original_id: region_id,
3156 artifact_id: region_id.into(),
3157 origin_sketch_id: None,
3158 on: segment.surface,
3159 paths: imbl::vector![first_path],
3160 inner_paths: vec![],
3161 units,
3162 mirror: Default::default(),
3163 clone: Default::default(),
3164 synthetic_jump_path_ids: vec![],
3165 meta: vec![args.source_range.into()],
3166 tags: Default::default(),
3167 start: start_base_path,
3168 is_closed: ProfileClosed::Explicitly,
3169 }
3170 }
3171 }
3172 };
3173 sketch.origin_sketch_id = Some(sketch.id);
3174 sketch.id = region_id;
3175 sketch.original_id = region_id;
3176 sketch.artifact_id = region_id.into();
3177
3178 let mut region_mapping = region_mapping;
3179 if args.ctx.no_engine_commands().await && region_mapping.is_empty() {
3180 let mut mock_mapping = HashMap::new();
3181 for path in &sketch.paths {
3182 mock_mapping.insert(exec_state.next_uuid(), path.get_id());
3183 }
3184 region_mapping = mock_mapping;
3185 }
3186 let original_segment_ids = sketch.paths.iter().map(|p| p.get_id()).collect::<Vec<_>>();
3187 let original_seg_to_region = build_reverse_region_mapping(®ion_mapping, &original_segment_ids);
3188
3189 {
3190 let mut new_paths = imbl::Vector::new();
3191 for path in &sketch.paths {
3192 let original_id = path.get_id();
3193 if let Some(region_ids) = original_seg_to_region.get(&original_id) {
3194 for region_id in region_ids {
3195 let mut new_path = path.clone();
3196 new_path.set_id(*region_id);
3197 new_paths.push_back(new_path);
3198 }
3199 }
3200 }
3201
3202 sketch.paths = new_paths;
3203
3204 for (_tag_name, tag) in &mut sketch.tags {
3205 let Some(info) = tag.get_cur_info().cloned() else {
3206 continue;
3207 };
3208 let original_id = info.id;
3209 if let Some(region_ids) = original_seg_to_region.get(&original_id) {
3210 let epoch = tag.info.last().map(|(e, _)| *e).unwrap_or(0);
3211 for (i, region_id) in region_ids.iter().enumerate() {
3212 if i == 0 {
3213 if let Some((_, existing)) = tag.info.last_mut() {
3214 existing.id = *region_id;
3215 }
3216 } else {
3217 let mut new_info = info.clone();
3218 new_info.id = *region_id;
3219 tag.info.push((epoch, new_info));
3220 }
3221 }
3222 }
3223 }
3224 }
3225
3226 if sketch.mirror.is_some() {
3230 sketch.mirror = sketch.paths.front().map(|p| p.get_id());
3231 }
3232
3233 sketch.meta.push(args.source_range.into());
3234 sketch.is_closed = ProfileClosed::Explicitly;
3235
3236 Ok(KclValue::Sketch {
3237 value: Box::new(sketch),
3238 })
3239}
3240
3241pub(crate) fn build_reverse_region_mapping(
3251 region_mapping: &HashMap<Uuid, Uuid>,
3252 original_segments: &[Uuid],
3253) -> IndexMap<Uuid, Vec<Uuid>> {
3254 let mut reverse: HashMap<Uuid, Vec<Uuid>> = HashMap::default();
3255 #[expect(
3256 clippy::iter_over_hash_type,
3257 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."
3258 )]
3259 for (region_id, original_id) in region_mapping {
3260 reverse.entry(*original_id).or_default().push(*region_id);
3261 }
3262 #[expect(
3263 clippy::iter_over_hash_type,
3264 reason = "This is safe since we're just sorting values."
3265 )]
3266 for values in reverse.values_mut() {
3267 values.sort_unstable();
3268 }
3269 let mut ordered = IndexMap::with_capacity(original_segments.len());
3270 for original_id in original_segments {
3271 let mut region_ids = Vec::new();
3272 reverse.entry(*original_id).and_modify(|entry_value| {
3273 region_ids = std::mem::take(entry_value);
3274 });
3275 if !region_ids.is_empty() {
3276 ordered.insert(*original_id, region_ids);
3277 }
3278 }
3279 ordered
3280}
3281
3282fn region_from_point(
3283 point: KclValue,
3284 sketch: Option<KclValue>,
3285 args: &Args,
3286) -> Result<(SketchOrSegment, [TyF64; 2]), KclError> {
3287 match point {
3288 KclValue::HomArray { .. } | KclValue::Tuple { .. } => {
3289 let Some(pt) = <[TyF64; 2]>::from_kcl_val(&point) else {
3290 return Err(KclError::new_semantic(KclErrorDetails::new(
3291 "Expected 2D point for point parameter".to_owned(),
3292 vec![args.source_range],
3293 )));
3294 };
3295
3296 let Some(sketch_value) = sketch else {
3297 return Err(KclError::new_semantic(KclErrorDetails::new(
3298 "Sketch must be provided when point is a 2D point".to_owned(),
3299 vec![args.source_range],
3300 )));
3301 };
3302 let sketch = match sketch_value {
3303 KclValue::Sketch { value } => *value,
3304 KclValue::Object { value, .. } => {
3305 let Some(meta_value) = value.get(SKETCH_OBJECT_META) else {
3306 return Err(KclError::new_semantic(KclErrorDetails::new(
3307 "Expected sketch to be of type Sketch with a meta field. Sketch must not be empty to create a region.".to_owned(),
3308 vec![args.source_range],
3309 )));
3310 };
3311 let meta_map = match meta_value {
3312 KclValue::Object { value, .. } => value,
3313 _ => {
3314 return Err(KclError::new_semantic(KclErrorDetails::new(
3315 "Expected sketch to be of type Sketch with a meta field that's an object".to_owned(),
3316 vec![args.source_range],
3317 )));
3318 }
3319 };
3320 let Some(sketch_value) = meta_map.get(SKETCH_OBJECT_META_SKETCH) else {
3321 return Err(KclError::new_semantic(KclErrorDetails::new(
3322 "Expected sketch meta to have a sketch field. Sketch must not be empty to create a region."
3323 .to_owned(),
3324 vec![args.source_range],
3325 )));
3326 };
3327 let Some(sketch) = sketch_value.as_sketch() else {
3328 return Err(KclError::new_semantic(KclErrorDetails::new(
3329 "Expected sketch meta to have a sketch field of type Sketch. Sketch must not be empty to create a region.".to_owned(),
3330 vec![args.source_range],
3331 )));
3332 };
3333 sketch.clone()
3334 }
3335 _ => {
3336 return Err(KclError::new_semantic(KclErrorDetails::new(
3337 "Expected sketch to be of type Sketch".to_owned(),
3338 vec![args.source_range],
3339 )));
3340 }
3341 };
3342
3343 Ok((SketchOrSegment::Sketch(sketch), pt))
3344 }
3345 KclValue::Segment { value } => match value.repr {
3346 crate::execution::SegmentRepr::Unsolved { .. } => Err(KclError::new_semantic(KclErrorDetails::new(
3347 "Segment provided to point parameter is unsolved; segments must be solved to be used as points"
3348 .to_owned(),
3349 vec![args.source_range],
3350 ))),
3351 crate::execution::SegmentRepr::Solved { segment } => {
3352 let pt = match &segment.kind {
3353 SegmentKind::Point { position, .. } => position.clone(),
3354 _ => {
3355 return Err(KclError::new_semantic(KclErrorDetails::new(
3356 "Expected segment to be a point segment".to_owned(),
3357 vec![args.source_range],
3358 )));
3359 }
3360 };
3361
3362 Ok((SketchOrSegment::Segment(*segment), pt))
3363 }
3364 },
3365 _ => Err(KclError::new_semantic(KclErrorDetails::new(
3366 "Expected point to be either a 2D point like `[0, 0]` or a point segment created from `point()`".to_owned(),
3367 vec![args.source_range],
3368 ))),
3369 }
3370}
3371#[cfg(test)]
3372mod tests {
3373
3374 use pretty_assertions::assert_eq;
3375
3376 use crate::errors::KclError;
3377 use crate::errors::KclErrorDetails;
3378 use crate::execution::TagIdentifier;
3379 use crate::std::sketch::PlaneData;
3380 use crate::std::sketch::REGION_DOCS_URL;
3381 use crate::std::sketch::REGION_FROM_POINT_ENGINE_HELP;
3382 use crate::std::sketch::REGION_FROM_SEGMENTS_ENGINE_HELP;
3383 use crate::std::sketch::with_region_help;
3384 use crate::std::utils::calculate_circle_center;
3385
3386 fn engine_error_details(message: &str) -> KclErrorDetails {
3387 KclErrorDetails::new(message.to_owned(), Vec::new())
3388 }
3389
3390 #[test]
3391 fn region_help_is_appended_to_an_engine_rejection() {
3392 let engine_message = "Unable to create a region that contains the requested query point";
3393 let error = with_region_help(
3394 KclError::Engine {
3395 details: engine_error_details(engine_message),
3396 },
3397 REGION_FROM_POINT_ENGINE_HELP,
3398 );
3399
3400 assert_eq!(
3401 error.message(),
3402 format!("{engine_message}. {REGION_FROM_POINT_ENGINE_HELP} See {REGION_DOCS_URL}")
3403 );
3404 }
3405
3406 #[test]
3407 fn region_help_does_not_double_up_sentence_punctuation() {
3408 let engine_message = "Unable create a region from the given segments.";
3409 let error = with_region_help(
3410 KclError::Engine {
3411 details: engine_error_details(engine_message),
3412 },
3413 REGION_FROM_SEGMENTS_ENGINE_HELP,
3414 );
3415
3416 assert_eq!(
3417 error.message(),
3418 format!("{engine_message} {REGION_FROM_SEGMENTS_ENGINE_HELP} See {REGION_DOCS_URL}")
3419 );
3420 }
3421
3422 #[test]
3423 fn region_help_is_not_appended_to_transport_or_internal_failures() {
3424 let hangup = KclError::EngineHangup {
3425 details: engine_error_details("connection closed"),
3426 api_call_id: None,
3427 };
3428 let internal = KclError::EngineInternal {
3429 details: engine_error_details("internal error"),
3430 };
3431 let semantic = KclError::new_semantic(engine_error_details("Sketch parameter must not be provided"));
3432
3433 for error in [hangup, internal, semantic] {
3434 let message = error.message().to_owned();
3435 let enriched = with_region_help(error, REGION_FROM_POINT_ENGINE_HELP);
3436 assert_eq!(enriched.message(), message);
3437 }
3438 }
3439
3440 #[test]
3441 fn test_deserialize_plane_data() {
3442 let data = PlaneData::XY;
3443 let mut str_json = serde_json::to_string(&data).unwrap();
3444 assert_eq!(str_json, "\"XY\"");
3445
3446 str_json = "\"YZ\"".to_string();
3447 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3448 assert_eq!(data, PlaneData::YZ);
3449
3450 str_json = "\"-YZ\"".to_string();
3451 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3452 assert_eq!(data, PlaneData::NegYZ);
3453
3454 str_json = "\"-xz\"".to_string();
3455 let data: PlaneData = serde_json::from_str(&str_json).unwrap();
3456 assert_eq!(data, PlaneData::NegXZ);
3457 }
3458
3459 #[test]
3460 fn test_deserialize_sketch_on_face_tag() {
3461 let data = "start";
3462 let mut str_json = serde_json::to_string(&data).unwrap();
3463 assert_eq!(str_json, "\"start\"");
3464
3465 str_json = "\"end\"".to_string();
3466 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3467 assert_eq!(
3468 data,
3469 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
3470 );
3471
3472 str_json = serde_json::to_string(&TagIdentifier {
3473 value: "thing".to_string(),
3474 info: Vec::new(),
3475 meta: Default::default(),
3476 })
3477 .unwrap();
3478 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3479 assert_eq!(
3480 data,
3481 crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
3482 value: "thing".to_string(),
3483 info: Vec::new(),
3484 meta: Default::default()
3485 }))
3486 );
3487
3488 str_json = "\"END\"".to_string();
3489 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3490 assert_eq!(
3491 data,
3492 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
3493 );
3494
3495 str_json = "\"start\"".to_string();
3496 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3497 assert_eq!(
3498 data,
3499 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
3500 );
3501
3502 str_json = "\"START\"".to_string();
3503 let data: crate::std::sketch::FaceTag = serde_json::from_str(&str_json).unwrap();
3504 assert_eq!(
3505 data,
3506 crate::std::sketch::FaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
3507 );
3508 }
3509
3510 #[test]
3511 fn test_circle_center() {
3512 let actual = calculate_circle_center([0.0, 0.0], [5.0, 5.0], [10.0, 0.0]);
3513 assert_eq!(actual[0], 5.0);
3514 assert_eq!(actual[1], 0.0);
3515 }
3516}