Commit Graph
3466 Commits
Author SHA1 Message Date
Harrison O'Connor-Hoover 672c0775de Reject control requests that expire before dispatch (#1525) 2026-10-09 11:11:47 +07:00
Harrison O'Connor-Hoover de9cc5f12c Do not replay bridge requests after an uncertain result (#1527) 2026-10-09 11:11:44 +07:00
David MannandClaude Opus 5.5 9b08da8d98 Free Transform: the reference point moves with the box when scaling (#1329) (#1535)
In Photoshop the reference point is attached to the bounding box: scaling
from a corner or edge, skewing, distorting and perspective carry it along
so it keeps its place within the box (the centre stays the centre, a
corner reference point stays on its corner). PhotoCraft left it at its old
canvas position for all of those drags.

After each of these drags the reference point is now mapped through the
box: its position in the old box's unit frame is mapped onto the new box
(carry_pivot). Alt-scaling, which scales about the reference point, and
rotating about it still leave it put; moving and the numeric options bar
already behaved correctly and are unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 11:11:40 +07:00
kubuntugamer 756ee3de87 fix(automation): MCP ui_set omits three fields it accepts (#1548)
The ui_set tool's `fields` parameter description listed 18 of the 21
fields the desktop `ui.set` control method accepts, omitting colorPanel,
gradientBlendMode and gradientClassic, and then ended with "Other fields
are an error". An agent that copies the list out of tools/list is told
three working fields are invalid.

The description now names all three, in the order UI_SET_FIELDS lists
them, with what each takes: colorPanel ({background: bool}) and
gradientClassic (bool) and gradientBlendMode (a blend mode name).

The new test reads UI_SET_FIELDS from the desktop shell's source and
asserts the schema names every entry, so the list cannot drift again --
which is how this omission arrived. It fails on colorPanel before this
change, and also fails if a later field is added to UI_SET_FIELDS without
updating the description. The parse is anchored on the declaration and
checked against its declared array length, because the doc comment
mentions UI_SET_FIELDS first and scraping that reads unrelated names.

Co-authored-by: Space Bunny Free <chasbigham@gmail.com>
2026-10-08 23:00:31 -05:00
BFlatandClaude 3e8d54896d Fix #1531: duplicate artboards beside the original, drag boards with the Move tool (#1584)
- Layer > Duplicate on an artboard stacked the copy exactly on the original, so
  nothing visible happened. The copy now goes right of the board (100 px gap,
  sliding past boards in the way) with its contents, and the canvas grows to
  show it, in one undo step (single and multi-layer duplicate). `inPlace: true`
  keeps the old behaviour; the Move tool's Alt-drag and Alt+Cmd+T use it.
- Move tool Auto-Select ignored artboards: a press on an empty spot of a board
  picked the Background under it, so the drag never moved the board. A click
  there now picks the board (layers on it first, clipped to the board; Group
  mode stops inside the board), and the drag moves the board with its contents.
- Moving a board past the right/bottom edge grows the canvas, as a new board
  does, instead of cutting it off.


Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 10:54:15 +07:00
BFlatandClaude b693d690e1 docs: MCP tool names and job tools, codec README JPEG metadata and deep EXR (#1552, #1553, #1559) (#1573)
- architecture.md listed dotted MCP tool names (doc.open, command.run...) that the server
  rejects; it now uses the advertised underscore names, adds the missing tools, and points
  out that dotted names belong to the JSON control protocol.
- development.md's MCP tool inventory gains jobs_list and jobs_cancel.
- crates/codecs/README.md: oversized JPEG EXIF/XMP is dropped with a warning (XMP first sheds
  its layered-document properties), not an error; supported deep EXR opens flattened with a
  DeepFlattened warning, and only deep ZIP and mip/rip-map tiles are unsupported.


Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 10:23:19 +07:00
Claude 26de59f54c automation test: new fill and adjustment layers' path is a vector path (#1419)
#1539 gives layer.newFillLayer.* and layer.newAdjustmentLayer.* a "path" parameter (the
vector mask), which registry_filesystem_path_params_are_classified took for a file path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA
2026-10-09 10:16:12 +07:00
David MannandClaude Opus 5.5 99b6aff26b Fill and adjustment layers take the active path as their vector mask (#1419)
In Photoshop, when a path is selected in the Paths panel (the work path
just drawn with the Pen or made from a selection, or a saved path), Layer
> New Fill Layer > Solid Color/Gradient/Pattern and every new adjustment
layer get a vector mask made from that path: a Solid Color fill with a
vector mask is a shape layer, editable as a vector. The path wins over an
active selection; with no path selected the selection becomes a layer
mask (#1250), else there is no mask. PhotoCraft always used the selection
and ignored the path, so Solid Color filled the whole canvas.

Engine: the new fill and adjustment layer commands take an optional
"path" param ("work", a saved path's name or a path object). The shared
helper mask_new_layer turns it into the layer's vector mask and falls back
to the selection mask without it; an unknown name is an error, not a
panic.

UI: run_command passes the path selected in the Paths panel as "path"
(callers can opt out with "path": null). Drawing a work path with the Pen
and Make Work Path select it, as in Photoshop, and clicking the panel's
empty area deselects it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:16:12 +07:00
Xus BadiaandClaude Opus 5.5 bfc59c784f macOS DMG: branded window background and icon layout (#1201)
The DMG opens on a 660x400 Finder window with the PhotoCraft artwork, 128 pt icons and the
Applications link beside the app. packaging/macos/dmg/ holds the SVG source, a reproducible
generator, an 87 KB HiDPI palette TIFF (was 1.0 MB) and the .DS_Store layout. The volume is
named "PhotoCraft" (the background alias resolves by volume name); the DMG file name keeps
the version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:11:00 -04:00
Filip PłachecińskiandClaude Opus 5.5 f0ae12dd22 Self-host the web app with Docker (nginx, community-maintained) (#1334)
A multi-stage Dockerfile builds photocraft-web with Trunk and serves dist/web from an
unprivileged nginx on :8080, with /healthz, precompressed gzip, the wasm MIME type and
immutable hashed assets. Rust and Trunk follow the release workflow, and the
PHOTOCRAFT_BUILD_SHA/DATE build args stamp About. Documented in packaging/web/README.md;
not built in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:10:55 -04:00
BFlatandClaude 9c554e2ead Fix #1544: Image Size refuses a resample past the raster budget; resampling writes bands as it goes (#1569)
image.imageSize accepted 64x48 -> 300000x300000 (90 billion pixels, each side within the
300000 px limit) and started allocating destination-sized f32 bands. The command now
refuses a resampled canvas whose raster alone would pass the decoders' default allocation
budget (8 GiB on 64-bit), with an actionable message, before touching the document;
resolution-only changes (Resample off) are unaffected. The resampler also writes its bands
into the output a group at a time instead of collecting every band first, so the f32 rows
held at once are bounded by the thread count, not the destination size.


Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 10:03:08 +07:00
BFlatandClaude f82a55f056 Fix #1543: oversized Satin and Bevel & Emboss sizes render instead of panicking (#1563)
A Satin size or a Bevel & Emboss size or softness of 1e308 reached the
box blur unclamped: the kernel radius saturated to ~2^62 taps and the
render panicked with "capacity overflow"; 1e6 ran for minutes. Drop
Shadow survived because its reach is clamped in margin(). The blur width
is now capped at MAX_REACH (and NaN is 1) in one place, box_width, which
both the CPU tent and the GPU's tent_kernel go through, so values from
commands and from files are bounded alike.


Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 09:47:13 +07:00
Harrison O'Connor-Hoover 47ad748638 Use rotated preview dimensions on the GPU canvas (#1459) 2026-10-09 09:02:10 +07:00
StreetblockandGPT-6 7fd610741b Fix PNG text metadata decode budget bypass (#1460)
Bound decoded PNG text and XMP by one shared max_alloc budget before
growing output, retaining CRC validation and malformed ancillary handling.
Cover compressed, plain, Unicode and post-IDAT metadata with small
synthetic regression fixtures, and document the budget's scope.

Opt Streetblock into public GitHub profile-name credits at their request.

Co-authored-by: GPT-6 (Codex) <noreply@openai.com>
2026-10-09 09:02:07 +07:00
kayrockscreenprintingandClaude Opus 5.5 fd74419aeb Crop: an explicit rectangle past 300000 px per side is refused (#960) (#1470)
image.crop took any explicit width and height, so a crop to 100000000 x
100000000 succeeded (the tiles are lazy) and left a canvas that a following
full-canvas flatten, such as image.trim or image.duplicate with mergedOnly,
could only abort on allocating.

An explicit crop now refuses a side over 300000 px with a bad-params error,
before the document changes. That is the limit file.new, Image Size and
Canvas Size already use. Extending the canvas up to it still works.

The test checks 1e8 x 1e8, 300001 x 10 and 10 x 300001 are refused with
the document and history unchanged, and that 300000 x 2 still extends the
canvas. It failed before the fix (the 1e8 crop returned Ok).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:02:04 +07:00
optimalbyteeandoptimalbytee c7dbb37b3c Speed up wide Motion Blur with adaptive FFT convolution (#1471)
Keep the merged upstream row kernel for shorter streaks and unsupported inputs.
Use bounded overlap-save convolution of its merged bilinear coefficients when
channel pairs, parallelism and memory estimates favor FFT. Preserve alpha cutoff
behavior, cancellation, selections and depth/model-neutral filtering.

Validate 303 algorithm tests in debug and release, plus native checks and full
image comparisons. Repeated current-upstream comparisons show 2.31x at 24 MP
and 4.15x at 1.5 MP for angle 30 / distance 1000 on the measured host.

Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
2026-10-09 09:02:00 +07:00
Nigel Ihza Firdausy 0d058f4c29 fix(psd): write Photoshop-compatible Photo Filter adjustment blocks (#1473)
* fix(psd): emit Photo Filter phfl block with required four-byte alignment (#1455)

* test(psd): golden phfl length, padding, legacy compatibility and Photo Filter variants

* test(psd): compare quantized Photo Filter channels with encoding tolerance

* fix(psd): normalize legacy 18-byte Photo Filter blocks on re-save

* test(psd): full exporter/importer roundtrip and repair of legacy invalid phfl blocks

* style(psd): format Photo Filter golden-block regression test

* style(psd): apply rustfmt layout to exporter compatibility tests
2026-10-09 09:01:57 +07:00
UnemarsandClaude Opus 5.5 d6aeb970ee Camera RAW: a measured Nikon D4 profile (image area, white level, colour matrix, tone curve) (#1475)
NEF carries no colour description, so D4 frames developed with the camera channels taken as
sRGB primaries and no tone curve (dark, pale), the sensor's optical-black columns showed as a
purple stripe on the right, and clipped highlights turned pink (green saturates first).

crates/raw/src/cameras.rs holds camera profiles measured from the cameras' own output (no
third-party tables), applied to TIFF/EP raws by make, model and sensor size. Sensor gains
tone_curve (like DNG ProfileToneCurve, per channel in linear ProPhoto), folded into the gamma
LUT; other cameras develop exactly as before.

Nikon D4, from 148 uncompressed NEFs of one body (ISO 100–10000):
- image area 4928x3280 at (8, 6): where the camera's own full-size JPEG sits, found by aligning
  the two (2 columns of optical black on the left, 50 + 8 on the right);
- black 0 (the data is black-subtracted in camera: the darkest image pixels reach 0);
- white 15520: green saturates at 15546–15672, red and blue at 15687–16383, so one level just
  below green makes clipped highlights white without moving the white balance;
- ForwardMatrix (white-balanced camera → XYZ D50) and tone curve fitted to the embedded JPEGs
  (Picture Control Standard, Active D-Lighting off; 52 frames, smooth areas). On 15 held-out
  frames PhotoCraft's development is 7.0/255 per channel from the camera's JPEG on average,
  against 34 before.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:01:54 +07:00
kayrockscreenprintingandClaude Opus 5.5 b2003a4329 .pcraft: a tile index outside the i32 pixel range is a corrupt file (#938) (#1483)
The loader accepted a manifest tile at any i32 index. TileCoord::rect
multiplies the index by the tile size, so an index past ±2²³ loaded fine
and then panicked ("attempt to multiply with overflow", geom/src/lib.rs)
wherever the layer's bounds were asked for: the status bar, Properties,
edit.transform, the compositor. Release builds wrapped to a rectangle
unrelated to the tile.

Loading now refuses a tile whose rectangle doesn't fit in i32 on both edges
(indices -8388608 ..= 8388606) with FormatError::Corrupt, as it refuses a
duplicate tile. In-app editing can't produce such an index; only a damaged
or crafted manifest can.

The test moves a raster layer's tile to just past each end of the range and
to i32::MAX / i32::MIN (refused), and to the extreme valid indices (loaded,
bounds computed). Before the fix the out-of-range bundles loaded, and asking
the layer for its content bounds panicked at the multiply.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:01:51 +07:00
David MannandClaude Opus 5.5 17be581390 Liquify: Alt reverses Twirl and swaps Pucker/Bloat, as in Photoshop (#1442) (#1484)
Holding Alt (Option) at pointer-down draws the stroke with the selected
brush's opposite: Twirl Clockwise twirls counterclockwise (and back),
Pucker bloats and Bloat puckers. The selected tool is left unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:01:48 +07:00
Jeremy 3955bf4f19 Canvas: checker moves with the image; pixel grid from 500%, over pixels only (#1491)
The GPU canvas anchored the checker to the top-left of the canvas viewport,
while the pixel grid sits on document pixel edges, so panning or zooming
slid the checker under the image and out of step with the grid. The checker
is now measured from the document origin; its 8 screen pixel cell is unchanged.

The pixel grid appeared from 800% (1200% on the CPU fallback), was faint and
was drawn across empty pixels. It now shows above 500%, lightens a dark pixel
by a quarter, and is skipped where the pixel is transparent (GPU canvas).

New offscreen render tests in tests/checker_canvas.rs cover cell size at nine
zooms, the phase, the grid threshold and strength, and the grid over empty
checker.
2026-10-09 09:01:45 +07:00
kayrockscreenprintingandClaude Opus 5.5 3f7d8afc78 Puppet Warp: pins that stretch the content too far are refused (#1019) (#1492)
edit.puppetWarp accepted pin destinations that are finite but arbitrarily
far away, and the triangle renderer sizes its tile, bin and lookup
collections from the deformed mesh's bounds: a pin dragged to (2^20, 2^20)
meant 17.6 million tiles (at least 846 MB of bookkeeping) before a pixel
was drawn.

- algo: MAX_WARP_PIXELS (2^30, about 16000 tiles; the Select > Modify
  budget). puppet_warp returns None when the deformed mesh's bounding box
  exceeds it or isn't finite, checked after the solve and before render.
- engine: edit.puppetWarp returns a bad-params error and the edit is
  discarded (layer unchanged, no history step). run_on_layer's and
  deform_selected's warp callbacks now return Result (a LayerFn alias);
  Liquify and Perspective Warp wrap theirs in Ok.
- A stored Puppet Warp smart filter too far to render leaves the smart
  object's pixels as they were.

Tests: algo (far pins refused, a near pin renders) and engine (the issue's
command refused at 8/16/32-bit, with and without a selection, layer and
history unchanged; a rigid move and a 4000 px pin still work; smart object
unchanged).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:01:42 +07:00
Harrison O'Connor-Hoover 5355221f81 Preserve document depth when splitting vector shapes (#1495) 2026-10-09 09:01:39 +07:00
David MannandClaude Opus 5.5 086d78a023 Apply the transform when another layer is selected (#1400) (#1496)
While Free Transform was open (Ctrl+T, or after dragging a Move-tool
transform control), selecting another layer in the Layers panel left the
box and the lifted preview on the old layer. end_if_left only ended the
session when its layer or document went away or the tool changed.

It now also commits the session when the active layer is no longer the
one being transformed, like Photoshop applying the transform on a layer
switch; the next transform (or the Move tool's controls) then starts on
the newly selected layer. Transform Selection is not tied to a layer and
stays open.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:01:36 +07:00
kayrockscreenprintingandClaude Opus 5.5 6c4d4e9dc9 Mesh warp of a gray surface writes the old and warped regions separately (#995) (#1500)
warp_mesh_gray (select.transformSelection with a warp mesh) cleared the
source and warped regions as one dense buffer over their union, so a mesh
that moves a selection far away allocated a buffer growing with the square
of the distance: about 37 GiB of floats for a 48 px selection moved
100000 px. b70fbd4 fixed the same thing in the homography path
(transform_cmds::warp_gray); this does the same here: clear the old region,
then write the warped one. Surface is tile-sparse, so a distant region
costs only its own tiles.

Moving a 48 px selection 8192 px, peak RSS of a test binary went from
346 MB to 3 MB (0.41 s to under 0.01 s).

The test warps 8- and 32-bit gray selections by ±100000 px and 300 px and
checks the result is the translated block with the old region cleared.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:01:33 +07:00
David MannandClaude Opus 5.5 2aa1eeadcc Selection tools: arrow keys nudge the selection outline (#1428) (#1502)
With a marquee, lasso, wand or other selection tool active, the arrow
keys now nudge the selection outline 1 px (Shift: 10 px) through
select.transformSelection, one undo step per press; the pixels stay put.
A floating piece (Cmd-dragged pixels) moves instead. Before, the arrows
did nothing unless the Move tool was active.

Also fixes the Move tool's arrow-key modifiers: egui's consume_key
ignores extra Shift/Alt, so the plain arrow matched first and Shift+arrow
nudged 1 px instead of 10 and Alt+arrow did not duplicate. The most
specific modifiers are now tried first.

Dragging inside the selection already moved the outline, with the move
cursor on hover (#686); a regression test now covers the cursor.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:01:30 +07:00
BFlatandClaude Opus 5.5 31cb502caf Fix #1313: Tab hides all panels, Shift+Tab the panel dock, as in Photoshop (#1388)
* Fix #1313: Tab hides all panels, Shift+Tab the panel dock, as in Photoshop

The right-hand panel dock could not be hidden. Photoshop's keys now work:
Tab hides the Tools panel, the options bar and the dock (Tab again brings
back what it hid), and Shift+Tab hides and shows the dock alone. Both are
shell commands (window.togglePanels, window.toggle.dock), remappable in
Keyboard Shortcuts and runnable over the control channel; a text field or
focused widget keeps Tab for navigation. Translated in every shipped
language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

* Tab hides all panels: Polish translations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 09:01:02 +07:00
BFlatandClaude Opus 5.5 b58b8df36b Non-Unicode arguments: translate the desktop notice (follow-up to #1168) (#1359)
* Non-Unicode arguments: translate the notice; keep run's doc comment on run

The "Could not open" notice for a path that is not valid Unicode is
translated in every shipped language, and the CLI's "Run the CLI" doc
comment moves back onto run (run_os has its own).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

* Non-Unicode arguments: Polish translations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 09:00:44 +07:00
c04624d119 Live strokes for the History Brush, Healing Brush and the dab tools (#1040)
* Live strokes for the History Brush, Healing Brush and the dab tools

The Clone Stamp's live stroke now covers every brush-based retouching tool:

- History Brush and Healing Brush: LiveClone becomes LiveRetouch, which
  paints from the clone source or the History Brush's history state
  (history_source, shared with the commit). The Healing Brush shows its
  cloned texture while drawing and blends it on release; it also gets the
  source preview under the brush.
- Dodge, Burn, Sponge, Blur, Sharpen and Smudge: their settings are parsed
  once into DabTool, whose per-dab effect both the command (dab_cmd) and the
  new LiveDab use. LiveDab runs each new dab's effect on a working copy of
  the target, in order, and mixes the changed area back at the brush
  opacity with mix_back (split out of apply_dab_stroke), as the commit does.
  Sample All Layers strokes keep the trail (their composite spans the whole
  stroke).

Part of #213 (live previews for brush-based tools).

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

* Format live retouch test

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

* Trail test: use Spot Healing, which still previews a trail

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

* LiveDab::push: read new dabs with get(), no slice-index panic path

Rebased onto #1026 as rebased on main; LiveRetouch::begin already reads the
first stroke point with first() (resolved in the rebase) and the Healing
Brush's own source overlay is dropped with clone_overlay.rs in favour of
main's Clone Source preview (#1042).

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

---------

Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 21:58:21 -04:00
BFlatandClaude Opus 5.5 ad857f1c93 Wayland: drop notice offers paste and the XWayland command for this install (#338, #386) (#1073)
* Wayland: drop guidance offers paste and this install's XWayland command (#338, #386)

winit 0.30 (eframe 0.36) delivers no file drops on Wayland. The notice told every
user to run `WAYLAND_DISPLAY= photocraft`, which does nothing for the AppImage
(no `photocraft` on PATH) or the Flatpak (fallback-x11 grants no X socket while
Wayland is available). It now names the command for this install (Flatpak flags,
the quoted $APPIMAGE path, or `photocraft`), omits it without an X server, and
offers pasting a copied image file with Edit › Paste's shortcut, which works on
Wayland since #384. The start screen hint on Wayland says the same.

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

* Wayland drop notice: Polish translations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:57:26 +07:00
7892365f93 Clone Stamp: live stroke, strokes start on the press, source preview under the brush (#1026)
* Clone Stamp: show the stroke live while it is drawn

The Clone Stamp painted a translucent grey trail while dragging and only
showed the cloned pixels on release. It now renders live, like the Brush:
`retouch_cmds::LiveClone` feeds the same StrokeRenderer the commit's
coverage uses and, for the tiles each step changes, redraws them from the
pre-stroke pixels with the clone source sampled through the coverage so far
(the same `clone_sample` / `apply_coverage` as `paint.cloneStamp`), so the
preview is what the commit paints. `StrokeRenderer` gains
`take_dirty_rect` / `coverage_in`; `clone_source::resolve` resolves the
mapping without storing the aligned anchor (the commit still stores it).
The canvas's live stroke holds either engine stroke; the Clone Stamp's
params come from one place shared with the commit (`clone_params`).

Part of #213 (live previews for brush-based tools).

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

* Painting strokes start on the press

The Brush, Pencil, Eraser and Clone Stamp started their stroke only once the
pointer moved past egui's click distance, so nothing showed on the press and
the first few pixels of movement arrived at once (a visible jump). A press
on the canvas with a live-stroking tool now starts the stroke immediately:
the first dab shows at once, the drag egui recognises later continues the
same stroke, and the release ends it, whether egui saw a drag, a click (no
second dab) or a long press that never moved. Other tools keep the click
distance.

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

* Clone Stamp live stroke: show the first dab on the press

A single point makes no dab until the stroke finishes, so the live Clone
Stamp drew nothing on the press and the stamp only appeared on release.
LiveClone now also draws what finishing the stroke would add (the renderer's
tail_preview: a lone first dab, the smoothing catch-up) and redraws it on
every step, as the Brush's live stroke does: the stamp shows on the press.

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

* Press-start strokes use the press event's modifiers

A Shift that arrives with the click (not yet in the frame's modifier state)
was dropped, so a Shift-click no longer connected to the previous stroke.

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

* Box the live brush stroke in EngineStroke (clippy large_enum_variant)

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

* Format live Clone Stamp code

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

* Live Clone Stamp: follow clone_sample's checked signature

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

* Live Clone Stamp: use main's source preview (#1042), no panic path on the first point

The rebase drops this PR's own source overlay (clone_overlay.rs) in favour of
the Clone Source panel preview that landed in #1042; remove its leftover module
line. LiveClone::begin reads the first stroke point with first() rather than
indexing.

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

---------

Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 21:53:52 -04:00
9cdcfbb1b7 Fix startup panic when UI Font Size is not Small. (#1467)
The font-size plugin was rescaling egui's default fonts on the first frame and overwriting the queued Inter stack, so Name("medium") was unbound on the next pass.

Co-authored-by: sunyd168 <sunyd168@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-09 08:37:01 +07:00
BFlatandClaude cb17db9a81 Numeric fields: arrow-key steps never clamp with a reversed or NaN range (#1450)
f32::clamp panics when the range is reversed or NaN, which a value
field's DragValue range tolerates; the step keeps the value unclamped
then.


Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 08:36:51 +07:00
MarekSpodBiedry c653e36f1f Show Extras Options: checkboxes read and set the Show flags they name (#957) (#1320) 2026-10-09 08:36:48 +07:00
MarekSpodBiedry c77e4122ce Control server: a bad JSON reply carries "id": null like every other unknown-id error (#1006) (#1304) 2026-10-09 08:36:44 +07:00
MarekSpodBiedry 84cca02d68 type.create: an unknown or out-of-range kerning is a bad-params error, as in type.edit (#994) (#1303) 2026-10-09 08:36:41 +07:00
Brandon ThomasandClaude Opus 5.5 0b5803500c Regenerate docs/scorecard.md and docs/parity.md after the latest community merges (#1530)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:24:37 -04:00
Brandon ThomasandTanish Manku f5a5a5c9ee Surface Blur: 4x to 20x faster on 16-bit and 32-bit layers (ranked window sums) (#1529)
The 8-bit sliding histogram (#631) can't serve 16-bit or float layers: a 16-bit threshold spans
thousands of levels and floats have none. The weight `1 - |v - v0| / t` is linear on each side of
the centre value, so the weighted window sum only needs the count, Σv and Σv² of the samples within
the threshold below and above it. A Fenwick tree over the window's distinct values holds those and
slides along the row, so a pixel costs O(radius · log n) instead of O(radius²), for any depth,
with the direct sum's own weight test deciding which samples count. Used from radius 10, where it
overtakes the direct sum; non-finite samples keep the direct sum.

Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
2026-10-08 21:23:19 -04:00
02984af969 Color Range: improve previews, sampling and eyedropper responsiveness (#1377)
* Refine Color Range preview sampling and add selection preview modes

* Avoid formatting ICC profiles on Color Range hover

* Color Range preview honours sampleAllLayers like OK; drop the env-var evidence/benchmark tests; Polish rows

The Sampled Colors preview always judged the composite while select.colorRange
judges the active layer for sampleAllLayers:false, so preview and OK could
disagree. It now reads the active layer's proxy pixels in that case (test:
preview_honours_sample_all_layers_like_ok). The two #[ignore]d tests that
wrote PNGs to COLOR_RANGE_EVIDENCE_DIR and timed a 24 MP hover are removed.
pl.tsv gets rows for the new strings.

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

---------

Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:22:03 -04:00
Brandon ThomasandClaude Opus 5.5 5341bff255 Swatches import/export and the Export As test use the async file dialog (#1528)
#1071 replaced Services::pick_open/pick_save with the async file_dialog
service while #886 (Swatches) and #1394 (Export As scaling test) still used
the old fields, so ui-egui stopped compiling once all three were on main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:18:04 -04:00
BFlatandClaude Opus 5.5 ccadeef41a Fix #1357: every Layer Style effect row shows its checkbox (#1422)
* Fix #1357: every Layer Style effect row shows its checkbox

An effect not on the layer yet was drawn as a greyed label with no
checkbox, so the box only appeared after the effect had been turned on
and off once. Those rows now draw an empty checkbox too, as in Photoshop;
a click on the row still adds the effect turned on. The checkbox drawing
is shared by both kinds of row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

* Layer Style test: count the effect list's checkboxes, not the pane's

Main's Blending Options pane now draws checkboxes of its own, so the test counts the leftmost column only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 08:08:06 +07:00
BFlatandClaude Opus 5.5 b97a72ac79 File dialogs no longer freeze or crash the app (#673, #574) (#1071)
* File dialogs no longer block the event loop (#673, #574)

Open, Save As, Export As, Place and every other file dialog ran
rfd's synchronous dialog inside winit's event handler. On macOS that
runs AppKit's modal loop there, and an event arriving meanwhile
aborted the app (SIGABRT, panic_cannot_unwind); on Linux the frozen
event loop made compositors offer to kill the app.

The platform shell now shows dialogs through one asynchronous
Services::file_dialog: the request is queued with what to do with the
answer, shown at the end of the frame with the window as its parent
(a sheet on macOS, an owned modal dialog elsewhere) and awaited on a
helper thread; the answer comes back through a channel that wakes the
app, which continues the action on a later frame. One dialog at a
time. The unsaved-changes prompt waits behind Save's dialog and closes
the document only once it saved. The web build uses the same service.

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

* brush picker: Import ignores the Open dialog's result, like the other callers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:08:03 +07:00
BFlatandClaude c1cb455c7a Fix #1381: selecting type on Wayland no longer deletes it (#1421)
Wayland input methods send an empty preedit (and at times an empty
commit) whenever the caret rectangle moves, as it does while Cmd+A,
Shift-arrows or a drag select text. With nothing being composed,
ime_update took that as "replace the selection with nothing" and deleted
the selected text. An empty preedit or commit now changes nothing unless a
composition is in progress; a real composition still replaces the
selection.


Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 08:08:01 +07:00
BFlatandLordFarquaadtheCreator c84ec1b43a Document tabs: elide and overflow into a » menu, carrying #1293 by @LordFarquaadtheCreator (#1367)
Co-authored-by: LordFarquaadtheCreator <fahadfaruqi1@gmail.com>
2026-10-09 08:07:57 +07:00
BFlatandClaude Opus 5.5 c4d1c1f8d1 Fix #1229: the Polygonal Lasso tool removes its last vertex on Backspace (follow-up to #1262) (#1338)
* Fix #1229: the Polygonal Lasso tool removes its last vertex on Backspace

Alt-click polygon mode. The Polygonal Lasso tool itself keeps its points
elsewhere (ui.polygon), so there Backspace still reached Edit › Clear and
wiped the layer, the data loss reported in #1229. It now removes the last
vertex there too (Delete as well), a right-click does the same, and
removing the only vertex cancels the polygon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

* Pipette cursor test: Windows draws the precise crosshair itself

input_tests::eyedropper_and_alt_sampling_show_a_pipette (#1028) expected
the OS crosshair with Other Cursors = Precise, but on Windows the canvas
draws its own crosshair and hides the OS cursor (visible_crosshair), so
the test failed on every Windows run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 08:07:54 +07:00
619095bfd0 Pattern Stamp tool in the S-group flyout (#922)
* Add Pattern Stamp as a Clone Stamp flyout sibling

Introduce paint.patternStamp to paint the current pattern through a
brush stroke. Aligned strokes keep a document-space tile origin;
unaligned strokes restart at the first point; Impressionist jitters
each dab with a seeded RNG. Empty patterns and strokes larger than
4096² pixels return an error instead of allocating.

* Record Pattern Stamp on the tools scorecard

Allow the aligned stamp helper's argument count under clippy.

* Pattern Stamp: raise the stroke cap to 8192² so a 24 MP diagonal fits

The 4096² bounding-box cap refused a corner-to-corner stroke on a 6016x4000 canvas (~24M px).
Unit test covers that diagonal and a still-rejected 9000² box; the hostile-input test uses a
footprint above the new cap. Scorecard note labels Impressionist as an approximation.

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

* i18n(pl): translate the Pattern Stamp strings

The Polish catalog landed on main after this PR was written; the
translation-coverage tests need every language to carry new UI strings.

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

* Tool::ALL holds 51 tools now that Red Eye and Pattern Stamp both landed

The rebase onto main (with #1084's Red Eye tool) leaves Tool::ALL one entry
longer; bump the array length and the count test.

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

* Flyout test: qualify egui::Vec2 (panels.rs no longer imports Vec2)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:07:06 -04:00
578bc905a6 i18n: split "Light" — font weights get their own context, Camera Raw keeps the panel term (#1061)
* i18n: split "Light" - font weights get their own context, Camera Raw keeps the panel term

The plain "Light" entry served two unrelated meanings, and every
catalog had resolved the collision in favour of the font weight (de
"Light", ja "ライト", fr "Maigre", ko "가늘게"...), so Camera Raw's
Light section header showed a weight word in most languages.

- The type tool's style dropdown now shows localized labels while the
  raw string stays the engine's fontStyle key (value/label pairs, like
  the anti-alias dropdown), with each word translated in a new
  fontWeight context (Thin...Black, Italic).
- Camera Raw's Light section reads cameraRaw|Light (Licht, Luz,
  Lumière, Свет, 光...); every catalog gains the missing rows, and a
  new i18n test pins both contexts plus their distinction per language.

* i18n(pl): font weights and Camera Raw keep their split contexts in Polish too

* i18n tests: close the Seafoam test before the font-weight test (rebase fixup)

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-08 21:00:58 -04:00
f3fda94c87 Large document: a notice explains a slow CPU fallback redraw (#1015) (#1427)
* Large document: a notice explains a slow CPU fallback redraw (#1015)

A full refresh that falls back to the CPU compositor blocks the canvas for as
long as it runs, with only a log line to say why. The first one per document
that takes over a second now posts a notice: how long it took, which edits
redraw the whole document, and for the GPU memory budget fallback, that
raising Memory usage raises the budget (up to a quarter of RAM).

* Slow-refresh notice test: check the real "Memory usage" casing; add Polish rows

The last assertion looked for "Memory Usage" while the string says
"Memory usage", so it passed vacuously. The budget case now asserts the
hint is present too. pl.tsv gets rows for the new strings.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:53:39 -04:00
Brandon ThomasandClaude Opus 5.5 7a9a2684a1 Toolbar test: qualify egui::Vec2 (main fails to compile ui-egui tests) (#1519)
#1376's test used the module-level Vec2 import that #886 dropped; each
compiled alone, but not together.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:49:21 -04:00
Brandon ThomasandClaude Opus 5.5 ed6f989bc6 Regenerate docs/scorecard.md and docs/parity.md after the latest community merges (#1518)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:47:09 -04:00