1use anyhow::Result;
4use kcmc::ModelingCmd;
5use kcmc::each_cmd as mcmd;
6use kcmc::length_unit::LengthUnit;
7use kcmc::shared::Angle;
8use kcmc::shared::CutStrategy;
9use kcmc::shared::CutTypeV2;
10use kcmc::shared::EdgeCutVersion;
11use kittycad_modeling_cmds::{self as kcmc};
12
13use super::args::TyF64;
14use crate::errors::KclError;
15use crate::errors::KclErrorDetails;
16use crate::execution::ChamferSurface;
17use crate::execution::EdgeCut;
18use crate::execution::ExecState;
19use crate::execution::ExtrudeSurface;
20use crate::execution::GeoMeta;
21use crate::execution::KclValue;
22use crate::execution::ModelingCmdMeta;
23use crate::execution::Sketch;
24use crate::execution::Solid;
25use crate::execution::types::RuntimeType;
26use crate::parsing::ast::types::TagNode;
27use crate::std::Args;
28use crate::std::csg::CsgAlgorithm;
29use crate::std::fillet::EdgeReference;
30
31pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
32
33pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
35 let solid: Box<Solid> = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
36 let length: TyF64 = args.get_kw_arg("length", &RuntimeType::length(), exec_state)?;
37 let second_length = args.get_kw_arg_opt("secondLength", &RuntimeType::length(), exec_state)?;
38 let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
39 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
40 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
41 let edge_cut_number: Option<u32> = args.get_kw_arg_opt("version", &RuntimeType::count(), exec_state)?;
42 let edge_cut_version: EdgeCutVersion = edge_cut_number
43 .map(|num| {
44 num.try_into().map_err(|()| {
45 KclError::new_semantic(KclErrorDetails::new(
46 format!("{} is not a version of the Zoo edge cut algorithm", num),
47 vec![args.source_range],
48 ))
49 })
50 })
51 .transpose()?
52 .unwrap_or_default();
53
54 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
55
56 let edge_refs = args.get_kw_arg_opt("edges", &RuntimeType::any_array(), exec_state)?;
59 let tags = args.kw_arg_edge_array_and_source_opt("tags")?;
60
61 let edge_inputs = super::fillet::parse_tagged_edge_inputs(
62 edge_refs,
63 tags,
64 exec_state,
65 &args,
66 "You must provide either 'tags' or 'edges' to chamfer edges",
67 "You must provide either 'tags' or 'edges' to chamfer edges, not both",
68 )
69 .await?;
70
71 match edge_inputs {
72 super::fillet::TaggedEdgeInputs::EngineRefs(edge_refs) => {
73 let value = inner_chamfer_with_engine_refs(
74 solid,
75 length,
76 edge_refs,
77 second_length,
78 angle,
79 csg_algorithm,
80 edge_cut_version,
81 tag,
82 exec_state,
83 args,
84 )
85 .await?;
86 Ok(KclValue::Solid { value })
87 }
88 super::fillet::TaggedEdgeInputs::Tags(tags) => {
89 let value = inner_chamfer(
90 solid,
91 length,
92 tags,
93 second_length,
94 angle,
95 None,
96 tag,
97 csg_algorithm,
98 edge_cut_version,
99 exec_state,
100 args,
101 )
102 .await?;
103 Ok(KclValue::Solid { value })
104 }
105 }
106}
107
108#[allow(clippy::too_many_arguments)]
109async fn inner_chamfer(
110 solid: Box<Solid>,
111 length: TyF64,
112 tags: Vec<EdgeReference>,
113 second_length: Option<TyF64>,
114 angle: Option<TyF64>,
115 custom_profile: Option<Sketch>,
116 tag: Option<TagNode>,
117 csg_algorithm: CsgAlgorithm,
118 edge_cut_version: EdgeCutVersion,
119 exec_state: &mut ExecState,
120 args: Args,
121) -> Result<Box<Solid>, KclError> {
122 if tag.is_some() && tags.len() > 1 {
125 return Err(KclError::new_type(KclErrorDetails::new(
126 "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 vec![args.source_range],
128 )));
129 }
130
131 if angle.is_some() && second_length.is_some() {
132 return Err(KclError::new_semantic(KclErrorDetails::new(
133 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
134 vec![args.source_range],
135 )));
136 }
137
138 let strategy = if second_length.is_some() || angle.is_some() || custom_profile.is_some() {
139 CutStrategy::Csg
140 } else {
141 Default::default()
142 };
143
144 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
145 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
146 if let Some(angle) = angle
147 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
148 {
149 return Err(KclError::new_semantic(KclErrorDetails::new(
150 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
151 vec![args.source_range],
152 )));
153 }
154
155 let cut_type = if let Some(custom_profile) = custom_profile {
156 exec_state
158 .batch_modeling_cmd(
159 ModelingCmdMeta::from_args(exec_state, &args),
160 ModelingCmd::from(
161 mcmd::ObjectVisible::builder()
162 .object_id(custom_profile.id)
163 .hidden(true)
164 .build(),
165 ),
166 )
167 .await?;
168 CutTypeV2::Custom {
169 path: custom_profile.id,
170 }
171 } else {
172 CutTypeV2::Chamfer {
173 distance: LengthUnit(length.to_mm()),
174 second_distance,
175 angle,
176 swap: false,
177 }
178 };
179
180 let mut solid = solid.clone();
181 for edge_tag in tags {
182 let edge_ids = edge_tag.get_all_engine_ids(exec_state, &args)?;
183 for edge_id in edge_ids {
184 let id = exec_state.next_uuid();
185 exec_state
186 .batch_end_cmd(
187 ModelingCmdMeta::from_args_id(exec_state, &args, id),
188 ModelingCmd::from(
189 mcmd::Solid3dCutEdges::builder()
190 .use_legacy(csg_algorithm.is_legacy())
191 .edge_ids(vec![edge_id])
192 .extra_face_ids(vec![])
193 .strategy(strategy)
194 .object_id(solid.id)
195 .tolerance(LengthUnit(DEFAULT_TOLERANCE))
197 .cut_type(cut_type)
198 .version(edge_cut_version)
199 .build(),
200 ),
201 )
202 .await?;
203
204 solid.edge_cuts.push(EdgeCut::Chamfer {
205 id,
206 edge_id,
207 length: length.clone(),
208 tag: Box::new(tag.clone()),
209 });
210
211 if let Some(ref tag) = tag {
212 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
213 face_id: id,
214 tag: Some(tag.clone()),
215 geo_meta: GeoMeta {
216 id,
217 metadata: args.source_range.into(),
218 },
219 }));
220 }
221 }
222 }
223
224 Ok(solid)
225}
226
227#[expect(clippy::too_many_arguments)]
228async fn inner_chamfer_with_engine_refs(
229 solid: Box<Solid>,
230 length: TyF64,
231 edge_references: Vec<kcmc::shared::EdgeSpecifier>,
232 second_length: Option<TyF64>,
233 angle: Option<TyF64>,
234 csg_algorithm: CsgAlgorithm,
235 edge_cut_version: EdgeCutVersion,
236 tag: Option<TagNode>,
237 exec_state: &mut ExecState,
238 args: Args,
239) -> Result<Box<Solid>, KclError> {
240 if tag.is_some() && edge_references.len() > 1 {
241 return Err(KclError::new_type(KclErrorDetails::new(
242 "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 edgeRef.".to_string(),
243 vec![args.source_range],
244 )));
245 }
246
247 if angle.is_some() && second_length.is_some() {
248 return Err(KclError::new_semantic(KclErrorDetails::new(
249 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
250 vec![args.source_range],
251 )));
252 }
253
254 let strategy = if second_length.is_some() || angle.is_some() {
255 CutStrategy::Csg
256 } else {
257 Default::default()
258 };
259
260 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
261 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
262 if let Some(angle) = angle
263 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
264 {
265 return Err(KclError::new_semantic(KclErrorDetails::new(
266 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
267 vec![args.source_range],
268 )));
269 }
270
271 let cut_type = CutTypeV2::Chamfer {
272 distance: LengthUnit(length.to_mm()),
273 second_distance,
274 angle,
275 swap: false,
276 };
277
278 let id = exec_state.next_uuid();
279 let num_extra_ids = edge_references.len().saturating_sub(1);
280 let mut extra_face_ids = Vec::with_capacity(num_extra_ids);
281 for _ in 0..num_extra_ids {
282 extra_face_ids.push(exec_state.next_uuid());
283 }
284
285 let mut solid = solid.clone();
286 exec_state
287 .batch_end_cmd(
288 ModelingCmdMeta::from_args_id(exec_state, &args, id),
289 ModelingCmd::from(
290 mcmd::Solid3dCutEdgeReferences::builder()
291 .object_id(solid.id)
292 .edges_references(edge_references)
293 .cut_type(cut_type)
294 .tolerance(LengthUnit(DEFAULT_TOLERANCE))
295 .strategy(strategy)
296 .extra_face_ids(extra_face_ids)
297 .use_legacy(csg_algorithm.is_legacy())
298 .version(edge_cut_version)
299 .build(),
300 ),
301 )
302 .await?;
303
304 solid.pending_edge_cut_ids.push(id);
305
306 if let Some(ref tag) = tag {
307 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
308 face_id: id,
309 tag: Some(tag.clone()),
310 geo_meta: GeoMeta {
311 id,
312 metadata: args.source_range.into(),
313 },
314 }));
315 }
316
317 Ok(solid)
318}