Projects opened or saved go into GTK's shared recent list, signed with the effector application name, and the project menu lists the newest ten of them. The GUI tests point XDG_DATA_HOME at a temporary folder so they leave the user's own list alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
212 lines
11 KiB
Markdown
212 lines
11 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 core 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`, `tiles`, `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.
|
|
|
|
The interface has its own binary, `./build/tests/effector_gui_tests`
|
|
(`tests/gui_tests.cpp`, same `check()` style), which opens the real window and
|
|
drives it by widget names and tooltips (`chain-list`, `add-effect`,
|
|
`sources`, "Move up", ...). It needs a display: ctest runs it under `xvfb-run`
|
|
when that is installed and otherwise reports it skipped (exit 77). To run it by
|
|
hand, use the Xvfb invocation below.
|
|
|
|
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`.
|
|
|
|
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`)
|
|
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.
|
|
|
|
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.
|
|
|
|
### 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 by dragging a row or with its arrows, 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`:
|
|
`-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 —
|
|
two effects must not declare the same param key.
|
|
|
|
### The canvas is a format or plain pixels
|
|
|
|
`src/core/canvas.cpp` holds the one table of standard sizes both front-ends
|
|
offer: web formats are a fixed pixel size, print formats a sheet in millimetres
|
|
that becomes pixels at a dpi. Adding a format is a line in `canvasFormats()` —
|
|
the dropdown, `--format` and `--help` all walk that table. The pixels stay the
|
|
truth everywhere (`renderChain` never hears about formats); a format is the
|
|
*presentation* of a canvas, carried alongside the pixels in `Options` and
|
|
`Project` and empty for a custom size. Where a format is named it decides the
|
|
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
|
|
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. 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.
|
|
|
|
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. Open Recent lists the projects opened or saved here, kept in
|
|
GTK's shared `RecentManager` list and told apart from other programs' files by
|
|
the `effector` application name on each entry; the GUI tests point
|
|
`XDG_DATA_HOME` at a temporary folder so they never touch the user's list. 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**. `effector_gui` (static) adds
|
|
`src/cli/` and `src/gui/` and links gtkmm, and the `effector` executable is
|
|
`src/main.cpp` on top of it; the GUI tests link `effector_gui` too. 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.
|