feat: a signal switched off switches its dependent math channels off too

A derived channel cannot be computed from a signal nobody acquires: `Source` resolves to a
*channel*, and a disabled signal has none -- so a channel that reads one silently computed
nothing, which is what "all math signals depending on it do not compute anymore" looked like
from outside.

Rather than leave it enabled and blank, the channels that read the signal follow it off:
`disable_dependent_math_channels()` walks the graphs to a **fixpoint**, because one math
channel may name another, and the switch goes through the same path the pane's own toggle
uses -- `MathChannel::enabled` then `apply_math_channels()`, the single writer -- so the
pane's switches follow. Their traces leave the plots with the signal's, and the notice says
both halves.

The channels that do *not* read it are untouched, which is the assertion's other half:

    [enable-all-probe] switched 'sine' off: traces_left=0 (want 0)
        math_off=[math x2,math3] math_on=[math lp]
        notice=''sine' is off: 1 trace left the plots; math off: math x2 and 1 more'

`math x2` and `math3` both source `sine`; `math lp` sources `noise` and stays on. The probe's
switch step now runs late enough that a math probe armed behind it has installed its graphs,
which is why the earlier reading was empty -- `app.math` had nothing in it yet, not
"no dependants".

Re-enabling the signal does not switch them back on, for the same reason it does not restore
the traces: that is the user's decision, and the rule is written down in
invariants-math-channels.md.
This commit is contained in:
Martino Ferrari committed 2026-10-12 22:25:15 +02:00
1 parent 5eb20f3cdb
commit b02419dee3
6 files changed
+92 -9

No files matched your search

+15 -5
View File
@@ -27,13 +27,23 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
app->model->set_signal_enabled(index, on);
if (!on) {
// A trace of a signal nobody acquires draws nothing, and holds the slot a live one could take.
const int gone = remove_signal_from_plots(*app, name);
if (gone > 0) {
int gone = remove_signal_from_plots(*app, name);
// ... and the derived channels that read it cannot be computed at all, so they follow it off, and their
// traces leave with them.
const std::vector<std::string> off = disable_dependent_math_channels(*app, name);
for (const std::string& channel : off) gone += remove_signal_from_plots(*app, channel);
if (!off.empty()) apply_math_channels(*app); // the single writer: the pane's switches follow it
if (gone > 0 || !off.empty()) {
app->relayout_pending = true;
rebuild_signal_panels(*app); // the chips of the signals that are still plotted
set_notice(*app, "'" + name + "' is off: " + std::to_string(gone) +
(gone == 1 ? " trace left the plots" : " traces left the plots"),
4);
std::string what = "'" + name + "' is off: " + std::to_string(gone) +
(gone == 1 ? " trace left the plots" : " traces left the plots");
if (!off.empty()) {
// The channel names already read as what they are ("math x2"), so the sentence does not prefix them.
what += "; math off: " + off.front() +
(off.size() == 1 ? "" : " and " + std::to_string(off.size() - 1) + " more");
}
set_notice(*app, what, 5);
}
}
// The enable state is part of the session (a signal can be on only as a math channel's source).