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