feat: a tidy button lays the graph out by depth, so no two boxes overlap

A single forward pass -- the list is already in evaluation order, so every
input's depth is in hand by the time the reader is reached -- then a fit. The
column pitch is `kNodeW + 48` rather than the add-node grid's 160, which left
28 units for a wire's 40-unit tread and made the bezier double back.
This commit is contained in:
Martino Ferrari committed 2026-10-07 02:44:50 +02:00
1 parent 5a69b2147b
commit 8918494acb
1 file changed
+73
+73
View File
@@ -3523,6 +3523,44 @@ void math_view_fit(MathDialog* d) {
if (d->canvas != nullptr) gtk_widget_queue_draw(d->canvas); if (d->canvas != nullptr) gtk_widget_queue_draw(d->canvas);
} }
/// Lays the graph out left to right by depth: a node sits one column right of the deepest thing it
/// reads, and its row is its position among the nodes at that depth. It needs no recursion and no
/// visited set -- the list is already in evaluation order, so a single forward pass has every
/// input's depth in hand by the time it reaches the node that reads it.
void math_tidy(MathDialog* d) {
const std::size_t n = d->nodes.size();
if (n == 0) return;
// Wider than the add-node grid's 160: a column pitch of `kNodeW + 48` leaves the wire its 40-unit
// tread on each side with room to curve. At 160 there were 28 units for a 40-unit tread, so the
// bezier's control points crossed and the wire doubled back on itself.
constexpr double kColPitch = kNodeW + 48.0;
constexpr double kRowPitch = kNodeH + 22.0;
std::vector<int> depth(n, 0);
std::vector<int> row(n, 0);
std::vector<int> used;
for (std::size_t i = 0; i < n; ++i) {
const MathNode& node = d->nodes[i];
int deepest = -1;
const int ins[2] = {node.in_a, node.in_b};
for (int k = 0; k < math_op_arity(node.op); ++k) {
if (ins[k] >= 0 && ins[k] < static_cast<int>(i)) {
deepest = std::max(deepest, depth[static_cast<std::size_t>(ins[k])]);
}
}
const int dep = deepest + 1;
depth[i] = dep;
if (static_cast<std::size_t>(dep) >= used.size()) used.resize(static_cast<std::size_t>(dep) + 1, 0);
row[i] = used[static_cast<std::size_t>(dep)]++;
}
for (std::size_t i = 0; i < n; ++i) {
d->nodes[i].x = 24.0 + static_cast<double>(depth[i]) * kColPitch;
d->nodes[i].y = 20.0 + static_cast<double>(row[i]) * kRowPitch;
}
// And frame it, since a tidy that leaves the graph off screen has moved the problem rather than
// solved it.
math_view_fit(d);
}
/// The port under a *canvas* position, with the tolerance in screen pixels. Every gesture goes /// The port under a *canvas* position, with the tolerance in screen pixels. Every gesture goes
/// through this rather than converting for itself: `kPort` is a size on screen, so the graph-space /// through this rather than converting for itself: `kPort` is a size on screen, so the graph-space
/// tolerance has to shrink as the view grows, and a handler that got that backwards makes a port /// tolerance has to shrink as the view grows, and a handler that got that backwards makes a port
@@ -4492,11 +4530,17 @@ void open_math_dialog(App& app, int edit) {
g_signal_connect(fit, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { g_signal_connect(fit, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
math_view_fit(static_cast<MathDialog*>(user)); math_view_fit(static_cast<MathDialog*>(user));
}), d); }), d);
GtkWidget* tidy = gtk_button_new_with_label("tidy");
gtk_widget_set_tooltip_text(tidy, "Lay the graph out left to right and frame it.");
g_signal_connect(tidy, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
math_tidy(static_cast<MathDialog*>(user));
}), d);
GtkWidget* buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); GtkWidget* buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_box_append(GTK_BOX(buttons), add); gtk_box_append(GTK_BOX(buttons), add);
gtk_box_append(GTK_BOX(buttons), del); gtk_box_append(GTK_BOX(buttons), del);
gtk_box_append(GTK_BOX(buttons), out); gtk_box_append(GTK_BOX(buttons), out);
gtk_box_append(GTK_BOX(buttons), fit); gtk_box_append(GTK_BOX(buttons), fit);
gtk_box_append(GTK_BOX(buttons), tidy);
gtk_box_append(GTK_BOX(form), buttons); gtk_box_append(GTK_BOX(form), buttons);
gtk_box_append(GTK_BOX(body), form); gtk_box_append(GTK_BOX(body), form);
gtk_box_append(GTK_BOX(outer), body); gtk_box_append(GTK_BOX(outer), body);
@@ -7638,6 +7682,35 @@ gboolean math_probe(gpointer user) {
std::fprintf(stderr, "[math-probe] op pick: index round trip=%d (want 1)\n", round); std::fprintf(stderr, "[math-probe] op pick: index round trip=%d (want 1)\n", round);
} }
} }
if (a->math_dialog != nullptr) {
auto* ty = static_cast<MathDialog*>(
g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog"));
if (ty != nullptr && ty->nodes.size() >= 2) {
// Pile every node on one spot, so a layout that did nothing would be visible.
for (MathNode& n : ty->nodes) {
n.x = 10.0;
n.y = 10.0;
}
math_tidy(ty);
// No two boxes may overlap -- `math_node_at()` searches topmost-first, so an overlap means
// a click or a drag lands on the wrong node, which reads as the dialog ignoring the pointer.
int overlaps = 0;
std::string xs;
for (std::size_t i = 0; i < ty->nodes.size(); ++i) {
xs += (i != 0 ? "," : "") + std::to_string(static_cast<int>(ty->nodes[i].x));
for (std::size_t j = i + 1; j < ty->nodes.size(); ++j) {
const MathNode& p = ty->nodes[i];
const MathNode& q = ty->nodes[j];
if (p.x < q.x + kNodeW && q.x < p.x + kNodeW && p.y < q.y + kNodeH &&
q.y < p.y + kNodeH) {
++overlaps;
}
}
}
std::fprintf(stderr, "[math-probe] tidy: xs=[%s] overlaps=%d (want 0)\n", xs.c_str(),
overlaps);
}
}
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }