Skip to main content

brep_app/workbench/
mod.rs

1//! Per-file WORKBENCH registry — a UI FILTER over feature-CREATION.
2//!
3//! A "workbench" trims the feature-creation UI (the "Add new feature" palette and
4//! the selection context bar) and can gate workbench-specific toolbar buttons /
5//! panels. It NEVER changes what the feature history executes, renders, or lets
6//! the user edit: a document full of sheet-metal features opens and works
7//! identically under Modeling. The ONLY things a workbench touches are the palette
8//! (see [`includes_feature`]) and the context offers.
9//!
10//! EXTENSIBILITY is the whole point: a new workbench = one new file here + one
11//! entry in [`WORKBENCHES`]. No enums to edit. Each file exposes a single
12//! `'static` [`Workbench`] that OWNS its own inclusion decision (`includes`), its
13//! extra toolbar buttons (data only), and the ids of existing panels it claims.
14//!
15//! `"All"` is special: it accepts every feature and its buttons are the DERIVED
16//! union of every other workbench's buttons ([`workbench_buttons`]) — never
17//! hand-maintained — so adding a workbench automatically grows it.
18
19pub mod all;
20pub mod assembly;
21pub mod modeling;
22pub mod pmi;
23pub mod sheet_metal;
24pub mod wire_harness;
25
26/// The minimal view of one catalogue entry a workbench predicate consumes: the
27/// feature `type` CODE (e.g. `"E"`, `"S"`, `"SM.F"`), read from the existing
28/// app-side catalogue accessor ([`brep_render::features::feature_catalogue`]).
29/// Each workbench file classifies DIRECTLY off this code (no kernel-stamped
30/// category — the kernel stays untouched). Borrows from the runtime catalogue
31/// JSON, hence the lifetime — the registry itself stays `'static` because the
32/// fn-pointer predicate is higher-ranked over the borrow.
33pub struct FeatureInfo<'a> {
34    pub type_code: &'a str,
35}
36
37/// A workbench-specific toolbar button, DATA ONLY (no egui, no behavior): the
38/// shared toolbar-button helper renders it and a click surfaces `id` out of the
39/// toolbar to the shell, which dispatches on `id`. Phase 1 ships none.
40pub struct WorkbenchButton {
41    /// Stable id the shell matches on when the button is clicked.
42    pub id: &'static str,
43    /// The single unicode glyph shown on the square button.
44    pub glyph: &'static str,
45    /// Hover tooltip / human label.
46    pub tooltip: &'static str,
47}
48
49/// One workbench: a UI filter + optional toolbar buttons + claimed panels. Fully
50/// `'static` — fn-pointer predicate and `&'static` slices, no `OnceLock`.
51pub struct Workbench {
52    /// Stable id, e.g. `"all"` / `"modeling"` / `"sheetMetal"` — the value stored
53    /// in `RenderSettings.workbench` and published to the verifier.
54    pub id: &'static str,
55    /// Human label for the dropdown, e.g. `"Sheet Metal"`.
56    pub label: &'static str,
57    /// SVG catalog key used by the workbench switcher.
58    pub glyph: &'static str,
59    /// This workbench OWNS its inclusion decision: does a catalogue entry belong
60    /// in this workbench's feature-creation UI?
61    pub includes: fn(&FeatureInfo<'_>) -> bool,
62    /// Extra toolbar buttons this workbench adds (data only). EMPTY in v1.
63    pub buttons: &'static [WorkbenchButton],
64    /// Ids of EXISTING panels this workbench CLAIMS. Claim-based, not hide-based:
65    /// a panel is visible unless it is claimed by ≥1 workbench and the active one
66    /// does not list it (see [`panel_visible`]). EMPTY in v1 (nothing claimed →
67    /// every panel visible everywhere). Phase 2 adds claims here, editing NO other
68    /// file — the same anti-rot property the button union gives.
69    pub panels: &'static [&'static str],
70}
71
72/// The fallback / default workbench id. Boot-read validation is implicit:
73/// [`resolve`] maps any unknown stored id to this, so there is no separate boot
74/// step — every consumer routes through `resolve`.
75pub const DEFAULT_WORKBENCH_ID: &str = "modeling";
76
77/// Every workbench, in DROPDOWN ORDER: All, Modeling, Sheet Metal. The dropdown
78/// iterates THIS — labels are never hardcoded. A new workbench is appended here.
79pub static WORKBENCHES: &[&Workbench] = &[
80    &all::ALL,
81    &modeling::MODELING,
82    &sheet_metal::SHEET_METAL,
83    // Placeholders — established for the dropdown, fleshed out later.
84    &wire_harness::WIRE_HARNESS,
85    &assembly::ASSEMBLY,
86    &pmi::PMI,
87];
88
89/// Pure lookup by id — `None` if there is no such workbench.
90pub fn workbench_by_id(id: &str) -> Option<&'static Workbench> {
91    WORKBENCHES.iter().copied().find(|w| w.id == id)
92}
93
94/// Resolve a (possibly stale / unknown) stored id to a live workbench, falling
95/// back to the default. This IS the boot/read validation — route ALL consumers
96/// (dropdown display, palette/offer filters, buttons, panels) through here.
97pub fn resolve(id: &str) -> &'static Workbench {
98    workbench_by_id(id).unwrap_or_else(|| {
99        workbench_by_id(DEFAULT_WORKBENCH_ID).expect("default workbench must be registered")
100    })
101}
102
103/// The active workbench's toolbar buttons. For `"all"` this is the DEDUPED UNION
104/// of every workbench's buttons (so "All" shows every icon without a
105/// hand-maintained list); for any other workbench it is that workbench's own
106/// buttons.
107pub fn workbench_buttons(id: &str) -> Vec<&'static WorkbenchButton> {
108    let wb = resolve(id);
109    if wb.id == "all" {
110        dedupe_buttons(WORKBENCHES.iter().map(|w| w.buttons))
111    } else {
112        wb.buttons.iter().collect()
113    }
114}
115
116/// Collect buttons across lists, keeping FIRST occurrence per `id` and preserving
117/// order. The union logic behind [`workbench_buttons`] for `"all"`.
118fn dedupe_buttons<'a>(
119    lists: impl Iterator<Item = &'a [WorkbenchButton]>,
120) -> Vec<&'a WorkbenchButton> {
121    let mut seen = std::collections::HashSet::new();
122    let mut out = Vec::new();
123    for list in lists {
124        for button in list {
125            if seen.insert(button.id) {
126                out.push(button);
127            }
128        }
129    }
130    out
131}
132
133/// Whether a catalogue entry belongs in workbench `active_id`'s feature-creation
134/// UI. The ONE entry point the palette + context-offer filters call: builds a
135/// [`FeatureInfo`] from the feature `type_code` and runs the resolved workbench's
136/// predicate (each workbench file owns its classification off the code).
137pub fn includes_feature(active_id: &str, type_code: &str) -> bool {
138    let info = FeatureInfo { type_code };
139    (resolve(active_id).includes)(&info)
140}
141
142/// Whether panel `panel_id` is visible in workbench `active_id`. Claim-based: a
143/// panel is visible UNLESS it is claimed by ≥1 workbench and the active workbench
144/// does not list it. `"all"` sees every panel (mirrors the button union). With
145/// v1's empty `panels` slices nothing is claimed, so every panel is visible in
146/// every workbench — the scaffolding is wired but hides nothing.
147pub fn panel_visible(active_id: &str, panel_id: &str) -> bool {
148    let claimed = WORKBENCHES.iter().any(|w| w.panels.contains(&panel_id));
149    if !claimed {
150        return true;
151    }
152    let wb = resolve(active_id);
153    if wb.id == "all" {
154        return true;
155    }
156    wb.panels.contains(&panel_id)
157}
158
159/// The workbench list as `{id, label}` JSON plus the resolved current id — the
160/// `__brepWorkbench` logical-state global the headed verifier reads to drive the
161/// dropdown and confirm the active workbench. `current` is the RESOLVED id (an
162/// unknown stored id reads back as the default).
163pub fn workbench_state_json(current_stored: &str) -> String {
164    let available: Vec<serde_json::Value> = WORKBENCHES
165        .iter()
166        .map(|w| serde_json::json!({ "id": w.id, "label": w.label }))
167        .collect();
168    serde_json::json!({
169        "current": resolve(current_stored).id,
170        "available": available,
171    })
172    .to_string()
173}
174
175// BREP private tests: 2f65fdf0861e7fcb