use brep_render::engine_state::EngineState;
mod commands;
pub use commands::*;
pub mod all;
pub mod assembly;
pub mod diagram;
pub mod drawing;
pub mod ecad;
pub mod modeling;
pub mod pads;
pub mod pcb;
pub mod pmi;
pub mod sheet_metal;
pub mod sketch;
pub mod symbol;
pub mod wire_harness;
pub struct FeatureInfo<'a> {
pub type_code: &'a str,
}
#[derive(Clone, Copy)]
pub struct ButtonState<'a> {
pub engine: &'a EngineState,
pub ecad: Option<&'a ecad::Editors>,
}
impl<'a> ButtonState<'a> {
pub fn of(engine: &'a EngineState) -> Self {
Self { engine, ecad: None }
}
pub fn with_ecad(engine: &'a EngineState, ecad: &'a ecad::Editors) -> Self {
Self { engine, ecad: Some(ecad) }
}
}
pub struct WorkbenchButton {
pub id: &'static str,
pub glyph: &'static str,
pub ribbon_path: &'static str,
pub size: CommandSize,
pub tooltip: &'static str,
pub when: Option<fn(&ButtonState) -> bool>,
pub pressed: Option<fn(&ButtonState) -> bool>,
pub disabled: Option<fn(&ButtonState) -> Option<&'static str>>,
pub caption: Option<fn(&ButtonState) -> String>,
pub menu: &'static [WorkbenchButton],
}
impl WorkbenchButton {
pub fn offered(&self, state: &ButtonState) -> bool {
self.when.is_none_or(|when| when(state))
&& (self.menu.is_empty() || self.menu.iter().any(|entry| entry.offered(state)))
}
pub fn is_pressed(&self, state: &ButtonState) -> bool {
self.pressed.is_some_and(|pressed| pressed(state))
}
pub fn disabled_reason(&self, state: &ButtonState) -> Option<&'static str> {
self.disabled.and_then(|disabled| disabled(state))
}
pub fn label(&self, _state: &ButtonState) -> String {
self.presentation().label().to_owned()
}
pub fn presentation(&self) -> CommandPresentation<'static> {
CommandPresentation { id: self.id, glyph: self.glyph, ribbon_path: self.ribbon_path, size: self.size }
}
pub fn detail(&self, state: &ButtonState) -> String {
self.caption.map_or_else(|| self.tooltip.to_owned(), |caption| caption(state))
}
}
pub struct Workbench {
pub id: &'static str,
pub label: &'static str,
pub glyph: &'static str,
pub includes: fn(&FeatureInfo<'_>) -> bool,
pub buttons: &'static [WorkbenchButton],
pub panels: &'static [&'static str],
}
pub const DEFAULT_WORKBENCH_ID: &str = "modeling";
pub static WORKBENCHES: &[&Workbench] = &[
&all::ALL,
&modeling::MODELING,
&sheet_metal::SHEET_METAL,
&wire_harness::WIRE_HARNESS,
&assembly::ASSEMBLY,
&pmi::PMI,
&drawing::DRAWING,
&diagram::DIAGRAM,
&pcb::PCB,
&symbol::SYMBOL,
&pads::PADS,
];
pub static UNREGISTERED: &[&Workbench] = &[];
pub fn workbench_by_id(id: &str) -> Option<&'static Workbench> {
WORKBENCHES.iter().copied().find(|w| w.id == id)
}
pub fn resolve(id: &str) -> &'static Workbench {
workbench_by_id(id).unwrap_or_else(|| {
workbench_by_id(DEFAULT_WORKBENCH_ID).expect("default workbench must be registered")
})
}
pub fn shared_buttons() -> &'static [WorkbenchButton] {
sketch::BUTTONS
}
pub static BORROWED: &[(&str, &[&WorkbenchButton])] =
&[("pcb", &[assembly::ADD_COMPONENT])];
pub fn borrowed_buttons(id: &str) -> &'static [&'static WorkbenchButton] {
BORROWED
.iter()
.find(|(borrower, _)| *borrower == id)
.map_or(&[], |(_, buttons)| *buttons)
}
pub fn workbench_buttons(id: &str) -> Vec<&'static WorkbenchButton> {
let wb = resolve(id);
let own = if wb.id == "all" {
union_in(WORKBENCHES)
} else {
wb.buttons.iter().collect::<Vec<_>>()
};
own.into_iter()
.chain(borrowed_buttons(wb.id).iter().copied())
.chain(shared_buttons())
.collect()
}
pub fn offered_buttons(id: &str, state: &ButtonState) -> Vec<&'static WorkbenchButton> {
if owned_workbenches(state.engine).iter().any(|w| w.id == id) {
return shared_buttons().iter().filter(|b| b.offered(state)).collect();
}
workbench_buttons(id).into_iter().filter(|button| button.offered(state)).collect()
}
pub fn button_by_id(id: &str) -> Option<&'static WorkbenchButton> {
button_in(WORKBENCHES, id)
}
pub fn button_prefix(wb: &Workbench) -> String {
format!("{}.", wb.id.to_ascii_lowercase())
}
fn button_in(registry: &[&'static Workbench], id: &str) -> Option<&'static WorkbenchButton> {
every_button_in(registry).find(|button| button.id == id)
}
pub fn press_refusal(id: &str, state: &ButtonState) -> Option<String> {
press_refusal_in(WORKBENCHES, id, state)
}
fn press_refusal_in(registry: &[&'static Workbench], id: &str, state: &ButtonState) -> Option<String> {
let Some(button) = button_in(registry, id) else {
return Some(format!("no workbench button `{id}` (see describe_workbenches)"));
};
if !button.offered(state) {
return Some(format!(
"the `{id}` button is not offered right now (see describe_workbenches `activeButtons`)"
));
}
if !button.menu.is_empty() {
let entries: Vec<&str> =
button.menu.iter().filter(|e| e.offered(state)).map(|e| e.id).collect();
return Some(format!("`{id}` opens a menu; press one of its entries: {}", entries.join(", ")));
}
button
.disabled_reason(state)
.map(|why| format!("the `{id}` button is disabled right now: {why}"))
}
pub const SYNONYMS: &[(&[&str], &str)] = &[
(
&[
"sketch.tool.select",
"diagram.sheet.tool.select",
"pcb.sheet.tool.select",
"pcb.board.tool.select",
"symbol.tool.select",
"pads.tool.select",
],
"select and drag, in every editor",
),
(&["sketch.tool.line", "symbol.tool.line", "pads.tool.line"], "draws a straight line"),
(&["sketch.tool.rect", "symbol.tool.rectangle", "pads.tool.rectangle"], "draws a rectangle"),
(&["sketch.tool.circle", "symbol.tool.circle", "pads.tool.circle"], "draws a circle"),
(&["pcb.sheet.wire.finish", "pcb.board.route.finish"], "ends the path being drawn"),
];
pub fn declared_workbenches() -> Vec<&'static Workbench> {
WORKBENCHES.iter().chain(UNREGISTERED).copied().collect()
}
pub(crate) fn button_by_declared_id(id: &str) -> Option<&'static WorkbenchButton> {
button_in(&declared_workbenches(), id)
}
pub fn every_button() -> impl Iterator<Item = &'static WorkbenchButton> {
every_button_in(WORKBENCHES)
}
fn every_button_in<'r>(
registry: &'r [&'static Workbench],
) -> impl Iterator<Item = &'static WorkbenchButton> + 'r {
registry
.iter()
.flat_map(|w| w.buttons.iter())
.chain(shared_buttons())
.flat_map(|button| std::iter::once(button).chain(button.menu.iter()))
}
fn union_in(registry: &[&'static Workbench]) -> Vec<&'static WorkbenchButton> {
dedupe_buttons(registry.iter().map(|w| w.buttons))
}
fn dedupe_buttons<'a>(
lists: impl Iterator<Item = &'a [WorkbenchButton]>,
) -> Vec<&'a WorkbenchButton> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for list in lists {
for button in list {
if seen.insert(button.id) {
out.push(button);
}
}
}
out
}
pub fn includes_feature(active_id: &str, type_code: &str) -> bool {
let info = FeatureInfo { type_code };
(resolve(active_id).includes)(&info)
}
pub const QUALIFY_PANEL_ID: &str = "qualify";
pub struct PanelCondition {
pub panel: &'static str,
pub when: fn(&ButtonState) -> bool,
}
pub static PANEL_CONDITIONS: &[PanelCondition] = &[
PanelCondition { panel: QUALIFY_PANEL_ID, when: declares_connection_points },
PanelCondition { panel: FAMILY_TABLE_PANEL_ID, when: is_family_document },
PanelCondition { panel: PLM_PANEL_ID, when: crate::panels::plm_host::in_plm_session },
];
pub const PLM_PANEL_ID: &str = "plm";
pub const FAMILY_TABLE_PANEL_ID: &str = "familyTable";
pub fn is_family_document(state: &ButtonState) -> bool {
crate::document_class::is_family(state.engine)
}
pub fn declares_connection_points(state: &ButtonState) -> bool {
state
.engine
.history
.ports_block()
.and_then(serde_json::Value::as_array)
.is_some_and(|groups| !groups.is_empty())
}
pub fn panel_visible(active_id: &str, panel_id: &str, state: &ButtonState) -> bool {
if !PANEL_CONDITIONS
.iter()
.filter(|condition| condition.panel == panel_id)
.all(|condition| (condition.when)(state))
{
return false;
}
let dynamic = owned_workbenches(state.engine);
let plugin = dynamic.iter().find(|w| w.id == active_id);
let claimed = WORKBENCHES.iter().any(|w| w.panels.contains(&panel_id))
|| dynamic.iter().any(|w| w.panels.iter().any(|p| p == panel_id));
if let Some(wb) = plugin {
return !claimed || wb.panels.iter().any(|p| p == panel_id);
}
if !claimed {
return true;
}
let wb = resolve(active_id);
if wb.id == "all" {
return true;
}
wb.panels.contains(&panel_id)
}
pub fn workbench_state_json(current_stored: &str) -> String {
let available: Vec<serde_json::Value> = WORKBENCHES
.iter()
.map(|w| serde_json::json!({ "id": w.id, "label": w.label }))
.collect();
serde_json::json!({
"current": resolve(current_stored).id,
"available": available,
})
.to_string()
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OwnedWorkbench {
pub id: String,
pub label: String,
#[serde(default)] pub feature_types: Vec<String>,
#[serde(default)] pub actions: Vec<String>,
#[serde(default)] pub panels: Vec<String>,
}
pub fn owned_workbenches(engine: &EngineState) -> Vec<OwnedWorkbench> {
serde_json::from_value(engine.plugin_workbenches()).unwrap_or_default()
}
pub fn resolved_id(engine: &EngineState, id: &str) -> String {
if workbench_by_id(id).is_some() || owned_workbenches(engine).iter().any(|w| w.id == id) {
id.to_owned()
} else { DEFAULT_WORKBENCH_ID.to_owned() }
}
pub fn includes_feature_scoped(engine: &EngineState, active: &str, ty: &str) -> bool {
if let Some(wb) = owned_workbenches(engine).iter().find(|w| w.id == active) {
wb.feature_types.iter().any(|f| f == ty)
} else { includes_feature(active, ty) }
}
pub fn plugin_actions(engine: &EngineState, active: &str) -> Vec<serde_json::Value> {
let workbenches = owned_workbenches(engine);
let ids = workbenches.iter().find(|w| w.id == active).map(|w| &w.actions);
engine.plugin_catalogue()["actions"].as_array().into_iter().flatten()
.filter(|a| active == "all" || ids.is_some_and(|ids| a["id"].as_str().is_some_and(|id| ids.iter().any(|i| i == id))))
.cloned().collect()
}
pub fn workbench_state_scoped(engine: &EngineState) -> serde_json::Value {
let mut state: serde_json::Value = serde_json::from_str(&workbench_state_json(&engine.settings.workbench)).unwrap();
state["saved"] = serde_json::json!(engine.settings.workbench);
state["current"] = serde_json::json!(resolved_id(engine, &engine.settings.workbench));
if let Some(available) = state["available"].as_array_mut() {
available.extend(owned_workbenches(engine).iter().map(|w| serde_json::json!({"id":w.id,"label":w.label})));
}
state
}
pub fn plugin_panels(engine: &EngineState, active: &str) -> Vec<serde_json::Value> {
let workbenches = owned_workbenches(engine);
let ids = workbenches.iter().find(|w| w.id == active).map(|w| &w.panels);
engine.plugin_catalogue()["panels"].as_array().into_iter().flatten()
.filter(|p| active == "all" || ids.is_some_and(|ids| p["id"].as_str().is_some_and(|id| ids.iter().any(|i| i == id))))
.cloned().collect()
}