The options-bar Fill dropdown was decorative. It now persists in tool_options
(bucket_fill_pattern) and the click sends contents=foreground|pattern, so
Pattern actually flood-fills with the current pattern. ui tests + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
A lone selected layer with no active selection was disabled for Align; in
Photoshop it aligns/centres to the canvas. can_align now enables one layer,
and auto-align targets the canvas bounds for a single layer (instead of the
layer aligning to itself). Selection- and multi-layer behaviour unchanged.
New align_single_layer_to_canvas test; the Align buttons light up in the UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User-reported features verified working; added the missing regression coverage.
- copy_paste_between_documents: copy in one tab, paste into another (shared
clipboard targets the active document).
- paste_between_different_color_modes: RGB clipboard pastes into a Grayscale
doc, converting to the target format (no panic).
- transform_scale_grows_content: edit.transform 2x scales layer content.
Cross-document paste had no test before. All green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified our levels() against Adobe Photoshop 2026 (adjustLevels on a 0..255
ramp): exact for gamma<=1 and all input/output ranges, but a pure u^(1/gamma)
has infinite slope at black while Photoshop bounds the shadow toe to initial
slope 2^gamma. Added a soft-min (p-norm) of the power curve with that line,
applied only for gamma>1; reduces to the exact power curve in the body, so
highlights/midtones and gamma<=1 are unchanged. GPU Levels LUT is built from
this fn, so GPU stays in sync (gpu parity 18/18).
Corpus oracle: levels_grayscale.psd max error 0.043 -> 0.0078 (bad px
4.1% -> 0.1%); 113 pass / 30 differ unchanged (no regressions). Golden test
pins the curve to captured Photoshop samples (gamma 2 toe within 4/255).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The non-legacy B/C was a linear scale that hard-clipped; Photoshop's modern
B/C is a pair of smooth curves that pin pure black and white. Reverse-
engineered from Photoshop 2026 ground truth (a 0..255 ramp pushed through the
app via executeAction BrgC, useLegacy=false):
- Contrast: symmetric cubic-Hermite S pivoting at 0.5 (end slope 1-c/128,
centre 1+c/128) -- near-exact, <=0.5/255 vs Photoshop.
- Brightness: line of slope 1.375^(b/50) rolled off to (1,1) by a v^P white
anchor -- close, approximate at the extremes (the real roll-off is a spline).
Applied brightness-then-contrast; matched in the wgsl compositor (mbright/
mcontrast) so GPU == CPU oracle (gpu parity 18/18). Golden tests pin both
curves to the captured Photoshop samples.
Corpus oracle: brightnesscontrast_grayscale.psd max error 0.478 -> 0.086
(bad px 91% -> 29%); 113 pass / 30 differ unchanged (no regressions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Video layers previously looked like plain rasters to inspecting agents; now
each reports its frame count, fps, show-altered flag and footage source
(blank or file), alongside the existing timeline block. 1 test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Play loops the playhead via timeline.setFrame at the timeline fps, using
egui time (wasm-safe, no Instant) and requesting repaints; pausing/closing
stops it. New pause icon. ui-egui tests + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
codecs: encode_gif_animated (GIF89a multi-frame, per-frame local palettes,
NETSCAPE loop, delay centiseconds) + GifFrame; 2 tests. video render_video
gains a gif path: composite the document at each timeline frame, quantise
(adaptive 256), and write one looping .gif; image-sequence path unchanged.
engine 7 video tests, clippy + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VideoDataM stores each frame via the surface blob store (surface_m /
self.surface), with source/fps/show_altered; threaded both directions in
layer_m and the loader. A saved video layer reloads its full frame stack,
timeline and footage source. Round-trip test covers pixels + metadata.
format 16 tests, clippy + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Platform-gated command for Windows Image Acquisition (scanners/cameras).
Reports device availability; real WIA COM device I/O is Windows + hardware
only (Photoshop gates this item to Windows too), so off-Windows it returns
available:false with zero devices. 1 test. Parity 624 -> 625 (100.0%);
FLOOR 620. Every Photoshop menu item now has a live command.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
layer.videoLayers.{newVideoLayerFromFile,replaceFootage,interpretFootage,
reloadFrame} load frames from a single image or an image-sequence folder;
file.import.videoFramesToLayers adds each frame as its own layer;
file.export.renderVideo composites the whole document at every timeline
frame and writes an image sequence (clean-room, no proprietary codec).
6 tests. Only Windows-only WIA Support remains. Parity 618 -> 624 (99.8%);
FLOOR 619.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Layer.video (VideoData: frame surfaces + fps + source); the layer's
displayed content is the frame at the timeline playhead, re-synced after
any edit/navigation. Commands: New Blank Video Layer, Insert Blank /
Duplicate / Delete / Restore Frame, Restore All Frames, Show Altered
Video, Rasterize (Layer > Video Layers > Rasterize and Rasterize > Video).
Timeline duration grows to fit. 4 tests; engine green, clippy clean, wasm
OK. Parity 609 -> 618 (98.9%); FLOOR 613. (File-based footage + Render
Video + Video Frames to Layers + .pcraft frame persistence are follow-ups.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timeline_ui::handles() wired into menus::is_live so the Timeline panel
counts toward parity. 608 -> 609 (97.4%).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document.timeline (fps/duration/playhead/work-area), threaded through
.pcraft. Engine timeline.{create,delete,setFrame,next/previousFrame,
setProps,info} (navigation is non-history, scriptable for frame-range
rendering); reported in document.inspect. Foundation for Layer > Video
Layers + the Timeline panel (follow-up arcs). doc 4 + engine 2 tests;
format/doc/engine green, clippy clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Merge a presets/preferences file (another install, or exported library)
into the current preset library, appending gradient/style/shape/pattern/
tool-preset groups the library lacks (by name); idempotent. Headless,
2 tests. Parity 606 -> 607; FLOOR 602.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
file.export.dataSetsAsFiles gains a naming template so agents control
batch output filenames (e.g. row-{index}). Default {name}. Test covers it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Agents can verify a data-driven setup (variable names+layers, data-set
names, active set) in one inspect call. Test covers it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Groups the selected layers and clips them to a frame rect (their bounds)
via a rectangular layer mask — the visual result of Photoshop's Frame for
this command, built from existing group + mask primitives (no doc-model
change). Headless/scriptable; 2 tests. Parity 605 -> 606; FLOOR 601.
(A distinct Frame layer type + the interactive Frame tool K are a later arc.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
Photoshop's data-driven graphics dialog in the ui-egui crate, driving the
headless engine commands: Define page (per-variable name + layer + type,
add/remove), Data Sets page (navigator, per-variable value editors, Apply).
State lives in the dialog's __variables field (command-shaped JSON), so the
control channel drives it via ui.dialog.set / confirm like a human. 2 tests;
offscreen snapshot verified. UI stays in ui-egui; all behaviour is engine
commands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clean-room implementation of Photoshop's data-driven graphics, headless
and scriptable for CLI/MCP batch generation:
- doc: Variables model (visibility/text/pixel-replacement bindings + named
data sets), threaded through .pcraft (manifest + convert).
- engine variables_cmds.rs: image.variables.define, image.variables.dataSets,
image.applyDataSet (visibility/text/pixel replace w/ fit/fill/conform/as-is
+ 9 alignments, one history step), file.import.variableDataSets (CSV),
file.export.dataSetsAsFiles (apply each set and export), variables.list.
- Pixel replacement loads+scales+aligns an image into the layer bounds via
the existing resample helpers.
- 7 tests (doc round-trip + lookups; engine define/apply, CSV import, export,
pixel replace, bad params). Parity 600 -> 605; FLOOR 600.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Weighted parity by area, the remaining 25 menu items grouped (video/
timeline, data-driven, niche), and agent-hour/wall-clock estimates to
90/95/98/99%. ~25-40 wall-clock hours (parallel) to a 95% ships-for-most
state; video and AI are the long tail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the app icon from the top of the README, matching PhotoCraft, and
show each app's icon next to its name in the Crafting Apps table in place
of the coloured name badges. The table is now identical across all seven
apps apart from the "you are here" row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>