1#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct SelectionProbe {
26 pub solids: usize,
27 pub sketches: usize,
28 pub faces: usize,
29 pub edges: usize,
30 pub planes: usize,
35 pub vertices: usize,
38 pub components: usize,
40 pub all_component: bool,
45 pub all_sheet_metal: bool,
51}
52
53impl SelectionProbe {
54 pub fn has_profile(&self) -> bool {
57 self.faces > 0 || self.sketches > 0
58 }
59}
60
61pub static FEATURE_APPLICABILITY: &[(&str, fn(&SelectionProbe) -> bool)] = &[
66 ("D", super::features::datum::context_applicable),
67 ("P", super::features::plane::context_applicable),
68 ("P.CU", super::features::cube::context_applicable),
69 ("P.CY", super::features::cylinder::context_applicable),
70 ("P.CO", super::features::cone::context_applicable),
71 ("P.S", super::features::sphere::context_applicable),
72 ("P.T", super::features::torus::context_applicable),
73 ("P.PY", super::features::pyramid::context_applicable),
74 ("IMPORT3D", super::features::import3d::context_applicable),
75 ("S", super::features::sketch::context_applicable),
76 ("SP", super::features::spline::context_applicable),
77 ("PORT", super::features::port::context_applicable),
78 ("HX", super::features::helix::context_applicable),
79 ("E", super::features::extrude::context_applicable),
80 ("B", super::features::boolean::context_applicable),
81 ("F", super::features::fillet::context_applicable),
82 ("CH", super::features::chamfer::context_applicable),
83 ("O.S", super::features::offset_shell::context_applicable),
84 ("O.F", super::features::offset_face::context_applicable),
85 ("PF", super::features::push_face::context_applicable),
86 ("DF", super::features::delete_face::context_applicable),
87 ("THK", super::features::thicken::context_applicable),
88 ("SM.TAB", super::features::sheet_metal_tab::context_applicable),
89 ("SM.CF", super::features::sheet_metal_contour_flange::context_applicable),
90 ("SM.F", super::features::sheet_metal_flange::context_applicable),
91 ("SM.HEM", super::features::sheet_metal_hem::context_applicable),
92 ("SM.FILLET", super::features::sheet_metal_corner::context_applicable),
93 ("SM.CHAMFER", super::features::sheet_metal_corner::context_applicable),
94 ("SM.CUTOUT", super::features::sheet_metal_cutout::context_applicable),
95 ("LOFT", super::features::loft::context_applicable),
96 ("M", super::features::mirror::context_applicable),
97 ("SPL", super::features::split::context_applicable),
98 ("R", super::features::revolve::context_applicable),
99 ("RIB", super::features::rib::context_applicable),
100 ("SW", super::features::sweep::context_applicable),
101 ("SWP", super::features::path_sweep::context_applicable),
102 ("H", super::features::hole::context_applicable),
103 ("TU", super::features::tube::context_applicable),
104 ("XFORM", super::features::transform::context_applicable),
105 ("PATTERN", super::features::pattern::context_applicable),
106 ("ACOMP", super::features::assembly_component::context_applicable),
107];
108
109pub fn feature_context_applicable(type_id: &str, probe: &SelectionProbe) -> bool {
112 FEATURE_APPLICABILITY
113 .iter()
114 .find(|(id, _)| *id == type_id)
115 .is_some_and(|(_, applicable)| applicable(probe))
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
126 fn applicability_registry_matches_the_catalogue() {
127 let catalogue = crate::feature_pipeline::feature_schema_catalogue();
128 let types: Vec<String> = catalogue["features"]
129 .as_array()
130 .expect("features array")
131 .iter()
132 .map(|f| f["type"].as_str().expect("type string").to_string())
133 .collect();
134 let registry: Vec<&str> = FEATURE_APPLICABILITY.iter().map(|(id, _)| *id).collect();
135 for ty in &types {
136 assert!(
137 registry.contains(&ty.as_str()),
138 "catalogue feature {ty} has no applicability predicate"
139 );
140 }
141 for id in ®istry {
142 assert!(
143 types.iter().any(|ty| ty == id),
144 "registry row {id} is not a catalogue feature"
145 );
146 }
147 assert_eq!(registry.len(), types.len(), "duplicate registry rows");
148 }
149
150 #[test]
153 fn revolve_requires_profile_and_axis() {
154 let face_only = SelectionProbe { faces: 1, ..Default::default() };
155 assert!(!feature_context_applicable("R", &face_only));
156 let sketch_only = SelectionProbe { sketches: 1, ..Default::default() };
157 assert!(!feature_context_applicable("R", &sketch_only));
158 let edge_only = SelectionProbe { edges: 1, ..Default::default() };
159 assert!(!feature_context_applicable("R", &edge_only));
160 let face_and_axis = SelectionProbe { faces: 1, edges: 1, ..Default::default() };
161 assert!(feature_context_applicable("R", &face_and_axis));
162 let sketch_and_axis = SelectionProbe { sketches: 1, edges: 1, ..Default::default() };
163 assert!(feature_context_applicable("R", &sketch_and_axis));
164 }
165
166 #[test]
171 fn sheet_metal_edits_require_a_sheet_metal_selection() {
172 for probe in [
173 SelectionProbe { faces: 1, ..Default::default() },
174 SelectionProbe { edges: 1, ..Default::default() },
175 SelectionProbe { solids: 1, ..Default::default() },
176 ] {
177 for ty in ["SM.F", "SM.FILLET", "SM.CHAMFER"] {
178 assert!(
179 !feature_context_applicable(ty, &probe),
180 "{ty} must not offer on non-sheet-metal geometry: {probe:?}"
181 );
182 }
183 }
184 let sm_face = SelectionProbe { faces: 1, all_sheet_metal: true, ..Default::default() };
187 let sm_edge = SelectionProbe { edges: 1, all_sheet_metal: true, ..Default::default() };
188 let sm_solid = SelectionProbe { solids: 1, all_sheet_metal: true, ..Default::default() };
189 for probe in [&sm_face, &sm_edge] {
190 for ty in ["SM.F", "SM.FILLET", "SM.CHAMFER"] {
191 assert!(feature_context_applicable(ty, probe), "{ty} offers on sheet metal");
192 }
193 }
194 assert!(feature_context_applicable("SM.FILLET", &sm_solid));
197 assert!(feature_context_applicable("SM.CHAMFER", &sm_solid));
198 assert!(!feature_context_applicable("SM.F", &sm_solid));
199 }
200
201 #[test]
205 fn plane_selection_offers_sketch() {
206 let plane_only = SelectionProbe { planes: 1, ..Default::default() };
207 assert!(
208 feature_context_applicable("S", &plane_only),
209 "a plane/datum-only selection should offer Sketch"
210 );
211 for ty in ["E", "F", "CH", "B", "XFORM", "R"] {
214 assert!(
215 !feature_context_applicable(ty, &plane_only),
216 "{ty} must not offer on a plane-only selection"
217 );
218 }
219 }
220
221 #[test]
224 fn parity_predicates_match_reference_filters() {
225 let face = SelectionProbe { faces: 1, ..Default::default() };
226 let edge = SelectionProbe { edges: 1, ..Default::default() };
227 let solid = SelectionProbe { solids: 1, ..Default::default() };
228 let sketch = SelectionProbe { sketches: 1, ..Default::default() };
229
230 for ty in ["E", "O.F", "PF", "O.S", "THK", "DF", "F", "CH", "M", "SPL", "PATTERN", "S"] {
232 assert!(feature_context_applicable(ty, &face), "FACE should offer {ty}");
233 }
234 for ty in ["F", "CH", "TU", "HX", "RIB", "S"] {
236 assert!(feature_context_applicable(ty, &edge), "EDGE should offer {ty}");
237 }
238 assert!(!feature_context_applicable("E", &edge), "EDGE must not offer Extrude");
239 for ty in ["B", "M", "XFORM", "PATTERN", "SPL", "RIB", "SM.CUTOUT"] {
241 assert!(feature_context_applicable(ty, &solid), "SOLID should offer {ty}");
242 }
243 assert!(!feature_context_applicable("F", &solid), "SOLID must not offer Fillet");
244 for ty in ["E", "H", "SM.CUTOUT", "SM.TAB", "SW", "SWP", "LOFT"] {
246 assert!(feature_context_applicable(ty, &sketch), "SKETCH should offer {ty}");
247 }
248 let all = SelectionProbe {
250 solids: 1,
251 sketches: 1,
252 faces: 1,
253 edges: 1,
254 ..Default::default()
255 };
256 for ty in ["P.CU", "P.CY", "P.CO", "P.S", "P.T", "P.PY", "D", "IMPORT3D", "SP", "ACOMP"] {
257 assert!(!feature_context_applicable(ty, &all), "{ty} must never be offered");
258 }
259 assert!(!feature_context_applicable("NOPE", &all));
261 }
262}