feat(render): independent major and minor grid classes tied to the tick set
This commit is contained in:
1 parent
69495a3799
commit
eaae07d8e0
7 files changed
+635
-16
No files matched your search
@@ -56,6 +56,41 @@ bool has_horizontal(const plot::Grid& g, f64 y, bool major) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The same question in *screen* space: the renderer places a line by mapping its
|
||||
/// `x0`/`y0` through the axis' transform, so this is the coincidence the pixels show.
|
||||
bool has_vertical_screen(const plot::Grid& g, const plot::Transform& tx, f64 sx, bool major) {
|
||||
for (const plot::GridLine& l : g.lines()) {
|
||||
if (l.x0 == l.x1 && l.major == major && tx.to_screen_x(l.x0) == sx) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool has_horizontal_screen(const plot::Grid& g, const plot::Transform& ty, f64 sy, bool major) {
|
||||
for (const plot::GridLine& l : g.lines()) {
|
||||
if (l.y0 == l.y1 && l.major == major && ty.to_screen_y(l.y0) == sy) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// How many of `g`'s lines carry one class: `vertical` picks the direction, `major`
|
||||
/// the class.
|
||||
u32 count_lines(const plot::Grid& g, bool vertical, bool major) {
|
||||
u32 n = 0;
|
||||
for (const plot::GridLine& l : g.lines()) {
|
||||
if (((l.x0 == l.x1) == vertical) && l.major == major) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Whether `ticks` holds `value` -- the "and every tick has a line" half of the
|
||||
/// coincidence check, asked of the list the grid was built from.
|
||||
bool tick_at(const std::vector<plot::Tick>& ticks, f64 value) {
|
||||
for (const plot::Tick& t : ticks) {
|
||||
if (t.value == value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PLOT_TEST(layout_without_text_backend_uses_default_label_areas) {
|
||||
@@ -574,6 +609,180 @@ PLOT_TEST(grid_lines_are_tick_aligned_data_space_segments) {
|
||||
PLOT_CHECK(minor > 0); // minor grid is generated too
|
||||
}
|
||||
|
||||
/// The grid's positions have one source -- the tick set -- and this pins that, in
|
||||
/// both directions and in both spaces: a line with no tick and a tick with no line
|
||||
/// are each a defect, and one direction alone is satisfied by a grid that drops
|
||||
/// ticks or by one that invents them. A labelled major tick and a major grid line
|
||||
/// therefore coincide *by construction* (spec §11), not by two derivations that
|
||||
/// happen to agree on the ranges this case tries.
|
||||
///
|
||||
/// Screen space is checked beside data space because the renderer places a line by
|
||||
/// mapping its endpoints through the axis' transform (`Renderer::draw_grid`), so
|
||||
/// screen coincidence is what the pixels show. A log axis is among the ranges: that
|
||||
/// transform is the one that is not the identity.
|
||||
PLOT_TEST(major_grid_lines_coincide_with_major_ticks) {
|
||||
/// Both directions, both spaces, one line per tick -- for one laid-out plot.
|
||||
const auto check_pair = [](const plot::Plot& p, u32 ax, u32 ay, const char* what) {
|
||||
const plot::Grid& g = p.grid();
|
||||
const plot::Transform tx = p.x_transform(ax);
|
||||
const plot::Transform ty = p.y_transform(ay);
|
||||
const std::vector<plot::Tick>& xt = p.x_axis(ax).ticks;
|
||||
const std::vector<plot::Tick>& yt = p.y_axis(ay).ticks;
|
||||
PLOT_CHECK(!xt.empty());
|
||||
PLOT_CHECK(!yt.empty());
|
||||
|
||||
// Exactly one line per tick of the tick set the grid was built from: none
|
||||
// dropped, none invented, and both classes carried through.
|
||||
PLOT_CHECK(count_lines(g, true, true) + count_lines(g, true, false) == xt.size());
|
||||
PLOT_CHECK(count_lines(g, false, true) + count_lines(g, false, false) == yt.size());
|
||||
PLOT_CHECK(count_lines(g, true, false) > 0u); // the range really has minors
|
||||
PLOT_CHECK(count_lines(g, false, false) > 0u);
|
||||
|
||||
// Every tick -> a line of its own class, and never of the other class.
|
||||
for (const plot::Tick& t : xt) {
|
||||
PLOT_CHECK(has_vertical(g, t.value, t.major));
|
||||
PLOT_CHECK(!has_vertical(g, t.value, !t.major));
|
||||
PLOT_CHECK(has_vertical_screen(g, tx, tx.to_screen_x(t.value), t.major));
|
||||
}
|
||||
for (const plot::Tick& t : yt) {
|
||||
PLOT_CHECK(has_horizontal(g, t.value, t.major));
|
||||
PLOT_CHECK(!has_horizontal(g, t.value, !t.major));
|
||||
PLOT_CHECK(has_horizontal_screen(g, ty, ty.to_screen_y(t.value), t.major));
|
||||
}
|
||||
// Every line -> a tick of its own class.
|
||||
for (const plot::GridLine& l : g.lines()) {
|
||||
if (l.x0 == l.x1) {
|
||||
PLOT_CHECK(tick_at(xt, l.x0));
|
||||
} else {
|
||||
PLOT_CHECK(tick_at(yt, l.y0));
|
||||
}
|
||||
}
|
||||
std::printf("[grid] %-22s x %u ticks (%u major) -> %u vertical lines; y %u ticks (%u major)"
|
||||
" -> %u horizontal, %s\n",
|
||||
what, static_cast<unsigned>(xt.size()), count_lines(g, true, true),
|
||||
count_lines(g, true, true) + count_lines(g, true, false),
|
||||
static_cast<unsigned>(yt.size()), count_lines(g, false, true),
|
||||
count_lines(g, false, true) + count_lines(g, false, false),
|
||||
(tx.log_x || ty.log_y) ? "log" : "linear");
|
||||
};
|
||||
|
||||
// Several ranges and axes widths, so the check is not one nice-number step's
|
||||
// accident.
|
||||
struct Scene {
|
||||
const char* what;
|
||||
f64 lo, hi;
|
||||
f32 w, h;
|
||||
};
|
||||
const Scene scenes[] = {
|
||||
{"[0, 10] 800x400", 0.0, 10.0, 800.0f, 400.0f},
|
||||
{"[0, 100] 800x400", 0.0, 100.0, 800.0f, 400.0f},
|
||||
{"[-5, 5] 640x360", -5.0, 5.0, 640.0f, 360.0f},
|
||||
{"[0, 3.3] 200x120", 0.0, 3.3, 200.0f, 120.0f}, // a narrow axes: few ticks
|
||||
};
|
||||
for (const Scene& s : scenes) {
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, s.w, s.h});
|
||||
const u32 ax = p.add_x_axis({"t", {s.lo, s.hi}, false, plot::AxisPlace::Bottom, false});
|
||||
const u32 ay = p.add_y_axis({"v", {s.lo, s.hi}, false, plot::AxisPlace::Left, false});
|
||||
(void)p.layout();
|
||||
check_pair(p, ax, ay, s.what);
|
||||
}
|
||||
|
||||
// A log pair: the same data-space coincidence, and the same once mapped through the
|
||||
// log10 pre-affine -- where an equal data step is *not* an equal screen step, so a
|
||||
// grid built on any second derivation would part company here first.
|
||||
{
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ax = p.add_x_axis({"t", {1.0, 1000.0}, false, plot::AxisPlace::Bottom, true});
|
||||
const u32 ay = p.add_y_axis({"v", {1.0, 100.0}, false, plot::AxisPlace::Left, true});
|
||||
(void)p.layout();
|
||||
PLOT_CHECK(p.x_axis(ax).log);
|
||||
PLOT_CHECK(p.y_axis(ay).log);
|
||||
PLOT_CHECK(p.x_transform(ax).log_x);
|
||||
PLOT_CHECK(p.y_transform(ay).log_y);
|
||||
check_pair(p, ax, ay, "log [1, 1000]");
|
||||
}
|
||||
|
||||
// A tick set with no minors in it -- `generate_ticks(..., include_minors = false)`,
|
||||
// the labelled set a caller draws numbers for -- makes a grid with no minors: the
|
||||
// grid owns no subdivision of its own. Both directions still hold, and the minor
|
||||
// class is empty on *both* sides, which is asserted rather than skipped: an empty
|
||||
// class on one side only is exactly the drift this case exists to catch.
|
||||
{
|
||||
std::vector<plot::Tick> xt;
|
||||
std::vector<plot::Tick> yt;
|
||||
plot::generate_ticks({0.0, 10.0}, 400.0f, 60.0f, xt, false);
|
||||
plot::generate_ticks({0.0, 1.0}, 300.0f, 60.0f, yt, false);
|
||||
PLOT_CHECK(!xt.empty());
|
||||
PLOT_CHECK(!yt.empty());
|
||||
for (const plot::Tick& t : xt) PLOT_CHECK(t.major);
|
||||
for (const plot::Tick& t : yt) PLOT_CHECK(t.major);
|
||||
|
||||
plot::Grid g;
|
||||
g.build(xt, {0.0, 10.0}, yt, {0.0, 1.0});
|
||||
PLOT_CHECK(g.size() == xt.size() + yt.size());
|
||||
for (const plot::Tick& t : xt) {
|
||||
PLOT_CHECK(has_vertical(g, t.value, true));
|
||||
PLOT_CHECK(!has_vertical(g, t.value, false));
|
||||
}
|
||||
for (const plot::Tick& t : yt) {
|
||||
PLOT_CHECK(has_horizontal(g, t.value, true));
|
||||
PLOT_CHECK(!has_horizontal(g, t.value, false));
|
||||
}
|
||||
PLOT_CHECK(count_lines(g, true, false) == 0u);
|
||||
PLOT_CHECK(count_lines(g, false, false) == 0u);
|
||||
std::printf("[grid] majors-only tick set %u x ticks, %u y ticks -> %zu lines, 0 minors\n",
|
||||
static_cast<unsigned>(xt.size()), static_cast<unsigned>(yt.size()), g.size());
|
||||
}
|
||||
|
||||
// Two axis pairs. `Plot::build_grid()` builds the grid from the *primary* pair (x
|
||||
// axis 0, y axis 0) and `Renderer::draw_grid()` maps it with that pair's transforms
|
||||
// -- the comment there says so -- so a second pair's ticks get no lines at all.
|
||||
// Pinned rather than assumed: the coincidence above is with the primary pair's
|
||||
// ticks, and a value only the second axis ticks at has no line, in data space or on
|
||||
// screen. (A trace on the second pair is still drawn in the data area; it simply
|
||||
// has no grid of its own.)
|
||||
{
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ax0 = p.add_x_axis({"a", {0.0, 100.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const u32 ax1 = p.add_x_axis({"b", {0.0, 1.0}, false, plot::AxisPlace::Top, false});
|
||||
const u32 ay0 = p.add_y_axis({"v", {0.0, 100.0}, false, plot::AxisPlace::Left, false});
|
||||
const u32 ay1 = p.add_y_axis({"w", {0.0, 1.0}, false, plot::AxisPlace::Right, false});
|
||||
(void)p.layout();
|
||||
check_pair(p, ax0, ay0, "primary pair only");
|
||||
|
||||
PLOT_CHECK(p.x_axis_count() == 2u);
|
||||
PLOT_CHECK(p.y_axis_count() == 2u);
|
||||
const plot::Grid& g = p.grid();
|
||||
const plot::Transform tx1 = p.x_transform(ax1);
|
||||
const plot::Transform ty1 = p.y_transform(ay1);
|
||||
u32 x_only_secondary = 0u;
|
||||
u32 y_only_secondary = 0u;
|
||||
for (const plot::Tick& t : p.x_axis(ax1).ticks) {
|
||||
if (tick_at(p.x_axis(ax0).ticks, t.value)) continue; // 0 is on both ranges
|
||||
++x_only_secondary;
|
||||
PLOT_CHECK(!has_vertical(g, t.value, t.major));
|
||||
PLOT_CHECK(!has_vertical_screen(g, tx1, tx1.to_screen_x(t.value), t.major));
|
||||
}
|
||||
for (const plot::Tick& t : p.y_axis(ay1).ticks) {
|
||||
if (tick_at(p.y_axis(ay0).ticks, t.value)) continue;
|
||||
++y_only_secondary;
|
||||
PLOT_CHECK(!has_horizontal(g, t.value, t.major));
|
||||
PLOT_CHECK(!has_horizontal_screen(g, ty1, ty1.to_screen_y(t.value), t.major));
|
||||
}
|
||||
// The case is not vacuous: the second pair really does tick where the first does
|
||||
// not, so "no line there" is a statement about the build and not about the ranges.
|
||||
PLOT_CHECK(x_only_secondary > 0u);
|
||||
PLOT_CHECK(y_only_secondary > 0u);
|
||||
std::printf("[grid] primary pair only %u secondary x ticks with no line, %u secondary y"
|
||||
" ticks with no line; the grid is the primary pair's %zu lines\n",
|
||||
static_cast<unsigned>(x_only_secondary),
|
||||
static_cast<unsigned>(y_only_secondary), g.size());
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- legend
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -2787,3 +2787,378 @@ PLOT_TEST(trace_stroke_weight_is_logical) {
|
||||
PLOT_CHECK_NEAR(trace_ratio / axis_ratio, 1.0f, 0.15f);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- grid classes
|
||||
|
||||
namespace {
|
||||
|
||||
/// The scene the grid-class cases below draw: two axes and **no trace, cursor,
|
||||
/// legend, measure or annotation**, so the only element that can put ink inside the
|
||||
/// axes rect is a grid line. The x range is wide relative to the axes (a major about
|
||||
/// every 100 px, its minors about every 28) so a window can be placed around one line
|
||||
/// and touch nothing else; the y range's ticks are close enough together that a band
|
||||
/// of rows fits between two of them.
|
||||
struct GridFrame {
|
||||
std::vector<plot::u8> pixels; ///< the whole framebuffer, top-down
|
||||
plot::u32 fb_w = 0u;
|
||||
plot::PlotLayout layout;
|
||||
std::vector<plot::Tick> x_ticks;
|
||||
std::vector<plot::Tick> y_ticks;
|
||||
plot::Transform x_transform;
|
||||
plot::Transform y_transform;
|
||||
plot::u32 major_lines = 0u; ///< grid segments of each class, as built
|
||||
plot::u32 minor_lines = 0u;
|
||||
plot::u32 staged_segments = 0u; ///< everything the frame staged, grid included
|
||||
};
|
||||
|
||||
/// Draws that scene at `scale` with the renderer's current style. `scale` 2 is a 2x
|
||||
/// framebuffer with a 2x viewport -- the same logical plot on a 2x display -- so the
|
||||
/// figures of the two scales are figures of two frames that were really drawn.
|
||||
GridFrame draw_grid_scene(plottest::HeadlessContext& ctx, plot::Renderer& r, plot::f32 scale) {
|
||||
const plot::u32 w = static_cast<plot::u32>(scale * static_cast<plot::f32>(kW));
|
||||
const plot::u32 h = static_cast<plot::u32>(scale * static_cast<plot::f32>(kH));
|
||||
const plot::Rect vp{0.0f, 0.0f, static_cast<plot::f32>(w), static_cast<plot::f32>(h)};
|
||||
plot::Plot p;
|
||||
p.set_viewport(vp);
|
||||
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, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
r.style().ui_scale = scale;
|
||||
r.begin_frame(ctx.width, ctx.height, vp);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
|
||||
GridFrame f;
|
||||
f.fb_w = ctx.width;
|
||||
f.layout = p.layout();
|
||||
f.x_ticks = p.x_axis(ax).ticks;
|
||||
f.y_ticks = p.y_axis(ay).ticks;
|
||||
f.x_transform = p.x_transform(ax);
|
||||
f.y_transform = p.y_transform(ay);
|
||||
for (const plot::GridLine& l : p.grid().lines()) {
|
||||
(l.major ? f.major_lines : f.minor_lines) += 1u;
|
||||
}
|
||||
f.staged_segments = r.stats().staged_segments;
|
||||
f.pixels = read_frame(ctx);
|
||||
return f;
|
||||
}
|
||||
|
||||
/// Half-width and height of a measurement window, in **device** pixels. Fixed counts
|
||||
/// rather than figures derived from the tick spacing, so the same window measures the
|
||||
/// same number of rows at 1x and 2x: a height derived from the ticks would double with
|
||||
/// the scale and then report four times the ink for twice the stroke.
|
||||
constexpr plot::f32 kWindowHalf = 3.0f;
|
||||
constexpr plot::f32 kWindowRows = 8.0f;
|
||||
|
||||
/// The top row of the `kWindowRows`-tall band of the axes rect furthest from any
|
||||
/// horizontal grid line: a slice where a window can only catch a *vertical* line's
|
||||
/// ink. The margin is asserted, not assumed -- a scene whose horizontal lines are too
|
||||
/// close together fails here instead of quietly measuring one.
|
||||
plot::f32 clear_band_top(const GridFrame& f) {
|
||||
std::vector<plot::f64> rows{f.layout.axes.y};
|
||||
for (const plot::Tick& t : f.y_ticks) {
|
||||
const plot::f64 y = f.y_transform.to_screen_y(t.value);
|
||||
if (y > f.layout.axes.y && y < f.layout.axes.bottom()) rows.push_back(y);
|
||||
}
|
||||
rows.push_back(f.layout.axes.bottom());
|
||||
std::sort(rows.begin(), rows.end());
|
||||
plot::f64 best_gap = 0.0;
|
||||
plot::f64 best_mid = 0.0;
|
||||
for (std::size_t i = 0; i + 1u < rows.size(); ++i) {
|
||||
if (rows[i + 1] - rows[i] > best_gap) {
|
||||
best_gap = rows[i + 1] - rows[i];
|
||||
best_mid = 0.5 * (rows[i] + rows[i + 1]);
|
||||
}
|
||||
}
|
||||
PLOT_CHECK(best_gap > kWindowRows + 4.0);
|
||||
return static_cast<plot::f32>(std::floor(best_mid - 0.5 * kWindowRows));
|
||||
}
|
||||
|
||||
/// The x tick of `want_major`'s class nearest the middle of the axes rect, together
|
||||
/// with the proof that its column band holds no other line: no tick of either class
|
||||
/// is within `kWindowHalf + 2` px of the chosen one. Derived rather than assumed, so a
|
||||
/// scene whose lines crowd each other fails here rather than measuring two strokes.
|
||||
plot::f64 pick_tick(const GridFrame& f, bool want_major) {
|
||||
const plot::Rect& axes = f.layout.axes;
|
||||
const plot::f64 middle = 0.5 * (static_cast<plot::f64>(axes.x) + axes.right());
|
||||
bool found = false;
|
||||
plot::f64 best = 0.0;
|
||||
plot::f64 best_distance = 0.0;
|
||||
for (const plot::Tick& t : f.x_ticks) {
|
||||
if (t.major != want_major) continue;
|
||||
const plot::f64 x = f.x_transform.to_screen_x(t.value);
|
||||
// Room at the edges: an axis line runs down them, and it is not grid ink.
|
||||
if (x < axes.x + kWindowHalf + 2.0 || x > axes.right() - kWindowHalf - 2.0) continue;
|
||||
const plot::f64 distance = std::fabs(x - middle);
|
||||
if (!found || distance < best_distance) {
|
||||
found = true;
|
||||
best = t.value;
|
||||
best_distance = distance;
|
||||
}
|
||||
}
|
||||
PLOT_CHECK(found);
|
||||
const plot::f64 chosen = f.x_transform.to_screen_x(best);
|
||||
for (const plot::Tick& t : f.x_ticks) {
|
||||
const plot::f64 dx = std::fabs(f.x_transform.to_screen_x(t.value) - chosen);
|
||||
PLOT_CHECK(dx == 0.0 || dx > kWindowHalf + 2.0);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// A window around one line of each class.
|
||||
struct GridWindows {
|
||||
plot::Rect major;
|
||||
plot::Rect minor;
|
||||
};
|
||||
|
||||
GridWindows grid_windows(const GridFrame& f) {
|
||||
const plot::f32 top = clear_band_top(f);
|
||||
const auto band = [&](plot::f64 value) {
|
||||
const plot::f32 cx = static_cast<plot::f32>(f.x_transform.to_screen_x(value));
|
||||
return plot::Rect{cx - kWindowHalf, top, 2.0f * kWindowHalf, kWindowRows};
|
||||
};
|
||||
return GridWindows{band(pick_tick(f, true)), band(pick_tick(f, false))};
|
||||
}
|
||||
|
||||
/// How much of a pixel is `c` blended over `bg`: the largest channel ratio
|
||||
/// `(px - bg) / (c - bg)`, clamped to [0, 1]. A stroke's anti-aliased fringe is partly
|
||||
/// covered and its core fully covered; the *sum* over a window is the ink the stroke
|
||||
/// laid down, which is proportional to its width. Counting pixels instead would
|
||||
/// measure how the stroke landed on the pixel grid, and counting exact colours would
|
||||
/// confuse the two classes: a major pixel at half coverage is the minor colour.
|
||||
plot::f32 pixel_coverage(const plot::u8 px[4], plot::Color bg, plot::Color c) {
|
||||
const plot::f32 value[3] = {px[0] / 255.0f, px[1] / 255.0f, px[2] / 255.0f};
|
||||
const plot::f32 base[3] = {bg.r, bg.g, bg.b};
|
||||
const plot::f32 ink[3] = {c.r, c.g, c.b};
|
||||
plot::f32 best = 0.0f;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const plot::f32 span = ink[i] - base[i];
|
||||
if (span <= 0.0f) continue;
|
||||
best = std::max(best, (value[i] - base[i]) / span);
|
||||
}
|
||||
return std::clamp(best, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
/// The ink a window of the frame holds, read with one class' colour: the coverage sum
|
||||
/// over its pixels. `kWindowRows` rows of one stroke is `width x kWindowRows` of ink,
|
||||
/// so the figure is in device pixels of stroke.
|
||||
plot::f32 window_ink(const GridFrame& f, const plot::Rect& window, plot::Color bg,
|
||||
plot::Color c) {
|
||||
plot::f32 ink = 0.0f;
|
||||
for (plot::u32 y = static_cast<plot::u32>(window.y);
|
||||
y < static_cast<plot::u32>(window.bottom()); ++y) {
|
||||
for (plot::u32 x = static_cast<plot::u32>(window.x);
|
||||
x < static_cast<plot::u32>(window.right()); ++x) {
|
||||
ink += pixel_coverage(&f.pixels[(plot::usize(y) * f.fb_w + x) * 4u], bg, c);
|
||||
}
|
||||
}
|
||||
return ink;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/// Spec §11's per-class switch, on the minor class: a class the style switches off is
|
||||
/// not drawn, and the *other* class is untouched. The second assertion is the one
|
||||
/// that catches an implementation which hides both, or which rebuilds the grid
|
||||
/// without the class it was told to hide.
|
||||
///
|
||||
/// The measurement is a window around one line of each class rather than a
|
||||
/// frame-wide colour count: the two grid colours are close enough that a half-covered
|
||||
/// pixel of the major colour reads as the minor one. Inside its window each class is
|
||||
/// alone, so the figure is that line's own ink and nothing else's.
|
||||
PLOT_TEST(disabling_the_minor_grid_removes_its_colour) {
|
||||
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 plot::Color bg = r.style().background;
|
||||
const plot::Color major_ink = r.style().grid_major;
|
||||
const plot::Color minor_ink = r.style().grid_minor;
|
||||
|
||||
const GridFrame both = draw_grid_scene(ctx, r, 1.0f);
|
||||
PLOT_CHECK(both.major_lines > 0u);
|
||||
PLOT_CHECK(both.minor_lines > 0u);
|
||||
const GridWindows win = grid_windows(both);
|
||||
const plot::f32 major_before = window_ink(both, win.major, bg, major_ink);
|
||||
const plot::f32 minor_before = window_ink(both, win.minor, bg, minor_ink);
|
||||
// A line, not one stray fringe pixel: an 8-row window over a 1 px stroke holds about
|
||||
// 8 px of ink per class, and this asks for most of it.
|
||||
PLOT_CHECK(major_before > 0.75f * kWindowRows);
|
||||
PLOT_CHECK(minor_before > 0.75f * kWindowRows);
|
||||
|
||||
r.style().grid_minor_visible = false;
|
||||
const GridFrame without = draw_grid_scene(ctx, r, 1.0f);
|
||||
// The windows are the same windows: visibility does not move the tick set.
|
||||
const GridWindows again = grid_windows(without);
|
||||
PLOT_CHECK_NEAR(again.major.x, win.major.x, 1e-3);
|
||||
PLOT_CHECK_NEAR(again.major.y, win.major.y, 1e-3);
|
||||
PLOT_CHECK_NEAR(again.minor.x, win.minor.x, 1e-3);
|
||||
|
||||
const plot::f32 major_after = window_ink(without, win.major, bg, major_ink);
|
||||
const plot::f32 minor_after = window_ink(without, win.minor, bg, minor_ink);
|
||||
const plot::f32 other_after = window_ink(without, win.minor, bg, major_ink);
|
||||
std::printf("[grid] minors off: major window %.3f -> %.3f, minor window %.3f -> %.3f"
|
||||
" (major colour there %.3f)\n",
|
||||
static_cast<double>(major_before), static_cast<double>(major_after),
|
||||
static_cast<double>(minor_before), static_cast<double>(minor_after),
|
||||
static_cast<double>(other_after));
|
||||
|
||||
// Nothing at all in the minor line's window: not the minor colour, and not the
|
||||
// major one either -- so the line was skipped, not recoloured or drawn faint.
|
||||
PLOT_CHECK_NEAR(minor_after, 0.0, 1e-4);
|
||||
PLOT_CHECK_NEAR(other_after, 0.0, 1e-4);
|
||||
// And the major class is exactly where it was: the same line, the same width, the
|
||||
// same colour, neither moved nor thinned by the other class' absence.
|
||||
PLOT_CHECK_NEAR(major_after, major_before, 1e-4);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// The symmetric case: with the major class off, the minor class is untouched and the
|
||||
/// major line's own window is empty. Together with the case above this is "each class
|
||||
/// has its own switch", not "the grid has one".
|
||||
PLOT_TEST(disabling_the_major_grid_leaves_the_minor_grid) {
|
||||
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 plot::Color bg = r.style().background;
|
||||
|
||||
const GridFrame both = draw_grid_scene(ctx, r, 1.0f);
|
||||
const GridWindows win = grid_windows(both);
|
||||
const plot::f32 major_before = window_ink(both, win.major, bg, r.style().grid_major);
|
||||
const plot::f32 minor_before = window_ink(both, win.minor, bg, r.style().grid_minor);
|
||||
PLOT_CHECK(major_before > 0.75f * kWindowRows);
|
||||
PLOT_CHECK(minor_before > 0.75f * kWindowRows);
|
||||
|
||||
r.style().grid_major_visible = false;
|
||||
const GridFrame without = draw_grid_scene(ctx, r, 1.0f);
|
||||
const plot::f32 major_after = window_ink(without, win.major, bg, r.style().grid_major);
|
||||
const plot::f32 minor_after = window_ink(without, win.minor, bg, r.style().grid_minor);
|
||||
const plot::f32 other_after = window_ink(without, win.major, bg, r.style().grid_minor);
|
||||
std::printf("[grid] majors off: major window %.3f -> %.3f (minor colour there %.3f), minor"
|
||||
" window %.3f -> %.3f\n",
|
||||
static_cast<double>(major_before), static_cast<double>(major_after),
|
||||
static_cast<double>(other_after), static_cast<double>(minor_before),
|
||||
static_cast<double>(minor_after));
|
||||
|
||||
PLOT_CHECK_NEAR(major_after, 0.0, 1e-4);
|
||||
PLOT_CHECK_NEAR(other_after, 0.0, 1e-4);
|
||||
PLOT_CHECK_NEAR(minor_after, minor_before, 1e-4);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// "A disabled class costs nothing to draw" is a statement about *staging*, and pixels
|
||||
/// cannot make it: a class drawn at zero alpha, or staged and then thrown away, leaves
|
||||
/// the same frame. So this counts the segments the frame staged and requires the count
|
||||
/// to fall by exactly that class' lines -- and, with both classes off, to be exactly
|
||||
/// what the rest of the frame stages on its own (here: the two axis lines).
|
||||
PLOT_TEST(a_disabled_grid_class_stages_nothing) {
|
||||
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 GridFrame both = draw_grid_scene(ctx, r, 1.0f);
|
||||
PLOT_CHECK(both.major_lines > 0u);
|
||||
PLOT_CHECK(both.minor_lines > 0u);
|
||||
|
||||
r.style().grid_minor_visible = false;
|
||||
const GridFrame no_minor = draw_grid_scene(ctx, r, 1.0f);
|
||||
r.style().grid_minor_visible = true;
|
||||
r.style().grid_major_visible = false;
|
||||
const GridFrame no_major = draw_grid_scene(ctx, r, 1.0f);
|
||||
r.style().grid_minor_visible = false;
|
||||
const GridFrame neither = draw_grid_scene(ctx, r, 1.0f);
|
||||
|
||||
std::printf("[grid] staged segments: both %u (majors %u, minors %u), no minors %u, no majors %u,"
|
||||
" neither %u\n",
|
||||
both.staged_segments, both.major_lines, both.minor_lines, no_minor.staged_segments,
|
||||
no_major.staged_segments, neither.staged_segments);
|
||||
|
||||
// Exactly the class' own lines disappear from staging -- one segment per tick -- and
|
||||
// the rest of the frame is still staged, so this is a skip and not a frame that
|
||||
// failed to draw.
|
||||
PLOT_CHECK(no_minor.staged_segments == both.major_lines + 2u);
|
||||
PLOT_CHECK(no_major.staged_segments == both.minor_lines + 2u);
|
||||
PLOT_CHECK(neither.staged_segments == 2u);
|
||||
PLOT_CHECK(both.staged_segments == both.major_lines + both.minor_lines + 2u);
|
||||
|
||||
// The pixels agree with the counters: with both classes off no grid line is in the
|
||||
// axes rect at all, whatever it is read with.
|
||||
const GridWindows win = grid_windows(both);
|
||||
const plot::Color bg = r.style().background;
|
||||
PLOT_CHECK_NEAR(window_ink(neither, win.major, bg, r.style().grid_major), 0.0, 1e-4);
|
||||
PLOT_CHECK_NEAR(window_ink(neither, win.minor, bg, r.style().grid_minor), 0.0, 1e-4);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// Widths are per class and logical (spec §11, on §10's rule): a 2:1 logical ratio
|
||||
/// between the classes draws a 2:1 stroke ratio, and the same logical widths at
|
||||
/// `ui_scale` 2 lay down twice the *device* ink. One window per class, the same
|
||||
/// kWindowRows device rows in every run, so the figures compare: a height taken from
|
||||
/// the ticks would double with the scale and turn "twice the stroke" into four times
|
||||
/// the measurement.
|
||||
PLOT_TEST(grid_widths_are_per_class_and_scale_with_dpi) {
|
||||
constexpr plot::u32 kBigW = 2u * kW;
|
||||
constexpr plot::u32 kBigH = 2u * kH;
|
||||
plottest::HeadlessContext ctx(kBigW, kBigH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
const plot::Color bg = r.style().background;
|
||||
|
||||
// 2:1 in the fields, and the same fields across both scales below.
|
||||
r.style().grid_major_width = 2.0f;
|
||||
r.style().grid_minor_width = 1.0f;
|
||||
|
||||
const GridFrame one = draw_grid_scene(ctx, r, 1.0f);
|
||||
const GridWindows win = grid_windows(one);
|
||||
const plot::f32 major_1x = window_ink(one, win.major, bg, r.style().grid_major);
|
||||
const plot::f32 minor_1x = window_ink(one, win.minor, bg, r.style().grid_minor);
|
||||
|
||||
const GridFrame two = draw_grid_scene(ctx, r, 2.0f);
|
||||
const GridWindows big = grid_windows(two);
|
||||
const plot::f32 major_2x = window_ink(two, big.major, bg, r.style().grid_major);
|
||||
const plot::f32 minor_2x = window_ink(two, big.minor, bg, r.style().grid_minor);
|
||||
|
||||
const plot::f32 ratio_1x = major_1x / minor_1x;
|
||||
std::printf("[grid] widths 2:1 logical: 1x major %.3f minor %.3f (ratio %.3f), 2x major %.3f"
|
||||
" minor %.3f (ratio %.3f); device widths %.2f/%.2f at scale %.1f\n",
|
||||
static_cast<double>(major_1x), static_cast<double>(minor_1x),
|
||||
static_cast<double>(ratio_1x), static_cast<double>(major_2x),
|
||||
static_cast<double>(minor_2x), static_cast<double>(major_2x / minor_2x),
|
||||
static_cast<double>(r.style().device(r.style().grid_major_width)),
|
||||
static_cast<double>(r.style().device(r.style().grid_minor_width)),
|
||||
static_cast<double>(r.style().ui_scale));
|
||||
|
||||
// Same window height, one stroke each: the ink ratio is the width ratio.
|
||||
PLOT_CHECK_NEAR(big.major.h, win.major.h, 1e-6);
|
||||
PLOT_CHECK(major_1x > 1.5f * kWindowRows);
|
||||
PLOT_CHECK(minor_1x > 0.75f * kWindowRows);
|
||||
PLOT_CHECK_NEAR(ratio_1x, 2.0, 0.25);
|
||||
PLOT_CHECK_NEAR(major_2x / minor_2x, 2.0, 0.25);
|
||||
|
||||
// Both classes double in device pixels at the 2x scale ...
|
||||
PLOT_CHECK_NEAR(major_2x, 2.0 * major_1x, 0.25 * major_1x);
|
||||
PLOT_CHECK_NEAR(minor_2x, 2.0 * minor_1x, 0.25 * minor_1x);
|
||||
// ... while the logical widths that produced them did not move: the same two fields,
|
||||
// with `device()` turning them into twice the pixels.
|
||||
PLOT_CHECK_NEAR(r.style().grid_major_width, 2.0f, 1e-6f);
|
||||
PLOT_CHECK_NEAR(r.style().grid_minor_width, 1.0f, 1e-6f);
|
||||
PLOT_CHECK_NEAR(r.style().ui_scale, 2.0f, 1e-6f);
|
||||
PLOT_CHECK_NEAR(r.style().device(r.style().grid_major_width), 4.0f, 1e-6f);
|
||||
PLOT_CHECK_NEAR(r.style().device(r.style().grid_minor_width), 2.0f, 1e-6f);
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user