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