use std::cell::RefCell;
use std::rc::Rc;
use egui_kittest::Harness;
use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state, default_wgpu_setup};
use rsplot::egui;
use rsplot::egui::Color32;
use rsplot::{
Colormap, ProfileLabels, ProfileMethod, ProfileMode, Roi, StackProfileDimension, StackView,
YAxis,
};
fn sample_volume() -> (Vec<f32>, [usize; 3]) {
let shape = [2usize, 3, 4];
let [d0, d1, d2] = shape;
let mut data = vec![0.0f32; d0 * d1 * d2];
for i in 0..d0 {
for j in 0..d1 {
for k in 0..d2 {
data[(i * d1 + j) * d2 + k] = (100 * i + 10 * j + k) as f32;
}
}
}
(data, shape)
}
fn harness() -> (Rc<RefCell<StackView>>, Harness<'static>) {
let rs = create_render_state(default_wgpu_setup());
rsplot::install(&rs);
let mut view = StackView::new(&rs, 0);
let (data, shape) = sample_volume();
view.set_volume(data, shape, Colormap::viridis(0.0, 200.0))
.expect("volume matches shape");
let app = Rc::new(RefCell::new(view));
let app_ui = app.clone();
let renderer = WgpuTestRenderer::from_render_state(rs.clone());
let mut harness = Harness::builder()
.with_size(egui::vec2(400.0, 400.0))
.with_pixels_per_point(1.0)
.renderer(renderer)
.build_ui(move |ui| {
app_ui.borrow_mut().show(ui);
});
harness.step();
harness.step();
(app, harness)
}
fn drag(harness: &mut Harness<'static>, p0: egui::Pos2, p1: egui::Pos2) {
harness.drag_at(p0);
harness.step();
for t in [0.2f32, 0.5, 0.8, 1.0] {
harness.hover_at(p0 + (p1 - p0) * t);
harness.step();
}
harness.drop_at(p1);
harness.step();
harness.step();
}
#[test]
fn dragging_a_horizontal_line_opens_the_2d_stacked_profile_window() {
let (app, mut harness) = harness();
app.borrow_mut().set_profile_mode(ProfileMode::Horizontal);
app.borrow_mut()
.set_profile_dimension(StackProfileDimension::TwoD);
assert!(!app.borrow().stack_profile_window().is_open());
assert!(!app.borrow().profile_window().is_open());
let p0 = app
.borrow()
.data_to_pixel(0.5, 1.5, YAxis::Left)
.expect("transform cached after a frame");
let p1 = app
.borrow()
.data_to_pixel(3.5, 1.5, YAxis::Left)
.expect("transform cached after a frame");
drag(&mut harness, p0, p1);
assert!(
app.borrow().stack_profile_window().is_open(),
"dragging in 2D mode must open the stacked-profile image window"
);
assert!(!app.borrow().profile_window().is_open());
let profile = app
.borrow()
.stack_aligned_profile(1.0, 1, true, ProfileMethod::Mean)
.expect("horizontal stack profile over the volume");
assert_eq!(profile.frame_count, 2);
assert_eq!(profile.profile_len, 4);
assert_eq!(
profile.values,
vec![10.0, 11.0, 12.0, 13.0, 110.0, 111.0, 112.0, 113.0]
);
}
#[test]
fn dragging_in_1d_mode_opens_the_curve_profile_window() {
let (app, mut harness) = harness();
assert_eq!(
app.borrow().profile_dimension(),
StackProfileDimension::OneD
);
app.borrow_mut().set_profile_mode(ProfileMode::Horizontal);
let p0 = app
.borrow()
.data_to_pixel(0.5, 1.5, YAxis::Left)
.expect("transform cached");
let p1 = app
.borrow()
.data_to_pixel(3.5, 1.5, YAxis::Left)
.expect("transform cached");
drag(&mut harness, p0, p1);
assert!(
app.borrow().profile_window().is_open(),
"dragging in 1D mode must open the current-frame curve profile window"
);
assert!(!app.borrow().stack_profile_window().is_open());
}
#[test]
fn switching_dimension_closes_the_inactive_window() {
let (app, mut harness) = harness();
app.borrow_mut().set_profile_mode(ProfileMode::Horizontal);
app.borrow_mut()
.set_profile_dimension(StackProfileDimension::TwoD);
let p0 = app
.borrow()
.data_to_pixel(0.5, 1.5, YAxis::Left)
.expect("transform cached");
let p1 = app
.borrow()
.data_to_pixel(3.5, 1.5, YAxis::Left)
.expect("transform cached");
drag(&mut harness, p0, p1);
assert!(app.borrow().stack_profile_window().is_open());
app.borrow_mut()
.set_profile_dimension(StackProfileDimension::OneD);
assert!(!app.borrow().stack_profile_window().is_open());
}
#[test]
fn two_d_profile_honors_the_shared_width_and_method() {
let (app, _harness) = harness();
app.borrow_mut().set_profile_mode(ProfileMode::Horizontal);
app.borrow_mut()
.set_profile_dimension(StackProfileDimension::TwoD);
app.borrow_mut().profile_window_mut().set_line_width(3);
app.borrow_mut()
.profile_window_mut()
.set_method(ProfileMethod::Sum);
assert!(app.borrow_mut().show_profile((0.5, 1.5), (3.5, 1.5)));
let expected = app
.borrow()
.stack_aligned_profile(1.0, 3, true, ProfileMethod::Sum)
.expect("horizontal stack profile");
let width1_mean = app
.borrow()
.stack_aligned_profile(1.0, 1, true, ProfileMethod::Mean)
.expect("horizontal stack profile");
let view = app.borrow();
let got = view
.stack_profile_window()
.profile()
.expect("2D window has a profile after show_profile");
assert_eq!(
*got, expected,
"2D profile must use the shared width/method"
);
assert_ne!(
*got, width1_mean,
"2D profile must not be the old hardcoded width-1 Mean"
);
}
#[test]
fn show_profile_returns_true_for_a_line_over_the_volume() {
let (app, _harness) = harness();
app.borrow_mut().set_profile_mode(ProfileMode::Line);
app.borrow_mut()
.set_profile_dimension(StackProfileDimension::TwoD);
assert!(
app.borrow_mut().show_profile((0.5, 0.5), (3.5, 2.5)),
"a line profile over a loaded volume must produce a 2D stacked profile"
);
assert!(app.borrow().stack_profile_window().is_open());
}
#[test]
fn width_and_method_edits_recompute_from_the_retained_source() {
let (app, _harness) = harness();
let ramp: Vec<f32> = (0..3)
.flat_map(|r| (0..3).map(move |c| (r * 10 + c) as f32))
.collect();
let mut view = app.borrow_mut();
let pw = view.profile_window_mut();
pw.update_profile(
3,
3,
&ramp,
&Roi::HRange { y: (1.0, 1.0) },
"Columns",
"Rows",
);
assert_eq!(pw.active_profile_values(), vec![vec![10.0, 11.0, 12.0]]);
pw.set_method(ProfileMethod::Sum);
assert_eq!(pw.active_profile_values(), vec![vec![10.0, 11.0, 12.0]]);
pw.set_line_width(3);
assert_eq!(pw.active_profile_values(), vec![vec![30.0, 33.0, 36.0]]);
}
#[test]
fn scrubbing_frames_recomputes_the_current_frame_profile() {
let (app, mut harness) = harness();
app.borrow_mut().set_profile_mode(ProfileMode::Horizontal);
let p0 = app
.borrow()
.data_to_pixel(0.5, 1.0, YAxis::Left)
.expect("transform cached");
let p1 = app
.borrow()
.data_to_pixel(3.5, 1.0, YAxis::Left)
.expect("transform cached");
drag(&mut harness, p0, p1);
assert!(app.borrow().profile_window().is_open());
let before = app.borrow().profile_window().active_profile_values();
assert!(!before.is_empty(), "a drag must retain an image-ROI source");
app.borrow_mut().set_frame(1);
harness.step();
let after = app.borrow().profile_window().active_profile_values();
let expected: Vec<Vec<f64>> = before
.iter()
.map(|c| c.iter().map(|v| v + 100.0).collect())
.collect();
assert_eq!(after, expected, "frame change must recompute the profile");
assert_ne!(after, before);
}
#[test]
fn precomputed_curve_clears_the_retained_source() {
let (app, _harness) = harness();
let ramp: Vec<f32> = (0..3)
.flat_map(|r| (0..3).map(move |c| (r * 10 + c) as f32))
.collect();
let mut view = app.borrow_mut();
let pw = view.profile_window_mut();
pw.update_profile(
3,
3,
&ramp,
&Roi::HRange { y: (1.0, 1.0) },
"Columns",
"Rows",
);
assert!(!pw.active_profile_values().is_empty());
pw.set_profile_curve(
"scatter",
Color32::RED,
vec![0.0, 1.0],
vec![5.0, 6.0],
&ProfileLabels {
title: "X = 0".to_string(),
x_label: "X".to_string(),
y_label: "Profile".to_string(),
},
);
assert!(
pw.active_profile_values().is_empty(),
"a precomputed curve must clear the image-ROI source"
);
pw.set_line_width(3);
assert!(pw.active_profile_values().is_empty());
}
#[test]
fn two_d_profile_requires_a_volume_not_flat_frames() {
let rs = create_render_state(default_wgpu_setup());
rsplot::install(&rs);
let mut view = StackView::new(&rs, 0);
let frames = vec![vec![1.0f32; 4 * 3], vec![2.0f32; 4 * 3]];
view.set_stack(4, 3, frames, Colormap::viridis(0.0, 2.0))
.expect("uniform frames");
view.set_profile_mode(ProfileMode::Horizontal);
view.set_profile_dimension(StackProfileDimension::TwoD);
assert!(
!view.show_profile((0.5, 1.5), (3.5, 1.5)),
"2D stacked profile must be unavailable without a loaded volume"
);
assert!(!view.stack_profile_window().is_open());
}