1use crate::style::{FieldKind, FormField};
25use serde_json::Value;
26
27pub fn feature_catalogue() -> Value {
29 brep_kernel::feature_schema_catalogue()
30}
31
32pub fn feature_schema(feature_type: &str) -> Option<Value> {
35 feature_catalogue()
36 .get("features")?
37 .as_array()?
38 .iter()
39 .find(|f| {
40 f.get("type").and_then(Value::as_str) == Some(feature_type)
41 || f.get("shortName").and_then(Value::as_str) == Some(feature_type)
42 })
43 .cloned()
44}
45
46pub fn feature_long_name(feature_type: &str) -> String {
48 feature_schema(feature_type)
49 .and_then(|f| f.get("longName").and_then(Value::as_str).map(String::from))
50 .unwrap_or_else(|| feature_type.to_string())
51}
52
53pub fn feature_short_name(feature_type: &str) -> String {
58 feature_schema(feature_type)
59 .and_then(|f| f.get("shortName").and_then(Value::as_str).map(String::from))
60 .unwrap_or_else(|| feature_type.to_string())
61}
62
63pub fn feature_default_params(feature_type: &str) -> Value {
70 let mut params = serde_json::Map::new();
71 if let Some(props) = feature_schema(feature_type)
72 .as_ref()
73 .and_then(|s| s.get("inputParamsSchema"))
74 .and_then(Value::as_object)
75 {
76 for (name, spec) in props {
77 let default = spec.get("default_value").cloned().unwrap_or(Value::Null);
78 params.insert(name.clone(), default);
79 }
80 }
81 Value::Object(params)
82}
83
84const BOOLEAN_OPS: &[&str] = &["NONE", "UNION", "SUBTRACT", "INTERSECT"];
87
88pub fn feature_form_fields(feature_type: &str) -> Vec<FormField> {
92 let Some(schema) = feature_schema(feature_type) else {
93 return Vec::new();
94 };
95 let Some(params) = schema
96 .get("inputParamsSchema")
97 .and_then(Value::as_object)
98 else {
99 return Vec::new();
100 };
101
102 let mut fields = Vec::new();
103 for (name, spec) in params {
104 let ty = spec.get("type").and_then(Value::as_str).unwrap_or("");
105 match ty {
106 "number" => fields.push(FormField {
107 path: vec![name.clone()],
108 label: prettify(name),
109 group: "Parameters".into(),
110 kind: FieldKind::Scalar { step: 0.5 },
111 }),
112 "string" => fields.push(FormField {
113 path: vec![name.clone()],
114 label: prettify(name),
115 group: "Parameters".into(),
116 kind: FieldKind::Text {
119 read_only: name == "id",
120 },
121 }),
122 "transform" => {
123 fields.push(vec3_field(name, "position", "Position", 0.5));
124 fields.push(vec3_field(name, "rotationEuler", "Rotation (deg)", 1.0));
125 fields.push(vec3_field(name, "scale", "Scale", 0.1));
126 }
127 "boolean_operation" => {
128 fields.push(FormField {
129 path: vec![name.clone(), "operation".into()],
130 label: "Operation".into(),
131 group: "Boolean".into(),
132 kind: FieldKind::Enum {
133 variants: BOOLEAN_OPS.to_vec(),
134 },
135 });
136 fields.push(FormField {
137 path: vec![name.clone(), "targets".into()],
138 label: "Tool solids".into(),
139 group: "Boolean".into(),
140 kind: FieldKind::Reference {
141 filter: vec!["SOLID".into()],
142 multiple: true,
143 },
144 });
145 fields.push(FormField {
146 path: vec![name.clone(), "mergeCoplanarFaces".into()],
147 label: "Merge coplanar faces".into(),
148 group: "Boolean".into(),
149 kind: FieldKind::Bool,
150 });
151 }
152 "button" => {
153 let label = spec
157 .get("label")
158 .and_then(Value::as_str)
159 .map(String::from)
160 .unwrap_or_else(|| prettify(name));
161 fields.push(FormField {
162 path: vec![name.clone()],
163 label: label.clone(),
164 group: "Parameters".into(),
165 kind: FieldKind::Button { label },
166 });
167 }
168 "reference_selection" => {
169 let filter = spec
170 .get("selectionFilter")
171 .and_then(Value::as_array)
172 .map(|a| {
173 a.iter()
174 .filter_map(|v| v.as_str().map(String::from))
175 .collect()
176 })
177 .unwrap_or_default();
178 let multiple = spec
179 .get("multiple")
180 .and_then(Value::as_bool)
181 .unwrap_or(false);
182 fields.push(FormField {
188 path: vec![name.clone()],
189 label: prettify(name),
190 group: "References".into(),
191 kind: FieldKind::Reference { filter, multiple },
192 });
193 }
194 _ => {}
197 }
198 }
199 fields
200}
201
202fn vec3_field(param: &str, sub: &str, label: &str, step: f64) -> FormField {
203 FormField {
204 path: vec![param.to_string(), sub.to_string()],
205 label: label.to_string(),
206 group: "Transform".into(),
207 kind: FieldKind::Vec3 { step },
208 }
209}
210
211fn prettify(key: &str) -> String {
213 let mut out = String::new();
214 for (i, ch) in key.chars().enumerate() {
215 if i == 0 {
216 out.extend(ch.to_uppercase());
217 } else if ch.is_ascii_uppercase() {
218 out.push(' ');
219 out.extend(ch.to_lowercase());
220 } else {
221 out.push(ch);
222 }
223 }
224 out
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn catalogue_is_reachable_from_native_rust() {
233 let cat = feature_catalogue();
234 assert!(cat.get("features").and_then(Value::as_array).is_some());
235 assert!(feature_schema("P.CU").is_some());
236 assert!(feature_schema("P.CY").is_some());
237 assert!(feature_schema("B").is_some());
238 assert_eq!(feature_long_name("P.CU"), "Primitive Cube");
239 }
240
241 #[test]
242 fn short_name_is_the_schema_code_or_the_type_fallback() {
243 assert_eq!(feature_short_name("P.CU"), "P.CU");
246 assert_eq!(feature_short_name("P.S"), "P.S");
247 assert_eq!(feature_short_name("S"), "S");
248 assert_eq!(feature_short_name("E"), "E");
249 assert_eq!(feature_short_name("NOPE"), "NOPE");
250 }
251
252 #[test]
253 fn cube_form_fields_map_types_correctly() {
254 let fields = feature_form_fields("P.CU");
255 let by_key = |k: &str| fields.iter().find(|f| f.key() == k).cloned();
258
259 assert!(matches!(
260 by_key("id").unwrap().kind,
261 FieldKind::Text { read_only: true }
262 ));
263 assert!(matches!(
264 by_key("sizeX").unwrap().kind,
265 FieldKind::Scalar { .. }
266 ));
267 let position = fields
269 .iter()
270 .find(|f| f.path == ["transform", "position"])
271 .expect("transform.position vec3");
272 assert!(matches!(position.kind, FieldKind::Vec3 { .. }));
273 assert_eq!(
274 fields
275 .iter()
276 .filter(|f| matches!(f.kind, FieldKind::Vec3 { .. }))
277 .count(),
278 3
279 );
280 let op = fields
282 .iter()
283 .find(|f| f.path == ["boolean", "operation"])
284 .expect("boolean.operation");
285 assert!(matches!(op.kind, FieldKind::Enum { .. }));
286 let targets = fields
287 .iter()
288 .find(|f| f.path == ["boolean", "targets"])
289 .expect("boolean.targets");
290 assert!(matches!(targets.kind, FieldKind::Reference { multiple: true, .. }));
291 }
292
293 #[test]
294 fn default_params_seed_from_schema_defaults() {
295 let cube = feature_default_params("P.CU");
297 assert_eq!(cube["sizeX"], 10.0);
298 assert_eq!(cube["sizeY"], 10.0);
299 assert_eq!(cube["transform"]["scale"], serde_json::json!([1, 1, 1]));
300 assert_eq!(cube["boolean"]["operation"], "NONE");
301 assert_eq!(cube["id"], Value::Null);
303 let boolean = feature_default_params("B");
305 assert!(boolean.as_object().unwrap().contains_key("targetSolid"));
306 assert_eq!(boolean["boolean"]["operation"], "UNION");
307 assert_eq!(feature_default_params("NOPE"), serde_json::json!({}));
309 }
310
311 #[test]
312 fn boolean_feature_target_is_a_reference_field() {
313 let fields = feature_form_fields("B");
314 let target = fields
315 .iter()
316 .find(|f| f.key() == "targetSolid")
317 .expect("targetSolid field");
318 match &target.kind {
319 FieldKind::Reference { filter, multiple } => {
320 assert_eq!(filter, &["SOLID".to_string()]);
321 assert!(!multiple);
322 }
323 other => panic!("targetSolid should be a Reference, got {other:?}"),
324 }
325 }
326
327 #[test]
328 fn sketch_feature_maps_button_fields() {
329 let fields = feature_form_fields("S");
332 let edit = fields
333 .iter()
334 .find(|f| f.key() == "editSketch")
335 .expect("editSketch button field");
336 assert!(
337 matches!(&edit.kind, FieldKind::Button { label } if label == "Edit Sketch"),
338 "editSketch should be Button('Edit Sketch'), got {:?}",
339 edit.kind
340 );
341 assert_eq!(edit.path, ["editSketch"], "button binds to its own key");
342 let dump = fields
343 .iter()
344 .find(|f| f.key() == "dumpSketchDiagnostics")
345 .expect("dumpSketchDiagnostics button field");
346 assert!(matches!(dump.kind, FieldKind::Button { .. }));
347 }
348
349 #[test]
350 fn reference_selection_field_is_a_self_titled_references_tagged_node() {
351 let sketch_plane = feature_form_fields("S")
359 .into_iter()
360 .find(|f| f.key() == "sketchPlane")
361 .expect("sketch has a sketchPlane reference_selection field");
362 assert_eq!(sketch_plane.label, "Sketch plane", "node title = field label");
363 assert_eq!(sketch_plane.group, "References", "kept as a semantic tag");
364 assert_eq!(sketch_plane.path, ["sketchPlane"], "a top-level (primary) reference");
365 match &sketch_plane.kind {
366 FieldKind::Reference { filter, multiple } => {
367 assert_eq!(filter, &["PLANE".to_string(), "FACE".to_string()]);
368 assert!(!multiple, "the sketch plane is a single reference");
369 }
370 other => panic!("sketchPlane should be a Reference, got {other:?}"),
371 }
372 }
373}