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