Commit Graph

29 Commits

Author SHA1 Message Date
storybored379 f5f0837aab
Select › Color Range: the dialog, Select presets, eyedroppers, Localized Color Clusters, Invert (#80)
* Select › Color Range: Select presets, samples, localized clusters, invert

select.colorRange only did one sampled colour. Add the dialog's Select
presets (Reds … Magentas, Highlights / Midtones / Shadows, Out of
Gamut), several samples (colors / eyedropper points), the Subtract from
Sample eyedropper (subtractPoints), Localized Color Clusters with Range,
tonal Range, and Invert. Bad params are rejected without touching the
selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Select › Color Range… opens a real dialog

The menu item ran select.colorRange immediately with the foreground
colour. It now opens a Color Range dialog: Select (Sampled Colors, the
hue families, Highlights / Midtones / Shadows, Out Of Gamut), Fuzziness,
Localized Color Clusters with Range, the tonal range sliders, the three
eyedroppers (on the preview; Shift adds, Alt subtracts), a grayscale
selection / image preview, Invert, OK and Cancel. Controls a mode
doesn't use are disabled. The preview and OK run the same engine
command, so the GUI, CLI and MCP select identically; OK is one history
step and Cancel leaves the selection untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Dialogs shrink back when their body gets shorter

The OK / Cancel row was laid out right-to-left with Align::Center, so it
filled the height left over from the previous frame's size and the
dialog could only ever grow. In the Color Range dialog, switching from
Midtones (two extra sliders) to Out Of Gamut or Sampled Colors left an
empty band around the buttons. Aligning the row to the top keeps it
exactly as tall as the buttons, so the dialog fits its content again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:10:43 -04:00
Brandon Thomas d3b540234c
Large images: adapter texture limits, banded compositing/export, proxy thumbnails, history byte budget (#71)
* Compose: render in place and in bands; area-reduced and proxy thumbnails

render_tiled wrote every tile into a Vec and then copied them into the output:
two full-size float copies (6.3 GB for a 14000² document). Tiles are now
written straight into the output band by band.

New streaming APIs so large documents never need a full-size float
composite: render_bands (sink per band of ~8 MP), render_reduced (banded
area average, bit-identical to the old flatten-then-average thumbnail) and
flatten_to_surface (Flatten Image, Merge Visible, Copy Merged and the other
flattened-surface users).

thumbnail() reduces a downsampled proxy (moved from ui-egui to
compose::proxy, now also scaling spot channels) for documents over 16 MP
when that is faithful (no layer effects or vector masks), so navigator,
channel and histogram thumbnails of a 196 MP document cost milliseconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU canvas: request the adapter's real limits; banded CPU fallback; navigator thumbnail

egui-wgpu asks for an 8192 px texture limit, so the wgpu compositor rejected
every document wider or taller than 8192 px and each refresh fell back to a
full CPU flatten. The desktop and web apps now request the adapter's own
max_texture_dimension_2d and max_buffer_size (gpu_canvas::use_adapter_limits);
the benches use the same limits.

The CPU fallback (documents beyond the GPU limit, or features it lacks) is
GpuCanvas::refresh: it renders and uploads the composite band by band
instead of a full float composite plus a full RGBA8 copy. The CPU canvas
texture is reduced in bands too, and the Navigator on the GPU canvas uses a
cached thumbnail instead of a full-resolution CPU composite per revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* IO: banded export and import; no full-size copies in encoders; larger decode limits

- Flat export renders the composite in bands and quantises each band; alpha
  is dropped in place when every pixel is opaque. The single-layer native
  path copies surface rows a band at a time (no f32 read_region and no
  per-pixel Vec). Buffers come from try_reserve: Err, not an abort.
- PSD export encodes the merged image band by band into its planes (no
  flatten, no second plane copy); near-opaque documents are re-encoded
  without the white matte so the output is unchanged. Layer interleaved
  bytes are freed before their planes are compressed.
- Flat import converts in ~32 MB bands (and not at all when already RGBA);
  the PSD merged-image un-matte works per tile row.
- Encoders borrow the image when no conversion is needed (Image::converted)
  instead of cloning it; PNG only copies 16-bit data for the byte swap.
- Decode limits: 2^30 pixels and 8 GiB on 64-bit targets (a 20000² JPEG
  was refused at 2^28).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Filters: write tile results in memory-bounded groups

apply_tiled collected every tile's float result before writing any (3.1 GB
for a 14000² layer). Results are now produced and written in groups of about
256 MB (at least one tile per core).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* History: bound pixel memory by Preferences > Performance > Memory Usage

History::max_bytes caps the current document's tiles plus the tiles only
history holds (layers, masks, alpha channels); History::trim drops the
oldest states beyond it after every recorded step and when preferences
change, always keeping the last step undoable. The engine sets it from
memory_usage_mb, which was unused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Large-image benchmark (14k / 20k): open, refresh, thumbnails, filter, stroke, saves

One operation per run on a cached synthetic photo, with the GPU device created
the way the app creates it, so /usr/bin/time -l reports that operation's peak
memory. Documented in docs/development.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:45:17 -04:00
Brandon Thomas abf785f7b1
Magic Eraser and Background Eraser tools; canvas rotation/flip/size move vector geometry (#70)
* Magic Eraser and Background Eraser engine commands

paint.magicEraser erases the wand region (tolerance, anti-alias,
contiguous, sample all layers, opacity) to transparency, tile by tile in
parallel; paint.backgroundEraser erases colours similar to the one under
the brush hotspot (continuous / once / background swatch sampling,
discontiguous / contiguous / find-edges limits, tolerance, protect
foreground colour). Both compare and erase in the layer's own channels at
any depth, respect the selection, turn the Background into Layer 0 like
Photoshop, and are one undo step. The Magic Eraser paints the background
colour on layers with locked transparency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Magic Eraser and Background Eraser tools in the Eraser group

Shift+E / E cycles Eraser, Background Eraser and Magic Eraser; the
toolbar flyout lists all three. Options bars: Magic Eraser tolerance,
anti-alias, contiguous, sample all layers and opacity; Background Eraser
sampling toggles, limits, tolerance and protect foreground colour. The
Background Eraser cursor always shows its sampling hotspot. Icons are
derived from Lucide's eraser (ISC), recorded in ATTRIBUTION.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Image Rotation, Canvas Size, Crop, Trim and Image Size move vector geometry

Canvas-wide operations remapped only pixels: type, shape and smart-object
transforms, vector masks, saved/work paths, guides, slices, notes and
Count/Ruler marks stayed put, so they jumped back on the next re-render.

New engine module canvas_geom applies the canvas map to all non-pixel
geometry. 90°/180°/flip turns remap every pixel surface exactly (incl.
type/shape/smart/fill caches and smart filter masks) and move geometry
through the matching affine; guides swap orientation under 90° turns;
rect/ellipse/line live shapes stay live; gradient fill angles follow.
Canvas Size/Crop/Trim translate geometry (shapes re-render for the new
canvas), Image Size scales it and re-renders vectors, Arbitrary rotation
also moves unlinked vector masks and document marks. Layer effect angles
stay (Global Light), as in Photoshop.

Tests compare the CPU composite of a rotated document (type, shapes,
vector mask, smart object) against the rotated flattened original, also
after re-rendering everything from geometry, at 8/16/32-bit RGB, CMYK
and Gray.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Magic Eraser: edit encoded tile bytes in parallel (24 MP: 1260 -> 375 ms)

Tiles are copied out as encoded bytes and only the touched samples are
decoded, so the parallel workers do all the work and the serial write-back
is a memcpy. The ignored bench prints a phase breakdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:17:39 -04:00
Brandon Thomas 6dffa0894c
Never crash: algo is panic-free (Rule 9) (#56)
- apply_tiled reads the shared image for a global filter instead of
  unreachable! if it were missing.
- panorama: bundle-adjustment params come from the placed transforms via
  filter_map; the pyramid size loop and the collapse use let-else instead
  of expect.
- inpaint/pyramid: the same while-let/let-else hunks as #40, so the two
  PRs merge in either order.
- quick selection's constant broad model keeps one documented narrow
  expect (eight fixed corners always fit).
- algo denies unwrap/expect/panic/unimplemented/todo/unreachable.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:40:02 -04:00
Brandon Thomas 91ef877f2f
Never crash: algo pyramids and ui-egui parity without expect (#40)
Three remaining expect() sites in files no open PR touches:
algo::inpaint::complete and algo::pyramid::gaussian loop with while-let
instead of levels.last().expect(), and ui-egui parity::compute indexes
the menu it just pushed instead of last_mut().expect().

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:26:13 -04:00
Brandon Thomas 1c09936154 Clippy 1.99: chunks_exact with a constant size -> as_chunks; drop a no-op Option filter
Rust 1.99's clippy adds chunks_exact_to_as_chunks and some_filter, which
broke the Clippy step on every CI runner (it uses the latest stable). All
sites are mechanical: as_chunks drops the remainder exactly like
chunks_exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:33:14 -04:00
Brandon Thomas f7eb6ea9e7 Format: adopt the craft-family rustfmt.toml and format the workspace
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:31:17 -04:00
storybored379 7ec0b4aa62
Filter › Other › Minimum / Maximum: Preserve Roundness (#27)
Photoshop's Minimum and Maximum have Preserve: Squareness | Roundness;
only the square element existed. Roundness takes the extreme over a
disc (dx² + dy² ≤ r²), one van Herk / Gil-Werman running min/max per
disc row, so cost stays O(r) per pixel. filter.other.minimum/maximum
take "preserve" (default squareness; older params deserialize as
Squareness).

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:13:23 -04:00
storybored379 b6a95b6c8d
Indexed Color: no panic when forced colours fill the palette (#17)
Image > Mode > Indexed Color with Colors 2 + Forced Black and White
(or 8 + Primaries, or <=216 + Web) leaves no room for computed entries.
median_cut(.., 0) returns no centres and kmeans then indexed into the
empty list ("index out of bounds: the len is 0 but the index is 0"),
taking the command down with a panic. kmeans now returns early when
there are no centres, so the palette is just the forced colours.
1 test (algo quantize).

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 00:57:38 -04:00
Brandon Thomas 5f84a7add8 Gradient dithering (reduces 8-bit banding), wired in the UI
paint_gradient gains a dither flag: one quantisation step of monochromatic
position-hash noise, breaking banding without colour speckle. paint.gradient
takes "dither":bool (default true, like Photoshop); the options-bar Dither
checkbox now persists (gradient_dither) and is sent. New gradient_dither_
breaks_banding test; clippy + wasm green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 18:26:30 -04:00
Brandon Thomas bb52e3aa46 Paint Bucket: fill with a pattern (not just a colour)
The Paint Bucket tool only filled the foreground colour; Photoshop's Fill
dropdown also offers Pattern. paint.bucket now takes contents="pattern"
(+ pattern/scale/angle), flood-filling the matched region with the pattern.
algo gains bucket_fill_src (fill colour from a per-pixel source closure);
bucket_fill delegates to it. pattern_cmds resolve_param/placement are now
pub(crate) for reuse. New bucket_fills_with_a_pattern test; clippy + wasm green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 13:07:52 -04:00
Brandon Thomas 33d51d2fa6 Fix clippy -D warnings in video/trap tests (needless mut, identity op)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 03:27:22 -04:00
Brandon Thomas 1c474b8c7e Image > Trap (CMYK prepress trapping)
Clean-room trap: per-ink grayscale dilation (separable max filter,
par_rows/wasm-safe) applied only under already-inked pixels, so internal
colour edges get a trap band while object-vs-paper edges stay clean.
Engine image.trap (CMYK-only, active pixel layer, one history step).
algo 3 tests + engine 2 tests. Parity 607 -> 608; FLOOR 603.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-03 00:44:18 -04:00
Brandon Thomas 2c40a93751 perf: parallelize feature matching (Photomerge's O(n^2) matcher)
match_descriptors queries each descriptor against the other set independently
(plus a cross-check), so it now runs via par_map (order preserved for
deterministic RANSAC). This is Photomerge/Auto-Align's main matching cost.
features + wasm + clippy green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 21:46:15 -04:00
Brandon Thomas fcfcfbddf9 perf: parallelize image-pyramid reduce/expand (multi-band blending)
The Burt-Adelson REDUCE/EXPAND passes write disjoint output rows; both now
use the wasm-safe par_rows helper. Speeds up every multi-band blend
(Auto-Blend Layers, Photomerge, Merge to HDR). pyramid tests + wasm green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 21:44:28 -04:00
Brandon Thomas aeaaa82684 Fix wasm build + clippy in the perf/feature arcs
rayon is native-only (algo's wasm build has no rayon). The perf arcs used
raw rayon::prelude, breaking the wasm gate; switch them to the cfg-gated
par_rows/par_map helpers (new par_rows2 for two-buffer loops: Sobel,
quantize). Replace/Match Color revert to serial (no w param for par_rows).
Also clears clippy -D warnings in frame_cmds/variables_cmds (let-chains,
needless refs, redundant max). Workspace: tests + clippy + wasm + layers green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 21:05:49 -04:00
Brandon Thomas c6ad1755ef perf: parallelize Indexed/GIF/PNG-8 quantization (non-diffusion dither)
None/Pattern/Noise dither have no cross-pixel dependency, so the palette
remap (nearest-colour lookup per pixel) now runs in parallel (rayon);
Diffusion stays serial (error propagation). Speeds up Image > Mode >
Indexed Color, PNG-8 and GIF export (the devlog's 'GIF no dither 8s'
path). quantize tests (all dither modes) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 20:34:32 -04:00
Brandon Thomas cb97c85027 perf: parallelize Filter Gallery cores (Sobel edges, Kuwahara)
Row-parallel (rayon) the Sobel gradient (used by Glowing Edges, Accented
Edges, Ink Outlines, Graphic Pen, Photocopy, etc.) and the Kuwahara
smoothing (Paint Daubs/Oil-paint family). Both read-only into precomputed
planes, write disjoint rows. Gallery tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 20:31:24 -04:00
Brandon Thomas 20d19f7a5d perf: parallelize Shadows/Highlights, Replace Color, Match Color (rayon)
Same safe per-pixel pattern as HDR Toning: par_iter_mut over pixels,
read-only index into mask/luma arrays, no shared mutation. All tone
correctness tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 20:29:00 -04:00
Brandon Thomas e067c2626e perf: parallelize HDR Toning per-pixel loop (rayon)
The local-adaptation tone map (and the log-luma precompute) ran serially;
it was the single-threaded bottleneck flagged in the dev log. Now
par_iter_mut over pixels (base/logl read by index, no shared mutation).
24 MP: ~0.77 s best-of-3 on a 14-core M-series. Correctness unchanged
(tone tests green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 20:27:07 -04:00
Brandon Thomas 42266291d5 Fidelity, GPU fallbacks, interactive performance; print spool-file fix
PSD oracle 111 -> 113 (tent effect blurs, bevel height/shading, glow
range, spread rounding, text gamma, gradient span, Lab blending, colour
burn/dodge edges), channel restrictions (brst), bevel contour/texture,
chisel techniques. Vector masks, pass-through clips, artboards, pattern
fills and bevel textures render on the GPU (parity <=1/255). PNG export
7x, PSD save 4x, .pcraft save 3.6x faster; CPU effects refresh 11x;
deadlock in the effect-map blur fixed. interactive_bench example.

Print: unique spool file per call (concurrent prints of same-named
documents overwrote each other).

Workspace: 2,040 tests pass, clippy clean, layers + wasm OK, parity 600/625.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:26:34 -04:00
Brandon Thomas 6f5aba08d3 Photomerge, Merge to HDR Pro, Lens Correction, Adaptive Wide Angle, Camera Raw, Vanishing Point
Harris+BRIEF/RANSAC panorama with bundle adjustment, seams and multi-band
blending (Auto-Align/Auto-Blend share it); Debevec-Malik HDR merge with
ghost removal and tone mapping; Brown-Conrady lens correction (filter,
batch, smart filter); wide-angle constraint mesh warp; Crop and
Straighten Photos; Camera Raw Filter (Basic, curves, mixer, grading,
detail, effects) with dialog; Vanishing Point planes (engine only).
Fixes warp_triangles tile binning.

Checkpoint also includes compiling in-progress work of the remaining agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:23:49 -04:00
Brandon Thomas ceb2667ac4 Preset panels, character/paragraph styles, Glyphs, Check Spelling
Gradients/Patterns/Styles/Shapes/Tool Presets/Clone Source panels with
headless preset commands and a Custom Shape tool; Color card tabs match
Essentials. Character and Paragraph Styles (doc model, .pcraft, PSD style
sheets), Glyphs panel + type.insertText, Check Spelling (SCOWL-50 list).

Checkpoint also includes compiling in-progress work of the remaining agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:02:34 -04:00
Brandon Thomas aa87012aab Analysis, notes, proof views, workspaces (+ in-progress work of other agents)
Image > Analysis (ruler, count, measurement scale/log/record, scale
marker), notes (PSD Anno round trip, Notes panel, import), Modifier Keys
panel, Proof Setup plates / Legacy Mac RGB / colour blindness (Vienot),
32-bit preview options, custom pixel aspect, extras options, New/Delete/
Lock Workspace. Parity 600/625, FLOOR 595.

Checkpoint: also contains compiling in-progress work from the parallel
panels, type styles, File menu, photo, filters, UI-parity and fidelity
agents, plus photocraft-cli serve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:49:07 -04:00
Brandon Thomas 6b44c393fa continued work 2026-10-01 09:16:17 -04:00
Brandon Thomas 536a7346fb more algorithms, progress 2026-10-01 08:25:06 -04:00
Brandon Thomas a517477da8 readme, more impl 2026-09-30 11:15:03 -04:00
Brandon Thomas ae92ac20bf continued wip 2026-09-30 09:37:18 -04:00
Brandon Thomas 16941f7198 one-shot 2026-09-30 05:02:08 -04:00