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