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::new_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_edge_array_and_source("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 if tag.is_some() && tags.len() > 1 {
88 return Err(KclError::new_type(KclErrorDetails {
89 message: "You can only tag one edge at a time with a tagged fillet. Either delete the tag for the fillet fn if you don't need it OR separate into individual fillet functions for each tag.".to_string(),
90 source_ranges: vec![args.source_range],
91 backtrace: Default::default(),
92 }));
93 }
94 if tags.is_empty() {
95 return Err(KclError::new_semantic(KclErrorDetails {
96 source_ranges: vec![args.source_range],
97 message: "You must fillet at least one tag".to_owned(),
98 backtrace: Default::default(),
99 }));
100 }
101
102 let mut solid = solid.clone();
103 let edge_ids = tags
104 .into_iter()
105 .map(|edge_tag| edge_tag.get_engine_id(exec_state, &args))
106 .collect::<Result<Vec<_>, _>>()?;
107
108 let id = exec_state.next_uuid();
109 let mut extra_face_ids = Vec::new();
110 let num_extra_ids = edge_ids.len() - 1;
111 for _ in 0..num_extra_ids {
112 extra_face_ids.push(exec_state.next_uuid());
113 }
114 args.batch_end_cmd(
115 id,
116 ModelingCmd::from(mcmd::Solid3dFilletEdge {
117 edge_id: None,
118 edge_ids: edge_ids.clone(),
119 extra_face_ids,
120 strategy: Default::default(),
121 object_id: solid.id,
122 radius: LengthUnit(radius.to_mm()),
123 tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
124 cut_type: CutType::Fillet,
125 }),
126 )
127 .await?;
128
129 let new_edge_cuts = edge_ids.into_iter().map(|edge_id| EdgeCut::Fillet {
130 id,
131 edge_id,
132 radius: radius.clone(),
133 tag: Box::new(tag.clone()),
134 });
135 solid.edge_cuts.extend(new_edge_cuts);
136
137 if let Some(ref tag) = tag {
138 solid.value.push(ExtrudeSurface::Fillet(FilletSurface {
139 face_id: id,
140 tag: Some(tag.clone()),
141 geo_meta: GeoMeta {
142 id,
143 metadata: args.source_range.into(),
144 },
145 }));
146 }
147
148 Ok(solid)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_validate_unique() {
157 let dup_a = SourceRange::from([1, 3, 0]);
158 let dup_b = SourceRange::from([10, 30, 0]);
159 let tags = vec![("abc", dup_a), ("abc", dup_b), ("def", SourceRange::from([2, 4, 0]))];
161 let actual = validate_unique(&tags);
162 let expected = vec![dup_a, dup_b];
166 assert_eq!(actual.err().unwrap().source_ranges(), expected);
167 }
168}