use egui_wgpu::RenderState;
use crate::core::backend::{ImageSpec, ItemHandle};
use crate::core::colormap::{Colormap, ColormapName};
use crate::core::plot::PlotId;
use crate::widget::high_level::{Plot2D, PlotDataError};
use crate::widget::plot_widget::PlotResponse;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ComplexMode {
Absolute,
Phase,
Real,
Imaginary,
SquareAmplitude,
Log10Amplitude,
AmplitudePhase,
}
impl ComplexMode {
pub const ALL: [ComplexMode; 7] = [
ComplexMode::Absolute,
ComplexMode::SquareAmplitude,
ComplexMode::Phase,
ComplexMode::Real,
ComplexMode::Imaginary,
ComplexMode::Log10Amplitude,
ComplexMode::AmplitudePhase,
];
pub fn label(self) -> &'static str {
match self {
ComplexMode::Absolute => "Amplitude",
ComplexMode::SquareAmplitude => "Square amplitude",
ComplexMode::Phase => "Phase",
ComplexMode::Real => "Real part",
ComplexMode::Imaginary => "Imaginary part",
ComplexMode::Log10Amplitude => "Log10(amplitude)",
ComplexMode::AmplitudePhase => "Amplitude and Phase",
}
}
pub fn is_rgba(self) -> bool {
matches!(self, ComplexMode::AmplitudePhase)
}
pub fn to_scalar(self, re: f32, im: f32) -> f32 {
match self {
ComplexMode::Absolute => re.hypot(im),
ComplexMode::Phase => im.atan2(re),
ComplexMode::Real => re,
ComplexMode::Imaginary => im,
ComplexMode::SquareAmplitude => re * re + im * im,
ComplexMode::Log10Amplitude => re.hypot(im).log10(),
ComplexMode::AmplitudePhase => 0.0,
}
}
}
pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> [u8; 3] {
let h = h.rem_euclid(1.0) * 6.0;
let sector = h.floor();
let f = h - sector;
let p = v * (1.0 - s);
let q = v * (1.0 - s * f);
let t = v * (1.0 - s * (1.0 - f));
let (r, g, b) = match sector as i32 % 6 {
0 => (v, t, p),
1 => (q, v, p),
2 => (p, v, t),
3 => (p, q, v),
4 => (t, p, v),
_ => (v, p, q),
};
[
(r * 255.0).round().clamp(0.0, 255.0) as u8,
(g * 255.0).round().clamp(0.0, 255.0) as u8,
(b * 255.0).round().clamp(0.0, 255.0) as u8,
]
}
pub fn phase_hsv_lut() -> [[u8; 4]; 256] {
let mut lut = [[0u8; 4]; 256];
for (i, entry) in lut.iter_mut().enumerate() {
let [r, g, b] = hsv_to_rgb(i as f32 / 255.0, 1.0, 1.0);
*entry = [r, g, b, 255];
}
lut
}
pub fn phase_colormap() -> Colormap {
Colormap {
lut: phase_hsv_lut(),
..Colormap::new(
ColormapName::Hsv,
-std::f64::consts::PI,
std::f64::consts::PI,
)
}
}
pub fn amplitude_phase_rgba(data: &[(f32, f32)]) -> Vec<[u8; 4]> {
let max_amp = data
.iter()
.map(|&(re, im)| re.hypot(im))
.fold(0.0f32, f32::max);
data.iter()
.map(|&(re, im)| {
let phase = im.atan2(re);
let hue = (phase + std::f32::consts::PI) / (2.0 * std::f32::consts::PI);
let value = if max_amp > 0.0 {
re.hypot(im) / max_amp
} else {
0.0
};
let [r, g, b] = hsv_to_rgb(hue, 1.0, value);
[r, g, b, 255]
})
.collect()
}
fn finite_range(values: &[f32]) -> (f64, f64) {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in values {
if v.is_finite() {
let v = v as f64;
if v < min {
min = v;
}
if v > max {
max = v;
}
}
}
if min.is_finite() && max.is_finite() && max > min {
(min, max)
} else {
(0.0, 1.0)
}
}
pub fn mode_toolbar_ui(ui: &mut egui::Ui, current: ComplexMode) -> Option<ComplexMode> {
let mut picked = None;
ui.horizontal(|ui| {
for mode in ComplexMode::ALL {
if ui.selectable_label(current == mode, mode.label()).clicked() && current != mode {
picked = Some(mode);
}
}
});
picked
}
pub struct ComplexImageView {
plot: Plot2D,
width: u32,
height: u32,
data: Vec<(f32, f32)>,
mode: ComplexMode,
image_handle: Option<ItemHandle>,
dirty: bool,
}
impl ComplexImageView {
pub fn new(render_state: &RenderState, id: PlotId) -> Self {
Self {
plot: Plot2D::new(render_state, id),
width: 0,
height: 0,
data: Vec::new(),
mode: ComplexMode::Absolute,
image_handle: None,
dirty: false,
}
}
pub fn set_data(
&mut self,
width: u32,
height: u32,
data: &[(f32, f32)],
) -> Result<(), PlotDataError> {
let expected = (width as usize).saturating_mul(height as usize);
if data.len() != expected {
return Err(PlotDataError::ImageDataLength {
expected,
actual: data.len(),
});
}
self.width = width;
self.height = height;
self.data = data.to_vec();
self.dirty = true;
Ok(())
}
pub fn mode(&self) -> ComplexMode {
self.mode
}
pub fn set_mode(&mut self, mode: ComplexMode) {
if mode != self.mode {
self.mode = mode;
self.dirty = true;
}
}
pub fn plot(&self) -> &Plot2D {
&self.plot
}
pub fn plot_mut(&mut self) -> &mut Plot2D {
&mut self.plot
}
pub fn show(&mut self, ui: &mut egui::Ui) -> PlotResponse {
if self.dirty && !self.data.is_empty() {
self.rebuild_image();
self.dirty = false;
}
self.plot.show(ui)
}
pub fn show_mode_controls(&mut self, ui: &mut egui::Ui) -> ComplexMode {
egui::ComboBox::from_label("Complex mode")
.selected_text(self.mode.label())
.show_ui(ui, |ui| {
for mode in ComplexMode::ALL {
if ui
.selectable_label(self.mode == mode, mode.label())
.clicked()
&& self.mode != mode
{
self.mode = mode;
self.dirty = true;
}
}
});
self.mode
}
pub fn show_mode_toolbar(&mut self, ui: &mut egui::Ui) -> ComplexMode {
if let Some(picked) = mode_toolbar_ui(ui, self.mode) {
self.set_mode(picked);
}
self.mode
}
fn rebuild_image(&mut self) {
if self.mode.is_rgba() {
let rgba = amplitude_phase_rgba(&self.data);
self.set_rgba_image(&rgba);
} else {
let scalar: Vec<f32> = self
.data
.iter()
.map(|&(re, im)| self.mode.to_scalar(re, im))
.collect();
let colormap = self.scalar_colormap(&scalar);
self.set_scalar_image(&scalar, colormap);
}
}
fn scalar_colormap(&self, scalar: &[f32]) -> Colormap {
if self.mode == ComplexMode::Phase {
phase_colormap()
} else {
let (vmin, vmax) = finite_range(scalar);
Colormap::viridis(vmin, vmax)
}
}
fn set_scalar_image(&mut self, scalar: &[f32], colormap: Colormap) {
if let Some(handle) = self.image_handle {
let spec = ImageSpec::scalar(self.width, self.height, scalar, colormap.clone());
if self.plot.update_image_spec(handle, spec) {
return;
}
}
let handle = self
.plot
.try_add_image(self.width, self.height, scalar, colormap)
.expect("scalar length validated by set_data");
self.image_handle = Some(handle);
}
fn set_rgba_image(&mut self, rgba: &[[u8; 4]]) {
if let Some(handle) = self.image_handle {
let spec = ImageSpec::rgba(self.width, self.height, rgba);
if self.plot.update_image_spec(handle, spec) {
return;
}
}
let handle = self
.plot
.try_add_rgba_image(self.width, self.height, rgba)
.expect("rgba length validated by set_data");
self.image_handle = Some(handle);
}
}
#[cfg(test)]
mod tests {
use super::*;
const PI: f32 = std::f32::consts::PI;
fn capture_button_rects(
ctx: &egui::Context,
current: ComplexMode,
) -> Vec<(ComplexMode, egui::Rect)> {
let mut rects = Vec::new();
let _ = ctx.run_ui(egui::RawInput::default(), |ui| {
ui.horizontal(|ui| {
for mode in ComplexMode::ALL {
let r = ui.selectable_label(current == mode, mode.label());
rects.push((mode, r.rect));
}
});
});
rects
}
fn run_toolbar(
ctx: &egui::Context,
current: ComplexMode,
raw: egui::RawInput,
) -> Option<ComplexMode> {
let mut picked = None;
let _ = ctx.run_ui(raw, |ui| {
picked = mode_toolbar_ui(ui, current);
});
picked
}
fn click_at(point: egui::Pos2) -> egui::RawInput {
egui::RawInput {
events: vec![
egui::Event::PointerMoved(point),
egui::Event::PointerButton {
pos: point,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: egui::Modifiers::default(),
},
egui::Event::PointerButton {
pos: point,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: egui::Modifiers::default(),
},
],
..Default::default()
}
}
#[test]
fn mode_toolbar_returns_none_without_a_click() {
let ctx = egui::Context::default();
let _ = run_toolbar(&ctx, ComplexMode::Absolute, egui::RawInput::default());
assert_eq!(
run_toolbar(&ctx, ComplexMode::Absolute, egui::RawInput::default()),
None
);
}
#[test]
fn mode_toolbar_click_selects_that_mode() {
let ctx = egui::Context::default();
let current = ComplexMode::Phase;
let rects = capture_button_rects(&ctx, current);
let (_, real_rect) = rects
.iter()
.find(|(m, _)| *m == ComplexMode::Real)
.copied()
.expect("Real button present");
let _ = run_toolbar(&ctx, current, egui::RawInput::default());
let picked = run_toolbar(&ctx, current, click_at(real_rect.center()));
assert_eq!(picked, Some(ComplexMode::Real));
}
#[test]
fn mode_toolbar_click_on_active_mode_is_noop() {
let ctx = egui::Context::default();
let current = ComplexMode::Absolute;
let rects = capture_button_rects(&ctx, current);
let (_, active_rect) = rects
.iter()
.find(|(m, _)| *m == ComplexMode::Absolute)
.copied()
.expect("Absolute button present");
let _ = run_toolbar(&ctx, current, egui::RawInput::default());
let picked = run_toolbar(&ctx, current, click_at(active_rect.center()));
assert_eq!(picked, None);
}
#[test]
fn absolute_is_hypot() {
assert_eq!(ComplexMode::Absolute.to_scalar(3.0, 4.0), 5.0);
assert_eq!(ComplexMode::Absolute.to_scalar(0.0, 0.0), 0.0);
}
#[test]
fn phase_is_atan2() {
assert_eq!(ComplexMode::Phase.to_scalar(1.0, 0.0), 0.0);
assert!((ComplexMode::Phase.to_scalar(0.0, 1.0) - PI / 2.0).abs() < 1e-6);
assert!((ComplexMode::Phase.to_scalar(-1.0, 0.0) - PI).abs() < 1e-6);
assert!((ComplexMode::Phase.to_scalar(0.0, -1.0) + PI / 2.0).abs() < 1e-6);
}
#[test]
fn real_is_re() {
assert_eq!(ComplexMode::Real.to_scalar(3.0, 4.0), 3.0);
assert_eq!(ComplexMode::Real.to_scalar(-2.5, 9.0), -2.5);
}
#[test]
fn imaginary_is_im() {
assert_eq!(ComplexMode::Imaginary.to_scalar(3.0, 4.0), 4.0);
assert_eq!(ComplexMode::Imaginary.to_scalar(-2.5, -9.0), -9.0);
}
#[test]
fn square_amplitude_is_re2_plus_im2() {
assert_eq!(ComplexMode::SquareAmplitude.to_scalar(3.0, 4.0), 25.0);
assert_eq!(ComplexMode::SquareAmplitude.to_scalar(0.0, 0.0), 0.0);
}
#[test]
fn log10_amplitude_is_log10_of_hypot() {
assert!((ComplexMode::Log10Amplitude.to_scalar(100.0, 0.0) - 2.0).abs() < 1e-6);
assert_eq!(ComplexMode::Log10Amplitude.to_scalar(1.0, 0.0), 0.0);
}
#[test]
fn amplitude_phase_returns_zero_scalar() {
assert_eq!(ComplexMode::AmplitudePhase.to_scalar(3.0, 4.0), 0.0);
}
#[test]
fn amplitude_phase_hue_at_zero_phase_is_cyan() {
let rgba = amplitude_phase_rgba(&[(1.0, 0.0)]);
assert_eq!(rgba, vec![[0, 255, 255, 255]]);
}
#[test]
fn amplitude_phase_hue_at_plus_half_pi() {
let rgba = amplitude_phase_rgba(&[(0.0, 1.0)]);
assert_eq!(rgba, vec![[128, 0, 255, 255]]);
}
#[test]
fn amplitude_phase_hue_at_minus_pi() {
let rgba = amplitude_phase_rgba(&[(-1.0, 0.0)]);
let rgba_neg = amplitude_phase_rgba(&[(-1.0, -0.0)]);
assert_eq!(rgba, vec![[255, 0, 0, 255]]);
assert_eq!(rgba_neg, vec![[255, 0, 0, 255]]);
}
#[test]
fn amplitude_phase_value_scales_with_amplitude() {
let rgba = amplitude_phase_rgba(&[(2.0, 0.0), (1.0, 0.0)]);
assert_eq!(rgba[0], [0, 255, 255, 255]);
assert_eq!(rgba[1], [0, 128, 128, 255]);
}
#[test]
fn amplitude_phase_empty_is_empty() {
assert!(amplitude_phase_rgba(&[]).is_empty());
}
#[test]
fn amplitude_phase_zero_amplitude_is_black() {
let rgba = amplitude_phase_rgba(&[(0.0, 0.0), (0.0, 0.0)]);
assert_eq!(rgba, vec![[0, 0, 0, 255], [0, 0, 0, 255]]);
}
#[test]
fn hsv_lut_endpoints_are_red() {
let lut = phase_hsv_lut();
assert_eq!(lut[0], [255, 0, 0, 255]);
assert_eq!(hsv_to_rgb(1.0, 1.0, 1.0), [255, 0, 0]);
}
#[test]
fn phase_colormap_range_is_minus_pi_to_pi() {
let cm = phase_colormap();
assert_eq!(cm.vmin, -std::f64::consts::PI);
assert_eq!(cm.vmax, std::f64::consts::PI);
}
#[test]
fn finite_range_ignores_non_finite_and_falls_back() {
assert_eq!(finite_range(&[1.0, 5.0, 3.0]), (1.0, 5.0));
assert_eq!(finite_range(&[2.0, 2.0]), (0.0, 1.0));
assert_eq!(finite_range(&[f32::NAN, f32::INFINITY, 4.0]), (0.0, 1.0));
assert_eq!(finite_range(&[f32::NAN]), (0.0, 1.0));
}
}