Skip to main content

kcl_lib/std/
edge.rs

1//! Edge helper functions.
2
3use 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/// Tag or UUID for use in an unresolved edge specifier (resolved to face UUIDs in blend).
35#[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/// Edge specifier payload (sideFaces, endFaces, index) as passed from KCL. Stored in BoundedEdge and resolved to `kcmc::shared::EdgeSpecifier` in blend().
43#[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
54/// Fetch the face ID(s) for an edge via Solid3dGetAllEdgeFaces.
55/// Returns 1 face for boundary edges (e.g. on surfaces) or 2 for interior edges.
56/// Used for refactor metadata (artifact-graph), fillet/chamfer, and blend edge specifier resolution.
57pub(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 two so that anything that is expecting an edge on a solid
65        // works.
66        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
204/// Check that a tag does not map to multiple edges (ambiguous region mapping).
205pub(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
220/// Get the opposite edge to the edge given.
221pub 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
274/// Get the next adjacent edge to the edge given.
275pub 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
334/// Get the previous adjacent edge to the edge given.
335pub 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
398/// Get the shared edge between two faces.
399pub 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    // Flush the batch for our fillets/chamfers if there are any.
460    // If we have a chamfer/fillet, flush the batch.
461    // TODO: we likely want to be a lot more persnickety _which_ fillets we are flushing
462    // but for now, we'll just flush everything.
463    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
627/// Parse a KCL object `{ sideFaces, endFaces?, index? }` into UnresolvedEdgeSpecifier. Used by getBoundedEdge and blend.
628pub(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
751/// Multiply group sizes into the number of combinations in their Cartesian
752/// product. Saturates at `usize::MAX` instead of overflowing, so a pathological
753/// product can't wrap around to a small value and slip under the limit check.
754fn combination_count(group_sizes: impl IntoIterator<Item = usize>) -> usize {
755    group_sizes.into_iter().fold(1, usize::saturating_mul)
756}
757
758/// Whether expanding the side/end face groups into concrete edge references
759/// would exceed `MAX_EDGE_COMBINATIONS`. Each axis is checked on its own in
760/// addition to the product: an empty group makes the product zero, which would
761/// otherwise mask a large count on the opposite axis.
762fn 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    // Before computing all combinations, count them. If there would be too
784    // many, generate a fatal error so that we don't get stuck doing large
785    // work on pathological input.
786    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    // TODO(face-api): Once modeling-commands can represent grouped logical face
796    // references, pass these groups through as one engine payload instead of
797    // expanding them into several flat EdgeSpecifier payloads.
798    // See https://github.com/KittyCAD/modeling-api/issues/1252.
799    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    // We should never duplicate the index. It should be used once on the engine
814    // side to resolve the entire set.
815    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
824/// Computes the Cartesian product of a list of groups of face UUIDs. Given N
825/// groups, it returns every way of picking exactly one UUID from each group,
826/// preserving positional order.
827///
828/// ```ignore
829/// face_id_combinations([[a, b], [c, d]])  ->  [[a, c], [a, d], [b, c], [b, d]]
830/// ```
831fn face_id_combinations(groups: &[Vec<Uuid>]) -> Vec<Vec<Uuid>> {
832    if groups.is_empty() {
833        // Callers expect at least one element in the outer Vec.
834        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
893/// Get the face (surface body) id from the first side_face of an unresolved
894/// specifier. Used when building a BoundedEdge from an edge specifier object in
895/// blend().
896pub(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
990/// Resolve an unresolved edge specifier (tags/UUIDs) to engine EdgeSpecifier (face UUIDs) for blend. Called from blend().
991pub(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        // The product of zero groups is a single empty tuple, not zero tuples.
1024        // Callers rely on this so that an absent endFaces still yields one
1025        // iteration rather than dropping every reference.
1026        assert_eq!(face_id_combinations(&[]), vec![Vec::<Uuid>::new()]);
1027    }
1028
1029    #[test]
1030    fn face_id_combinations_empty_group_annihilates() {
1031        // A single zero-length group collapses the whole product to nothing.
1032        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); // empty product
1053        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        // Must pin at usize::MAX rather than wrapping to a small value, which
1065        // would let a huge product slip under the limit.
1066        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        // Exactly at the limit on a single axis is allowed.
1074        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        // 16 and 17 are each within the limit but 16 * 17 = 272 is not, so the
1087        // product check is needed in addition to the per-axis checks.
1088        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        // Regression for the zero case: an empty group zeroes the product, so
1094        // without the per-axis checks the huge opposite axis would still be
1095        // expanded.
1096        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        // A count that already saturated must be over the limit, and the final
1104        // multiply must neither panic nor wrap.
1105        assert!(edge_combinations_exceed_limit(usize::MAX, 2));
1106        assert!(edge_combinations_exceed_limit(usize::MAX, usize::MAX));
1107    }
1108}