diff --git a/src/ui.cpp b/src/ui.cpp index 0a7a19a..6757ed8 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -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(d->nodes.size()) - 1; i >= 0; --i) { + const MathNode& n = d->nodes[static_cast(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(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(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(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(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(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(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(d->nodes.size())) { + MathNode& n = d->nodes[static_cast(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( + 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(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; }