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;
30use crate::std::fillet::default_edge_cut_version;
31
32pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
33
34pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
36 let solid: Box<Solid> = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
37 let length: TyF64 = args.get_kw_arg("length", &RuntimeType::length(), exec_state)?;
38 let second_length = args.get_kw_arg_opt("secondLength", &RuntimeType::length(), exec_state)?;
39 let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
40 let legacy_csg: Option<bool> = args.get_kw_arg_opt("legacyMethod", &RuntimeType::bool(), exec_state)?;
41 let csg_algorithm = CsgAlgorithm::legacy(legacy_csg.unwrap_or_default());
42 let edge_cut_number: Option<u32> = args.get_kw_arg_opt("version", &RuntimeType::count(), exec_state)?;
43 let edge_cut_version: EdgeCutVersion = edge_cut_number
44 .map(|num| {
45 num.try_into().map_err(|()| {
46 KclError::new_semantic(KclErrorDetails::new(
47 format!("{} is not a version of the Zoo edge cut algorithm", num),
48 vec![args.source_range],
49 ))
50 })
51 })
52 .transpose()?
53 .unwrap_or_else(|| default_edge_cut_version(exec_state.kcl_version()));
54
55 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
56
57 let edge_refs = args.get_kw_arg_opt("edges", &RuntimeType::any_array(), exec_state)?;
60 let tags = args.kw_arg_edge_array_and_source_opt("tags")?;
61
62 let edge_inputs = super::fillet::parse_tagged_edge_inputs(
63 edge_refs,
64 tags,
65 Some(solid.as_ref()),
66 exec_state,
67 &args,
68 "You must provide either 'tags' or 'edges' to chamfer edges",
69 "You must provide either 'tags' or 'edges' to chamfer edges, not both",
70 )
71 .await?;
72
73 match edge_inputs {
74 super::fillet::TaggedEdgeInputs::EngineRefs(edge_refs) => {
75 let value = inner_chamfer_with_engine_refs(
76 solid,
77 length,
78 edge_refs,
79 second_length,
80 angle,
81 csg_algorithm,
82 edge_cut_version,
83 tag,
84 exec_state,
85 args,
86 )
87 .await?;
88 Ok(KclValue::Solid { value })
89 }
90 super::fillet::TaggedEdgeInputs::Tags(tags) => {
91 match edge_cut_version {
92 EdgeCutVersion::V2 => {
93 let value = inner_chamfer_v2(
94 solid,
95 length,
96 tags,
97 second_length,
98 angle,
99 None,
100 tag,
101 csg_algorithm,
102 edge_cut_version,
103 exec_state,
104 args,
105 )
106 .await?;
107 Ok(KclValue::Solid { value })
108 }
109 _ => {
111 let value = inner_chamfer(
112 solid,
113 length,
114 tags,
115 second_length,
116 angle,
117 None,
118 tag,
119 csg_algorithm,
120 edge_cut_version,
121 exec_state,
122 args,
123 )
124 .await?;
125 Ok(KclValue::Solid { value })
126 }
127 }
128 }
129 }
130}
131
132#[allow(clippy::too_many_arguments)]
133async fn inner_chamfer(
134 solid: Box<Solid>,
135 length: TyF64,
136 tags: Vec<(EdgeReference, crate::SourceRange)>,
137 second_length: Option<TyF64>,
138 angle: Option<TyF64>,
139 custom_profile: Option<Sketch>,
140 tag: Option<TagNode>,
141 csg_algorithm: CsgAlgorithm,
142 edge_cut_version: EdgeCutVersion,
143 exec_state: &mut ExecState,
144 args: Args,
145) -> Result<Box<Solid>, KclError> {
146 if tag.is_some() && tags.len() > 1 {
149 return Err(KclError::new_type(KclErrorDetails::new(
150 "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(),
151 vec![args.source_range],
152 )));
153 }
154
155 if angle.is_some() && second_length.is_some() {
156 return Err(KclError::new_semantic(KclErrorDetails::new(
157 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
158 vec![args.source_range],
159 )));
160 }
161
162 let strategy = if second_length.is_some() || angle.is_some() || custom_profile.is_some() {
163 CutStrategy::Csg
164 } else {
165 Default::default()
166 };
167
168 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
169 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
170 if let Some(angle) = angle
171 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
172 {
173 return Err(KclError::new_semantic(KclErrorDetails::new(
174 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
175 vec![args.source_range],
176 )));
177 }
178
179 let cut_type = if let Some(custom_profile) = custom_profile {
180 exec_state
182 .batch_modeling_cmd(
183 ModelingCmdMeta::from_args(exec_state, &args),
184 ModelingCmd::from(
185 mcmd::ObjectVisible::builder()
186 .object_id(custom_profile.id)
187 .hidden(true)
188 .build(),
189 ),
190 )
191 .await?;
192 CutTypeV2::Custom {
193 path: custom_profile.id,
194 }
195 } else {
196 CutTypeV2::Chamfer {
197 distance: LengthUnit(length.to_mm()),
198 second_distance,
199 angle,
200 swap: false,
201 }
202 };
203
204 let mut solid = solid.clone();
205 let mut tag_entries: Vec<crate::execution::DirectTagFilletTagEntry> = Vec::new();
206 for (edge_ref, source_range) in &tags {
207 let edge_id = match edge_ref {
208 EdgeReference::Uuid(u) => *u,
209 EdgeReference::Tag(t) => args.get_tag_engine_info(exec_state, t)?.id,
210 };
211 if let Ok(face_ids) = super::edge::get_face_ids_for_edge(exec_state, solid.id, edge_id, &args).await
212 && let [a, b] = face_ids.as_slice()
213 {
214 let tag_identifier = match edge_ref {
215 EdgeReference::Tag(t) => t.value.clone(),
216 EdgeReference::Uuid(_) => String::new(),
217 };
218 if !tag_identifier.is_empty() {
219 tag_entries.push(crate::execution::DirectTagFilletTagEntry {
220 tag_identifier,
221 edge_id,
222 face_ids: [*a, *b],
223 });
224 } else {
225 exec_state.record_edge_refactor_meta_from_pending(edge_id, *source_range, [*a, *b]);
226 }
227 }
228 }
229 if !tag_entries.is_empty() {
230 exec_state.record_direct_tag_fillet_meta(crate::execution::DirectTagFilletMeta {
231 call_source_range: args.source_range,
232 tags: tag_entries,
233 });
234 }
235 for (edge_tag, _) in tags {
236 let edge_ids = edge_tag.get_all_engine_ids(exec_state, &args)?;
237 for edge_id in edge_ids {
238 let id = exec_state.next_uuid();
239 exec_state
240 .batch_end_cmd(
241 ModelingCmdMeta::from_args_id(exec_state, &args, id),
242 ModelingCmd::from(
243 mcmd::Solid3dCutEdges::builder()
244 .use_legacy(csg_algorithm.is_legacy())
245 .edge_ids(vec![edge_id])
246 .extra_face_ids(vec![])
247 .strategy(strategy)
248 .object_id(solid.id)
249 .tolerance(LengthUnit(DEFAULT_TOLERANCE))
251 .cut_type(cut_type)
252 .version(edge_cut_version)
253 .build(),
254 ),
255 )
256 .await?;
257
258 solid.edge_cuts.push(EdgeCut::Chamfer {
259 id,
260 edge_id,
261 length: length.clone(),
262 tag: Box::new(tag.clone()),
263 });
264
265 if let Some(ref tag) = tag {
266 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
267 face_id: id,
268 tag: Some(tag.clone()),
269 geo_meta: GeoMeta {
270 id,
271 metadata: args.source_range.into(),
272 },
273 }));
274 }
275 }
276 }
277
278 Ok(solid)
279}
280
281#[allow(clippy::too_many_arguments)]
282async fn inner_chamfer_v2(
283 solid: Box<Solid>,
284 length: TyF64,
285 tags: Vec<(EdgeReference, crate::SourceRange)>,
286 second_length: Option<TyF64>,
287 angle: Option<TyF64>,
288 custom_profile: Option<Sketch>,
289 tag: Option<TagNode>,
290 csg_algorithm: CsgAlgorithm,
291 edge_cut_version: EdgeCutVersion,
292 exec_state: &mut ExecState,
293 args: Args,
294) -> Result<Box<Solid>, KclError> {
295 if tag.is_some() && tags.len() > 1 {
298 return Err(KclError::new_type(KclErrorDetails::new(
299 "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(),
300 vec![args.source_range],
301 )));
302 }
303 if tags.is_empty() {
304 return Err(KclError::new_semantic(KclErrorDetails {
305 source_ranges: vec![args.source_range],
306 message: "You must chamfer at least one tag".to_owned(),
307 backtrace: Default::default(),
308 }));
309 }
310
311 if angle.is_some() && second_length.is_some() {
312 return Err(KclError::new_semantic(KclErrorDetails::new(
313 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
314 vec![args.source_range],
315 )));
316 }
317
318 let strategy = if second_length.is_some() || angle.is_some() || custom_profile.is_some() {
319 CutStrategy::Csg
320 } else {
321 Default::default()
322 };
323
324 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
325 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
326 if let Some(angle) = angle
327 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
328 {
329 return Err(KclError::new_semantic(KclErrorDetails::new(
330 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
331 vec![args.source_range],
332 )));
333 }
334
335 let cut_type = if let Some(custom_profile) = custom_profile {
336 exec_state
338 .batch_modeling_cmd(
339 ModelingCmdMeta::from_args(exec_state, &args),
340 ModelingCmd::from(
341 mcmd::ObjectVisible::builder()
342 .object_id(custom_profile.id)
343 .hidden(true)
344 .build(),
345 ),
346 )
347 .await?;
348 CutTypeV2::Custom {
349 path: custom_profile.id,
350 }
351 } else {
352 CutTypeV2::Chamfer {
353 distance: LengthUnit(length.to_mm()),
354 second_distance,
355 angle,
356 swap: false,
357 }
358 };
359
360 let mut solid = solid.clone();
361 let mut edge_ids = Vec::new();
362 let mut tag_entries: Vec<crate::execution::DirectTagFilletTagEntry> = Vec::new();
363 for (edge_ref, source_range) in &tags {
364 let ids = edge_ref.get_all_engine_ids(exec_state, &args)?;
365 edge_ids.extend(ids.iter().copied());
366 let tag_identifier = match edge_ref {
367 EdgeReference::Tag(t) => t.value.clone(),
368 EdgeReference::Uuid(_) => String::new(),
369 };
370 for edge_id in ids {
371 if let Ok(face_ids) = super::edge::get_face_ids_for_edge(exec_state, solid.id, edge_id, &args).await
372 && let [a, b] = face_ids.as_slice()
373 {
374 if !tag_identifier.is_empty() {
375 tag_entries.push(crate::execution::DirectTagFilletTagEntry {
376 tag_identifier: tag_identifier.clone(),
377 edge_id,
378 face_ids: [*a, *b],
379 });
380 } else {
381 exec_state.record_edge_refactor_meta_from_pending(edge_id, *source_range, [*a, *b]);
382 }
383 }
384 }
385 }
386 if !tag_entries.is_empty() {
387 exec_state.record_direct_tag_fillet_meta(crate::execution::DirectTagFilletMeta {
388 call_source_range: args.source_range,
389 tags: tag_entries,
390 });
391 }
392
393 let id = exec_state.next_uuid();
394 let num_extra_ids = edge_ids.len().saturating_sub(1);
395 let mut extra_face_ids = Vec::with_capacity(num_extra_ids);
396 for _ in 0..num_extra_ids {
397 extra_face_ids.push(exec_state.next_uuid());
398 }
399 exec_state
400 .batch_end_cmd(
401 ModelingCmdMeta::from_args_id(exec_state, &args, id),
402 ModelingCmd::from(
403 mcmd::Solid3dCutEdges::builder()
404 .use_legacy(csg_algorithm.is_legacy())
405 .edge_ids(edge_ids.clone())
406 .extra_face_ids(extra_face_ids)
407 .strategy(strategy)
408 .object_id(solid.id)
409 .tolerance(LengthUnit(DEFAULT_TOLERANCE))
411 .cut_type(cut_type)
412 .version(edge_cut_version)
413 .build(),
414 ),
415 )
416 .await?;
417
418 let new_edge_cuts = edge_ids.into_iter().map(|edge_id| EdgeCut::Chamfer {
419 id,
420 edge_id,
421 length: length.clone(),
422 tag: Box::new(tag.clone()),
423 });
424 solid.edge_cuts.extend(new_edge_cuts);
425
426 if let Some(ref tag) = tag {
427 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
428 face_id: id,
429 tag: Some(tag.clone()),
430 geo_meta: GeoMeta {
431 id,
432 metadata: args.source_range.into(),
433 },
434 }));
435 }
436
437 Ok(solid)
438}
439
440#[expect(clippy::too_many_arguments)]
441async fn inner_chamfer_with_engine_refs(
442 solid: Box<Solid>,
443 length: TyF64,
444 edge_references: Vec<kcmc::shared::EdgeSpecifier>,
445 second_length: Option<TyF64>,
446 angle: Option<TyF64>,
447 csg_algorithm: CsgAlgorithm,
448 edge_cut_version: EdgeCutVersion,
449 tag: Option<TagNode>,
450 exec_state: &mut ExecState,
451 args: Args,
452) -> Result<Box<Solid>, KclError> {
453 if tag.is_some() && edge_references.len() > 1 {
454 return Err(KclError::new_type(KclErrorDetails::new(
455 "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(),
456 vec![args.source_range],
457 )));
458 }
459
460 if angle.is_some() && second_length.is_some() {
461 return Err(KclError::new_semantic(KclErrorDetails::new(
462 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
463 vec![args.source_range],
464 )));
465 }
466
467 let strategy = if second_length.is_some() || angle.is_some() {
468 CutStrategy::Csg
469 } else {
470 Default::default()
471 };
472
473 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
474 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
475 if let Some(angle) = angle
476 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
477 {
478 return Err(KclError::new_semantic(KclErrorDetails::new(
479 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
480 vec![args.source_range],
481 )));
482 }
483
484 let cut_type = CutTypeV2::Chamfer {
485 distance: LengthUnit(length.to_mm()),
486 second_distance,
487 angle,
488 swap: false,
489 };
490
491 let id = exec_state.next_uuid();
492 let num_extra_ids = edge_references.len().saturating_sub(1);
493 let mut extra_face_ids = Vec::with_capacity(num_extra_ids);
494 for _ in 0..num_extra_ids {
495 extra_face_ids.push(exec_state.next_uuid());
496 }
497
498 let mut solid = solid.clone();
499 exec_state
500 .batch_end_cmd(
501 ModelingCmdMeta::from_args_id(exec_state, &args, id),
502 ModelingCmd::from(
503 mcmd::Solid3dCutEdgeReferences::builder()
504 .object_id(solid.id)
505 .edges_references(edge_references)
506 .cut_type(cut_type)
507 .tolerance(LengthUnit(DEFAULT_TOLERANCE))
508 .strategy(strategy)
509 .extra_face_ids(extra_face_ids)
510 .use_legacy(csg_algorithm.is_legacy())
511 .version(edge_cut_version)
512 .build(),
513 ),
514 )
515 .await?;
516
517 solid.pending_edge_cut_ids.push(id);
518
519 if let Some(ref tag) = tag {
520 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
521 face_id: id,
522 tag: Some(tag.clone()),
523 geo_meta: GeoMeta {
524 id,
525 metadata: args.source_range.into(),
526 },
527 }));
528 }
529
530 Ok(solid)
531}