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.
This commit is contained in:
Martino Ferrari committed 2026-10-07 01:59:27 +02:00
1 parent a7702acb63
commit 8dd6b3db12
1 file changed
+35 -14
+35 -14
View File
@@ -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<std::size_t>(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<std::size_t>(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;