Let a chain stage be switched off without taking it apart
Trying a stage out meant removing it and building it again from scratch, settings and all, to see what the picture looked like without it. A stage now carries an `enabled` flag and can simply be switched off: it keeps its place in the chain and everything it was set to. `enabledStages` is the one place that reads the flag, and renderChain, chainNeedsSources, chainTitle and chainSlug all go through it. A disabled stage is skipped whole — handed no image and drawing nothing from the shared rng — so switching one off renders byte for byte what the chain rendered before that stage was added, which is the only thing that makes the switch worth anything while comparing. Whichever stage runs first is the one handed the sources, so disabling a leading scanlines lets blur lead and the chain needs no images at all. The status line and the generated file name follow too: they name what was rendered, not what sits in the sidebar. A chain with every stage disabled throws rather than quietly rendering nothing. In the interface it is a check on the chain row: the row dims, the params heading says "(off)" and the settings stay editable — a stage being worked on is not the same as one being deleted. On the command line it is `--disable NAME`, which picks its stage by effect name exactly as the params do. A project writes `"enabled": false` only for a stage that is off, so existing project files are unchanged and an ordinary one stays uncluttered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,10 +73,20 @@ the whole of the cascade — an effect neither knows nor cares whether its input
|
|||||||
came from disk. All stages share one `Rng` and run strictly in sequence, so a
|
came from disk. All stages share one `Rng` and run strictly in sequence, so a
|
||||||
seed still reproduces the cascade byte for byte.
|
seed still reproduces the cascade byte for byte.
|
||||||
|
|
||||||
|
A stage carries a `ChainStage::enabled` flag, and `enabledStages`
|
||||||
|
(`src/core/chain.cpp`) is the one place that reads it: a disabled stage is
|
||||||
|
skipped whole — handed no image and drawing nothing from the shared rng — so
|
||||||
|
switching one off renders exactly what the chain rendered before it was added.
|
||||||
|
`renderChain`, `chainNeedsSources`, `chainTitle` and `chainSlug` all go through
|
||||||
|
it, which is why the status line and the file name never mention a stage that
|
||||||
|
did not run. A chain with every stage disabled throws rather than rendering
|
||||||
|
nothing.
|
||||||
|
|
||||||
The interface builds the chain in the sidebar ("Effect chain": add from the
|
The interface builds the chain in the sidebar ("Effect chain": add from the
|
||||||
menu, reorder, remove, one stage selected at a time with its params below), the
|
menu, reorder, remove, switch off with the check on the row, one stage selected
|
||||||
CLI builds it from repeated `-e`: `-e scanlines --strip-height 6 -e blur
|
at a time with its params below), the CLI builds it from repeated `-e`:
|
||||||
--radius 5`. Flags share one namespace across all effects, so `buildChain`
|
`-e scanlines --strip-height 6 -e blur --radius 5`, with `--disable NAME`
|
||||||
|
leaving a stage in place but skipping it. Flags share one namespace across all effects, so `buildChain`
|
||||||
(`src/cli/options.cpp`) hands each flag to the stage whose effect declares it —
|
(`src/cli/options.cpp`) hands each flag to the stage whose effect declares it —
|
||||||
two effects must not declare the same param key.
|
two effects must not declare the same param key.
|
||||||
|
|
||||||
@@ -94,7 +104,8 @@ pixels, so a project saved as A4 reopens as A4.
|
|||||||
### Projects are plain JSON
|
### Projects are plain JSON
|
||||||
|
|
||||||
`src/core/project.cpp` reads and writes the whole setup — source paths, the
|
`src/core/project.cpp` reads and writes the whole setup — source paths, the
|
||||||
chain with its params, canvas and seed — as a small JSON file, with its own
|
chain with its params (and its `"enabled": false`, written only for a stage
|
||||||
|
that is switched off), canvas and seed — as a small JSON file, with its own
|
||||||
minimal reader rather than a JSON dependency. Writing is driven by the effects'
|
minimal reader rather than a JSON dependency. Writing is driven by the effects'
|
||||||
`ParamSpec`s, not by whatever happens to sit in a stage's `ParamValues`, so a
|
`ParamSpec`s, not by whatever happens to sit in a stage's `ParamValues`, so a
|
||||||
key an effect does not declare is never written and never read back: an old
|
key an effect does not declare is never written and never read back: an old
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "cli/options.hpp"
|
#include "cli/options.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -89,6 +90,10 @@ Options parseOptions(int argc, char** argv) {
|
|||||||
std::to_string(kMinDpi) + "-" + std::to_string(kMaxDpi));
|
std::to_string(kMinDpi) + "-" + std::to_string(kMaxDpi));
|
||||||
}
|
}
|
||||||
options.dpi = static_cast<int>(dpi);
|
options.dpi = static_cast<int>(dpi);
|
||||||
|
} else if (arg == "--disable") {
|
||||||
|
const std::string name = requireValue(argc, argv, i, arg);
|
||||||
|
effectByName(name); // validate now, exactly as -e does
|
||||||
|
options.disabled.push_back(name);
|
||||||
} else if (arg == "--seed") {
|
} else if (arg == "--seed") {
|
||||||
const std::string value = requireValue(argc, argv, i, arg);
|
const std::string value = requireValue(argc, argv, i, arg);
|
||||||
long seed = 0;
|
long seed = 0;
|
||||||
@@ -176,6 +181,7 @@ std::string usage(const char* programName) {
|
|||||||
<< " --format NAME canvas format, overrides --size (see below)\n"
|
<< " --format NAME canvas format, overrides --size (see below)\n"
|
||||||
<< " --dpi N resolution of a print format, " << kMinDpi << "-" << kMaxDpi
|
<< " --dpi N resolution of a print format, " << kMinDpi << "-" << kMaxDpi
|
||||||
<< " (default: " << kDefaultDpi << ")\n"
|
<< " (default: " << kDefaultDpi << ")\n"
|
||||||
|
<< " --disable NAME keep this effect in the chain but skip it\n"
|
||||||
<< " --seed N random seed (default: random)\n"
|
<< " --seed N random seed (default: random)\n"
|
||||||
<< " --list-effects list available effects and exit\n"
|
<< " --list-effects list available effects and exit\n"
|
||||||
<< " -h, --help show this help and exit\n";
|
<< " -h, --help show this help and exit\n";
|
||||||
@@ -207,6 +213,8 @@ std::vector<ChainStage> buildChain(const Options& options) {
|
|||||||
ChainStage stage;
|
ChainStage stage;
|
||||||
stage.effect = effectByName(name);
|
stage.effect = effectByName(name);
|
||||||
stage.values = stage.effect->defaults();
|
stage.values = stage.effect->defaults();
|
||||||
|
stage.enabled = std::find(options.disabled.begin(), options.disabled.end(), name) ==
|
||||||
|
options.disabled.end();
|
||||||
for (const auto& spec : stage.effect->params()) {
|
for (const auto& spec : stage.effect->params()) {
|
||||||
const auto it = options.params.find(spec.key);
|
const auto it = options.params.find(spec.key);
|
||||||
if (it != options.params.end()) stage.values[spec.key] = it->second;
|
if (it != options.params.end()) stage.values[spec.key] = it->second;
|
||||||
|
|||||||
+6
-1
@@ -14,6 +14,10 @@ namespace effector {
|
|||||||
struct Options {
|
struct Options {
|
||||||
std::vector<std::string> images;
|
std::vector<std::string> images;
|
||||||
std::vector<std::string> effects; // one per cascade stage, in order
|
std::vector<std::string> effects; // one per cascade stage, in order
|
||||||
|
// The effects named by --disable: their stages stay in the chain, in
|
||||||
|
// place, but are skipped. Named rather than numbered, exactly as the
|
||||||
|
// params are, so two stages of one effect are disabled together.
|
||||||
|
std::vector<std::string> disabled;
|
||||||
std::string output; // empty -> launch the interface
|
std::string output; // empty -> launch the interface
|
||||||
int width = 1920;
|
int width = 1920;
|
||||||
int height = 1080;
|
int height = 1080;
|
||||||
@@ -33,7 +37,8 @@ std::string usage(const char* programName);
|
|||||||
|
|
||||||
// Turns the parsed names and flags into cascade stages: every flag goes to the
|
// Turns the parsed names and flags into cascade stages: every flag goes to the
|
||||||
// stage whose effect declares it, so --strip-height and --radius can be given
|
// stage whose effect declares it, so --strip-height and --radius can be given
|
||||||
// together for `-e scanlines -e blur`.
|
// together for `-e scanlines -e blur`. A stage named by --disable is built
|
||||||
|
// with all its settings and marked to be skipped.
|
||||||
std::vector<ChainStage> buildChain(const Options& options);
|
std::vector<ChainStage> buildChain(const Options& options);
|
||||||
|
|
||||||
} // namespace effector
|
} // namespace effector
|
||||||
|
|||||||
+27
-11
@@ -2,14 +2,31 @@
|
|||||||
|
|
||||||
namespace effector {
|
namespace effector {
|
||||||
|
|
||||||
|
std::vector<const ChainStage*> enabledStages(const std::vector<ChainStage>& stages) {
|
||||||
|
std::vector<const ChainStage*> running;
|
||||||
|
running.reserve(stages.size());
|
||||||
|
for (const auto& stage : stages) {
|
||||||
|
if (stage.enabled && stage.effect) running.push_back(&stage);
|
||||||
|
}
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
Image renderChain(const std::vector<ChainStage>& stages, const std::vector<Image>& sources,
|
Image renderChain(const std::vector<ChainStage>& stages, const std::vector<Image>& sources,
|
||||||
int width, int height, Rng& rng) {
|
int width, int height, Rng& rng) {
|
||||||
if (stages.empty()) throw EffectError("The effect chain is empty.");
|
if (stages.empty()) throw EffectError("The effect chain is empty.");
|
||||||
|
for (const auto& stage : stages) {
|
||||||
|
if (!stage.effect) throw EffectError("The effect chain has an empty stage.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A disabled stage never reaches the render at all: it is not handed an
|
||||||
|
// image and draws nothing from the shared rng, so switching one off gives
|
||||||
|
// back exactly the picture the chain made before it was added.
|
||||||
|
const auto running = enabledStages(stages);
|
||||||
|
if (running.empty()) throw EffectError("Every effect in the chain is disabled.");
|
||||||
|
|
||||||
Image image;
|
Image image;
|
||||||
for (std::size_t i = 0; i < stages.size(); ++i) {
|
for (std::size_t i = 0; i < running.size(); ++i) {
|
||||||
const auto& stage = stages[i];
|
const auto& stage = *running[i];
|
||||||
if (!stage.effect) throw EffectError("The effect chain has an empty stage.");
|
|
||||||
|
|
||||||
auto values = stage.effect->defaults();
|
auto values = stage.effect->defaults();
|
||||||
for (const auto& [key, value] : stage.values) values[key] = value;
|
for (const auto& [key, value] : stage.values) values[key] = value;
|
||||||
@@ -27,26 +44,25 @@ Image renderChain(const std::vector<ChainStage>& stages, const std::vector<Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool chainNeedsSources(const std::vector<ChainStage>& stages) {
|
bool chainNeedsSources(const std::vector<ChainStage>& stages) {
|
||||||
if (stages.empty() || !stages.front().effect) return false;
|
const auto running = enabledStages(stages);
|
||||||
return stages.front().effect->needsSources();
|
if (running.empty()) return false;
|
||||||
|
return running.front()->effect->needsSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string chainTitle(const std::vector<ChainStage>& stages) {
|
std::string chainTitle(const std::vector<ChainStage>& stages) {
|
||||||
std::string text;
|
std::string text;
|
||||||
for (const auto& stage : stages) {
|
for (const auto* stage : enabledStages(stages)) {
|
||||||
if (!stage.effect) continue;
|
|
||||||
if (!text.empty()) text += " → ";
|
if (!text.empty()) text += " → ";
|
||||||
text += stage.effect->title();
|
text += stage->effect->title();
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string chainSlug(const std::vector<ChainStage>& stages) {
|
std::string chainSlug(const std::vector<ChainStage>& stages) {
|
||||||
std::string text;
|
std::string text;
|
||||||
for (const auto& stage : stages) {
|
for (const auto* stage : enabledStages(stages)) {
|
||||||
if (!stage.effect) continue;
|
|
||||||
if (!text.empty()) text += "-";
|
if (!text.empty()) text += "-";
|
||||||
text += stage.effect->name();
|
text += stage->effect->name();
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-8
@@ -21,23 +21,34 @@ inline constexpr int kMaxCanvasSide = 20000;
|
|||||||
struct ChainStage {
|
struct ChainStage {
|
||||||
EffectPtr effect;
|
EffectPtr effect;
|
||||||
ParamValues values; // missing keys fall back to the effect's defaults
|
ParamValues values; // missing keys fall back to the effect's defaults
|
||||||
|
// A disabled stage is skipped whole, keeping its settings and its place in
|
||||||
|
// the chain, so a stage can be switched off and back on while trying
|
||||||
|
// things out without being taken apart and rebuilt.
|
||||||
|
bool enabled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Renders the stages in order. The first one sees the loaded sources; every
|
// The stages that actually run, in order, as pointers into `stages`. A
|
||||||
// later one sees the previous stage's output as its only source, so blur after
|
// disabled stage draws nothing from the rng and is handed no image, so a chain
|
||||||
// scanlines softens the scanlines rather than the photographs.
|
// renders exactly as it would if that stage had never been added -- this is
|
||||||
|
// the one place the rest of the code asks what "disabled" means.
|
||||||
|
std::vector<const ChainStage*> enabledStages(const std::vector<ChainStage>& stages);
|
||||||
|
|
||||||
|
// Renders the enabled stages in order. The first one sees the loaded sources;
|
||||||
|
// every later one sees the previous stage's output as its only source, so blur
|
||||||
|
// after scanlines softens the scanlines rather than the photographs.
|
||||||
//
|
//
|
||||||
// All stages share one Rng and run strictly in sequence, so a seed still
|
// All stages share one Rng and run strictly in sequence, so a seed still
|
||||||
// reproduces the cascade byte for byte.
|
// reproduces the cascade byte for byte. Throws when no stage is left to run.
|
||||||
Image renderChain(const std::vector<ChainStage>& stages, const std::vector<Image>& sources,
|
Image renderChain(const std::vector<ChainStage>& stages, const std::vector<Image>& sources,
|
||||||
int width, int height, Rng& rng);
|
int width, int height, Rng& rng);
|
||||||
|
|
||||||
// Whether the chain has to be given source images. Only the first stage ever
|
// Whether the chain has to be given source images. Only the first stage that
|
||||||
// sees them — every later one works on the stage before it — so this is simply
|
// runs ever sees them — every later one works on the stage before it — so this
|
||||||
// whether the first effect declares that it needs them.
|
// is simply whether that effect declares that it needs them.
|
||||||
bool chainNeedsSources(const std::vector<ChainStage>& stages);
|
bool chainNeedsSources(const std::vector<ChainStage>& stages);
|
||||||
|
|
||||||
// "Scanlines → Blur", for window titles and status lines.
|
// "Scanlines → Blur", for window titles and status lines. Both of these name
|
||||||
|
// what was rendered, so a disabled stage is left out of them.
|
||||||
std::string chainTitle(const std::vector<ChainStage>& stages);
|
std::string chainTitle(const std::vector<ChainStage>& stages);
|
||||||
|
|
||||||
// "scanlines-blur", for file names.
|
// "scanlines-blur", for file names.
|
||||||
|
|||||||
@@ -359,6 +359,10 @@ std::string projectToJson(const Project& project, const std::string& directory)
|
|||||||
if (!stage.effect) continue;
|
if (!stage.effect) continue;
|
||||||
out << (i == 0 ? "\n" : ",\n");
|
out << (i == 0 ? "\n" : ",\n");
|
||||||
out << " {\n \"effect\": " << quote(stage.effect->name()) << ",\n";
|
out << " {\n \"effect\": " << quote(stage.effect->name()) << ",\n";
|
||||||
|
// Only written when it is false: a stage is enabled unless the file
|
||||||
|
// says otherwise, so an ordinary project stays as uncluttered as it
|
||||||
|
// was before a stage could be switched off at all.
|
||||||
|
if (!stage.enabled) out << " \"enabled\": false,\n";
|
||||||
out << " \"params\": {";
|
out << " \"params\": {";
|
||||||
|
|
||||||
const auto& specs = stage.effect->params();
|
const auto& specs = stage.effect->params();
|
||||||
@@ -466,6 +470,10 @@ Project projectFromJson(const std::string& text, const std::string& directory) {
|
|||||||
ChainStage stage;
|
ChainStage stage;
|
||||||
stage.effect = effectByName(name->text); // throws on an unknown effect
|
stage.effect = effectByName(name->text); // throws on an unknown effect
|
||||||
stage.values = stage.effect->defaults();
|
stage.values = stage.effect->defaults();
|
||||||
|
if (const auto* enabled = item.member("enabled");
|
||||||
|
enabled != nullptr && enabled->kind == Json::Kind::Bool) {
|
||||||
|
stage.enabled = enabled->boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Driven by the specs, not by the file: a key the effect does not
|
// Driven by the specs, not by the file: a key the effect does not
|
||||||
// declare is dropped, and an old project simply keeps the defaults for
|
// declare is dropped, and an old project simply keeps the defaults for
|
||||||
|
|||||||
+41
-4
@@ -366,8 +366,10 @@ void EffectorWindow::buildParamWidgets() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto& stage = stages_[static_cast<std::size_t>(selectedStage_)];
|
const auto& stage = stages_[static_cast<std::size_t>(selectedStage_)];
|
||||||
|
// The settings of a stage that is switched off stay editable: it is still
|
||||||
|
// being worked on, it just is not in the picture for the moment.
|
||||||
paramHeading_.set_text(std::to_string(selectedStage_ + 1) + ". " + stage.effect->title() +
|
paramHeading_.set_text(std::to_string(selectedStage_ + 1) + ". " + stage.effect->title() +
|
||||||
" settings");
|
(stage.enabled ? " settings" : " settings (off)"));
|
||||||
|
|
||||||
int row = 0;
|
int row = 0;
|
||||||
for (const auto& spec : stage.effect->params()) {
|
for (const auto& spec : stage.effect->params()) {
|
||||||
@@ -913,6 +915,12 @@ void EffectorWindow::rebuildChainRows() {
|
|||||||
// removing it would leave the empty list blank.
|
// removing it would leave the empty list blank.
|
||||||
while (auto* row = chainList_.get_row_at_index(0)) chainList_.remove(*row);
|
while (auto* row = chainList_.get_row_at_index(0)) chainList_.remove(*row);
|
||||||
|
|
||||||
|
// The first stage that actually runs is the one handed the sources, which
|
||||||
|
// is not stage 1 once stage 1 is switched off.
|
||||||
|
const auto running = enabledStages(stages_);
|
||||||
|
const int firstRunning =
|
||||||
|
running.empty() ? -1 : static_cast<int>(running.front() - stages_.data());
|
||||||
|
|
||||||
for (std::size_t i = 0; i < stages_.size(); ++i) {
|
for (std::size_t i = 0; i < stages_.size(); ++i) {
|
||||||
const int index = static_cast<int>(i);
|
const int index = static_cast<int>(i);
|
||||||
auto* row = Gtk::make_managed<Gtk::ListBoxRow>();
|
auto* row = Gtk::make_managed<Gtk::ListBoxRow>();
|
||||||
@@ -922,13 +930,28 @@ void EffectorWindow::rebuildChainRows() {
|
|||||||
box->set_margin_start(8);
|
box->set_margin_start(8);
|
||||||
box->set_margin_end(4);
|
box->set_margin_end(4);
|
||||||
|
|
||||||
|
// The check is what switches a stage off: it keeps its settings and
|
||||||
|
// its place in the chain, and the render simply skips it.
|
||||||
|
auto* check = Gtk::make_managed<Gtk::CheckButton>();
|
||||||
|
check->set_active(stages_[i].enabled);
|
||||||
|
check->set_tooltip_text("Apply this effect");
|
||||||
|
check->set_valign(Gtk::Align::CENTER);
|
||||||
|
// Connected after set_active, so filling in the rebuilt rows does not
|
||||||
|
// look like the user reaching for the check.
|
||||||
|
check->signal_toggled().connect([this, index, check] {
|
||||||
|
setStageEnabled(index, check->get_active());
|
||||||
|
});
|
||||||
|
box->append(*check);
|
||||||
|
|
||||||
auto* label = Gtk::make_managed<Gtk::Label>(std::to_string(index + 1) + ". " +
|
auto* label = Gtk::make_managed<Gtk::Label>(std::to_string(index + 1) + ". " +
|
||||||
stages_[i].effect->title());
|
stages_[i].effect->title());
|
||||||
label->set_xalign(0.0F);
|
label->set_xalign(0.0F);
|
||||||
label->set_hexpand(true);
|
label->set_hexpand(true);
|
||||||
label->set_ellipsize(Pango::EllipsizeMode::END);
|
label->set_ellipsize(Pango::EllipsizeMode::END);
|
||||||
label->set_tooltip_text(index == 0 ? "Applied to the source images"
|
if (!stages_[i].enabled) label->add_css_class("dim-label");
|
||||||
: "Applied to the output of the stage above");
|
label->set_tooltip_text(!stages_[i].enabled ? "Skipped while it is switched off"
|
||||||
|
: index == firstRunning ? "Applied to the source images"
|
||||||
|
: "Applied to the output so far");
|
||||||
box->append(*label);
|
box->append(*label);
|
||||||
|
|
||||||
struct Action {
|
struct Action {
|
||||||
@@ -965,6 +988,19 @@ void EffectorWindow::rebuildChainRows() {
|
|||||||
updateSourcesVisible();
|
updateSourcesVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Switching a stage off leaves everything about it alone -- its settings, its
|
||||||
|
// place in the chain, the stage the params below are showing -- so the only
|
||||||
|
// visible change is the dimmed row and the picture the render comes back with.
|
||||||
|
void EffectorWindow::setStageEnabled(int index, bool enabled) {
|
||||||
|
if (index < 0 || static_cast<std::size_t>(index) >= stages_.size()) return;
|
||||||
|
auto& stage = stages_[static_cast<std::size_t>(index)];
|
||||||
|
if (stage.enabled == enabled) return;
|
||||||
|
stage.enabled = enabled;
|
||||||
|
rebuildChainRows(); // the row dims, and another stage may now lead the chain
|
||||||
|
if (index == selectedStage_) buildParamWidgets(); // the heading says "off"
|
||||||
|
requestRender();
|
||||||
|
}
|
||||||
|
|
||||||
void EffectorWindow::selectStage(int index) {
|
void EffectorWindow::selectStage(int index) {
|
||||||
if (auto* row = chainList_.get_row_at_index(index)) chainList_.select_row(*row);
|
if (auto* row = chainList_.get_row_at_index(index)) chainList_.select_row(*row);
|
||||||
}
|
}
|
||||||
@@ -1039,12 +1075,13 @@ void EffectorWindow::onRecreate() {
|
|||||||
// ever handed the sources, so an effect that does not want them (blur) renders
|
// ever handed the sources, so an effect that does not want them (blur) renders
|
||||||
// happily with none loaded.
|
// happily with none loaded.
|
||||||
bool EffectorWindow::canRender() const {
|
bool EffectorWindow::canRender() const {
|
||||||
if (stages_.empty()) return false;
|
if (enabledStages(stages_).empty()) return false;
|
||||||
return !sources_->empty() || !chainNeedsSources(stages_);
|
return !sources_->empty() || !chainNeedsSources(stages_);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string EffectorWindow::missingText() const {
|
std::string EffectorWindow::missingText() const {
|
||||||
if (stages_.empty()) return "Add an effect to start.";
|
if (stages_.empty()) return "Add an effect to start.";
|
||||||
|
if (enabledStages(stages_).empty()) return "Every effect is switched off.";
|
||||||
return "Add source images to start.";
|
return "Add source images to start.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ private:
|
|||||||
void appendStage(const EffectPtr& effect);
|
void appendStage(const EffectPtr& effect);
|
||||||
void removeStage(int index);
|
void removeStage(int index);
|
||||||
void moveStage(int index, int delta);
|
void moveStage(int index, int delta);
|
||||||
|
// The check on a chain row: the stage stays where it is, and is skipped.
|
||||||
|
void setStageEnabled(int index, bool enabled);
|
||||||
void rebuildChainRows();
|
void rebuildChainRows();
|
||||||
void selectStage(int index);
|
void selectStage(int index);
|
||||||
void onStageSelected();
|
void onStageSelected();
|
||||||
|
|||||||
@@ -889,6 +889,50 @@ int main() {
|
|||||||
scanlinesThrew = true;
|
scanlinesThrew = true;
|
||||||
}
|
}
|
||||||
check(scanlinesThrew, "scanlines still refuses to run without sources");
|
check(scanlinesThrew, "scanlines still refuses to run without sources");
|
||||||
|
|
||||||
|
// A stage switched off is skipped whole: it is handed no image and
|
||||||
|
// draws nothing from the shared rng, so the cascade comes out exactly
|
||||||
|
// as it would have without that stage in it at all.
|
||||||
|
ChainStage blurOff = blur;
|
||||||
|
blurOff.enabled = false;
|
||||||
|
ChainStage scanlinesOff = scanlines;
|
||||||
|
scanlinesOff.enabled = false;
|
||||||
|
|
||||||
|
Rng skipping(5);
|
||||||
|
const auto skipped = renderChain({scanlines, blurOff}, sources, 400, 300, skipping);
|
||||||
|
check(sameBytes(skipped, direct), "a disabled stage is skipped, its rng draws with it");
|
||||||
|
check(chainTitle({scanlines, blurOff}) == "Scanlines", "the title names only what ran");
|
||||||
|
check(chainSlug({scanlines, blurOff}) == "scanlines", "and so does the slug");
|
||||||
|
|
||||||
|
// Whoever leads the chain is handed the sources, and that is the first
|
||||||
|
// stage still switched on.
|
||||||
|
check(!chainNeedsSources({scanlinesOff, blur}),
|
||||||
|
"a disabled leading stage leaves the question to the stage behind it");
|
||||||
|
Rng leading(5);
|
||||||
|
const auto ledByBlur = renderChain({scanlinesOff, blur}, {}, 200, 150, leading);
|
||||||
|
check(sameBytes(ledByBlur, sourceless), "the stage behind it leads instead");
|
||||||
|
|
||||||
|
bool allOffThrew = false;
|
||||||
|
try {
|
||||||
|
Rng rng(1);
|
||||||
|
renderChain({scanlinesOff, blurOff}, sources, 100, 100, rng);
|
||||||
|
} catch (const EffectError&) {
|
||||||
|
allOffThrew = true;
|
||||||
|
}
|
||||||
|
check(allOffThrew, "a chain with every stage disabled throws");
|
||||||
|
|
||||||
|
// The command line switches a stage off by effect name, the same way
|
||||||
|
// the params pick out the stage they belong to.
|
||||||
|
const auto cli =
|
||||||
|
parsedOptions({"-e", "scanlines", "-e", "blur", "--disable", "blur", "--radius", "4"});
|
||||||
|
check(cli.has_value(), "--disable is accepted");
|
||||||
|
const auto cliStages = buildChain(*cli);
|
||||||
|
check(cliStages.size() == 2 && cliStages[0].enabled && !cliStages[1].enabled,
|
||||||
|
"--disable keeps the stage in the chain and switches it off");
|
||||||
|
check(paramInt(cliStages[1].values, "radius", 0) == 4,
|
||||||
|
"a switched-off stage still takes its flags");
|
||||||
|
check(!parsedOptions({"-e", "blur", "--disable", "nope"}).has_value(),
|
||||||
|
"--disable refuses an unknown effect");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "project\n";
|
std::cout << "project\n";
|
||||||
@@ -949,6 +993,21 @@ int main() {
|
|||||||
check(stripped.stages[0].values.find("radius") == stripped.stages[0].values.end(),
|
check(stripped.stages[0].values.find("radius") == stripped.stages[0].values.end(),
|
||||||
"a stage only keeps the params its effect declares");
|
"a stage only keeps the params its effect declares");
|
||||||
|
|
||||||
|
// A stage switched off keeps its place and its settings in the file and
|
||||||
|
// comes back switched off; nothing is written for the ordinary case.
|
||||||
|
Project toggled = project;
|
||||||
|
toggled.stages[1].enabled = false;
|
||||||
|
const auto offFile = dir / "disabled.json";
|
||||||
|
saveProject(offFile.string(), toggled);
|
||||||
|
const auto offBack = loadProject(offFile.string());
|
||||||
|
check(offBack.stages.size() == 2 && offBack.stages[0].enabled && !offBack.stages[1].enabled,
|
||||||
|
"a switched-off stage comes back switched off");
|
||||||
|
check(paramInt(offBack.stages[1].values, "radius", 0) == 3,
|
||||||
|
"and with the settings it was switched off with");
|
||||||
|
check(projectToJson(toggled).find("\"enabled\"") != std::string::npos &&
|
||||||
|
projectToJson(project).find("\"enabled\"") == std::string::npos,
|
||||||
|
"only a switched-off stage is written out as one");
|
||||||
|
|
||||||
int refusals = 0;
|
int refusals = 0;
|
||||||
for (const char* bad : {"{\"chain\": [{\"effect\": \"nope\"}]}", "{\"chain\": []}",
|
for (const char* bad : {"{\"chain\": [{\"effect\": \"nope\"}]}", "{\"chain\": []}",
|
||||||
"{\"chain\": [{\"effect\": \"blur\"},]}", "not json at all"}) {
|
"{\"chain\": [{\"effect\": \"blur\"},]}", "not json at all"}) {
|
||||||
|
|||||||
Reference in New Issue
Block a user