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::{PlotWidget, SnappingMode};
fn plot_rendered(
build: impl FnOnce(&mut PlotWidget),
) -> (Rc<RefCell<PlotWidget>>, Harness<'static>) {
plot_rendered_at(1.0, build)
}
fn plot_rendered_at(
pixels_per_point: f32,
build: impl FnOnce(&mut PlotWidget),
) -> (Rc<RefCell<PlotWidget>>, Harness<'static>) {
let rs = create_render_state(default_wgpu_setup());
rsplot::install(&rs);
let mut plot = PlotWidget::new(&rs, 0);
build(&mut plot);
let plot = Rc::new(RefCell::new(plot));
let plot_ui = plot.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(pixels_per_point)
.renderer(renderer)
.build_ui(move |ui| {
plot_ui.borrow_mut().show(ui);
});
harness.step();
harness.step();
(plot, harness)
}
fn cursor_at_pixel_offset(
plot: &PlotWidget,
at: [f64; 2],
dir_px: [f32; 2],
offset_px: f32,
) -> [f64; 2] {
let v = plot
.data_to_pixel(at[0], at[1], rsplot::YAxis::Left)
.expect("cached transform");
let norm = (dir_px[0] * dir_px[0] + dir_px[1] * dir_px[1]).sqrt();
let p = egui::pos2(
v.x + dir_px[0] / norm * offset_px,
v.y + dir_px[1] / norm * offset_px,
);
let (x, y) = plot
.pixel_to_data(p, rsplot::YAxis::Left)
.expect("cached transform");
[x, y]
}
fn plot_with_line() -> (Rc<RefCell<PlotWidget>>, Harness<'static>) {
plot_rendered(|plot| {
let xs: Vec<f64> = (0..=10).map(|i| i as f64).collect();
let ys = xs.clone();
plot.add_curve(&xs, &ys, egui::Color32::from_rgb(0, 120, 255));
})
}
#[test]
fn snap_lands_on_the_nearest_curve_vertex() {
let (plot, _harness) = plot_with_line();
let plot = plot.borrow();
let snap = plot
.snap_cursor([5.0, 5.0], SnappingMode::CURVE)
.expect("a cursor on the (5,5) vertex must snap to it");
assert!(
(snap.data[0] - 5.0).abs() < 1e-9 && (snap.data[1] - 5.0).abs() < 1e-9,
"snapped to the wrong vertex: {:?}",
snap.data
);
}
#[test]
fn snap_returns_none_when_no_vertex_is_within_the_threshold() {
let (plot, _harness) = plot_with_line();
let plot = plot.borrow();
assert!(
plot.snap_cursor([5.5, 4.5], SnappingMode::CURVE).is_none(),
"a cursor far from every vertex must not snap"
);
}
#[test]
fn disabled_mode_never_snaps() {
let (plot, _harness) = plot_with_line();
let plot = plot.borrow();
assert!(
plot.snap_cursor([5.0, 5.0], SnappingMode::DISABLED)
.is_none(),
"SnappingMode::DISABLED must never snap"
);
assert!(
plot.snap_cursor([5.0, 5.0], SnappingMode::SCATTER)
.is_none(),
"a scatter-only mode must not snap a curve vertex"
);
}
#[test]
fn scatter_points_snap_under_scatter_mode_only() {
let (plot, _harness) = plot_rendered(|plot| {
let xs: Vec<f64> = (0..=10).map(|i| i as f64).collect();
let ys = xs.clone();
plot.add_scatter(&xs, &ys, egui::Color32::from_rgb(255, 120, 0));
});
let plot = plot.borrow();
let snap = plot
.snap_cursor([5.0, 5.0], SnappingMode::SCATTER)
.expect("SCATTER mode must snap to a scatter point");
assert!(
(snap.data[0] - 5.0).abs() < 1e-9 && (snap.data[1] - 5.0).abs() < 1e-9,
"snapped to the wrong scatter point: {:?}",
snap.data
);
assert!(
plot.snap_cursor([5.0, 5.0], SnappingMode::CURVE).is_none(),
"CURVE mode must not snap a scatter point (kind filtering)"
);
}
#[test]
fn histogram_snaps_to_bin_centre_and_count() {
let (plot, _harness) = plot_rendered(|plot| {
plot.add_histogram(
&[0.0, 2.0, 4.0, 6.0, 8.0, 10.0],
&[1.0, 3.0, 5.0, 7.0, 9.0],
egui::Color32::from_rgb(0, 180, 90),
)
.expect("N + 1 edges for N counts");
});
let plot = plot.borrow();
let snap = plot
.snap_cursor([5.0, 5.0], SnappingMode::CURVE)
.expect("a cursor on a bin centre must snap to it");
assert!(
(snap.data[0] - 5.0).abs() < 1e-9 && (snap.data[1] - 5.0).abs() < 1e-9,
"snapped to the wrong point: {:?}",
snap.data
);
}
#[test]
fn filled_bar_interior_snaps_far_from_the_apex() {
let (plot, _harness) = plot_rendered(|plot| {
plot.add_histogram(
&[0.0, 2.0, 4.0, 6.0, 8.0, 10.0],
&[2.0, 6.0, 4.0, 8.0, 9.0],
egui::Color32::from_rgb(0, 180, 90),
)
.expect("N + 1 edges for N counts");
});
let plot = plot.borrow();
let snap = plot
.snap_cursor([9.0, 2.0], SnappingMode::CURVE)
.expect("a cursor inside a filled bar must area-pick it");
assert!(
(snap.data[0] - 9.0).abs() < 1e-9 && (snap.data[1] - 9.0).abs() < 1e-9,
"must snap to the bin centre + count: {:?}",
snap.data
);
assert_eq!(snap.index, 4, "picked the wrong bin");
}
#[test]
fn picked_histogram_outranks_a_nearer_curve_vertex() {
let (plot, _harness) = plot_rendered(|plot| {
let xs: Vec<f64> = (0..=10).map(|i| i as f64).collect();
let ys = xs.clone();
plot.add_curve(&xs, &ys, egui::Color32::from_rgb(0, 120, 255));
plot.add_histogram(
&[0.0, 2.0, 4.0, 6.0, 8.0, 10.0],
&[2.0, 6.0, 4.0, 8.0, 9.0],
egui::Color32::from_rgb(0, 180, 90),
)
.expect("N + 1 edges for N counts");
});
let plot = plot.borrow();
let snap = plot
.snap_cursor([3.0, 3.0], SnappingMode::CURVE)
.expect("the filled bar must pick");
assert!(
(snap.data[0] - 3.0).abs() < 1e-9 && (snap.data[1] - 6.0).abs() < 1e-9,
"the picked histogram must outrank the curve vertex: {:?}",
snap.data
);
}
#[test]
fn vertex_within_radius_but_outside_the_pick_box_does_not_snap() {
let (plot, _harness) = plot_with_line();
let plot = plot.borrow();
let p5 = plot.data_to_pixel(5.0, 5.0, rsplot::YAxis::Left).unwrap();
let p6 = plot.data_to_pixel(6.0, 6.0, rsplot::YAxis::Left).unwrap();
let dir = [p6.x - p5.x, p6.y - p5.y];
let perp = [-dir[1], dir[0]];
let far = cursor_at_pixel_offset(&plot, [5.0, 5.0], perp, 4.6);
assert!(
plot.snap_cursor(far, SnappingMode::CURVE).is_none(),
"an unpicked vertex must not snap even inside the snap radius"
);
let near = cursor_at_pixel_offset(&plot, [5.0, 5.0], perp, 2.0);
let snap = plot
.snap_cursor(near, SnappingMode::CURVE)
.expect("a picked vertex within the radius must snap");
assert!(
(snap.data[0] - 5.0).abs() < 1e-9 && (snap.data[1] - 5.0).abs() < 1e-9,
"snapped to the wrong vertex: {:?}",
snap.data
);
}
#[test]
fn snap_radius_scales_with_pixels_per_point() {
let build = |plot: &mut PlotWidget| {
let xs: Vec<f64> = (0..=10).map(|i| i as f64).collect();
let ys = xs.clone();
plot.add_curve(&xs, &ys, egui::Color32::from_rgb(0, 120, 255));
};
let (plot, _harness) = plot_rendered_at(2.0, build);
let plot = plot.borrow();
let p5 = plot.data_to_pixel(5.0, 5.0, rsplot::YAxis::Left).unwrap();
let p6 = plot.data_to_pixel(6.0, 6.0, rsplot::YAxis::Left).unwrap();
let dir = [p6.x - p5.x, p6.y - p5.y];
let cursor = cursor_at_pixel_offset(&plot, [5.0, 5.0], dir, 7.0);
let snap = plot
.snap_cursor(cursor, SnappingMode::CURVE)
.expect("7 px is inside the DPR-2 radius of 10 px");
assert!(
(snap.data[0] - 5.0).abs() < 1e-9 && (snap.data[1] - 5.0).abs() < 1e-9,
"snapped to the wrong vertex: {:?}",
snap.data
);
drop(plot);
let (plot, _harness) = plot_rendered_at(1.0, build);
let plot = plot.borrow();
let p5 = plot.data_to_pixel(5.0, 5.0, rsplot::YAxis::Left).unwrap();
let p6 = plot.data_to_pixel(6.0, 6.0, rsplot::YAxis::Left).unwrap();
let dir = [p6.x - p5.x, p6.y - p5.y];
let cursor = cursor_at_pixel_offset(&plot, [5.0, 5.0], dir, 7.0);
assert!(
plot.snap_cursor(cursor, SnappingMode::CURVE).is_none(),
"7 px is outside the DPR-1 radius of 5 px"
);
}
#[test]
fn uncached_transform_yields_no_snap() {
let rs = create_render_state(default_wgpu_setup());
rsplot::install(&rs);
let mut plot = PlotWidget::new(&rs, 0);
let xs: Vec<f64> = (0..=10).map(|i| i as f64).collect();
let ys = xs.clone();
plot.add_curve(&xs, &ys, egui::Color32::from_rgb(0, 120, 255));
assert!(
plot.snap_cursor([5.0, 5.0], SnappingMode::CURVE).is_none(),
"snapping before any frame is rendered must return None"
);
}