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::CompilationIssue;
21use crate::ExecutorContext;
22use crate::KclVersion;
23use crate::NodePath;
24use crate::SourceRange;
25use crate::errors::KclError;
26use crate::errors::KclErrorDetails;
27use crate::errors::Severity;
28use crate::errors::Tag;
29use crate::execution::ArtifactId;
30use crate::execution::EarlyReturn;
31use crate::execution::ExecState;
32use crate::execution::Geometries;
33use crate::execution::Geometry;
34use crate::execution::ImportedGeometry;
35use crate::execution::KclObjectFields;
36use crate::execution::KclValue;
37use crate::execution::KclValueControlFlow;
38use crate::execution::ModelingCmdMeta;
39use crate::execution::Sketch;
40use crate::execution::Solid;
41use crate::execution::SolidOrImportedGeometry;
42use crate::execution::annotations;
43use crate::execution::early_return;
44use crate::execution::fn_call::Arg;
45use crate::execution::fn_call::Args;
46use crate::execution::kcl_value::FunctionSource;
47use crate::execution::types::CoercionMode;
48use crate::execution::types::NumericType;
49use crate::execution::types::NumericTypeExt;
50use crate::execution::types::PrimitiveType;
51use crate::execution::types::RuntimeType;
52use crate::std::args::TyF64;
53use crate::std::axis_or_reference::Axis2dOrPoint2d;
54use crate::std::shapes::POINT_ZERO_ZERO;
55use crate::std::utils::point_3d_to_mm;
56use crate::std::utils::point_to_mm;
57pub const POINT_ZERO_ZERO_ZERO: [TyF64; 3] = [
58 TyF64::new(
59 0.0,
60 crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
61 ),
62 TyF64::new(
63 0.0,
64 crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
65 ),
66 TyF64::new(
67 0.0,
68 crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
69 ),
70];
71
72const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
73const PATTERN_LINEAR_2D_REGIONS_ONLY: &str = "patternLinear2d should only be used with regions";
74
75#[derive(Debug)]
77pub(crate) enum Patternable3d {
78 Solids(Vec<Solid>),
79 ImportedGeometry(ImportedGeometry),
80}
81
82fn pattern_geometry_3d_type() -> RuntimeType {
85 RuntimeType::Union(vec![RuntimeType::solids(), RuntimeType::imported()])
86}
87
88pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValueControlFlow, KclError> {
90 let (geometry, instances, transform, use_original) = pattern_transform_parse_args(&args, exec_state)?;
91
92 match inner_pattern_transform(geometry, instances, transform, use_original, exec_state, &args).await {
93 Ok(geometry) => Ok(KclValue::normal(geometry)),
94 Err(EarlyReturn::Value(cf)) => Ok(cf),
97 Err(EarlyReturn::Error(err)) => Err(err),
98 }
99}
100
101pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValueControlFlow, KclError> {
103 let (sketches, instances, transform, use_original) = pattern_transform_2d_parse_args(&args, exec_state)?;
104
105 match inner_pattern_transform_2d(sketches, instances, transform, use_original, exec_state, &args).await {
106 Ok(sketches) => Ok(KclValue::normal(sketches.into())),
107 Err(EarlyReturn::Value(cf)) => Ok(cf),
110 Err(EarlyReturn::Error(err)) => Err(err),
111 }
112}
113
114pub(crate) fn pattern_transform_parse_args(
116 args: &Args,
117 exec_state: &mut ExecState,
118) -> Result<(Patternable3d, u32, FunctionSource, Option<bool>), KclError> {
119 let geometry: SolidOrImportedGeometry =
120 args.get_unlabeled_kw_arg("solids", &pattern_geometry_3d_type(), exec_state)?;
121 let geometry = match geometry {
122 SolidOrImportedGeometry::SolidSet(solids) => Patternable3d::Solids(solids),
123 SolidOrImportedGeometry::ImportedGeometry(geometry) => Patternable3d::ImportedGeometry(*geometry),
124 };
125 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
126 let transform: FunctionSource = args.get_kw_arg("transform", &RuntimeType::function(), exec_state)?;
127 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
128 Ok((geometry, instances, transform, use_original))
129}
130
131pub(crate) fn pattern_transform_2d_parse_args(
133 args: &Args,
134 exec_state: &mut ExecState,
135) -> Result<(Vec<Sketch>, u32, FunctionSource, Option<bool>), KclError> {
136 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
137 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
138 let transform: FunctionSource = args.get_kw_arg("transform", &RuntimeType::function(), exec_state)?;
139 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
140 Ok((sketches, instances, transform, use_original))
141}
142
143pub(crate) fn pattern_check_instances(instances: u32, source_range: SourceRange) -> Result<(), KclError> {
145 if instances < 1 {
146 return Err(KclError::new_semantic(KclErrorDetails::new(
147 MUST_HAVE_ONE_INSTANCE.to_owned(),
148 vec![source_range],
149 )));
150 }
151 Ok(())
152}
153
154pub(crate) fn transform_callback_args(
156 i: u32,
157 source_range: SourceRange,
158 node_path: Option<NodePath>,
159 exec_state: &mut ExecState,
160 ctxt: &ExecutorContext,
161) -> Args<crate::execution::fn_call::Sugary> {
162 let repetition_num = KclValue::Number {
163 value: i.into(),
164 ty: NumericType::count(),
165 meta: vec![source_range.into()],
166 };
167 Args::new(
168 Default::default(),
169 vec![(None, Arg::new(repetition_num, source_range))],
170 source_range,
171 node_path,
172 exec_state,
173 ctxt.clone(),
174 Some("transform closure".to_owned()),
175 )
176}
177
178pub(crate) fn transform_missing_value_error(source_range: SourceRange) -> KclError {
180 KclError::new_semantic(KclErrorDetails::new(
181 "Transform function must return a value".to_string(),
182 vec![source_range],
183 ))
184}
185
186pub(crate) fn transforms_from_callback_value<T: GeometryTrait>(
189 transform_fn_return: KclValue,
190 source_range: SourceRange,
191 exec_state: &mut ExecState,
192) -> Result<Vec<Transform>, KclError> {
193 let source_ranges = vec![source_range];
194 let transforms = match transform_fn_return {
195 KclValue::Object { value, .. } => vec![value],
196 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
197 let transforms: Vec<_> = value
198 .into_iter()
199 .map(|val| {
200 val.into_object().ok_or(KclError::new_semantic(KclErrorDetails::new(
201 "Transform function must return a transform object".to_string(),
202 source_ranges.clone(),
203 )))
204 })
205 .collect::<Result<_, KclError>>()?;
206 transforms
207 }
208 _ => {
209 return Err(KclError::new_semantic(KclErrorDetails::new(
210 "Transform function must return a transform object".to_string(),
211 source_ranges,
212 )));
213 }
214 };
215
216 let transforms = transforms
217 .into_iter()
218 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
219 .collect::<Result<_, KclError>>()?;
220 Ok(transforms)
221}
222
223async fn inner_pattern_transform(
224 geometry: Patternable3d,
225 instances: u32,
226 transform: FunctionSource,
227 use_original: Option<bool>,
228 exec_state: &mut ExecState,
229 args: &Args,
230) -> Result<KclValue, EarlyReturn> {
231 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
233 pattern_check_instances(instances, args.source_range)?;
234 for i in 1..instances {
235 let t = match &geometry {
236 Patternable3d::Solids(_) => {
237 make_transform::<Solid>(
238 i,
239 &transform,
240 args.source_range,
241 args.node_path.clone(),
242 exec_state,
243 &args.ctx,
244 )
245 .await?
246 }
247 Patternable3d::ImportedGeometry(_) => {
248 make_transform::<ImportedGeometry>(
249 i,
250 &transform,
251 args.source_range,
252 args.node_path.clone(),
253 exec_state,
254 &args.ctx,
255 )
256 .await?
257 }
258 };
259 transform_vec.push(t);
260 }
261 match geometry {
262 Patternable3d::Solids(solids) => Ok(execute_pattern_transform::<Solid>(
263 transform_vec,
264 solids,
265 use_original.unwrap_or_default(),
266 exec_state,
267 args,
268 )
269 .await?
270 .into()),
271 Patternable3d::ImportedGeometry(geometry) => Ok(KclValue::from_imported_geometries(
272 execute_pattern_transform(
273 transform_vec,
274 vec![geometry],
275 use_original.unwrap_or_default(),
276 exec_state,
277 args,
278 )
279 .await?,
280 )),
281 }
282}
283
284async fn inner_pattern_transform_2d(
285 sketches: Vec<Sketch>,
286 instances: u32,
287 transform: FunctionSource,
288 use_original: Option<bool>,
289 exec_state: &mut ExecState,
290 args: &Args,
291) -> Result<Vec<Sketch>, EarlyReturn> {
292 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
294 pattern_check_instances(instances, args.source_range)?;
295 for i in 1..instances {
296 let t = make_transform::<Sketch>(
297 i,
298 &transform,
299 args.source_range,
300 args.node_path.clone(),
301 exec_state,
302 &args.ctx,
303 )
304 .await?;
305 transform_vec.push(t);
306 }
307 Ok(execute_pattern_transform(
308 transform_vec,
309 sketches,
310 use_original.unwrap_or_default(),
311 exec_state,
312 args,
313 )
314 .await?)
315}
316
317pub(crate) async fn execute_pattern_transform<T: GeometryTrait>(
318 transforms: Vec<Vec<Transform>>,
319 geo_set: T::Set,
320 use_original: bool,
321 exec_state: &mut ExecState,
322 args: &Args,
323) -> Result<Vec<T>, KclError> {
324 T::flush_batch(args, exec_state, &geo_set).await?;
328 let starting: Vec<T> = geo_set.into();
329
330 let mut output = Vec::new();
331 for mut geo in starting {
332 let new = send_pattern_transform(transforms.clone(), &mut geo, use_original, exec_state, args).await?;
333 output.extend(new)
334 }
335 Ok(output)
336}
337
338async fn send_pattern_transform<T: GeometryTrait>(
339 transforms: Vec<Vec<Transform>>,
342 geometry: &mut T,
343 use_original: bool,
344 exec_state: &mut ExecState,
345 args: &Args,
346) -> Result<Vec<T>, KclError> {
347 let extra_instances = transforms.len();
348 let geometry_id = geometry.id(&args.ctx).await?;
349 let entity_id = if use_original {
350 geometry.topology_id()
351 } else {
352 geometry_id
353 };
354
355 let resp = exec_state
356 .send_modeling_cmd(
357 ModelingCmdMeta::from_args(exec_state, args),
358 ModelingCmd::from(
359 mcmd::EntityLinearPatternTransform::builder()
360 .entity_id(entity_id)
361 .transform(Default::default())
362 .transforms(transforms)
363 .build(),
364 ),
365 )
366 .await?;
367
368 let mut mock_ids = Vec::new();
369 let entity_ids = if let OkWebSocketResponseData::Modeling {
370 modeling_response: OkModelingCmdResponse::EntityLinearPatternTransform(pattern_info),
371 } = &resp
372 {
373 &pattern_info.entity_face_edge_ids.iter().map(|x| x.object_id).collect()
374 } else if args.ctx.no_engine_commands().await {
375 mock_ids.reserve(extra_instances);
376 for _ in 0..extra_instances {
377 mock_ids.push(exec_state.next_uuid());
378 }
379 &mock_ids
380 } else {
381 return Err(KclError::new_engine(KclErrorDetails::new(
382 format!("EntityLinearPattern response was not as expected: {resp:?}"),
383 vec![args.source_range],
384 )));
385 };
386
387 let mut geometries = vec![geometry.clone()];
388 for id in entity_ids.iter().copied() {
389 let mut new_geometry = geometry.clone();
390 new_geometry.set_id(id);
391 new_geometry.set_artifact_id(id);
392 geometries.push(new_geometry);
393 }
394 Ok(geometries)
395}
396
397async fn make_transform<T: GeometryTrait>(
398 i: u32,
399 transform: &FunctionSource,
400 source_range: SourceRange,
401 node_path: Option<NodePath>,
402 exec_state: &mut ExecState,
403 ctxt: &ExecutorContext,
404) -> Result<Vec<Transform>, EarlyReturn> {
405 let transform_fn_args = transform_callback_args(i, source_range, node_path, exec_state, ctxt);
407 let transform_fn_return = transform
408 .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
409 .await?;
410
411 let transform_fn_return = transform_fn_return.ok_or_else(|| transform_missing_value_error(source_range))?;
413
414 let transform_fn_return = early_return!(transform_fn_return);
418
419 Ok(transforms_from_callback_value::<T>(
420 transform_fn_return,
421 source_range,
422 exec_state,
423 )?)
424}
425
426fn transform_from_obj_fields<T: GeometryTrait>(
427 transform: KclObjectFields,
428 source_ranges: Vec<SourceRange>,
429 exec_state: &mut ExecState,
430) -> Result<Transform, KclError> {
431 let replicate = match transform.get("replicate") {
433 Some(KclValue::Bool { value: true, .. }) => true,
434 Some(KclValue::Bool { value: false, .. }) => false,
435 Some(_) => {
436 return Err(KclError::new_semantic(KclErrorDetails::new(
437 "The 'replicate' key must be a bool".to_string(),
438 source_ranges,
439 )));
440 }
441 None => true,
442 };
443
444 let scale = match transform.get("scale") {
445 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
446 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
447 };
448
449 for (dim, name) in [(scale.x, "x"), (scale.y, "y"), (scale.z, "z")] {
450 if dim == 0.0 {
451 return Err(KclError::new_semantic(KclErrorDetails::new(
452 format!("cannot set {name} = 0, scale factor must be nonzero"),
453 source_ranges,
454 )));
455 }
456 }
457 let translate = match transform.get("translate") {
458 Some(x) => {
459 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
460 kcmc::shared::Point3d::<LengthUnit> {
461 x: LengthUnit(arr[0]),
462 y: LengthUnit(arr[1]),
463 z: LengthUnit(arr[2]),
464 }
465 }
466 None => kcmc::shared::Point3d::<LengthUnit> {
467 x: LengthUnit(0.0),
468 y: LengthUnit(0.0),
469 z: LengthUnit(0.0),
470 },
471 };
472
473 let mut rotation = Rotation::default();
474 if let Some(rot) = transform.get("rotation") {
475 let KclValue::Object { value: rot, .. } = rot else {
476 return Err(KclError::new_semantic(KclErrorDetails::new(
477 "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')".to_owned(),
478 source_ranges,
479 )));
480 };
481 if let Some(axis) = rot.get("axis") {
482 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
483 }
484 if let Some(angle) = rot.get("angle") {
485 match angle {
486 KclValue::Number { value: number, .. } => {
487 rotation.angle = Angle::from_degrees(*number);
488 }
489 _ => {
490 return Err(KclError::new_semantic(KclErrorDetails::new(
491 "The 'rotation.angle' key must be a number (of degrees)".to_owned(),
492 source_ranges,
493 )));
494 }
495 }
496 }
497 if let Some(origin) = rot.get("origin") {
498 rotation.origin = match origin {
499 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
500 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
501 other => {
502 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges, exec_state)?).into();
503 OriginType::Custom { origin }
504 }
505 };
506 }
507 }
508
509 let transform = Transform::builder()
510 .replicate(replicate)
511 .scale(scale)
512 .translate(translate)
513 .rotation(rotation)
514 .build();
515 Ok(transform)
516}
517
518fn array_to_point3d(
519 val: &KclValue,
520 source_ranges: Vec<SourceRange>,
521 exec_state: &mut ExecState,
522) -> Result<[TyF64; 3], KclError> {
523 val.coerce(&RuntimeType::point3d(), CoercionMode::implicit(), exec_state)
524 .map_err(|e| {
525 KclError::new_semantic(KclErrorDetails::new(
526 format!(
527 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
528 e.found
529 .map(|t| t.human_friendly_type())
530 .unwrap_or_else(|| val.human_friendly_type())
531 ),
532 source_ranges,
533 ))
534 })
535 .map(|val| val.as_point3d().unwrap())
536}
537
538fn array_to_point2d(
539 val: &KclValue,
540 source_ranges: Vec<SourceRange>,
541 exec_state: &mut ExecState,
542) -> Result<[TyF64; 2], KclError> {
543 val.coerce(&RuntimeType::point2d(), CoercionMode::implicit(), exec_state)
544 .map_err(|e| {
545 KclError::new_semantic(KclErrorDetails::new(
546 format!(
547 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
548 e.found
549 .map(|t| t.human_friendly_type())
550 .unwrap_or_else(|| val.human_friendly_type())
551 ),
552 source_ranges,
553 ))
554 })
555 .map(|val| val.as_point2d().unwrap())
556}
557
558pub trait GeometryTrait: Clone {
559 type Set: Into<Vec<Self>> + Clone;
560 #[allow(async_fn_in_trait)]
561 async fn id(&mut self, ctx: &ExecutorContext) -> Result<Uuid, KclError>;
562 fn topology_id(&self) -> Uuid;
563 fn set_id(&mut self, id: Uuid);
564 fn set_artifact_id(&mut self, id: Uuid);
565 fn array_to_point3d(
566 val: &KclValue,
567 source_ranges: Vec<SourceRange>,
568 exec_state: &mut ExecState,
569 ) -> Result<[TyF64; 3], KclError>;
570 #[allow(async_fn_in_trait)]
571 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
572}
573
574impl GeometryTrait for Sketch {
575 type Set = Vec<Sketch>;
576 fn set_id(&mut self, id: Uuid) {
577 self.id = id;
578 }
579 fn set_artifact_id(&mut self, id: Uuid) {
580 self.artifact_id = ArtifactId::new(id);
581 }
582 async fn id(&mut self, _: &ExecutorContext) -> Result<Uuid, KclError> {
583 Ok(self.id)
584 }
585 fn topology_id(&self) -> Uuid {
586 self.original_id
587 }
588 fn array_to_point3d(
589 val: &KclValue,
590 source_ranges: Vec<SourceRange>,
591 exec_state: &mut ExecState,
592 ) -> Result<[TyF64; 3], KclError> {
593 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
594 let ty = x.ty;
595 Ok([x, y, TyF64::new(0.0, ty)])
596 }
597
598 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
599 Ok(())
600 }
601}
602
603impl GeometryTrait for Solid {
604 type Set = Vec<Solid>;
605 fn set_id(&mut self, id: Uuid) {
606 self.id = id;
607 self.value_id = id;
608 if let Some(sketch) = self.sketch_mut() {
610 sketch.id = id;
611 }
612 }
613
614 fn set_artifact_id(&mut self, id: Uuid) {
615 self.become_pattern_copy(id);
616 }
617
618 async fn id(&mut self, _: &ExecutorContext) -> Result<Uuid, KclError> {
619 Ok(self.id)
620 }
621
622 fn topology_id(&self) -> Uuid {
623 Solid::topology_id(self)
624 }
625
626 fn array_to_point3d(
627 val: &KclValue,
628 source_ranges: Vec<SourceRange>,
629 exec_state: &mut ExecState,
630 ) -> Result<[TyF64; 3], KclError> {
631 array_to_point3d(val, source_ranges, exec_state)
632 }
633
634 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
635 exec_state
636 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, args), solid_set)
637 .await
638 }
639}
640
641impl GeometryTrait for ImportedGeometry {
642 type Set = Vec<ImportedGeometry>;
643
644 async fn id(&mut self, ctx: &ExecutorContext) -> Result<Uuid, KclError> {
645 ImportedGeometry::id(self, ctx).await
646 }
647
648 fn topology_id(&self) -> Uuid {
649 self.id
650 }
651
652 fn set_id(&mut self, id: Uuid) {
653 self.id = id;
654 }
655
656 fn set_artifact_id(&mut self, _: Uuid) {}
657
658 fn array_to_point3d(
659 val: &KclValue,
660 source_ranges: Vec<SourceRange>,
661 exec_state: &mut ExecState,
662 ) -> Result<[TyF64; 3], KclError> {
663 array_to_point3d(val, source_ranges, exec_state)
664 }
665
666 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
667 Ok(())
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use crate::execution::KclValueView;
675 use crate::execution::types::NumericType;
676 use crate::execution::types::PrimitiveType;
677
678 async fn assert_imported_pattern_executes(code: &str) {
679 let current_file = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
680 .join("tests")
681 .join("inputs")
682 .join("main.kcl");
683 let program = crate::Program::parse_no_errs(code).unwrap();
684 let ctx = crate::test_server::new_context(true, Some(current_file), true, program.language_version().unwrap())
685 .await
686 .unwrap();
687 let result = ctx.run_with_caching(program).await.unwrap();
688
689 let KclValueView::HomArray { value } = result.variables.get("patterned").unwrap() else {
690 panic!("Expected the imported geometry pattern to return an array");
691 };
692 assert_eq!(value.len(), 3);
693 assert!(
694 value
695 .iter()
696 .all(|value| matches!(value, KclValueView::ImportedGeometry(_)))
697 );
698 let ids = value
699 .iter()
700 .map(|value| match value {
701 KclValueView::ImportedGeometry(geometry) => geometry.id,
702 _ => unreachable!(),
703 })
704 .collect::<std::collections::HashSet<_>>();
705 assert_eq!(ids.len(), 3);
706
707 ctx.close().await;
708 }
709
710 #[tokio::test(flavor = "multi_thread")]
711 async fn imported_geometry_pattern_linear_3d() {
712 assert_imported_pattern_executes(
713 r#"import "cube.step" as cube
714
715patterned = patternLinear3d(cube, instances = 3, distance = 20, axis = X)
716"#,
717 )
718 .await;
719 }
720
721 #[tokio::test(flavor = "multi_thread")]
722 async fn imported_geometry_pattern_circular_3d() {
723 assert_imported_pattern_executes(
724 r#"import "cube.step" as cube
725
726patterned = patternCircular3d(cube, instances = 3, axis = Z, center = [20, 0, 0])
727"#,
728 )
729 .await;
730 }
731
732 #[tokio::test(flavor = "multi_thread")]
733 async fn imported_geometry_pattern_transform() {
734 assert_imported_pattern_executes(
735 r#"import "cube.step" as cube
736
737fn shift(@i) {
738 return { translate = [20 * i, 0, 0] }
739}
740
741patterned = patternTransform(cube, instances = 3, transform = shift)
742"#,
743 )
744 .await;
745 }
746
747 #[tokio::test(flavor = "multi_thread")]
748 async fn test_array_to_point3d() {
749 let ctx = ExecutorContext::new_mock(None).await;
750 let mut exec_state = ExecState::new(&ctx);
751 let input = KclValue::HomArray {
752 value: vec![
753 KclValue::Number {
754 value: 1.1,
755 meta: Default::default(),
756 ty: NumericType::mm(),
757 },
758 KclValue::Number {
759 value: 2.2,
760 meta: Default::default(),
761 ty: NumericType::mm(),
762 },
763 KclValue::Number {
764 value: 3.3,
765 meta: Default::default(),
766 ty: NumericType::mm(),
767 },
768 ],
769 ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
770 };
771 let expected = [
772 TyF64::new(1.1, NumericType::mm()),
773 TyF64::new(2.2, NumericType::mm()),
774 TyF64::new(3.3, NumericType::mm()),
775 ];
776 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
777 assert_eq!(actual.unwrap(), expected);
778 ctx.close().await;
779 }
780
781 #[tokio::test(flavor = "multi_thread")]
782 async fn test_tuple_to_point3d() {
783 let ctx = ExecutorContext::new_mock(None).await;
784 let mut exec_state = ExecState::new(&ctx);
785 let input = KclValue::Tuple {
786 value: vec![
787 KclValue::Number {
788 value: 1.1,
789 meta: Default::default(),
790 ty: NumericType::mm(),
791 },
792 KclValue::Number {
793 value: 2.2,
794 meta: Default::default(),
795 ty: NumericType::mm(),
796 },
797 KclValue::Number {
798 value: 3.3,
799 meta: Default::default(),
800 ty: NumericType::mm(),
801 },
802 ],
803 meta: Default::default(),
804 };
805 let expected = [
806 TyF64::new(1.1, NumericType::mm()),
807 TyF64::new(2.2, NumericType::mm()),
808 TyF64::new(3.3, NumericType::mm()),
809 ];
810 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
811 assert_eq!(actual.unwrap(), expected);
812 ctx.close().await;
813 }
814
815 fn pattern_linear_2d_code(kcl_version: &str, input: &str) -> String {
816 format!(
817 r#"@settings(kclVersion = {kcl_version}, defaultLengthUnit = mm, experimentalFeatures = allow)
818
819profile = sketch(on = XY) {{
820 circle1 = circle(start = [var 10mm, var 0mm], center = [var 0mm, var 0mm])
821}}
822patterned = patternLinear2d({input}, instances = 2, distance = 20mm, axis = X)
823"#
824 )
825 }
826
827 async fn run_mock(code: &str) -> Result<crate::ExecOutcome, crate::KclErrorWithOutputs> {
828 let program = crate::Program::parse_no_errs(code).unwrap();
829 let ctx = ExecutorContext::new_mock(None).await;
830 let result = ctx.run_mock(&program, &crate::execution::MockConfig::default()).await;
831 ctx.close().await;
832 result
833 }
834
835 #[tokio::test(flavor = "multi_thread")]
836 async fn pattern_linear_2d_with_sketch_warns_before_v3() {
837 for version in ["1.0", "2.0"] {
838 let code = pattern_linear_2d_code(version, "profile");
839 let outcome = run_mock(&code).await.unwrap();
840 let warnings = outcome
841 .issues
842 .iter()
843 .filter(|issue| issue.message == PATTERN_LINEAR_2D_REGIONS_ONLY)
844 .collect::<Vec<_>>();
845
846 assert_eq!(
847 warnings.len(),
848 1,
849 "unexpected issues for KCL {version}: {:#?}",
850 outcome.issues
851 );
852 assert_eq!(warnings[0].severity, Severity::Warning);
853 }
854 }
855
856 #[tokio::test(flavor = "multi_thread")]
857 async fn pattern_linear_2d_with_sketch_is_an_error_in_v3() {
858 let code = pattern_linear_2d_code(r#""3.0-preview""#, "profile");
859 let error = run_mock(&code).await.unwrap_err();
860
861 assert!(matches!(error.error, KclError::Semantic { .. }));
862 assert_eq!(error.error.message(), PATTERN_LINEAR_2D_REGIONS_ONLY);
863 }
864
865 #[tokio::test(flavor = "multi_thread")]
866 async fn pattern_linear_2d_with_region_is_allowed_in_v3() {
867 let code = pattern_linear_2d_code(r#""3.0-preview""#, "region(segments = [profile.circle1])");
868 let outcome = run_mock(&code).await.unwrap();
869
870 assert!(
871 outcome
872 .issues
873 .iter()
874 .all(|issue| issue.message != PATTERN_LINEAR_2D_REGIONS_ONLY),
875 "unexpected regions-only issue: {:#?}",
876 outcome.issues
877 );
878 }
879
880 #[tokio::test(flavor = "multi_thread")]
881 async fn pattern_linear_2d_with_sketch_v1_profile_is_allowed() {
882 let code = r#"@settings(kclVersion = 2.0, defaultLengthUnit = mm, experimentalFeatures = allow)
883
884patterned = startSketchOn(XY)
885 |> rectangle(width = 4mm, height = 3mm, center = [0mm, 0mm])
886 |> patternLinear2d(instances = 10, distance = 10mm, axis = [1, 0])
887"#;
888 let outcome = run_mock(code).await.unwrap();
889
890 assert!(
891 outcome
892 .issues
893 .iter()
894 .all(|issue| issue.message != PATTERN_LINEAR_2D_REGIONS_ONLY),
895 "unexpected regions-only issue: {:#?}",
896 outcome.issues
897 );
898 }
899}
900
901pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
903 let sketches_source_range = args
904 .unlabeled_kw_arg_unconverted()
905 .map_or(args.source_range, |arg| arg.source_range);
906 let sketches: Vec<Sketch> = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
907 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
908 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
909 let axis: Axis2dOrPoint2d = args.get_kw_arg(
910 "axis",
911 &RuntimeType::Union(vec![
912 RuntimeType::Primitive(PrimitiveType::Axis2d),
913 RuntimeType::point2d(),
914 ]),
915 exec_state,
916 )?;
917 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
918
919 let has_sketch_solver_block = sketches.iter().any(|sketch| {
920 sketch.origin_sketch_id.is_none()
921 && (exec_state.is_sketch_block_path(sketch.artifact_id)
922 || exec_state.is_sketch_block_path(sketch.original_id.into()))
923 });
924 if has_sketch_solver_block {
925 if exec_state.kcl_version() >= KclVersion::V3Preview {
926 return Err(KclError::new_semantic(KclErrorDetails::new(
927 PATTERN_LINEAR_2D_REGIONS_ONLY.to_owned(),
928 vec![sketches_source_range],
929 )));
930 }
931
932 exec_state.warn(
933 CompilationIssue {
934 source_range: sketches_source_range,
935 message: PATTERN_LINEAR_2D_REGIONS_ONLY.to_owned(),
936 suggestion: None,
937 severity: Severity::Warning,
938 tag: Tag::Deprecated,
939 },
940 annotations::WARN_DEPRECATED,
941 );
942 }
943
944 let axis = axis.to_point2d();
945 if axis[0].n == 0.0 && axis[1].n == 0.0 {
946 return Err(KclError::new_semantic(KclErrorDetails::new(
947 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
948 .to_owned(),
949 vec![args.source_range],
950 )));
951 }
952
953 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
954 Ok(sketches.into())
955}
956
957async fn inner_pattern_linear_2d(
958 sketches: Vec<Sketch>,
959 instances: u32,
960 distance: TyF64,
961 axis: [TyF64; 2],
962 use_original: Option<bool>,
963 exec_state: &mut ExecState,
964 args: Args,
965) -> Result<Vec<Sketch>, KclError> {
966 let [x, y] = point_to_mm(axis);
967 let axis_len = f64::sqrt(x * x + y * y);
968 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
969 let transforms: Vec<_> = (1..instances)
970 .map(|i| {
971 let d = distance.unwrap_to_mm() * (i as f64);
972 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
973 vec![Transform::builder().translate(translate).build()]
974 })
975 .collect();
976 execute_pattern_transform(
977 transforms,
978 sketches,
979 use_original.unwrap_or_default(),
980 exec_state,
981 &args,
982 )
983 .await
984}
985
986pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
988 let geometry: SolidOrImportedGeometry =
989 args.get_unlabeled_kw_arg("solids", &pattern_geometry_3d_type(), exec_state)?;
990 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
991 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
992 let axis: Axis3dOrPoint3d = args.get_kw_arg(
993 "axis",
994 &RuntimeType::Union(vec![
995 RuntimeType::Primitive(PrimitiveType::Axis3d),
996 RuntimeType::point3d(),
997 ]),
998 exec_state,
999 )?;
1000 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
1001
1002 let axis = axis.to_point3d();
1003 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
1004 return Err(KclError::new_semantic(KclErrorDetails::new(
1005 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
1006 .to_owned(),
1007 vec![args.source_range],
1008 )));
1009 }
1010
1011 match geometry {
1012 SolidOrImportedGeometry::SolidSet(solids) => {
1013 Ok(
1014 inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args)
1015 .await?
1016 .into(),
1017 )
1018 }
1019 SolidOrImportedGeometry::ImportedGeometry(geometry) => Ok(KclValue::from_imported_geometries(
1020 inner_pattern_linear_3d(
1021 vec![*geometry],
1022 instances,
1023 distance,
1024 axis,
1025 use_original,
1026 exec_state,
1027 args,
1028 )
1029 .await?,
1030 )),
1031 }
1032}
1033
1034async fn inner_pattern_linear_3d<T: GeometryTrait<Set = Vec<T>>>(
1035 geometry: Vec<T>,
1036 instances: u32,
1037 distance: TyF64,
1038 axis: [TyF64; 3],
1039 use_original: Option<bool>,
1040 exec_state: &mut ExecState,
1041 args: Args,
1042) -> Result<Vec<T>, KclError> {
1043 let [x, y, z] = point_3d_to_mm(axis);
1044 let axis_len = f64::sqrt(x * x + y * y + z * z);
1045 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
1046 let transforms: Vec<_> = (1..instances)
1047 .map(|i| {
1048 let d = distance.unwrap_to_mm() * (i as f64);
1049 let translate = (normalized_axis * d).map(LengthUnit);
1050 vec![Transform::builder().translate(translate).build()]
1051 })
1052 .collect();
1053 execute_pattern_transform(
1054 transforms,
1055 geometry,
1056 use_original.unwrap_or_default(),
1057 exec_state,
1058 &args,
1059 )
1060 .await
1061}
1062
1063#[derive(Debug, Clone, Serialize, PartialEq)]
1065#[serde(rename_all = "camelCase")]
1066struct CircularPattern2dData {
1067 pub instances: u32,
1072 pub center: [TyF64; 2],
1074 pub arc_degrees: Option<f64>,
1076 pub rotate_duplicates: Option<bool>,
1078 #[serde(default)]
1081 pub use_original: Option<bool>,
1082}
1083
1084#[derive(Debug, Clone, Serialize, PartialEq)]
1086#[serde(rename_all = "camelCase")]
1087struct CircularPattern3dData {
1088 pub instances: u32,
1093 pub axis: [f64; 3],
1096 pub center: [TyF64; 3],
1098 pub arc_degrees: Option<f64>,
1100 pub rotate_duplicates: Option<bool>,
1102 #[serde(default)]
1105 pub use_original: Option<bool>,
1106}
1107
1108#[allow(clippy::large_enum_variant)]
1109#[derive(Clone)]
1110enum CircularPattern {
1111 ThreeD(CircularPattern3dData),
1112 TwoD(CircularPattern2dData),
1113}
1114
1115enum RepetitionsNeeded {
1116 More(u32),
1118 None,
1120 Invalid,
1122}
1123
1124impl From<u32> for RepetitionsNeeded {
1125 fn from(n: u32) -> Self {
1126 match n.cmp(&1) {
1127 Ordering::Less => Self::Invalid,
1128 Ordering::Equal => Self::None,
1129 Ordering::Greater => Self::More(n - 1),
1130 }
1131 }
1132}
1133
1134impl CircularPattern {
1135 pub fn axis(&self) -> [f64; 3] {
1136 match self {
1137 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
1138 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
1139 }
1140 }
1141
1142 pub fn center_mm(&self) -> [f64; 3] {
1143 match self {
1144 CircularPattern::TwoD(lp) => [lp.center[0].unwrap_to_mm(), lp.center[1].unwrap_to_mm(), 0.0],
1145 CircularPattern::ThreeD(lp) => [
1146 lp.center[0].unwrap_to_mm(),
1147 lp.center[1].unwrap_to_mm(),
1148 lp.center[2].unwrap_to_mm(),
1149 ],
1150 }
1151 }
1152
1153 fn repetitions(&self) -> RepetitionsNeeded {
1154 let n = match self {
1155 CircularPattern::TwoD(lp) => lp.instances,
1156 CircularPattern::ThreeD(lp) => lp.instances,
1157 };
1158 RepetitionsNeeded::from(n)
1159 }
1160
1161 pub fn arc_degrees(&self) -> Option<f64> {
1162 match self {
1163 CircularPattern::TwoD(lp) => lp.arc_degrees,
1164 CircularPattern::ThreeD(lp) => lp.arc_degrees,
1165 }
1166 }
1167
1168 pub fn rotate_duplicates(&self) -> Option<bool> {
1169 match self {
1170 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
1171 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
1172 }
1173 }
1174
1175 pub fn use_original(&self) -> bool {
1176 match self {
1177 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
1178 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
1179 }
1180 }
1181}
1182
1183pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1185 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
1186 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
1187 let center: Option<[TyF64; 2]> = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
1188 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1189 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
1190 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
1191
1192 let sketches = inner_pattern_circular_2d(
1193 sketches,
1194 instances,
1195 center,
1196 arc_degrees.map(|x| x.n),
1197 rotate_duplicates,
1198 use_original,
1199 exec_state,
1200 args,
1201 )
1202 .await?;
1203 Ok(sketches.into())
1204}
1205
1206#[allow(clippy::too_many_arguments)]
1207async fn inner_pattern_circular_2d(
1208 sketch_set: Vec<Sketch>,
1209 instances: u32,
1210 center: Option<[TyF64; 2]>,
1211 arc_degrees: Option<f64>,
1212 rotate_duplicates: Option<bool>,
1213 use_original: Option<bool>,
1214 exec_state: &mut ExecState,
1215 args: Args,
1216) -> Result<Vec<Sketch>, KclError> {
1217 let starting_sketches = sketch_set;
1218
1219 if args.ctx.context_type == crate::execution::ContextType::Mock {
1220 return Ok(starting_sketches);
1221 }
1222 let center = center.unwrap_or(POINT_ZERO_ZERO);
1223 let data = CircularPattern2dData {
1224 instances,
1225 center,
1226 arc_degrees,
1227 rotate_duplicates,
1228 use_original,
1229 };
1230
1231 let mut sketches = Vec::new();
1232 for sketch in starting_sketches.iter() {
1233 let geometries =
1234 pattern_circular_sketch(data.clone(), Geometry::Sketch(sketch.clone()), exec_state, args.clone()).await?;
1235
1236 let Geometries::Sketches(new_sketches) = geometries else {
1237 return Err(KclError::new_semantic(KclErrorDetails::new(
1238 "Expected a vec of sketches".to_string(),
1239 vec![args.source_range],
1240 )));
1241 };
1242
1243 sketches.extend(new_sketches);
1244 }
1245
1246 Ok(sketches)
1247}
1248
1249async fn pattern_circular_sketch(
1250 data: CircularPattern2dData,
1251 geometry: Geometry,
1252 exec_state: &mut ExecState,
1253 args: Args,
1254) -> Result<Geometries, KclError> {
1255 let Geometry::Sketch(mut sketch) = geometry else {
1256 return Err(KclError::new_internal(KclErrorDetails::new(
1257 "A 2D circular pattern requires a sketch".to_owned(),
1258 vec![args.source_range],
1259 )));
1260 };
1261 let geometries = pattern_circular(&CircularPattern::TwoD(data), &mut sketch, exec_state, &args).await?;
1262 Ok(Geometries::Sketches(geometries))
1263}
1264
1265pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1267 let geometry: SolidOrImportedGeometry =
1268 args.get_unlabeled_kw_arg("solids", &pattern_geometry_3d_type(), exec_state)?;
1269 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
1274 let axis: Axis3dOrPoint3d = args.get_kw_arg(
1276 "axis",
1277 &RuntimeType::Union(vec![
1278 RuntimeType::Primitive(PrimitiveType::Axis3d),
1279 RuntimeType::point3d(),
1280 ]),
1281 exec_state,
1282 )?;
1283 let axis = axis.to_point3d();
1284
1285 let center: Option<[TyF64; 3]> = args.get_kw_arg_opt("center", &RuntimeType::point3d(), exec_state)?;
1287 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1289 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
1291 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
1294
1295 match geometry {
1296 SolidOrImportedGeometry::SolidSet(solids) => Ok(inner_pattern_circular_3d(
1297 solids,
1298 instances,
1299 [axis[0].n, axis[1].n, axis[2].n],
1300 center,
1301 arc_degrees.map(|x| x.n),
1302 rotate_duplicates,
1303 use_original,
1304 exec_state,
1305 args,
1306 )
1307 .await?
1308 .into()),
1309 SolidOrImportedGeometry::ImportedGeometry(geometry) => Ok(KclValue::from_imported_geometries(
1310 inner_pattern_circular_3d(
1311 vec![*geometry],
1312 instances,
1313 [axis[0].n, axis[1].n, axis[2].n],
1314 center,
1315 arc_degrees.map(|x| x.n),
1316 rotate_duplicates,
1317 use_original,
1318 exec_state,
1319 args,
1320 )
1321 .await?,
1322 )),
1323 }
1324}
1325
1326#[allow(clippy::too_many_arguments)]
1327async fn inner_pattern_circular_3d<T: GeometryTrait<Set = Vec<T>>>(
1328 geometry: Vec<T>,
1329 instances: u32,
1330 axis: [f64; 3],
1331 center: Option<[TyF64; 3]>,
1332 arc_degrees: Option<f64>,
1333 rotate_duplicates: Option<bool>,
1334 use_original: Option<bool>,
1335 exec_state: &mut ExecState,
1336 args: Args,
1337) -> Result<Vec<T>, KclError> {
1338 let center = center.unwrap_or(POINT_ZERO_ZERO_ZERO);
1339 let data = CircularPattern3dData {
1340 instances,
1341 axis,
1342 center,
1343 arc_degrees,
1344 rotate_duplicates,
1345 use_original,
1346 };
1347 execute_pattern_circular(CircularPattern::ThreeD(data), geometry, exec_state, args).await
1348}
1349
1350async fn execute_pattern_circular<T: GeometryTrait>(
1351 data: CircularPattern,
1352 geometry_set: T::Set,
1353 exec_state: &mut ExecState,
1354 args: Args,
1355) -> Result<Vec<T>, KclError> {
1356 T::flush_batch(&args, exec_state, &geometry_set).await?;
1357 let starting: Vec<T> = geometry_set.into();
1358 if args.ctx.context_type == crate::execution::ContextType::Mock {
1359 let seed = starting
1360 .first()
1361 .cloned()
1362 .ok_or(KclError::new_internal(KclErrorDetails::new(
1363 "Unexpected empty set".to_owned(),
1364 vec![args.source_range],
1365 )))?;
1366 let mut mock_responses = starting;
1367 let num_repetitions = match data.repetitions() {
1368 RepetitionsNeeded::More(n) => n,
1369 RepetitionsNeeded::None => {
1370 return Ok(mock_responses);
1371 }
1372 RepetitionsNeeded::Invalid => {
1373 return Err(KclError::new_semantic(KclErrorDetails::new(
1374 MUST_HAVE_ONE_INSTANCE.to_owned(),
1375 vec![args.source_range],
1376 )));
1377 }
1378 };
1379 for _ in 0..num_repetitions {
1380 let new_id = exec_state.next_uuid();
1381 let mut new_geometry = seed.clone();
1382 new_geometry.set_id(new_id);
1383 new_geometry.set_artifact_id(new_id);
1384 mock_responses.push(new_geometry);
1385 }
1386
1387 return Ok(mock_responses);
1388 }
1389
1390 let mut output = Vec::new();
1391 for mut geometry in starting {
1392 output.extend(pattern_circular(&data, &mut geometry, exec_state, &args).await?);
1393 }
1394 Ok(output)
1395}
1396
1397async fn pattern_circular<T: GeometryTrait>(
1398 data: &CircularPattern,
1399 geometry: &mut T,
1400 exec_state: &mut ExecState,
1401 args: &Args,
1402) -> Result<Vec<T>, KclError> {
1403 let num_repetitions = match data.repetitions() {
1404 RepetitionsNeeded::More(n) => n,
1405 RepetitionsNeeded::None => {
1406 return Ok(vec![geometry.clone()]);
1407 }
1408 RepetitionsNeeded::Invalid => {
1409 return Err(KclError::new_semantic(KclErrorDetails::new(
1410 MUST_HAVE_ONE_INSTANCE.to_owned(),
1411 vec![args.source_range],
1412 )));
1413 }
1414 };
1415
1416 let geometry_id = geometry.id(&args.ctx).await?;
1417 let center = data.center_mm();
1418 let resp = exec_state
1419 .send_modeling_cmd(
1420 ModelingCmdMeta::from_args(exec_state, args),
1421 ModelingCmd::from(
1422 mcmd::EntityCircularPattern::builder()
1423 .axis(kcmc::shared::Point3d::from(data.axis()))
1424 .entity_id(if data.use_original() {
1425 geometry.topology_id()
1426 } else {
1427 geometry_id
1428 })
1429 .center(kcmc::shared::Point3d {
1430 x: LengthUnit(center[0]),
1431 y: LengthUnit(center[1]),
1432 z: LengthUnit(center[2]),
1433 })
1434 .num_repetitions(num_repetitions)
1435 .arc_degrees(data.arc_degrees().unwrap_or(360.0))
1436 .rotate_duplicates(data.rotate_duplicates().unwrap_or(true))
1437 .build(),
1438 ),
1439 )
1440 .await?;
1441
1442 let mut mock_ids = Vec::new();
1445 let entity_ids = if let OkWebSocketResponseData::Modeling {
1446 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1447 } = &resp
1448 {
1449 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1450 } else if args.ctx.no_engine_commands().await {
1451 mock_ids.reserve(num_repetitions as usize);
1452 for _ in 0..num_repetitions {
1453 mock_ids.push(exec_state.next_uuid());
1454 }
1455 &mock_ids
1456 } else {
1457 return Err(KclError::new_engine(KclErrorDetails::new(
1458 format!("EntityCircularPattern response was not as expected: {resp:?}"),
1459 vec![args.source_range],
1460 )));
1461 };
1462
1463 let mut geometries = vec![geometry.clone()];
1464 for id in entity_ids.iter().copied() {
1465 let mut new_geometry = geometry.clone();
1466 new_geometry.set_id(id);
1467 new_geometry.set_artifact_id(id);
1468 geometries.push(new_geometry);
1469 }
1470 Ok(geometries)
1471}