Skip to main content

kcl_lib/std/
patterns.rs

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