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