1use 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::ExecutorContext;
21use crate::NodePath;
22use crate::SourceRange;
23use crate::errors::KclError;
24use crate::errors::KclErrorDetails;
25use crate::execution::ArtifactId;
26use crate::execution::EarlyReturn;
27use crate::execution::ExecState;
28use crate::execution::Geometries;
29use crate::execution::Geometry;
30use crate::execution::ImportedGeometry;
31use crate::execution::KclObjectFields;
32use crate::execution::KclValue;
33use crate::execution::KclValueControlFlow;
34use crate::execution::ModelingCmdMeta;
35use crate::execution::Sketch;
36use crate::execution::Solid;
37use crate::execution::SolidOrImportedGeometry;
38use crate::execution::early_return;
39use crate::execution::fn_call::Arg;
40use crate::execution::fn_call::Args;
41use crate::execution::kcl_value::FunctionSource;
42use crate::execution::types::CoercionMode;
43use crate::execution::types::NumericType;
44use crate::execution::types::NumericTypeExt;
45use crate::execution::types::PrimitiveType;
46use crate::execution::types::RuntimeType;
47use crate::std::args::TyF64;
48use crate::std::axis_or_reference::Axis2dOrPoint2d;
49use crate::std::shapes::POINT_ZERO_ZERO;
50use crate::std::utils::point_3d_to_mm;
51use crate::std::utils::point_to_mm;
52pub const POINT_ZERO_ZERO_ZERO: [TyF64; 3] = [
53 TyF64::new(
54 0.0,
55 crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
56 ),
57 TyF64::new(
58 0.0,
59 crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
60 ),
61 TyF64::new(
62 0.0,
63 crate::exec::NumericType::Known(crate::exec::UnitType::Length(crate::exec::UnitLength::Millimeters)),
64 ),
65];
66
67const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
68
69#[derive(Debug)]
71pub(crate) enum Patternable3d {
72 Solids(Vec<Solid>),
73 ImportedGeometry(ImportedGeometry),
74}
75
76fn pattern_geometry_3d_type() -> RuntimeType {
79 RuntimeType::Union(vec![RuntimeType::solids(), RuntimeType::imported()])
80}
81
82pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValueControlFlow, KclError> {
84 let (geometry, instances, transform, use_original) = pattern_transform_parse_args(&args, exec_state)?;
85
86 match inner_pattern_transform(geometry, instances, transform, use_original, exec_state, &args).await {
87 Ok(geometry) => Ok(KclValue::continue_(geometry)),
88 Err(EarlyReturn::Value(cf)) => Ok(cf),
91 Err(EarlyReturn::Error(err)) => Err(err),
92 }
93}
94
95pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValueControlFlow, KclError> {
97 let (sketches, instances, transform, use_original) = pattern_transform_2d_parse_args(&args, exec_state)?;
98
99 match inner_pattern_transform_2d(sketches, instances, transform, use_original, exec_state, &args).await {
100 Ok(sketches) => Ok(KclValue::continue_(sketches.into())),
101 Err(EarlyReturn::Value(cf)) => Ok(cf),
104 Err(EarlyReturn::Error(err)) => Err(err),
105 }
106}
107
108pub(crate) fn pattern_transform_parse_args(
110 args: &Args,
111 exec_state: &mut ExecState,
112) -> Result<(Patternable3d, u32, FunctionSource, Option<bool>), KclError> {
113 let geometry: SolidOrImportedGeometry =
114 args.get_unlabeled_kw_arg("solids", &pattern_geometry_3d_type(), exec_state)?;
115 let geometry = match geometry {
116 SolidOrImportedGeometry::SolidSet(solids) => Patternable3d::Solids(solids),
117 SolidOrImportedGeometry::ImportedGeometry(geometry) => Patternable3d::ImportedGeometry(*geometry),
118 };
119 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
120 let transform: FunctionSource = args.get_kw_arg("transform", &RuntimeType::function(), exec_state)?;
121 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
122 Ok((geometry, instances, transform, use_original))
123}
124
125pub(crate) fn pattern_transform_2d_parse_args(
127 args: &Args,
128 exec_state: &mut ExecState,
129) -> Result<(Vec<Sketch>, u32, FunctionSource, Option<bool>), KclError> {
130 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
131 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
132 let transform: FunctionSource = args.get_kw_arg("transform", &RuntimeType::function(), exec_state)?;
133 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
134 Ok((sketches, instances, transform, use_original))
135}
136
137pub(crate) fn pattern_check_instances(instances: u32, source_range: SourceRange) -> Result<(), KclError> {
140 if instances < 1 {
141 return Err(KclError::new_semantic(KclErrorDetails::new(
142 MUST_HAVE_ONE_INSTANCE.to_owned(),
143 vec![source_range],
144 )));
145 }
146 Ok(())
147}
148
149pub(crate) fn transform_callback_args(
152 i: u32,
153 source_range: SourceRange,
154 node_path: Option<NodePath>,
155 exec_state: &mut ExecState,
156 ctxt: &ExecutorContext,
157) -> Args<crate::execution::fn_call::Sugary> {
158 let repetition_num = KclValue::Number {
159 value: i.into(),
160 ty: NumericType::count(),
161 meta: vec![source_range.into()],
162 };
163 Args::new(
164 Default::default(),
165 vec![(None, Arg::new(repetition_num, source_range))],
166 source_range,
167 node_path,
168 exec_state,
169 ctxt.clone(),
170 Some("transform closure".to_owned()),
171 )
172}
173
174pub(crate) fn transform_missing_value_error(source_range: SourceRange) -> KclError {
177 KclError::new_semantic(KclErrorDetails::new(
178 "Transform function must return a value".to_string(),
179 vec![source_range],
180 ))
181}
182
183pub(crate) fn transforms_from_callback_value<T: GeometryTrait>(
186 transform_fn_return: KclValue,
187 source_range: SourceRange,
188 exec_state: &mut ExecState,
189) -> Result<Vec<Transform>, KclError> {
190 let source_ranges = vec![source_range];
191 let transforms = match transform_fn_return {
192 KclValue::Object { value, .. } => vec![value],
193 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
194 let transforms: Vec<_> = value
195 .into_iter()
196 .map(|val| {
197 val.into_object().ok_or(KclError::new_semantic(KclErrorDetails::new(
198 "Transform function must return a transform object".to_string(),
199 source_ranges.clone(),
200 )))
201 })
202 .collect::<Result<_, KclError>>()?;
203 transforms
204 }
205 _ => {
206 return Err(KclError::new_semantic(KclErrorDetails::new(
207 "Transform function must return a transform object".to_string(),
208 source_ranges,
209 )));
210 }
211 };
212
213 let transforms = transforms
214 .into_iter()
215 .map(|obj| transform_from_obj_fields::<T>(obj, source_ranges.clone(), exec_state))
216 .collect::<Result<_, KclError>>()?;
217 Ok(transforms)
218}
219
220async fn inner_pattern_transform(
221 geometry: Patternable3d,
222 instances: u32,
223 transform: FunctionSource,
224 use_original: Option<bool>,
225 exec_state: &mut ExecState,
226 args: &Args,
227) -> Result<KclValue, EarlyReturn> {
228 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
230 pattern_check_instances(instances, args.source_range)?;
231 for i in 1..instances {
232 let t = match &geometry {
233 Patternable3d::Solids(_) => {
234 make_transform::<Solid>(
235 i,
236 &transform,
237 args.source_range,
238 args.node_path.clone(),
239 exec_state,
240 &args.ctx,
241 )
242 .await?
243 }
244 Patternable3d::ImportedGeometry(_) => {
245 make_transform::<ImportedGeometry>(
246 i,
247 &transform,
248 args.source_range,
249 args.node_path.clone(),
250 exec_state,
251 &args.ctx,
252 )
253 .await?
254 }
255 };
256 transform_vec.push(t);
257 }
258 match geometry {
259 Patternable3d::Solids(solids) => Ok(execute_pattern_transform::<Solid>(
260 transform_vec,
261 solids,
262 use_original.unwrap_or_default(),
263 exec_state,
264 args,
265 )
266 .await?
267 .into()),
268 Patternable3d::ImportedGeometry(geometry) => Ok(KclValue::from_imported_geometries(
269 execute_pattern_transform(
270 transform_vec,
271 vec![geometry],
272 use_original.unwrap_or_default(),
273 exec_state,
274 args,
275 )
276 .await?,
277 )),
278 }
279}
280
281async fn inner_pattern_transform_2d(
282 sketches: Vec<Sketch>,
283 instances: u32,
284 transform: FunctionSource,
285 use_original: Option<bool>,
286 exec_state: &mut ExecState,
287 args: &Args,
288) -> Result<Vec<Sketch>, EarlyReturn> {
289 let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
291 pattern_check_instances(instances, args.source_range)?;
292 for i in 1..instances {
293 let t = make_transform::<Sketch>(
294 i,
295 &transform,
296 args.source_range,
297 args.node_path.clone(),
298 exec_state,
299 &args.ctx,
300 )
301 .await?;
302 transform_vec.push(t);
303 }
304 Ok(execute_pattern_transform(
305 transform_vec,
306 sketches,
307 use_original.unwrap_or_default(),
308 exec_state,
309 args,
310 )
311 .await?)
312}
313
314pub(crate) async fn execute_pattern_transform<T: GeometryTrait>(
315 transforms: Vec<Vec<Transform>>,
316 geo_set: T::Set,
317 use_original: bool,
318 exec_state: &mut ExecState,
319 args: &Args,
320) -> Result<Vec<T>, KclError> {
321 T::flush_batch(args, exec_state, &geo_set).await?;
325 let starting: Vec<T> = geo_set.into();
326
327 let mut output = Vec::new();
328 for mut geo in starting {
329 let new = send_pattern_transform(transforms.clone(), &mut geo, use_original, exec_state, args).await?;
330 output.extend(new)
331 }
332 Ok(output)
333}
334
335async fn send_pattern_transform<T: GeometryTrait>(
336 transforms: Vec<Vec<Transform>>,
339 geometry: &mut T,
340 use_original: bool,
341 exec_state: &mut ExecState,
342 args: &Args,
343) -> Result<Vec<T>, KclError> {
344 let extra_instances = transforms.len();
345 let geometry_id = geometry.id(&args.ctx).await?;
346 let entity_id = if use_original {
347 geometry.topology_id()
348 } else {
349 geometry_id
350 };
351
352 let resp = exec_state
353 .send_modeling_cmd(
354 ModelingCmdMeta::from_args(exec_state, args),
355 ModelingCmd::from(
356 mcmd::EntityLinearPatternTransform::builder()
357 .entity_id(entity_id)
358 .transform(Default::default())
359 .transforms(transforms)
360 .build(),
361 ),
362 )
363 .await?;
364
365 let mut mock_ids = Vec::new();
366 let entity_ids = if let OkWebSocketResponseData::Modeling {
367 modeling_response: OkModelingCmdResponse::EntityLinearPatternTransform(pattern_info),
368 } = &resp
369 {
370 &pattern_info.entity_face_edge_ids.iter().map(|x| x.object_id).collect()
371 } else if args.ctx.no_engine_commands().await {
372 mock_ids.reserve(extra_instances);
373 for _ in 0..extra_instances {
374 mock_ids.push(exec_state.next_uuid());
375 }
376 &mock_ids
377 } else {
378 return Err(KclError::new_engine(KclErrorDetails::new(
379 format!("EntityLinearPattern response was not as expected: {resp:?}"),
380 vec![args.source_range],
381 )));
382 };
383
384 let mut geometries = vec![geometry.clone()];
385 for id in entity_ids.iter().copied() {
386 let mut new_geometry = geometry.clone();
387 new_geometry.set_id(id);
388 new_geometry.set_artifact_id(id);
389 geometries.push(new_geometry);
390 }
391 Ok(geometries)
392}
393
394async fn make_transform<T: GeometryTrait>(
395 i: u32,
396 transform: &FunctionSource,
397 source_range: SourceRange,
398 node_path: Option<NodePath>,
399 exec_state: &mut ExecState,
400 ctxt: &ExecutorContext,
401) -> Result<Vec<Transform>, EarlyReturn> {
402 let transform_fn_args = transform_callback_args(i, source_range, node_path, exec_state, ctxt);
404 let transform_fn_return = transform
405 .call_kw(None, exec_state, ctxt, transform_fn_args, source_range)
406 .await?;
407
408 let transform_fn_return = transform_fn_return.ok_or_else(|| transform_missing_value_error(source_range))?;
410
411 let transform_fn_return = early_return!(transform_fn_return);
415
416 Ok(transforms_from_callback_value::<T>(
417 transform_fn_return,
418 source_range,
419 exec_state,
420 )?)
421}
422
423fn transform_from_obj_fields<T: GeometryTrait>(
424 transform: KclObjectFields,
425 source_ranges: Vec<SourceRange>,
426 exec_state: &mut ExecState,
427) -> Result<Transform, KclError> {
428 let replicate = match transform.get("replicate") {
430 Some(KclValue::Bool { value: true, .. }) => true,
431 Some(KclValue::Bool { value: false, .. }) => false,
432 Some(_) => {
433 return Err(KclError::new_semantic(KclErrorDetails::new(
434 "The 'replicate' key must be a bool".to_string(),
435 source_ranges,
436 )));
437 }
438 None => true,
439 };
440
441 let scale = match transform.get("scale") {
442 Some(x) => point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?).into(),
443 None => kcmc::shared::Point3d { x: 1.0, y: 1.0, z: 1.0 },
444 };
445
446 for (dim, name) in [(scale.x, "x"), (scale.y, "y"), (scale.z, "z")] {
447 if dim == 0.0 {
448 return Err(KclError::new_semantic(KclErrorDetails::new(
449 format!("cannot set {name} = 0, scale factor must be nonzero"),
450 source_ranges,
451 )));
452 }
453 }
454 let translate = match transform.get("translate") {
455 Some(x) => {
456 let arr = point_3d_to_mm(T::array_to_point3d(x, source_ranges.clone(), exec_state)?);
457 kcmc::shared::Point3d::<LengthUnit> {
458 x: LengthUnit(arr[0]),
459 y: LengthUnit(arr[1]),
460 z: LengthUnit(arr[2]),
461 }
462 }
463 None => kcmc::shared::Point3d::<LengthUnit> {
464 x: LengthUnit(0.0),
465 y: LengthUnit(0.0),
466 z: LengthUnit(0.0),
467 },
468 };
469
470 let mut rotation = Rotation::default();
471 if let Some(rot) = transform.get("rotation") {
472 let KclValue::Object { value: rot, .. } = rot else {
473 return Err(KclError::new_semantic(KclErrorDetails::new(
474 "The 'rotation' key must be an object (with optional fields 'angle', 'axis' and 'origin')".to_owned(),
475 source_ranges,
476 )));
477 };
478 if let Some(axis) = rot.get("axis") {
479 rotation.axis = point_3d_to_mm(T::array_to_point3d(axis, source_ranges.clone(), exec_state)?).into();
480 }
481 if let Some(angle) = rot.get("angle") {
482 match angle {
483 KclValue::Number { value: number, .. } => {
484 rotation.angle = Angle::from_degrees(*number);
485 }
486 _ => {
487 return Err(KclError::new_semantic(KclErrorDetails::new(
488 "The 'rotation.angle' key must be a number (of degrees)".to_owned(),
489 source_ranges,
490 )));
491 }
492 }
493 }
494 if let Some(origin) = rot.get("origin") {
495 rotation.origin = match origin {
496 KclValue::String { value: s, meta: _ } if s == "local" => OriginType::Local,
497 KclValue::String { value: s, meta: _ } if s == "global" => OriginType::Global,
498 other => {
499 let origin = point_3d_to_mm(T::array_to_point3d(other, source_ranges, exec_state)?).into();
500 OriginType::Custom { origin }
501 }
502 };
503 }
504 }
505
506 let transform = Transform::builder()
507 .replicate(replicate)
508 .scale(scale)
509 .translate(translate)
510 .rotation(rotation)
511 .build();
512 Ok(transform)
513}
514
515fn array_to_point3d(
516 val: &KclValue,
517 source_ranges: Vec<SourceRange>,
518 exec_state: &mut ExecState,
519) -> Result<[TyF64; 3], KclError> {
520 val.coerce(&RuntimeType::point3d(), CoercionMode::implicit(), exec_state)
521 .map_err(|e| {
522 KclError::new_semantic(KclErrorDetails::new(
523 format!(
524 "Expected an array of 3 numbers (i.e., a 3D point), found {}",
525 e.found
526 .map(|t| t.human_friendly_type())
527 .unwrap_or_else(|| val.human_friendly_type())
528 ),
529 source_ranges,
530 ))
531 })
532 .map(|val| val.as_point3d().unwrap())
533}
534
535fn array_to_point2d(
536 val: &KclValue,
537 source_ranges: Vec<SourceRange>,
538 exec_state: &mut ExecState,
539) -> Result<[TyF64; 2], KclError> {
540 val.coerce(&RuntimeType::point2d(), CoercionMode::implicit(), exec_state)
541 .map_err(|e| {
542 KclError::new_semantic(KclErrorDetails::new(
543 format!(
544 "Expected an array of 2 numbers (i.e., a 2D point), found {}",
545 e.found
546 .map(|t| t.human_friendly_type())
547 .unwrap_or_else(|| val.human_friendly_type())
548 ),
549 source_ranges,
550 ))
551 })
552 .map(|val| val.as_point2d().unwrap())
553}
554
555pub trait GeometryTrait: Clone {
556 type Set: Into<Vec<Self>> + Clone;
557 #[allow(async_fn_in_trait)]
558 async fn id(&mut self, ctx: &ExecutorContext) -> Result<Uuid, KclError>;
559 fn topology_id(&self) -> Uuid;
560 fn set_id(&mut self, id: Uuid);
561 fn set_artifact_id(&mut self, id: Uuid);
562 fn array_to_point3d(
563 val: &KclValue,
564 source_ranges: Vec<SourceRange>,
565 exec_state: &mut ExecState,
566 ) -> Result<[TyF64; 3], KclError>;
567 #[allow(async_fn_in_trait)]
568 async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: &Self::Set) -> Result<(), KclError>;
569}
570
571impl GeometryTrait for Sketch {
572 type Set = Vec<Sketch>;
573 fn set_id(&mut self, id: Uuid) {
574 self.id = id;
575 }
576 fn set_artifact_id(&mut self, id: Uuid) {
577 self.artifact_id = ArtifactId::new(id);
578 }
579 async fn id(&mut self, _: &ExecutorContext) -> Result<Uuid, KclError> {
580 Ok(self.id)
581 }
582 fn topology_id(&self) -> Uuid {
583 self.original_id
584 }
585 fn array_to_point3d(
586 val: &KclValue,
587 source_ranges: Vec<SourceRange>,
588 exec_state: &mut ExecState,
589 ) -> Result<[TyF64; 3], KclError> {
590 let [x, y] = array_to_point2d(val, source_ranges, exec_state)?;
591 let ty = x.ty;
592 Ok([x, y, TyF64::new(0.0, ty)])
593 }
594
595 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
596 Ok(())
597 }
598}
599
600impl GeometryTrait for Solid {
601 type Set = Vec<Solid>;
602 fn set_id(&mut self, id: Uuid) {
603 self.id = id;
604 self.value_id = id;
605 if let Some(sketch) = self.sketch_mut() {
607 sketch.id = id;
608 }
609 }
610
611 fn set_artifact_id(&mut self, id: Uuid) {
612 self.become_pattern_copy(id);
613 }
614
615 async fn id(&mut self, _: &ExecutorContext) -> Result<Uuid, KclError> {
616 Ok(self.id)
617 }
618
619 fn topology_id(&self) -> Uuid {
620 Solid::topology_id(self)
621 }
622
623 fn array_to_point3d(
624 val: &KclValue,
625 source_ranges: Vec<SourceRange>,
626 exec_state: &mut ExecState,
627 ) -> Result<[TyF64; 3], KclError> {
628 array_to_point3d(val, source_ranges, exec_state)
629 }
630
631 async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: &Self::Set) -> Result<(), KclError> {
632 exec_state
633 .flush_batch_for_solids(ModelingCmdMeta::from_args(exec_state, args), solid_set)
634 .await
635 }
636}
637
638impl GeometryTrait for ImportedGeometry {
639 type Set = Vec<ImportedGeometry>;
640
641 async fn id(&mut self, ctx: &ExecutorContext) -> Result<Uuid, KclError> {
642 ImportedGeometry::id(self, ctx).await
643 }
644
645 fn topology_id(&self) -> Uuid {
646 self.id
647 }
648
649 fn set_id(&mut self, id: Uuid) {
650 self.id = id;
651 }
652
653 fn set_artifact_id(&mut self, _: Uuid) {}
654
655 fn array_to_point3d(
656 val: &KclValue,
657 source_ranges: Vec<SourceRange>,
658 exec_state: &mut ExecState,
659 ) -> Result<[TyF64; 3], KclError> {
660 array_to_point3d(val, source_ranges, exec_state)
661 }
662
663 async fn flush_batch(_: &Args, _: &mut ExecState, _: &Self::Set) -> Result<(), KclError> {
664 Ok(())
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use crate::execution::KclValueView;
672 use crate::execution::types::NumericType;
673 use crate::execution::types::PrimitiveType;
674
675 async fn assert_imported_pattern_executes(code: &str) {
676 let current_file = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
677 .join("tests")
678 .join("inputs")
679 .join("main.kcl");
680 let ctx = crate::test_server::new_context(true, Some(current_file)).await.unwrap();
681 let program = crate::Program::parse_no_errs(code).unwrap();
682 let result = ctx.run_with_caching(program).await.unwrap();
683
684 let KclValueView::HomArray { value } = result.variables.get("patterned").unwrap() else {
685 panic!("Expected the imported geometry pattern to return an array");
686 };
687 assert_eq!(value.len(), 3);
688 assert!(
689 value
690 .iter()
691 .all(|value| matches!(value, KclValueView::ImportedGeometry(_)))
692 );
693 let ids = value
694 .iter()
695 .map(|value| match value {
696 KclValueView::ImportedGeometry(geometry) => geometry.id,
697 _ => unreachable!(),
698 })
699 .collect::<std::collections::HashSet<_>>();
700 assert_eq!(ids.len(), 3);
701
702 ctx.close().await;
703 }
704
705 #[tokio::test(flavor = "multi_thread")]
706 async fn imported_geometry_pattern_linear_3d() {
707 assert_imported_pattern_executes(
708 r#"import "cube.step" as cube
709
710patterned = patternLinear3d(cube, instances = 3, distance = 20, axis = X)
711"#,
712 )
713 .await;
714 }
715
716 #[tokio::test(flavor = "multi_thread")]
717 async fn imported_geometry_pattern_circular_3d() {
718 assert_imported_pattern_executes(
719 r#"import "cube.step" as cube
720
721patterned = patternCircular3d(cube, instances = 3, axis = Z, center = [20, 0, 0])
722"#,
723 )
724 .await;
725 }
726
727 #[tokio::test(flavor = "multi_thread")]
728 async fn imported_geometry_pattern_transform() {
729 assert_imported_pattern_executes(
730 r#"import "cube.step" as cube
731
732fn shift(@i) {
733 return { translate = [20 * i, 0, 0] }
734}
735
736patterned = patternTransform(cube, instances = 3, transform = shift)
737"#,
738 )
739 .await;
740 }
741
742 #[tokio::test(flavor = "multi_thread")]
743 async fn test_array_to_point3d() {
744 let ctx = ExecutorContext::new_mock(None).await;
745 let mut exec_state = ExecState::new(&ctx);
746 let input = KclValue::HomArray {
747 value: vec![
748 KclValue::Number {
749 value: 1.1,
750 meta: Default::default(),
751 ty: NumericType::mm(),
752 },
753 KclValue::Number {
754 value: 2.2,
755 meta: Default::default(),
756 ty: NumericType::mm(),
757 },
758 KclValue::Number {
759 value: 3.3,
760 meta: Default::default(),
761 ty: NumericType::mm(),
762 },
763 ],
764 ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
765 };
766 let expected = [
767 TyF64::new(1.1, NumericType::mm()),
768 TyF64::new(2.2, NumericType::mm()),
769 TyF64::new(3.3, NumericType::mm()),
770 ];
771 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
772 assert_eq!(actual.unwrap(), expected);
773 ctx.close().await;
774 }
775
776 #[tokio::test(flavor = "multi_thread")]
777 async fn test_tuple_to_point3d() {
778 let ctx = ExecutorContext::new_mock(None).await;
779 let mut exec_state = ExecState::new(&ctx);
780 let input = KclValue::Tuple {
781 value: vec![
782 KclValue::Number {
783 value: 1.1,
784 meta: Default::default(),
785 ty: NumericType::mm(),
786 },
787 KclValue::Number {
788 value: 2.2,
789 meta: Default::default(),
790 ty: NumericType::mm(),
791 },
792 KclValue::Number {
793 value: 3.3,
794 meta: Default::default(),
795 ty: NumericType::mm(),
796 },
797 ],
798 meta: Default::default(),
799 };
800 let expected = [
801 TyF64::new(1.1, NumericType::mm()),
802 TyF64::new(2.2, NumericType::mm()),
803 TyF64::new(3.3, NumericType::mm()),
804 ];
805 let actual = array_to_point3d(&input, Vec::new(), &mut exec_state);
806 assert_eq!(actual.unwrap(), expected);
807 ctx.close().await;
808 }
809}
810
811pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
813 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
814 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
815 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
816 let axis: Axis2dOrPoint2d = args.get_kw_arg(
817 "axis",
818 &RuntimeType::Union(vec![
819 RuntimeType::Primitive(PrimitiveType::Axis2d),
820 RuntimeType::point2d(),
821 ]),
822 exec_state,
823 )?;
824 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
825
826 let axis = axis.to_point2d();
827 if axis[0].n == 0.0 && axis[1].n == 0.0 {
828 return Err(KclError::new_semantic(KclErrorDetails::new(
829 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
830 .to_owned(),
831 vec![args.source_range],
832 )));
833 }
834
835 let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
836 Ok(sketches.into())
837}
838
839async fn inner_pattern_linear_2d(
840 sketches: Vec<Sketch>,
841 instances: u32,
842 distance: TyF64,
843 axis: [TyF64; 2],
844 use_original: Option<bool>,
845 exec_state: &mut ExecState,
846 args: Args,
847) -> Result<Vec<Sketch>, KclError> {
848 let [x, y] = point_to_mm(axis);
849 let axis_len = f64::sqrt(x * x + y * y);
850 let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
851 let transforms: Vec<_> = (1..instances)
852 .map(|i| {
853 let d = distance.to_mm() * (i as f64);
854 let translate = (normalized_axis * d).with_z(0.0).map(LengthUnit);
855 vec![Transform::builder().translate(translate).build()]
856 })
857 .collect();
858 execute_pattern_transform(
859 transforms,
860 sketches,
861 use_original.unwrap_or_default(),
862 exec_state,
863 &args,
864 )
865 .await
866}
867
868pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
870 let geometry: SolidOrImportedGeometry =
871 args.get_unlabeled_kw_arg("solids", &pattern_geometry_3d_type(), exec_state)?;
872 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
873 let distance: TyF64 = args.get_kw_arg("distance", &RuntimeType::length(), exec_state)?;
874 let axis: Axis3dOrPoint3d = args.get_kw_arg(
875 "axis",
876 &RuntimeType::Union(vec![
877 RuntimeType::Primitive(PrimitiveType::Axis3d),
878 RuntimeType::point3d(),
879 ]),
880 exec_state,
881 )?;
882 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
883
884 let axis = axis.to_point3d();
885 if axis[0].n == 0.0 && axis[1].n == 0.0 && axis[2].n == 0.0 {
886 return Err(KclError::new_semantic(KclErrorDetails::new(
887 "The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
888 .to_owned(),
889 vec![args.source_range],
890 )));
891 }
892
893 match geometry {
894 SolidOrImportedGeometry::SolidSet(solids) => {
895 Ok(
896 inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args)
897 .await?
898 .into(),
899 )
900 }
901 SolidOrImportedGeometry::ImportedGeometry(geometry) => Ok(KclValue::from_imported_geometries(
902 inner_pattern_linear_3d(
903 vec![*geometry],
904 instances,
905 distance,
906 axis,
907 use_original,
908 exec_state,
909 args,
910 )
911 .await?,
912 )),
913 }
914}
915
916async fn inner_pattern_linear_3d<T: GeometryTrait<Set = Vec<T>>>(
917 geometry: Vec<T>,
918 instances: u32,
919 distance: TyF64,
920 axis: [TyF64; 3],
921 use_original: Option<bool>,
922 exec_state: &mut ExecState,
923 args: Args,
924) -> Result<Vec<T>, KclError> {
925 let [x, y, z] = point_3d_to_mm(axis);
926 let axis_len = f64::sqrt(x * x + y * y + z * z);
927 let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
928 let transforms: Vec<_> = (1..instances)
929 .map(|i| {
930 let d = distance.to_mm() * (i as f64);
931 let translate = (normalized_axis * d).map(LengthUnit);
932 vec![Transform::builder().translate(translate).build()]
933 })
934 .collect();
935 execute_pattern_transform(
936 transforms,
937 geometry,
938 use_original.unwrap_or_default(),
939 exec_state,
940 &args,
941 )
942 .await
943}
944
945#[derive(Debug, Clone, Serialize, PartialEq)]
947#[serde(rename_all = "camelCase")]
948struct CircularPattern2dData {
949 pub instances: u32,
954 pub center: [TyF64; 2],
956 pub arc_degrees: Option<f64>,
958 pub rotate_duplicates: Option<bool>,
960 #[serde(default)]
963 pub use_original: Option<bool>,
964}
965
966#[derive(Debug, Clone, Serialize, PartialEq)]
968#[serde(rename_all = "camelCase")]
969struct CircularPattern3dData {
970 pub instances: u32,
975 pub axis: [f64; 3],
978 pub center: [TyF64; 3],
980 pub arc_degrees: Option<f64>,
982 pub rotate_duplicates: Option<bool>,
984 #[serde(default)]
987 pub use_original: Option<bool>,
988}
989
990#[allow(clippy::large_enum_variant)]
991#[derive(Clone)]
992enum CircularPattern {
993 ThreeD(CircularPattern3dData),
994 TwoD(CircularPattern2dData),
995}
996
997enum RepetitionsNeeded {
998 More(u32),
1000 None,
1002 Invalid,
1004}
1005
1006impl From<u32> for RepetitionsNeeded {
1007 fn from(n: u32) -> Self {
1008 match n.cmp(&1) {
1009 Ordering::Less => Self::Invalid,
1010 Ordering::Equal => Self::None,
1011 Ordering::Greater => Self::More(n - 1),
1012 }
1013 }
1014}
1015
1016impl CircularPattern {
1017 pub fn axis(&self) -> [f64; 3] {
1018 match self {
1019 CircularPattern::TwoD(_lp) => [0.0, 0.0, 0.0],
1020 CircularPattern::ThreeD(lp) => [lp.axis[0], lp.axis[1], lp.axis[2]],
1021 }
1022 }
1023
1024 pub fn center_mm(&self) -> [f64; 3] {
1025 match self {
1026 CircularPattern::TwoD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), 0.0],
1027 CircularPattern::ThreeD(lp) => [lp.center[0].to_mm(), lp.center[1].to_mm(), lp.center[2].to_mm()],
1028 }
1029 }
1030
1031 fn repetitions(&self) -> RepetitionsNeeded {
1032 let n = match self {
1033 CircularPattern::TwoD(lp) => lp.instances,
1034 CircularPattern::ThreeD(lp) => lp.instances,
1035 };
1036 RepetitionsNeeded::from(n)
1037 }
1038
1039 pub fn arc_degrees(&self) -> Option<f64> {
1040 match self {
1041 CircularPattern::TwoD(lp) => lp.arc_degrees,
1042 CircularPattern::ThreeD(lp) => lp.arc_degrees,
1043 }
1044 }
1045
1046 pub fn rotate_duplicates(&self) -> Option<bool> {
1047 match self {
1048 CircularPattern::TwoD(lp) => lp.rotate_duplicates,
1049 CircularPattern::ThreeD(lp) => lp.rotate_duplicates,
1050 }
1051 }
1052
1053 pub fn use_original(&self) -> bool {
1054 match self {
1055 CircularPattern::TwoD(lp) => lp.use_original.unwrap_or_default(),
1056 CircularPattern::ThreeD(lp) => lp.use_original.unwrap_or_default(),
1057 }
1058 }
1059}
1060
1061pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1063 let sketches = args.get_unlabeled_kw_arg("sketches", &RuntimeType::sketches(), exec_state)?;
1064 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
1065 let center: Option<[TyF64; 2]> = args.get_kw_arg_opt("center", &RuntimeType::point2d(), exec_state)?;
1066 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1067 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
1068 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
1069
1070 let sketches = inner_pattern_circular_2d(
1071 sketches,
1072 instances,
1073 center,
1074 arc_degrees.map(|x| x.n),
1075 rotate_duplicates,
1076 use_original,
1077 exec_state,
1078 args,
1079 )
1080 .await?;
1081 Ok(sketches.into())
1082}
1083
1084#[allow(clippy::too_many_arguments)]
1085async fn inner_pattern_circular_2d(
1086 sketch_set: Vec<Sketch>,
1087 instances: u32,
1088 center: Option<[TyF64; 2]>,
1089 arc_degrees: Option<f64>,
1090 rotate_duplicates: Option<bool>,
1091 use_original: Option<bool>,
1092 exec_state: &mut ExecState,
1093 args: Args,
1094) -> Result<Vec<Sketch>, KclError> {
1095 let starting_sketches = sketch_set;
1096
1097 if args.ctx.context_type == crate::execution::ContextType::Mock {
1098 return Ok(starting_sketches);
1099 }
1100 let center = center.unwrap_or(POINT_ZERO_ZERO);
1101 let data = CircularPattern2dData {
1102 instances,
1103 center,
1104 arc_degrees,
1105 rotate_duplicates,
1106 use_original,
1107 };
1108
1109 let mut sketches = Vec::new();
1110 for sketch in starting_sketches.iter() {
1111 let geometries =
1112 pattern_circular_sketch(data.clone(), Geometry::Sketch(sketch.clone()), exec_state, args.clone()).await?;
1113
1114 let Geometries::Sketches(new_sketches) = geometries else {
1115 return Err(KclError::new_semantic(KclErrorDetails::new(
1116 "Expected a vec of sketches".to_string(),
1117 vec![args.source_range],
1118 )));
1119 };
1120
1121 sketches.extend(new_sketches);
1122 }
1123
1124 Ok(sketches)
1125}
1126
1127async fn pattern_circular_sketch(
1128 data: CircularPattern2dData,
1129 geometry: Geometry,
1130 exec_state: &mut ExecState,
1131 args: Args,
1132) -> Result<Geometries, KclError> {
1133 let Geometry::Sketch(mut sketch) = geometry else {
1134 return Err(KclError::new_internal(KclErrorDetails::new(
1135 "A 2D circular pattern requires a sketch".to_owned(),
1136 vec![args.source_range],
1137 )));
1138 };
1139 let geometries = pattern_circular(&CircularPattern::TwoD(data), &mut sketch, exec_state, &args).await?;
1140 Ok(Geometries::Sketches(geometries))
1141}
1142
1143pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
1145 let geometry: SolidOrImportedGeometry =
1146 args.get_unlabeled_kw_arg("solids", &pattern_geometry_3d_type(), exec_state)?;
1147 let instances: u32 = args.get_kw_arg("instances", &RuntimeType::count(), exec_state)?;
1152 let axis: Axis3dOrPoint3d = args.get_kw_arg(
1154 "axis",
1155 &RuntimeType::Union(vec![
1156 RuntimeType::Primitive(PrimitiveType::Axis3d),
1157 RuntimeType::point3d(),
1158 ]),
1159 exec_state,
1160 )?;
1161 let axis = axis.to_point3d();
1162
1163 let center: Option<[TyF64; 3]> = args.get_kw_arg_opt("center", &RuntimeType::point3d(), exec_state)?;
1165 let arc_degrees: Option<TyF64> = args.get_kw_arg_opt("arcDegrees", &RuntimeType::degrees(), exec_state)?;
1167 let rotate_duplicates = args.get_kw_arg_opt("rotateDuplicates", &RuntimeType::bool(), exec_state)?;
1169 let use_original = args.get_kw_arg_opt("useOriginal", &RuntimeType::bool(), exec_state)?;
1172
1173 match geometry {
1174 SolidOrImportedGeometry::SolidSet(solids) => Ok(inner_pattern_circular_3d(
1175 solids,
1176 instances,
1177 [axis[0].n, axis[1].n, axis[2].n],
1178 center,
1179 arc_degrees.map(|x| x.n),
1180 rotate_duplicates,
1181 use_original,
1182 exec_state,
1183 args,
1184 )
1185 .await?
1186 .into()),
1187 SolidOrImportedGeometry::ImportedGeometry(geometry) => Ok(KclValue::from_imported_geometries(
1188 inner_pattern_circular_3d(
1189 vec![*geometry],
1190 instances,
1191 [axis[0].n, axis[1].n, axis[2].n],
1192 center,
1193 arc_degrees.map(|x| x.n),
1194 rotate_duplicates,
1195 use_original,
1196 exec_state,
1197 args,
1198 )
1199 .await?,
1200 )),
1201 }
1202}
1203
1204#[allow(clippy::too_many_arguments)]
1205async fn inner_pattern_circular_3d<T: GeometryTrait<Set = Vec<T>>>(
1206 geometry: Vec<T>,
1207 instances: u32,
1208 axis: [f64; 3],
1209 center: Option<[TyF64; 3]>,
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<T>, KclError> {
1216 let center = center.unwrap_or(POINT_ZERO_ZERO_ZERO);
1217 let data = CircularPattern3dData {
1218 instances,
1219 axis,
1220 center,
1221 arc_degrees,
1222 rotate_duplicates,
1223 use_original,
1224 };
1225 execute_pattern_circular(CircularPattern::ThreeD(data), geometry, exec_state, args).await
1226}
1227
1228async fn execute_pattern_circular<T: GeometryTrait>(
1229 data: CircularPattern,
1230 geometry_set: T::Set,
1231 exec_state: &mut ExecState,
1232 args: Args,
1233) -> Result<Vec<T>, KclError> {
1234 T::flush_batch(&args, exec_state, &geometry_set).await?;
1235 let starting: Vec<T> = geometry_set.into();
1236 if args.ctx.context_type == crate::execution::ContextType::Mock {
1237 let seed = starting
1238 .first()
1239 .cloned()
1240 .ok_or(KclError::new_internal(KclErrorDetails::new(
1241 "Unexpected empty set".to_owned(),
1242 vec![args.source_range],
1243 )))?;
1244 let mut mock_responses = starting;
1245 let num_repetitions = match data.repetitions() {
1246 RepetitionsNeeded::More(n) => n,
1247 RepetitionsNeeded::None => {
1248 return Ok(mock_responses);
1249 }
1250 RepetitionsNeeded::Invalid => {
1251 return Err(KclError::new_semantic(KclErrorDetails::new(
1252 MUST_HAVE_ONE_INSTANCE.to_owned(),
1253 vec![args.source_range],
1254 )));
1255 }
1256 };
1257 for _ in 0..num_repetitions {
1258 let new_id = exec_state.next_uuid();
1259 let mut new_geometry = seed.clone();
1260 new_geometry.set_id(new_id);
1261 new_geometry.set_artifact_id(new_id);
1262 mock_responses.push(new_geometry);
1263 }
1264
1265 return Ok(mock_responses);
1266 }
1267
1268 let mut output = Vec::new();
1269 for mut geometry in starting {
1270 output.extend(pattern_circular(&data, &mut geometry, exec_state, &args).await?);
1271 }
1272 Ok(output)
1273}
1274
1275async fn pattern_circular<T: GeometryTrait>(
1276 data: &CircularPattern,
1277 geometry: &mut T,
1278 exec_state: &mut ExecState,
1279 args: &Args,
1280) -> Result<Vec<T>, KclError> {
1281 let num_repetitions = match data.repetitions() {
1282 RepetitionsNeeded::More(n) => n,
1283 RepetitionsNeeded::None => {
1284 return Ok(vec![geometry.clone()]);
1285 }
1286 RepetitionsNeeded::Invalid => {
1287 return Err(KclError::new_semantic(KclErrorDetails::new(
1288 MUST_HAVE_ONE_INSTANCE.to_owned(),
1289 vec![args.source_range],
1290 )));
1291 }
1292 };
1293
1294 let geometry_id = geometry.id(&args.ctx).await?;
1295 let center = data.center_mm();
1296 let resp = exec_state
1297 .send_modeling_cmd(
1298 ModelingCmdMeta::from_args(exec_state, args),
1299 ModelingCmd::from(
1300 mcmd::EntityCircularPattern::builder()
1301 .axis(kcmc::shared::Point3d::from(data.axis()))
1302 .entity_id(if data.use_original() {
1303 geometry.topology_id()
1304 } else {
1305 geometry_id
1306 })
1307 .center(kcmc::shared::Point3d {
1308 x: LengthUnit(center[0]),
1309 y: LengthUnit(center[1]),
1310 z: LengthUnit(center[2]),
1311 })
1312 .num_repetitions(num_repetitions)
1313 .arc_degrees(data.arc_degrees().unwrap_or(360.0))
1314 .rotate_duplicates(data.rotate_duplicates().unwrap_or(true))
1315 .build(),
1316 ),
1317 )
1318 .await?;
1319
1320 let mut mock_ids = Vec::new();
1323 let entity_ids = if let OkWebSocketResponseData::Modeling {
1324 modeling_response: OkModelingCmdResponse::EntityCircularPattern(pattern_info),
1325 } = &resp
1326 {
1327 &pattern_info.entity_face_edge_ids.iter().map(|e| e.object_id).collect()
1328 } else if args.ctx.no_engine_commands().await {
1329 mock_ids.reserve(num_repetitions as usize);
1330 for _ in 0..num_repetitions {
1331 mock_ids.push(exec_state.next_uuid());
1332 }
1333 &mock_ids
1334 } else {
1335 return Err(KclError::new_engine(KclErrorDetails::new(
1336 format!("EntityCircularPattern response was not as expected: {resp:?}"),
1337 vec![args.source_range],
1338 )));
1339 };
1340
1341 let mut geometries = vec![geometry.clone()];
1342 for id in entity_ids.iter().copied() {
1343 let mut new_geometry = geometry.clone();
1344 new_geometry.set_id(id);
1345 new_geometry.set_artifact_id(id);
1346 geometries.push(new_geometry);
1347 }
1348 Ok(geometries)
1349}