kcl_lib/std/
edge.rs

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