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