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