kcl_lib/std/
edge.rs

1//! Edge helper functions.
2
3use anyhow::Result;
4use kcl_derive_docs::stdlib;
5use kcmc::{each_cmd as mcmd, ok_response::OkModelingCmdResponse, websocket::OkWebSocketResponseData, ModelingCmd};
6use kittycad_modeling_cmds as kcmc;
7use uuid::Uuid;
8
9use crate::{
10    errors::{KclError, KclErrorDetails},
11    execution::{types::RuntimeType, ExecState, ExtrudeSurface, KclValue, TagIdentifier},
12    std::Args,
13};
14
15/// Get the opposite edge to the edge given.
16pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
17    let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::tag_identifier(), exec_state)?;
18
19    let edge = inner_get_opposite_edge(input_edge, exec_state, args.clone()).await?;
20    Ok(KclValue::Uuid {
21        value: edge,
22        meta: vec![args.source_range.into()],
23    })
24}
25
26/// Get the opposite edge to the edge given.
27///
28/// ```no_run
29/// exampleSketch = startSketchOn(XZ)
30///   |> startProfile(at = [0, 0])
31///   |> line(end = [10, 0])
32///   |> angledLine(
33///        angle = 60,
34///        length = 10,
35///      )
36///   |> angledLine(
37///        angle = 120,
38///        length = 10,
39///      )
40///   |> line(end = [-10, 0])
41///   |> angledLine(
42///        angle = 240,
43///        length = 10,
44///        tag = $referenceEdge,
45///      )
46///   |> close()
47///
48/// example = extrude(exampleSketch, length = 5)
49///   |> fillet(
50///     radius = 3,
51///     tags = [getOppositeEdge(referenceEdge)],
52///   )
53/// ```
54#[stdlib {
55    name = "getOppositeEdge",
56    unlabeled_first = true,
57    args = {
58        edge = { docs = "The tag of the edge you want to find the opposite edge of." },
59    },
60    tags = ["sketch"]
61}]
62async fn inner_get_opposite_edge(
63    edge: TagIdentifier,
64    exec_state: &mut ExecState,
65    args: Args,
66) -> Result<Uuid, KclError> {
67    if args.ctx.no_engine_commands().await {
68        return Ok(exec_state.next_uuid());
69    }
70    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
71
72    let id = exec_state.next_uuid();
73    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
74
75    let resp = args
76        .send_modeling_cmd(
77            id,
78            ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
79                edge_id: tagged_path.id,
80                object_id: tagged_path.sketch,
81                face_id,
82            }),
83        )
84        .await?;
85    let OkWebSocketResponseData::Modeling {
86        modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
87    } = &resp
88    else {
89        return Err(KclError::Engine(KclErrorDetails::new(
90            format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {:?}", resp),
91            vec![args.source_range],
92        )));
93    };
94
95    Ok(opposite_edge.edge)
96}
97
98/// Get the next adjacent edge to the edge given.
99pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
100    let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::tag_identifier(), exec_state)?;
101
102    let edge = inner_get_next_adjacent_edge(input_edge, exec_state, args.clone()).await?;
103    Ok(KclValue::Uuid {
104        value: edge,
105        meta: vec![args.source_range.into()],
106    })
107}
108
109/// Get the next adjacent edge to the edge given.
110///
111/// ```no_run
112/// exampleSketch = startSketchOn(XZ)
113///   |> startProfile(at = [0, 0])
114///   |> line(end = [10, 0])
115///   |> angledLine(
116///        angle = 60,
117///        length = 10,
118///      )
119///   |> angledLine(
120///        angle = 120,
121///        length = 10,
122///      )
123///   |> line(end = [-10, 0])
124///   |> angledLine(
125///        angle = 240,
126///        length = 10,
127///        tag = $referenceEdge,
128///      )
129///   |> close()
130///
131/// example = extrude(exampleSketch, length = 5)
132///   |> fillet(
133///     radius = 3,
134///     tags = [getNextAdjacentEdge(referenceEdge)],
135///   )
136/// ```
137#[stdlib {
138    name = "getNextAdjacentEdge",
139    unlabeled_first = true,
140    args = {
141        edge = { docs = "The tag of the edge you want to find the next adjacent edge of." },
142    },
143    tags = ["sketch"]
144}]
145async fn inner_get_next_adjacent_edge(
146    edge: TagIdentifier,
147    exec_state: &mut ExecState,
148    args: Args,
149) -> Result<Uuid, KclError> {
150    if args.ctx.no_engine_commands().await {
151        return Ok(exec_state.next_uuid());
152    }
153    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
154
155    let id = exec_state.next_uuid();
156    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
157
158    let resp = args
159        .send_modeling_cmd(
160            id,
161            ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
162                edge_id: tagged_path.id,
163                object_id: tagged_path.sketch,
164                face_id,
165            }),
166        )
167        .await?;
168
169    let OkWebSocketResponseData::Modeling {
170        modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
171    } = &resp
172    else {
173        return Err(KclError::Engine(KclErrorDetails::new(
174            format!(
175                "mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {:?}",
176                resp
177            ),
178            vec![args.source_range],
179        )));
180    };
181
182    adjacent_edge.edge.ok_or_else(|| {
183        KclError::Type(KclErrorDetails::new(
184            format!("No edge found next adjacent to tag: `{}`", edge.value),
185            vec![args.source_range],
186        ))
187    })
188}
189
190/// Get the previous adjacent edge to the edge given.
191pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
192    let input_edge = args.get_unlabeled_kw_arg_typed("edge", &RuntimeType::tag_identifier(), exec_state)?;
193
194    let edge = inner_get_previous_adjacent_edge(input_edge, exec_state, args.clone()).await?;
195    Ok(KclValue::Uuid {
196        value: edge,
197        meta: vec![args.source_range.into()],
198    })
199}
200
201/// Get the previous adjacent edge to the edge given.
202///
203/// ```no_run
204/// exampleSketch = startSketchOn(XZ)
205///   |> startProfile(at = [0, 0])
206///   |> line(end = [10, 0])
207///   |> angledLine(
208///        angle = 60,
209///        length = 10,
210///      )
211///   |> angledLine(
212///        angle = 120,
213///        length = 10,
214///      )
215///   |> line(end = [-10, 0])
216///   |> angledLine(
217///        angle = 240,
218///        length = 10,
219///        tag = $referenceEdge,
220///      )
221///   |> close()
222///
223/// example = extrude(exampleSketch, length = 5)
224///   |> fillet(
225///     radius = 3,
226///     tags = [getPreviousAdjacentEdge(referenceEdge)],
227///   )
228/// ```
229#[stdlib {
230    name = "getPreviousAdjacentEdge",
231    unlabeled_first = true,
232    args = {
233        edge = { docs = "The tag of the edge you want to find the previous adjacent edge of." },
234    },
235    tags = ["sketch"]
236}]
237async fn inner_get_previous_adjacent_edge(
238    edge: TagIdentifier,
239    exec_state: &mut ExecState,
240    args: Args,
241) -> Result<Uuid, KclError> {
242    if args.ctx.no_engine_commands().await {
243        return Ok(exec_state.next_uuid());
244    }
245    let face_id = args.get_adjacent_face_to_tag(exec_state, &edge, false).await?;
246
247    let id = exec_state.next_uuid();
248    let tagged_path = args.get_tag_engine_info(exec_state, &edge)?;
249
250    let resp = args
251        .send_modeling_cmd(
252            id,
253            ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge {
254                edge_id: tagged_path.id,
255                object_id: tagged_path.sketch,
256                face_id,
257            }),
258        )
259        .await?;
260    let OkWebSocketResponseData::Modeling {
261        modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
262    } = &resp
263    else {
264        return Err(KclError::Engine(KclErrorDetails::new(
265            format!(
266                "mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {:?}",
267                resp
268            ),
269            vec![args.source_range],
270        )));
271    };
272
273    adjacent_edge.edge.ok_or_else(|| {
274        KclError::Type(KclErrorDetails::new(
275            format!("No edge found previous adjacent to tag: `{}`", edge.value),
276            vec![args.source_range],
277        ))
278    })
279}
280
281/// Get the shared edge between two faces.
282pub async fn get_common_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
283    let faces: Vec<TagIdentifier> = args.get_kw_arg("faces")?;
284
285    let edge = inner_get_common_edge(faces, exec_state, args.clone()).await?;
286    Ok(KclValue::Uuid {
287        value: edge,
288        meta: vec![args.source_range.into()],
289    })
290}
291
292/// Get the shared edge between two faces.
293///
294/// ```no_run
295/// // Get an edge shared between two faces, created after a chamfer.
296///
297/// scale = 20
298/// part001 = startSketchOn(XY)
299///     |> startProfile(at = [0, 0])
300///     |> line(end = [0, scale])
301///     |> line(end = [scale, 0])
302///     |> line(end = [0, -scale])
303///     |> close(tag = $line0)
304///     |> extrude(length = 20, tagEnd = $end0)
305///     // We tag the chamfer to reference it later.
306///     |> chamfer(length = 10, tags = [getOppositeEdge(line0)], tag = $chamfer0)
307///
308/// // Get the shared edge between the chamfer and the extrusion.
309/// commonEdge = getCommonEdge(faces = [chamfer0, end0])
310///
311/// // Chamfer the shared edge.
312/// // TODO: uncomment this when ssi for fillets lands
313/// // chamfer(part001, length = 5, tags = [commonEdge])
314/// ```
315#[stdlib {
316    name = "getCommonEdge",
317    feature_tree_operation = false,
318    unlabeled_first = false,
319    args = {
320        faces = { docs = "The tags of the faces you want to find the common edge between" },
321    },
322    tags = ["sketch"]
323}]
324async fn inner_get_common_edge(
325    faces: Vec<TagIdentifier>,
326    exec_state: &mut ExecState,
327    args: Args,
328) -> Result<Uuid, KclError> {
329    let id = exec_state.next_uuid();
330    if args.ctx.no_engine_commands().await {
331        return Ok(id);
332    }
333
334    if faces.len() != 2 {
335        return Err(KclError::Type(KclErrorDetails::new(
336            "getCommonEdge requires exactly two tags for faces".to_string(),
337            vec![args.source_range],
338        )));
339    }
340    let first_face_id = args.get_adjacent_face_to_tag(exec_state, &faces[0], false).await?;
341    let second_face_id = args.get_adjacent_face_to_tag(exec_state, &faces[1], false).await?;
342
343    let first_tagged_path = args.get_tag_engine_info(exec_state, &faces[0])?.clone();
344    let second_tagged_path = args.get_tag_engine_info(exec_state, &faces[1])?;
345
346    if first_tagged_path.sketch != second_tagged_path.sketch {
347        return Err(KclError::Type(KclErrorDetails::new(
348            "getCommonEdge requires the faces to be in the same original sketch".to_string(),
349            vec![args.source_range],
350        )));
351    }
352
353    // Flush the batch for our fillets/chamfers if there are any.
354    // If we have a chamfer/fillet, flush the batch.
355    // TODO: we likely want to be a lot more persnickety _which_ fillets we are flushing
356    // but for now, we'll just flush everything.
357    if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = first_tagged_path.surface {
358        args.ctx.engine.flush_batch(true, args.source_range).await?;
359    } else if let Some(ExtrudeSurface::Chamfer { .. } | ExtrudeSurface::Fillet { .. }) = second_tagged_path.surface {
360        args.ctx.engine.flush_batch(true, args.source_range).await?;
361    }
362
363    let resp = args
364        .send_modeling_cmd(
365            id,
366            ModelingCmd::from(mcmd::Solid3dGetCommonEdge {
367                object_id: first_tagged_path.sketch,
368                face_ids: [first_face_id, second_face_id],
369            }),
370        )
371        .await?;
372    let OkWebSocketResponseData::Modeling {
373        modeling_response: OkModelingCmdResponse::Solid3dGetCommonEdge(common_edge),
374    } = &resp
375    else {
376        return Err(KclError::Engine(KclErrorDetails::new(
377            format!("mcmd::Solid3dGetCommonEdge response was not as expected: {:?}", resp),
378            vec![args.source_range],
379        )));
380    };
381
382    common_edge.edge.ok_or_else(|| {
383        KclError::Type(KclErrorDetails::new(
384            format!(
385                "No common edge was found between `{}` and `{}`",
386                faces[0].value, faces[1].value
387            ),
388            vec![args.source_range],
389        ))
390    })
391}