From 8dd6b3db12ba0a7633f9590ee71ecb53b96860ba Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 7 Oct 2026 01:59:27 +0200 Subject: [PATCH] fix: a drag's release point is the press plus the offset, so a detached wire can be dropped Record the press in canvas coordinates at drag-begin; use press+total-offset in both the Wire update and drag-end, replacing the output-port round-trip that placed the release a column away on a detach. Run the gesture probes at a non-identity view (scale=2, x=50, y=20) so a missing math_to_graph call causes failures rather than passing silently. Add a fifth probe: detach and release in place must reconnect. Update the stale comment that described the deleted two-click protocol. --- src/ui.cpp | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/ui.cpp b/src/ui.cpp index 7e441b0..0a7a19a 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3280,9 +3280,10 @@ void add_math_channel(App& app) { // The node editor: a compact graph. Each node is a box; inputs are the squares on its left edge, // the output the square on its right. Click a box to select it (its parameters appear in the form -// beside the canvas), click an output square and then an input square to wire them, drag a box to -// move it. The graph is a *list*, walked in order, so a node may only read ones above it -- which -// is why an input square offers only the nodes before it and a cycle cannot be drawn at all. +// beside the canvas), drag from an output square to an input square to wire them, drag a wired +// input off to detach it, drag a box to move it. The graph is a *list*, walked in order, so a node +// may only read ones above it -- which is why an input square offers only the nodes before it and a +// cycle cannot be drawn at all. constexpr double kNodeW = 132.0; constexpr double kNodeH = 46.0; constexpr double kPort = 10.0; @@ -3306,6 +3307,11 @@ struct MathDialog { double wire_y = 0.0; double pan_x0 = 0.0; double pan_y0 = 0.0; + /// `Wire`: the press point in canvas coordinates. The gesture's `dx`/`dy` is relative to this + /// point, so `press_cx + dx` is always the pointer, even on a detach where `wire_from` is a + /// different node from the one the pointer touched. + double press_cx = 0.0; + double press_cy = 0.0; /// Where the box being dragged *was* when the drag began, and which box it is. The gesture /// reports the total offset from its own start point, so adding it to a value that the previous /// update already moved is a runaway: the box accelerates away from the pointer and leaves the @@ -3620,6 +3626,8 @@ void math_canvas_drag_begin(GtkGestureDrag*, double x, double y, gpointer user) d->drag_node = -1; d->wire_from = -1; d->wire_detach = -1; + d->press_cx = x; + d->press_cy = y; // Precedence: a port, then a box, then the background. A port sticks out past the box edge, so // asking the box first would make the output square unreachable -- the same geometry that made // the old two-click protocol disarm itself. @@ -3686,8 +3694,7 @@ void math_canvas_drag(GtkGestureDrag*, double dx, double dy, gpointer user) { // The loose end, so the user can see what is being connected. double gx = 0.0; double gy = 0.0; - math_to_graph(d, d->wire_x * d->view_scale + d->view_x + dx, - d->wire_y * d->view_scale + d->view_y + dy, gx, gy); + math_to_graph(d, d->press_cx + dx, d->press_cy + dy, gx, gy); d->wire_x = gx; d->wire_y = gy; break; @@ -3709,15 +3716,13 @@ void math_canvas_drag_end(GtkGestureDrag*, double dx, double dy, gpointer user) d->drag_node = -1; return; } - // Where the release landed, recomputed from the gesture's own offset rather than from `wire_x`: - // `drag-end` carries the whole offset, and the update that would have moved `wire_x` to it is - // not guaranteed to have arrived. - double ox = 0.0; - double oy = 0.0; - math_output_pos(d->nodes[static_cast(d->wire_from)], ox, oy); + // Where the release landed: the press point plus the gesture's total offset is the pointer in + // every case, including a detach where `wire_from` is a node the press never touched. Using + // `wire_x` instead would be wrong when `drag-end` arrives without a preceding final `drag-update` + // (GTK makes no guarantee); using the output port would be wrong on a detach, where the source + // node may be a column away from the press point. int which = kPortOut; - const int target = math_port_at_canvas(d, ox * d->view_scale + d->view_x + dx, - oy * d->view_scale + d->view_y + dy, which); + 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(target)]; (which == 0 ? n.in_a : n.in_b) = d->wire_from; @@ -7149,7 +7154,13 @@ gboolean math_probe(gpointer user) { g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog")); // The editor's two graphs are source -> gain, so node 1 has one input and node 0 an output. if (g != nullptr && g->nodes.size() >= 2) { - math_view_reset(g); + // Set a non-identity view (scale=2, x=50, y=20) before the gesture probes so that a + // transform bug -- e.g. passing canvas coords straight to math_port_at() without + // math_to_graph() -- would produce wrong hit results. At the identity view canvas coords equal + // graph coords, making such a bug invisible. The trailing math_view_reset() below tidies up. + math_set_scale(g, 2.0); + g->view_x = 50.0; + g->view_y = 20.0; const auto canvas_of = [&](double gx, double gy, double& cx, double& cy) { cx = gx * g->view_scale + g->view_x; cy = gy * g->view_scale + g->view_y; @@ -7195,6 +7206,16 @@ gboolean math_probe(gpointer user) { std::fprintf(stderr, "[math-probe] detach: escape restores in_a=%d (want 0)\n", g->nodes[1].in_a); + // Detach and release in place: a wire detached from an input and released at the same canvas + // position must reconnect. With Finding 1 unfixed the release point is computed from the wrong + // origin (the source node's output port rather than the press point), so this reads in_a=-1. + g->nodes[1].in_a = 0; + math_canvas_drag_begin(nullptr, icx, icy, g); + math_canvas_drag(nullptr, 0.0, 0.0, g); + math_canvas_drag_end(nullptr, 0.0, 0.0, g); + std::fprintf(stderr, "[math-probe] detach: release in place reconnects in_a=%d (want 0)\n", + g->nodes[1].in_a); + // A drag that begins on the background pans and moves nothing. const double was_x = g->view_x; const double node_x = g->nodes[0].x;