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