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