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