Feature/source plugins #1
No files matched your search
+14
-1
@@ -611,6 +611,15 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
st.rows.back().signals = names;
|
||||
} else if (key == "weight") {
|
||||
st.rows.back().weight = std::strtod(value.c_str(), nullptr);
|
||||
} else if (key == "row") {
|
||||
try {
|
||||
const int r = std::stoi(value);
|
||||
if (r >= 0) st.rows.back().row = r;
|
||||
} catch (...) {
|
||||
}
|
||||
} else if (key == "width") {
|
||||
const double w = std::strtod(value.c_str(), nullptr);
|
||||
if (w > 0.0) st.rows.back().width = w;
|
||||
}
|
||||
} else if (section == Section::Math && !st.math.empty()) {
|
||||
MathChannel& m = st.math.back();
|
||||
@@ -784,7 +793,11 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
||||
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
|
||||
for (const auto& row : state.rows) {
|
||||
o << "\n[[plot]]\nweight = " << row.weight << "\nsignals = [";
|
||||
o << "\n[[plot]]\n";
|
||||
// A row of -1 is a state built without the split (nothing in the app does that now); leaving the
|
||||
// key out keeps it readable as the one-cell-per-row file it is.
|
||||
if (row.row >= 0) o << "row = " << row.row << "\nwidth = " << row.width << "\n";
|
||||
o << "weight = " << row.weight << "\nsignals = [";
|
||||
for (std::size_t i = 0; i < row.signals.size(); ++i) {
|
||||
if (i != 0) o << ", ";
|
||||
o << toml_escape(row.signals[i]);
|
||||
|
||||
@@ -24,6 +24,13 @@ struct LayoutState {
|
||||
/// The row's share of the figure's height, 1 for equal rows. Part of the layout because a sizing a
|
||||
/// user dragged is an arrangement, and an arrangement that is not saved is one that is lost.
|
||||
double weight = 1.0;
|
||||
/// The figure row this plot sits in; plots of one row are the cells of a horizontal split, in
|
||||
/// order. -1 means the file had no `row` key (a session from before the split), which loads as
|
||||
/// one cell per row.
|
||||
int row = -1;
|
||||
/// This cell's share of its row's width, 1 for equal cells. `weight` is the *row's* and is
|
||||
/// written on every cell of the row; `width` is the cell's own.
|
||||
double width = 1.0;
|
||||
};
|
||||
|
||||
double window_s = 0.0; ///< 0 keeps the default window
|
||||
|
||||
@@ -223,6 +223,9 @@ struct App {
|
||||
int dbg_layout_step = 0;
|
||||
/// Which step of the split probe runs next (`UDPSCOPE_PROBE_SPLIT_MS`).
|
||||
int dbg_split_step = 0;
|
||||
/// The split probe's round trip: the arrangement saved, and the legacy copy of its file.
|
||||
std::string dbg_split_expect;
|
||||
std::string dbg_split_legacy_path;
|
||||
/// Which step of the cursors probe runs next (`UDPSCOPE_PROBE_CURSORS_MS`).
|
||||
int dbg_cursors_step = 0;
|
||||
/// Which step of the fit probe runs next (`UDPSCOPE_PROBE_FIT_MS`).
|
||||
|
||||
+61
-3
@@ -30,8 +30,16 @@ LayoutState collect_layout(App& app) {
|
||||
row.signals.push_back(r.layer[si] > 0 ? std::string()
|
||||
: r.signal_name[si]);
|
||||
}
|
||||
// The figure row's own weight and this cell's share of the row's width.
|
||||
const int fr = app.rows->figure_row_of(i);
|
||||
const std::vector<float>& weights = app.rows->row_weights();
|
||||
if (static_cast<std::size_t>(i) < weights.size()) row.weight = weights[static_cast<std::size_t>(i)];
|
||||
row.row = fr;
|
||||
if (fr >= 0 && static_cast<std::size_t>(fr) < weights.size()) {
|
||||
row.weight = weights[static_cast<std::size_t>(fr)];
|
||||
}
|
||||
const std::vector<float>& cw = app.rows->cell_weights(fr);
|
||||
const int cell = app.rows->cell_of(i);
|
||||
if (cell >= 0 && static_cast<std::size_t>(cell) < cw.size()) row.width = cw[static_cast<std::size_t>(cell)];
|
||||
st.rows.push_back(std::move(row));
|
||||
}
|
||||
for (std::size_t si = 0; si < model.signals().size(); ++si) {
|
||||
@@ -444,17 +452,67 @@ void apply_layout(App& app, const LayoutState& st) {
|
||||
|
||||
int rows = static_cast<int>(st.rows.size());
|
||||
if (rows > RowSet::kMaxRows) rows = RowSet::kMaxRows;
|
||||
if (rows > 0) app.rows->set_row_count(rows);
|
||||
|
||||
// The arrangement. A file that names a figure row for every plot (`row = ...`) carries a horizontal
|
||||
// split; one that names none -- every session written before the split -- is one cell per row, which
|
||||
// is what it was. A `row` sequence that cannot be a layout (hand-edited, truncated, from a build
|
||||
// with other limits) is not a reason to refuse the file: it loads as the old shape, with the signals
|
||||
// where they were.
|
||||
std::vector<int> cells_per_row;
|
||||
{
|
||||
bool all_rows = !st.rows.empty() && st.rows.size() <= static_cast<std::size_t>(RowSet::kMaxRows);
|
||||
int prev = -1;
|
||||
for (const LayoutState::Row& r : st.rows) {
|
||||
if (!all_rows) break;
|
||||
if (r.row < 0 || r.row < prev || r.row > prev + 1 || (prev < 0 && r.row != 0)) {
|
||||
all_rows = false;
|
||||
} else if (r.row != prev) {
|
||||
cells_per_row.push_back(1);
|
||||
} else if (++cells_per_row.back() > RowSet::kMaxCellsPerRow) {
|
||||
all_rows = false;
|
||||
}
|
||||
prev = r.row;
|
||||
}
|
||||
if (!all_rows) cells_per_row.clear();
|
||||
}
|
||||
const bool split_file = !cells_per_row.empty();
|
||||
if (split_file) {
|
||||
app.rows->set_cells_per_row(cells_per_row);
|
||||
} else if (rows > 0) {
|
||||
app.rows->set_row_count(rows);
|
||||
}
|
||||
|
||||
// The rows' shares of the height come from the same file as their signals: a sizing a user dragged is
|
||||
// an arrangement, and the layout is where an arrangement lives. Absent from an older file, the weights
|
||||
// are all 1 -- equal rows -- which is what such a file was saved with.
|
||||
// are all 1 -- equal rows -- which is what such a file was saved with. The weight is the *figure row's*
|
||||
// and is written on every cell of it; the row's first cell is the one read.
|
||||
{
|
||||
std::vector<float> weights;
|
||||
weights.reserve(st.rows.size());
|
||||
if (split_file) {
|
||||
std::size_t at = 0;
|
||||
for (const int n : cells_per_row) {
|
||||
weights.push_back(static_cast<float>(st.rows[at].weight));
|
||||
at += static_cast<std::size_t>(n);
|
||||
}
|
||||
} else {
|
||||
for (const LayoutState::Row& wr : st.rows) weights.push_back(static_cast<float>(wr.weight));
|
||||
}
|
||||
app.rows->set_row_weights(std::move(weights));
|
||||
}
|
||||
// The cells' shares of a split row's width, after the arrangement (which resets them).
|
||||
if (split_file) {
|
||||
std::size_t at = 0;
|
||||
for (std::size_t fr = 0; fr < cells_per_row.size(); ++fr) {
|
||||
const std::size_t n = static_cast<std::size_t>(cells_per_row[fr]);
|
||||
if (n >= 2) {
|
||||
std::vector<float> widths;
|
||||
for (std::size_t c = 0; c < n; ++c) widths.push_back(static_cast<float>(st.rows[at + c].width));
|
||||
app.rows->set_cell_weights(static_cast<int>(fr), std::move(widths));
|
||||
}
|
||||
at += n;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < rows; ++i) {
|
||||
int slot = 0;
|
||||
for (const std::string& name : st.rows[static_cast<std::size_t>(i)].signals) {
|
||||
|
||||
+117
-1
@@ -18,6 +18,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <tuple>
|
||||
|
||||
namespace udpscope {
|
||||
@@ -3900,6 +3901,34 @@ void split_probe_report(App& a, const char* tag) {
|
||||
}
|
||||
|
||||
namespace {
|
||||
/// The arrangement and what is in it, as one comparable string: per figure row its weight, per cell
|
||||
/// its width share and its traces. Weights are read back as stored, so a load is compared with a save
|
||||
/// to the precision the file keeps (`%.4f`).
|
||||
std::string split_probe_shape(const RowSet& rs) {
|
||||
std::string out;
|
||||
char buf[48];
|
||||
for (int fr = 0; fr < rs.figure_row_count(); ++fr) {
|
||||
const std::vector<float>& rw = rs.row_weights();
|
||||
const std::vector<float>& cw = rs.cell_weights(fr);
|
||||
std::snprintf(buf, sizeof(buf), "%sr%d(h=%.4f)", fr == 0 ? "" : " ", fr,
|
||||
static_cast<std::size_t>(fr) < rw.size() ? static_cast<double>(rw[static_cast<std::size_t>(fr)]) : 1.0);
|
||||
out += buf;
|
||||
const int first = rs.first_plot_of_row(fr);
|
||||
for (int k = 0; k < rs.cells_in_row(fr); ++k) {
|
||||
std::snprintf(buf, sizeof(buf), "[w=%.4f:", static_cast<std::size_t>(k) < cw.size() ? static_cast<double>(cw[static_cast<std::size_t>(k)]) : 1.0);
|
||||
out += buf;
|
||||
bool any = false;
|
||||
for (const std::string& n : rs.row(first + k).signal_name) {
|
||||
if (n.empty()) continue;
|
||||
out += (any ? "," : "") + n;
|
||||
any = true;
|
||||
}
|
||||
out += any ? "]" : "-]";
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// 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());
|
||||
@@ -4002,8 +4031,95 @@ gboolean split_probe_step(gpointer user) {
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
default:
|
||||
case 6: {
|
||||
split_probe_report(*a, "remove");
|
||||
// The session round trip, through the real entry points. Plot 0 alone on row 0, plots 1 and 2 side
|
||||
// by side on row 1, each cell holding a different trace, row 0 at twice row 1's height and the
|
||||
// split row at 3:1.
|
||||
rs.set_cells_per_row({1, 2});
|
||||
split_probe_clear_all(*a);
|
||||
for (int p = 0; p < 3; ++p) split_probe_assign(*a, p, static_cast<std::size_t>(p));
|
||||
rs.set_row_weights({2.0f, 1.0f});
|
||||
rs.set_cell_weights(1, {3.0f, 1.0f});
|
||||
a->dbg_split_expect = split_probe_shape(rs);
|
||||
const LayoutState saved = collect_layout(*a);
|
||||
const std::string path = std::string(g_get_tmp_dir()) + "/udpscope-split-probe.toml";
|
||||
const bool wrote = save_layout(path, saved);
|
||||
// Back to the plain arrangement, so a load that did nothing cannot pass.
|
||||
rs.set_cells_per_row({1});
|
||||
split_probe_clear_all(*a);
|
||||
LayoutState loaded;
|
||||
const bool read = load_layout(path, loaded);
|
||||
// The same file with its `row`/`width` lines stripped: a session from before the split.
|
||||
std::string legacy_path = path + ".legacy";
|
||||
{
|
||||
std::ifstream in(path);
|
||||
std::ofstream out(legacy_path);
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
if (line.rfind("row = ", 0) == 0 || line.rfind("width = ", 0) == 0) continue;
|
||||
out << line << "\n";
|
||||
}
|
||||
}
|
||||
a->dbg_split_legacy_path = legacy_path;
|
||||
std::remove(path.c_str());
|
||||
if (read) apply_layout(*a, loaded);
|
||||
std::fprintf(stderr, "[split-probe] roundtrip-io wrote=%d read=%d plots_in_file=%zu\n",
|
||||
wrote ? 1 : 0, read ? 1 : 0, loaded.rows.size());
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 7: {
|
||||
split_probe_report(*a, "roundtrip");
|
||||
const std::string now = split_probe_shape(rs);
|
||||
std::fprintf(stderr, "[split-probe] roundtrip rt=%d expect=%s got=%s\n",
|
||||
now == a->dbg_split_expect ? 1 : 0, a->dbg_split_expect.c_str(), now.c_str());
|
||||
rs.set_cells_per_row({1});
|
||||
split_probe_clear_all(*a);
|
||||
LayoutState loaded;
|
||||
const bool read = load_layout(a->dbg_split_legacy_path, loaded);
|
||||
std::remove(a->dbg_split_legacy_path.c_str());
|
||||
int rows_unset = 1;
|
||||
for (const LayoutState::Row& r : loaded.rows) {
|
||||
if (r.row != -1) rows_unset = 0;
|
||||
}
|
||||
if (read) apply_layout(*a, loaded);
|
||||
std::fprintf(stderr, "[split-probe] legacy-io read=%d plots_in_file=%zu row_unset=%d\n",
|
||||
read ? 1 : 0, loaded.rows.size(), rows_unset);
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
split_probe_report(*a, "legacy");
|
||||
// A `row` sequence that cannot be a layout loads as one cell per row rather than refusing the
|
||||
// file: five cells in one row (over the cap), a gap, a row that goes back, and one that does not
|
||||
// start at 0.
|
||||
const LayoutState base = collect_layout(*a);
|
||||
const std::vector<std::vector<int>> bad = {{0, 0, 0, 0, 0}, {0, 2, 2}, {0, 1, 0}, {1, 1, 2}};
|
||||
std::string out;
|
||||
int ok = 1;
|
||||
for (const std::vector<int>& seq : bad) {
|
||||
LayoutState st = base;
|
||||
st.rows.clear();
|
||||
for (const int r : seq) {
|
||||
LayoutState::Row row = base.rows[0];
|
||||
row.row = r;
|
||||
st.rows.push_back(row);
|
||||
}
|
||||
apply_layout(*a, st);
|
||||
const bool one_each = rs.figure_row_count() == static_cast<int>(seq.size()) &&
|
||||
rs.row_count() == static_cast<int>(seq.size());
|
||||
if (!one_each) ok = 0;
|
||||
out += (out.empty() ? "" : " ") + std::to_string(rs.figure_row_count()) + "/" +
|
||||
std::to_string(rs.row_count());
|
||||
}
|
||||
std::fprintf(stderr, "[split-probe] invalid-rows ok=%d frows/plots=[%s]\n", ok, out.c_str());
|
||||
split_probe_restructured(*a);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
|
||||
Reference in new issue
Block a user