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::ExecState;
18use crate::execution::ExtrudeSurface;
19use crate::execution::KclObjectFields;
20use crate::execution::KclValue;
21use crate::execution::ModelingCmdMeta;
22use crate::execution::Solid;
23use crate::execution::TagIdentifier;
24use crate::execution::types::ArrayLen;
25use crate::execution::types::RuntimeType;
26use crate::std::Args;
27use crate::std::args::TyF64;
28use crate::std::fillet::EdgeReference;
29use crate::std::sketch::FaceTag;
30
31/// Tag or UUID for use in an unresolved edge specifier (resolved to face UUIDs in blend).
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, ts_rs::TS)]
33#[serde(untagged)]
34pub enum TagOrUuid {
35    Uuid(Uuid),
36    Tag(Box<TagIdentifier>),
37}
38
39/// Edge specifier payload (sideFaces, endFaces, index) as passed from KCL. Stored in BoundedEdge and resolved to `kcmc::shared::EdgeSpecifier` in blend().
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ts_rs::TS)]
41#[serde(rename_all = "camelCase")]
42pub struct UnresolvedEdgeSpecifier {
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub side_faces: Vec<TagOrUuid>,
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    pub end_faces: Vec<TagOrUuid>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub index: Option<u32>,
49}
50
51/// Fetch the face ID(s) for an edge via Solid3dGetAllEdgeFaces.
52/// Returns 1 face for boundary edges (e.g. on surfaces) or 2 for interior edges.
53/// Used for refactor metadata (artifact-graph), fillet/chamfer, and blend edge specifier resolution.
54pub(crate) async fn get_face_ids_for_edge(
55    exec_state: &mut ExecState,
56    object_id: Uuid,
57    edge_id: Uuid,
58    args: &Args,
59) -> Result<Vec<Uuid>, KclError> {
60    if args.ctx.no_engine_commands().await {
61        // Return two so that anything that is expecting an edge on a solid
62        // works.
63        return Ok(vec![exec_state.next_uuid(), exec_state.next_uuid()]);
64    }
65
66    let resp = exec_state
67        .send_modeling_cmd(
68            ModelingCmdMeta::from_args(exec_state, args),
69            ModelingCmd::from(
70                mcmd::Solid3dGetAllEdgeFaces::builder()
71                    .object_id(object_id)
72                    .edge_id(edge_id)
73                    .build(),
74            ),
75        )
76        .await?;
77    let OkWebSocketResponseData::Modeling {
78        modeling_response: OkModelingCmdResponse::Solid3dGetAllEdgeFaces(info),
79    } = &resp
80    else {
81        return Err(KclError::new_engine(KclErrorDetails::new(
82            format!("Solid3dGetAllEdgeFaces response was not as expected: {resp:?}"),
83            vec![args.source_range],
84        )));
85    };
86    if info.faces.is_empty() || info.faces.len() > 2 {
87        return Err(KclError::new_engine(KclErrorDetails::new(
88            format!(
89                "Solid3dGetAllEdgeFaces returned {} face(s) for edge {edge_id}, expected 1 or 2",
90                info.faces.len()
91            ),
92            vec![args.source_range],
93        )));
94    }
95    Ok(info.faces.clone())
96}
97
98/// Check that a tag does not map to multiple edges (ambiguous region mapping).
99pub(super) fn check_tag_not_ambiguous(tag: &TagIdentifier, args: &Args) -> Result<(), KclError> {
100    let all_infos = tag.get_all_cur_info();
101    if all_infos.len() > 1 {
102        return Err(KclError::new_semantic(KclErrorDetails::new(
103            format!(
104                "Tag `{}` is ambiguous: it maps to {} edges in the region. Use a more specific reference.",
105                tag.value,
106                all_infos.len()
107            ),
108            vec![args.source_range],
109        )));
110    }
111    Ok(())
112}
113
114/// Get the opposite edge to the edge given.
115pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
116    let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
117
118    let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
119    Ok(KclValue::Uuid {
120        value: edge,
121        meta: vec![args.source_range.into()],
122    })
123}
124
125async fn inner_get_opposite_edge(
126    edge: TagIdentifier,
127    exec_state: &mut ExecState,
128    args: Args,
129) -> Result<Uuid, KclError> {
130    check_tag_not_ambiguous(&edge, &args)?;
131    if args.ctx.no_engine_commands().await {
132        return Ok(exec_state.next_uuid());
133    }
134    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
135
136    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
137    let tagged_path_id = tagged_path.id;
138    let sketch_id = tagged_path.geometry.id();
139
140    let resp = exec_state
141        .send_modeling_cmd(
142            ModelingCmdMeta::from_args(exec_state, &args),
143            ModelingCmd::from(
144                mcmd::Solid3dGetOppositeEdge::builder()
145                    .edge_id(tagged_path_id)
146                    .object_id(sketch_id)
147                    .face_id(face_id)
148                    .build(),
149            ),
150        )
151        .await?;
152    let OkWebSocketResponseData::Modeling {
153        modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
154    } = &resp
155    else {
156        return Err(KclError::new_engine(KclErrorDetails::new(
157            format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {resp:?}"),
158            vec![args.source_range],
159        )));
160    };
161
162    Ok(opposite_edge.edge)
163}
164
165/// Get the next adjacent edge to the edge given.
166pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
167    let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
168
169    let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
170    Ok(KclValue::Uuid {
171        value: edge,
172        meta: vec![args.source_range.into()],
173    })
174}
175
176async fn inner_get_next_adjacent_edge(
177    edge: TagIdentifier,
178    exec_state: &mut ExecState,
179    args: Args,
180) -> Result<Uuid, KclError> {
181    check_tag_not_ambiguous(&edge, &args)?;
182    if args.ctx.no_engine_commands().await {
183        return Ok(exec_state.next_uuid());
184    }
185    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
186
187    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
188    let tagged_path_id = tagged_path.id;
189    let sketch_id = tagged_path.geometry.id();
190
191    let resp = exec_state
192        .send_modeling_cmd(
193            ModelingCmdMeta::from_args(exec_state, &args),
194            ModelingCmd::from(
195                mcmd::Solid3dGetNextAdjacentEdge::builder()
196                    .edge_id(tagged_path_id)
197                    .object_id(sketch_id)
198                    .face_id(face_id)
199                    .build(),
200            ),
201        )
202        .await?;
203
204    let OkWebSocketResponseData::Modeling {
205        modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
206    } = &resp
207    else {
208        return Err(KclError::new_engine(KclErrorDetails::new(
209            format!("mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {resp:?}"),
210            vec![args.source_range],
211        )));
212    };
213
214    adjacent_edge.edge.ok_or_else(|| {
215        KclError::new_type(KclErrorDetails::new(
216            format!("No edge found next adjacent to tag: `{}`", edge.value),
217            vec![args.source_range],
218        ))
219    })
220}
221
222/// Get the previous adjacent edge to the edge given.
223pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
224    let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
225
226    let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
227    Ok(KclValue::Uuid {
228        value: edge,
229        meta: vec![args.source_range.into()],
230    })
231}
232
233async fn inner_get_previous_adjacent_edge(
234    edge: TagIdentifier,
235    exec_state: &mut ExecState,
236    args: Args,
237) -> Result<Uuid, KclError> {
238    check_tag_not_ambiguous(&edge, &args)?;
239    if args.ctx.no_engine_commands().await {
240        return Ok(exec_state.next_uuid());
241    }
242    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
243
244    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
245    let tagged_path_id = tagged_path.id;
246    let sketch_id = tagged_path.geometry.id();
247
248    let resp = exec_state
249        .send_modeling_cmd(
250            ModelingCmdMeta::from_args(exec_state, &args),
251            ModelingCmd::from(
252                mcmd::Solid3dGetPrevAdjacentEdge::builder()
253                    .edge_id(tagged_path_id)
254                    .object_id(sketch_id)
255                    .face_id(face_id)
256                    .build(),
257            ),
258        )
259        .await?;
260    let OkWebSocketResponseData::Modeling {
261        modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
262    } = &resp
263    else {
264        return Err(KclError::new_engine(KclErrorDetails::new(
265            format!("mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {resp:?}"),
266            vec![args.source_range],
267        )));
268    };
269
270    adjacent_edge.edge.ok_or_else(|| {
271        KclError::new_type(KclErrorDetails::new(
272            format!("No edge found previous adjacent to tag: `{}`", edge.value),
273            vec![args.source_range],
274        ))
275    })
276}
277
278/// Get the shared edge between two faces.
279pub async fn get_common_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
280    let faces: Vec<FaceTag> = args.get_kw_arg(
281        "faces",
282        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Known(2)),
283        exec_state,
284    )?;
285
286    fn into_tag(face: FaceTag, source_range: SourceRange) -> Result<TagIdentifier, KclError> {
287        match face {
288            FaceTag::StartOrEnd(_) => Err(KclError::new_type(KclErrorDetails::new(
289                "getCommonEdge requires a tagged face, it cannot use `START` or `END` faces".to_owned(),
290                vec![source_range],
291            ))),
292            FaceTag::Tag(tag_identifier) => Ok(*tag_identifier),
293        }
294    }
295
296    let [face1, face2]: [FaceTag; 2] = faces.try_into().map_err(|_: Vec<FaceTag>| {
297        KclError::new_type(KclErrorDetails::new(
298            "getCommonEdge requires exactly two tags for faces".to_owned(),
299            vec![args.source_range],
300        ))
301    })?;
302
303    let face1 = into_tag(face1, args.source_range)?;
304    let face2 = into_tag(face2, args.source_range)?;
305
306    let edge = inner_get_common_edge(face1, face2, exec_state, args.clone()).await?;
307    Ok(KclValue::Uuid {
308        value: edge,
309        meta: vec![args.source_range.into()],
310    })
311}
312
313async fn inner_get_common_edge(
314    face1: TagIdentifier,
315    face2: TagIdentifier,
316    exec_state: &mut ExecState,
317    args: Args,
318) -> Result<Uuid, KclError> {
319    check_tag_not_ambiguous(&face1, &args)?;
320    check_tag_not_ambiguous(&face2, &args)?;
321    let id = exec_state.next_uuid();
322    if args.ctx.no_engine_commands().await {
323        return Ok(id);
324    }
325
326    let first_face_id = args.get_adjacent_face_to_tag(exec_state, &face1, false).await?;
327    let second_face_id = args.get_adjacent_face_to_tag(exec_state, &face2, false).await?;
328
329    let first_tagged_path = args.get_tag_engine_info(exec_state, &face1)?.clone();
330    let second_tagged_path = args.get_tag_engine_info(exec_state, &face2)?;
331
332    if first_tagged_path.geometry.id() != second_tagged_path.geometry.id() {
333        return Err(KclError::new_type(KclErrorDetails::new(
334            "getCommonEdge requires the faces to be in the same original sketch".to_string(),
335            vec![args.source_range],
336        )));
337    }
338
339    // Flush the batch for our fillets/chamfers if there are any.
340    // If we have a chamfer/fillet, flush the batch.
341    // TODO: we likely want to be a lot more persnickety _which_ fillets we are flushing
342    // but for now, we'll just flush everything.
343    if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
344        exec_state
345            .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
346            .await?;
347    } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
348        exec_state
349            .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
350            .await?;
351    }
352
353    let resp = exec_state
354        .send_modeling_cmd(
355            ModelingCmdMeta::from_args_id(exec_state, &args, id),
356            ModelingCmd::from(
357                mcmd::Solid3dGetCommonEdge::builder()
358                    .object_id(first_tagged_path.geometry.id())
359                    .face_ids([first_face_id, second_face_id])
360                    .build(),
361            ),
362        )
363        .await?;
364    let OkWebSocketResponseData::Modeling {
365        modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
366    } = &resp
367    else {
368        return Err(KclError::new_engine(KclErrorDetails::new(
369            format!("mcmd::Solid3dGetCommonEdge response was not as expected: {resp:?}"),
370            vec![args.source_range],
371        )));
372    };
373
374    common_edge.edge.ok_or_else(|| {
375        KclError::new_type(KclErrorDetails::new(
376            format!(
377                "No common edge was found between `{}` and `{}`",
378                face1.value, face2.value
379            ),
380            vec![args.source_range],
381        ))
382    })
383}
384
385pub async fn get_bounded_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
386    let face = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
387    let edge_val = args.get_kw_arg("edge", &RuntimeType::any(), exec_state)?;
388    let lower_bound = args.get_kw_arg_opt("lowerBound", &RuntimeType::num_any(), exec_state)?;
389    let upper_bound = args.get_kw_arg_opt("upperBound", &RuntimeType::num_any(), exec_state)?;
390
391    let bounded_edge = match &edge_val {
392        KclValue::Uuid { value, .. } => {
393            inner_get_bounded_edge_with_id(face, EdgeReference::Uuid(*value), lower_bound, upper_bound, exec_state, args.clone()).await?
394        }
395        KclValue::TagIdentifier(tag) => {
396            inner_get_bounded_edge_with_id(face, EdgeReference::Tag(tag.clone()), lower_bound, upper_bound, exec_state, args.clone()).await?
397        }
398        KclValue::Object { value: obj, .. } => {
399            let spec = parse_edge_specifier_object(obj, &args)?;
400            inner_get_bounded_edge_with_specifier(face, spec, lower_bound, upper_bound, &args)?
401        }
402        _ => {
403            return Err(KclError::new_type(KclErrorDetails::new(
404                "edge must be a tagged edge, edge UUID, or edge specifier object (e.g. { sideFaces = [...], endFaces = [...], index = 0 })".to_owned(),
405                vec![args.source_range],
406            )))
407        }
408    };
409    Ok(KclValue::BoundedEdge {
410        value: bounded_edge,
411        meta: vec![args.source_range.into()],
412    })
413}
414
415fn tag_or_uuid_from_value(value: &KclValue, field_name: &str, args: &Args) -> Result<TagOrUuid, KclError> {
416    match value {
417        KclValue::Uuid { value, .. } => Ok(TagOrUuid::Uuid(*value)),
418        KclValue::TagIdentifier(tag) => Ok(TagOrUuid::Tag(tag.clone())),
419        _ => Err(KclError::new_type(KclErrorDetails::new(
420            format!("{field_name} elements must be tags or UUIDs"),
421            vec![args.source_range],
422        ))),
423    }
424}
425
426fn parse_tag_or_uuid_array(
427    obj: &KclObjectFields,
428    field_name: &str,
429    required: bool,
430    args: &Args,
431) -> Result<Vec<TagOrUuid>, KclError> {
432    let Some(value) = obj.get(field_name) else {
433        return if required {
434            Err(KclError::new_type(KclErrorDetails::new(
435                format!("edge specifier object must have {field_name}"),
436                vec![args.source_range],
437            )))
438        } else {
439            Ok(Vec::new())
440        };
441    };
442    let values = value.as_slice().ok_or_else(|| {
443        KclError::new_type(KclErrorDetails::new(
444            format!("{field_name} must be an array"),
445            vec![args.source_range],
446        ))
447    })?;
448    values
449        .iter()
450        .map(|value| tag_or_uuid_from_value(value, field_name, args))
451        .collect()
452}
453
454fn parse_edge_specifier_index(obj: &KclObjectFields, args: &Args) -> Result<Option<u32>, KclError> {
455    let Some(index) = obj.get("index") else {
456        return Ok(None);
457    };
458    let KclValue::Number { value, .. } = index else {
459        return Err(KclError::new_type(KclErrorDetails::new(
460            "edge specifier 'index' must be a non-negative integer".to_owned(),
461            vec![args.source_range],
462        )));
463    };
464    if !value.is_finite() || value.fract() != 0.0 || *value < 0.0 || *value > u32::MAX as f64 {
465        return Err(KclError::new_type(KclErrorDetails::new(
466            "edge specifier 'index' must be a non-negative integer".to_owned(),
467            vec![args.source_range],
468        )));
469    }
470    Ok(Some(*value as u32))
471}
472
473pub(crate) fn is_edge_specifier_object(value: &KclValue) -> bool {
474    matches!(value, KclValue::Object { value, .. } if value.contains_key("sideFaces"))
475}
476
477pub(crate) fn parse_edge_specifier_value(value: &KclValue, args: &Args) -> Result<UnresolvedEdgeSpecifier, KclError> {
478    let KclValue::Object { value: obj, .. } = value else {
479        return Err(KclError::new_type(KclErrorDetails::new(
480            "edge specifier must be an object with 'sideFaces'".to_owned(),
481            vec![args.source_range],
482        )));
483    };
484    parse_edge_specifier_object(obj, args)
485}
486
487/// Parse a KCL object `{ sideFaces, endFaces?, index? }` into UnresolvedEdgeSpecifier. Used by getBoundedEdge and blend.
488pub(crate) fn parse_edge_specifier_object(
489    obj: &KclObjectFields,
490    args: &Args,
491) -> Result<UnresolvedEdgeSpecifier, KclError> {
492    let side_faces = parse_tag_or_uuid_array(obj, "sideFaces", true, args)?;
493    if side_faces.is_empty() {
494        return Err(KclError::new_semantic(KclErrorDetails::new(
495            "sideFaces must be an array of at least one face, but zero were given".to_owned(),
496            vec![args.source_range],
497        )));
498    }
499    let end_faces = parse_tag_or_uuid_array(obj, "endFaces", false, args)?;
500    let index = parse_edge_specifier_index(obj, args)?;
501    Ok(UnresolvedEdgeSpecifier {
502        side_faces,
503        end_faces,
504        index,
505    })
506}
507
508async fn resolve_as_face_id(value: &TagOrUuid, exec_state: &mut ExecState, args: &Args) -> Result<Uuid, KclError> {
509    match value {
510        TagOrUuid::Uuid(uuid) => Ok(*uuid),
511        TagOrUuid::Tag(tag) => {
512            FaceTag::Tag(tag.clone())
513                .get_face_id_from_tag(exec_state, args, false)
514                .await
515        }
516    }
517}
518
519async fn resolve_as_adjacent_face_or_tag_id(
520    value: &TagOrUuid,
521    exec_state: &mut ExecState,
522    args: &Args,
523) -> Result<Uuid, KclError> {
524    match value {
525        TagOrUuid::Uuid(uuid) => Ok(*uuid),
526        TagOrUuid::Tag(tag) => match args.get_adjacent_face_to_tag(exec_state, tag, false).await {
527            Ok(face_id) => Ok(face_id),
528            Err(_) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
529        },
530    }
531}
532
533async fn resolve_as_edge_faces(
534    value: &TagOrUuid,
535    object_id: Uuid,
536    exec_state: &mut ExecState,
537    args: &Args,
538) -> Result<Vec<Uuid>, KclError> {
539    match value {
540        TagOrUuid::Uuid(uuid) => Ok(vec![*uuid]),
541        TagOrUuid::Tag(tag) => {
542            let edge_id = args.get_tag_engine_info(exec_state, tag)?.id;
543            get_face_ids_for_edge(exec_state, object_id, edge_id, args).await
544        }
545    }
546}
547
548pub(crate) async fn resolve_edge_specifier_with_face_tags(
549    unresolved: &UnresolvedEdgeSpecifier,
550    exec_state: &mut ExecState,
551    args: &Args,
552) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
553    let mut side_faces = Vec::with_capacity(unresolved.side_faces.len());
554    for value in &unresolved.side_faces {
555        side_faces.push(resolve_as_face_id(value, exec_state, args).await?);
556    }
557    let mut end_faces = Vec::with_capacity(unresolved.end_faces.len());
558    for value in &unresolved.end_faces {
559        end_faces.push(resolve_as_face_id(value, exec_state, args).await?);
560    }
561    Ok(kcmc::shared::EdgeSpecifier::builder()
562        .side_faces(side_faces)
563        .end_faces(end_faces)
564        .maybe_index(unresolved.index)
565        .build())
566}
567
568pub(crate) async fn resolve_edge_specifier_with_adjacent_faces_or_tag_ids(
569    unresolved: &UnresolvedEdgeSpecifier,
570    exec_state: &mut ExecState,
571    args: &Args,
572) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
573    let mut side_faces = Vec::with_capacity(unresolved.side_faces.len());
574    for value in &unresolved.side_faces {
575        side_faces.push(resolve_as_adjacent_face_or_tag_id(value, exec_state, args).await?);
576    }
577    let mut end_faces = Vec::with_capacity(unresolved.end_faces.len());
578    for value in &unresolved.end_faces {
579        end_faces.push(resolve_as_adjacent_face_or_tag_id(value, exec_state, args).await?);
580    }
581    Ok(kcmc::shared::EdgeSpecifier::builder()
582        .side_faces(side_faces)
583        .end_faces(end_faces)
584        .maybe_index(unresolved.index)
585        .build())
586}
587
588pub(crate) async fn parse_edge_refs_to_references(
589    edge_refs: Vec<KclValue>,
590    exec_state: &mut ExecState,
591    args: &Args,
592) -> Result<Vec<kcmc::shared::EdgeSpecifier>, KclError> {
593    if edge_refs.is_empty() {
594        return Err(KclError::new_semantic(KclErrorDetails::new(
595            "You must provide at least one edge".to_owned(),
596            vec![args.source_range],
597        )));
598    }
599
600    let mut edge_references = Vec::with_capacity(edge_refs.len());
601    for edge_ref_value in &edge_refs {
602        let spec = parse_edge_specifier_value(edge_ref_value, args)?;
603        edge_references.push(resolve_edge_specifier_with_face_tags(&spec, exec_state, args).await?);
604    }
605    Ok(edge_references)
606}
607
608/// Get the face (surface body) id from the first side_face of an unresolved
609/// specifier. Used when building a BoundedEdge from an edge specifier object in
610/// blend().
611pub(super) fn face_id_from_first_side_face(
612    spec: &UnresolvedEdgeSpecifier,
613    exec_state: &mut ExecState,
614    args: &Args,
615) -> Result<Uuid, KclError> {
616    let first = spec.side_faces.first().ok_or_else(|| {
617        KclError::new_type(KclErrorDetails::new(
618            "edge specifier must have at least one sideFace".to_owned(),
619            vec![args.source_range],
620        ))
621    })?;
622    match first {
623        TagOrUuid::Uuid(u) => Ok(*u),
624        TagOrUuid::Tag(t) => {
625            let info = args.get_tag_engine_info(exec_state, t)?;
626            Ok(info.geometry.id())
627        }
628    }
629}
630
631pub(crate) async fn inner_get_bounded_edge_with_id(
632    face: Solid,
633    edge: EdgeReference,
634    lower_bound: Option<TyF64>,
635    upper_bound: Option<TyF64>,
636    exec_state: &mut ExecState,
637    args: Args,
638) -> Result<BoundedEdge, KclError> {
639    let (lb, ub) = bounds_from_opts(lower_bound, upper_bound, &args)?;
640    let edge_id = edge.get_engine_id(exec_state, &args)?;
641    Ok(BoundedEdge {
642        face_id: face.id,
643        edge_id: Some(edge_id),
644        edge_specifier: None,
645        lower_bound: lb,
646        upper_bound: ub,
647    })
648}
649
650fn inner_get_bounded_edge_with_specifier(
651    face: Solid,
652    spec: UnresolvedEdgeSpecifier,
653    lower_bound: Option<TyF64>,
654    upper_bound: Option<TyF64>,
655    args: &Args,
656) -> Result<BoundedEdge, KclError> {
657    let (lb, ub) = bounds_from_opts(lower_bound, upper_bound, args)?;
658    Ok(BoundedEdge {
659        face_id: face.id,
660        edge_id: None,
661        edge_specifier: Some(spec),
662        lower_bound: lb,
663        upper_bound: ub,
664    })
665}
666
667fn bounds_from_opts(
668    lower_bound: Option<TyF64>,
669    upper_bound: Option<TyF64>,
670    args: &Args,
671) -> Result<(f32, f32), KclError> {
672    let lower_bound = if let Some(lower_bound) = lower_bound {
673        let val = lower_bound.n as f32;
674        if !(0.0..=1.0).contains(&val) {
675            return Err(KclError::new_semantic(KclErrorDetails::new(
676                format!(
677                    "Invalid value: lowerBound must be between 0.0 and 1.0, provided {}",
678                    val
679                ),
680                vec![args.source_range],
681            )));
682        }
683        val
684    } else {
685        0.0_f32
686    };
687    let upper_bound = if let Some(upper_bound) = upper_bound {
688        let val = upper_bound.n as f32;
689        if !(0.0..=1.0).contains(&val) {
690            return Err(KclError::new_semantic(KclErrorDetails::new(
691                format!(
692                    "Invalid value: upperBound must be between 0.0 and 1.0, provided {}",
693                    val
694                ),
695                vec![args.source_range],
696            )));
697        }
698        val
699    } else {
700        1.0_f32
701    };
702    Ok((lower_bound, upper_bound))
703}
704
705/// Resolve an unresolved edge specifier (tags/UUIDs) to engine EdgeSpecifier (face UUIDs) for blend. Called from blend().
706pub(crate) async fn resolve_unresolved_edge_specifier(
707    object_id: Uuid,
708    unresolved: &UnresolvedEdgeSpecifier,
709    exec_state: &mut ExecState,
710    args: &Args,
711) -> Result<kcmc::shared::EdgeSpecifier, KclError> {
712    let mut side_faces = Vec::new();
713    for v in &unresolved.side_faces {
714        side_faces.extend(resolve_as_edge_faces(v, object_id, exec_state, args).await?);
715    }
716    let mut end_faces = Vec::new();
717    for v in &unresolved.end_faces {
718        end_faces.extend(resolve_as_edge_faces(v, object_id, exec_state, args).await?);
719    }
720    Ok(kcmc::shared::EdgeSpecifier::builder()
721        .side_faces(side_faces)
722        .end_faces(end_faces)
723        .maybe_index(unresolved.index)
724        .build())
725}