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}