* Brushes: import an image file as a brush tip (#3279)
`brush.presets.importImage` reads an image (path, or base64 data with
an optional fileName) through the normal importer, so any format and
bit depth File > Open reads works. Its composite becomes a sampled tip
the way Define Brush Preset reads a selection: darkness x alpha,
cropped to the painted extent, at most 2500 px a side. The bounds are
found band by band first, so a large mostly-white image never needs a
full-size buffer. The preset is named after the file (never replacing
an existing one) and becomes the current brush unless select is false.
Import Brushes... in the Brushes panel and the brush preset picker now
opens a picker for .abr and image files; images go to the new command,
.abr files to brush.presets.importAbr as before. Automation refuses an
ambient `path` for it like the other import commands.
Define Brush Preset and the new command share `sampled_brush`.
Scorecard TOOL-213-12 -> done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate scorecard
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>
* Regenerate docs/scorecard.md after merging main
* Regenerate docs/scorecard.md after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(ui): remember the selected tool between launches
* fix(ui): preserve explicit Eraser selection during temporary tool overrides
Clear temporary pen and spring state for explicit toolbar, menu, preset and control selections. Keep spring-loaded key behavior; only permanent key choices clear the pen marker. Add exit/relaunch coverage for explicit Eraser with pen and spring overrides.
* Regenerate docs/scorecard.md after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Batch: Include All Subfolders (AUTO-219-5, #3753)
File > Automate > Batch and File > Scripts > Image Processor take an
optional "includeSubfolders" (default false): a folder input also yields
the images in its subfolders, walked depth first in name order, bounded at
32 levels and 100000 files, with a seen-set of canonical folders against
symlink loops and the output folder left out. Each result keeps its
relative subfolder under the output folder, so same-named files in
different subfolders no longer collide. A non-boolean value is BadParams.
Both dialogs gain an "Include All Subfolders" checkbox (translated in
every complete catalog). Scorecard AUTO-219-5 stays partial (naming
template, progress/cancel, error log file); gaps.md G11 updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Batch: translate Include All Subfolders for Hungarian and Turkish
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
With a selection, ⌘T and the Move tool's Show Transform Controls framed the
layer's content clipped to the selection, so the box was smaller than the
marching ants wherever the layer did not fill the selection. The ants then
stayed where the selection started until the transform was applied.
- transform_frame / target_frame (engine): the box is the selection's own
bounds when a selection limits the transform. transform_bounds is unchanged,
so snapping and the command defaults are as before.
- The selection outline is drawn through the open box (homography, or the warp
in Warp mode), so it follows move, scale, rotate, skew, distort, perspective
and warp before the transform is applied. The whole outline is traced while
such a box is open, not only the part in view where it started.
- marching_ants_lines: ants along screen-space lines; non-finite lines are
skipped and very long ones are clipped to the view.
Part of #3402.
Claude-Session: https://claude.ai/code/session_015vKPoMXFhSvWmX5D9XM646
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
adjust_as_smart_filter recorded image.adjustments.* (and Shadows/Highlights)
as a smart filter without checking locks, so a Lock All or Lock Pixels smart
object still took the filter. It now refuses with check_pixels_unlocked, the
same rule filter.* smart filters use. Regression test covers both locks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both commands stored the `fps` param unchecked: 0 or a negative rate was
kept, and 1e39 became an infinite rate the next .pcraft save refused. They
now use timeline_cmds::fps_param (finite, above 0, at most MAX_FPS), as
timeline.create and timeline.setProps do; a missing fps keeps the old
fallback. Regression test covers 0, -1, 1e39 and "x" for both commands.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
count.deleteGroup only clamped the active index to the new length, so
removing a group before the active one silently made the next group
active and later count.add calls landed there. Shift the index down
when the removed group precedes it; deleting the active last group
still selects the new last group.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An action can take one of Photoshop's button colours, and the panel can switch to Button Mode so a click plays that action. The colour is stored on the action and is empty until it is set. The new labels are in every complete catalog.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The engine, renderer and PSD mapping already supported Smooth, Chisel Hard
and Chisel Soft, but the dialog's Bevel & Emboss page had no control for it.
Add the Technique dropdown under Style (Photoshop's order), a smooth default
for new bevels, and translations.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* ACEScg: File Handling option opens colour-less EXR renders in the ACES working space
Our render pipelines write ACEScg (AP1 primaries, ACES white ~D60,
linear) and — like most — state no colour space in the EXR header, so
io's linear-sRGB default misrepresented them (saturated hues shifted).
- cms: AcesCg joins the built-in profiles, generated from the published
AP1 primaries and ACES white point with a linear TRC (id "aces-cg",
aliases acescg).
- engine: File Handling gets "Open color-less EXR as ACEScg". When on,
an EXR that io tagged with the linear-sRGB default opens as ACEScg
instead; embedded or EXIF-declared profiles stay untouched.
Translated in every catalog. ACES view transforms (OCIO-based display)
remain future work; this covers the document's source colours.
* scorecard: 26 of 158 (main's #3472 preference added one too)
* Regenerate docs/scorecard.md after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layer styles: editing effects no longer re-enables a disabled effect list (#3648)
* Layer styles: follow #3648's exact dialog steps in a test
Preview off shows the document as saved (the in-dialog stroke disable is
a pending edit), so the state the issue reports is Photoshop's preview
semantics, not a resurrected effect: the live preview honours the
disable, and OK applies it.
* style: cargo fmt
On top of #5001. Two UI-thread stalls it leaves on a 16000 × 10000, 30-layer PSB
whose layers exceed the GPU memory budget:
- GpuCanvas::composite checked the memory budget for full refreshes only. Hiding
or showing a layer that covers most but not all of the canvas took the
synchronous GPU path, paging every layer in on the UI thread. Damage of
DEFER_PX or more now gets the same check, so it goes to the background redraw.
Small edits keep the GPU path without the check.
- The first draw after a background open scanned every layer's content bounds
on the UI thread (GPU planning). The open job now warms the shared tile bounds
cache before handing the document over (warm_layer_bounds, cancellable between
layers).
Co-authored-by: syncroot <24667144+syncroot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Interface: a preference to hide the Home screen's recent files (#4382)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: drop the hu/tr 'Enable animations' lines, #4362 adds them
Both PRs added the same two lines; whichever merged second would leave a duplicate key (bundled_catalogs_are_consistent rejects it).
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>
* Merge main; fix i18n (duplicate ja row, missing sr row) and regenerate scorecard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the duplicate hu row
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>
Hiding Edit › Menus (or Keyboard Shortcuts) left no way back into the
dialog, and Save, Save As, Exit and Preferences could disappear too.
edit.menus now refuses to hide them, the dialog greys their Visible box,
and the menus keep them even when a preferences file lists them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The eyedroppers (the points/subtractPoints params, and clicks on the
canvas while the dialog is open) refused points outside the active
selection. Photoshop samples anywhere and only limits the result to the
selection, which Color Range already does; the refusal is gone.
Deleting or dragging away the only smart filter now clears the filter mask, as
Clear Smart Filters already did, so the next filter takes the new selection.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Record action layer selections by name and show their targets
* Use document pixel format in grouped-layer regression
* Document the named layer action UI with a synthetic capture
The layers were only in document.inspect, together with history,
channels and much more, so agents looked for a layer.* command that
didn't exist.
layer.list returns the layers top-down with id, name, kind, visibility,
bounds, selected and active; groups list their children, or with
"flat": true every layer is one row with its depth and parent group.
It takes the same optional "document" index as document.inspect, is not
journaled and bad params are errors.
Stretching a 1 px row or column faded out along the stretch: bicubic and
bilinear sampling read transparent pixels past the source edge, and with a
selection the edge was the whole layer's bounds (lifted pixels kept their
colour at zero alpha). Samples past the source rect now repeat the edge and
the outline is antialiased by coverage, and the lifted pixels' bounds are the
selected pixels'.
Fixes#3773
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Smart Objects: inspect and Replace Contents show shared instances (#4075)
Placed layers that share a source have imported as one set of contents
since #2222, and Replace Contents changes every instance, but nothing
reported it: `linkGroup` is Link Layers, and `smartSource` can't tell
copies of one contents from independent copies of the same file.
document.inspect now gives each smart object `smartContents`: the
contents `id` its copies share and how many `instances` there are.
Replace Contents, Relink to File and Convert to Embedded/Linked list the
`instances` they changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test rendered caches after shared Smart Object replacement
---------
Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(commands): find the Elliptical Marquee by 'ellipse' or 'marquee' (#5060)
The Elliptical and Single Row/Column Marquee are options of select.rect
("ellipse":true, "line"), but the command-list filters only looked at
the id and the label ("Rectangular Selection"), so 'ellipse' and
'marquee' found nothing.
Commands can now carry search keywords. command.list / engine.commands
rows include them as "keywords", and the three filters (CLI commands
--filter, the engine.commands RPC and the MCP command_list) share one
matcher over id, label and keywords. select.rect gets the Photoshop
marquee names as keywords.
* Command list replies: document the optional keywords key
The reply-shape test from #3684 pins the exact keys of each row; select.rect
now also carries keywords, so the test and docs/control-protocol.md allow
that one optional string.
* Map .atn duplicate-into-new-document (Mk) to layer.duplicate
Photoshop records Layer > Duplicate Layer with Destination: New as a
Mk event (document class + layer Usng, Nm = new doc name, LyrN = copy
name), verified against a ScriptListener/XTensions export. Import maps
it to layer.duplicate {document: new}; export writes it back, so the
steps round-trip instead of becoming kept Photoshop steps that fail at
playback. A Dplc with a T document reference (another open doc) now
imports as a kept step rather than misplaying as a same-doc copy.
* Fix Rust formatting in ATN command tests
* Fix rustfmt formatting in ATN command tests
* Tests: bad-layer-id checks use an id that can't exist (#3343)
Layer ids come from one process-wide counter (shared with document and
smart-object ids, and bumped by ensure_ids_above on file loads). A full
`cargo test` run passes ids like 9999, so a layer a test just created can
get the "bad" id, the command succeeds and the assertion fails at random
(seen on CI in type_extra_cmds::tests::vertical_orientation_renders_columns_inside_the_canvas).
Use u64::MAX, which the counter never reaches, for every "no such layer"
sentinel in the engine and ui-egui tests. Test-only change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: use u64::MAX for bad-layer-id sentinels added on main since (#3343)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A mask made from the selection takes the selection with it in Photoshop: after
Layer > Layer Mask > Reveal Selection (the Layers panel's mask button with a
selection) the marching ants are gone. PhotoCraft kept the selection, so the
usual next step, Invert (Cmd/Ctrl+I) on the new mask, inverted only the part
inside the old selection and left the whole mask black, instead of flipping
the mask. Hide Selection (the Alt-click variant) already dropped it.
Reveal Selection now clears the selection in the same history step as the
mask: one undo brings the selection back and removes the mask.
Tests: adjust_cmds::tests::reveal_selection_drops_the_selection_so_invert_flips_the_whole_mask
(8/16/32-bit). Tests that deselected right after Reveal Selection no longer
need to (Deselect is disabled without a selection); layer_mask_depth and the
automation agent tasks now expect the selection to be gone.
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Session::execute catches a command's panic and returns an ordinary Err (the
last-resort guard in jobs.rs), so the unwind never reached panic_hunt's own
catch_unwind and a panicking command counted as a pass.
The harness now installs a panic hook that counts every panic in the process,
including on rayon workers, and a run fails if the count moved. Runs are
serialised so a panic is charged to the run that caused it. No production
code changes.
Test: contained_panics_count_and_graceful_errors_do_not. A contained panic
fails the run and a graceful Err passes; the first assertion fails without
the hook. The full --ignored fuzz stays green, so no command panics today.
Co-authored-by: prekabreki <prekabreki@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test(engine): add Color Range fuzziness 0 exact-match test (#214)
Adds an explicit test for Sampled Colors at Fuzziness 0 (point sampling
and hex colors) asserting that exact matches are fully selected and
adjacent shades are rejected. Updates scorecard TOOL-214-3 to done.
* docs: regenerate scorecard (TOOL-214-3 done)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WeVmjnnPPKdxVTpr2Vjtw
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Opacity and Fill are fractions in layer.setProps but percentages in the
Layer Style commands, and a value above 1 was clamped silently: 55 gave
an opaque layer. Out-of-range or non-numeric values now fail with an
error that names the range, and the layer is left alone.
select.object samples the active layer by default. On an empty layer
(scripts often add one above the photo) it found nothing and returned
without an error; the failure only surfaced at the next command that
needed a selection. It now fails with an error that names the cause and
the sampleAllLayers option, also when the layer's pixels lie outside the
rectangle or point.
Fibers renders from the foreground and background colours without
reading the layer, but its output area was the layer's content grown by
a zero halo: on an empty layer that is empty, so nothing was drawn (and
a following transform found nothing to transform). On a partly filled
layer only the content's bounding box was filled. It now writes the
reference bounds (the selection's, else the canvas) like the global
filters, without reading the whole layer as they do.
The Background has none of them. layer.setProps applied them to it as it
was, and Select and Mask's Layer Mask output gave it a mask. Like
Photoshop (and like PhotoCraft already does for moving, transforming,
erasing, styles and Layer > Layer Mask), it now becomes a normal layer,
"Layer 0", first. Values that leave its look alone (opacity and fill
100 %, Normal) keep it the Background.
Fixes#4836
* Reject clipboard origins that cannot be moved into a new document.
* Fill Clip.resolution in the unrepresentable-origin clipboard test.
main added resolution to Clip; the synthetic overflow fixture omitted it and failed engine lib tests.
* Format the clipboard origin test (cargo fmt)
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>
Conflicts:
- edit_menu_cmds.rs: main's #4133 (Sample All Layers off unless the active layer has no pixels
in the window) now decides `sampleAllLayers` inside the workspace's fill plan; the job keeps
the plan-based fill, the spec keeps the workspace's parameters.
- lib.rs: both module lines (caf_ui, cam_preview).
- hu, tr, vi catalogs: both sides' rows. Dropped de's High/Low (main now has them) and hu's
older "Vanishing Point" row main reworded. Added the 15 workspace strings to sr.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drag out of the rulers' upper-left corner to move the zero point: cross hairs mark where it
lands, Shift snaps it to the ruler ticks and View > Snap To targets apply otherwise; dropping it
back on the corner cancels. Double-clicking the corner puts it back at the image's top-left.
The rulers then read 0 at the zero point (negative left of and above it) and the Info panel's
X/Y measure from it.
The zero point is a document field (`ruler_origin`, document pixels), saved in .pcraft with
`#[serde(default)]` so older files open at the top-left. It is set by two undoable engine
commands, `view.setRulerOrigin {x, y}` and `view.resetRulerOrigin`, so agents can drive it.
It is not written to PSD: the public PSD spec documents no resource for it.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Auto-Blend Stack Images: a depth map by depth from focus, for Lens Blur
`edit.autoBlendLayers` with `method: stack` and `depthMap: true` adds a hidden gray Depth layer, the layer each pixel is sharpest in (black = the bottom layer, white = the top), from `photocraft_algo::dff`: the ring difference filter as focus measure, guided-filter aggregation with the merged picture as guide, a streaming peak search with Gaussian interpolation and a confidence, edge-aware weighted least squares solved by multigrid-preconditioned conjugate gradients, and guided upsampling. Once on a layer mask it drives Lens Blur's depth map.
Closes#2067.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Depth from focus: a guide or plane of the wrong length is the empty map
Review follow-up on #2073: `depth_from_focus` is public and indexed
`guide[i]` without checking that `guide` and every `luma` plane are
`w × h` long. It now returns the empty map (depth 0, confidence 0) for a
mismatch, before anything is read, and the test covers a short guide, a
short plane and a size that matches neither.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Profile prompts on open: one file at a time, before it opens, like Photoshop
With Ask When Opening on, files opened together put up every Embedded
Profile Mismatch prompt at once, each answer went to the active document
rather than its own file, and no prompt named its file. Checked in
Photoshop 27.11: one prompt at a time, naming the file, before that file
opens; OK opens it and asks about the next; Cancel opens neither it nor the
files still to come.
A file that asks is now held back until answered: the engine's background
open parks the decoded document (ColorState::open_question,
Session::take_held_open) and the shell queues it (open_prompt). The answer
is applied to that document (ColorState::resolve_open) before it opens.
Files that finish reading meanwhile wait their turn. A developed raw has no
embedded profile, so it doesn't ask.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Profile prompts on open: the prompt's text is localized in every catalog
The two messages are templates now, one line each so the catalog check covers
them: the sentence naming the file and the mode, "Embedded: {embedded}" and
"Working: {working}". The mode reads as the catalogs name it (French "RVB"),
and the file's name goes in last, so braces in it are never taken for a
placeholder. Rows added to all 17 catalogs; main's two "colour profile"
templates (Czech and Italian only) are removed, nothing uses them.
The Camera Raw control test from main expected a mismatch prompt when a raw
opens. With this change a developed raw is not asked about (it has no embedded
profile), so the test now checks that no prompt stands before Camera Raw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Profile prompts: rows for the hu, sr and tr catalogs; tests hold for any read order
The prompt's four templates join the Hungarian, Serbian and Turkish catalogs
that landed on main.
Files read in the background ask in the order their reads finish, which is the
order given unless a later read overtakes an earlier one. Three tests assumed
the order given and one failed once under a loaded test run; they now follow
whichever file the prompt names (and still require the order given for files
read on the spot).
New test: a file held for its prompt still reports type in a font that isn't
installed, once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Select Subject ran every graph cut (the GrabCut iterations and the
full-resolution band re-cut in `refine_band`, tile by tile on rayon)
through the non-cancellable `grid_cut`, so the Remove Background job only
saw Cancel/Esc before and after the whole "Finding subject" step.
Add `select_subject_with`, `grabcut_with` and `finish_region_with`, which
take an `Interrupt` and return `Err(Cancelled)`: each band tile checks it
before reading pixels and its cut runs through `grid_cut_cancellable`, so
in-flight tiles stop at the max-flow's next poll and unstarted ones never
start. Remove Background runs the step under `JobCtx::stage`. The old
functions keep their signatures and results (they pass `Interrupt::NONE`),
so `select.subject`, Object Selection and the layer menu are unchanged.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Brush: remember the last selected brush across restarts (split from #2516)
The persistence half of #2516 on its own, so it can merge and be maintained
apart from the brush-outline half. The tool's brush (settings, tip
references, and the name of the preset it was picked from) is saved once the
pointer is up and restored at launch: options bar, painting cursor and
Brushes panel highlight all start where the session left off.
Review fix from #2516 -- no per-frame work:
- `ToolState::brush_rev` is bumped by every brush/preset setter
(`tools.setBrush`, preset save/delete/rename/setCurrent/deleteGroup,
`brush.defineFromSelection`, `brush.presets.importAbr`, the store's tip
re-linking, and the shell's tool-switch/reset paths); `remember_brush`
serialises only when it moved since `brush_saved_rev`, so an unchanged
brush costs a counter compare per frame, not a clone and a JSON dump of
the whole brush (sampled tip previews included).
- Restoring counts as saved: the launch value is never rewritten.
Bitmaps stay out of the preferences document: remembered tips and patterns
are references (a loaded stored tile drops its full bitmap), and a sampled
bitmap too big to embed is skipped, keeping the previous value so a preview
never enters the prefs file. A malformed remembered brush falls back to the
default.
docs/scorecard.md regenerated with `cargo xtask scorecard` (34 of 154).
Tests: engine 1267 lib (5 new: revision gating, rejected changes, picked
preset, restore round trip, no embedded bitmaps), ui-egui 1982 lib; clippy,
layers, wasm green.
* rustfmt the StoredTile line and regenerate the scorecard after the main merge
* engine tests: destructive Levels keep the signed float overshoot at 32 bits (#2592)
* Scorecard: regenerate after merging upstream/main
* Export As: compare batch paths without the Windows separator
* font fix ?
* fix(ui): only Filter menu dialogs get the in-dialog preview window
#3877 drew the preview window in every schema dialog with a Preview
checkbox, including Image > Rotation > Arbitrary. Photoshop's Rotate
Canvas and adjustment dialogs have no preview window, and the taller
dialog covered the canvas, which broke
arbitrary_rotation_preview::size_changing_rotation_preview_is_visible_and_preserves_the_panned_center
on main. The window and its reserved height now apply to filter.*
commands only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* cargo fmt --check max_width = 160 fix
---------
Co-authored-by: JJ <jj@dot.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>