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