1use std::cmp::Ordering;
4
5use anyhow::Result;
6use kcmc::ModelingCmd;
7use kcmc::each_cmd as mcmd;
8use kcmc::length_unit::LengthUnit;
9use kcmc::ok_response::OkModelingCmdResponse;
10use kcmc::shared::Transform;
11use kcmc::websocket::OkWebSocketResponseData;
12use kittycad_modeling_cmds::shared::Angle;
13use kittycad_modeling_cmds::shared::OriginType;
14use kittycad_modeling_cmds::shared::Rotation;
15use kittycad_modeling_cmds::{self as kcmc};
16use serde::Serialize;
17use uuid::Uuid;
18
19use super::axis_or_reference::Axis3dOrPoint3d;
20use crate::ExecutorContext;
21use crate::NodePath;
22use crate::SourceRange;
23use crate::errors::KclError;
24use crate::errors::KclErrorDetails;
25use crate::execution::ControlFlowKind;
26use crate::execution::ExecState;
27use crate::execution::Geometries;
28use crate::execution::Geometry;
29use crate::execution::KclObjectFields;
30use crate::execution::KclValue;
31use crate::execution::ModelingCmdMeta;
32use crate::execution::Sketch;
33use crate::execution::Solid;
34use crate::execution::fn_call::Arg;
35use crate::execution::fn_call::Args;
36use crate::execution::kcl_value::FunctionSource;
37use crate::execution::types::NumericType;
38use crate::execution::types::PrimitiveType;
39use crate::execution::types::RuntimeType;
40use crate::std::args::TyF64;
41use crate::std::axis_or_reference::Axis2dOrPoint2d;
42use crate::std::shapes::POINT_ZERO_ZERO;
43use crate::std::utils::point_3d_to_mm;
44use crate::std::utils::point_to_mm;
45pub const POINT_ZERO_ZERO_ZERO: [TyF64; 3] = [
46 TyF64::new(0.0, crate::exec::NumericType::mm()),
47 TyF64::new(0.0, crate::exec::NumericType::mm()),
48 TyF64::new(0.0, crate::exec::NumericType::mm()),
49];
50
51const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
52
53pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
55 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
56 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
57 let transform: FunctionSource = args.get_kw_arg("transform", &RuntimeType::function(), exec_state)?;
58 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
59
60 let solids = inner_pattern_transform(solids, instances, transform, use_original, exec_state, &args).await?;
61 Ok(solids.into())
62}
63
64pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
66 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
67 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
68 let transform: FunctionSource = args.get_kw_arg("transform", &RuntimeType::function(), exec_state)?;
69 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
70
71 let sketches = inner_pattern_transform_2d(sketches, instances, transform, use_original, exec_state, &args).await?;
72 Ok(sketches.into())
73}
74
75async fn inner_pattern_transform(
76 solids: Vec<Solid>,
77 instances: u32,
78 transform: FunctionSource,
79 use_original: Option<bool>,
80 exec_state: &mut ExecState,
81 args: &Args,
82) -> Result<Vec<Solid>, KclError> {
83 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
85 if instances < 1 {
86 return Err(KclError::new_semantic(KclErrorDetails::new(
87 MUST_HAVE_ONE_INSTANCE.to_owned(),
88 vec![args.source_range],
89 )));
90 }
91 for i in 1..instances {
92 let t = make_transform::<Solid>(
93 i,
94 &transform,
95 args.source_range,
96 args.node_path.clone(),
97 exec_state,
98 &args.ctx,
99 )
100 .await?;
101 transform_vec.push(t);
102 }
103 execute_pattern_transform(
104 transform_vec,
105 solids,
106 use_original.unwrap_or_default(),
107 exec_state,
108 args,
109 )
110 .await
111}
112
113async fn inner_pattern_transform_2d(
114 sketches: Vec<Sketch>,
115 instances: u32,
116 transform: FunctionSource,
117 use_original: Option<bool>,
118 exec_state: &mut ExecState,
119 args: &Args,
120) -> Result<Vec<Sketch>, KclError> {
121 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
123 if instances < 1 {
124 return Err(KclError::new_semantic(KclErrorDetails::new(
125 MUST_HAVE_ONE_INSTANCE.to_owned(),
126 vec![args.source_range],
127 )));
128 }
129 for i in 1..instances {
130 let t = make_transform::<Sketch>(
131 i,
132 &transform,
133 args.source_range,
134 args.node_path.clone(),
135 exec_state,
136 &args.ctx,
137 )
138 .await?;
139 transform_vec.push(t);
140 }
141 execute_pattern_transform(
142 transform_vec,
143 sketches,
144 use_original.unwrap_or_default(),
145 exec_state,
146 args,
147 )
148 .await
149}
150
151async fn execute_pattern_transform<T: GeometryTrait>(
152 transforms: Vec<Vec<Transform>>,
153 geo_set: T::Set,
154 use_original: bool,
155 exec_state: &mut ExecState,
156 args: &Args,
157) -> Result<Vec<T>, KclError> {
158 T::flush_batch(args, exec_state, &geo_set).await?;
162 let starting: Vec<T> = geo_set.into();
163
164 if args.ctx.context_type == crate::execution::ContextType::Mock {
165 return Ok(starting);
166 }
167
168 let mut output = Vec::new();
169 for geo in starting {
170 let new = send_pattern_transform(transforms.clone(), &geo, use_original, exec_state, args).await?;
171 output.extend(new)
172 }
173 Ok(output)
174}
175
176async fn send_pattern_transform<T: GeometryTrait>(
177 transforms: Vec<Vec<Transform>>,
180 solid: &T,
181 use_original: bool,
182 exec_state: &mut ExecState,
183 args: &Args,
184) -> Result<Vec<T>, KclError> {
185 let extra_instances = transforms.len();
186
187 let resp = exec_state
188 .send_modeling_cmd(
189 ModelingCmdMeta::from_args(exec_state, args),
190 ModelingCmd::from(
191 mcmd::EntityLinearPatternTransform::builder()
192 .entity_id(if use_original { solid.original_id() } else { solid.id() })
193 .transform(Default::default())
194 .transforms(transforms)
195 .build(),
196 ),
197 )
198 .await?;
199
200 let mut mock_ids = Vec::new();
201 let entity_ids = if let OkWebSocketResponseData::Modeling {
202 modeling_response: OkModelingCmdResponse::EntityLinearPatternTransform(pattern_info),
203 } = &resp
204 {
205 &pattern_info.entity_face_edge_ids.iter().map(|x| x.object_id).collect()
206 } else if args.ctx.no_engine_commands().await {
207 mock_ids.reserve(extra_instances);
208 for _ in 0..extra_instances {
209 mock_ids.push(exec_state.next_uuid());
210 }
211 &mock_ids
212 } else {
213 return Err(KclError::new_engine(KclErrorDetails::new(
214 format!("EntityLinearPattern response was not as expected: {resp:?}"),
215 vec![args.source_range],
216 )));
217 };
218
219 let mut geometries = vec![solid.clone()];
220 for id in entity_ids.iter().copied() {
221 let mut new_solid = solid.clone();
222 new_solid.set_id(id);
223 geometries.push(new_solid);
224 }
225 Ok(geometries)
226}
227
228async fn make_transform<T: GeometryTrait>(
229 i: u32,
230 transform: &FunctionSource,
231 source_range: SourceRange,
232 node_path: Option<NodePath>,
233 exec_state: &mut ExecState,
234 ctxt: &ExecutorContext,
235) -> Result<Vec<Transform>, KclError> {
236 let repetition_num = KclValue::Number {
238 value: i.into(),
239 ty: NumericType::count(),
240 meta: vec![source_range.into()],
241 };
242 let transform_fn_args = Args::new(
243 Default::default(),
244 vec![(None, Arg::new(repetition_num, source_range))],
245 source_range,
246 node_path,
247 exec_state,
248 ctxt.clone(),
249 Some("transform closure".to_owned()),
250 );
251 let transform_fn_return = transform
252 .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
253 .await?;
254
255 let source_ranges = vec![source_range];
257 let transform_fn_return = transform_fn_return.ok_or_else(|| {
258 KclError::new_semantic(KclErrorDetails::new(
259 "Transform function must return a value".to_string(),
260 source_ranges.clone(),
261 ))
262 })?;
263
264 let transform_fn_return = match transform_fn_return.control {
265 ControlFlowKind::Continue => transform_fn_return.into_value(),
266 ControlFlowKind::Exit => {
267 let message = "Early return inside pattern transform function is currently not supported".to_owned();
268 debug_assert!(false, "{}", &message);
269 return Err(KclError::new_internal(KclErrorDetails::new(
270 message,
271 vec![source_range],
272 )));
273 }
274 };
275
276 let transforms = match transform_fn_return {
277 KclValue::Object { value, .. } => vec![value],
278 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
279 let transforms: Vec<_> = value
280 .into_iter()
281 .map(|val| {
282 val.into_object().ok_or(KclError::new_semantic(KclErrorDetails::new(
283 "Transform function must return a transform object".to_string(),
284 source_ranges.clone(),
285 )))
286 })
287 .collect::<Result<_, _>>()?;
288 transforms
289 }
290 _ => {
291 return Err(KclError::new_semantic(KclErrorDetails::new(
292 "Transform function must return a transform object".to_string(),
293 source_ranges,
294 )));
295 }
296 };
297
298 transforms
299 .into_iter()
300 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
301 .collect()
302}
303
304fn transform_from_obj_fields<T: GeometryTrait>(
305 transform: KclObjectFields,
306 source_ranges: Vec<SourceRange>,
307 exec_state: &mut ExecState,
308) -> Result<Transform, KclError> {
309 let replicate = match transform.get("replicate") {
311 Some(KclValue::Bool { value: true, .. }) => true,
312 Some(KclValue::Bool { value: false, .. }) => false,
313 Some(_) => {
314 return Err(KclError::new_semantic(KclErrorDetails::new(
315 "The 'replicate' key must be a bool".to_string(),
316 source_ranges,
317 )));
318 }
319 None => true,
320 };
321
322 let scale = match transform.get("scale") {
323 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
324 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
325 };
326
327 for (dim, name) in [(scale.x, "x"), (scale.y, "y"), (scale.z, "z")] {
328 if dim == 0.0 {
329 return Err(KclError::new_semantic(KclErrorDetails::new(
330 format!("cannot set {name} = 0, scale factor must be nonzero"),
331 source_ranges,
332 )));
333 }
334 }
335 let translate = match transform.get("translate") {
336 Some(x) => {
337 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
338 kcmc::shared::Point3d::<LengthUnit> {
339 x: LengthUnit(arr[0]),
340 y: LengthUnit(arr[1]),
341 z: LengthUnit(arr[2]),
342 }
343 }
344 None => kcmc::shared::Point3d::<LengthUnit> {
345 x: LengthUnit(0.0),
346 y: LengthUnit(0.0),
347 z: LengthUnit(0.0),
348 },
349 };
350
351 let mut rotation = Rotation::default();
352 if let Some(rot) = transform.get("rotation") {
353 let KclValue::Object { value: rot, .. } = rot else {
354 return Err(KclError::new_semantic(KclErrorDetails::new(
355 "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')".to_owned(),
356 source_ranges,
357 )));
358 };
359 if let Some(axis) = rot.get("axis") {
360 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
361 }
362 if let Some(angle) = rot.get("angle") {
363 match angle {
364 KclValue::Number { value: number, .. } => {
365 rotation.angle = Angle::from_degrees(*number);
366 }
367 _ => {
368 return Err(KclError::new_semantic(KclErrorDetails::new(
369 "The 'rotation.angle' key must be a number (of degrees)".to_owned(),
370 source_ranges,
371 )));
372 }
373 }
374 }
375 if let Some(origin) = rot.get("origin") {
376 rotation.origin = match origin {
377 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
378 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
379 other => {
380 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges, exec_state)?).into();
381 OriginType::Custom { origin }
382 }
383 };
384 }
385 }
386
387 let transform = Transform::builder()
388 .replicate(replicate)
389 .scale(scale)
390 .translate(translate)
391 .rotation(rotation)
392 .build();
393 Ok(transform)
394}
395
396fn array_to_point3d(
397 val: &KclValue,
398 source_ranges: Vec<SourceRange>,
399 exec_state: &mut ExecState,
400) -> Result<[TyF64; 3], KclError> {
401 val.coerce(&RuntimeType::point3d(), true, exec_state)
402 .map_err(|e| {
403 KclError::new_semantic(KclErrorDetails::new(
404 format!(
405 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
406 e.found
407 .map(|t| t.human_friendly_type())
408 .unwrap_or_else(|| val.human_friendly_type())
409 ),
410 source_ranges,
411 ))
412 })
413 .map(|val| val.as_point3d().unwrap())
414}
415
416fn array_to_point2d(
417 val: &KclValue,
418 source_ranges: Vec<SourceRange>,
419 exec_state: &mut ExecState,
420) -> Result<[TyF64; 2], KclError> {
421 val.coerce(&RuntimeType::point2d(), true, exec_state)
422 .map_err(|e| {
423 KclError::new_semantic(KclErrorDetails::new(
424 format!(
425 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
426 e.found
427 .map(|t| t.human_friendly_type())
428 .unwrap_or_else(|| val.human_friendly_type())
429 ),
430 source_ranges,
431 ))
432 })
433 .map(|val| val.as_point2d().unwrap())
434}
435
436pub trait GeometryTrait: Clone {
437 type Set: Into<Vec<Self>> + Clone;
438 fn id(&self) -> Uuid;
439 fn original_id(&self) -> Uuid;
440 fn set_id(&mut self, id: Uuid);
441 fn array_to_point3d(
442 val: &KclValue,
443 source_ranges: Vec<SourceRange>,
444 exec_state: &mut ExecState,
445 ) -> Result<[TyF64; 3], KclError>;
446 #[allow(async_fn_in_trait)]
447 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
448}
449
450impl GeometryTrait for Sketch {
451 type Set = Vec<Sketch>;
452 fn set_id(&mut self, id: Uuid) {
453 self.id = id;
454 }
455 fn id(&self) -> Uuid {
456 self.id
457 }
458 fn original_id(&self) -> Uuid {
459 self.original_id
460 }
461 fn array_to_point3d(
462 val: &KclValue,
463 source_ranges: Vec<SourceRange>,
464 exec_state: &mut ExecState,
465 ) -> Result<[TyF64; 3], KclError> {
466 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
467 let ty = x.ty;
468 Ok([x, y, TyF64::new(0.0, ty)])
469 }
470
471 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
472 Ok(())
473 }
474}
475
476impl GeometryTrait for Solid {
477 type Set = Vec<Solid>;
478 fn set_id(&mut self, id: Uuid) {
479 self.id = id;
480 if let Some(sketch) = self.sketch_mut() {
482 sketch.id = id;
483 }
484 }
485
486 fn id(&self) -> Uuid {
487 self.id
488 }
489
490 fn original_id(&self) -> Uuid {
491 Solid::original_id(self)
492 }
493
494 fn array_to_point3d(
495 val: &KclValue,
496 source_ranges: Vec<SourceRange>,
497 exec_state: &mut ExecState,
498 ) -> Result<[TyF64; 3], KclError> {
499 array_to_point3d(val, source_ranges, exec_state)
500 }
501
502 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
503 exec_state
504 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, args), solid_set)
505 .await
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use crate::execution::types::NumericType;
513 use crate::execution::types::PrimitiveType;
514
515 #[tokio::test(flavor = "multi_thread")]
516 async fn test_array_to_point3d() {
517 let ctx = ExecutorContext::new_mock(None).await;
518 let mut exec_state = ExecState::new(&ctx);
519 let input = KclValue::HomArray {
520 value: vec![
521 KclValue::Number {
522 value: 1.1,
523 meta: Default::default(),
524 ty: NumericType::mm(),
525 },
526 KclValue::Number {
527 value: 2.2,
528 meta: Default::default(),
529 ty: NumericType::mm(),
530 },
531 KclValue::Number {
532 value: 3.3,
533 meta: Default::default(),
534 ty: NumericType::mm(),
535 },
536 ],
537 ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
538 };
539 let expected = [
540 TyF64::new(1.1, NumericType::mm()),
541 TyF64::new(2.2, NumericType::mm()),
542 TyF64::new(3.3, NumericType::mm()),
543 ];
544 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
545 assert_eq!(actual.unwrap(), expected);
546 ctx.close().await;
547 }
548
549 #[tokio::test(flavor = "multi_thread")]
550 async fn test_tuple_to_point3d() {
551 let ctx = ExecutorContext::new_mock(None).await;
552 let mut exec_state = ExecState::new(&ctx);
553 let input = KclValue::Tuple {
554 value: vec![
555 KclValue::Number {
556 value: 1.1,
557 meta: Default::default(),
558 ty: NumericType::mm(),
559 },
560 KclValue::Number {
561 value: 2.2,
562 meta: Default::default(),
563 ty: NumericType::mm(),
564 },
565 KclValue::Number {
566 value: 3.3,
567 meta: Default::default(),
568 ty: NumericType::mm(),
569 },
570 ],
571 meta: Default::default(),
572 };
573 let expected = [
574 TyF64::new(1.1, NumericType::mm()),
575 TyF64::new(2.2, NumericType::mm()),
576 TyF64::new(3.3, NumericType::mm()),
577 ];
578 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
579 assert_eq!(actual.unwrap(), expected);
580 ctx.close().await;
581 }
582}
583
584pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
586 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
587 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
588 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
589 let axis: Axis2dOrPoint2d = args.get_kw_arg(
590 "axis",
591 &RuntimeType::Union(vec![
592 RuntimeType::Primitive(PrimitiveType::Axis2d),
593 RuntimeType::point2d(),
594 ]),
595 exec_state,
596 )?;
597 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
598
599 let axis = axis.to_point2d();
600 if axis[0].n == 0.0 && axis[1].n == 0.0 {
601 return Err(KclError::new_semantic(KclErrorDetails::new(
602 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
603 .to_owned(),
604 vec![args.source_range],
605 )));
606 }
607
608 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
609 Ok(sketches.into())
610}
611
612async fn inner_pattern_linear_2d(
613 sketches: Vec<Sketch>,
614 instances: u32,
615 distance: TyF64,
616 axis: [TyF64; 2],
617 use_original: Option<bool>,
618 exec_state: &mut ExecState,
619 args: Args,
620) -> Result<Vec<Sketch>, KclError> {
621 let [x, y] = point_to_mm(axis);
622 let axis_len = f64::sqrt(x * x + y * y);
623 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
624 let transforms: Vec<_> = (1..instances)
625 .map(|i| {
626 let d = distance.to_mm() * (i as f64);
627 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
628 vec![Transform::builder().translate(translate).build()]
629 })
630 .collect();
631 execute_pattern_transform(
632 transforms,
633 sketches,
634 use_original.unwrap_or_default(),
635 exec_state,
636 &args,
637 )
638 .await
639}
640
641pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
643 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
644 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
645 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
646 let axis: Axis3dOrPoint3d = args.get_kw_arg(
647 "axis",
648 &RuntimeType::Union(vec![
649 RuntimeType::Primitive(PrimitiveType::Axis3d),
650 RuntimeType::point3d(),
651 ]),
652 exec_state,
653 )?;
654 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
655
656 let axis = axis.to_point3d();
657 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
658 return Err(KclError::new_semantic(KclErrorDetails::new(
659 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
660 .to_owned(),
661 vec![args.source_range],
662 )));
663 }
664
665 let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
666 Ok(solids.into())
667}
668
669async fn inner_pattern_linear_3d(
670 solids: Vec<Solid>,
671 instances: u32,
672 distance: TyF64,
673 axis: [TyF64; 3],
674 use_original: Option<bool>,
675 exec_state: &mut ExecState,
676 args: Args,
677) -> Result<Vec<Solid>, KclError> {
678 let [x, y, z] = point_3d_to_mm(axis);
679 let axis_len = f64::sqrt(x * x + y * y + z * z);
680 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
681 let transforms: Vec<_> = (1..instances)
682 .map(|i| {
683 let d = distance.to_mm() * (i as f64);
684 let translate = (normalized_axis * d).map(LengthUnit);
685 vec![Transform::builder().translate(translate).build()]
686 })
687 .collect();
688 execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
689}
690
691#[derive(Debug, Clone, Serialize, PartialEq)]
693#[serde(rename_all = "camelCase")]
694struct CircularPattern2dData {
695 pub instances: u32,
700 pub center: [TyF64; 2],
702 pub arc_degrees: Option<f64>,
704 pub rotate_duplicates: Option<bool>,
706 #[serde(default)]
709 pub use_original: Option<bool>,
710}
711
712#[derive(Debug, Clone, Serialize, PartialEq)]
714#[serde(rename_all = "camelCase")]
715struct CircularPattern3dData {
716 pub instances: u32,
721 pub axis: [f64; 3],
724 pub center: [TyF64; 3],
726 pub arc_degrees: Option<f64>,
728 pub rotate_duplicates: Option<bool>,
730 #[serde(default)]
733 pub use_original: Option<bool>,
734}
735
736#[allow(clippy::large_enum_variant)]
737enum CircularPattern {
738 ThreeD(CircularPattern3dData),
739 TwoD(CircularPattern2dData),
740}
741
742enum RepetitionsNeeded {
743 More(u32),
745 None,
747 Invalid,
749}
750
751impl From<u32> for RepetitionsNeeded {
752 fn from(n: u32) -> Self {
753 match n.cmp(&1) {
754 Ordering::Less => Self::Invalid,
755 Ordering::Equal => Self::None,
756 Ordering::Greater => Self::More(n - 1),
757 }
758 }
759}
760
761impl CircularPattern {
762 pub fn axis(&self) -> [f64; 3] {
763 match self {
764 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
765 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
766 }
767 }
768
769 pub fn center_mm(&self) -> [f64; 3] {
770 match self {
771 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
772 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
773 }
774 }
775
776 fn repetitions(&self) -> RepetitionsNeeded {
777 let n = match self {
778 CircularPattern::TwoD(lp) => lp.instances,
779 CircularPattern::ThreeD(lp) => lp.instances,
780 };
781 RepetitionsNeeded::from(n)
782 }
783
784 pub fn arc_degrees(&self) -> Option<f64> {
785 match self {
786 CircularPattern::TwoD(lp) => lp.arc_degrees,
787 CircularPattern::ThreeD(lp) => lp.arc_degrees,
788 }
789 }
790
791 pub fn rotate_duplicates(&self) -> Option<bool> {
792 match self {
793 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
794 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
795 }
796 }
797
798 pub fn use_original(&self) -> bool {
799 match self {
800 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
801 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
802 }
803 }
804}
805
806pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
808 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
809 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
810 let center: Option<[TyF64; 2]> = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
811 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
812 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
813 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
814
815 let sketches = inner_pattern_circular_2d(
816 sketches,
817 instances,
818 center,
819 arc_degrees.map(|x| x.n),
820 rotate_duplicates,
821 use_original,
822 exec_state,
823 args,
824 )
825 .await?;
826 Ok(sketches.into())
827}
828
829#[allow(clippy::too_many_arguments)]
830async fn inner_pattern_circular_2d(
831 sketch_set: Vec<Sketch>,
832 instances: u32,
833 center: Option<[TyF64; 2]>,
834 arc_degrees: Option<f64>,
835 rotate_duplicates: Option<bool>,
836 use_original: Option<bool>,
837 exec_state: &mut ExecState,
838 args: Args,
839) -> Result<Vec<Sketch>, KclError> {
840 let starting_sketches = sketch_set;
841
842 if args.ctx.context_type == crate::execution::ContextType::Mock {
843 return Ok(starting_sketches);
844 }
845 let center = center.unwrap_or(POINT_ZERO_ZERO);
846 let data = CircularPattern2dData {
847 instances,
848 center,
849 arc_degrees,
850 rotate_duplicates,
851 use_original,
852 };
853
854 let mut sketches = Vec::new();
855 for sketch in starting_sketches.iter() {
856 let geometries = pattern_circular(
857 CircularPattern::TwoD(data.clone()),
858 Geometry::Sketch(sketch.clone()),
859 exec_state,
860 args.clone(),
861 )
862 .await?;
863
864 let Geometries::Sketches(new_sketches) = geometries else {
865 return Err(KclError::new_semantic(KclErrorDetails::new(
866 "Expected a vec of sketches".to_string(),
867 vec![args.source_range],
868 )));
869 };
870
871 sketches.extend(new_sketches);
872 }
873
874 Ok(sketches)
875}
876
877pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
879 let solids = args.get_unlabeled_kw_arg("solids", &RuntimeType::solids(), exec_state)?;
880 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
885 let axis: Axis3dOrPoint3d = args.get_kw_arg(
887 "axis",
888 &RuntimeType::Union(vec![
889 RuntimeType::Primitive(PrimitiveType::Axis3d),
890 RuntimeType::point3d(),
891 ]),
892 exec_state,
893 )?;
894 let axis = axis.to_point3d();
895
896 let center: Option<[TyF64; 3]> = args.get_kw_arg_opt("center", &RuntimeType::point3d(), exec_state)?;
898 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
900 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
902 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
905
906 let solids = inner_pattern_circular_3d(
907 solids,
908 instances,
909 [axis[0].n, axis[1].n, axis[2].n],
910 center,
911 arc_degrees.map(|x| x.n),
912 rotate_duplicates,
913 use_original,
914 exec_state,
915 args,
916 )
917 .await?;
918 Ok(solids.into())
919}
920
921#[allow(clippy::too_many_arguments)]
922async fn inner_pattern_circular_3d(
923 solids: Vec<Solid>,
924 instances: u32,
925 axis: [f64; 3],
926 center: Option<[TyF64; 3]>,
927 arc_degrees: Option<f64>,
928 rotate_duplicates: Option<bool>,
929 use_original: Option<bool>,
930 exec_state: &mut ExecState,
931 args: Args,
932) -> Result<Vec<Solid>, KclError> {
933 exec_state
937 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, &args), &solids)
938 .await?;
939
940 let starting_solids = solids;
941
942 if args.ctx.context_type == crate::execution::ContextType::Mock {
943 return Ok(starting_solids);
944 }
945
946 let mut solids = Vec::new();
947 let center = center.unwrap_or(POINT_ZERO_ZERO_ZERO);
948 let data = CircularPattern3dData {
949 instances,
950 axis,
951 center,
952 arc_degrees,
953 rotate_duplicates,
954 use_original,
955 };
956 for solid in starting_solids.iter() {
957 let geometries = pattern_circular(
958 CircularPattern::ThreeD(data.clone()),
959 Geometry::Solid(solid.clone()),
960 exec_state,
961 args.clone(),
962 )
963 .await?;
964
965 let Geometries::Solids(new_solids) = geometries else {
966 return Err(KclError::new_semantic(KclErrorDetails::new(
967 "Expected a vec of solids".to_string(),
968 vec![args.source_range],
969 )));
970 };
971
972 solids.extend(new_solids);
973 }
974
975 Ok(solids)
976}
977
978async fn pattern_circular(
979 data: CircularPattern,
980 geometry: Geometry,
981 exec_state: &mut ExecState,
982 args: Args,
983) -> Result<Geometries, KclError> {
984 let num_repetitions = match data.repetitions() {
985 RepetitionsNeeded::More(n) => n,
986 RepetitionsNeeded::None => {
987 return Ok(Geometries::from(geometry));
988 }
989 RepetitionsNeeded::Invalid => {
990 return Err(KclError::new_semantic(KclErrorDetails::new(
991 MUST_HAVE_ONE_INSTANCE.to_owned(),
992 vec![args.source_range],
993 )));
994 }
995 };
996
997 let center = data.center_mm();
998 let resp = exec_state
999 .send_modeling_cmd(
1000 ModelingCmdMeta::from_args(exec_state, &args),
1001 ModelingCmd::from(
1002 mcmd::EntityCircularPattern::builder()
1003 .axis(kcmc::shared::Point3d::from(data.axis()))
1004 .entity_id(if data.use_original() {
1005 geometry.original_id()
1006 } else {
1007 geometry.id()
1008 })
1009 .center(kcmc::shared::Point3d {
1010 x: LengthUnit(center[0]),
1011 y: LengthUnit(center[1]),
1012 z: LengthUnit(center[2]),
1013 })
1014 .num_repetitions(num_repetitions)
1015 .arc_degrees(data.arc_degrees().unwrap_or(360.0))
1016 .rotate_duplicates(data.rotate_duplicates().unwrap_or(true))
1017 .build(),
1018 ),
1019 )
1020 .await?;
1021
1022 let mut mock_ids = Vec::new();
1025 let entity_ids = if let OkWebSocketResponseData::Modeling {
1026 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1027 } = &resp
1028 {
1029 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1030 } else if args.ctx.no_engine_commands().await {
1031 mock_ids.reserve(num_repetitions as usize);
1032 for _ in 0..num_repetitions {
1033 mock_ids.push(exec_state.next_uuid());
1034 }
1035 &mock_ids
1036 } else {
1037 return Err(KclError::new_engine(KclErrorDetails::new(
1038 format!("EntityCircularPattern response was not as expected: {resp:?}"),
1039 vec![args.source_range],
1040 )));
1041 };
1042
1043 let geometries = match geometry {
1044 Geometry::Sketch(sketch) => {
1045 let mut geometries = vec![sketch.clone()];
1046 for id in entity_ids.iter().copied() {
1047 let mut new_sketch = sketch.clone();
1048 new_sketch.id = id;
1049 geometries.push(new_sketch);
1050 }
1051 Geometries::Sketches(geometries)
1052 }
1053 Geometry::Solid(solid) => {
1054 let mut geometries = vec![solid.clone()];
1055 for id in entity_ids.iter().copied() {
1056 let mut new_solid = solid.clone();
1057 new_solid.id = id;
1058 geometries.push(new_solid);
1059 }
1060 Geometries::Solids(geometries)
1061 }
1062 };
1063
1064 Ok(geometries)
1065}