1use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kcmc::ok_response::OkModelingCmdResponse;
7use kcmc::websocket::OkWebSocketResponseData;
8use kittycad_modeling_cmds as kcmc;
9use serde::Deserialize;
10use serde::Serialize;
11use uuid::Uuid;
12
13use crate::SourceRange;
14use crate::errors::KclError;
15use crate::errors::KclErrorDetails;
16use crate::execution::BoundedEdge;
17use crate::execution::EdgeRefactorMeta;
18use crate::execution::EdgeRefactorStdlibFn;
19use crate::execution::ExecState;
20use crate::execution::ExtrudeSurface;
21use crate::execution::KclObjectFields;
22use crate::execution::KclValue;
23use crate::execution::ModelingCmdMeta;
24use crate::execution::PendingEdgeRefactorMeta;
25use crate::execution::Solid;
26use crate::execution::TagIdentifier;
27use crate::execution::types::ArrayLen;
28use crate::execution::types::RuntimeType;
29use crate::std::Args;
30use crate::std::args::TyF64;
31use crate::std::fillet::EdgeReference;
32use crate::std::sketch::FaceTag;
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, ts_rs::TS)]
36#[serde(untagged)]
37pub enum TagOrUuid {
38 Uuid(Uuid),
39 Tag(Box<TagIdentifier>),
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS)]
44#[serde(rename_all = "camelCase")]
45pub struct UnresolvedEdgeSpecifier {
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub side_faces: Vec<TagOrUuid>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub end_faces: Vec<TagOrUuid>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub index: Option<u32>,
52}
53
54pub(crate) async fn get_face_ids_for_edge(
58 exec_state: &mut ExecState,
59 object_id: Uuid,
60 edge_id: Uuid,
61 args: &Args,
62) -> Result<Vec<Uuid>, KclError> {
63 if args.ctx.no_engine_commands().await {
64 return Ok(vec![exec_state.next_uuid(), exec_state.next_uuid()]);
67 }
68
69 let resp = exec_state
70 .send_untracked_modeling_cmd(
71 ModelingCmdMeta::from_args(exec_state, args),
72 ModelingCmd::from(
73 mcmd::Solid3dGetAllEdgeFaces::builder()
74 .object_id(object_id)
75 .edge_id(edge_id)
76 .build(),
77 ),
78 )
79 .await?;
80 let OkWebSocketResponseData::Modeling {
81 modeling_response: OkModelingCmdResponse::Solid3dGetAllEdgeFaces(info),
82 } = &resp
83 else {
84 return Err(KclError::new_engine(KclErrorDetails::new(
85 format!("Solid3dGetAllEdgeFaces response was not as expected: {resp:?}"),
86 vec![args.source_range],
87 )));
88 };
89 if info.faces.is_empty() || info.faces.len() > 2 {
90 return Err(KclError::new_engine(KclErrorDetails::new(
91 format!(
92 "Solid3dGetAllEdgeFaces returned {} face(s) for edge {edge_id}, expected 1 or 2",
93 info.faces.len()
94 ),
95 vec![args.source_range],
96 )));
97 }
98 Ok(info.faces.clone())
99}
100
101pub(crate) async fn get_refactor_meta_for_edge(
102 exec_state: &mut ExecState,
103 edge_id: Uuid,
104 args: &Args,
105 source_range: SourceRange,
106 stdlib_fn: EdgeRefactorStdlibFn,
107) -> Result<EdgeRefactorMeta, KclError> {
108 if args.ctx.no_engine_commands().await {
109 return Ok(EdgeRefactorMeta {
110 edge_id,
111 face_ids: [exec_state.next_uuid(), exec_state.next_uuid()],
112 end_face_ids: Vec::new(),
113 source_range,
114 stdlib_fn,
115 });
116 }
117
118 let query = serde_json::from_value::<mcmd::QueryEntityType>(serde_json::json!({
119 "entity_id": edge_id,
120 }))
121 .map_err(|error| {
122 KclError::new_engine(KclErrorDetails::new(
123 format!("Failed to construct QueryEntityType for edge metadata: {error}"),
124 vec![args.source_range],
125 ))
126 })?;
127 let response = exec_state
128 .send_untracked_modeling_cmd(ModelingCmdMeta::from_args(exec_state, args), ModelingCmd::from(query))
129 .await?;
130 let OkWebSocketResponseData::Modeling {
131 modeling_response: OkModelingCmdResponse::QueryEntityType(info),
132 } = &response
133 else {
134 return Err(KclError::new_engine(KclErrorDetails::new(
135 format!("QueryEntityType response was not as expected: {response:?}"),
136 vec![args.source_range],
137 )));
138 };
139 let kcmc::shared::EntityReference::Edge { inner, .. } = &info.reference else {
140 return Err(KclError::new_engine(KclErrorDetails::new(
141 format!("QueryEntityType returned a non-edge reference for edge {edge_id}"),
142 vec![args.source_range],
143 )));
144 };
145 let [first, second] = inner.side_faces.as_slice() else {
146 return Err(KclError::new_engine(KclErrorDetails::new(
147 format!(
148 "QueryEntityType returned {} side faces for edge {edge_id}, expected exactly 2",
149 inner.side_faces.len()
150 ),
151 vec![args.source_range],
152 )));
153 };
154
155 Ok(EdgeRefactorMeta {
156 edge_id,
157 face_ids: [*first, *second],
158 end_face_ids: inner.end_faces.clone(),
159 source_range,
160 stdlib_fn,
161 })
162}
163
164pub(crate) async fn record_refactor_meta_for_consumed_edge(
165 exec_state: &mut ExecState,
166 edge_id: Uuid,
167 argument_source_range: SourceRange,
168 args: &Args,
169) {
170 let Some(pending) = exec_state.pending_edge_refactor_meta(edge_id, argument_source_range) else {
171 return;
172 };
173 let Ok(meta) = get_refactor_meta_for_edge(exec_state, edge_id, args, pending.source_range, pending.stdlib_fn).await
174 else {
175 return;
176 };
177 exec_state.record_edge_refactor_meta(meta);
178}
179
180pub(crate) async fn record_refactor_meta_for_direct_edge(
181 exec_state: &mut ExecState,
182 edge_id: Uuid,
183 source_range: SourceRange,
184 args: &Args,
185) -> Result<(), KclError> {
186 if !args.ctx.no_engine_commands().await {
187 exec_state
188 .flush_batch(ModelingCmdMeta::from_args(exec_state, args), false)
189 .await?;
190 }
191
192 if let Ok(meta) = get_refactor_meta_for_edge(
193 exec_state,
194 edge_id,
195 args,
196 source_range,
197 EdgeRefactorStdlibFn::DirectEdgeTag,
198 )
199 .await
200 {
201 exec_state.record_edge_refactor_meta(meta);
202 }
203
204 Ok(())
205}
206
207pub(crate) async fn record_refactor_meta_for_direct_tag(
208 exec_state: &mut ExecState,
209 tag: &TagIdentifier,
210 source_range: SourceRange,
211 args: &Args,
212) -> Result<(), KclError> {
213 let Some(tag_info) = tag.get_cur_info() else {
214 return Ok(());
215 };
216 if !args.ctx.no_engine_commands().await {
217 exec_state
218 .flush_batch(ModelingCmdMeta::from_args(exec_state, args), false)
219 .await?;
220 }
221 if args.ctx.no_engine_commands().await {
222 let face_ids = [exec_state.next_uuid(), exec_state.next_uuid()];
223 exec_state.record_edge_refactor_meta(EdgeRefactorMeta {
224 edge_id: tag_info.id,
225 face_ids,
226 end_face_ids: Vec::new(),
227 source_range,
228 stdlib_fn: EdgeRefactorStdlibFn::DirectEdgeTag,
229 });
230 return Ok(());
231 }
232
233 let response = exec_state
234 .send_untracked_modeling_cmd(
235 ModelingCmdMeta::from_args(exec_state, args),
236 ModelingCmd::from(
237 mcmd::Solid3dGetAdjacencyInfo::builder()
238 .object_id(tag_info.geometry.id())
239 .edge_id(tag_info.id)
240 .build(),
241 ),
242 )
243 .await;
244 let Ok(OkWebSocketResponseData::Modeling {
245 modeling_response: OkModelingCmdResponse::Solid3dGetAdjacencyInfo(info),
246 }) = response
247 else {
248 return Ok(());
249 };
250 let Some(edge_info) = info
251 .edges
252 .iter()
253 .filter_map(|edge| edge.original_info.as_ref())
254 .find(|edge| edge.edge_id == tag_info.id)
255 else {
256 return Ok(());
257 };
258 let [first, second] = edge_info.faces.as_slice() else {
259 return Ok(());
260 };
261 exec_state.record_edge_refactor_meta(EdgeRefactorMeta {
262 edge_id: tag_info.id,
263 face_ids: [*first, *second],
264 end_face_ids: Vec::new(),
265 source_range,
266 stdlib_fn: EdgeRefactorStdlibFn::DirectEdgeTag,
267 });
268 Ok(())
269}
270
271fn record_pending_edge_refactor_meta(
272 exec_state: &mut ExecState,
273 edge_id: Uuid,
274 stdlib_fn: EdgeRefactorStdlibFn,
275 args: &Args,
276) {
277 exec_state.record_pending_edge_refactor_meta(PendingEdgeRefactorMeta {
278 edge_id,
279 source_range: args.source_range,
280 stdlib_fn,
281 });
282}
283
284pub(super) fn check_tag_not_ambiguous(tag: &TagIdentifier, args: &Args) -> Result<(), KclError> {
286 let all_infos = tag.get_all_cur_info();
287 if all_infos.len() > 1 {
288 return Err(KclError::new_semantic(KclErrorDetails::new(
289 format!(
290 "Tag `{}` is ambiguous: it maps to {} edges in the region. Use a more specific reference.",
291 tag.value,
292 all_infos.len()
293 ),
294 vec![args.source_range],
295 )));
296 }
297 Ok(())
298}
299
300pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
302 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
303
304 let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
305 Ok(KclValue::Uuid {
306 value: edge,
307 meta: vec![args.source_range.into()],
308 })
309}
310
311async fn inner_get_opposite_edge(
312 edge: TagIdentifier,
313 exec_state: &mut ExecState,
314 args: Args,
315) -> Result<Uuid, KclError> {
316 check_tag_not_ambiguous(&edge, &args)?;
317 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
321 if args.ctx.no_engine_commands().await {
322 return Ok(exec_state.next_uuid());
323 }
324
325 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
326 let tagged_path_id = tagged_path.id;
327 let sketch_id = tagged_path.geometry.id();
328
329 let resp = exec_state
330 .send_modeling_cmd(
331 ModelingCmdMeta::from_args(exec_state, &args),
332 ModelingCmd::from(
333 mcmd::Solid3dGetOppositeEdge::builder()
334 .edge_id(tagged_path_id)
335 .object_id(sketch_id)
336 .face_id(face_id)
337 .build(),
338 ),
339 )
340 .await?;
341 let OkWebSocketResponseData::Modeling {
342 modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
343 } = &resp
344 else {
345 return Err(KclError::new_engine(KclErrorDetails::new(
346 format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {resp:?}"),
347 vec![args.source_range],
348 )));
349 };
350
351 let edge_id = opposite_edge.edge;
352
353 if let Ok(meta) = get_refactor_meta_for_edge(
354 exec_state,
355 edge_id,
356 &args,
357 args.source_range,
358 EdgeRefactorStdlibFn::GetOppositeEdge,
359 )
360 .await
361 {
362 exec_state.record_edge_refactor_meta(meta);
363 } else {
364 record_pending_edge_refactor_meta(exec_state, edge_id, EdgeRefactorStdlibFn::GetOppositeEdge, &args);
365 }
366 Ok(edge_id)
367}
368
369pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
371 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
372
373 let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
374 Ok(KclValue::Uuid {
375 value: edge,
376 meta: vec![args.source_range.into()],
377 })
378}
379
380async fn inner_get_next_adjacent_edge(
381 edge: TagIdentifier,
382 exec_state: &mut ExecState,
383 args: Args,
384) -> Result<Uuid, KclError> {
385 check_tag_not_ambiguous(&edge, &args)?;
386 if args.ctx.no_engine_commands().await {
387 return Ok(exec_state.next_uuid());
388 }
389 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
390
391 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
392 let tagged_path_id = tagged_path.id;
393 let sketch_id = tagged_path.geometry.id();
394
395 let resp = exec_state
396 .send_modeling_cmd(
397 ModelingCmdMeta::from_args(exec_state, &args),
398 ModelingCmd::from(
399 mcmd::Solid3dGetNextAdjacentEdge::builder()
400 .edge_id(tagged_path_id)
401 .object_id(sketch_id)
402 .face_id(face_id)
403 .build(),
404 ),
405 )
406 .await?;
407
408 let OkWebSocketResponseData::Modeling {
409 modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
410 } = &resp
411 else {
412 return Err(KclError::new_engine(KclErrorDetails::new(
413 format!("mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {resp:?}"),
414 vec![args.source_range],
415 )));
416 };
417
418 let edge_id = adjacent_edge.edge.ok_or_else(|| {
419 KclError::new_type(KclErrorDetails::new(
420 format!("No edge found next adjacent to tag: `{}`", edge.value),
421 vec![args.source_range],
422 ))
423 })?;
424
425 if let Ok(meta) = get_refactor_meta_for_edge(
426 exec_state,
427 edge_id,
428 &args,
429 args.source_range,
430 EdgeRefactorStdlibFn::GetNextAdjacentEdge,
431 )
432 .await
433 {
434 exec_state.record_edge_refactor_meta(meta);
435 } else {
436 record_pending_edge_refactor_meta(exec_state, edge_id, EdgeRefactorStdlibFn::GetNextAdjacentEdge, &args);
437 }
438 Ok(edge_id)
439}
440
441pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
443 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
444
445 let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
446 Ok(KclValue::Uuid {
447 value: edge,
448 meta: vec![args.source_range.into()],
449 })
450}
451
452async fn inner_get_previous_adjacent_edge(
453 edge: TagIdentifier,
454 exec_state: &mut ExecState,
455 args: Args,
456) -> Result<Uuid, KclError> {
457 check_tag_not_ambiguous(&edge, &args)?;
458 if args.ctx.no_engine_commands().await {
459 return Ok(exec_state.next_uuid());
460 }
461 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
462
463 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
464 let tagged_path_id = tagged_path.id;
465 let sketch_id = tagged_path.geometry.id();
466
467 let resp = exec_state
468 .send_modeling_cmd(
469 ModelingCmdMeta::from_args(exec_state, &args),
470 ModelingCmd::from(
471 mcmd::Solid3dGetPrevAdjacentEdge::builder()
472 .edge_id(tagged_path_id)
473 .object_id(sketch_id)
474 .face_id(face_id)
475 .build(),
476 ),
477 )
478 .await?;
479 let OkWebSocketResponseData::Modeling {
480 modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
481 } = &resp
482 else {
483 return Err(KclError::new_engine(KclErrorDetails::new(
484 format!("mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {resp:?}"),
485 vec![args.source_range],
486 )));
487 };
488
489 let edge_id = adjacent_edge.edge.ok_or_else(|| {
490 KclError::new_type(KclErrorDetails::new(
491 format!("No edge found previous adjacent to tag: `{}`", edge.value),
492 vec![args.source_range],
493 ))
494 })?;
495
496 if let Ok(meta) = get_refactor_meta_for_edge(
497 exec_state,
498 edge_id,
499 &args,
500 args.source_range,
501 EdgeRefactorStdlibFn::GetPreviousAdjacentEdge,
502 )
503 .await
504 {
505 exec_state.record_edge_refactor_meta(meta);
506 } else {
507 record_pending_edge_refactor_meta(
508 exec_state,
509 edge_id,
510 EdgeRefactorStdlibFn::GetPreviousAdjacentEdge,
511 &args,
512 );
513 }
514 Ok(edge_id)
515}
516
517pub async fn get_common_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
519 let faces: Vec<FaceTag> = args.get_kw_arg(
520 "faces",
521 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Known(2)),
522 exec_state,
523 )?;
524
525 fn into_tag(face: FaceTag, source_range: SourceRange) -> Result<TagIdentifier, KclError> {
526 match face {
527 FaceTag::StartOrEnd(_) => Err(KclError::new_type(KclErrorDetails::new(
528 "getCommonEdge requires a tagged face, it cannot use `START` or `END` faces".to_owned(),
529 vec![source_range],
530 ))),
531 FaceTag::Tag(tag_identifier) => Ok(*tag_identifier),
532 }
533 }
534
535 let [face1, face2]: [FaceTag; 2] = faces.try_into().map_err(|_: Vec<FaceTag>| {
536 KclError::new_type(KclErrorDetails::new(
537 "getCommonEdge requires exactly two tags for faces".to_owned(),
538 vec![args.source_range],
539 ))
540 })?;
541
542 let face1 = into_tag(face1, args.source_range)?;
543 let face2 = into_tag(face2, args.source_range)?;
544
545 let edge = inner_get_common_edge(face1, face2, exec_state, args.clone()).await?;
546 Ok(KclValue::Uuid {
547 value: edge,
548 meta: vec![args.source_range.into()],
549 })
550}
551
552async fn inner_get_common_edge(
553 face1: TagIdentifier,
554 face2: TagIdentifier,
555 exec_state: &mut ExecState,
556 args: Args,
557) -> Result<Uuid, KclError> {
558 check_tag_not_ambiguous(&face1, &args)?;
559 check_tag_not_ambiguous(&face2, &args)?;
560 let id = exec_state.next_uuid();
561 if args.ctx.no_engine_commands().await {
562 return Ok(id);
563 }
564
565 let first_face_id = args.get_adjacent_face_to_tag(exec_state, &face1, false).await?;
566 let second_face_id = args.get_adjacent_face_to_tag(exec_state, &face2, false).await?;
567
568 let first_tagged_path = args.get_tag_engine_info(exec_state, &face1)?.clone();
569 let second_tagged_path = args.get_tag_engine_info(exec_state, &face2)?;
570
571 if first_tagged_path.geometry.id() != second_tagged_path.geometry.id() {
572 return Err(KclError::new_type(KclErrorDetails::new(
573 "getCommonEdge requires the faces to be in the same original sketch".to_string(),
574 vec![args.source_range],
575 )));
576 }
577
578 if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
583 exec_state
584 .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
585 .await?;
586 } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
587 exec_state
588 .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
589 .await?;
590 }
591
592 let resp = exec_state
593 .send_modeling_cmd(
594 ModelingCmdMeta::from_args_id(exec_state, &args, id),
595 ModelingCmd::from(
596 mcmd::Solid3dGetCommonEdge::builder()
597 .object_id(first_tagged_path.geometry.id())
598 .face_ids([first_face_id, second_face_id])
599 .build(),
600 ),
601 )
602 .await?;
603 let OkWebSocketResponseData::Modeling {
604 modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
605 } = &resp
606 else {
607 return Err(KclError::new_engine(KclErrorDetails::new(
608 format!("mcmd::Solid3dGetCommonEdge response was not as expected: {resp:?}"),
609 vec![args.source_range],
610 )));
611 };
612
613 let edge_id = common_edge.edge.ok_or_else(|| {
614 KclError::new_type(KclErrorDetails::new(
615 format!(
616 "No common edge was found between `{}` and `{}`",
617 face1.value, face2.value
618 ),
619 vec![args.source_range],
620 ))
621 })?;
622
623 let meta = get_refactor_meta_for_edge(
624 exec_state,
625 edge_id,
626 &args,
627 args.source_range,
628 EdgeRefactorStdlibFn::GetCommonEdge,
629 )
630 .await
631 .unwrap_or(EdgeRefactorMeta {
632 edge_id,
633 face_ids: [first_face_id, second_face_id],
634 end_face_ids: Vec::new(),
635 source_range: args.source_range,
636 stdlib_fn: EdgeRefactorStdlibFn::GetCommonEdge,
637 });
638 exec_state.record_edge_refactor_meta(meta);
639 Ok(edge_id)
640}
641
642pub async fn get_bounded_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
643 let face = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
644 let edge_val = args.get_kw_arg("edge", &RuntimeType::any(), exec_state)?;
645 let lower_bound = args.get_kw_arg_opt("lowerBound", &RuntimeType::num_any(), exec_state)?;
646 let upper_bound = args.get_kw_arg_opt("upperBound", &RuntimeType::num_any(), exec_state)?;
647
648 let bounded_edge = match &edge_val {
649 KclValue::Uuid { value, .. } => {
650 inner_get_bounded_edge_with_id(face, EdgeReference::Uuid(*value), lower_bound, upper_bound, exec_state, args.clone()).await?
651 }
652 KclValue::TagIdentifier(tag) => {
653 inner_get_bounded_edge_with_id(face, EdgeReference::Tag(tag.clone()), lower_bound, upper_bound, exec_state, args.clone()).await?
654 }
655 KclValue::Object { value: obj, .. } => {
656 let spec = parse_edge_specifier_object(obj, &args)?;
657 inner_get_bounded_edge_with_specifier(face, spec, lower_bound, upper_bound, &args)?
658 }
659 _ => {
660 return Err(KclError::new_type(KclErrorDetails::new(
661 "edge must be a tagged edge, edge UUID, or edge specifier object (e.g. { sideFaces = [...], endFaces = [...], index = 0 })".to_owned(),
662 vec![args.source_range],
663 )))
664 }
665 };
666 Ok(KclValue::BoundedEdge {
667 value: bounded_edge,
668 meta: vec![args.source_range.into()],
669 })
670}
671
672fn tag_or_uuid_from_value(
673 value: &KclValue,
674 field_name: &str,
675 source_range: SourceRange,
676) -> Result<TagOrUuid, KclError> {
677 match value {
678 KclValue::Uuid { value, .. } => Ok(TagOrUuid::Uuid(*value)),
679 KclValue::TagIdentifier(tag) => Ok(TagOrUuid::Tag(tag.clone())),
680 _ => Err(KclError::new_type(KclErrorDetails::new(
681 format!("{field_name} elements must be tags or UUIDs"),
682 vec![source_range],
683 ))),
684 }
685}
686
687fn parse_tag_or_uuid_array(
688 obj: &KclObjectFields,
689 field_name: &str,
690 required: bool,
691 source_range: SourceRange,
692) -> Result<Vec<TagOrUuid>, KclError> {
693 let Some(value) = obj.get(field_name) else {
694 return if required {
695 Err(KclError::new_type(KclErrorDetails::new(
696 format!("edge specifier object must have {field_name}"),
697 vec![source_range],
698 )))
699 } else {
700 Ok(Vec::new())
701 };
702 };
703 let values = value.as_slice().ok_or_else(|| {
704 KclError::new_type(KclErrorDetails::new(
705 format!("{field_name} must be an array"),
706 vec![source_range],
707 ))
708 })?;
709 values
710 .iter()
711 .map(|value| tag_or_uuid_from_value(value, field_name, source_range))
712 .collect()
713}
714
715fn parse_edge_specifier_index(obj: &KclObjectFields, source_range: SourceRange) -> Result<Option<u32>, KclError> {
716 let Some(index) = obj.get("index") else {
717 return Ok(None);
718 };
719 let KclValue::Number { value, .. } = index else {
720 return Err(KclError::new_type(KclErrorDetails::new(
721 "edge specifier 'index' must be a non-negative integer".to_owned(),
722 vec![source_range],
723 )));
724 };
725 if !value.is_finite() || value.fract() != 0.0 || *value < 0.0 || *value > u32::MAX as f64 {
726 return Err(KclError::new_type(KclErrorDetails::new(
727 "edge specifier 'index' must be a non-negative integer".to_owned(),
728 vec![source_range],
729 )));
730 }
731 Ok(Some(*value as u32))
732}
733
734pub(crate) fn is_edge_specifier_object(value: &KclValue) -> bool {
735 matches!(value, KclValue::Object { value, .. } if value.contains_key("sideFaces"))
736}
737
738pub(crate) fn parse_edge_specifier_value(value: &KclValue, args: &Args) -> Result<UnresolvedEdgeSpecifier, KclError> {
739 parse_edge_specifier_value_at(value, args.source_range)
740}
741
742pub(crate) fn parse_edge_specifier_value_at(
743 value: &KclValue,
744 source_range: SourceRange,
745) -> Result<UnresolvedEdgeSpecifier, KclError> {
746 let KclValue::Object { value: obj, .. } = value else {
747 return Err(KclError::new_type(KclErrorDetails::new(
748 "edge specifier must be an object with 'sideFaces'".to_owned(),
749 vec![source_range],
750 )));
751 };
752 parse_edge_specifier_object_at(obj, source_range)
753}
754
755pub(crate) fn parse_edge_specifier_object(
757 obj: &KclObjectFields,
758 args: &Args,
759) -> Result<UnresolvedEdgeSpecifier, KclError> {
760 parse_edge_specifier_object_at(obj, args.source_range)
761}
762
763pub(crate) fn parse_edge_specifier_object_at(
764 obj: &KclObjectFields,
765 source_range: SourceRange,
766) -> Result<UnresolvedEdgeSpecifier, KclError> {
767 let side_faces = parse_tag_or_uuid_array(obj, "sideFaces", true, source_range)?;
768 if side_faces.is_empty() {
769 return Err(KclError::new_semantic(KclErrorDetails::new(
770 "sideFaces must be an array of at least one face, but zero were given".to_owned(),
771 vec![source_range],
772 )));
773 }
774 let end_faces = parse_tag_or_uuid_array(obj, "endFaces", false, source_range)?;
775 let index = parse_edge_specifier_index(obj, source_range)?;
776 Ok(UnresolvedEdgeSpecifier {
777 side_faces,
778 end_faces,
779 index,
780 })
781}
782
783async fn resolve_as_face_id(value: &TagOrUuid, exec_state: &mut ExecState, args: &Args) -> Result<Uuid, KclError> {
784 match value {
785 TagOrUuid::Uuid(uuid) => Ok(*uuid),
786 TagOrUuid::Tag(tag) => {
787 FaceTag::Tag(tag.clone())
788 .get_face_id_from_tag(exec_state, args, false)
789 .await
790 }
791 }
792}
793
794async fn resolve_as_face_ids(
795 value: &TagOrUuid,
796 solid: Option<&Solid>,
797 exec_state: &mut ExecState,
798 args: &Args,
799) -> Result<Vec<Uuid>, KclError> {
800 match value {
801 TagOrUuid::Uuid(uuid) => Ok(vec![*uuid]),
802 TagOrUuid::Tag(tag) => {
803 let infos = tag.get_all_cur_info();
804 if !infos.is_empty() {
805 let face_ids = infos
806 .iter()
807 .map(|info| {
808 info.surface
809 .as_ref()
810 .map(ExtrudeSurface::face_id)
811 .or_else(|| solid.and_then(|solid| face_id_for_tag_info_from_solid(info.id, solid)))
812 })
813 .collect::<Option<Vec<_>>>();
814 if let Some(face_ids) = face_ids {
815 return Ok(face_ids);
816 }
817 }
818
819 Ok(vec![resolve_as_face_id(value, exec_state, args).await?])
820 }
821 }
822}
823
824fn face_id_for_tag_info_from_solid(tag_info_id: Uuid, solid: &Solid) -> Option<Uuid> {
825 solid
826 .value
827 .iter()
828 .find(|surface| surface.get_id() == tag_info_id)
829 .map(ExtrudeSurface::face_id)
830}
831
832async fn resolve_as_adjacent_face_or_tag_id(
833 value: &TagOrUuid,
834 exec_state: &mut ExecState,
835 args: &Args,
836) -> Result<Uuid, KclError> {
837 match value {
838 TagOrUuid::Uuid(uuid) => Ok(*uuid),
839 TagOrUuid::Tag(tag) => match args.get_adjacent_face_to_tag(exec_state, tag, false).await {
840 Ok(face_id) => Ok(face_id),
841 Err(_) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
842 },
843 }
844}
845
846async fn resolve_as_edge_faces(
847 value: &TagOrUuid,
848 object_id: Uuid,
849 exec_state: &mut ExecState,
850 args: &Args,
851) -> Result<Vec<Uuid>, KclError> {
852 match value {
853 TagOrUuid::Uuid(uuid) => Ok(vec![*uuid]),
854 TagOrUuid::Tag(tag) => {
855 let edge_id = args.get_tag_engine_info(exec_state, tag)?.id;
856 get_face_ids_for_edge(exec_state, object_id, edge_id, args).await
857 }
858 }
859}
860
861pub(crate) async fn resolve_edge_specifier_with_face_tags(
862 unresolved: &UnresolvedEdgeSpecifier,
863 solid: Option<&Solid>,
864 exec_state: &mut ExecState,
865 args: &Args,
866) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
867 let mut references = resolve_edge_specifiers_with_face_tags(unresolved, solid, exec_state, args).await?;
868 if references.len() != 1 {
869 return Err(KclError::new_semantic(KclErrorDetails::new(
870 "edge specifier resolved to multiple edge references where exactly one was expected".to_owned(),
871 vec![args.source_range],
872 )));
873 }
874 Ok(references.remove(0))
875}
876
877const MAX_EDGE_COMBINATIONS: usize = 256;
878
879fn combination_count(group_sizes: impl IntoIterator<Item = usize>) -> usize {
883 group_sizes.into_iter().fold(1, usize::saturating_mul)
884}
885
886fn edge_combinations_exceed_limit(side_face_count: usize, end_face_count: usize) -> bool {
891 side_face_count > MAX_EDGE_COMBINATIONS
892 || end_face_count > MAX_EDGE_COMBINATIONS
893 || side_face_count.saturating_mul(end_face_count) > MAX_EDGE_COMBINATIONS
894}
895
896async fn resolve_edge_specifiers_with_face_tags(
897 unresolved: &UnresolvedEdgeSpecifier,
898 solid: Option<&Solid>,
899 exec_state: &mut ExecState,
900 args: &Args,
901) -> Result<Vec<kcmc::shared::EdgeSpecifier>, KclError> {
902 let mut side_face_groups = Vec::with_capacity(unresolved.side_faces.len());
903 for value in &unresolved.side_faces {
904 side_face_groups.push(resolve_as_face_ids(value, solid, exec_state, args).await?);
905 }
906 let mut end_face_groups = Vec::with_capacity(unresolved.end_faces.len());
907 for value in &unresolved.end_faces {
908 end_face_groups.push(resolve_as_face_ids(value, solid, exec_state, args).await?);
909 }
910
911 let side_face_count = combination_count(side_face_groups.iter().map(Vec::len));
915 let end_face_count = combination_count(end_face_groups.iter().map(Vec::len));
916 if edge_combinations_exceed_limit(side_face_count, end_face_count) {
917 return Err(KclError::new_semantic(KclErrorDetails::new(
918 "This edge specifier is too ambiguous. The maximum number of effective edges specified has been exceeded. Either specify fewer faces or use faces that have been split fewer times.".to_owned(),
919 vec![args.source_range],
920 )));
921 }
922
923 let side_face_combinations = face_id_combinations(&side_face_groups);
928 let end_face_combinations = face_id_combinations(&end_face_groups);
929 let mut references = Vec::with_capacity(side_face_combinations.len() * end_face_combinations.len());
930 for side_faces in side_face_combinations {
931 for end_faces in &end_face_combinations {
932 references.push(
933 kcmc::shared::EdgeSpecifier::builder()
934 .side_faces(side_faces.clone())
935 .end_faces(end_faces.clone())
936 .maybe_index(unresolved.index)
937 .build(),
938 );
939 }
940 }
941 if references.len() > 1 && unresolved.index.is_some() {
944 return Err(KclError::new_semantic(KclErrorDetails::new(
945 "You tried to use an index with sideFaces or endFaces that were split, which isn't supported yet. Please report this to Zoo and include your KCL to help improve this.".to_owned(),
946 vec![args.source_range],
947 )));
948 }
949 Ok(references)
950}
951
952fn face_id_combinations(groups: &[Vec<Uuid>]) -> Vec<Vec<Uuid>> {
960 if groups.is_empty() {
961 return vec![Vec::new()];
963 }
964
965 let mut combinations = vec![Vec::new()];
966 for group in groups {
967 let mut next = Vec::with_capacity(combinations.len() * group.len());
968 for combination in &combinations {
969 for face_id in group {
970 let mut new_combination = combination.clone();
971 new_combination.push(*face_id);
972 next.push(new_combination);
973 }
974 }
975 combinations = next;
976 }
977 combinations
978}
979
980pub(crate) async fn resolve_edge_specifier_with_adjacent_faces_or_tag_ids(
981 unresolved: &UnresolvedEdgeSpecifier,
982 exec_state: &mut ExecState,
983 args: &Args,
984) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
985 let mut side_faces = Vec::with_capacity(unresolved.side_faces.len());
986 for value in &unresolved.side_faces {
987 side_faces.push(resolve_as_adjacent_face_or_tag_id(value, exec_state, args).await?);
988 }
989 let mut end_faces = Vec::with_capacity(unresolved.end_faces.len());
990 for value in &unresolved.end_faces {
991 end_faces.push(resolve_as_adjacent_face_or_tag_id(value, exec_state, args).await?);
992 }
993 Ok(kcmc::shared::EdgeSpecifier::builder()
994 .side_faces(side_faces)
995 .end_faces(end_faces)
996 .maybe_index(unresolved.index)
997 .build())
998}
999
1000pub(crate) async fn parse_edge_refs_to_references(
1001 edge_refs: Vec<KclValue>,
1002 solid: Option<&Solid>,
1003 exec_state: &mut ExecState,
1004 args: &Args,
1005) -> Result<Vec<kcmc::shared::EdgeSpecifier>, KclError> {
1006 if edge_refs.is_empty() {
1007 return Err(KclError::new_semantic(KclErrorDetails::new(
1008 "You must provide at least one edge".to_owned(),
1009 vec![args.source_range],
1010 )));
1011 }
1012
1013 let mut edge_references = Vec::with_capacity(edge_refs.len());
1014 for edge_ref_value in &edge_refs {
1015 let spec = parse_edge_specifier_value(edge_ref_value, args)?;
1016 edge_references.extend(resolve_edge_specifiers_with_face_tags(&spec, solid, exec_state, args).await?);
1017 }
1018 Ok(edge_references)
1019}
1020
1021pub(super) fn face_id_from_first_side_face(
1025 spec: &UnresolvedEdgeSpecifier,
1026 exec_state: &mut ExecState,
1027 args: &Args,
1028) -> Result<Uuid, KclError> {
1029 let first = spec.side_faces.first().ok_or_else(|| {
1030 KclError::new_type(KclErrorDetails::new(
1031 "edge specifier must have at least one sideFace".to_owned(),
1032 vec![args.source_range],
1033 ))
1034 })?;
1035 match first {
1036 TagOrUuid::Uuid(u) => Ok(*u),
1037 TagOrUuid::Tag(t) => {
1038 let info = args.get_tag_engine_info(exec_state, t)?;
1039 Ok(info.geometry.id())
1040 }
1041 }
1042}
1043
1044pub(crate) async fn inner_get_bounded_edge_with_id(
1045 face: Solid,
1046 edge: EdgeReference,
1047 lower_bound: Option<TyF64>,
1048 upper_bound: Option<TyF64>,
1049 exec_state: &mut ExecState,
1050 args: Args,
1051) -> Result<BoundedEdge, KclError> {
1052 let (lb, ub) = bounds_from_opts(lower_bound, upper_bound, &args)?;
1053 let edge_id = edge.get_engine_id(exec_state, &args)?;
1054 Ok(BoundedEdge {
1055 face_id: face.id,
1056 edge_id: Some(edge_id),
1057 edge_specifier: None,
1058 lower_bound: lb,
1059 upper_bound: ub,
1060 })
1061}
1062
1063fn inner_get_bounded_edge_with_specifier(
1064 face: Solid,
1065 spec: UnresolvedEdgeSpecifier,
1066 lower_bound: Option<TyF64>,
1067 upper_bound: Option<TyF64>,
1068 args: &Args,
1069) -> Result<BoundedEdge, KclError> {
1070 let (lb, ub) = bounds_from_opts(lower_bound, upper_bound, args)?;
1071 Ok(BoundedEdge {
1072 face_id: face.id,
1073 edge_id: None,
1074 edge_specifier: Some(spec),
1075 lower_bound: lb,
1076 upper_bound: ub,
1077 })
1078}
1079
1080fn bounds_from_opts(
1081 lower_bound: Option<TyF64>,
1082 upper_bound: Option<TyF64>,
1083 args: &Args,
1084) -> Result<(f32, f32), KclError> {
1085 let lower_bound = if let Some(lower_bound) = lower_bound {
1086 let val = lower_bound.n as f32;
1087 if !(0.0..=1.0).contains(&val) {
1088 return Err(KclError::new_semantic(KclErrorDetails::new(
1089 format!(
1090 "Invalid value: lowerBound must be between 0.0 and 1.0, provided {}",
1091 val
1092 ),
1093 vec![args.source_range],
1094 )));
1095 }
1096 val
1097 } else {
1098 0.0_f32
1099 };
1100 let upper_bound = if let Some(upper_bound) = upper_bound {
1101 let val = upper_bound.n as f32;
1102 if !(0.0..=1.0).contains(&val) {
1103 return Err(KclError::new_semantic(KclErrorDetails::new(
1104 format!(
1105 "Invalid value: upperBound must be between 0.0 and 1.0, provided {}",
1106 val
1107 ),
1108 vec![args.source_range],
1109 )));
1110 }
1111 val
1112 } else {
1113 1.0_f32
1114 };
1115 Ok((lower_bound, upper_bound))
1116}
1117
1118pub(crate) async fn resolve_unresolved_edge_specifier(
1120 object_id: Uuid,
1121 unresolved: &UnresolvedEdgeSpecifier,
1122 exec_state: &mut ExecState,
1123 args: &Args,
1124) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
1125 let mut side_faces = Vec::new();
1126 for v in &unresolved.side_faces {
1127 side_faces.extend(resolve_as_edge_faces(v, object_id, exec_state, args).await?);
1128 }
1129 let mut end_faces = Vec::new();
1130 for v in &unresolved.end_faces {
1131 end_faces.extend(resolve_as_edge_faces(v, object_id, exec_state, args).await?);
1132 }
1133 Ok(kcmc::shared::EdgeSpecifier::builder()
1134 .side_faces(side_faces)
1135 .end_faces(end_faces)
1136 .maybe_index(unresolved.index)
1137 .build())
1138}
1139
1140#[cfg(test)]
1141mod tests {
1142 use uuid::Uuid;
1143
1144 use super::MAX_EDGE_COMBINATIONS;
1145 use super::combination_count;
1146 use super::edge_combinations_exceed_limit;
1147 use super::face_id_combinations;
1148 use crate::execution::MockConfig;
1149
1150 #[test]
1151 fn face_id_combinations_empty_input_is_one_empty_combination() {
1152 assert_eq!(face_id_combinations(&[]), vec![Vec::<Uuid>::new()]);
1156 }
1157
1158 #[test]
1159 fn face_id_combinations_empty_group_annihilates() {
1160 let a = Uuid::from_u128(1);
1162 assert_eq!(face_id_combinations(&[vec![a], vec![]]), Vec::<Vec<Uuid>>::new());
1163 }
1164
1165 #[test]
1166 fn face_id_combinations_is_ordered_cartesian_product() {
1167 let (a, b, c, d) = (
1168 Uuid::from_u128(1),
1169 Uuid::from_u128(2),
1170 Uuid::from_u128(3),
1171 Uuid::from_u128(4),
1172 );
1173 assert_eq!(
1174 face_id_combinations(&[vec![a, b], vec![c, d]]),
1175 vec![vec![a, c], vec![a, d], vec![b, c], vec![b, d]],
1176 );
1177 }
1178
1179 #[test]
1180 fn combination_count_is_product_of_group_sizes() {
1181 assert_eq!(combination_count(std::iter::empty::<usize>()), 1); assert_eq!(combination_count([1usize, 1, 1]), 1);
1183 assert_eq!(combination_count([2usize, 3, 4]), 24);
1184 }
1185
1186 #[test]
1187 fn combination_count_with_empty_group_is_zero() {
1188 assert_eq!(combination_count([5usize, 0, 5]), 0);
1189 }
1190
1191 #[test]
1192 fn combination_count_saturates_instead_of_overflowing() {
1193 assert_eq!(combination_count([usize::MAX, 2]), usize::MAX);
1196 assert_eq!(combination_count([usize::MAX, usize::MAX]), usize::MAX);
1197 }
1198
1199 #[test]
1200 fn within_limit_is_allowed() {
1201 assert!(!edge_combinations_exceed_limit(1, 1));
1202 assert!(!edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS, 1));
1204 assert!(!edge_combinations_exceed_limit(1, MAX_EDGE_COMBINATIONS));
1205 }
1206
1207 #[test]
1208 fn one_past_the_limit_on_a_single_axis_is_rejected() {
1209 assert!(edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS + 1, 1));
1210 assert!(edge_combinations_exceed_limit(1, MAX_EDGE_COMBINATIONS + 1));
1211 }
1212
1213 #[test]
1214 fn product_of_two_in_range_axes_still_exceeds_limit() {
1215 assert!(edge_combinations_exceed_limit(16, 17));
1218 }
1219
1220 #[test]
1221 fn large_axis_is_rejected_even_when_the_other_axis_is_zero() {
1222 assert_eq!((MAX_EDGE_COMBINATIONS + 1).saturating_mul(0), 0);
1226 assert!(edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS + 1, 0));
1227 assert!(edge_combinations_exceed_limit(0, MAX_EDGE_COMBINATIONS + 1));
1228 }
1229
1230 #[test]
1231 fn saturated_count_is_rejected_without_overflowing() {
1232 assert!(edge_combinations_exceed_limit(usize::MAX, 2));
1235 assert!(edge_combinations_exceed_limit(usize::MAX, usize::MAX));
1236 }
1237
1238 #[tokio::test(flavor = "multi_thread")]
1239 async fn mock_get_opposite_edge_rejects_unextruded_sketch_edge() {
1240 let code = r#"
1241profile = sketch(on = XY) {
1242 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
1243}
1244profileRegion = region(segments = [profile.circle1])
1245opposite = getOppositeEdge(profileRegion.tags.circle1)
1246"#;
1247 let program = crate::Program::parse_no_errs(code).unwrap();
1248 let ctx = crate::ExecutorContext::new_mock(None).await;
1249 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
1250 ctx.close().await;
1251
1252 assert!(
1253 err.error.message().contains("refers to a sketch edge")
1254 && err.error.message().contains("requires a face tag"),
1255 "{err:?}"
1256 );
1257 }
1258
1259 #[tokio::test(flavor = "multi_thread")]
1260 async fn mock_get_opposite_edge_accepts_extruded_sketch_edge() {
1261 let code = r#"
1262profile = sketch(on = XY) {
1263 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
1264}
1265profileRegion = region(segments = [profile.circle1])
1266body = extrude(profileRegion, length = 5)
1267opposite = getOppositeEdge(profileRegion.tags.circle1)
1268"#;
1269 let program = crate::Program::parse_no_errs(code).unwrap();
1270 let ctx = crate::ExecutorContext::new_mock(None).await;
1271 let outcome = ctx.run_mock(&program, &MockConfig::default()).await;
1272 ctx.close().await;
1273
1274 outcome.unwrap();
1275 }
1276}