Commit Graph
75 Commits
Author SHA1 Message Date
aiongg 050caf9ecd Merge PR #6681: Content-Aware Move: live preview for large selections (TOOL-213-4, #6680)
# Conflicts:
#	docs/gaps.md
#	docs/scorecard.md
#	docs/target-app-parity.md
2026-10-21 05:04:04 +00:00
aiongg 4804148ff9 Merge PR #3759: Pattern Stamp and Background Eraser draw live; Spot Healing shows its heal area (TOOL-213-7, #3754)
# Conflicts:
#	docs/gaps.md
#	docs/target-app-parity.md
2026-10-21 05:04:04 +00:00
aiongg fc371711e6 Merge PR #5847: Shapes: Density and Feather on shape layers (#5846)
# Conflicts:
#	docs/scorecard.md
#	docs/target-app-parity.md
#	docs/ui-parity.md
2026-10-21 05:04:03 +00:00
aiongg 5c9ba773b4 Merge PR #3516: scorecard: five rows catch up with work already on main
# Conflicts:
#	docs/scorecard.md
2026-10-21 01:06:08 +00:00
dotCooCoo 8f971cc79f Mixer Brush: Clean Brush empties the brush, and the automatic Load and Clean can be switched off
The Mixer Brush could not be emptied. MixerState::clean dropped only
the pickup well, paint.mixerBrush refilled the reservoir from the
foreground before every stroke unless loadAfterStroke was false, and
the app always sent the defaults. A stroke on a new layer over an image
always carried the foreground color, and the options bar had no Load
Brush or Clean Brush control.

Cleaning now empties both wells, so a clean brush lays down only the
color it picks up from the canvas. The new commands brush.mixer.load
and brush.mixer.clean load and empty the brush. paint.mixerBrush reads
loadAfterStroke and cleanAfterStroke from the brush and applies them
after the stroke. Both are on by default, and a stroke with both on
paints as before. A stroke whose color differs from the one recorded
by the last stroke, load or clean loads that color first. A load or
clean records the foreground, so choosing a new foreground loads the
reservoir.

