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 plugin_action: Option<String>,
}
#[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>,
shown_component_targets: Vec<String>,
pub max_height: Option<f32>,
}
impl ContextBarPanel {
pub(crate) fn clear_view(&mut self) {
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;
self.shown_component_targets.clear();
}
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;
self.shown_component_targets.clear();
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,
&crate::workbench::ButtonState::of(state),
)
});
if (!has_geometry && constraint_target.is_none())
|| state.ref_select_active()
|| state.any_sketch_mode()
|| crate::workbench::ecad::Target::of_workbench(&state.settings.workbench).is_some()
{
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_scoped(&probe, &sel, &state.settings.workbench, Some(state))
};
let constraint_types =
constraint_offers(&probe, &state.settings.workbench, &crate::workbench::ButtonState::of(state));
let pmi_types = pmi_offers(
&probe,
&state.settings.workbench,
state.pmi_active_view().is_some(),
&crate::workbench::ButtonState::of(state),
);
let component_target = component_action_target(
&comp,
&state.settings.workbench,
&crate::workbench::ButtonState::of(state),
)
.filter(|id| brep_kernel::pattern_component_instance(id).is_none())
.map(|id| {
let fixed = state.component_info(id).map(|info| info.fixed).unwrap_or(false);
(id.to_string(), fixed)
});
let instance_seed = comp
.sole_target()
.and_then(brep_kernel::pattern_component_instance)
.map(|(_, seed, _)| seed);
let move_targets = if component_target.is_some() {
Vec::new()
} else {
let mut targets: Vec<String> = Vec::new();
for id in component_move_targets(&comp, &state.settings.workbench, &crate::workbench::ButtonState::of(state)) {
let id = brep_kernel::pattern_component_instance(&id).map_or(id, |(_, seed, _)| seed);
if !targets.contains(&id) {
targets.push(id);
}
}
targets
};
let mut items = vec![ActionItem::new(
"action:clear",
"\u{2716} Clear",
"Clear the selection",
)];
self.shown_actions.push("clear".into());
if has_geometry && component_target.is_none() {
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());
}
let feature_target = match crate::delete_selection::model_target(state) {
Some(crate::delete_selection::DeleteTarget::Feature(id)) => Some(id),
_ => None,
};
if let Some(id) = &feature_target {
items.push(ActionItem::new(
"action:delete-feature",
"\u{2715} Delete feature",
format!("Delete feature {id} (Delete key; one undo step)"),
));
self.shown_actions.push("delete-feature".into());
}
if let Some(item) = edit_owning_item(&sel).filter(|_| component_target.is_none()) {
items.push(item);
self.shown_actions.push("edit-owning".into());
}
if let Some(seed) = &instance_seed {
items.push(ActionItem::new(
"action:select-seed",
format!("Select seed {seed}"),
"Select the component this instance follows: move, mate or edit it, and every instance follows",
));
self.shown_actions.push("select-seed".into());
}
let pattern_instance = sel.single.as_deref().and_then(|name| state.pattern_instance_of(name));
if let Some((pattern, number)) = &pattern_instance {
items.push(ActionItem::new(
"action:suppress-instance",
format!("Suppress instance {number}"),
format!("Make instance {number} of {pattern} produce no geometry (one undo step)"),
));
self.shown_actions.push("suppress-instance".into());
let suppressed = state.pattern_suppressed(pattern).len();
if suppressed > 0 {
items.push(ActionItem::new(
"action:unsuppress-instances",
format!("Unsuppress instances ({suppressed})"),
format!("Bring back every suppressed instance of {pattern}"),
));
self.shown_actions.push("unsuppress-instances".into());
}
}
if let Some((target, _)) = &component_target {
let subject = super::component_actions::MenuSubject::of(state, std::slice::from_ref(target));
for entry in super::component_actions::component_menu(&subject) {
items.push(
ActionItem::new(format!("component:{}", entry.id), entry.label, entry.tooltip)
.enabled(entry.enabled),
);
self.shown_component_actions.push(entry.id);
}
self.shown_component_target = Some(target.clone());
self.shown_component_targets = vec![target.clone()];
}
if !move_targets.is_empty() {
let action = ComponentAction::Move;
let tooltip = if move_targets.len() == 1 {
"Move the component that owns the selection (move/rotate gizmo on-off)"
} else {
"Move the selected components together with one move/rotate gizmo"
};
items.push(ActionItem::new(
format!("component:{}", action.id()),
action.label(false),
tooltip,
));
self.shown_component_actions.push(action.id().to_string());
self.shown_component_targets = move_targets.clone();
}
let unfix_targets = if component_target.is_some() {
Vec::new()
} else {
unfix_move_targets(state, &self.shown_component_targets)
};
if !unfix_targets.is_empty() {
items.push(ActionItem::new(
format!("component:{UNFIX_MOVE}"),
"\u{1F513} Unfix and Move",
"Unfix the fixed components in the selection and move them with the rest \
(unfixed only when you drag; one undo step)",
));
self.shown_component_actions.push(UNFIX_MOVE.to_string());
}
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(),
};
for action in crate::workbench::plugin_actions(state, &state.settings.workbench) {
if let Some(id) = action["id"].as_str() {
items.push(ActionItem::new(format!("plugin:{id}"), action["ribbonPath"].as_str().and_then(|p| p.rsplit('/').next()).or(action["label"].as_str()).unwrap_or(id), "Open plugin action form"));
}
}
let max_height = self.max_height;
let clicked = if crate::viewport::touch::compact(ui.ctx()) {
action_rail(ui, Some("Selection actions"), Some(&summary), &items, &mut self.hits)
} else {
egui::Frame::popup(ui.style())
.show(ui, |ui| {
let Some(limit) = max_height else {
return action_rail(ui, Some("Selection actions"), Some(&summary), &items, &mut self.hits);
};
let limit = (limit - ui.style().spacing.menu_margin.sum().y).max(4.0 * ui.spacing().interact_size.y);
let scroll = egui::ScrollArea::vertical()
.id_salt("context-bar-rail")
.max_height(limit)
.min_scrolled_height(limit)
.show(ui, |ui| action_rail(ui, Some("Selection actions"), Some(&summary), &items, &mut self.hits));
if scroll.content_size.y > scroll.inner_rect.height() + 0.5 {
self.hits.insert("panel:clip".into(), scroll.inner_rect);
}
scroll.inner
})
.inner
};
let mut outcome = ContextOutcome::default();
match clicked.as_deref() {
Some(key) if key.starts_with("plugin:") => { outcome.plugin_action = Some(key[7..].to_owned()); }
Some("action:clear") => {
state.clear_selection();
}
Some("action:delete-constraint") => {
if let Some(cid) = &constraint_target {
let target = crate::delete_selection::DeleteTarget::Constraint(cid.clone());
if let Err(error) = crate::delete_selection::apply_engine(state, &target) {
state.push_notice(error);
}
}
}
Some("action:delete-feature") => {
if let Some(id) = &feature_target {
let target = crate::delete_selection::DeleteTarget::Feature(id.clone());
if let Err(error) = crate::delete_selection::apply_engine(state, &target) {
state.push_notice(error);
}
}
}
Some("action:hide") => {
state.hide_selected();
}
Some("action:select-seed") => {
if let Some(seed) = &instance_seed {
state.select_component(seed);
}
}
Some("action:suppress-instance") => {
if let Some((pattern, number)) = &pattern_instance {
match state.pattern_set_suppressed(pattern, Some(*number), true) {
Ok(()) => {
state.clear_selection();
}
Err(error) => state.push_notice(error),
}
}
}
Some("action:unsuppress-instances") => {
if let Some((pattern, _)) = &pattern_instance {
if let Err(error) = state.pattern_set_suppressed(pattern, None, false) {
state.push_notice(error);
}
}
}
Some("action:info") => {
outcome.info_targets = sel.all_names();
}
Some("action:edit-owning") => {
if let Some(spline) = sel.harness_bundle() {
state.push_notice(bundle_edit_hint(&spline));
} else 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);
}
match outcome.component.take() {
Some(ComponentActionRequest::Info { targets }) => outcome.info_targets = targets,
Some(ComponentActionRequest::EditFeature { feature_id }) => outcome.focus = Some(feature_id),
other => outcome.component = other,
}
} else if key == "component:move" && !move_targets.is_empty() {
state.component_move_group_toggle(&move_targets);
}
if key == format!("component:{UNFIX_MOVE}") && !unfix_targets.is_empty() {
state.component_move_unfix_toggle(&unfix_targets);
}
}
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) {
if let Err(error) = add_pmi_from_selection(state, type_id) {
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,
"componentTargets": self.shown_component_targets,
})
.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 move_targets(&self) -> Vec<String> {
if self.suppress_features() {
self.ids.clone()
} else {
Vec::new()
}
}
}
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 {
any = true;
match state.selected_vertex_components() {
Some(owners) => {
for id in owners {
if !ids.contains(&id) {
ids.push(id);
}
}
}
None => 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,
}
}
fn edit_owning_item(sel: &Selection) -> Option<ActionItem> {
sel.owning_feature.as_ref()?;
let item = match sel.harness_bundle() {
Some(spline) => ActionItem::new(
"action:edit-owning",
"Edit owning feature",
bundle_edit_hint(&spline),
)
.enabled(false),
None => ActionItem::new(
"action:edit-owning",
"Edit owning feature",
"Roll to and edit the feature that created this",
),
};
Some(item)
}
fn bundle_edit_hint(spline: &str) -> String {
format!(
"This bundle is built by the wire harness, not by a feature. \
Edit spline {spline} (the path it follows) or the wires in the Wire Harness panel instead"
)
}
struct Selection {
solids: Vec<String>,
sketches: Vec<String>,
faces: Vec<String>,
edges: Vec<String>,
planes: Vec<String>,
vertices: usize,
owning_feature: Option<String>,
single: Option<String>,
}
impl Selection {
fn harness_bundle(&self) -> Option<String> {
if self.owning_feature.as_deref() != Some(brep_kernel::WIRE_HARNESS_FEATURE_ID) {
return None;
}
let name = self
.solids
.first()
.or_else(|| self.faces.first())
.or_else(|| self.edges.first())?;
let rest = name.strip_prefix(brep_kernel::BUNDLE_SOLID_PREFIX)?;
let spline = rest.split(':').next().filter(|id| !id.is_empty())?;
Some(spline.to_string())
}
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,
single,
}
}
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 {
let count = crate::panels::count;
let parts: Vec<String> = [
(self.solids.len(), "solid", "solids"),
(self.sketches.len(), "sketch", "sketches"),
(self.faces.len(), "face", "faces"),
(self.edges.len(), "edge", "edges"),
(self.planes.len(), "plane", "planes"),
(self.vertices, "vertex", "vertices"),
]
.into_iter()
.filter(|(n, _, _)| *n > 0)
.map(|(n, one, many)| count(n, one, many))
.collect();
if parts.is_empty() {
"Selected: nothing".to_string()
} else {
format!("Selected: {}", parts.join(", "))
}
}
}
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_scoped(probe: &SelectionProbe, sel: &Selection, workbench: &str, engine: Option<&EngineState>) -> Vec<Offer> {
let kinds = sel.kinds_present();
if kinds.is_empty() {
return Vec::new();
}
let catalogue = engine.map_or_else(features::feature_catalogue, |e| e.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 !engine.map_or_else(|| crate::workbench::includes_feature(workbench, ty), |e| crate::workbench::includes_feature_scoped(e, workbench, ty)) {
continue;
}
if !ty.contains('/') && !brep_kernel::feature_context_applicable(ty, probe) {
continue;
}
let fields = offer_fields(engine, ty, &kinds);
if ty.contains('/') && fields.is_empty() { continue; }
out.push(Offer {
type_code: ty.to_string(),
label: crate::workbench::feature_command_label(feature),
fields,
});
}
}
out
}
fn offer_fields(engine: Option<&EngineState>, ty: &str, kinds: &[&str]) -> Vec<OfferField> {
engine
.map_or_else(|| features::feature_form_fields(ty), |e| e.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()
}
pub(crate) fn prefill_from_selection(state: &EngineState, type_code: &str, params: &mut Value) -> usize {
if state.any_sketch_mode() || state.ref_select_active() {
return 0;
}
let sel = Selection::read(state);
let kinds = sel.kinds_present();
if kinds.is_empty() || component_selection(&sel, state).suppress_features() {
return 0;
}
let writes = prefill_references(&offer_fields(Some(state), type_code, &kinds), &sel);
let count = writes.len();
for (path, value) in writes {
form::set_at(params, &path, value);
}
count
}
pub(crate) fn component_action_target_of(state: &EngineState) -> Option<String> {
let sel = Selection::read(state);
let comp = component_selection(&sel, state);
component_action_target(&comp, &state.settings.workbench, &crate::workbench::ButtonState::of(state))
.filter(|id| brep_kernel::pattern_component_instance(id).is_none())
.map(str::to_string)
}
fn component_action_target<'a>(
comp: &'a ComponentSelection,
workbench: &str,
panels: &crate::workbench::ButtonState,
) -> Option<&'a str> {
let list_shown = crate::workbench::panel_visible(
workbench,
crate::workbench::assembly::BOM_PANEL_ID,
panels,
);
list_shown.then(|| comp.sole_target()).flatten()
}
const UNFIX_MOVE: &str = "unfix-move";
fn unfix_move_targets(state: &EngineState, targets: &[String]) -> Vec<String> {
let any_fixed = targets
.iter()
.any(|id| state.component_info(id).is_some_and(|info| info.fixed));
if any_fixed {
targets.to_vec()
} else {
Vec::new()
}
}
fn component_move_targets(
comp: &ComponentSelection,
workbench: &str,
panels: &crate::workbench::ButtonState,
) -> Vec<String> {
let list_shown = crate::workbench::panel_visible(
workbench,
crate::workbench::assembly::BOM_PANEL_ID,
panels,
);
if list_shown && comp.sole_target().is_none() {
comp.move_targets()
} else {
Vec::new()
}
}
fn pmi_offers(
probe: &SelectionProbe,
workbench: &str,
view_active: bool,
panels: &crate::workbench::ButtonState,
) -> Vec<&'static brep_kernel::PmiTypeDef> {
if !view_active
|| !crate::workbench::panel_visible(workbench, crate::workbench::pmi::PANEL_ID, panels)
{
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,
panels: &crate::workbench::ButtonState,
) -> Vec<&'static brep_kernel::ConstraintTypeDef> {
if !crate::workbench::panel_visible(
workbench,
crate::workbench::assembly::CONSTRAINTS_PANEL_ID,
panels,
) {
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 short = state.feature_schema(&offer.type_code).and_then(|s| s["shortName"].as_str().filter(|s| !s.is_empty()).map(str::to_owned)).unwrap_or_else(|| features::feature_short_name(&offer.type_code));
let id = state.next_feature_id(&short);
let mut params = state.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: "panel:clip", meaning: "the action rail's visible region, published only while the rail is taller than the room above the ViewCube and scrolls", command: None },
HitKeyDoc { panel: "context", prefix: "plugin:", meaning: "open a plugin action form for current selection", command: Some("plugin_action") },
HitKeyDoc { panel: "context", prefix: "action:clear", meaning: "clear the selection", command: Some("select_clear") },
HitKeyDoc { panel: "context", prefix: "action:hide", meaning: "hide or show the selected geometry", command: Some("set_visible") },
HitKeyDoc { panel: "context", prefix: "action:info", meaning: "open one pinned Info window per selected entity (mass properties, topology, metadata)", command: None },
HitKeyDoc { panel: "context", prefix: "action:delete-feature", meaning: "delete the feature selected from its history-tree row (the Delete key's command)", command: Some("selection_delete") },
HitKeyDoc { panel: "context", prefix: "action:delete-constraint", meaning: "delete the assembly constraint whose viewport label is selected", command: Some("assembly_remove_constraint") },
HitKeyDoc { panel: "context", prefix: "action:suppress-instance", meaning: "suppress the selected pattern instance (a part Pattern copy makes no geometry; a Pattern Component instance places nothing)", command: None },
HitKeyDoc { panel: "context", prefix: "action:select-seed", meaning: "select the seed component a Pattern Component instance follows", command: None },
HitKeyDoc { panel: "context", prefix: "action:unsuppress-instances", meaning: "bring back every suppressed instance of the selected instance's Pattern", command: None },
HitKeyDoc { panel: "context", prefix: "action:edit-owning", meaning: "roll to and edit the feature that created the selection (disabled for a wire-harness bundle solid, which no history feature owns)", command: None },
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 },
];