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::ExecState;
18use crate::execution::ExtrudeSurface;
19use crate::execution::KclObjectFields;
20use crate::execution::KclValue;
21use crate::execution::ModelingCmdMeta;
22use crate::execution::Solid;
23use crate::execution::TagIdentifier;
24use crate::execution::types::ArrayLen;
25use crate::execution::types::RuntimeType;
26use crate::std::Args;
27use crate::std::args::TyF64;
28use crate::std::fillet::EdgeReference;
29use crate::std::sketch::FaceTag;
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, ts_rs::TS)]
33#[serde(untagged)]
34pub enum TagOrUuid {
35 Uuid(Uuid),
36 Tag(Box<TagIdentifier>),
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS)]
41#[serde(rename_all = "camelCase")]
42pub struct UnresolvedEdgeSpecifier {
43 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub side_faces: Vec<TagOrUuid>,
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub end_faces: Vec<TagOrUuid>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub index: Option<u32>,
49}
50
51pub(crate) async fn get_face_ids_for_edge(
55 exec_state: &mut ExecState,
56 object_id: Uuid,
57 edge_id: Uuid,
58 args: &Args,
59) -> Result<Vec<Uuid>, KclError> {
60 if args.ctx.no_engine_commands().await {
61 return Ok(vec![exec_state.next_uuid(), exec_state.next_uuid()]);
64 }
65
66 let resp = exec_state
67 .send_modeling_cmd(
68 ModelingCmdMeta::from_args(exec_state, args),
69 ModelingCmd::from(
70 mcmd::Solid3dGetAllEdgeFaces::builder()
71 .object_id(object_id)
72 .edge_id(edge_id)
73 .build(),
74 ),
75 )
76 .await?;
77 let OkWebSocketResponseData::Modeling {
78 modeling_response: OkModelingCmdResponse::Solid3dGetAllEdgeFaces(info),
79 } = &resp
80 else {
81 return Err(KclError::new_engine(KclErrorDetails::new(
82 format!("Solid3dGetAllEdgeFaces response was not as expected: {resp:?}"),
83 vec![args.source_range],
84 )));
85 };
86 if info.faces.is_empty() || info.faces.len() > 2 {
87 return Err(KclError::new_engine(KclErrorDetails::new(
88 format!(
89 "Solid3dGetAllEdgeFaces returned {} face(s) for edge {edge_id}, expected 1 or 2",
90 info.faces.len()
91 ),
92 vec![args.source_range],
93 )));
94 }
95 Ok(info.faces.clone())
96}
97
98pub(super) fn check_tag_not_ambiguous(tag: &TagIdentifier, args: &Args) -> Result<(), KclError> {
100 let all_infos = tag.get_all_cur_info();
101 if all_infos.len() > 1 {
102 return Err(KclError::new_semantic(KclErrorDetails::new(
103 format!(
104 "Tag `{}` is ambiguous: it maps to {} edges in the region. Use a more specific reference.",
105 tag.value,
106 all_infos.len()
107 ),
108 vec![args.source_range],
109 )));
110 }
111 Ok(())
112}
113
114pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
116 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
117
118 let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
119 Ok(KclValue::Uuid {
120 value: edge,
121 meta: vec![args.source_range.into()],
122 })
123}
124
125async fn inner_get_opposite_edge(
126 edge: TagIdentifier,
127 exec_state: &mut ExecState,
128 args: Args,
129) -> Result<Uuid, KclError> {
130 check_tag_not_ambiguous(&edge, &args)?;
131 if args.ctx.no_engine_commands().await {
132 return Ok(exec_state.next_uuid());
133 }
134 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
135
136 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
137 let tagged_path_id = tagged_path.id;
138 let sketch_id = tagged_path.geometry.id();
139
140 let resp = exec_state
141 .send_modeling_cmd(
142 ModelingCmdMeta::from_args(exec_state, &args),
143 ModelingCmd::from(
144 mcmd::Solid3dGetOppositeEdge::builder()
145 .edge_id(tagged_path_id)
146 .object_id(sketch_id)
147 .face_id(face_id)
148 .build(),
149 ),
150 )
151 .await?;
152 let OkWebSocketResponseData::Modeling {
153 modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
154 } = &resp
155 else {
156 return Err(KclError::new_engine(KclErrorDetails::new(
157 format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {resp:?}"),
158 vec![args.source_range],
159 )));
160 };
161
162 Ok(opposite_edge.edge)
163}
164
165pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
167 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
168
169 let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
170 Ok(KclValue::Uuid {
171 value: edge,
172 meta: vec![args.source_range.into()],
173 })
174}
175
176async fn inner_get_next_adjacent_edge(
177 edge: TagIdentifier,
178 exec_state: &mut ExecState,
179 args: Args,
180) -> Result<Uuid, KclError> {
181 check_tag_not_ambiguous(&edge, &args)?;
182 if args.ctx.no_engine_commands().await {
183 return Ok(exec_state.next_uuid());
184 }
185 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
186
187 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
188 let tagged_path_id = tagged_path.id;
189 let sketch_id = tagged_path.geometry.id();
190
191 let resp = exec_state
192 .send_modeling_cmd(
193 ModelingCmdMeta::from_args(exec_state, &args),
194 ModelingCmd::from(
195 mcmd::Solid3dGetNextAdjacentEdge::builder()
196 .edge_id(tagged_path_id)
197 .object_id(sketch_id)
198 .face_id(face_id)
199 .build(),
200 ),
201 )
202 .await?;
203
204 let OkWebSocketResponseData::Modeling {
205 modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
206 } = &resp
207 else {
208 return Err(KclError::new_engine(KclErrorDetails::new(
209 format!("mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {resp:?}"),
210 vec![args.source_range],
211 )));
212 };
213
214 adjacent_edge.edge.ok_or_else(|| {
215 KclError::new_type(KclErrorDetails::new(
216 format!("No edge found next adjacent to tag: `{}`", edge.value),
217 vec![args.source_range],
218 ))
219 })
220}
221
222pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
224 let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
225
226 let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
227 Ok(KclValue::Uuid {
228 value: edge,
229 meta: vec![args.source_range.into()],
230 })
231}
232
233async fn inner_get_previous_adjacent_edge(
234 edge: TagIdentifier,
235 exec_state: &mut ExecState,
236 args: Args,
237) -> Result<Uuid, KclError> {
238 check_tag_not_ambiguous(&edge, &args)?;
239 if args.ctx.no_engine_commands().await {
240 return Ok(exec_state.next_uuid());
241 }
242 let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
243
244 let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
245 let tagged_path_id = tagged_path.id;
246 let sketch_id = tagged_path.geometry.id();
247
248 let resp = exec_state
249 .send_modeling_cmd(
250 ModelingCmdMeta::from_args(exec_state, &args),
251 ModelingCmd::from(
252 mcmd::Solid3dGetPrevAdjacentEdge::builder()
253 .edge_id(tagged_path_id)
254 .object_id(sketch_id)
255 .face_id(face_id)
256 .build(),
257 ),
258 )
259 .await?;
260 let OkWebSocketResponseData::Modeling {
261 modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
262 } = &resp
263 else {
264 return Err(KclError::new_engine(KclErrorDetails::new(
265 format!("mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {resp:?}"),
266 vec![args.source_range],
267 )));
268 };
269
270 adjacent_edge.edge.ok_or_else(|| {
271 KclError::new_type(KclErrorDetails::new(
272 format!("No edge found previous adjacent to tag: `{}`", edge.value),
273 vec![args.source_range],
274 ))
275 })
276}
277
278pub async fn get_common_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
280 let faces: Vec<FaceTag> = args.get_kw_arg(
281 "faces",
282 &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Known(2)),
283 exec_state,
284 )?;
285
286 fn into_tag(face: FaceTag, source_range: SourceRange) -> Result<TagIdentifier, KclError> {
287 match face {
288 FaceTag::StartOrEnd(_) => Err(KclError::new_type(KclErrorDetails::new(
289 "getCommonEdge requires a tagged face, it cannot use `START` or `END` faces".to_owned(),
290 vec![source_range],
291 ))),
292 FaceTag::Tag(tag_identifier) => Ok(*tag_identifier),
293 }
294 }
295
296 let [face1, face2]: [FaceTag; 2] = faces.try_into().map_err(|_: Vec<FaceTag>| {
297 KclError::new_type(KclErrorDetails::new(
298 "getCommonEdge requires exactly two tags for faces".to_owned(),
299 vec![args.source_range],
300 ))
301 })?;
302
303 let face1 = into_tag(face1, args.source_range)?;
304 let face2 = into_tag(face2, args.source_range)?;
305
306 let edge = inner_get_common_edge(face1, face2, exec_state, args.clone()).await?;
307 Ok(KclValue::Uuid {
308 value: edge,
309 meta: vec![args.source_range.into()],
310 })
311}
312
313async fn inner_get_common_edge(
314 face1: TagIdentifier,
315 face2: TagIdentifier,
316 exec_state: &mut ExecState,
317 args: Args,
318) -> Result<Uuid, KclError> {
319 check_tag_not_ambiguous(&face1, &args)?;
320 check_tag_not_ambiguous(&face2, &args)?;
321 let id = exec_state.next_uuid();
322 if args.ctx.no_engine_commands().await {
323 return Ok(id);
324 }
325
326 let first_face_id = args.get_adjacent_face_to_tag(exec_state, &face1, false).await?;
327 let second_face_id = args.get_adjacent_face_to_tag(exec_state, &face2, false).await?;
328
329 let first_tagged_path = args.get_tag_engine_info(exec_state, &face1)?.clone();
330 let second_tagged_path = args.get_tag_engine_info(exec_state, &face2)?;
331
332 if first_tagged_path.geometry.id() != second_tagged_path.geometry.id() {
333 return Err(KclError::new_type(KclErrorDetails::new(
334 "getCommonEdge requires the faces to be in the same original sketch".to_string(),
335 vec![args.source_range],
336 )));
337 }
338
339 if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
344 exec_state
345 .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
346 .await?;
347 } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
348 exec_state
349 .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
350 .await?;
351 }
352
353 let resp = exec_state
354 .send_modeling_cmd(
355 ModelingCmdMeta::from_args_id(exec_state, &args, id),
356 ModelingCmd::from(
357 mcmd::Solid3dGetCommonEdge::builder()
358 .object_id(first_tagged_path.geometry.id())
359 .face_ids([first_face_id, second_face_id])
360 .build(),
361 ),
362 )
363 .await?;
364 let OkWebSocketResponseData::Modeling {
365 modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
366 } = &resp
367 else {
368 return Err(KclError::new_engine(KclErrorDetails::new(
369 format!("mcmd::Solid3dGetCommonEdge response was not as expected: {resp:?}"),
370 vec![args.source_range],
371 )));
372 };
373
374 common_edge.edge.ok_or_else(|| {
375 KclError::new_type(KclErrorDetails::new(
376 format!(
377 "No common edge was found between `{}` and `{}`",
378 face1.value, face2.value
379 ),
380 vec![args.source_range],
381 ))
382 })
383}
384
385pub async fn get_bounded_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
386 let face = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
387 let edge_val = args.get_kw_arg("edge", &RuntimeType::any(), exec_state)?;
388 let lower_bound = args.get_kw_arg_opt("lowerBound", &RuntimeType::num_any(), exec_state)?;
389 let upper_bound = args.get_kw_arg_opt("upperBound", &RuntimeType::num_any(), exec_state)?;
390
391 let bounded_edge = match &edge_val {
392 KclValue::Uuid { value, .. } => {
393 inner_get_bounded_edge_with_id(face, EdgeReference::Uuid(*value), lower_bound, upper_bound, exec_state, args.clone()).await?
394 }
395 KclValue::TagIdentifier(tag) => {
396 inner_get_bounded_edge_with_id(face, EdgeReference::Tag(tag.clone()), lower_bound, upper_bound, exec_state, args.clone()).await?
397 }
398 KclValue::Object { value: obj, .. } => {
399 let spec = parse_edge_specifier_object(obj, &args)?;
400 inner_get_bounded_edge_with_specifier(face, spec, lower_bound, upper_bound, &args)?
401 }
402 _ => {
403 return Err(KclError::new_type(KclErrorDetails::new(
404 "edge must be a tagged edge, edge UUID, or edge specifier object (e.g. { sideFaces = [...], endFaces = [...], index = 0 })".to_owned(),
405 vec![args.source_range],
406 )))
407 }
408 };
409 Ok(KclValue::BoundedEdge {
410 value: bounded_edge,
411 meta: vec![args.source_range.into()],
412 })
413}
414
415fn tag_or_uuid_from_value(value: &KclValue, field_name: &str, args: &Args) -> Result<TagOrUuid, KclError> {
416 match value {
417 KclValue::Uuid { value, .. } => Ok(TagOrUuid::Uuid(*value)),
418 KclValue::TagIdentifier(tag) => Ok(TagOrUuid::Tag(tag.clone())),
419 _ => Err(KclError::new_type(KclErrorDetails::new(
420 format!("{field_name} elements must be tags or UUIDs"),
421 vec![args.source_range],
422 ))),
423 }
424}
425
426fn parse_tag_or_uuid_array(
427 obj: &KclObjectFields,
428 field_name: &str,
429 required: bool,
430 args: &Args,
431) -> Result<Vec<TagOrUuid>, KclError> {
432 let Some(value) = obj.get(field_name) else {
433 return if required {
434 Err(KclError::new_type(KclErrorDetails::new(
435 format!("edge specifier object must have {field_name}"),
436 vec![args.source_range],
437 )))
438 } else {
439 Ok(Vec::new())
440 };
441 };
442 let values = value.as_slice().ok_or_else(|| {
443 KclError::new_type(KclErrorDetails::new(
444 format!("{field_name} must be an array"),
445 vec![args.source_range],
446 ))
447 })?;
448 values
449 .iter()
450 .map(|value| tag_or_uuid_from_value(value, field_name, args))
451 .collect()
452}
453
454fn parse_edge_specifier_index(obj: &KclObjectFields, args: &Args) -> Result<Option<u32>, KclError> {
455 let Some(index) = obj.get("index") else {
456 return Ok(None);
457 };
458 let KclValue::Number { value, .. } = index else {
459 return Err(KclError::new_type(KclErrorDetails::new(
460 "edge specifier 'index' must be a non-negative integer".to_owned(),
461 vec![args.source_range],
462 )));
463 };
464 if !value.is_finite() || value.fract() != 0.0 || *value < 0.0 || *value > u32::MAX as f64 {
465 return Err(KclError::new_type(KclErrorDetails::new(
466 "edge specifier 'index' must be a non-negative integer".to_owned(),
467 vec![args.source_range],
468 )));
469 }
470 Ok(Some(*value as u32))
471}
472
473pub(crate) fn is_edge_specifier_object(value: &KclValue) -> bool {
474 matches!(value, KclValue::Object { value, .. } if value.contains_key("sideFaces"))
475}
476
477pub(crate) fn parse_edge_specifier_value(value: &KclValue, args: &Args) -> Result<UnresolvedEdgeSpecifier, KclError> {
478 let KclValue::Object { value: obj, .. } = value else {
479 return Err(KclError::new_type(KclErrorDetails::new(
480 "edge specifier must be an object with 'sideFaces'".to_owned(),
481 vec![args.source_range],
482 )));
483 };
484 parse_edge_specifier_object(obj, args)
485}
486
487pub(crate) fn parse_edge_specifier_object(
489 obj: &KclObjectFields,
490 args: &Args,
491) -> Result<UnresolvedEdgeSpecifier, KclError> {
492 let side_faces = parse_tag_or_uuid_array(obj, "sideFaces", true, args)?;
493 if side_faces.is_empty() {
494 return Err(KclError::new_semantic(KclErrorDetails::new(
495 "sideFaces must be an array of at least one face, but zero were given".to_owned(),
496 vec![args.source_range],
497 )));
498 }
499 let end_faces = parse_tag_or_uuid_array(obj, "endFaces", false, args)?;
500 let index = parse_edge_specifier_index(obj, args)?;
501 Ok(UnresolvedEdgeSpecifier {
502 side_faces,
503 end_faces,
504 index,
505 })
506}
507
508async fn resolve_as_face_id(value: &TagOrUuid, exec_state: &mut ExecState, args: &Args) -> Result<Uuid, KclError> {
509 match value {
510 TagOrUuid::Uuid(uuid) => Ok(*uuid),
511 TagOrUuid::Tag(tag) => {
512 FaceTag::Tag(tag.clone())
513 .get_face_id_from_tag(exec_state, args, false)
514 .await
515 }
516 }
517}
518
519async fn resolve_as_face_ids(
520 value: &TagOrUuid,
521 solid: Option<&Solid>,
522 exec_state: &mut ExecState,
523 args: &Args,
524) -> Result<Vec<Uuid>, KclError> {
525 match value {
526 TagOrUuid::Uuid(uuid) => Ok(vec![*uuid]),
527 TagOrUuid::Tag(tag) => {
528 let infos = tag.get_all_cur_info();
529 if !infos.is_empty() {
530 let face_ids = infos
531 .iter()
532 .map(|info| {
533 info.surface
534 .as_ref()
535 .map(ExtrudeSurface::face_id)
536 .or_else(|| solid.and_then(|solid| face_id_for_tag_info_from_solid(info.id, solid)))
537 })
538 .collect::<Option<Vec<_>>>();
539 if let Some(face_ids) = face_ids {
540 return Ok(face_ids);
541 }
542 }
543
544 Ok(vec![resolve_as_face_id(value, exec_state, args).await?])
545 }
546 }
547}
548
549fn face_id_for_tag_info_from_solid(tag_info_id: Uuid, solid: &Solid) -> Option<Uuid> {
550 solid
551 .value
552 .iter()
553 .find(|surface| surface.get_id() == tag_info_id)
554 .map(ExtrudeSurface::face_id)
555}
556
557async fn resolve_as_adjacent_face_or_tag_id(
558 value: &TagOrUuid,
559 exec_state: &mut ExecState,
560 args: &Args,
561) -> Result<Uuid, KclError> {
562 match value {
563 TagOrUuid::Uuid(uuid) => Ok(*uuid),
564 TagOrUuid::Tag(tag) => match args.get_adjacent_face_to_tag(exec_state, tag, false).await {
565 Ok(face_id) => Ok(face_id),
566 Err(_) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
567 },
568 }
569}
570
571async fn resolve_as_edge_faces(
572 value: &TagOrUuid,
573 object_id: Uuid,
574 exec_state: &mut ExecState,
575 args: &Args,
576) -> Result<Vec<Uuid>, KclError> {
577 match value {
578 TagOrUuid::Uuid(uuid) => Ok(vec![*uuid]),
579 TagOrUuid::Tag(tag) => {
580 let edge_id = args.get_tag_engine_info(exec_state, tag)?.id;
581 get_face_ids_for_edge(exec_state, object_id, edge_id, args).await
582 }
583 }
584}
585
586pub(crate) async fn resolve_edge_specifier_with_face_tags(
587 unresolved: &UnresolvedEdgeSpecifier,
588 solid: Option<&Solid>,
589 exec_state: &mut ExecState,
590 args: &Args,
591) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
592 let mut references = resolve_edge_specifiers_with_face_tags(unresolved, solid, exec_state, args).await?;
593 if references.len() != 1 {
594 return Err(KclError::new_semantic(KclErrorDetails::new(
595 "edge specifier resolved to multiple edge references where exactly one was expected".to_owned(),
596 vec![args.source_range],
597 )));
598 }
599 Ok(references.remove(0))
600}
601
602const MAX_EDGE_COMBINATIONS: usize = 256;
603
604fn combination_count(group_sizes: impl IntoIterator<Item = usize>) -> usize {
608 group_sizes.into_iter().fold(1, usize::saturating_mul)
609}
610
611fn edge_combinations_exceed_limit(side_face_count: usize, end_face_count: usize) -> bool {
616 side_face_count > MAX_EDGE_COMBINATIONS
617 || end_face_count > MAX_EDGE_COMBINATIONS
618 || side_face_count.saturating_mul(end_face_count) > MAX_EDGE_COMBINATIONS
619}
620
621async fn resolve_edge_specifiers_with_face_tags(
622 unresolved: &UnresolvedEdgeSpecifier,
623 solid: Option<&Solid>,
624 exec_state: &mut ExecState,
625 args: &Args,
626) -> Result<Vec<kcmc::shared::EdgeSpecifier>, KclError> {
627 let mut side_face_groups = Vec::with_capacity(unresolved.side_faces.len());
628 for value in &unresolved.side_faces {
629 side_face_groups.push(resolve_as_face_ids(value, solid, exec_state, args).await?);
630 }
631 let mut end_face_groups = Vec::with_capacity(unresolved.end_faces.len());
632 for value in &unresolved.end_faces {
633 end_face_groups.push(resolve_as_face_ids(value, solid, exec_state, args).await?);
634 }
635
636 let side_face_count = combination_count(side_face_groups.iter().map(Vec::len));
640 let end_face_count = combination_count(end_face_groups.iter().map(Vec::len));
641 if edge_combinations_exceed_limit(side_face_count, end_face_count) {
642 return Err(KclError::new_semantic(KclErrorDetails::new(
643 "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(),
644 vec![args.source_range],
645 )));
646 }
647
648 let side_face_combinations = face_id_combinations(&side_face_groups);
653 let end_face_combinations = face_id_combinations(&end_face_groups);
654 let mut references = Vec::with_capacity(side_face_combinations.len() * end_face_combinations.len());
655 for side_faces in side_face_combinations {
656 for end_faces in &end_face_combinations {
657 references.push(
658 kcmc::shared::EdgeSpecifier::builder()
659 .side_faces(side_faces.clone())
660 .end_faces(end_faces.clone())
661 .maybe_index(unresolved.index)
662 .build(),
663 );
664 }
665 }
666 if references.len() > 1 && unresolved.index.is_some() {
669 return Err(KclError::new_semantic(KclErrorDetails::new(
670 "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(),
671 vec![args.source_range],
672 )));
673 }
674 Ok(references)
675}
676
677fn face_id_combinations(groups: &[Vec<Uuid>]) -> Vec<Vec<Uuid>> {
685 if groups.is_empty() {
686 return vec![Vec::new()];
688 }
689
690 let mut combinations = vec![Vec::new()];
691 for group in groups {
692 let mut next = Vec::with_capacity(combinations.len() * group.len());
693 for combination in &combinations {
694 for face_id in group {
695 let mut new_combination = combination.clone();
696 new_combination.push(*face_id);
697 next.push(new_combination);
698 }
699 }
700 combinations = next;
701 }
702 combinations
703}
704
705pub(crate) async fn resolve_edge_specifier_with_adjacent_faces_or_tag_ids(
706 unresolved: &UnresolvedEdgeSpecifier,
707 exec_state: &mut ExecState,
708 args: &Args,
709) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
710 let mut side_faces = Vec::with_capacity(unresolved.side_faces.len());
711 for value in &unresolved.side_faces {
712 side_faces.push(resolve_as_adjacent_face_or_tag_id(value, exec_state, args).await?);
713 }
714 let mut end_faces = Vec::with_capacity(unresolved.end_faces.len());
715 for value in &unresolved.end_faces {
716 end_faces.push(resolve_as_adjacent_face_or_tag_id(value, exec_state, args).await?);
717 }
718 Ok(kcmc::shared::EdgeSpecifier::builder()
719 .side_faces(side_faces)
720 .end_faces(end_faces)
721 .maybe_index(unresolved.index)
722 .build())
723}
724
725pub(crate) async fn parse_edge_refs_to_references(
726 edge_refs: Vec<KclValue>,
727 solid: Option<&Solid>,
728 exec_state: &mut ExecState,
729 args: &Args,
730) -> Result<Vec<kcmc::shared::EdgeSpecifier>, KclError> {
731 if edge_refs.is_empty() {
732 return Err(KclError::new_semantic(KclErrorDetails::new(
733 "You must provide at least one edge".to_owned(),
734 vec![args.source_range],
735 )));
736 }
737
738 let mut edge_references = Vec::with_capacity(edge_refs.len());
739 for edge_ref_value in &edge_refs {
740 let spec = parse_edge_specifier_value(edge_ref_value, args)?;
741 edge_references.extend(resolve_edge_specifiers_with_face_tags(&spec, solid, exec_state, args).await?);
742 }
743 Ok(edge_references)
744}
745
746pub(super) fn face_id_from_first_side_face(
750 spec: &UnresolvedEdgeSpecifier,
751 exec_state: &mut ExecState,
752 args: &Args,
753) -> Result<Uuid, KclError> {
754 let first = spec.side_faces.first().ok_or_else(|| {
755 KclError::new_type(KclErrorDetails::new(
756 "edge specifier must have at least one sideFace".to_owned(),
757 vec![args.source_range],
758 ))
759 })?;
760 match first {
761 TagOrUuid::Uuid(u) => Ok(*u),
762 TagOrUuid::Tag(t) => {
763 let info = args.get_tag_engine_info(exec_state, t)?;
764 Ok(info.geometry.id())
765 }
766 }
767}
768
769pub(crate) async fn inner_get_bounded_edge_with_id(
770 face: Solid,
771 edge: EdgeReference,
772 lower_bound: Option<TyF64>,
773 upper_bound: Option<TyF64>,
774 exec_state: &mut ExecState,
775 args: Args,
776) -> Result<BoundedEdge, KclError> {
777 let (lb, ub) = bounds_from_opts(lower_bound, upper_bound, &args)?;
778 let edge_id = edge.get_engine_id(exec_state, &args)?;
779 Ok(BoundedEdge {
780 face_id: face.id,
781 edge_id: Some(edge_id),
782 edge_specifier: None,
783 lower_bound: lb,
784 upper_bound: ub,
785 })
786}
787
788fn inner_get_bounded_edge_with_specifier(
789 face: Solid,
790 spec: UnresolvedEdgeSpecifier,
791 lower_bound: Option<TyF64>,
792 upper_bound: Option<TyF64>,
793 args: &Args,
794) -> Result<BoundedEdge, KclError> {
795 let (lb, ub) = bounds_from_opts(lower_bound, upper_bound, args)?;
796 Ok(BoundedEdge {
797 face_id: face.id,
798 edge_id: None,
799 edge_specifier: Some(spec),
800 lower_bound: lb,
801 upper_bound: ub,
802 })
803}
804
805fn bounds_from_opts(
806 lower_bound: Option<TyF64>,
807 upper_bound: Option<TyF64>,
808 args: &Args,
809) -> Result<(f32, f32), KclError> {
810 let lower_bound = if let Some(lower_bound) = lower_bound {
811 let val = lower_bound.n as f32;
812 if !(0.0..=1.0).contains(&val) {
813 return Err(KclError::new_semantic(KclErrorDetails::new(
814 format!(
815 "Invalid value: lowerBound must be between 0.0 and 1.0, provided {}",
816 val
817 ),
818 vec![args.source_range],
819 )));
820 }
821 val
822 } else {
823 0.0_f32
824 };
825 let upper_bound = if let Some(upper_bound) = upper_bound {
826 let val = upper_bound.n as f32;
827 if !(0.0..=1.0).contains(&val) {
828 return Err(KclError::new_semantic(KclErrorDetails::new(
829 format!(
830 "Invalid value: upperBound must be between 0.0 and 1.0, provided {}",
831 val
832 ),
833 vec![args.source_range],
834 )));
835 }
836 val
837 } else {
838 1.0_f32
839 };
840 Ok((lower_bound, upper_bound))
841}
842
843pub(crate) async fn resolve_unresolved_edge_specifier(
845 object_id: Uuid,
846 unresolved: &UnresolvedEdgeSpecifier,
847 exec_state: &mut ExecState,
848 args: &Args,
849) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
850 let mut side_faces = Vec::new();
851 for v in &unresolved.side_faces {
852 side_faces.extend(resolve_as_edge_faces(v, object_id, exec_state, args).await?);
853 }
854 let mut end_faces = Vec::new();
855 for v in &unresolved.end_faces {
856 end_faces.extend(resolve_as_edge_faces(v, object_id, exec_state, args).await?);
857 }
858 Ok(kcmc::shared::EdgeSpecifier::builder()
859 .side_faces(side_faces)
860 .end_faces(end_faces)
861 .maybe_index(unresolved.index)
862 .build())
863}
864
865#[cfg(test)]
866mod tests {
867 use uuid::Uuid;
868
869 use super::MAX_EDGE_COMBINATIONS;
870 use super::combination_count;
871 use super::edge_combinations_exceed_limit;
872 use super::face_id_combinations;
873
874 #[test]
875 fn face_id_combinations_empty_input_is_one_empty_combination() {
876 assert_eq!(face_id_combinations(&[]), vec![Vec::<Uuid>::new()]);
880 }
881
882 #[test]
883 fn face_id_combinations_empty_group_annihilates() {
884 let a = Uuid::from_u128(1);
886 assert_eq!(face_id_combinations(&[vec![a], vec![]]), Vec::<Vec<Uuid>>::new());
887 }
888
889 #[test]
890 fn face_id_combinations_is_ordered_cartesian_product() {
891 let (a, b, c, d) = (
892 Uuid::from_u128(1),
893 Uuid::from_u128(2),
894 Uuid::from_u128(3),
895 Uuid::from_u128(4),
896 );
897 assert_eq!(
898 face_id_combinations(&[vec![a, b], vec![c, d]]),
899 vec![vec![a, c], vec![a, d], vec![b, c], vec![b, d]],
900 );
901 }
902
903 #[test]
904 fn combination_count_is_product_of_group_sizes() {
905 assert_eq!(combination_count(std::iter::empty::<usize>()), 1); assert_eq!(combination_count([1usize, 1, 1]), 1);
907 assert_eq!(combination_count([2usize, 3, 4]), 24);
908 }
909
910 #[test]
911 fn combination_count_with_empty_group_is_zero() {
912 assert_eq!(combination_count([5usize, 0, 5]), 0);
913 }
914
915 #[test]
916 fn combination_count_saturates_instead_of_overflowing() {
917 assert_eq!(combination_count([usize::MAX, 2]), usize::MAX);
920 assert_eq!(combination_count([usize::MAX, usize::MAX]), usize::MAX);
921 }
922
923 #[test]
924 fn within_limit_is_allowed() {
925 assert!(!edge_combinations_exceed_limit(1, 1));
926 assert!(!edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS, 1));
928 assert!(!edge_combinations_exceed_limit(1, MAX_EDGE_COMBINATIONS));
929 }
930
931 #[test]
932 fn one_past_the_limit_on_a_single_axis_is_rejected() {
933 assert!(edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS + 1, 1));
934 assert!(edge_combinations_exceed_limit(1, MAX_EDGE_COMBINATIONS + 1));
935 }
936
937 #[test]
938 fn product_of_two_in_range_axes_still_exceeds_limit() {
939 assert!(edge_combinations_exceed_limit(16, 17));
942 }
943
944 #[test]
945 fn large_axis_is_rejected_even_when_the_other_axis_is_zero() {
946 assert_eq!((MAX_EDGE_COMBINATIONS + 1).saturating_mul(0), 0);
950 assert!(edge_combinations_exceed_limit(MAX_EDGE_COMBINATIONS + 1, 0));
951 assert!(edge_combinations_exceed_limit(0, MAX_EDGE_COMBINATIONS + 1));
952 }
953
954 #[test]
955 fn saturated_count_is_rejected_without_overflowing() {
956 assert!(edge_combinations_exceed_limit(usize::MAX, 2));
959 assert!(edge_combinations_exceed_limit(usize::MAX, usize::MAX));
960 }
961}