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 Some(solid.as_ref()),
65 exec_state,
66 &args,
67 "You must provide either 'tags' or 'edges' to chamfer edges",
68 "You must provide either 'tags' or 'edges' to chamfer edges, not both",
69 )
70 .await?;
71
72 match edge_inputs {
73 super::fillet::TaggedEdgeInputs::EngineRefs(edge_refs) => {
74 let value = inner_chamfer_with_engine_refs(
75 solid,
76 length,
77 edge_refs,
78 second_length,
79 angle,
80 csg_algorithm,
81 edge_cut_version,
82 tag,
83 exec_state,
84 args,
85 )
86 .await?;
87 Ok(KclValue::Solid { value })
88 }
89 super::fillet::TaggedEdgeInputs::Tags(tags) => {
90 let value = inner_chamfer(
91 solid,
92 length,
93 tags,
94 second_length,
95 angle,
96 None,
97 tag,
98 csg_algorithm,
99 edge_cut_version,
100 exec_state,
101 args,
102 )
103 .await?;
104 Ok(KclValue::Solid { value })
105 }
106 }
107}
108
109#[allow(clippy::too_many_arguments)]
110async fn inner_chamfer(
111 solid: Box<Solid>,
112 length: TyF64,
113 tags: Vec<EdgeReference>,
114 second_length: Option<TyF64>,
115 angle: Option<TyF64>,
116 custom_profile: Option<Sketch>,
117 tag: Option<TagNode>,
118 csg_algorithm: CsgAlgorithm,
119 edge_cut_version: EdgeCutVersion,
120 exec_state: &mut ExecState,
121 args: Args,
122) -> Result<Box<Solid>, KclError> {
123 if tag.is_some() && tags.len() > 1 {
126 return Err(KclError::new_type(KclErrorDetails::new(
127 "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(),
128 vec![args.source_range],
129 )));
130 }
131
132 if angle.is_some() && second_length.is_some() {
133 return Err(KclError::new_semantic(KclErrorDetails::new(
134 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
135 vec![args.source_range],
136 )));
137 }
138
139 let strategy = if second_length.is_some() || angle.is_some() || custom_profile.is_some() {
140 CutStrategy::Csg
141 } else {
142 Default::default()
143 };
144
145 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
146 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
147 if let Some(angle) = angle
148 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
149 {
150 return Err(KclError::new_semantic(KclErrorDetails::new(
151 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
152 vec![args.source_range],
153 )));
154 }
155
156 let cut_type = if let Some(custom_profile) = custom_profile {
157 exec_state
159 .batch_modeling_cmd(
160 ModelingCmdMeta::from_args(exec_state, &args),
161 ModelingCmd::from(
162 mcmd::ObjectVisible::builder()
163 .object_id(custom_profile.id)
164 .hidden(true)
165 .build(),
166 ),
167 )
168 .await?;
169 CutTypeV2::Custom {
170 path: custom_profile.id,
171 }
172 } else {
173 CutTypeV2::Chamfer {
174 distance: LengthUnit(length.to_mm()),
175 second_distance,
176 angle,
177 swap: false,
178 }
179 };
180
181 let mut solid = solid.clone();
182 for edge_tag in tags {
183 let edge_ids = edge_tag.get_all_engine_ids(exec_state, &args)?;
184 for edge_id in edge_ids {
185 let id = exec_state.next_uuid();
186 exec_state
187 .batch_end_cmd(
188 ModelingCmdMeta::from_args_id(exec_state, &args, id),
189 ModelingCmd::from(
190 mcmd::Solid3dCutEdges::builder()
191 .use_legacy(csg_algorithm.is_legacy())
192 .edge_ids(vec![edge_id])
193 .extra_face_ids(vec![])
194 .strategy(strategy)
195 .object_id(solid.id)
196 .tolerance(LengthUnit(DEFAULT_TOLERANCE))
198 .cut_type(cut_type)
199 .version(edge_cut_version)
200 .build(),
201 ),
202 )
203 .await?;
204
205 solid.edge_cuts.push(EdgeCut::Chamfer {
206 id,
207 edge_id,
208 length: length.clone(),
209 tag: Box::new(tag.clone()),
210 });
211
212 if let Some(ref tag) = tag {
213 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
214 face_id: id,
215 tag: Some(tag.clone()),
216 geo_meta: GeoMeta {
217 id,
218 metadata: args.source_range.into(),
219 },
220 }));
221 }
222 }
223 }
224
225 Ok(solid)
226}
227
228#[expect(clippy::too_many_arguments)]
229async fn inner_chamfer_with_engine_refs(
230 solid: Box<Solid>,
231 length: TyF64,
232 edge_references: Vec<kcmc::shared::EdgeSpecifier>,
233 second_length: Option<TyF64>,
234 angle: Option<TyF64>,
235 csg_algorithm: CsgAlgorithm,
236 edge_cut_version: EdgeCutVersion,
237 tag: Option<TagNode>,
238 exec_state: &mut ExecState,
239 args: Args,
240) -> Result<Box<Solid>, KclError> {
241 if tag.is_some() && edge_references.len() > 1 {
242 return Err(KclError::new_type(KclErrorDetails::new(
243 "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(),
244 vec![args.source_range],
245 )));
246 }
247
248 if angle.is_some() && second_length.is_some() {
249 return Err(KclError::new_semantic(KclErrorDetails::new(
250 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
251 vec![args.source_range],
252 )));
253 }
254
255 let strategy = if second_length.is_some() || angle.is_some() {
256 CutStrategy::Csg
257 } else {
258 Default::default()
259 };
260
261 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
262 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
263 if let Some(angle) = angle
264 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
265 {
266 return Err(KclError::new_semantic(KclErrorDetails::new(
267 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
268 vec![args.source_range],
269 )));
270 }
271
272 let cut_type = CutTypeV2::Chamfer {
273 distance: LengthUnit(length.to_mm()),
274 second_distance,
275 angle,
276 swap: false,
277 };
278
279 let id = exec_state.next_uuid();
280 let num_extra_ids = edge_references.len().saturating_sub(1);
281 let mut extra_face_ids = Vec::with_capacity(num_extra_ids);
282 for _ in 0..num_extra_ids {
283 extra_face_ids.push(exec_state.next_uuid());
284 }
285
286 let mut solid = solid.clone();
287 exec_state
288 .batch_end_cmd(
289 ModelingCmdMeta::from_args_id(exec_state, &args, id),
290 ModelingCmd::from(
291 mcmd::Solid3dCutEdgeReferences::builder()
292 .object_id(solid.id)
293 .edges_references(edge_references)
294 .cut_type(cut_type)
295 .tolerance(LengthUnit(DEFAULT_TOLERANCE))
296 .strategy(strategy)
297 .extra_face_ids(extra_face_ids)
298 .use_legacy(csg_algorithm.is_legacy())
299 .version(edge_cut_version)
300 .build(),
301 ),
302 )
303 .await?;
304
305 solid.pending_edge_cut_ids.push(id);
306
307 if let Some(ref tag) = tag {
308 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
309 face_id: id,
310 tag: Some(tag.clone()),
311 geo_meta: GeoMeta {
312 id,
313 metadata: args.source_range.into(),
314 },
315 }));
316 }
317
318 Ok(solid)
319}