feat(render): mark the samples of a window sparse enough to see them
`Style::marker_below_samples` (50 by default, `0` for never) draws a `marker_px` dot on every *raw* sample when the visible window holds at most that many, in the trace's own colour. Zoomed in far enough to see individual samples, the polyline between them stops being the shape a reader is looking at -- a handful of points across a whole panel ARE the picture -- and a dot makes each one a place rather than a bend in a line. Level 0 only: above it the geometry is pyramid buckets, which are not points anyone can find. Test: a_sparse_window_marks_its_samples -- the same flat series at ten samples and at a hundred. Ten gives exactly ten dots, evenly spaced (their centres are checked against the panel's own step, so "they are the samples" is asserted rather than "something was painted"); a hundred is past the threshold and gives none. The dots *group* into runs of columns (a marker is `marker_px` wide), which is how the test reads one marker per sample -- counting columns read three.
This commit is contained in:
1 parent
fba8d985cf
commit
7dbe4e7183
5 files changed
+121
No files matched your search
@@ -1383,6 +1383,93 @@ PLOT_TEST(a_cursor_stroke_can_be_thinner_and_dashed) {
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// A window sparse enough that its samples *are* the shape gets a marker on each one. Ten samples
|
||||
/// across a panel are places a reader counts, and the polyline between them is not the shape any
|
||||
/// more; a hundred are a curve and the markers would be noise, which is what the threshold is for.
|
||||
/// The test reads the vertical run of trace-coloured pixels per column: a marker is `marker_px`
|
||||
/// across, so at a sample's own column the run is taller than the line, and at a midpoint it is
|
||||
/// the line alone. The dense case proves the second half -- same series shape, no column taller
|
||||
/// than the stroke.
|
||||
PLOT_TEST(a_sparse_window_marks_its_samples) {
|
||||
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_CHECK(r.style().marker_below_samples >= 50.0f); // the default is the spec's 50
|
||||
|
||||
const auto run_at = [&](plot::Plot& p, const plot::Rect& a) {
|
||||
const plot::u32 first_row = static_cast<plot::u32>(a.y);
|
||||
const plot::u32 rows = static_cast<plot::u32>(a.h);
|
||||
std::vector<plot::u32> tall;
|
||||
for (plot::u32 cx = static_cast<plot::u32>(a.x);
|
||||
cx < static_cast<plot::u32>(a.x + a.w); ++cx) {
|
||||
plot::u32 best = 0;
|
||||
plot::u32 run = 0;
|
||||
for (plot::u32 yy = 0; yy < rows; ++yy) {
|
||||
plot::u8 px[4] = {};
|
||||
PLOT_CHECK(ctx.read_pixel(cx, ctx.height - 1u - (first_row + yy), px));
|
||||
if (green_pixel(px)) {
|
||||
++run;
|
||||
best = std::max(best, run);
|
||||
} else {
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
if (best >= 3u) tall.push_back(cx);
|
||||
}
|
||||
(void)p;
|
||||
return tall;
|
||||
};
|
||||
// The dots *group*: a marker is `marker_px` wide, so it spans several adjacent columns. What
|
||||
// identifies one is a run of tall columns, exactly as a cursor's stroke spans two -- the first
|
||||
// version of this test counted columns and read three markers per sample.
|
||||
const auto groups = [](const std::vector<plot::u32>& cols) {
|
||||
std::vector<plot::u32> centres;
|
||||
for (std::size_t i = 0; i < cols.size();) {
|
||||
std::size_t j = i;
|
||||
while (j + 1 < cols.size() && cols[j + 1] == cols[j] + 1u) ++j;
|
||||
centres.push_back((cols[i] + cols[j]) / 2u);
|
||||
i = j + 1;
|
||||
}
|
||||
return centres;
|
||||
};
|
||||
|
||||
// Ten samples, a flat line: every sample is a place, and none of them is a bend.
|
||||
const auto sparse = [&](plot::u32 samples, const char* what) {
|
||||
std::vector<plot::f32> y(samples, 0.25f);
|
||||
const plot::data::ArraySeries series(y, 0.0, 10.0 / static_cast<plot::f64>(samples));
|
||||
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", {0.0, 10.0}, false, 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({&series, plot::Color::hex(0x00FF00), 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());
|
||||
const std::vector<plot::u32> tall = run_at(p, p.layout().axes);
|
||||
const std::vector<plot::u32> centres = groups(tall);
|
||||
std::printf("[markers] %s: %zu columns, %zu groups, visible=%llu\n", what, tall.size(),
|
||||
centres.size(), static_cast<unsigned long long>(r.stats().visible_samples));
|
||||
return centres;
|
||||
};
|
||||
|
||||
const std::vector<plot::u32> ten = sparse(10u, "10 samples");
|
||||
PLOT_CHECK(ten.size() == 10u); // one marker per sample
|
||||
// ... and at the samples: ten of them across the panel, evenly spaced. This is the assertion
|
||||
// that says the dots are *the samples* and not something the style happened to paint.
|
||||
const double step = static_cast<double>(ten.back() - ten.front()) / 9.0;
|
||||
for (std::size_t i = 1; i < ten.size(); ++i) {
|
||||
PLOT_CHECK_NEAR(static_cast<double>(ten[i] - ten[i - 1]), step, 2.0);
|
||||
}
|
||||
// A hundred samples: the same shape, past the threshold, and no group is a dot.
|
||||
const std::vector<plot::u32> hundred = sparse(100u, "100 samples");
|
||||
PLOT_CHECK(hundred.size() <= 5u);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
PLOT_TEST(grid_hairlines_land_on_whole_pixels) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
|
||||
Reference in new issue
Block a user