kcl_lib/std/
fillet.rs

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