1use anyhow::Result;
4use kcmc::{
5 ModelingCmd, each_cmd as mcmd,
6 length_unit::LengthUnit,
7 shared::{CutStrategy, CutTypeV2},
8};
9use kittycad_modeling_cmds::{self as kcmc, shared::Angle};
10
11use super::args::TyF64;
12use crate::{
13 errors::{KclError, KclErrorDetails},
14 execution::{
15 ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta, Sketch, Solid,
16 types::RuntimeType,
17 },
18 parsing::ast::types::TagNode,
19 std::{Args, fillet::EdgeReference},
20};
21
22pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
23
24pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
26 let solid = args.get_unlabeled_kw_arg("solid", &RuntimeType::solid(), exec_state)?;
27 let length: TyF64 = args.get_kw_arg("length", &RuntimeType::length(), exec_state)?;
28 let tags = args.kw_arg_edge_array_and_source("tags")?;
29 let second_length = args.get_kw_arg_opt("secondLength", &RuntimeType::length(), exec_state)?;
30 let angle = args.get_kw_arg_opt("angle", &RuntimeType::angle(), exec_state)?;
31 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
34
35 super::fillet::validate_unique(&tags)?;
36 let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
37 let value = inner_chamfer(solid, length, tags, second_length, angle, None, tag, exec_state, args).await?;
38 Ok(KclValue::Solid { value })
39}
40
41#[allow(clippy::too_many_arguments)]
42async fn inner_chamfer(
43 solid: Box<Solid>,
44 length: TyF64,
45 tags: Vec<EdgeReference>,
46 second_length: Option<TyF64>,
47 angle: Option<TyF64>,
48 custom_profile: Option<Sketch>,
49 tag: Option<TagNode>,
50 exec_state: &mut ExecState,
51 args: Args,
52) -> Result<Box<Solid>, KclError> {
53 if tag.is_some() && tags.len() > 1 {
56 return Err(KclError::new_type(KclErrorDetails::new(
57 "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(),
58 vec![args.source_range],
59 )));
60 }
61
62 if angle.is_some() && second_length.is_some() {
63 return Err(KclError::new_semantic(KclErrorDetails::new(
64 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
65 vec![args.source_range],
66 )));
67 }
68
69 let strategy = if second_length.is_some() || angle.is_some() || custom_profile.is_some() {
70 CutStrategy::Csg
71 } else {
72 Default::default()
73 };
74
75 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
76 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
77 if let Some(angle) = angle
78 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
79 {
80 return Err(KclError::new_semantic(KclErrorDetails::new(
81 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
82 vec![args.source_range],
83 )));
84 }
85
86 let cut_type = if let Some(custom_profile) = custom_profile {
87 exec_state
89 .batch_modeling_cmd(
90 ModelingCmdMeta::from(&args),
91 ModelingCmd::from(mcmd::ObjectVisible {
92 object_id: custom_profile.id,
93 hidden: true,
94 }),
95 )
96 .await?;
97 CutTypeV2::Custom {
98 path: custom_profile.id,
99 }
100 } else {
101 CutTypeV2::Chamfer {
102 distance: LengthUnit(length.to_mm()),
103 second_distance,
104 angle,
105 swap: false,
106 }
107 };
108
109 let mut solid = solid.clone();
110 for edge_tag in tags {
111 let edge_id = match edge_tag {
112 EdgeReference::Uuid(uuid) => uuid,
113 EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id,
114 };
115
116 let id = exec_state.next_uuid();
117 exec_state
118 .batch_end_cmd(
119 ModelingCmdMeta::from_args_id(&args, id),
120 ModelingCmd::from(mcmd::Solid3dCutEdges {
121 edge_ids: vec![edge_id],
122 extra_face_ids: vec![],
123 strategy,
124 object_id: solid.id,
125 tolerance: LengthUnit(DEFAULT_TOLERANCE), cut_type,
127 }),
128 )
129 .await?;
130
131 solid.edge_cuts.push(EdgeCut::Chamfer {
132 id,
133 edge_id,
134 length: length.clone(),
135 tag: Box::new(tag.clone()),
136 });
137
138 if let Some(ref tag) = tag {
139 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
140 face_id: id,
141 tag: Some(tag.clone()),
142 geo_meta: GeoMeta {
143 id,
144 metadata: args.source_range.into(),
145 },
146 }));
147 }
148 }
149
150 Ok(solid)
151}