plot host: rows hold 1..4 cells; split probe scaffold

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-03 17:11:01 +01:00
1 parent 79e69c28fa
commit ae1d262be4
5 files changed
+433 -18

No files matched your search

+213
View File
@@ -3796,6 +3796,219 @@ gboolean layout_probe_step(gpointer user) {
return G_SOURCE_REMOVE;
}
// --- the horizontal plot split (`UDPSCOPE_PROBE_SPLIT_MS`) -------------------------------------
//
// Each step changes the arrangement of cells, re-points the area at the new figure, and lets the
// next step (250 ms on) read what a frame was actually laid out with.
void split_probe_report(App& a, const char* tag) {
RowSet& rs = *a.rows;
const double scale = (a.area != nullptr && a.area->device_scale() > 0.0f)
? static_cast<double>(a.area->device_scale()) : 1.0;
const plot::FigureLayout& fl = rs.figure().layout();
const int frows = rs.figure_row_count();
// tile: every row's cells fill its rect edge to edge, share its y and height, and are one
// `col_gap` apart.
int tile = fl.cells.size() == static_cast<std::size_t>(rs.row_count()) &&
fl.rows.size() == static_cast<std::size_t>(frows)
? 1 : 0;
std::string cells_s;
std::string widths_s;
std::string weights_s;
for (int fr = 0; fr < frows && tile; ++fr) {
const int first = rs.first_plot_of_row(fr);
const int n = rs.cells_in_row(fr);
const plot::Rect& row = fl.rows[static_cast<std::size_t>(fr)];
const plot::Rect& c0 = fl.cells[static_cast<std::size_t>(first)];
const plot::Rect& cn = fl.cells[static_cast<std::size_t>(first + n - 1)];
if (std::fabs(c0.x - row.x) > 0.5f) tile = 0;
if (std::fabs(cn.right() - row.right()) > 0.5f) tile = 0;
for (int k = 0; k < n; ++k) {
const plot::Rect& c = fl.cells[static_cast<std::size_t>(first + k)];
if (std::fabs(c.y - row.y) > 0.5f || std::fabs(c.h - row.h) > 0.5f) tile = 0;
if (k > 0) {
const plot::Rect& prev = fl.cells[static_cast<std::size_t>(first + k - 1)];
if (std::fabs((c.x - prev.right()) - fl.col_gap) > 0.5f) tile = 0;
}
}
}
for (int fr = 0; fr < frows; ++fr) {
const int first = rs.first_plot_of_row(fr);
const int n = rs.cells_in_row(fr);
cells_s += (fr == 0 ? "" : ",") + std::to_string(n);
widths_s += fr == 0 ? "" : "|";
weights_s += fr == 0 ? "" : "|";
for (int k = 0; k < n; ++k) {
char buf[32];
const std::size_t idx = static_cast<std::size_t>(first + k);
std::snprintf(buf, sizeof(buf), "%s%.0f", k == 0 ? "" : ",",
idx < fl.cells.size() ? static_cast<double>(fl.cells[idx].w) / scale : -1.0);
widths_s += buf;
}
const std::vector<float>& w = rs.cell_weights(fr);
if (w.empty()) {
weights_s += "-";
} else {
for (std::size_t k = 0; k < w.size(); ++k) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%s%g", k == 0 ? "" : ",", static_cast<double>(w[k]));
weights_s += buf;
}
}
}
// x_equal: every plot's x range is plot 0's. cursors_aligned: every plot's two vertical cursors
// sit where plot 0's do.
int x_equal = 1;
int aligned = 1;
const RowSet::Row& r0 = rs.row(0);
if (r0.plot != nullptr) {
const plot::Range ref = r0.plot->x_range(r0.axis_x);
const double ref_a = r0.plot->cursor(r0.cursor_a).x;
const double ref_b = r0.plot->cursor(r0.cursor_b).x;
const double span = std::max(1e-12, std::fabs(ref.max - ref.min));
for (int p = 1; p < rs.row_count(); ++p) {
const RowSet::Row& r = rs.row(p);
if (r.plot == nullptr) { x_equal = 0; aligned = 0; continue; }
const plot::Range x = r.plot->x_range(r.axis_x);
if (std::fabs(x.min - ref.min) > 1e-9 * span || std::fabs(x.max - ref.max) > 1e-9 * span) {
x_equal = 0;
}
if (std::fabs(r.plot->cursor(r.cursor_a).x - ref_a) > 1e-9 * span) aligned = 0;
if (std::fabs(r.plot->cursor(r.cursor_b).x - ref_b) > 1e-9 * span) aligned = 0;
}
}
std::string traces;
for (int p = 0; p < rs.row_count(); ++p) {
traces += p == 0 ? "" : "|";
traces += rs.plot_label(p) + ":";
bool any = false;
const RowSet::Row& row = rs.row(p);
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
const std::string& n = row.signal_name[static_cast<std::size_t>(s)];
if (n.empty()) continue;
traces += (any ? "," : "") + n;
any = true;
}
if (!any) traces += "-";
}
std::fprintf(stderr,
"[split-probe] %-14s plots=%d frows=%d cells=[%s] widths=[%s] tile=%d x_equal=%d "
"cursors_aligned=%d weights=[%s] traces=[%s]\n",
tag, rs.row_count(), frows, cells_s.c_str(), widths_s.c_str(), tile, x_equal,
aligned, weights_s.c_str(), traces.c_str());
std::fflush(stderr);
}
namespace {
/// After any structural change: the new figure is the area's, and the frame tick re-lays it out.
void split_probe_restructured(App& a) {
if (a.area != nullptr) a.area->set_figure(&a.rows->figure());
a.relayout_pending = true;
}
/// Channel `c` onto plot `p` (the first free slot), by the same name/colour route a drop takes.
void split_probe_assign(App& a, int p, std::size_t c) {
if (c >= a.model->channel_count() || p < 0 || p >= a.rows->row_count()) return;
const ChannelInfo& ch = a.model->channels()[c];
a.rows->assign(p, 0, ch.name, ch.name, signal_color_index(a.model->signals(), ch.signal));
}
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);
}
}
} // namespace
void split_probe_next(App& a) {
++a.dbg_split_step;
g_timeout_add(250, split_probe_step, &a);
}
gboolean split_probe_step(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr || a->rows == nullptr) return G_SOURCE_REMOVE;
RowSet& rs = *a->rows;
switch (a->dbg_split_step) {
case 0: {
rs.set_cells_per_row({1});
split_probe_clear_all(*a);
// The `enable_all_probe` assignment, in one call and onto plot 0's slots.
std::vector<std::size_t> all(a->model->signals().size());
for (std::size_t i = 0; i < all.size(); ++i) all[i] = i;
a->model->set_signals_enabled(all, true);
int slot = 0;
for (std::size_t c = 0; c < a->model->channel_count() && slot < RowSet::kSlotsPerRow; ++c) {
const ChannelInfo& ch = a->model->channels()[c];
rs.assign(0, slot++, ch.name, ch.name, signal_color_index(a->model->signals(), ch.signal));
}
split_probe_restructured(*a);
split_probe_next(*a);
break;
}
case 1:
split_probe_report(*a, "start");
rs.insert_cell(0, 1);
split_probe_clear_all(*a);
split_probe_assign(*a, 0, 0);
split_probe_assign(*a, 1, 1);
rs.set_cursors_visible(true);
rs.reset_cursors();
split_probe_restructured(*a);
split_probe_next(*a);
break;
case 2:
split_probe_report(*a, "split");
rs.set_cell_weights(0, {3.0f, 1.0f});
split_probe_restructured(*a);
split_probe_next(*a);
break;
case 3:
split_probe_report(*a, "weights 3:1");
rs.insert_row(1);
rs.insert_cell(1, 1);
split_probe_restructured(*a);
split_probe_next(*a);
break;
case 4: {
split_probe_report(*a, "row+cell");
// Fill row 0 to its cap, then keep adding rows to the plot cap; the first refusal of each kind
// is what is being read.
std::string cell_calls;
int refused_cell = 0;
for (int i = 0; i < RowSet::kMaxCellsPerRow + 1; ++i) {
const bool ok = rs.insert_cell(0, rs.cells_in_row(0));
cell_calls += (i == 0 ? "" : ",") + std::to_string(ok ? 1 : 0);
if (!ok) { ++refused_cell; break; }
}
std::string row_calls;
int refused_row = 0;
for (int i = 0; i < RowSet::kMaxRows; ++i) {
const bool ok = rs.insert_row(rs.figure_row_count());
row_calls += (i == 0 ? "" : ",") + std::to_string(ok ? 1 : 0);
if (!ok) { ++refused_row; break; }
}
std::fprintf(stderr,
"[split-probe] caps-calls insert_cell=[%s] refused_cell=%d insert_row=[%s] "
"refused_row=%d plots=%d\n",
cell_calls.c_str(), refused_cell, row_calls.c_str(), refused_row, rs.row_count());
split_probe_restructured(*a);
split_probe_next(*a);
break;
}
case 5:
split_probe_report(*a, "caps");
rs.remove_cell(rs.first_plot_of_row(0) + rs.cells_in_row(0) - 1);
split_probe_restructured(*a);
split_probe_next(*a);
break;
default:
split_probe_report(*a, "remove");
break;
}
return G_SOURCE_REMOVE;
}
gboolean enable_all_probe(gpointer user) {
App* a = static_cast<App*>(user);
// In one call, the way a restored session does it: enabling them one at a time re-plans