kcl_lib/std/
patterns.rs

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