kcl_lib/std/
chamfer.rs

1//! Standard library chamfers.
2
3use anyhow::Result;
4use derive_docs::stdlib;
5use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
6use kittycad_modeling_cmds as kcmc;
7
8use crate::{
9    errors::{KclError, KclErrorDetails},
10    execution::{ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid},
11    parsing::ast::types::TagNode,
12    std::{fillet::EdgeReference, Args},
13};
14
15pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
16
17/// Create chamfers on tagged paths.
18pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
19    let solid = args.get_unlabeled_kw_arg("solid")?;
20    let length = args.get_kw_arg("length")?;
21    let tags = args.get_kw_arg("tags")?;
22    let tag = args.get_kw_arg_opt("tag")?;
23
24    let value = inner_chamfer(solid, length, tags, tag, exec_state, args).await?;
25    Ok(KclValue::Solid { value })
26}
27
28/// Cut a straight transitional edge along a tagged path.
29///
30/// Chamfer is similar in function and use to a fillet, except
31/// a fillet will blend the transition along an edge, rather than cut
32/// a sharp, straight transitional edge.
33///
34/// ```no_run
35/// // Chamfer a mounting plate.
36/// width = 20
37/// length = 10
38/// thickness = 1
39/// chamferLength = 2
40///
41/// mountingPlateSketch = startSketchOn("XY")
42///   |> startProfileAt([-width/2, -length/2], %)
43///   |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
44///   |> line(endAbsolute = [width/2, length/2], tag = $edge2)
45///   |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
46///   |> close(tag = $edge4)
47///
48/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
49///   |> chamfer(
50///     length = chamferLength,
51///     tags = [
52///       getNextAdjacentEdge(edge1),
53///       getNextAdjacentEdge(edge2),
54///       getNextAdjacentEdge(edge3),
55///       getNextAdjacentEdge(edge4)
56///     ],
57///   )
58/// ```
59///
60/// ```no_run
61/// // Sketch on the face of a chamfer.
62/// fn cube(pos, scale) {
63/// sg = startSketchOn('XY')
64///     |> startProfileAt(pos, %)
65///     |> line(end = [0, scale])
66///     |> line(end = [scale, 0])
67///     |> line(end = [0, -scale])
68///
69///     return sg
70/// }
71///
72/// part001 = cube([0,0], 20)
73///     |> close(tag = $line1)
74///     |> extrude(length = 20)
75///     // We tag the chamfer to reference it later.
76///     |> chamfer(
77///         length = 10,
78///         tags = [getOppositeEdge(line1)],
79///         tag = $chamfer1,
80///     )  
81///
82/// sketch001 = startSketchOn(part001, chamfer1)
83///     |> startProfileAt([10, 10], %)
84///     |> line(end = [2, 0])
85///     |> line(end = [0, 2])
86///     |> line(end = [-2, 0])
87///     |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
88///     |> close()
89///     |> extrude(length = 10)
90/// ```
91#[stdlib {
92    name = "chamfer",
93    feature_tree_operation = true,
94    keywords = true,
95    unlabeled_first = true,
96    args = {
97        solid = { docs = "The solid whose edges should be chamfered" },
98        length = { docs = "The length of the chamfer" },
99        tags = { docs = "The paths you want to chamfer" },
100        tag = { docs = "Create a new tag which refers to this chamfer"},
101    }
102}]
103async fn inner_chamfer(
104    solid: Box<Solid>,
105    length: f64,
106    tags: Vec<EdgeReference>,
107    tag: Option<TagNode>,
108    exec_state: &mut ExecState,
109    args: Args,
110) -> Result<Box<Solid>, KclError> {
111    // Check if tags contains any duplicate values.
112    let mut tags = tags.clone();
113    tags.sort();
114    tags.dedup();
115    if tags.len() != tags.len() {
116        return Err(KclError::Type(KclErrorDetails {
117            message: "Duplicate tags are not allowed.".to_string(),
118            source_ranges: vec![args.source_range],
119        }));
120    }
121
122    // If you try and tag multiple edges with a tagged chamfer, we want to return an
123    // error to the user that they can only tag one edge at a time.
124    if tag.is_some() && tags.len() > 1 {
125        return Err(KclError::Type(KclErrorDetails {
126            message: "You can only tag one edge at a time with a tagged chamfer. Either delete the tag for the chamfer fn if you don't need it OR separate into individual chamfer functions for each tag.".to_string(),
127            source_ranges: vec![args.source_range],
128        }));
129    }
130
131    let mut solid = solid.clone();
132    for edge_tag in tags {
133        let edge_id = match edge_tag {
134            EdgeReference::Uuid(uuid) => uuid,
135            EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id,
136        };
137
138        let id = exec_state.next_uuid();
139        args.batch_end_cmd(
140            id,
141            ModelingCmd::from(mcmd::Solid3dFilletEdge {
142                edge_id,
143                object_id: solid.id,
144                radius: LengthUnit(length),
145                tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
146                cut_type: CutType::Chamfer,
147                // We make this a none so that we can remove it in the future.
148                face_id: None,
149            }),
150        )
151        .await?;
152
153        solid.edge_cuts.push(EdgeCut::Chamfer {
154            id,
155            edge_id,
156            length,
157            tag: Box::new(tag.clone()),
158        });
159
160        if let Some(ref tag) = tag {
161            solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
162                face_id: id,
163                tag: Some(tag.clone()),
164                geo_meta: GeoMeta {
165                    id,
166                    metadata: args.source_range.into(),
167                },
168            }));
169        }
170    }
171
172    Ok(solid)
173}