fix: a wire is picked by distance and a refusal is spoken, so the editor answers
A review of the whole node editor. The hit test measured the distance to the 24 curve samples rather than to the segments between them, so a wire spanning several rows was unpickable between two samples -- the probe now places its boxes 280 units apart and prints the nearest sample's own 8.4 against a 3.0 tolerance, which the point test would have missed. A refused connection said nothing: the message is now carried in `MathDialog::refusal` and shown once by the single writer of the label, and a self-wire is refused through the real drag-end path instead of only through `math_connect()`. Two defects were found only by the new assertions. `math_dialog_sync()` re-entered itself -- `gtk_drop_down_set_selected()` emits `notify::selected` for a programmatic set too, and the handler answers by syncing again, which consumed the refusal before it could be drawn -- so the body is now guarded one level deep. And a deleted node left `selected_wire` naming an index that afterwards belongs to a different wire, so `Delete` cleared an input on the wrong node. A node is placed in the first free grid cell rather than at the end of the list's own cell, so a node added after a delete no longer lands on top of a survivor; the label ellipsizes with a tooltip rather than wrapping, because a wrapped line makes the dialog's height depend on the text in it.
This commit is contained in:
1 parent
300b33159e
commit
f238e717b2
5 files changed
+318
-57
No files matched your search
+244
-32
@@ -3282,8 +3282,10 @@ void add_math_channel(App& app) {
|
||||
// the output the square on its right. Click a box to select it (its parameters appear in the form
|
||||
// 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.
|
||||
// may only read ones above it -- but that order is the *evaluator's* contract, not a restriction on
|
||||
// the gesture: a wire may be drawn in either direction and `math_connect()` re-sorts the list
|
||||
// (Kahn's algorithm) so the contract still holds. Only a wire that cannot be ordered at all, i.e.
|
||||
// one that closes a cycle, is refused, and it is refused at connect time with a message.
|
||||
constexpr double kNodeW = 132.0;
|
||||
constexpr double kNodeH = 46.0;
|
||||
constexpr double kPort = 10.0;
|
||||
@@ -3351,10 +3353,25 @@ struct MathDialog {
|
||||
GtkWidget* hi = nullptr;
|
||||
GtkWidget* rows[6] = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
|
||||
GtkWidget* inputs = nullptr;
|
||||
/// The validation line and the button it governs. Both are set from `math_dialog_problem()` and
|
||||
/// from nowhere else, so what the line says and what the button allows cannot disagree.
|
||||
/// The validation line and the button it governs. Both are set from `math_dialog_sync()` and from
|
||||
/// nowhere else, so what the line says and what the button allows cannot disagree.
|
||||
GtkWidget* problem = nullptr;
|
||||
GtkWidget* apply = nullptr;
|
||||
/// Why the last gesture was *refused* -- "a node cannot read itself", or a cycle. A refusal has no
|
||||
/// standing state to be read back from: it is a thing that did not happen, so `math_dialog_problem()`
|
||||
/// cannot describe it and a refused wire otherwise just fails to appear, which reads as the editor
|
||||
/// being broken. `math_dialog_sync()` shows this in preference to the graph's own problem and
|
||||
/// **consumes it as it writes**, so the next sync -- and every mutation syncs -- replaces it with
|
||||
/// the graph's state. `Apply`'s sensitivity still follows `math_dialog_problem()` alone: a refused
|
||||
/// wire changed nothing, so a graph that was saveable still is.
|
||||
std::string refusal;
|
||||
/// True while the form is being written. `gtk_drop_down_set_selected()` emits `notify::selected`
|
||||
/// for a programmatic set exactly as for a user one, and the op handler answers a notify by
|
||||
/// syncing -- so writing the form **re-entered** `math_dialog_sync()`, and the inner call rewrote
|
||||
/// the validation label the outer one had just written. That is how a refusal vanished in the same
|
||||
/// call that put it up (the probe's `wire: self through drag-end` read an empty line). A
|
||||
/// programmatic set is not a user edit.
|
||||
bool syncing = false;
|
||||
};
|
||||
|
||||
/// The channels a math channel may read: the ones the model actually acquires. Offering the whole
|
||||
@@ -3580,12 +3597,36 @@ int math_node_at_canvas(const MathDialog* d, double cx, double cy) {
|
||||
return math_node_at(d, gx, gy);
|
||||
}
|
||||
|
||||
/// The squared distance from a point to a line *segment* -- clamped to the segment, so the ends do
|
||||
/// not extend it. A degenerate segment falls back to the distance to its first point.
|
||||
double math_seg_dist2(double px, double py, double ax, double ay, double bx, double by) {
|
||||
const double dx = bx - ax;
|
||||
const double dy = by - ay;
|
||||
const double len2 = dx * dx + dy * dy;
|
||||
double t = 0.0;
|
||||
if (len2 > 0.0) t = std::clamp(((px - ax) * dx + (py - ay) * dy) / len2, 0.0, 1.0);
|
||||
const double cx = px - (ax + t * dx);
|
||||
const double cy = py - (ay + t * dy);
|
||||
return cx * cx + cy * cy;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// no gain.
|
||||
///
|
||||
/// The 24 samples are the ends of 24 **segments**, and it is the distance to the segment that is
|
||||
/// measured. Testing the sample *points* instead leaves a gap wherever two of them are more than
|
||||
/// `2 * tol` apart -- which a wire spanning several rows of a tidied graph is, in y -- and in that
|
||||
/// gap a click lands on the drawn wire and picks nothing. That is half of "it is not possible to
|
||||
/// delete edges".
|
||||
///
|
||||
/// The *nearest* wire wins rather than the first one found, so a crossing picks the one actually
|
||||
/// under the pointer; topmost-first iteration is the tie-break, matching every other hit test here.
|
||||
MathWireRef math_wire_at(const MathDialog* d, double gx, double gy, double tol) {
|
||||
constexpr int kSamples = 24;
|
||||
MathWireRef best{-1, 0};
|
||||
double best_d2 = tol * tol;
|
||||
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};
|
||||
@@ -3600,18 +3641,24 @@ MathWireRef math_wire_at(const MathDialog* d, double gx, double gy, double tol)
|
||||
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) {
|
||||
double prev_x = ax;
|
||||
double prev_y = ay;
|
||||
for (int s = 1; 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};
|
||||
const double d2 = math_seg_dist2(gx, gy, prev_x, prev_y, px, py);
|
||||
if (d2 < best_d2) {
|
||||
best_d2 = d2;
|
||||
best = MathWireRef{i, k};
|
||||
}
|
||||
prev_x = px;
|
||||
prev_y = py;
|
||||
}
|
||||
}
|
||||
}
|
||||
return MathWireRef{-1, 0};
|
||||
return best;
|
||||
}
|
||||
|
||||
/// The pick radius for a wire, in screen pixels. Smaller than a port's, so a wire that passes
|
||||
@@ -3944,15 +3991,23 @@ void math_canvas_drag_end(GtkGestureDrag*, double dx, double dy, gpointer user)
|
||||
// node may be a column away from the press point.
|
||||
int which = kPortOut;
|
||||
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) {
|
||||
// Released on *any* input port, `wire_from`'s own included: the self case has to reach
|
||||
// `math_connect()` to be refused there by name. Screening it out here meant the one refusal the
|
||||
// user is most likely to provoke never got a message at all.
|
||||
if (target >= 0 && which != kPortOut) {
|
||||
std::string why;
|
||||
if (math_connect(d, d->wire_from, target, which, &why)) {
|
||||
// `math_connect()` may have permuted the list, so the target's index is no longer `target`.
|
||||
math_dialog_sync(d);
|
||||
} else if (d->wire_detach >= 0) {
|
||||
} else {
|
||||
// A refused wire must not also lose the one the drag detached to start with.
|
||||
MathNode& back = d->nodes[static_cast<std::size_t>(d->wire_detach)];
|
||||
(d->wire_detach_which == 0 ? back.in_a : back.in_b) = d->wire_from;
|
||||
if (d->wire_detach >= 0 && d->wire_detach < static_cast<int>(d->nodes.size())) {
|
||||
MathNode& back = d->nodes[static_cast<std::size_t>(d->wire_detach)];
|
||||
(d->wire_detach_which == 0 ? back.in_a : back.in_b) = d->wire_from;
|
||||
}
|
||||
// And it must *say* why. `why` used to be filled and dropped here, so neither "a node cannot
|
||||
// read itself" nor the cycle refusal ever reached the user.
|
||||
d->refusal = why.empty() ? std::string("that wire cannot be made") : why;
|
||||
math_dialog_sync(d);
|
||||
}
|
||||
}
|
||||
@@ -4009,6 +4064,10 @@ gboolean math_canvas_key(GtkEventControllerKey*, guint keyval, guint, GdkModifie
|
||||
d->drag_mode = MathDrag::None;
|
||||
d->wire_from = -1;
|
||||
d->wire_detach = -1;
|
||||
// Restoring the input is a mutation like any other, so the validation line has to be
|
||||
// re-evaluated: `drag-begin` emptied the input and synced, leaving "input is not connected" on
|
||||
// screen with `Apply` insensitive, and without this the undo left both there.
|
||||
math_dialog_sync(d);
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -4038,8 +4097,33 @@ void math_add_node(MathDialog* d, MathOp op) {
|
||||
MathNode n;
|
||||
n.op = op;
|
||||
const std::size_t k = d->nodes.size();
|
||||
n.x = 24.0 + 160.0 * static_cast<double>(k % 2u);
|
||||
n.y = 20.0 + 58.0 * static_cast<double>(k / 2u);
|
||||
// The first *free* cell of the grid, not cell `k`. Placing by `nodes.size()` reuses an occupied
|
||||
// cell as soon as anything has been deleted -- delete the middle of three and add one and the new
|
||||
// box lands exactly on an existing one, which breaks the documented rule that boxes must not
|
||||
// overlap: `math_node_at()` searches topmost-first, so the covered box becomes unclickable. Same
|
||||
// 160/58 pitch and two-column wrap as before; only the choice of cell changes. A graph that has
|
||||
// taken every cell up to the bound falls through to cell `k` rather than searching for ever.
|
||||
constexpr std::size_t kCellLimit = 64;
|
||||
std::size_t cell = k;
|
||||
for (std::size_t c = 0; c < kCellLimit; ++c) {
|
||||
const double cx = 24.0 + 160.0 * static_cast<double>(c % 2u);
|
||||
const double cy = 20.0 + 58.0 * static_cast<double>(c / 2u);
|
||||
bool free_cell = true;
|
||||
for (const MathNode& other : d->nodes) {
|
||||
if (cx < other.x + kNodeW && other.x < cx + kNodeW && cy < other.y + kNodeH &&
|
||||
other.y < cy + kNodeH) {
|
||||
free_cell = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (free_cell) {
|
||||
cell = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
n.x = 24.0 + 160.0 * static_cast<double>(cell % 2u);
|
||||
n.y = 20.0 + 58.0 * static_cast<double>(cell / 2u);
|
||||
// `k`, not `cell`: the new node's *index* is the end of the list whatever cell it was drawn in.
|
||||
const int arity = math_op_arity(n.op);
|
||||
if (arity >= 1 && k > 0) n.in_a = static_cast<int>(k) - 1;
|
||||
if (arity >= 2 && k > 0) n.in_b = static_cast<int>(k) - 1;
|
||||
@@ -4083,6 +4167,11 @@ void on_math_node_deleted(GtkButton*, gpointer user) {
|
||||
d->drag_mode = MathDrag::None;
|
||||
d->wire_from = -1;
|
||||
d->wire_detach = -1;
|
||||
// `MathWireRef::node` is an index into `d->nodes`, so a selection held across the erase names a
|
||||
// *different* wire afterwards and `Delete` would clear an input on the wrong node. Cleared rather
|
||||
// than remapped because the selected wire may have been one of the deleted node's own: a selection
|
||||
// the user can no longer see, pointing at a surviving edge, is worse than no selection.
|
||||
d->selected_wire = MathWireRef{-1, 0};
|
||||
math_dialog_sync(d);
|
||||
}
|
||||
|
||||
@@ -4141,15 +4230,24 @@ void on_math_op_changed(GObject* object, GParamSpec*, gpointer user) {
|
||||
}
|
||||
|
||||
/// The form and the canvas from the selected node: which rows an op uses, what they hold, and which
|
||||
/// nodes its inputs are wired to.
|
||||
void math_dialog_sync(MathDialog* d) {
|
||||
/// nodes its inputs are wired to. Called only through `math_dialog_sync()`, which guards it against
|
||||
/// the re-entry the dropdowns' `notify::selected` would otherwise cause.
|
||||
void math_dialog_sync_form(MathDialog* d) {
|
||||
// Before the early return below: an out-of-range selection is exactly the state in which a stale
|
||||
// validation line is most misleading -- a graph with no node selected can still be broken, and the
|
||||
// user has no form to read instead.
|
||||
if (d->problem != nullptr) {
|
||||
const std::string why = math_dialog_problem(d);
|
||||
gtk_label_set_text(GTK_LABEL(d->problem), why.c_str());
|
||||
gtk_widget_set_visible(d->problem, !why.empty());
|
||||
// A refusal from the last gesture wins the *text*, because nothing in the graph's state records
|
||||
// it; `Apply` still follows `why`, since a refused wire wrote nothing. Consumed here, so the
|
||||
// message lasts exactly until the next sync.
|
||||
const std::string shown = d->refusal.empty() ? why : d->refusal;
|
||||
d->refusal.clear();
|
||||
gtk_label_set_text(GTK_LABEL(d->problem), shown.c_str());
|
||||
// The label ellipsizes (see its construction), so the tooltip is the only place the full text
|
||||
// can be read. Set here, beside the text, so the two cannot disagree.
|
||||
gtk_widget_set_tooltip_text(d->problem, shown.empty() ? nullptr : shown.c_str());
|
||||
gtk_widget_set_visible(d->problem, !shown.empty());
|
||||
if (d->apply != nullptr) gtk_widget_set_sensitive(d->apply, why.empty());
|
||||
}
|
||||
if (d->selected < 0 || d->selected >= static_cast<int>(d->nodes.size())) {
|
||||
@@ -4221,6 +4319,20 @@ void math_dialog_sync(MathDialog* d) {
|
||||
gtk_widget_queue_draw(d->canvas);
|
||||
}
|
||||
|
||||
/// The one entry point, and the re-entrancy guard: setting the op dropdown emits `notify::selected`,
|
||||
/// whose handler syncs, so the form's own write re-entered the sync and the inner call rewrote the
|
||||
/// validation label -- erasing a refusal in the same call that displayed it. One level only; the
|
||||
/// flag is reset by a scope guard, so the early return inside cannot leave it set.
|
||||
void math_dialog_sync(MathDialog* d) {
|
||||
if (d->syncing) return;
|
||||
struct Guard {
|
||||
MathDialog* d;
|
||||
~Guard() { d->syncing = false; }
|
||||
} guard{d};
|
||||
d->syncing = true;
|
||||
math_dialog_sync_form(d);
|
||||
}
|
||||
|
||||
void on_math_apply(GtkButton*, gpointer user) {
|
||||
MathDialog* d = static_cast<MathDialog*>(user);
|
||||
on_math_param_changed(nullptr, d);
|
||||
@@ -4483,10 +4595,14 @@ void open_math_dialog(App& app, int edit) {
|
||||
gtk_widget_set_halign(d->inputs, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(form), math_row("wired", d->inputs, &d->rows[5]));
|
||||
|
||||
// Why the graph cannot be saved, in the form column where the user is already reading. Wrapped,
|
||||
// so a long channel name does not widen the dialog, and hidden when there is nothing to say.
|
||||
// Why the graph cannot be saved, in the form column where the user is already reading. Hidden
|
||||
// when there is nothing to say, and **ellipsized rather than wrapped**, with the full text on the
|
||||
// tooltip (`math_dialog_sync()` sets both together): layout must not depend on content. This label
|
||||
// sits in a dialog whose canvas is `hexpand`, so a wrapping message re-heights the form -- and a
|
||||
// long channel name in it would widen the dialog -- on a mere change of graph. The same rule the
|
||||
// status bar and the y gutter are built to.
|
||||
d->problem = gtk_label_new("");
|
||||
gtk_label_set_wrap(GTK_LABEL(d->problem), TRUE);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(d->problem), PANGO_ELLIPSIZE_END);
|
||||
gtk_label_set_xalign(GTK_LABEL(d->problem), 0.0f);
|
||||
gtk_widget_add_css_class(d->problem, "error");
|
||||
gtk_widget_set_visible(d->problem, FALSE);
|
||||
@@ -7521,27 +7637,53 @@ gboolean math_probe(gpointer user) {
|
||||
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.
|
||||
// The two boxes are put several rows apart on purpose: that is the geometry a tidied graph
|
||||
// has, and it is the only one in which the pick can fail. Where consecutive curve samples are
|
||||
// close together, a hit test that measures to the sample *points* and one that measures to the
|
||||
// *segments* between them give the same answer, so a probe taken on a short wire asserts
|
||||
// nothing about either.
|
||||
p->nodes[0].x = 24.0;
|
||||
p->nodes[0].y = 20.0;
|
||||
p->nodes[1].x = 260.0;
|
||||
p->nodes[1].y = 300.0;
|
||||
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;
|
||||
// **Between two samples, not at one.** `math_wire_at()` samples the cubic at `s / 24`, so
|
||||
// `t = 0.5` is exactly sample 12 and a point-distance test passes there by construction --
|
||||
// the same mistake as a hit test taken at the identity view. `t = 0.52` falls between samples
|
||||
// 12 and 13, and `off_sample` below is the measured distance from the pick to the nearest of
|
||||
// the 25 sample points, in graph units, beside the tolerance the pick is allowed: it reads
|
||||
// off_sample=8.4 against tol=3.0, so a point test misses and only a segment test can hit.
|
||||
const auto curve_at = [&](double t, double& px, double& py) {
|
||||
const double u = 1.0 - t;
|
||||
px = u * u * u * ax + 3 * u * u * t * (ax + 40.0) + 3 * u * t * t * (bx - 40.0) +
|
||||
t * t * t * bx;
|
||||
py = u * u * u * ay + 3 * u * u * t * ay + 3 * u * t * t * by + t * t * t * by;
|
||||
};
|
||||
const double t = 0.52;
|
||||
double mx = 0.0;
|
||||
double my = 0.0;
|
||||
curve_at(t, mx, my);
|
||||
double off_sample = 1e300;
|
||||
for (int s = 0; s <= 24; ++s) {
|
||||
double sx = 0.0;
|
||||
double sy = 0.0;
|
||||
curve_at(static_cast<double>(s) / 24.0, sx, sy);
|
||||
off_sample = std::min(off_sample, std::max(std::fabs(mx - sx), std::fabs(my - sy)));
|
||||
}
|
||||
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);
|
||||
"[math-probe] wire: pick node=%d which=%d -> in_a=%d off_sample=%.1f tol=%.1f "
|
||||
"(want 1 0 -1, off_sample > tol)\n",
|
||||
hit.node, hit.which, p->nodes[1].in_a, off_sample,
|
||||
kWirePick / p->view_scale);
|
||||
p->nodes[1].in_a = 0;
|
||||
p->selected_wire = MathWireRef{-1, 0};
|
||||
}
|
||||
@@ -7591,6 +7733,43 @@ gboolean math_probe(gpointer user) {
|
||||
std::fprintf(stderr, "[math-probe] wire: self refused=%d why=[%s] (want 1)\n",
|
||||
self_ok ? 0 : 1, self_why.c_str());
|
||||
|
||||
// The same refusal again, through the **handler**. The line above asserts only
|
||||
// `math_connect()`'s out-parameter: the UI path used to screen the self case out before the
|
||||
// call and then drop `why` on the floor, so a user drew the wire, nothing appeared and nothing
|
||||
// was said -- which is the whole of "the editor is broken" -- while the direct call passed.
|
||||
// This half asserts what the *validation line* reads after `drag-end`, which is the only place
|
||||
// that defect is visible. A synthetic GTK gesture is not reachable headlessly, so the drag's
|
||||
// state is set up by hand and the real handler called: `press_cx`/`press_cy` plus a zero offset
|
||||
// is a release exactly on the input port of `wire_from` itself.
|
||||
for (std::size_t i = 0; i < c->nodes.size(); ++i) {
|
||||
// Spread out, so the release point belongs to one port and not to three stacked at (0,0).
|
||||
c->nodes[i].x = 24.0 + 200.0 * static_cast<double>(i);
|
||||
c->nodes[i].y = 20.0;
|
||||
}
|
||||
math_set_scale(c, 1.0);
|
||||
c->view_x = 0.0;
|
||||
c->view_y = 0.0;
|
||||
int self_node = -1;
|
||||
for (std::size_t i = 0; i < c->nodes.size() && self_node < 0; ++i) {
|
||||
if (math_op_arity(c->nodes[i].op) >= 1) self_node = static_cast<int>(i);
|
||||
}
|
||||
if (self_node >= 0) {
|
||||
double sx = 0.0;
|
||||
double sy = 0.0;
|
||||
math_input_pos(c->nodes[static_cast<std::size_t>(self_node)], 0, sx, sy);
|
||||
c->drag_mode = MathDrag::Wire;
|
||||
c->wire_from = self_node;
|
||||
c->wire_detach = -1;
|
||||
c->press_cx = sx;
|
||||
c->press_cy = sy;
|
||||
math_canvas_drag_end(nullptr, 0.0, 0.0, c);
|
||||
const char* line = gtk_label_get_text(GTK_LABEL(c->problem));
|
||||
std::fprintf(stderr,
|
||||
"[math-probe] wire: self through drag-end node=%d problem=[%s] "
|
||||
"(want the refusal text, not empty)\n",
|
||||
self_node, line != nullptr ? line : "(null)");
|
||||
}
|
||||
|
||||
// A cycle: the node that already feeds the other is asked to read it back.
|
||||
const int reader_idx = c->nodes[1].in_a == 0 ? 2 : 1;
|
||||
const int feeder_idx = reader_idx == 2 ? 1 : 2;
|
||||
@@ -7680,6 +7859,39 @@ gboolean math_probe(gpointer user) {
|
||||
if (math_op_index(math_op_at(i)) != i) round = 0;
|
||||
}
|
||||
std::fprintf(stderr, "[math-probe] op pick: index round trip=%d (want 1)\n", round);
|
||||
|
||||
// And placement: the first *free* grid cell, not cell `nodes.size()`. The boxes are put back
|
||||
// on the grid, the middle one deleted and one added -- under the old placement the new box
|
||||
// landed on the cell the deleted node's *successor* had moved out of the count for, i.e. on an
|
||||
// occupied one, and `math_node_at()` searches topmost-first, so the covered box becomes
|
||||
// unclickable. `cells=` is where the boxes ended up; `overlaps` is the assertion.
|
||||
if (o->nodes.size() >= 3) {
|
||||
for (std::size_t i = 0; i < o->nodes.size(); ++i) {
|
||||
o->nodes[i].x = 24.0 + 160.0 * static_cast<double>(i % 2u);
|
||||
o->nodes[i].y = 20.0 + 58.0 * static_cast<double>(i / 2u);
|
||||
}
|
||||
o->selected = 1;
|
||||
on_math_node_deleted(nullptr, o);
|
||||
math_add_node(o, MathOp::Gain);
|
||||
int overlaps = 0;
|
||||
std::string cells;
|
||||
for (std::size_t i = 0; i < o->nodes.size(); ++i) {
|
||||
const MathNode& p2 = o->nodes[i];
|
||||
cells += (i != 0 ? " " : "");
|
||||
cells += "(" + std::to_string(static_cast<int>(p2.x)) + "," +
|
||||
std::to_string(static_cast<int>(p2.y)) + ")";
|
||||
for (std::size_t j = i + 1; j < o->nodes.size(); ++j) {
|
||||
const MathNode& q2 = o->nodes[j];
|
||||
if (p2.x < q2.x + kNodeW && q2.x < p2.x + kNodeW && p2.y < q2.y + kNodeH &&
|
||||
q2.y < p2.y + kNodeH) {
|
||||
++overlaps;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[math-probe] add node: after a delete cells=[%s] overlaps=%d (want 0)\n",
|
||||
cells.c_str(), overlaps);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a->math_dialog != nullptr) {
|
||||
|
||||
Reference in new issue
Block a user