fix(plot,render,glfw): a hue-neutral crosshair, an em-dash placeholder, one layout per figure event, and a usable readout switch
This commit is contained in:
1 parent
10d5129fbf
commit
a14f333dbe
10 files changed
+175
-70
No files matched your search
+20
-17
@@ -212,15 +212,16 @@ bool Host::run(const RunConfig& cfg, const FrameFn& frame, Status* st) {
|
|||||||
// controller_at() or injected by a scripted run -- is hit-tested against the
|
// controller_at() or injected by a scripted run -- is hit-tested against the
|
||||||
// geometry the last frame drew, and the two readers of a row's geometry cannot
|
// geometry the last frame drew, and the two readers of a row's geometry cannot
|
||||||
// disagree about it.
|
// disagree about it.
|
||||||
|
const std::vector<Rect>& rows = figure_->layout().rows; // the one layout of this frame
|
||||||
for (u32 i = 0; i < row_controllers_.size(); ++i) {
|
for (u32 i = 0; i < row_controllers_.size(); ++i) {
|
||||||
row_controllers_[i]->set_viewport(figure_->layout().rows[i]);
|
if (i < rows.size()) row_controllers_[i]->set_viewport(rows[i]);
|
||||||
}
|
}
|
||||||
// One hover, every row (spec §15): the crosshair the row controller under the pointer
|
// One hover, every row (spec §15): the crosshair the row controller under the pointer
|
||||||
// placed is applied to the whole figure here, after the pointers of this frame have been
|
// placed is applied to the whole figure here, after the pointers of this frame have been
|
||||||
// handled and before anything lays the rows out -- so every row's readout lists its own
|
// handled and before anything lays the rows out -- so every row's readout lists its own
|
||||||
// signals at the one x the pointer is over. In the run loop rather than in a callback, so a
|
// signals at the one x the pointer is over. In the run loop rather than in a callback, so a
|
||||||
// scripted run that injects its events straight into a row controller is synced too.
|
// scripted run that injects its events straight into a row controller is synced too.
|
||||||
sync_figure_crosshair();
|
sync_figure_crosshair(rows);
|
||||||
} else {
|
} else {
|
||||||
controller_.set_viewport(viewport); // also Plot::set_viewport, then layout()
|
controller_.set_viewport(viewport); // also Plot::set_viewport, then layout()
|
||||||
}
|
}
|
||||||
@@ -430,20 +431,23 @@ input::Controller* Host::row_controller(u32 row) noexcept {
|
|||||||
|
|
||||||
input::Controller& Host::controller_at(f32 x, f32 y) noexcept {
|
input::Controller& Host::controller_at(f32 x, f32 y) noexcept {
|
||||||
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
if (figure_ == nullptr || row_controllers_.empty()) return controller_;
|
||||||
const u32 row = row_at(x, y);
|
// The figure's one layout for this call: both readers below take the row rects it produced, so an
|
||||||
if (row >= row_controllers_.size()) return controller_;
|
// event lays the figure out once and not once per reader -- this is the hottest path a multi-row
|
||||||
|
// figure has, and a layout now rebuilds every row's ticks, grid and crosshair geometry.
|
||||||
|
const std::vector<Rect>& rows = figure_->layout().rows;
|
||||||
|
if (rows.empty() || rows.size() != row_controllers_.size()) return controller_;
|
||||||
|
(void)x; // the rows tile the figure vertically, so y alone picks one -- see row_in()
|
||||||
|
const u32 row = row_in(rows, y);
|
||||||
last_row_ = row;
|
last_row_ = row;
|
||||||
input::Controller& controller = *row_controllers_[row];
|
input::Controller& controller = *row_controllers_[row];
|
||||||
// The row's own viewport, so the geometry this controller hit-tests is the geometry
|
// The row's own viewport, so the geometry this controller hit-tests is the geometry
|
||||||
// the last frame drew: the same rect `Figure::layout()` gave the row's plot.
|
// the last frame drew: the same rect `Figure::layout()` gave the row's plot.
|
||||||
controller.set_viewport(figure_->layout().rows[row]);
|
controller.set_viewport(rows[row]);
|
||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 Host::row_at(f32 x, f32 y) const noexcept {
|
u32 Host::row_in(const std::vector<Rect>& rows, f32 y) noexcept {
|
||||||
if (figure_ == nullptr || row_controllers_.empty()) return 0u;
|
if (rows.empty()) return 0u;
|
||||||
const std::vector<Rect>& rows = figure_->layout().rows;
|
|
||||||
if (rows.empty() || rows.size() != row_controllers_.size()) return 0u;
|
|
||||||
// The row the pixel is in, or -- when it is in none of them, which is the row gap a
|
// The row the pixel is in, or -- when it is in none of them, which is the row gap a
|
||||||
// host has asked for with `Figure::set_row_gap()` or the figure's own margins -- the
|
// host has asked for with `Figure::set_row_gap()` or the figure's own margins -- the
|
||||||
// row whose rect is nearest. Selection by distance rather than by "the first that
|
// row whose rect is nearest. Selection by distance rather than by "the first that
|
||||||
@@ -462,13 +466,12 @@ u32 Host::row_at(f32 x, f32 y) const noexcept {
|
|||||||
if (distance == 0.0f) break; // inside this row: nothing can be nearer
|
if (distance == 0.0f) break; // inside this row: nothing can be nearer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(void)x; // the rows tile the figure vertically, so y alone picks one
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Host::sync_figure_crosshair() noexcept {
|
void Host::sync_figure_crosshair(const std::vector<Rect>& rows) noexcept {
|
||||||
const u32 rows = figure_ != nullptr ? figure_->row_count() : 0u;
|
if (rows.empty() || row_controllers_.empty()) return;
|
||||||
if (rows == 0u || row_controllers_.empty()) return;
|
const u32 row_count = static_cast<u32>(rows.size());
|
||||||
|
|
||||||
// Which row's crosshair is the hover. With a pointer of the host's own it is the row that pointer
|
// Which row's crosshair is the hover. With a pointer of the host's own it is the row that pointer
|
||||||
// is over -- the same rule events are routed by. A scripted run injects its events straight into
|
// is over -- the same rule events are routed by. A scripted run injects its events straight into
|
||||||
@@ -477,18 +480,18 @@ void Host::sync_figure_crosshair() noexcept {
|
|||||||
// any row holding something else was changed from outside, and that is the one the run touched.
|
// any row holding something else was changed from outside, and that is the one the run touched.
|
||||||
// Nothing differing means every row still holds the last spread -- no new hover -- and the figure
|
// Nothing differing means every row still holds the last spread -- no new hover -- and the figure
|
||||||
// keeps it.
|
// keeps it.
|
||||||
u32 hovered = rows;
|
u32 hovered = row_count;
|
||||||
if (have_pointer_) {
|
if (have_pointer_) {
|
||||||
hovered = row_at(last_pointer_x_, last_pointer_y_);
|
hovered = row_in(rows, last_pointer_y_);
|
||||||
} else {
|
} else {
|
||||||
for (u32 i = 0; i < rows; ++i) {
|
for (u32 i = 0; i < row_count; ++i) {
|
||||||
const Crosshair& candidate = figure_->plot(i).crosshair();
|
const Crosshair& candidate = figure_->plot(i).crosshair();
|
||||||
if (candidate.active != spread_active_ || (candidate.active && candidate.x != spread_x_)) {
|
if (candidate.active != spread_active_ || (candidate.active && candidate.x != spread_x_)) {
|
||||||
hovered = i;
|
hovered = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hovered >= rows) return;
|
if (hovered >= row_count) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applied to the whole figure: one x, every row. A row whose controller has *no* crosshair (the
|
// Applied to the whole figure: one x, every row. A row whose controller has *no* crosshair (the
|
||||||
|
|||||||
@@ -174,10 +174,11 @@ class Host {
|
|||||||
void on_scroll(f64 dx, f64 dy) noexcept;
|
void on_scroll(f64 dx, f64 dy) noexcept;
|
||||||
void on_key(int key, int action, int mods) noexcept;
|
void on_key(int key, int action, int mods) noexcept;
|
||||||
|
|
||||||
/// The index of the figure row a framebuffer pixel belongs to, by the rule
|
/// The index of the row of `rows` a framebuffer y belongs to: the row it is in, or the nearest
|
||||||
/// `controller_at()` routes by: the row the pixel is in, or the nearest row when it is in none
|
/// row when it is in none (a row gap, a margin outside the figure). One rule, shared by the event
|
||||||
/// (a row gap, or a margin outside the figure). 0 while no figure is set.
|
/// routing and the crosshair sync -- and taking the rects rather than reading them, so a caller
|
||||||
u32 row_at(f32 x, f32 y) const noexcept;
|
/// that has just laid the figure out does not make it lay out again for this.
|
||||||
|
static u32 row_in(const std::vector<Rect>& rows, f32 y) noexcept;
|
||||||
|
|
||||||
/// Spreads the hover over a whole `Figure` (spec §15).
|
/// Spreads the hover over a whole `Figure` (spec §15).
|
||||||
///
|
///
|
||||||
@@ -193,8 +194,9 @@ class Host {
|
|||||||
/// whose controller has no crosshair clears the figure's.
|
/// whose controller has no crosshair clears the figure's.
|
||||||
///
|
///
|
||||||
/// Called once per frame from the run loop, so a scripted run that bypasses the GLFW callbacks
|
/// Called once per frame from the run loop, so a scripted run that bypasses the GLFW callbacks
|
||||||
/// gets the same synced figure a real pointer does.
|
/// gets the same synced figure a real pointer does. `rows` is the figure's row rects, which the
|
||||||
void sync_figure_crosshair() noexcept;
|
/// run loop has just read -- the one layout per frame this shares with the row viewports.
|
||||||
|
void sync_figure_crosshair(const std::vector<Rect>& rows) noexcept;
|
||||||
|
|
||||||
Plot plot_;
|
Plot plot_;
|
||||||
input::Controller controller_; ///< forwards its viewport to `plot_`
|
input::Controller controller_; ///< forwards its viewport to `plot_`
|
||||||
|
|||||||
@@ -130,8 +130,11 @@ struct CrosshairRow {
|
|||||||
f64 y = 0.0;
|
f64 y = 0.0;
|
||||||
/// The trace's name, or the positional placeholder of an unnamed one.
|
/// The trace's name, or the positional placeholder of an unnamed one.
|
||||||
std::string label;
|
std::string label;
|
||||||
/// The formatted value, written in the trace's y axis' own tick format (`Axis::format_value()`,
|
/// What the row's value column shows. For a `valid` row it is the formatted number, written in
|
||||||
/// so a readout reads exactly like the labels beside it), and empty when `!valid`.
|
/// the trace's y axis' own tick format (`Axis::format_value()`, so a readout reads exactly like
|
||||||
|
/// the labels beside it); for a row whose trace has **no sample at the crosshair's x** it is
|
||||||
|
/// `kCrosshairNoValue`, the em dash. The absence is therefore *legible* -- a blank cell reads as
|
||||||
|
/// "still loading" rather than as "nothing here" -- and nothing numeric is fabricated for it.
|
||||||
std::string value;
|
std::string value;
|
||||||
/// The trace's intersection with the crosshair, in framebuffer pixels: the pen the
|
/// The trace's intersection with the crosshair, in framebuffer pixels: the pen the
|
||||||
/// renderer draws the marker at. Placed through the *trace's own* axis pair, so the
|
/// renderer draws the marker at. Placed through the *trace's own* axis pair, so the
|
||||||
@@ -143,8 +146,10 @@ struct CrosshairRow {
|
|||||||
/// The label's line box: its top-left is the pen the run is staged at and its height one
|
/// The label's line box: its top-left is the pen the run is staged at and its height one
|
||||||
/// text line. Empty when `!listed`.
|
/// text line. Empty when `!listed`.
|
||||||
Rect label_box{};
|
Rect label_box{};
|
||||||
/// The value's line box, one gutter (the legend's padding) after the label's. Empty when
|
/// The value's line box, right-aligned on the panel's inner edge and one gutter's worth of
|
||||||
/// `!listed` or `!valid`: a row with no value stages no run.
|
/// measurement away from the label's own box. Empty when `!listed`: a row that is not in the panel
|
||||||
|
/// stages neither run, while a listed row always stages something -- the number, or
|
||||||
|
/// `kCrosshairNoValue`.
|
||||||
Rect value_box{};
|
Rect value_box{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,6 +190,16 @@ struct CrosshairLayout {
|
|||||||
bool empty() const noexcept { return !active; }
|
bool empty() const noexcept { return !active; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// What a listed readout row shows in its value column when its trace has **no sample at the
|
||||||
|
/// crosshair's x**: an em dash, not a blank.
|
||||||
|
///
|
||||||
|
/// The row is listed either way -- the trace *is* under the pointer, and a panel that grew and
|
||||||
|
/// shrank with the pointer would be worse to read than one that says "nothing here" -- so the
|
||||||
|
/// absence has to be *said*: a blank value cell reads as a value that has not arrived yet. The em
|
||||||
|
/// dash is in the embedded font's coverage (spec §2 covers U+2013/U+2014), it is not a number, and
|
||||||
|
/// it cannot be mistaken for one.
|
||||||
|
inline constexpr const char* kCrosshairNoValue = "\u2014";
|
||||||
|
|
||||||
/// The label an **unnamed** trace's readout row carries: `#` and the trace's position, e.g.
|
/// The label an **unnamed** trace's readout row carries: `#` and the trace's position, e.g.
|
||||||
/// `#2`.
|
/// `#2`.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -567,6 +567,12 @@ class Plot {
|
|||||||
/// calls this when the pointer leaves the axes.
|
/// calls this when the pointer leaves the axes.
|
||||||
void clear_crosshair() noexcept { crosshair_.active = false; }
|
void clear_crosshair() noexcept { crosshair_.active = false; }
|
||||||
|
|
||||||
|
/// Switches the readout panel off -- or back on -- without moving the crosshair: the lines and
|
||||||
|
/// the per-trace markers stay, the panel is not measured and no row is listed. A host that wants
|
||||||
|
/// the pointer's column marked but not a panel over its data sets this once; it is deliberately
|
||||||
|
/// **not** part of `set_crosshair()`, so placing a hover cannot silently change it.
|
||||||
|
void set_crosshair_readout(bool on) noexcept { crosshair_.show_readout = on; }
|
||||||
|
|
||||||
/// The crosshair's state: where it is, which lines it draws and whether the readout is
|
/// The crosshair's state: where it is, which lines it draws and whether the readout is
|
||||||
/// on. Read-only -- `set_crosshair()` and `clear_crosshair()` are how it moves -- so the
|
/// on. Read-only -- `set_crosshair()` and `clear_crosshair()` are how it moves -- so the
|
||||||
/// coordinates it holds and the geometry `layout()` places from them cannot disagree.
|
/// coordinates it holds and the geometry `layout()` places from them cannot disagree.
|
||||||
|
|||||||
@@ -41,13 +41,18 @@ struct Style {
|
|||||||
Color axis_text = Color::hex(0xC8D0DC);
|
Color axis_text = Color::hex(0xC8D0DC);
|
||||||
Color cursor = Color::hex(0xFFB020);
|
Color cursor = Color::hex(0xFFB020);
|
||||||
Color measure_fill = Color::hex(0xFFB020, 0.15f);
|
Color measure_fill = Color::hex(0xFFB020, 0.15f);
|
||||||
/// The crosshair's colour: its two lines, its markers' own colour excepted, and the
|
/// The crosshair's colour: its two lines and the readout panel's border (spec §15). Markers are
|
||||||
/// readout panel's border (spec §15). Deliberately **not** `cursor`: the cursor accent is
|
/// deliberately *not* this colour -- they carry each trace's own, because a marker says *which*
|
||||||
/// an orange that a caller's own cursors, a region's bounds and its fill all already use,
|
/// trace and the line says *where*.
|
||||||
/// while the crosshair is a passive hover instrument that has to read as *chrome* over a
|
///
|
||||||
/// dense trace rather than as another handle to grab. A pale cyan does that against the
|
/// **Hue-neutral by design**, and that is the whole choice: a saturated colour here would be
|
||||||
/// dark default theme, and it is a colour no default element of a frame shares.
|
/// confusable with a trace, and the library's own default trace colour is a saturated cyan
|
||||||
Color crosshair = Color::hex(0xB8E8FF);
|
/// (`Trace`'s default, 0x40C0FF) that a hue-based reading cannot tell from a cyan crosshair --
|
||||||
|
/// only lightness would separate them, which no reader can rely on. A near-white can never be a
|
||||||
|
/// saturated trace of *any* hue, and it is still plainly not `cursor`'s orange, which is what
|
||||||
|
/// the spec asks for. The 1 px stroke is kept: form (a full-height thin line over a polyline) is
|
||||||
|
/// already distinct, so no dashes.
|
||||||
|
Color crosshair = Color::hex(0xE6EBF2);
|
||||||
|
|
||||||
// --- the scale ----------------------------------------------------------
|
// --- the scale ----------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
+17
-13
@@ -1305,9 +1305,10 @@ void Plot::build_crosshair() {
|
|||||||
row.label_box = Rect{};
|
row.label_box = Rect{};
|
||||||
row.value_box = Rect{};
|
row.value_box = Rect{};
|
||||||
row.label.assign(trace.name.empty() ? crosshair_unnamed_label(i) : trace.name);
|
row.label.assign(trace.name.empty() ? crosshair_unnamed_label(i) : trace.name);
|
||||||
row.value.clear();
|
// The value: the interpolated number where the trace has a sample at this x, and the em dash
|
||||||
// The value, and with it the marker: a trace with no sample at this x writes no number
|
// where it does not. The row is listed either way, so the absence has to be *said* -- a blank
|
||||||
// and gets no marker, rather than a value clamped to the end of its series.
|
// value cell reads as a value that has not arrived -- and the dash is measured into the panel's
|
||||||
|
// width like any other value, so the row's boxes are placed by one rule.
|
||||||
if (crosshair_value(i, crosshair_.x, row.y)) {
|
if (crosshair_value(i, crosshair_.x, row.y)) {
|
||||||
row.valid = true;
|
row.valid = true;
|
||||||
char value[32];
|
char value[32];
|
||||||
@@ -1319,6 +1320,8 @@ void Plot::build_crosshair() {
|
|||||||
// actually drawn however its axes are scaled.
|
// actually drawn however its axes are scaled.
|
||||||
row.marker = Vec2{static_cast<f32>(x_transform(trace.axis_x).to_screen_x(crosshair_.x)),
|
row.marker = Vec2{static_cast<f32>(x_transform(trace.axis_x).to_screen_x(crosshair_.x)),
|
||||||
static_cast<f32>(y_transform(trace.axis_y).to_screen_y(row.y))};
|
static_cast<f32>(y_transform(trace.axis_y).to_screen_y(row.y))};
|
||||||
|
} else {
|
||||||
|
row.value.assign(kCrosshairNoValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
crosshair.rows.resize(count);
|
crosshair.rows.resize(count);
|
||||||
@@ -1328,13 +1331,13 @@ void Plot::build_crosshair() {
|
|||||||
if (count == 0u || !crosshair_.show_readout || text_ == nullptr) return;
|
if (count == 0u || !crosshair_.show_readout || text_ == nullptr) return;
|
||||||
|
|
||||||
const ReadoutChrome chrome = readout_chrome(legend_, style_, *text_);
|
const ReadoutChrome chrome = readout_chrome(legend_, style_, *text_);
|
||||||
// Each row's content: its label, the gutter, and its value when it has one -- an invalid
|
// Each row's content: its label, the gutter, and its value -- which is a number or the em dash,
|
||||||
// row is narrower, and the panel is sized by the widest row that will be listed, which the
|
// so every row is the same two runs and the panel is sized by the widest *listed* one, read in
|
||||||
// prefix-max reads in O(1) after a row was dropped from the end.
|
// O(1) by the prefix-max after a row was dropped from the end.
|
||||||
crosshair_content_px_.clear();
|
crosshair_content_px_.clear();
|
||||||
for (const CrosshairRow& row : crosshair.rows) {
|
for (const CrosshairRow& row : crosshair.rows) {
|
||||||
f32 content = text_->measure(row.label.c_str());
|
const f32 content =
|
||||||
if (row.valid) content += chrome.pad + text_->measure(row.value.c_str());
|
text_->measure(row.label.c_str()) + chrome.pad + text_->measure(row.value.c_str());
|
||||||
crosshair_content_px_.push_back(crosshair_content_px_.empty()
|
crosshair_content_px_.push_back(crosshair_content_px_.empty()
|
||||||
? content
|
? content
|
||||||
: std::max(crosshair_content_px_.back(), content));
|
: std::max(crosshair_content_px_.back(), content));
|
||||||
@@ -1393,11 +1396,12 @@ void Plot::build_crosshair() {
|
|||||||
row.swatch = readout_swatch(chrome, crosshair.panel, i);
|
row.swatch = readout_swatch(chrome, crosshair.panel, i);
|
||||||
const Vec2 pen = readout_text_pen(chrome, crosshair.panel, i);
|
const Vec2 pen = readout_text_pen(chrome, crosshair.panel, i);
|
||||||
row.label_box = Rect{pen.x, pen.y, text_->measure(row.label.c_str()), chrome.line_h};
|
row.label_box = Rect{pen.x, pen.y, text_->measure(row.label.c_str()), chrome.line_h};
|
||||||
if (row.valid) {
|
// Every listed row stages a value run: the number, or `kCrosshairNoValue` where the trace has
|
||||||
const f32 width = text_->measure(row.value.c_str());
|
// no sample here. A blank cell would read as a value still to come, which is a different thing
|
||||||
row.value_box =
|
// from "nothing at this x".
|
||||||
Rect{crosshair.panel.right() - chrome.pad - width, pen.y, width, chrome.line_h};
|
const f32 width = text_->measure(row.value.c_str());
|
||||||
}
|
row.value_box =
|
||||||
|
Rect{crosshair.panel.right() - chrome.pad - width, pen.y, width, chrome.line_h};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1190,7 +1190,7 @@ void Renderer::draw_swatch(const Plot& plot, u32 trace, const Rect& box) {
|
|||||||
|
|
||||||
void Renderer::draw_crosshair_lines(const Plot& plot, const PlotLayout& layout) {
|
void Renderer::draw_crosshair_lines(const Plot& plot, const PlotLayout& layout) {
|
||||||
const CrosshairLayout& crosshair = layout.crosshair;
|
const CrosshairLayout& crosshair = layout.crosshair;
|
||||||
if (!crosshair.active) return;
|
if (crosshair.empty()) return;
|
||||||
|
|
||||||
const f32 stroke = style_.device(style_.line_width_px);
|
const f32 stroke = style_.device(style_.line_width_px);
|
||||||
// The two lines. Each is staged as one data-space segment at the *centre* of the stroke box
|
// The two lines. Each is staged as one data-space segment at the *centre* of the stroke box
|
||||||
@@ -1231,7 +1231,7 @@ void Renderer::draw_crosshair_lines(const Plot& plot, const PlotLayout& layout)
|
|||||||
|
|
||||||
void Renderer::draw_crosshair_readout(const Plot& plot, const PlotLayout& layout) {
|
void Renderer::draw_crosshair_readout(const Plot& plot, const PlotLayout& layout) {
|
||||||
const CrosshairLayout& crosshair = layout.crosshair;
|
const CrosshairLayout& crosshair = layout.crosshair;
|
||||||
if (!crosshair.active) return;
|
if (crosshair.empty()) return;
|
||||||
|
|
||||||
// The readout: the same backplate a legend has (the shared `draw_readout_panel()`), with its
|
// The readout: the same backplate a legend has (the shared `draw_readout_panel()`), with its
|
||||||
// border in the crosshair's own colour -- which is what makes the panel read as part of the
|
// border in the crosshair's own colour -- which is what makes the panel read as part of the
|
||||||
@@ -1242,9 +1242,9 @@ void Renderer::draw_crosshair_readout(const Plot& plot, const PlotLayout& layout
|
|||||||
if (!row.listed) continue;
|
if (!row.listed) continue;
|
||||||
draw_swatch(plot, row.trace, row.swatch);
|
draw_swatch(plot, row.trace, row.swatch);
|
||||||
stage_text({row.label_box.x, row.label_box.y}, row.label.c_str());
|
stage_text({row.label_box.x, row.label_box.y}, row.label.c_str());
|
||||||
// A row with no sample at this x has no value to write -- and it is *not* written as a
|
// The value is a number, or `kCrosshairNoValue` for a trace with no sample at this x (the plot
|
||||||
// zero or a dash: the box is empty and the number is simply absent (see CrosshairRow).
|
// put it there): either way there is a run to stage, so a row is never a blank cell.
|
||||||
if (row.valid) stage_text({row.value_box.x, row.value_box.y}, row.value.c_str());
|
stage_text({row.value_box.x, row.value_box.y}, row.value.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -650,16 +650,21 @@ PLOT_TEST(figure_crosshair_applies_one_x_to_every_row) {
|
|||||||
PLOT_CHECK(third.panel.w == 0.0f);
|
PLOT_CHECK(third.panel.w == 0.0f);
|
||||||
|
|
||||||
// Row 3's series starts at x = 100: it is a visible signal with nothing at x = 3, so it is
|
// Row 3's series starts at x = 100: it is a visible signal with nothing at x = 3, so it is
|
||||||
// listed with **no value** rather than a value clamped to the end of its window.
|
// listed with **no number** rather than one clamped to the end of its window -- and the absence
|
||||||
|
// is said out loud with the documented placeholder, not left as a blank cell that would read as a
|
||||||
|
// value still to come.
|
||||||
const plot::CrosshairLayout& fourth = figure.plot(3u).layout().crosshair;
|
const plot::CrosshairLayout& fourth = figure.plot(3u).layout().crosshair;
|
||||||
PLOT_CHECK(fourth.rows.size() == 1u);
|
PLOT_CHECK(fourth.rows.size() == 1u);
|
||||||
PLOT_CHECK(fourth.rows[0].trace == 0u);
|
PLOT_CHECK(fourth.rows[0].trace == 0u);
|
||||||
PLOT_CHECK(fourth.rows[0].label == "elsewhere");
|
PLOT_CHECK(fourth.rows[0].label == "elsewhere");
|
||||||
PLOT_CHECK(!fourth.rows[0].valid);
|
PLOT_CHECK(!fourth.rows[0].valid);
|
||||||
PLOT_CHECK(fourth.rows[0].value.empty());
|
PLOT_CHECK(fourth.rows[0].value == plot::kCrosshairNoValue);
|
||||||
// ... and it is still listed, so the row's presence does not depend on having a value.
|
PLOT_CHECK(fourth.rows[0].marker.x == 0.0f && fourth.rows[0].marker.y == 0.0f); // no intersection
|
||||||
|
// ... and it is still listed, with a value column, so the row's presence and its shape do not
|
||||||
|
// depend on having a sample there.
|
||||||
PLOT_CHECK(fourth.rows[0].listed);
|
PLOT_CHECK(fourth.rows[0].listed);
|
||||||
PLOT_CHECK(fourth.rows[0].value_box.w == 0.0f);
|
PLOT_CHECK(fourth.rows[0].value_box.w > 0.0f);
|
||||||
|
PLOT_CHECK(fourth.rows[0].value_box.right() <= fourth.panel.right());
|
||||||
|
|
||||||
// The crosshair is a data coordinate, so the x is the *same* whether or not the rows sample
|
// The crosshair is a data coordinate, so the x is the *same* whether or not the rows sample
|
||||||
// there -- moving it moves every row, and the rows that have data report their own value at
|
// there -- moving it moves every row, and the rows that have data report their own value at
|
||||||
|
|||||||
+35
-6
@@ -2038,12 +2038,41 @@ PLOT_TEST(crosshair_readout_lists_every_visible_trace_including_unnamed_ones) {
|
|||||||
PLOT_CHECK(cleared.panel.w == 0.0f && cleared.panel.h == 0.0f);
|
PLOT_CHECK(cleared.panel.w == 0.0f && cleared.panel.h == 0.0f);
|
||||||
PLOT_CHECK(cleared.vertical.w == 0.0f && cleared.horizontal.h == 0.0f);
|
PLOT_CHECK(cleared.vertical.w == 0.0f && cleared.horizontal.h == 0.0f);
|
||||||
|
|
||||||
// A crosshair with the readout switched off still draws its lines -- and lists nothing.
|
// A crosshair whose x is outside every trace's window: the row is still listed -- the trace is
|
||||||
scene.plot.set_crosshair(2.0, true);
|
// one of the signals under the pointer -- and says so with the documented placeholder rather than
|
||||||
plot::Crosshair no_readout = scene.plot.crosshair();
|
// a blank cell or a fabricated number.
|
||||||
PLOT_CHECK(no_readout.active);
|
scene.plot.set_crosshair(10.0, true); // past the last sample of the 4-sample series
|
||||||
PLOT_CHECK(!no_readout.horizontal); // the one-coordinate form draws no row
|
const plot::CrosshairLayout& past_the_end = scene.plot.layout().crosshair;
|
||||||
PLOT_CHECK(no_readout.show_readout);
|
PLOT_CHECK(past_the_end.rows.size() == 2u); // the two visible traces, both with no sample here
|
||||||
|
PLOT_CHECK(!past_the_end.rows[0].valid);
|
||||||
|
PLOT_CHECK(past_the_end.rows[0].value == plot::kCrosshairNoValue);
|
||||||
|
PLOT_CHECK(!past_the_end.rows[0].value.empty());
|
||||||
|
PLOT_CHECK(past_the_end.rows[0].value_box.w > 0.0f); // the dash has a box of its own
|
||||||
|
PLOT_CHECK(past_the_end.rows[0].listed);
|
||||||
|
PLOT_CHECK(past_the_end.rows[0].value_box.w <= past_the_end.panel.w);
|
||||||
|
|
||||||
|
// The readout switch, and the reason it is a switch rather than a field nobody can reach: with it
|
||||||
|
// off the crosshair itself is untouched -- the lines are still placed and the trace is still a
|
||||||
|
// row -- while nothing is listed in a panel, so a host that wants the pointer's column marked and
|
||||||
|
// not a panel over its data can say so. Switching it back on lists the rows again.
|
||||||
|
scene.plot.set_crosshair(1.5, true);
|
||||||
|
scene.plot.set_crosshair_readout(false);
|
||||||
|
const plot::CrosshairLayout& off = scene.plot.layout().crosshair;
|
||||||
|
PLOT_CHECK(off.active);
|
||||||
|
PLOT_CHECK(off.vertical.w > 0.0f); // the switch is the readout's: the lines are untouched
|
||||||
|
PLOT_CHECK(off.rows.size() == 2u); // both traces are still there, with their markers
|
||||||
|
for (const plot::CrosshairRow& row : off.rows) {
|
||||||
|
PLOT_CHECK(!row.listed); // ... and neither is a line in a panel
|
||||||
|
PLOT_CHECK(row.value_box.w == 0.0f);
|
||||||
|
PLOT_CHECK(row.marker.x > 0.0f); // the marker is placed either way
|
||||||
|
}
|
||||||
|
PLOT_CHECK(off.panel.w == 0.0f && off.panel.h == 0.0f);
|
||||||
|
scene.plot.set_crosshair_readout(true);
|
||||||
|
const plot::CrosshairLayout& on = scene.plot.layout().crosshair;
|
||||||
|
PLOT_CHECK(on.rows[0].listed);
|
||||||
|
PLOT_CHECK(on.rows[1].listed);
|
||||||
|
PLOT_CHECK(on.panel.w > 0.0f && on.panel.h > 0.0f);
|
||||||
|
PLOT_CHECK(on.rows[0].value == "15");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spec §15, the panel's placement: near the pointer, flipped over it where it would otherwise
|
/// Spec §15, the panel's placement: near the pointer, flipped over it where it would otherwise
|
||||||
|
|||||||
@@ -4575,18 +4575,54 @@ PLOT_TEST(horizontal_ruler_stays_inside_the_axes_at_the_edges) {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
/// The crosshair's own colour (0xB8E8FF) at real coverage: a pale cyan, green clearly above red
|
/// The crosshair's own ink: **bright and hue-neutral**, which is the property the default colour
|
||||||
/// and blue above green, which no other element of the frames this case draws produces. The two
|
/// has on purpose (see `Style::crosshair`) so that it can never be read as a trace of any hue.
|
||||||
/// traces here are magenta and green -- deliberately *not* blue, which is the one other colour
|
/// This predicate is therefore about brightness and chroma rather than about a hue, and it needs no
|
||||||
/// that would satisfy "blue above green and green above red" (0x40C0FF does). The axis text
|
/// "not this trace's colour" exception: every other element of these frames is either dark (the
|
||||||
/// (0xC8D0DC) is grey enough to fail the green-over-red term, the chrome and the grid are darker
|
/// background, the grid), saturated (the magenta, green and orange traces) or dimmer than the
|
||||||
/// still, and the same hue at lower coverage still passes, so a partially covered stroke counts
|
/// crosshair (the axis text 0xC8D0DC, whose red channel is 200 against the crosshair's 230).
|
||||||
/// as ink rather than the test needing a fully covered pixel to exist.
|
|
||||||
bool crosshair_pixel(const plot::u8 px[4]) {
|
bool crosshair_pixel(const plot::u8 px[4]) {
|
||||||
return px[1] > px[0] + 25 && px[2] > px[1] + 12 && px[2] > 120;
|
const int lo = std::min(px[0], std::min(px[1], px[2]));
|
||||||
|
const int hi = std::max(px[0], std::max(px[1], px[2]));
|
||||||
|
return px[0] > 215 && hi - lo <= 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
/// The crosshair's colour has to be tellable from a trace, and telling colours apart by hue is the
|
||||||
|
/// one thing a reader cannot be asked to do at a glance: the library's own default trace colour is a
|
||||||
|
/// saturated cyan, so a saturated cyan crosshair would differ from it in lightness alone. The
|
||||||
|
/// default is therefore near-white -- a colour no saturated trace of any hue can be -- and still
|
||||||
|
/// plainly not the cursor accent. Both halves are asserted, so a future recolouring that makes it
|
||||||
|
/// confusable with a trace or with a handle fails here rather than in a screenshot.
|
||||||
|
PLOT_TEST(crosshair_colour_cannot_be_mistaken_for_a_trace) {
|
||||||
|
const plot::render::Style style;
|
||||||
|
const plot::Trace default_trace;
|
||||||
|
// `Color` stores normalised channels; the comparisons below are in 0..255, like the hex literals
|
||||||
|
// the colours are written with.
|
||||||
|
const auto chan = [](plot::f32 c) { return static_cast<int>(c * 255.0f + 0.5f); };
|
||||||
|
const int lo = std::min({chan(style.crosshair.r), chan(style.crosshair.g), chan(style.crosshair.b)});
|
||||||
|
const int hi = std::max({chan(style.crosshair.r), chan(style.crosshair.g), chan(style.crosshair.b)});
|
||||||
|
// Hue-neutral: its channels are within a few levels of each other, so it is a grey/white and not
|
||||||
|
// a hue at all. A saturated colour -- the default trace's cyan included -- is 150+ levels apart.
|
||||||
|
PLOT_CHECK(hi - lo <= 24);
|
||||||
|
PLOT_CHECK(lo > 200); // bright chrome, not a mid-tone that a dimmed trace could reach
|
||||||
|
// `Color` is a plain channel struct with no comparison of its own, so the two "not this colour"
|
||||||
|
// assertions spell the channels out.
|
||||||
|
const auto same = [](plot::Color a, plot::Color b) {
|
||||||
|
return a.r == b.r && a.g == b.g && a.b == b.b;
|
||||||
|
};
|
||||||
|
PLOT_CHECK(!same(style.crosshair, default_trace.color));
|
||||||
|
PLOT_CHECK(!same(style.crosshair, style.cursor));
|
||||||
|
// ... and a saturated trace of any hue is out of reach for it by construction: the *most*
|
||||||
|
// chromatic colour the crosshair could be confused with has to be within the chroma bound above,
|
||||||
|
// and every trace colour in this library's demos is far outside it.
|
||||||
|
const plot::Color demo_traces[3] = {plot::Color::hex(0x40C0FF), plot::Color::hex(0xFFB020),
|
||||||
|
plot::Color::hex(0x35D0A8)};
|
||||||
|
for (const plot::Color trace : demo_traces) {
|
||||||
|
PLOT_CHECK(!same(style.crosshair, trace));
|
||||||
|
}
|
||||||
|
}
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
/// A third colour predicate for the lateral case below: the orange 0xFFB020 of a trace of its
|
/// A third colour predicate for the lateral case below: the orange 0xFFB020 of a trace of its
|
||||||
|
|||||||
Reference in new issue
Block a user