BREP_app 0.4.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
//! Expressions / parameters panel — the **variable sheet** that drives feature
//! params. The engine-owned history document carries an `expressions` source
//! string (a small `name = expr;` DSL) plus a `configurator` object; the pipeline
//! evaluates it when running features, so a numeric feature field may be the
//! expression string `"boxW"` — resolved against the variables defined here.
//!
//! Following the panel pattern, this owns NO model state: it edits + reads the
//! engine (`state.expressions_json()` / `state.set_expressions()` /
//! `state.expression_variables_json()` / `state.configurator_json()`), which write
//! into the single-source-of-truth `EngineState.history` and re-run the rolled-to
//! prefix so var-referencing params update live. The panel holds only a transient
//! editor buffer (mirrored from the engine when unfocused, committed on
//! Apply / focus-loss) and the per-frame widget hit-rects the headed verifier reads.

use crate::automation::hit_keys::HitKeyDoc;
use brep_render::engine_state::EngineState;
use eframe::egui;
use serde_json::Value;
use std::collections::HashMap;

/// The expressions panel's transient UI state (the model lives in the engine).
pub struct ExpressionsPanel {
    /// The multiline editor buffer. Mirrored from the engine's `expressions`
    /// whenever the editor is UNFOCUSED (so an Open / undo / redo that changed the
    /// document reflects here), and committed BACK to the engine on Apply or when
    /// the editor loses focus.
    buf: String,
    /// Per-frame widget hit-rects, published to JS for the headed verifier (wasm).
    hits: HashMap<String, egui::Rect>,
}

impl Default for ExpressionsPanel {
    fn default() -> Self {
        Self::new()
    }
}

impl ExpressionsPanel {
    pub fn new() -> Self {
        Self {
            buf: String::new(),
            hits: HashMap::new(),
        }
    }

    /// Draw the panel under its own collapsing header (matches the sibling panels).
    pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
        self.hits.clear();

        egui::CollapsingHeader::new("Expressions / parameters")
            .id_salt("expressions-panel")
            .default_open(true)
            .show(ui, |ui| self.body(ui, state));
    }

    fn body(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
        ui.label("Variable sheet — feature params may reference these (e.g. a size field set to `boxW`).");

        // --- the editor, bound to the engine-owned `expressions` source --------
        // Syntax highlighting via egui's BUILT-IN facility
        // (`egui_extras::syntax_highlighting`). A `layouter` re-lays every
        // frame: `highlight(...)` returns a colored `LayoutJob`, and the theme
        // is read from egui memory so it tracks the light/dark visuals. Results
        // are memoized inside `highlight`, so this is cheap per-frame.
        //
        // The language is `"c"`, not `"js"`. We build egui_extras WITHOUT its
        // `syntect` feature (16 crates natively, 20 on wasm, for this one
        // call), so `highlight` runs the crate's own fallback lexer, whose
        // `Language::new` knows only c/cpp, py, rust and toml — `"js"` returns
        // None there and falls through to unstyled monospace. The sheet is
        // `name = expr;` lines, so the C lane colours exactly what matters:
        // `//` comments, strings, numbers, identifiers and punctuation.
        let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
            let theme =
                egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx(), ui.style());
            let mut job = egui_extras::syntax_highlighting::highlight(
                ui.ctx(),
                ui.style(),
                &theme,
                buf.as_str(),
                "c",
            );
            job.wrap.max_width = wrap_width;
            ui.fonts_mut(|f| f.layout_job(job))
        };
        let editor = ui.add(
            egui::TextEdit::multiline(&mut self.buf)
                .id_salt("expressions-editor")
                .code_editor()
                .desired_rows(5)
                .desired_width(f32::INFINITY)
                .hint_text("boxW = 30;\nboxH = boxW / 2;")
                .layouter(&mut layouter),
        );
        self.hits.insert("expr:editor".into(), editor.rect);

        // --- commit: Apply button OR focus-loss; re-run only when text changed --
        let apply = ui.button("Apply (re-run history)");
        self.hits.insert("expr:apply".into(), apply.rect);

        let commit = editor.lost_focus() || apply.clicked();
        if commit {
            // Re-run only on a real change (each commit re-runs the rolled-to
            // prefix); a no-op commit must not thrash the kernel.
            if self.buf != state.expressions_json() {
                let _ = state.set_expressions(&self.buf);
            }
        } else if !editor.has_focus() {
            // Not editing → mirror the engine (an Open / New / undo / redo may have
            // replaced the document out from under the buffer).
            self.buf = state.expressions_json();
        }

        // --- parsed variable list (name = defining expression) -----------------
        ui.separator();
        ui.label(egui::RichText::new("Variables").strong());
        let vars: Vec<Value> =
            serde_json::from_str(&state.expression_variables_json()).unwrap_or_default();
        if vars.is_empty() {
            ui.weak("(no variables defined)");
        } else {
            egui::Grid::new("expr-vars")
                .num_columns(2)
                .striped(true)
                .show(ui, |ui| {
                    for v in &vars {
                        let name = v.get("name").and_then(Value::as_str).unwrap_or("");
                        let expr = v.get("expr").and_then(Value::as_str).unwrap_or("");
                        ui.monospace(name);
                        ui.monospace(format!("= {expr}"));
                        ui.end_row();
                    }
                });
        }

        // --- configurator (typed named inputs) — read/display; editing deferred -
        ui.separator();
        ui.collapsing("Configurator (read-only)", |ui| {
            let cfg = state.configurator_json();
            let pretty = serde_json::from_str::<Value>(&cfg)
                .and_then(|v| serde_json::to_string_pretty(&v))
                .unwrap_or(cfg);
            ui.monospace(pretty);
            ui.weak("Typed named inputs — a deeper configurator editor is deferred.");
        });
    }

    /// The published widget hit-rects (egui points) for the headed verifier.
    pub fn hits_json(&self) -> String {
        crate::automation::hit_rects::hits_json(&self.hits)
    }
}

// BREP private tests: 218eadaab7b230b7

/// The hit keys this panel publishes (see `automation::hit_keys`).
pub static HIT_KEYS: &[HitKeyDoc] = &[
    HitKeyDoc { panel: "expr", prefix: "expr:editor", meaning: "the expressions script editor", command: None },
    HitKeyDoc { panel: "expr", prefix: "expr:apply", meaning: "apply the script and rerun", command: None },
];