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_engine_graphics(true, Some(current_file))
687            .await
688            .unwrap();
689        let program = crate::Program::parse_no_errs(code).unwrap();
690        let result = ctx.run_with_caching(program).await.unwrap();
691
692        let KclValueView::HomArray { value } = result.variables.get("patterned").unwrap() else {
693            panic!("Expected the imported geometry pattern to return an array");
694        };
695        assert_eq!(value.len(), 3);
696        assert!(
697            value
698                .iter()
699                .all(|value| matches!(value, KclValueView::ImportedGeometry(_)))
700        );
701        let ids = value
702            .iter()
703            .map(|value| match value {
704                KclValueView::ImportedGeometry(geometry) => geometry.id,
705                _ => unreachable!(),
706            })
707            .collect::<std::collections::HashSet<_>>();
708        assert_eq!(ids.len(), 3);
709
710        ctx.close().await;
711    }
712
713    #[tokio::test(flavor = "multi_thread")]
714    async fn imported_geometry_pattern_linear_3d() {
715        assert_imported_pattern_executes(
716            r#"import "cube.step" as cube
717
718patterned = patternLinear3d(cube, instances = 3, distance = 20, axis = X)
719"#,
720        )
721        .await;
722    }
723
724    #[tokio::test(flavor = "multi_thread")]
725    async fn imported_geometry_pattern_circular_3d() {
726        assert_imported_pattern_executes(
727            r#"import "cube.step" as cube
728
729patterned = patternCircular3d(cube, instances = 3, axis = Z, center = [20, 0, 0])
730"#,
731        )
732        .await;
733    }
734
735    #[tokio::test(flavor = "multi_thread")]
736    async fn imported_geometry_pattern_transform() {
737        assert_imported_pattern_executes(
738            r#"import "cube.step" as cube
739
740fn shift(@i) {
741  return { translate = [20 * i, 0, 0] }
742}
743
744patterned = patternTransform(cube, instances = 3, transform = shift)
745"#,
746        )
747        .await;
748    }
749
750    #[tokio::test(flavor = "multi_thread")]
751    async fn test_array_to_point3d() {
752        let ctx = ExecutorContext::new_mock(None).await;
753        let mut exec_state = ExecState::new(&ctx);
754        let input = KclValue::HomArray {
755            value: vec![
756                KclValue::Number {
757                    value: 1.1,
758                    meta: Default::default(),
759                    ty: NumericType::mm(),
760                },
761                KclValue::Number {
762                    value: 2.2,
763                    meta: Default::default(),
764                    ty: NumericType::mm(),
765                },
766                KclValue::Number {
767                    value: 3.3,
768                    meta: Default::default(),
769                    ty: NumericType::mm(),
770                },
771            ],
772            ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
773        };
774        let expected = [
775            TyF64::new(1.1, NumericType::mm()),
776            TyF64::new(2.2, NumericType::mm()),
777            TyF64::new(3.3, NumericType::mm()),
778        ];
779        let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
780        assert_eq!(actual.unwrap(), expected);
781        ctx.close().await;
782    }
783
784    #[tokio::test(flavor = "multi_thread")]
785    async fn test_tuple_to_point3d() {
786        let ctx = ExecutorContext::new_mock(None).await;
787        let mut exec_state = ExecState::new(&ctx);
788        let input = KclValue::Tuple {
789            value: vec![
790                KclValue::Number {
791                    value: 1.1,
792                    meta: Default::default(),
793                    ty: NumericType::mm(),
794                },
795                KclValue::Number {
796                    value: 2.2,
797                    meta: Default::default(),
798                    ty: NumericType::mm(),
799                },
800                KclValue::Number {
801                    value: 3.3,
802                    meta: Default::default(),
803                    ty: NumericType::mm(),
804                },
805            ],
806            meta: Default::default(),
807        };
808        let expected = [
809            TyF64::new(1.1, NumericType::mm()),
810            TyF64::new(2.2, NumericType::mm()),
811            TyF64::new(3.3, NumericType::mm()),
812        ];
813        let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
814        assert_eq!(actual.unwrap(), expected);
815        ctx.close().await;
816    }
817
818    fn pattern_linear_2d_code(kcl_version: &str, input: &str) -> String {
819        format!(
820            r#"@settings(kclVersion = {kcl_version}, defaultLengthUnit = mm, experimentalFeatures = allow)
821
822profile = sketch(on = XY) {{
823  circle1 = circle(start = [var 10mm, var 0mm], center = [var 0mm, var 0mm])
824}}
825patterned = patternLinear2d({input}, instances = 2, distance = 20mm, axis = X)
826"#
827        )
828    }
829
830    async fn run_mock(code: &str) -> Result<crate::ExecOutcome, crate::KclErrorWithOutputs> {
831        let program = crate::Program::parse_no_errs(code).unwrap();
832        let ctx = ExecutorContext::new_mock(None).await;
833        let result = ctx.run_mock(&program, &crate::execution::MockConfig::default()).await;
834        ctx.close().await;
835        result
836    }
837
838    #[tokio::test(flavor = "multi_thread")]
839    async fn pattern_linear_2d_with_sketch_warns_before_v3() {
840        for version in ["1.0", "2.0"] {
841            let code = pattern_linear_2d_code(version, "profile");
842            let outcome = run_mock(&code).await.unwrap();
843            let warnings = outcome
844                .issues
845                .iter()
846                .filter(|issue| issue.message == PATTERN_LINEAR_2D_REGIONS_ONLY)
847                .collect::<Vec<_>>();
848
849            assert_eq!(
850                warnings.len(),
851                1,
852                "unexpected issues for KCL {version}: {:#?}",
853                outcome.issues
854            );
855            assert_eq!(warnings[0].severity, Severity::Warning);
856        }
857    }
858
859    #[tokio::test(flavor = "multi_thread")]
860    async fn pattern_linear_2d_with_sketch_is_an_error_in_v3() {
861        let code = pattern_linear_2d_code(r#""3.0-preview""#, "profile");
862        let error = run_mock(&code).await.unwrap_err();
863
864        assert!(matches!(error.error, KclError::Semantic { .. }));
865        assert_eq!(error.error.message(), PATTERN_LINEAR_2D_REGIONS_ONLY);
866    }
867
868    #[tokio::test(flavor = "multi_thread")]
869    async fn pattern_linear_2d_with_region_is_allowed_in_v3() {
870        let code = pattern_linear_2d_code(r#""3.0-preview""#, "region(segments = [profile.circle1])");
871        let outcome = run_mock(&code).await.unwrap();
872
873        assert!(
874            outcome
875                .issues
876                .iter()
877                .all(|issue| issue.message != PATTERN_LINEAR_2D_REGIONS_ONLY),
878            "unexpected regions-only issue: {:#?}",
879            outcome.issues
880        );
881    }
882
883    #[tokio::test(flavor = "multi_thread")]
884    async fn pattern_linear_2d_with_sketch_v1_profile_is_allowed() {
885        let code = r#"@settings(kclVersion = 2.0, defaultLengthUnit = mm, experimentalFeatures = allow)
886
887patterned = startSketchOn(XY)
888  |> rectangle(width = 4mm, height = 3mm, center = [0mm, 0mm])
889  |> patternLinear2d(instances = 10, distance = 10mm, axis = [1, 0])
890"#;
891        let outcome = run_mock(code).await.unwrap();
892
893        assert!(
894            outcome
895                .issues
896                .iter()
897                .all(|issue| issue.message != PATTERN_LINEAR_2D_REGIONS_ONLY),
898            "unexpected regions-only issue: {:#?}",
899            outcome.issues
900        );
901    }
902}
903
904/// A linear pattern on a 2D sketch.
905pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
906    let sketches_source_range = args
907        .unlabeled_kw_arg_unconverted()
908        .map_or(args.source_range, |arg| arg.source_range);
909    let sketches: Vec<Sketch> = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
910    let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
911    let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
912    let axis: Axis2dOrPoint2d = args.get_kw_arg(
913        "axis",
914        &RuntimeType::Union(vec![
915            RuntimeType::Primitive(PrimitiveType::Axis2d),
916            RuntimeType::point2d(),
917        ]),
918        exec_state,
919    )?;
920    let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
921
922    let has_sketch_solver_block = sketches.iter().any(|sketch| {
923        sketch.origin_sketch_id.is_none()
924            && (exec_state.is_sketch_block_path(sketch.artifact_id)
925                || exec_state.is_sketch_block_path(sketch.original_id.into()))
926    });
927    if has_sketch_solver_block {
928        if exec_state.kcl_version() >= KclVersion::V3Preview {
929            return Err(KclError::new_semantic(KclErrorDetails::new(
930                PATTERN_LINEAR_2D_REGIONS_ONLY.to_owned(),
931                vec![sketches_source_range],
932            )));
933        }
934
935        exec_state.warn(
936            CompilationIssue {
937                source_range: sketches_source_range,
938                message: PATTERN_LINEAR_2D_REGIONS_ONLY.to_owned(),
939                suggestion: None,
940                severity: Severity::Warning,
941                tag: Tag::Deprecated,
942            },
943            annotations::WARN_DEPRECATED,
944        );
945    }
946
947    let axis = axis.to_point2d();
948    if axis[0].n == 0.0 && axis[1].n == 0.0 {
949        return Err(KclError::new_semantic(KclErrorDetails::new(
950            "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
951                .to_owned(),
952            vec![args.source_range],
953        )));
954    }
955
956    let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
957    Ok(sketches.into())
958}
959
960async fn inner_pattern_linear_2d(
961    sketches: Vec<Sketch>,
962    instances: u32,
963    distance: TyF64,
964    axis: [TyF64; 2],
965    use_original: Option<bool>,
966    exec_state: &mut ExecState,
967    args: Args,
968) -> Result<Vec<Sketch>, KclError> {
969    let [x, y] = point_to_mm(axis);
970    let axis_len = f64::sqrt(x * x + y * y);
971    let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
972    let transforms: Vec<_> = (1..instances)
973        .map(|i| {
974            let d = distance.to_mm() * (i as f64);
975            let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
976            vec![Transform::builder().translate(translate).build()]
977        })
978        .collect();
979    execute_pattern_transform(
980        transforms,
981        sketches,
982        use_original.unwrap_or_default(),
983        exec_state,
984        &args,
985    )
986    .await
987}
988
989/// A linear pattern on a 3D model.
990pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
991    let geometry: SolidOrImportedGeometry =
992        args.get_unlabeled_kw_arg("solids", &pattern_geometry_3d_type(), exec_state)?;
993    let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
994    let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
995    let axis: Axis3dOrPoint3d = args.get_kw_arg(
996        "axis",
997        &RuntimeType::Union(vec![
998            RuntimeType::Primitive(PrimitiveType::Axis3d),
999            RuntimeType::point3d(),
1000        ]),
1001        exec_state,
1002    )?;
1003    let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
1004
1005    let axis = axis.to_point3d();
1006    if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
1007        return Err(KclError::new_semantic(KclErrorDetails::new(
1008            "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
1009                .to_owned(),
1010            vec![args.source_range],
1011        )));
1012    }
1013
1014    match geometry {
1015        SolidOrImportedGeometry::SolidSet(solids) => {
1016            Ok(
1017                inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args)
1018                    .await?
1019                    .into(),
1020            )
1021        }
1022        SolidOrImportedGeometry::ImportedGeometry(geometry) => Ok(KclValue::from_imported_geometries(
1023            inner_pattern_linear_3d(
1024                vec![*geometry],
1025                instances,
1026                distance,
1027                axis,
1028                use_original,
1029                exec_state,
1030                args,
1031            )
1032            .await?,
1033        )),
1034    }
1035}
1036
1037async fn inner_pattern_linear_3d<T: GeometryTrait<Set = Vec<T>>>(
1038    geometry: Vec<T>,
1039    instances: u32,
1040    distance: TyF64,
1041    axis: [TyF64; 3],
1042    use_original: Option<bool>,
1043    exec_state: &mut ExecState,
1044    args: Args,
1045) -> Result<Vec<T>, KclError> {
1046    let [x, y, z] = point_3d_to_mm(axis);
1047    let axis_len = f64::sqrt(x * x + y * y + z * z);
1048    let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
1049    let transforms: Vec<_> = (1..instances)
1050        .map(|i| {
1051            let d = distance.to_mm() * (i as f64);
1052            let translate = (normalized_axis * d).map(LengthUnit);
1053            vec![Transform::builder().translate(translate).build()]
1054        })
1055        .collect();
1056    execute_pattern_transform(
1057        transforms,
1058        geometry,
1059        use_original.unwrap_or_default(),
1060        exec_state,
1061        &args,
1062    )
1063    .await
1064}
1065
1066/// Data for a circular pattern on a 2D sketch.
1067#[derive(Debug, Clone, Serialize, PartialEq)]
1068#[serde(rename_all = "camelCase")]
1069struct CircularPattern2dData {
1070    /// The number of total instances. Must be greater than or equal to 1.
1071    /// This includes the original entity. For example, if instances is 2,
1072    /// there will be two copies -- the original, and one new copy.
1073    /// If instances is 1, this has no effect.
1074    pub instances: u32,
1075    /// The center about which to make the pattern. This is a 2D vector.
1076    pub center: [TyF64; 2],
1077    /// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
1078    pub arc_degrees: Option<f64>,
1079    /// Whether or not to rotate the duplicates as they are copied.
1080    pub rotate_duplicates: Option<bool>,
1081    /// If the target being patterned is itself a pattern, then, should you use the original solid,
1082    /// or the pattern?
1083    #[serde(default)]
1084    pub use_original: Option<bool>,
1085}
1086
1087/// Data for a circular pattern on a 3D model.
1088#[derive(Debug, Clone, Serialize, PartialEq)]
1089#[serde(rename_all = "camelCase")]
1090struct CircularPattern3dData {
1091    /// The number of total instances. Must be greater than or equal to 1.
1092    /// This includes the original entity. For example, if instances is 2,
1093    /// there will be two copies -- the original, and one new copy.
1094    /// If instances is 1, this has no effect.
1095    pub instances: u32,
1096    /// The axis around which to make the pattern. This is a 3D vector.
1097    // Only the direction should matter, not the magnitude so don't adjust units to avoid normalisation issues.
1098    pub axis: [f64; 3],
1099    /// The center about which to make the pattern. This is a 3D vector.
1100    pub center: [TyF64; 3],
1101    /// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
1102    pub arc_degrees: Option<f64>,
1103    /// Whether or not to rotate the duplicates as they are copied.
1104    pub rotate_duplicates: Option<bool>,
1105    /// If the target being patterned is itself a pattern, then, should you use the original solid,
1106    /// or the pattern?
1107    #[serde(default)]
1108    pub use_original: Option<bool>,
1109}
1110
1111#[allow(clippy::large_enum_variant)]
1112#[derive(Clone)]
1113enum CircularPattern {
1114    ThreeD(CircularPattern3dData),
1115    TwoD(CircularPattern2dData),
1116}
1117
1118enum RepetitionsNeeded {
1119    /// Add this number of repetitions
1120    More(u32),
1121    /// No repetitions needed
1122    None,
1123    /// Invalid number of total instances.
1124    Invalid,
1125}
1126
1127impl From<u32> for RepetitionsNeeded {
1128    fn from(n: u32) -> Self {
1129        match n.cmp(&1) {
1130            Ordering::Less => Self::Invalid,
1131            Ordering::Equal => Self::None,
1132            Ordering::Greater => Self::More(n - 1),
1133        }
1134    }
1135}
1136
1137impl CircularPattern {
1138    pub fn axis(&self) -> [f64; 3] {
1139        match self {
1140            CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
1141            CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
1142        }
1143    }
1144
1145    pub fn center_mm(&self) -> [f64; 3] {
1146        match self {
1147            CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
1148            CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
1149        }
1150    }
1151
1152    fn repetitions(&self) -> RepetitionsNeeded {
1153        let n = match self {
1154            CircularPattern::TwoD(lp) => lp.instances,
1155            CircularPattern::ThreeD(lp) => lp.instances,
1156        };
1157        RepetitionsNeeded::from(n)
1158    }
1159
1160    pub fn arc_degrees(&self) -> Option<f64> {
1161        match self {
1162            CircularPattern::TwoD(lp) => lp.arc_degrees,
1163            CircularPattern::ThreeD(lp) => lp.arc_degrees,
1164        }
1165    }
1166
1167    pub fn rotate_duplicates(&self) -> Option<bool> {
1168        match self {
1169            CircularPattern::TwoD(lp) => lp.rotate_duplicates,
1170            CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
1171        }
1172    }
1173
1174    pub fn use_original(&self) -> bool {
1175        match self {
1176            CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
1177            CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
1178        }
1179    }
1180}
1181
1182/// A circular pattern on a 2D sketch.
1183pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1184    let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
1185    let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
1186    let center: Option<[TyF64; 2]> = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
1187    let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1188    let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
1189    let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
1190
1191    let sketches = inner_pattern_circular_2d(
1192        sketches,
1193        instances,
1194        center,
1195        arc_degrees.map(|x| x.n),
1196        rotate_duplicates,
1197        use_original,
1198        exec_state,
1199        args,
1200    )
1201    .await?;
1202    Ok(sketches.into())
1203}
1204
1205#[allow(clippy::too_many_arguments)]
1206async fn inner_pattern_circular_2d(
1207    sketch_set: Vec<Sketch>,
1208    instances: u32,
1209    center: Option<[TyF64; 2]>,
1210    arc_degrees: Option<f64>,
1211    rotate_duplicates: Option<bool>,
1212    use_original: Option<bool>,
1213    exec_state: &mut ExecState,
1214    args: Args,
1215) -> Result<Vec<Sketch>, KclError> {
1216    let starting_sketches = sketch_set;
1217
1218    if args.ctx.context_type == crate::execution::ContextType::Mock {
1219        return Ok(starting_sketches);
1220    }
1221    let center = center.unwrap_or(POINT_ZERO_ZERO);
1222    let data = CircularPattern2dData {
1223        instances,
1224        center,
1225        arc_degrees,
1226        rotate_duplicates,
1227        use_original,
1228    };
1229
1230    let mut sketches = Vec::new();
1231    for sketch in starting_sketches.iter() {
1232        let geometries =
1233            pattern_circular_sketch(data.clone(), Geometry::Sketch(sketch.clone()), exec_state, args.clone()).await?;
1234
1235        let Geometries::Sketches(new_sketches) = geometries else {
1236            return Err(KclError::new_semantic(KclErrorDetails::new(
1237                "Expected a vec of sketches".to_string(),
1238                vec![args.source_range],
1239            )));
1240        };
1241
1242        sketches.extend(new_sketches);
1243    }
1244
1245    Ok(sketches)
1246}
1247
1248async fn pattern_circular_sketch(
1249    data: CircularPattern2dData,
1250    geometry: Geometry,
1251    exec_state: &mut ExecState,
1252    args: Args,
1253) -> Result<Geometries, KclError> {
1254    let Geometry::Sketch(mut sketch) = geometry else {
1255        return Err(KclError::new_internal(KclErrorDetails::new(
1256            "A 2D circular pattern requires a sketch".to_owned(),
1257            vec![args.source_range],
1258        )));
1259    };
1260    let geometries = pattern_circular(&CircularPattern::TwoD(data), &mut sketch, exec_state, &args).await?;
1261    Ok(Geometries::Sketches(geometries))
1262}
1263
1264/// A circular pattern on a 3D model.
1265pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1266    let geometry: SolidOrImportedGeometry =
1267        args.get_unlabeled_kw_arg("solids", &pattern_geometry_3d_type(), exec_state)?;
1268    // The number of total instances. Must be greater than or equal to 1.
1269    // This includes the original entity. For example, if instances is 2,
1270    // there will be two copies -- the original, and one new copy.
1271    // If instances is 1, this has no effect.
1272    let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
1273    // The axis around which to make the pattern. This is a 3D vector.
1274    let axis: Axis3dOrPoint3d = args.get_kw_arg(
1275        "axis",
1276        &RuntimeType::Union(vec![
1277            RuntimeType::Primitive(PrimitiveType::Axis3d),
1278            RuntimeType::point3d(),
1279        ]),
1280        exec_state,
1281    )?;
1282    let axis = axis.to_point3d();
1283
1284    // The center about which to make the pattern. This is a 3D vector.
1285    let center: Option<[TyF64; 3]> = args.get_kw_arg_opt("center", &RuntimeType::point3d(), exec_state)?;
1286    // The arc angle (in degrees) to place the repetitions. Must be greater than 0.
1287    let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1288    // Whether or not to rotate the duplicates as they are copied.
1289    let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
1290    // If the target being patterned is itself a pattern, then, should you use the original solid,
1291    // or the pattern?
1292    let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
1293
1294    match geometry {
1295        SolidOrImportedGeometry::SolidSet(solids) => Ok(inner_pattern_circular_3d(
1296            solids,
1297            instances,
1298            [axis[0].n, axis[1].n, axis[2].n],
1299            center,
1300            arc_degrees.map(|x| x.n),
1301            rotate_duplicates,
1302            use_original,
1303            exec_state,
1304            args,
1305        )
1306        .await?
1307        .into()),
1308        SolidOrImportedGeometry::ImportedGeometry(geometry) => Ok(KclValue::from_imported_geometries(
1309            inner_pattern_circular_3d(
1310                vec![*geometry],
1311                instances,
1312                [axis[0].n, axis[1].n, axis[2].n],
1313                center,
1314                arc_degrees.map(|x| x.n),
1315                rotate_duplicates,
1316                use_original,
1317                exec_state,
1318                args,
1319            )
1320            .await?,
1321        )),
1322    }
1323}
1324
1325#[allow(clippy::too_many_arguments)]
1326async fn inner_pattern_circular_3d<T: GeometryTrait<Set = Vec<T>>>(
1327    geometry: Vec<T>,
1328    instances: u32,
1329    axis: [f64; 3],
1330    center: Option<[TyF64; 3]>,
1331    arc_degrees: Option<f64>,
1332    rotate_duplicates: Option<bool>,
1333    use_original: Option<bool>,
1334    exec_state: &mut ExecState,
1335    args: Args,
1336) -> Result<Vec<T>, KclError> {
1337    let center = center.unwrap_or(POINT_ZERO_ZERO_ZERO);
1338    let data = CircularPattern3dData {
1339        instances,
1340        axis,
1341        center,
1342        arc_degrees,
1343        rotate_duplicates,
1344        use_original,
1345    };
1346    execute_pattern_circular(CircularPattern::ThreeD(data), geometry, exec_state, args).await
1347}
1348
1349async fn execute_pattern_circular<T: GeometryTrait>(
1350    data: CircularPattern,
1351    geometry_set: T::Set,
1352    exec_state: &mut ExecState,
1353    args: Args,
1354) -> Result<Vec<T>, KclError> {
1355    T::flush_batch(&args, exec_state, &geometry_set).await?;
1356    let starting: Vec<T> = geometry_set.into();
1357    if args.ctx.context_type == crate::execution::ContextType::Mock {
1358        let seed = starting
1359            .first()
1360            .cloned()
1361            .ok_or(KclError::new_internal(KclErrorDetails::new(
1362                "Unexpected empty set".to_owned(),
1363                vec![args.source_range],
1364            )))?;
1365        let mut mock_responses = starting;
1366        let num_repetitions = match data.repetitions() {
1367            RepetitionsNeeded::More(n) => n,
1368            RepetitionsNeeded::None => {
1369                return Ok(mock_responses);
1370            }
1371            RepetitionsNeeded::Invalid => {
1372                return Err(KclError::new_semantic(KclErrorDetails::new(
1373                    MUST_HAVE_ONE_INSTANCE.to_owned(),
1374                    vec![args.source_range],
1375                )));
1376            }
1377        };
1378        for _ in 0..num_repetitions {
1379            let new_id = exec_state.next_uuid();
1380            let mut new_geometry = seed.clone();
1381            new_geometry.set_id(new_id);
1382            new_geometry.set_artifact_id(new_id);
1383            mock_responses.push(new_geometry);
1384        }
1385
1386        return Ok(mock_responses);
1387    }
1388
1389    let mut output = Vec::new();
1390    for mut geometry in starting {
1391        output.extend(pattern_circular(&data, &mut geometry, exec_state, &args).await?);
1392    }
1393    Ok(output)
1394}
1395
1396async fn pattern_circular<T: GeometryTrait>(
1397    data: &CircularPattern,
1398    geometry: &mut T,
1399    exec_state: &mut ExecState,
1400    args: &Args,
1401) -> Result<Vec<T>, KclError> {
1402    let num_repetitions = match data.repetitions() {
1403        RepetitionsNeeded::More(n) => n,
1404        RepetitionsNeeded::None => {
1405            return Ok(vec![geometry.clone()]);
1406        }
1407        RepetitionsNeeded::Invalid => {
1408            return Err(KclError::new_semantic(KclErrorDetails::new(
1409                MUST_HAVE_ONE_INSTANCE.to_owned(),
1410                vec![args.source_range],
1411            )));
1412        }
1413    };
1414
1415    let geometry_id = geometry.id(&args.ctx).await?;
1416    let center = data.center_mm();
1417    let resp = exec_state
1418        .send_modeling_cmd(
1419            ModelingCmdMeta::from_args(exec_state, args),
1420            ModelingCmd::from(
1421                mcmd::EntityCircularPattern::builder()
1422                    .axis(kcmc::shared::Point3d::from(data.axis()))
1423                    .entity_id(if data.use_original() {
1424                        geometry.topology_id()
1425                    } else {
1426                        geometry_id
1427                    })
1428                    .center(kcmc::shared::Point3d {
1429                        x: LengthUnit(center[0]),
1430                        y: LengthUnit(center[1]),
1431                        z: LengthUnit(center[2]),
1432                    })
1433                    .num_repetitions(num_repetitions)
1434                    .arc_degrees(data.arc_degrees().unwrap_or(360.0))
1435                    .rotate_duplicates(data.rotate_duplicates().unwrap_or(true))
1436                    .build(),
1437            ),
1438        )
1439        .await?;
1440
1441    // The common case is borrowing from the response.  Instead of cloning,
1442    // create a Vec to borrow from in mock mode.
1443    let mut mock_ids = Vec::new();
1444    let entity_ids = if let OkWebSocketResponseData::Modeling {
1445        modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1446    } = &resp
1447    {
1448        &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1449    } else if args.ctx.no_engine_commands().await {
1450        mock_ids.reserve(num_repetitions as usize);
1451        for _ in 0..num_repetitions {
1452            mock_ids.push(exec_state.next_uuid());
1453        }
1454        &mock_ids
1455    } else {
1456        return Err(KclError::new_engine(KclErrorDetails::new(
1457            format!("EntityCircularPattern response was not as expected: {resp:?}"),
1458            vec![args.source_range],
1459        )));
1460    };
1461
1462    let mut geometries = vec![geometry.clone()];
1463    for id in entity_ids.iter().copied() {
1464        let mut new_geometry = geometry.clone();
1465        new_geometry.set_id(id);
1466        new_geometry.set_artifact_id(id);
1467        geometries.push(new_geometry);
1468    }
1469    Ok(geometries)
1470}