Files
effector/CLAUDE.md
T
SiandClaude Opus 5 a82fba7886 Save and open projects as JSON
A project is the whole setup as a small text file: the source paths, the
effect chain with its params, the canvas and the seed. Reading and writing
are driven by the effects' ParamSpecs rather than by whatever sits in a
stage's values, so a new effect is saveable without touching this code, a
key an effect does not declare is never written or read back, and an old
project keeps the defaults for params that did not exist when it was
saved. Paths under the project's own folder are written relative, so a
folder of images and their project can be moved whole.

src/core/project.cpp carries its own small JSON reader instead of a
dependency, and names the line when a hand-edited file is wrong.

The interface gets a header-bar menu: New, Open, Save, Save As, with
Ctrl+N/O/S. The title shows the project name. Opening a project replaces
the sources, discarding a decode still in flight rather than mixing it
into the new project.

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

138 lines
6.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 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. Values travel as `ParamValues` (a
`map<string, variant<long,double,bool,string>>`), read back with
`paramInt`/`paramDouble`/`paramBool`/`paramChoice`.
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` + 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. 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.