use egui::Color32;
use egui_wgpu::RenderState;
use crate::core::backend::ItemHandle;
use crate::core::plot::PlotId;
use crate::core::roi::Roi;
use crate::render::gpu_curve::CurveData;
use crate::widget::high_level::{
Plot1D, ProfileMethod, aligned_profile_values, line_profile_band, rect_profile_values,
};
struct ProfileCurve {
label: &'static str,
color: Color32,
x: Vec<f64>,
y: Vec<f64>,
}
fn horizontal_profile_xy(
width: u32,
height: u32,
data: &[f32],
row: f64,
line_width: u32,
method: ProfileMethod,
) -> Option<(Vec<f64>, Vec<f64>)> {
aligned_profile_values(width, height, data, row, line_width, true, method)
.ok()
.map(|y| {
let x = (0..width as usize).map(|i| i as f64).collect();
(x, y)
})
}
fn vertical_profile_xy(
width: u32,
height: u32,
data: &[f32],
col: f64,
line_width: u32,
method: ProfileMethod,
) -> Option<(Vec<f64>, Vec<f64>)> {
aligned_profile_values(width, height, data, col, line_width, false, method)
.ok()
.map(|y| {
let x = (0..height as usize).map(|i| i as f64).collect();
(x, y)
})
}
fn profiles_for_roi(
width: u32,
height: u32,
data: &[f32],
roi: &Roi,
line_width: u32,
method: ProfileMethod,
) -> Vec<ProfileCurve> {
match roi {
Roi::Line { start, end } => {
line_profile_band(width, height, data, *start, *end, line_width, method)
.ok()
.map(|(x, y)| ProfileCurve {
label: "profile",
color: Color32::YELLOW,
x,
y,
})
.into_iter()
.collect()
}
Roi::Rect { x, y } => {
rect_profile_values(width, height, data, (x.0, x.1, y.0, y.1), true, method)
.ok()
.map(|(x, y)| ProfileCurve {
label: "profile",
color: Color32::YELLOW,
x,
y,
})
.into_iter()
.collect()
}
Roi::HRange { y } => {
let row = (y.0 + y.1) / 2.0;
horizontal_profile_xy(width, height, data, row, line_width, method)
.map(|(x, y)| ProfileCurve {
label: "profile",
color: Color32::YELLOW,
x,
y,
})
.into_iter()
.collect()
}
Roi::VRange { x } => {
let col = (x.0 + x.1) / 2.0;
vertical_profile_xy(width, height, data, col, line_width, method)
.map(|(x, y)| ProfileCurve {
label: "profile",
color: Color32::YELLOW,
x,
y,
})
.into_iter()
.collect()
}
Roi::Cross { center } => {
let (cx, cy) = *center;
let h =
horizontal_profile_xy(width, height, data, cy, line_width, method).map(|(x, y)| {
ProfileCurve {
label: "h profile",
color: Color32::YELLOW,
x,
y,
}
});
let v =
vertical_profile_xy(width, height, data, cx, line_width, method).map(|(x, y)| {
ProfileCurve {
label: "v profile",
color: Color32::from_rgb(0, 200, 255),
x,
y,
}
});
[h, v].into_iter().flatten().collect()
}
_ => Vec::new(),
}
}
pub struct ProfileWindow {
plot: Plot1D,
curve_handles: Vec<ItemHandle>,
window_id: egui::Id,
open: bool,
line_width: u32,
method: ProfileMethod,
size: egui::Vec2,
placement: Option<egui::Pos2>,
remembered_pos: Option<egui::Pos2>,
}
impl ProfileWindow {
pub fn new(render_state: &RenderState, plot_id: PlotId) -> Self {
let mut plot = Plot1D::new(render_state, plot_id);
plot.set_graph_title("Profile");
Self {
plot,
curve_handles: Vec::new(),
window_id: egui::Id::new(plot_id).with("profile_window"),
open: false,
line_width: 1,
method: ProfileMethod::Mean,
size: egui::vec2(420.0, 320.0),
placement: None,
remembered_pos: None,
}
}
pub fn line_width(&self) -> u32 {
self.line_width
}
pub fn set_line_width(&mut self, width: u32) {
self.line_width = width.max(1);
}
pub fn method(&self) -> ProfileMethod {
self.method
}
pub fn set_method(&mut self, method: ProfileMethod) {
self.method = method;
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn set_open(&mut self, open: bool) {
if !open {
self.placement = None;
}
self.open = open;
}
pub fn update_profile(&mut self, width: u32, height: u32, data: &[f32], roi: &Roi) {
let curves = profiles_for_roi(width, height, data, roi, self.line_width, self.method);
self.set_curves(curves);
}
pub fn set_profile_curve(
&mut self,
label: &'static str,
color: Color32,
x: Vec<f64>,
y: Vec<f64>,
) {
if x.is_empty() {
return;
}
self.set_curves(vec![ProfileCurve { label, color, x, y }]);
}
fn set_curves(&mut self, curves: Vec<ProfileCurve>) {
if curves.is_empty() {
return;
}
if self.curve_handles.len() != curves.len() {
for handle in self.curve_handles.drain(..) {
self.plot.remove(handle);
}
}
for (i, c) in curves.into_iter().enumerate() {
if let Some(&handle) = self.curve_handles.get(i) {
let curve = CurveData::new(c.x, c.y, c.color);
self.plot.update_curve_data(handle, &curve);
} else {
let handle = self
.plot
.add_curve_with_legend(&c.x, &c.y, c.color, c.label);
self.curve_handles.push(handle);
}
}
self.plot.reset_zoom_to_data();
}
pub fn show(&mut self, ctx: &egui::Context) {
if !self.open {
return;
}
if self.placement.is_none() {
self.placement = self
.remembered_pos
.or_else(|| crate::widget::detached::beside_main_window(ctx, self.size));
}
let viewport_id = egui::ViewportId::from_hash_of(self.window_id);
let mut builder = egui::ViewportBuilder::default()
.with_title("Profile")
.with_inner_size(self.size);
if let Some(pos) = self.placement {
builder = builder.with_position(pos);
}
let mut close_requested = false;
let mut live_pos = None;
ctx.show_viewport_immediate(viewport_id, builder, |ui, _class| {
ui.horizontal(|ui| {
ui.label("Width:");
let mut width = self.line_width;
if ui
.add(
egui::DragValue::new(&mut width)
.speed(1.0)
.range(1..=u32::MAX),
)
.on_hover_text("Profile band width in pixels")
.changed()
{
self.set_line_width(width);
}
ui.separator();
ui.label("Method:");
egui::ComboBox::from_id_salt("profile_method")
.selected_text(match self.method {
ProfileMethod::Mean => "Mean",
ProfileMethod::Sum => "Sum",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.method, ProfileMethod::Mean, "Mean");
ui.selectable_value(&mut self.method, ProfileMethod::Sum, "Sum");
});
});
ui.separator();
self.plot.show(ui);
ui.ctx().input(|i| {
let vp = i.viewport();
if vp.close_requested() {
close_requested = true;
}
live_pos = vp.outer_rect.map(|r| r.min);
});
});
if let Some(pos) = live_pos {
self.remembered_pos = Some(pos);
}
if close_requested {
self.open = false;
self.placement = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ramp_3x3() -> Vec<f32> {
let mut v = Vec::with_capacity(9);
for row in 0..3 {
for col in 0..3 {
v.push((row * 10 + col) as f32);
}
}
v
}
#[test]
fn profile_for_roi_hrange_width_and_method() {
let data = ramp_3x3();
let curves = profiles_for_roi(
3,
3,
&data,
&Roi::HRange { y: (1.0, 1.0) },
1,
ProfileMethod::Mean,
);
assert_eq!(curves.len(), 1);
assert_eq!(curves[0].y, vec![10.0, 11.0, 12.0]);
let curves = profiles_for_roi(
3,
3,
&data,
&Roi::HRange { y: (1.0, 1.0) },
3,
ProfileMethod::Sum,
);
assert_eq!(curves.len(), 1);
assert_eq!(curves[0].y, vec![30.0, 33.0, 36.0]);
}
#[test]
fn profile_for_roi_cross_yields_horizontal_and_vertical_curves() {
let data = ramp_3x3();
let curves = profiles_for_roi(
3,
3,
&data,
&Roi::Cross { center: (1.0, 1.0) },
1,
ProfileMethod::Mean,
);
assert_eq!(curves.len(), 2);
assert_eq!(curves[0].label, "h profile");
assert_eq!(curves[0].y, vec![10.0, 11.0, 12.0]);
assert_eq!(curves[1].label, "v profile");
assert_eq!(curves[1].y, vec![1.0, 11.0, 21.0]);
}
#[test]
fn profile_for_roi_returns_empty_for_unsupported_kind() {
let data = ramp_3x3();
assert!(
profiles_for_roi(
3,
3,
&data,
&Roi::Point { x: 1.0, y: 1.0 },
1,
ProfileMethod::Mean,
)
.is_empty()
);
}
}