fix(render): floor the axes so chrome cannot erase it, one title row per titled axis
Two findings on the pass-1 fix: - The reserved strips were clamped only to the viewport, so a title wider than a narrow panel took the rest of it and the axes collapsed: draw_traces() returned on a zero-width rect (no trace drawn, nothing reported) and a zero-height one flattened every trace onto a single row. The chrome is now capped at half the viewport -- the label strip is served first, the title strip takes what is left of that allowance -- an over-long title is clipped to its strip by a walk that both measures and stages it, and draw_traces() tests the height as well as the width. - Every titled y axis was staged at the same centred position inside one strip, so a Left/Right pair of titles overprinted. Each titled axis now owns a text line: the x strip grows one line per titled x axis (x_title_height), and the y runs stack down the axes-tall strip, which needs no extra width for it. Tests: `narrow_panel_with_a_long_y_title_still_draws` and `short_panel_with_an_x_title_still_draws` pin the floor (the axes keeps half the panel and the trace still covers the columns, with the title clipped into its strip), `titled_axis_pairs_get_their_own_rows` pins one text row per titled axis in both directions, and `layout_stacks_one_title_line_per_titled_axis` pins the strip sizing. All fail against the pass-1 sources.
This commit is contained in:
1 parent
8f45108559
commit
1f5aa11de2
6 files changed
+330
-51
No files matched your search
@@ -99,6 +99,30 @@ PLOT_TEST(layout_derives_label_areas_from_font_metrics) {
|
||||
PLOT_CHECK_NEAR(l.x_label_area.y, l.axes.bottom(), 1e-4);
|
||||
}
|
||||
|
||||
/// Every titled axis of a direction gets its own text line inside its strip;
|
||||
/// without that, the second titled axis of an edge is drawn on top of the first.
|
||||
PLOT_TEST(layout_stacks_one_title_line_per_titled_axis) {
|
||||
plot::render::BitmapFont font;
|
||||
plot::Plot p;
|
||||
p.set_text_backend(&font);
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const f32 pad = p.style().grid_label_pad;
|
||||
p.add_x_axis({"seconds", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_x_axis({"samples", {0.0, 1.0}, false, plot::AxisPlace::Top, false});
|
||||
p.add_y_axis({"volts", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_y_axis({"amps", {-1.0, 1.0}, false, plot::AxisPlace::Right, false});
|
||||
const plot::PlotLayout& l = p.layout();
|
||||
PLOT_CHECK(p.titled_x_axis_count() == 2u);
|
||||
PLOT_CHECK(p.titled_y_axis_count() == 2u);
|
||||
// The x titles stack in rows, so the strip grows by a line per titled axis...
|
||||
PLOT_CHECK_NEAR(l.x_title_height, 2.0f * font.height() + 2.0f * pad, 1e-4);
|
||||
// ... while the y titles stack down the axes-tall strip, so the second one costs
|
||||
// height the axes already has rather than a second column of width.
|
||||
PLOT_CHECK_NEAR(l.y_title_width,
|
||||
std::max(font.measure("volts"), font.measure("amps")) + 2.0f * pad, 1e-4);
|
||||
PLOT_CHECK_NEAR(l.axes.h, 400.0f - (l.x_axis_height + l.x_title_height), 1e-4);
|
||||
}
|
||||
|
||||
/// An axis with no title reserves nothing: the strips exist so that a titled axis
|
||||
/// has somewhere outside `axes` to draw in, not as a fixed cost per plot.
|
||||
PLOT_TEST(layout_reserves_no_title_strip_without_a_title) {
|
||||
|
||||
@@ -511,6 +511,161 @@ PLOT_TEST(mapped_ring_fallback_still_draws_after_refused_map) {
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// Fix pass 2, finding 1: the reserved strips were clamped only to the viewport, so
|
||||
// a y title wider than a narrow panel took the rest of it and `axes` collapsed to
|
||||
// no width -- and `draw_traces()` returns on a zero-width axes, so the frame drew
|
||||
// no trace at all and reported nothing. The chrome is capped now (the axes keeps
|
||||
// half the panel, the title strip takes what is left of that allowance, and the
|
||||
// title is clipped to the strip), so a narrow panel still draws its data. The
|
||||
// titles here are long on purpose: ~24 glyphs against a 240 px panel is past the
|
||||
// point where the reserve used to consume the plot.
|
||||
PLOT_TEST(narrow_panel_with_a_long_y_title_still_draws) {
|
||||
constexpr plot::u32 kPanelW = 240;
|
||||
constexpr plot::u32 kPanelH = 200;
|
||||
plottest::HeadlessContext ctx(kPanelW, kPanelH);
|
||||
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>(kPanelW), static_cast<plot::f32>(kPanelH)});
|
||||
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({"millivolts per division", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
||||
p.set_x_range(ax, {0.0, 150.0}); // a sample every few pixels: one polyline, all columns
|
||||
|
||||
r.begin_frame(kPanelW, kPanelH,
|
||||
{0, 0, static_cast<plot::f32>(kPanelW), static_cast<plot::f32>(kPanelH)});
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
|
||||
const plot::PlotLayout& layout = p.layout();
|
||||
// The chrome gave way, not the plot: the axes kept its half of the panel...
|
||||
PLOT_CHECK(layout.axes.w >= 0.5f * static_cast<plot::f32>(kPanelW));
|
||||
PLOT_CHECK(layout.axes.h > 0.0f);
|
||||
// ... and the title is still drawn, inside the strip it was cut to.
|
||||
PLOT_CHECK(layout.y_title_area.w > 0.0f);
|
||||
PLOT_CHECK(covered_columns(ctx, layout.axes, 16) >= 15);
|
||||
|
||||
const plot::Color title = r.style().axis_text;
|
||||
auto area_has_title = [&](const plot::Rect& area) {
|
||||
for (plot::u32 row = 0; row < static_cast<plot::u32>(area.h); ++row) {
|
||||
for (plot::u32 column = 0; column < static_cast<plot::u32>(area.w); ++column) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(static_cast<plot::u32>(area.x) + column,
|
||||
ctx.height - 1u - (static_cast<plot::u32>(area.y) + row), px));
|
||||
if (pixel_is(px, title)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
PLOT_CHECK(area_has_title(layout.y_title_area)); // present, though cut short
|
||||
PLOT_CHECK(!area_has_title(layout.axes)); // and never over the data
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// The same finding in the x direction: `x_title_height` collapsed `axes.h` on a
|
||||
// short panel, and the old guard tested only the width, so every trace was
|
||||
// flattened onto one row instead of being skipped where a caller could see it.
|
||||
// A 48 px panel is the case where the two strips used to be all of it.
|
||||
PLOT_TEST(short_panel_with_an_x_title_still_draws) {
|
||||
constexpr plot::u32 kPanelW = 320;
|
||||
constexpr plot::u32 kPanelH = 48;
|
||||
plottest::HeadlessContext ctx(kPanelW, kPanelH);
|
||||
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>(kPanelW), static_cast<plot::f32>(kPanelH)});
|
||||
const plot::u32 ax =
|
||||
p.add_x_axis({"time since the first sample, in seconds", s.x_bounds(), true,
|
||||
plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
||||
p.set_x_range(ax, {0.0, 150.0});
|
||||
|
||||
r.begin_frame(kPanelW, kPanelH,
|
||||
{0, 0, static_cast<plot::f32>(kPanelW), static_cast<plot::f32>(kPanelH)});
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
|
||||
const plot::PlotLayout& layout = p.layout();
|
||||
PLOT_CHECK(layout.axes.h >= 0.5f * static_cast<plot::f32>(kPanelH));
|
||||
PLOT_CHECK(layout.axes.w > 0.0f);
|
||||
PLOT_CHECK(covered_columns(ctx, layout.axes, 16) >= 15);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// Fix pass 2, finding 2: every titled y axis was staged at the same centred
|
||||
// position inside the one strip, so a Left "volts" and a Right "amps" drew over
|
||||
// each other -- the overprint the pre-strip placement did not have. Each titled
|
||||
// axis now owns a text line, so a titled pair of either direction shows two
|
||||
// separated runs of title-coloured pixels where it used to show one.
|
||||
PLOT_TEST(titled_axis_pairs_get_their_own_rows) {
|
||||
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({"seconds", s.x_bounds(), true, plot::AxisPlace::Bottom, false});
|
||||
p.add_x_axis({"samples", s.x_bounds(), true, plot::AxisPlace::Top, false});
|
||||
const plot::u32 ay = p.add_y_axis({"volts", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
p.add_y_axis({"amps", {-1.0, 1.0}, true, plot::AxisPlace::Right, false});
|
||||
p.add_trace({&s, plot::Color::hex(0x40C0FF), 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, "pairs.ppm");
|
||||
|
||||
const plot::PlotLayout& layout = p.layout();
|
||||
const plot::Color title = r.style().axis_text;
|
||||
// The rows of one strip that hold title-coloured pixels, as the runs of rows
|
||||
// they form: one run per titled axis, because the two runs of a pair are a line
|
||||
// apart and each covers several pixel rows of glyph ink.
|
||||
auto row_bands = [&](const plot::Rect& strip) {
|
||||
plot::u32 bands = 0;
|
||||
bool previous = false;
|
||||
plot::u32 rows = 0;
|
||||
for (plot::u32 row = 0; row < static_cast<plot::u32>(strip.h); ++row) {
|
||||
bool has = false;
|
||||
for (plot::u32 column = 0; column < static_cast<plot::u32>(strip.w) && !has; ++column) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(static_cast<plot::u32>(strip.x) + column,
|
||||
ctx.height - 1u - (static_cast<plot::u32>(strip.y) + row), px));
|
||||
has = pixel_is(px, title);
|
||||
}
|
||||
if (has && !previous) ++bands;
|
||||
if (has) ++rows;
|
||||
previous = has;
|
||||
}
|
||||
PLOT_CHECK(rows >= 4u); // real glyph ink, not one stray pixel
|
||||
return bands;
|
||||
};
|
||||
|
||||
PLOT_CHECK(layout.x_title_area.h > 0.0f);
|
||||
PLOT_CHECK(layout.y_title_area.w > 0.0f);
|
||||
PLOT_CHECK(row_bands(layout.x_title_area) == 2u); // "seconds" above "samples"
|
||||
PLOT_CHECK(row_bands(layout.y_title_area) == 2u); // "volts" above "amps"
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// Finding 4 of the review: `mark_drawn()` dropped a failed fence's status, so the
|
||||
// slot stayed unsynchronised and the next upload memcpy'd into storage the GPU may
|
||||
// still have been reading -- exactly the tearing the ring exists to prevent. A
|
||||
|
||||
Reference in new issue
Block a user