Files
effector/CLAUDE.md
T
SiandClaude Opus 5 1d0343b90a Add a text effect
Writes a line or several over its input in a chosen family, weight, size
and colour, optionally on a box filled with a colour or with a picture
from disk. Like the blur and the overlay it needs no sources of its own,
so it can lead a chain; given sources it fills the canvas from a randomly
chosen one first.

Two param types rather than effect-specific branches in the front-ends:
Text, free-form text the user types, and Color, a colour written
"#rrggbb". Both travel as strings, so the interface gets an entry and a
colour button, the command line gets a flag (a colour that does not parse
is refused there rather than drawn as black), and a project keeps them
across a round trip.

The glyphs come from Pango, which draws through Cairo without a display,
so the core stays headless — but they are the one thing here that is not
byte-identical across machines: a machine without the chosen family
renders the substitute its fontconfig picks. Everything the effect is
handed, the rng included, is used exactly as every other effect uses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:31:24 +02:00

153 lines
7.5 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build and test
```bash
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure
```
Everything is warning-clean under `-Wall -Wextra -Wpedantic`; keep it that way.
The test suite is a single hand-rolled binary (no framework), so there is no
per-test filter: run `./build/tests/effector_tests` directly to see the section
headings (`rng`, `registry`, `scanlines`, `blur`, `chain`, `project`, `image
io`, `batch loading`) and the `ok`/`FAIL` lines. To iterate on one area, comment
out the other blocks in `tests/core_tests.cpp` or add a new
`std::cout << "section\n"` block — checks are plain
`check(condition, "description")` calls and `main()` returns the failure count.
Headless render (no display needed, the fastest end-to-end smoke test):
```bash
build/effector -o /tmp/out.png -s 800x600 --strip-height 6 --seed 1 a.jpg b.png
```
### Running the GUI from this shell
GTK picks Wayland from an inherited `WAYLAND_DISPLAY` and will not draw into an
Xvfb display, which silently yields black screenshots. Under Xvfb always use:
```bash
env -u WAYLAND_DISPLAY GDK_BACKEND=x11 DISPLAY=:79 build/effector ...
```
## Architecture
### The plug-in contract is declarative
`Effect` (`src/core/effect.hpp`) exposes `params()` returning `std::vector<ParamSpec>`
(`src/core/params.hpp`). Those specs are the *only* thing the GUI and CLI know
about an effect: `src/gui/window.cpp` builds SpinButtons/Switches/DropDowns/file
choosers from them and `src/cli/options.cpp` generates flags from them
(`strip_height` → `--strip-height`). Never add effect-specific branches to the
GUI or the parser — add a `ParamSpec` instead. A `Path` param names a file on
disk; like a source path it is written relative to the project folder. A `Text`
param is free-form text the user types, and a `Color` param is a colour written
`#rrggbb` (`parseColor`/`formatColor`/`paramColor` in `src/core/params.hpp`),
shown as a colour button in the interface. Values
travel as `ParamValues` (a
`map<string, variant<long,double,bool,string>>`), read back with
`paramInt`/`paramDouble`/`paramBool`/`paramChoice`.
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`)
just asks the first stage — a chain led by blur renders with no images loaded at
all, and both front-ends gate on that rather than on "are there any images".
Adding an effect: new `src/effects/<name>.{hpp,cpp}` subclassing `Effect`, one
line in the registry list in `src/core/registry.cpp`, one line in the
`effector_core` sources in `CMakeLists.txt`. Nothing else — it shows up in the
CLI, in the chain menu and as a cascade stage on its own.
### Effects cascade
Effects are never applied on their own: `renderChain` (`src/core/chain.cpp`)
runs a `std::vector<ChainStage>` in order, handing stage 0 the loaded sources
and every later stage the previous stage's output as its *only* source. That is
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
seed still reproduces the cascade byte for byte.
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
CLI builds it from repeated `-e`: `-e scanlines --strip-height 6 -e blur
--radius 5`. Flags share one namespace across all effects, so `buildChain`
(`src/cli/options.cpp`) hands each flag to the stage whose effect declares it —
two effects must not declare the same param key.
### Projects are plain JSON
`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
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.
Source paths under the project's own folder are written relative, so a folder
can be moved or shared whole.
The interface opens and saves projects from the header-bar menu (New, Open,
Save, Save As, with the usual Ctrl+N/O/S accelerators); the window title shows
the project name. Opening one replaces the sources, so a decode still in flight
is discarded rather than mixed into the new project (`dropLoadedImages_`).
### Two link targets, deliberately
`effector_core` (static) holds image IO, params, effects, rng and links only
`gdk-pixbuf-2.0`, `pangocairo` (the text effect's glyphs; Pango and Cairo need
no display) + Threads — **no gtkmm, no display**. The `effector` executable
adds `src/main.cpp`, `src/cli/`, `src/gui/` and links gtkmm. Keep GTK out of
`src/core/` and `src/effects/`; the tests link the core alone and rely on this.
### Determinism is a contract
`Rng` (`src/core/rng.hpp`) is a hand-written xoshiro256\*\* rather than
`<random>`, because libstdc++ distributions are not portable across
implementations. Same sources + settings + seed must produce byte-identical
output on any machine — the tests assert this. The one thing outside this is the
text effect's glyphs, which are whatever shapes the machine's fonts and
fontconfig give: everything it is handed, the rng included, is still used the
same way. Effects must draw *every* random
value from the `Rng&` they are handed, and must not depend on thread scheduling:
the scanlines effect first draws the whole strip plan serially from the rng,
then copies pixels in parallel.
### Threading model
- `parallelFor` (`src/core/parallel.hpp`) splits fixed contiguous chunks across
hardware threads — used for pixel work, where items cost the same.
- `loadImages` (`src/core/image.cpp`) deliberately does *not* use `parallelFor`:
files vary wildly in size, so workers pull the next index from a shared
`std::atomic` cursor. It reports through `LoadProgress::onFile` (called from
decoding threads: must be thread-safe and non-blocking) and aborts when
`LoadProgress::cancel` is set, in which case the partial result is discarded
by the caller, not used.
- The GUI never blocks the main thread. Both decoding and rendering run on a
`std::thread` and signal back with a `Glib::Dispatcher`
(`loadProgress_`/`loadFinished_`/`renderDone_` in `src/gui/window.hpp`); the
handlers are the only code that touches widgets. Renders are coalesced through
a debounce timer (`requestRender()` → `startRender()`), and the progress
window is created on a 250 ms delay so short batches never flash a dialog.
`loadWindow_` is rebuilt from scratch each time, so `loadBar_`/`loadFileLabel_`
are raw pointers that must be nulled in `closeLoadWindow()`.
- Sources are held as an immutable `shared_ptr<const SourceList>` and edited
copy-on-write via `mutateSources()`, so a render in flight can never race with
an add/remove.
### Image buffer
`Image` is tightly packed 8-bit RGB with stride exactly `width * 3` — effects
rely on this for `memcpy`, and GDK uploads the buffer as is. Decoding applies
EXIF orientation. `Image::load` throws; `loadImages` collects per-file
`LoadFailure`s instead.
## Reference
`python-reference/` is the original Python prototype, kept only for comparison.
Do not modify it or import from it.