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
@@ -2,6 +2,8 @@
|
|||||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <span>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <plot/core/rect.hpp>
|
#include <plot/core/rect.hpp>
|
||||||
@@ -105,6 +107,13 @@ struct PlotLayout {
|
|||||||
/// controller's hit test reads the very same rects, so where a region is drawn and
|
/// controller's hit test reads the very same rects, so where a region is drawn and
|
||||||
/// where a pointer can grab it cannot drift apart.
|
/// where a pointer can grab it cannot drift apart.
|
||||||
std::vector<RegionLayout> regions;
|
std::vector<RegionLayout> regions;
|
||||||
|
/// Every invalid span's shading, in `Plot::invalid_spans()` order and clipped to the axes'
|
||||||
|
/// columns. Measured here for the regions' first reason -- the frame draws exactly these
|
||||||
|
/// numbers -- and reserving nothing, so a hole cannot move the strips the labels were
|
||||||
|
/// measured into. A span entirely outside the window contributes no rect at all, so this is
|
||||||
|
/// not index-aligned with `invalid_spans()` and nothing needs it to be: there is no hit test
|
||||||
|
/// on a band and no label beside it.
|
||||||
|
std::vector<Rect> invalid_spans;
|
||||||
/// The crosshair's geometry, when one is active: the two lines' stroke boxes, the
|
/// The crosshair's geometry, when one is active: the two lines' stroke boxes, the
|
||||||
/// readout panel and one row per visible trace with the value of the sample nearest the
|
/// readout panel and one row per visible trace with the value of the sample nearest the
|
||||||
/// pointer. Placed here rather than derived by the renderer, exactly as the legend and
|
/// pointer. Placed here rather than derived by the renderer, exactly as the legend and
|
||||||
@@ -525,6 +534,21 @@ class Plot {
|
|||||||
void set_x_note(std::string text);
|
void set_x_note(std::string text);
|
||||||
const std::string& x_note() const noexcept { return x_note_; }
|
const std::string& x_note() const noexcept { return x_note_; }
|
||||||
|
|
||||||
|
/// Spans of the primary x axis the host has declared invalid -- in udpscope's case a reserved
|
||||||
|
/// hole: samples the stream's counter says exist and that have not been delivered. Each pair
|
||||||
|
/// is `(x_begin, x_end)` in **data** coordinates, `Region`'s convention, and is shaded behind
|
||||||
|
/// the traces and over the grid.
|
||||||
|
///
|
||||||
|
/// Deliberately *not* a `Region`: a region is interactive -- draggable bounds, hot zones, a
|
||||||
|
/// label, a cached statistic, a controller hit test -- and a plot can carry dozens of holes at
|
||||||
|
/// once, which would put a hundred-odd grab targets in the way of every pointer event for
|
||||||
|
/// marks nobody can usefully drag. This is passive shading and nothing else.
|
||||||
|
///
|
||||||
|
/// The span is copied, so the caller's storage need not outlive the call. Spans need not be
|
||||||
|
/// sorted and may overlap; an empty call clears the shading.
|
||||||
|
void set_invalid_spans(std::span<const std::pair<f64, f64>> spans);
|
||||||
|
std::span<const std::pair<f64, f64>> invalid_spans() const noexcept { return invalid_spans_; }
|
||||||
|
|
||||||
/// The measurement for `id`. Re-evaluated only when a cursor moved, when the
|
/// The measurement for `id`. Re-evaluated only when a cursor moved, when the
|
||||||
/// measure was added, or when the series underneath it changed: a different
|
/// measure was added, or when the series underneath it changed: a different
|
||||||
/// series, a longer one, or a sliding window whose sample count stayed the same
|
/// series, a longer one, or a sliding window whose sample count stayed the same
|
||||||
@@ -852,6 +876,7 @@ class Plot {
|
|||||||
/// region's pixel geometry is derived -- the controller hit-tests what this
|
/// region's pixel geometry is derived -- the controller hit-tests what this
|
||||||
/// measured instead of mapping the bounds through the transform again.
|
/// measured instead of mapping the bounds through the transform again.
|
||||||
void build_regions();
|
void build_regions();
|
||||||
|
void build_invalid_spans();
|
||||||
/// Exact statistics of `region` over `series`, or an invalid result when the
|
/// Exact statistics of `region` over `series`, or an invalid result when the
|
||||||
/// interval covers no sample. The one place the region arithmetic lives.
|
/// interval covers no sample. The one place the region arithmetic lives.
|
||||||
static RegionStats evaluate_region_stats(const Region& region, const data::Series& series);
|
static RegionStats evaluate_region_stats(const Region& region, const data::Series& series);
|
||||||
@@ -899,6 +924,7 @@ class Plot {
|
|||||||
std::vector<MeasureSlot> measures_;
|
std::vector<MeasureSlot> measures_;
|
||||||
std::vector<Annotation> annotations_;
|
std::vector<Annotation> annotations_;
|
||||||
std::vector<Region> regions_;
|
std::vector<Region> regions_;
|
||||||
|
std::vector<std::pair<f64, f64>> invalid_spans_;
|
||||||
/// The (region, trace) statistics cache, filled on demand by `region_stats()`
|
/// The (region, trace) statistics cache, filled on demand by `region_stats()`
|
||||||
/// and so mutable: a lookup on a const Plot may cache, exactly as `measures_`
|
/// and so mutable: a lookup on a const Plot may cache, exactly as `measures_`
|
||||||
/// does. Entries are created for the pairs a caller actually asks about, never
|
/// does. Entries are created for the pairs a caller actually asks about, never
|
||||||
|
|||||||
@@ -513,6 +513,12 @@ class Renderer {
|
|||||||
void draw_plot(Plot& plot, const PlotLayout& layout);
|
void draw_plot(Plot& plot, const PlotLayout& layout);
|
||||||
|
|
||||||
void draw_grid(const Plot& plot);
|
void draw_grid(const Plot& plot);
|
||||||
|
/// `Plot::invalid_spans()`, as `PlotLayout::invalid_spans` measured them: one fill per span in
|
||||||
|
/// `style_.invalid_fill`. Over the grid and under everything else inside the axes, so a hole
|
||||||
|
/// reads as a property of the plot's background -- the grid shows through it and a trace that
|
||||||
|
/// does have data in those columns is drawn on top, which is what distinguishes a reserved
|
||||||
|
/// hole from a trace that has gone quiet.
|
||||||
|
void draw_invalid_spans(const PlotLayout& layout);
|
||||||
void draw_axis_lines(const Plot& plot);
|
void draw_axis_lines(const Plot& plot);
|
||||||
/// The chrome helpers take the frame's layout: `Plot::layout()` recomputes the
|
/// The chrome helpers take the frame's layout: `Plot::layout()` recomputes the
|
||||||
/// ticks, the strips and the grid, so it is computed once, in `draw()`, and handed
|
/// ticks, the strips and the grid, so it is computed once, in `draw()`, and handed
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ struct Style {
|
|||||||
Color axis_text = Color::hex(0xC8D0DC);
|
Color axis_text = Color::hex(0xC8D0DC);
|
||||||
Color cursor = Color::hex(0xFFB020);
|
Color cursor = Color::hex(0xFFB020);
|
||||||
Color measure_fill = Color::hex(0xFFB020, 0.15f);
|
Color measure_fill = Color::hex(0xFFB020, 0.15f);
|
||||||
|
/// The shading over a span the host has declared invalid (`Plot::set_invalid_spans()`): the
|
||||||
|
/// axis furniture's own neutral grey at a low alpha, on the same model as `measure_fill`
|
||||||
|
/// above. Neutral and not a trace colour on purpose -- a band is the *absence* of data, and
|
||||||
|
/// one in a hue a trace could have would read as a signal rather than as a gap. The alpha is
|
||||||
|
/// low enough that the grid under it stays legible, which is what makes the band read as a
|
||||||
|
/// region of the plot rather than as a panel laid over it.
|
||||||
|
Color invalid_fill = Color::hex(0x8A94A6, 0.18f);
|
||||||
/// The crosshair's colour: its two lines and the readout panel's border (spec §15). Markers are
|
/// The crosshair's colour: its two lines and the readout panel's border (spec §15). Markers are
|
||||||
/// deliberately *not* this colour -- they carry each trace's own, because a marker says *which*
|
/// deliberately *not* this colour -- they carry each trace's own, because a marker says *which*
|
||||||
/// trace and the line says *where*.
|
/// trace and the line says *where*.
|
||||||
|
|||||||
@@ -344,6 +344,10 @@ void Plot::set_y_axis_origin_from_range(u32 axis, bool on) {
|
|||||||
|
|
||||||
void Plot::set_x_note(std::string text) { x_note_ = std::move(text); }
|
void Plot::set_x_note(std::string text) { x_note_ = std::move(text); }
|
||||||
|
|
||||||
|
void Plot::set_invalid_spans(std::span<const std::pair<f64, f64>> spans) {
|
||||||
|
invalid_spans_.assign(spans.begin(), spans.end());
|
||||||
|
}
|
||||||
|
|
||||||
f64 Plot::x_axis_origin(u32 axis) const {
|
f64 Plot::x_axis_origin(u32 axis) const {
|
||||||
return axis < x_axes_.size() ? x_axes_[axis].origin : 0.0;
|
return axis < x_axes_.size() ? x_axes_[axis].origin : 0.0;
|
||||||
}
|
}
|
||||||
@@ -1091,6 +1095,7 @@ const PlotLayout& Plot::layout() {
|
|||||||
// measured from the *placed* live windows, which is what makes a region's data
|
// measured from the *placed* live windows, which is what makes a region's data
|
||||||
// interval map to the pixel columns the frame will actually use.
|
// interval map to the pixel columns the frame will actually use.
|
||||||
build_regions();
|
build_regions();
|
||||||
|
build_invalid_spans();
|
||||||
build_legend();
|
build_legend();
|
||||||
// The crosshair last: its panel is fitted into `axes` and its lines are held inside it, and
|
// The crosshair last: its panel is fitted into `axes` and its lines are held inside it, and
|
||||||
// its rows are the sample values nearest a data x -- so it wants the transforms the
|
// its rows are the sample values nearest a data x -- so it wants the transforms the
|
||||||
@@ -1358,6 +1363,41 @@ void Plot::build_regions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Plot::build_invalid_spans() {
|
||||||
|
layout_.invalid_spans.clear();
|
||||||
|
if (invalid_spans_.empty()) return;
|
||||||
|
|
||||||
|
const Rect& axes = layout_.axes;
|
||||||
|
// The primary x axis' transform, taken here for `build_regions()`' reason: after the live
|
||||||
|
// windows have been placed and the strips have settled, so these are the columns the frame
|
||||||
|
// draws with and a band is anchored to the data by construction rather than by a second
|
||||||
|
// mapping that would have to be kept in step with this one.
|
||||||
|
const Transform tx = x_transform(0u);
|
||||||
|
// A hole can be narrower than a column -- one missing sample in a window of hundreds of
|
||||||
|
// thousands -- and a fill rounded to zero width would draw nothing, losing exactly the case
|
||||||
|
// the band is most needed for. One device pixel is the floor.
|
||||||
|
const f32 min_w = std::max(1.0f, style_.device(1.0f));
|
||||||
|
|
||||||
|
for (const std::pair<f64, f64>& span : invalid_spans_) {
|
||||||
|
const f64 a = std::min(span.first, span.second);
|
||||||
|
const f64 b = std::max(span.first, span.second);
|
||||||
|
if (!std::isfinite(a) || !std::isfinite(b)) continue;
|
||||||
|
const f32 lo = static_cast<f32>(tx.to_screen_x(a));
|
||||||
|
const f32 hi = static_cast<f32>(tx.to_screen_x(b));
|
||||||
|
// Scrolled out of the window: nothing to shade. Tested before the floor is applied, so a
|
||||||
|
// span off the left edge is dropped rather than pinned to it as a one-pixel band that
|
||||||
|
// accuses the samples at the window's start.
|
||||||
|
if (hi < axes.x || lo > axes.right()) continue;
|
||||||
|
const f32 first = std::clamp(lo, axes.x, axes.right());
|
||||||
|
const f32 last = std::clamp(hi, axes.x, axes.right());
|
||||||
|
const f32 w = std::min(std::max(last - first, min_w), axes.w);
|
||||||
|
// Widening a band at the right edge would run it over the axis line, so the floor is taken
|
||||||
|
// out of the left instead: the span's own edge is kept wherever there is room for it.
|
||||||
|
const f32 x = std::clamp(first, axes.x, axes.right() - w);
|
||||||
|
layout_.invalid_spans.push_back(Rect{x, axes.y, w, axes.h});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- crosshair (spec §15) --------------------------------------------------
|
// --- crosshair (spec §15) --------------------------------------------------
|
||||||
|
|
||||||
CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept {
|
CrosshairSpan crosshair_span(const data::Series& series, f64 x) noexcept {
|
||||||
|
|||||||
@@ -516,6 +516,9 @@ void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
|
|||||||
// against the line passes would not change a pixel.)
|
// against the line passes would not change a pixel.)
|
||||||
set_clip(layout.axes);
|
set_clip(layout.axes);
|
||||||
draw_grid(plot);
|
draw_grid(plot);
|
||||||
|
// The holes over the grid and under the data: a band is chrome of the *background*, not an
|
||||||
|
// overlay, so everything a frame draws inside the axes is drawn on top of it.
|
||||||
|
draw_invalid_spans(layout);
|
||||||
clear_clip();
|
clear_clip();
|
||||||
draw_axis_lines(plot);
|
draw_axis_lines(plot);
|
||||||
set_clip(layout.axes);
|
set_clip(layout.axes);
|
||||||
@@ -1728,6 +1731,12 @@ void Renderer::draw_cursor_label(const Plot& plot, const PlotLayout& layout, con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Renderer::draw_invalid_spans(const PlotLayout& layout) {
|
||||||
|
for (const Rect& band : layout.invalid_spans) {
|
||||||
|
if (band.w > 0.0f) fill_pixels(band, style_.invalid_fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Renderer::draw_regions(const Plot& plot, const PlotLayout& layout) {
|
void Renderer::draw_regions(const Plot& plot, const PlotLayout& layout) {
|
||||||
const HotHandles hot = plot.hot_handles();
|
const HotHandles hot = plot.hot_handles();
|
||||||
const f32 edge_w = style_.device(style_.line_width_px);
|
const f32 edge_w = style_.device(style_.line_width_px);
|
||||||
|
|||||||
@@ -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(std::abs(dense.riser - sparse.riser) < 24);
|
||||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
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