1use crate::automation::hit_keys::HitKeyDoc;
15use brep_render::engine_state::EngineState;
16use eframe::egui;
17use std::collections::HashMap;
18
19pub 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
39pub struct ExplodedSection {
41 spread: f64,
43 deep: bool,
45 views: Option<(u64, std::rc::Rc<Vec<brep_render::engine_state::ExplodedView>>)>,
50 message: Option<String>,
52 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
63const 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
80const 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 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 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 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, ¶ms.to_string())
272 }
273 None => Err(format!("no step '{id}'")),
274 },
275 Action::Up(id) => {
276 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}