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 uuid::Uuid;
10
11use crate::SourceRange;
12use crate::errors::KclError;
13use crate::errors::KclErrorDetails;
14use crate::execution::BoundedEdge;
15use crate::execution::ExecState;
16use crate::execution::ExtrudeSurface;
17use crate::execution::KclValue;
18use crate::execution::ModelingCmdMeta;
19use crate::execution::Solid;
20use crate::execution::TagIdentifier;
21use crate::execution::types::ArrayLen;
22use crate::execution::types::RuntimeType;
23use crate::std::Args;
24use crate::std::args::TyF64;
25use crate::std::fillet::EdgeReference;
26use crate::std::sketch::FaceTag;
27
28/// Check that a tag does not map to multiple edges (ambiguous region mapping).
29pub(super) fn check_tag_not_ambiguous(tag: &TagIdentifier, args: &Args) -> Result<(), KclError> {
30    let all_infos = tag.get_all_cur_info();
31    if all_infos.len() > 1 {
32        return Err(KclError::new_semantic(KclErrorDetails::new(
33            format!(
34                "Tag `{}` is ambiguous: it maps to {} edges in the region. Use a more specific reference.",
35                tag.value,
36                all_infos.len()
37            ),
38            vec![args.source_range],
39        )));
40    }
41    Ok(())
42}
43
44/// Get the opposite edge to the edge given.
45pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
46    let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
47
48    let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
49    Ok(KclValue::Uuid {
50        value: edge,
51        meta: vec![args.source_range.into()],
52    })
53}
54
55async fn inner_get_opposite_edge(
56    edge: TagIdentifier,
57    exec_state: &mut ExecState,
58    args: Args,
59) -> Result<Uuid, KclError> {
60    check_tag_not_ambiguous(&edge, &args)?;
61    if args.ctx.no_engine_commands().await {
62        return Ok(exec_state.next_uuid());
63    }
64    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
65
66    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
67    let tagged_path_id = tagged_path.id;
68    let sketch_id = tagged_path.geometry.id();
69
70    let resp = exec_state
71        .send_modeling_cmd(
72            ModelingCmdMeta::from_args(exec_state, &args),
73            ModelingCmd::from(
74                mcmd::Solid3dGetOppositeEdge::builder()
75                    .edge_id(tagged_path_id)
76                    .object_id(sketch_id)
77                    .face_id(face_id)
78                    .build(),
79            ),
80        )
81        .await?;
82    let OkWebSocketResponseData::Modeling {
83        modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
84    } = &resp
85    else {
86        return Err(KclError::new_engine(KclErrorDetails::new(
87            format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {resp:?}"),
88            vec![args.source_range],
89        )));
90    };
91
92    Ok(opposite_edge.edge)
93}
94
95/// Get the next adjacent edge to the edge given.
96pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
97    let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
98
99    let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
100    Ok(KclValue::Uuid {
101        value: edge,
102        meta: vec![args.source_range.into()],
103    })
104}
105
106async fn inner_get_next_adjacent_edge(
107    edge: TagIdentifier,
108    exec_state: &mut ExecState,
109    args: Args,
110) -> Result<Uuid, KclError> {
111    check_tag_not_ambiguous(&edge, &args)?;
112    if args.ctx.no_engine_commands().await {
113        return Ok(exec_state.next_uuid());
114    }
115    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
116
117    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
118    let tagged_path_id = tagged_path.id;
119    let sketch_id = tagged_path.geometry.id();
120
121    let resp = exec_state
122        .send_modeling_cmd(
123            ModelingCmdMeta::from_args(exec_state, &args),
124            ModelingCmd::from(
125                mcmd::Solid3dGetNextAdjacentEdge::builder()
126                    .edge_id(tagged_path_id)
127                    .object_id(sketch_id)
128                    .face_id(face_id)
129                    .build(),
130            ),
131        )
132        .await?;
133
134    let OkWebSocketResponseData::Modeling {
135        modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
136    } = &resp
137    else {
138        return Err(KclError::new_engine(KclErrorDetails::new(
139            format!("mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {resp:?}"),
140            vec![args.source_range],
141        )));
142    };
143
144    adjacent_edge.edge.ok_or_else(|| {
145        KclError::new_type(KclErrorDetails::new(
146            format!("No edge found next adjacent to tag: `{}`", edge.value),
147            vec![args.source_range],
148        ))
149    })
150}
151
152/// Get the previous adjacent edge to the edge given.
153pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
154    let input_edge = args.get_unlabeled_kw_arg("edge", &RuntimeType::tagged_edge(), exec_state)?;
155
156    let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
157    Ok(KclValue::Uuid {
158        value: edge,
159        meta: vec![args.source_range.into()],
160    })
161}
162
163async fn inner_get_previous_adjacent_edge(
164    edge: TagIdentifier,
165    exec_state: &mut ExecState,
166    args: Args,
167) -> Result<Uuid, KclError> {
168    check_tag_not_ambiguous(&edge, &args)?;
169    if args.ctx.no_engine_commands().await {
170        return Ok(exec_state.next_uuid());
171    }
172    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
173
174    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
175    let tagged_path_id = tagged_path.id;
176    let sketch_id = tagged_path.geometry.id();
177
178    let resp = exec_state
179        .send_modeling_cmd(
180            ModelingCmdMeta::from_args(exec_state, &args),
181            ModelingCmd::from(
182                mcmd::Solid3dGetPrevAdjacentEdge::builder()
183                    .edge_id(tagged_path_id)
184                    .object_id(sketch_id)
185                    .face_id(face_id)
186                    .build(),
187            ),
188        )
189        .await?;
190    let OkWebSocketResponseData::Modeling {
191        modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
192    } = &resp
193    else {
194        return Err(KclError::new_engine(KclErrorDetails::new(
195            format!("mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {resp:?}"),
196            vec![args.source_range],
197        )));
198    };
199
200    adjacent_edge.edge.ok_or_else(|| {
201        KclError::new_type(KclErrorDetails::new(
202            format!("No edge found previous adjacent to tag: `{}`", edge.value),
203            vec![args.source_range],
204        ))
205    })
206}
207
208/// Get the shared edge between two faces.
209pub async fn get_common_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
210    let faces: Vec<FaceTag> = args.get_kw_arg(
211        "faces",
212        &RuntimeType::Array(Box::new(RuntimeType::tagged_face()), ArrayLen::Known(2)),
213        exec_state,
214    )?;
215
216    fn into_tag(face: FaceTag, source_range: SourceRange) -> Result<TagIdentifier, KclError> {
217        match face {
218            FaceTag::StartOrEnd(_) => Err(KclError::new_type(KclErrorDetails::new(
219                "getCommonEdge requires a tagged face, it cannot use `START` or `END` faces".to_owned(),
220                vec![source_range],
221            ))),
222            FaceTag::Tag(tag_identifier) => Ok(*tag_identifier),
223        }
224    }
225
226    let [face1, face2]: [FaceTag; 2] = faces.try_into().map_err(|_: Vec<FaceTag>| {
227        KclError::new_type(KclErrorDetails::new(
228            "getCommonEdge requires exactly two tags for faces".to_owned(),
229            vec![args.source_range],
230        ))
231    })?;
232
233    let face1 = into_tag(face1, args.source_range)?;
234    let face2 = into_tag(face2, args.source_range)?;
235
236    let edge = inner_get_common_edge(face1, face2, exec_state, args.clone()).await?;
237    Ok(KclValue::Uuid {
238        value: edge,
239        meta: vec![args.source_range.into()],
240    })
241}
242
243async fn inner_get_common_edge(
244    face1: TagIdentifier,
245    face2: TagIdentifier,
246    exec_state: &mut ExecState,
247    args: Args,
248) -> Result<Uuid, KclError> {
249    check_tag_not_ambiguous(&face1, &args)?;
250    check_tag_not_ambiguous(&face2, &args)?;
251    let id = exec_state.next_uuid();
252    if args.ctx.no_engine_commands().await {
253        return Ok(id);
254    }
255
256    let first_face_id = args.get_adjacent_face_to_tag(exec_state, &face1, false).await?;
257    let second_face_id = args.get_adjacent_face_to_tag(exec_state, &face2, false).await?;
258
259    let first_tagged_path = args.get_tag_engine_info(exec_state, &face1)?.clone();
260    let second_tagged_path = args.get_tag_engine_info(exec_state, &face2)?;
261
262    if first_tagged_path.geometry.id() != second_tagged_path.geometry.id() {
263        return Err(KclError::new_type(KclErrorDetails::new(
264            "getCommonEdge requires the faces to be in the same original sketch".to_string(),
265            vec![args.source_range],
266        )));
267    }
268
269    // Flush the batch for our fillets/chamfers if there are any.
270    // If we have a chamfer/fillet, flush the batch.
271    // TODO: we likely want to be a lot more persnickety _which_ fillets we are flushing
272    // but for now, we'll just flush everything.
273    if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
274        exec_state
275            .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
276            .await?;
277    } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
278        exec_state
279            .flush_batch(ModelingCmdMeta::from_args(exec_state, &args), true)
280            .await?;
281    }
282
283    let resp = exec_state
284        .send_modeling_cmd(
285            ModelingCmdMeta::from_args_id(exec_state, &args, id),
286            ModelingCmd::from(
287                mcmd::Solid3dGetCommonEdge::builder()
288                    .object_id(first_tagged_path.geometry.id())
289                    .face_ids([first_face_id, second_face_id])
290                    .build(),
291            ),
292        )
293        .await?;
294    let OkWebSocketResponseData::Modeling {
295        modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
296    } = &resp
297    else {
298        return Err(KclError::new_engine(KclErrorDetails::new(
299            format!("mcmd::Solid3dGetCommonEdge response was not as expected: {resp:?}"),
300            vec![args.source_range],
301        )));
302    };
303
304    common_edge.edge.ok_or_else(|| {
305        KclError::new_type(KclErrorDetails::new(
306            format!(
307                "No common edge was found between `{}` and `{}`",
308                face1.value, face2.value
309            ),
310            vec![args.source_range],
311        ))
312    })
313}
314
315pub async fn get_bounded_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
316    let face = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
317    let edge = args.get_kw_arg("edge", &RuntimeType::edge(), exec_state)?;
318    let lower_bound = args.get_kw_arg_opt("lowerBound", &RuntimeType::num_any(), exec_state)?;
319    let upper_bound = args.get_kw_arg_opt("upperBound", &RuntimeType::num_any(), exec_state)?;
320
321    let bounded_edge = inner_get_bounded_edge(face, edge, lower_bound, upper_bound, exec_state, args.clone()).await?;
322    Ok(KclValue::BoundedEdge {
323        value: bounded_edge,
324        meta: vec![args.source_range.into()],
325    })
326}
327
328pub async fn inner_get_bounded_edge(
329    face: Solid,
330    edge: EdgeReference,
331    lower_bound: Option<TyF64>,
332    upper_bound: Option<TyF64>,
333    exec_state: &mut ExecState,
334    args: Args,
335) -> Result<BoundedEdge, KclError> {
336    let lower_bound = if let Some(lower_bound) = lower_bound {
337        let val = lower_bound.n as f32;
338        if !(0.0..=1.0).contains(&val) {
339            return Err(KclError::new_semantic(KclErrorDetails::new(
340                format!(
341                    "Invalid value: lowerBound must be between 0.0 and 1.0, provided {}",
342                    val
343                ),
344                vec![args.source_range],
345            )));
346        }
347        val
348    } else {
349        0.0_f32
350    };
351
352    let upper_bound = if let Some(upper_bound) = upper_bound {
353        let val = upper_bound.n as f32;
354        if !(0.0..=1.0).contains(&val) {
355            return Err(KclError::new_semantic(KclErrorDetails::new(
356                format!(
357                    "Invalid value: upperBound must be between 0.0 and 1.0, provided {}",
358                    val
359                ),
360                vec![args.source_range],
361            )));
362        }
363        val
364    } else {
365        1.0_f32
366    };
367
368    Ok(BoundedEdge {
369        face_id: face.id,
370        edge_id: edge.get_engine_id(exec_state, &args)?,
371        lower_bound,
372        upper_bound,
373    })
374}