* Color Sampler tool (#1046) Add the Color Sampler to the Eyedropper (I) flyout: click to pin numbered sample points to canvas pixels, drag to move, Alt-click (or drag off-canvas) to remove, Clear All in the options bar, at most 10 per document. Each point reads one composite pixel (RGB 0-255 and naive device CMYK, same as the pointer readout) and the Info panel lists them under their number with the position in the ruler unit; the readouts refresh when the document changes. Points are per-document engine view state (DocState.color_samplers): they belong to their document, are discarded on close, and are not saved to .pcraft or PSD. They are driven by commands shared by the UI, CLI, control channel and MCP (view.colorSamplers.add/.move/.delete/.clear/.list); the mutations change no pixels and add no History step. Points follow the canvas geometry (crop, Canvas Size, Image Size, Image rotation/flip, rotated crop) through a pending Session.sampler_map that Session::edit applies before culling the points that land outside the new canvas; the count removed is reported in the status bar. - engine: new sampler_cmds.rs (commands + shared readout helpers) and the sampler_map wiring in lib.rs, image_cmds.rs, mode_cmds.rs, commands.rs, analysis_cmds.rs (compound rolls points back on error). - ui-egui: Tool::ColorSampler + original sampler.svg icon, toolbar slot, Info panel list, options bar, pointer handling, overlay markers, cursor. - docs/scorecard.md regenerated (TOOL-1649-1 no longer says the tool is missing). No menu item, so parity is unchanged (627/627). * Color Sampler follow-up: tool count, README, i18n, roadmap The full ui-egui suite (not just the filtered tests) surfaced these gaps: - state.rs: Tool::ALL.len() assertion 52 -> 53. - README.md: "52 tools" -> "53 tools" and add Color Sampler to the list, so readme_tool_list_matches_tool_all passes. - i18n: the options-bar hint, "{n} of {max} sample points" and "No document open" need entries in every catalog that claims complete menus (every_tl_literal_is_translated); added to all 16 (coverage 1584/1584). The translations were drafted alongside the tool. - docs/roadmap.md: drop Color Sampler from the missing-tools list and note it landed. * Color Sampler: fit main's new tools and Image Size resample type Reset Tool covers ColorSampler, the test ViewXform sets aspect, and Image Size checks Method::None instead of an Option. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
67 KiB
PhotoCraft scorecard
Generated by cargo xtask scorecard from perf/budgets.toml, perf/baseline.json, crates/io/tests/corpus.rs, scorecard/*.toml and the source tree. Do not edit by hand: change the sources and regenerate (CI runs cargo xtask scorecard --check). Checklist statuses are verified against the code; numbers here supersede estimates elsewhere.
Summary
| Area | Done | Partial | Missing | Key numbers |
|---|---|---|---|---|
| Performance | 3 | 22 | 22 | 56 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 17 not run, 5 not measurable yet (baseline 2026-10-05) |
| File compatibility | 6 | 12 | 9 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
| Tools | 25 | 16 | 2 | |
| Workspace and UI | 9 | 8 | 6 | settings that do nothing: 32 of 154 (target 0) |
| Type | 1 | 4 | 4 | |
| Automation | 3 | 2 | 6 | 631 of 631 menu items live (100.0%) |
| Reliability | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present |
| Distribution | 5 | 4 | 2 | |
| Open bugs | 29 | 2 | 1 |
Performance
Budgets: perf/budgets.toml (targets from #209, #210, #211). Numbers: the committed baseline (perf/baseline.json, full run), updated only by cargo xtask perf --update-baseline. The nightly perf-nightly workflow runs cargo xtask perf on a fixed macOS runner and fails on a broken enforced budget or a p50 regression over 15 % against a baseline from the same machine class. "Over budget" rows are targets the code doesn't meet yet: they are reported, and only their regressions fail.
Baseline: 2026-10-05, commit f5d2bcb54a, Apple M4 Pro (48 GB), GPU Apple M4 Pro, macOS 15.6; load average 224.90 / 226.31 / 196.03 (1/5/15 min) at the start of the run.
| Id | Scenario | Target | p50 ms | p95 ms | max ms | Peak RSS | GPU | Status | Issue |
|---|---|---|---|---|---|---|---|---|---|
| P1 | Move a layer with layer styles (per drag update) | ≤ 25 ms p95 | 321 | 535 | 535 | 825 MB | 409 MB | over budget (target not met yet) | #209 |
| P2 | Change layer opacity | ≤ 20 ms p95 | 276 | 443 | 443 | 639 MB | 409 MB | over budget (target not met yet) | #209 |
| P3 | Change layer fill | ≤ 20 ms p95 | 279 | 467 | 467 | 506 MB | 409 MB | over budget (target not met yet) | #209 |
| P4 | Change blend mode | ≤ 20 ms p95 | 206 | 423 | 423 | 579 MB | 409 MB | over budget (target not met yet) | #209 |
| P5 | Toggle visibility | ≤ 10 ms p95 | 178 | 425 | 425 | 668 MB | 409 MB | over budget (target not met yet) | #209 |
| P6 | Undo a move | ≤ 25 ms p95 | 256 | 530 | 530 | 650 MB | 409 MB | over budget (target not met yet) | #209 |
| P7 | Redo a property change | ≤ 25 ms p95 | 144 | 212 | 212 | 669 MB | 409 MB | over budget (target not met yet) | #209 |
| P8 | Duplicate a 12 MP layer (first) | ≤ 100 ms p95 | 508 | 699 | 699 | 907 MB | 919 MB | over budget (target not met yet) | #209 |
| P9 | Duplicate a 12 MP layer (repeat) | ≤ 20 ms p95 | 406 | 627 | 627 | 749 MB | 919 MB | over budget (target not met yet) | #209 |
| P10 | Paste a 12 MP layer (first) | ≤ 100 ms p95 | 1232 | 3136 | 3136 | 1.3 GB | 1.4 GB | over budget (target not met yet) | #209 |
| P11 | Paste a 12 MP layer (repeat) | ≤ 20 ms p95 | 445 | 1174 | 1174 | 958 MB | 1.4 GB | over budget (target not met yet) | #209 |
| P12 | Arrow-key nudge in a 150-layer document | ≤ 30 ms p95 | – | – | – | – | – | missing: panicked: wgpu error: Validation Error / Caused by: / In Queue::write_buffer / Buffer with 'pc_compose_uniforms' label is invalid | #209 |
| P51 | New layer in a 150-layer document | ≤ 16 ms p95 | – | – | – | – | – | not run | #1771 |
| P13 | Brush dab on a 15000×10000 16-bit document | ≤ 80 ms p95 | 6.42 | 146 | 146 | 839 MB | 2.0 GB | over budget (target not met yet) | #209 |
| P14 | Move a layer on a 15000×10000 16-bit document | ≤ 80 ms p95 | 2698 | 4318 | 4318 | 1.5 GB | 2.0 GB | over budget (target not met yet) | #209 |
| P16 | Brush dab on a 32-bit document with 20 layers | ≤ 100 ms p95 | 21.5 | 161 | 161 | 793 MB | 1.6 GB | over budget (target not met yet) | #209 |
| P17 | Move a layer on a 32-bit document with 20 layers | ≤ 100 ms p95 | 386 | 681 | 681 | 1.2 GB | 1.6 GB | over budget (target not met yet) | #209 |
| P18 | Open a 20 MP layered PSD: first pixels (today: fully opened) | ≤ 1000 ms p95 | 1753 | 2794 | 2794 | 2.2 GB | 1.9 GB | over budget (target not met yet) | #210 |
| P19 | Open a 20 MP layered PSD: editable | ≤ 3000 ms p95 | 1753 | 2794 | 2794 | 2.2 GB | 1.9 GB | pass | #210 |
| P21 | Content-Aware Fill on 10 MP | ≤ 1500 ms p95 | 218 | 256 | 256 | 1.8 GB | 1.8 GB | pass | #210 |
| P22 | Spot Healing stroke on 10 MP | ≤ 500 ms p95 | 157 | 247 | 247 | 1.8 GB | 1.9 GB | pass | #210 |
| P24 | Import 500 brushes (longest UI-thread step) | ≤ 100 ms max | – | – | – | – | – | not run | #210 |
| P25 | Cancel a Gaussian Blur job on 24 MP (until the worker exits) | ≤ 200 ms max | – | – | – | – | – | not run | #210 |
| P49 | Cancel a Content-Aware Fill job on 24 MP (until the worker exits) | ≤ 200 ms max | – | – | – | – | – | not run | #210 |
| P50 | App frame time while a 24 MP Gaussian Blur job runs | ≤ 16 ms p95 | – | – | – | – | – | not run | #210 |
| P26 | Select › Modify › Smooth on 24 MP (radius 50) | ≤ 100 ms p95 | 99.9 | 216 | 251 | 1.0 GB | – | over budget (target not met yet) | #211 |
| P27 | Select › Modify › Feather on 24 MP (radius 50) | ≤ 100 ms p95 | 122 | 357 | 360 | 1.1 GB | – | over budget (target not met yet) | #211 |
| P28 | Content-Aware Scale 24 MP (to 90 % width) | ≤ 3000 ms p95 | 954192 | 954192 | 954192 | 2.1 GB | – | over budget (target not met yet) | #211 |
| P29 | Select Subject on 24 MP, repeated | ≤ 100 ms p95 | 559 | 693 | 693 | 1.4 GB | – | over budget (target not met yet) | #211 |
| P30 | Liquify 800 px brush on 3500×2200 at full resolution | ≤ 1 ms per update | – | – | – | – | – | not run | #211 |
| P31 | Camera Raw exposure change (proxy) | ≤ 10 ms | – | – | – | – | – | not run | #211 |
| P32 | Brush release on a long stroke | ≤ 16 ms (no hitch at mouse-up) | – | – | – | – | – | not run | #211 |
| P33 | Brush dab on A4 300 ppi CMYK (dab to screen) | ≤ 6 ms p95 | 34.7 | 235 | 235 | 775 MB | 87 MB | over budget (target not met yet) | #211 |
| P36 | Camera Raw exposure change with all SDR scopes (CPU) | ≤ 10 ms p95 | – | – | – | – | – | not run | #211 |
| P40 | Brush dab 60 px on 36 MP | track only | 1.45 | 3.19 | 3.19 | 1.3 GB | – | tracked | |
| P41 | Gaussian Blur r 40 on 36 MP | track only | 2963 | 3069 | 3069 | 1.9 GB | – | tracked | |
| P42 | Curves dialog change, layer preview, 36 MP | track only | 38.7 | 158 | 158 | 1.1 GB | – | tracked | |
| P43 | Save .psd, 36 MP, 3 layers | track only | 3047 | 3446 | 3446 | 1.2 GB | – | tracked | |
| P44 | Open .psd, 36 MP, 3 layers | track only | 3535 | 4547 | 4547 | 1.6 GB | – | tracked | |
| P45 | Full refresh with layer effects, warm caches, 36 MP (GPU) | track only | 105 | 160 | 160 | 714 MB | – | tracked | |
| P46 | Brush dab on a layer with effects, 36 MP (GPU) | track only | 30.6 | 83.3 | 83.3 | 863 MB | – | tracked | |
| P47 | Font-size drag step, 50 type layers | track only | 23.3 | 60.5 | 71.2 | 600 MB | – | tracked | |
| P48 | Full refresh of a 14000×14000 document | track only | 8715 | 9897 | 9897 | – | – | tracked | |
| P52 | Chisel bevel size change, 12 MP ellipse, size 8 (GPU) | track only | – | – | – | – | – | not run | |
| P53 | Chisel bevel size change, 12 MP ellipse, size 50 (GPU) | track only | – | – | – | – | – | not run | |
| P54 | Chisel bevel size change, 12 MP ellipse, size 250 (GPU) | track only | – | – | – | – | – | not run | |
| P55 | Chisel bevel size change, 12 MP text paragraph, size 8 (GPU) | track only | – | – | – | – | – | not run | |
| P56 | Chisel bevel size change, 12 MP text paragraph, size 50 (GPU) | track only | – | – | – | – | – | not run | |
| P57 | Chisel bevel size change, 12 MP text paragraph, size 250 (GPU) | track only | – | – | – | – | – | not run | |
| P58 | Chisel bevel size change, 12 MP ellipse, size 8 (CPU) | track only | – | – | – | – | – | not run | |
| P59 | Chisel bevel size change, 12 MP text paragraph, size 8 (CPU) | track only | – | – | – | – | – | not run |
Not measurable yet (the code can't do it yet; no number is recorded):
| Id | Scenario | Target | Why | Issue |
|---|---|---|---|---|
| P15 | Zoom / pan frame time on a 15000×10000 16-bit document | ≤ 16 ms per frame | no headless canvas frame harness yet (zoom and pan sample the GPU texture in the egui paint callback) | #209 |
| P20 | Open a 2 GB PSD / 4–5 GB PSB | opens with progress, editable, never blank | opens run in the background with progress (#210), but there are no generated large-file fixtures yet (#216) | #210 |
| P23 | Warp / Puppet / Perspective preview at 4K | ≤ 16 ms per frame, commit in the background | the previews run inside the UI tools; no headless frame harness, no background commit | #210 |
| P34 | Brush preview memory, small dab on an 8K viewport | ≤ 2 MiB | the live brush preview buffers are UI-side; no headless counter yet | #211 |
| P35 | Marquee drag on 24 MP with Alt/Shift | ≤ 17 ms p95 | Shift/Alt marquee modifiers don't exist yet (#208) and the drag preview is UI-side | #211 |
File compatibility
Corpus floors (crates/io/tests/corpus.rs): the enforced minimums; the corpus tests (cargo feature corpus, cargo xtask test-corpus, always run in CI) fail below them.
| Corpus | Directory | Files | Oracle pass floor | Round-trip floor |
|---|---|---|---|---|
| io corpus | corpus/psd |
170 | 146 | 169 |
| psd-tools corpus | corpus/psd-tools |
309 | 236 | 308 |
| photoshop oracles | corpus/photoshop |
256 | 133 | 258 |
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| FILE-215-1 | Smart Objects export as embedded Smart Objects (SoLd + lnk2), not flat pixels | done | #215 | psd_export.rs smart_blocks: PlLd + SoLd, source in lnk2 (.pcraft -> PSB); engine tests/smart_psd.rs; Photoshop 2026 opens 8/16-bit exports as live smart objects. Linked files are embedded |
| FILE-215-2 | Smart Filters survive PSD save and re-open (filterFX descriptor read and written) | partial | #215 | smart_map.rs: 14 filters plus a verified Camera Raw subset mapped both ways, others kept verbatim, mask in FEid; io tests/smart_corpus.rs. PhotoCraft-only filters (Twirl, Liquify…) and unmapped Camera Raw settings are left out of filterFX with a warning |
| FILE-215-3 | Filter mask can be painted directly, with undo | missing | #215 | smart_cmds.rs:382 mask only from the selection |
| FILE-215-4 | Adjustments (Hue/Sat + Colorize, Curves, Levels, Shadows/Highlights) apply as Smart Filters | done | #215 | Image › Adjustments on a smart object records image.adjustments. as a smart filter (adjust_cmds.rs adjust_as_smart_filter; the selection becomes the filter mask), re-rendered from the source by adjust_as_filter and re-editable with layer.smartFilter.setParams; PSD maps Levels/Curves/Shadows-Highlights both ways (smart_map.rs). Opening the editor from the smart-filter row is FILE-215-5's row work |
| FILE-215-5 | Smart Filter rows: collapse, mask thumb, blend icon, drag reorder, context menu, live preview | partial | #215 | smart_ui.rs:31 rows + visibility + editor; no collapse, drag or context menu |
| FILE-215-6 | Smart Object corner skew and perspective (quad or homography transform) | missing | #215 | doc lib.rs:312 SmartObject.transform is an Affine |
| FILE-215-7 | Layered TIFF read and write (tag 37724), honouring the ask-before-saving pref | partial | #215 | psd tiff.rs (ImageSourceData, both byte orders), io tiff_layers.rs, ui tiff_options_ui.rs (Discard Layers and Save a Copy); extra alpha channels and a Quick Mask are not written (the TIFF codec holds one transparency sample) |
| FILE-215-8 | Large and BigTIFF import with banded decode across all IFDs | done | #215 | codecs tiff_ifd.rs (bounds-checked IFD chain + SubIFD walk, classic and BigTIFF, cycle-safe; tiff_info / decode_tiff_page), tiff.rs banded parallel strip/tile decode, planar 1 and 2, BigTIFF write (tiff_bigtiff or > 4 GiB); io import_tiff_page; 30 MP RGB8 Deflate 345 -> 32 ms; no page picker in the UI yet |
| FILE-215-9 | SVG opens and places as shape layers or a vector Smart Object | done | #215 | io svg.rs: paths, groups, text outlines as shape layers (gradients approximated; clip/mask/filter/pattern/image subtrees rasterised); smart_cmds.rs renders an SVG source at its placement scale |
| FILE-215-10 | Multi-page PDF save, PDF presentation, layer comps to PDF, vector/text output, PDF import | missing | #215 | Only print_cmds.rs:133 print_pdf (raster print page) |
| FILE-215-11 | RAW: lossy DNG, highlight modes, .xmp sidecars, RAW prefs wired, per-camera corpus | missing | #215 | raw sensor.rs:210 lossy DNG unsupported; raw_defaults unread; no xmp |
| FILE-215-12 | Camera Raw dialog shows a histogram; settings persist as a PSD Smart Filter | partial | #215 | Interactive RGB/Lab scopes. PSD WB/Light/Presence, curves, HSL, grading, sharpening/noise detail, grain and numeric vignette controls mapped with descriptor preservation. Active manual Optics and unverified vignette style keep the updated ACR 18.4 fixture opaque; PSD/.pcraft saves preserve it. HDR, remaining settings and Photoshop acceptance still open |
| FILE-215-13 | Video layer frames persist in .pcraft; PSD video layers mapped | partial | #215 | format convert.rs:89 + tests/roundtrip.rs:282 persist frames; no PSD video mapping |
| FILE-215-14 | User swatch model with ACO/ASE import and export | done | #215 | engine presets/swatches.rs (RGB/HSB/CMYK/Lab/Gray + verbatim colour books, persisted in prefs) + swatch_cmds.rs (swatches.*); psd aco.rs (v1/v2) and ase.rs (groups, spot/global) read+write; ui swatches_ui.rs, Color Picker Add to Swatches |
| FILE-215-15 | Layer Style contour picker, curve editor, presets, .shc import/export | partial | #2912 | Layer Style dialog: contour thumbnail + preset picker (12 standard shapes, own control points) and Anti-aliased on Drop/Inner Shadow, Outer/Inner Glow, Satin, Bevel Gloss Contour and Contour element; Contour Editor (add/drag/delete, Corner, Input/Output, Preset, Load…/Save… .shc per Adobe's public spec, psd shc.rs); corner points render (compose contour_curve_lut) and map to PSD Cnty. Missing: a user contour library (New…, picker Load/Save/Replace Contours), Anti-aliased doesn't change the render yet, .shc not yet checked against a Photoshop-written file |
| FILE-215-16 | Advanced Blending (knockout, blend as group, mask hides effects) end to end | partial | #215 | doc advanced.rs + .pcraft + PSD/layered TIFF (knko infx clbl tsly lmgm vmgm), CPU compose advanced.rs (GPU falls back), blendingOptions command + dialog; 7 psd-tools knockout oracles now pass; knockout-isolated-groups (0.45 % AA edges), advanced-blending, blend-and-clipping still differ |
| FILE-215-17 | CMYK/RGB documents preview spot inks; spot plates export (TIFF extra samples, PDF) | missing | #215 | compose multichannel.rs:1 spot preview for Multichannel only |
| FILE-215-18 | Save for Web per-slice settings remembered; Export As remembers and offers nearest scaling | partial | #215 | file_ui.rs:188 last_web in-session only; export_dialog.rs:16 fixed defaults |
| FILE-215-19 | Save As JPEG opens a JPEG Options dialog (quality 0-12, format options) | missing | #215 | ExportSettings has jpeg_quality 1-100 only (lib.rs:118) |
| FILE-215-20 | Artboard tool with handles and + buttons, Duplicate Artboard beside, vector PDF pages | partial | #215 | artboard_tool.rs: Artboard tool draws, selects, moves and resizes boards by their handles; Duplicate Artboard places the copy beside (#1584); no + buttons, raster PDF pages only |
| FILE-215-21 | Local libraries of colours, patterns, images and layers, as linked items or copies | missing | #215 | Only preset libraries (preset_store.rs) |
| FILE-216-1 | Every corpus fetched and enforced in CI, cached by pin | done | #216 | ci.yml has a corpus job (name: corpus tests) on every PR and on main: the corpora are pinned in xtask/src/corpus_pins.rs, sha256-verified after both a cache restore and a download, and a missing corpus fails the tests. The cache key follows the pins, so it is cached by pin as this row asks (#190) |
| FILE-216-2 | Large-file fixtures: 20 MP, 150-layer, generated 2 GB PSD and 4-5 GB PSB stress tests | partial | #216 | 4-5 GB PSB fixtures: psd example big_psb writes them (raw and RLE, planes past 2 GiB), io example big_psb_check opens and checks them; opt-in io tests/large_psb.rs (2.5 GB merged image). No 2 GB PSD, 150-layer or 20 MP fixture, nothing in CI |
| FILE-216-3 | TIFF, RAW and EXR corpora read by tests; per-camera RAW coverage reported | partial | #216 | EXR: corpus/exr (the OpenEXR deep test images, pinned) is read by codecs tests/deep_exr.rs in the CI corpus job, against the ID manifests of the upstream sidecars. corpus/tiff and corpus/raw are still copied in by hand and no test reads them; no per-camera RAW report |
| FILE-216-4 | Exports re-open in a third-party oracle editor | missing | #216 | The round trip is our export -> our import |
| FILE-216-5 | Save-path tests: layered TIFF, PDF, SVG, ACO/ASE, .shc, Smart Filters, atomic-save failure | missing | #216 | Save paths for layered TIFF, PDF, SVG, ACO/ASE, .shc and Smart Filters are absent. Atomic-save failure injection is covered: format/src/atomic/tests.rs write_failure_halfway_keeps_original and rename_failure_keeps_original (see BUG-203-6) |
| FILE-216-6 | 30-50 end-to-end workflow acceptance tasks driven through MCP or commands | partial | #216 | automation/tests/agent_tasks.rs: 10 MCP tasks |
Tools
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| TOOL-213-1 | Pencil tool: aliased 1 px painting, square pixel-aligned cursor | done | #213 | Tool::Pencil (B group): live aliased strokes on the pixel grid, square cursor, Auto Erase; pencil_tests.rs |
| TOOL-213-2 | Mixer Brush tool on the toolbar | done | #213 | Tool::MixerBrush in the Brush (B) flyout; Wet/Load/Mix/Flow/Sample All Layers options, command/control dispatch and selection-clipping tests |
| TOOL-213-3 | Patch tool with a live preview of the patched area | done | #213 | paint.patch (retouch_cmds/patch.rs, source/destination) + Tool::Patch (J group: lasso, then drag the selection); the drag shows the heal live (patch_preview.rs, coarse colour fit: 1000x1000 in 52 ms) |
| TOOL-213-4 | Content-Aware Move tool reusing inpaint completion | partial | #213 | paint.contentAwareMove (retouch_cmds/content_aware_move.rs: move/extend, Structure 1-7, Color 0-10, Sample All Layers; background job) + Tool::ContentAwareMove (J group: lasso, then drag). Missing: Transform On Drop, a live preview of the result (the drag shows the outline only) |
| TOOL-213-5 | Painting a type/shape/smart/fill layer offers to rasterize, then continues the stroke | done | #213 | rasterize_prompt.rs: OK rasterizes then paints (two history states), Cancel does nothing; drivable via ui.dialog.confirm |
| TOOL-213-6 | Fill dialog: contents, blend, opacity, preserve transparency; remembered; Shift+Backspace | done | #213 | fill_ui.rs dialog over edit.fill (fill_cmds.rs): all contents, mode, opacity, preserve; remembered in prefs; Shift+F5 / Shift+Backspace |
| TOOL-213-7 | Live stroke preview for every brush-based retouch tool | partial | #213 | Live (canvas.rs strokes_live): Brush, Pencil, Eraser, Blur, Sharpen, Smudge, Dodge, Burn, Sponge, Clone Stamp, Healing Brush, History Brush (#1026, #1040); the release commits the live stroke's pixels (P32). Not yet live: Spot Healing Brush, Mixer Brush, Background Eraser, Pattern Stamp |
| TOOL-213-8 | Clone Source: W/H link, drawn overlay (opacity/blend/clip/auto-hide), Alt target cursor | partial | #213 | Clone Source overlay drawn with opacity, inversion, circular clipping and auto-hide; W/H link, custom-tip clipping and overlay blend modes remain missing |
| TOOL-213-9 | Eyedropper comparison ring (new colour above, current below) while sampling | done | #213 | eyedropper_ring (canvas.rs): the sampled colour on the upper arc, the foreground below, drawn for the Eyedropper and Alt-sampling while the button is held; the Precise-cursor preference keeps the crosshair |
| TOOL-213-10 | Ctrl+Alt drag resizes brush size/hardness with an on-canvas readout | done | #213 | brush_resize.rs (#297, #370): Alt+right-drag, or Control+Option-drag on macOS (Control+Alt everywhere), sets diameter horizontally and hardness vertically, circle plus Diameter/Hardness readout, one coalesced tools.setBrush, no paint or history; 9 tests there |
| TOOL-213-11 | Editable pressure curve, global and per control | partial | #213 | Global: Preferences › Tools › Pressure Curve (prefs tools.pressureCurve; monotone cubic, engine prefs::PressureCurve; applied to every pen sample in ui-egui stylus.rs before any brush; editor pressure_curve_ui.rs with Linear/Soft/Hard presets). Missing: per-control curves (each brush dynamic's own response to pressure, tilt, rotation) |
| TOOL-213-12 | Import an image file as a brush tip | missing | #213 | Only brush.defineFromSelection and brush.presets.importAbr |
| TOOL-213-13 | Brush regression tests: no seams, hard edge, zero-scatter defaults, ABR never black | done | #213 | Seam, hard-edge and zero-scatter tests in brush_tests.rs; ABR tip colour test in abr_map/tests.rs |
| TOOL-213-14 | Tablet: no OS arrow cursor over the canvas on macOS, Windows and Linux | partial | #213 | Brush tools set CursorIcon::None (canvas.rs:1259-1275); not verified per OS |
| TOOL-213-18 | Pattern Stamp tool: sibling of Clone Stamp in the S flyout | done | #213 | Tool::PatternStamp in the S group; paint.patternStamp stamps the Patterns-panel tile with Aligned and Impressionist (an approximation: it jitters each dab's tile phase, not Photoshop's smeared impressionist look); engine tests cover depths, seams, hostile input |
| TOOL-214-1 | Select and Mask workspace: view modes, Refine Edge brush, 6 outputs | partial | #214 | select.refineEdge dialog with a live preview in Photoshop's view modes: Onion Skin, Marching Ants, Overlay, On Black, On White, Black & White, On Layers, per-view Opacity, Show Edge (J), Show Original (P), F cycles, X shows the image (select_mask_view.rs, #1754); no workspace layout or Refine Edge brush |
| TOOL-214-2 | Marquee: Shift square, Alt from centre, live W x H readout | done | #214 | canvas.rs marquee_corners: Shift square or circle, Alt from the centre, live W x H readout beside the cursor (marquee_readout); marquee_tests.rs |
| TOOL-214-3 | Color Range test: Sampled Colors at Fuzziness 0 selects the exact match only | partial | #214 | Fuzziness-0 tests cover midtones/shadows only (selection_cmds.rs:444-447) |
| TOOL-214-4 | Move tool: box-select layers, Auto-Select hover outline, double-click fit, drag to a tab | partial | #214 | Auto-Select hover outline (Preferences › Tools › Show Highlight on Rollover, #2834) and box-select layers from an empty spot (move_box.rs, layer.selectInRect, #2844) done; double-click fit and drag to a tab open |
| TOOL-214-5 | Right-click menu during Free Transform (scale, rotate, skew, warp, flip, rotate 90/180) | done | #214 | canvas_tool_menu.rs TRANSFORM_MENU: modes, Content-Aware Scale / Puppet Warp greyed, Rotate 180 / 90 CW / 90 CCW and Flip turn the open box (transform_tool::intercept, #2832) |
| TOOL-214-6 | Puppet Warp mesh follows the object outline (constrained triangulation) | missing | #214 | algo/puppet.rs:136 builds a block-grid mesh |
| TOOL-214-7 | Content-Aware Scale through the Free Transform box with a live proxy preview | partial | #214 | edit.contentAwareScale (edit_menu_cmds.rs:899) behind a parameter dialog |
| TOOL-214-8 | A measured ruler angle prefills Image Rotation > Arbitrary (0.01 deg) | done | #214 | analysis_ui.rs: image.rotation.arbitrary opens seeded with the ruler angle and the direction that straightens it; arbitrary_rotation_starts_at_the_ruler_angle checks 5.7106 deg within 1e-3 and cw/ccw for three measured lines |
| TOOL-214-9 | Crop: W x H x Resolution with units, saved crop presets, Straighten in the options bar | done | #214 | Ratio dropdown and fields, Straighten (TOOL-214-21); W x H x Resolution with units, built-in size presets and Front Image (crop_size.rs, image.crop targetWidth/targetHeight/resolution, #2443); New / Delete Crop Preset save and delete crop tool presets (crop_presets.rs, tool.presets.* with tool crop, persisted with the preferences) |
| TOOL-214-10 | Pen: Alt breaks a handle, Shift 45 deg, Ctrl direct-selects, Backspace, close-drag handle | partial | #214 | Ctrl/Cmd = Direct Selection and Alt = Convert Point on existing paths (direct_select.rs, #790); no Shift 45 deg, Backspace or Alt while drawing |
| TOOL-214-11 | Stroke Path dialog: pick any brush-based tool, Simulate Pressure | partial | #214 | path.stroke brush, pencil, eraser (vector_cmds.rs:1170); brush-only button, no dialog |
| TOOL-214-12 | Shapes: on-canvas corner radius handles, shape feather, selection to shape with tolerance | partial | #214 | select.convertToShape with tolerance (vector_cmds.rs:1256); radius as a field only |
| TOOL-214-13 | Mask Properties: Density and Feather controls | partial | #214 | Vector-mask density/feather in the engine (vector_cmds.rs:948); no UI controls |
| TOOL-214-14 | Vanishing Point window: draw and edit planes, paste, clone in perspective | partial | #214 | filter.vanishingPoint (vp_cmds.rs:15) behind a parameter dialog |
| TOOL-214-15 | Content-Aware Fill workspace: paint the sampling area, preview, rotate/scale/mirror, new layer | partial | #214 | Engine sampling auto, rect, custom (edit_menu_cmds.rs:324); generic dialog |
| TOOL-214-16 | Liquify: redo (Cmd+Shift+Z) | done | #214 | liquify_ui.rs supports bounded redo through Cmd+Shift+Z and the control channel; new strokes clear redo |
| TOOL-213-17 | Red Eye tool: click a pupil to neutralize the red reflection | done | #213 | paint.redEye (redeye_cmds.rs) + Tool::RedEye in the J flyout; Pupil Size and Darken Amount; catchlights kept; a miss is an error |
| TOOL-214-17 | Rotate View tool: drag to turn the view, Reset View, Match Rotation copies the angle | done | #214 | Tool::RotateView in the Hand flyout; view.rotateView / view.resetView; Match Rotation copies views[].rotation; enhanced_controls.rotate_view_with_trackpad is read |
| TOOL-1649-1 | Eyedropper Sample Size (Point, 3×3 … 101×101 Average), Sample layers, Show Sampling Ring | done | #1649 | document.sampleColor (sample_cmds.rs): average of the square clipped to the canvas, in the document's model and depth; eyedropper_ui.rs options bar; shared with Alt-click, dialog eyedroppers and the Info panel; ui.set eyedropperSampleSize/eyedropperSample/eyedropperRing. Color Sampler tool is separate (#1046, sampler_cmds.rs) |
| TOOL-214-20 | Crop overlay options: Rule of Thirds, Grid, Diagonal, Triangle, Golden Ratio, Golden Spiral; Auto / Always / Never Show Overlay; O cycles the overlay, Shift+O its orientation | done | #1919 | crop_overlay.rs: unit-square guides shared by upright and turned frames (canvas.rs), options-bar menu, O / Shift+O ahead of the tool keys in shortcuts.rs; ToolOptions crop_overlay / crop_overlay_show / crop_overlay_orientation, ui.set cropOverlay / cropOverlayShow / cropOverlayOrientation |
| TOOL-214-18 | Crop: drag outside the frame to rotate it (Shift 15 deg steps, angle readout); Enter rotates and crops in one step | done | #1792 | image.crop angle param (image_cmds.rs: Image Rotation > Arbitrary + crop, one undo step); crop_ui.rs turns, moves and resizes the frame along its axes (Classic Mode; default mode turns the image instead, TOOL-214-23); UiState.crop_angle |
| TOOL-214-19 | Crop: arrow keys nudge the crop box (Shift 10 px), X swaps portrait/landscape, W x H readout while drawing or resizing | done | #1919 | crop_ui.rs keys/nudge/swap_orientation (ahead of the selection nudge and tools.swapColors in shortcuts.rs; X also swaps the options-bar ratio) and sizing (canvas.rs draws it with the marquee readout); turned frames included |
| TOOL-214-21 | Crop: Straighten button in the options bar and Cmd/Ctrl-drag draw a line; the frame turns so the line ends up level (or plumb when nearer vertical), with an angle readout | done | #1919 | crop_straighten.rs: one-shot options-bar button, Cmd/Ctrl held is a temporary Straighten (ui.pointer command flag); the Ruler's straighten_angle convention; the frame is fitted to the largest of its proportions inside the image (Classic Mode); a pending edit committed by Enter |
| TOOL-214-22 | Crop gear menu: Show Cropped Area (H), Enable Crop Shield with Match Canvas / custom colour, opacity and Auto Adjust Opacity | done | #1919 | crop_shield.rs: fill() resolves the shield (canvas.rs paints it around upright and turned frames), gear popup in the options bar, H ahead of the Hand key in shortcuts.rs, Custom… opens the Color Picker; ToolOptions crop_shield, ui.set cropShield. Use Classic Mode / Auto Center Preview: TOOL-214-23 |
| TOOL-214-23 | Crop: Photoshop's default mode (the image turns and moves behind an upright box, Auto Center Preview keeps a resized box's centre in place and centres a drawn one) and Use Classic Mode (P) in the gear menu | done | #1919 | crop_mode.rs: the crop model (crop_rect + crop_angle) and image.crop are shared by both modes; default mode sets the view's rotation to -crop_angle while the tool shows a frame (picking the tool, in either mode, turns a Rotate View turn upright about the view centre, not restored after Enter/Esc or by P; Classic Mode keeps the camera upright with the frame drawn turned; as Photoshop 25.1), pivoting so the box stays put, moves view and frame together for drags inside and arrow keys, keeps a resized box's centre in place live (Auto Center Preview; a drawn box is centred on release, a moved one never), readout = the image's turn; gear menu order and Classic Mode greying as Photoshop 25.1; CropShield classic_mode / auto_center_preview (ui.set cropShield), P ahead of the Pen key |
| TOOL-1053-1 | Shape stroke controls: solid/dashed/dotted, custom dash/gap/offset, caps, joins, alignment, miter limit and opacity | done | #1053 | shape_stroke_ui.rs: shared staged editor, renderer samples, cached Properties canvas preview; existing shape.create/edit/info; serde tool defaults, ui.set shapeStroke; pcraft and vstk style round trips. Gradient fill UI and saved stroke libraries remain open; Line retains filled-bar geometry |
| TOOL-213-19 | Remove tool: paint over an object (or circle it) and it is filled from its surroundings | partial | #213 | paint.remove (retouch_cmds/remove.rs, background job) + Tool::Remove in the J flyout; loops close over what they enclose; fill by non-local patch completion with texture features, onion-peel start, best-patch copy and gradient-domain seam hiding (algo/nonlocal.rs, Newson et al. IPOL 2017); sparse holes in tiles (algo/remove.rs). remove_quality on 20 holes in public-domain photos: the fill keeps 0.89 of the texture it replaces (0.54 with Content-Aware Fill's completion), edge gradient 1.03 of the original's; remove_bench 24 MP: 100 px ring 120 ms, 1000 px ring 2.5 s, 3000 px line 1.7 s. Missing: generative AI mode, Remove after each stroke off, live preview of the result |
| TOOL-1045-1 | Color Replacement tool on the toolbar | done | #1045 | Tool::ColorReplacement in the Brush (B) flyout after the Mixer Brush; Mode/Sampling/Limits/Tolerance/Anti-alias options drive paint.colorReplacement with the foreground colour; live preview (LiveDab with the stroke's finishing dab), one History step; color_replacement_tests.rs |
Workspace and UI
Settings that do nothing: 32 of 154 (target 0, #204). Counted by the prefs audit (xtask/src/scorecard.rs): a Preferences field counts as read when code outside the prefs model (prefs.rs except its impl Session blocks; tests excluded; the schema-driven dialog never names a field) accesses section.field, or .section and .field in the same file, or calls a section method that reads self.field. It is a heuristic lower bound: a generic name can look read when it isn't.
Unread: general.color_picker, general.animated_zoom, general.zoom_resizes_windows, workspace.auto_collapse_icon_panels, workspace.auto_show_hidden_panels, workspace.enable_floating_document_window_docking, workspace.enable_narrow_options_bar, file_handling.image_previews, file_handling.save_in_background, file_handling.ignore_exif_profile_tag, file_handling.maximize_psd_compatibility, performance.cache_levels, performance.legacy_compositing, scratch_disks.disks, units_and_rulers.column_width, units_and_rulers.gutter, plug_ins.show_extension_panels, plug_ins.allow_scripts_to_connect, plug_ins.generator_enabled, type_.missing_glyph_protection, type_.text_engine, type_.recent_fonts, enhanced_controls.scrubby_slider_acceleration, enhanced_controls.touch_gestures, raw_defaults.color_space, raw_defaults.bit_depth, raw_defaults.resolution, raw_defaults.sharpen_for, raw_defaults.open_as_smart_object, raw_defaults.apply_auto_tone, integrations.allow_agent_control, integrations.control_port.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| UI-217-1 | Tool options, last Export As / Save for Web / Fill settings and font recents persist | partial | #217 | Fill (fill_ui.rs:72) and Liquify (liquify_ui.rs:64, #418) keep their settings in Preferences (prefs.dialogs); no tool_options/export fields in Preferences; export_dialog.rs:21 png/85. Each painting tool keeps its own brush (paint_mouse.rs sync_tool_brush, #218), but those live in UiState, so they are lost at quit like the other tool options |
| UI-217-2 | Window size and dock width remembered across restarts | done | #217 | eframe persistence (ui.ron): persist_window for the window, egui memory for the dock panel width; window_and_panel_geometry_survive_a_restart asserts the round trip (apps/photocraft/src/main.rs; b07a2e5b, #587) |
| UI-217-3 | File > Open starts in the last-used folder | done | #217 | file_dialog.rs last_used_dir: open dialogs ask with initial_dir (the most recent file's folder) and apps services.rs passes it to rfd set_directory; open_starts_in_the_last_used_folder + last_used_dir_takes_the_most_recent_parent |
| UI-217-4 | Preferences all take effect; recent files persist and honour recent_file_count (#204) | missing | #217 | See the prefs audit and BUG-204-* |
| UI-217-5 | Panel groups can float (menu + drag out of the dock) and are restored at launch | missing | #217 | No floating groups in dock.rs/panels.rs |
| UI-217-6 | Document tabs: overflow list, context menu (Close/Others/All/Reveal), drag to reorder | done | #217 | canvas.rs: Close/Others/All menu with unsaved prompt; Reveal reuses file.revealInFinder (engine layer_menu_cmds::reveal); the » chevron (tab_strip) lists overflow; dragging a tab reorders via document.move |
| UI-217-7 | Command palette shows recently run commands on an empty query | done | #217 | palette.rs: note_recent from menus::invoke keeps the last 8 commands (dedup, newest first); an empty query lists them via recent_hits; the_palette_records_and_replays_recent_commands_on_an_empty_query covers record -> empty query -> replay |
| UI-217-8 | Tool-specific canvas right-click menu; right-click spelling suggestions in text fields | partial | #217 | paint_mouse.rs brush picker and canvas_tool_menu.rs selection actions; text_field_menu.rs gives every text field its right-click Undo/Cut/Copy/Paste/Delete/Select All menu (#2921); other tools missing; spelling suggestions (edit.checkSpelling suggest) not in the text-field menu yet |
| UI-217-9 | UI scale presets 100/115/125/150/200 % through zoom_factor, remembered | done | #217 | prefs::UiScale offers Auto plus 75 to 300 percent (13 presets, more than the five this row lists); prefs_ui.rs display_scale resolves it (Auto follows the system scale, fractional and unscaled 4K included, #1072/#1278) and sync_display_scale applies it with ctx.set_zoom_factor. It is a preference, so it is remembered. The zoom_factor assert in input_tests.rs is about canvas zoom not scaling the interface, not about this setting |
| UI-217-10 | Appearance: animation toggle, canvas pattern toggle per theme, start-screen recents | missing | #217 | No such prefs; start screen (canvas.rs:825) has no recents |
| UI-217-11 | Soft dark and soft light themes with picker previews; outlined knobs in light themes | missing | #217 | theme.rs ThemeKind has twelve themes (the four Pro brightness levels, Studio and Studio Light, Classic, Adwaita and Adwaita Dark, Solarized Dark, Breeze Light and Breeze Dark); no Soft dark or Soft light, and no outlined knobs in the light themes |
| UI-217-12 | Save custom New Document presets; a real Recent category | partial | #217 | document_preset_cmds.rs and new_doc_ui.rs: named presets save/select/delete through preferences; Recent remains a static list plus Clipboard |
| UI-217-13 | Numeric fields accept arithmetic and units; scrubby labels; double-click resets a slider | partial | #217 | widgets.rs:132 DragValue scrubs and takes + - * / arithmetic; no units in expressions, label scrub or reset |
| UI-217-14 | Layers panel: wheel steps blend modes, drop on New/Trash duplicates/deletes, checker behind art | partial | #217 | panels.rs: the wheel steps the blend-mode dropdown (widgets::dropdown_wheel_hovered, #1747); trash is click-only, no drop on New/Trash, no checker behind art |
| UI-217-15 | Curves: smoothing / pencil mode, black/grey/white eyedroppers, tooltips | partial | #217 | adjust_dialog.rs adds black/gray/white eyedroppers; smoothing and pencil modes remain missing |
| UI-217-16 | Hue/Saturation: Saturation and Lightness sliders get hue-following colour tracks | done | #217 | saturation_track/lightness_track (adjust_editors.rs): grey -> the hue and black -> the hue -> white, recolouring as the Hue slider moves; saturation_and_lightness_tracks_follow_the_hue |
| UI-217-17 | Color panel: wheel plus RGB/HSB/CMYK/Lab/Gray/Web slider modes; resizes with its group | missing | #217 | panels.rs:1837 single field + hex/RGB readout |
| UI-217-18 | Alt+wheel zooms in gentle ~5 % steps; zoom controls uncramped | missing | #217 | canvas.rs:1107-1125 no Alt handling |
| UI-217-19 | Shortcut hints rendered per platform; Reveal label per OS | done | #217 | hints and tooltips go through shortcuts::pretty (6266b7a9, #262; pretty_uses_the_platform_notation); menus::reveal_label names Reveal in Finder per OS (Show in Explorer / Show in Folder) in the Smart Objects menu and the tab menu; reveal_label_names_the_platform_file_manager + smart_objects_reveal_item_names_the_platform_file_manager |
| UI-217-20 | 'Show rulers in new documents' preference; guides stored as f64 | done | #217 | View › Rulers is remembered in unitsAndRulers.showRulers and applied at launch and on change (#2918; rulers_open_as_last_set_after_a_restart); doc Guides are Vec through New Guide, .pcraft and PSD 1032 (guides_keep_sub_pixel_positions_through_pcraft_and_psd) |
| UI-217-21 | One Preset Manager for every preset kind with folders, search, reorder, load/save sets | partial | #217 | edit_menu_cmds.rs:714 covers brushes, custom shapes, patterns with reorder |
| UI-217-22 | Stamp Visible into a new layer keeping originals; Alt+Merge keeps originals | done | #217 | stamp_cmds.rs layer.stampVisible (Cmd+Alt+Shift+E) merges every visible layer into a new layer above the active one and keeps the originals; layer.stampDown (Cmd+Alt+E) too. As in Photoshop they have no menu item of their own: Alt + Layer > Merge Visible / Merge Down / Merge Layers run them (stamp_cmds::alt_variant, menus.rs); stamp_tests.rs alt_click_on_merge_items_stamps and cmd_alt_shift_e_stamps_visible_and_can_be_rebound |
| UI-217-23 | Layer clipboard: copy/paste whole layers (masks, styles, type) between documents | partial | #217 | layer.copyToDocument copies layers into another open document keeping names, content, masks, styles and blending, converting to the destination's profile and depth, as one history step (layer_copy_cmds.rs, #589). It is the command a drag between documents dispatches; there is still no copy/paste layer clipboard |
Type
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| TYPE-218-1 | Type on a path: path shape, glyphs along the curve, caret follows, PSD round trip | missing | #218 | doc text.rs:209 TextShape has Point and Box only |
| TYPE-218-2 | Paragraph direction (Auto/LTR/RTL) in the panel and options bar; visual RTL caret | missing | #218 | Engine has TextDirection (text.rs:155); no UI |
| TYPE-218-3 | Thai/Khmer/Indic fallback fonts; golden shaping tests for complex scripts and RTL | partial | #218 | Thai is covered: THAI_FAMILIES (fonts.rs) lists the Windows, macOS and Linux families and thai_in_latin_font_falls_back_to_installed_thai_font checks a sample with above and below marks (#1909); Arabic and Hebrew fall back too. Khmer and Indic have no fallback list, and there are no per-script golden shaping tests (only a bidi test) |
| TYPE-218-4 | Variable font axis sliders from the font's axes; opsz follows the point size | partial | #218 | type_cmds.rs:184 accepts variations; FaceInfo.axes unused in the UI |
| TYPE-218-5 | OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus | partial | #218 | Type > OpenType toggles (type_extra_cmds.rs:512); no panel buttons or submenus |
| TYPE-218-6 | Manual pair kerning (Alt+arrows), optical kerning rendered, both round-trip PSD (#206) | done | #218 | Alt+Left/Right at a collapsed caret kerns the pair (type_tool.rs kern_pair, 20 per step and 100 with Cmd; type_tool_alt_arrows_kern_the_pair). Optical kerning is computed from the glyph outlines in text/src/optical.rs and applied by layout.rs when the run's mode is Optical (optical_matches_photoshop, optical_kerning_tightens_open_pairs). Both modes and manual values round-trip through PSD (io/tests/psd_kerning.rs; the optical mode travels in Txt2, txt2_carries_optical_kerning) |
| TYPE-218-7 | Warp Text dialog previews live and opens with the layer's current warp | missing | #218 | view_cmds.rs:834 always seeds arc, bend 50 |
| TYPE-218-8 | Text editing: select all, double/triple/quadruple-click selection, paste, with tests | partial | #218 | Cmd+A, paste and double-click exist; no triple/quadruple click, no tests |
| TYPE-218-9 | Offer 'Rasterize and continue?' for Distort/Perspective/Warp on live type | missing | #218 | No such prompt |
Automation
Menu wiring (docs/parity-checklist.md): 631 of 631 menu items live (100.0%). This counts dispatch, not behaviour.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| AUTO-1 | Every menu item dispatches a command id (UI, CLI, control channel and MCP share them) | done | docs/parity-checklist.md: all menu items live; menus.rs is_live | |
| AUTO-219-1 | Record actions in a document-independent form (implicit layer, merged type keystrokes) | missing | #219 | actions.rs:47 copies raw journal entries with explicit layer ids |
| AUTO-219-2 | Actions panel: step and dialog toggles, reorder, delete a step, Stop, sets, .atn import | missing | #219 | Panel deletes whole actions only (actions.rs:146) |
| AUTO-219-3 | actions.record/stop/play/list commands for agents and the CLI | done | #219 | actions.list/get/record/stop/play/delete in actions_cmds.rs; the panel, CLI and MCP share them. Nested steps pass the session authorize hook. |
| AUTO-219-4 | Replay tests for crop, filters, selections and styled text on a second document | missing | #219 | One replay test (actions.rs:171: new layer, rect select, fill) |
| AUTO-219-5 | Batch: subfolders, open documents, naming template, progress/cancel, error log, stop | partial | #219 | file_cmds.rs:590-631 folder/array input, per-file errors; no recursion, template or cancel |
| AUTO-219-6 | Plug-in ABI v2: panel kind, progress/cancel calls, read other layers, Last Filter repeats | missing | #219 | runtime.rs:15 ABI_VERSION = 1; manifest.rs:18 Kind::Filter only |
| AUTO-219-7 | Local ONNX models behind a flag, managed in Preferences; subject select, background removal | missing | #219 | No ONNX runtime dependency |
| AUTO-219-8 | xtask check that every panel button routes through a command id | missing | #219 | No such xtask check |
| AUTO-219-9 | Headless commands for in-canvas type editing, Liquify/Puppet strokes, transform handles | done | #219 | type.hitTest, type.caret, type.navigate; filter.liquify strokes, edit.puppetWarp pins, type.edit, edit.transform |
| AUTO-219-10 | Agent acceptance suite grown to 30-50 workflows | partial | #219 | automation/tests/agent_tasks.rs runs 10 workflows over MCP |
Reliability
Never-crash attribute on 28 of 28 crate roots.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| REL-1 | panic_hunt fuzzes every command with adversarial params | done | engine/tests/panic_hunt.rs: every command, own thread, 4 s timeout; #[ignore], run in the gate | |
| REL-2 | Every workspace crate carries the never-crash deny(clippy::unwrap_used, ...) attribute | done | Counted automatically above (crate roots under crates/ and apps/); testkit gained it in #221 | |
| REL-3 | The app catches escaped panics around command dispatch and import/export | done | a_panic_in_a_command_... runs the test-only test.panic spec through Session::execute and asserts Err plus an unchanged document (engine jobs_tests.rs); the worker guard is covered by a_panic_in_the_worker_...; crash_guard.rs guard_turns_a_panic_into_an_error covers import/export (apps/photocraft) |
|
| REL-4 | Atomic save (temp, fsync, rename) for every document write | done | #203 | photocraft_format::atomic_write (format/src/atomic.rs, #230): same-dir temp, sync_all, rename with Windows PermissionDenied retry, Unix dir fsync; every document write site routes through it and format/src/atomic/tests.rs failure-injects write, rename and read-only-dir errors |
| REL-212-1 | Fallible allocation for paste/erase/fill/transform/save/import, clear out-of-memory error | partial | #212 | Raster region/tile buffers, paste/erase/fill/transform and warp output use fallible reservations; save/import boundaries preserve existing state, but encoder/decoder internals still need a broader fallible-allocation audit |
| REL-212-2 | Export switches to PSB when the estimated file size exceeds 2 GB | done | #212 | PSD export uses checked size estimates for raster, masks, channels, and retained metadata |
| REL-212-3 | Tests: an allocation-limit injection makes each command Err with the document unchanged | partial | #212 | Engine failpoint tests cover paste, erase, fill, transform, import and save transaction boundaries; codec-level allocator failure coverage remains open |
Distribution
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| DIST-1 | macOS universal build (aarch64 + x86_64), signed and notarized | done | release.yml:66,75,93 (macos-15); 0.2.0 shipped notarized | |
| DIST-2 | Windows x64 and x86 builds | partial | release.yml:113-114; signing skipped with a warning until the signing material exists | |
| DIST-3 | Linux x86_64 and aarch64: AppImage, deb, rpm, tar.gz | done | release.yml:162-163,186 | |
| DIST-4 | Flatpak single-file bundles, x86_64 and aarch64 | done | release.yml:204-216 (#163) | |
| DIST-5 | Web build (wasm32, trunk) | done | release.yml:312 | |
| DIST-6 | FreeBSD 14 x86_64 tar.gz (/usr/local tree) built in a FreeBSD VM | partial | release.yml:242-283 (freebsd job) + packaging/freebsd/package.sh; layout checked with --dry-run, not yet produced by a release run | |
| DIST-220-1 | Help > Check for Updates plus an opt-out periodic release-feed check on all desktops | missing | #220 | No update check in crates |
| DIST-220-2 | Windows ARM64 (aarch64-pc-windows-msvc) in the release matrix, signed like x64 | done | #220 | release.yml has { arch: arm64, target: aarch64-pc-windows-msvc } in the Windows matrix, and .github/workflows/windows-arm64.yml packages that MSI, installs it on a Windows 11 ARM64 runner, checks the installed binaries are ARM64 and runs the CLI natively before uninstalling. Signing takes the same path as x64: the secrets are optional and a missing one produces an unsigned artifact and a warning (see DIST-2) |
| DIST-220-3 | In-app bug report with reviewable diagnostics that opens a prefilled issue | missing | #220 | No bug report dialog in crates |
| DIST-220-4 | Translation coverage per language reported by xtask; RTL layout for Arabic and Hebrew | partial | #220 | cargo xtask i18n-coverage reports per-language coverage (#427); RTL languages mirror the shell (direction.rs), not yet panel interiors or dialogs |
| DIST-220-5 | Linux AppImage x86_64 + aarch64 ship; Flatpak kept; Flathub later (#173) | partial | #220 | AppImage and Flatpak ship (release.yml:162-216); Flathub not done |
Open bugs
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| BUG-203-1 | Every document write goes through one atomic_write (temp, fsync, rename) | done | #203 | photocraft_format::atomic_write (format/src/atomic.rs, #230/8227071); every write site routes through it: engine file_cmds.rs write_file and preset_store.rs, automation files.rs/headless.rs/workspace.rs, app services.rs write_atomic, format store.rs and autosave.rs |
| BUG-203-2 | Temp file in the same dir, sync_all, rename over target, fsync dir on Unix | done | #203 | atomic.rs: temp_name in the same dir, create_new + write_all + sync_all, Unix directory fsync after the rename |
| BUG-203-3 | Windows: retry the rename with backoff on PermissionDenied | done | #203 | atomic.rs RenameRetry::platform(): seven retries from 10 ms on Windows; retry_rename is shared with automation's cap-std workspace write |
| BUG-203-4 | Unwritable directory errors clearly, never falls back to writing in place | done | #203 | atomic.rs maps read-only dir, disk-full and cross-device rename errors to messages that say the original was not changed; there is no in-place fallback path |
| BUG-203-5 | On any failure the original file is untouched and the temp file removed | done | #203 | atomic.rs removes the temp file on every failure; tests assert the original bytes survive write and rename failures |
| BUG-203-6 | Failure-injection tests: mid-write, rename and read-only dir errors keep original bytes | done | #203 | format/src/atomic/tests.rs: Faulty AtomicFs injects mid-write StorageFull, rename failure and a read-only dir; locked-target retries, symlink and file-mode preservation covered too |
| BUG-204-1 | Interface prefs (UI scale, font size, channels in colour, sliders, tooltips) wired or hidden | done | #204 | prefs_ui.rs tick reads ui_scale (sync_display_scale), ui_font_size (theme::set_ui_font_size) and show_tooltips (sync_tooltips); show_channels_in_color is wired in channel_thumbs and the canvas channel view (#2385); dynamic_color_sliders is in prefs::HIDDEN_UNTIL_IMPLEMENTED |
| BUG-204-2 | workspace.large_tabs, animated_zoom, auto_show_home_screen wired or hidden | done | #204 | workspace.large_tabs controls document tab height in canvas.rs; auto_show_home_screen drives the Home screen; animated_zoom is in prefs::HIDDEN_UNTIL_IMPLEMENTED |
| BUG-204-3 | RAW prefs and ask_before_saving_layered_tiff read, or hidden | done | #204 | ask_before_saving_layered_tiff drives the TIFF Options prompt (ui-egui tiff_options_ui.rs); raw_defaults.open_in_camera_raw drives the Camera Raw dialog (camera_raw_ui.rs); the other raw_defaults fields are in prefs::HIDDEN_UNTIL_IMPLEMENTED |
| BUG-204-4 | Recent files persisted through prefs across restarts | done | #204 | crates/ui-egui/src/lib.rs::push_recent stores the list in preferences; prefs_ui::load restores it and recent_files_survive_a_restart_and_honour_the_count verifies reload |
| BUG-204-5 | Recent-files list honours recent_file_count (default 20) | done | #204 | lib.rs push_recent caps the list with recent_cap (recent_file_count); menus.rs covers default and custom limits |
| BUG-204-6 | Test: every Prefs field is read somewhere or on a hidden allowlist | done | #204 | crates/engine/tests/prefs_usage.rs every_preference_is_read_or_hidden: each field is read outside prefs.rs or listed in prefs::HIDDEN_UNTIL_IMPLEMENTED, never both |
| BUG-205-1 | Free Transform with a selection moves only the selected part of a linked mask | done | #205 | linked_mask_with_a_selection_moves_only_the_selected_region (transform_cmds.rs; 5a5facc, #242) |
| BUG-205-2 | Live transform preview draws the layer with its mask applied | done | #205 | transform_tool.rs preview_mask + preview_applies_the_linked_mask (5a5facc, #242) |
| BUG-205-3 | edit.transform honours target mask: transforms only a targeted, unlinked mask | done | #205 | targeted_unlinked_mask_transforms_alone and targeted_linked_mask_moves_with_its_layer (transform_cmds.rs) plus the through-the-box tests (transform_tool.rs; 5a5facc, #242) |
| BUG-205-4 | Test: a linked raster mask with no selection moves with the pixels | done | #205 | linked_raster_mask_moves_with_the_pixels (transform_cmds.rs): a reveal-all mask with a hidden half, linked, no selection; asserts the hidden half travels with the pixels and nothing hidden is left behind |
| BUG-205-5 | Tests: linked mask + selection stays aligned; unlinked stays; targeted mask only | done | #205 | All three in transform_cmds.rs: linked_mask_with_a_selection_moves_only_the_selected_region checks the mask value under each moved pixel, unlinked_mask_stays_put covers it with and without a selection, targeted_unlinked_mask_transforms_alone moves the mask while the layer bounds stay |
| BUG-206-1 | CharStyle models per-character manual kerning separately from the auto-kern mode | done | #206 | doc/src/text.rs CharStyle carries both: kerning: Kerning for the automatic mode (Metrics/Optical/Off) and kern: f32 for manual kerning in 1/1000 em between this character and the next, added on top of the pair's automatic kerning |
| BUG-206-2 | PSD import keeps Optical and manual Kerning values | done | #206 | io/tests/psd_kerning.rs manual_and_off_kerning_round_trip_through_psd and optical_kerning_round_trips_through_psd import an exported layer and compare every character's (mode, kern) pair; the optical mode travels in the document Txt2 block (#1348) |
| BUG-206-3 | PSD export writes the real AutoKerning mode and Kerning values | done | #206 | manual_and_off_kerning_round_trip_through_psd (io/tests/psd_kerning.rs) reads the AutoKerning flag and Kerning value of every character back out of the written TySh: (false, 100) after A, (true, -50) after T, and automatic kerning off where the style says so |
| BUG-206-4 | Alt+Left/Right at a collapsed caret adjusts pair kerning; Character panel field | done | #206 | type_tool.rs: Alt+Left/Right at a collapsed caret calls kern_pair (20 per step, 100 with Cmd); the Character panel field takes Metrics, Optical or a manual value (kerning_label / parse_kerning). Covered by type_tool_alt_arrows_kern_the_pair and kerning_field_values (input_tests.rs) |
| BUG-206-5 | Tests: PSD round trip of manual and Optical kerning, a real file in the oracle corpus | partial | #206 | Round trips covered by io/tests/psd_kerning.rs (manual, Off and Optical, plus hostile_kerning_values and txt2_extras_survive_a_resave_of_unchanged_text); corpus_text_layers reads real PSDs and applies their Txt2 optical kerning, but no oracle file is dedicated to kerning values |
| BUG-207-1 | Retouch tools respect the paint target (mask, alpha channel, Quick Mask) | done | #207 | run_stroke paints channel_cmds::target_surface; retouch_strokes_paint_the_targeted_mask (retouch_ui.rs) and engine retouch_cmds/tests.rs (e2444ce, #234) |
| BUG-207-2 | Sample All Layers applies to Blur, Sharpen and Smudge | done | #207 | retouch_ui.rs passes sampleAllLayers to paint.blur/sharpen/smudge; sample_all_layers_reaches_the_command (e2444ce, #234) |
| BUG-207-3 | Smudge mixes premultiplied RGBA (no dark fringe at transparent edges) | done | #207 | paint/src/retouch.rs premultiplies; smudge_into_transparency_leaves_no_dark_fringe (e2444ce, #234) |
| BUG-207-4 | edit.transform transforms a targeted alpha channel | done | #207 | edit.transform follows a targeted alpha channel or Quick Mask (explicit target since #242, Channels panel target with no target/layer param since #2835); Edit > Transform > Rotate/Flip are routed to the target too; transforms_follow_the_targeted_alpha_channel |
| BUG-207-5 | Tests: per-tool mask/channel targeting, Sample All Layers, no smudge fringe | missing | #207 | Only smudge_drags_colour_along_the_stroke (retouch.rs:407) |
| BUG-208-1 | Shift while dragging a Rect/Elliptical Marquee constrains to a square/circle | done | #208 | canvas.rs marquee_corners applies Shift via Drag::marquee_mods; marquee_tests.rs::shift_during_the_drag_draws_a_square_and_the_readout_follows covers the drag |
| BUG-208-2 | Alt while dragging a marquee draws from the centre | done | #208 | canvas.rs marquee_corners centres Alt drags; marquee_tests.rs::alt_draws_from_the_centre_and_shift_alt_a_centred_circle verifies rectangles and ellipses |
| BUG-208-3 | W x H px readout beside the cursor while dragging a marquee | done | #208 | canvas.rs marquee_readout draws the readout; marquee_tests.rs asserts dimensions while dragging and its removal after release |
| BUG-208-4 | Add/subtract/intersect from modifiers pressed before the drag, kept by a test | partial | #208 | Mods captured at press (canvas.rs:1679, used :1779); only a badge test (tool_feedback.rs:195) |
| BUG-208-5 | Kittest: Shift drag gives a square, Alt drag is centred, readout matches the size | done | #208 | crates/ui-egui/src/marquee_tests.rs covers Shift, Alt, selection bounds, W/H readout and drag completion |