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:
1 parent
8dd6b3db12
commit
4c726adb54
1 file changed
+115
-2
+115
-2
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user