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, KwArgs},
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 kw_args = KwArgs {
431        unlabeled: Some(Arg::new(repetition_num, source_range)),
432        labeled: Default::default(),
433    };
434    let transform_fn_args = Args::new_kw(
435        kw_args,
436        source_range,
437        ctxt.clone(),
438        exec_state.pipe_value().map(|v| Arg::new(v.clone(), source_range)),
439    );
440    let transform_fn_return = transform
441        .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
442        .await?;
443
444    // Unpack the returned transform object.
445    let source_ranges = vec![source_range];
446    let transform_fn_return = transform_fn_return.ok_or_else(|| {
447        KclError::Semantic(KclErrorDetails {
448            message: "Transform function must return a value".to_string(),
449            source_ranges: source_ranges.clone(),
450        })
451    })?;
452    let transforms = match transform_fn_return {
453        KclValue::Object { value, meta: _ } => vec![value],
454        KclValue::MixedArray { value, meta: _ } => {
455            let transforms: Vec<_> = value
456                .into_iter()
457                .map(|val| {
458                    val.into_object().ok_or(KclError::Semantic(KclErrorDetails {
459                        message: "Transform function must return a transform object".to_string(),
460                        source_ranges: source_ranges.clone(),
461                    }))
462                })
463                .collect::<Result<_, _>>()?;
464            transforms
465        }
466        _ => {
467            return Err(KclError::Semantic(KclErrorDetails {
468                message: "Transform function must return a transform object".to_string(),
469                source_ranges: source_ranges.clone(),
470            }))
471        }
472    };
473
474    transforms
475        .into_iter()
476        .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
477        .collect()
478}
479
480fn transform_from_obj_fields<T: GeometryTrait>(
481    transform: KclObjectFields,
482    source_ranges: Vec<SourceRange>,
483    exec_state: &mut ExecState,
484) -> Result<Transform, KclError> {
485    // Apply defaults to the transform.
486    let replicate = match transform.get("replicate") {
487        Some(KclValue::Bool { value: true, .. }) => true,
488        Some(KclValue::Bool { value: false, .. }) => false,
489        Some(_) => {
490            return Err(KclError::Semantic(KclErrorDetails {
491                message: "The 'replicate' key must be a bool".to_string(),
492                source_ranges: source_ranges.clone(),
493            }));
494        }
495        None => true,
496    };
497
498    let scale = match transform.get("scale") {
499        Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
500        None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
501    };
502
503    let translate = match transform.get("translate") {
504        Some(x) => {
505            let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
506            kcmc::shared::Point3d::<LengthUnit> {
507                x: LengthUnit(arr[0]),
508                y: LengthUnit(arr[1]),
509                z: LengthUnit(arr[2]),
510            }
511        }
512        None => kcmc::shared::Point3d::<LengthUnit> {
513            x: LengthUnit(0.0),
514            y: LengthUnit(0.0),
515            z: LengthUnit(0.0),
516        },
517    };
518
519    let mut rotation = Rotation::default();
520    if let Some(rot) = transform.get("rotation") {
521        let KclValue::Object { value: rot, meta: _ } = rot else {
522            return Err(KclError::Semantic(KclErrorDetails {
523                message: "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')"
524                    .to_string(),
525                source_ranges: source_ranges.clone(),
526            }));
527        };
528        if let Some(axis) = rot.get("axis") {
529            rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
530        }
531        if let Some(angle) = rot.get("angle") {
532            match angle {
533                KclValue::Number { value: number, .. } => {
534                    rotation.angle = Angle::from_degrees(*number);
535                }
536                _ => {
537                    return Err(KclError::Semantic(KclErrorDetails {
538                        message: "The 'rotation.angle' key must be a number (of degrees)".to_string(),
539                        source_ranges: source_ranges.clone(),
540                    }));
541                }
542            }
543        }
544        if let Some(origin) = rot.get("origin") {
545            rotation.origin = match origin {
546                KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
547                KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
548                other => {
549                    let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges.clone(), exec_state)?).into();
550                    OriginType::Custom { origin }
551                }
552            };
553        }
554    }
555
556    Ok(Transform {
557        replicate,
558        scale,
559        translate,
560        rotation,
561    })
562}
563
564fn array_to_point3d(
565    val: &KclValue,
566    source_ranges: Vec<SourceRange>,
567    exec_state: &mut ExecState,
568) -> Result<[TyF64; 3], KclError> {
569    val.coerce(&RuntimeType::point3d(), exec_state)
570        .map_err(|e| {
571            KclError::Semantic(KclErrorDetails {
572                message: format!(
573                    "Expected an array of 3 numbers (i.e., a 3D point), found {}",
574                    e.found
575                        .map(|t| t.human_friendly_type())
576                        .unwrap_or_else(|| val.human_friendly_type().to_owned())
577                ),
578                source_ranges,
579            })
580        })
581        .map(|val| val.as_point3d().unwrap())
582}
583
584fn array_to_point2d(
585    val: &KclValue,
586    source_ranges: Vec<SourceRange>,
587    exec_state: &mut ExecState,
588) -> Result<[TyF64; 2], KclError> {
589    val.coerce(&RuntimeType::point2d(), exec_state)
590        .map_err(|e| {
591            KclError::Semantic(KclErrorDetails {
592                message: format!(
593                    "Expected an array of 2 numbers (i.e., a 2D point), found {}",
594                    e.found
595                        .map(|t| t.human_friendly_type())
596                        .unwrap_or_else(|| val.human_friendly_type().to_owned())
597                ),
598                source_ranges,
599            })
600        })
601        .map(|val| val.as_point2d().unwrap())
602}
603
604trait GeometryTrait: Clone {
605    type Set: Into<Vec<Self>> + Clone;
606    fn id(&self) -> Uuid;
607    fn original_id(&self) -> Uuid;
608    fn set_id(&mut self, id: Uuid);
609    fn array_to_point3d(
610        val: &KclValue,
611        source_ranges: Vec<SourceRange>,
612        exec_state: &mut ExecState,
613    ) -> Result<[TyF64; 3], KclError>;
614    async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
615}
616
617impl GeometryTrait for Sketch {
618    type Set = Vec<Sketch>;
619    fn set_id(&mut self, id: Uuid) {
620        self.id = id;
621    }
622    fn id(&self) -> Uuid {
623        self.id
624    }
625    fn original_id(&self) -> Uuid {
626        self.original_id
627    }
628    fn array_to_point3d(
629        val: &KclValue,
630        source_ranges: Vec<SourceRange>,
631        exec_state: &mut ExecState,
632    ) -> Result<[TyF64; 3], KclError> {
633        let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
634        let ty = x.ty.clone();
635        Ok([x, y, TyF64::new(0.0, ty)])
636    }
637
638    async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
639        Ok(())
640    }
641}
642
643impl GeometryTrait for Solid {
644    type Set = Vec<Solid>;
645    fn set_id(&mut self, id: Uuid) {
646        self.id = id;
647    }
648
649    fn id(&self) -> Uuid {
650        self.id
651    }
652
653    fn original_id(&self) -> Uuid {
654        self.sketch.original_id
655    }
656
657    fn array_to_point3d(
658        val: &KclValue,
659        source_ranges: Vec<SourceRange>,
660        exec_state: &mut ExecState,
661    ) -> Result<[TyF64; 3], KclError> {
662        array_to_point3d(val, source_ranges, exec_state)
663    }
664
665    async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
666        args.flush_batch_for_solids(exec_state, solid_set).await
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use crate::execution::types::NumericType;
674
675    #[tokio::test(flavor = "multi_thread")]
676    async fn test_array_to_point3d() {
677        let mut exec_state = ExecState::new(&ExecutorContext::new_mock().await);
678        let input = KclValue::MixedArray {
679            value: vec![
680                KclValue::Number {
681                    value: 1.1,
682                    meta: Default::default(),
683                    ty: NumericType::mm(),
684                },
685                KclValue::Number {
686                    value: 2.2,
687                    meta: Default::default(),
688                    ty: NumericType::mm(),
689                },
690                KclValue::Number {
691                    value: 3.3,
692                    meta: Default::default(),
693                    ty: NumericType::mm(),
694                },
695            ],
696            meta: Default::default(),
697        };
698        let expected = [
699            TyF64::new(1.1, NumericType::mm()),
700            TyF64::new(2.2, NumericType::mm()),
701            TyF64::new(3.3, NumericType::mm()),
702        ];
703        let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
704        assert_eq!(actual.unwrap(), expected);
705    }
706}
707
708/// A linear pattern on a 2D sketch.
709pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
710    let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
711    let instances: u32 = args.get_kw_arg("instances")?;
712    let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
713    let axis: [TyF64; 2] = args.get_kw_arg_typed("axis", &RuntimeType::point2d(), exec_state)?;
714    let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
715
716    if axis[0].n == 0.0 && axis[1].n == 0.0 {
717        return Err(KclError::Semantic(KclErrorDetails {
718            message:
719                "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
720                    .to_string(),
721            source_ranges: vec![args.source_range],
722        }));
723    }
724
725    let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
726    Ok(sketches.into())
727}
728
729/// Repeat a 2-dimensional sketch along some dimension, with a dynamic amount
730/// of distance between each repetition, some specified number of times.
731///
732/// ```no_run
733/// exampleSketch = startSketchOn(XZ)
734///   |> circle(center = [0, 0], radius = 1)
735///   |> patternLinear2d(
736///        axis = [1, 0],
737///        instances = 7,
738///        distance = 4
739///      )
740///
741/// example = extrude(exampleSketch, length = 1)
742/// ```
743#[stdlib {
744    name = "patternLinear2d",
745    keywords = true,
746    unlabeled_first = true,
747    args = {
748        sketches = { docs = "The sketch(es) to duplicate" },
749        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." },
750        distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
751        axis = { docs = "The axis of the pattern. A 2D vector." },
752        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." },
753    }
754}]
755async fn inner_pattern_linear_2d(
756    sketches: Vec<Sketch>,
757    instances: u32,
758    distance: TyF64,
759    axis: [TyF64; 2],
760    use_original: Option<bool>,
761    exec_state: &mut ExecState,
762    args: Args,
763) -> Result<Vec<Sketch>, KclError> {
764    let [x, y] = point_to_mm(axis);
765    let axis_len = f64::sqrt(x * x + y * y);
766    let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
767    let transforms: Vec<_> = (1..instances)
768        .map(|i| {
769            let d = distance.to_mm() * (i as f64);
770            let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
771            vec![Transform {
772                translate,
773                ..Default::default()
774            }]
775        })
776        .collect();
777    execute_pattern_transform(
778        transforms,
779        sketches,
780        use_original.unwrap_or_default(),
781        exec_state,
782        &args,
783    )
784    .await
785}
786
787/// A linear pattern on a 3D model.
788pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
789    let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
790    let instances: u32 = args.get_kw_arg("instances")?;
791    let distance: TyF64 = args.get_kw_arg_typed("distance", &RuntimeType::length(), exec_state)?;
792    let axis: [TyF64; 3] = args.get_kw_arg_typed("axis", &RuntimeType::point3d(), exec_state)?;
793    let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
794
795    if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
796        return Err(KclError::Semantic(KclErrorDetails {
797            message:
798                "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
799                    .to_string(),
800            source_ranges: vec![args.source_range],
801        }));
802    }
803
804    let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
805    Ok(solids.into())
806}
807
808/// Repeat a 3-dimensional solid along a linear path, with a dynamic amount
809/// of distance between each repetition, some specified number of times.
810///
811/// ```no_run
812/// exampleSketch = startSketchOn(XZ)
813///   |> startProfile(at = [0, 0])
814///   |> line(end = [0, 2])
815///   |> line(end = [3, 1])
816///   |> line(end = [0, -4])
817///   |> close()
818///
819/// example = extrude(exampleSketch, length = 1)
820///   |> patternLinear3d(
821///       axis = [1, 0, 1],
822///       instances = 7,
823///       distance = 6
824///     )
825/// ```
826///
827/// ///
828/// ```no_run
829/// // Pattern a whole sketch on face.
830/// size = 100
831/// case = startSketchOn(XY)
832///     |> startProfile(at = [-size, -size])
833///     |> line(end = [2 * size, 0])
834///     |> line(end = [0, 2 * size])
835///     |> tangentialArc(endAbsolute = [-size, size])
836///     |> close(%)
837///     |> extrude(length = 65)
838///
839/// thing1 = startSketchOn(case, face = END)
840///     |> circle(center = [-size / 2, -size / 2], radius = 25)
841///     |> extrude(length = 50)
842///
843/// thing2 = startSketchOn(case, face = END)
844///     |> circle(center = [size / 2, -size / 2], radius = 25)
845///     |> extrude(length = 50)
846///
847/// // We pass in the "case" here since we want to pattern the whole sketch.
848/// // And the case was the base of the sketch.
849/// patternLinear3d(case,
850///     axis= [1, 0, 0],
851///     distance= 250,
852///     instances=2,
853///  )
854/// ```
855///
856/// ```no_run
857/// // Pattern an object on a face.
858/// size = 100
859/// case = startSketchOn(XY)
860///     |> startProfile(at = [-size, -size])
861///     |> line(end = [2 * size, 0])
862///     |> line(end = [0, 2 * size])
863///     |> tangentialArc(endAbsolute = [-size, size])
864///     |> close(%)
865///     |> extrude(length = 65)
866///
867/// thing1 = startSketchOn(case, face = END)
868///     |> circle(center =[-size / 2, -size / 2], radius = 25)
869///     |> extrude(length = 50)
870///
871/// // We pass in `thing1` here with `useOriginal` since we want to pattern just this object on the face.
872/// patternLinear3d(thing1,
873///     axis = [1, 0, 0],
874///     distance = size,
875///     instances =2,
876///     useOriginal = true
877/// )
878/// ```
879#[stdlib {
880    name = "patternLinear3d",
881    feature_tree_operation = true,
882    keywords = true,
883    unlabeled_first = true,
884    args = {
885        solids = { docs = "The solid(s) to duplicate" },
886        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." },
887        distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
888        axis = { docs = "The axis of the pattern. A 2D vector." },
889        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." },
890    },
891    tags = ["solid"]
892}]
893async fn inner_pattern_linear_3d(
894    solids: Vec<Solid>,
895    instances: u32,
896    distance: TyF64,
897    axis: [TyF64; 3],
898    use_original: Option<bool>,
899    exec_state: &mut ExecState,
900    args: Args,
901) -> Result<Vec<Solid>, KclError> {
902    let [x, y, z] = point_3d_to_mm(axis);
903    let axis_len = f64::sqrt(x * x + y * y + z * z);
904    let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
905    let transforms: Vec<_> = (1..instances)
906        .map(|i| {
907            let d = distance.to_mm() * (i as f64);
908            let translate = (normalized_axis * d).map(LengthUnit);
909            vec![Transform {
910                translate,
911                ..Default::default()
912            }]
913        })
914        .collect();
915    execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
916}
917
918/// Data for a circular pattern on a 2D sketch.
919#[derive(Debug, Clone, Serialize, PartialEq)]
920#[serde(rename_all = "camelCase")]
921struct CircularPattern2dData {
922    /// The number of total instances. Must be greater than or equal to 1.
923    /// This includes the original entity. For example, if instances is 2,
924    /// there will be two copies -- the original, and one new copy.
925    /// If instances is 1, this has no effect.
926    pub instances: u32,
927    /// The center about which to make the pattern. This is a 2D vector.
928    pub center: [TyF64; 2],
929    /// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
930    pub arc_degrees: f64,
931    /// Whether or not to rotate the duplicates as they are copied.
932    pub rotate_duplicates: bool,
933    /// If the target being patterned is itself a pattern, then, should you use the original solid,
934    /// or the pattern?
935    #[serde(default)]
936    pub use_original: Option<bool>,
937}
938
939/// Data for a circular pattern on a 3D model.
940#[derive(Debug, Clone, Serialize, PartialEq)]
941#[serde(rename_all = "camelCase")]
942struct CircularPattern3dData {
943    /// The number of total instances. Must be greater than or equal to 1.
944    /// This includes the original entity. For example, if instances is 2,
945    /// there will be two copies -- the original, and one new copy.
946    /// If instances is 1, this has no effect.
947    pub instances: u32,
948    /// The axis around which to make the pattern. This is a 3D vector.
949    // Only the direction should matter, not the magnitude so don't adjust units to avoid normalisation issues.
950    pub axis: [f64; 3],
951    /// The center about which to make the pattern. This is a 3D vector.
952    pub center: [TyF64; 3],
953    /// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
954    pub arc_degrees: f64,
955    /// Whether or not to rotate the duplicates as they are copied.
956    pub rotate_duplicates: bool,
957    /// If the target being patterned is itself a pattern, then, should you use the original solid,
958    /// or the pattern?
959    #[serde(default)]
960    pub use_original: Option<bool>,
961}
962
963#[allow(clippy::large_enum_variant)]
964enum CircularPattern {
965    ThreeD(CircularPattern3dData),
966    TwoD(CircularPattern2dData),
967}
968
969enum RepetitionsNeeded {
970    /// Add this number of repetitions
971    More(u32),
972    /// No repetitions needed
973    None,
974    /// Invalid number of total instances.
975    Invalid,
976}
977
978impl From<u32> for RepetitionsNeeded {
979    fn from(n: u32) -> Self {
980        match n.cmp(&1) {
981            Ordering::Less => Self::Invalid,
982            Ordering::Equal => Self::None,
983            Ordering::Greater => Self::More(n - 1),
984        }
985    }
986}
987
988impl CircularPattern {
989    pub fn axis(&self) -> [f64; 3] {
990        match self {
991            CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
992            CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
993        }
994    }
995
996    pub fn center_mm(&self) -> [f64; 3] {
997        match self {
998            CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
999            CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
1000        }
1001    }
1002
1003    fn repetitions(&self) -> RepetitionsNeeded {
1004        let n = match self {
1005            CircularPattern::TwoD(lp) => lp.instances,
1006            CircularPattern::ThreeD(lp) => lp.instances,
1007        };
1008        RepetitionsNeeded::from(n)
1009    }
1010
1011    pub fn arc_degrees(&self) -> f64 {
1012        match self {
1013            CircularPattern::TwoD(lp) => lp.arc_degrees,
1014            CircularPattern::ThreeD(lp) => lp.arc_degrees,
1015        }
1016    }
1017
1018    pub fn rotate_duplicates(&self) -> bool {
1019        match self {
1020            CircularPattern::TwoD(lp) => lp.rotate_duplicates,
1021            CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
1022        }
1023    }
1024
1025    pub fn use_original(&self) -> bool {
1026        match self {
1027            CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
1028            CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
1029        }
1030    }
1031}
1032
1033/// A circular pattern on a 2D sketch.
1034pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1035    let sketches = args.get_unlabeled_kw_arg_typed("sketches", &RuntimeType::sketches(), exec_state)?;
1036    let instances: u32 = args.get_kw_arg("instances")?;
1037    let center: [TyF64; 2] = args.get_kw_arg_typed("center", &RuntimeType::point2d(), exec_state)?;
1038    let arc_degrees: TyF64 = args.get_kw_arg_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1039    let rotate_duplicates: bool = args.get_kw_arg("rotateDuplicates")?;
1040    let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1041
1042    let sketches = inner_pattern_circular_2d(
1043        sketches,
1044        instances,
1045        center,
1046        arc_degrees.n,
1047        rotate_duplicates,
1048        use_original,
1049        exec_state,
1050        args,
1051    )
1052    .await?;
1053    Ok(sketches.into())
1054}
1055
1056/// Repeat a 2-dimensional sketch some number of times along a partial or
1057/// complete circle some specified number of times. Each object may
1058/// additionally be rotated along the circle, ensuring orientation of the
1059/// solid with respect to the center of the circle is maintained.
1060///
1061/// ```no_run
1062/// exampleSketch = startSketchOn(XZ)
1063///   |> startProfile(at = [.5, 25])
1064///   |> line(end = [0, 5])
1065///   |> line(end = [-1, 0])
1066///   |> line(end = [0, -5])
1067///   |> close()
1068///   |> patternCircular2d(
1069///        center = [0, 0],
1070///        instances = 13,
1071///        arcDegrees = 360,
1072///        rotateDuplicates = true
1073///      )
1074///
1075/// example = extrude(exampleSketch, length = 1)
1076/// ```
1077#[stdlib {
1078    name = "patternCircular2d",
1079    keywords = true,
1080    unlabeled_first = true,
1081    args = {
1082        sketch_set = { docs = "Which sketch(es) to pattern" },
1083        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."},
1084        center = { docs = "The center about which to make the pattern. This is a 2D vector."},
1085        arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0."},
1086        rotate_duplicates= { docs = "Whether or not to rotate the duplicates as they are copied."},
1087        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."},
1088    },
1089    tags = ["sketch"]
1090}]
1091#[allow(clippy::too_many_arguments)]
1092async fn inner_pattern_circular_2d(
1093    sketch_set: Vec<Sketch>,
1094    instances: u32,
1095    center: [TyF64; 2],
1096    arc_degrees: f64,
1097    rotate_duplicates: bool,
1098    use_original: Option<bool>,
1099    exec_state: &mut ExecState,
1100    args: Args,
1101) -> Result<Vec<Sketch>, KclError> {
1102    let starting_sketches = sketch_set;
1103
1104    if args.ctx.context_type == crate::execution::ContextType::Mock {
1105        return Ok(starting_sketches);
1106    }
1107    let data = CircularPattern2dData {
1108        instances,
1109        center,
1110        arc_degrees,
1111        rotate_duplicates,
1112        use_original,
1113    };
1114
1115    let mut sketches = Vec::new();
1116    for sketch in starting_sketches.iter() {
1117        let geometries = pattern_circular(
1118            CircularPattern::TwoD(data.clone()),
1119            Geometry::Sketch(sketch.clone()),
1120            exec_state,
1121            args.clone(),
1122        )
1123        .await?;
1124
1125        let Geometries::Sketches(new_sketches) = geometries else {
1126            return Err(KclError::Semantic(KclErrorDetails {
1127                message: "Expected a vec of sketches".to_string(),
1128                source_ranges: vec![args.source_range],
1129            }));
1130        };
1131
1132        sketches.extend(new_sketches);
1133    }
1134
1135    Ok(sketches)
1136}
1137
1138/// A circular pattern on a 3D model.
1139pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1140    let solids = args.get_unlabeled_kw_arg_typed("solids", &RuntimeType::solids(), exec_state)?;
1141    // The number of total instances. Must be greater than or equal to 1.
1142    // This includes the original entity. For example, if instances is 2,
1143    // there will be two copies -- the original, and one new copy.
1144    // If instances is 1, this has no effect.
1145    let instances: u32 = args.get_kw_arg_typed("instances", &RuntimeType::count(), exec_state)?;
1146    // The axis around which to make the pattern. This is a 3D vector.
1147    let axis: [TyF64; 3] = args.get_kw_arg_typed("axis", &RuntimeType::point3d(), exec_state)?;
1148    // The center about which to make the pattern. This is a 3D vector.
1149    let center: [TyF64; 3] = args.get_kw_arg_typed("center", &RuntimeType::point3d(), exec_state)?;
1150    // The arc angle (in degrees) to place the repetitions. Must be greater than 0.
1151    let arc_degrees: TyF64 = args.get_kw_arg_typed("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1152    // Whether or not to rotate the duplicates as they are copied.
1153    let rotate_duplicates: bool = args.get_kw_arg("rotateDuplicates")?;
1154    // If the target being patterned is itself a pattern, then, should you use the original solid,
1155    // or the pattern?
1156    let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
1157
1158    let solids = inner_pattern_circular_3d(
1159        solids,
1160        instances,
1161        [axis[0].n, axis[1].n, axis[2].n],
1162        center,
1163        arc_degrees.n,
1164        rotate_duplicates,
1165        use_original,
1166        exec_state,
1167        args,
1168    )
1169    .await?;
1170    Ok(solids.into())
1171}
1172
1173/// Repeat a 3-dimensional solid some number of times along a partial or
1174/// complete circle some specified number of times. Each object may
1175/// additionally be rotated along the circle, ensuring orientation of the
1176/// solid with respect to the center of the circle is maintained.
1177///
1178/// ```no_run
1179/// exampleSketch = startSketchOn(XZ)
1180///   |> circle(center = [0, 0], radius = 1)
1181///
1182/// example = extrude(exampleSketch, length = -5)
1183///   |> patternCircular3d(
1184///        axis = [1, -1, 0],
1185///        center = [10, -20, 0],
1186///        instances = 11,
1187///        arcDegrees = 360,
1188///        rotateDuplicates = true
1189///      )
1190/// ```
1191#[stdlib {
1192    name = "patternCircular3d",
1193    feature_tree_operation = true,
1194    keywords = true,
1195    unlabeled_first = true,
1196    args = {
1197        solids = { docs = "Which solid(s) to pattern" },
1198        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."},
1199        axis = { docs = "The axis around which to make the pattern. This is a 3D vector"},
1200        center = { docs = "The center about which to make the pattern. This is a 3D vector."},
1201        arc_degrees = { docs = "The arc angle (in degrees) to place the repetitions. Must be greater than 0."},
1202        rotate_duplicates = { docs = "Whether or not to rotate the duplicates as they are copied."},
1203        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."},
1204    },
1205    tags = ["solid"]
1206}]
1207#[allow(clippy::too_many_arguments)]
1208async fn inner_pattern_circular_3d(
1209    solids: Vec<Solid>,
1210    instances: u32,
1211    axis: [f64; 3],
1212    center: [TyF64; 3],
1213    arc_degrees: f64,
1214    rotate_duplicates: bool,
1215    use_original: Option<bool>,
1216    exec_state: &mut ExecState,
1217    args: Args,
1218) -> Result<Vec<Solid>, KclError> {
1219    // Flush the batch for our fillets/chamfers if there are any.
1220    // If we do not flush these, then you won't be able to pattern something with fillets.
1221    // Flush just the fillets/chamfers that apply to these solids.
1222    args.flush_batch_for_solids(exec_state, &solids).await?;
1223
1224    let starting_solids = solids;
1225
1226    if args.ctx.context_type == crate::execution::ContextType::Mock {
1227        return Ok(starting_solids);
1228    }
1229
1230    let mut solids = Vec::new();
1231    let data = CircularPattern3dData {
1232        instances,
1233        axis,
1234        center,
1235        arc_degrees,
1236        rotate_duplicates,
1237        use_original,
1238    };
1239    for solid in starting_solids.iter() {
1240        let geometries = pattern_circular(
1241            CircularPattern::ThreeD(data.clone()),
1242            Geometry::Solid(solid.clone()),
1243            exec_state,
1244            args.clone(),
1245        )
1246        .await?;
1247
1248        let Geometries::Solids(new_solids) = geometries else {
1249            return Err(KclError::Semantic(KclErrorDetails {
1250                message: "Expected a vec of solids".to_string(),
1251                source_ranges: vec![args.source_range],
1252            }));
1253        };
1254
1255        solids.extend(new_solids);
1256    }
1257
1258    Ok(solids)
1259}
1260
1261async fn pattern_circular(
1262    data: CircularPattern,
1263    geometry: Geometry,
1264    exec_state: &mut ExecState,
1265    args: Args,
1266) -> Result<Geometries, KclError> {
1267    let id = exec_state.next_uuid();
1268    let num_repetitions = match data.repetitions() {
1269        RepetitionsNeeded::More(n) => n,
1270        RepetitionsNeeded::None => {
1271            return Ok(Geometries::from(geometry));
1272        }
1273        RepetitionsNeeded::Invalid => {
1274            return Err(KclError::Semantic(KclErrorDetails {
1275                source_ranges: vec![args.source_range],
1276                message: MUST_HAVE_ONE_INSTANCE.to_owned(),
1277            }));
1278        }
1279    };
1280
1281    let center = data.center_mm();
1282    let resp = args
1283        .send_modeling_cmd(
1284            id,
1285            ModelingCmd::from(mcmd::EntityCircularPattern {
1286                axis: kcmc::shared::Point3d::from(data.axis()),
1287                entity_id: if data.use_original() {
1288                    geometry.original_id()
1289                } else {
1290                    geometry.id()
1291                },
1292                center: kcmc::shared::Point3d {
1293                    x: LengthUnit(center[0]),
1294                    y: LengthUnit(center[1]),
1295                    z: LengthUnit(center[2]),
1296                },
1297                num_repetitions,
1298                arc_degrees: data.arc_degrees(),
1299                rotate_duplicates: data.rotate_duplicates(),
1300            }),
1301        )
1302        .await?;
1303
1304    // The common case is borrowing from the response.  Instead of cloning,
1305    // create a Vec to borrow from in mock mode.
1306    let mut mock_ids = Vec::new();
1307    let entity_ids = if let OkWebSocketResponseData::Modeling {
1308        modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1309    } = &resp
1310    {
1311        if !pattern_info.entity_ids.is_empty() {
1312            &pattern_info.entity_ids.clone()
1313        } else {
1314            &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1315        }
1316    } else if args.ctx.no_engine_commands().await {
1317        mock_ids.reserve(num_repetitions as usize);
1318        for _ in 0..num_repetitions {
1319            mock_ids.push(exec_state.next_uuid());
1320        }
1321        &mock_ids
1322    } else {
1323        return Err(KclError::Engine(KclErrorDetails {
1324            message: format!("EntityCircularPattern response was not as expected: {:?}", resp),
1325            source_ranges: vec![args.source_range],
1326        }));
1327    };
1328
1329    let geometries = match geometry {
1330        Geometry::Sketch(sketch) => {
1331            let mut geometries = vec![sketch.clone()];
1332            for id in entity_ids.iter().copied() {
1333                let mut new_sketch = sketch.clone();
1334                new_sketch.id = id;
1335                geometries.push(new_sketch);
1336            }
1337            Geometries::Sketches(geometries)
1338        }
1339        Geometry::Solid(solid) => {
1340            let mut geometries = vec![solid.clone()];
1341            for id in entity_ids.iter().copied() {
1342                let mut new_solid = solid.clone();
1343                new_solid.id = id;
1344                geometries.push(new_solid);
1345            }
1346            Geometries::Solids(geometries)
1347        }
1348    };
1349
1350    Ok(geometries)
1351}