kcl_lib/std/
fillet.rs

1//! Standard library fillets.
2
3use anyhow::Result;
4use indexmap::IndexMap;
5use kcl_derive_docs::stdlib;
6use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
7use kittycad_modeling_cmds as kcmc;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    errors::{KclError, KclErrorDetails},
13    execution::{
14        kcl_value::RuntimeType, EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, PrimitiveType,
15        Solid, TagIdentifier,
16    },
17    parsing::ast::types::TagNode,
18    settings::types::UnitLength,
19    std::Args,
20    SourceRange,
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
43pub(super) fn validate_unique<T: Eq + std::hash::Hash>(tags: &[(T, SourceRange)]) -> Result<(), KclError> {
44    // Check if tags contains any duplicate values.
45    let mut tag_counts: IndexMap<&T, Vec<SourceRange>> = Default::default();
46    for tag in tags {
47        tag_counts.entry(&tag.0).or_insert(Vec::new()).push(tag.1);
48    }
49    let mut duplicate_tags_source = Vec::new();
50    for (_tag, count) in tag_counts {
51        if count.len() > 1 {
52            duplicate_tags_source.extend(count)
53        }
54    }
55    if !duplicate_tags_source.is_empty() {
56        return Err(KclError::Type(KclErrorDetails {
57            message: "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge".to_string(),
58            source_ranges: duplicate_tags_source,
59        }));
60    }
61    Ok(())
62}
63
64/// Create fillets on tagged paths.
65pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
66    let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::Primitive(PrimitiveType::Solid), exec_state)?;
67    let radius = args.get_kw_arg("radius")?;
68    let tolerance = args.get_kw_arg_opt("tolerance")?;
69    let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
70    let tag = args.get_kw_arg_opt("tag")?;
71
72    // Run the function.
73    validate_unique(&tags)?;
74    let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
75    let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
76    Ok(KclValue::Solid { value })
77}
78
79/// Blend a transitional edge along a tagged path, smoothing the sharp edge.
80///
81/// Fillet is similar in function and use to a chamfer, except
82/// a chamfer will cut a sharp transition along an edge while fillet
83/// will smoothly blend the transition.
84///
85/// ```no_run
86/// width = 20
87/// length = 10
88/// thickness = 1
89/// filletRadius = 2
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///     tags = [
102///       getNextAdjacentEdge(edge1),
103///       getNextAdjacentEdge(edge2),
104///       getNextAdjacentEdge(edge3),
105///       getNextAdjacentEdge(edge4)
106///     ],
107///   )
108/// ```
109///
110/// ```no_run
111/// width = 20
112/// length = 10
113/// thickness = 1
114/// filletRadius = 1
115///
116/// mountingPlateSketch = startSketchOn("XY")
117///   |> startProfileAt([-width/2, -length/2], %)
118///   |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
119///   |> line(endAbsolute = [width/2, length/2], tag = $edge2)
120///   |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
121///   |> close(tag = $edge4)
122///
123/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
124///   |> fillet(
125///     radius = filletRadius,
126///     tolerance = 0.000001,
127///     tags = [
128///       getNextAdjacentEdge(edge1),
129///       getNextAdjacentEdge(edge2),
130///       getNextAdjacentEdge(edge3),
131///       getNextAdjacentEdge(edge4)
132///     ],
133///   )
134/// ```
135#[stdlib {
136    name = "fillet",
137    feature_tree_operation = true,
138    keywords = true,
139    unlabeled_first = true,
140    args = {
141        solid = { docs = "The solid whose edges should be filletted" },
142        radius = { docs = "The radius of the fillet" },
143        tags = { docs = "The paths you want to fillet" },
144        tolerance = { docs = "The tolerance for this fillet" },
145        tag = { docs = "Create a new tag which refers to this fillet"},
146    }
147}]
148async fn inner_fillet(
149    solid: Box<Solid>,
150    radius: f64,
151    tags: Vec<EdgeReference>,
152    tolerance: Option<f64>,
153    tag: Option<TagNode>,
154    exec_state: &mut ExecState,
155    args: Args,
156) -> Result<Box<Solid>, KclError> {
157    let mut solid = solid.clone();
158    for edge_tag in tags {
159        let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
160
161        let id = exec_state.next_uuid();
162        args.batch_end_cmd(
163            id,
164            ModelingCmd::from(mcmd::Solid3dFilletEdge {
165                edge_id,
166                object_id: solid.id,
167                radius: LengthUnit(radius),
168                tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
169                cut_type: CutType::Fillet,
170                // We make this a none so that we can remove it in the future.
171                face_id: None,
172            }),
173        )
174        .await?;
175
176        solid.edge_cuts.push(EdgeCut::Fillet {
177            id,
178            edge_id,
179            radius,
180            tag: Box::new(tag.clone()),
181        });
182
183        if let Some(ref tag) = tag {
184            solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
185                face_id: id,
186                tag: Some(tag.clone()),
187                geo_meta: GeoMeta {
188                    id,
189                    metadata: args.source_range.into(),
190                },
191            }));
192        }
193    }
194
195    Ok(solid)
196}
197
198pub(crate) fn default_tolerance(units: &UnitLength) -> f64 {
199    match units {
200        UnitLength::Mm => 0.0000001,
201        UnitLength::Cm => 0.0000001,
202        UnitLength::In => 0.0000001,
203        UnitLength::Ft => 0.0001,
204        UnitLength::Yd => 0.001,
205        UnitLength::M => 0.001,
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_validate_unique() {
215        let dup_a = SourceRange::from([1, 3, 0]);
216        let dup_b = SourceRange::from([10, 30, 0]);
217        // Two entries are duplicates (abc) with different source ranges.
218        let tags = vec![("abc", dup_a), ("abc", dup_b), ("def", SourceRange::from([2, 4, 0]))];
219        let actual = validate_unique(&tags);
220        // Both the duplicates should show up as errors, with both of the
221        // source ranges they correspond to.
222        // But the unique source range 'def' should not.
223        let expected = vec![dup_a, dup_b];
224        assert_eq!(actual.err().unwrap().source_ranges(), expected);
225    }
226}