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::new(
53 "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge"
54 .to_string(),
55 duplicate_tags_source,
56 )));
57 }
58 Ok(())
59}
60
61pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
63 let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::solid(), exec_state)?;
64 let radius: TyF64 = args.get_kw_arg_typed("radius", &RuntimeType::length(), exec_state)?;
65 let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
66 let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
67 let tag = args.get_kw_arg_opt("tag")?;
68
69 validate_unique(&tags)?;
71 let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
72 let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
73 Ok(KclValue::Solid { value })
74}
75
76async fn inner_fillet(
77 solid: Box<Solid>,
78 radius: TyF64,
79 tags: Vec<EdgeReference>,
80 tolerance: Option<TyF64>,
81 tag: Option<TagNode>,
82 exec_state: &mut ExecState,
83 args: Args,
84) -> Result<Box<Solid>, KclError> {
85 let mut solid = solid.clone();
86 for edge_tag in tags {
87 let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
88
89 let id = exec_state.next_uuid();
90 args.batch_end_cmd(
91 id,
92 ModelingCmd::from(mcmd::Solid3dFilletEdge {
93 edge_id: None,
94 edge_ids: vec![edge_id],
95 extra_face_ids: vec![],
96 strategy: Default::default(),
97 object_id: solid.id,
98 radius: LengthUnit(radius.to_mm()),
99 tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
100 cut_type: CutType::Fillet,
101 }),
102 )
103 .await?;
104
105 solid.edge_cuts.push(EdgeCut::Fillet {
106 id,
107 edge_id,
108 radius: radius.clone(),
109 tag: Box::new(tag.clone()),
110 });
111
112 if let Some(ref tag) = tag {
113 solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
114 face_id: id,
115 tag: Some(tag.clone()),
116 geo_meta: GeoMeta {
117 id,
118 metadata: args.source_range.into(),
119 },
120 }));
121 }
122 }
123
124 Ok(solid)
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_validate_unique() {
133 let dup_a = SourceRange::from([1, 3, 0]);
134 let dup_b = SourceRange::from([10, 30, 0]);
135 let tags = vec![("abc", dup_a), ("abc", dup_b), ("def", SourceRange::from([2, 4, 0]))];
137 let actual = validate_unique(&tags);
138 let expected = vec![dup_a, dup_b];
142 assert_eq!(actual.err().unwrap().source_ranges(), expected);
143 }
144}