use crate::automation::hit_keys::HitKeyDoc;
use super::action_rail::{action_rail, ActionItem};
use super::component_actions::{run_component_action, ComponentAction, ComponentActionRequest};
use crate::form;
use brep_render::brep_kernel::{self, SelectionProbe};
use brep_render::engine_state::EngineState;
use brep_render::features;
use brep_render::style::FieldKind;
use eframe::egui;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
pub type FocusRequest = Option<String>;
#[derive(Default)]
pub struct ContextOutcome {
pub focus: FocusRequest,
pub info_targets: Vec<String>,
pub component: Option<ComponentActionRequest>,
pub pmi_added: bool,
}
#[derive(Default)]
pub struct ContextBarPanel {
hits: HashMap<String, egui::Rect>,
shown_actions: Vec<String>,
shown_features: Vec<String>,
shown_constraints: Vec<String>,
shown_pmi: Vec<String>,
shown_component_actions: Vec<String>,
shown_component_target: Option<String>,
}
impl ContextBarPanel {
pub fn new() -> Self {
Self::default()
}
pub fn card(&mut self, ui: &mut egui::Ui, state: &mut EngineState) -> ContextOutcome {
self.hits.clear();
self.shown_actions.clear();
self.shown_features.clear();
self.shown_constraints.clear();
self.shown_pmi.clear();
self.shown_component_actions.clear();
self.shown_component_target = None;
let has_geometry = state.has_selection();
let constraint_target = state.selected_constraint().filter(|_| {
crate::workbench::panel_visible(
&state.settings.workbench,
crate::workbench::assembly::CONSTRAINTS_PANEL_ID,
)
});
if (!has_geometry && constraint_target.is_none())
|| state.ref_select_active()
|| state.sketch_mode()
{
return ContextOutcome::default();
}
let sel = Selection::read(state);
let comp = component_selection(&sel, state);
let probe = selection_probe(&sel, &comp, all_on_sheet_metal(&sel, state));
let offers = if comp.suppress_features() {
Vec::new()
} else {
feature_offers(&probe, &sel, &state.settings.workbench)
};
let constraint_types = constraint_offers(&probe, &state.settings.workbench);
let pmi_types = pmi_offers(&probe, &state.settings.workbench, state.pmi_active_view().is_some());
let component_target = component_action_target(&comp, &state.settings.workbench).map(|id| {
let fixed = state.component_info(id).map(|info| info.fixed).unwrap_or(false);
(id.to_string(), fixed)
});
let mut items = vec![ActionItem::new(
"action:clear",
"\u{2716} Clear",
"Clear the selection",
)];
self.shown_actions.push("clear".into());
if has_geometry {
items.push(ActionItem::new("action:hide", "\u{1f441} Hide", "Hide/Show selection"));
items.push(ActionItem::new(
"action:info",
"\u{1f575} Info",
"Open a pinned Info window per selected entity",
));
self.shown_actions.push("hide".into());
self.shown_actions.push("info".into());
}
if let Some(cid) = &constraint_target {
items.push(ActionItem::new(
"action:delete-constraint",
"\u{2715} Delete constraint",
format!("Delete constraint {cid}"),
));
self.shown_actions.push("delete-constraint".into());
}
if sel.owning_feature.is_some() {
items.push(ActionItem::new(
"action:edit-owning",
"Edit owning feature",
"Roll to and edit the feature that created this",
));
self.shown_actions.push("edit-owning".into());
}
if let Some((target, fixed)) = &component_target {
for action in ComponentAction::ALL {
items.push(ActionItem::new(
format!("component:{}", action.id()),
action.label(*fixed),
action.tooltip(),
));
self.shown_component_actions.push(action.id().to_string());
}
self.shown_component_target = Some(target.clone());
}
for def in &constraint_types {
items.push(ActionItem::new(
format!("constraint:{}", def.type_id),
def.long_name,
format!("Add a {} constraint from the selection", def.label),
));
self.shown_constraints.push(def.type_id.to_string());
}
for offer in &offers {
items.push(ActionItem::new(
format!("feature:{}", offer.type_code),
offer.label.clone(),
format!("Create {} from the selection", offer.label),
));
self.shown_features.push(offer.type_code.clone());
}
for def in &pmi_types {
items.push(ActionItem::new(
format!("pmi:{}", def.type_id),
def.long_name,
format!("Add a {} to the active PMI view from the selection", def.label),
));
self.shown_pmi.push(def.type_id.to_string());
}
let summary = match (&constraint_target, has_geometry) {
(Some(cid), false) => format!("Selected: constraint {cid}"),
_ => sel.summary(),
};
let clicked = egui::Frame::popup(ui.style())
.show(ui, |ui| {
action_rail(
ui,
Some("Selection actions"),
Some(&summary),
&items,
&mut self.hits,
)
})
.inner;
let mut outcome = ContextOutcome::default();
match clicked.as_deref() {
Some("action:clear") => {
state.clear_selection();
}
Some("action:delete-constraint") => {
if let Some(cid) = &constraint_target {
let _ = state.assembly_remove_constraint(cid);
state.constraint_deselect();
}
}
Some("action:hide") => {
state.hide_selected();
}
Some("action:info") => {
outcome.info_targets = sel.all_names();
}
Some("action:edit-owning") => {
if let Some(fid) = sel.owning_feature.clone() {
if let Some(index) = feature_index(state, &fid) {
state.roll_to(index);
}
outcome.focus = Some(fid);
}
}
Some(key) if key.starts_with("component:") => {
if let Some((target, _)) = &component_target {
if let Some(action) = ComponentAction::from_id(&key["component:".len()..]) {
outcome.component = run_component_action(state, action, target);
}
}
}
Some(key) if key.starts_with("constraint:") => {
let type_id = &key["constraint:".len()..];
if constraint_types.iter().any(|def| def.type_id == type_id) {
if let Err(error) = add_constraint_from_selection(state, type_id) {
state.push_notice(format!("Add constraint: {error}"));
}
}
}
Some(key) if key.starts_with("feature:") => {
let code = &key["feature:".len()..];
if let Some(offer) = offers.iter().find(|o| o.type_code == code) {
outcome.focus = create_feature_from_selection(state, offer, &sel);
}
}
Some(key) if key.starts_with("pmi:") => {
let type_id = &key["pmi:".len()..];
if pmi_types.iter().any(|def| def.type_id == type_id) {
match add_pmi_from_selection(state, type_id) {
Ok(_) => outcome.pmi_added = true,
Err(error) => state.push_notice(format!("Add PMI: {error}")),
}
}
}
_ => {}
}
outcome
}
pub fn hits_json(&self) -> String {
crate::automation::hit_rects::hits_json(&self.hits)
}
pub fn state_json(&self) -> String {
serde_json::json!({
"shown": !self.hits.is_empty(),
"actions": self.shown_actions,
"features": self.shown_features,
"constraints": self.shown_constraints,
"pmi": self.shown_pmi,
"componentActions": self.shown_component_actions,
"componentTarget": self.shown_component_target,
})
.to_string()
}
}
struct ComponentSelection {
ids: Vec<String>,
all_component: bool,
solids_only: bool,
}
impl ComponentSelection {
fn suppress_features(&self) -> bool {
self.all_component && !self.ids.is_empty()
}
fn sole_target(&self) -> Option<&str> {
(self.suppress_features() && self.solids_only && self.ids.len() == 1)
.then(|| self.ids[0].as_str())
}
}
fn component_selection(sel: &Selection, state: &EngineState) -> ComponentSelection {
let mut ids: Vec<String> = Vec::new();
let mut all = true;
let mut any = false;
for name in sel.all_names() {
any = true;
match state.component_of_solid(&name) {
Some(id) => {
if !ids.contains(&id) {
ids.push(id);
}
}
None => all = false,
}
}
if sel.vertices > 0 {
all = false;
}
ComponentSelection {
ids,
all_component: all && any,
solids_only: !sel.solids.is_empty()
&& sel.sketches.is_empty()
&& sel.faces.is_empty()
&& sel.edges.is_empty()
&& sel.vertices == 0,
}
}
struct Selection {
solids: Vec<String>,
sketches: Vec<String>,
faces: Vec<String>,
edges: Vec<String>,
planes: Vec<String>,
vertices: usize,
owning_feature: Option<String>,
}
impl Selection {
fn read(state: &EngineState) -> Self {
let v: Value = serde_json::from_str(&state.selection_json()).unwrap_or(Value::Null);
let names = |key: &str| -> Vec<String> {
v[key]
.as_array()
.map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
.unwrap_or_default()
};
let sketch_ids: std::collections::HashSet<String> = state
.committed_sketches()
.into_iter()
.map(|(id, _visible)| id)
.collect();
let (sketches, solids): (Vec<String>, Vec<String>) = names("solids")
.into_iter()
.partition(|name| sketch_ids.contains(name));
let faces = names("faces");
let edges = names("edges");
let planes = names("datums");
let vertices = v["vertices"].as_u64().unwrap_or(0) as usize;
let total = solids.len() + sketches.len() + faces.len() + edges.len() + planes.len();
let single = if total == 1 && vertices == 0 {
faces
.first()
.or_else(|| edges.first())
.or_else(|| solids.first())
.or_else(|| sketches.first())
.or_else(|| planes.first())
.cloned()
} else {
None
};
let owning_feature = single
.as_deref()
.and_then(|name| state.creating_feature(name))
.map(|(id, _ty)| id);
Self {
solids,
sketches,
faces,
edges,
planes,
vertices,
owning_feature,
}
}
fn kinds_present(&self) -> Vec<&'static str> {
let mut kinds = Vec::new();
if !self.solids.is_empty() {
kinds.push("SOLID");
}
if !self.sketches.is_empty() {
kinds.push("SKETCH");
}
if !self.faces.is_empty() {
kinds.push("FACE");
}
if !self.edges.is_empty() {
kinds.push("EDGE");
}
if !self.planes.is_empty() {
kinds.push("PLANE");
}
kinds
}
fn names_for_filter(&self, filter: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let push = |src: &[String], out: &mut Vec<String>| {
for name in src {
if !out.iter().any(|n| n == name) {
out.push(name.clone());
}
}
};
for f in filter {
match f.as_str() {
"SOLID" | "COMPONENT" => push(&self.solids, &mut out),
"SKETCH" => push(&self.sketches, &mut out),
"FACE" => push(&self.faces, &mut out),
"PLANE" | "DATUM" => push(&self.planes, &mut out),
"EDGE" => push(&self.edges, &mut out),
_ => {}
}
}
out
}
fn all_names(&self) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for src in [&self.solids, &self.sketches, &self.faces, &self.edges] {
for name in src {
if !name.is_empty() && !out.iter().any(|n| n == name) {
out.push(name.clone());
}
}
}
out
}
fn summary(&self) -> String {
format!(
"Selected: {} solid, {} sketch, {} face, {} edge, {} plane, {} vertex",
self.solids.len(),
self.sketches.len(),
self.faces.len(),
self.edges.len(),
self.planes.len(),
self.vertices,
)
}
}
struct OfferField {
path: Vec<String>,
filter: Vec<String>,
multiple: bool,
}
struct Offer {
type_code: String,
label: String,
fields: Vec<OfferField>,
}
fn selection_probe(
sel: &Selection,
comp: &ComponentSelection,
all_sheet_metal: bool,
) -> SelectionProbe {
SelectionProbe {
solids: sel.solids.len(),
sketches: sel.sketches.len(),
faces: sel.faces.len(),
edges: sel.edges.len(),
planes: sel.planes.len(),
vertices: sel.vertices,
components: comp.ids.len(),
all_component: comp.all_component,
all_sheet_metal,
}
}
fn all_on_sheet_metal(sel: &Selection, state: &EngineState) -> bool {
let names = sel.all_names();
!names.is_empty()
&& sel.vertices == 0
&& names.iter().all(|name| state.is_sheet_metal_object(name))
}
fn feature_offers(probe: &SelectionProbe, sel: &Selection, workbench: &str) -> Vec<Offer> {
let kinds = sel.kinds_present();
if kinds.is_empty() {
return Vec::new();
}
let catalogue = features::feature_catalogue();
let mut out = Vec::new();
if let Some(list) = catalogue.get("features").and_then(Value::as_array) {
for feature in list {
let Some(ty) = feature.get("type").and_then(Value::as_str) else {
continue;
};
if ty.is_empty() {
continue;
}
if !crate::workbench::includes_feature(workbench, ty) {
continue;
}
if !brep_kernel::feature_context_applicable(ty, probe) {
continue;
}
let fields: Vec<OfferField> = features::feature_form_fields(ty)
.iter()
.filter(|field| field.group == "References")
.filter_map(|field| {
let FieldKind::Reference { filter, multiple } = &field.kind else {
return None;
};
filter
.iter()
.any(|f| kinds.iter().any(|k| *k == f.as_str()))
.then(|| OfferField {
path: field.path.clone(),
filter: filter.clone(),
multiple: *multiple,
})
})
.collect();
out.push(Offer {
type_code: ty.to_string(),
label: features::feature_long_name(ty),
fields,
});
}
}
out
}
fn component_action_target<'a>(comp: &'a ComponentSelection, workbench: &str) -> Option<&'a str> {
let list_shown = crate::workbench::panel_visible(
workbench,
crate::workbench::assembly::BOM_PANEL_ID,
);
list_shown.then(|| comp.sole_target()).flatten()
}
fn pmi_offers(
probe: &SelectionProbe,
workbench: &str,
view_active: bool,
) -> Vec<&'static brep_kernel::PmiTypeDef> {
if !view_active || !crate::workbench::panel_visible(workbench, crate::workbench::pmi::PANEL_ID) {
return Vec::new();
}
brep_kernel::PMI_TYPES
.iter()
.filter(|def| (def.applicable)(probe))
.collect()
}
pub(crate) fn add_pmi_from_selection(state: &mut EngineState, type_id: &str) -> Result<String, String> {
let catalogue = brep_kernel::pmi_schema_catalogue();
let schema = catalogue
.as_array()
.and_then(|entries| entries.iter().find(|e| e.get("type").and_then(Value::as_str) == Some(type_id)))
.cloned()
.ok_or_else(|| format!("no schema for PMI type '{type_id}'"))?;
let selection: Value = serde_json::from_str(&state.selection_json()).unwrap_or(Value::Null);
let names = |key: &str| -> Vec<String> {
selection[key]
.as_array()
.map(|items| items.iter().filter_map(|item| item.as_str().map(String::from)).collect())
.unwrap_or_default()
};
let vertices: Vec<String> = state
.emphasis
.selected_vertices
.iter()
.map(|vertex| brep_render::engine_state::world_vertex_ref(&vertex.solid, vertex.position))
.collect();
let mut seeded = serde_json::Map::new();
let mut consumed: Vec<String> = Vec::new();
if let Some(fields) = schema.get("inputParamsSchema").and_then(Value::as_object) {
for (key, spec) in fields {
if spec.get("type").and_then(Value::as_str) != Some("reference_selection") || key == "plane" {
continue;
}
let filter: Vec<String> = spec
.get("selectionFilter")
.and_then(Value::as_array)
.map(|kinds| kinds.iter().filter_map(|k| k.as_str().map(String::from)).collect())
.unwrap_or_default();
let multiple = spec.get("multiple").and_then(Value::as_bool).unwrap_or(false);
let cap = spec.get("maxSelections").and_then(Value::as_u64).unwrap_or(if multiple { 64 } else { 1 }) as usize;
let mut picked: Vec<String> = Vec::new();
for kind in &filter {
let source: Vec<String> = match kind.to_ascii_uppercase().as_str() {
"FACE" => names("faces"),
"EDGE" => names("edges"),
"PLANE" | "DATUM" => names("datums"),
"SOLID" | "COMPONENT" => names("solids"),
"VERTEX" => vertices.clone(),
_ => Vec::new(),
};
for name in source {
if picked.len() < cap && !picked.contains(&name) && !consumed.contains(&name) {
picked.push(name);
}
}
}
if picked.is_empty() {
continue;
}
consumed.extend(picked.iter().cloned());
let value = if multiple {
Value::Array(picked.into_iter().map(Value::String).collect())
} else {
Value::String(picked.remove(0))
};
seeded.insert(key.clone(), value);
}
}
state.pmi_add_annotation(None, type_id, &Value::Object(seeded).to_string())
}
fn constraint_offers(
probe: &SelectionProbe,
workbench: &str,
) -> Vec<&'static brep_kernel::ConstraintTypeDef> {
if !crate::workbench::panel_visible(workbench, crate::workbench::assembly::CONSTRAINTS_PANEL_ID)
{
return Vec::new();
}
brep_kernel::CONSTRAINT_TYPES
.iter()
.filter(|def| (def.applicable)(probe))
.collect()
}
pub(crate) fn add_constraint_from_selection(
state: &mut EngineState,
type_id: &str,
) -> Result<String, String> {
let catalogue = brep_kernel::constraint_schema_catalogue();
let schemas: Vec<Value> = catalogue.as_array().cloned().unwrap_or_default();
let seed = super::assembly_constraints::seeded_elements(state, &schemas, type_id);
let id = state.assembly_add_constraint(type_id, &seed.to_string())?;
let _ = state.assembly_set_constraint_open(&id, true);
Ok(id)
}
fn prefill_references(fields: &[OfferField], sel: &Selection) -> Vec<(Vec<String>, Value)> {
let mut consumed: HashSet<String> = HashSet::new();
let mut writes = Vec::new();
for field in fields {
let names: Vec<String> = sel
.names_for_filter(&field.filter)
.into_iter()
.filter(|name| !consumed.contains(name))
.collect();
if names.is_empty() {
continue;
}
let value = if field.multiple {
consumed.extend(names.iter().cloned());
Value::Array(names.into_iter().map(Value::String).collect())
} else {
let name = names.into_iter().next().unwrap_or_default();
consumed.insert(name.clone());
Value::String(name)
};
writes.push((field.path.clone(), value));
}
writes
}
fn create_feature_from_selection(
state: &mut EngineState,
offer: &Offer,
sel: &Selection,
) -> Option<String> {
let id = state.next_feature_id(&features::feature_short_name(&offer.type_code));
let mut params = features::feature_default_params(&offer.type_code);
if let Value::Object(map) = &mut params {
map.insert("id".into(), Value::String(id.clone()));
}
for (path, value) in prefill_references(&offer.fields, sel) {
form::set_at(&mut params, &path, value);
}
let feature = serde_json::json!({
"type": offer.type_code,
"inputParams": params,
"persistentData": {},
});
if state.add_feature(&feature.to_string()).is_ok() {
Some(id)
} else {
None
}
}
fn feature_index(state: &EngineState, id: &str) -> Option<usize> {
(0..state.history_len()).find(|&i| state.feature_id_at(i).as_deref() == Some(id))
}
pub static HIT_KEYS: &[HitKeyDoc] = &[
HitKeyDoc { panel: "context", prefix: "feature:", meaning: "add the offered feature from the selection (feature:type)", command: None },
HitKeyDoc { panel: "context", prefix: "component:", meaning: "a component action", command: None },
HitKeyDoc { panel: "context", prefix: "constraint:", meaning: "add the offered assembly constraint", command: None },
HitKeyDoc { panel: "context", prefix: "pmi:", meaning: "add the offered PMI annotation (pmi:type)", command: None },
HitKeyDoc { panel: "context", prefix: "Selected:", meaning: "the selection summary chip", command: None },
];