use crate::style::{FieldKind, FormField};
use serde_json::Value;
pub fn feature_catalogue() -> Value {
brep_kernel::feature_schema_catalogue()
}
pub fn feature_schema(feature_type: &str) -> Option<Value> {
feature_catalogue()
.get("features")?
.as_array()?
.iter()
.find(|f| {
f.get("type").and_then(Value::as_str) == Some(feature_type)
|| f.get("shortName").and_then(Value::as_str) == Some(feature_type)
})
.cloned()
}
pub fn feature_long_name(feature_type: &str) -> String {
let name = feature_plain_name(feature_type);
match feature_icon(feature_type) {
Some(glyph) => format!("{glyph} {name}"),
None => name,
}
}
pub fn feature_plain_name(feature_type: &str) -> String {
feature_schema(feature_type)
.and_then(|f| f.get("longName").and_then(Value::as_str).map(String::from))
.unwrap_or_else(|| feature_type.to_string())
}
pub fn feature_short_name(feature_type: &str) -> String {
feature_schema(feature_type)
.and_then(|f| f.get("shortName").and_then(Value::as_str).map(String::from))
.unwrap_or_else(|| feature_type.to_string())
}
pub fn feature_icon(kind: &str) -> Option<char> {
let cp: u32 = match kind.trim().to_ascii_uppercase().as_str() {
"D" | "DATUM" | "DATIUM" => 0xE030,
"P" | "PLANE" => 0xE031,
"P.CU" | "CUBE" => 0xE032,
"P.CY" | "CYLINDER" => 0xE033,
"P.CO" | "CONE" => 0xE034,
"P.S" | "SPHERE" => 0xE035,
"P.T" | "TORUS" => 0xE036,
"P.PY" | "PYRAMID" => 0xE037,
"IMPORT3D" => 0xE038,
"S" | "SKETCH" => 0xE039,
"SP" | "SPLINE" => 0xE03A,
"PORT" => 0xE03B,
"HX" | "HELIX" => 0xE03C,
"E" | "EXTRUDE" => 0xE03D,
"B" | "BOOLEAN" => 0xE03E,
"F" | "FILLET" => 0xE03F,
"CH" | "CHAMFER" => 0xE040,
"O.S" | "OFFSET SHELL" | "OFFSETSHELL" => 0xE041,
"O.F" | "OFFSET FACE" | "OFFSETFACE" => 0xE042,
"PF" | "PUSHFACE" | "PUSH FACE" => 0xE043,
"DF" | "DELETE FACE" | "DELETEFACE" => 0xE044,
"THK" | "THICKEN" => 0xE045,
"SM.TAB" => 0xE046,
"SM.CF" => 0xE047,
"SM.F" => 0xE048,
"SM.HEM" => 0xE049,
"SM.FILLET" | "SM.CFIL" => 0xE04A,
"SM.CHAMFER" | "SM.CCHM" => 0xE04B,
"SM.CUTOUT" => 0xE04C,
"LOFT" => 0xE04D,
"M" | "MIRROR" => 0xE04E,
"SPL" | "SPLIT" => 0xE04F,
"R" | "REVOLVE" => 0xE050,
"RIB" => 0xE051,
"SW" | "SWEEP" => 0xE052,
"SWP" | "PATH SWEEP" | "PATHSWEEP" => 0xE053,
"H" | "HOLE" => 0xE054,
"TU" | "TUBE" => 0xE055,
"XFORM" | "TRANSFORM" => 0xE056,
"PATTERN" => 0xE057,
"ACOMP" | "ASSEMBLY COMPONENT" => 0xE058,
"SM.UNFOLD" => 0xE059,
_ => return None,
};
char::from_u32(cp)
}
pub fn feature_default_params(feature_type: &str) -> Value {
let mut params = serde_json::Map::new();
if let Some(props) = feature_schema(feature_type)
.as_ref()
.and_then(|s| s.get("inputParamsSchema"))
.and_then(Value::as_object)
{
for (name, spec) in props {
let default = spec.get("default_value").cloned().unwrap_or(Value::Null);
params.insert(name.clone(), default);
}
}
Value::Object(params)
}
const BOOLEAN_OPS: &[&str] = &["NONE", "UNION", "SUBTRACT", "INTERSECT"];
pub fn feature_form_fields(feature_type: &str) -> Vec<FormField> {
let Some(schema) = feature_schema(feature_type) else {
return Vec::new();
};
form_fields_from_schema(&schema)
}
pub fn form_fields_from_schema(schema: &Value) -> Vec<FormField> {
let Some(params) = schema
.get("inputParamsSchema")
.and_then(Value::as_object)
else {
return Vec::new();
};
let mut fields = Vec::new();
for (name, spec) in params {
let ty = spec.get("type").and_then(Value::as_str).unwrap_or("");
match ty {
"number" => fields.push(FormField {
path: vec![name.clone()],
label: prettify(name),
group: "Parameters".into(),
kind: FieldKind::Scalar { step: 0.5 },
}),
"string" => fields.push(FormField {
path: vec![name.clone()],
label: prettify(name),
group: "Parameters".into(),
kind: FieldKind::Text {
read_only: name == "id",
},
}),
"transform" => {
let is_rigid_pose = spec
.get("default_value")
.map(|d| d.get("translate").is_some() || d.get("rotateEulerDeg").is_some())
.unwrap_or(false);
if is_rigid_pose {
fields.push(vec3_field(name, "translate", "Translate", 0.5));
fields.push(vec3_field(name, "rotateEulerDeg", "Rotation (deg)", 1.0));
} else {
fields.push(vec3_field(name, "position", "Position", 0.5));
fields.push(vec3_field(name, "rotationEuler", "Rotation (deg)", 1.0));
fields.push(vec3_field(name, "scale", "Scale", 0.1));
}
}
"boolean" => {
fields.push(FormField {
path: vec![name.clone()],
label: field_label(spec, name),
group: "Parameters".into(),
kind: FieldKind::Bool,
});
}
"boolean_operation" => {
fields.push(FormField {
path: vec![name.clone(), "operation".into()],
label: "Operation".into(),
group: "Boolean".into(),
kind: FieldKind::Enum {
variants: BOOLEAN_OPS.iter().map(|s| s.to_string()).collect(),
},
});
fields.push(FormField {
path: vec![name.clone(), "targets".into()],
label: "Tool solids".into(),
group: "Boolean".into(),
kind: FieldKind::Reference {
filter: vec!["SOLID".into()],
multiple: true,
},
});
fields.push(FormField {
path: vec![name.clone(), "mergeCoplanarFaces".into()],
label: "Merge coplanar faces".into(),
group: "Boolean".into(),
kind: FieldKind::Bool,
});
}
"button" => {
let label = spec
.get("label")
.and_then(Value::as_str)
.map(String::from)
.unwrap_or_else(|| prettify(name));
fields.push(FormField {
path: vec![name.clone()],
label: label.clone(),
group: "Parameters".into(),
kind: FieldKind::Button { label },
});
}
"reference_selection" => {
let filter = spec
.get("selectionFilter")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let multiple = spec
.get("multiple")
.and_then(Value::as_bool)
.unwrap_or(false);
fields.push(FormField {
path: vec![name.clone()],
label: prettify(name),
group: "References".into(),
kind: FieldKind::Reference { filter, multiple },
});
}
"options" => {
let variants = spec
.get("options")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
fields.push(FormField {
path: vec![name.clone()],
label: field_label(spec, name),
group: "Parameters".into(),
kind: FieldKind::Enum { variants },
});
}
_ => {}
}
}
fields
}
fn vec3_field(param: &str, sub: &str, label: &str, step: f64) -> FormField {
FormField {
path: vec![param.to_string(), sub.to_string()],
label: label.to_string(),
group: "Transform".into(),
kind: FieldKind::Vec3 { step },
}
}
fn field_label(spec: &Value, name: &str) -> String {
spec.get("label")
.and_then(Value::as_str)
.map(String::from)
.unwrap_or_else(|| prettify(name))
}
fn prettify(key: &str) -> String {
let mut out = String::new();
for (i, ch) in key.chars().enumerate() {
if i == 0 {
out.extend(ch.to_uppercase());
} else if ch.is_ascii_uppercase() {
out.push(' ');
out.extend(ch.to_lowercase());
} else {
out.push(ch);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalogue_is_reachable_from_native_rust() {
let cat = feature_catalogue();
assert!(cat.get("features").and_then(Value::as_array).is_some());
assert!(feature_schema("P.CU").is_some());
assert!(feature_schema("P.CY").is_some());
assert!(feature_schema("B").is_some());
assert!(feature_long_name("P.CU").ends_with("Primitive Cube"));
assert!(feature_long_name("P.CU").starts_with(feature_icon("P.CU").unwrap()));
}
#[test]
fn every_catalogue_feature_type_has_an_icon() {
let cat = feature_catalogue();
for feature in cat["features"].as_array().expect("features") {
let ty = feature["type"].as_str().expect("type");
assert!(
feature_icon(ty).is_some(),
"feature type {ty} has no icon (add one under BREP_app/assets/glyphs/ + features::feature_icon)"
);
}
assert_eq!(feature_icon("CHAMFER"), feature_icon("CH"));
assert_eq!(feature_icon("DATIUM"), feature_icon("D"));
assert_eq!(feature_icon("push face"), feature_icon("PF"));
assert!(feature_icon("NOPE").is_none());
}
#[test]
fn short_name_is_the_schema_code_or_the_type_fallback() {
assert_eq!(feature_short_name("P.CU"), "P.CU");
assert_eq!(feature_short_name("P.S"), "P.S");
assert_eq!(feature_short_name("S"), "S");
assert_eq!(feature_short_name("E"), "E");
assert_eq!(feature_short_name("NOPE"), "NOPE");
}
#[test]
fn cube_form_fields_map_types_correctly() {
let fields = feature_form_fields("P.CU");
let by_key = |k: &str| fields.iter().find(|f| f.key() == k).cloned();
assert!(matches!(
by_key("id").unwrap().kind,
FieldKind::Text { read_only: true }
));
assert!(matches!(
by_key("sizeX").unwrap().kind,
FieldKind::Scalar { .. }
));
let position = fields
.iter()
.find(|f| f.path == ["transform", "position"])
.expect("transform.position vec3");
assert!(matches!(position.kind, FieldKind::Vec3 { .. }));
assert_eq!(
fields
.iter()
.filter(|f| matches!(f.kind, FieldKind::Vec3 { .. }))
.count(),
3
);
let op = fields
.iter()
.find(|f| f.path == ["boolean", "operation"])
.expect("boolean.operation");
assert!(matches!(op.kind, FieldKind::Enum { .. }));
let targets = fields
.iter()
.find(|f| f.path == ["boolean", "targets"])
.expect("boolean.targets");
assert!(matches!(targets.kind, FieldKind::Reference { multiple: true, .. }));
}
#[test]
fn flange_options_and_boolean_fields_render() {
let fields = feature_form_fields("SM.F");
let by_key = |k: &str| fields.iter().find(|f| f.key() == k).cloned();
let length_ref = by_key("flangeLengthReference").expect("flangeLengthReference renders");
assert_eq!(length_ref.label, "Length reference");
if let FieldKind::Enum { variants } = &length_ref.kind {
assert!(variants.iter().any(|v| v.as_str() == "Inner Virtual Sharp"));
assert!(variants.iter().any(|v| v.as_str() == "Outer Virtual Sharp"));
assert!(variants.iter().any(|v| v.as_str() == "Tangent to Bend"));
} else {
panic!("flangeLengthReference should render as an Enum dropdown");
}
let inset = by_key("inset").expect("inset (Flange position) renders");
assert_eq!(inset.label, "Flange position");
assert!(matches!(inset.kind, FieldKind::Enum { .. }));
let reverse = by_key("useOppositeCenterline").expect("useOppositeCenterline renders");
assert_eq!(reverse.label, "Reverse direction");
assert!(matches!(reverse.kind, FieldKind::Bool));
}
#[test]
fn default_params_seed_from_schema_defaults() {
let cube = feature_default_params("P.CU");
assert_eq!(cube["sizeX"], 10.0);
assert_eq!(cube["sizeY"], 10.0);
assert_eq!(cube["transform"]["scale"], serde_json::json!([1, 1, 1]));
assert_eq!(cube["boolean"]["operation"], "NONE");
assert_eq!(cube["id"], Value::Null);
let boolean = feature_default_params("B");
assert!(boolean.as_object().unwrap().contains_key("targetSolid"));
assert_eq!(boolean["boolean"]["operation"], "UNION");
assert_eq!(feature_default_params("NOPE"), serde_json::json!({}));
}
#[test]
fn boolean_feature_target_is_a_reference_field() {
let fields = feature_form_fields("B");
let target = fields
.iter()
.find(|f| f.key() == "targetSolid")
.expect("targetSolid field");
match &target.kind {
FieldKind::Reference { filter, multiple } => {
assert_eq!(filter, &["SOLID".to_string()]);
assert!(!multiple);
}
other => panic!("targetSolid should be a Reference, got {other:?}"),
}
}
#[test]
fn sketch_feature_maps_button_fields() {
let fields = feature_form_fields("S");
let edit = fields
.iter()
.find(|f| f.key() == "editSketch")
.expect("editSketch button field");
assert!(
matches!(&edit.kind, FieldKind::Button { label } if label == "Edit Sketch"),
"editSketch should be Button('Edit Sketch'), got {:?}",
edit.kind
);
assert_eq!(edit.path, ["editSketch"], "button binds to its own key");
assert!(
fields.iter().all(|f| f.key() != "dumpSketchDiagnostics"),
"the dead Dump Diagnostics button must not come back"
);
}
#[test]
fn acomp_form_fields_map_rigid_pose_and_is_fixed() {
let fields = feature_form_fields("ACOMP");
let translate = fields
.iter()
.find(|f| f.path == ["transform", "translate"])
.expect("transform.translate vec3");
assert!(matches!(translate.kind, FieldKind::Vec3 { .. }));
let rotate = fields
.iter()
.find(|f| f.path == ["transform", "rotateEulerDeg"])
.expect("transform.rotateEulerDeg vec3");
assert!(matches!(rotate.kind, FieldKind::Vec3 { .. }));
assert!(
!fields.iter().any(|f| f.path == ["transform", "scale"]),
"a rigid pose has no scale row"
);
let fixed = fields
.iter()
.find(|f| f.key() == "isFixed")
.expect("isFixed checkbox");
assert!(matches!(fixed.kind, FieldKind::Bool));
assert_eq!(fixed.label, "Fixed", "schema label wins over the prettified key");
let cube = feature_form_fields("P.CU");
assert!(cube.iter().any(|f| f.path == ["transform", "scale"]));
}
#[test]
fn constraint_schemas_map_through_the_shared_field_engine() {
let catalogue = brep_kernel::constraint_schema_catalogue();
let distance = catalogue
.as_array()
.unwrap()
.iter()
.find(|s| s.get("type").and_then(Value::as_str) == Some("distance"))
.expect("distance schema in the catalogue");
let fields = form_fields_from_schema(distance);
let elements = fields
.iter()
.find(|f| f.key() == "elements")
.expect("elements reference field");
match &elements.kind {
FieldKind::Reference { filter, multiple } => {
assert_eq!(filter, &["FACE".to_string(), "VERTEX".into(), "EDGE".into()]);
assert!(multiple, "two-element constraints take a list");
}
other => panic!("elements should be a Reference, got {other:?}"),
}
assert!(matches!(
fields.iter().find(|f| f.key() == "distance").unwrap().kind,
FieldKind::Scalar { .. }
));
assert!(matches!(
fields.iter().find(|f| f.key() == "opposeNormals").unwrap().kind,
FieldKind::Bool
));
assert!(matches!(
fields.iter().find(|f| f.key() == "id").unwrap().kind,
FieldKind::Text { read_only: true }
));
}
#[test]
fn reference_selection_field_is_a_self_titled_references_tagged_node() {
let sketch_plane = feature_form_fields("S")
.into_iter()
.find(|f| f.key() == "sketchPlane")
.expect("sketch has a sketchPlane reference_selection field");
assert_eq!(sketch_plane.label, "Sketch plane", "node title = field label");
assert_eq!(sketch_plane.group, "References", "kept as a semantic tag");
assert_eq!(sketch_plane.path, ["sketchPlane"], "a top-level (primary) reference");
match &sketch_plane.kind {
FieldKind::Reference { filter, multiple } => {
assert_eq!(filter, &["PLANE".to_string(), "FACE".to_string()]);
assert!(!multiple, "the sketch plane is a single reference");
}
other => panic!("sketchPlane should be a Reference, got {other:?}"),
}
}
}