feat: a span declared invalid is shaded behind the data it is missing from
This commit is contained in:
1 parent
1ae72d4903
commit
ebc67bd318
6 files changed
+250
No files matched your search
@@ -7212,3 +7212,165 @@ PLOT_TEST(a_sparse_trace_is_graded_by_slope_like_a_dense_one) {
|
||||
PLOT_CHECK(std::abs(dense.riser - sparse.riser) < 24);
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
}
|
||||
|
||||
/// An invalid span is the opposite of the x note: the note is chrome that a pan must not move,
|
||||
/// and a band marks a *region of the data*, so it must pan and zoom with it. A band that stayed
|
||||
/// put under a pan would accuse the wrong samples of being missing, which is worse than drawing
|
||||
/// nothing. It also must reserve no layout space -- the label strips were measured before any
|
||||
/// band existed, and a hole must not be able to move them.
|
||||
///
|
||||
/// Measured as a footprint for the x note's reason: the band's rect is deliberately not readable
|
||||
/// back, so the only way to see it is the pixels a frame *with* it differs from the same frame
|
||||
/// without one, at the same view. Diffing at a fixed view is also what makes the measurement
|
||||
/// robust: whatever the trace does under a pan cancels between the two frames, so the bounding
|
||||
/// box is the band and nothing else.
|
||||
PLOT_TEST(an_invalid_span_is_shaded_and_moves_with_the_data) {
|
||||
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});
|
||||
PLOT_CHECK(p.add_trace({&s, plot::Color::hex(0x00E070, 1.0f), 1.5f, ax, ay, "signal"}) == 0u);
|
||||
|
||||
// The bounding box of the pixels the band, and only the band, is responsible for, plus the
|
||||
// axes rect the frame was laid out into so the caller can check it did not move.
|
||||
const auto footprint = [&](const plot::Range& view, std::pair<plot::f64, plot::f64> span,
|
||||
plot::Rect& axes_out, const char* dump) {
|
||||
p.set_x_range(ax, view);
|
||||
const std::pair<plot::f64, plot::f64> one[1] = {span};
|
||||
p.set_invalid_spans(one);
|
||||
r.begin_frame(kW, kH, kFrameRect);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
dump_if_requested(r, dump);
|
||||
const std::vector<plot::u8> with = read_frame(ctx);
|
||||
axes_out = p.layout().axes;
|
||||
p.set_invalid_spans({});
|
||||
r.begin_frame(kW, kH, kFrameRect);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
const std::vector<plot::u8> without = read_frame(ctx);
|
||||
plot::u32 lo_x = kW, lo_y = kH, hi_x = 0, hi_y = 0, differing = 0;
|
||||
for (plot::u32 y = 0; y < kH; ++y) {
|
||||
for (plot::u32 x = 0; x < kW; ++x) {
|
||||
const plot::usize at = (plot::usize(y) * kW + x) * 4u;
|
||||
if (with[at] == without[at] && with[at + 1u] == without[at + 1u] &&
|
||||
with[at + 2u] == without[at + 2u] && with[at + 3u] == without[at + 3u]) {
|
||||
continue;
|
||||
}
|
||||
++differing;
|
||||
lo_x = std::min(lo_x, x);
|
||||
lo_y = std::min(lo_y, y);
|
||||
hi_x = std::max(hi_x, x);
|
||||
hi_y = std::max(hi_y, y);
|
||||
}
|
||||
}
|
||||
// A comparison of two frames is only evidence if the band was in one of them.
|
||||
PLOT_CHECK(differing > 0u);
|
||||
return plot::Rect{static_cast<plot::f32>(lo_x), static_cast<plot::f32>(lo_y),
|
||||
static_cast<plot::f32>(hi_x - lo_x + 1u),
|
||||
static_cast<plot::f32>(hi_y - lo_y + 1u)};
|
||||
};
|
||||
|
||||
// A band over the middle third of a 640-wide window.
|
||||
plot::Rect axes_a{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
const plot::Rect none_box = footprint({0.0, 640.0}, {640.0 / 3.0, 1280.0 / 3.0}, axes_a,
|
||||
"invalid_span.ppm");
|
||||
// Inside the axes on every side -- a hole is not allowed to paint over a label strip -- and
|
||||
// the full height of the data area, because the whole column has no data.
|
||||
PLOT_CHECK(none_box.x >= axes_a.x && none_box.right() <= axes_a.right() + 1.0f);
|
||||
PLOT_CHECK(none_box.y >= axes_a.y && none_box.bottom() <= axes_a.bottom() + 1.0f);
|
||||
PLOT_CHECK_NEAR(none_box.h, axes_a.h, 2.0);
|
||||
// The middle third: a third of the width, starting a third of the way across. Two pixels of
|
||||
// slack for the fill's fractional edges.
|
||||
PLOT_CHECK_NEAR(none_box.x - axes_a.x, axes_a.w / 3.0f, 2.0);
|
||||
PLOT_CHECK_NEAR(none_box.w, axes_a.w / 3.0f, 2.0);
|
||||
|
||||
// The same span under a pan of exactly 64 device pixels' worth of data. The band is anchored
|
||||
// in the data, so it must move by 64 pixels -- the x note, measured the same way, does not
|
||||
// move at all, which is the distinction this pair of cases draws.
|
||||
const plot::f64 per_px = 640.0 / plot::f64(axes_a.w);
|
||||
const plot::f64 dx = 64.0 * per_px;
|
||||
plot::Rect axes_b{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
const plot::Rect panned = footprint({dx, 640.0 + dx}, {640.0 / 3.0, 1280.0 / 3.0}, axes_b,
|
||||
"invalid_span_panned.ppm");
|
||||
PLOT_CHECK_NEAR(panned.x, none_box.x - 64.0f, 2.0);
|
||||
PLOT_CHECK_NEAR(panned.w, none_box.w, 2.0);
|
||||
// The layout is identical across both: the band reserved nothing, so the strips the labels
|
||||
// were measured into are where they were before any band existed.
|
||||
PLOT_CHECK_NEAR(axes_b.x, axes_a.x, 1e-4);
|
||||
PLOT_CHECK_NEAR(axes_b.y, axes_a.y, 1e-4);
|
||||
PLOT_CHECK_NEAR(axes_b.w, axes_a.w, 1e-4);
|
||||
PLOT_CHECK_NEAR(axes_b.h, axes_a.h, 1e-4);
|
||||
|
||||
std::printf("[invalid span] band %.0f,%.0f %.0fx%.0f panned %.0f,%.0f %.0fx%.0f "
|
||||
"(shift %.1f px) in axes %.0f,%.0f %.0fx%.0f\n",
|
||||
none_box.x, none_box.y, none_box.w, none_box.h, panned.x, panned.y, panned.w,
|
||||
panned.h, none_box.x - panned.x, axes_a.x, axes_a.y, axes_a.w, axes_a.h);
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// A hole narrower than a pixel is still a report. A reserved span of one sample at a window
|
||||
/// width of hundreds of thousands maps to a fraction of a column, and a fill rounded to zero
|
||||
/// width would draw nothing -- the one case where the feature is most needed is the one where
|
||||
/// naive rounding silently drops it. The band therefore has a one-device-pixel floor.
|
||||
PLOT_TEST(a_sub_pixel_invalid_span_is_still_shaded) {
|
||||
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});
|
||||
PLOT_CHECK(p.add_trace({&s, plot::Color::hex(0x00E070, 1.0f), 1.5f, ax, ay, "signal"}) == 0u);
|
||||
|
||||
// One sample's worth of hole in a 200 000-wide window: well under a hundredth of a column.
|
||||
p.set_x_range(ax, {0.0, 200000.0});
|
||||
const std::pair<plot::f64, plot::f64> one[1] = {{100000.0, 100000.0}};
|
||||
p.set_invalid_spans(one);
|
||||
r.begin_frame(kW, kH, kFrameRect);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
dump_if_requested(r, "invalid_span_subpixel.ppm");
|
||||
const std::vector<plot::u8> with = read_frame(ctx);
|
||||
p.set_invalid_spans({});
|
||||
r.begin_frame(kW, kH, kFrameRect);
|
||||
r.draw(p);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
const std::vector<plot::u8> without = read_frame(ctx);
|
||||
|
||||
plot::u32 columns = 0, differing = 0;
|
||||
for (plot::u32 x = 0; x < kW; ++x) {
|
||||
bool any = false;
|
||||
for (plot::u32 y = 0; y < kH; ++y) {
|
||||
const plot::usize at = (plot::usize(y) * kW + x) * 4u;
|
||||
if (with[at] != without[at] || with[at + 1u] != without[at + 1u] ||
|
||||
with[at + 2u] != without[at + 2u] || with[at + 3u] != without[at + 3u]) {
|
||||
any = true;
|
||||
++differing;
|
||||
}
|
||||
}
|
||||
if (any) ++columns;
|
||||
}
|
||||
// Drawn, and narrow: the floor is one device pixel, not a visible band widened to be seen.
|
||||
PLOT_CHECK(differing > 0u);
|
||||
PLOT_CHECK(columns >= 1u && columns <= 2u);
|
||||
std::printf("[invalid span] sub-pixel hole lit %u column(s), %u pixels\n", columns, differing);
|
||||
ctx.destroy();
|
||||
}
|
||||
Reference in new issue
Block a user