Skip to main content

kcl_lib/std/
patterns.rs

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