fix(render): convert Trace::width through the UI scale, and fold the controller's tolerance conversion into Style::device()

This commit is contained in:
Martino Ferrari committed 2026-09-28 12:26:06 +02:00
1 parent 7500fc90d2
commit 69495a3799
7 files changed
+158 -24

No files matched your search

+124
View File
@@ -2418,6 +2418,20 @@ PLOT_TEST(legend_does_not_cover_the_plot_when_placed_bottom_left) {
namespace {
/// True for a pixel holding the axis line's chalk grey at a usable coverage: the line
/// is `axis_line` (0x8A94A6) over the frame's dark background, so a third of the
/// line's own level per channel is a pixel the stroke lifted off the background --
/// `text_ink`'s coverage test, with a tone test on top that no blue trace and no dark
/// grid line satisfies. The tick labels are the same chalk family, which is why the
/// scans below keep clear of the label strip's own columns.
bool axis_line_pixel(const plot::u8 px[4], plot::Color c) {
const auto lifted = [](plot::u8 got, plot::f32 want) {
return static_cast<plot::f32>(got) >= want * 255.0f * 0.33f;
};
const bool grey = static_cast<int>(px[2]) - static_cast<int>(px[0]) < 40;
return lifted(px[0], c.r) && lifted(px[1], c.g) && lifted(px[2], c.b) && grey;
}
/// The chrome one render reports, in **device** pixels. Both runs of the two-scale
/// tests below fill this from a frame they really drew, so a comparison of the two
/// figures is a comparison of two frames and not of two derivations of one rule.
@@ -2663,3 +2677,113 @@ PLOT_TEST(dense_threshold_stays_a_device_ratio) {
static_cast<double>(kThreshold));
ctx.destroy();
}
/// A trace's stroke is a chrome size like the axis lines', so the *weight* it is drawn
/// with -- and with it the trace/axis hierarchy a reader sees -- must not depend on the
/// display. `Trace::width` reaches the same `flush_lines()` the axis lines do, so it has
/// to go through the same `Style::device()` conversion; left raw, a 1.5 px trace is 1.5
/// *device* px against a 3 device px axis line at scale 2.
///
/// The measurement is the ink a fixed-size **device** rectangle holds over a flat trace
/// (so the count is the stroke's width times a constant length, and cleaning up the
/// anti-aliasing costs a fraction of a pixel rather than the whole figure), plus the
/// same count over the y axis line. The assertion is the ratio between them: it is 1 at
/// both scales when both strokes are converted, and it halves at scale 2 when the trace
/// alone is not.
PLOT_TEST(trace_stroke_weight_is_logical) {
constexpr plot::u32 kBigW = 2u * kW;
constexpr plot::u32 kBigH = 2u * kH;
/// The width both strokes are given, in logical pixels: equal on purpose, so the
/// trace/axis ratio the case pins is 1 rather than some arbitrary figure.
constexpr plot::f32 kWidth = 3.0f;
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));
r.style().line_width_px = kWidth; // the axis lines and cursor markers
// A flat trace, so every column of the scan crosses it exactly once and the ink
// count is its width times the scan's length. y = 0.5 on a [0,1] axis is not a tick,
// so no grid line runs along it.
std::vector<plot::f32> flat(64, 0.5f);
plot::data::ArraySeries s(std::move(flat), 0.0, 1.0);
/// The device ink of both strokes at one scale: pixels of trace colour in a
/// 200x8 px box over the trace, and axis-line pixels in a 6x200 px box over the y
/// axis line. Both boxes are the same *device* size at every scale, which is what
/// makes the count proportional to the stroke's width alone.
struct StrokeInk {
plot::u32 trace = 0u;
plot::u32 axis = 0u;
};
const auto ink_at = [&](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));
plot::Plot p;
p.set_viewport({0.0f, 0.0f, static_cast<plot::f32>(w), static_cast<plot::f32>(h)});
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", {0.0, 1.0}, false, plot::AxisPlace::Left, false});
p.add_trace({&s, plot::Color::hex(0x40C0FF), kWidth, ax, ay});
r.style().ui_scale = scale;
r.begin_frame(kBigW, kBigH, {0.0f, 0.0f, static_cast<plot::f32>(w), static_cast<plot::f32>(h)});
r.draw(p);
r.end_frame();
PLOT_CHECK(!r.last_frame_had_gl_error());
const plot::PlotLayout& lay = p.layout();
// Generic over the predicate: the two strokes are recognised by different rules
// (the trace by blue dominance, the axis line by its chalk grey).
const auto count = [&](plot::u32 x0, plot::u32 y0, plot::u32 bw, plot::u32 bh, auto pred) {
plot::u32 hits = 0;
for (plot::u32 y = y0; y < y0 + bh; ++y) {
for (plot::u32 x = x0; x < x0 + bw; ++x) {
plot::u8 px[4] = {};
PLOT_CHECK(ctx.read_pixel(x, kBigH - 1u - y, px));
if (pred(px)) ++hits;
}
}
return hits;
};
StrokeInk ink;
// The trace: a fixed device box inside the axes, centred on the stroke's row.
const plot::u32 ty = static_cast<plot::u32>(p.y_transform(ay).to_screen_y(0.5));
const plot::u32 tx0 = static_cast<plot::u32>(lay.axes.x) + 40u;
ink.trace = count(tx0, ty - 4u, 200u, 8u,
[](const plot::u8* px) { return trace_pixel(px); });
// The axis line: two columns left of it, so the tick labels' own ink (which ends
// `grid_label_pad` short of the line) is outside the scan at both scales.
const plot::Color line = r.style().axis_line;
ink.axis = count(static_cast<plot::u32>(lay.axes.x) - 2u,
static_cast<plot::u32>(lay.axes.y) + 40u, 6u, 200u,
[line](const plot::u8* px) { return axis_line_pixel(px, line); });
return ink;
};
const StrokeInk one = ink_at(1.0f);
const StrokeInk two = ink_at(2.0f);
const plot::f32 trace_ratio =
static_cast<plot::f32>(two.trace) / static_cast<plot::f32>(one.trace);
const plot::f32 axis_ratio =
static_cast<plot::f32>(two.axis) / static_cast<plot::f32>(one.axis);
std::printf("[trace] 1x trace %u axis %u; 2x trace %u axis %u; ratios %.3f / %.3f ->"
" weight ratio %.3f\n",
one.trace, one.axis, two.trace, two.axis, static_cast<double>(trace_ratio),
static_cast<double>(axis_ratio),
static_cast<double>(trace_ratio / axis_ratio));
// Not vacuous: the trace really is in the box (and the axis line in its own).
PLOT_CHECK(one.trace > 100u);
PLOT_CHECK(one.axis > 100u);
// The same logical width at twice the scale is twice the device ink -- for the trace
// exactly as for the axis line it is drawn against. A trace left in device pixels
// fails this: its ink count is the same at both scales.
PLOT_CHECK_NEAR(trace_ratio, 2.0f, 0.35f);
PLOT_CHECK_NEAR(axis_ratio, 2.0f, 0.35f);
PLOT_CHECK_NEAR(trace_ratio / axis_ratio, 1.0f, 0.15f);
ctx.destroy();
}