brep_app/panels/workbench_toolbar.rs
1//! Workbench actions toolbar — a second top strip drawn UNDER the primary
2//! toolbar, listing the ACTIVE WORKBENCH's creatable features as one square
3//! icon button each, and (in a workbench that shows the Constraints panel —
4//! Assembly, and All as its union) a second group with one button per
5//! assembly-constraint type.
6//!
7//! It is a shortcut surface over creation, nothing more: a feature button does
8//! exactly what picking that entry in the **Add new feature** palette does
9//! (`HistoryPanel::add_feature_of_type`, so ACOMP still routes to the
10//! component selector), and a constraint button does what the context bar's
11//! constraint offer does (`context_bar::add_constraint_from_selection`, seeding
12//! `elements` from the current selection). Which features appear is the SAME
13//! workbench filter the palette applies ([`workbench::includes_feature`]); the
14//! constraints group is gated on the SAME panel claim the dock and the context
15//! bar use ([`workbench::panel_visible`]). Nothing here decides anything on
16//! its own.
17//!
18//! Shown only while `settings.show_workbench_toolbar` is on (the Settings
19//! "Show workbench actions toolbar" checkbox), and NEVER in a special mode: a
20//! sketch takes over the shell with its own tool strip, and reference
21//! selection bypasses the dock the new feature's form would open in. Follows
22//! the toolbar pattern: a small state struct owning only the per-frame `hits`
23//! map (widget rects) the headed verifier reads, a `show(...)` the shell calls
24//! right after the primary toolbar so the strip lands below it, and clicks
25//! flow OUT through a returned [`WorkbenchToolbarOutcome`] — the shell
26//! dispatches, because adding a feature touches panels this strip cannot
27//! borrow.
28
29use crate::automation::hit_keys::HitKeyDoc;
30use crate::panels::toolbar_button;
31use crate::workbench;
32use brep_render::engine_state::EngineState;
33use brep_render::features;
34use eframe::egui;
35use serde_json::Value;
36use std::collections::HashMap;
37
38/// What one frame of the strip produced for the shell to act on.
39#[derive(Default)]
40pub struct WorkbenchToolbarOutcome {
41 /// A feature button click — the feature TYPE CODE to add (`"E"`, `"ACOMP"`).
42 pub feature: Option<String>,
43 /// A constraint button click — the constraint type id to add (`"fixed"`).
44 pub constraint: Option<String>,
45}
46
47/// One drawn button: its stable id, the glyph on the square, and the tooltip.
48struct ActionButton {
49 id: String,
50 glyph: String,
51 tooltip: String,
52}
53
54/// The strip's own state: the per-frame hit-rects, plus the button lists,
55/// which are derived from the kernel catalogues and so are rebuilt only when
56/// the workbench changes rather than re-walked every frame.
57#[derive(Default)]
58pub struct WorkbenchToolbarPanel {
59 hits: HashMap<String, egui::Rect>,
60 /// The feature buttons for `cached_workbench` (the catalogue filtered by
61 /// that workbench, in catalogue order).
62 features: Vec<ActionButton>,
63 cached_workbench: Option<String>,
64 /// The constraint buttons — the constraint catalogue is static, so this is
65 /// built once.
66 constraints: Vec<ActionButton>,
67}
68
69impl WorkbenchToolbarPanel {
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 /// Whether the strip is drawn this frame: the setting is on AND no special
75 /// mode owns the shell (a sketch has its own tool strip; reference
76 /// selection hides the dock the new feature's form would open in).
77 pub fn visible(state: &EngineState) -> bool {
78 state.settings.show_workbench_toolbar && !state.sketch_mode() && !state.ref_select_active()
79 }
80
81 /// Draw the strip as a top panel — called right AFTER the primary toolbar
82 /// so egui stacks it directly underneath. Draws nothing (and publishes no
83 /// hit-rects) when [`Self::visible`] is false, or when the active
84 /// workbench offers nothing to put on it (the placeholder workbenches).
85 pub fn show(&mut self, ui: &mut egui::Ui, state: &EngineState) -> WorkbenchToolbarOutcome {
86 self.hits.clear();
87 let mut outcome = WorkbenchToolbarOutcome::default();
88 if !Self::visible(state) {
89 return outcome;
90 }
91 let active = state.settings.workbench.clone();
92 self.refresh_buttons(&active);
93 let with_constraints =
94 workbench::panel_visible(&active, workbench::assembly::CONSTRAINTS_PANEL_ID);
95 if self.features.is_empty() && !with_constraints {
96 return outcome;
97 }
98 egui::containers::panel::Panel::top("brep-workbench-toolbar")
99 .resizable(false)
100 .show(ui, |ui| {
101 ui.add_space(2.0);
102 ui.horizontal_wrapped(|ui| {
103 if !self.features.is_empty() {
104 caption(ui, "Features");
105 outcome.feature = Self::draw_group(ui, &self.features, &mut self.hits);
106 }
107 if with_constraints {
108 if !self.features.is_empty() {
109 ui.separator();
110 }
111 caption(ui, "Constraints");
112 outcome.constraint =
113 Self::draw_group(ui, &self.constraints, &mut self.hits);
114 }
115 });
116 ui.add_space(2.0);
117 });
118 outcome
119 }
120
121 /// Rebuild the button lists when the workbench changed (or on first use).
122 fn refresh_buttons(&mut self, active: &str) {
123 if self.cached_workbench.as_deref() != Some(active) {
124 self.features = feature_buttons(active);
125 self.cached_workbench = Some(active.to_string());
126 }
127 if self.constraints.is_empty() {
128 self.constraints = constraint_buttons();
129 }
130 }
131
132 /// Draw one group of square buttons through the shared toolbar-button
133 /// helper, publishing each hit-rect under `wbtb:<id>`; returns the clicked
134 /// button's payload (the part of the id after the group prefix).
135 fn draw_group(
136 ui: &mut egui::Ui,
137 buttons: &[ActionButton],
138 hits: &mut HashMap<String, egui::Rect>,
139 ) -> Option<String> {
140 let mut clicked = None;
141 for button in buttons {
142 let resp = toolbar_button::button(ui, &button.glyph, &button.tooltip);
143 hits.insert(format!("wbtb:{}", button.id), resp.rect);
144 if resp.clicked() {
145 clicked = button.id.split_once(':').map(|(_, payload)| payload.to_string());
146 }
147 }
148 clicked
149 }
150
151 /// The published widget hit-rects (egui points) for the headed verifier —
152 /// `wbtb:feature:<type>` / `wbtb:constraint:<type>`; empty while hidden.
153 pub fn hits_json(&self) -> String {
154 crate::automation::hit_rects::hits_json(&self.hits)
155 }
156}
157
158/// A small, dim group caption ahead of its buttons.
159fn caption(ui: &mut egui::Ui, text: &str) {
160 ui.label(egui::RichText::new(text).weak().small());
161}
162
163/// One button per catalogue feature the workbench INCLUDES, in catalogue
164/// order — the same list, same filter, as the Add-feature palette. The glyph is
165/// the feature's icon (the catalogued artwork paints it); a feature with no icon
166/// falls back to its short name as text.
167fn feature_buttons(active: &str) -> Vec<ActionButton> {
168 let catalogue = features::feature_catalogue();
169 let mut buttons = Vec::new();
170 if let Some(list) = catalogue.get("features").and_then(Value::as_array) {
171 for feature in list {
172 let ty = feature.get("type").and_then(Value::as_str).unwrap_or("");
173 if ty.is_empty() || !workbench::includes_feature(active, ty) {
174 continue;
175 }
176 let name = feature
177 .get("longName")
178 .and_then(Value::as_str)
179 .unwrap_or(ty)
180 .to_string();
181 let glyph = match features::feature_icon(ty) {
182 Some(icon) => icon.to_string(),
183 None => feature
184 .get("shortName")
185 .and_then(Value::as_str)
186 .unwrap_or(ty)
187 .to_string(),
188 };
189 buttons.push(ActionButton {
190 id: format!("feature:{ty}"),
191 glyph,
192 tooltip: format!("Add {name}"),
193 });
194 }
195 }
196 buttons
197}
198
199/// One button per assembly-constraint type, spec §4 order (the same order as
200/// the constraint catalogue and the panel's `+` dropdown). The glyph is the
201/// type's icon (`ConstraintTypeDef::icon` — the picture the context bar's
202/// offer and the viewport chip show), drawn as catalogued artwork; the plain
203/// name rides in the tooltip.
204fn constraint_buttons() -> Vec<ActionButton> {
205 brep_render::brep_kernel::CONSTRAINT_TYPES
206 .iter()
207 .map(|def| ActionButton {
208 id: format!("constraint:{}", def.type_id),
209 glyph: def.icon.to_string(),
210 tooltip: format!("Add {} constraint from the selection", def.label),
211 })
212 .collect()
213}
214
215// BREP private tests: b9d6f42ef8a9605e
216
217/// The hit keys this panel publishes (see `automation::hit_keys`).
218pub static HIT_KEYS: &[HitKeyDoc] = &[
219 HitKeyDoc { panel: "wbtoolbar", prefix: "wbtb:feature:", meaning: "add a feature of that type (one button per feature the active workbench offers)", command: Some("feature_add") },
220 HitKeyDoc { panel: "wbtoolbar", prefix: "wbtb:constraint:", meaning: "add an assembly constraint of that type, seeded from the current selection", command: Some("assembly_add_constraint") },
221];