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 = 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 tags = args.kw_arg_edge_array_and_source("tags")?;
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_default();
54
55 let tag = args.get_kw_arg_opt("tag", &RuntimeType::tag_decl(), exec_state)?;
56
57 super::fillet::validate_unique(&tags)?;
58 let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
59 let value = inner_chamfer(
60 solid,
61 length,
62 tags,
63 second_length,
64 angle,
65 None,
66 tag,
67 csg_algorithm,
68 edge_cut_version,
69 exec_state,
70 args,
71 )
72 .await?;
73 Ok(KclValue::Solid { value })
74}
75
76#[allow(clippy::too_many_arguments)]
77async fn inner_chamfer(
78 solid: Box<Solid>,
79 length: TyF64,
80 tags: Vec<EdgeReference>,
81 second_length: Option<TyF64>,
82 angle: Option<TyF64>,
83 custom_profile: Option<Sketch>,
84 tag: Option<TagNode>,
85 csg_algorithm: CsgAlgorithm,
86 edge_cut_version: EdgeCutVersion,
87 exec_state: &mut ExecState,
88 args: Args,
89) -> Result<Box<Solid>, KclError> {
90 if tag.is_some() && tags.len() > 1 {
93 return Err(KclError::new_type(KclErrorDetails::new(
94 "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(),
95 vec![args.source_range],
96 )));
97 }
98
99 if angle.is_some() && second_length.is_some() {
100 return Err(KclError::new_semantic(KclErrorDetails::new(
101 "Cannot specify both an angle and a second length. Specify only one.".to_string(),
102 vec![args.source_range],
103 )));
104 }
105
106 let strategy = if second_length.is_some() || angle.is_some() || custom_profile.is_some() {
107 CutStrategy::Csg
108 } else {
109 Default::default()
110 };
111
112 let second_distance = second_length.map(|x| LengthUnit(x.to_mm()));
113 let angle = angle.map(|x| Angle::from_degrees(x.to_degrees(exec_state, args.source_range)));
114 if let Some(angle) = angle
115 && (angle.ge(&Angle::quarter_circle()) || angle.le(&Angle::zero()))
116 {
117 return Err(KclError::new_semantic(KclErrorDetails::new(
118 "The angle of a chamfer must be greater than zero and less than 90 degrees.".to_string(),
119 vec![args.source_range],
120 )));
121 }
122
123 let cut_type = if let Some(custom_profile) = custom_profile {
124 exec_state
126 .batch_modeling_cmd(
127 ModelingCmdMeta::from_args(exec_state, &args),
128 ModelingCmd::from(
129 mcmd::ObjectVisible::builder()
130 .object_id(custom_profile.id)
131 .hidden(true)
132 .build(),
133 ),
134 )
135 .await?;
136 CutTypeV2::Custom {
137 path: custom_profile.id,
138 }
139 } else {
140 CutTypeV2::Chamfer {
141 distance: LengthUnit(length.to_mm()),
142 second_distance,
143 angle,
144 swap: false,
145 }
146 };
147
148 let mut solid = solid.clone();
149 for edge_tag in tags {
150 let edge_ids = edge_tag.get_all_engine_ids(exec_state, &args)?;
151 for edge_id in edge_ids {
152 let id = exec_state.next_uuid();
153 exec_state
154 .batch_end_cmd(
155 ModelingCmdMeta::from_args_id(exec_state, &args, id),
156 ModelingCmd::from(
157 mcmd::Solid3dCutEdges::builder()
158 .use_legacy(csg_algorithm.is_legacy())
159 .edge_ids(vec![edge_id])
160 .extra_face_ids(vec![])
161 .strategy(strategy)
162 .object_id(solid.id)
163 .tolerance(LengthUnit(DEFAULT_TOLERANCE))
165 .cut_type(cut_type)
166 .version(edge_cut_version)
167 .build(),
168 ),
169 )
170 .await?;
171
172 solid.edge_cuts.push(EdgeCut::Chamfer {
173 id,
174 edge_id,
175 length: length.clone(),
176 tag: Box::new(tag.clone()),
177 });
178
179 if let Some(ref tag) = tag {
180 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
181 face_id: id,
182 tag: Some(tag.clone()),
183 geo_meta: GeoMeta {
184 id,
185 metadata: args.source_range.into(),
186 },
187 }));
188 }
189 }
190 }
191
192 Ok(solid)
193}