The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
127 lines
5.8 KiB
C++
127 lines
5.8 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#pragma once
|
|
#include <string>
|
|
|
|
#include <plot/core/color.hpp>
|
|
#include <plot/core/rect.hpp>
|
|
#include <plot/core/types.hpp>
|
|
|
|
namespace plot {
|
|
|
|
/// Where a region's bounds sit relative to the data's sample grid.
|
|
enum class RegionSnap {
|
|
Free, ///< the bounds stay exactly where they were put.
|
|
NearestSample, ///< both bounds are pulled onto the nearest sample of the x axis'
|
|
///< first visible trace, so the statistics cover whole samples.
|
|
};
|
|
|
|
/// A shaded area on an x interval, with exact statistics over the samples it
|
|
/// covers (`Plot::region_stats()`).
|
|
///
|
|
/// The bounds are **ordered**: `Plot::set_region()` stores their min and max, so
|
|
/// `x0 > x1` cannot be held however the bounds arrived -- which is what keeps a
|
|
/// drag that crosses one bound over the other a *swap* (the region stays draggable
|
|
/// and never turns inside out) rather than a degenerate interval. A pair that is not
|
|
/// both finite is not a pair of bounds and is refused rather than stored (see
|
|
/// `Plot::set_region()`); the bounds below are finite once the region is in a plot.
|
|
/// The interval is inclusive of both ends: the samples a region covers are those whose x
|
|
/// is at or after `x0` and at or before `x1`.
|
|
///
|
|
/// A region names no x axis, unlike a cursor: it shades an interval of the plot's
|
|
/// data area as a whole -- the rect `PlotLayout::axes` and the grid are built from
|
|
/// the primary axis pair -- so its coordinates are on x axis 0.
|
|
struct Region {
|
|
f64 x0 = 0.0;
|
|
f64 x1 = 0.0;
|
|
RegionSnap snap = RegionSnap::NearestSample;
|
|
/// The shaded interior, and the line drawn at each bound. The defaults are the
|
|
/// cursor accent at a low alpha, which is what makes a region read as an area of
|
|
/// interest rather than a second set of bars over the data.
|
|
Color fill = Color::hex(0xFFB020, 0.15f);
|
|
Color edge = Color::hex(0xFFB020);
|
|
/// Optional caption, drawn on the first text line inside the fill.
|
|
std::string label;
|
|
bool visible = true;
|
|
};
|
|
|
|
/// Exact statistics over the samples a region's interval covers.
|
|
///
|
|
/// `count` is how many samples the interval holds, `mean`/`rms` are over those
|
|
/// samples, `min`/`max` their exact extremes, and `integral` their left-Riemann
|
|
/// area (`sum * dx`), the same convention `Measurement` documents -- the region and
|
|
/// the measurement read the same series the same way, so an overlay of the two
|
|
/// never shows two different "areas".
|
|
///
|
|
/// `valid` is false (and every field zero) for an id that names nothing, a trace
|
|
/// with no series, an inverted or non-finite interval, and an interval that covers
|
|
/// no sample at all.
|
|
struct RegionStats {
|
|
f64 x0 = 0.0;
|
|
f64 x1 = 0.0;
|
|
f64 width = 0.0;
|
|
f64 mean = 0.0;
|
|
f64 rms = 0.0;
|
|
f64 min = 0.0;
|
|
f64 max = 0.0;
|
|
f64 integral = 0.0;
|
|
u64 count = 0;
|
|
bool valid = false;
|
|
};
|
|
|
|
/// Which part of a region a pointer is on -- the zone `input::Controller` resolves
|
|
/// nearest-first and a host sets its resize cursor from.
|
|
///
|
|
/// It is declared here, with the primitive it describes, because two layers name
|
|
/// it: `input` reports it (and imports the name into `plot::input`, where the rest
|
|
/// of its vocabulary lives) and the renderer colours by it, since the plot
|
|
/// publishes which zone is hot (`Plot::hot_handles()`). Declaring it in either one
|
|
/// of those layers would make the other reach across a boundary to name a concept
|
|
/// neither of them owns.
|
|
enum class RegionGrab {
|
|
None, ///< the pointer is not on this region.
|
|
Left, ///< on the bound at `x0`, within the grab radius.
|
|
Right, ///< on the bound at `x1`.
|
|
Body, ///< inside the interval but not on a grabbable bound.
|
|
};
|
|
|
|
/// One region's geometry for a frame, in the same device-pixel space as
|
|
/// `PlotLayout` and filled alongside it by `Plot::layout()`. Two consumers read it
|
|
/// -- the renderer paints it and `input::Controller` hit-tests it -- and they read
|
|
/// the *same* numbers, so the rect a test asserts, the rect a frame paints and the
|
|
/// column a pointer is measured against cannot disagree.
|
|
///
|
|
/// It is declared here, beside the primitive whose geometry it is, for the same
|
|
/// reason `RegionGrab` is: two layers name it, and it belongs to the region rather
|
|
/// than to either of them.
|
|
struct RegionLayout {
|
|
/// Index into `Plot::region()`: the colours, the label text and the snap state
|
|
/// are the region's own. Only visible regions get an entry, so the layout is
|
|
/// exactly what a frame draws and what a pointer can hit.
|
|
u32 region = 0;
|
|
/// The shaded interval over the axes' full height, clipped to `axes`. A zero
|
|
/// width means the region does not intersect the visible x window (or the axes
|
|
/// have no width) and nothing of the fill is drawn.
|
|
Rect fill;
|
|
/// The grab tab at each bound: `Style::handle_width_px` wide in device pixels,
|
|
/// the axes' height tall, centred on the bound -- `handle_column()` of it is the
|
|
/// bound's screen x, which is what the controller's grab test measures against
|
|
/// and what the renderer draws the edge and its highlight in. Its height is zero
|
|
/// only when the axes have none.
|
|
Rect left_handle;
|
|
Rect right_handle;
|
|
/// The label's line box: its top-left is the pen the run is staged at and its
|
|
/// height one text line. Empty when the region has no label, nothing can measure
|
|
/// one, or the run plus its padding does not fit across `axes`.
|
|
Rect label;
|
|
};
|
|
|
|
/// The column a region's grab tab is centred on: the tab marks one bound, and its
|
|
/// centre line *is* that bound's screen x. The layout reports tabs rather than a
|
|
/// second x per bound, so this is the one derivation -- the controller measures a
|
|
/// pointer against it and the renderer draws the bound and its highlight in it.
|
|
constexpr f32 handle_column(const Rect& tab) noexcept { return tab.x + 0.5f * tab.w; }
|
|
|
|
} // namespace plot
|