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