The options bar has the Current Brush Load swatch, whose menu holds
Load Brush and Clean Brush, and two toggles for the automatic Load and
Clean. Brush Settings shows the same two options. The new strings have
rows in every catalog.
2026-10-16 22:49:47 -07:00
David MannandClaude Opus 5.5 2f21722eb4 Content-Aware Move: live preview for large selections (TOOL-213-4, #6680)
The live preview only covered selections up to 256x256 px; larger ones (the
command takes up to 16 M) errored, the UI remembered the failure and showed
nothing until release.

Engine: a preview whose selection (or landed area) is over 256x256 is now
solved on a scaled-down copy of the area around the selection and where it
lands (area-average resize of the target, or of the composite with Sample All
Layers, and of the selection), on a one-layer document of its own, then
enlarged back (bilinear, float samples, so any depth) over the selection's
shape where it lands and, for a move, where it left. The landed content is
sampled through the scaled placement, so rounding the scaled offset does not
shift it. ContentAwarePreview::refine gives the full-resolution job, with the
command's own margin, so the refined preview is the command's result bit for
bit. Every step checks the JobCtx; the session is never touched; bad params
and selections over the command's limit still error. Selections within
256x256 take the unchanged path (same output as before, checked by hash
against main). The bilinear resampler used by Transform On Drop now runs its
rows in parallel (same results).

Measured (release, 24 MP 8-bit document, Ryzen 9 7950X with other builds
running): coarse preview 600x400 0.26 s, 1500x1000 0.38 s, 3000x2000 0.69 s
(256x256 full-resolution preview: 0.21 s); the exact refine takes as long as
the command (0.8 s, 4.1 s, 9.4 s).

UI: cam_preview shows the coarse result, then starts the refine as a second
cancellable job and swaps it in; a failed refine leaves the coarse result
shown, and a failed first job is still not retried.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 17:32:28 +02:00
David MannandClaude Opus 5.5 e8f50f69c3 scorecard: TOOL-214-4 note and the pasteboard menu row catch up with main
Dragging layers onto another document's tab landed in #851 (#589), and the
pasteboard colour menu in #2633 (#2462); the TOOL-214-4 note and the
context-menu parity row still listed both as open.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 21:17:33 +02:00
David MannandClaude Opus 5.5 6bb45c8d5b Shapes: Density and Feather on shape layers (#5846)
A shape layer's path is its vector mask in Photoshop (Properties > Masks),
with its own vector-mask density and feather. PhotoCraft's ShapeLayer had
neither, so soft shapes could not be made, and stroked soft shapes from PSDs
lost both on import.

- doc: ShapeLayer gains `density` (0..1, default 1) and `feather` (px,
  default 0).
- vector: the shape renderer applies them to the fill as a soft vector mask
  (fill shows beyond the path at 1 - density; the feather is the vector
  mask's Gaussian), so the shape's own pixels, the GPU (which samples them)
  and the stroke/fill split all follow. The stroke stays crisp. The feather
  blur moves from compose::masks to vector::feather and both share it and the
  feathered area, so a shape and an equal vector mask render the same pixels.
- compose: soft shapes' bounds reach their fill, their layer effects follow
  the soft alpha, and the split cache key includes density and feather.
- engine: `layer.vectorMask.edit` on a shape layer sets density/feather in one
  history step (path/enabled/linked/invert are refused there); density is now
  validated (finite 0..100) instead of clamped. `layer.vectorMask.info`
  reports a shape's values.
- ui: Properties shows the Vector Mask Density and Feather sliders for a
  selected shape layer in both themes (live drag, one undo per gesture).
- .pcraft: manifest and convert carry both with serde defaults (older files
  load at 100 % and 0 px).
- PSD: export writes them as the vector density/feather mask parameters;
  import reads them into the shape (re-rendered, as the stored pixels lack the
  fill beyond the path). Soft unstroked solid/gradient shapes now import as
  shapes instead of fill layers with a vector mask; pattern-filled ones stay
  fill layers.

Photoshop's 10 px feather on a 100 px black square, row 100, x = 30..72:
ours is within 1 level of every measured value (8, 16 and 32 bits);
density 50 % shows 127/128 beyond the path and 0 inside.

Scorecard: TOOL-214-12 done.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 03:07:46 +02:00
David MannandClaude Opus 5.5 91bffdec0b Merge main into feat/tool-213-7-live-retouch
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 00:57:30 +02:00
David MannandClaude Opus 5.5 5a4df1a363 Merge main; BUG-208-4 done, rows re-checked against main
Re-checked every row this PR flips against current main: the cited
tests still exist and none of the rows was flipped meanwhile.
BUG-208-4 is done too: shift_alt_before_the_drag_intersects_the_selection
(marquee_tests.rs) covers the marquee Shift+Alt intersect.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 00:29:48 +02:00
Sir StarchandBrandon Thomas cd046f4a70 Arrow-key nudges: a run of nudges is one History state, with Tools preferences (#2259) (#2764)
* Arrow-key nudges: a run of nudges is one History state, with Tools preferences (#2259)

Tapping or holding an arrow key nudged the selected layers one pixel per
History state, so one Undo took back one pixel. Nudges of the same thing
that follow each other now share one `coalesce` key (the engine already
folds edits with the same key into one step), so a run is one state and
one Undo takes it back.

- nudge_bundle.rs decides which run a nudge belongs to: the previous nudge
  was of the same thing (layers, selection outline), came within the
  pause, and left the document as it is now (no other edit, Undo/Redo,
  layer selection or save in between).
- move_mods.rs runs layer.translate and select.transformSelection through
  it; Alt-nudges (a copy each) never join a run. Selected pixels float
  until dropped, which is one state already.
- Free Transform keeps its own session history; its box nudges fold into
  one session step (transform_tool.rs).
- Tools preferences: Bundle Arrow-Key Nudges (default on; off gives a
  state per press as before) and Nudge Bundle Pause (ms) (default 1000,
  100..10000, hidden while bundling is off). Labels translated in all
  catalogues.

The two #1428 tests that asserted a state per press for the selection
outline now cover both settings.

* Regenerate docs/scorecard.md after merging main

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 15:32:39 -04:00
9df067b297 Shapes: on-canvas corner radius widgets for live rectangles (TOOL-214-12) (#5276)
* Shapes: on-canvas corner radius widgets for live rectangles (TOOL-214-12)

As in Photoshop, with Path Selection or the Rectangle tool the active shape
layer's live rectangle shows a round widget at the centre of each corner arc
(a few screen points inside a sharp corner). Dragging one rounds every corner
alike, Alt drags that corner alone; the radius follows the move along the
corner's diagonal from the press, within half the shorter side. The drag
previews the new outline and commits one shape.edit with radii on release,
one history step; a click changes nothing.

Docs: ui-parity row done, TOOL-214-12 note updated (still partial: shape
feather), target-app-parity, regenerated scorecard.

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

* Corner radius widgets: every selected rectangle, Move tool, cursor and readout

As in Photoshop:
- every selected live rectangle shows its widgets, also with the Move tool
  when Show Transform Controls is on;
- dragging any widget gives every selected rectangle the same radius (each
  capped at half its own shorter side); Alt changes that corner on each;
  all the shape.edit calls share one history step (coalesce);
- the pointer over a widget is the arrow with a rounded-corner badge, and a
  drag shows an "R: 25.0 px" readout;
- while dragging, the transform box and the Move rollover highlight hide, and
  the Move tool's snapping started by the press is dropped.

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-14 15:14:49 -04:00
835f33a476 Pen tool: Shift constrains segments and handles to 45° (TOOL-214-10, #3751) (#3756)
* Pen tool: Shift constrains segments and handles to 45° (TOOL-214-10, #3751)

Shift-click puts the next Pen anchor on the nearest 45-degree ray from the
previous anchor (at the pointer's projection onto it); Shift held while
dragging out direction handles turns them to 45-degree steps, read live
per move. The first anchor, closing on the first anchor and the last-anchor
break are unaffected; after Alt breaks the pair Shift constrains the
outgoing handle alone. The rubber band previews the constrained point.
Reuses stroke_constraint::snap45. Flips scorecard TOOL-214-10 to done.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Regenerate docs/scorecard.md after merging main

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Regenerate docs/scorecard.md after merging main

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Regenerate docs/scorecard.md after merging main

* Regenerate docs/scorecard.md after merging main

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-14 14:10:01 -04:00
f5606093cd Brush Settings: a response curve per dynamic control (#3294) (#3302)
* Brush Settings: a response curve per dynamic control (#3294)

Every brush Dynamic (Size, Opacity, Flow, Roundness, Scatter, Count,
Depth, Foreground/Background...) now carries its own response curve for
its control (Fade, Pen Pressure, Pen Tilt, Stylus Wheel, Rotation),
applied after the global Preferences pen curve. It is part of the brush
JSON, so tools.setBrush sets it (e.g. shapeDynamics.size.curve), actions
record it and presets keep it; linear by default, so existing brushes
paint unchanged.

- paint: ResponseCurve, the global pen curve's monotone cubic moved down
  from the engine as a fixed-size Copy type (no allocation per dab);
  serialises as its point list. dynamics::dynamic_value applies it.
- engine: prefs::PressureCurve is now that type.
- ui-egui: each Control row gets a curve button (lit when the curve isn't
  linear) that shows the same editor as Preferences > Tools.
- scorecard: TOOL-213-11 done.

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

* i18n: Response curve for hu, tr

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>

* Fix i18n after merge: Serbian Response curve, drop duplicate hu row; regenerate scorecard

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>
2026-10-14 14:09:39 -04:00
973e868173 Brushes: import an image file as a brush tip (#3279) (#3292)
* 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>
2026-10-14 14:09:08 -04:00
David MannandClaude Opus 5.5 e795a09d31 Merge main into docs/scorecard-stale-rows-2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:10:52 +02:00
David MannandClaude Opus 5.5 0543f25682 Merge main into feat/tool-213-7-live-retouch
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:07:24 +02:00
Nicolas FalesyandClaude Opus 5.5 7a0e60c104 test(engine): add Color Range fuzziness 0 exact-match test (TOOL-214-3) (#3581)
* 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>
2026-10-14 01:59:13 -04:00
David MannandClaude Opus 5.5 c951480e99 Merge origin/main into feat/tool-213-7-live-retouch
Background Eraser: main now samples and matches against the pre-stroke pixels (#4552); the
shared dab effect takes the pre-stroke surface so the live preview, whose working areas are
per dab, reads the same pixels as the commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:33:25 +02:00
David MannandClaude Opus 5.5 402d005e05 Merge origin/main into docs/scorecard-stale-rows-2
Conflicts in docs/gaps.md, docs/target-app-parity.md and docs/ui-parity.md:
kept main's status lines and rows and this branch's Type, selection-modifier
and Alt-wheel evidence. docs/scorecard.md regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:04:36 +02:00
Valentin KleinandClaude Opus 5.5 5aa6e33adf Content-Aware Move: live preview of the result (#4385) (#4393)
* Content-Aware Move: live preview of the result while the pointer rests (#4385)

A cancellable, debounced background job shows the move/extend result during the drag and in the Transform On Drop box.

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>
2026-10-12 22:24:03 -04:00
a9bb38c96e Clone Source: clip the overlay to a shaped brush tip (#4372) (#4376)
* Clone Source: clip the overlay to a shaped brush tip (#4372)

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 21:56:09 -04:00
David Mann 018213b8ea Merge main into docs/scorecard-stale-rows-2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/scorecard.md
#	docs/ui-parity.md
2026-10-13 00:29:04 +02:00
David MannandClaude Opus 5.5 8c7a2de4a7 Merge main into feat/tool-213-7-live-retouch
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 00:04:59 +02:00
Diego Chagas de22bca760 Remove tool: Create a new layer, and Remove after each stroke off (#1750) (#3808)
paint.remove takes several strokes at once (`strokes`) and fills them as one job and one undo
step, so the fill sees every hole together; and `newLayer` writes the fill to a new layer above
the target, only where the hole is, leaving the target as it was (with Sample All Layers it
fills from the composite). The options bar shows Photoshop's three options; with Remove after
each stroke off, strokes collect as a tint on the canvas until Enter or the ✓ applies them and
Esc or ⊘ drops them. Strokes are dropped when the tool or document changes under them.
2026-10-12 08:51:56 -04:00
David Mann 9c2d6ac892 Merge remote-tracking branch 'origin/main' into docs/scorecard-stale-rows-2
# Conflicts:
#	docs/gaps.md
#	docs/scorecard.md
#	docs/ui-parity.md
2026-10-12 12:27:32 +02:00
David Mann 89f4bb51dd Merge remote-tracking branch 'origin/main' into feat/tool-213-7-live-retouch
# Conflicts:
#	docs/brush-parity.md
#	docs/gaps.md
#	docs/scorecard.md
2026-10-12 12:07:03 +02:00
0e2c34af3e Vanishing Point: on-canvas plane editor with Create/Edit Plane, grid and paste (#3122) (#3133)
Filter > Vanishing Point now opens a dialog mode on the canvas instead of
the generic parameter dialog:

- Create Plane (C): four clicks, in any order, make a plane (corners are
  normalised clockwise from the top-left, the order paste expects).
- Edit Plane (V): drag corner nodes, or edge nodes to slide an edge along
  the plane's own perspective; click a plane to select it, Delete removes it.
- The plane grid is drawn at the Grid Size, in the accent colour when the
  plane is valid, warning yellow when its perspective is doubtful and red
  (outline only) when the quad isn't convex (VpPlane::check).
- With an image on the clipboard the selected plane previews it pasted in
  perspective; drag to move it, Width sets its size, New Layer renders it
  on a new layer. OK / Enter runs filter.vanishingPoint with the planes and
  the paste as one history step; the control channel drives the same
  session through {"ui": {...}}.

The engine now rejects a drawn plane whose corners don't form a convex
quad (bow-tie, dented) instead of warping along it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 22:02:04 -04:00
Valentin KleinandClaude Opus 5.5 638b5cc4e6 Clone Source: W/H link and overlay blend modes (#3298) (#3310)
The Clone Source panel gets Photoshop's W/H chain (Maintain aspect
ratio): with it on, a new W or H scales the other by the same factor.
cloneSource.set takes "linked" and cloneSource.list reports it.

The overlay's Darken, Lighten and Difference modes were stored but
never drawn. draw_clone_preview now blends the source with the
composite under the brush (clone_source::overlay_blend), so Difference
goes black where source and image line up.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:40:37 -04:00
7fd53f0e3f Puppet Warp: the mesh follows the object outline (#3105) (#3117)
The mesh was every square grid cell touching the layer's opaque pixels:
a staircase up to a cell outside the shape, and shapes closer than about
a cell shared cells, so pinning one dragged the other along.

build_mesh now traces the outline of the opaque region grown (or shrunk)
by Expansion from a signed distance field, simplifies it (Douglas-Peucker,
corners kept), splits its sides to the Density spacing, fills the inside
with a triangular lattice and triangulates the points (Bowyer-Watson
Delaunay, outline sides recovered by midpoint splits), keeping the
triangles inside the outline. Separate shapes get separate meshes, so an
unpinned one stays put, as in Photoshop. Vertices are numbered in reverse
Cuthill-McKee order to keep the solver's banded factor narrow, and the
density's spacing counts the grown region so a large Expansion on a small
layer no longer builds tens of thousands of vertices.

Scorecard TOOL-214-6 done; docs/gaps.md G7 updated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:40:06 -04:00
42ee0d9675 Content-Aware Scale: transform box with live preview (#3096) (#3103)
* Content-Aware Scale: transform box with live preview (#3096)

Edit > Content-Aware Scale opened a parameter dialog. Photoshop has no
dialog: it puts a scale-only transform box on the layer (or the
selection's bounds). The menu now opens that box.

- The box reuses Free Transform. Corner and edge drags scale and an
  inside drag moves; dragging outside does not rotate, Cmd does not
  skew or distort, and the Warp switch and the Transform modes are off.
- The options bar shows X/Y and W/H, then Amount, Protect (an alpha
  channel) and Protect skin tones.
- The live preview runs the command's own carving
  (ContentAwareScalePreview) on a 384 px copy on a worker thread. A
  run for a box that has changed since is cancelled after 150 ms, and
  the latest result is drawn through the box.
- Enter runs edit.contentAwareScale with the box's size and its new
  x/y params as one history step; Esc cancels.
- With params the command still runs directly.

Scorecard TOOL-214-7: done.

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

* Content-Aware Move's transform box: set the new content_aware field

#3095 (Transform On Drop) builds a TransformSession by hand; this PR adds
content_aware to it.

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-11 19:33:15 -04:00
Valentin KleinandClaude Opus 5.5 661ba806d1 Content-Aware Move: Transform On Drop (#3071) (#3095)
* Content-Aware Move: Transform On Drop (#3071)

The Content-Aware Move tool gains Photoshop's Transform On Drop checkbox. With it on, the drop
puts a transform box on the moved patch (the Free Transform box's handles, limited to move,
scale and rotate); Enter, the check button or a double-click computes the content-aware blend
with the transformed patch, Esc or the cancel button drops the whole move. The document is
untouched until the commit, so the move stays one History step.

Engine: paint.contentAwareMove takes scaleX / scaleY (%, 1..10000, negative flips) and rotate
(degrees, clockwise) about the selection's centre before the offset, validated (Err, not a
panic, for non-finite, out-of-range or off-canvas placements). The content and the selection's
shape are resampled through the inverse map on float samples (any depth); the selection is
transformed with the content. A plain move keeps the old, exact code path.

UI: cam_transform.rs holds the box (UiState.cam_transform, view state); the option is remembered
in ToolOptions and settable with ui.set camTransformOnDrop; ui.inspect reports camTransform.

Scorecard TOOL-213-4 stays partial: no live preview of the blended result yet.

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

* Tests: keep Transform On Drop cases in the Content-Aware Move test, not Patch (#3071)

paint.patch takes no scaleX/scaleY/rotate, so those cases (which only
paint.contentAwareMove rejects) failed patch_fails_gracefully. They are
already covered by content_aware_move_fails_gracefully.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:44:10 -04:00
Brandon ThomasandClaude Opus 5.5 c824a61ea1 Merge main; recount Type evidence and regenerate the scorecard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:21:38 -04:00
David Mann d701538974 Merge origin/main
# Conflicts:
#	docs/gaps.md
2026-10-11 23:51:37 +02:00
David MannandClaude Opus 5.5 086a82d547 Pattern Stamp and Background Eraser draw live; Spot Healing shows its heal area (TOOL-213-7, #3754)
Pattern Stamp (aligned through LiveRetouch, Impressionist dab by dab) and
Background Eraser now paint during the drag; the release commits exactly the
pixels the live stroke showed (per-pixel tests at 8 and 16 bits). Spot
Healing keeps healing on release and shows a dark overlay while dragging.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 23:50:35 +02:00
Valentin KleinandClaude Opus 5.5 55c1847f89 Paths: Stroke Path with any brush-based tool and Simulate Pressure (#2978) (#3039)
* Paths: Stroke Path with any brush-based tool and Simulate Pressure (#2978)

path.stroke now takes every brush-based tool of Photoshop's Stroke Path
dialog: besides the Brush, Pencil and Eraser it renders itself, the
Background Eraser, Clone and Pattern Stamp, Healing and Spot Healing
Brush, History Brush, Blur, Sharpen, Smudge, Dodge, Burn, Sponge, Color
Replacement and Mixer Brush run their paint.* command along each
subpath, in one history step, with only path.stroke journalled.

simulatePressure resamples each subpath and ramps the pressure from 0 at
its start to full at the middle and back to 0 at its end, so a brush
whose size follows pen pressure tapers at both ends.

The Stroke Path dialog (Pen/Path Selection right-click menu, the Paths
panel row menu, Alt-click on the panel's Stroke button) asks for the
tool and Simulate Pressure, as in Photoshop. Scorecard TOOL-214-11 done.

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

* Automation: classify path.stroke's params as a document vector path (#2978)

The new Stroke Path params doc says the tools run "along the path", which
registry_filesystem_path_params_are_classified reads as a possible host
file path. path.stroke strokes a document path and forwards only to
paint.* commands that take no files, so it joins the vector-path list.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:18:39 -04:00
Valentin KleinandClaude Opus 5.5 bd3be48785 Pen: Alt breaks a handle while drawing, and close-drag pulls out the first anchor's handles (#3306) (#3314)
Alt during a drag unlinks the handle pair (only the outgoing one keeps following),
Alt-press on the last anchor's outgoing handle turns that handle alone, and pressing
the first anchor now closes on release, so a drag pulls out that anchor's handles
(a stand-in knot folded into the first one on commit). A plain click still closes
without touching the first anchor's handles. Still one History step per path.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:29:33 -04:00
28874bbfad Select and Mask: Output To New Document, with or without a layer mask (#2998) (#3007)
* Select and Mask: Output To New Document, with or without a layer mask (#2998)

select.refineEdge takes output newDocument / newDocumentWithMask: a new
document like the source (size, mode, depth, resolution, profile) with
the active layer cut out by the refined edge, or with the matte as a
layer mask. The source document is left untouched. The dialog's Output
choice lists all six targets, translated in every catalog.

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

* i18n: drop the duplicate pt-br New Layer row (main already has it)

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-11 15:16:45 -04:00
David MannandClaude Opus 5.5 1c8199d417 Merge remote-tracking branch 'origin/main' into docs/scorecard-stale-rows-2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 20:52:18 +02:00
Valentin KleinandClaude Opus 5.5 c1bd296dcf Select and Mask: Refine Edge Brush (#2972) (#2989)
* Select and Mask: live preview in Photoshop's view modes (#1754)

The Select and Mask dialog had no preview of the refined selection. It now
previews live in Photoshop's view modes: Onion Skin (default), Marching
Ants, Overlay, On Black, On White, Black & White and On Layers, with a
per-view Opacity, Show Edge (J) and Show Original (P). F cycles the views,
O/M/V/A/T/K/Y pick one and holding X shows the image without a view.

The view state lives in the dialog's fields, so ui.dialog.set drives it.
The preview runs select.refineEdge on the proxy with output=selection and
builds the view from it (veil fill layers, a layer mask, or the refined
selection, whose marching ants the canvas draws scaled back up), through
the existing filter-preview worker.

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

* Select and Mask: Refine Edge Brush (#2972)

Select and Mask had no Refine Edge Brush. select.refineEdge now takes
refineStrokes: [{"points":[[x,y],...],"size":px,"mode":"add|subtract"}],
applied in painting order. Added areas become unknown and take a colour
estimate: each pixel's colour projected on the line between the mean known
foreground and background colours around it (normalised convolution over
a box of the brush size, four times as far where none are near), so hair
or fur beyond the edge-detection band joins the selection. Subtracted
areas keep the original edge. Malformed or oversized strokes are rejected
(stroke / point / swept-area limits), never a panic.

The dialog gets the brush: Refine Edge Brush (R), Mode (Expand Detection
Area / Restore Original Edge, Alt swaps), Size ([ and ]) and Clear Brush
Strokes. With it on, dragging on the canvas paints strokes into the
dialog's refineStrokes field, so the live preview, Show Edge and OK use
them, and ui.dialog.set / agents drive them like any other param.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:00:25 -04:00
5025fd2895 Color Sampler tool (#1046) (#2753)
* Color Sampler tool (#1046)

Add the Color Sampler to the Eyedropper (I) flyout: click to pin numbered
sample points to canvas pixels, drag to move, Alt-click (or drag off-canvas)
to remove, Clear All in the options bar, at most 10 per document. Each point
reads one composite pixel (RGB 0-255 and naive device CMYK, same as the
pointer readout) and the Info panel lists them under their number with the
position in the ruler unit; the readouts refresh when the document changes.

Points are per-document engine view state (DocState.color_samplers): they
belong to their document, are discarded on close, and are not saved to
.pcraft or PSD. They are driven by commands shared by the UI, CLI, control
channel and MCP (view.colorSamplers.add/.move/.delete/.clear/.list); the
mutations change no pixels and add no History step.

Points follow the canvas geometry (crop, Canvas Size, Image Size, Image
rotation/flip, rotated crop) through a pending Session.sampler_map that
Session::edit applies before culling the points that land outside the new
canvas; the count removed is reported in the status bar.

- engine: new sampler_cmds.rs (commands + shared readout helpers) and the
  sampler_map wiring in lib.rs, image_cmds.rs, mode_cmds.rs, commands.rs,
  analysis_cmds.rs (compound rolls points back on error).
- ui-egui: Tool::ColorSampler + original sampler.svg icon, toolbar slot,
  Info panel list, options bar, pointer handling, overlay markers, cursor.
- docs/scorecard.md regenerated (TOOL-1649-1 no longer says the tool is
  missing). No menu item, so parity is unchanged (627/627).

* Color Sampler follow-up: tool count, README, i18n, roadmap

The full ui-egui suite (not just the filtered tests) surfaced these gaps:

- state.rs: Tool::ALL.len() assertion 52 -> 53.
- README.md: "52 tools" -> "53 tools" and add Color Sampler to the list,
  so readme_tool_list_matches_tool_all passes.
- i18n: the options-bar hint, "{n} of {max} sample points" and
  "No document open" need entries in every catalog that claims complete
  menus (every_tl_literal_is_translated); added to all 16 (coverage
  1584/1584). The translations were drafted alongside the tool.
- docs/roadmap.md: drop Color Sampler from the missing-tools list and note
  it landed.

* Color Sampler: fit main's new tools and Image Size resample type

Reset Tool covers ColorSampler, the test ViewXform sets aspect, and
Image Size checks Method::None instead of an Option.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:43:09 -04:00
Valentin KleinandClaude Opus 5.5 0aaf5b60fe Select and Mask: live preview in Photoshop's view modes (#1754) (#2970)
The Select and Mask dialog had no preview of the refined selection. It now
previews live in Photoshop's view modes: Onion Skin (default), Marching
Ants, Overlay, On Black, On White, Black & White and On Layers, with a
per-view Opacity, Show Edge (J) and Show Original (P). F cycles the views,
O/M/V/A/T/K/Y pick one and holding X shows the image without a view.

The view state lives in the dialog's fields, so ui.dialog.set drives it.
The preview runs select.refineEdge on the proxy with output=selection and
builds the view from it (veil fill layers, a layer mask, or the refined
selection, whose marching ants the canvas draws scaled back up), through
the existing filter-preview worker.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:48:26 -04:00
David MannandClaude Opus 5.5 46f046ca4e scorecard: five rows catch up with work already on main
AGENTS.md sends contributors to the scorecard's missing and partial rows
when picking work; these five described code that has since landed.

- TYPE-218-7 missing -> done: Warp Text opens with the layer's warp
  (filter_dialog.rs warp_defaults) and previews live; covered by
  warp_text_dialog_tests.rs and
  warp_text_preview_matches_the_full_size_result_on_a_reduced_proxy.
- TOOL-214-13 partial -> done: Properties shows Density and Feather for
  both pixel and vector masks (mask_props_ui.rs, mask_props_ui_tests.rs).
- BUG-207-5 missing -> done: all twelve retouch tools are tested against
  mask, alpha channel, Channels-panel and Quick Mask targets; Sample All
  Layers and the smudge fringe have their own tests (retouch_cmds/tests.rs).
  Pencil and Mixer Brush targets are still untested, noted in the row.
- UI-217-18 missing -> partial: Alt+wheel zooms around the pointer at
  x1.1 a notch, measured on Photoshop in #1718, not the ~5 % the row asks
  for; the status bar zoom controls fit (#2823); Navigator not checked.
- BUG-208-4 stays partial with a corrected note: marquee add/subtract,
  Lasso add/subtract/intersect and Polygonal Lasso are tested from
  modifiers held at the press; a marquee Shift+Alt intersect test is
  still missing.

docs/gaps.md (G8), docs/target-app-parity.md (Type row) and
docs/ui-parity.md updated to match, with revision-history rows.
docs/scorecard.md regenerated: Tools 26/15/2, Workspace and UI 8/9/6,
Type 2/4/3, Open bugs 30/2/0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 16:27:16 +02:00
984bc5c715 scorecard: twenty rows describe work that is already in the tree (#2894)
* scorecard: eight Open bugs rows describe work that is already in the tree

AGENTS.md sends contributors to the scorecard's missing and partial rows when
picking work, so a stale row costs someone a session: they sit down to write a
test that is already there. These eight ask for work that exists and is covered
by passing tests.

#205 / #207, mask and channel transforms:
- BUG-205-4 ("no raster-mask test"): linked_raster_mask_moves_with_the_pixels.
- BUG-205-5 ("the other two not implemented"): all three cases are covered, by
  linked_mask_with_a_selection_moves_only_the_selected_region,
  unlinked_mask_stays_put and targeted_unlinked_mask_transforms_alone.
- BUG-207-4 ("transform() has no target handling"): edit.transform takes target
  (pixels, mask, quickMask or {channel: i}) and documents it in its params doc;
  targeted_alpha_channel_transforms_alone covers the alpha channel.

#206, kerning — the whole section:
- BUG-206-1: CharStyle has both `kerning: Kerning` and `kern: f32`.
- BUG-206-2 / BUG-206-3: io/tests/psd_kerning.rs round-trips manual, Off and
  Optical kerning, and reads the AutoKerning flag and Kerning value of every
  character back out of the written TySh.
- BUG-206-4: Alt+Left/Right at a collapsed caret calls kern_pair, and the
  Character panel field parses Metrics / Optical / a manual value; both tested.
- BUG-206-5 stays partial, not done: the round trips are covered, but no oracle
  corpus file is dedicated to kerning values, which that row also asks for.

FILE-216-5 keeps its missing status (no save-path tests for layered TIFF, PDF,
SVG, ACO/ASE, .shc or Smart Filters), but its note claimed there is no
atomic-save failure-injection test; format/src/atomic/tests.rs has two, and
BUG-203-6 already records them as done.

docs/scorecard.md regenerated with cargo xtask scorecard; Open bugs moves from
22/2/8 to 29/2/1.

* scorecard: the Type section's kerning and Thai rows are out of date too

TYPE-218-6 ("Optical renders as Metrics; Alt+arrow moves by word") is done:
Alt+Left/Right at a collapsed caret calls kern_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, and both modes round-trip through PSD. Covered by
type_tool_alt_arrows_kern_the_pair, optical_matches_photoshop,
optical_kerning_tightens_open_pairs, txt2_carries_optical_kerning and
io/tests/psd_kerning.rs. The row points at #206, whose own rows are in the same
state.

TYPE-218-3 goes to partial, not done: Thai is covered (THAI_FAMILIES in fonts.rs
and thai_in_latin_font_falls_back_to_installed_thai_font, #1909), but Khmer and
Indic have no fallback list and there are still no per-script golden tests.

Unchanged because they read correctly: TYPE-218-1 (TextShape is still Point and
Box), TYPE-218-2 (no direction UI), TYPE-218-4 (the axes in the shell are the
brush's), TYPE-218-8 (no triple or quadruple click) and TYPE-218-9.

* scorecard: Tools and Distribution rows are out of date too

TOOL-213-7 ("Live stroke only for Brush and Eraser"): canvas.rs strokes_live
covers Brush, Pencil, Eraser, Blur, Sharpen, Smudge, Dodge, Burn and Sponge,
and live_retouch_command adds the Clone Stamp, Healing Brush and History Brush.

TOOL-214-8 ("ruler only feeds straighten"): image.rotation.arbitrary opens
seeded with the measured ruler angle and the direction that straightens it;
arbitrary_rotation_starts_at_the_ruler_angle checks 5.7106 deg within 1e-3 and
the direction for three lines.

TOOL-214-5 goes to partial, not done ("No context menu in canvas.rs or
transform_tool.rs"): TRANSFORM_MENU does open on a right click during a
transform, with Free Transform, Scale, Rotate, Skew, Distort, Perspective and
Warp, and three tests cover it — but the row also asks for Flip and
Rotate 90/180, which are not there.

DIST-220-2 ("release.yml only x86_64 and i686 msvc"): the Windows matrix has
{ arch: arm64, target: aarch64-pc-windows-msvc }, and windows-arm64.yml
installs that MSI on a Windows 11 ARM64 runner, checks the binaries are ARM64
and runs the CLI natively. Signing takes the same path as x64 (DIST-2).

Unchanged because they read correctly: the whole Automation section (ABI_VERSION
is still 1 with Kind::Filter only, there is no ONNX dependency, no such xtask
check, and agent_tasks.rs really does run ten scripted tasks), TOOL-213-12,
TOOL-214-4, TOOL-214-6, DIST-220-1 and DIST-220-3.

* scorecard: Workspace and UI and File compatibility rows are out of date too

UI-217-9 ("prefs.rs UiScale unused"): prefs::UiScale offers Auto plus 75 to 300
percent, display_scale resolves it and sync_display_scale applies it with
ctx.set_zoom_factor; being a preference, it is remembered. The zoom_factor
assert the note cites is in a canvas-zoom test, checking that zooming the
document does not scale the interface.

FILE-216-1 ("No corpus step in ci.yml; in progress in #190"): ci.yml has a
corpus job on every PR and on main, pinned, sha256-verified and cached by the
pins. #190 is merged.

UI-217-22 and UI-217-23 go to partial, not done:
- layer.stampVisible merges the visible layers into a new layer and leaves the
  originals, and layer.stampDown exists, but neither has a menu entry and
  Alt+Merge is still missing.
- layer.copyToDocument copies layers into another document keeping masks,
  styles and blending (#589), but there is still no copy/paste layer clipboard.

UI-217-11 keeps missing; only its note changes, from "five themes" to the eight
ThemeKind has. No Soft dark/Soft light, previews or outlined knobs.

Unchanged because they read correctly: UI-217-20 (guides are still Vec<f32> and
there is no rulers preference), FILE-215-6 (SmartObject.transform is still an
Affine), FILE-215-19 (no JPEG Options dialog), and the rest of both sections.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:10:23 -04:00
bacc825d33 Move tool: drag a marquee from an empty spot to select the layers it touches (#2844) (#2852)
With Auto-Select on (or Cmd held with it off), a Move-tool drag that starts where
no layer has pixels (an empty spot, or the pasteboard) now draws a marquee instead
of moving the previously selected layer. Releasing selects every layer whose
content touches it (Auto-Select: Group selects their outermost groups); Shift adds.

New engine command layer.selectInRect; the UI side lives in move_box.rs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 06:47:53 -04:00
d79fc5e8ef Free Transform: right-click menu with the transform modes, rotate and flip (#2832) (#2840)
The Free Transform context menu listed only the modes. It now follows
Photoshop's order: the modes, then Content-Aware Scale and Puppet Warp
(greyed while a box is open), Rotate 180°, Rotate 90° CW and CCW, and Flip
Horizontal and Vertical.

The Rotate and Flip items, whether chosen from this menu, Edit › Transform,
a shortcut or the control channel, now turn or mirror the live box about its
reference point without committing it (transform_tool::intercept). Before,
they ran the engine preset on the layer under the open box. They are greyed
while warping. Distort and Perspective are greyed over a box on editable
type, matching #2701. A control-channel right-click during a transform opens
the same menu as the canvas.

Scorecard TOOL-214-5 is now done.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 06:30:33 -04:00
468b8f2792 Crop tool: New / Delete Crop Preset saves and removes user presets (#1919) (#2723)
* Crop tool: New / Delete Crop Preset saves and removes user presets (#1919)

The Crop options-bar dropdown ends with New Crop Preset... and Delete Crop
Preset..., as in Photoshop. A crop preset is a "crop" tool preset
(tool.presets.*, persisted with the preferences), so it also shows in the
Tool Presets panel. New saves W x H x Resolution (W, H, resolution and
unit) or a ratio under a name made from the values; it is greyed out with
nothing to save. Delete lists the saved crop presets for a multi-select;
built-in size presets stay. The dropdown shows the preset the fields hold.

Engine: tool preset names are checked (1-255 chars) and the store capped;
saved tool presets load entry by entry so a damaged one is skipped; a
multi-name delete is all or nothing. ui.dialog.open gains newCropPreset /
deleteCropPreset.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Regenerate the scorecard after rebasing onto main

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>
2026-10-11 05:31:19 -04:00
073e1cc1d2 Add the Color Replacement tool to the Brush toolbar group (#1045) (#2719)
The engine already had paint.colorReplacement, but the desktop UI had no
tool for it: no Tool entry, no toolbar slot, no options bar and no stroke
routing, so only agents and the CLI could use it.

Tool::ColorReplacement joins the Brush (B) flyout after the Mixer Brush,
so B / Shift+B cycle to it and ui.set / ui.inspect name it. Its options bar
has Mode, Sampling (Continuous / Once / Background Swatch), Limits,
Tolerance and Anti-alias, all mapped to the command's params, with the
foreground as the replacement colour. Strokes are drawn live through
LiveDab, which can now preview the stroke's finishing dab, so the preview
at the last move matches the commit. The command refuses a mask, channel
or Quick Mask target instead of silently recolouring the layer. New icon
derived from Lucide's brush; label translated in all 16 catalogs.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 05:20:00 -04:00
Tanish Manku c6a525cc99 Releasing a brush stroke no longer freezes: the commit takes the pixels the live stroke already drew (P32: 136 ms → 0.8 ms) (#2131)
* Releasing a brush stroke no longer freezes: the commit takes the pixels the live stroke already drew (P32: 136 ms → 0.8 ms)

Brush phase 4. On release the canvas dropped its live stroke and ran `paint.stroke` over every
point again (the whole stroke re-rendered), then re-composited the stroke's bounding box, although
the canvas already showed exactly those pixels. Long strokes froze for 100–250 ms at mouse-up
(#2068).

- `Session::offer_live_stroke` hands the finished live stroke to the next command. A
  `paint.stroke` / `paint.pencil` whose command, document revision, params, seed and points match
  it exactly takes its pixels instead of rendering (result `"fromLive": true`). Anything else
  ignores the offer and renders as before. It still runs through the normal dispatch
  (authorization, journal, one history step), and a replay renders identical pixels.
- The canvas feeds the release frame's last points to the live stroke first, then remembers the
  stroke's per-step damage for one revision (`CommittedStroke`): each canvas cache re-composites
  only the steps it hadn't shown, usually nothing, instead of the bounding box.
- P32 (brush release on a long stroke) becomes measurable: perf_scenarios times a 600-point
  200 px soft stroke on 24 MP. 136 ms → 0.83 ms on the GPU canvas, 257 ms → 0.71 ms on the CPU
  canvas (RTX 5090, release). Budget 16 ms, not enforced until a baseline run.
- Scorecard: TOOL-213-10 (⌃⌥-drag resize with readout) is done; TOOL-213-7 (live strokes for every
  brush-based retouch tool) is partial (Spot Healing, Mixer Brush, Background Eraser and Pattern
  Stamp still aren't live).

* Mask view test: a commit that takes the live stroke uploads nothing it already showed
2026-10-11 02:42:12 -04:00
5b778b49f2 Move tool: highlight the layer Auto-Select would pick on rollover (#2834) (#2843)
* Move tool: highlight the layer Auto-Select would pick on rollover (#2834)

With the Move tool and Auto-Select on (or ⌘ held while it is off), hovering
the canvas outlines, in the theme accent, the layer (or group, per the
Auto-Select target) that a click there would pick. Preferences › Tools ›
Show Highlight on Rollover turns it off (on by default).

The hover asks the click's own hit test (pick_cmds::auto_select_target, now
shared with layer.pickAt), so a type layer is outlined anywhere inside its
text and fully locked layers are clicked through, as on a click. The answer
is cached per document revision and pointer pixel. Nothing is drawn while a
button is down, a drag or transform runs, or the selected pixels would move.

Part of #214 (scorecard TOOL-214-4, now partial).

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

* Move hover: follow main's moves_selected_pixels_with rename

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-10 19:04:19 -04:00