From 523446d08fc2ef7bbb2de10b13580ff373c8f41f Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Fri, 2 Oct 2026 23:59:53 +0200 Subject: [PATCH] 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. --- tests/test_render_smoke.cpp | 77 +++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/test_render_smoke.cpp b/tests/test_render_smoke.cpp index 1d86217..f9ad2ba 100644 --- a/tests/test_render_smoke.cpp +++ b/tests/test_render_smoke.cpp @@ -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 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(kW), static_cast(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(kW), static_cast(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 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(area.y); + y < static_cast(area.bottom()); ++y) { + plot::u32 run = 0; + for (plot::u32 x = static_cast(area.x); + x < static_cast(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; +}