test(render): a figure draws each row's own horizontal rulers

The figure-mode cursor test covered the *vertical* marker, which is one time
shared by every row. A horizontal ruler is the other half: its y is on that
row's own axis, so two rows' rulers are two different values, and every row's
has to be drawn in its own band. A host that puts two rulers on every plot
(`udpscope`'s cursors pane) needs exactly this, and had no test for it.

It asserts the strokes only -- the ruler's value label goes through the text
path, which this case is not about, so it uses the harness's no-atlas backend.
This commit is contained in:
Martino Ferrari committed 2026-10-02 23:59:53 +02:00
1 parent 133cd680ce
commit 523446d08f
1 file changed
+77
+77
View File
@@ -5971,3 +5971,80 @@ PLOT_TEST(release_gl_shuts_down_and_the_renderer_can_reinit) {
PLOT_CHECK(!r.last_frame_had_gl_error());
ctx.destroy();
}
// The other orientation, in a figure: a horizontal ruler is per row (its `y` is on that
// row's y axis, so two rows' rulers are two different values), and every row's has to be
// drawn in its own band. The vertical case above covers the shared-time half; this is the
// half where each row owns its instrument, which is what an app's "two rulers per plot"
// needs -- and which the app found undrawn.
PLOT_TEST(figure_rows_draw_their_own_horizontal_rulers) {
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));
const std::vector<plot::f32> samples = wave(101u, 0.0f);
plot::data::ArraySeries series(samples, 0.0, 1.0);
// A strongly red ruler and a strongly green one, so a pixel that is red- or
// green-dominant inside a row's axes can only be that row's ink: the trace is blue and
// the chrome grey (all three channels equal).
const plot::Color red = plot::Color::hex(0xFF2020);
const plot::Color green = plot::Color::hex(0x20FF20);
const plot::f32 y_scales[3] = {671.0e6f, 90.0f, 2.51f};
// The harness's no-atlas backend: a ruler always writes its value as text, and this case
// is about the *stroke*, so the text is asked for and staged as nothing. (`OnePxPerString
// Backend::glyph()` is null, which is what a real host without an atlas looks like.)
OnePxPerStringBackend no_text;
plot::Figure figure(3u);
figure.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
for (plot::u32 row = 0; row < 3u; ++row) {
plot::Plot& p = figure.plot(row);
const plot::u32 ax = p.add_x_axis({"t", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
const plot::u32 ay =
p.add_y_axis({"v", {0.0, y_scales[row]}, false, plot::AxisPlace::Left, false});
p.add_trace({&series, kRowBlue, 1.5f, ax, ay});
p.set_text_backend(&no_text);
// A quarter and three quarters up this row's own range: inside its axes whatever the
// scale is, which is the point of a ruler being per row.
p.add_h_ruler(0.25 * y_scales[row], red);
p.add_h_ruler(0.75 * y_scales[row], green);
}
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
r.draw(figure);
r.end_frame();
PLOT_CHECK(!r.last_frame_had_gl_error());
dump_if_requested(r, "figure_h_rulers.ppm");
const std::vector<plot::u8> frame = read_frame(ctx);
// Rows of the frame that hold a fully covered red or green line, and which row's band
// each falls in. The predicate demands full coverage exactly as the vertical case does.
const auto lines_in = [&](const plot::Rect& area, bool want_red) {
plot::u32 found = 0;
for (plot::u32 y = static_cast<plot::u32>(area.y);
y < static_cast<plot::u32>(area.bottom()); ++y) {
plot::u32 run = 0;
for (plot::u32 x = static_cast<plot::u32>(area.x);
x < static_cast<plot::u32>(area.right()); ++x) {
const plot::u8* px = &frame[(plot::usize(y) * ctx.width + x) * 4u];
const bool ok = want_red ? (px[0] >= 200 && px[1] <= 80 && px[2] <= 80)
: (px[1] >= 200 && px[0] <= 80 && px[2] <= 80);
if (ok) ++run;
}
if (run > (area.w * 3u) / 4u) ++found;
}
return found;
};
const plot::FigureLayout layout = figure.layout();
for (plot::u32 row = 0; row < 3u; ++row) {
const plot::Rect& axes = figure.plot(row).layout().axes;
PLOT_CHECK(lines_in(axes, true) >= 1u); // this row's own A ruler
PLOT_CHECK(lines_in(axes, false) >= 1u); // and its B ruler
}
(void)layout;
}