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:
+27
-11
@@ -2,14 +2,31 @@
|
||||
|
||||
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,
|
||||
int width, int height, Rng& rng) {
|
||||
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;
|
||||
for (std::size_t i = 0; i < stages.size(); ++i) {
|
||||
const auto& stage = stages[i];
|
||||
if (!stage.effect) throw EffectError("The effect chain has an empty stage.");
|
||||
for (std::size_t i = 0; i < running.size(); ++i) {
|
||||
const auto& stage = *running[i];
|
||||
|
||||
auto values = stage.effect->defaults();
|
||||
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) {
|
||||
if (stages.empty() || !stages.front().effect) return false;
|
||||
return stages.front().effect->needsSources();
|
||||
const auto running = enabledStages(stages);
|
||||
if (running.empty()) return false;
|
||||
return running.front()->effect->needsSources();
|
||||
}
|
||||
|
||||
std::string chainTitle(const std::vector<ChainStage>& stages) {
|
||||
std::string text;
|
||||
for (const auto& stage : stages) {
|
||||
if (!stage.effect) continue;
|
||||
for (const auto* stage : enabledStages(stages)) {
|
||||
if (!text.empty()) text += " → ";
|
||||
text += stage.effect->title();
|
||||
text += stage->effect->title();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string chainSlug(const std::vector<ChainStage>& stages) {
|
||||
std::string text;
|
||||
for (const auto& stage : stages) {
|
||||
if (!stage.effect) continue;
|
||||
for (const auto* stage : enabledStages(stages)) {
|
||||
if (!text.empty()) text += "-";
|
||||
text += stage.effect->name();
|
||||
text += stage->effect->name();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
+19
-8
@@ -21,23 +21,34 @@ inline constexpr int kMaxCanvasSide = 20000;
|
||||
struct ChainStage {
|
||||
EffectPtr effect;
|
||||
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
|
||||
// later one sees the previous stage's output as its only source, so blur after
|
||||
// scanlines softens the scanlines rather than the photographs.
|
||||
// The stages that actually run, in order, as pointers into `stages`. A
|
||||
// disabled stage draws nothing from the rng and is handed no image, so a chain
|
||||
// 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
|
||||
// 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,
|
||||
int width, int height, Rng& rng);
|
||||
|
||||
// Whether the chain has to be given source images. Only the first stage ever
|
||||
// sees them — every later one works on the stage before it — so this is simply
|
||||
// whether the first effect declares that it needs them.
|
||||
// Whether the chain has to be given source images. Only the first stage that
|
||||
// runs ever sees them — every later one works on the stage before it — so this
|
||||
// is simply whether that effect declares that it needs them.
|
||||
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);
|
||||
|
||||
// "scanlines-blur", for file names.
|
||||
|
||||
@@ -359,6 +359,10 @@ std::string projectToJson(const Project& project, const std::string& directory)
|
||||
if (!stage.effect) continue;
|
||||
out << (i == 0 ? "\n" : ",\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\": {";
|
||||
|
||||
const auto& specs = stage.effect->params();
|
||||
@@ -466,6 +470,10 @@ Project projectFromJson(const std::string& text, const std::string& directory) {
|
||||
ChainStage stage;
|
||||
stage.effect = effectByName(name->text); // throws on an unknown effect
|
||||
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
|
||||
// declare is dropped, and an old project simply keeps the defaults for
|
||||
|
||||
Reference in New Issue
Block a user