use crate::panels::toolbar_button;
use crate::workbench;
use brep_render::engine_state::EngineState;
use brep_render::features;
use eframe::egui;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Default)]
pub struct WorkbenchToolbarOutcome {
pub feature: Option<String>,
pub constraint: Option<String>,
}
struct ActionButton {
id: String,
glyph: String,
tooltip: String,
}
#[derive(Default)]
pub struct WorkbenchToolbarPanel {
hits: HashMap<String, egui::Rect>,
features: Vec<ActionButton>,
cached_workbench: Option<String>,
constraints: Vec<ActionButton>,
}
impl WorkbenchToolbarPanel {
pub fn new() -> Self {
Self::default()
}
pub fn visible(state: &EngineState) -> bool {
state.settings.show_workbench_toolbar && !state.sketch_mode() && !state.ref_select_active()
}
pub fn show(&mut self, ui: &mut egui::Ui, state: &EngineState) -> WorkbenchToolbarOutcome {
self.hits.clear();
let mut outcome = WorkbenchToolbarOutcome::default();
if !Self::visible(state) {
return outcome;
}
let active = state.settings.workbench.clone();
self.refresh_buttons(&active);
let with_constraints =
workbench::panel_visible(&active, workbench::assembly::CONSTRAINTS_PANEL_ID);
if self.features.is_empty() && !with_constraints {
return outcome;
}
egui::containers::panel::Panel::top("brep-workbench-toolbar")
.resizable(false)
.show(ui, |ui| {
ui.add_space(2.0);
ui.horizontal_wrapped(|ui| {
if !self.features.is_empty() {
caption(ui, "Features");
outcome.feature = Self::draw_group(ui, &self.features, &mut self.hits);
}
if with_constraints {
if !self.features.is_empty() {
ui.separator();
}
caption(ui, "Constraints");
outcome.constraint =
Self::draw_group(ui, &self.constraints, &mut self.hits);
}
});
ui.add_space(2.0);
});
outcome
}
fn refresh_buttons(&mut self, active: &str) {
if self.cached_workbench.as_deref() != Some(active) {
self.features = feature_buttons(active);
self.cached_workbench = Some(active.to_string());
}
if self.constraints.is_empty() {
self.constraints = constraint_buttons();
}
}
fn draw_group(
ui: &mut egui::Ui,
buttons: &[ActionButton],
hits: &mut HashMap<String, egui::Rect>,
) -> Option<String> {
let mut clicked = None;
for button in buttons {
let resp = toolbar_button::button(ui, &button.glyph, &button.tooltip);
hits.insert(format!("wbtb:{}", button.id), resp.rect);
if resp.clicked() {
clicked = button.id.split_once(':').map(|(_, payload)| payload.to_string());
}
}
clicked
}
pub fn hits_json(&self) -> String {
let map: serde_json::Map<String, Value> = self
.hits
.iter()
.map(|(k, r)| {
(
k.clone(),
serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
)
})
.collect();
Value::Object(map).to_string()
}
}
fn caption(ui: &mut egui::Ui, text: &str) {
ui.label(egui::RichText::new(text).weak().small());
}
fn feature_buttons(active: &str) -> Vec<ActionButton> {
let catalogue = features::feature_catalogue();
let mut buttons = Vec::new();
if let Some(list) = catalogue.get("features").and_then(Value::as_array) {
for feature in list {
let ty = feature.get("type").and_then(Value::as_str).unwrap_or("");
if ty.is_empty() || !workbench::includes_feature(active, ty) {
continue;
}
let name = feature
.get("longName")
.and_then(Value::as_str)
.unwrap_or(ty)
.to_string();
let glyph = match features::feature_icon(ty) {
Some(icon) => icon.to_string(),
None => feature
.get("shortName")
.and_then(Value::as_str)
.unwrap_or(ty)
.to_string(),
};
buttons.push(ActionButton {
id: format!("feature:{ty}"),
glyph,
tooltip: format!("Add {name}"),
});
}
}
buttons
}
fn constraint_buttons() -> Vec<ActionButton> {
brep_render::brep_kernel::CONSTRAINT_TYPES
.iter()
.map(|def| ActionButton {
id: format!("constraint:{}", def.type_id),
glyph: def.icon.to_string(),
tooltip: format!("Add {} constraint from the selection", def.label),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn frame(
ctx: &egui::Context,
panel: &mut WorkbenchToolbarPanel,
state: &EngineState,
events: Vec<egui::Event>,
) -> WorkbenchToolbarOutcome {
let raw = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::pos2(0.0, 0.0),
egui::vec2(1600.0, 300.0),
)),
events,
..Default::default()
};
let mut out = WorkbenchToolbarOutcome::default();
let _ = ctx.run_ui(raw, |ui| out = panel.show(ui, state));
out
}
fn keys(panel: &WorkbenchToolbarPanel) -> Vec<String> {
let mut keys: Vec<_> = panel.hits.keys().cloned().collect();
keys.sort();
keys
}
#[test]
fn modeling_lists_its_features_and_no_constraints() {
let ctx = egui::Context::default();
let state = EngineState::new();
assert!(state.settings.show_workbench_toolbar, "the strip is on by default");
let mut panel = WorkbenchToolbarPanel::new();
let out = frame(&ctx, &mut panel, &state, vec![]);
assert!(out.feature.is_none() && out.constraint.is_none(), "a passive render is not a click");
let keys = keys(&panel);
assert!(keys.contains(&"wbtb:feature:E".to_string()), "extrude offered: {keys:?}");
assert!(keys.contains(&"wbtb:feature:S".to_string()), "sketch offered");
assert!(!keys.contains(&"wbtb:feature:SM.F".to_string()), "sheet metal filtered");
assert!(!keys.contains(&"wbtb:feature:ACOMP".to_string()), "assembly component filtered");
assert!(
!keys.iter().any(|k| k.starts_with("wbtb:constraint:")),
"no constraints group in Modeling: {keys:?}"
);
}
#[test]
fn strip_matches_the_palette_filter_per_workbench() {
let ctx = egui::Context::default();
let catalogue = features::feature_catalogue();
let all_types: Vec<String> = catalogue["features"]
.as_array()
.unwrap()
.iter()
.filter_map(|f| f.get("type").and_then(Value::as_str).map(String::from))
.collect();
for wb in ["all", "modeling", "sheetMetal", "assembly"] {
let mut state = EngineState::new();
state
.apply_settings_json(&format!(r#"{{"workbench":"{wb}"}}"#))
.unwrap();
let mut panel = WorkbenchToolbarPanel::new();
frame(&ctx, &mut panel, &state, vec![]);
for ty in &all_types {
let shown = panel.hits.contains_key(&format!("wbtb:feature:{ty}"));
assert_eq!(
shown,
workbench::includes_feature(wb, ty),
"{wb}: strip and palette disagree on {ty}"
);
}
}
}
#[test]
fn assembly_and_all_show_the_constraints_group() {
let ctx = egui::Context::default();
let constraint_types: Vec<String> =
brep_render::brep_kernel::constraint_schema_catalogue()
.as_array()
.unwrap()
.iter()
.filter_map(|s| s.get("type").and_then(Value::as_str).map(String::from))
.collect();
assert!(constraint_types.contains(&"fixed".to_string()));
for wb in ["assembly", "all"] {
let mut state = EngineState::new();
state
.apply_settings_json(&format!(r#"{{"workbench":"{wb}"}}"#))
.unwrap();
let mut panel = WorkbenchToolbarPanel::new();
frame(&ctx, &mut panel, &state, vec![]);
for ty in &constraint_types {
assert!(
panel.hits.contains_key(&format!("wbtb:constraint:{ty}")),
"{wb} offers the {ty} constraint: {:?}",
keys(&panel)
);
}
assert!(panel.hits.contains_key("wbtb:feature:ACOMP"), "{wb} offers ACOMP");
}
let mut state = EngineState::new();
state.apply_settings_json(r#"{"workbench":"assembly"}"#).unwrap();
let mut panel = WorkbenchToolbarPanel::new();
frame(&ctx, &mut panel, &state, vec![]);
assert!(!panel.hits.contains_key("wbtb:feature:E"), "assembly hides Extrude");
let mut state = EngineState::new();
state.apply_settings_json(r#"{"workbench":"sheetMetal"}"#).unwrap();
let mut panel = WorkbenchToolbarPanel::new();
frame(&ctx, &mut panel, &state, vec![]);
assert!(!keys(&panel).iter().any(|k| k.starts_with("wbtb:constraint:")));
}
#[test]
fn setting_off_hides_the_strip() {
let ctx = egui::Context::default();
let mut state = EngineState::new();
state.apply_settings_json(r#"{"showWorkbenchToolbar": false}"#).unwrap();
assert!(!WorkbenchToolbarPanel::visible(&state));
let mut panel = WorkbenchToolbarPanel::new();
frame(&ctx, &mut panel, &state, vec![]);
assert!(panel.hits.is_empty(), "hidden strip publishes nothing: {:?}", keys(&panel));
let json = state.settings_json();
assert!(json.contains(r#""showWorkbenchToolbar":false"#), "{json}");
}
#[test]
fn sketch_mode_hides_the_strip() {
let ctx = egui::Context::default();
let mut state = EngineState::new();
let history = serde_json::json!({
"features": [{
"type": "S",
"inputParams": { "id": "Sk" },
"persistentData": {
"basis": { "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1] },
"sketch": {
"points": [
{ "id": 0, "x": 0.0, "y": 0.0 },
{ "id": 1, "x": 10.0, "y": 0.0 }
],
"geometries": [
{ "id": 10, "type": "line", "points": [0, 1], "construction": false }
],
"constraints": []
}
}
}]
});
state.set_history_json(&history.to_string()).expect("sketch history loads");
let mut panel = WorkbenchToolbarPanel::new();
frame(&ctx, &mut panel, &state, vec![]);
assert!(panel.hits.contains_key("wbtb:feature:E"), "visible before the sketch opens");
state.enter_sketch_mode("Sk").expect("enter sketch mode");
assert!(state.sketch_mode());
assert!(!WorkbenchToolbarPanel::visible(&state));
frame(&ctx, &mut panel, &state, vec![]);
assert!(panel.hits.is_empty(), "hidden in sketch mode: {:?}", keys(&panel));
let _ = state.exit_sketch_mode(false);
frame(&ctx, &mut panel, &state, vec![]);
assert!(panel.hits.contains_key("wbtb:feature:E"), "back after the sketch closes");
}
#[test]
fn click_surfaces_the_feature_type() {
let ctx = egui::Context::default();
let state = EngineState::new();
let mut panel = WorkbenchToolbarPanel::new();
frame(&ctx, &mut panel, &state, vec![]);
let pos = panel
.hits
.get("wbtb:feature:P.CU")
.expect("the cube button publishes a hit-rect")
.center();
frame(
&ctx,
&mut panel,
&state,
vec![
egui::Event::PointerMoved(pos),
egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: egui::Modifiers::default(),
},
],
);
let out = frame(
&ctx,
&mut panel,
&state,
vec![egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: egui::Modifiers::default(),
}],
);
assert_eq!(out.feature.as_deref(), Some("P.CU"));
assert!(out.constraint.is_none());
}
#[test]
fn constraint_click_on_a_plain_part_adds_or_refuses_loudly() {
let mut state = EngineState::new();
state
.set_history_json(&crate::app::seed_history_json())
.expect("seed history loads");
let before = state.assembly_state_value()["constraints"]
.as_array()
.map_or(0, Vec::len);
let outcome = crate::panels::context_bar::add_constraint_from_selection(&mut state, "fixed");
let after = state.assembly_state_value()["constraints"]
.as_array()
.map_or(0, Vec::len);
eprintln!("plain-part fixed constraint: before={before} after={after} outcome={outcome:?}");
match &outcome {
Ok(id) => assert_eq!(after, before + 1, "row {id} added"),
Err(error) => assert!(!error.is_empty(), "a refusal names its reason"),
}
}
#[test]
fn click_surfaces_the_constraint_type() {
let ctx = egui::Context::default();
let mut state = EngineState::new();
state.apply_settings_json(r#"{"workbench":"assembly"}"#).unwrap();
let mut panel = WorkbenchToolbarPanel::new();
frame(&ctx, &mut panel, &state, vec![]);
let pos = panel
.hits
.get("wbtb:constraint:fixed")
.expect("the fixed-constraint button publishes a hit-rect")
.center();
frame(
&ctx,
&mut panel,
&state,
vec![
egui::Event::PointerMoved(pos),
egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: egui::Modifiers::default(),
},
],
);
let out = frame(
&ctx,
&mut panel,
&state,
vec![egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: egui::Modifiers::default(),
}],
);
assert_eq!(out.constraint.as_deref(), Some("fixed"));
assert!(out.feature.is_none());
}
#[test]
fn constraint_buttons_carry_the_type_icon_as_artwork() {
let buttons = constraint_buttons();
assert_eq!(buttons.len(), 10);
for (button, def) in buttons.iter().zip(brep_render::brep_kernel::CONSTRAINT_TYPES.iter()) {
assert_eq!(button.glyph, def.icon, "{}", def.type_id);
assert!(
crate::icons::artwork(&button.glyph).is_some(),
"{}: the icon {:?} must be catalogued artwork, not a font character",
def.type_id,
button.glyph
);
assert!(button.tooltip.contains(def.label), "{}: {}", def.type_id, button.tooltip);
assert!(!button.glyph.contains(def.short_name), "{}: no short names on the strip", def.type_id);
}
assert_eq!(buttons[1].glyph, "\u{2261}", "coincident is \u{2261}, as in the sketch solver");
}
}