feat: a backwards wire re-sorts the graph instead of being refused
The evaluator walks the node list in order, which used to make the order the user's problem -- an input of an earlier node was not even drawn as connectable. `math_connect()` runs Kahn's algorithm over the trial edge set, with ties broken by the current index so a graph already in order is not permuted, and remaps every index the dialog holds in one block. A self wire and a cycle are refused with a message and change nothing.
This commit is contained in:
1 parent
4c726adb54
commit
5ca762031d
3 files changed
+169
-8
No files matched your search
+155
-4
@@ -3565,6 +3565,97 @@ MathWireRef math_wire_at_canvas(const MathDialog* d, double cx, double cy) {
|
||||
return math_wire_at(d, gx, gy, kWirePick / d->view_scale);
|
||||
}
|
||||
|
||||
/// Wires `from`'s output into `to`'s input `which`. The evaluator walks the node list in order, so
|
||||
/// a reader must sit after what it reads -- and rather than refuse a wire drawn right to left (the
|
||||
/// old editor did not even draw such an input as connectable, which made the order the *user's*
|
||||
/// problem), the list is re-sorted so that it holds. Returns false and sets `why` for a wire that
|
||||
/// cannot be ordered at all, i.e. one that would close a cycle; nothing is written in that case.
|
||||
bool math_connect(MathDialog* d, int from, int to, int which, std::string* why) {
|
||||
const int n = static_cast<int>(d->nodes.size());
|
||||
if (from < 0 || from >= n || to < 0 || to >= n) return false;
|
||||
if (from == to) {
|
||||
if (why != nullptr) *why = "a node cannot read itself";
|
||||
return false;
|
||||
}
|
||||
// Kahn's algorithm over the *trial* edge set, so a cycle is found before anything is written.
|
||||
std::vector<int> in_a(static_cast<std::size_t>(n));
|
||||
std::vector<int> in_b(static_cast<std::size_t>(n));
|
||||
for (int i = 0; i < n; ++i) {
|
||||
in_a[static_cast<std::size_t>(i)] = d->nodes[static_cast<std::size_t>(i)].in_a;
|
||||
in_b[static_cast<std::size_t>(i)] = d->nodes[static_cast<std::size_t>(i)].in_b;
|
||||
}
|
||||
(which == 0 ? in_a : in_b)[static_cast<std::size_t>(to)] = from;
|
||||
|
||||
std::vector<int> indegree(static_cast<std::size_t>(n), 0);
|
||||
const auto edges_of = [&](int i, int (&out)[2]) {
|
||||
const MathNode& node = d->nodes[static_cast<std::size_t>(i)];
|
||||
const int arity = math_op_arity(node.op);
|
||||
out[0] = arity >= 1 ? in_a[static_cast<std::size_t>(i)] : -1;
|
||||
out[1] = arity >= 2 ? in_b[static_cast<std::size_t>(i)] : -1;
|
||||
};
|
||||
for (int i = 0; i < n; ++i) {
|
||||
int e[2] = {-1, -1};
|
||||
edges_of(i, e);
|
||||
for (int k = 0; k < 2; ++k) {
|
||||
if (e[k] >= 0 && e[k] < n && e[k] != i) ++indegree[static_cast<std::size_t>(i)];
|
||||
}
|
||||
}
|
||||
// Ties broken by the current list index, so a graph that is already in order is not permuted:
|
||||
// the positions are the user's layout as much as the evaluator's order, and shuffling a valid
|
||||
// graph on every wire would read as the editor rearranging itself.
|
||||
std::vector<int> order;
|
||||
order.reserve(static_cast<std::size_t>(n));
|
||||
std::vector<bool> done(static_cast<std::size_t>(n), false);
|
||||
for (int step = 0; step < n; ++step) {
|
||||
int pick = -1;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!done[static_cast<std::size_t>(i)] && indegree[static_cast<std::size_t>(i)] == 0) {
|
||||
pick = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pick < 0) {
|
||||
if (why != nullptr) *why = "that wire would make a loop";
|
||||
return false;
|
||||
}
|
||||
done[static_cast<std::size_t>(pick)] = true;
|
||||
order.push_back(pick);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (done[static_cast<std::size_t>(i)]) continue;
|
||||
int e[2] = {-1, -1};
|
||||
edges_of(i, e);
|
||||
for (int k = 0; k < 2; ++k) {
|
||||
if (e[k] == pick) --indegree[static_cast<std::size_t>(i)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `order[p]` is the node that moves to position `p`; `where[old]` is where it ends up. Every
|
||||
// index the dialog holds is in the old space and has to be remapped in one block -- a missed one
|
||||
// silently renames an operation or re-points a wire.
|
||||
std::vector<int> where(static_cast<std::size_t>(n), -1);
|
||||
for (int p = 0; p < n; ++p) where[static_cast<std::size_t>(order[static_cast<std::size_t>(p)])] = p;
|
||||
std::vector<MathNode> sorted;
|
||||
sorted.reserve(static_cast<std::size_t>(n));
|
||||
for (int p = 0; p < n; ++p) sorted.push_back(d->nodes[static_cast<std::size_t>(order[static_cast<std::size_t>(p)])]);
|
||||
const auto remap = [&](int i) { return i >= 0 && i < n ? where[static_cast<std::size_t>(i)] : -1; };
|
||||
for (int p = 0; p < n; ++p) {
|
||||
MathNode& node = sorted[static_cast<std::size_t>(p)];
|
||||
const int old = order[static_cast<std::size_t>(p)];
|
||||
node.in_a = math_op_arity(node.op) >= 1 ? remap(in_a[static_cast<std::size_t>(old)]) : -1;
|
||||
node.in_b = math_op_arity(node.op) >= 2 ? remap(in_b[static_cast<std::size_t>(old)]) : -1;
|
||||
}
|
||||
d->nodes = std::move(sorted);
|
||||
d->output = remap(d->output);
|
||||
d->selected = remap(d->selected);
|
||||
d->drag_node = remap(d->drag_node);
|
||||
d->wire_from = remap(d->wire_from);
|
||||
d->wire_detach = remap(d->wire_detach);
|
||||
d->selected_wire.node = remap(d->selected_wire.node);
|
||||
if (why != nullptr) why->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void math_canvas_draw(GtkDrawingArea*, cairo_t* cr, int width, int height, gpointer user) {
|
||||
MathDialog* d = static_cast<MathDialog*>(user);
|
||||
cairo_set_source_rgb(cr, 0.06, 0.078, 0.094);
|
||||
@@ -3793,10 +3884,16 @@ void math_canvas_drag_end(GtkGestureDrag*, double dx, double dy, gpointer user)
|
||||
int which = kPortOut;
|
||||
const int target = math_port_at_canvas(d, d->press_cx + dx, d->press_cy + dy, which);
|
||||
if (target >= 0 && which != kPortOut && target != d->wire_from) {
|
||||
MathNode& n = d->nodes[static_cast<std::size_t>(target)];
|
||||
(which == 0 ? n.in_a : n.in_b) = d->wire_from;
|
||||
d->selected = target;
|
||||
math_dialog_sync(d);
|
||||
std::string why;
|
||||
if (math_connect(d, d->wire_from, target, which, &why)) {
|
||||
// `math_connect()` may have permuted the list, so the target's index is no longer `target`.
|
||||
math_dialog_sync(d);
|
||||
} else if (d->wire_detach >= 0) {
|
||||
// A refused wire must not also lose the one the drag detached to start with.
|
||||
MathNode& back = d->nodes[static_cast<std::size_t>(d->wire_detach)];
|
||||
(d->wire_detach_which == 0 ? back.in_a : back.in_b) = d->wire_from;
|
||||
math_dialog_sync(d);
|
||||
}
|
||||
}
|
||||
// A release over nothing leaves the input the drag emptied empty: that *is* the detach.
|
||||
d->drag_mode = MathDrag::None;
|
||||
@@ -7343,6 +7440,60 @@ gboolean math_probe(gpointer user) {
|
||||
p->selected_wire = MathWireRef{-1, 0};
|
||||
}
|
||||
}
|
||||
if (a->math_dialog != nullptr) {
|
||||
auto* c = static_cast<MathDialog*>(
|
||||
g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog"));
|
||||
if (c != nullptr && c->nodes.size() >= 2) {
|
||||
// A wire drawn right-to-left is legal: the editor re-sorts so that the reader still sits
|
||||
// after what it reads. Node 0 is the source and node 1 the gain, so wiring the gain into
|
||||
// the source is impossible (a source has no inputs) -- a second gain reading the first,
|
||||
// placed *before* it, is the case.
|
||||
c->nodes.clear();
|
||||
MathNode src;
|
||||
src.op = MathOp::Source;
|
||||
src.source = name;
|
||||
// The reader is placed *before* what it reads, which is the case the evaluator cannot run
|
||||
// and the editor has to repair. Index 1 will be wired to read index 2, and index 2 reads
|
||||
// the source.
|
||||
MathNode reader;
|
||||
reader.op = MathOp::Gain;
|
||||
MathNode feeder;
|
||||
feeder.op = MathOp::Gain;
|
||||
c->nodes = {src, reader, feeder};
|
||||
c->nodes[2].in_a = 0;
|
||||
// The output is the reader, so the remap is visible in `output` as well as in the wiring.
|
||||
c->output = 1;
|
||||
c->selected = 1;
|
||||
std::string why;
|
||||
const bool ok = math_connect(c, 2, 1, 0, &why);
|
||||
// After the sort node 1 must still be the one that reads 0, and the reader of it must sit
|
||||
// after it. The op is the same for both, so the *wiring* is the reading.
|
||||
std::string order;
|
||||
for (std::size_t i = 0; i < c->nodes.size(); ++i) {
|
||||
order += (i != 0 ? "," : "");
|
||||
order += std::to_string(i) + ":" + std::to_string(c->nodes[i].in_a);
|
||||
}
|
||||
std::string valid_why;
|
||||
const bool valid = math_channel_valid_nodes(c->nodes, c->output, &valid_why);
|
||||
std::fprintf(stderr,
|
||||
"[math-probe] wire: backwards ok=%d order=[%s] output=%d valid=%d why=[%s] "
|
||||
"(want 1 ... 1 [])\n",
|
||||
ok ? 1 : 0, order.c_str(), c->output, valid ? 1 : 0, valid_why.c_str());
|
||||
|
||||
std::string self_why;
|
||||
const bool self_ok = math_connect(c, 1, 1, 0, &self_why);
|
||||
std::fprintf(stderr, "[math-probe] wire: self refused=%d why=[%s] (want 1)\n",
|
||||
self_ok ? 0 : 1, self_why.c_str());
|
||||
|
||||
// A cycle: the node that already feeds the other is asked to read it back.
|
||||
const int reader_idx = c->nodes[1].in_a == 0 ? 2 : 1;
|
||||
const int feeder_idx = reader_idx == 2 ? 1 : 2;
|
||||
std::string cyc_why;
|
||||
const bool cyc_ok = math_connect(c, reader_idx, feeder_idx, 0, &cyc_why);
|
||||
std::fprintf(stderr, "[math-probe] wire: cycle refused=%d why=[%s] (want 1)\n",
|
||||
cyc_ok ? 0 : 1, cyc_why.c_str());
|
||||
}
|
||||
}
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user