use egui_wgpu::RenderState;
use crate::core::backend::{ImageSpec, ItemHandle};
use crate::core::colormap::{AutoscaleMode, Colormap, ColormapName};
use crate::core::plot::PlotId;
use crate::widget::high_level::{Plot2D, PlotDataError};
use crate::widget::plot_widget::PlotResponse;
pub use crate::core::complex::ComplexMode;
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,
)
}
}
fn scalar_mode_colormap(mode: ComplexMode, base: &Colormap, scalar: &[f32]) -> Colormap {
if mode == ComplexMode::Phase {
phase_colormap()
} else {
let data: Vec<f64> = scalar.iter().map(|&v| f64::from(v)).collect();
base.resolved(AutoscaleMode::MinMax, &data)
}
}
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()
}
pub const DEFAULT_AMPLITUDE_DELTA: f32 = 2.0;
pub fn amplitude_phase_log_rgba(
data: &[(f32, f32)],
max_amplitude: Option<f32>,
delta: f32,
) -> Vec<[u8; 4]> {
if data.is_empty() {
return Vec::new();
}
let logs: Vec<f32> = data
.iter()
.map(|&(re, im)| {
let mut a = re.hypot(im);
if let Some(m) = max_amplitude
&& a > m
{
a = m;
}
(a + 1e-20_f32).log10()
})
.collect();
let log_max = logs.iter().copied().fold(f32::NEG_INFINITY, f32::max);
data.iter()
.zip(logs)
.map(|(&(re, im), log_a)| {
let a = log_a - (log_max - delta);
let value = (a / delta).clamp(0.0, 1.0);
let phase = im.atan2(re);
let hue = (phase + std::f32::consts::PI) / (2.0 * std::f32::consts::PI);
let [r, g, b] = hsv_to_rgb(hue, 1.0, value);
[r, g, b, 255]
})
.collect()
}
fn data_max_amplitude(data: &[(f32, f32)]) -> f32 {
data.iter()
.map(|&(re, im)| re.hypot(im))
.filter(|a| a.is_finite())
.fold(0.0_f32, f32::max)
}
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,
colormap: Colormap,
max_amplitude: Option<f32>,
delta: f32,
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,
colormap: Colormap::autoscale(ColormapName::Gray),
max_amplitude: None,
delta: DEFAULT_AMPLITUDE_DELTA,
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 set_amplitude_range_info(&mut self, max_amplitude: Option<f32>, delta: f32) {
if self.max_amplitude != max_amplitude || self.delta != delta {
self.max_amplitude = max_amplitude;
self.delta = delta;
self.dirty = true;
}
}
pub fn amplitude_range_info(&self) -> (Option<f32>, f32) {
(self.max_amplitude, self.delta)
}
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
}
pub fn show_amplitude_range_controls(&mut self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
let mut autoscale = self.max_amplitude.is_none();
if ui
.checkbox(&mut autoscale, "autoscale")
.on_hover_text("Autoscale the displayed max to the data's max amplitude")
.changed()
{
let max = (!autoscale).then(|| data_max_amplitude(&self.data));
self.set_amplitude_range_info(max, self.delta);
}
ui.label("Displayed Max.:");
let mut max_val = self
.max_amplitude
.unwrap_or_else(|| data_max_amplitude(&self.data));
if ui
.add_enabled(!autoscale, egui::DragValue::new(&mut max_val).speed(0.1))
.changed()
&& !autoscale
{
self.set_amplitude_range_info(Some(max_val.max(0.0)), self.delta);
}
ui.label("Displayed delta (log10 unit):");
let mut delta = self.delta;
if ui
.add(egui::DragValue::new(&mut delta).speed(0.1))
.changed()
{
self.set_amplitude_range_info(self.max_amplitude, delta.max(1.0));
}
});
}
fn rebuild_image(&mut self) {
if self.mode.is_rgba() {
let rgba = match self.mode {
ComplexMode::Log10AmplitudePhase => {
amplitude_phase_log_rgba(&self.data, self.max_amplitude, self.delta)
}
_ => 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 {
scalar_mode_colormap(self.mode, &self.colormap, scalar)
}
pub fn colormap(&self) -> &Colormap {
&self.colormap
}
pub fn set_colormap(&mut self, colormap: Colormap) {
self.colormap = colormap;
self.dirty = true;
}
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 log10_amplitude_phase_mode_is_rgba_with_no_scalar() {
assert!(ComplexMode::Log10AmplitudePhase.is_rgba());
assert_eq!(ComplexMode::Log10AmplitudePhase.to_scalar(3.0, 4.0), 0.0);
assert!(ComplexMode::ALL.contains(&ComplexMode::Log10AmplitudePhase));
assert_eq!(
ComplexMode::Log10AmplitudePhase.label(),
"Log10 Amplitude and Phase"
);
}
#[test]
fn log_composite_empty_is_empty() {
assert!(amplitude_phase_log_rgba(&[], None, DEFAULT_AMPLITUDE_DELTA).is_empty());
}
#[test]
fn log_composite_normalizes_over_delta_decades() {
let rgba = amplitude_phase_log_rgba(&[(100.0, 0.0), (10.0, 0.0)], None, 2.0);
assert_eq!(rgba[0], [0, 255, 255, 255]);
assert_eq!(rgba[1], [0, 128, 128, 255]);
}
#[test]
fn log_composite_floor_below_window_is_zero() {
let rgba = amplitude_phase_log_rgba(&[(100.0, 0.0), (1.0, 0.0)], None, 2.0);
assert_eq!(rgba[0], [0, 255, 255, 255]);
assert_eq!(rgba[1], [0, 0, 0, 255]);
}
#[test]
fn log_composite_clamps_to_displayed_max() {
let data = [(1000.0, 0.0), (10.0, 0.0)];
let uncapped = amplitude_phase_log_rgba(&data, None, 2.0);
assert_eq!(uncapped[1], [0, 0, 0, 255]);
let capped = amplitude_phase_log_rgba(&data, Some(100.0), 2.0);
assert_eq!(capped[0], [0, 255, 255, 255]); assert_eq!(capped[1], [0, 128, 128, 255]); }
#[test]
fn log_composite_uniform_amplitude_is_full_value() {
let rgba = amplitude_phase_log_rgba(&[(5.0, 0.0), (5.0, 0.0)], None, 2.0);
assert_eq!(rgba, vec![[0, 255, 255, 255], [0, 255, 255, 255]]);
}
#[test]
fn log_composite_zero_amplitude_matches_silx_degenerate() {
let rgba = amplitude_phase_log_rgba(&[(0.0, 0.0), (0.0, 0.0)], None, 2.0);
assert_eq!(rgba, vec![[0, 255, 255, 255], [0, 255, 255, 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 scalar_mode_default_colormap_tracks_the_image() {
let base = Colormap::autoscale(ColormapName::Gray);
let cm = scalar_mode_colormap(ComplexMode::Absolute, &base, &[2.0, 8.0, 5.0]);
assert_eq!((cm.vmin, cm.vmax), (2.0, 8.0));
assert!(cm.is_autoscale(), "flags preserved for the next rebuild");
}
#[test]
fn scalar_mode_pinned_colormap_survives_data_change() {
let base = Colormap::new(ColormapName::Viridis, 0.0, 100.0);
let cm = scalar_mode_colormap(ComplexMode::Real, &base, &[3.0, 7.0]);
assert_eq!((cm.vmin, cm.vmax), (0.0, 100.0));
let cm2 = scalar_mode_colormap(ComplexMode::Real, &base, &[-50.0, 900.0]);
assert_eq!((cm2.vmin, cm2.vmax), (0.0, 100.0));
}
#[test]
fn scalar_mode_phase_ignores_the_base_colormap() {
let base = Colormap::new(ColormapName::Viridis, 0.0, 1.0);
let cm = scalar_mode_colormap(ComplexMode::Phase, &base, &[100.0, 200.0]);
assert_eq!(cm.vmin, -std::f64::consts::PI);
assert_eq!(cm.vmax, std::f64::consts::PI);
assert_eq!(cm.lut, phase_colormap().lut);
}
#[test]
fn data_max_amplitude_is_finite_max_modulus() {
assert_eq!(
data_max_amplitude(&[(3.0, 4.0), (0.0, 0.0), (1.0, 1.0)]),
5.0
);
assert_eq!(data_max_amplitude(&[]), 0.0);
assert_eq!(
data_max_amplitude(&[(f32::NAN, 0.0), (f32::INFINITY, 0.0), (6.0, 8.0)]),
10.0
);
}
}