kcl_lib/std/
patterns.rs

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