feat(plot)!: harden pipeline, XY series, dash/markers, touch, packaging
Breaking: Series::gather emits x relative to x0() and takes a GatherMode; consumers reconstruct absolute x in f64 before mapping (series.hpp documents the contract). Bugs, each pinned by a fail-before regression test: - render: f32 staging quantised epoch-time x into 128 px columns; every data-space draw now stages to_space(x) - to_space(origin) with the origin folded into the offset uniform in f64 (epoch/zero-origin twins rasterise identically) - ticks: major test was ULP-fragile and blanked epoch axes; majors are an integer-index test and are never truncated by the minor cap - gather: LOD envelope emitted buckets straddling [begin,end); ceil/floor bucket edges + raw lead/trail spans - live follow: log x axes held a linear span and zoomed every frame; span and offset now live in axis space (plot + both figure link-group paths) - input: a zero-notch wheel disarmed auto_scale on every axis; dy==0 pans x, factor == 1 / inf refused in wheel and drag zoom - render: the single text flush painted legend names over the crosshair readout panel; two batches, under flushed before the panel - data: NaN dropped from min/max unless it seeded the fold; non-finite samples are now excluded from every aggregate and from cached bounds Robustness: viewport normalisation (no clamp UB), non-finite range refusal, pyramid level cap at 57 (no shift/div UB through public gather), rounded ring capacity documented + pre-sized, GL blend func and UNPACK_ALIGNMENT save/restore, Renderer::release_gl() teardown, u64 buffer epochs, annotation captions clamped, button-aware GLFW row capture, cached figure row rects (no per-event layout), ruler-pair measurement refused, span-nonfinite guards in axis_ends. Features: XY series (sorted x, binary-search neighbours/snap, LOD over sample index); StreamSeries::freeze(); ArraySeries::push(); per-trace dash and markers (scatter at width 0) with auto or literal stride; GatherMode Envelope/Nearest/Mean via bucket_sum/sumsq; Style::line_aa_px; per-glyph text colour (readout values in trace colour); wheel-dx x pan; arrow-key pan; Escape cancels a drag and restores captured ranges; modifier-masked box zoom (Shift both, Ctrl x, Alt y); double-click reset via PointerEvent::time_s; ImGui key repeat unified; pinch gestures on Qt and GTK; pixelDelta wheel path and DevicePixelRatioChange handling; GNUInstallDirs, SOVERSION, SameMinorVersion, generated plot/version.hpp, relocatable plot.pc, licence install, FILES_MATCHING, GitHub Actions CI (debug/coverage/asan). Suite 283 -> 350 cases, 0 failed; coverage 93.9% (3730/3971) over 24 files, floor 80/90 held; ASAN preset suite green; all four demos run headless (the GTK scripted-resize check needs a window manager and fails identically on the parent commit); readme_main.cpp committed and compiles with the README command; pkg-config and find_package consumption verified from a relocated install prefix.
This commit is contained in:
1 parent
f132a5ec92
commit
38ef459897
77 files changed
+5905
-688
No files matched your search
@@ -786,6 +786,82 @@ PLOT_TEST(a_following_group_advances_all_rows_together) {
|
||||
}
|
||||
}
|
||||
|
||||
/// B4, on a following *log* group: the group's span and offset live in the shared
|
||||
/// axis' axis space -- decades -- exactly as a single `Live` axis' do, so the
|
||||
/// window slides with the newest sample without rescaling. The visible decades
|
||||
/// stay what was asked for on every row, and a member panned mid-follow restates
|
||||
/// its scroll state in the same decades rather than in a linear width that would
|
||||
/// decay as the edge moved.
|
||||
PLOT_TEST(a_following_log_group_holds_decades) {
|
||||
plot::data::StreamSeries left(1024u, 1.0, 10.0); // samples at x = 1, 11, 21, ...
|
||||
plot::data::StreamSeries right(1024u, 1.0, 10.0);
|
||||
for (int i = 0; i < 30; ++i) left.push(0.5f);
|
||||
for (int i = 0; i < 20; ++i) right.push(0.5f);
|
||||
left.acquire();
|
||||
right.acquire();
|
||||
|
||||
plot::Figure figure(2u);
|
||||
figure.set_viewport(whole_figure());
|
||||
for (plot::u32 row = 0; row < 2u; ++row) {
|
||||
plot::Plot& p = figure.plot(row);
|
||||
p.add_x_axis({"t", {1.0, 1000.0}, false, plot::AxisPlace::Bottom, /*log=*/true});
|
||||
p.add_y_axis({"v", {0.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
}
|
||||
figure.plot(0u).add_trace({&left, plot::Color::hex(0x4FA3FF), 1.5f, 0u, 0u});
|
||||
figure.plot(1u).add_trace({&right, plot::Color::hex(0xFF8080), 1.5f, 0u, 0u});
|
||||
|
||||
const plot::u32 group = figure.add_x_link();
|
||||
PLOT_CHECK(figure.link_x(0u, group));
|
||||
PLOT_CHECK(figure.link_x(1u, group));
|
||||
PLOT_CHECK(figure.set_x_link_follow(group, plot::XFollow::Live));
|
||||
|
||||
const auto decades_of = [](const plot::Plot& p) {
|
||||
const plot::Range r = p.x_range(0u);
|
||||
return std::log10(r.max) - std::log10(r.min);
|
||||
};
|
||||
|
||||
// One decade wide, ending at the fastest row's edge.
|
||||
const plot::f64 edge0 = left.x_bounds().max;
|
||||
PLOT_CHECK(figure.set_x_link_range(group, {edge0 / 10.0, edge0}));
|
||||
figure.layout();
|
||||
PLOT_CHECK_NEAR(decades_of(figure.plot(0u)), 1.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(decades_of(figure.plot(1u)), 1.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(figure.plot(0u).x_range(0u).max, edge0, 1e-8);
|
||||
|
||||
// The streams advance at different rates; the window follows the group's edge --
|
||||
// the faster row's -- and holds its decade on every row.
|
||||
for (int step = 1; step <= 8; ++step) {
|
||||
for (int i = 0; i < 3; ++i) left.push(0.5f);
|
||||
for (int i = 0; i < 1; ++i) right.push(0.5f);
|
||||
left.acquire();
|
||||
right.acquire();
|
||||
figure.layout();
|
||||
PLOT_CHECK_NEAR(decades_of(figure.plot(0u)), 1.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(decades_of(figure.plot(1u)), 1.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(figure.plot(0u).x_range(0u).max, left.x_bounds().max, 1e-8);
|
||||
PLOT_CHECK_NEAR(figure.plot(1u).x_range(0u).max, left.x_bounds().max, 1e-8);
|
||||
}
|
||||
|
||||
// A member panned while the group follows: the adopted scroll state is the
|
||||
// adopted range's span *in decades* -- two here (edge/1000 to edge/10 is a
|
||||
// factor of 100) -- and the offset one decade behind the edge, both of which
|
||||
// survive the next frames' advances.
|
||||
const plot::f64 edge = left.x_bounds().max;
|
||||
figure.plot(1u).set_x_range(0u, {edge / 1000.0, edge / 10.0});
|
||||
figure.layout();
|
||||
PLOT_CHECK_NEAR(decades_of(figure.plot(0u)), 2.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(decades_of(figure.plot(1u)), 2.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(figure.plot(0u).x_range(0u).max, edge / 10.0, 1e-8);
|
||||
for (int step = 0; step < 3; ++step) {
|
||||
left.push(0.5f);
|
||||
left.acquire();
|
||||
figure.layout();
|
||||
PLOT_CHECK_NEAR(decades_of(figure.plot(0u)), 2.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(decades_of(figure.plot(1u)), 2.0, 1e-9);
|
||||
}
|
||||
PLOT_CHECK(figure.x_link_follow(group) == plot::XFollow::Live);
|
||||
}
|
||||
|
||||
// ------------------------------------------------- the crosshair (spec §15)
|
||||
|
||||
namespace {
|
||||
|
||||
Reference in new issue
Block a user