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
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <plot/core/rect.hpp>
|
||||
#include <plot/core/types.hpp>
|
||||
#include <plot/input/events.hpp>
|
||||
@@ -47,14 +49,21 @@ using plot::RegionGrab;
|
||||
/// | gesture | mode | effect |
|
||||
/// |---|---|---|
|
||||
/// | shift + left drag | `BoxZoom` | both ranges from the selection rect |
|
||||
/// | ctrl + left drag | `BoxZoom` | x range only from the selection rect |
|
||||
/// | alt + left drag | `BoxZoom` | y range only from the selection rect |
|
||||
/// | left drag on a handle | `MoveCursor` / `MoveRegion` / `ResizeRegion*` | move the handle the press grabbed |
|
||||
/// | left drag elsewhere | `Pan` | translate both ranges |
|
||||
/// | right drag | `Pan` | same, for hosts that reserve left |
|
||||
/// | middle drag on the x label strip | `AxisZoomX` | x-only zoom |
|
||||
/// | middle drag on the y label strip | `AxisZoomY` | y-only zoom |
|
||||
/// | wheel (shift: x only, ctrl: y only) | -- | zoom about the pointer |
|
||||
/// | wheel `dx` (no `dy`) | -- | pan x, 5% of the span per notch |
|
||||
/// | `+` / `-` | -- | zoom about the plot centre |
|
||||
/// | `f` | -- | auto-fit every axis |
|
||||
/// | Left/Right | -- | pan every x axis, 10% of its span |
|
||||
/// | Up/Down | -- | pan every y axis, 10% of its span |
|
||||
/// | Escape | -- | cancel the active drag: the ranges captured at `begin_drag` are restored, a box zoom applies nothing |
|
||||
/// | double click (same button, 0.35 s, 4 px, `PointerEvent::time_s`) | -- | re-fit every axis; the second press begins no drag |
|
||||
///
|
||||
/// The right button pans *and* grabs: both are buttons a host drives drags with, so
|
||||
/// neither one is singled out for handles. Middle keeps its own role on the label
|
||||
@@ -74,9 +83,10 @@ using plot::RegionGrab;
|
||||
/// Cursors and regions are *grabbable*: a press on one enters a drag that moves it,
|
||||
/// with no modifier, and a press anywhere else pans exactly as it always has -- so
|
||||
/// the new gesture is a superset of the old one and every host that drives a drag
|
||||
/// keeps panning. `Ctrl` is no longer needed for a cursor move and does not change
|
||||
/// what a press on one does; it only means the same thing it always did (a press
|
||||
/// away from a handle pans, with or without it).
|
||||
/// keeps panning. `Ctrl` and `Alt` no longer fall through to that press: they are
|
||||
/// the modifier box zoom's masks now (see the gesture table -- Ctrl frames the x
|
||||
/// range only, Alt the y range only, from the selection, over whatever was under
|
||||
/// the press), which is why a *plain* press is the one that grabs a handle or pans.
|
||||
///
|
||||
/// One hit test runs for both, in this order, and that precedence is the whole rule:
|
||||
///
|
||||
@@ -203,9 +213,18 @@ class Controller {
|
||||
/// zeroed: a zero-notch zoom changes no range but still goes through the range
|
||||
/// setters, which clear auto scale -- so a sanitised amount would quietly disarm
|
||||
/// auto-fit. An unusable amount leaves the plot exactly as it was; an unusable
|
||||
/// *position* only loses its anchor, falling back to the last known one.
|
||||
/// *position* only loses its anchor, falling back to the last known one. The
|
||||
/// same refusal covers `dx`, which pans when `dy` is zero.
|
||||
void on_wheel(const WheelEvent& e);
|
||||
void on_key(const KeyEvent& e);
|
||||
/// A pinch frame from a touch host (see `GestureEvent`): `scale` zooms about
|
||||
/// `(x, y)` exactly like a wheel notch pair would -- the same setters, the
|
||||
/// same live-axis behaviour, the same guards -- and `(dx, dy)` pans by the
|
||||
/// focal point's movement. A frame with `scale == 1` and no movement is
|
||||
/// refused outright, for the same reason a zero-notch wheel is: the setters
|
||||
/// would clear `auto_scale` for no visible change. An amount that is not a
|
||||
/// number is refused rather than sanitised.
|
||||
void on_gesture(const GestureEvent& e);
|
||||
|
||||
DragMode drag_mode() const noexcept { return mode_; }
|
||||
/// The current box-zoom selection in pixels; meaningful while BoxZoom.
|
||||
@@ -237,6 +256,35 @@ class Controller {
|
||||
void update_drag(const PointerEvent& e);
|
||||
void end_drag(const PointerEvent& e);
|
||||
|
||||
/// The ranges (and, for x, the live offsets and auto-scale flags) every axis held
|
||||
/// when a drag began -- what Escape restores. Captured at `begin_drag`, before the
|
||||
/// gesture can move anything.
|
||||
void capture_ranges();
|
||||
/// Escape: the active drag is cancelled -- the ranges captured at `begin_drag`
|
||||
/// are restored, a box zoom applies nothing, and the grab state is cleared. The
|
||||
/// restore goes through the plot's own setters, so a live axis is restored as the
|
||||
/// span and offset it held (the setters' native reading) rather than as positions
|
||||
/// that the next frame's follow would overwrite anyway.
|
||||
void cancel_drag();
|
||||
/// Re-fits every axis (`auto_fit_x()` plus one `auto_fit_y()` per y axis), which
|
||||
/// re-arms auto scale with the fit: the double-click reset.
|
||||
void fit_all();
|
||||
/// True when `e` is the second press of a double-click: the same button, within
|
||||
/// 0.35 s (both timestamps > 0 -- a host that leaves `time_s` at 0 never produces
|
||||
/// one) and within 4 logical px of the recorded press. Records `e` as the press a
|
||||
/// third one would be measured against either way; a detected double-click clears
|
||||
/// the record, so a third press starts a fresh gesture rather than re-firing.
|
||||
bool double_click(const PointerEvent& e);
|
||||
|
||||
/// Pans every x axis by `fraction` of its own span, in axis space (log-aware:
|
||||
/// a log axis translates multiplicatively). A positive fraction scrolls toward
|
||||
/// larger x. The wheel's `dx` path and the Left/Right arrows share it.
|
||||
void shift_x_axes(f64 fraction);
|
||||
/// Same for every y axis: a positive fraction scrolls toward larger y (the Up
|
||||
/// arrow). Screen y is inverted, so this is *not* the sign `pan()` derives from a
|
||||
/// drag's `dy` -- an arrow says where to look, a drag says where the data goes.
|
||||
void shift_y_axes(f64 fraction);
|
||||
|
||||
void pan(const PointerEvent& e);
|
||||
void box_zoom_update(const PointerEvent& e);
|
||||
void apply_box_zoom();
|
||||
@@ -317,6 +365,34 @@ class Controller {
|
||||
Rect selection_{};
|
||||
f32 origin_x_ = 0.0f;
|
||||
f32 origin_y_ = 0.0f;
|
||||
/// The box-zoom mask, resolved once at `begin_drag` from the modifier table
|
||||
/// (Shift = both, Ctrl = x-only, Alt = y-only, any other combo = both): which
|
||||
/// axes the selection's release frames. `apply_box_zoom()` reads these for the
|
||||
/// per-axis application *and* for the click threshold -- a masked-out dimension
|
||||
/// of a thin selection does not make the gesture a click.
|
||||
bool box_zoom_x_ = true;
|
||||
bool box_zoom_y_ = true;
|
||||
/// The range state a drag began with, per axis in order: what `cancel_drag()`
|
||||
/// restores. Empty while no drag is open.
|
||||
struct SavedX {
|
||||
Range range{};
|
||||
f64 offset = 0.0;
|
||||
bool auto_scale = false;
|
||||
};
|
||||
struct SavedY {
|
||||
Range range{};
|
||||
bool auto_scale = false;
|
||||
};
|
||||
std::vector<SavedX> saved_x_;
|
||||
std::vector<SavedY> saved_y_;
|
||||
/// The last press seen, for the double-click reset: the button, the host
|
||||
/// timestamp, and the position it was made at. `has_click_` is false until the
|
||||
/// first press arrives.
|
||||
bool has_click_ = false;
|
||||
int click_button_ = -1;
|
||||
f64 click_time_s_ = 0.0;
|
||||
f32 click_x_ = 0.0f;
|
||||
f32 click_y_ = 0.0f;
|
||||
u64 drag_cursor_ = kInvalid;
|
||||
/// The region drag's state. `drag_region_` is the region and `drag_zone_` the zone
|
||||
/// the press resolved, kept for the whole gesture so the highlight a grab started
|
||||
|
||||
Reference in new issue
Block a user