test(plot): pin the crosshair's lookup, readout, placement, sync and clip
This commit is contained in:
1 parent
fd0c471430
commit
8e27e22ebe
6 files changed
+798
-17
No files matched your search
@@ -4570,3 +4570,177 @@ PLOT_TEST(horizontal_ruler_stays_inside_the_axes_at_the_edges) {
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ the crosshair (spec §15)
|
||||
|
||||
namespace {
|
||||
|
||||
/// The crosshair's own colour (0xB8E8FF) at real coverage: a pale cyan, green clearly above red
|
||||
/// and blue above green, which no other element of the frames this case draws produces. The two
|
||||
/// traces here are magenta and green -- deliberately *not* blue, which is the one other colour
|
||||
/// that would satisfy "blue above green and green above red" (0x40C0FF does). The axis text
|
||||
/// (0xC8D0DC) is grey enough to fail the green-over-red term, the chrome and the grid are darker
|
||||
/// still, and the same hue at lower coverage still passes, so a partially covered stroke counts
|
||||
/// as ink rather than the test needing a fully covered pixel to exist.
|
||||
bool crosshair_pixel(const plot::u8 px[4]) {
|
||||
return px[1] > px[0] + 25 && px[2] > px[1] + 12 && px[2] > 120;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
namespace {
|
||||
|
||||
/// A third colour predicate for the lateral case below: the orange 0xFFB020 of a trace of its
|
||||
/// own, which no other element of this frame draws (there is no cursor, no region and no
|
||||
/// measurement band in it, and the crosshair's own colour is a pale cyan).
|
||||
bool orange_pixel(const plot::u8 px[4]) {
|
||||
return px[0] > 200 && px[1] > 150 && px[1] < px[0] && px[2] < 80;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// Spec §15's clip half, and with it a regression guard for Task 1: **everything the crosshair
|
||||
/// draws stays inside `axes`** -- both lines, every marker and the readout panel -- and so does
|
||||
/// the data it points at.
|
||||
///
|
||||
/// Three traces make the case discriminating, one per way a marker can leave the rect:
|
||||
///
|
||||
/// * an **in-range** trace, whose line and marker are inside it -- the complement that says the
|
||||
/// clip is the axes rect and not something smaller;
|
||||
/// * a trace **below the axis**: y = -0.05 on a [0, 1] axis maps some thirteen pixels under the
|
||||
/// axes' bottom edge, i.e. *into the x label strip*. Nothing in the drawing path tests a
|
||||
/// sample's y against the visible range, and a crosshair marker is deliberately staged where
|
||||
/// its trace is drawn rather than skipped, so only Task 1's scissor keeps it off the strip;
|
||||
/// * a trace **beside the axes**: one bound to a *second* x axis whose range starts past the
|
||||
/// crosshair's x, so the data x under the pointer maps to a column *left* of the axes -- in
|
||||
/// the y label strip. The marker is placed through the trace's own axis pair (which is what
|
||||
/// puts it there, and what the row's geometry asserts as the premise), and again only the clip
|
||||
/// keeps it out.
|
||||
///
|
||||
/// A build without the scissor puts marker ink in the x and the y label strip respectively, and
|
||||
/// this case fails on both; the panel's own placement is asserted too, because a panel that
|
||||
/// overhung `axes` would be the same defect by a different route.
|
||||
PLOT_TEST(crosshair_ink_stays_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));
|
||||
|
||||
// A viewport inside the framebuffer with the host's grey painted behind it, so the frame may
|
||||
// paint inside that rect and nowhere else -- and a marker that escaped `axes` would land either
|
||||
// in a strip or on the host's own surface, both of which are asserted empty below.
|
||||
const plot::Rect vp{30.0f, 40.0f, 580.0f, 290.0f};
|
||||
|
||||
// The in-range signal: a wave inside the [0, 1] axis, so its trace and its marker are both
|
||||
// inside the rect.
|
||||
std::vector<plot::f32> in_range(640u);
|
||||
for (plot::u32 i = 0; i < in_range.size(); ++i) {
|
||||
in_range[i] = 0.5f + 0.4f * std::sin(0.03f * static_cast<plot::f32>(i));
|
||||
}
|
||||
plot::data::ArraySeries good(in_range, 0.0, 1.0);
|
||||
// Five percent below the bottom of the axis: some thirteen pixels, which is the x label strip.
|
||||
plot::data::ArraySeries below(std::vector<plot::f32>(640u, -0.05f), 0.0, 1.0);
|
||||
// In range on y, but drawn through an x axis the crosshair's x is outside of: its marker lands
|
||||
// in the y label strip, beside the axes.
|
||||
plot::data::ArraySeries aside(std::vector<plot::f32>(2000u, 0.25f), 0.0, 1.0);
|
||||
|
||||
plot::Plot p;
|
||||
p.set_viewport(vp);
|
||||
const plot::u32 ax = p.add_x_axis({"t", {0.0, 639.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
// The second x axis: a window that starts *above* the crosshair's x, so the same data x is a
|
||||
// column to the left of the axes when the trace reads it through this one.
|
||||
const plot::u32 ax2 = p.add_x_axis({"t2", {700.0, 1400.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_trace({&good, kRowMagenta, 1.5f, ax, ay, "good"});
|
||||
p.add_trace({&below, plot::Color::hex(0x00E070), 1.5f, ax, ay, "below"});
|
||||
p.add_trace({&aside, plot::Color::hex(0xFFB020), 1.5f, ax2, ay, "aside"});
|
||||
|
||||
plot::gl::glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
|
||||
// A warm-up frame first: the renderer installs its text backend on the first `draw()`, and the
|
||||
// strips and the panel are measured with it -- so the frame under test is placed and asserted
|
||||
// against the rects a *drawn* frame has, not an empty plot's defaults.
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
|
||||
// The crosshair three pixels inside the bottom-right corner of the data area, so its readout
|
||||
// panel would leave the rect on two sides if it were not flipped and clamped.
|
||||
const plot::Rect axes = p.layout().axes;
|
||||
const plot::Transform tx = p.x_transform(ax);
|
||||
const plot::Transform ty = p.y_transform(ay);
|
||||
p.set_crosshair(tx.to_data_x(axes.right() - 3.0f), ty.to_data_y(axes.bottom() - 3.0f), true);
|
||||
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
dump_if_requested(r, "crosshair_axes.ppm");
|
||||
|
||||
const std::vector<plot::u8> frame = read_frame(ctx);
|
||||
const plot::PlotLayout layout = p.layout();
|
||||
const plot::CrosshairLayout& crosshair = layout.crosshair;
|
||||
PLOT_CHECK(crosshair.active);
|
||||
PLOT_CHECK(crosshair.rows.size() == 3u);
|
||||
|
||||
// The panel exists, and is inside the axes on every side -- the placement rule, asserted
|
||||
// against the very rect the renderer clipped to.
|
||||
PLOT_CHECK(crosshair.panel.w > 0.0f);
|
||||
PLOT_CHECK(crosshair.panel.h > 0.0f);
|
||||
PLOT_CHECK(crosshair.panel.x >= axes.x);
|
||||
PLOT_CHECK(crosshair.panel.y >= axes.y);
|
||||
PLOT_CHECK(crosshair.panel.right() <= axes.right());
|
||||
PLOT_CHECK(crosshair.panel.bottom() <= axes.bottom());
|
||||
|
||||
// The premises: the two off-scale markers are staged where nothing but the clip is between them
|
||||
// and a strip. Row 1's is below the axes' bottom edge, within the x label strip; row 2's is left
|
||||
// of the axes' left edge, within the y label strip.
|
||||
PLOT_CHECK(crosshair.rows[0].valid);
|
||||
PLOT_CHECK(crosshair.rows[1].valid);
|
||||
PLOT_CHECK(crosshair.rows[2].valid);
|
||||
PLOT_CHECK(crosshair.rows[1].marker.y > axes.bottom());
|
||||
PLOT_CHECK(crosshair.rows[1].marker.y < layout.x_label_area.bottom());
|
||||
PLOT_CHECK(crosshair.rows[2].marker.x < axes.x);
|
||||
PLOT_CHECK(crosshair.rows[2].marker.x >= layout.y_label_area.x);
|
||||
|
||||
// Every crosshair-coloured pixel of the frame is inside `axes`, and there are some: not one in
|
||||
// a label or title strip, not one over the host's surface outside the viewport.
|
||||
const plot::u32 inside = predicate_pixels(frame, kW, axes, crosshair_pixel);
|
||||
PLOT_CHECK(inside > 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, crosshair_pixel) == inside);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_label_area, crosshair_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_title_area, crosshair_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_label_area, crosshair_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_title_area, crosshair_pixel) == 0u);
|
||||
plot::u8 host[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(4u, kH - 4u, host));
|
||||
PLOT_CHECK(host[0] > 100 && host[0] < 160 && host[1] > 100 && host[2] > 100);
|
||||
// The panel's own border is inside the panel, so the readout is drawn where the layout said and
|
||||
// not merely clipped somewhere else.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, crosshair.panel, crosshair_pixel) > 0u);
|
||||
|
||||
// The trace below the axis: none of its colour anywhere outside `axes` -- the x label strip and
|
||||
// the host's surface included -- while its own swatch in the readout proves the colour is one
|
||||
// this frame really draws (so the absence is the clip's doing and not a colour nothing draws).
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_label_area, green_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, green_pixel) ==
|
||||
predicate_pixels(frame, kW, axes, green_pixel));
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, crosshair.rows[1].swatch, green_pixel) > 0u);
|
||||
|
||||
// The trace beside the axes: the same statement for the y label strip, where its marker would
|
||||
// have been drawn.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_label_area, orange_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_title_area, orange_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, orange_pixel) ==
|
||||
predicate_pixels(frame, kW, axes, orange_pixel));
|
||||
|
||||
// The complement: the in-range trace's ink -- its polyline and the marker the crosshair put on
|
||||
// it -- is inside `axes`, and all of it is, which a clip smaller than the rect could not do.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, axes, magenta_pixel) > 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, magenta_pixel) ==
|
||||
predicate_pixels(frame, kW, axes, magenta_pixel));
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user