plot host: split rows share y, rulers per row
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
135310ceca
commit
5f0bc75c27
5 files changed
+174
-24
No files matched your search
@@ -2857,7 +2857,8 @@ inside the panel does not close it), `click_outside_closes=1` (a click on the pl
|
||||
[split-probe] weights 3:1 plots=2 frows=1 cells=[2] widths=[533,220] tile=1 x_equal=1 cursors_aligned=1 weights=[3,1] traces=[1a:sine|1b:square]
|
||||
[split-probe] caps-calls insert_cell=[1,1,0] refused_cell=1 insert_row=[1,1,0] refused_row=1 plots=8
|
||||
[split-probe] roundtrip rt=1 expect=r0(h=2.0000)[w=1.0000:sine] r1(h=1.0000)[w=3.0000:square][w=1.0000:ramp] got=r0(h=2.0000)[w=1.0000:sine] r1(h=1.0000)[w=3.0000:square][w=1.0000:ramp]
|
||||
[split-probe] ruler-labels split=[plot 1a,plot 1b,plot 2a,plot 2b] mixed=[row 0,plot 2a,plot 2b]
|
||||
[split-probe] ruler-labels split=[row 1,row 2] mixed=[row 0,row 2]
|
||||
[split-probe] shared-y y_shared=1 y_gutter=1 y_fit=1 ruler_mirror=1 ranges=[A=-3.000000..7.000000 B=-3.000000..7.000000 | fit A=-1.04..1.04 B=-1.04..1.04 data=-1..1] axes_w=[642.4,642.4] auto=1
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_SPLIT_MS` (stderr; see [verification.md](verification.md)). Nineteen steps, 250 ms apart;
|
||||
@@ -2903,7 +2904,8 @@ The other lines, in order:
|
||||
| `plot-on-buttons` | `[1a:row 1, cell 1 of 2\|1b:row 1, cell 2 of 2\|2a:row 2, cell 1 of 2\|2b:row 2, cell 2 of 2]` | a signals-page button labelled with the flat index, or without the cell tooltip; a one-cell row keeps its plain label and the tooltip "draw this signal on this plot" |
|
||||
| `assign-1b` / `drop-2b` | `assigned=1 notice='plotting 'sine' on plot 1b'`; `dropped=1 notice='plotting 'square' on plot 2b'` | a notice that names the plot by number alone |
|
||||
| `after-assign` (state line) | `traces=[1a:-\|1b:sine\|2a:-\|2b:square]` | the signal went to a neighbouring cell |
|
||||
| `ruler-labels` | `split=[plot 1a,plot 1b,plot 2a,plot 2b]` and, with cells `{1,2}`, `mixed=[row 0,plot 2a,plot 2b]` | the cursors pane's per-plot ruler line (a label read through the pane's own widgets): a split row's plots are `plot <label>`, an unsplit row keeps `row <plot index>` (**0-based**, as it always was -- `row 0` is the first plot, not a defect) |
|
||||
| `ruler-labels` | `split=[row 1,row 2]` and, with cells `{1,2}`, `mixed=[row 0,row 2]` | the cursors pane's ruler lines (a label read through the pane's own widgets): **one line per figure row**, because the cells of a split row share their y axis and so their rulers. A split row is `row <figure row, from 1>` and sits on its first cell; an unsplit row keeps `row <plot index>` (**0-based**, as it always was -- `row 0` is the first plot, not a defect). One line per cell (`plot 1a,plot 1b,...`) is the pre-sharing reading |
|
||||
| `shared-y` | `y_shared=1 y_gutter=1 y_fit=1 ruler_mirror=1`, `ranges=[A=-3..7 B=-3..7 | fit A=B]`, `axes_w` equal to within a pixel, `auto=1` | a row of two cells with a trace each. `y_shared`: a `set_y_range(-3, 7)` on cell A is B's after one frame. `y_gutter`: only cell A has a y gutter (B's `y_axis_width` and `y_label_area.w` are 0) and the two plots are equally wide. `y_fit`: `fit_all()` leaves both cells on one range that covers both traces (not the manual one). `ruler_mirror`: a ruler written on A and read back by `sync_cursors(0)` is B's too. A 0 in the first two is `set_share_row_y(true)` not reaching the figure; in `y_fit`, no frame between `fit_all()` and the read (the probe restructures to pump one) or a group that did not follow the flag |
|
||||
|
||||
The run ends with the usual `gtk warnings: 0` and no `Gtk-CRITICAL`. `x_equal=1` and `cursors_aligned=1` on every state line are
|
||||
the only readings of the shared time window across cells.
|
||||
@@ -205,15 +205,15 @@ precedent, not an exception.
|
||||
with, so a synchronous sequence would read every position one action stale. It clicks by
|
||||
emitting `::clicked`, never `gtk_widget_activate()`, which holds a `GtkButton` for its
|
||||
250 ms `ACTIVATE_TIMEOUT` and then emits into a button the rebuild has already destroyed.
|
||||
- `UDPSCOPE_PROBE_SPLIT_MS` — drive the horizontal plot split, 19 steps 250 ms apart (so a run needs
|
||||
`UDPSCOPE_PROBE_SPLIT_MS=1500 UDPSCOPE_EXIT_AFTER_MS=7500` at least; it enables and assigns the
|
||||
- `UDPSCOPE_PROBE_SPLIT_MS` — drive the horizontal plot split, 23 steps 250 ms apart (so a run needs
|
||||
`UDPSCOPE_PROBE_SPLIT_MS=1500 UDPSCOPE_EXIT_AFTER_MS=9500` at least; it enables and assigns the
|
||||
signals itself, so `UDPSCOPE_ENABLE_ALL_MS` is not needed). Each step restructures the cells,
|
||||
re-points the area at the new figure and lets the *next* step read a frame that was actually laid out
|
||||
with them, which is why every report line describes the previous step's change. It splits and
|
||||
weights a row, fills the caps, round-trips a session through `collect_layout()` / `save_layout()` /
|
||||
`load_layout()` / `apply_layout()` (and a copy with its `row`/`width` lines stripped), clicks the layout
|
||||
editor's real cell `+` button, drives the column drag handler, and checks the signals page, the drop's
|
||||
`row_at()` and the cursors pane's ruler labels. Run it once, serially, on its own mock port, from a
|
||||
`row_at()`, the cursors pane's ruler labels and the shared y axis of a split row (`shared-y`). Run it once, serially, on its own mock port, from a
|
||||
fresh `XDG_CONFIG_HOME` (the probe deletes the temp files `udpscope-split-probe.toml` and `.legacy` it
|
||||
writes under `g_get_tmp_dir()`, but the editor's clicks schedule a real session save). Read it against
|
||||
the `[split-probe]` section of [probe-readings.md](probe-readings.md); the run must still end with
|
||||
|
||||
@@ -289,6 +289,13 @@ bool RowSet::insert_cell(int fr, int at) {
|
||||
const int p = first_plot_of_row(fr) + k;
|
||||
for (int i = rows_; i > p; --i) carry_row(row(i), row(i - 1));
|
||||
clear_row(row(p));
|
||||
// The row's rulers belong to the new cell as well (they are one pair for the whole row): taken from
|
||||
// a neighbour that is not the new cell, which sits at `p` and so cannot be its own source.
|
||||
{
|
||||
const Row& src = row(k == 0 ? p + 1 : first_plot_of_row(fr));
|
||||
row(p).ruler_y = src.ruler_y;
|
||||
row(p).ruler_b_y = src.ruler_b_y;
|
||||
}
|
||||
++rows_;
|
||||
++cells_per_row_[static_cast<std::size_t>(fr)];
|
||||
auto& w = cell_weights_[static_cast<std::size_t>(fr)];
|
||||
@@ -469,6 +476,10 @@ void RowSet::rebuild_all() {
|
||||
figure_ = std::make_unique<plot::Figure>(std::span<const plot::u32>(cells));
|
||||
figure_->set_row_gap(kRowGap);
|
||||
figure_->set_col_gap(kColGap);
|
||||
// The cells of a split row are one plot with a vertical divider, not unrelated plots: they share
|
||||
// one y range and auto state, and only the first draws the y axis. Before `install()`, which adds
|
||||
// the axes the figure enrols; a row of one cell is never a group, so nothing changes for it.
|
||||
figure_->set_share_row_y(true);
|
||||
apply_row_weights();
|
||||
apply_cell_weights();
|
||||
x_group_ = figure_->add_x_link();
|
||||
@@ -480,6 +491,13 @@ void RowSet::rebuild_all() {
|
||||
continue;
|
||||
}
|
||||
install(r, i, figure_->plot(static_cast<plot::u32>(i)));
|
||||
// A ruler is the row's (its y axis is shared), so every cell of a split row holds the first
|
||||
// cell's values -- including a cell a restructure put next to one it had nothing to do with.
|
||||
if (const int fr = figure_row_of(i); fr >= 0 && cell_of(i) > 0) {
|
||||
const Row& first = row(first_plot_of_row(fr));
|
||||
r.ruler_y = first.ruler_y;
|
||||
r.ruler_b_y = first.ruler_b_y;
|
||||
}
|
||||
place_cursors(r);
|
||||
if (trigger_marker_on_) {
|
||||
r.plot->set_cursor_x(r.trigger_marker, trigger_marker_x_, plot::CursorPlacement::Free);
|
||||
@@ -1250,6 +1268,18 @@ void RowSet::sync_cursors(int drag_row) {
|
||||
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;
|
||||
// The row's cells share one y axis, so the dragged ruler is the row's: every other cell of the
|
||||
// figure row takes the values and draws them (`Free`: the dragged cell's own value just came
|
||||
// out of its plot and must not be snapped).
|
||||
const int fr = figure_row_of(drag_row);
|
||||
const int first = first_plot_of_row(fr);
|
||||
for (int i = first; i < first + cells_in_row(fr); ++i) {
|
||||
if (i == drag_row) continue;
|
||||
Row& mate = row(i);
|
||||
mate.ruler_y = r.ruler_y;
|
||||
mate.ruler_b_y = r.ruler_b_y;
|
||||
place_cursors(mate, plot::CursorPlacement::Free);
|
||||
}
|
||||
}
|
||||
bool push = false;
|
||||
if (cursor_mode_ != CursorMode::Horizontal) {
|
||||
|
||||
+26
-16
@@ -1752,7 +1752,8 @@ GtkWidget* cursors_header(App& app) {
|
||||
|
||||
/// The unit a row's y rulers are in: the one its bound channels agree on, or none when they
|
||||
/// disagree -- a ruler sits on the row's y axis, and two signals of different units on one
|
||||
/// axis make "the" unit a guess.
|
||||
/// axis make "the" unit a guess. The cells of a split row share that axis, so the channels of
|
||||
/// every cell of `row`'s figure row count.
|
||||
///
|
||||
/// The channel list is rebuilt by the producer, so it is read under the model mutex, like
|
||||
/// every other read of it from this thread.
|
||||
@@ -1760,18 +1761,23 @@ std::string row_unit(App& app, int row) {
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
std::string unit;
|
||||
bool first = true;
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||
const std::size_t si = static_cast<std::size_t>(s);
|
||||
const RowSet::Row& r = app.rows->row(row);
|
||||
if (r.signal_name[si].empty()) continue;
|
||||
const int ch = r.channel[si];
|
||||
if (ch < 0 || static_cast<std::size_t>(ch) >= app.model->channels().size()) continue;
|
||||
const std::string& u = app.model->channels()[static_cast<std::size_t>(ch)].unit;
|
||||
if (first) {
|
||||
unit = u;
|
||||
first = false;
|
||||
} else if (unit != u) {
|
||||
return "";
|
||||
const int fr = app.rows->figure_row_of(row);
|
||||
const int begin = fr >= 0 ? app.rows->first_plot_of_row(fr) : row;
|
||||
const int end = fr >= 0 ? begin + app.rows->cells_in_row(fr) : row + 1;
|
||||
for (int p = begin; p < end; ++p) {
|
||||
const RowSet::Row& r = app.rows->row(p);
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||
const std::size_t si = static_cast<std::size_t>(s);
|
||||
if (r.signal_name[si].empty()) continue;
|
||||
const int ch = r.channel[si];
|
||||
if (ch < 0 || static_cast<std::size_t>(ch) >= app.model->channels().size()) continue;
|
||||
const std::string& u = app.model->channels()[static_cast<std::size_t>(ch)].unit;
|
||||
if (first) {
|
||||
unit = u;
|
||||
first = false;
|
||||
} else if (unit != u) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
return unit;
|
||||
@@ -1840,10 +1846,14 @@ void rebuild_cursors_pane(App& app) {
|
||||
for (int r = 0; r < app.rows->row_count(); ++r) {
|
||||
if (!rulers) break;
|
||||
char who[64];
|
||||
// A row split into cells names its plots the way the rest of the UI does ("plot 2a"); an
|
||||
const int fr = app.rows->figure_row_of(r);
|
||||
const bool split = app.rows->cells_in_row(fr) > 1;
|
||||
// A ruler pair belongs to the figure row (its cells share one y axis), so a split row gets one
|
||||
// line, on its first cell, named by the row's number -- the one `plot_label()` leads with. An
|
||||
// unsplit row keeps the wording it always had.
|
||||
if (app.rows->cells_in_row(app.rows->figure_row_of(r)) > 1) {
|
||||
std::snprintf(who, sizeof(who), "plot %s", app.rows->plot_label(r).c_str());
|
||||
if (split && app.rows->cell_of(r) != 0) continue;
|
||||
if (split) {
|
||||
std::snprintf(who, sizeof(who), "row %d", fr + 1);
|
||||
} else {
|
||||
std::snprintf(who, sizeof(who), "row %d", r);
|
||||
}
|
||||
|
||||
+111
-3
@@ -3974,6 +3974,18 @@ void split_probe_assign(App& a, int p, std::size_t c) {
|
||||
a.rows->assign(p, 0, ch.name, ch.name, signal_color_index(a.model->signals(), ch.signal));
|
||||
}
|
||||
|
||||
/// What the shared-y steps carry from the step that measures to the step that prints.
|
||||
struct SharedYProbe {
|
||||
int y_shared = 0;
|
||||
int y_gutter = 0;
|
||||
int ruler_mirror = 0;
|
||||
std::string ranges;
|
||||
std::string axes_w;
|
||||
RowSet::CursorMode was_mode = RowSet::CursorMode::Mix;
|
||||
bool was_on = false;
|
||||
};
|
||||
SharedYProbe g_shared_y;
|
||||
|
||||
void split_probe_clear_all(App& a) {
|
||||
for (int r = 0; r < RowSet::kMaxRows; ++r) {
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) a.rows->assign(r, s, "", "", 0);
|
||||
@@ -4393,9 +4405,10 @@ gboolean split_probe_step(gpointer user) {
|
||||
}
|
||||
case 18: {
|
||||
split_probe_report(*a, "after-assign");
|
||||
// The cursors pane's ruler labels (`rebuild_cursors_pane()`, ruler modes): a plot of a split row is
|
||||
// "plot <label>", a plot of an unsplit row keeps "row <index>". Both arrangements are read off the
|
||||
// pane's own label widgets, the all-split one first and a mixed one second.
|
||||
// The cursors pane's ruler labels (`rebuild_cursors_pane()`, ruler modes): one line per figure row,
|
||||
// since a split row's cells share their y axis and their rulers. A split row is "row <figure row,
|
||||
// from 1>" (one line, not one per cell); an unsplit row keeps "row <index>". Both arrangements are
|
||||
// read off the pane's own label widgets, the all-split one first and a mixed one second.
|
||||
const RowSet::CursorMode was_mode = rs.cursor_mode();
|
||||
rs.set_cursor_mode(RowSet::CursorMode::Both);
|
||||
const auto ruler_labels = [&]() {
|
||||
@@ -4415,6 +4428,101 @@ gboolean split_probe_step(gpointer user) {
|
||||
mixed_labels.c_str());
|
||||
rs.set_cursor_mode(was_mode);
|
||||
rebuild_cursors_pane(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
// --- Steps 19..21: a split row shares one y axis ---
|
||||
case 19: {
|
||||
// One figure row of two cells with a different trace in each (sine, ramp), rulers on. The next
|
||||
// step sets a range on cell A against a frame that was laid out with this arrangement.
|
||||
g_shared_y.was_mode = rs.cursor_mode();
|
||||
g_shared_y.was_on = rs.cursors_visible();
|
||||
rs.set_cells_per_row({2});
|
||||
split_probe_clear_all(*a);
|
||||
if (a->model->channel_count() >= 3) {
|
||||
split_probe_assign(*a, 0, 0);
|
||||
split_probe_assign(*a, 1, 2);
|
||||
}
|
||||
rs.set_cursor_mode(RowSet::CursorMode::Both);
|
||||
rs.set_cursors_visible(true);
|
||||
rs.reset_cursors();
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 20: {
|
||||
// A range set on cell A; the frame between this step and the next is the row's next layout.
|
||||
rs.set_y_range(0, -3.0, 7.0);
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 21: {
|
||||
const RowSet::Row& ra = rs.row(0);
|
||||
const RowSet::Row& rb = rs.row(1);
|
||||
if (ra.plot == nullptr || rb.plot == nullptr) {
|
||||
std::fprintf(stderr, "[split-probe] shared-y no plots\n");
|
||||
break;
|
||||
}
|
||||
// 1. The range set on A is B's.
|
||||
const plot::Range a_rng = ra.plot->y_range(ra.axis_y);
|
||||
const plot::Range b_rng = rb.plot->y_range(rb.axis_y);
|
||||
const int y_shared = (std::fabs(b_rng.min + 3.0) < 1e-9 && std::fabs(b_rng.max - 7.0) < 1e-9 &&
|
||||
std::fabs(a_rng.min + 3.0) < 1e-9 && std::fabs(a_rng.max - 7.0) < 1e-9)
|
||||
? 1 : 0;
|
||||
// 2. Only the first cell has a gutter, and the two plots are the same width.
|
||||
const plot::PlotLayout& la = ra.plot->layout();
|
||||
const plot::PlotLayout& lb = rb.plot->layout();
|
||||
const int y_gutter = (la.y_axis_width > 0.0f && lb.y_axis_width == 0.0f && lb.y_label_area.w == 0.0f &&
|
||||
std::fabs(la.axes.w - lb.axes.w) <= 1.0f)
|
||||
? 1 : 0;
|
||||
const std::string ranges_before =
|
||||
"A=" + std::to_string(a_rng.min) + ".." + std::to_string(a_rng.max) +
|
||||
" B=" + std::to_string(b_rng.min) + ".." + std::to_string(b_rng.max);
|
||||
const std::string axes_w = std::to_string(la.axes.w) + "," + std::to_string(lb.axes.w);
|
||||
// 4. A ruler written on A's plot and read back by sync_cursors() is B's too.
|
||||
ra.plot->set_cursor_y(ra.ruler, 1.25, plot::CursorPlacement::Free);
|
||||
ra.plot->set_cursor_y(ra.ruler_b, 4.5, plot::CursorPlacement::Free);
|
||||
rs.sync_cursors(0);
|
||||
const int ruler_mirror = (rs.ruler_y(0) == 1.25 && rs.ruler_y(1) == 1.25 && rs.ruler_b_y(0) == 4.5 &&
|
||||
rs.ruler_b_y(1) == 4.5 && rb.plot->cursor(rb.ruler).y == 1.25 &&
|
||||
rb.plot->cursor(rb.ruler_b).y == 4.5)
|
||||
? 1 : 0;
|
||||
g_shared_y.y_shared = y_shared;
|
||||
g_shared_y.y_gutter = y_gutter;
|
||||
g_shared_y.ruler_mirror = ruler_mirror;
|
||||
g_shared_y.ranges = ranges_before;
|
||||
g_shared_y.axes_w = axes_w;
|
||||
// 3. Fit all: the next frame's layout fits the union of both cells' traces.
|
||||
rs.fit_all();
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 22: {
|
||||
const RowSet::Row& ra = rs.row(0);
|
||||
const RowSet::Row& rb = rs.row(1);
|
||||
if (ra.plot == nullptr || rb.plot == nullptr) break;
|
||||
const plot::Range a_rng = ra.plot->y_range(ra.axis_y);
|
||||
const plot::Range b_rng = rb.plot->y_range(rb.axis_y);
|
||||
plot::Range da{};
|
||||
plot::Range db{};
|
||||
const bool have_a = ra.plot->y_data_bounds(ra.axis_y, da);
|
||||
const bool have_b = rb.plot->y_data_bounds(rb.axis_y, db);
|
||||
const double lo = have_a && have_b ? std::min(da.min, db.min) : 0.0;
|
||||
const double hi = have_a && have_b ? std::max(da.max, db.max) : 0.0;
|
||||
const int y_fit = (have_a && have_b && a_rng.min == b_rng.min && a_rng.max == b_rng.max &&
|
||||
a_rng.min <= lo && a_rng.max >= hi && a_rng.max - a_rng.min < 10.0)
|
||||
? 1 : 0;
|
||||
std::fprintf(stderr,
|
||||
"[split-probe] shared-y y_shared=%d y_gutter=%d y_fit=%d ruler_mirror=%d "
|
||||
"ranges=[%s | fit A=%.9g..%.9g B=%.9g..%.9g data=%.4g..%.4g] axes_w=[%s] auto=%d\n",
|
||||
g_shared_y.y_shared, g_shared_y.y_gutter, y_fit, g_shared_y.ruler_mirror,
|
||||
g_shared_y.ranges.c_str(), a_rng.min, a_rng.max, b_rng.min, b_rng.max, lo, hi,
|
||||
g_shared_y.axes_w.c_str(), rs.figure().row_y_auto(0) ? 1 : 0);
|
||||
rs.set_cursor_mode(g_shared_y.was_mode);
|
||||
rs.set_cursors_visible(g_shared_y.was_on);
|
||||
rebuild_cursors_pane(*a);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user