Skip to main content

brep_app/panels/
exploded_views.rs

1//! The Exploded views section at the foot of the Assembly Constraints pane.
2//!
3//! An exploded view is a PMI view whose explode rows are its ordered steps
4//! (`brep_render::engine_state` `exploded_ops`), so this section is a front for
5//! the PMI view and annotation calls the PMI pane makes, plus the assembly
6//! ones: **New exploded view** (auto-explode, one step per top-level
7//! component), **Show** / **Hide** (activate the view in place, without the PMI
8//! workbench), **Explode** / **Collapse** (play the steps out or home) and a
9//! progress slider. A step row selects the step and arms the move gizmo on it
10//! (the same gizmo the PMI pane gives an open explode annotation); its trail
11//! box and delete and up / down buttons edit the PMI rows. **Hide selected**
12//! adds a HIDE step (a `hide` row: the selected components stay out of the
13//! view while it is shown), listed under the steps with its own delete.
14use crate::automation::hit_keys::HitKeyDoc;
15use brep_render::engine_state::EngineState;
16use eframe::egui;
17use std::collections::HashMap;
18
19/// Hit keys this section publishes, into the Constraints pane's `acon:` set.
20pub static HIT_KEYS: &[HitKeyDoc] = &[
21    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:header", meaning: "open or close the Exploded views section", command: None },
22    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:new", meaning: "make a new exploded view (auto-explode)", command: Some("assembly_auto_explode") },
23    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:deep", meaning: "a new exploded view opens the sub-assemblies too", command: Some("assembly_auto_explode") },
24    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:spread", meaning: "how far a new exploded view spreads the components", command: Some("assembly_auto_explode") },
25    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:show:", meaning: "show or hide an exploded view (explode:show:<view id>)", command: Some("pmi_activate_view") },
26    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:play", meaning: "play the shown view's steps out", command: Some("exploded_animate") },
27    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:collapse", meaning: "bring the shown view's steps home", command: Some("exploded_animate") },
28    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:progress", meaning: "the shown view's progress slider", command: Some("exploded_set_progress") },
29    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:add", meaning: "add a step that moves the selected components (a part inside a sub-assembly picked from the BOM is one)", command: Some("exploded_add_step") },
30    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:step:", meaning: "a step's row: select it and move it with the gizmo (explode:step:<id>)", command: None },
31    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:trace:", meaning: "a step's trail box", command: Some("pmi_update_annotation") },
32    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:up:", meaning: "move a step earlier", command: Some("pmi_move_annotation") },
33    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:del:", meaning: "delete a step", command: Some("pmi_remove_annotation") },
34    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:hide", meaning: "add a hide step that keeps the selected components (or solids) out of the shown view", command: Some("pmi_add_annotation") },
35    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:hidden:", meaning: "a hide step's row: select it (explode:hidden:<id>)", command: None },
36    HitKeyDoc { panel: "assemblyconstraints", prefix: "explode:unhide:", meaning: "delete a hide step (explode:unhide:<id>)", command: Some("pmi_remove_annotation") },
37];
38
39/// The section's own UI state.
40pub struct ExplodedSection {
41    /// A new view's spread (multiples of half the assembly diagonal).
42    spread: f64,
43    /// A new view opens the sub-assemblies too (their parts separate).
44    deep: bool,
45    /// The exploded views as last read, and the history revision they were
46    /// read at. Reading them parses the PMI block and names every step's
47    /// part: twice a frame, that was ~12 ms of every frame the pane showed a
48    /// 499-step view on l500.
49    views: Option<(u64, std::rc::Rc<Vec<brep_render::engine_state::ExplodedView>>)>,
50    /// The last refusal, shown under the buttons.
51    message: Option<String>,
52    /// The step row under the pointer: it lights the step's parts and shows
53    /// its chip (a deep view draws chips only for the selected or hovered step).
54    hovered: Option<String>,
55}
56
57impl Default for ExplodedSection {
58    fn default() -> Self {
59        Self { spread: 1.0, deep: false, views: None, message: None, hovered: None }
60    }
61}
62
63/// How deep "into sub-assemblies" opens.
64const DEEP: usize = 8;
65
66enum Action {
67    New,
68    Add,
69    HideSelected,
70    Show(String),
71    Hide,
72    Animate(bool),
73    Progress(f64),
74    Select(String),
75    Trace(String, bool),
76    Up(String),
77    Delete(String),
78}
79
80/// How fast Explode / Collapse play, in steps per second.
81const STEPS_PER_SECOND: f64 = 1.5;
82
83impl ExplodedSection {
84    pub fn show(&mut self, ui: &mut egui::Ui, state: &mut EngineState, hits: &mut HashMap<String, egui::Rect>) {
85        let revision = state.history.revision();
86        let views = match &self.views {
87            Some((at, views)) if *at == revision => views.clone(),
88            _ => {
89                let views = std::rc::Rc::new(state.exploded_views());
90                self.views = Some((revision, views.clone()));
91                views
92            }
93        };
94        let active = state.pmi_active_view().map(str::to_string);
95        let locked = state.history.locked().is_some();
96        let mut action = None;
97        let mut hovered = None;
98        ui.add_space(6.0);
99        let header = egui::CollapsingHeader::new(format!("Exploded views ({})", views.len()))
100            .id_salt("explode:section")
101            .show(ui, |ui| {
102                ui.horizontal(|ui| {
103                    let new = ui.add_enabled(!locked, egui::Button::new("New exploded view"));
104                    hits.insert("explode:new".into(), new.rect);
105                    if new.on_hover_text("One step per component, each moved straight away from the base component").clicked() {
106                        action = Some(Action::New);
107                    }
108                    let spread = ui.add(egui::DragValue::new(&mut self.spread).range(0.1..=10.0).speed(0.05).prefix("spread ×"));
109                    hits.insert("explode:spread".into(), spread.rect);
110                    let deep = ui.checkbox(&mut self.deep, "into sub-assemblies");
111                    hits.insert("explode:deep".into(), deep.rect);
112                    deep.on_hover_text("Also take each sub-assembly apart, after the assembly's own components");
113                });
114                if views.is_empty() {
115                    ui.weak("No exploded views yet. A PMI view with explode annotations is one too.");
116                }
117                for view in views.iter() {
118                    let shown = active.as_deref() == Some(view.id.as_str());
119                    ui.horizontal(|ui| {
120                        let toggle = ui.selectable_label(shown, if shown { "◉ Hide" } else { "○ Show" });
121                        hits.insert(format!("explode:show:{}", view.id), toggle.rect);
122                        if toggle.clicked() {
123                            action = Some(if shown { Action::Hide } else { Action::Show(view.id.clone()) });
124                        }
125                        ui.label(format!("{} · {} step{}", view.name, view.steps.len(), if view.steps.len() == 1 { "" } else { "s" }));
126                    });
127                    if !shown {
128                        continue;
129                    }
130                    ui.horizontal(|ui| {
131                        let play = ui.button("▶ Explode");
132                        hits.insert("explode:play".into(), play.rect);
133                        if play.clicked() {
134                            action = Some(Action::Animate(true));
135                        }
136                        let collapse = ui.button("◀ Collapse");
137                        hits.insert("explode:collapse".into(), collapse.rect);
138                        if collapse.clicked() {
139                            action = Some(Action::Animate(false));
140                        }
141                        let steps = state.exploded_step_count() as f64;
142                        let mut progress = state.exploded_progress();
143                        let slider = ui.add(egui::Slider::new(&mut progress, 0.0..=steps.max(1e-9)).show_value(false));
144                        hits.insert("explode:progress".into(), slider.rect);
145                        if slider.changed() {
146                            action = Some(Action::Progress(progress));
147                        }
148                    });
149                    let add = ui.add_enabled(!locked, egui::Button::new("+ Add step from selection"));
150                    hits.insert("explode:add".into(), add.rect);
151                    if add
152                        .on_hover_text("Move the selected components as a new step, then drag its gizmo. A part inside a sub-assembly: pick its row in the BOM")
153                        .clicked()
154                    {
155                        action = Some(Action::Add);
156                    }
157                    let selected = state.pmi_open_annotation().map(str::to_string);
158                    for (n, step) in view.steps.iter().enumerate() {
159                        ui.horizontal(|ui| {
160                            let mut trace = step.trace;
161                            let trace_box = ui.add_enabled(!locked, egui::Checkbox::new(&mut trace, ""));
162                            hits.insert(format!("explode:trace:{}", step.id), trace_box.rect);
163                            if trace_box.on_hover_text("Draw this step's trail").changed() {
164                                action = Some(Action::Trace(step.id.clone(), trace));
165                            }
166                            let [x, y, z] = step.translate;
167                            let row = ui.selectable_label(
168                                selected.as_deref() == Some(step.id.as_str()),
169                                format!("{}. {}  ({x:.1}, {y:.1}, {z:.1})", n + 1, step.labels.join(", ")),
170                            );
171                            hits.insert(format!("explode:step:{}", step.id), row.rect);
172                            if row.hovered() {
173                                hovered = Some(step.id.clone());
174                            }
175                            if row.on_hover_text("Select, then drag the gizmo to move this step").clicked() {
176                                action = Some(Action::Select(step.id.clone()));
177                            }
178                            if n > 0 {
179                                let up = ui.add_enabled(!locked, egui::Button::new("↑").small());
180                                hits.insert(format!("explode:up:{}", step.id), up.rect);
181                                if up.on_hover_text("Earlier").clicked() {
182                                    action = Some(Action::Up(step.id.clone()));
183                                }
184                            }
185                            let del = ui.add_enabled(!locked, egui::Button::new("✕").small());
186                            hits.insert(format!("explode:del:{}", step.id), del.rect);
187                            if del.on_hover_text("Delete this step").clicked() {
188                                action = Some(Action::Delete(step.id.clone()));
189                            }
190                        });
191                    }
192                    // The view's hide steps: what it keeps out of sight, tracked
193                    // like its explode steps (added from the selection, listed,
194                    // deleted, undone), not steps of the playback.
195                    let hide = ui.add_enabled(!locked, egui::Button::new("Hide selected"));
196                    hits.insert("explode:hide".into(), hide.rect);
197                    if hide
198                        .on_hover_text("Keep the selected components out of this view, as a hide step. A part inside a sub-assembly: pick its row in the BOM")
199                        .clicked()
200                    {
201                        action = Some(Action::HideSelected);
202                    }
203                    for step in view.hides.iter() {
204                        ui.horizontal(|ui| {
205                            let row = ui.selectable_label(
206                                selected.as_deref() == Some(step.id.as_str()),
207                                format!("{}hidden: {}", if step.enabled { "" } else { "(off) " }, step.labels.join(", ")),
208                            );
209                            hits.insert(format!("explode:hidden:{}", step.id), row.rect);
210                            if row.on_hover_text("Select this hide step; its form edits what it hides").clicked() {
211                                action = Some(Action::Select(step.id.clone()));
212                            }
213                            let del = ui.add_enabled(!locked, egui::Button::new("✕").small());
214                            hits.insert(format!("explode:unhide:{}", step.id), del.rect);
215                            if del.on_hover_text("Delete this hide step: its parts show again").clicked() {
216                                action = Some(Action::Delete(step.id.clone()));
217                            }
218                        });
219                    }
220                }
221                if let Some(message) = &self.message {
222                    ui.colored_label(ui.visuals().error_fg_color, message);
223                }
224            });
225        hits.insert("explode:header".into(), header.header_response.rect);
226        if hovered != self.hovered {
227            match &hovered {
228                Some(id) => state.pmi_hover(id),
229                None => state.pmi_hover_end(),
230            }
231            self.hovered = hovered;
232        }
233        // The gizmo follows the selected step, as it follows an open explode
234        // annotation in the PMI pane.
235        let open_step = state
236            .pmi_open_annotation()
237            .filter(|id| views.iter().any(|v| active.as_deref() == Some(v.id.as_str()) && v.steps.iter().any(|s| s.id == *id)))
238            .map(str::to_string);
239        if let Some(id) = open_step {
240            if !state.transform_armed_for(&id) {
241                state.arm_transform(&id);
242            }
243            state.sync_transform_gizmo();
244        }
245        let Some(action) = action else { return };
246        let result: Result<(), String> = match action {
247            Action::New => state.assembly_auto_explode_deep(None, self.spread, if self.deep { DEEP } else { 1 }).map(|_| ()),
248            Action::Add => state.exploded_add_step(&[]).map(|_| ()),
249            Action::HideSelected => state.pmi_hide_selected(None).map(|_| ()),
250            Action::Show(id) => state.pmi_activate_view(&id),
251            Action::Hide => {
252                state.pmi_deactivate_view();
253                Ok(())
254            }
255            // A deep view has many steps: the whole play stays about 4 s.
256            Action::Animate(explode) => {
257                let steps = state.exploded_step_count() as f64;
258                state.exploded_animate(explode, STEPS_PER_SECOND.max(steps / 4.0))
259            }
260            Action::Progress(progress) => {
261                state.exploded_set_progress(progress);
262                Ok(())
263            }
264            Action::Select(id) => {
265                state.pmi_set_annotation_open(Some(&id));
266                Ok(())
267            }
268            Action::Trace(id, trace) => match state.pmi_state().find_annotation(&id).map(|(_, a)| a.params.clone()) {
269                Some(mut params) => {
270                    params["showTraceLine"] = serde_json::json!(trace);
271                    state.pmi_update_annotation(&id, &params.to_string())
272                }
273                None => Err(format!("no step '{id}'")),
274            },
275            Action::Up(id) => {
276                // Just before the explode row ahead of it, in the view's own order.
277                let pmi = state.pmi_state();
278                let index = pmi.find_annotation(&id).and_then(|(view, _)| {
279                    let own = view.annotations.iter().position(|a| a.id() == id)?;
280                    view.annotations[..own].iter().rposition(|a| a.kind == "explode")
281                });
282                drop(pmi);
283                match index {
284                    Some(index) => state.pmi_move_annotation(&id, index),
285                    None => Ok(()),
286                }
287            }
288            Action::Delete(id) => state.pmi_remove_annotation(&id),
289        };
290        self.message = result.err();
291    }
292}