Every applied removal (one stroke, waiting strokes applied together,
Sample All Layers, Create new layer, any colour mode and depth) now adds
one History step named "Remove Tool", as the reference application does.
The command id, its params and the job's progress label are unchanged.
* perf: add P85, the app frame after Paste Into on 24 MP (#9329)
Track-only row in the redraw area group: Paste Into a 5 % selection (untimed),
then time the next kittest frame. Guards: the command moves revision by >= 2 and
the frame records a refresh. Prints refresh px.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf: P85 re-creates the selection before each Paste Into (#9329)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf: a command's redraw area spans all its revisions; count whole redraws of small edits (#9329)
Session::dispatch is the one place damage spans revisions: a command that moved
the revision more than once reports step_damage of the document before and after
it, measured from DocState::damage_from. Deletes note_insert (Paste Into), Stroke
Path's hand-made union and the failed compound's None. Debug and test builds
count a whole-document redraw of an edit under 25 % in Session::whole_redraws.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf: the canvas redraws the damage since the revision it showed (#9329)
damage_since, the navigator and the histograms accept any cached revision within
DocState::damage_from..revision, not only the one before. The Move tool's
Auto-Select clause becomes the generic rule: no pixels changed since the cache.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf: P85 shows the selection for a frame before Paste Into (#9329)
Without that frame the canvas last showed the revision before select.rect,
two commands back, so it redrew everything whatever Paste Into reported.
The app always draws frames between a selection and the next command.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf: a newly added pattern no layer refers to changes no pixels (#9329)
Pattern Stamp copies its pattern into the document on first use, which made
a multi-subpath Stroke Path report the whole canvas. Appended patterns are
now pixel-equal unless a layer reference could resolve to one. The small-edit
sweep also covers layer.translate and layer.showOnly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf: renumber the Paste Into frame row P85 -> P89 (#9329)
Main uses P83-P86 for #10259's open benchmarks. Regenerate docs/scorecard.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* style: rustfmt viewport_pages damage lookup
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Canvas: bound the pattern-reference walk; P89 undoes each paste
- `any_layer` stops at the same depth as the file's other layer walks,
treating a deeper tree as referring to the pattern.
- The Paste Into frame row undoes each paste, untimed, and checks the
layer stack is restored, so every sample pastes onto the same document.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Canvas: a same-named pattern a reference still resolves past is bounded
- `patterns_pixels_equal` compares where each reference resolves before
and after the append, since `pattern::find` tries ids before names.
- P89 also undoes its selection each sample and checks the history
length is restored; the timed frame is unchanged.
- Main's new `damage_since` callers take the `(damage_from, last_damage)` pair.
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: FredApps <FredApps@users.noreply.github.com>
* fix: brush hints keep a feathered mask's ring and the effects' reach (#9329)
A command's damage hint (the bare dab) was intersected with the edit's
own damage, which `tile_damage` had grown by the mask's feather and the
layer's and enclosing groups' effects. The intersection cut that growth
off, so painting on a feathered layer mask left a stale soft ring around
each dab, and a dab's drop shadow was outside the reported damage.
`hint_damage` now grows the hint by the same rule first (`grow_hint`,
sharing `grow_in_layer` and `effects_reach` with `tile_damage` and the
group walk). The hint is still checked against the changed tiles as is.
The group-shadow test now cuts a hole at a tile's right edge, so its
shadow crosses into the next tile (it passed without the group's growth
before), and the clip-base test gains a dab at a tile edge.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: brush hints skip the effects of layers that aren't drawn (#9329)
`grow_hint` grew a dab by the effects of every layer in the document,
hidden ones included, so a large drop shadow on a hidden layer widened
every `paint.stroke` dab elsewhere to all the tiles `step_damage`
reported, and each dab recomposited and uploaded that much.
It now skips layers the compositor skips, by the same rule as
`effect_layers`: a hidden layer, a clipped layer over a hidden base, and
everything inside a hidden group. A change there shows nowhere, and
every drawn layer still grows the hint as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test: the group-shadow brush test goes through the hint (#9329)
`a_dab_inside_a_group_with_a_drop_shadow_damages_the_shadow` painted
with a plain `Session::edit`, so its damage came from `step_damage` and
never exercised `grow_hint`. It now paints with `paint.stroke`, and the
child carries a shadow of its own inside the shadowed group: the group's
shadow of the child's shadow lands past either shadow's reach alone, in
the next tile, so dropping the enclosing groups' reach from `grow_hint`
fails it.
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: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
* perf: add P83/P84 redraw-area rows for a masked adjustment toggle and a reorder (#9329)
Track-only rows in the redraw area group that check they hit the path
they claim and print the refreshed pixel count: toggling a Curves masked
to <= 5 % of the canvas with >= 4 layers above it (so the GPU prefix
cache can't cover it), and bring forward / send backward of a <= 2 %
layer past an overlapping neighbour in the many-layer document.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf: bound adjustment, reorder and clipping damage through context-aware change_bounds_in (#9329)
An unclipped adjustment reaches only the layers beneath it in its isolation
scope (and, in a 32-bit document, only under its mask; integer documents
round the whole backdrop after each adjustment, #9033). Reorders damage only
the moved layers at both ends; clipping and channel-exclusion toggles on
non-group layers are bounded. ui-egui's move and blend previews use the
context-aware bounds. Every new bound is checked against a full composite.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: keep Move preview on the context-free bound; cover a clip-base group's mode switch (#9329)
change_bounds_in is an in-place bound (the layers beneath an adjustment, or
its clip base, stay put), so the Move drag preview, which translates its
bound, goes back to the context-free change_bounds and its mask no longer
slides over layers with stale pixels. Hiding the last visible unclipped
adjustment in a pass-through group that is a clip base switches the group
between isolated and pass-through rendering, so its reach now joins the
group's bounds, as the clipped branch does. Doc fixes on change_bounds_in,
layer_change_bounds, one_sided_bounds and step_damage.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: reorder damage rejects nested video and grows by enclosing effects (#9329)
A moved group holding a video layer was bounded by the group's extents; the
moved-layer guard now uses the same recursive `contained` check as
`one_sided_bounds`. A moved layer's old and new bounds now grow by the reach of
the enclosing groups' effects, as the paired-layer branch does, so a reorder
that changes a parent group's shape redraws its drop shadow.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Engine: adding or deleting a layer in a group grows by the group's effect reach (#9329)
A layer added to or removed from a group with a drop shadow (or any effect
with reach) changes the group's effect output past the layer's own extent.
The one-sided branch of step_damage added only the layer's bounds, so the
shadow pixels outside them went stale. Grow them by the enclosing groups'
reach, as the reorder and pixel branches already do, sharing one helper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: bound added, removed and moved layers within the effect reach of the canvas (#9329)
change_bounds_in clips to the canvas, so a layer just off the canvas inside a
group with a drop shadow had an empty bound, and growing it by the group's
reach kept it empty: deleting, adding back or reordering it left the shadow it
cast onto the canvas stale. The one-sided and reorder branches now clip to the
canvas grown by the enclosing groups' reach, then grow and clip to the canvas.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix: grow clipping and property changes inside a group by its effect reach (#9329)
A paired layer whose clipping or properties changed was bounded by its own
extents on the canvas, and so were the clipped layers above it; nothing grew
them by the enclosing groups' effects. Unclipping a layer in a group with a
drop shadow, or changing a child's opacity, left the shadow past the layer
stale. Those rectangles now bound within the canvas grown by the reach, then
grow and clip to the canvas, as added, removed and moved layers do.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf: renumber this PR's rows P87/P88 after #10259 took P83–P86 (#9329)
The masked adjustment toggle and the bring forward / send backward rows
were P83 and P84; main now uses P83–P86 for the JPEG/PNG open benchmarks.
Regenerate docs/scorecard.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test: drop deselects main's new adjustment layers no longer need (#9329)
Since #8801 a new adjustment layer takes the selection as its mask and
drops it, so Deselect after it has nothing to act on. Two damage tests and
the masked Curves perf row still deselected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Engine: bound shapeless, off-canvas and frame-relayout group damage
- `composite_bounds` gives the canvas for a shapeless group, and bounds a
group's children within the canvas grown by its effect reach, so a
shadow cast onto the canvas by off-canvas pixels is kept.
- `beneath` includes a pass-through artboard's background.
- `layer_change_bounds` applies the enclosing groups' reach, as
`step_damage` does.
- `step_damage` adds a group's whole extents when a child change moves
its frame under a frame-spanning effect (Gradient Overlay).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Liquify: Forward Warp and Push Left follow the pointer; stationary tools use real time
Forward Warp and Push Left only changed the image once every dab spacing (a twentieth of
the brush radius), so a slow drag moved in jumps. The stationary tools (Twirl, Pucker,
Bloat, ...) ticked at a fixed 30 per second and stalled while the brush crept.
Points may now carry a fourth number, the milliseconds since the previous point. A segment
ending on such a live point applies the motion left over after the last dab as a
provisional dab that is taken back before the next segment, so a slow and a fast pointer
give the same warp. The stationary tools apply the larger of elapsed time and distance
travelled. The dialog records every point live and feeds every pointer position reported
in a frame. Points without times replay exactly as before.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* cargo fmt
* Liquify: correct two comments to describe the provisional dab
* Liquify: keep a pointer detour that returns to its start; skip the provisional dab at zero strength
* Liquify: also cover a zero stroke pressure in the live-segment test
* Liquify: test that the next live segment takes back the provisional dab
* Liquify: tighten the take-back test (repaint area, continuation with strength)
* Liquify: check that the take-back covers the area the first segment repainted
* Liquify: a dab far off the field touches no nodes instead of panicking
node_span converted a far-off centre to an index before checking the overlap: the lower index saturated to usize::MAX, read back as -1 it passed the check, and the provisional dab's snapshot then subtracted past zero (debug) or sliced out of range (release). The overlap is now decided in f64 before any conversion, NaN included. Tests: a timed stroke far off the field, on both axes and every side, for every brush tool; node_span on and off the grid.
* Liquify: a frame's batched pointer moves share its time evenly
The dialog handles a frame's queued moves one after another, and each read the clock: the first got nearly the whole frame and the rest only processing time, so a stationary tool's strength and where it landed depended on how the platform batched the moves. The frame's time since the last point is now shared evenly by its moves, as if they had arrived one per frame; after a stall the frame counts for at most MAX_LIVE_MS in all, as one point would. Tests: batched and one-per-frame moves give the same field (Freeze at 10 %, Twirl, Bloat, Forward Warp); a stalled frame's moves total MAX_LIVE_MS.
---------
Co-authored-by: terminalgate <351039321+terminalgate@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Playback reads the map path a loaded action file names. An action set is
shared like a document, so the path now follows the same rule as paths
stored in documents: a local absolute file. A network share, device, URL
or relative path stays a kept step with its bytes, so playing a
downloaded set never opens a remote share. Test paths are now native to
the platform, so the round trips also hold on Linux and macOS.
Co-Authored-By: yoriswind <yoriswind@yahoo.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A corner moved one pixel could take a quad across the parallel-sides limit and the output from 602 x 400 to 719 x 289. The estimate now has to survive one pixel of error on every corner, with its proportions moving by no more than 10 %. Also test the commit path with W and H empty, and a known rectangle seen by a camera model.
Related to #5310. Display labels only; command ids (filter.cameraRaw, edit.preferences.rawDefaults), preference keys, serialized settings, PSD/action/XMP Adobe Camera Raw identifiers and .atn step names are unchanged.
- Camera Raw / Camera Raw Filter / Camera Raw Defaults -> RAW Develop / RAW Develop Filter / RAW Defaults in menus, preferences, filter names, diagnostics and all 34 catalogs.
- Saved Photoshop actions keep the "Camera Raw Filter" step name, so .atn files round-trip byte for byte (test extended).
- "Camera Raw" stays a command-search keyword.
- Removes dormant Adobe/Photoshop-branded catalog rows; display-name guard tests.
Protect Foreground Color and Sampling Background Swatch converted the
foreground and background from their sRGB stand-ins, so a colour picked
as CMYK inks (100/0/0/0) came back as other inks and pixels painted with
exactly those inks were erased or missed. BgEraseSettings now carries the
picked inks, as BrushSettings::color_cmyk does, and a CMYK target compares
against them while the colour is still that sRGB colour. RGB, Gray and Lab
documents are unchanged.
The Shift-click override only reached the Brush, Pencil, Eraser and Mixer
Brush. The retouching tools take their brush from the engine
(`retouch_cmds::stroke_brush`, or `resolve_brush` for the Background Eraser
and Color Replacement), so with Catch-up on Stroke End off their lines
stopped short of the click.
- `stroke_brush` takes `smoothing` (0..100, the options bar's Smoothing).
- `canvas::unsmoothed_line` sends `smoothing: 0` for a Shift-click line in
every retouching tool's live preview and in both commits, so a commit
still takes the pixels its preview drew.
The Set of the document's measurementScale property (T: pixelLength long,
logicalLength double, logicalUnits text) loads as
image.analysis.setMeasurementScale with the custom preset and saves back
with its recorded descriptor. Other forms stay kept steps; a native step
that reads, resets or partly sets the scale refuses export.
A surface drag moves the cylinder without touching its shape, however narrow;
the bend handle shifts both arcs' depths by the same amount and a depth past
the limit is kept as it is rather than clamped by a press; a stored warp is
edited only when its source could be previewed without it, else the box keeps
a fresh mesh as before; the depths and wrap of a cylinder are compared with a
dimensionless tolerance; the parity row names the placements that keep a
stored cylinder editable. Also the explicit imports main now requires.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`smart_warp_doc_space` returns a stored Cylinder warp as a cylinder, with its
seven values in document space, when the placement only scales and moves; under
any other placement it is its mesh, as before. `is_stored_cylinder` accepts both
forms, so an untouched Enter is still no history step. The axis-aligned mapping
is shared by both directions (`cylinder_through`).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With W and H empty the output is the rectangle the quad shows in
perspective: the width is the largest horizontal scale at a corner and
the height follows from the recovered aspect ratio. Quads whose sides
are nearly parallel, that no camera at the document centre can see as a
rectangle, or whose estimate is implausible take their longest edges.
A size set in W or H behaves as before.
The spread was measured on opaque colours only, and the issue keeps
today's behaviour for partly transparent ones, in the match and in the
anti-alias fringe.
On 8-bit RGB a pixel now matches the clicked colour when the spread of
{0, dR, dG, dB} is within the Tolerance, so opposite changes in two
channels count double and green and blue stay at Tolerance 255. The
anti-alias fringe uses the same distance. The Magic Wand, the Paint
Bucket and other modes and depths keep the per-channel limit.