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