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