pub mod all;
pub mod assembly;
pub mod modeling;
pub mod pmi;
pub mod sheet_metal;
pub mod wire_harness;
pub struct FeatureInfo<'a> {
pub type_code: &'a str,
}
pub struct WorkbenchButton {
pub id: &'static str,
pub glyph: &'static str,
pub tooltip: &'static str,
}
pub struct Workbench {
pub id: &'static str,
pub label: &'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,
];
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 workbench_buttons(id: &str) -> Vec<&'static WorkbenchButton> {
let wb = resolve(id);
if wb.id == "all" {
dedupe_buttons(WORKBENCHES.iter().map(|w| w.buttons))
} else {
wb.buttons.iter().collect()
}
}
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 fn panel_visible(active_id: &str, panel_id: &str) -> bool {
let claimed = WORKBENCHES.iter().any(|w| w.panels.contains(&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()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_order_and_labels() {
let ids: Vec<_> = WORKBENCHES.iter().map(|w| w.id).collect();
assert_eq!(
ids,
["all", "modeling", "sheetMetal", "wireHarness", "assembly", "pmi"]
);
let labels: Vec<_> = WORKBENCHES.iter().map(|w| w.label).collect();
assert_eq!(
labels,
["All", "Modeling", "Sheet Metal", "Wire harness", "Assembly", "PMI"]
);
}
#[test]
fn placeholder_workbenches_are_empty_shells() {
let info = |code| FeatureInfo { type_code: code };
for id in ["wireHarness", "pmi"] {
let wb = resolve(id);
assert_eq!(wb.id, id, "placeholder `{id}` resolves");
for code in ["S", "D", "P", "E", "SM.F", "B"] {
assert!(
!(wb.includes)(&info(code)),
"placeholder `{id}` includes no features yet ({code})"
);
}
assert!(wb.buttons.is_empty() && wb.panels.is_empty());
}
}
#[test]
fn assembly_workbench_is_defined() {
let info = |code| FeatureInfo { type_code: code };
let wb = resolve("assembly");
for code in ["S", "D", "P", "ACOMP"] {
assert!((wb.includes)(&info(code)), "assembly includes {code}");
}
for code in ["E", "B", "SM.F", "SM.TAB"] {
assert!(!(wb.includes)(&info(code)), "assembly excludes {code}");
}
assert!(!includes_feature("modeling", "ACOMP"));
assert!(!includes_feature("sheetMetal", "ACOMP"));
assert!(includes_feature("all", "ACOMP"));
assert!(includes_feature("assembly", "ACOMP"));
for panel in [assembly::BOM_PANEL_ID, assembly::CONSTRAINTS_PANEL_ID] {
assert!(panel_visible("assembly", panel), "{panel} visible in assembly");
assert!(panel_visible("all", panel), "{panel} visible in All");
assert!(!panel_visible("modeling", panel), "{panel} hidden in modeling");
assert!(!panel_visible("sheetMetal", panel), "{panel} hidden in sheet metal");
}
assert!(panel_visible("assembly", "history"));
assert!(panel_visible("assembly", "scene"));
}
#[test]
fn unknown_id_falls_back_to_default() {
assert!(workbench_by_id("bogus").is_none());
assert!(workbench_by_id("").is_none());
assert_eq!(resolve("bogus").id, DEFAULT_WORKBENCH_ID);
assert_eq!(resolve("").id, "modeling");
assert_eq!(resolve("sheetMetal").id, "sheetMetal");
}
#[test]
fn predicates_classify_by_type_code() {
let info = |code| FeatureInfo { type_code: code };
let all = resolve("all");
for code in ["SM.F", "S", "D", "P", "E"] {
assert!((all.includes)(&info(code)), "All includes {code}");
}
let m = resolve("modeling");
assert!(!(m.includes)(&info("SM.F")), "modeling excludes SM.F");
assert!(!(m.includes)(&info("SM.TAB")), "modeling excludes SM.TAB");
for code in ["S", "D", "P", "E"] {
assert!((m.includes)(&info(code)), "modeling includes {code}");
}
let s = resolve("sheetMetal");
assert!((s.includes)(&info("SM.F")), "sheet metal includes SM.F");
for code in ["S", "D", "P"] {
assert!((s.includes)(&info(code)), "sheet metal includes common {code}");
}
assert!(!(s.includes)(&info("E")), "sheet metal excludes Extrude");
}
#[test]
fn includes_feature_entry_point() {
assert!(!includes_feature("modeling", "SM.TAB"));
assert!(includes_feature("modeling", "E"));
assert!(includes_feature("modeling", "S"));
assert!(includes_feature("sheetMetal", "SM.TAB"));
assert!(!includes_feature("sheetMetal", "E"));
assert!(includes_feature("all", "E"));
assert!(includes_feature("all", "SM.TAB"));
assert!(!includes_feature("bogus", "SM.TAB"));
}
#[test]
fn all_buttons_is_the_deduped_union() {
let ids = |wb: &str| -> Vec<&str> {
workbench_buttons(wb).iter().map(|b| b.id).collect()
};
assert_eq!(ids("sheetMetal"), ["sheetmetal.flat_pattern"]);
assert_eq!(
ids("assembly"),
[
"assembly.add_component",
"assembly.step_parts_library",
"assembly.interference"
]
);
assert!(workbench_buttons("modeling").is_empty());
assert_eq!(
ids("all"),
[
"sheetmetal.flat_pattern",
"assembly.add_component",
"assembly.step_parts_library",
"assembly.interference"
]
);
}
#[test]
fn dedupe_preserves_order_and_drops_duplicate_ids() {
let a = [
WorkbenchButton { id: "x", glyph: "1", tooltip: "X" },
WorkbenchButton { id: "y", glyph: "2", tooltip: "Y" },
];
let b = [
WorkbenchButton { id: "y", glyph: "3", tooltip: "Y-dup" },
WorkbenchButton { id: "z", glyph: "4", tooltip: "Z" },
];
let out = dedupe_buttons([a.as_slice(), b.as_slice()].into_iter());
let ids: Vec<_> = out.iter().map(|button| button.id).collect();
assert_eq!(ids, ["x", "y", "z"]);
assert_eq!(out[1].glyph, "2");
}
#[test]
fn panels_default_visible_when_unclaimed() {
for wb in ["all", "modeling", "sheetMetal", "bogus"] {
assert!(panel_visible(wb, "history"));
assert!(panel_visible(wb, "scene"));
assert!(panel_visible(wb, "expressions"));
}
}
#[test]
fn state_json_reports_current_and_available() {
let json: serde_json::Value =
serde_json::from_str(&workbench_state_json("sheetMetal")).unwrap();
assert_eq!(json["current"], "sheetMetal");
let fallback: serde_json::Value =
serde_json::from_str(&workbench_state_json("bogus")).unwrap();
assert_eq!(fallback["current"], "modeling");
let available = json["available"].as_array().unwrap();
assert_eq!(available.len(), WORKBENCHES.len());
assert_eq!(available[0]["id"], "all");
assert_eq!(available[2]["label"], "Sheet Metal");
assert_eq!(available[5]["id"], "pmi");
}
}