diff --git a/src/ui.cpp b/src/ui.cpp index 14fa795..9f10466 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3523,6 +3523,44 @@ void math_view_fit(MathDialog* d) { 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 depth(n, 0); + std::vector row(n, 0); + std::vector 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(i)) { + deepest = std::max(deepest, depth[static_cast(ins[k])]); + } + } + const int dep = deepest + 1; + depth[i] = dep; + if (static_cast(dep) >= used.size()) used.resize(static_cast(dep) + 1, 0); + row[i] = used[static_cast(dep)]++; + } + for (std::size_t i = 0; i < n; ++i) { + d->nodes[i].x = 24.0 + static_cast(depth[i]) * kColPitch; + d->nodes[i].y = 20.0 + static_cast(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 /// 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 @@ -4492,11 +4530,17 @@ void open_math_dialog(App& app, int edit) { g_signal_connect(fit, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { math_view_fit(static_cast(user)); }), 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(user)); + }), d); GtkWidget* buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); gtk_box_append(GTK_BOX(buttons), add); gtk_box_append(GTK_BOX(buttons), del); gtk_box_append(GTK_BOX(buttons), out); 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(body), form); 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); } } + if (a->math_dialog != nullptr) { + auto* ty = static_cast( + 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(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); return G_SOURCE_REMOVE; }