The Crop options bar gets Photoshop's gear menu (Set additional Crop options): Show Cropped Area
(H toggles it while the Crop tool shows a box; off hides everything outside the box behind the
canvas colour) and Enable Crop Shield with Color (Match Canvas, the pasteboard colour, or Custom…,
which opens the Color Picker), Opacity (default 75 %) and Auto Adjust Opacity (half the opacity
while the box is dragged). The shield used to be a fixed black at about 51 %. The options persist in
ToolOptions.crop_shield (older settings get Photoshop's defaults; a stray opacity is clamped) and
ui.set takes cropShield. Classic Mode and Auto Center Preview are left out: PhotoCraft always crops
in Classic Mode.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Crop options bar gets Photoshop's Straighten button (one-shot), and holding ⌘/Ctrl with the
Crop tool is a temporary Straighten. The drawn line (with an angle readout) turns the frame so the
line ends up level, or plumb when it is nearer vertical (the Ruler's straighten_angle convention),
and fits the frame to the largest of its proportions inside the image (Classic Mode). It is a
pending frame edit: Enter commits, Esc cancels. New logic lives in crop_straighten.rs.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The wheel over the Layers panel's blend-mode dropdown (closed or open) steps to the next or previous mode, one per notch and one undo step each, stopping at the ends, as in Photoshop. Fractional wheel deltas add up to whole notches, and the wheel there no longer scrolls the panel (#1747).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Crop options bar's overlay button opens Photoshop's overlay menu: six overlays, Auto/Always/Never
show modes, and O / Shift+O to cycle the overlay and its orientation. The default is Rule of Thirds with
Auto Show Overlay, as in Photoshop. ui.set takes cropOverlay, cropOverlayShow and cropOverlayOrientation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Large Tabs makes the document tab strip taller (34 px) with larger type; turning off Export Clipboard stops copies being mirrored to the OS clipboard; Show Channels in Color tints the Channels panel thumbnails (RGB from black to the primary, CMYK from white to the ink). Settings that do nothing: 48 of 141 (#204).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a pending crop frame, the arrows move it 1 px (Shift 10 px) instead of the selection or layer, X
swaps it between portrait and landscape about its centre (with the options-bar ratio), and the frame's
W x H shows beside the pointer while drawing or resizing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The document tab's context menu gains Reveal in Finder (a new file.revealInFinder command that reuses the smart-object reveal helper and targets the clicked tab's document), and dragging a tab sideways reorders the open documents through document.move, with a drop line, in both tab strips. Flips UI-217-6 to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Outside the crop frame the pointer is Photoshop's curved turn cursor and a drag turns the frame
about its centre (Shift: 15 degree steps) with an angle readout; handles resize along the turned
axes and a double-click inside commits. image.crop takes an optional angle: Image Rotation >
Arbitrary so the frame is upright, then the crop, in one undo step. Fixes#1792.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
menus::invoke records each successfully run command (deduplicated, newest first, last 8) and the palette lists them when its query is empty, falling back to the full list in a fresh session. Flips UI-217-7 to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open dialogs (File > Open and the single-file pickers for Place, scripts, notes and presets) now carry the most recent file's folder as initial_dir, and the desktop shell hands it to rfd's set_directory. Flips UI-217-3 to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Saturation slider's track runs grey to the hue at full saturation and the Lightness track black, the hue, white; both recolour as the Hue slider moves, as in Photoshop. Flips UI-217-16 to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cfg(test)-only test.panic command drives Session::execute into the catch_unwind
guard in jobs::dispatch; the test asserts an internal-error Err, an untouched document
and history, and a working session afterwards. REL-3 flips to done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BUG-205-1/2/3 (5a5facc, #242) and BUG-207-1/2/3 (e2444ce, #234) flip to done;
each row now names the test that covers it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UI-217-2 flips to done: b07a2e5b (#587) persists the window and dock width via eframe,
and window_and_panel_geometry_survive_a_restart asserts the round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Eyedropper options bar now has Photoshop's Sample Size (Point Sample,
3 by 3 ... 101 by 101 Average), Sample (Current Layer, Current & Below,
All Layers, All Layers No Adjustments, Current & Below No Adjustments)
and Show Sampling Ring.
- engine: new `document.sampleColor {x, y, size, sampleLayer}`
(sample_cmds.rs). Averages the square centred on the pixel, clipped to
the canvas, alpha-weighted, in the document's colour model (Gray, Lab;
CMYK documents use their composite RGB, since the profile round trip
is not exact) and rounded to the document's bit depth.
- ui: eyedropper_ui.rs options bar; state.rs ToolOptions
eyedropper_size / eyedropper_sample / eyedropper_ring (serde). The
size is shared with the painting tools' Alt-click eyedropper, the
Curves / Color Picker eyedroppers and the Info panel readout.
- control: ui.set eyedropperSampleSize / eyedropperSample /
eyedropperRing; documented in control-protocol.md and the MCP schema.
- i18n: new strings in all 14 catalogs (plus the missing Russian hint
the tl! scan had not reached).
- scorecard: TOOL-1649-1.
The brush was one global session setting (only its smoothing was per tool), so
switching tools carried the size, hardness, mode, opacity, flow and dynamics
over. sync_tool_smoothing becomes sync_tool_brush: it saves the session brush
for the outgoing tool and loads the new tool's on every switch. UiState's
smoothing_tool/tool_smoothing become brush_tool/tool_brushes (serde default, so
saved UI state still loads), and the session's own fields (colours, erase flag,
stroke seed) are never carried across a swap.
Called from the frame start, the options bar before the brush chip, both canvas
stroke paths, ui.set {tool} (so tool + brushSize in one call sets the new tool's
size) and select_tool_preset. A tool seen for the first time inherits the brush
in hand at Photoshop's 10 % smoothing.
Tests: each_tool_keeps_its_own_brush, the renamed per-tool smoothing test,
per-tool coverage in the ui.set brushSize test, and pencil_tests switching tools
before setting the smoothing as the options bar does.
Save Preset... in File > New stores width, height, resolution, mode, depth, background (with
its colour) and display units in preferences. They are listed under a Saved category, where a
preset can be selected or deleted. Adds document.presets.save/list/get/delete; get returns
params for file.new, which gains an optional backgroundColor. Bad saved entries are skipped one
at a time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turns the canvas camera around its centre without rewriting pixels.
Reset View zeros the angle; Match Rotation copies it across documents.
Trackpad rotate follows Preferences › Enhanced Controls › Rotate View with Trackpad.
* Add Pattern Stamp as a Clone Stamp flyout sibling
Introduce paint.patternStamp to paint the current pattern through a
brush stroke. Aligned strokes keep a document-space tile origin;
unaligned strokes restart at the first point; Impressionist jitters
each dab with a seeded RNG. Empty patterns and strokes larger than
4096² pixels return an error instead of allocating.
* Record Pattern Stamp on the tools scorecard
Allow the aligned stamp helper's argument count under clippy.
* Pattern Stamp: raise the stroke cap to 8192² so a 24 MP diagonal fits
The 4096² bounding-box cap refused a corner-to-corner stroke on a 6016x4000 canvas (~24M px).
Unit test covers that diagonal and a still-rejected 9000² box; the hostile-input test uses a
footprint above the new cap. Scorecard note labels Impressionist as an approximation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): translate the Pattern Stamp strings
The Polish catalog landed on main after this PR was written; the
translation-coverage tests need every language to carry new UI strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tool::ALL holds 51 tools now that Red Eye and Pattern Stamp both landed
The rebase onto main (with #1084's Red Eye tool) leaves Tool::ALL one entry
longer; bump the array length and the count test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Flyout test: qualify egui::Vec2 (panels.rs no longer imports Vec2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- photocraft-psd: clean-room .aco (v1/v2, UTF-16 names, RGB/HSB/CMYK/Lab/Gray at 16 bits,
other spaces kept verbatim) and .ase (groups, global/spot/process, RGB/CMYK/LAB/Gray floats,
unknown models kept) readers and writers; byte-stable round trips, hostile input -> Err.
- engine: presets::swatches (colours in their own model, display/foreground conversion through
photocraft-cms working spaces, persisted with the prefs, default 40 colours in 4 groups) and
swatch_cmds (swatches.list/add/delete/rename/move/newGroup/renameGroup/deleteGroup/use/reset/
import/export) with graceful-failure tests.
- ui: swatches_ui.rs replaces the const grid (groups, tooltips, click = foreground and selected
type, Alt/Ctrl-click = background, rename/delete/move, panel menu Import/Replace/Export/Reset),
Color Picker "Add to Swatches", .aco/.ase via File > Open and drag-and-drop; strings translated
in every complete catalog.
- Scorecard FILE-215-14 -> done.
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* feat(curves): add black gray and white point eyedroppers
* fix(curves): preserve luminance with gray-point eyedropper
* Curves eyedroppers: drivable by the command's eyedropper param (rule 1)
image.adjustments.curves and layer.newAdjustmentLayer.curves accept
{"eyedropper":{"point":"black|gray|white","at":[x,y]}} (samples the merged composite) or
{"point",…,"color":[r,g,b]}, resolved by the same curves_eyedropper the dialog uses, so agents
and the CLI can set black/gray/white points without ui.pointer. Bad points, positions outside
the image, transparent samples and malformed values are errors. The picker swatches are the
tone each picker maps to (image data, like the Curves gradient bars), drawn as
Color32::from_gray(level) with a token ring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(pl): translate the Curves eyedropper strings
The Polish catalog landed on main after this PR was written; the
translation-coverage tests need every language to carry new UI strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (part of #215)
Blending Options > Advanced Blending now works end to end:
- doc: AdvancedBlending on every layer (knockout none/shallow/deep, blend
interior effects as group, blend clipped layers as group, transparency
shapes layer, layer/vector mask hides effects), saved in .pcraft.
- PSD and layered TIFF: knko, infx, clbl, tsly, lmgm, vmgm read into the
model and written back (in place, or added when not at the defaults);
the TIFF byte-order transcoder handles lmgm/vmgm as byte flags.
- CPU compositor (compose/src/advanced.rs): shallow and deep knockouts
(isolated groups and clipping groups stop a deep knockout), clipped
layers blended individually, effects over the whole layer when
Transparency Shapes is off, effects built from an unmasked copy when a
mask hides them, interior effects under fill opacity.
- GPU: documents using Advanced Blending fall back to the CPU compositor.
- engine: layer.layerStyle.blendingOptions takes channels, knockout and the
five switches; inspect reports them; Copy/Paste Layer Style carries them.
- UI: the Blending Options page laid out like Photoshop's (General,
Advanced Blending, Blend If with split sliders), translated.
psd-tools corpus oracle 229 -> 236 (seven knockout files now pass; floor
raised); other corpora and all round trips unchanged. Scorecard
FILE-215-16 -> partial.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Polish catalog rows for Advanced Blending; regenerate docs/scorecard.md
pl.tsv landed on main and the coverage test needs every catalog to translate new UI strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Undo damage: compare Layer.advanced in layer_pixels_equal; regenerate scorecard
#810's exhaustive Layer destructure must classify the new advanced-blending
field; it changes the composite, so it is compared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Review: keep unknown blocks with their byte order, io behind the engine, fixtures documented
Addresses the review on #672:
* Unknown layer blocks read from a little-endian TIFF are no longer dropped. Their bytes are
kept verbatim with the signature `MIB8` (`46B8` for `8B64`) marking the little-endian
layout (`TaggedBlock::is_foreign_order`), so a save back to TIFF restores them exactly. A
malformed known block is kept the same way. Only a save to the other byte order has to drop
such a block, with a warning: a PSD export strips them (`strip_foreign_order_blocks`), and a
big-endian block of unknown layout still cannot go into a little-endian TIFF.
* The UI no longer depends on `photocraft-io`: the layer predicate is exposed by the engine
(`file_cmds::tiff_would_write_layers`); io is a dev-dependency again.
* The layered TIFF path ends in the same `encode_image` as every flat export, so export-wide
metadata policies (such as #733's XMP choice) apply to its composite too.
* Scorecard FILE-215-7 is `partial`: extra alpha channels and a Quick Mask are not written.
* The committed fixtures get an ATTRIBUTION.md row and a README describing their contents and
how they were generated (psdtags + tifffile); the Python generator script is removed.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* tiff_corpus: ask for layers explicitly (headless default is flat since #825)
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* i18n: drop the duplicate Korean "Ambient" entry that fails bundled_catalogs_are_consistent
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* i18n: drop the duplicate Korean "Warmth" entry as well
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* agent_tasks: tolerate Windows holding the test directory open during teardown
The three tests that remove their directory right after cancelling the client failed on Windows (CI and locally) with os error 32: the spawned server task is still closing its files. The cleanup now retries briefly and otherwise leaves the directory for the next run's tmp() to clear, as it already did at the start.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* preset store: a missing directory is an empty store on Windows too
DirBackend::read passed the OS message through, and Windows words a missing directory as "cannot find the path specified (os error 3)", which missing_file() did not recognise, so attaching a store on a path that does not exist yet (first launch, and two engine unit tests) reported actions.json as skipped. The backend now reports every NotFound as "<name>: not found", the wording the predicate and the memory backend already use. Regression test: opening a store on a nested missing directory warns about nothing and writes nothing.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* Review: keep scripts/layered_tiff_fixtures.py, drop the fixtures README and attribution rows
The four .tif fixtures those described were never committed (binary test data stays out of the repo; the tiff_layers tests read them from the gitignored corpus/layered-tiff/, written by the script that tiff_layers.rs:181 still points to).
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* automation: the Direct Selection path commands edit vector paths, not files
#865 added path.moveAnchors, path.moveHandle, path.bendSegment and path.convertPoint, whose registry docs mention the vector path they return, so registry_filesystem_path_params_are_classified fails on main (Linux and Windows): they join the list of commands whose "path" is a document path.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* i18n(cs): the lower-case "{n} layer|{n} layers" plural (main red since #829)
xtask's source_key_set_is_covered_by_complete_catalogs fails on main: the key reached every catalog but the Czech one. Same forms as the capitalised twin already in cs.tsv.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* Drop the duplicate path.* entries from the workspace test (main already lists them)
main already has path.moveAnchors/moveHandle/bendSegment/convertPoint in this match; the
duplicates trip unreachable_patterns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Add the Red Eye tool to the J-group
Click a red pupil to neutralize the flash reflection. Pupil Size
sets the search window around the click; Darken Amount controls how
far corrected pixels go toward a darker, desaturated pupil.
Catchlights stay white. A miss reports an error instead of a silent
no-op. The kernel only reads a window plus a 5 px mask-filter halo.
* paint.redEye: write back only changed pixels, so CMYK/Lab stay untouched
write_rgba re-encoded every pixel of the click window through the naive
RGBA round trip, which regenerates K in CMYK and shifts Lab values. Now
untouched pixels keep their original channel data; only pixels whose RGB
changed are re-encoded. Adds a CMYK test checking untouched pixels inside
the window stay bit-exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* algo::redeye: cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Polish catalog rows for the Red Eye tool; regenerate docs/scorecard.md
pl.tsv landed on main and the coverage tests need every catalog to translate new UI strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Eyedropper: the comparison ring while sampling (#213)
The colour under the pointer on the upper arc, the current foreground below,
over a thin dark outline, drawn at the cursor for the Eyedropper and for
Alt-sampling with a painting tool. The Precise-cursor preference keeps the
plain crosshair. Scorecard row flipped to done.
* Eyedropper ring: only while the button is held, theme outline
Photoshop shows the comparison ring only while sampling (button down), not
on hover; hovering now draws the plain crosshair and samples nothing, so
there is no per-frame document.pixel call when idle. The outline uses the
theme's shadow token instead of a hard-coded colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Eyedropper cursor: ring first, then main's pipette or Precise crosshair
After rebasing onto main (#1028's pipette cursor), the Eyedropper had two
match arms (the second unreachable) and Precise Other Cursors hid the
comparison ring. One arm now shows the ring while sampling, otherwise the
pipette, or the crosshair when Other Cursors is Precise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rustfmt canvas.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Brush tests: seams, hard edge, zero scatter and ABR tip colour (#213)
Adds the four TOOL-213-13 regressions: minimum coverage inside a stroke
(spacing 0.1/0.25/0.5), a hardness-1 rim measured against a soft falloff,
dabs staying on the path at zero scatter, and an imported ABR tip painting
the brush colour instead of black. Scorecard row flipped to done.
* Contributors: add Reynold Hu to people.toml
Real name credited, display name synced from the public GitHub profile at the
next contributors.py refresh (every field is opt-in).
* Regenerate docs/scorecard.md after rebase onto main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Show Clone Stamp source preview inside the brush cursor
* Drop the Clone Stamp preview screenshots and doc page
Binary screenshots and a standalone doc page aren't wanted in-tree (review):
remove docs/clone-stamp-preview.md, the two PNGs, their licence file and the
ATTRIBUTION.md row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Clone preview: pass the command name to clone_sample and propagate its far-source error
clone_sample gained a cmd parameter and returns Result since #1165.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* SVG opens as shape layers and places as a vector smart object (FILE-215-9, part of #215)
crates/io/src/svg.rs: usvg parses (already in the tree through egui_extras); paths become
ShapeLayer knots with the transform chain baked in, quadratics raised to cubics and every
subpath after the first joining the component so the fill rule spans the path as SVG does;
groups keep opacity and blend mode (transform-only and id-only wrapper groups vanish);
text is shaped into outline shapes with the craft fonts plus the system's; gradients become
the layer's gradient fill (stops and angle, with a warning). Clip paths, masks, filters,
pattern paints and images are rasterised by resvg into pixel layers over their bounding box
rather than dropped. Limits: 16384 px canvas, 2000 elements, 24 M px per raster, bounded
recursion; malformed input is IoError::Svg.
smart_cmds::render rasterises an SVG source at the placement's axis scale (1/16 steps,
cached per scale) and divides the placement by it, affine and projective alike; place_bytes
renders that cache up front. svg/svgz join the openable lists; book page; scorecard row done.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* SVG import: never read local files from <image href>
usvg's default image resolver reads any absolute or relative path in an
<image href>, so opening an untrusted SVG could embed local files
(/etc/..., ~/.ssh/...) into the document. Parse with a resolver that
only accepts data: URLs (resources_dir None, string hrefs ignored).
Embedded sub-SVGs already get no <image> resolution from usvg. The font
database (and its system-font scan) is now only loaded when the drawing
may contain text. Regression test: a PNG on disk referenced by absolute
path, file:// URL and relative path is not loaded; a data: URL still is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The deep EXR decoder (#642) was verified against the OpenEXR project's deep test images, but those
lived in a hand-copied `corpus/exr/` and the test skipped when they were absent, so CI never read
them. `cargo xtask corpus --exr` (and `--all`) now fetches the five `*.deep.exr` files of
openexr v3.5.2 `src/test/bin/test_images`, sha256-verified against `xtask/exr-corpus.sha256`.
`real_deep_files_decode` is behind the `corpus` feature and fails when the corpus is missing. It
checks each file's sample total, largest per-pixel count, cumulative counts and channel lengths,
and that the decoded u32 ID channels hold exactly the IDs listed by the upstream `.txt` sidecars
(an independent oracle), plus the flattened decode. FILE-216-3 moves to partial (EXR read in CI;
TIFF and RAW still are not).
* Direct Selection tool: reshape paths after they're drawn (#790)
Adds Photoshop's Direct Selection tool (A, in the Path Selection flyout) and
the Pen's Cmd/Ctrl (Direct Selection) and Alt (Convert Point) modifiers. It
selects and drags anchors, direction handles and segments of the work path,
the active shape layer's path and a targeted vector mask.
The geometry lives in photocraft_vector::edit; new engine commands
path.moveAnchors, path.moveHandle, path.bendSegment and path.convertPoint
apply it (one history step per drag, drivable over MCP and the control
channel), and the canvas previews a drag with the same code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tool::ALL holds 49 tools now that Direct Selection and Magnetic Lasso both landed
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>
* TIFF: BigTIFF, every IFD and SubIFD, banded parallel decode (FILE-215-8)
Large and multi-page TIFFs now open the way Photoshop opens them, quickly and without a second
full-size buffer, and any directory of the file can be opened on request.
* photocraft-codecs `tiff_ifd`: a bounds-checked directory walker for classic TIFF and BigTIFF
(LONG8 / IFD8, both byte orders). It follows the whole IFD chain and the SubIFD trees (tag
330) with a seen-set, a depth limit and a 10 000-directory cap, and checks every value range
against the file. `tiff_info` lists the pages (size, kind: page / reduced-resolution / mask,
parent, storage); `default_page` is the first full-resolution page of the main chain, so a
leading thumbnail is skipped. `decode_tiff_page`, `tiff_orientation`, `tiff_page_orientation`.
* Banded decode: the `tiff` crate reads the chosen directory through a patched byte view (header
-> that IFD, its next pointer -> 0, WhiteIsZero / palette -> BlackIsZero), so SubIFDs and
cyclic chains decode. The image is allocated once (fallibly, zeroed in parallel) and each
strip or row of tiles, across every plane, is decoded straight into it, in parallel on native
targets, with one capped chunk-sized scratch buffer per worker. New: planar configuration 2
(was an error), palette images (as RGB), 64-bit integers, associated alpha made straight,
non-alpha extra samples dropped. Image data cut off by the end of the file decodes with a
`Truncated` warning. Before the crate sees a file: structural tag values must lie inside it,
the chunk tables must match the geometry (checked in 64 bits; the crate overflowed and
panicked on `(h / rows + 1) * planes`), and a decompression-bomb guard refuses files far too
small for their declared size.
* WhiteIsZero and BigTIFF Orientation keep upstream #746's behaviour (#691, #693) through the
new decode path; regression tests cover both.
* Encode: `EncodeOptions::tiff_bigtiff` writes BigTIFF; without it, BigTIFF is chosen when the
file could pass 4 GiB (and a classic write that overflows 32-bit offsets is redone as BigTIFF).
* photocraft-io: `import_tiff_page(name, bytes, page)`; the TIFF branch of import goes through it.
* Tests: codecs `tiff_pages.rs` on hand-built files (`tests/common/tiffgen.rs`): BigTIFF LE/BE at
8/16/32f, padded tiles and planar strips/tiles, pages and SubIFDs, cycles, the IFD cap,
truncated and past-EOF strips, huge dimensions / counts / tiles, WhiteIsZero, palette, wide
formats, BigTIFF write, and a cut / bit-flip / random-mutation zoo (an ignored long run did
200 000 mutations without a panic); io `tiff_pages.rs`. The `decode_tiff` fuzz target walks
every page. `tiff_bench.rs` (ignored) times a 30 MP decode against the previous path.
* 30 MP RGB (release, 16 threads, same run): Deflate 344 -> 32 ms, LZW 428 -> 43 ms, 16-bit
Deflate 497 -> 46 ms, uncompressed 15 -> 10 ms, 256 px tiles 36 -> 10 ms, planar error -> 12 ms.
* Scorecard FILE-215-8 done; README and roadmap updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rustfmt the TIFF reader and tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* PSD: decode planes up to 8 GiB, so large PSBs keep their pixels (#375)
MAX_DECODED_BYTES capped a single decode at 2 GiB. Real PSBs go past
that in one plane set: the merged image of a 30000x30000 RGB file is
2.7 GB, a 33000x33000 16-bit layer channel 2.2 GB. Opening such a file
gave a document with no layers (flattened files) or layers whose
channels were "treated as empty" (layered files). Saving hit the same
cap on the encode side and silently fell back to uncompressed data.
The cap is now 8 GiB on 64-bit targets and 2 GiB elsewhere, the budget
photocraft-codecs already uses for Limits::max_alloc. It stays a
backstop: the decoders bound their output by their input on their own
(RLE expands at most 64x, Raw needs every byte present). The Raw and
RLE output buffers are reserved with try_reserve_exact, so a machine
without that much memory gets an error instead of an abort.
generated with Claude Code
* PSD import: a channel that can't be decoded leaves its layer empty (#375)
The warning says the channel is "treated as empty", but the importer
filled a missing transparency channel with opaque alpha, so a layer
whose channels all failed became an opaque black rectangle over the
document. A channel that is present but undecodable now leaves the
whole layer empty (transparent), as the warning says.
tests/large_psb.rs: the damaged-channel case (PSD and PSB), and an
opt-in release test that opens a flattened 29000x29000 PSB whose
merged image decodes to 2.52 GB (16 s, 11.6 GB peak here).
generated with Claude Code
* Large-file fixtures: generate and check 4-5 GB PSBs (#375, #216)
photocraft-psd example big_psb writes five PSBs straight from the spec,
independent of this crate's writer and its limits: flattened and
layered, raw (4.4 and 4.8 GB files, data past 4 GiB) and RLE (small
files whose planes decode past 2 GiB, up to a 33000x33000 16-bit
layer). Every sample follows one formula, so photocraft-io example
big_psb_check opens a file through photocraft_io::import and checks
pixels without a reference; with SAVE set it saves as PSB and checks
the re-opened file.
Release build, this machine: all five open with correct pixels and
survive a PSB save and re-open (5-14 s per import). Before the 8 GiB
cap, three of the five opened empty or black.
Scorecard FILE-216-2 missing -> partial: no 2 GB PSD, 150-layer or
20 MP fixture yet, and nothing runs in CI.
generated with Claude Code
* psd: ZIP channels reserve fallibly and read in bounded chunks
A small ZIP channel declaring a huge decoded size could make
zip_decompress try a multi-GiB with_capacity/read_to_end and abort on
allocation failure. Reserve with try_reserve_exact/try_reserve, read
through take(expected) in 1 MiB chunks, and report LimitExceeded
instead. Tests: absurd declared sizes, past MAX_DECODED_BYTES, garbage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Content-Aware Move: move or extend the selected pixels, the gaps filled content-aware (#213)
New command paint.contentAwareMove {offset, mode: move|extend, structure 1..7,
color 0..10, sampleAllLayers}. The content is pasted at the offset, its colour
fitted to the new place by `color` (0 keeps it, 10 is a full seamless clone),
and an edge band whose width `structure` sets is re-synthesised from the
surroundings. In move mode the old place is then filled with
content_aware::fill, never sampling the moved content (no ghost). The
selection follows the content. One undo step; runs as a background job with
progress and cancel. Works on layer pixels, masks and channels, at any depth.
Timing (retouch_bench, release, 6016x6016, elliptical selections dragged
300 px): 250x250 move 240 ms / extend 129 ms, 500x500 0.93 s / 0.53 s,
1000x1000 2.6 s / 1.5 s.
Known limit: at low Structure the wide band comes out smeared (the PatchMatch
vote blurs a thin band between high-contrast regions); that is the shared
completion, not this command.
generated with Claude Code
* Content-Aware Move Tool in the J group (#213)
Tool::ContentAwareMove next to the Patch Tool: outside the selection a drag
draws a lasso, inside it drags the selection and runs paint.contentAwareMove
on release (as a background job). Options bar: Mode (Move/Extend),
Structure, Color, Sample All Layers. The drag shows the moving outline; a
live preview of the result is not part of this change. New strings are
translated in every catalog. Scorecard TOOL-213-4: missing -> partial
(Transform On Drop and the live preview are still open).
generated with Claude Code
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Camera Raw: read and write settings as a live PSD Smart Filter (part of #215)
- Map Camera Raw 18.4 (process 5/6) filter settings to and from PSD: custom white
balance, Light, Presence, parametric and four point curves, HSL, Color Grading,
sharpening, noise, grain and numeric post-crop vignette controls.
- Keep the original Photoshop descriptor; edits patch only changed fields. Unknown
settings or versions stay an opaque filter, preserved byte for byte.
- PSD stores integer slider levels: export rounds continuous PhotoCraft values
(hue 360 -> 0) instead of dropping the filter.
- Tests: field mapping, patching, opaque fallback, malformed input, rounding, and
8/16/32-bit PSD -> edit -> .pcraft -> PSD with mask, filter order and undo/redo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Camera Raw PSD: use as_chunks for curve pairs (clippy 1.99)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Camera Raw PSD: no product names in Rust comments, strings or test names
Only the descriptor class key itself keeps its literal name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Actions: actions.record/stop/play/list commands for agents and the CLI (part of #219)
The Actions list moves from UiState to the engine Session, so the panel, photocraft-cli, the control channel and MCP share one list. actions.play checks every nested step with the same authorization a top-level command gets in untrusted sessions.
* Regenerate docs/scorecard.md after rebase
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Layered TIFF: open and save Photoshop layer data in TIFF files, in either byte order (part of #215)
Photoshop keeps a layered TIFF's layers in two private tags: 37724 (ImageSourceData, the same
layer section a PSD has) and 34377 (the image resources). PhotoCraft now reads and writes both,
so a TIFF opens with its groups, masks, layer styles, type, shapes, smart objects and adjustment
layers exactly as the PSD path would, and saves them back.
* photocraft-psd: a `tiff` module with `ImageSourceData` (parse/write, PSD- and PSB-sized),
the 34377 resource helpers, and a field-by-field byte-order transcoder. In Intel-order TIFFs
Photoshop byte-swaps the whole block structure (reversed keys, little-endian integers and
doubles) while pixel samples stay big-endian; the transcoder knows the layer-info structure,
masks, blending ranges and every block photocraft-io maps (descriptors, TySh, lrFX, the
binary adjustments, paths, patterns, filter effects, metadata settings, linked layers,
annotations...), recognizes descriptor-shaped unknown blocks, and drops the rest with a
warning instead of passing byte-swapped data to other readers. The reader gained an optional
little-endian mode. 23 unit tests; a fuzz target in the CI matrix.
* photocraft-codecs: the two tags travel as opaque bytes on `Metadata`, read by a
bounds-checked walk of IFD0 (classic and BigTIFF, both byte orders) and written back as
BYTE / UNDEFINED tags. `tiff_writes_little_endian()` tells which order the encoder produces.
* photocraft-io: `tiff_layers` rebuilds a PSD model from the TIFF's pixels plus the tags on
import, and on export runs the PSD exporter (new `merged_matte` / `merged_compression`
options), interleaves its merged planes for the TIFF image and stores the layer section in
the encoder's byte order. `ExportOptions.tiff_layers` (default on). Lab and Multichannel
documents save flat; extra alpha channels are not written (one transparency sample).
* UI: the TIFF Options prompt ("Including layers will increase file size." / "Discard Layers
and Save a Copy" / "Don't show again") behind Preferences > File Handling > Ask Before
Saving Layered TIFF Files; `ExportSettings.tiff_layers`; catalogs updated.
* Tests: io round trips at 8/16/32 bits in Gray/RGB/CMYK, equivalence with the PSD path, every
TIFF compression, the flat fall-backs, malformed input, and files written by psdtags in both
byte orders (fixtures from scripts/layered_tiff_fixtures.py); a corpus test (feature
`corpus`) writes every corpus PSD as a layered TIFF and compares the result with the PSD
round trip (700 files, 0 differences, 0 crashes). Codecs tests cover the tag walk on
hand-made files.
* Scorecard FILE-215-7 done, BUG-204-3 partial; README and book updated.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* Layered TIFF: de and pt-br strings; neutral wording in the psd tiff docs
The four psdtags fixtures stay out of the repository (no binary test data here): the
generator now writes them to the gitignored corpus/layered-tiff/, and the tests that read
them skip with a message when absent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Type: hitTest, caret and navigate commands so agents can edit text by position (part of #219)
Stateless queries in document pixels. The word, line and hit-test helpers move from type_tool.rs into photocraft-text, and the Type tool calls the same code.
* Regenerate docs/scorecard.md after rebase
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Typing 1280*2 or 20*2+5-2 into a numeric field applies the result on
Enter, Tab or click-away (+ - * /, * and / first). Plain numbers still
apply as they're typed. Arithmetic is held until it's committed, so a
caller that rounds a partial result can't cut it short, and changed()
now means a new value rather than a keystroke.
The floating Properties card scopes its fields per layer, so text still
being typed for one layer can't commit to the next.
Fixes#601
The BUG-203-1..6 and REL-4 rows still say missing, but the atomic-save
work landed in #230 (8227071, 2026-10-05) - apparently after the 4b53f62
snapshot these statuses were verified against. Re-verified against the
current tree:
- one shared photocraft_format::atomic_write (same-dir temp, create_new +
write_all + sync_all, rename, Unix directory fsync, Windows
PermissionDenied rename retry with backoff), and every document write
site routes through it (engine file_cmds/preset_store, automation
files/headless/workspace, app services, format store/autosave);
- no in-place fallback exists; read-only dir, disk-full and cross-device
errors name the cause and state the original was not changed;
- format/src/atomic/tests.rs failure-injects mid-write, rename and
read-only-dir errors (11 tests; re-run for this change: green).
The regenerated docs/scorecard.md moves Reliability to 4/1/2 and Open
bugs to 12/3/17. No code changes.
* feat(xtask): report UI translation coverage
Add a deterministic per-language coverage report backed by a checked English key inventory. Reject malformed, duplicate, and unknown catalog keys, and document the invocation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
* fix(xtask): derive i18n coverage keys from source
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
* fix(xtask): handle Windows line endings in i18n scan
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
* Scorecard: DIST-220-4 is partial now that xtask reports translation coverage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
* Add blend modes to Gradient tool
* Establish context menu parity contract and first workflows
* Align Select menu actions and style selection context rows
* Add Pen canvas Make Selection context action
* Complete Pen path context menu and symmetry actions
* Select menu: no extra top-level Feather (keep Select > Modify > Feather only, for 1:1 menu parity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate docs/scorecard.md and docs/parity.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate docs/scorecard.md and docs/parity.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add the Patch tool (#213)
paint.patch heals the selected area with texture from where the
selection is dragged (Source) or copies the selection onto the
dragged-to area (Destination), seamless-cloned like the Healing Brush.
The solve is clipped to the canvas, so patches at the edge stay opaque.
Tool::Patch joins the J group: outside the selection it draws a lasso
(Shift/Alt add/subtract), inside it drags the patch, the outline follows
the pointer and the offset is clamped to the canvas. Options bar:
Source / Destination. Strings translated in all six catalogs.
Tests at 8/16/32 bit, a UI gesture test, panic_hunt green. Scorecard
TOOL-213-3 is partial: the drag previews the outline, not the pixels.
retouch_bench: patch 250x250 22 ms, 1000x1000 362 ms (release).
generated with Claude Code
* Patch tool: live preview of the heal while dragging (#213)
The canvas shows the document healed at the pointer's offset
(patch_preview.rs, modelled on the Move tool's preview) and
recomposites only the healed area; release commits the exact
paint.patch and its damage rect refreshes the same area.
The preview (retouch_cmds::patch_preview) solves the colour fit on a
coarse grid (at most 128x128 cells) and interpolates it back: the
Poisson result is source + a harmonic correction, so the texture stays
exact and only the smooth correction is approximated (worst pixel 0.022
at 8x coarser in the test). Selection coverage is read tile-wise and
the interpolation runs per row in parallel.
retouch_bench (release, 6016x6016): preview 250x250 7 ms, 500x500
17 ms, 1000x1000 52 ms (exact patch: 21 / 90 / 354 ms). Scorecard
TOOL-213-3 done. Also applies rustfmt to the first commit's code.
generated with Claude Code
* Patch tool: cap patches at 16M pixels, drop product names from comments (#213)
The solve needs roughly 150 bytes per pixel, so the old 64M cap allowed
about 10 GB and risked an allocation abort instead of an error.
generated with Claude Code
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tool::ALL holds both the Patch tool and the Mixer Brush (rebase onto #433)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Camera Raw: interactive histogram, clipping warnings and scopes (part of #215)
- RGB histogram of the preview proxy (algo::histogram), drawn as RGB ribbons;
Before/After, five draggable tonal zones, shadow/highlight clipping warnings
(U/O) and Alt-drag channel diagnostics.
- Pixel hover readout, up to nine RGB/Lab colour samplers (S), an ICC-managed
vectorscope (algo::vectorscope) with skin-tone line, and a floating scope panel.
- Settings regrouped as Light, Color, Effects, Curve, Color Mixer, Color Grading
and Detail; Camera Raw labels translated through the cameraRaw context.
- One point-curve editor (point_curve.rs) shared by Camera Raw, the Curves dialog
and adjustment-layer Properties; fixes Camera Raw curve points not dragging.
- Double-clicking a Camera Raw smart filter reopens the Camera Raw dialog with its
settings and updates that filter, instead of the generic parameter form.
- New point curves are validated (2-16 points, increasing inputs); stored Smart
Filters keep loading.
- camera_raw_bench for P31/P36 (not enforced until a baseline includes them).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Histogram tests: as_chunks for triangle indices (clippy 1.99)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate docs/scorecard.md after rebasing onto main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n tests: close the Mixer Brush test before the Camera Raw one (rebase)
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>
* docs: sync scorecard with implemented behavior
* Scorecard: cite symbols instead of line numbers in the updated bug notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(ui): expose Mixer Brush tool (#213)
Wire the existing Mixer Brush command into the selectable UI tool model, options bar, and control-driven painting path. Cover shortcut cycling, options persistence, and selection clipping.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
* docs(scorecard): mark Mixer Brush slice complete (#213)
Reflect the new Mixer Brush toolbar, options, control dispatch, and selection tests in TOOL-213-2 and regenerate the scorecard.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
* fix(i18n): translate Mixer Brush labels in zh-Hans
Add Simplified Chinese entries for the Mixer Brush tool, its options, and the brush-settings heading. Test all Mixer Brush UI strings against every registered language catalog.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
* Polish Mixer Brush UI controls
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
- Roadmap (updated 2026-10-07): the M11 row notes the stricter CLI,
batch collision reports, per-step `wait` in MCP batches and safe
headless saves; the UI / UX row lists the keyboard-only shortcuts
(layer navigation, number keys, hardness, step and repeat), Alt-click
sampling, double-click for Layer Style, New from Clipboard, the
centred window and remembered Liquify settings.
- Scorecard: UI-217-1 is partial (Fill and Liquify settings persist);
UI-217-2's note points at the new `native_options()`. docs/scorecard.md
regenerated; docs/parity.md is unchanged (no new catalogue items).
- README: `<subcommand> --help` in the CLI snippet.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>