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