1use anyhow::Result;
4use indexmap::IndexMap;
5use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
6use kittycad_modeling_cmds as kcmc;
7use serde::{Deserialize, Serialize};
8
9use super::{args::TyF64, DEFAULT_TOLERANCE};
10use crate::{
11 errors::{KclError, KclErrorDetails},
12 execution::{
13 types::RuntimeType, EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier,
14 },
15 parsing::ast::types::TagNode,
16 std::Args,
17 SourceRange,
18};
19
20#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
22#[serde(untagged)]
23pub enum EdgeReference {
24 Uuid(uuid::Uuid),
26 Tag(Box<TagIdentifier>),
28}
29
30impl EdgeReference {
31 pub fn get_engine_id(&self, exec_state: &mut ExecState, args: &Args) -> Result<uuid::Uuid, KclError> {
32 match self {
33 EdgeReference::Uuid(uuid) => Ok(*uuid),
34 EdgeReference::Tag(tag) => Ok(args.get_tag_engine_info(exec_state, tag)?.id),
35 }
36 }
37}
38
39pub(super) fn validate_unique<T: Eq + std::hash::Hash>(tags: &[(T, SourceRange)]) -> Result<(), KclError> {
40 let mut tag_counts: IndexMap<&T, Vec<SourceRange>> = Default::default();
42 for tag in tags {
43 tag_counts.entry(&tag.0).or_insert(Vec::new()).push(tag.1);
44 }
45 let mut duplicate_tags_source = Vec::new();
46 for (_tag, count) in tag_counts {
47 if count.len() > 1 {
48 duplicate_tags_source.extend(count)
49 }
50 }
51 if !duplicate_tags_source.is_empty() {
52 return Err(KclError::Type(KclErrorDetails {
53 message: "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge".to_string(),
54 source_ranges: duplicate_tags_source,
55 }));
56 }
57 Ok(())
58}
59
60pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
62 let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::solid(), exec_state)?;
63 let radius: TyF64 = args.get_kw_arg_typed("radius", &RuntimeType::length(), exec_state)?;
64 let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
65 let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
66 let tag = args.get_kw_arg_opt("tag")?;
67
68 validate_unique(&tags)?;
70 let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
71 let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
72 Ok(KclValue::Solid { value })
73}
74
75async fn inner_fillet(
76 solid: Box<Solid>,
77 radius: TyF64,
78 tags: Vec<EdgeReference>,
79 tolerance: Option<TyF64>,
80 tag: Option<TagNode>,
81 exec_state: &mut ExecState,
82 args: Args,
83) -> Result<Box<Solid>, KclError> {
84 let mut solid = solid.clone();
85 for edge_tag in tags {
86 let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
87
88 let id = exec_state.next_uuid();
89 args.batch_end_cmd(
90 id,
91 ModelingCmd::from(mcmd::Solid3dFilletEdge {
92 edge_id: None,
93 edge_ids: vec![edge_id],
94 extra_face_ids: vec![],
95 strategy: Default::default(),
96 object_id: solid.id,
97 radius: LengthUnit(radius.to_mm()),
98 tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
99 cut_type: CutType::Fillet,
100 }),
101 )
102 .await?;
103
104 solid.edge_cuts.push(EdgeCut::Fillet {
105 id,
106 edge_id,
107 radius: radius.clone(),
108 tag: Box::new(tag.clone()),
109 });
110
111 if let Some(ref tag) = tag {
112 solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
113 face_id: id,
114 tag: Some(tag.clone()),
115 geo_meta: GeoMeta {
116 id,
117 metadata: args.source_range.into(),
118 },
119 }));
120 }
121 }
122
123 Ok(solid)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_validate_unique() {
132 let dup_a = SourceRange::from([1, 3, 0]);
133 let dup_b = SourceRange::from([10, 30, 0]);
134 let tags = vec![("abc", dup_a), ("abc", dup_b), ("def", SourceRange::from([2, 4, 0]))];
136 let actual = validate_unique(&tags);
137 let expected = vec![dup_a, dup_b];
141 assert_eq!(actual.err().unwrap().source_ranges(), expected);
142 }
143}