Commit Graph
13 Commits
Author SHA1 Message Date
Brandon Thomas e2444ceb68 Retouch tools paint the mask/channel target; Sample All Layers for Blur/Sharpen/Smudge; premultiplied Smudge (#207) (#234) (#234) 2026-10-05 11:26:20 -04:00
Brandon ThomasandClaude Opus 5.5 1c4cb8307c Brushes: Tilt Scale, Brush Projection, Spacing checkbox, section locks, Mixer Wetness/Mix; Brushes grid + drag and drop; options bar through tools.setBrush (#174)
* paint: Tilt Scale, Brush Projection, speed spacing, section locks, mixer settings in the brush

Shape Dynamics gains Tilt Scale (pen tilt squashes the tip height when the size
control is Pen Tilt) and Brush Projection (tilt foreshortens the tip along the
tilt direction, barrel rotation turns it). Spacing gets Photoshop's checkbox:
off spaces dabs by stroke time, so speed sets the spacing. Section locks keep a
section when another preset is picked. Transfer gains Wetness/Mix jitter, and
the brush carries the Mixer Brush options (Wet, Load, Mix, Flow, Sample All
Layers).

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

* engine, io: mixer settings from the brush, preset organising commands, one journal entry per setBrush gesture, ABR mapping of the new fields

- paint.mixerBrush defaults to the brush's Mixer settings (Wet/Load/Mix/Flow/Sample All Layers).
- brush.presets.rename/move/moveGroup/renameGroup/deleteGroup (drag and drop and context menus in
  the Brushes panel); the preset store index keeps the full panel order, built-ins included.
- tools.setBrush calls sharing a coalesce key journal as one entry (the patch from the brush before
  the gesture), so a slider drag is one command.
- .abr: Tilt Scale, Brush Projection, the Spacing checkbox (Intr), Wetness/Mix jitter and mixer
  tool options map instead of warning.

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

* ui: Tilt Scale, Brush Projection, Spacing checkbox, section locks, mixer controls; Brushes grid/list, drag and drop, rename/delete; options bar through tools.setBrush

- Brush Settings: Tilt Scale (enabled with Size on Pen Tilt) and Brush Projection in Shape
  Dynamics, the Spacing checkbox, Wetness/Mix Jitter plus the Mixer Brush options in Transfer,
  and a lock beside every section.
- Brushes tab (new brushes_tab.rs): list/grid toggle, drag presets within and between groups,
  drag group headers to reorder, context menus to rename or delete presets and groups; all via
  brush.presets.* commands.
- Options bar, the canvas brush picker and the [ ] keys edit the brush through tools.setBrush;
  edits of one gesture share a coalesce key, so a drag is one journal entry.
- ui.set accepts brushesView (list|grid).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:21:23 -04:00
Brandon ThomasandClaude Opus 5.5 7e7864afae Painting: right-click opens the brush picker (or erases), real Smoothing, eraser-on-Background tests (#86)
* 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>
2026-10-04 23:31:17 -04:00
Brandon ThomasandClaude Opus 5.5 d881785fde Photoshop brushes: .abr import, stylus pressure/tilt, 36 grouped presets, .grd import (#74)
* 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>
2026-10-04 16:26:16 -04:00
Brandon ThomasandClaude Opus 5.5 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 ThomasandClaude Opus 5.5 296ced38d6 Never crash: text, color, doc, paint, geom, raster, compose, vector (#39)
- 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>
2026-10-04 13:25:44 -04:00
Brandon ThomasandClaude Opus 5.5 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 ThomasandClaude Opus 5.5 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
Brandon Thomas 536a7346fb more algorithms, progress 2026-10-01 08:25:06 -04:00
Brandon Thomas 14c5c1bc51 readme, misc 2026-09-30 11:37:50 -04:00
Brandon Thomas b00b24739a more work 2026-09-30 11:36:20 -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