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