kcl_lib/std/
edge.rs

1//! Edge helper functions.
2
3use anyhow::Result;
4use kcmc::{each_cmd as mcmd, ok_response::OkModelingCmdResponse, websocket::OkWebSocketResponseData, ModelingCmd};
5use kittycad_modeling_cmds as kcmc;
6use uuid::Uuid;
7
8use crate::{
9    errors::{KclError, KclErrorDetails},
10    execution::{
11        types::{ArrayLen, RuntimeType},
12        ExecState, ExtrudeSurface, KclValue, TagIdentifier,
13    },
14    std::Args,
15};
16
17/// Get the opposite edge to the edge given.
18pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
19    let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::tag_identifier(), exec_state)?;
20
21    let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
22    Ok(KclValue::Uuid {
23        value: edge,
24        meta: vec![args.source_range.into()],
25    })
26}
27
28async fn inner_get_opposite_edge(
29    edge: TagIdentifier,
30    exec_state: &mut ExecState,
31    args: Args,
32) -> Result<Uuid, KclError> {
33    if args.ctx.no_engine_commands().await {
34        return Ok(exec_state.next_uuid());
35    }
36    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
37
38    let id = exec_state.next_uuid();
39    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
40
41    let resp = args
42        .send_modeling_cmd(
43            id,
44            ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
45                edge_id: tagged_path.id,
46                object_id: tagged_path.sketch,
47                face_id,
48            }),
49        )
50        .await?;
51    let OkWebSocketResponseData::Modeling {
52        modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
53    } = &resp
54    else {
55        return Err(KclError::new_engine(KclErrorDetails::new(
56            format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {:?}", resp),
57            vec![args.source_range],
58        )));
59    };
60
61    Ok(opposite_edge.edge)
62}
63
64/// Get the next adjacent edge to the edge given.
65pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
66    let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::tag_identifier(), exec_state)?;
67
68    let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
69    Ok(KclValue::Uuid {
70        value: edge,
71        meta: vec![args.source_range.into()],
72    })
73}
74
75async fn inner_get_next_adjacent_edge(
76    edge: TagIdentifier,
77    exec_state: &mut ExecState,
78    args: Args,
79) -> Result<Uuid, KclError> {
80    if args.ctx.no_engine_commands().await {
81        return Ok(exec_state.next_uuid());
82    }
83    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
84
85    let id = exec_state.next_uuid();
86    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
87
88    let resp = args
89        .send_modeling_cmd(
90            id,
91            ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
92                edge_id: tagged_path.id,
93                object_id: tagged_path.sketch,
94                face_id,
95            }),
96        )
97        .await?;
98
99    let OkWebSocketResponseData::Modeling {
100        modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
101    } = &resp
102    else {
103        return Err(KclError::new_engine(KclErrorDetails::new(
104            format!(
105                "mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {:?}",
106                resp
107            ),
108            vec![args.source_range],
109        )));
110    };
111
112    adjacent_edge.edge.ok_or_else(|| {
113        KclError::new_type(KclErrorDetails::new(
114            format!("No edge found next adjacent to tag: `{}`", edge.value),
115            vec![args.source_range],
116        ))
117    })
118}
119
120/// Get the previous adjacent edge to the edge given.
121pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
122    let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::tag_identifier(), exec_state)?;
123
124    let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
125    Ok(KclValue::Uuid {
126        value: edge,
127        meta: vec![args.source_range.into()],
128    })
129}
130
131async fn inner_get_previous_adjacent_edge(
132    edge: TagIdentifier,
133    exec_state: &mut ExecState,
134    args: Args,
135) -> Result<Uuid, KclError> {
136    if args.ctx.no_engine_commands().await {
137        return Ok(exec_state.next_uuid());
138    }
139    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
140
141    let id = exec_state.next_uuid();
142    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
143
144    let resp = args
145        .send_modeling_cmd(
146            id,
147            ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge {
148                edge_id: tagged_path.id,
149                object_id: tagged_path.sketch,
150                face_id,
151            }),
152        )
153        .await?;
154    let OkWebSocketResponseData::Modeling {
155        modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
156    } = &resp
157    else {
158        return Err(KclError::new_engine(KclErrorDetails::new(
159            format!(
160                "mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {:?}",
161                resp
162            ),
163            vec![args.source_range],
164        )));
165    };
166
167    adjacent_edge.edge.ok_or_else(|| {
168        KclError::new_type(KclErrorDetails::new(
169            format!("No edge found previous adjacent to tag: `{}`", edge.value),
170            vec![args.source_range],
171        ))
172    })
173}
174
175/// Get the shared edge between two faces.
176pub async fn get_common_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
177    let faces: Vec<TagIdentifier> = args.get_kw_arg_typed(
178        "faces",
179        &RuntimeType::Array(Box::new(RuntimeType::tag_identifier()), ArrayLen::Known(2)),
180        exec_state,
181    )?;
182
183    let edge = inner_get_common_edge(faces, exec_state, args.clone()).await?;
184    Ok(KclValue::Uuid {
185        value: edge,
186        meta: vec![args.source_range.into()],
187    })
188}
189
190async fn inner_get_common_edge(
191    faces: Vec<TagIdentifier>,
192    exec_state: &mut ExecState,
193    args: Args,
194) -> Result<Uuid, KclError> {
195    let id = exec_state.next_uuid();
196    if args.ctx.no_engine_commands().await {
197        return Ok(id);
198    }
199
200    if faces.len() != 2 {
201        return Err(KclError::new_type(KclErrorDetails::new(
202            "getCommonEdge requires exactly two tags for faces".to_string(),
203            vec![args.source_range],
204        )));
205    }
206    let first_face_id = args.get_adjacent_face_to_tag(exec_state, &faces[0], false).await?;
207    let second_face_id = args.get_adjacent_face_to_tag(exec_state, &faces[1], false).await?;
208
209    let first_tagged_path = args.get_tag_engine_info(exec_state, &faces[0])?.clone();
210    let second_tagged_path = args.get_tag_engine_info(exec_state, &faces[1])?;
211
212    if first_tagged_path.sketch != second_tagged_path.sketch {
213        return Err(KclError::new_type(KclErrorDetails::new(
214            "getCommonEdge requires the faces to be in the same original sketch".to_string(),
215            vec![args.source_range],
216        )));
217    }
218
219    // Flush the batch for our fillets/chamfers if there are any.
220    // If we have a chamfer/fillet, flush the batch.
221    // TODO: we likely want to be a lot more persnickety _which_ fillets we are flushing
222    // but for now, we'll just flush everything.
223    if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
224        args.ctx.engine.flush_batch(true, args.source_range).await?;
225    } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
226        args.ctx.engine.flush_batch(true, args.source_range).await?;
227    }
228
229    let resp = args
230        .send_modeling_cmd(
231            id,
232            ModelingCmd::from(mcmd::Solid3dGetCommonEdge {
233                object_id: first_tagged_path.sketch,
234                face_ids: [first_face_id, second_face_id],
235            }),
236        )
237        .await?;
238    let OkWebSocketResponseData::Modeling {
239        modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
240    } = &resp
241    else {
242        return Err(KclError::new_engine(KclErrorDetails::new(
243            format!("mcmd::Solid3dGetCommonEdge response was not as expected: {:?}", resp),
244            vec![args.source_range],
245        )));
246    };
247
248    common_edge.edge.ok_or_else(|| {
249        KclError::new_type(KclErrorDetails::new(
250            format!(
251                "No common edge was found between `{}` and `{}`",
252                faces[0].value, faces[1].value
253            ),
254            vec![args.source_range],
255        ))
256    })
257}