kcl_lib/std/
fillet.rs

1//! Standard library fillets.
2
3use anyhow::Result;
4use kcl_derive_docs::stdlib;
5use kcmc::{
6    each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::CutType,
7    websocket::OkWebSocketResponseData, ModelingCmd,
8};
9use kittycad_modeling_cmds as kcmc;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14use super::utils::unique_count;
15use crate::{
16    errors::{KclError, KclErrorDetails},
17    execution::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
18    parsing::ast::types::TagNode,
19    settings::types::UnitLength,
20    std::Args,
21};
22
23/// A tag or a uuid of an edge.
24#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Hash)]
25#[ts(export)]
26#[serde(untagged)]
27pub enum EdgeReference {
28    /// A uuid of an edge.
29    Uuid(uuid::Uuid),
30    /// A tag of an edge.
31    Tag(Box<TagIdentifier>),
32}
33
34impl EdgeReference {
35    pub fn get_engine_id(&self, exec_state: &mut ExecState, args: &Args) -> Result<uuid::Uuid, KclError> {
36        match self {
37            EdgeReference::Uuid(uuid) => Ok(*uuid),
38            EdgeReference::Tag(tag) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
39        }
40    }
41}
42
43/// Create fillets on tagged paths.
44pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
45    let solid = args.get_unlabeled_kw_arg("solid")?;
46    let radius = args.get_kw_arg("radius")?;
47    let tolerance = args.get_kw_arg_opt("tolerance")?;
48    let tags = args.get_kw_arg("tags")?;
49    let tag = args.get_kw_arg_opt("tag")?;
50    let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
51    Ok(KclValue::Solid { value })
52}
53
54/// Blend a transitional edge along a tagged path, smoothing the sharp edge.
55///
56/// Fillet is similar in function and use to a chamfer, except
57/// a chamfer will cut a sharp transition along an edge while fillet
58/// will smoothly blend the transition.
59///
60/// ```no_run
61/// width = 20
62/// length = 10
63/// thickness = 1
64/// filletRadius = 2
65///
66/// mountingPlateSketch = startSketchOn("XY")
67///   |> startProfileAt([-width/2, -length/2], %)
68///   |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
69///   |> line(endAbsolute = [width/2, length/2], tag = $edge2)
70///   |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
71///   |> close(tag = $edge4)
72///
73/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
74///   |> fillet(
75///     radius = filletRadius,
76///     tags = [
77///       getNextAdjacentEdge(edge1),
78///       getNextAdjacentEdge(edge2),
79///       getNextAdjacentEdge(edge3),
80///       getNextAdjacentEdge(edge4)
81///     ],
82///   )
83/// ```
84///
85/// ```no_run
86/// width = 20
87/// length = 10
88/// thickness = 1
89/// filletRadius = 1
90///
91/// mountingPlateSketch = startSketchOn("XY")
92///   |> startProfileAt([-width/2, -length/2], %)
93///   |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
94///   |> line(endAbsolute = [width/2, length/2], tag = $edge2)
95///   |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
96///   |> close(tag = $edge4)
97///
98/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
99///   |> fillet(
100///     radius = filletRadius,
101///     tolerance = 0.000001,
102///     tags = [
103///       getNextAdjacentEdge(edge1),
104///       getNextAdjacentEdge(edge2),
105///       getNextAdjacentEdge(edge3),
106///       getNextAdjacentEdge(edge4)
107///     ],
108///   )
109/// ```
110#[stdlib {
111    name = "fillet",
112    feature_tree_operation = true,
113    keywords = true,
114    unlabeled_first = true,
115    args = {
116        solid = { docs = "The solid whose edges should be filletted" },
117        radius = { docs = "The radius of the fillet" },
118        tags = { docs = "The paths you want to fillet" },
119        tolerance = { docs = "The tolerance for this fillet" },
120        tag = { docs = "Create a new tag which refers to this fillet"},
121    }
122}]
123async fn inner_fillet(
124    solid: Box<Solid>,
125    radius: f64,
126    tags: Vec<EdgeReference>,
127    tolerance: Option<f64>,
128    tag: Option<TagNode>,
129    exec_state: &mut ExecState,
130    args: Args,
131) -> Result<Box<Solid>, KclError> {
132    // Check if tags contains any duplicate values.
133    let unique_tags = unique_count(tags.clone());
134    if unique_tags != tags.len() {
135        return Err(KclError::Type(KclErrorDetails {
136            message: "Duplicate tags are not allowed.".to_string(),
137            source_ranges: vec![args.source_range],
138        }));
139    }
140
141    let mut solid = solid.clone();
142    for edge_tag in tags {
143        let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
144
145        let id = exec_state.next_uuid();
146        args.batch_end_cmd(
147            id,
148            ModelingCmd::from(mcmd::Solid3dFilletEdge {
149                edge_id,
150                object_id: solid.id,
151                radius: LengthUnit(radius),
152                tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
153                cut_type: CutType::Fillet,
154                // We make this a none so that we can remove it in the future.
155                face_id: None,
156            }),
157        )
158        .await?;
159
160        solid.edge_cuts.push(EdgeCut::Fillet {
161            id,
162            edge_id,
163            radius,
164            tag: Box::new(tag.clone()),
165        });
166
167        if let Some(ref tag) = tag {
168            solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
169                face_id: id,
170                tag: Some(tag.clone()),
171                geo_meta: GeoMeta {
172                    id,
173                    metadata: args.source_range.into(),
174                },
175            }));
176        }
177    }
178
179    Ok(solid)
180}
181
182/// Get the opposite edge to the edge given.
183pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
184    let tag: TagIdentifier = args.get_data()?;
185
186    let edge = inner_get_opposite_edge(tag, exec_state, args.clone()).await?;
187    Ok(KclValue::Uuid {
188        value: edge,
189        meta: vec![args.source_range.into()],
190    })
191}
192
193/// Get the opposite edge to the edge given.
194///
195/// ```no_run
196/// exampleSketch = startSketchOn('XZ')
197///   |> startProfileAt([0, 0], %)
198///   |> line(end = [10, 0])
199///   |> angledLine({
200///     angle = 60,
201///     length = 10,
202///   }, %)
203///   |> angledLine({
204///     angle = 120,
205///     length = 10,
206///   }, %)
207///   |> line(end = [-10, 0])
208///   |> angledLine({
209///     angle = 240,
210///     length = 10,
211///   }, %, $referenceEdge)
212///   |> close()
213///
214/// example = extrude(exampleSketch, length = 5)
215///   |> fillet(
216///     radius = 3,
217///     tags = [getOppositeEdge(referenceEdge)],
218///   )
219/// ```
220#[stdlib {
221    name = "getOppositeEdge",
222}]
223async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<Uuid, KclError> {
224    if args.ctx.no_engine_commands().await {
225        return Ok(exec_state.next_uuid());
226    }
227    let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
228
229    let id = exec_state.next_uuid();
230    let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
231
232    let resp = args
233        .send_modeling_cmd(
234            id,
235            ModelingCmd::from(mcmd::Solid3dGetOppositeEdge {
236                edge_id: tagged_path.id,
237                object_id: tagged_path.sketch,
238                face_id,
239            }),
240        )
241        .await?;
242    let OkWebSocketResponseData::Modeling {
243        modeling_response: OkModelingCmdResponse::Solid3dGetOppositeEdge(opposite_edge),
244    } = &resp
245    else {
246        return Err(KclError::Engine(KclErrorDetails {
247            message: format!("mcmd::Solid3dGetOppositeEdge response was not as expected: {:?}", resp),
248            source_ranges: vec![args.source_range],
249        }));
250    };
251
252    Ok(opposite_edge.edge)
253}
254
255/// Get the next adjacent edge to the edge given.
256pub async fn get_next_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
257    let tag: TagIdentifier = args.get_data()?;
258
259    let edge = inner_get_next_adjacent_edge(tag, exec_state, args.clone()).await?;
260    Ok(KclValue::Uuid {
261        value: edge,
262        meta: vec![args.source_range.into()],
263    })
264}
265
266/// Get the next adjacent edge to the edge given.
267///
268/// ```no_run
269/// exampleSketch = startSketchOn('XZ')
270///   |> startProfileAt([0, 0], %)
271///   |> line(end = [10, 0])
272///   |> angledLine({
273///     angle = 60,
274///     length = 10,
275///   }, %)
276///   |> angledLine({
277///     angle = 120,
278///     length = 10,
279///   }, %)
280///   |> line(end = [-10, 0])
281///   |> angledLine({
282///     angle = 240,
283///     length = 10,
284///   }, %, $referenceEdge)
285///   |> close()
286///
287/// example = extrude(exampleSketch, length = 5)
288///   |> fillet(
289///     radius = 3,
290///     tags = [getNextAdjacentEdge(referenceEdge)],
291///   )
292/// ```
293#[stdlib {
294    name = "getNextAdjacentEdge",
295}]
296async fn inner_get_next_adjacent_edge(
297    tag: TagIdentifier,
298    exec_state: &mut ExecState,
299    args: Args,
300) -> Result<Uuid, KclError> {
301    if args.ctx.no_engine_commands().await {
302        return Ok(exec_state.next_uuid());
303    }
304    let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
305
306    let id = exec_state.next_uuid();
307    let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
308
309    let resp = args
310        .send_modeling_cmd(
311            id,
312            ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
313                edge_id: tagged_path.id,
314                object_id: tagged_path.sketch,
315                face_id,
316            }),
317        )
318        .await?;
319    let OkWebSocketResponseData::Modeling {
320        modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
321    } = &resp
322    else {
323        return Err(KclError::Engine(KclErrorDetails {
324            message: format!(
325                "mcmd::Solid3dGetNextAdjacentEdge response was not as expected: {:?}",
326                resp
327            ),
328            source_ranges: vec![args.source_range],
329        }));
330    };
331
332    adjacent_edge.edge.ok_or_else(|| {
333        KclError::Type(KclErrorDetails {
334            message: format!("No edge found next adjacent to tag: `{}`", tag.value),
335            source_ranges: vec![args.source_range],
336        })
337    })
338}
339
340/// Get the previous adjacent edge to the edge given.
341pub async fn get_previous_adjacent_edge(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
342    let tag: TagIdentifier = args.get_data()?;
343
344    let edge = inner_get_previous_adjacent_edge(tag, exec_state, args.clone()).await?;
345    Ok(KclValue::Uuid {
346        value: edge,
347        meta: vec![args.source_range.into()],
348    })
349}
350
351/// Get the previous adjacent edge to the edge given.
352///
353/// ```no_run
354/// exampleSketch = startSketchOn('XZ')
355///   |> startProfileAt([0, 0], %)
356///   |> line(end = [10, 0])
357///   |> angledLine({
358///     angle = 60,
359///     length = 10,
360///   }, %)
361///   |> angledLine({
362///     angle = 120,
363///     length = 10,
364///   }, %)
365///   |> line(end = [-10, 0])
366///   |> angledLine({
367///     angle = 240,
368///     length = 10,
369///   }, %, $referenceEdge)
370///   |> close()
371///
372/// example = extrude(exampleSketch, length = 5)
373///   |> fillet(
374///     radius = 3,
375///     tags = [getPreviousAdjacentEdge(referenceEdge)],
376///   )
377/// ```
378#[stdlib {
379    name = "getPreviousAdjacentEdge",
380}]
381async fn inner_get_previous_adjacent_edge(
382    tag: TagIdentifier,
383    exec_state: &mut ExecState,
384    args: Args,
385) -> Result<Uuid, KclError> {
386    if args.ctx.no_engine_commands().await {
387        return Ok(exec_state.next_uuid());
388    }
389    let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
390
391    let id = exec_state.next_uuid();
392    let tagged_path = args.get_tag_engine_info(exec_state, &tag)?;
393
394    let resp = args
395        .send_modeling_cmd(
396            id,
397            ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge {
398                edge_id: tagged_path.id,
399                object_id: tagged_path.sketch,
400                face_id,
401            }),
402        )
403        .await?;
404    let OkWebSocketResponseData::Modeling {
405        modeling_response: OkModelingCmdResponse::Solid3dGetPrevAdjacentEdge(adjacent_edge),
406    } = &resp
407    else {
408        return Err(KclError::Engine(KclErrorDetails {
409            message: format!(
410                "mcmd::Solid3dGetPrevAdjacentEdge response was not as expected: {:?}",
411                resp
412            ),
413            source_ranges: vec![args.source_range],
414        }));
415    };
416
417    adjacent_edge.edge.ok_or_else(|| {
418        KclError::Type(KclErrorDetails {
419            message: format!("No edge found previous adjacent to tag: `{}`", tag.value),
420            source_ranges: vec![args.source_range],
421        })
422    })
423}
424
425pub(crate) fn default_tolerance(units: &UnitLength) -> f64 {
426    match units {
427        UnitLength::Mm => 0.0000001,
428        UnitLength::Cm => 0.0000001,
429        UnitLength::In => 0.0000001,
430        UnitLength::Ft => 0.0001,
431        UnitLength::Yd => 0.001,
432        UnitLength::M => 0.001,
433    }
434}