added auto measuraments

This commit is contained in:
Martino Ferrari committed 2026-10-03 10:34:15 +02:00
1 parent 44fc4cdf36
commit 0eceaf537b
7 files changed
+2182 -55

No files matched your search

+227 -24
View File
@@ -26,6 +26,10 @@ constexpr plot::u32 kUnplottedColor = 0x6E6E6Eu;
constexpr plot::u32 kCursorAColor = 0xFFD24Fu;
constexpr plot::u32 kCursorBColor = 0x7FE0FFu;
constexpr plot::u32 kRulerColor = 0xB0B0B0u;
constexpr plot::u32 kRulerBColor = 0x8FA8C8u;
/// The measurements' own colour, distinct from both cursor rulers so a readout mark cannot be
/// mistaken for something the user placed.
constexpr plot::u32 kMeasureColor = 0x9BE8A0u;
constexpr plot::u32 kTriggerColor = 0xFF5B5Bu;
constexpr double kParked = -1.0e12; ///< hides an instrument without removing it
@@ -59,10 +63,9 @@ void RowSet::carry_row(Row& dst, const Row& src) {
dst.name = src.name;
dst.channel = src.channel;
dst.color_index = src.color_index;
dst.cursors_visible = src.cursors_visible;
dst.cursor_a_x = src.cursor_a_x;
dst.cursor_b_x = src.cursor_b_x;
dst.ruler_y = src.ruler_y;
dst.ruler_b_y = src.ruler_b_y;
// The slot's series stays where it is, but what it is bound to belonged to the row
// this state came *from*. `bind()` re-resolves every slot next frame; clearing it here
// is what stops the frame in between from drawing the moved row's data under the new
@@ -75,9 +78,6 @@ void RowSet::clear_row(Row& r) {
r.name = {};
r.channel = {-1, -1, -1, -1};
r.color_index = {0, 1, 2, 3};
r.cursors_visible = false;
r.cursor_a_x = 0.0;
r.cursor_b_x = 0.0;
r.ruler_y = 0.0;
for (SlotSeries& s : r.slots) s.bind(nullptr);
}
@@ -115,6 +115,30 @@ void RowSet::install(Row& r, int /*row_index*/, plot::Plot& p) {
r.cursor_a = r.plot->add_cursor(0.0, plot::Color::hex(kCursorAColor));
r.cursor_b = r.plot->add_cursor(0.0, plot::Color::hex(kCursorBColor));
r.ruler = r.plot->add_h_ruler(0.0, plot::Color::hex(kRulerColor));
r.ruler_b = r.plot->add_h_ruler(0.0, plot::Color::hex(kRulerBColor));
// The `mix` crossings, one pair per trace slot, hidden until that mode is on. Created
// here because a `Plot` fixes its annotation set the same way it fixes its traces --
// `add_annotation()` only appends -- so the markers are made up front and moved.
for (int s = 0; s < kSlotsPerRow; ++s) {
plot::Annotation a{0.0, 0.0, "", plot::Color::hex(kCursorAColor), r.axis_x, r.axis_y, false};
plot::Annotation b{0.0, 0.0, "", plot::Color::hex(kCursorBColor), r.axis_x, r.axis_y, false};
r.crossing_a[static_cast<std::size_t>(s)] = r.plot->add_annotation(a);
r.crossing_b[static_cast<std::size_t>(s)] = r.plot->add_annotation(b);
}
// The measurements' readout marks: a fixed pool, parked. Levels are rulers (they read a y)
// and times are markers (they read an x), so the pool has one of each per slot.
for (int m = 0; m < kMeasureMarks; ++m) {
const std::size_t mi = static_cast<std::size_t>(m);
r.measure_h[mi] = r.plot->add_h_ruler(kParked, plot::Color::hex(kMeasureColor));
r.measure_v[mi] = r.plot->add_cursor(kParked, plot::Color::hex(kMeasureColor));
// Readouts, not instruments: the pointer must not be able to hover or grab one. Parking
// them outside the axis and re-placing every frame does not stop a *drag* -- the
// controller holds the marker for the gesture's life, so it follows the pointer between
// frames and the readout looks draggable. Excluding it from the hit test is the only
// place that can say no.
r.plot->set_cursor_selectable(r.measure_h[mi], false);
r.plot->set_cursor_selectable(r.measure_v[mi], false);
}
r.trigger_marker = r.plot->add_cursor(kParked, plot::Color::hex(kTriggerColor));
// A marker, not an instrument: it must sit exactly on the edge's time, so its
// snap is Free and the controller never rounds it onto a sample.
@@ -303,13 +327,190 @@ void RowSet::set_y_range(int row_index, double lo, double hi) {
void RowSet::place_cursors(Row& r) {
if (!r.plot) return;
const bool show = r.cursors_visible;
const double a = show ? r.cursor_a_x : kParked;
const double b = show ? r.cursor_b_x : kParked;
const double h = show ? r.ruler_y : kParked;
r.plot->set_cursor_x(r.cursor_a, a);
r.plot->set_cursor_x(r.cursor_b, b);
r.plot->set_cursor_y(r.ruler, h);
const bool on = cursors_on_;
// Which instruments the mode uses; the rest are parked (a `Plot` cannot drop a
// cursor, so hiding is the only way to take one off screen).
const bool vertical = on && cursor_mode_ != CursorMode::Horizontal;
const bool horizontal = on && (cursor_mode_ == CursorMode::Horizontal ||
cursor_mode_ == CursorMode::Both);
r.plot->set_cursor_x(r.cursor_a, vertical ? cursor_a_x_ : kParked);
r.plot->set_cursor_x(r.cursor_b, vertical ? cursor_b_x_ : kParked);
r.plot->set_cursor_y(r.ruler, horizontal ? r.ruler_y : kParked);
r.plot->set_cursor_y(r.ruler_b, horizontal ? r.ruler_b_y : kParked);
}
void RowSet::set_cursor_mode(CursorMode mode) {
if (mode == cursor_mode_) return;
cursor_mode_ = mode;
for (int i = 0; i < kMaxRows; ++i) place_cursors(row(i));
place_crossings();
}
void RowSet::place_crossings() {
const bool mix = cursor_mode_ == CursorMode::Mix && cursors_visible();
for (int i = 0; i < kMaxRows; ++i) {
Row& r = row(i);
if (!r.plot) continue;
for (int s = 0; s < kSlotsPerRow; ++s) {
const std::size_t si = static_cast<std::size_t>(s);
const plot::u32 ida = r.crossing_a[si];
const plot::u32 idb = r.crossing_b[si];
if (ida >= r.plot->annotation_count() || idb >= r.plot->annotation_count()) continue;
plot::Annotation a = r.plot->annotation(ida);
plot::Annotation b = r.plot->annotation(idb);
double ya = 0.0;
double yb = 0.0;
const bool has_a = mix && r.plot->crosshair_value(static_cast<plot::u32>(s), cursor_a_x_, ya);
const bool has_b = mix && r.plot->crosshair_value(static_cast<plot::u32>(s), cursor_b_x_, yb);
a.visible = has_a;
b.visible = has_b;
if (has_a) {
a.x = cursor_a_x_;
a.y = ya;
}
if (has_b) {
b.x = cursor_b_x_;
b.y = yb;
}
r.plot->set_annotation(ida, a);
r.plot->set_annotation(idb, b);
}
}
}
double RowSet::ruler_y(int r) const {
return r >= 0 && r < rows_ ? row(r).ruler_y : 0.0;
}
double RowSet::ruler_b_y(int r) const {
return r >= 0 && r < rows_ ? row(r).ruler_b_y : 0.0;
}
int RowSet::crossings_visible() const {
int n = 0;
for (int i = 0; i < rows_; ++i) {
const Row& r = row(i);
if (!r.plot) continue;
for (int s = 0; s < kSlotsPerRow; ++s) {
const std::size_t si = static_cast<std::size_t>(s);
if (r.crossing_a[si] < r.plot->annotation_count() &&
r.plot->annotation(r.crossing_a[si]).visible) {
++n;
}
if (r.crossing_b[si] < r.plot->annotation_count() &&
r.plot->annotation(r.crossing_b[si]).visible) {
++n;
}
}
}
return n;
}
bool RowSet::crossing_at(int i, int slot, bool second, double& x, double& y) const {
if (i < 0 || i >= rows_ || slot < 0 || slot >= kSlotsPerRow) return false;
const Row& r = row(i);
if (!r.plot) return false;
const std::size_t si = static_cast<std::size_t>(slot);
const plot::u32 id = second ? r.crossing_b[si] : r.crossing_a[si];
if (id >= r.plot->annotation_count()) return false;
const plot::Annotation& a = r.plot->annotation(id);
if (!a.visible) return false;
x = a.x;
y = a.y;
return true;
}
void RowSet::set_measure_marks(const std::vector<MeasureMark>& marks) {
measure_marks_placed_ = static_cast<int>(marks.size());
for (int i = 0; i < kMaxRows; ++i) {
Row& r = row(i);
if (!r.plot) continue;
int h = 0;
int v = 0;
for (const MeasureMark& m : marks) {
if (m.row != i) continue;
if (m.level) {
if (h >= kMeasureMarks) continue;
const std::size_t mi = static_cast<std::size_t>(h++);
r.plot->set_cursor_y(r.measure_h[mi], m.value, plot::CursorPlacement::Free);
r.plot->set_cursor_color(r.measure_h[mi], plot::Color::hex(m.color));
} else {
if (v >= kMeasureMarks) continue;
const std::size_t mi = static_cast<std::size_t>(v++);
r.plot->set_cursor_x(r.measure_v[mi], m.value, plot::CursorPlacement::Free);
r.plot->set_cursor_color(r.measure_v[mi], plot::Color::hex(m.color));
}
}
// What the measurements left over is parked rather than kept: a stale readout at the old
// window's position is worse than none.
for (; h < kMeasureMarks; ++h) {
r.plot->set_cursor_y(r.measure_h[static_cast<std::size_t>(h)], kParked);
}
for (; v < kMeasureMarks; ++v) {
r.plot->set_cursor_x(r.measure_v[static_cast<std::size_t>(v)], kParked);
}
}
}
void RowSet::fit_x() {
Row& first = row(0);
if (!first.plot || !figure_) return;
first.plot->auto_fit_x();
const plot::Range fitted = first.plot->x_range(first.axis_x);
const double span = fitted.span();
if (!std::isfinite(span) || !(span > 0.0)) return;
// The same two paths `fit_all()` takes: a held event owns its window and states it
// through the link group, a live view re-arms the follow and frames the newest span.
if (pinned_) {
window_s_ = span;
figure_->set_x_link_range(x_group_, fitted);
} else {
set_time_window(span);
}
}
void RowSet::sync_cursors() {
if (!cursors_visible()) return;
// Only a mode that *shows* the vertical pair reads them back. With them parked (the
// `horizontal` mode) whatever the plot reports is our own park coming back -- and a
// snapped cursor parked far outside the axis reads back as something finite, which
// would be adopted as a gesture and destroy the shared times.
// Anything within this much of what we last pushed is our own placement coming back
// (the plot stores f64, so an exact compare would be brittle across a rebuild).
const auto moved = [](double a, double b) {
return std::fabs(a - b) > 1.0e-9 * std::max(1.0, std::fabs(b));
};
bool push = false;
for (int i = 0; i < rows_; ++i) {
Row& r = row(i);
if (!r.plot) continue;
// A gesture writes the *plot*; this is the only place its result is visible. A
// ruler is its own row's value -- nothing to share, but it still has to be read
// back or the pane would keep showing wherever the app last put it.
const bool rulers = cursor_mode_ == CursorMode::Horizontal || cursor_mode_ == CursorMode::Both;
if (rulers) {
const double y = r.plot->cursor(r.ruler).y;
const double yb = r.plot->cursor(r.ruler_b).y;
if (y > kParked * 0.5) r.ruler_y = y;
if (yb > kParked * 0.5) r.ruler_b_y = yb;
}
if (cursor_mode_ == CursorMode::Horizontal) continue; // the pair is parked: not a value
const double a = r.plot->cursor(r.cursor_a).x;
const double b = r.plot->cursor(r.cursor_b).x;
if (a < kParked * 0.5 || b < kParked * 0.5) continue; // parked: not a value
if (moved(a, cursor_a_x_)) {
cursor_a_x_ = a;
push = true;
}
if (moved(b, cursor_b_x_)) {
cursor_b_x_ = b;
push = true;
}
}
if (push) {
for (int i = 0; i < rows_; ++i) place_cursors(row(i));
}
place_crossings();
}
void RowSet::clamp_to_data() {
@@ -442,11 +643,8 @@ void RowSet::set_trigger_marker(double x, bool visible) {
}
void RowSet::set_cursors_visible(bool on) {
for (int i = 0; i < kMaxRows; ++i) {
Row& r = row(i);
r.cursors_visible = on;
place_cursors(r);
}
cursors_on_ = on;
for (int i = 0; i < kMaxRows; ++i) place_cursors(row(i));
}
void RowSet::reset_cursors() {
@@ -455,13 +653,18 @@ void RowSet::reset_cursors() {
if (!r.plot) continue;
const plot::Range xr = r.plot->x_range(r.axis_x);
const double span = xr.span() > 0.0 ? xr.span() : window_s_;
r.cursor_a_x = xr.min + 0.25 * span;
r.cursor_b_x = xr.min + 0.75 * span;
r.ruler_y = r.plot->y_range(r.axis_y).min +
0.5 * (r.plot->y_range(r.axis_y).span() > 0.0 ? r.plot->y_range(r.axis_y).span()
: 1.0);
place_cursors(r);
cursor_a_x_ = xr.min + 0.25 * span;
cursor_b_x_ = xr.min + 0.75 * span;
const double y_lo = r.plot->y_range(r.axis_y).min;
const double y_span = r.plot->y_range(r.axis_y).span();
r.ruler_y = y_lo + 0.35 * (y_span > 0.0 ? y_span : 1.0);
r.ruler_b_y = y_lo + 0.65 * (y_span > 0.0 ? y_span : 1.0);
}
// Every row's instruments are written from what was just set. Without this the plots
// keep their build-time cursors (x = 0) and `sync_cursors()` -- which reads the plots
// because a *gesture* writes them -- adopts that zero as a moved cursor.
for (int i = 0; i < kMaxRows; ++i) place_cursors(row(i));
place_crossings();
}
} // namespace udpscope
+102 -5
View File
@@ -48,9 +48,27 @@ class RowSet {
/// about this many a row is chrome and no plot.
static constexpr int kMaxRows = 8;
static constexpr int kSlotsPerRow = 4;
/// Readout marks per row per orientation: enough for a handful of level and time
/// measurements to be shown at once, and a fixed number because a `Plot` fixes its cursor
/// set at construction.
static constexpr int kMeasureMarks = 6;
RowSet();
/// Which cursor instruments are up, from the toolbar's toggle. The two vertical
/// markers are shared by every row (one stream time each, so they line up down the
/// column); a horizontal ruler is per row, because each row has its own y axis.
///
/// `Mix` is the vertical pair with the *per-trace* readout instead of rulers: the
/// pane then reports where each cursor crosses each trace, which is the value a
/// ruler would have to be dragged to express.
enum class CursorMode {
Vertical, ///< two x markers, shared by all rows
Horizontal, ///< two y rulers per row
Both, ///< both instruments
Mix, ///< the x pair, read off each trace
};
struct Row {
/// The figure's row, borrowed. Null for a row beyond `row_count()`, which is how
/// every loop here and in the UI stays inside the rows that exist. Invalidated by
@@ -82,16 +100,28 @@ class RowSet {
plot::u32 cursor_a = 0;
plot::u32 cursor_b = 0;
plot::u32 ruler = 0;
plot::u32 ruler_b = 0;
/// The vertical line marking the trigger instant, drawn whenever an event is
/// on screen (and parked outside the axis otherwise). Not the user's cursor:
/// it keeps its position whatever the cursor toggle says.
plot::u32 trigger_marker = 0;
/// Off by default: the cursors are switched on from the toolbar, and a rebuild
/// (any drop, any row-count change) must not switch them back off.
bool cursors_visible = false;
double cursor_a_x = 0.0;
double cursor_b_x = 0.0;
/// The measurements' readout marks: a pool created with the plot (a `Plot` cannot add or
/// drop a cursor after the fact) and parked unless a measurement is using the slot.
std::array<plot::u32, kMeasureMarks> measure_h{};
std::array<plot::u32, kMeasureMarks> measure_v{};
/// One marker per trace per cursor: where each vertical cursor crosses that trace's
/// series, drawn only in `mix` mode. Two per slot because the pair crosses a trace
/// at two places, and an annotation cannot be shared between them.
std::array<plot::u32, kSlotsPerRow> crossing_a{};
std::array<plot::u32, kSlotsPerRow> crossing_b{};
/// The two rulers' values. The vertical pair is *not* here: one stream time is
/// shared by every row, so it lives on the `RowSet` where it cannot drift between
/// rows.
double ruler_y = 0.0;
double ruler_b_y = 0.0;
};
int row_count() const noexcept { return rows_; }
@@ -152,6 +182,67 @@ class RowSet {
void set_y_range(int row, double lo, double hi);
void set_cursors_visible(bool on);
/// What the cursors are (see `CursorMode`). Switching parks the instruments the mode
/// does not use rather than removing them: a `Plot` cannot drop a cursor.
void set_cursor_mode(CursorMode mode);
/// Places (or hides) the `mix` mode's crossing markers: for every plotted trace, the
/// point where each cursor meets it. Called from `sync_cursors()` and when the mode
/// changes, so the markers are never a frame behind the markers that cast them.
void place_crossings();
CursorMode cursor_mode() const noexcept { return cursor_mode_; }
/// The two vertical cursors' stream times; shared by every row.
double cursor_a_x() const noexcept { return cursor_a_x_; }
double cursor_b_x() const noexcept { return cursor_b_x_; }
/// One row's ruler values, or 0 for a row that does not exist.
double ruler_y(int row) const;
double ruler_b_y(int row) const;
/// How many `mix` crossing markers are on screen, and the first one's position: the
/// probe's window onto "the marks follow the cursors".
int crossings_visible() const;
bool crossing_at(int row, int slot, bool second, double& x, double& y) const;
/// Whether the cursors are on. One flag for the whole set, not a copy per row: it is one
/// piece of state (the toolbar's toggle), and a per-row copy is something a rebuild, an
/// insert or a carried row can clear behind the user's back -- which is how a row could
/// end up with its instruments parked while every other row showed them.
bool cursors_visible() const noexcept { return cursors_on_; }
/// One measurement's readout mark on a row: a *level* (drawn as a horizontal ruler) or a
/// *time* (a vertical marker). Which one a measurement needs is the measurement's own
/// business; the row is the one that plots its signal.
struct MeasureMark {
int row = 0;
bool level = false;
double value = 0.0;
plot::u32 color = 0;
};
/// Places the measurements' marks and parks every pool slot they do not use. The marks are
/// created unselectable (`Plot::set_cursor_selectable()`), so the pointer can neither hover
/// nor drag them; this runs every frame because a mark is only correct for the window its
/// measurement was computed over.
void set_measure_marks(const std::vector<MeasureMark>& marks);
/// How many marks the last `set_measure_marks()` placed: the probe's window onto "the
/// measurement is drawn", as opposed to merely computed.
int measure_marks_placed() const noexcept { return measure_marks_placed_; }
/// Frames the x window on the data row 0 holds, leaving every y axis alone. The x
/// counterpart of `fit_all()`, for the one thing the y axes cannot say: how wide the
/// window should be.
void fit_x();
/// Adopts the instruments a gesture moved. The vertical pair is one time for the whole
/// figure, so a drag on any row's marker moves every row's; a ruler belongs to its own
/// row and is only read back. Either way the plot is where a drag writes, so this is the
/// one place the app can see it -- and the pane reads what is stored here, which is why a
/// ruler that was not read back showed a stale value while its marker on the plot moved.
/// Called once a frame from the tick.
void sync_cursors();
/// Puts both vertical cursors at a quarter and three quarters of the x window, and
/// the two rulers at a third and two thirds of each row's y range. Also switches the
/// cursors on, so the button cannot look dead.
void reset_cursors();
/// Places the trigger marker at stream time `x` (seconds since the epoch), or
@@ -237,6 +328,12 @@ class RowSet {
double y_gutter_px_ = 0.0;
bool y_gutter_reserved_ = false;
std::array<Row, static_cast<std::size_t>(kMaxRows)> storage_{};
CursorMode cursor_mode_ = CursorMode::Vertical;
bool cursors_on_ = false;
int measure_marks_placed_ = 0;
/// The one stream time each vertical cursor is at, whatever row's copy was last moved.
double cursor_a_x_ = 0.0;
double cursor_b_x_ = 0.0;
std::array<bool, static_cast<std::size_t>(kMaxRows)> row_dirty_{};
};
+1595 -14
View File
File diff suppressed because it is too large. Load diff