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
@@ -352,7 +352,17 @@ PLOT_TEST(imgui_widget_invalid_position_does_not_move_the_range) {
|
||||
// and not from the sentinel. This is asserted with the drag alive on purpose:
|
||||
// nothing can move a range once the button is up, so the same check after the
|
||||
// release could not fail and would prove nothing.
|
||||
//
|
||||
// The quiet frame before the second press is aged past the double-click window on
|
||||
// purpose: this press lands in the same spot within the window of the first one,
|
||||
// and the widget stamps presses with ImGui's clock, so without the age the two
|
||||
// presses *are* a double-click -- the controller re-fits and begins no drag, which
|
||||
// is the gesture the next test pins, not the sentinel behaviour this one is here
|
||||
// for.
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.DeltaTime = 0.4f;
|
||||
f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
io.DeltaTime = 1.0f / 60.0f;
|
||||
f.frame(ImVec2(200.0f, 200.0f), true);
|
||||
const plot::Range second_press = f.x_range();
|
||||
f.frame(ImVec2(-kMaxFloat, -kMaxFloat), true);
|
||||
@@ -492,3 +502,111 @@ PLOT_TEST(imgui_widget_states_the_display_scale_to_the_renderer) {
|
||||
PLOT_CHECK_NEAR(f.renderer.axis_font_px(), cell_1x, 1e-3);
|
||||
PLOT_CHECK_NEAR(f.plot.layout().y_axis_width, strip_1x, 1e-3);
|
||||
}
|
||||
|
||||
// --- keys -------------------------------------------------------------------
|
||||
|
||||
// A held key repeats. This is the adapter's own policy decision, not ImGui's
|
||||
// default: ImGui edge-detects a press once, and `IsKeyPressed` only re-fires
|
||||
// while the key is held when it is asked with repeat = true -- which the widget
|
||||
// now does for every key it maps, because GLFW/Qt/GTK each deliver the OS's
|
||||
// auto-repeat as extra presses and a held `+` or arrow must behave the same here
|
||||
// (docs/embedding.md's shared policy). Held with `repeat = false`, the zoom
|
||||
// happens exactly once; with `repeat = true`, once per repeat ImGui's timer
|
||||
// fires, so the span keeps growing for as long as the key is down.
|
||||
PLOT_TEST(imgui_widget_repeats_a_held_key) {
|
||||
Fixture f;
|
||||
PLOT_CHECK(f.init());
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
// A repeat clock a test can afford: first repeat one frame after the press,
|
||||
// then one per frame, each frame a twentieth of a second of ImGui time.
|
||||
io.DeltaTime = 0.05f;
|
||||
io.KeyRepeatDelay = 0.05f;
|
||||
io.KeyRepeatRate = 0.05f;
|
||||
|
||||
// Hover the plot (the keys are gated on hover) and press `-` once.
|
||||
f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
io.AddKeyEvent(ImGuiKey_Minus, true);
|
||||
f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
const double span_after_press = f.x_range().span();
|
||||
PLOT_CHECK(span_after_press > 20.0); // one zoom-out detent on the {0, 20} fixture
|
||||
|
||||
// Hold it for half a second of ImGui time. Each repeat zooms again, so the
|
||||
// span ends up several detents past the single press -- a single-fire widget
|
||||
// would leave it exactly where the press left it.
|
||||
for (int i = 0; i < 10; ++i) f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
PLOT_CHECK(f.x_range().span() > span_after_press * 1.5);
|
||||
|
||||
io.AddKeyEvent(ImGuiKey_Minus, false);
|
||||
f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
}
|
||||
|
||||
// The arrows pan and Escape cancels a drag, mapped to the controller's
|
||||
// host-translated constants (`kKeyLeft`, `kKeyEscape`): one press of Left moves
|
||||
// every x axis 10% of its span, and Escape hands the axes back the ranges they
|
||||
// had when the drag began.
|
||||
PLOT_TEST(imgui_widget_maps_arrows_and_escape) {
|
||||
Fixture f;
|
||||
PLOT_CHECK(f.init());
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
const double span = f.x_range().span();
|
||||
const double min_before = f.x_range().min;
|
||||
|
||||
// One Left press: every x axis slides by 10% of its span, and the span itself
|
||||
// is untouched (a pan, not a zoom).
|
||||
io.AddKeyEvent(ImGuiKey_LeftArrow, true);
|
||||
f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
io.AddKeyEvent(ImGuiKey_LeftArrow, false);
|
||||
f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
PLOT_CHECK_NEAR(f.x_range().span(), span, 1e-9);
|
||||
PLOT_CHECK(std::abs(f.x_range().min - min_before) > 0.05 * span);
|
||||
PLOT_CHECK(std::abs(f.x_range().min - min_before) < 0.15 * span);
|
||||
|
||||
// A drag that has panned, then Escape while the button is still down: the
|
||||
// ranges go back to where they were when the drag began, and the drag is over.
|
||||
const double before_drag = f.x_range().min;
|
||||
f.frame(ImVec2(200.0f, 200.0f), true);
|
||||
PLOT_CHECK(f.panning());
|
||||
f.frame(ImVec2(260.0f, 200.0f), true);
|
||||
PLOT_CHECK(std::abs(f.x_range().min - before_drag) > 1e-6); // the pan moved it
|
||||
io.AddKeyEvent(ImGuiKey_Escape, true);
|
||||
f.frame(ImVec2(260.0f, 200.0f), true);
|
||||
io.AddKeyEvent(ImGuiKey_Escape, false);
|
||||
PLOT_CHECK(std::abs(f.x_range().min - before_drag) < 1e-9);
|
||||
f.frame(ImVec2(260.0f, 200.0f), false); // the release ends nothing: already cancelled
|
||||
PLOT_CHECK(std::abs(f.x_range().min - before_drag) < 1e-9);
|
||||
}
|
||||
|
||||
// The widget stamps every pointer event with ImGui's clock, which is what makes
|
||||
// the double-click -- two presses of the same button within 0.35 s and 4 px --
|
||||
// exist on this host: re-fitting every axis is the controller's gesture, and the
|
||||
// widget is the only supplier of the times it differences. Two quick presses on
|
||||
// the fixture (whose x range was pinned to {0, 20}) must hand the axes back to
|
||||
// the data -- the fit's own bounds -- and the second press must not begin a drag.
|
||||
PLOT_TEST(imgui_widget_stamps_presses_so_a_double_click_can_reset) {
|
||||
Fixture f;
|
||||
PLOT_CHECK(f.init());
|
||||
|
||||
// One quiet hovered frame first. The very first frame's events are not the
|
||||
// widget's yet: ImGui has not decided the hover (and its input queue trickles),
|
||||
// so a press queued on frame 1 is swallowed by the hover gate and the controller
|
||||
// never sees it. Every press this test means to deliver needs the hover live,
|
||||
// which is the same reason the other tests here warm up with a hovered frame.
|
||||
f.frame(ImVec2(200.0f, 200.0f), false);
|
||||
|
||||
f.frame(ImVec2(200.0f, 200.0f), true); // press 1
|
||||
f.frame(ImVec2(200.0f, 200.0f), false); // release
|
||||
// Press 2 in the same spot one frame later: ~1/60 s apart with the default
|
||||
// DeltaTime, well inside the 0.35 s and 4 px window.
|
||||
f.frame(ImVec2(200.0f, 200.0f), true);
|
||||
const double fitted = f.x_range().span();
|
||||
const double fitted_min = f.x_range().min;
|
||||
PLOT_CHECK(fitted > 40.0); // the data's own span, not the fixture's {0, 20}
|
||||
f.frame(ImVec2(220.0f, 200.0f), true); // a move with the second press's button
|
||||
// ... begins no drag: the fit's values stand exactly.
|
||||
PLOT_CHECK_NEAR(f.x_range().span(), fitted, 1e-9);
|
||||
PLOT_CHECK_NEAR(f.x_range().min, fitted_min, 1e-9);
|
||||
|
||||
f.frame(ImVec2(220.0f, 200.0f), false);
|
||||
}
|
||||
Reference in new issue
Block a user