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
//! The state registry: every JSON blob the app publishes per frame, by name.
//!
//! This replaces the per-module `publish_to_js` helpers (which wrote
//! `window.<name>` on wasm only). A publisher hands [`publish`] a name, a
//! one-line doc, and the JSON text; a typed publisher ([`publish_typed`], with
//! the `automation` feature) also records the derived schema once. Readers —
//! the automation queue's `state_get`, the `hit_rects` merge, the generated
//! docs — see the same map. On wasm every publish still mirrors to
//! `window.<name>` so the browser build stays inspectable from a devtools
//! console; that mirror is why the verify scripts keep working unchanged.
//!
//! The registry is process-global, like the `window.*` globals it replaces:
//! one app instance per process is the only configuration that exists (a host
//! owns exactly one). Publishing is gated by [`enabled`] so a plain native run
//! never pays for serialising the blobs; a host enables it, and wasm has it on
//! by default.
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, MutexGuard};

/// One published blob.
#[derive(Debug, Clone)]
pub struct Published {
    pub json: String,
    pub doc: &'static str,
    /// The derived JSON Schema when the blob was published from a typed value;
    /// `None` = "by example" (a `serde_json::json!`-built blob).
    pub schema: Option<serde_json::Value>,
}

#[derive(Debug, Default)]
pub struct Registry {
    map: BTreeMap<String, Published>,
}

static REGISTRY: Mutex<Registry> = Mutex::new(Registry { map: BTreeMap::new() });
static ENABLED: AtomicBool = AtomicBool::new(cfg!(target_arch = "wasm32"));

/// Are publishers active this frame? Off in a plain native run; a host turns
/// it on; always on for the wasm build (its globals are part of the page).
pub fn enabled() -> bool {
    ENABLED.load(Ordering::Relaxed)
}

pub fn set_enabled(on: bool) {
    ENABLED.store(on, Ordering::Relaxed);
}

/// The registry, locked. Keep the guard short.
pub fn lock() -> MutexGuard<'static, Registry> {
    REGISTRY.lock().unwrap_or_else(|p| p.into_inner())
}

/// Publish an untyped JSON string (schema by example).
pub fn publish(name: &str, doc: &'static str, json: &str) {
    mirror_to_js(name, json);
    lock().insert(name, doc, json, None);
}

/// Publish a typed value; its JSON Schema is recorded the first time.
#[cfg(feature = "automation")]
pub fn publish_typed<T: serde::Serialize + schemars::JsonSchema>(name: &str, doc: &'static str, value: &T) {
    let json = serde_json::to_string(value).unwrap_or_else(|_| "null".into());
    mirror_to_js(name, &json);
    let mut r = lock();
    let schema = if r.map.contains_key(name) { None } else { serde_json::to_value(schemars::schema_for!(T)).ok() };
    r.insert(name, doc, &json, schema);
}

impl Registry {
    fn insert(&mut self, name: &str, doc: &'static str, json: &str, schema: Option<serde_json::Value>) {
        match self.map.get_mut(name) {
            Some(p) => {
                p.json.clear();
                p.json.push_str(json);
                p.doc = doc;
                if schema.is_some() {
                    p.schema = schema;
                }
            }
            None => {
                self.map.insert(name.to_string(), Published { json: json.to_string(), doc, schema });
            }
        }
    }

    pub fn get(&self, name: &str) -> Option<&Published> {
        self.map.get(name)
    }

    pub fn names(&self) -> Vec<&str> {
        self.map.keys().map(String::as_str).collect()
    }

    pub fn iter(&self) -> impl Iterator<Item = (&str, &Published)> {
        self.map.iter().map(|(k, v)| (k.as_str(), v))
    }

    /// Every hit-rect blob merged under `panel/key`, in egui points. The panel
    /// name derives from the blob name (`__brepSceneHit` → `scene`); the
    /// historical exceptions (`__brepHit` is the history panel, `__brepToolbar`
    /// and `__brepWorkbenchToolbar` carry no `Hit` suffix) are named once in
    /// [`hit_blob_panel`].
    pub fn hit_rects(&self, prefix: Option<&str>) -> BTreeMap<String, [f32; 4]> {
        let mut out = BTreeMap::new();
        for (name, p) in &self.map {
            let Some(panel) = hit_blob_panel(name) else { continue };
            let Ok(serde_json::Value::Object(map)) = serde_json::from_str::<serde_json::Value>(&p.json) else { continue };
            for (key, rect) in map {
                let full = format!("{panel}/{key}");
                if let Some(pre) = prefix {
                    if !full.starts_with(pre) {
                        continue;
                    }
                }
                if let Some(a) = rect.as_array() {
                    if a.len() == 4 {
                        let f = |i: usize| a[i].as_f64().unwrap_or(0.0) as f32;
                        out.insert(full, [f(0), f(1), f(2), f(3)]);
                    }
                }
            }
        }
        out
    }
}

/// The panel a hit-rect blob belongs to, or `None` for a state blob.
pub fn hit_blob_panel(name: &str) -> Option<String> {
    match name {
        "__brepHit" => Some("history".into()),
        "__brepToolbar" => Some("toolbar".into()),
        "__brepWorkbenchToolbar" => Some("wbtoolbar".into()),
        _ => name
            .strip_prefix("__brep")
            .and_then(|n| n.strip_suffix("Hit"))
            .filter(|n| !n.is_empty())
            .map(|n| n.to_ascii_lowercase()),
    }
}

#[cfg(target_arch = "wasm32")]
fn mirror_to_js(name: &str, json: &str) {
    if let Some(win) = web_sys::window() {
        let _ = js_sys::Reflect::set(
            &win,
            &wasm_bindgen::JsValue::from_str(name),
            &wasm_bindgen::JsValue::from_str(json),
        );
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn mirror_to_js(_name: &str, _json: &str) {}

// BREP private tests: e0cf515307492c68