BREP_render 0.1.0

BREP Rust rendering engine: kernel-fed scene store + wgpu renderer (headless artifact, desktop window, and wasm canvas shells).
Documentation
//! Feature-schema access + the feature-schema → [`FormField`] mapping.
//!
//! The kernel OWNS the feature definitions (`feature_pipeline/schema.rs`; each
//! `features/<feat>.rs` declares its own `schema()` with an `inputParamsSchema`).
//! This module reaches that catalogue from native Rust (re-exported by the
//! kernel as [`brep_kernel::feature_schema_catalogue`]) and maps ONE feature's
//! parameter schema into the general [`FormField`] list the shared egui form
//! engine renders — the SAME `FormField`s the settings dialog uses.
//!
//! The map classifies each kernel param `type`:
//!   * `number`             → [`FieldKind::Scalar`]      (unbounded drag value)
//!   * `string`             → [`FieldKind::Text`]        (`id` is read-only)
//!   * `transform`          → three [`FieldKind::Vec3`]  (position/rotation/scale)
//!   * `boolean_operation`  → an [`FieldKind::Enum`] (operation) + a
//!                            [`FieldKind::Reference`] (tools) + a
//!                            [`FieldKind::Bool`] (mergeCoplanarFaces)
//!   * `reference_selection`→ [`FieldKind::Reference`]   (the #42 seam)
//!
//! REFERENCE-SELECTION field types (rendered as the disabled placeholder — the
//! seam for the next slice's real picker): the top-level `reference_selection`
//! params, and the `targets` list inside a `boolean_operation`. (`transform`'s
//! optional start `reference` is not surfaced here yet.)

use crate::style::{FieldKind, FormField};
use serde_json::Value;

/// The kernel-owned feature-schema catalogue (`{ version, features: [...] }`).
pub fn feature_catalogue() -> Value {
    brep_kernel::feature_schema_catalogue()
}

/// One feature's full schema entry, by its `type` / `shortName` (e.g. `"P.CU"`,
/// `"B"`). `None` if the type is not in the 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()
}

/// Human display name for a feature type (`longName`, else the type itself).
pub fn feature_long_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())
}

/// A feature type's SHORT name (`shortName`, e.g. `P.CU`, `S`, `E`), else the
/// type code itself. This is the BASE for a new feature's id: the engine appends
/// the part history's persistent global counter to it (`P.CU` → `P.CU7`,
/// `S` → `S8`) in [`crate::history::History::next_feature_id`].
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())
}

/// A feature type's DEFAULT `inputParams`, built from its schema: one entry per
/// `inputParamsSchema` param seeded with that param's `default_value` (missing →
/// `null`). The caller assigns the unique `id` afterwards (the schema's `id`
/// default is `null`). Additive, engine-owned, and reusable by any "add feature"
/// path so a new feature's params always track the kernel schema. Unknown types
/// yield an empty object.
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)
}

/// The boolean operation choices (a fixed kernel enum — the schema stores only a
/// default operation, not the variant set, so the known set rides here).
const BOOLEAN_OPS: &[&str] = &["NONE", "UNION", "SUBTRACT", "INTERSECT"];

/// Map a feature type's `inputParamsSchema` into the general form fields, in
/// schema (insertion) order — the kernel builds schemas with serde_json's
/// `preserve_order`, so grouped fields stay contiguous for the group headers.
pub fn feature_form_fields(feature_type: &str) -> Vec<FormField> {
    let Some(schema) = feature_schema(feature_type) else {
        return Vec::new();
    };
    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(),
                // The `id` is the feature's identity; editing it must cascade to
                // references, so it is shown read-only for now.
                kind: FieldKind::Text {
                    read_only: name == "id",
                },
            }),
            "transform" => {
                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_operation" => {
                fields.push(FormField {
                    path: vec![name.clone(), "operation".into()],
                    label: "Operation".into(),
                    group: "Boolean".into(),
                    kind: FieldKind::Enum {
                        variants: BOOLEAN_OPS.to_vec(),
                    },
                });
                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" => {
                // An action button (e.g. Edit Sketch): binds to no value. Its
                // `key` (the schema key) identifies the click to the host. The
                // caption is the schema `label` (falling back to the key).
                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);
                // `"References"` is a SEMANTIC TAG, not a rendered wrapper: the
                // history tree inlines this group (each reference renders as its
                // own self-titled node — no "References" parent), and the context
                // bar keys on it to find a feature's PRIMARY reference (a top-level
                // `reference_selection`, vs a boolean-op `targets` in `Boolean`).
                fields.push(FormField {
                    path: vec![name.clone()],
                    label: prettify(name),
                    group: "References".into(),
                    kind: FieldKind::Reference { filter, multiple },
                });
            }
            // Unmapped kernel param types (bool/vec3-array/etc. arrive as more
            // features are wired) are skipped rather than mis-rendered.
            _ => {}
        }
    }
    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 },
    }
}

/// `sizeX` → `Size X`, `rotationEuler` → `Rotation euler`, `id` → `Id`.
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_eq!(feature_long_name("P.CU"), "Primitive Cube");
    }

    #[test]
    fn short_name_is_the_schema_code_or_the_type_fallback() {
        // The base for a new feature's id: the schema shortName (incl. dotted
        // primitive codes), falling back to the type code for an unknown type.
        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");
        // id (read-only Text), sizeX/Y/Z (Scalar), transform → 3× Vec3,
        // boolean → Enum + Reference + Bool.
        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 { .. }
        ));
        // transform expands into three Vec3 sub-fields on nested paths.
        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
        );
        // boolean_operation expands: operation Enum, targets Reference, merge Bool.
        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 default_params_seed_from_schema_defaults() {
        // A primitive's defaults are COMPLETE (build-ready) straight from schema.
        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");
        // The `id` default is null — the caller assigns a unique one.
        assert_eq!(cube["id"], Value::Null);
        // A feature whose reference default is null still yields the key.
        let boolean = feature_default_params("B");
        assert!(boolean.as_object().unwrap().contains_key("targetSolid"));
        assert_eq!(boolean["boolean"]["operation"], "UNION");
        // Unknown types → empty object (no panic).
        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() {
        // The SKETCH schema's `editSketch` / `dumpSketchDiagnostics` (`type:"button"`)
        // become `FieldKind::Button` fields carrying the schema label + their key.
        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");
        let dump = fields
            .iter()
            .find(|f| f.key() == "dumpSketchDiagnostics")
            .expect("dumpSketchDiagnostics button field");
        assert!(matches!(dump.kind, FieldKind::Button { .. }));
    }

    #[test]
    fn reference_selection_field_is_a_self_titled_references_tagged_node() {
        // The history tree renders a `reference_selection` field as its OWN
        // top-level-under-the-feature node (no "References" wrapper): the node
        // title is the field's LABEL and it carries the Select button. This pins
        // the descriptor contract that the tree's inlining relies on — the field's
        // `label` is the prettified param name ("Sketch plane"), and its `group`
        // stays `"References"` as the semantic tag (tree inline marker + the
        // context bar's primary-reference discriminator).
        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:?}"),
        }
    }
}