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(exec_state, &args),
91 ModelingCmd::from(
92 mcmd::ObjectVisible::builder()
93 .object_id(custom_profile.id)
94 .hidden(true)
95 .build(),
96 ),
97 )
98 .await?;
99 CutTypeV2::Custom {
100 path: custom_profile.id,
101 }
102 } else {
103 CutTypeV2::Chamfer {
104 distance: LengthUnit(length.to_mm()),
105 second_distance,
106 angle,
107 swap: false,
108 }
109 };
110
111 let mut solid = solid.clone();
112 for edge_tag in tags {
113 let edge_id = match edge_tag {
114 EdgeReference::Uuid(uuid) => uuid,
115 EdgeReference::Tag(edge_tag) => args.get_tag_engine_info(exec_state, &edge_tag)?.id,
116 };
117
118 let id = exec_state.next_uuid();
119 exec_state
120 .batch_end_cmd(
121 ModelingCmdMeta::from_args_id(exec_state, &args, id),
122 ModelingCmd::from(
123 mcmd::Solid3dCutEdges::builder()
124 .edge_ids(vec![edge_id])
125 .extra_face_ids(vec![])
126 .strategy(strategy)
127 .object_id(solid.id)
128 .tolerance(LengthUnit(DEFAULT_TOLERANCE)) .cut_type(cut_type)
130 .build(),
131 ),
132 )
133 .await?;
134
135 solid.edge_cuts.push(EdgeCut::Chamfer {
136 id,
137 edge_id,
138 length: length.clone(),
139 tag: Box::new(tag.clone()),
140 });
141
142 if let Some(ref tag) = tag {
143 solid.value.push(ExtrudeSurface::Chamfer(ChamferSurface {
144 face_id: id,
145 tag: Some(tag.clone()),
146 geo_meta: GeoMeta {
147 id,
148 metadata: args.source_range.into(),
149 },
150 }));
151 }
152 }
153
154 Ok(solid)
155}