* fix(compose): Preserve native CMYK fills through PSD round trips Share depth-quantized ink samples between rendering and PSD caches. Use the document profile for RGB fill colours and preserve valid imported caches. Uncached CMYK fills use CPU rendering. Cover all sample depths, gradient styles and tiled renders. Fix the real 16-bit gradient round trip and raise the psd-tools floor from 307 to 308. * perf(raster): Reuse exact 8-bit CMYK conversions within rows Cache the previous ink bytes and ICC result per row, with alpha decoded independently. Keep the 16/32-bit conversion loops unchanged. Matched 1 MP reads improve by 77% for smooth ramps, 33% with dithering and 10% for random colours. Bitwise regression coverage includes all depths, alpha and tile boundaries.
60 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, 15 not run, 7 not measurable yet (baseline 2026-10-05) |
| File compatibility | 5 | 11 | 11 | 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 | 20 | 15 | 6 | |
| Workspace and UI | 5 | 7 | 11 | settings that do nothing: 46 of 149 (target 0) |
| Type | 0 | 3 | 6 | |
| Automation | 3 | 2 | 6 | 627 of 627 menu items live (100.0%) |
| Reliability | 5 | 0 | 2 | never-crash lints on 28 of 28 crates; panic_hunt present |
| Distribution | 4 | 4 | 3 | |
| Open bugs | 22 | 2 | 8 |
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 |
| P31 | Camera Raw exposure change (proxy) | ≤ 10 ms | – | – | – | – | – | 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 |
| P30 | Liquify 800 px brush on 3500×2200 at full resolution | ≤ 1 ms per update | the Liquify proxy is capped at 1600 px and the stroke path lives in the UI tool | #211 |
| P32 | Brush release on a long stroke | no hitch at mouse-up | the live renderer and the release re-render are UI-side; needs a frame harness | #211 |
| 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 | missing | #215 | doc effects.rs:57 Contour model only |
| 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_ui.rs:1 names/properties only; no tool, + buttons or duplicate |
| 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 | missing | #216 | No corpus step in ci.yml; in progress in #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 | Features absent; no atomic-save failure-injection test |
| 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 | missing | #213 | Live stroke only for Brush and Eraser (canvas.rs:1684) |
| 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 | Opens the generic select.refineEdge dialog (menus.rs:308) |
| 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 | missing | #214 | move_mods.rs covers Shift/Alt/arrows only |
| TOOL-214-5 | Right-click menu during Free Transform (scale, rotate, skew, warp, flip, rotate 90/180) | missing | #214 | No context menu in canvas.rs or transform_tool.rs |
| 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) | missing | #214 | Arbitrary rotation is a generic dialog; ruler only feeds straighten |
| TOOL-214-9 | Crop: W x H x Resolution with units, saved crop presets, Straighten in the options bar | partial | #214 | Ratio dropdown and fields, Straighten (TOOL-214-21); no W x H x Resolution with units or saved presets |
| 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 still missing |
| 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 visuals); 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. No Classic Mode / Auto Center Preview (PhotoCraft is always Classic Mode) |
| 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 |
Workspace and UI
Settings that do nothing: 46 of 149 (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.beep_when_done, general.resize_image_during_place, general.always_create_smart_objects_when_placing, general.animated_zoom, general.zoom_resizes_windows, interface.dynamic_color_sliders, workspace.auto_collapse_icon_panels, workspace.auto_show_hidden_panels, workspace.enable_floating_document_window_docking, workspace.enable_narrow_options_bar, tools.enable_flick_panning, tools.vary_round_brush_hardness_on_hud, tools.show_transformation_values, 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, cursors.brush_preview_color, units_and_rulers.type_units, units_and_rulers.column_width, units_and_rulers.gutter, units_and_rulers.print_resolution, plug_ins.show_extension_panels, plug_ins.allow_scripts_to_connect, plug_ins.generator_enabled, type_.smart_quotes, type_.missing_glyph_protection, type_.show_font_names_in_english, type_.text_engine, type_.font_preview, type_.recent_fonts, enhanced_controls.scrubby_slider_acceleration, enhanced_controls.touch_gestures, enhanced_controls.zoom_with_trackpad_pinch, 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; other tools and text spelling menus missing |
| UI-217-9 | UI scale presets 100/115/125/150/200 % through zoom_factor, remembered | missing | #217 | prefs.rs:61 UiScale unused; input_tests.rs:71 asserts zoom_factor stays 1.0 |
| 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:28 five 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 | missing | #217 | ~33 hard-coded modifier glyph strings in ui-egui; menu_catalog.rs:341 one label |
| UI-217-20 | 'Show rulers in new documents' preference; guides stored as f64 | missing | #217 | No rulers pref; doc lib.rs:523 guides are Vec |
| 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 | missing | #217 | Only layer.mergeVisible (edit_cmds.rs:416) |
| UI-217-23 | Layer clipboard: copy/paste whole layers (masks, styles, type) between documents | missing | #217 | Only copy/paste layer style (extra_cmds.rs:592) |
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 | missing | #218 | fonts.rs:28 fallback covers Arabic/Hebrew; no per-script golden 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) | missing | #218 | Optical renders as Metrics (text.rs:21); Alt+arrow moves by word |
| 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.md): 627 of 627 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.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 | missing | #212 | try_reserve only in io/flat.rs:221; codec max_alloc limits cover decode only |
| 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 | missing | #212 | Only codec/raw max_alloc tests (codecs/tests/limits.rs:48) |
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 | missing | #220 | release.yml:113-114 only x86_64 and i686 msvc |
| 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); no RTL layout yet |
| 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; 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 | partial | #205 | Implemented at transform_cmds.rs:127; no raster-mask test |
| BUG-205-5 | Tests: linked mask + selection stays aligned; unlinked stays; targeted mask only | missing | #205 | Unlinked case honoured (m.linked check, l.128) but untested; the other two not implemented |
| BUG-206-1 | CharStyle models per-character manual kerning separately from the auto-kern mode | missing | #206 | doc/text.rs:76 only kerning: Kerning enum (Metrics/Optical/Off) |
| BUG-206-2 | PSD import keeps Optical and manual Kerning values | missing | #206 | text/psd.rs:264 maps anything not Off to Metrics; Kerning value ignored |
| BUG-206-3 | PSD export writes the real AutoKerning mode and Kerning values | missing | #206 | text/psd.rs:380-381 AutoKerning bool only; Kerning hard-coded 0 |
| BUG-206-4 | Alt+Left/Right at a collapsed caret adjusts pair kerning; Character panel field | missing | #206 | type_tool.rs:896-912 only a mode combo |
| BUG-206-5 | Tests: PSD round trip of manual and Optical kerning, a real file in the oracle corpus | missing | #206 | No kerning round-trip test |
| 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 | missing | #207 | transform_cmds.rs transform() has no target handling |
| 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 |