1use crate::automation::hit_keys::HitKeyDoc;
13use brep_render::engine_state::EngineState;
14use eframe::egui;
15use std::collections::HashMap;
16
17pub 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
34pub struct ExplodedSection {
36 spread: f64,
38 deep: bool,
40 views: Option<(u64, std::rc::Rc<Vec<brep_render::engine_state::ExplodedView>>)>,
45 message: Option<String>,
47 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
58const 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
74const 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 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 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, ¶ms.to_string())
237 }
238 None => Err(format!("no step '{id}'")),
239 },
240 Action::Up(id) => {
241 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}