1use crate::audio::AudioSource;
6use crate::simulation::{Medium, Position3D, SimulationEngine};
7use crate::visualization::{ColorMap, SliceAxis, SliceConfig};
8
9#[derive(Debug, Clone)]
11pub struct GuiState {
12 pub is_playing: bool,
14 pub simulation_speed: f32,
15 pub steps_per_frame: u32,
16
17 pub temperature_c: f32,
19 pub humidity_percent: f32,
20 pub medium: Medium,
21
22 pub xy_slice_position: f32,
24 pub xz_slice_position: f32,
25 pub yz_slice_position: f32,
26 pub show_xy_slice: bool,
27 pub show_xz_slice: bool,
28 pub show_yz_slice: bool,
29
30 pub source_x: f32,
32 pub source_y: f32,
33 pub source_z: f32,
34 pub source_amplitude: f32,
35 pub source_frequency: f32,
36 pub source_type_idx: usize,
37
38 pub listener_x: f32,
40 pub listener_y: f32,
41 pub listener_z: f32,
42 pub listener_yaw: f32,
43 pub show_listener: bool,
44
45 pub color_map_idx: usize,
47 pub auto_scale: bool,
48 pub max_pressure: f32,
49 pub slice_opacity: f32,
50 pub show_bounding_box: bool,
51 pub show_floor_grid: bool,
52 pub show_sources: bool,
53
54 pub show_info_panel: bool,
56}
57
58impl Default for GuiState {
59 fn default() -> Self {
60 Self {
61 is_playing: false,
62 simulation_speed: 1.0,
63 steps_per_frame: 1,
64
65 temperature_c: 20.0,
66 humidity_percent: 50.0,
67 medium: Medium::Air,
68
69 xy_slice_position: 0.5,
70 xz_slice_position: 0.5,
71 yz_slice_position: 0.5,
72 show_xy_slice: true,
73 show_xz_slice: true,
74 show_yz_slice: true,
75
76 source_x: 0.5,
77 source_y: 0.5,
78 source_z: 0.5,
79 source_amplitude: 1.0,
80 source_frequency: 440.0,
81 source_type_idx: 0,
82
83 listener_x: 0.5,
84 listener_y: 0.5,
85 listener_z: 0.7,
86 listener_yaw: 0.0,
87 show_listener: true,
88
89 color_map_idx: 0,
90 auto_scale: true,
91 max_pressure: 1.0,
92 slice_opacity: 0.85,
93 show_bounding_box: true,
94 show_floor_grid: true,
95 show_sources: true,
96
97 show_info_panel: true,
98 }
99 }
100}
101
102impl GuiState {
103 pub fn color_map(&self) -> ColorMap {
105 match self.color_map_idx {
106 0 => ColorMap::BlueWhiteRed,
107 1 => ColorMap::CoolWarm,
108 2 => ColorMap::Hot,
109 3 => ColorMap::Diverging,
110 _ => ColorMap::Grayscale,
111 }
112 }
113
114 pub fn source_type_name(&self) -> &'static str {
116 match self.source_type_idx {
117 0 => "Impulse",
118 1 => "Tone",
119 2 => "Chirp",
120 3 => "White Noise",
121 4 => "Pink Noise",
122 _ => "Gaussian Pulse",
123 }
124 }
125
126 pub fn create_source(&self, grid_size: (f32, f32, f32)) -> AudioSource {
128 let position = Position3D::new(
129 self.source_x * grid_size.0,
130 self.source_y * grid_size.1,
131 self.source_z * grid_size.2,
132 );
133
134 match self.source_type_idx {
135 0 => AudioSource::impulse(0, position, self.source_amplitude),
136 1 => AudioSource::tone(0, position, self.source_frequency, self.source_amplitude),
137 2 => AudioSource::chirp(0, position, 100.0, 2000.0, 0.5, self.source_amplitude),
138 3 => AudioSource::noise(0, position, self.source_amplitude, false),
139 4 => AudioSource::noise(0, position, self.source_amplitude, true),
140 _ => AudioSource::gaussian_pulse(0, position, 0.01, 0.002, self.source_amplitude),
141 }
142 }
143
144 pub fn source_position(&self, grid_size: (f32, f32, f32)) -> Position3D {
146 Position3D::new(
147 self.source_x * grid_size.0,
148 self.source_y * grid_size.1,
149 self.source_z * grid_size.2,
150 )
151 }
152
153 pub fn listener_position(&self, grid_size: (f32, f32, f32)) -> Position3D {
155 Position3D::new(
156 self.listener_x * grid_size.0,
157 self.listener_y * grid_size.1,
158 self.listener_z * grid_size.2,
159 )
160 }
161
162 pub fn slice_configs(&self) -> Vec<SliceConfig> {
164 let mut configs = Vec::new();
165
166 if self.show_xy_slice {
167 configs.push(SliceConfig {
168 axis: SliceAxis::XY,
169 position: self.xy_slice_position,
170 opacity: self.slice_opacity,
171 color_map: self.color_map(),
172 show_grid: false,
173 });
174 }
175
176 if self.show_xz_slice {
177 configs.push(SliceConfig {
178 axis: SliceAxis::XZ,
179 position: self.xz_slice_position,
180 opacity: self.slice_opacity,
181 color_map: self.color_map(),
182 show_grid: false,
183 });
184 }
185
186 if self.show_yz_slice {
187 configs.push(SliceConfig {
188 axis: SliceAxis::YZ,
189 position: self.yz_slice_position,
190 opacity: self.slice_opacity,
191 color_map: self.color_map(),
192 show_grid: false,
193 });
194 }
195
196 configs
197 }
198}
199
200pub struct SimulationControls {
202 state: GuiState,
203}
204
205impl SimulationControls {
206 pub fn new() -> Self {
207 Self {
208 state: GuiState::default(),
209 }
210 }
211
212 pub fn state(&self) -> &GuiState {
213 &self.state
214 }
215
216 pub fn state_mut(&mut self) -> &mut GuiState {
217 &mut self.state
218 }
219
220 pub fn ui(&mut self, ctx: &egui::Context, engine: &SimulationEngine) {
222 egui::SidePanel::left("controls")
223 .default_width(280.0)
224 .show(ctx, |ui| {
225 egui::ScrollArea::vertical().show(ui, |ui| {
226 self.simulation_section(ui, engine);
227 ui.separator();
228 self.environment_section(ui);
229 ui.separator();
230 self.source_section(ui);
231 ui.separator();
232 self.listener_section(ui);
233 ui.separator();
234 self.visualization_section(ui);
235 ui.separator();
236 self.slice_section(ui);
237 });
238 });
239
240 if self.state.show_info_panel {
241 egui::Window::new("Simulation Info")
242 .anchor(egui::Align2::RIGHT_TOP, [-10.0, 10.0])
243 .show(ctx, |ui| {
244 self.info_panel(ui, engine);
245 });
246 }
247 }
248
249 fn simulation_section(&mut self, ui: &mut egui::Ui, engine: &SimulationEngine) {
250 ui.heading("Simulation");
251
252 ui.horizontal(|ui| {
253 if ui
254 .button(if self.state.is_playing {
255 "⏸ Pause"
256 } else {
257 "▶ Play"
258 })
259 .clicked()
260 {
261 self.state.is_playing = !self.state.is_playing;
262 }
263 if ui.button("⟲ Reset").clicked() {
264 self.state.is_playing = false;
265 }
266 });
267
268 ui.horizontal(|ui| {
269 ui.label("Speed:");
270 ui.add(egui::Slider::new(
271 &mut self.state.simulation_speed,
272 0.1..=5.0,
273 ));
274 });
275
276 ui.horizontal(|ui| {
277 ui.label("Steps/frame:");
278 ui.add(egui::Slider::new(&mut self.state.steps_per_frame, 1..=50));
279 });
280
281 let using_gpu = engine.is_using_gpu();
282 ui.label(format!(
283 "Backend: {}",
284 if using_gpu { "GPU (CUDA)" } else { "CPU" }
285 ));
286 }
287
288 fn environment_section(&mut self, ui: &mut egui::Ui) {
289 ui.heading("Environment");
290
291 ui.horizontal(|ui| {
292 ui.label("Temperature (°C):");
293 ui.add(egui::Slider::new(
294 &mut self.state.temperature_c,
295 -20.0..=40.0,
296 ));
297 });
298
299 ui.horizontal(|ui| {
300 ui.label("Humidity (%):");
301 ui.add(egui::Slider::new(
302 &mut self.state.humidity_percent,
303 0.0..=100.0,
304 ));
305 });
306
307 ui.horizontal(|ui| {
308 ui.label("Medium:");
309 egui::ComboBox::from_id_salt("medium_select")
310 .selected_text(match self.state.medium {
311 Medium::Air => "Air",
312 Medium::Water => "Water",
313 Medium::Steel => "Steel",
314 Medium::Aluminum => "Aluminum",
315 Medium::Custom => "Custom",
316 })
317 .show_ui(ui, |ui| {
318 ui.selectable_value(&mut self.state.medium, Medium::Air, "Air");
319 ui.selectable_value(&mut self.state.medium, Medium::Water, "Water");
320 ui.selectable_value(&mut self.state.medium, Medium::Steel, "Steel");
321 ui.selectable_value(&mut self.state.medium, Medium::Aluminum, "Aluminum");
322 });
323 });
324 }
325
326 fn source_section(&mut self, ui: &mut egui::Ui) {
327 ui.heading("Audio Source");
328
329 ui.horizontal(|ui| {
330 ui.label("Type:");
331 egui::ComboBox::from_id_salt("source_type")
332 .selected_text(self.state.source_type_name())
333 .show_ui(ui, |ui| {
334 ui.selectable_value(&mut self.state.source_type_idx, 0, "Impulse");
335 ui.selectable_value(&mut self.state.source_type_idx, 1, "Tone");
336 ui.selectable_value(&mut self.state.source_type_idx, 2, "Chirp");
337 ui.selectable_value(&mut self.state.source_type_idx, 3, "White Noise");
338 ui.selectable_value(&mut self.state.source_type_idx, 4, "Pink Noise");
339 ui.selectable_value(&mut self.state.source_type_idx, 5, "Gaussian Pulse");
340 });
341 });
342
343 ui.label("Position:");
344 ui.horizontal(|ui| {
345 ui.label("X:");
346 ui.add(egui::Slider::new(&mut self.state.source_x, 0.0..=1.0).text(""));
347 });
348 ui.horizontal(|ui| {
349 ui.label("Y:");
350 ui.add(egui::Slider::new(&mut self.state.source_y, 0.0..=1.0).text(""));
351 });
352 ui.horizontal(|ui| {
353 ui.label("Z:");
354 ui.add(egui::Slider::new(&mut self.state.source_z, 0.0..=1.0).text(""));
355 });
356
357 ui.horizontal(|ui| {
358 ui.label("Amplitude:");
359 ui.add(egui::Slider::new(
360 &mut self.state.source_amplitude,
361 0.1..=5.0,
362 ));
363 });
364
365 if self.state.source_type_idx == 1 {
366 ui.horizontal(|ui| {
368 ui.label("Frequency (Hz):");
369 ui.add(
370 egui::Slider::new(&mut self.state.source_frequency, 20.0..=2000.0)
371 .logarithmic(true),
372 );
373 });
374 }
375
376 if ui.button("🔊 Fire Source").clicked() {
377 }
379 }
380
381 fn listener_section(&mut self, ui: &mut egui::Ui) {
382 ui.heading("Virtual Listener");
383
384 ui.checkbox(&mut self.state.show_listener, "Show listener");
385
386 ui.label("Position:");
387 ui.horizontal(|ui| {
388 ui.label("X:");
389 ui.add(egui::Slider::new(&mut self.state.listener_x, 0.0..=1.0).text(""));
390 });
391 ui.horizontal(|ui| {
392 ui.label("Y:");
393 ui.add(egui::Slider::new(&mut self.state.listener_y, 0.0..=1.0).text(""));
394 });
395 ui.horizontal(|ui| {
396 ui.label("Z:");
397 ui.add(egui::Slider::new(&mut self.state.listener_z, 0.0..=1.0).text(""));
398 });
399
400 ui.horizontal(|ui| {
401 ui.label("Yaw (°):");
402 ui.add(egui::Slider::new(
403 &mut self.state.listener_yaw,
404 -180.0..=180.0,
405 ));
406 });
407 }
408
409 fn visualization_section(&mut self, ui: &mut egui::Ui) {
410 ui.heading("Visualization");
411
412 ui.horizontal(|ui| {
413 ui.label("Color map:");
414 egui::ComboBox::from_id_salt("color_map")
415 .selected_text(match self.state.color_map_idx {
416 0 => "Blue-White-Red",
417 1 => "Cool-Warm",
418 2 => "Hot",
419 3 => "Diverging",
420 _ => "Grayscale",
421 })
422 .show_ui(ui, |ui| {
423 ui.selectable_value(&mut self.state.color_map_idx, 0, "Blue-White-Red");
424 ui.selectable_value(&mut self.state.color_map_idx, 1, "Cool-Warm");
425 ui.selectable_value(&mut self.state.color_map_idx, 2, "Hot");
426 ui.selectable_value(&mut self.state.color_map_idx, 3, "Diverging");
427 ui.selectable_value(&mut self.state.color_map_idx, 4, "Grayscale");
428 });
429 });
430
431 ui.checkbox(&mut self.state.auto_scale, "Auto-scale colors");
432
433 if !self.state.auto_scale {
434 ui.horizontal(|ui| {
435 ui.label("Max pressure:");
436 ui.add(
437 egui::Slider::new(&mut self.state.max_pressure, 0.1..=10.0).logarithmic(true),
438 );
439 });
440 }
441
442 ui.horizontal(|ui| {
443 ui.label("Slice opacity:");
444 ui.add(egui::Slider::new(&mut self.state.slice_opacity, 0.1..=1.0));
445 });
446
447 ui.checkbox(&mut self.state.show_bounding_box, "Show bounding box");
448 ui.checkbox(&mut self.state.show_floor_grid, "Show floor grid");
449 ui.checkbox(&mut self.state.show_sources, "Show source markers");
450 ui.checkbox(&mut self.state.show_info_panel, "Show info panel");
451 }
452
453 fn slice_section(&mut self, ui: &mut egui::Ui) {
454 ui.heading("Slices");
455
456 ui.horizontal(|ui| {
457 ui.checkbox(&mut self.state.show_xy_slice, "XY");
458 if self.state.show_xy_slice {
459 ui.add(egui::Slider::new(&mut self.state.xy_slice_position, 0.0..=1.0).text("Z"));
460 }
461 });
462
463 ui.horizontal(|ui| {
464 ui.checkbox(&mut self.state.show_xz_slice, "XZ");
465 if self.state.show_xz_slice {
466 ui.add(egui::Slider::new(&mut self.state.xz_slice_position, 0.0..=1.0).text("Y"));
467 }
468 });
469
470 ui.horizontal(|ui| {
471 ui.checkbox(&mut self.state.show_yz_slice, "YZ");
472 if self.state.show_yz_slice {
473 ui.add(egui::Slider::new(&mut self.state.yz_slice_position, 0.0..=1.0).text("X"));
474 }
475 });
476 }
477
478 fn info_panel(&self, ui: &mut egui::Ui, engine: &SimulationEngine) {
479 let (w, h, d) = engine.dimensions();
480 let (pw, ph, pd) = engine.grid.physical_size();
481
482 ui.label(format!("Grid: {}×{}×{} cells", w, h, d));
483 ui.label(format!("Size: {:.1}×{:.1}×{:.1} m", pw, ph, pd));
484 ui.label(format!("Step: {}", engine.step_count()));
485 ui.label(format!("Time: {:.4} s", engine.time()));
486 ui.label(format!("Max pressure: {:.4}", engine.grid.max_pressure()));
487 ui.label(format!("Total energy: {:.4}", engine.grid.total_energy()));
488
489 let speed = engine.grid.params.medium.speed_of_sound;
490 ui.label(format!("Speed of sound: {:.1} m/s", speed));
491 }
492}
493
494impl Default for SimulationControls {
495 fn default() -> Self {
496 Self::new()
497 }
498}