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
@@ -56,15 +56,21 @@ struct Axis {
|
||||
/// a y axis has no live edge -- and `Off` on both, so nothing changes for a
|
||||
/// caller who never asks for it. See XFollow.
|
||||
XFollow follow = XFollow::Off;
|
||||
/// How far behind the live edge the visible window's right edge sits, in data
|
||||
/// units, under `XFollow::Live`. Stored rather than implied by the range,
|
||||
/// because the range is recomputed from the series' newest x on every frame:
|
||||
/// this is the part of the view the user chose, so it is the part that survives
|
||||
/// the recompute. Three things write it, all of them through one clamp: a pan or
|
||||
/// a zoom through `Plot::set_x_range()`, which derives it from the range the
|
||||
/// gesture asked for, `Plot::set_x_offset()` for a caller that states it
|
||||
/// directly, and `Plot::layout()`'s placement, which re-clamps it against the
|
||||
/// retained window every frame. `Plot::x_offset()` reads it back.
|
||||
/// How far behind the live edge the visible window's right edge sits, in **axis
|
||||
/// space**, under `XFollow::Live`: data units on a linear axis, `log10` decades
|
||||
/// on a logarithmic one -- the same space the controller's pan/zoom gestures run
|
||||
/// in (`to_space`/`from_space`), so a log axis scrolls and holds *decades* the
|
||||
/// way a linear axis scrolls and holds units. (`to_space(live.max) -
|
||||
/// to_space(hi)` is the definition: the distance the axis' own transform
|
||||
/// measures between the live edge and the window's right edge.) Stored rather
|
||||
/// than implied by the range, because the range is recomputed from the series'
|
||||
/// newest x on every frame: this is the part of the view the user chose, so it
|
||||
/// is the part that survives the recompute. Three things write it, all of them
|
||||
/// through one clamp: a pan or a zoom through `Plot::set_x_range()`, which
|
||||
/// derives it from the range the gesture asked for, `Plot::set_x_offset()` for a
|
||||
/// caller that states it directly, and `Plot::layout()`'s placement, which
|
||||
/// re-clamps it against the retained window -- measured in the same space --
|
||||
/// every frame. `Plot::x_offset()` reads it back.
|
||||
f64 offset = 0.0;
|
||||
/// Set by an *owner* that is following this axis' window on the plot's behalf: a
|
||||
/// `Figure` link group, while it follows, writes its own live edge here every frame.
|
||||
|
||||
@@ -152,8 +152,16 @@ class Plot {
|
||||
|
||||
// --- viewport, style, text metrics ------------------------------------
|
||||
|
||||
/// The rectangle this plot occupies on screen, in framebuffer pixels.
|
||||
void set_viewport(Rect px) noexcept { viewport_ = px; }
|
||||
/// The rectangle this plot occupies on screen, in framebuffer pixels. A
|
||||
/// negative width or height is normalised to 0: the layout clamps against
|
||||
/// the viewport's dimensions and a negative would make those clamps
|
||||
/// ill-formed (`std::clamp` with `lo > hi`), so the invariant is repaired
|
||||
/// at the one place a hostile rect can enter.
|
||||
void set_viewport(Rect px) noexcept {
|
||||
if (px.w < 0.0f) px.w = 0.0f;
|
||||
if (px.h < 0.0f) px.h = 0.0f;
|
||||
viewport_ = px;
|
||||
}
|
||||
const Rect& viewport() const noexcept { return viewport_; }
|
||||
|
||||
/// Set from the renderer, which owns presentation: `Renderer::draw()` copies
|
||||
|
||||
@@ -68,8 +68,10 @@ inline constexpr usize kMaxTicks = 256;
|
||||
/// `include_minors` adds, for grid geometry, the minor ticks subdividing each
|
||||
/// major step by 5 (by 4 when the step's leading digit is 2). Without it `out`
|
||||
/// holds exactly the labelled major ticks -- the labelled set a caller draws
|
||||
/// numbers for, with no unlabelled entries to filter. Either way the total is
|
||||
/// capped at kMaxTicks.
|
||||
/// numbers for, with no unlabelled entries to filter. The cap counts **minor**
|
||||
/// ticks only: a range dense enough to exceed it loses its subdivisions, never
|
||||
/// its labelled majors -- every major at or below `hi` is emitted, so the far
|
||||
/// end of the axis keeps its numbers.
|
||||
///
|
||||
/// Hostile input clears `out` and returns: a non-finite endpoint, `max <= min`
|
||||
/// (a zero or negative span, which includes the collapsed range of a
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
|
||||
namespace plot {
|
||||
|
||||
/// How a trace's geometry is gathered from its LOD pyramid. The level choice
|
||||
/// is unchanged; this picks what a bucket contributes (see
|
||||
/// `data::GatherMode`).
|
||||
using TraceMode = data::GatherMode;
|
||||
|
||||
/// A marker shape for per-sample points. Drawn with the trace's own colour
|
||||
/// through the shared line pipeline; `marker_every == 0` strides
|
||||
/// automatically (one marker per pixel column at most).
|
||||
enum class Marker { None, Cross, Circle, Square };
|
||||
|
||||
/// How a caller asks for a trace. The series pointer is borrowed and must outlive
|
||||
/// the plot's use of it; `axis_x`/`axis_y` index the plot's x and y axis lists.
|
||||
struct TraceDesc {
|
||||
@@ -16,7 +26,8 @@ struct TraceDesc {
|
||||
Color color = Color::hex(0x4FA3FF);
|
||||
/// The stroke's width in **logical** pixels, like every stroke in `Style` (see
|
||||
/// that struct's unit rule): the renderer converts it once, so a trace has the
|
||||
/// same weight against the axis lines whatever the display's DPI.
|
||||
/// same weight against the axis lines whatever the display's DPI. `0` draws
|
||||
/// the markers only -- a scatter trace.
|
||||
f32 width = 1.5f;
|
||||
u32 axis_x = 0, axis_y = 0;
|
||||
/// What the trace is called, for the legend. Empty means "do not list it", which
|
||||
@@ -26,6 +37,20 @@ struct TraceDesc {
|
||||
/// unchanged, exactly as `AxisDesc`'s trailing format does (and, in a build with
|
||||
/// `-Wextra`, warning-clean).
|
||||
std::string name{};
|
||||
/// Downsample policy for the LOD buckets (Envelope by default).
|
||||
TraceMode mode = TraceMode::Envelope;
|
||||
/// Dash pattern in logical pixels: ink, then gap. `dash_off <= 0` (and the
|
||||
/// default) is a solid line -- no geometry is added, the dash is a
|
||||
/// fragment-shader discard.
|
||||
f32 dash_on = 0.0f;
|
||||
f32 dash_off = 0.0f;
|
||||
/// Per-sample markers (see `Marker`). `marker_every == 0` strides
|
||||
/// automatically -- at most about one marker per pixel column -- so a dense
|
||||
/// trace never stages more markers than it has columns; a positive value
|
||||
/// draws every n-th gathered sample literally.
|
||||
Marker marker = Marker::None;
|
||||
f32 marker_size_px = 3.0f;
|
||||
u32 marker_every = 0;
|
||||
};
|
||||
|
||||
/// A series bound to a colour, width and axis pair. The series pointer may be
|
||||
@@ -35,13 +60,21 @@ struct Trace {
|
||||
const data::Series* series = nullptr;
|
||||
Color color = Color::hex(0x4FA3FF);
|
||||
/// Logical pixels, as in `TraceDesc::width`: converted to device pixels by the
|
||||
/// renderer's one `Style::device()` call per stroke.
|
||||
/// renderer's one `Style::device()` call per stroke. `0` = scatter (markers
|
||||
/// only, if any).
|
||||
f32 width = 1.5f;
|
||||
u32 axis_x = 0, axis_y = 0;
|
||||
/// The legend's entry for this trace, empty when it has none. A name does not
|
||||
/// make a trace drawable: the legend lists a trace only when it is visible, so a
|
||||
/// null series is skipped there exactly as it is by every other consumer.
|
||||
std::string name{};
|
||||
/// Same fields as `TraceDesc`, applied by the renderer.
|
||||
TraceMode mode = TraceMode::Envelope;
|
||||
f32 dash_on = 0.0f;
|
||||
f32 dash_off = 0.0f;
|
||||
Marker marker = Marker::None;
|
||||
f32 marker_size_px = 3.0f;
|
||||
u32 marker_every = 0;
|
||||
|
||||
/// True when the trace has a series with at least one sample to draw.
|
||||
bool visible() const noexcept { return series != nullptr && series->size() > 0; }
|
||||
|
||||
Reference in new issue
Block a user