use brep_render::features;
use serde_json::{json, Map, Value};
pub const BOOLEAN_OPS: &[&str] = &["NONE", "UNION", "SUBTRACT", "INTERSECT"];
pub const UNMAPPED_TYPES: &[&str] = &[
"button",
"spline_points",
"file",
"textarea",
"thread_designation",
"vec3",
];
pub fn catalogue() -> Value {
features::feature_catalogue()
}
pub fn entries() -> Vec<Value> {
catalogue()
.get("features")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default()
}
pub fn entry(feature_type: &str) -> Option<Value> {
features::feature_schema(feature_type)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FeatureIdentity {
pub feature_type: String,
pub short_name: String,
pub long_name: String,
pub display_builder: bool,
}
pub fn identity(entry: &Value) -> FeatureIdentity {
let s = |k: &str| entry.get(k).and_then(Value::as_str).unwrap_or("").to_string();
FeatureIdentity {
feature_type: s("type"),
short_name: s("shortName"),
long_name: s("longName"),
display_builder: entry
.get("displayBuilder")
.and_then(Value::as_bool)
.unwrap_or(false),
}
}
pub fn to_json_schema(entry: &Value) -> Value {
let id = identity(entry);
let mut properties = Map::new();
let mut unmapped = Vec::new();
if let Some(params) = entry.get("inputParamsSchema").and_then(Value::as_object) {
for (name, spec) in params {
match param_schema(name, spec) {
Some(schema) => {
properties.insert(name.clone(), schema);
}
None => unmapped.push(json!({
"name": name,
"type": spec.get("type").cloned().unwrap_or(Value::Null),
})),
}
}
}
json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": id.long_name,
"description": format!("inputParams of the {} feature (type `{}`)", id.long_name, id.feature_type),
"type": "object",
"properties": Value::Object(properties),
"additionalProperties": false,
"x-feature": {
"type": id.feature_type,
"shortName": id.short_name,
"longName": id.long_name,
"displayBuilder": id.display_builder,
},
"x-unmapped": unmapped,
})
}
fn param_schema(name: &str, spec: &Value) -> Option<Value> {
let ty = spec.get("type").and_then(Value::as_str).unwrap_or("");
let hint = spec.get("hint").and_then(Value::as_str);
let label = spec.get("label").and_then(Value::as_str);
let mut description = String::new();
if let Some(l) = label {
description.push_str(l);
}
if let Some(h) = hint {
if !description.is_empty() {
description.push_str(" — ");
}
description.push_str(h);
}
let mut schema = match ty {
"number" => {
let mut num = Map::new();
num.insert("type".into(), json!("number"));
if let Some(min) = spec.get("min") {
num.insert("minimum".into(), min.clone());
}
if let Some(max) = spec.get("max") {
num.insert("maximum".into(), max.clone());
}
json!({
"oneOf": [
Value::Object(num),
{ "type": "string", "description": "an expression over the Expressions panel variables and `configurator.*`" }
]
})
}
"string" => {
let mut s = json!({ "type": "string" });
if name == "id" {
s["readOnly"] = json!(true);
s["description"] = json!("the feature's unique id; assigned by the engine, never edited");
}
s
}
"boolean" => json!({ "type": "boolean" }),
"options" => {
let options = spec.get("options").cloned().unwrap_or_else(|| json!([]));
json!({ "enum": options })
}
"reference_selection" => {
let filter = spec
.get("selectionFilter")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default();
let multiple = spec.get("multiple").and_then(Value::as_bool).unwrap_or(false);
let note = format!("a reference name; accepts: {filter}");
if multiple {
json!({ "type": "array", "items": { "type": "string" }, "x-selectionFilter": spec.get("selectionFilter").cloned().unwrap_or(json!([])), "description": note })
} else {
json!({ "type": ["string", "null"], "x-selectionFilter": spec.get("selectionFilter").cloned().unwrap_or(json!([])), "description": note })
}
}
"boolean_operation" => json!({
"type": "object",
"properties": {
"operation": { "enum": BOOLEAN_OPS },
"targets": { "type": "array", "items": { "type": "string" }, "description": "tool solids (SOLID references)" },
"mergeCoplanarFaces": { "type": "boolean" }
},
"additionalProperties": false
}),
"transform" => {
let default = spec.get("default_value");
let rigid = default
.map(|d| d.get("translate").is_some() || d.get("rotateEulerDeg").is_some())
.unwrap_or(false);
let vec3 = json!({ "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 });
if rigid {
json!({
"type": "object",
"properties": { "translate": vec3, "rotateEulerDeg": vec3 },
"additionalProperties": false
})
} else {
json!({
"type": "object",
"properties": { "position": vec3, "rotationEuler": vec3, "scale": vec3 },
"additionalProperties": false
})
}
}
_ => return None,
};
if !description.is_empty() && schema.get("description").is_none() {
schema["description"] = json!(description);
}
if let Some(default) = spec.get("default_value") {
schema["default"] = default.clone();
}
schema["x-paramType"] = json!(ty);
Some(schema)
}
pub fn defaults(feature_type: &str) -> Value {
features::feature_default_params(feature_type)
}
pub fn carries_sketch(feature_type: &str) -> bool {
matches!(feature_type, "S" | "SKETCH")
}
pub fn sketch_persistent_schema() -> Value {
json!({
"type": "object",
"description": "A sketch feature's persistentData. `sketch` holds the profile geometry; `basis` gives the plane when `inputParams.sketchPlane` is null.",
"properties": {
"basis": {
"type": "object",
"description": "The sketch plane's frame in world space, used when `inputParams.sketchPlane` names nothing. Sketch coordinates are (u, v) in this frame: `origin + u*x + v*y`.",
"properties": {
"origin": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 },
"x": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 },
"y": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 },
"z": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 }
}
},
"sketch": {
"type": "object",
"description": "The profile. Geometry references POINT IDS, never inline coordinates, so two segments meet exactly when they name the same point id — which is also what makes a loop closed. An open chain is a path (a sweep spine), not a face: a solid needs a closed loop.",
"properties": {
"points": {
"type": "array",
"description": "The sketch's points, in plane-local (u, v) millimetres.",
"items": {
"type": "object",
"properties": {
"id": { "description": "Stable id, referenced by `geometries[].points`. A number in practice; a string is legal." },
"x": { "type": "number", "description": "u, in mm" },
"y": { "type": "number", "description": "v, in mm" },
"fixed": { "type": "boolean", "description": "Pinned: the solver never moves it. True is the right default for authored geometry." },
"construction": { "type": "boolean" },
"externalReference": { "type": "boolean", "description": "Adopted from a picked edge endpoint." }
},
"required": ["id", "x", "y"]
}
},
"geometries": {
"type": "array",
"description": "The segments, each naming point ids: line = [start, end]; arc = [centre, start, end] (counter-clockwise start→end); circle = [centre, radiusPoint]; ellipse = [centre, majorEnd, minorEnd]; bezier = [p0, p1, p2, p3, …] (every three ids a further cubic span).",
"items": {
"type": "object",
"properties": {
"id": { "description": "Stable id. A revolve axis names a sketch line as `{sketchFeatureId}:G{id}`." },
"type": { "type": "string", "enum": ["line", "arc", "circle", "ellipse", "bezier"] },
"points": { "type": "array", "description": "Point ids, in the order this geometry type defines." },
"construction": { "type": "boolean", "description": "Constrains but never models: excluded from profiles, drawn dashed." }
},
"required": ["id", "type", "points"]
}
},
"constraints": {
"type": "array",
"description": "Solver constraints. `[]` is valid and is what authored-by-coordinate geometry uses — every point `fixed: true` needs no solving.",
"items": { "type": "object" }
}
},
"required": ["points", "geometries"]
}
}
})
}
pub fn sketch_example() -> Value {
json!({
"basis": { "origin": [0.0, 0.0, 0.0], "x": [1.0, 0.0, 0.0], "y": [0.0, 1.0, 0.0], "z": [0.0, 0.0, 1.0] },
"sketch": {
"points": [
{ "id": 1, "x": 0.0, "y": 0.0, "fixed": true, "construction": false, "externalReference": false },
{ "id": 2, "x": 10.0, "y": 0.0, "fixed": true, "construction": false, "externalReference": false },
{ "id": 3, "x": 10.0, "y": 10.0, "fixed": true, "construction": false, "externalReference": false },
{ "id": 4, "x": 0.0, "y": 10.0, "fixed": true, "construction": false, "externalReference": false }
],
"geometries": [
{ "id": 10, "type": "line", "points": [1, 2], "construction": false },
{ "id": 11, "type": "line", "points": [2, 3], "construction": false },
{ "id": 12, "type": "line", "points": [3, 4], "construction": false },
{ "id": 13, "type": "line", "points": [4, 1], "construction": false }
],
"constraints": []
},
"dimOffsets": {},
"externalRefs": []
})
}
pub fn all() -> Value {
let cat = catalogue();
let features: Vec<Value> = entries()
.iter()
.map(|e| {
let id = identity(e);
let mut entry = json!({
"type": id.feature_type,
"shortName": id.short_name,
"longName": id.long_name,
"displayBuilder": id.display_builder,
"schema": to_json_schema(e),
"defaults": defaults(&id.feature_type),
});
if carries_sketch(&id.feature_type) {
entry["persistentData"] = sketch_persistent_schema();
entry["persistentDataExample"] = sketch_example();
}
entry
})
.collect();
json!({
"version": cat.get("version").cloned().unwrap_or(Value::Null),
"features": features,
})
}