feat: a wire can be picked and deleted, so a graph can be rewired

\`math_wire_at()\` samples the same cubic the draw lays down -- 24 segments,
well under a pixel of chord error at the scales this canvas allows -- and
\`Delete\` removes the picked one. It deliberately does not fall through to
deleting the selected node: a key whose target depends on an invisible
selection is how a graph gets destroyed by accident.
This commit is contained in:
Martino Ferrari committed 2026-10-07 02:07:08 +02:00
1 parent 8dd6b3db12
commit 4c726adb54
1 file changed
+115 -2
+115 -2
View File
@@ -3288,6 +3288,14 @@ constexpr double kNodeW = 132.0;
constexpr double kNodeH = 46.0;
constexpr double kPort = 10.0;
/// A wire, named by the node that *consumes* it and which of its inputs. The consumer is the
/// identity because that is where a wire is stored: `in_a`/`in_b` on the reader. A producer may
/// feed several inputs and so cannot name one.
struct MathWireRef {
int node = -1;
int which = 0;
};
enum class MathDrag { None, Node, Wire, Pan };
struct MathDialog {
@@ -3319,6 +3327,8 @@ struct MathDialog {
double grab_x = 0.0;
double grab_y = 0.0;
int drag_node = -1;
/// The wire `Delete` would remove, or `{-1, 0}`.
MathWireRef selected_wire{-1, 0};
/// The view: canvas pixels per graph unit, and where graph (0,0) is drawn. Applied once to the
/// cairo context by `math_canvas_draw()`; every hit test runs the inverse (`math_to_graph()`)
/// rather than comparing a pointer position against a graph one, and sizes its tolerance in
@@ -3509,6 +3519,52 @@ int math_node_at_canvas(const MathDialog* d, double cx, double cy) {
return math_node_at(d, gx, gy);
}
/// The wire under a graph position, or `{-1, 0}`. The wire is the cubic the draw lays down, so it
/// is *sampled* rather than solved: 24 segments is well under a pixel of chord error at the scales
/// this canvas allows, and a closed form for "nearest point on a cubic" is a quartic root find for
/// no gain. Topmost-first, matching every other hit test here.
MathWireRef math_wire_at(const MathDialog* d, double gx, double gy, double tol) {
constexpr int kSamples = 24;
for (int i = static_cast<int>(d->nodes.size()) - 1; i >= 0; --i) {
const MathNode& n = d->nodes[static_cast<std::size_t>(i)];
const int ins[2] = {n.in_a, n.in_b};
for (int k = 0; k < math_op_arity(n.op); ++k) {
const int from = ins[k];
if (from < 0 || from >= static_cast<int>(i)) continue;
double ax = 0.0;
double ay = 0.0;
double bx = 0.0;
double by = 0.0;
math_output_pos(d->nodes[static_cast<std::size_t>(from)], ax, ay);
math_input_pos(n, k, bx, by);
const double c1x = ax + 40.0;
const double c2x = bx - 40.0;
for (int s = 0; s <= kSamples; ++s) {
const double t = static_cast<double>(s) / kSamples;
const double u = 1.0 - t;
const double px = u * u * u * ax + 3 * u * u * t * c1x + 3 * u * t * t * c2x + t * t * t * bx;
const double py = u * u * u * ay + 3 * u * u * t * ay + 3 * u * t * t * by + t * t * t * by;
if (std::fabs(gx - px) <= tol && std::fabs(gy - py) <= tol) {
return MathWireRef{i, k};
}
}
}
}
return MathWireRef{-1, 0};
}
/// The pick radius for a wire, in screen pixels. Smaller than a port's, so a wire that passes
/// behind a port never wins the press -- the port is tested first anyway, but a user aiming at a
/// port should not be able to miss it onto a wire either.
constexpr double kWirePick = 6.0;
MathWireRef math_wire_at_canvas(const MathDialog* d, double cx, double cy) {
double gx = 0.0;
double gy = 0.0;
math_to_graph(d, cx, cy, gx, gy);
return math_wire_at(d, gx, gy, kWirePick / d->view_scale);
}
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);
@@ -3532,8 +3588,10 @@ void math_canvas_draw(GtkDrawingArea*, cairo_t* cr, int width, int height, gpoin
double by = 0.0;
math_output_pos(d->nodes[static_cast<std::size_t>(from)], ax, ay);
math_input_pos(n, k, bx, by);
cairo_set_source_rgb(cr, 0.55, 0.62, 0.72);
cairo_set_line_width(cr, 1.5);
const bool picked = d->selected_wire.node == static_cast<int>(i) &&
d->selected_wire.which == k;
cairo_set_source_rgb(cr, picked ? 1.0 : 0.55, picked ? 0.69 : 0.62, picked ? 0.13 : 0.72);
cairo_set_line_width(cr, picked ? 3.0 : 1.5);
cairo_move_to(cr, ax, ay);
cairo_curve_to(cr, ax + 40.0, ay, bx - 40.0, by, bx, by);
cairo_stroke(cr);
@@ -3607,16 +3665,27 @@ void math_canvas_pressed(GtkGestureClick*, int, double x, double y, gpointer use
int which = kPortOut;
const int port = math_port_at_canvas(d, x, y, which);
if (port >= 0) {
d->selected_wire = MathWireRef{-1, 0};
d->selected = port;
math_dialog_sync(d);
return;
}
const MathWireRef wire = math_wire_at_canvas(d, x, y);
if (wire.node >= 0) {
d->selected_wire = wire;
d->selected = wire.node;
math_dialog_sync(d);
gtk_widget_queue_draw(d->canvas);
return;
}
const int node = math_node_at_canvas(d, x, y);
if (node >= 0) {
d->selected_wire = MathWireRef{-1, 0};
d->selected = node;
math_dialog_sync(d);
return;
}
d->selected_wire = MathWireRef{-1, 0};
gtk_widget_queue_draw(d->canvas);
}
@@ -3785,6 +3854,17 @@ gboolean math_canvas_key(GtkEventControllerKey*, guint keyval, guint, GdkModifie
gtk_widget_queue_draw(d->canvas);
return TRUE;
}
if (keyval == GDK_KEY_Delete || keyval == GDK_KEY_BackSpace) {
if (d->selected_wire.node >= 0 &&
d->selected_wire.node < static_cast<int>(d->nodes.size())) {
MathNode& n = d->nodes[static_cast<std::size_t>(d->selected_wire.node)];
(d->selected_wire.which == 0 ? n.in_a : n.in_b) = -1;
d->selected_wire = MathWireRef{-1, 0};
math_dialog_sync(d);
gtk_widget_queue_draw(d->canvas);
return TRUE;
}
}
return FALSE;
}
@@ -7230,6 +7310,39 @@ gboolean math_probe(gpointer user) {
math_view_reset(g);
}
}
if (a->math_dialog != nullptr) {
auto* p = static_cast<MathDialog*>(
g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog"));
if (p != nullptr && p->nodes.size() >= 2) {
math_set_scale(p, 2.0);
p->view_x = 50.0;
p->view_y = 20.0;
p->nodes[1].in_a = 0;
// Halfway along the wire in *graph* units: the bezier's own midpoint, which for a cubic
// with these control points is the average of the two ends plus three eighths of the
// tread -- so sampling the curve is the only honest way to find a point on it.
double ax = 0.0;
double ay = 0.0;
double bx = 0.0;
double by = 0.0;
math_output_pos(p->nodes[0], ax, ay);
math_input_pos(p->nodes[1], 0, bx, by);
const double t = 0.5;
const double u = 1.0 - t;
const double mx = u * u * u * ax + 3 * u * u * t * (ax + 40.0) +
3 * u * t * t * (bx - 40.0) + t * t * t * bx;
const double my = u * u * u * ay + 3 * u * u * t * ay + 3 * u * t * t * by + t * t * t * by;
const MathWireRef hit = math_wire_at_canvas(p, mx * p->view_scale + p->view_x,
my * p->view_scale + p->view_y);
p->selected_wire = hit;
math_canvas_key(nullptr, GDK_KEY_Delete, 0, static_cast<GdkModifierType>(0), p);
std::fprintf(stderr,
"[math-probe] wire: pick node=%d which=%d -> in_a=%d (want 1 0 -1)\n",
hit.node, hit.which, p->nodes[1].in_a);
p->nodes[1].in_a = 0;
p->selected_wire = MathWireRef{-1, 0};
}
}
std::fflush(stderr);
return G_SOURCE_REMOVE;
}