kcl_lib/std/
patterns.rs

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