wip(plot): region layout, hot handles and draggable handles (build restored)
This commit is contained in:
1 parent
7d2349555d
commit
e4d48cc6c3
9 files changed
+915
-37
No files matched your search
+138
-4
@@ -10,6 +10,7 @@
|
||||
#include <plot/plot/grid.hpp>
|
||||
#include <plot/plot/legend.hpp>
|
||||
#include <plot/plot/measure.hpp>
|
||||
#include <plot/plot/region.hpp>
|
||||
#include <plot/plot/trace.hpp>
|
||||
#include <plot/render/style.hpp>
|
||||
#include <plot/render/text.hpp>
|
||||
@@ -59,6 +60,38 @@ struct PlotLayout {
|
||||
/// the panel a test asserts and the panel the renderer paints are one set of
|
||||
/// numbers rather than two derivations of one rule (see LegendLayout).
|
||||
LegendLayout legend;
|
||||
/// Every visible region's geometry, in `Plot::region()` order: the shaded
|
||||
/// interval, the grab tab at each bound and the label's line box. Measured here
|
||||
/// rather than derived by the renderer, for the legend's reason and one more: the
|
||||
/// controller's hit test reads the very same rects, so where a region is drawn and
|
||||
/// where a pointer can grab it cannot drift apart.
|
||||
std::vector<RegionLayout> regions;
|
||||
};
|
||||
|
||||
/// What a pointer is on, as of the last pointer event: the cursor marker under it, the
|
||||
/// cursor a drag has hold of, and the region zone that is hot. Published to the plot by
|
||||
/// `input::Controller` -- the layer that owns the only hit test -- and read back by the
|
||||
/// renderer, which draws the handles being manipulated while knowing nothing about a
|
||||
/// pointer, a button or a modifier.
|
||||
///
|
||||
/// It lives on `Plot` rather than on the renderer, or in `input` where it is produced,
|
||||
/// because the renderer must not name the input layer -- that is the direction of every
|
||||
/// dependency here -- and because it is per-plot state: two plots drawn into one frame
|
||||
/// each draw their own hot handle, and the controller that drives one of them knows
|
||||
/// nothing of the other.
|
||||
///
|
||||
/// `cursor` is the marker the pointer is over and `grabbed_cursor` the one a drag is
|
||||
/// moving. They are two fields rather than one because a drag can leave its marker
|
||||
/// behind -- the pointer moves on while the cursor follows it -- and a marker is drawn
|
||||
/// highlighted while it is either. `region` and `zone` are the region and the zone the
|
||||
/// pointer is on, or the zone an open drag resolved: a grab therefore keeps its own zone
|
||||
/// emphasized for the whole gesture rather than the emphasis following the pointer. Every
|
||||
/// id field is `kInvalid`, and `zone` `RegionGrab::None`, when nothing is hot.
|
||||
struct HotHandles {
|
||||
u64 cursor = kInvalid;
|
||||
u64 grabbed_cursor = kInvalid;
|
||||
u64 region = kInvalid;
|
||||
RegionGrab zone = RegionGrab::None;
|
||||
};
|
||||
|
||||
/// The public façade: axes, traces, cursors, annotations and their layout.
|
||||
@@ -124,6 +157,11 @@ class Plot {
|
||||
/// Measures the interval between two cursors over the series of `trace`.
|
||||
u32 add_measure(u32 cursor_a, u32 cursor_b, u32 trace = 0);
|
||||
u32 add_annotation(const Annotation& annotation);
|
||||
/// Adds a region. Its bounds are ordered and, when the region asks to snap, pulled
|
||||
/// onto the sample grid as it is added -- exactly as `add_cursor()` places a
|
||||
/// snapping cursor -- so a region handed to a plot that already holds samples is on
|
||||
/// the grid from its first frame.
|
||||
u32 add_region(const Region& region);
|
||||
|
||||
// --- accessors --------------------------------------------------------
|
||||
// An id that does not name an object yields a default-constructed one, so a
|
||||
@@ -140,12 +178,16 @@ class Plot {
|
||||
u32 cursor_count() const noexcept { return static_cast<u32>(cursors_.size()); }
|
||||
u32 measure_count() const noexcept { return static_cast<u32>(measures_.size()); }
|
||||
u32 annotation_count() const noexcept { return static_cast<u32>(annotations_.size()); }
|
||||
u32 region_count() const noexcept { return static_cast<u32>(regions_.size()); }
|
||||
|
||||
const Axis& x_axis(u32 axis) const;
|
||||
const Axis& y_axis(u32 axis) const;
|
||||
const Trace& trace(u32 id) const;
|
||||
const Cursor& cursor(u32 id) const;
|
||||
const Annotation& annotation(u32 id) const;
|
||||
/// The region with that id, or a default-constructed one: as everywhere else on
|
||||
/// this class, a stale id reads a default rather than out of bounds.
|
||||
const Region& region(u32 id) const;
|
||||
|
||||
// --- ranges and transforms --------------------------------------------
|
||||
|
||||
@@ -283,11 +325,60 @@ class Plot {
|
||||
/// are in measurement().
|
||||
MeasureSpan measure_span(u32 id) const noexcept;
|
||||
|
||||
/// Drops every cached measurement. Needed after a series is mutated in place
|
||||
/// (data::ArraySeries::commit() with the same sample count), which a pointer
|
||||
/// and size check cannot detect.
|
||||
/// Drops every cached measurement **and every cached region statistic**: both
|
||||
/// caches key on the same series identity, so a mutation that neither of those
|
||||
/// keys can see (data::ArraySeries::commit() with the same sample count) has to
|
||||
/// invalidate both, and one entry point is what keeps a caller from clearing one
|
||||
/// and leaving the other frozen.
|
||||
void invalidate_measurements() noexcept;
|
||||
|
||||
// --- regions ----------------------------------------------------------
|
||||
|
||||
/// Replaces both bounds, ordered: `min`/`max` are stored, so `x0 > x1` cannot be
|
||||
/// held whatever the caller passes. Deliberately *verbatim* otherwise -- the snap a
|
||||
/// region asks for is a separate act (`snap_region()`) -- so a drag can move the
|
||||
/// bounds under the pointer and put them on the sample grid once, on release,
|
||||
/// instead of both ends sticking as the pointer moves. Also drops the region's
|
||||
/// cached statistics.
|
||||
void set_region(u32 id, f64 x0, f64 x1);
|
||||
|
||||
/// Pulls a snapping region's bounds onto the sample grid: onto the nearest sample
|
||||
/// of the first visible trace on the region's x axis, the rule `set_cursor_x()`
|
||||
/// already applies to a snapping cursor. A `Free` region, one with no visible
|
||||
/// trace to snap to, and a series with a non-positive `dx()` are left exactly as
|
||||
/// they are.
|
||||
void snap_region(u32 id);
|
||||
|
||||
/// Removes a region. The ids above it shift down by one, as an element of a
|
||||
/// vector does: a caller holding ids drops the ones past the removed region, and
|
||||
/// region 0 is always the first region in `region()` order. Every cached
|
||||
/// statistic of every remaining region is dropped with it.
|
||||
void remove_region(u32 id);
|
||||
|
||||
/// Exact statistics over the samples `region`'s interval covers, on the series of
|
||||
/// `trace`: `reduce`/`sum`/`sumsq` over exactly those samples, as a measurement
|
||||
/// is, and never the pyramid's `*_buckets` forms -- those answer for whole buckets
|
||||
/// and would silently report the wrong extremes and sums for an interval that is
|
||||
/// not bucket-aligned, which is what an unaligned region is. `valid` is false for
|
||||
/// an id that names nothing, a trace with no series, and an interval that covers
|
||||
/// no sample at all.
|
||||
///
|
||||
/// Re-evaluated only when the region moved, when the trace's series changed, when
|
||||
/// it grew, or when its retained window slid under a constant sample count
|
||||
/// (`x0()` is the third key -- see RegionStatsSlot) -- never once per frame, so a
|
||||
/// saturated stream with a stationary region goes on reporting live values. const
|
||||
/// because recomputing fills a cache: nothing a caller can observe changes.
|
||||
const RegionStats& region_stats(u32 region, u32 trace) const;
|
||||
|
||||
// --- hot handles ------------------------------------------------------
|
||||
|
||||
/// Publishes which handles the pointer is on, for the renderer to draw
|
||||
/// highlighted. Set by `input::Controller`, which owns the only hit test (and
|
||||
/// clears it back to "nothing is hot" when the pointer leaves); a plot nobody
|
||||
/// drives keeps the default. See HotHandles for why this lives on the plot.
|
||||
void set_hot_handles(const HotHandles& hot) noexcept { hot_ = hot; }
|
||||
const HotHandles& hot_handles() const noexcept { return hot_; }
|
||||
|
||||
// --- layout -----------------------------------------------------------
|
||||
|
||||
/// Recomputes the layout, every axis' ticks and the grid, then returns the
|
||||
@@ -346,6 +437,21 @@ class Plot {
|
||||
mutable f64 last_x0 = 0.0;
|
||||
};
|
||||
|
||||
/// One (region, trace) pair's cached statistics, keyed exactly as a MeasureSlot is
|
||||
/// and for its same three reasons: `dirty` for a region that moved, the series
|
||||
/// pointer for a trace pointed at a different series, the sample count for one that
|
||||
/// grew, and `x0()` for a saturated ring whose window slid under a constant size.
|
||||
/// Keyed by the *pair*: one region's statistics over two traces are two answers.
|
||||
struct RegionStatsSlot {
|
||||
u32 region = 0;
|
||||
u32 trace = 0;
|
||||
mutable RegionStats result;
|
||||
mutable bool dirty = true;
|
||||
mutable const data::Series* last_series = nullptr;
|
||||
mutable u64 last_size = 0;
|
||||
mutable f64 last_x0 = 0.0;
|
||||
};
|
||||
|
||||
/// Fit one axis to its traces' bounds, leaving auto_scale alone. False when the
|
||||
/// axis cannot be fitted (no visible trace bound to it, or a log axis whose
|
||||
/// traces hold no positive value), in which case the range is untouched.
|
||||
@@ -377,8 +483,28 @@ class Plot {
|
||||
void refresh_ticks();
|
||||
void build_grid();
|
||||
void mark_measures_dirty() noexcept;
|
||||
/// `c.x` pulled onto the nearest sample when the cursor asks to snap.
|
||||
/// Marks every cached statistic of one region dirty -- every trace's -- which
|
||||
/// `set_region()` and `remove_region()` are the only two things that need: they
|
||||
/// are the only ways a region's own interval changes.
|
||||
void mark_region_stats_dirty(u32 region) noexcept;
|
||||
/// The series the sample grid comes from when an x coordinate is snapped: the
|
||||
/// first visible trace with a series on `axis_x`, or null when the axis has none.
|
||||
/// One rule for the cursor snap and the region snap both.
|
||||
const data::Series* snap_series(u32 axis_x) const;
|
||||
/// `x` pulled onto the nearest sample of `snap_series(axis_x)`, or `x` when there
|
||||
/// is nothing to snap to.
|
||||
f64 snapped_x(f64 x, u32 axis_x) const;
|
||||
/// `c.x` pulled onto the nearest sample when the cursor asks to snap: the same one
|
||||
/// rule, applied to the axis the cursor names.
|
||||
f64 snapped_x(const Cursor& c) const;
|
||||
/// Measures every visible region into `layout_.regions`: the fill's clipped
|
||||
/// interval, one grab tab per bound, and the label's line box. Also the place a
|
||||
/// region's pixel geometry is derived -- the controller hit-tests what this
|
||||
/// measured instead of mapping the bounds through the transform again.
|
||||
void build_regions();
|
||||
/// Exact statistics of `region` over `series`, or an invalid result when the
|
||||
/// interval covers no sample. The one place the region arithmetic lives.
|
||||
static RegionStats evaluate_region_stats(const Region& region, const data::Series& series);
|
||||
/// Widest y tick label plus the label padding on both sides: the left strip the
|
||||
/// text backend asks for. Zero with no backend or no y axis.
|
||||
f32 y_label_width() const;
|
||||
@@ -397,6 +523,8 @@ class Plot {
|
||||
render::Style style_{};
|
||||
const render::TextBackend* text_ = nullptr;
|
||||
Legend legend_{};
|
||||
/// Which handles a pointer is on, published by the input layer (see HotHandles).
|
||||
HotHandles hot_{};
|
||||
|
||||
std::vector<Axis> x_axes_;
|
||||
std::vector<Axis> y_axes_;
|
||||
@@ -404,6 +532,12 @@ class Plot {
|
||||
std::vector<Cursor> cursors_;
|
||||
std::vector<MeasureSlot> measures_;
|
||||
std::vector<Annotation> annotations_;
|
||||
std::vector<Region> regions_;
|
||||
/// The (region, trace) statistics cache, filled on demand by `region_stats()`
|
||||
/// and so mutable: a lookup on a const Plot may cache, exactly as `measures_`
|
||||
/// does. Entries are created for the pairs a caller actually asks about, never
|
||||
/// for the whole cross product.
|
||||
mutable std::vector<RegionStatsSlot> region_stats_;
|
||||
/// Scratch for build_legend(): the name width of every trace that has an entry, in
|
||||
/// trace order and prefix-maxed in place, so the fit loop reads the width a given
|
||||
/// number of kept entries needs in O(1) instead of re-measuring the names it is
|
||||
|
||||
Reference in new issue
Block a user