Files
photocraft/docs/scorecard.md
T
15aac8d7b6 Type: type on a path, glyphs follow the curve (#4365) (#4377)
* Type: type on a path, glyphs follow the curve (#4365)

Add TextShape::Path (path, start offset, flip). The text is laid out as one
point-text line and each glyph is placed rigidly on the flattened path by arc
length, turned to the tangent at its centre; glyphs past the end are hidden and
a degenerate path falls back to straight text. New commands type.attachToPath
(work path or shape layer subpath) and type.detachFromPath. TextShape is no
longer Copy. .pcraft round-trips the new variant.

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

* Automation policy: type.attachToPath/detachFromPath take document vector paths, not host files (#4365)

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

* docs: regenerate scorecard after merging main

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 23:11:09 -04:00

75 KiB
Raw Blame History

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 13 8 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 28 14 1
Workspace and UI 13 6 4 settings that do nothing: 26 of 156 (target 0)
Type 4 3 2
Automation 5 2 4 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 Save for Web settings survive a restart through the remembered dialog settings in prefs.dialogs (web_cmds::remember_web_settings stores them on a complete export, never the destination; file_ui.rs open_web seeds from the session's last export, else the preferences). export_dialog seeds Export As the same way (seed_remembered: format, quality, transparency, scale, metadata, TGA bits) and confirm remembers them as used. Missing: Export As nearest-neighbour scaling
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 (⇧ keeps proportions), its + buttons add a board or (⌥) a copy beside the selected one (#3033); Duplicate Artboard places the copy beside (#1584); 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 partial #216 Every save path that exists is tested by saving and reading back: layered TIFF io tests/tiff_layers.rs (every_compression_keeps_the_layers, advanced_blending_survives_layered_tiff_in_both_byte_orders), print PDF engine print_cmds/tests.rs (print_dry_run_renders_a_pdf_and_reports_the_platform_backend decodes the image back), ACO/ASE engine swatch_cmds/tests.rs (aco_round_trip_keeps_16_bit_values_and_books, ase_round_trip_keeps_groups_floats_and_kinds), .shc psd shc.rs shc_round_trips_and_rejects_hostile_input and engine layer_style.rs contour_presets_and_custom_curves_apply_in_one_undo_step, Smart Filters engine tests/smart_psd.rs (smart_filters_survive_psd_in_every_colour_model), atomic-save failure format/src/atomic/tests.rs (BUG-203-6). There is no SVG writer (SVG opens only) and no Photoshop PDF save (FILE-215-10), so those have nothing to test yet; the row stays partial until they exist
FILE-216-6 30-50 end-to-end workflow acceptance tasks driven through MCP or commands partial #216 automation/tests/agent_tasks.rs: 20 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, Transform On Drop's scaleX/scaleY/rotate; background job) + Tool::ContentAwareMove (J group: lasso, then drag; Transform On Drop puts a scale/rotate box on the dropped patch, cam_transform.rs: Enter commits as one step, Esc cancels). cam_preview.rs: once the pointer or the box rests, the result shows (a cancellable background job on a smaller sampling window, small selections only, #4385). Missing: a progressive preview for big selections
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, auto-hide and the Normal/Darken/Lighten/Difference blend against the image under the brush (canvas.rs draw_clone_preview, clone_source::overlay_blend); a shaped tip (sampled bitmap, rotation, roundness) clips the overlay to its outline (brush_cursor::tip_mask, #4372); W/H link (cloneSource.set linked, panel chain button, #3298)
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); Refine Edge Brush (R, size, Expand Detection Area / Restore Original Edge, Alt swaps) paints select.refineEdge refineStrokes, estimated by colour-line matting (matting/brush.rs, #2972); outputs Selection, Layer Mask, New Layer, New Layer with Mask, New Document, New Document with Mask (#2998); no workspace layout
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) done #214 algo/puppet.rs build_mesh: traced outline of the opaque region grown by Expansion, triangulated with a Density-spaced lattice (conforming Delaunay); separate shapes get separate meshes (#3105)
TOOL-214-7 Content-Aware Scale through the Free Transform box with a live proxy preview done #214 Edit > Content-Aware Scale opens a scale-only Free Transform box (cas_transform.rs) with Amount, Protect and Protect Skin Tones in the options bar; edit.contentAwareScale carves a 384 px copy in the background for the live preview, and Enter runs it with the box's size and x/y as one step (#3096)
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); Backspace/Delete retracts the last point while drawing (#1466); Alt during a drag breaks the handle pair, Alt-drag turns the last anchor's outgoing handle alone, and dragging on the first anchor closes with that anchor's handles (vector_ui.rs, #3306); no Shift 45 deg
TOOL-214-11 Stroke Path dialog: pick any brush-based tool, Simulate Pressure done #214 path.stroke tool: Brush, Pencil, Eraser and every other brush-based tool (Background Eraser, Clone/Pattern Stamp, Healing/Spot Healing Brush, History Brush, Blur, Sharpen, Smudge, Dodge, Burn, Sponge, Color Replacement, Mixer Brush: their paint.* command along the path, one step) plus simulatePressure (taper both ends); Stroke Path dialog from the Pen menu, the Paths panel menu and Alt-click on its Stroke button (#2978)
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); on-canvas plane editor (vp_ui.rs: Create/Edit Plane, edge nodes, valid/doubtful/invalid grid colours, Grid Size, clipboard pasted onto the plane on OK); clone stamp, marquee and tear-off planes in the window still missing
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. Options bar: Sample All Layers, Create a new layer (the fill on a new layer above, newLayer), Remove after each stroke off (strokes collect as a tint and ↩ applies them as one strokes job, Esc drops them). Missing: generative AI mode, 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: 26 of 156 (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_show_hidden_panels, workspace.enable_floating_document_window_docking, file_handling.save_in_background, 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_.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; the Type tool's edit menu starts with spelling suggestions and Add to Dictionary over a misspelled word (#4370); Ignore / Ignore All, suggestions in the non-editing type menu and in generic text fields are not there 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 partial #217 interface.enableAnimations (prefs_ui.rs sync_animations, #3273) zeroes egui animation_time and scroll animation in both styles; the Home screen lists recent files (canvas.rs home_recent, #2691); no canvas pattern toggle yet (#2264) and no option to hide the Home screen recents
UI-217-11 Soft dark and soft light themes with picker previews; outlined knobs in light themes partial #217 theme.rs ThemeKind has fourteen themes: the twelve earlier ones plus Soft Dark and Soft Light (low-contrast Studio-layout palettes, text at 4.5:1, selectable in Preferences with the existing theme-card previews, ui.set {theme}, Window > Theme); outlined knobs in the light themes are still missing
UI-217-12 Save custom New Document presets; a real Recent category done #217 document_preset_cmds.rs and new_doc_ui.rs: named presets save/select/delete through preferences; Recent lists Clipboard, the last 20 documents created in the dialog (document.recent.*, persisted with the presets, newest first, deduplicated) and the uncovered defaults (#3131); created_documents_fill_the_recent_category
UI-217-13 Numeric fields accept arithmetic and units; scrubby labels; double-click resets a slider done #217 widgets::value_field: arithmetic plus units (1in, 5mm, 72pt, 50%) converted at the document resolution (#3099); widgets::scrub_label makes options-bar, slider-row and Character labels scrubby (Shift x10, Alt fine); slider rows with a known default reset on double-click
UI-217-14 Layers panel: wheel steps blend modes, drop on New/Trash duplicates/deletes, checker behind art done #217 panels.rs: the wheel steps the blend-mode dropdown (widgets::dropdown_wheel_hovered, #1747); rows dropped on New Layer/Group/Delete duplicate/group/delete them (footer_drop, #736), effect rows dropped on Delete delete that effect or the whole style (fx_trash_drop, #3102); pixel thumbnails draw widgets::checker behind the art; Shift+Plus/Minus step the layer's or the painting tool's blend mode (blend_keys.rs, #3102)
UI-217-15 Curves: smoothing / pencil mode, black/grey/white eyedroppers, tooltips done #217 Curves editor (adjust_editors.rs, dialog and Properties): Edit points / Draw (pencil) buttons and Smooth, all with tooltips; pencil curves are 256-level maps (Adjustment::Curves pencil, params pencil/smooth, PSD curv map form and smart-filter Mpng; #3088). Set Black/Neutral Gray/White Point eyedroppers in the Image > Adjustments > Curves dialog (adjust_dialog.rs, #873) and in a Curves adjustment layer's Properties panel (layer_picker.rs: a click on the image is one layer.setAdjustment with the eyedropper param, sampling below the layer; Esc or another tool disarms; #3110)
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 partial #218 TextShape::Path (doc text.rs) with glyphs placed rigidly along the curve by arc length (text warp.rs PathFrame), type.attachToPath and type.detachFromPath (#4365), .pcraft round trip. Missing: clicking a path with the Type tool, the caret and selection following the curve, and PSD path text (it saves as point text)
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 Fallback fonts are covered for Thai, Khmer, Lao, Myanmar (THAI_FAMILIES, KHMER_FAMILIES, LAO_FAMILIES, MYANMAR_FAMILIES in fonts.rs; #1909, #3259), the nine Indic scripts (INDIC_FAMILIES: Nirmala UI, Kohinoor and Sangam MN, Noto Sans) and Arabic and Hebrew. Still missing: per-script golden shaping tests against reference output (only smoke tests that an installed font shapes without .notdef, plus a bidi test)
TYPE-218-4 Variable font axis sliders from the font's axes; opsz follows the point size done #218 One slider per axis of the style's font in the Character section (Properties and Window > Character), setting type.setStyle variations; opsz follows the type size unless set (layout.rs variation_list); a style or weight resets the wght axis (#2985). PSD does not round-trip axis values yet
TYPE-218-5 OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus partial #218 Type > OpenType toggles (type_extra_cmds.rs:512). Character panel (and the Properties Character section) has Photoshop's eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals, Fractions, pressed while the feature is on at the selection and dispatching the type.openType.* commands (type_tool.rs opentype_row; character_panel_opentype_buttons_toggle_and_show_the_feature, #3752). Still missing: the Numerals and Stylistic Sets submenus, and greying out features the font lacks
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 done #218 Like Photoshop, 2 clicks select the word, 3 the laid-out line (a column in vertical type), 4 the paragraph up to the hard return, 5 all text, and the next quick click starts over (#3493; type_tool.rs next_click_count, click_selection, multi_click). Tested through real canvas clicks in point, paragraph and vertical type, with paste replacing the selection and Cmd+A selecting all (type_tool_tests.rs multi_click_selects_word_line_paragraph_and_all_in_point_text, multi_click_separates_wrapped_lines_from_paragraphs, click_counts_cycle_and_restart, click_selection_without_layout_and_on_hostile_input, multi_click_without_a_session_does_nothing)
TYPE-218-9 Distort/Perspective/Warp on live type as in Photoshop (was: offer 'Rasterize and continue?'; Photoshop doesn't prompt) done #218 Photoshop 2026 prompts for none of these: Edit › Transform › Warp on live type stays enabled with no rasterize prompt and offers only the warp styles (the mesh can't be dragged), so the type stays editable; Distort, Perspective and the split warps are greyed. PhotoCraft does the same: Warp on type commits the layer's Warp Text in one step (transform_tool.rs enter_type_warp, warp_cmds.rs warp_type_layer, #3004; warp_on_type_takes_a_style_and_keeps_the_type_editable), Distort/Perspective greyed (enable_rules.rs, #2701)

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 done #219 2026-10-12: set folders (collapsible, drag between sets, actions.assignSet), dialog toggle column (playback opens the step's dialog seeded with its values and resumes on OK; actions.dialog), Insert Stop… (actions.insert), step toggles, reorder, delete a step; Load/Save Actions (.atn) in the panel menu, CLI batch and actions.load/save (atn_cmds.rs): 90 Photoshop events map to commands both ways, the rest are kept verbatim and fail at playback by name; step toggles, reorder and delete a step done; Stop steps play (actions.stopMessage) but there is no Insert Stop menu item; sets and dialog toggles are stored and round-trip but the panel neither groups by set nor edits the dialog toggle
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 done #219 actions_cmds.rs: a_recorded_edit_run_replays_on_a_second_document records a crop, a Gaussian Blur, a selection fill and faux-bold type on one document and replays all of them onto a second (plus the original layer/select/fill replay)
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