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