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 let face_ids = [exec_state.next_uuid(), exec_state.next_uuid()];
110 return Ok(EdgeRefactorMeta {
111 edge_id,
112 face_ids,
113 end_face_ids: Vec::new(),
114 source_range,
115 stdlib_fn,
116 });
117 }
118
119 let query_entity_type = serde_json::from_value::<mcmd::QueryEntityType>(serde_json::json!({
120 "entity_id": edge_id,
121 }))
122 .map_err(|err| {
123 KclError::new_engine(KclErrorDetails::new(
124 format!("Failed to construct QueryEntityType command for edge refactor metadata: {err}"),
125 vec![args.source_range],
126 ))
127 })?;
128
129 let resp = exec_state
130 .send_untracked_modeling_cmd(
131 ModelingCmdMeta::from_args(exec_state, args),
132 ModelingCmd::from(query_entity_type),
133 )
134 .await?;
135
136 let OkWebSocketResponseData::Modeling {
137 modeling_response: OkModelingCmdResponse::QueryEntityType(info),
138 } = &resp
139 else {
140 return Err(KclError::new_engine(KclErrorDetails::new(
141 format!("QueryEntityType response was not as expected: {resp:?}"),
142 vec![args.source_range],
143 )));
144 };
145
146 let kcmc::shared::EntityReference::Edge { inner, .. } = &info.reference else {
147 return Err(KclError::new_engine(KclErrorDetails::new(
148 format!(
149 "QueryEntityType returned a non-edge reference for edge {edge_id}: {:?}",
150 info.reference
151 ),
152 vec![args.source_range],
153 )));
154 };
155
156 let [a, b] = inner.side_faces.as_slice() else {
157 return Err(KclError::new_engine(KclErrorDetails::new(
158 format!(
159 "QueryEntityType returned {} side face(s) for edge {edge_id}, expected exactly 2",
160 inner.side_faces.len()
161 ),
162 vec![args.source_range],
163 )));
164 };
165
166 Ok(EdgeRefactorMeta {
167 edge_id,
168 face_ids: [*a, *b],
169 end_face_ids: inner.end_faces.clone(),
170 source_range,
171 stdlib_fn,
172 })
173}
174
175fn record_pending_edge_refactor_meta(
176 exec_state: &mut ExecState,
177 edge_id: Uuid,
178 stdlib_fn: EdgeRefactorStdlibFn,
179 args: &Args,
180) {
181 exec_state.record_pending_edge_refactor_meta(PendingEdgeRefactorMeta {
182 edge_id,
183 source_range: args.source_range,
184 stdlib_fn,
185 });
186}
187
188pub(super) fn check_tag_not_ambiguous(tag: &TagIdentifier, args: &Args) -> Result<(), KclError> {
190 let all_infos = tag.get_all_cur_info();
191 if all_infos.len() > 1 {
192 return Err(KclError::new_semantic(KclErrorDetails::new(
193 format!(
194 "Tag `{}` is ambiguous: it maps to {} edges in the region. Use a more specific reference.",
195 tag.value,
196 all_infos.len()
197 ),
198 vec![args.source_range],
199 )));
200 }
201 Ok(())
202}
203
204pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
206 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
207
208 let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
209 Ok(KclValue::Uuid {
210 value: edge,
211 meta: vec![args.source_range.into()],
212 })
213}
214
215async fn inner_get_opposite_edge(
216 edge: TagIdentifier,
217 exec_state: &mut ExecState,
218 args: Args,
219) -> Result<Uuid, KclError> {
220 check_tag_not_ambiguous(&edge, &args)?;
221 if args.ctx.no_engine_commands().await {
222 return Ok(exec_state.next_uuid());
223 }
224 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
225
226 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
227 let tagged_path_id = tagged_path.id;
228 let sketch_id = tagged_path.geometry.id();
229
230 let resp = exec_state
231 .send_modeling_cmd(
232 ModelingCmdMeta::from_args(exec_state, &args),
233 ModelingCmd::from(
234 mcmd::Solid3dGetOppositeEdge::builder()
235 .edge_id(tagged_path_id)
236 .object_id(sketch_id)
237 .face_id(face_id)
238 .build(),
239 ),
240 )
241 .await?;
242 let OkWebSocketResponseData::Modeling {
243 modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
244 } = &resp
245 else {
246 return Err(KclError::new_engine(KclErrorDetails::new(
247 format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {resp:?}"),
248 vec![args.source_range],
249 )));
250 };
251
252 let edge_id = opposite_edge.edge;
253
254 record_pending_edge_refactor_meta(exec_state, edge_id, EdgeRefactorStdlibFn::GetOppositeEdge, &args);
255 Ok(edge_id)
256}
257
258pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
260 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
261
262 let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
263 Ok(KclValue::Uuid {
264 value: edge,
265 meta: vec![args.source_range.into()],
266 })
267}
268
269async fn inner_get_next_adjacent_edge(
270 edge: TagIdentifier,
271 exec_state: &mut ExecState,
272 args: Args,
273) -> Result<Uuid, KclError> {
274 check_tag_not_ambiguous(&edge, &args)?;
275 if args.ctx.no_engine_commands().await {
276 return Ok(exec_state.next_uuid());
277 }
278 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
279
280 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
281 let tagged_path_id = tagged_path.id;
282 let sketch_id = tagged_path.geometry.id();
283
284 let resp = exec_state
285 .send_modeling_cmd(
286 ModelingCmdMeta::from_args(exec_state, &args),
287 ModelingCmd::from(
288 mcmd::Solid3dGetNextAdjacentEdge::builder()
289 .edge_id(tagged_path_id)
290 .object_id(sketch_id)
291 .face_id(face_id)
292 .build(),
293 ),
294 )
295 .await?;
296
297 let OkWebSocketResponseData::Modeling {
298 modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
299 } = &resp
300 else {
301 return Err(KclError::new_engine(KclErrorDetails::new(
302 format!("mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {resp:?}"),
303 vec![args.source_range],
304 )));
305 };
306
307 let edge_id = adjacent_edge.edge.ok_or_else(|| {
308 KclError::new_type(KclErrorDetails::new(
309 format!("No edge found next adjacent to tag: `{}`", edge.value),
310 vec![args.source_range],
311 ))
312 })?;
313
314 record_pending_edge_refactor_meta(exec_state, edge_id, EdgeRefactorStdlibFn::GetNextAdjacentEdge, &args);
315 Ok(edge_id)
316}
317
318pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
320 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
321
322 let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
323 Ok(KclValue::Uuid {
324 value: edge,
325 meta: vec![args.source_range.into()],
326 })
327}
328
329async fn inner_get_previous_adjacent_edge(
330 edge: TagIdentifier,
331 exec_state: &mut ExecState,
332 args: Args,
333) -> Result<Uuid, KclError> {
334 check_tag_not_ambiguous(&edge, &args)?;
335 if args.ctx.no_engine_commands().await {
336 return Ok(exec_state.next_uuid());
337 }
338 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
339
340 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
341 let tagged_path_id = tagged_path.id;
342 let sketch_id = tagged_path.geometry.id();
343
344 let resp = exec_state
345 .send_modeling_cmd(
346 ModelingCmdMeta::from_args(exec_state, &args),
347 ModelingCmd::from(
348 mcmd::Solid3dGetPrevAdjacentEdge::builder()
349 .edge_id(tagged_path_id)
350 .object_id(sketch_id)
351 .face_id(face_id)
352 .build(),
353 ),
354 )
355 .await?;
356 let OkWebSocketResponseData::Modeling {
357 modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
358 } = &resp
359 else {
360 return Err(KclError::new_engine(KclErrorDetails::new(
361 format!("mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {resp:?}"),
362 vec![args.source_range],
363 )));
364 };
365
366 let edge_id = adjacent_edge.edge.ok_or_else(|| {
367 KclError::new_type(KclErrorDetails::new(
368 format!("No edge found previous adjacent to tag: `{}`", edge.value),
369 vec![args.source_range],
370 ))
371 })?;
372
373 record_pending_edge_refactor_meta(
374 exec_state,
375 edge_id,
376 EdgeRefactorStdlibFn::GetPreviousAdjacentEdge,
377 &args,
378 );
379 Ok(edge_id)
380}
381
382pub async fn get_common_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
384 let faces: Vec<FaceTag> = args.get_kw_arg(
385 "faces",
386 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Known(2)),
387 exec_state,
388 )?;
389
390 fn into_tag(face: FaceTag, source_range: SourceRange) -> Result<TagIdentifier, KclError> {
391 match face {
392 FaceTag::StartOrEnd(_) => Err(KclError::new_type(KclErrorDetails::new(
393 "getCommonEdge requires a tagged face, it cannot use `START` or `END` faces".to_owned(),
394 vec![source_range],
395 ))),
396 FaceTag::Tag(tag_identifier) => Ok(*tag_identifier),
397 }
398 }
399
400 let [face1, face2]: [FaceTag; 2] = faces.try_into().map_err(|_: Vec<FaceTag>| {
401 KclError::new_type(KclErrorDetails::new(
402 "getCommonEdge requires exactly two tags for faces".to_owned(),
403 vec![args.source_range],
404 ))
405 })?;
406
407 let face1 = into_tag(face1, args.source_range)?;
408 let face2 = into_tag(face2, args.source_range)?;
409
410 let edge = inner_get_common_edge(face1, face2, exec_state, args.clone()).await?;
411 Ok(KclValue::Uuid {
412 value: edge,
413 meta: vec![args.source_range.into()],
414 })
415}
416
417async fn inner_get_common_edge(
418 face1: TagIdentifier,
419 face2: TagIdentifier,
420 exec_state: &mut ExecState,
421 args: Args,
422) -> Result<Uuid, KclError> {
423 check_tag_not_ambiguous(&face1, &args)?;
424 check_tag_not_ambiguous(&face2, &args)?;
425 let id = exec_state.next_uuid();
426 if args.ctx.no_engine_commands().await {
427 return Ok(id);
428 }
429
430 let first_face_id = args.get_adjacent_face_to_tag(exec_state, &face1, false).await?;
431 let second_face_id = args.get_adjacent_face_to_tag(exec_state, &face2, false).await?;
432
433 let first_tagged_path = args.get_tag_engine_info(exec_state, &face1)?.clone();
434 let second_tagged_path = args.get_tag_engine_info(exec_state, &face2)?;
435
436 if first_tagged_path.geometry.id() != second_tagged_path.geometry.id() {
437 return Err(KclError::new_type(KclErrorDetails::new(
438 "getCommonEdge requires the faces to be in the same original sketch".to_string(),
439 vec![args.source_range],
440 )));
441 }
442
443 if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
448 exec_state
449 .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
450 .await?;
451 } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
452 exec_state
453 .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
454 .await?;
455 }
456
457 let resp = exec_state
458 .send_modeling_cmd(
459 ModelingCmdMeta::from_args_id(exec_state, &args, id),
460 ModelingCmd::from(
461 mcmd::Solid3dGetCommonEdge::builder()
462 .object_id(first_tagged_path.geometry.id())
463 .face_ids([first_face_id, second_face_id])
464 .build(),
465 ),
466 )
467 .await?;
468 let OkWebSocketResponseData::Modeling {
469 modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
470 } = &resp
471 else {
472 return Err(KclError::new_engine(KclErrorDetails::new(
473 format!("mcmd::Solid3dGetCommonEdge response was not as expected: {resp:?}"),
474 vec![args.source_range],
475 )));
476 };
477
478 let edge_id = common_edge.edge.ok_or_else(|| {
479 KclError::new_type(KclErrorDetails::new(
480 format!(
481 "No common edge was found between `{}` and `{}`",
482 face1.value, face2.value
483 ),
484 vec![args.source_range],
485 ))
486 })?;
487
488 exec_state.record_edge_refactor_meta(EdgeRefactorMeta {
489 edge_id,
490 face_ids: [first_face_id, second_face_id],
491 end_face_ids: Vec::new(),
492 source_range: args.source_range,
493 stdlib_fn: EdgeRefactorStdlibFn::GetCommonEdge,
494 });
495 Ok(edge_id)
496}
497
498pub async fn get_bounded_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
499 let face = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
500 let edge_val = args.get_kw_arg("edge", &RuntimeType::any(), exec_state)?;
501 let lower_bound = args.get_kw_arg_opt("lowerBound", &RuntimeType::num_any(), exec_state)?;
502 let upper_bound = args.get_kw_arg_opt("upperBound", &RuntimeType::num_any(), exec_state)?;
503
504 let bounded_edge = match &edge_val {
505 KclValue::Uuid { value, .. } => {
506 inner_get_bounded_edge_with_id(face, EdgeReference::Uuid(*value), lower_bound, upper_bound, exec_state, args.clone()).await?
507 }
508 KclValue::TagIdentifier(tag) => {
509 inner_get_bounded_edge_with_id(face, EdgeReference::Tag(tag.clone()), lower_bound, upper_bound, exec_state, args.clone()).await?
510 }
511 KclValue::Object { value: obj, .. } => {
512 let spec = parse_edge_specifier_object(obj, &args)?;
513 inner_get_bounded_edge_with_specifier(face, spec, lower_bound, upper_bound, &args)?
514 }
515 _ => {
516 return Err(KclError::new_type(KclErrorDetails::new(
517 "edge must be a tagged edge, edge UUID, or edge specifier object (e.g. { sideFaces = [...], endFaces = [...], index = 0 })".to_owned(),
518 vec![args.source_range],
519 )))
520 }
521 };
522 Ok(KclValue::BoundedEdge {
523 value: bounded_edge,
524 meta: vec![args.source_range.into()],
525 })
526}
527
528fn tag_or_uuid_from_value(value: &KclValue, field_name: &str, args: &Args) -> Result<TagOrUuid, KclError> {
529 match value {
530 KclValue::Uuid { value, .. } => Ok(TagOrUuid::Uuid(*value)),
531 KclValue::TagIdentifier(tag) => Ok(TagOrUuid::Tag(tag.clone())),
532 _ => Err(KclError::new_type(KclErrorDetails::new(
533 format!("{field_name} elements must be tags or UUIDs"),
534 vec![args.source_range],
535 ))),
536 }
537}
538
539fn parse_tag_or_uuid_array(
540 obj: &KclObjectFields,
541 field_name: &str,
542 required: bool,
543 args: &Args,
544) -> Result<Vec<TagOrUuid>, KclError> {
545 let Some(value) = obj.get(field_name) else {
546 return if required {
547 Err(KclError::new_type(KclErrorDetails::new(
548 format!("edge specifier object must have {field_name}"),
549 vec![args.source_range],
550 )))
551 } else {
552 Ok(Vec::new())
553 };
554 };
555 let values = value.as_slice().ok_or_else(|| {
556 KclError::new_type(KclErrorDetails::new(
557 format!("{field_name} must be an array"),
558 vec![args.source_range],
559 ))
560 })?;
561 values
562 .iter()
563 .map(|value| tag_or_uuid_from_value(value, field_name, args))
564 .collect()
565}
566
567fn parse_edge_specifier_index(obj: &KclObjectFields, args: &Args) -> Result<Option<u32>, KclError> {
568 let Some(index) = obj.get("index") else {
569 return Ok(None);
570 };
571 let KclValue::Number { value, .. } = index else {
572 return Err(KclError::new_type(KclErrorDetails::new(
573 "edge specifier 'index' must be a non-negative integer".to_owned(),
574 vec![args.source_range],
575 )));
576 };
577 if !value.is_finite() || value.fract() != 0.0 || *value < 0.0 || *value > u32::MAX as f64 {
578 return Err(KclError::new_type(KclErrorDetails::new(
579 "edge specifier 'index' must be a non-negative integer".to_owned(),
580 vec![args.source_range],
581 )));
582 }
583 Ok(Some(*value as u32))
584}
585
586pub(crate) fn is_edge_specifier_object(value: &KclValue) -> bool {
587 matches!(value, KclValue::Object { value, .. } if value.contains_key("sideFaces"))
588}
589
590pub(crate) fn parse_edge_specifier_value(value: &KclValue, args: &Args) -> Result<UnresolvedEdgeSpecifier, KclError> {
591 let KclValue::Object { value: obj, .. } = value else {
592 return Err(KclError::new_type(KclErrorDetails::new(
593 "edge specifier must be an object with 'sideFaces'".to_owned(),
594 vec![args.source_range],
595 )));
596 };
597 parse_edge_specifier_object(obj, args)
598}
599
600pub(crate) fn parse_edge_specifier_object(
602 obj: &KclObjectFields,
603 args: &Args,
604) -> Result<UnresolvedEdgeSpecifier, KclError> {
605 let side_faces = parse_tag_or_uuid_array(obj, "sideFaces", true, args)?;
606 if side_faces.is_empty() {
607 return Err(KclError::new_semantic(KclErrorDetails::new(
608 "sideFaces must be an array of at least one face, but zero were given".to_owned(),
609 vec![args.source_range],
610 )));
611 }
612 let end_faces = parse_tag_or_uuid_array(obj, "endFaces", false, args)?;
613 let index = parse_edge_specifier_index(obj, args)?;
614 Ok(UnresolvedEdgeSpecifier {
615 side_faces,
616 end_faces,
617 index,
618 })
619}
620
621async fn resolve_as_face_id(value: &TagOrUuid, exec_state: &mut ExecState, args: &Args) -> Result<Uuid, KclError> {
622 match value {
623 TagOrUuid::Uuid(uuid) => Ok(*uuid),
624 TagOrUuid::Tag(tag) => {
625 FaceTag::Tag(tag.clone())
626 .get_face_id_from_tag(exec_state, args, false)
627 .await
628 }
629 }
630}
631
632async fn resolve_as_face_ids(
633 value: &TagOrUuid,
634 solid: Option<&Solid>,
635 exec_state: &mut ExecState,
636 args: &Args,
637) -> Result<Vec<Uuid>, KclError> {
638 match value {
639 TagOrUuid::Uuid(uuid) => Ok(vec![*uuid]),
640 TagOrUuid::Tag(tag) => {
641 let infos = tag.get_all_cur_info();
642 if !infos.is_empty() {
643 let face_ids = infos
644 .iter()
645 .map(|info| {
646 info.surface
647 .as_ref()
648 .map(ExtrudeSurface::face_id)
649 .or_else(|| solid.and_then(|solid| face_id_for_tag_info_from_solid(info.id, solid)))
650 })
651 .collect::<Option<Vec<_>>>();
652 if let Some(face_ids) = face_ids {
653 return Ok(face_ids);
654 }
655 }
656
657 Ok(vec![resolve_as_face_id(value, exec_state, args).await?])
658 }
659 }
660}
661
662fn face_id_for_tag_info_from_solid(tag_info_id: Uuid, solid: &Solid) -> Option<Uuid> {
663 solid
664 .value
665 .iter()
666 .find(|surface| surface.get_id() == tag_info_id)
667 .map(ExtrudeSurface::face_id)
668}
669
670async fn resolve_as_adjacent_face_or_tag_id(
671 value: &TagOrUuid,
672 exec_state: &mut ExecState,
673 args: &Args,
674) -> Result<Uuid, KclError> {
675 match value {
676 TagOrUuid::Uuid(uuid) => Ok(*uuid),
677 TagOrUuid::Tag(tag) => match args.get_adjacent_face_to_tag(exec_state, tag, false).await {
678 Ok(face_id) => Ok(face_id),
679 Err(_) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
680 },
681 }
682}
683
684async fn resolve_as_edge_faces(
685 value: &TagOrUuid,
686 object_id: Uuid,
687 exec_state: &mut ExecState,
688 args: &Args,
689) -> Result<Vec<Uuid>, KclError> {
690 match value {
691 TagOrUuid::Uuid(uuid) => Ok(vec![*uuid]),
692 TagOrUuid::Tag(tag) => {
693 let edge_id = args.get_tag_engine_info(exec_state, tag)?.id;
694 get_face_ids_for_edge(exec_state, object_id, edge_id, args).await
695 }
696 }
697}
698
699pub(crate) async fn resolve_edge_specifier_with_face_tags(
700 unresolved: &UnresolvedEdgeSpecifier,
701 solid: Option<&Solid>,
702 exec_state: &mut ExecState,
703 args: &Args,
704) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
705 let mut references = resolve_edge_specifiers_with_face_tags(unresolved, solid, exec_state, args).await?;
706 if references.len() != 1 {
707 return Err(KclError::new_semantic(KclErrorDetails::new(
708 "edge specifier resolved to multiple edge references where exactly one was expected".to_owned(),
709 vec![args.source_range],
710 )));
711 }
712 Ok(references.remove(0))
713}
714
715const MAX_EDGE_COMBINATIONS: usize = 256;
716
717fn combination_count(group_sizes: impl IntoIterator<Item = usize>) -> usize {
721 group_sizes.into_iter().fold(1, usize::saturating_mul)
722}
723
724fn edge_combinations_exceed_limit(side_face_count: usize, end_face_count: usize) -> bool {
729 side_face_count > MAX_EDGE_COMBINATIONS
730 || end_face_count > MAX_EDGE_COMBINATIONS
731 || side_face_count.saturating_mul(end_face_count) > MAX_EDGE_COMBINATIONS
732}
733
734async fn resolve_edge_specifiers_with_face_tags(
735 unresolved: &UnresolvedEdgeSpecifier,
736 solid: Option<&Solid>,
737 exec_state: &mut ExecState,
738 args: &Args,
739) -> Result<Vec<kcmc::shared::EdgeSpecifier>, KclError> {
740 let mut side_face_groups = Vec::with_capacity(unresolved.side_faces.len());
741 for value in &unresolved.side_faces {
742 side_face_groups.push(resolve_as_face_ids(value, solid, exec_state, args).await?);
743 }
744 let mut end_face_groups = Vec::with_capacity(unresolved.end_faces.len());
745 for value in &unresolved.end_faces {
746 end_face_groups.push(resolve_as_face_ids(value, solid, exec_state, args).await?);
747 }
748
749 let side_face_count = combination_count(side_face_groups.iter().map(Vec::len));
753 let end_face_count = combination_count(end_face_groups.iter().map(Vec::len));
754 if edge_combinations_exceed_limit(side_face_count, end_face_count) {
755 return Err(KclError::new_semantic(KclErrorDetails::new(
756 "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(),
757 vec![args.source_range],
758 )));
759 }
760
761 let side_face_combinations = face_id_combinations(&side_face_groups);
766 let end_face_combinations = face_id_combinations(&end_face_groups);
767 let mut references = Vec::with_capacity(side_face_combinations.len() * end_face_combinations.len());
768 for side_faces in side_face_combinations {
769 for end_faces in &end_face_combinations {
770 references.push(
771 kcmc::shared::EdgeSpecifier::builder()
772 .side_faces(side_faces.clone())
773 .end_faces(end_faces.clone())
774 .maybe_index(unresolved.index)
775 .build(),
776 );
777 }
778 }
779 if references.len() > 1 && unresolved.index.is_some() {
782 return Err(KclError::new_semantic(KclErrorDetails::new(
783 "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(),
784 vec![args.source_range],
785 )));
786 }
787 Ok(references)
788}
789
790fn face_id_combinations(groups: &[Vec<Uuid>]) -> Vec<Vec<Uuid>> {
798 if groups.is_empty() {
799 return vec![Vec::new()];
801 }
802
803 let mut combinations = vec![Vec::new()];
804 for group in groups {
805 let mut next = Vec::with_capacity(combinations.len() * group.len());
806 for combination in &combinations {
807 for face_id in group {
808 let mut new_combination = combination.clone();
809 new_combination.push(*face_id);
810 next.push(new_combination);
811 }
812 }
813 combinations = next;
814 }
815 combinations
816}
817
818pub(crate) async fn resolve_edge_specifier_with_adjacent_faces_or_tag_ids(
819 unresolved: &UnresolvedEdgeSpecifier,
820 exec_state: &mut ExecState,
821 args: &Args,
822) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
823 let mut side_faces = Vec::with_capacity(unresolved.side_faces.len());
824 for value in &unresolved.side_faces {
825 side_faces.push(resolve_as_adjacent_face_or_tag_id(value, exec_state, args).await?);
826 }
827 let mut end_faces = Vec::with_capacity(unresolved.end_faces.len());
828 for value in &unresolved.end_faces {
829 end_faces.push(resolve_as_adjacent_face_or_tag_id(value, exec_state, args).await?);
830 }
831 Ok(kcmc::shared::EdgeSpecifier::builder()
832 .side_faces(side_faces)
833 .end_faces(end_faces)
834 .maybe_index(unresolved.index)
835 .build())
836}
837
838pub(crate) async fn parse_edge_refs_to_references(
839 edge_refs: Vec<KclValue>,
840 solid: Option<&Solid>,
841 exec_state: &mut ExecState,
842 args: &Args,
843) -> Result<Vec<kcmc::shared::EdgeSpecifier>, KclError> {
844 if edge_refs.is_empty() {
845 return Err(KclError::new_semantic(KclErrorDetails::new(
846 "You must provide at least one edge".to_owned(),
847 vec![args.source_range],
848 )));
849 }
850
851 let mut edge_references = Vec::with_capacity(edge_refs.len());
852 for edge_ref_value in &edge_refs {
853 let spec = parse_edge_specifier_value(edge_ref_value, args)?;
854 edge_references.extend(resolve_edge_specifiers_with_face_tags(&spec, solid, exec_state, args).await?);
855 }
856 Ok(edge_references)
857}
858
859pub(super) fn face_id_from_first_side_face(
863 spec: &UnresolvedEdgeSpecifier,
864 exec_state: &mut ExecState,
865 args: &Args,
866) -> Result<Uuid, KclError> {
867 let first = spec.side_faces.first().ok_or_else(|| {
868 KclError::new_type(KclErrorDetails::new(
869 "edge specifier must have at least one sideFace".to_owned(),
870 vec![args.source_range],
871 ))
872 })?;
873 match first {
874 TagOrUuid::Uuid(u) => Ok(*u),
875 TagOrUuid::Tag(t) => {
876 let info = args.get_tag_engine_info(exec_state, t)?;
877 Ok(info.geometry.id())
878 }
879 }
880}
881
882pub(crate) async fn inner_get_bounded_edge_with_id(
883 face: Solid,
884 edge: EdgeReference,
885 lower_bound: Option<TyF64>,
886 upper_bound: Option<TyF64>,
887 exec_state: &mut ExecState,
888 args: Args,
889) -> Result<BoundedEdge, KclError> {
890 let (lb, ub) = bounds_from_opts(lower_bound, upper_bound, &args)?;
891 let edge_id = edge.get_engine_id(exec_state, &args)?;
892 Ok(BoundedEdge {
893 face_id: face.id,
894 edge_id: Some(edge_id),
895 edge_specifier: None,
896 lower_bound: lb,
897 upper_bound: ub,
898 })
899}
900
901fn inner_get_bounded_edge_with_specifier(
902 face: Solid,
903 spec: UnresolvedEdgeSpecifier,
904 lower_bound: Option<TyF64>,
905 upper_bound: Option<TyF64>,
906 args: &Args,
907) -> Result<BoundedEdge, KclError> {
908 let (lb, ub) = bounds_from_opts(lower_bound, upper_bound, args)?;
909 Ok(BoundedEdge {
910 face_id: face.id,
911 edge_id: None,
912 edge_specifier: Some(spec),
913 lower_bound: lb,
914 upper_bound: ub,
915 })
916}
917
918fn bounds_from_opts(
919 lower_bound: Option<TyF64>,
920 upper_bound: Option<TyF64>,
921 args: &Args,
922) -> Result<(f32, f32), KclError> {
923 let lower_bound = if let Some(lower_bound) = lower_bound {
924 let val = lower_bound.n as f32;
925 if !(0.0..=1.0).contains(&val) {
926 return Err(KclError::new_semantic(KclErrorDetails::new(
927 format!(
928 "Invalid value: lowerBound must be between 0.0 and 1.0, provided {}",
929 val
930 ),
931 vec![args.source_range],
932 )));
933 }
934 val
935 } else {
936 0.0_f32
937 };
938 let upper_bound = if let Some(upper_bound) = upper_bound {
939 let val = upper_bound.n as f32;
940 if !(0.0..=1.0).contains(&val) {
941 return Err(KclError::new_semantic(KclErrorDetails::new(
942 format!(
943 "Invalid value: upperBound must be between 0.0 and 1.0, provided {}",
944 val
945 ),
946 vec![args.source_range],
947 )));
948 }
949 val
950 } else {
951 1.0_f32
952 };
953 Ok((lower_bound, upper_bound))
954}
955
956pub(crate) async fn resolve_unresolved_edge_specifier(
958 object_id: Uuid,
959 unresolved: &UnresolvedEdgeSpecifier,
960 exec_state: &mut ExecState,
961 args: &Args,
962) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
963 let mut side_faces = Vec::new();
964 for v in &unresolved.side_faces {
965 side_faces.extend(resolve_as_edge_faces(v, object_id, exec_state, args).await?);
966 }
967 let mut end_faces = Vec::new();
968 for v in &unresolved.end_faces {
969 end_faces.extend(resolve_as_edge_faces(v, object_id, exec_state, args).await?);
970 }
971 Ok(kcmc::shared::EdgeSpecifier::builder()
972 .side_faces(side_faces)
973 .end_faces(end_faces)
974 .maybe_index(unresolved.index)
975 .build())
976}
977
978#[cfg(test)]
979mod tests {
980 use uuid::Uuid;
981
982 use super::MAX_EDGE_COMBINATIONS;
983 use super::combination_count;
984 use super::edge_combinations_exceed_limit;
985 use super::face_id_combinations;
986
987 #[test]
988 fn face_id_combinations_empty_input_is_one_empty_combination() {
989 assert_eq!(face_id_combinations(&[]), vec![Vec::<Uuid>::new()]);
993 }
994
995 #[test]
996 fn face_id_combinations_empty_group_annihilates() {
997 let a = Uuid::from_u128(1);
999 assert_eq!(face_id_combinations(&[vec![a], vec![]]), Vec::<Vec<Uuid>>::new());
1000 }
1001
1002 #[test]
1003 fn face_id_combinations_is_ordered_cartesian_product() {
1004 let (a, b, c, d) = (
1005 Uuid::from_u128(1),
1006 Uuid::from_u128(2),
1007 Uuid::from_u128(3),
1008 Uuid::from_u128(4),
1009 );
1010 assert_eq!(
1011 face_id_combinations(&[vec![a, b], vec![c, d]]),
1012 vec![vec![a, c], vec![a, d], vec![b, c], vec![b, d]],
1013 );
1014 }
1015
1016 #[test]
1017 fn combination_count_is_product_of_group_sizes() {
1018 assert_eq!(combination_count(std::iter::empty::<usize>()), 1); assert_eq!(combination_count([1usize, 1, 1]), 1);
1020 assert_eq!(combination_count([2usize, 3, 4]), 24);
1021 }
1022
1023 #[test]
1024 fn combination_count_with_empty_group_is_zero() {
1025 assert_eq!(combination_count([5usize, 0, 5]), 0);
1026 }
1027
1028 #[test]
1029 fn combination_count_saturates_instead_of_overflowing() {
1030 assert_eq!(combination_count([usize::MAX, 2]), usize::MAX);
1033 assert_eq!(combination_count([usize::MAX, usize::MAX]), usize::MAX);
1034 }
1035
1036 #[test]
1037 fn within_limit_is_allowed() {
1038 assert!(!edge_combinations_exceed_limit(1, 1));
1039 assert!(!edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS, 1));
1041 assert!(!edge_combinations_exceed_limit(1, MAX_EDGE_COMBINATIONS));
1042 }
1043
1044 #[test]
1045 fn one_past_the_limit_on_a_single_axis_is_rejected() {
1046 assert!(edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS + 1, 1));
1047 assert!(edge_combinations_exceed_limit(1, MAX_EDGE_COMBINATIONS + 1));
1048 }
1049
1050 #[test]
1051 fn product_of_two_in_range_axes_still_exceeds_limit() {
1052 assert!(edge_combinations_exceed_limit(16, 17));
1055 }
1056
1057 #[test]
1058 fn large_axis_is_rejected_even_when_the_other_axis_is_zero() {
1059 assert_eq!((MAX_EDGE_COMBINATIONS + 1).saturating_mul(0), 0);
1063 assert!(edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS + 1, 0));
1064 assert!(edge_combinations_exceed_limit(0, MAX_EDGE_COMBINATIONS + 1));
1065 }
1066
1067 #[test]
1068 fn saturated_count_is_rejected_without_overflowing() {
1069 assert!(edge_combinations_exceed_limit(usize::MAX, 2));
1072 assert!(edge_combinations_exceed_limit(usize::MAX, usize::MAX));
1073 }
1074}