1use anyhow::Result;
4use indexmap::IndexMap;
5use kcl_derive_docs::stdlib;
6use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
7use kittycad_modeling_cmds as kcmc;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 errors::{KclError, KclErrorDetails},
13 execution::{
14 kcl_value::RuntimeType, EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, PrimitiveType,
15 Solid, TagIdentifier,
16 },
17 parsing::ast::types::TagNode,
18 settings::types::UnitLength,
19 std::Args,
20 SourceRange,
21};
22
23#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Hash)]
25#[ts(export)]
26#[serde(untagged)]
27pub enum EdgeReference {
28 Uuid(uuid::Uuid),
30 Tag(Box<TagIdentifier>),
32}
33
34impl EdgeReference {
35 pub fn get_engine_id(&self, exec_state: &mut ExecState, args: &Args) -> Result<uuid::Uuid, KclError> {
36 match self {
37 EdgeReference::Uuid(uuid) => Ok(*uuid),
38 EdgeReference::Tag(tag) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
39 }
40 }
41}
42
43pub(super) fn validate_unique<T: Eq + std::hash::Hash>(tags: &[(T, SourceRange)]) -> Result<(), KclError> {
44 let mut tag_counts: IndexMap<&T, Vec<SourceRange>> = Default::default();
46 for tag in tags {
47 tag_counts.entry(&tag.0).or_insert(Vec::new()).push(tag.1);
48 }
49 let mut duplicate_tags_source = Vec::new();
50 for (_tag, count) in tag_counts {
51 if count.len() > 1 {
52 duplicate_tags_source.extend(count)
53 }
54 }
55 if !duplicate_tags_source.is_empty() {
56 return Err(KclError::Type(KclErrorDetails {
57 message: "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge".to_string(),
58 source_ranges: duplicate_tags_source,
59 }));
60 }
61 Ok(())
62}
63
64pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
66 let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::Primitive(PrimitiveType::Solid), exec_state)?;
67 let radius = args.get_kw_arg("radius")?;
68 let tolerance = args.get_kw_arg_opt("tolerance")?;
69 let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
70 let tag = args.get_kw_arg_opt("tag")?;
71
72 validate_unique(&tags)?;
74 let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
75 let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
76 Ok(KclValue::Solid { value })
77}
78
79#[stdlib {
136 name = "fillet",
137 feature_tree_operation = true,
138 keywords = true,
139 unlabeled_first = true,
140 args = {
141 solid = { docs = "The solid whose edges should be filletted" },
142 radius = { docs = "The radius of the fillet" },
143 tags = { docs = "The paths you want to fillet" },
144 tolerance = { docs = "The tolerance for this fillet" },
145 tag = { docs = "Create a new tag which refers to this fillet"},
146 }
147}]
148async fn inner_fillet(
149 solid: Box<Solid>,
150 radius: f64,
151 tags: Vec<EdgeReference>,
152 tolerance: Option<f64>,
153 tag: Option<TagNode>,
154 exec_state: &mut ExecState,
155 args: Args,
156) -> Result<Box<Solid>, KclError> {
157 let mut solid = solid.clone();
158 for edge_tag in tags {
159 let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
160
161 let id = exec_state.next_uuid();
162 args.batch_end_cmd(
163 id,
164 ModelingCmd::from(mcmd::Solid3dFilletEdge {
165 edge_id,
166 object_id: solid.id,
167 radius: LengthUnit(radius),
168 tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
169 cut_type: CutType::Fillet,
170 face_id: None,
172 }),
173 )
174 .await?;
175
176 solid.edge_cuts.push(EdgeCut::Fillet {
177 id,
178 edge_id,
179 radius,
180 tag: Box::new(tag.clone()),
181 });
182
183 if let Some(ref tag) = tag {
184 solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
185 face_id: id,
186 tag: Some(tag.clone()),
187 geo_meta: GeoMeta {
188 id,
189 metadata: args.source_range.into(),
190 },
191 }));
192 }
193 }
194
195 Ok(solid)
196}
197
198pub(crate) fn default_tolerance(units: &UnitLength) -> f64 {
199 match units {
200 UnitLength::Mm => 0.0000001,
201 UnitLength::Cm => 0.0000001,
202 UnitLength::In => 0.0000001,
203 UnitLength::Ft => 0.0001,
204 UnitLength::Yd => 0.001,
205 UnitLength::M => 0.001,
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_validate_unique() {
215 let dup_a = SourceRange::from([1, 3, 0]);
216 let dup_b = SourceRange::from([10, 30, 0]);
217 let tags = vec![("abc", dup_a), ("abc", dup_b), ("def", SourceRange::from([2, 4, 0]))];
219 let actual = validate_unique(&tags);
220 let expected = vec![dup_a, dup_b];
224 assert_eq!(actual.err().unwrap().source_ranges(), expected);
225 }
226}