fix(render): clip grid, traces, regions and cursors to the plot area
This commit is contained in:
1 parent
d853db62ae
commit
2d819c4228
3 files changed
+610
-7
No files matched your search
@@ -3845,3 +3845,477 @@ PLOT_TEST(figure_x_title_is_drawn_on_the_bottom_row_only) {
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
// ---------------------------------------------------- the axes clip (spec §13)
|
||||
|
||||
// The reported defect this section is about: a trace is staged in *data* space and
|
||||
// mapped through the axes transform, so a sample outside the visible range maps
|
||||
// outside `axes` and was drawn there -- over the label strips, over the host's own
|
||||
// surface outside the viewport, and in a `Figure` straight into the neighbouring row.
|
||||
// The frame's only scissor was the one around its clear, which bounds that overflow at
|
||||
// the framebuffer and nowhere else. The fix is a clip to `layout().axes`, intersected
|
||||
// with the viewport, around every pass that draws inside the axes; the cases below
|
||||
// assert it from the pixels, and prove it is neither missing nor too small.
|
||||
|
||||
namespace {
|
||||
|
||||
/// `n` samples alternating between +50 and -50. On the [0, 1] y axis these cases fix,
|
||||
/// every sample maps fifty axes' heights off the rect: the +50 one 49 `axes.h` above its
|
||||
/// top edge, the -50 one 50 below its bottom edge -- thousands of pixels, i.e. past the
|
||||
/// target -- so the polyline leaves the axes at its first segment whatever window is
|
||||
/// framed.
|
||||
std::vector<plot::f32> off_scale_wave(plot::u32 n) {
|
||||
std::vector<plot::f32> y(n);
|
||||
for (plot::u32 i = 0; i < n; ++i) y[i] = (i % 2u == 0u) ? 50.0f : -50.0f;
|
||||
return y;
|
||||
}
|
||||
|
||||
/// `n` samples alternating between 0.25 and 0.75: inside the [0, 1] axis these cases fix, so
|
||||
/// this trace's overflow is the *window's* alone. The renderer gathers one sample of slack
|
||||
/// on each side of the visible window -- which is what covers a partially visible end
|
||||
/// bucket, and why nothing is clamped in CPU -- so a coarse series hangs a whole segment,
|
||||
/// some fifty pixels wide, over the axes' left edge and across both y strips.
|
||||
std::vector<plot::f32> in_range_wave(plot::u32 n) {
|
||||
std::vector<plot::f32> y(n);
|
||||
for (plot::u32 i = 0; i < n; ++i) y[i] = (i % 2u == 0u) ? 0.25f : 0.75f;
|
||||
return y;
|
||||
}
|
||||
|
||||
/// The x window these cases frame and the two resolutions they sample it at: forty samples
|
||||
/// over it (a segment some 120 px wide, so the gathered window's one sample of slack hangs a
|
||||
/// whole segment over the end of the axes) and six thousand (a near-vertical segment per
|
||||
/// pixel of the axes, i.e. a comb that crosses every column of it).
|
||||
constexpr plot::f64 kOffScaleSpan = 100.0;
|
||||
constexpr plot::f64 kOffScaleMin = 10.4;
|
||||
constexpr plot::f64 kOffScaleMax = 20.6;
|
||||
constexpr plot::u32 kCoarseSamples = 40u;
|
||||
constexpr plot::u32 kCombSamples = 6000u;
|
||||
|
||||
/// The trace-coloured ink a frame holds *outside* `axes`: how many pixels, and how far
|
||||
/// past each edge the furthest of them reached. The assertions in this section are the
|
||||
/// clip's; this is the defect *measured*, which is where its size is quoted from.
|
||||
struct OutsideInk {
|
||||
plot::u32 pixels = 0u;
|
||||
plot::f32 left = 0.0f;
|
||||
plot::f32 right = 0.0f;
|
||||
plot::f32 above = 0.0f;
|
||||
plot::f32 below = 0.0f;
|
||||
};
|
||||
|
||||
OutsideInk outside_ink(const std::vector<plot::u8>& frame, plot::u32 fb_w, plot::u32 fb_h,
|
||||
const plot::Rect& axes) {
|
||||
OutsideInk out;
|
||||
for (plot::u32 y = 0; y < fb_h; ++y) {
|
||||
for (plot::u32 x = 0; x < fb_w; ++x) {
|
||||
if (!trace_pixel(&frame[(plot::usize(y) * fb_w + x) * 4u])) continue;
|
||||
const plot::f32 cx = static_cast<plot::f32>(x);
|
||||
const plot::f32 cy = static_cast<plot::f32>(y);
|
||||
// Entirely inside the rect: not an overflow. A pixel straddling an edge is
|
||||
// counted, because that is a pixel the ink's own extent reached outside.
|
||||
if (cx >= axes.x && cx + 1.0f <= axes.right() && cy >= axes.y && cy + 1.0f <= axes.bottom()) {
|
||||
continue;
|
||||
}
|
||||
++out.pixels;
|
||||
out.left = std::max(out.left, axes.x - cx);
|
||||
out.right = std::max(out.right, (cx + 1.0f) - axes.right());
|
||||
out.above = std::max(out.above, axes.y - cy);
|
||||
out.below = std::max(out.below, (cy + 1.0f) - axes.bottom());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Any pixel of the axes' row `y` (framebuffer rows, top-left origin, like every `Rect`
|
||||
/// here) that a predicate accepts: the row-wise twin of `column_has` above, so a case can
|
||||
/// ask whether the clip reaches the rect's own top and bottom rows.
|
||||
bool axes_row_has(plottest::HeadlessContext& ctx, const plot::Rect& axes, plot::u32 y,
|
||||
bool (*pred)(const plot::u8*)) {
|
||||
for (plot::u32 x = static_cast<plot::u32>(axes.x); x < static_cast<plot::u32>(axes.right()); ++x) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - y, px));
|
||||
if (pred(px)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The cursor colour (0xFFB020) at any coverage: warm, with green strictly between red
|
||||
/// and blue. Nothing else in these frames produces it -- the trace is blue, the region
|
||||
/// magenta or green, the chrome and the text grey, and the measure band's own tint is
|
||||
/// 0xFFB020 at 15% over the background, too dark for the red bound below -- so a count of
|
||||
/// it is a count of cursor strokes.
|
||||
bool accent_pixel(const plot::u8 px[4]) {
|
||||
return px[0] > 90 && px[1] < px[0] && px[1] > px[2];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// Spec §13, the defect reported from use: "you can see signals outside the plot area".
|
||||
///
|
||||
/// The three failure modes are asserted separately, so a clip that fixes one of them and
|
||||
/// not the others cannot pass: no trace ink in the y label strip (the coarse trace's
|
||||
/// window slack hangs a whole segment over the left edge of the axes), none in the x
|
||||
/// label strip (the segments cross the bottom edge on their way to the -50 samples), and
|
||||
/// none anywhere outside `axes` at all -- in a strip, in the band between the axes and the
|
||||
/// frame's edge, or over the host's own surface outside the viewport. The complement is
|
||||
/// asserted too: the ink still reaches all four edges of the axes, because "nothing
|
||||
/// outside" is also satisfied by a clip narrower than the rect.
|
||||
PLOT_TEST(off_scale_trace_is_clipped_to_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));
|
||||
|
||||
// Two traces, and both directions of the defect: the coarse one is in range, so the ink
|
||||
// it puts over the y strips is the window slack's alone; the comb is fifty axes' heights
|
||||
// off scale, one near-vertical segment per pixel, so it crosses the x strips below the
|
||||
// axes and the row above them.
|
||||
plot::data::ArraySeries coarse(in_range_wave(kCoarseSamples), 0.0,
|
||||
kOffScaleSpan / static_cast<plot::f64>(kCoarseSamples));
|
||||
plot::data::ArraySeries comb(off_scale_wave(kCombSamples), 0.0,
|
||||
kOffScaleSpan / static_cast<plot::f64>(kCombSamples));
|
||||
|
||||
plot::Plot p;
|
||||
// A viewport inside the framebuffer, as an embedding host's is, with the host's grey
|
||||
// painted behind it: the frame may paint inside the rect and nowhere else at all.
|
||||
const plot::Rect vp{40.0f, 24.0f, 560.0f, 300.0f};
|
||||
p.set_viewport(vp);
|
||||
const plot::u32 ax =
|
||||
p.add_x_axis({"t", {0.0, kOffScaleSpan}, false, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&coarse, kRowBlue, 1.5f, ax, ay});
|
||||
p.add_trace({&comb, kRowBlue, 1.5f, ax, ay});
|
||||
p.set_x_range(ax, {kOffScaleMin, kOffScaleMax});
|
||||
|
||||
plot::gl::glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
|
||||
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
dump_if_requested(r, "clip_axes.ppm");
|
||||
|
||||
const std::vector<plot::u8> frame = read_frame(ctx);
|
||||
const plot::PlotLayout layout = p.layout();
|
||||
const plot::Rect& axes = layout.axes;
|
||||
|
||||
// The premise, from the transform rather than from the pixels: what a frame shows of
|
||||
// these samples is bounded by the clip and by nothing else.
|
||||
const plot::Transform ty = p.y_transform(ay);
|
||||
PLOT_CHECK_NEAR(static_cast<plot::f64>(axes.y) - ty.to_screen_y(50.0),
|
||||
49.0 * static_cast<plot::f64>(axes.h), 1e-3);
|
||||
PLOT_CHECK_NEAR(ty.to_screen_y(-50.0) - static_cast<plot::f64>(axes.bottom()),
|
||||
50.0 * static_cast<plot::f64>(axes.h), 1e-3);
|
||||
|
||||
// The overflow, measured: the ink the three checks below are about. Everything it names
|
||||
// is what the clip has to remove, and its magnitude -- how far past each edge the
|
||||
// furthest pixel landed -- is the defect's size.
|
||||
const OutsideInk outside = outside_ink(frame, kW, kH, axes);
|
||||
// What is left of the defect when it comes back is a strip of ink nobody would see in a
|
||||
// screenshot, so the failure says where it landed: the size and direction print only
|
||||
// when there is an overflow to print, immediately before the check that fails on it.
|
||||
if (outside.pixels != 0u) {
|
||||
std::printf("[clip] %u trace pixels outside the axes -- left %.1f right %.1f above %.1f"
|
||||
" below %.1f px (axes %.1f,%.1f %.1fx%.1f); y label strip %u, x label strip"
|
||||
" %u\n",
|
||||
outside.pixels, static_cast<double>(outside.left),
|
||||
static_cast<double>(outside.right), static_cast<double>(outside.above),
|
||||
static_cast<double>(outside.below), static_cast<double>(axes.x),
|
||||
static_cast<double>(axes.y), static_cast<double>(axes.w),
|
||||
static_cast<double>(axes.h),
|
||||
predicate_pixels(frame, kW, layout.y_label_area, trace_pixel),
|
||||
predicate_pixels(frame, kW, layout.x_label_area, trace_pixel));
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
// (1) The y label strip and the y title strip beyond it: the coarse trace's first
|
||||
// segment lies one whole x unit -- some fifty pixels -- left of the axes' left edge.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_label_area, trace_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_title_area, trace_pixel) == 0u);
|
||||
|
||||
// (2) The x label strip and the x title strip below it.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_label_area, trace_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_title_area, trace_pixel) == 0u);
|
||||
|
||||
// (3) Nothing outside the axes anywhere in the framebuffer -- including over the host's
|
||||
// surface outside the viewport, which the viewport-scissored clear left bare for the
|
||||
// overflow to land on.
|
||||
PLOT_CHECK(outside.pixels == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, trace_pixel) ==
|
||||
predicate_pixels(frame, kW, axes, trace_pixel));
|
||||
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);
|
||||
|
||||
// And the complement, so the clip cannot be smaller than the rect either: the comb
|
||||
// crosses every column of the axes, reaches its first and last columns, and its
|
||||
// segments span its first and last rows. A clip inset by even a strip passes every
|
||||
// check above and fails here.
|
||||
PLOT_CHECK(covered_columns(ctx, axes, 24) == 24u);
|
||||
PLOT_CHECK(column_has(ctx, static_cast<plot::u32>(std::ceil(axes.x)), axes, trace_pixel));
|
||||
PLOT_CHECK(
|
||||
column_has(ctx, static_cast<plot::u32>(std::floor(axes.right())) - 1u, axes, trace_pixel));
|
||||
PLOT_CHECK(axes_row_has(ctx, axes, static_cast<plot::u32>(std::ceil(axes.y)), trace_pixel));
|
||||
PLOT_CHECK(axes_row_has(ctx, axes, static_cast<plot::u32>(std::floor(axes.bottom())) - 1u,
|
||||
trace_pixel));
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// The same defect across a `Figure`'s rows. Each row is a `Plot` laid out in its own
|
||||
/// rect but drawn into one framebuffer, so the bottom row's off-scale samples were drawn
|
||||
/// straight into the row above -- over that row's data and its chrome -- and clearing the
|
||||
/// framebuffer once per frame did nothing to stop it. The clip is per row, so the ink
|
||||
/// stops at the axes the row owns.
|
||||
PLOT_TEST(figure_row_overshoot_stays_out_of_the_neighbouring_row) {
|
||||
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));
|
||||
|
||||
// The top row carries an ordinary in-range wave, so the row the overflow would land in
|
||||
// has content of its own to be told apart from it; the bottom row carries the comb,
|
||||
// off scale by fifty axes' heights.
|
||||
const std::vector<plot::f32> top = wave(640u, 0.0f);
|
||||
plot::data::ArraySeries top_series(top, 0.0, 1.0);
|
||||
plot::data::ArraySeries bottom_series(off_scale_wave(kCombSamples), 0.0,
|
||||
kOffScaleSpan / static_cast<plot::f64>(kCombSamples));
|
||||
|
||||
plot::Figure figure(2u);
|
||||
figure.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
add_row_trace(figure.plot(0u), top_series, kRowBlue);
|
||||
{
|
||||
plot::Plot& p = figure.plot(1u);
|
||||
const plot::u32 ax =
|
||||
p.add_x_axis({"t", {0.0, kOffScaleSpan}, false, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&bottom_series, kRowMagenta, 1.5f, ax, ay});
|
||||
p.set_x_range(ax, {kOffScaleMin, kOffScaleMax});
|
||||
}
|
||||
|
||||
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, "clip_figure_rows.ppm");
|
||||
|
||||
const std::vector<plot::u8> frame = read_frame(ctx);
|
||||
const plot::FigureLayout layout = figure.layout();
|
||||
PLOT_CHECK(layout.rows.size() == 2u);
|
||||
const plot::Rect& row0 = layout.rows[0];
|
||||
const plot::PlotLayout& bottom = figure.plot(1u).layout();
|
||||
const plot::Rect& axes1 = bottom.axes;
|
||||
|
||||
// Not vacuous in either direction: both rows drew their own trace.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, axes1, magenta_pixel) > 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, row0, trace_pixel) > 0u);
|
||||
// The bottom row's off-scale ink used to be drawn here -- over the top row's data, its
|
||||
// strips and its chrome -- which is the neighbouring row it does not own.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, row0, magenta_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, bottom.y_label_area, magenta_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, bottom.x_label_area, magenta_pixel) == 0u);
|
||||
// All of its ink is inside its own axes and none of it is anywhere else in the frame.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, magenta_pixel) ==
|
||||
predicate_pixels(frame, kW, axes1, magenta_pixel));
|
||||
// The top row is untouched by the clip as well: its own trace is still in its rect.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, row0, trace_pixel) ==
|
||||
predicate_pixels(frame, kW, kFrameRect, trace_pixel));
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// The fix's neighbours: the overlay geometry that already declined to draw over the
|
||||
/// strips did so by its own column rules -- a region's bounds outside the window are
|
||||
/// skipped by `draw_region_edge`, a cursor scrolled out of it by `draw_cursors`, and a
|
||||
/// region's fill is clamped to the axes' columns by `Plot::build_regions()`. The clip
|
||||
/// makes that structural rather than a rule every pass has to remember, and this case
|
||||
/// pins both halves: the off-window overlay puts no pixel in a strip, and the geometry
|
||||
/// that *is* on screen is not clipped away with it.
|
||||
PLOT_TEST(out_of_window_region_and_cursor_stay_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, 300.0}); // ~1 sample/pixel: every column is crossed
|
||||
|
||||
// A region entirely right of the window: nothing of it may be drawn.
|
||||
plot::Region reg;
|
||||
reg.x0 = 500.0;
|
||||
reg.x1 = 800.0;
|
||||
reg.snap = plot::RegionSnap::Free;
|
||||
reg.fill = plot::Color::hex(0x33FF00, 0.35f);
|
||||
reg.edge = plot::Color::hex(0xFF00FF);
|
||||
const plot::u32 rid = p.add_region(reg);
|
||||
// One cursor inside the window and one past its right edge, with a measurement between
|
||||
// them: the band is bounded by the window's edge rather than by the off-window cursor.
|
||||
const plot::u32 in_cursor = p.add_cursor(150.0, plot::Color::hex(0xFFB020));
|
||||
const plot::u32 out_cursor = p.add_cursor(400.0, plot::Color::hex(0x35D0A8));
|
||||
const plot::u32 measure = p.add_measure(in_cursor, out_cursor, 0u);
|
||||
|
||||
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, "clip_neighbours.ppm");
|
||||
|
||||
const std::vector<plot::u8> frame = read_frame(ctx);
|
||||
const plot::PlotLayout layout = p.layout();
|
||||
const plot::Rect& axes = layout.axes;
|
||||
PLOT_CHECK(layout.regions.size() == 1u);
|
||||
PLOT_CHECK(layout.regions[0].region == rid);
|
||||
// The layout agrees with the clip: an off-window region has no fill to shade and no
|
||||
// column inside the axes for either bound.
|
||||
PLOT_CHECK(layout.regions[0].fill.w == 0.0f);
|
||||
|
||||
// Nothing of the off-window region was drawn, in a strip or anywhere else.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, magenta_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, green_dominant) == 0u);
|
||||
// The off-window cursor drew nothing; the in-window one drew its stroke, and all of
|
||||
// that ink is inside the axes.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, axes, accent_pixel) > 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, accent_pixel) ==
|
||||
predicate_pixels(frame, kW, axes, accent_pixel));
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_label_area, accent_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_label_area, accent_pixel) == 0u);
|
||||
// The measurement band between an on-window and an off-window cursor is drawn, and it
|
||||
// stops at the axes: it is the one pass whose geometry is built from two cursors, so a
|
||||
// clamp that forgot one of them would put the tint over the strip.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, axes, band_pixel) > 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, band_pixel) ==
|
||||
predicate_pixels(frame, kW, axes, band_pixel));
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_label_area, band_pixel) == 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_title_area, band_pixel) == 0u);
|
||||
// And the trace is still whole: the clip is the axes rect, so the data still fills it.
|
||||
PLOT_CHECK(covered_columns(ctx, axes, 16) >= 15u);
|
||||
PLOT_CHECK(p.measurement(measure).valid);
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// The clip's other half: what it must *not* do. A trace off scale is held inside `axes`
|
||||
/// in this frame as in the cases above, while the legend -- panel, swatch and name, all
|
||||
/// measured inside the rect -- survives, and the tick labels and axis titles, which live
|
||||
/// outside it by design, are still drawn: the clip is released before those passes. All
|
||||
/// three are asserted from the pixels, so a clip that was never released (which loses the
|
||||
/// strips) or one released too early (which loses the legend) fails here rather than in a
|
||||
/// screenshot nobody looks at.
|
||||
PLOT_TEST(legend_and_strip_labels_survive_the_axes_clip) {
|
||||
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)});
|
||||
// Titled axes, so both title strips exist and hold something to keep.
|
||||
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});
|
||||
// A translucent trace whose swatch is drawn opaque: the exact colour of the swatch is
|
||||
// then one only a swatch can produce, which is what makes the count below a statement
|
||||
// about the legend rather than about the trace that carries the same colour.
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF, 0.25f), 1.5f, ax, ay, "signal"});
|
||||
p.set_x_range(ax, {0.0, 300.0});
|
||||
// The trace that has to stay inside the axes -- the same off-scale values as the cases
|
||||
// above, sampled across this one's whole window -- in a second blue no other element of
|
||||
// the frame produces, and unnamed, so it adds no legend entry.
|
||||
plot::data::ArraySeries off_scale(off_scale_wave(3600u), 0.0, 300.0 / 3600.0);
|
||||
p.add_trace({&off_scale, plot::Color::hex(0x35A0FF), 1.5f, ax, ay});
|
||||
|
||||
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, "clip_legend.ppm");
|
||||
|
||||
const std::vector<plot::u8> frame = read_frame(ctx);
|
||||
const plot::PlotLayout layout = p.layout();
|
||||
const plot::Color ink = r.style().axis_text;
|
||||
PLOT_CHECK(layout.legend.rows.size() == 1u);
|
||||
const plot::LegendRow& row = layout.legend.rows[0];
|
||||
PLOT_CHECK(row.swatch.w > 0.0f && row.swatch.h > 0.0f);
|
||||
// The legend is inside the axes, as its layout promises, and its swatch is still drawn
|
||||
// -- opaquely, and nowhere outside the rect the clip holds it in.
|
||||
PLOT_CHECK(row.swatch.x >= layout.axes.x && row.swatch.right() <= layout.axes.right());
|
||||
PLOT_CHECK(row.swatch.y >= layout.axes.y && row.swatch.bottom() <= layout.axes.bottom());
|
||||
const plot::u32 swatch = exact_pixels(frame, kW, row.swatch, p.trace(0u).color);
|
||||
PLOT_CHECK(swatch > 0u);
|
||||
PLOT_CHECK(exact_pixels(frame, kW, kFrameRect, p.trace(0u).color) ==
|
||||
exact_pixels(frame, kW, layout.axes, p.trace(0u).color));
|
||||
|
||||
// And the off-scale trace is held in: its ink is inside the axes and nowhere else in the
|
||||
// frame, the same statement the first case makes with the same clip and a different
|
||||
// frame around it.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, layout.axes, trace_pixel) > 0u);
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, trace_pixel) ==
|
||||
predicate_pixels(frame, kW, layout.axes, trace_pixel));
|
||||
|
||||
// The strips outside the axes are drawn, in all four: the clip was released before the
|
||||
// tick-label and axis-title passes, which is what keeps them from being erased.
|
||||
PLOT_CHECK(scan_rect_ink(ctx, layout.y_label_area, ink).pixels > 0u);
|
||||
PLOT_CHECK(scan_rect_ink(ctx, layout.x_label_area, ink).pixels > 0u);
|
||||
PLOT_CHECK(scan_rect_ink(ctx, layout.y_title_area, ink).pixels > 0u);
|
||||
PLOT_CHECK(scan_rect_ink(ctx, layout.x_title_area, ink).pixels > 0u);
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// The clip is the frame's, not the host's. `renderer.hpp` documents the GL state a frame
|
||||
/// leaves behind, and the scissor is now one of the pieces it moves: the frame ends with
|
||||
/// the scissor test off -- not left enabled on the axes rect of its last interior pass,
|
||||
/// which would clip whatever the embedding host drew next, and not on the viewport rect
|
||||
/// either, which would do the same to a host drawing outside its plot's region. Observed
|
||||
/// rather than documented: a clear the host issues after `end_frame()` has to reach the
|
||||
/// whole framebuffer.
|
||||
PLOT_TEST(a_frame_leaves_no_clip_for_the_host_to_inherit) {
|
||||
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;
|
||||
const plot::Rect vp{80.0f, 40.0f, 400.0f, 240.0f};
|
||||
p.set_viewport(vp);
|
||||
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, 300.0});
|
||||
|
||||
r.begin_frame(kW, kH, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
|
||||
// The host paints its own surface next, as an embedding host does. With the frame's
|
||||
// clip left engaged -- at the axes rect, or at the viewport's -- this clear would cover
|
||||
// only that rect, and the corners below would still hold the frame's background.
|
||||
plot::gl::glClearColor(0.75f, 0.25f, 0.5f, 1.0f);
|
||||
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
|
||||
const auto host_clear = [&](plot::u32 x, plot::u32 y) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(x, y, px));
|
||||
return px[0] > 180 && px[1] < 90 && px[2] > 100 && px[2] < 160;
|
||||
};
|
||||
PLOT_CHECK(host_clear(2u, 2u)); // a corner, outside the viewport
|
||||
PLOT_CHECK(host_clear(kW - 3u, kH - 3u)); // the opposite corner
|
||||
PLOT_CHECK(host_clear(2u, kH - 3u)); // under the viewport, in its columns
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user