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