* Right-click opens the Brush Preset picker (or erases), eraser-on-Background tests, README comma
- #75: the right mouse button never paints with the tool. With a painting tool it opens the
Brush Preset picker at the pointer (Photoshop). New preference Preferences › Tools ›
Right-click with painting tools (tools.rightClickWithPaintingTools: brushPicker | erase,
serde default brushPicker): with erase, a right drag with the Brush erases with the current
brush (one paint.stroke, one undo step, pen pressure/tilt as usual). ui.pointer accepts
"button": "secondary". New module ui-egui/src/paint_mouse.rs; picker body shared with the
options-bar chip.
- #76: the engine already paints the background colour when erasing a transparency-locked
layer (the Background); added tests at 8/16/32-bit in RGB/CMYK/Gray, with a selection, on a
normal layer (erases to transparency), on a transparency-locked layer, and through the UI.
- paint.stroke/pencil refuse coordinates beyond ±1e6 (an f64::MAX point ran out of memory).
- #73: test that the canvas hands the live stroke over to the committed document with no
frame lacking the stroke, refreshing only the stroke's rect.
- #69: README comma.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Brush smoothing follows the options bar; the live stroke draws the catch-up tail (#73)
- The options-bar Smoothing % (pro and classic bars, Brush and Eraser) edits the session
brush's smoothing, which the live stroke and the committed paint.stroke both use; the
canvas no longer forces 0.3. The session brush starts at Photoshop's 10 %.
- With smoothing the brush lags the pointer and catches up when the stroke ends. LiveStroke
now draws that catch-up tail on every step (StrokeRenderer::tail_preview renders only the
tiles the tail touches; the previous step's tail is restored first), so the preview at the
last move equals the commit and nothing pops in on release. At 0 % there is no lag.
- Path › Stroke Path ignores smoothing (it strokes the exact geometry).
- Tests: paint tail_preview = finished stroke; canvas: 0 % preview = commit, 50 % preview =
commit with the end reaching the pointer, smoothing changes the stroke, commit equals
paint.stroke with that smoothing, no stale tails after sharp turns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Smoothing is a per-tool option that brush presets keep
- Picking a brush preset (tools.setBrush / paint.* "preset", .abr import with select) keeps the
current smoothing amount and mode instead of the preset's (BrushSettings::picked_over),
as Photoshop's options bar does. An explicit smoothing alongside the preset still applies.
- The Brush and the Eraser each remember their smoothing (UiState smoothing_tool /
tool_smoothing, synced on tool switch before the options bar, the canvas and each stroke);
a tool seen for the first time starts at 10 %.
- Tests: engine presets keep 10 % / a user value with pulled-string mode; UI per-tool
smoothing across Brush/Eraser/other tools and on the first stroke after a switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* psd: .abr brush (v1/v2/v6+) and .grd gradient (v5) parsers and writers
Clean-room from public format descriptions: legacy computed/sampled brushes,
8BIM-sectioned samp/patt/desc files, 8/16-bit raw and PackBits tips. Bounded
(MAX_EDGE, MAX_TOTAL_BYTES), damaged brushes are skipped with warnings, hostile
input returns Err. Adds an abr fuzz target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* paint: 36 grouped built-in brush presets and new procedural tips
General / Dry Media / Wet Media / Special Effects groups (pressure size/opacity
variants, airbrush, pencils, charcoal, pastel, crayon, rake, watercolour, ink,
oil, foliage, grass, stipple, sparkle, halftone…). Leaf, grass, charcoal,
sponge, star and rake tips are generated in code. BrushPreset gains a group.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* io: map .abr brushes onto BrushSettings
Tip shape, shape dynamics, scattering, texture (embedded patt patterns),
dual brush, colour dynamics, transfer, pose, noise, wet edges, build-up and
captured tool options; unmapped settings are reported as warnings. Includes
bench_abr (100-brush import, 5000-dab stroke).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* engine: brush.presets.importAbr and gradient.presets.importGrd
Path or base64 data, group per file (re-import replaces), unique names next to
the built-ins; graceful-failure tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ui: real stylus pressure/tilt, grouped Brushes panel, Import Brushes
Pen force from egui Touch events (Windows WM_POINTER via winit), Pointer Events
pressure/tilt/twist on the web, tilt via ui.pointer; mouse stays 1.0. .abr/.grd
open from File > Open, drag-and-drop, Brushes panel and Preset Manager. Preset
previews keyed on a cheap signature instead of the full brush JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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>
- Real crash: build_engine_data took its template from the PSD's text
engine data and did expect("EngineDict"), so a text layer whose engine
data lacked EngineDict (or wasn't a dictionary) crashed PSD export.
The template is now repaired; regression test
engine_data_template_without_engine_dict.
- color: the built-in CMYK<->sRGB transforms fall back to the naive
formulas instead of expect if they ever fail to link.
- doc: insert_above falls back to the top of the root instead of
expect("valid path"); next_layer_name searches a bounded range.
- paint: Mips::new loops with while-let instead of last().expect.
- All eight crates deny unwrap/expect/panic/unimplemented/todo/unreachable.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>