# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
`wonfy-tools` is a single Rust crate that ships three faces of the same code (Cargo edition 2024):
- **Library** (`rlib`) — `ImageStitcherBuilder` / `ImageStitcher`, the public API.
- **CLI** (`wonfy-tools-cli`) — `src/main.rs`, gated behind the `cli` feature.
- **WASM/npm package** (`cdylib`, `@wonfsy/wonfy-tools`) — `#[wasm_bindgen]` exports in `src/tool/stitcher/wasm_exports.rs`, compiled only for `wasm32`.
The only tool so far is the **image stitcher**: it reassembles overlapping image strips (e.g. scrolling screenshots) by finding where consecutive images overlap and blitting them into one composite. Since 0.2.0 the overlap search runs on the GPU via `wgpu`.
## Commands
The binary requires the `cli` feature (rust-analyzer is already configured with it in `.vscode/settings.json`):
```sh
cargo run --features cli -- -f ./0.png ./1.png -d vertical # run the CLI
cargo run --features cli -- --help # list all flags
cargo build --features cli # build the CLI
cargo build # build lib only (no cli deps)
```
Tests:
```sh
cargo test # unit + e2e (see GPU note below)
cargo test --test stitch_e2e # only the end-to-end suite
cargo test vertical_ordered_recovers_known_offsets # a single test by name
```
WASM package (output goes to `/pkg`, which is gitignored) is produced with `wasm-pack build`.
**GPU note:** most tests need a real GPU adapter (Vulkan/DX12/Metal). Tests are written to *skip gracefully* — they match `StitchError::NoAdapter`, print a "skipping" line, and return early rather than fail. If a test silently passes without asserting, check whether it bailed for lack of an adapter.
## Architecture
### Flow: builder → validate → stitch
`ImageStitcherBuilder` (`builder.rs`) collects options and calls `validate()` (in `stitcher.rs`) at build time. Validation enforces min image count, window fits within height minus crop, and — importantly — that `255 * window * 3 * width` can't overflow the `u32` GPU score accumulator (`StitchError::ScoreOverflow`). `stitch()` is `async`; `stitch_blocking()` is a native-only `pollster::block_on` wrapper (absent on wasm, where the caller `await`s).
Two stitch strategies in `stitcher.rs`:
- **`Ordered`** — fold images left-to-right, stitching each onto the growing composite.
- **`Unordered`** — score every pair both directions (`flipped`), normalize by overlap width, greedily merge the best pair, repeat. `O(n²)` pairs cached in a `scores` map keyed by synthetic image ids.
`stitch_pair()` handles the geometry: crop the seam edges, compute where each image lands, allocate the output, and `blit()` raw RGBA rows. `Position` shifts are propagated back so callers learn where each original strip ended up in the final image.
### "Match space" — the key abstraction
Scoring is written to only ever match **along rows, seam at the bottom**. Direction is normalized into this space so the GPU code stays direction-agnostic:
- `CheckDirection::Horizontal` sets `transpose = true`, swapping width/height (`match_dims` / `match_space_dims`) so column-matching becomes row-matching.
- `Sideways*` directions additionally sweep a horizontal `offset` range (allowing x-misalignment between strips); `Vertical`/`Horizontal` use offset 0.
Whenever you touch scoring, think in match-space (transposed) coordinates, then map back to real x/y at the boundary in `search_pair`.
### GPU pipeline (`src/tool/stitcher/gpu/`)
`GpuContext` (`gpu/mod.rs`) is a process-wide singleton (`OnceLock` native, thread-local `Rc` on wasm) holding the device, queue, and two compute pipelines. Each stitch pair runs two shaders:
1. **`preprocess.wgsl`** — converts an RGBA8 region into per-pixel `u32` "match values" (0..255): `mode 0` = mean of RGB, `mode 1` (`MatchMode::Edges`, the default) = Sobel L1 edge magnitude. Transparent pixels (alpha 0 — unfilled composite padding) get the `0x100` **invalid flag**. Also does the transpose into match space.
2. **`sad.wgsl`** — scores every `(anchor row, offset)` candidate as the Sum of Absolute Differences over the `window` rows, plus a margin penalty for non-overlapping pixels. **Lower score = better match.** Invalid-flagged pixels cost the max (255) inside the overlap so empty padding can't fake a match, but are free in the margins. Each 16×16 workgroup reduces its candidates to one `(score, index)` pair; the CPU picks the global best.
`score.rs` orchestrates this and enforces two limits worth knowing:
- **Slabbing** — anchors are processed in slabs bounded by `SLAB_BYTE_BUDGET` (64 MiB) and `RESULT_CANDIDATE_BUDGET`, so large images don't allocate unbounded GPU buffers.
- **`MAX_DISPATCH_THREADS`** (`65_535 * 16`) caps any single image dimension (wgpu's max workgroup count × workgroup size).
The CPU reference implementation of the scoring math lives in `score.rs`'s `#[cfg(test)]` module (`cpu_match_values`) and is used to verify the shaders — keep the two in sync when changing scoring semantics (the shader comments spell out the intended algorithm).
### Errors
All stitcher errors are the `StitchError` enum in `src/error.rs` with `Display` impls; the wasm layer stringifies them. `UnknownError` + the `unknown_error_expected!` macro drive the `FromStr` parsers for the CLI/JS enums (`CheckDirection`, `MatchMode`, `Order`) and produce the "expected one of…" messages.
### Utilities (`src/util/`)
`dhash` (perceptual hash, used by the wasm `rgba_bytes_to_image` export), `image/` (`ImageCrop`, `EncodeFormat`, encoding helpers), `string/parsing` (`parse_first_number`, used by the CLI to sort files numerically). These are lower-level and rarely need changes for stitcher work.