test(input): region, cursor and band-edge direct-manipulation cases (spec §5)
This commit is contained in:
1 parent
e4d48cc6c3
commit
f423cf6f99
3 files changed
+819
No files matched your search
@@ -12,6 +12,7 @@
|
||||
#include <plot/render/buffer.hpp>
|
||||
#include <plot/render/detail/ring_stats.hpp>
|
||||
#include <plot/render/renderer.hpp>
|
||||
#include <plot/input/controller.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
@@ -2207,9 +2208,40 @@ plot::u32 exact_pixels(const std::vector<plot::u8>& frame, plot::u32 fb_w, const
|
||||
return count;
|
||||
}
|
||||
|
||||
/// True when any row of the column at `x` inside `axes` holds a pixel the predicate
|
||||
/// accepts. The overlay strokes are blended over whatever is under them, so for them a
|
||||
/// presence test is the honest one, and it is measured in the axes' own rows rather
|
||||
/// than over the whole framebuffer: a bound is the axes' height by construction.
|
||||
bool column_has(plottest::HeadlessContext& ctx, plot::u32 x, const plot::Rect& axes,
|
||||
bool (*pred)(const plot::u8*)) {
|
||||
for (plot::u32 row = 0; row < static_cast<plot::u32>(axes.h); ++row) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - static_cast<plot::u32>(axes.y) - row, px));
|
||||
if (pred(px)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The whole framebuffer as a Rect, for the counts that ask "and nowhere else".
|
||||
const plot::Rect kFrameRect{0.0f, 0.0f, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)};
|
||||
|
||||
/// The pixels of `area` a predicate accepts. `exact_pixels` above answers "the
|
||||
/// colour only this element can produce"; this one answers the same question for an
|
||||
/// element that is *blended* -- a translucent region fill, an anti-aliased stroke --
|
||||
/// where the test has to name the pattern rather than the colour.
|
||||
plot::u32 predicate_pixels(const std::vector<plot::u8>& frame, plot::u32 fb_w,
|
||||
const plot::Rect& area, bool (*pred)(const plot::u8*)) {
|
||||
plot::u32 count = 0;
|
||||
for (plot::u32 y = static_cast<plot::u32>(area.y);
|
||||
y < static_cast<plot::u32>(area.bottom()); ++y) {
|
||||
for (plot::u32 x = static_cast<plot::u32>(area.x);
|
||||
x < static_cast<plot::u32>(area.right()); ++x) {
|
||||
if (pred(&frame[(plot::usize(y) * fb_w + x) * 4u])) ++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// The legend's entries are drawn where `Plot::layout()` measured them: a filled
|
||||
/// swatch per trace in that trace's own colour, and the name's ink inside the panel
|
||||
/// and nowhere else in the data area -- and the panel's presence changes nothing
|
||||
@@ -3162,3 +3194,239 @@ PLOT_TEST(grid_widths_are_per_class_and_scale_with_dpi) {
|
||||
PLOT_CHECK_NEAR(r.style().device(r.style().grid_minor_width), 2.0f, 1e-6f);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// --- regions: the fill, the bounds and the emphasized handle (spec §5) ---------
|
||||
|
||||
namespace {
|
||||
|
||||
/// Green dominance: what the region's translucent `0x33FF00` fill leaves over the dark
|
||||
/// background, over a grey grid line and over the axis line, and what an opaque
|
||||
/// `0x00FF00` marker leaves at any coverage. Nothing else in these frames is
|
||||
/// green-dominant -- the trace is blue (`g - b < 0`), the chrome and the text are
|
||||
/// grey/chalk (`r == g == b`), the bounds are yellow (`r == g > b`) -- so a count of
|
||||
/// these pixels is a statement about the fill (or the marker) and about nothing else.
|
||||
/// Conservative on purpose: the fill's own half-pixel fringe at the top and bottom rows
|
||||
/// is not counted.
|
||||
bool green_dominant(const plot::u8 px[4]) {
|
||||
return px[1] > px[0] + 60 && px[1] > px[2] + 60;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// A region's fill and its two bounds are drawn from the geometry `Plot::layout()`
|
||||
/// measured -- the same rects the controller hit-tests -- so the rect a test asserts, the
|
||||
/// rect a frame paints and the column a pointer is grabbed at are one set of numbers.
|
||||
/// The fill shades the interval between the bounds and nothing outside it; each bound is
|
||||
/// one vertical stroke of the axes' height in the region's own edge colour, in its own
|
||||
/// column and nowhere else in the frame.
|
||||
PLOT_TEST(region_fill_and_edges_are_drawn_inside_the_axes) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
plot::data::ArraySeries s(sine_samples(), 0.0, 1.0);
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::u32 ax = p.add_x_axis({"t", s.x_bounds(), true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
||||
p.set_x_range(ax, {0.0, 640.0}); // one sample per pixel: a flat, cheap frame
|
||||
|
||||
plot::Region reg;
|
||||
reg.x0 = 100.0;
|
||||
reg.x1 = 300.0;
|
||||
reg.snap = plot::RegionSnap::Free;
|
||||
// Colours no other element of the frame produces, so each count below can only be
|
||||
// satisfied by the element it names.
|
||||
reg.fill = plot::Color::hex(0x33FF00, 0.35f);
|
||||
// Magenta, which `magenta_pixel` already names and neither the fill, the trace, the
|
||||
// chrome nor the accent (0xFFB020) can produce.
|
||||
reg.edge = plot::Color::hex(0xFF00FF);
|
||||
const plot::u32 rid = p.add_region(reg);
|
||||
|
||||
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
dump_if_requested(r, "regions.ppm");
|
||||
|
||||
const std::vector<plot::u8> frame = read_frame(ctx);
|
||||
const plot::PlotLayout layout = p.layout();
|
||||
PLOT_CHECK(layout.regions.size() == 1u);
|
||||
const plot::RegionLayout& rl = layout.regions[0];
|
||||
PLOT_CHECK(rl.region == rid);
|
||||
const plot::Rect axes = layout.axes;
|
||||
|
||||
// The measured geometry is the drawn geometry: the fill spans the axes' full height
|
||||
// and is clipped to their columns, so it can never reach over a label strip.
|
||||
PLOT_CHECK(rl.fill.w > 0.0f);
|
||||
PLOT_CHECK(rl.fill.x >= axes.x && rl.fill.right() <= axes.right());
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(rl.fill.h), static_cast<plot::f64>(axes.h), 1e-6);
|
||||
// ... and it is the interval the two bounds mark: its edges are the accepted columns
|
||||
// of the bound strokes, so the rect that is painted is the interval that is grabbed.
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(rl.fill.x),
|
||||
static_cast<plot::f64>(plot::handle_column(rl.left_handle)), 1e-4);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(rl.fill.right()),
|
||||
static_cast<plot::f64>(plot::handle_column(rl.right_handle)), 1e-4);
|
||||
|
||||
const plot::u32 in_fill = predicate_pixels(frame, kW, rl.fill, green_dominant);
|
||||
const plot::u32 anywhere = predicate_pixels(frame, kW, kFrameRect, green_dominant);
|
||||
const plot::u32 area = static_cast<plot::u32>(rl.fill.w) * static_cast<plot::u32>(rl.fill.h);
|
||||
PLOT_CHECK(in_fill > 0u);
|
||||
// The fill shades essentially the whole of its rect -- the trace and the grid it is
|
||||
// drawn over take a few percent -- and tints nothing anywhere else in the frame.
|
||||
PLOT_CHECK(in_fill >= area - area / 8u);
|
||||
PLOT_CHECK(anywhere == in_fill);
|
||||
|
||||
// The bounds: one stroke per bound, drawn in the bound's own column, of the axes'
|
||||
// height, and nowhere else in the frame.
|
||||
const plot::u32 left_col =
|
||||
static_cast<plot::u32>(plot::handle_column(rl.left_handle));
|
||||
const plot::u32 right_col =
|
||||
static_cast<plot::u32>(plot::handle_column(rl.right_handle));
|
||||
PLOT_CHECK(column_has(ctx, left_col, axes, magenta_pixel));
|
||||
PLOT_CHECK(column_has(ctx, right_col, axes, magenta_pixel));
|
||||
PLOT_CHECK(!column_has(ctx, left_col + 25u, axes, magenta_pixel)); // the fill is not an edge
|
||||
PLOT_CHECK(!column_has(ctx, 0u, axes, magenta_pixel));
|
||||
// Each band is the bound's own column grown by a pixel either side -- and one row
|
||||
// taller than the axes, because a stroke drawn to `tab.bottom()` covers the
|
||||
// half-pixel row a rect's truncating cast ends at.
|
||||
const plot::Rect left_band{static_cast<plot::f32>(left_col) - 1.0f, axes.y, 3.0f,
|
||||
axes.h + 1.0f};
|
||||
const plot::Rect right_band{static_cast<plot::f32>(right_col) - 1.0f, axes.y, 3.0f,
|
||||
axes.h + 1.0f};
|
||||
const plot::u32 left_edge = predicate_pixels(frame, kW, left_band, magenta_pixel);
|
||||
const plot::u32 right_edge = predicate_pixels(frame, kW, right_band, magenta_pixel);
|
||||
const plot::u32 all_edges = predicate_pixels(frame, kW, kFrameRect, magenta_pixel);
|
||||
PLOT_CHECK(left_edge >= static_cast<plot::u32>(axes.h) / 2u);
|
||||
PLOT_CHECK(right_edge >= static_cast<plot::u32>(axes.h) / 2u);
|
||||
PLOT_CHECK(all_edges == left_edge + right_edge);
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// The handles a pointer is on are emphasized, and the emphasis is the plot's to draw:
|
||||
/// `Plot::hot_handles()` carries what the input layer's one hit test found, and the
|
||||
/// renderer draws that handle in the style's cursor colour and at the handle width
|
||||
/// `PlotLayout` sized it with. A grabbed handle keeps its emphasis for the whole
|
||||
/// gesture, so what is emphasized follows the *grab* rather than the pointer -- which is
|
||||
/// what the last frame here pins, with the pointer off the plot entirely.
|
||||
PLOT_TEST(grabbed_handle_is_highlighted) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
plot::data::ArraySeries s(sine_samples(), 0.0, 1.0);
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::u32 ax = p.add_x_axis({"t", s.x_bounds(), true, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
||||
p.set_x_range(ax, {0.0, 640.0});
|
||||
|
||||
plot::Region reg;
|
||||
reg.x0 = 100.0;
|
||||
reg.x1 = 300.0;
|
||||
reg.snap = plot::RegionSnap::Free;
|
||||
reg.edge = plot::Color::hex(0xFF00FF);
|
||||
const plot::u32 rid = p.add_region(reg);
|
||||
// A marker in a colour of its own, so the accent count below can only come from a
|
||||
// highlighted handle and never from the marker as it is drawn when it is idle.
|
||||
const plot::u32 cid = p.add_cursor(500.0, plot::Color::hex(0x00FF00));
|
||||
|
||||
plot::input::Controller c(p);
|
||||
c.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
const plot::Color accent = r.style().cursor;
|
||||
|
||||
auto draw_frame = [&] {
|
||||
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
return read_frame(ctx);
|
||||
};
|
||||
|
||||
// Frame 1: the controller has seen no pointer, so nothing is hot and no handle is
|
||||
// emphasized -- while the bounds themselves are still drawn, in their own colour.
|
||||
const std::vector<plot::u8> bare = draw_frame();
|
||||
PLOT_CHECK(exact_pixels(bare, kW, kFrameRect, accent) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(bare, kW, kFrameRect, magenta_pixel) > 0u);
|
||||
|
||||
const plot::PlotLayout layout = p.layout();
|
||||
PLOT_CHECK(layout.regions.size() == 1u);
|
||||
const plot::Rect tab = layout.regions[0].left_handle;
|
||||
// The tab grown by a pixel on each side -- and by that row plus one, because a
|
||||
// stroke drawn to `tab.bottom()` covers the half-pixel row the truncating cast of a
|
||||
// rect ends at. The window is the handle's, and the handle's alone.
|
||||
const plot::Rect band{tab.x - 1.0f, tab.y, tab.w + 2.0f, tab.h + 1.0f};
|
||||
const plot::f32 col = plot::handle_column(tab);
|
||||
const plot::f32 cy = layout.axes.y + 0.5f * layout.axes.h;
|
||||
|
||||
// Frame 2: the pointer is on the bound, so the bound is the handle being emphasized.
|
||||
c.on_pointer({col, cy, 0.0f, 0.0f, plot::input::Button::Left, 0u, false, false, false});
|
||||
PLOT_CHECK(c.region_grab(rid) == plot::RegionGrab::Left);
|
||||
PLOT_CHECK(p.hot_handles().zone == plot::RegionGrab::Left);
|
||||
const std::vector<plot::u8> hovered = draw_frame();
|
||||
const plot::u32 hot = exact_pixels(hovered, kW, kFrameRect, accent);
|
||||
PLOT_CHECK(hot > 0u);
|
||||
// At the handle and nowhere else in the frame.
|
||||
PLOT_CHECK(exact_pixels(hovered, kW, band, accent) == hot);
|
||||
// The handle is a handle, not a hairline: the accent covers the tab's own width,
|
||||
// where the idle bound was drawn 1.5 logical px wide in the region's colour.
|
||||
PLOT_CHECK(hot >= static_cast<plot::u32>(tab.h) * (static_cast<plot::u32>(tab.w) - 2u));
|
||||
PLOT_CHECK(hot <= static_cast<plot::u32>(tab.h) * (static_cast<plot::u32>(tab.w) + 2u));
|
||||
PLOT_CHECK(predicate_pixels(hovered, kW, band, magenta_pixel) == 0u);
|
||||
|
||||
// Frame 3: the bound is grabbed and dragged, then the pointer leaves the plot. The
|
||||
// hover is over -- `region_grab()` says so -- but the gesture is not, so the bound the
|
||||
// drag holds stays emphasized, at the column the drag has moved it to.
|
||||
c.on_pointer({col, cy, 0.0f, 0.0f, plot::input::Button::Left, 0u, true, true, false});
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::ResizeRegionLeft);
|
||||
PLOT_CHECK(c.grabbed_region() == rid);
|
||||
const plot::f32 moved = col + 60.0f;
|
||||
c.on_pointer({moved, cy, 60.0f, 0.0f, plot::input::Button::Left, 0u, true, false, false});
|
||||
c.on_pointer({moved, static_cast<plot::f32>(kH) + 20.0f, 0.0f, 0.0f, plot::input::Button::Left,
|
||||
0u, true, false, false});
|
||||
PLOT_CHECK(c.region_grab(rid) == plot::RegionGrab::None); // the pointer is off the data area
|
||||
PLOT_CHECK(p.hot_handles().region == rid);
|
||||
PLOT_CHECK(p.hot_handles().zone == plot::RegionGrab::Left);
|
||||
const std::vector<plot::u8> dragged = draw_frame();
|
||||
const plot::PlotLayout moved_layout = p.layout();
|
||||
const plot::Rect moved_tab = moved_layout.regions[0].left_handle;
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(plot::handle_column(moved_tab)),
|
||||
static_cast<plot::f64>(moved), 0.5);
|
||||
const plot::Rect moved_band{moved_tab.x - 1.0f, moved_tab.y, moved_tab.w + 2.0f,
|
||||
moved_tab.h + 1.0f};
|
||||
const plot::u32 held = exact_pixels(dragged, kW, kFrameRect, accent);
|
||||
PLOT_CHECK(held > 0u);
|
||||
PLOT_CHECK(exact_pixels(dragged, kW, moved_band, accent) == held);
|
||||
|
||||
// Releasing off the plot ends both the gesture and the emphasis.
|
||||
c.on_pointer({moved, static_cast<plot::f32>(kH) + 20.0f, 0.0f, 0.0f, plot::input::Button::Left,
|
||||
0u, false, false, true});
|
||||
PLOT_CHECK(c.grabbed_region() == plot::kInvalid);
|
||||
PLOT_CHECK(p.hot_handles().region == plot::kInvalid);
|
||||
PLOT_CHECK(exact_pixels(draw_frame(), kW, kFrameRect, accent) == 0u);
|
||||
|
||||
// The same rule for a cursor marker: hovered, it is drawn as a handle in the accent,
|
||||
// in its own column and nowhere else, and its own colour is gone from that column.
|
||||
const plot::f32 marker_col = static_cast<plot::f32>(p.x_transform(ax).to_screen_x(p.cursor(cid).x));
|
||||
c.on_pointer({marker_col, cy, 0.0f, 0.0f, plot::input::Button::Left, 0u, false, false, false});
|
||||
PLOT_CHECK(c.hovered_cursor() == cid);
|
||||
PLOT_CHECK(p.hot_handles().cursor == cid);
|
||||
const std::vector<plot::u8> over_marker = draw_frame();
|
||||
const plot::u32 marker_hot = exact_pixels(over_marker, kW, kFrameRect, accent);
|
||||
const plot::Rect marker_band{marker_col - 4.0f, layout.axes.y, 9.0f,
|
||||
layout.axes.h + 1.0f};
|
||||
PLOT_CHECK(marker_hot > 0u);
|
||||
PLOT_CHECK(exact_pixels(over_marker, kW, marker_band, accent) == marker_hot);
|
||||
PLOT_CHECK(predicate_pixels(over_marker, kW, marker_band, green_dominant) == 0u);
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user