Let several text stages share one named look

A picture with a title, a caption and a credit meant typing the same
font, size and colour into every text stage, and changing the look meant
typing them all again. A project now holds named styles: a stage follows
one, and editing it moves every stage that follows it at once.

The front-ends learn nothing effect-specific. A ParamSpec can be marked
.styled(), saying a style carries it rather than the stage, and
Effect::styleKind() names the pool it comes from -- so styleSpecs(kind)
is to a style what params() is to a stage, and a second text-ish effect
would slot into the styles that already exist. The look is what a style
carries; the words and the placement stay on the stage, which is what
lets two stages share a typography in two different corners.

stageValues() resolves one in a single place, and a stage keeps a full
set of values of its own beside the style, so switching it to Custom,
dropping the style, or opening the project in a build that does not know
it renders exactly the same picture. A style moves stages together; it
never holds the only copy of anything.

The command line builds no styles and passes none, so it renders what it
always did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-19 21:55:55 +02:00
co-authored by Claude Opus 5
parent eb417bb41a
commit 3aade6f361
17 changed files with 890 additions and 131 deletions
+29 -3
View File
@@ -53,6 +53,17 @@ travel as `ParamValues` (a
`map<string, variant<long,double,bool,string>>`), read back with
`paramInt`/`paramDouble`/`paramBool`/`paramChoice`.
A param can also be marked `.styled()`, which says a *named style* carries it
rather than the stage it sits on: the font and the colour of a text stage, but
never its words or its placement. The pool a style comes from is named by
`Effect::styleKind()` (empty by default — the effect takes no style; `"text"`
for the text effect), and effects answering the same kind share one pool, so a
second text-ish effect would slot into the styles that already exist. The
front-ends know nothing more than that: the interface builds the style row and
the project file writes a style's params from `styleSpecs(kind)`
(`src/core/style.cpp`), which is to a style exactly what `params()` is to a
stage.
Whether an effect works from the loaded photographs is part of the same
contract: `Effect::needsSources()` (default true, false for blur). Only the
first stage ever sees the sources, so `chainNeedsSources` (`src/core/chain.cpp`)
@@ -82,6 +93,18 @@ 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.
### A style is a look several stages share
`ChainStage::style` names a style, and `stageValues` (`src/core/chain.cpp`) is
the one place that resolves one: an effect's defaults, then the stage's own
values, then the style's values for the params it carries. A stage always keeps
a full set of values of its own beside the style, so switching it to "Custom",
dropping the style, or opening the project in a build that does not know the
style renders exactly the same picture — a style moves stages together, it
never holds the only copy of anything. The interface writes an edited param to
both the style and the stage for that reason. `renderChain` takes the pool as a
trailing argument; the command line builds no styles and passes none.
The interface builds the chain in the sidebar ("Effect chain": add from the
menu, reorder, remove, switch off with the check on the row, one stage selected
at a time with its params below), the CLI builds it from repeated `-e`:
@@ -104,12 +127,15 @@ pixels, so a project saved as A4 reopens as A4.
### Projects are plain JSON
`src/core/project.cpp` reads and writes the whole setup — source paths, the
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
named styles, the chain with its params (and its `"enabled": false` and
`"style"`, each written only when there is something to say), canvas and seed — as a small JSON file, with its own
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
key an effect does not declare is never written and never read back: an old
project keeps the defaults for params that did not exist when it was saved.
project keeps the defaults for params that did not exist when it was saved. The
same leniency covers styles: one of a kind this build does not know, one with
no name or a repeated one, and a stage naming a style the file does not hold
are each dropped on their own rather than refusing the project.
Source paths under the project's own folder are written relative, so a folder
can be moved or shared whole.