Commit Graph
8944 Commits
Author SHA1 Message Date
Trevor BrownandClaude Fable 5.1 8fdcce3cd1 Lock Layers shortcut test: assert the key the panel menu paints (#11397)
From review: the panel-menu test collected no output, so a hardcoded or missing shortcut text would have passed. It now gathers the text shapes the menu paints and checks the default key, a changed key and a removed binding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 06:00:40 -05:00
Trevor BrownandClaude Fable 5.1 fa1814a076 Shortcuts: Ctrl+/ opens Layer › Lock Layers…, shown in the Layer menu and the Layers panel menu (#11397)
layer.lockLayers now defaults to Cmd+/ (Ctrl+/ off the Mac) in its engine spec and in the Layer menu row. The Layers panel menu's Lock Layers… row shows the command's current key, so a key changed in Edit › Keyboard Shortcuts shows up there. Plain / still toggles Lock Transparent Pixels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 05:07:47 -05:00
Khawar Zamman WaniandFredApps 98bcbfab90 perf(io): build JPEG/PNG tiles straight from the decoded rows, in parallel (#10259) (#10281)
* perf(io): build JPEG/PNG tiles straight from the decoded rows, in parallel (#10259)

Surface::try_write_rows_with fills tiles row by row on all cores; image_surface
converts from the borrowed decoded rows into it (no per-band copy, no staging
Image). Opaque sources skip the prune scan. import_flat_with checks cancel after
the decode and the tile build and reports progress.

* fix(io): store an F16 target as F32 so its opaque alpha is 1.0

An image without alpha converted to an F16 target got the alpha 0.0: the
opaque-alpha path copied two bytes of 1.0f32. The surface keeps 16-bit float
as 32-bit float anyway, so image_surface now converts an F16 target to F32.

* Format fused tile import integration

* Route default flat imports through interrupt wrapper

* Remove unused flat import cancellation wrapper

* Exercise options import API in cancellation tests

* Stop flat image tile construction on cancellation

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 10:33:29 +01:00
coqui-softwareandFredApps 51131b82cf UI: 2-column Home screen with recent file cards and centered workspace (#7231)
* UI: 2-column Home screen with recent file cards and centered workspace

* Rate-limit live-document thumbnail composition on Home

While an open document is being edited, Home recomposed its preview on the UI
thread on every revision. Regenerate at most every 500 ms, keep showing the
previous preview in between, and schedule a repaint so the last revision is
still drawn.

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 08:44:21 +01:00
d7d138b1e5 Art History Brush: Opacity on the options bar (#10263) (#10278)
* Art History Brush: Opacity on the options bar (#10263)

The Art History Brush's own brush opacity is shown in its options bar
(0-100%) and sent to paint.artHistoryBrush. It is kept per tool, saved
across launches, set by the 1-0 keys and restored to 100% by Reset Tool,
as the Clone Stamp's is. Tests cover the UI wiring, per-tool persistence,
Reset Tool and stroke output at 8/16/32-bit.

* Update crates/ui-egui/src/panels/options_bar.rs

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>

* Art History Brush starts at 100% opacity on first use

A tool picked for the first time takes the outgoing brush; the Art
History Brush now takes it at full opacity, as the issue's default asks,
so a 30% Brush no longer makes the first Art History stroke 30%. Other
tools keep inheriting opacity.

* Art History Brush: document Opacity in the command contract and carry it in tool presets

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 07:15:57 +01:00
Khawar Zamman WaniandFredApps 316e00a97e Bound Photomerge memory and safely reject out-of-budget jobs (#7851) (#10463)
* Reduce Photomerge input and fill memory

* Bounded working-memory budgeting and safe allocation for Photomerge

* Photomerge memory: estimate warped sizes, no RAM ceiling, shared RAM detection

- Budget is half of physical RAM (at least 1 GiB) with no 8 GiB ceiling.
- The pre-flight estimate uses each image's warped extent and the band
  grid's exact bound (band_bytes_bound), at the levels seam_blend uses.
- Auto-Blend keeps its old batching with no single-band cap.
- multiband returns a Result instead of a blank image on failure.
- Physical-memory detection moves to photocraft_engine::memory; the GPU
  budgets reuse it, so PHOTOCRAFT_RAM_MB means the same in both.
- Document memoryBudget; restore comments; tests for the band cap and the
  budget precedence.

* Photomerge: lower the blend's peak and estimate it by phase

- The band build makes its binary coverage straight from the alpha and
  writes each Laplacian level in place, so a band holds two fewer planes.
- The weight sums are freed before the collapse, which adds each level
  in place and frees it as it goes.
- The estimate is the largest phase (warp, ROIs, bands, collapse, output,
  fill) rather than the sum of all of them, and the bands get what is
  left of the budget after the ROIs and accumulators.

Every output pixel is unchanged: each change drops a plane or does the
same elementwise operation in place.

* Fix Photomerge band budget validation and fallible pyramid allocation

* Keep reference pyramid test helpers infallible

* Photomerge: bound source loading with the memory budget

Resolve the budget before any source is read, and admit each flattened
source before its surface is allocated. The accounting charges the
retained surface, the same-canvas temporary the flatten writes while the
source document is alive, and a file import's encoded bytes, with
saturating math so an overflow cannot wrap under a finite budget. Merge
to HDR Pro passes no budget and keeps its previous behaviour.

Tests cover the exact retained-plus-temporary charge, a refused source,
the within/over-budget command paths, the post-load estimate path, and
the unbudgeted HDR loader.

* Photomerge: bound source reads and imports by the remaining budget

Read each source file with what is left of the budget as its limit, lower the importer's own PSD/TIFF preflight or the flat decoders' pixel allowance to the remainder after the retained sources and encoded bytes, and refuse formats with no pre-decode estimate. Charge the decoded document's actual pixels (every layer) plus the flatten's working and output surfaces before flattening. Merge to HDR Pro keeps its unbudgeted read-and-import path.

* Remove redundant TIFF decode options update

* Clean up Photomerge budget Clippy findings

* Accept early decoder admission in source budget regression

* Photomerge: report unreadable sources as file errors; never reserve past a read limit

- A budgeted Photomerge reported a missing or unreadable source file as
  'not enough memory'. Only a file over the remaining budget, or one memory
  cannot hold, is a memory error now; others are file errors, as without a budget.
- read_all_with reserved a whole read chunk before checking the caller's limit
  for a file that grew after it was statted. It now refuses first and caps the
  reservation at the limit.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 06:23:56 +01:00
b152f4175e Move tool: auto-nest layers into the artboard they're dropped on (#10218) (#10512)
* Move tool: auto-nest layers into the artboard they're dropped on (#10218)

In Photoshop a layer dragged with the Move tool onto another artboard joins
that board, and one dragged onto the pasteboard leaves its board. PhotoCraft
had the Prevent Auto-Nesting lock but not the behaviour, so a layer dragged
onto another board stayed in its old board and was clipped away.

layer.translate takes an opt-in "nest" flag. With it, a moved layer whose
centre now lies on a different board joins that board on top of its stack,
and one moved off every board goes to the top level above the board it left,
in the same undo step. The Move tool's drag release sets it, which covers
Alt-drag copies; nudges, actions and scripts are unchanged. Boards,
the Background, empty layers, over-deep groups and layers locked with Prevent
Auto-Nesting stay where they are.

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

* Auto-nesting: true, floored middle for the board under a layer

Review follow-up: board_under found the middle of a dropped layer by halving
each edge first, so a layer at 9..11 counted as centred at 9 and could join
(or stay in) the wrong one of two boards meeting at x = 10.

The middle now comes from one helper, layer_place::centre, shared with the
placement of new and pasted layers: the edges are added in i64 before halving
and the sum is floored, so -4..3 (middle -0.5) is left of a board starting at
x = 0, not on it. Adds regressions for both.

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

* Document the auto-nesting rule and exclusions in layer.translate

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 06:18:46 +01:00
Khawar Zamman WaniandFredApps ad59d9962c Type: add OpenType Stylistic Sets submenu (Set 1–20) (#10304)
* Type: add OpenType Stylistic Sets submenu (Set 1–20)

Type > OpenType > Stylistic Sets toggles ss01–ss20 through new
type.openType.stylisticSets.setN commands on the usual formatting scope
(selected characters while editing, else the selected type layers).
The toggle direction and the menu checkmark follow the style at the
selection start. Updates TYPE-218-5 and G8.

Closes #10285

* Stylistic Sets: round-trip test through a full PSD; scorecard parity count

* Stylistic Sets: test PSD round trip on an opened file; record the new-document PSD gap

Documents made in PhotoCraft get no Txt2 block on PSD save, and the set
mask lives only there, so their sets are not saved to PSD yet. The engine
test keeps the .pcraft round trip, an io test covers the PSD round trip
of a file with type objects, and TYPE-218-5 and G8 record the gap.

* Stylistic Sets: share one tag table between the engine and the menu

The engine exports STYLISTIC_SET_TAGS; the Set 1–20 commands index it and
the menu checkmarks read it, so the 20 tags are listed once.

* Stylistic Sets: check malformed menu ids through the resolver, not the menu list

* Scorecard: TYPE-218-5 notes PSD save of stylistic sets in documents made here (#10308)

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 06:16:54 +01:00
Khawar Zamman WaniandFredApps efbe97bd53 Fix imported gradient presets sharing selection names (#10521)
* Fix duplicate gradient preset names on import and restore

* Preserve addressable gradient names during duplicate migration

* Preserve gradient identities during import and restore

* Assert restored selected gradient retains its stops

* Pass replace in the duplicate gradient names test after #10640

Main now imports a re-imported .grd beside its group unless replace is set
(#10640). The test re-imports over the same group, so ask for that.

* Use the same replace default for gradient name allocation and group insertion

After #10640 the group insertion defaults replace to false, but the name
allocation still defaulted it to true, so a default re-import created a
sibling group with the same preset names. Resolve replace once and add a
regression for the default and explicit replace paths.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 06:04:12 +01:00
Khawar Zamman WaniandFredApps 13b481f892 Expose optional GPU composition and export in photocraft-cli (#10912)
* Add headless GPU backend foundation for composition and flat export (issue #8062 PR 1)

* Fix headless GPU readback bounds, API compliance, and error preservation (#8062)

* Fix GPU readback bounds, context thread spawn error, and test parity (#8062)

* Fix BandRenderer Send bound, PNG reborrows, and clippy lints (#8062)

* Fix effect shadow test construction and remove dead code wrappers (#8062)

* Use short reborrows for Headless renderer without lifetime escaping (#8062)

* Fix clippy collapsible_if and ensure multi-band geometry in flat export tests (#8062)

* Fix automation Rect import, add renderer diagnostics, and harden flat export retries (#8062)

* Expose optional GPU rendering in photocraft-cli (#8062)

* Fix GPU feature test APIs and native target gates

* Fix CLI locked metadata, bounded benchmark params, and protocol diagnostics (#8062)

* Verify unsupported GPU effects before testing band seams

* Fix CLI renderer borrows and GPU-enabled test compilation

* Use compositor buffer ownership API after upstream memory accounting change

* Fix GPU CLI lint failures and validate run environment policy

* Update export renderer test helper for owned compositor buffers

* Fix needless benchmark layer name borrow

* Fix headless GPU backend review findings (#10842)

- release a closed document's GPU caches through a BandRenderer hook
- bound the monitored readback wait and report a stalled GPU as device loss
- reject 32-bit float documents before band readback so exports fall back to CPU
- keep previews at 8-bit RGBA and report the palette/Duotone renderer bypass
- retain renderer diagnostics when encoding fails
- strengthen renderer, save-path and GPU diagnostics tests

* Use valid batch arguments and inspect method in GPU CLI tests

* Add GPU close-hook and 8-bit preview regression tests (#10842)

* Fix GPU CLI review findings (#10912)

- guard the benchmark's modulo divisors for one-pixel dimensions
- count a cold export as GPU only when the renderer context was used
- report every batch file's export warnings, deduplicating only GPU diagnostics
- drain renderer diagnostics before propagating convert and run save errors
- cover renderer use in the GPU precedence, bridge and serve tests

* fix(io): correct renderer binding mutability

* test(cli): verify narrow benchmark inputs and GPU diagnostics

* Fix remaining headless GPU review findings (#10842)

- release a document's GPU caches when file.close, file.closeAll or
  file.closeOthers removes it through a command
- honor cancellation while band readback waits, polling in 100 ms slices
- fault the device after a readback timeout so later exports skip it
- report single-layer, CMYK ink-mixing, ORA and PDF CPU paths to a
  requested renderer
- keep fallback warnings in their export instead of leaking them into the
  next one
- requeue renderer diagnostics when a renderer-aware save's final write
  fails
- write MCP/serve diagnostics without panicking on a broken stderr
- document the actual colour-space contract and drop F32 from the GPU
  depth list

* Fix remaining headless GPU review findings (#10842)

- release a document's GPU caches when file.close, file.closeAll or
  file.closeOthers removes it through a command
- honor cancellation while band readback waits, polling in 100 ms slices
- fault the device after a readback timeout so later exports skip it
- report single-layer, CMYK ink-mixing, ORA and PDF CPU paths to a
  requested renderer
- keep fallback warnings in their export instead of leaking them into the
  next one
- requeue renderer diagnostics when a renderer-aware save's final write
  fails
- write MCP/serve diagnostics without panicking on a broken stderr
- document the actual colour-space contract and drop F32 from the GPU
  depth list

* Fix remaining GPU CLI review findings (#10912)

- reject non-UTF-8 PHOTOCRAFT_GPU values instead of silently using the CPU
- release a batch item's document caches before taking the renderer to the
  next item, and report fallbacks once
- count warm benchmark runs that fell back to CPU in the summary
- write serve diagnostics without panicking on a broken stderr
- assert that the batch test's --gpu policy reaches the renderer

* Use question mark for cancellable readback errors

* Use question mark for cancellable readback errors

* Keep GPU export diagnostics with their attempt and stop bands on cancel

Encode native .pcraft bytes before draining renderer diagnostics, and
requeue export warnings when a workspace write fails, so callers still
report the adapter or fallback reason with the error.

Move a failed GPU attempt's queued diagnostics into its CPU retry result
instead of leaving them for a later successful export to report.

Stop GPU band readback at the next band after cancellation, and check the
interrupt before each CpuRenderer sink delivery.

Band readback uploads half-precision textures, so reject U16 alongside
F32: their samples cannot survive the upload. Tighten the 8-bit CPU/GPU
parity checks to 1/255.

* Keep GPU export diagnostics with their attempt and stop bands on cancel

Encode native .pcraft bytes before draining renderer diagnostics, and
requeue export warnings when a workspace write fails, so callers still
report the adapter or fallback reason with the error.

Move a failed GPU attempt's queued diagnostics into its CPU retry result
instead of leaving them for a later successful export to report.

Stop GPU band readback at the next band after cancellation, and check the
interrupt before each CpuRenderer sink delivery.

Band readback uploads half-precision textures, so reject U16 alongside
F32: their samples cannot survive the upload. Tighten the 8-bit CPU/GPU
parity checks to 1/255.

* Report partial warm fallbacks and CPU fallback on stderr

The averaged GPU Warm row now says how many warm runs fell back to the
CPU and were excluded from the average, through the existing helper.

The single-layer convert diagnostic test checks that the CPU fallback is
reported on stderr, in GPU-enabled and CPU-only builds alike.

* Keep headless export warnings when a workspace write fails (#10842)

A capability-scoped save encodes the export, then writes it; when the write
fails, the export warnings (folded together with renderer diagnostics) were
pushed back through `BandRenderer::push_diagnostic`, whose default is a no-op.
A custom renderer that overrides only `drain_diagnostics` therefore lost the
adapter or fallback reason that belonged with the error.

Keep them in `Headless` instead and return them from the next
`drain_diagnostics`, alongside the renderer's own queue. The regression uses a
renderer without a `push_diagnostic` override and checks the warnings survive
the failed write and drain exactly once.

Also name the actual source of the depth limit in the headless GPU matrix:
layer content is uploaded as half-precision textures, not band readback.

* Stop CPU bands and CPU retries on cancellation (#10912)

The CPU retry after a retryable GPU error composes through
`FlatSource::bands`, whose shared `emit` sink did not poll the export
interrupt: a cancel was only observed after the whole composite finished.
Check it once per band, as `CpuRenderer` does, for both RGB and CMYK ink
bands. The flat and streamed-PNG retry branches also check before starting
the full CPU composite again, so a cancelled export returns
`IoError::Cancelled` instead of re-rendering the document.

Tests: band-sink cancellation for RGB and CMYK inks, a renderer call counter
proving a second export reaches the renderer, and a pending `wait_slices`
poll observing cancellation set mid-wait.

* Merge origin/main into GPU foundation (#10842)

* Keep GPU-path diagnostics on every failure path, honour cancel in native copy

- save: export warnings of a failed local write stay in Headless pending diagnostics
- render: preview fallback warnings are returned, drained, and added to the doc.render reply
- io: renderer-neutral fallback wording; native single-layer copy checks cancellation per band
- gpu: gate wait_slices tests off wasm32 (#10842)

* Address CodeRabbit review: keep fallback reasons on failed retries, report native-copy and quick-export CPU use, guard diagnostics drain (#10912)

* Accept render diagnostics from the doc.render reply in the serve GPU test (#10912)

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 05:39:59 +01:00
davidvhkandFredApps f1137013a2 feat(paint): add History Brush opacity/flow controls, source selection, and custom icon (#4446)
* feat(paint): add History Brush opacity/flow controls, custom icon, and options bar source label

- Options bar and keyboard shortcuts (digits 0-9 and Shift+digits) for History Brush opacity and flow %
- Options bar displays translated source state or snapshot name with missing-source hint when deleted
- Dedicated clean-room history-brush icon combining Lucide brush with counter-clockwise rewind arc
- Preserved JetBrainsMono attribution in ATTRIBUTION.md and added history-brush icon attribution
- Add contributor credit in people.toml

* History Brush options bar: rustfmt the source label

* Opacity keys: 00 gives Clone Stamp and History Brush their 1% minimum

Their Opacity fields and the retouch command start at 1%, so 00 set a 0%
the stroke then painted at 1% (CodeRabbit). The Brush still goes to 0%.

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 05:29:57 +01:00
a331258dff bs: Add Bosnian (bs) to the interface languages (#10290)
* Add Bosnian (bs) to the interface languages

bs.tsv: 4,552 rows, Latin script, Ijekavian, the same keys as the newest
catalogs. Wording shared with storytold/craft-i18n locales/bs/; the 306
strings craft-i18n doesn't have yet are new. Every Bosnian locale,
Cyrillic included, resolves to it, as for Serbian; plurals reuse the
one/few/other rule of plural_russian.

Locale and plural tests, live locale case, localization docs and an
ATTRIBUTION row (AI-assisted, not yet reviewed by a native speaker).

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

* Add Bosnian to the translation license notice; document the locale test

LICENSE-translations.txt now lists the Bosnian catalog and its
provenance (shared with storytold/craft-i18n locales/bs/, AI-assisted,
not yet native-reviewed), as ATTRIBUTION.md points to it. The touched
live locale test gets a doc comment.

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

* bs: wording from the craft-i18n catalog review

The Bosnian catalog review (storytold/craft-i18n#72) asked for one word
per concept; this catalog follows: pan is "pomjeranje" (not
"pomicanje"), a spreadsheet is "proračunska tabela", Greek & Cyrillic
reads "Grčko pismo i ćirilica". Placeholders and ellipses are unchanged.

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

* bs: translate the 30 strings added on main; match Camera Raw labels

Main gained Path operations, the SVG smart object editor, Esc/Return pen
status messages and more since this catalog was written, so the
every-language tests (esc_ends_a_refused_draft_and_return_keeps_it) failed
on bs. Five cameraRaw-context entries (White Balance: As Shot, Tone
Histogram, Show Selected Region, Opened {name}, Loaded Camera Raw settings
from {file}) differed from the plain entries the panel actually draws, which
failed the Camera Raw localization test; they now match.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 05:16:02 +01:00
ca151c6b84 Paste into another artboard keeps the place on the board (#8788) (#10538)
* Paste into another artboard keeps the place on the board (#8788)

Copying from one artboard and pasting with another board (or a layer on it)
selected put the pixels at their old canvas spot on the first board, where
the second board hid them, or in the middle of the view; the new layer still
went beside the selected one, inside the second board.

The clipboard now remembers the artboard a Copy came from (the copied layer's
board, every copied layer's when several are selected, or for Copy Merged the
board under the middle of the copied area). Paste and Paste in Place into a
different board of the same document move the pixels by the offset between
the two boards' origins, so they land at the same spot on the new board, and
the layer goes inside it as before. Pasting on the same board, outside the
boards or into another document is unchanged, and Paste Into / Outside keep
centring on the selection.

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

* Paste between artboards: copied layers and true middle for the source board

Review follow-ups:
- With a pixel selection, Copy takes the active layer only, even when several
  layers are selected, but the source board was worked out from every
  selected layer, so selected layers on another board cancelled it. It now
  uses the layers Copy actually takes.
- Copy Merged found the board under the middle of the copied area by halving
  each edge first, which rounds 9..11 to 9 and could pick the wrong one of two
  boards that meet. The edges are now added before halving.

Adds a test for each, both failing before.

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

* Paste between artboards: locked boards and the placement midpoint

Review follow-ups:
- With a locked board itself selected, the pasted layer goes above the board,
  not into it, yet it was still moved to the board's spot. The board shift is
  now skipped in that case; a layer inside a locked board still lands on it.
- layer_place found the board a new layer lands on by halving each edge
  first, so a paste moved to 19..21 had its middle at 19 and was turned away
  from a board starting at 20. The edges are now added before halving, as for
  the copy's source board.

Adds a test for each, both failing before.

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

* Paste between artboards: floored placement middle, pastes join smaller boards

Review follow-ups (FredApps):
- The placement middle truncated toward zero, so content at -4..3 (middle
  -0.5) counted as 0 and a board starting at x = 0 took it in. One helper,
  layer_place::centre, now adds the edges in i64 and floors the sum; Copy
  Merged's source board uses it too, and so does #10512's auto-nesting, with
  a byte-identical layer_place.rs so the two merge in either order.
- Pasting onto a smaller board kept the offset from its origin, but the
  layer's middle then fell past the board's edge and it was placed outside.
  A paste moved onto a board now joins it; the board clips the rest.

Tests: negative-boundary placement, a document without boards, a smaller
board (offset kept, board clips), each failing before where it applies.

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

* Paste between artboards: keep a clipped paste's clipping on a board

Review follow-ups (CodeRabbit):
- A paste that joins a board always lost its clipping. It now keeps its own
  clipping flag (the copy's, from before placement) while the board holds a
  layer it can clip to, and drops it on an empty board, as a new layer does.
- Documented that boards more than the 32-bit range apart can't shift a paste,
  which then keeps its usual placement rather than wrapping around.

Adds a test (a clipped group pasted onto a board with a layer and onto an
empty one), failing before.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 04:55:07 +01:00
Khawar Zamman WaniandFredApps fba5d43a02 Use native Pen cursors to avoid canvas-frame delay (#10860)
* feat(ui-egui): native Pen cursor bitmaps with bounded cache (#9122)

- Render standard Pen cursors (Pen, Freeform Pen, Curvature Pen, Add
  Anchor, Delete Anchor, Convert Point) as OS cursor bitmaps to eliminate
  presentation/canvas frame dependency and tracking latency.
- Preserve exact 8-offset dark outline and white foreground composition,
  native platform scaling, and tool hotspots.
- Implement bounded caching (up to 16 entries) keyed on shape and scale to
  ensure zero re-rasterization on warm pointer movement and rapid tool toggling.
- Preserve precise/crosshair mode, vector close-path cursor state, and painted
  tooltip fallback for unsupported platforms/viewports.
- Add comprehensive regression tests for rasterization, alpha/color fidelity,
  hotspots, lifecycle, and cache bounding.
- Add release benchmark example (cursor_bench) measuring cold rasterization
  and warm movement/cache performance with optional PNG artifact export.
- Retain Pencil painted outline: documented that rigid OS cursor hardware
  planes cannot preserve document pixel grid snapping across fractional
  zoom/scales without continuous drift or per-frame churn.

* fix(ui-egui): correct Pen cursor rasterization, caching, and benchmark (#9122)

- Fix cursor_bench imports to use public tool_cursor::pen with correct frame lifecycle.
- Apply and verify requested native scale per platform in benchmark Context.
- Replace benchmark hardcoded metrics with truthful measured counters and Result error returns.
- Fix Arc::ptr_eq identity checks, unused imports, and Image::from_raw codecs API.
- Add baseline painted fallback measurements to benchmark for real speedup comparisons.
- Keep original single-entry cache for non-Pen shapes and add separately bounded 6-entry Pen cache without whole-cache cloning.
- Preserve fractional-scale logical geometry via subpixel resampling and compensate for OS integer hotspot quantization.
- Add independent comparison test against painted path across scales and backgrounds (light/dark/midgrey).
- Fix Shape::Pen tuple match hazard and doc comments on pen().
- Register cursor_bench in perf/budgets.toml for xtask perf runner integration.

* fix(ui): benchmark cursor preparation inside real egui frames

* feat(ui-egui): visual validation reference, comparison metrics, and contact sheet for native Pen cursors (#9122)

* fix(ui): render cursor reference pairs without duplicate Area ids

* fix(ui): use egui 0.37 frame API in cursor benchmark

* fix(ui): retain rendered cursor textures and restore existing documentation

* fix(ui-egui): restore crisp Pen cursor rasterization and update cursor tests (#9122)

- Eliminate double-resampling blur in Pen cursor rasterization: use direct discrete
  vector glyph rasterization matching the painted fallback without fractional softening.
- Update CapsLock test to assert native OS cursor bitmap and fallback Crosshair for
  Pen tools, validating exact bitmap RGBA, dimensions, and hotspot.
- Add synthetic test proving mathematical equivalence between straight RGBA hardware
  blending and multi-pass painter fallback across dark, midgrey, and light backgrounds.

* test(ui): expect the native bitmap for Precise crosshair cursors

* fix(ui): bound cursor benchmark samples and strengthen bitmap tests

(cherry picked from commit 118c673e5d822742da0a1d99c5aa3a9989b47ff8)

* test(ui): pin cursor percentile rank and glyph-derived hotspots

* test(ui): cover every Pen cursor mapping and per-variant edge samples

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 04:54:22 +01:00
Khawar Zamman WaniandFredApps 1089932ae9 Add a headless GPU backend for composition and flat export (#10842)
* Add headless GPU backend foundation for composition and flat export (issue #8062 PR 1)

* Fix headless GPU readback bounds, API compliance, and error preservation (#8062)

* Fix GPU readback bounds, context thread spawn error, and test parity (#8062)

* Fix BandRenderer Send bound, PNG reborrows, and clippy lints (#8062)

* Fix effect shadow test construction and remove dead code wrappers (#8062)

* Use short reborrows for Headless renderer without lifetime escaping (#8062)

* Fix clippy collapsible_if and ensure multi-band geometry in flat export tests (#8062)

* Fix automation Rect import, add renderer diagnostics, and harden flat export retries (#8062)

* Fix GPU feature test APIs and native target gates

* Verify unsupported GPU effects before testing band seams

* Use compositor buffer ownership API after upstream memory accounting change

* Update export renderer test helper for owned compositor buffers

* Fix headless GPU backend review findings (#10842)

- release a closed document's GPU caches through a BandRenderer hook
- bound the monitored readback wait and report a stalled GPU as device loss
- reject 32-bit float documents before band readback so exports fall back to CPU
- keep previews at 8-bit RGBA and report the palette/Duotone renderer bypass
- retain renderer diagnostics when encoding fails
- strengthen renderer, save-path and GPU diagnostics tests

* Add GPU close-hook and 8-bit preview regression tests (#10842)

* fix(io): correct renderer binding mutability

* Fix remaining headless GPU review findings (#10842)

- release a document's GPU caches when file.close, file.closeAll or
  file.closeOthers removes it through a command
- honor cancellation while band readback waits, polling in 100 ms slices
- fault the device after a readback timeout so later exports skip it
- report single-layer, CMYK ink-mixing, ORA and PDF CPU paths to a
  requested renderer
- keep fallback warnings in their export instead of leaking them into the
  next one
- requeue renderer diagnostics when a renderer-aware save's final write
  fails
- write MCP/serve diagnostics without panicking on a broken stderr
- document the actual colour-space contract and drop F32 from the GPU
  depth list

* Use question mark for cancellable readback errors

* Keep GPU export diagnostics with their attempt and stop bands on cancel

Encode native .pcraft bytes before draining renderer diagnostics, and
requeue export warnings when a workspace write fails, so callers still
report the adapter or fallback reason with the error.

Move a failed GPU attempt's queued diagnostics into its CPU retry result
instead of leaving them for a later successful export to report.

Stop GPU band readback at the next band after cancellation, and check the
interrupt before each CpuRenderer sink delivery.

Band readback uploads half-precision textures, so reject U16 alongside
F32: their samples cannot survive the upload. Tighten the 8-bit CPU/GPU
parity checks to 1/255.

* Keep headless export warnings when a workspace write fails (#10842)

A capability-scoped save encodes the export, then writes it; when the write
fails, the export warnings (folded together with renderer diagnostics) were
pushed back through `BandRenderer::push_diagnostic`, whose default is a no-op.
A custom renderer that overrides only `drain_diagnostics` therefore lost the
adapter or fallback reason that belonged with the error.

Keep them in `Headless` instead and return them from the next
`drain_diagnostics`, alongside the renderer's own queue. The regression uses a
renderer without a `push_diagnostic` override and checks the warnings survive
the failed write and drain exactly once.

Also name the actual source of the depth limit in the headless GPU matrix:
layer content is uploaded as half-precision textures, not band readback.

* Stop CPU bands and CPU retries on cancellation (#10912)

The CPU retry after a retryable GPU error composes through
`FlatSource::bands`, whose shared `emit` sink did not poll the export
interrupt: a cancel was only observed after the whole composite finished.
Check it once per band, as `CpuRenderer` does, for both RGB and CMYK ink
bands. The flat and streamed-PNG retry branches also check before starting
the full CPU composite again, so a cancelled export returns
`IoError::Cancelled` instead of re-rendering the document.

Tests: band-sink cancellation for RGB and CMYK inks, a renderer call counter
proving a second export reaches the renderer, and a pending `wait_slices`
poll observing cancellation set mid-wait.

* Merge origin/main into GPU foundation (#10842)

* Keep GPU-path diagnostics on every failure path, honour cancel in native copy

- save: export warnings of a failed local write stay in Headless pending diagnostics
- render: preview fallback warnings are returned, drained, and added to the doc.render reply
- io: renderer-neutral fallback wording; native single-layer copy checks cancellation per band
- gpu: gate wait_slices tests off wasm32 (#10842)

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 04:53:25 +01:00
Khawar Zamman WaniandFredApps 3c225dbfef perf(ui): cache Adaptive Wide Angle preview mesh for constraint overlays (#9872)
* fix(ui): cache adaptive wide angle preview mesh

* test(ui): match explicit constraint stroke color mode

* test(ui): clear unused texture updates in redraw regression

* test(ui): inspect grouped overlay paths independent of animation

* fix(ui): address wide-angle mesh cache review feedback

Repaint after panel edits so overlays catch up, share the proxy rect
between render and overlays, document the dirty invariant, and make the
overlay test measure its own baseline instead of hard-coded layout.

* Remove unused colour format import after integration

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 04:48:35 +01:00
Khawar Zamman Wani b3fd0159ec perf(paint): create a large dab's new coverage tiles on the workers (#11349)
Each new coverage tile zeroes 16 KB. The parallel path of
CoverageMap::accumulate did that on the calling thread while building
its work list, so a large dab's new tiles ran one at a time. Create
them inside the parallel section instead.

coverage_bench gains a large-dab line (3457x3457, what a soft 2000 px
tip covers). big_dab_bench's "accumulate" figure is relabelled: it is
a cold first push minus a warm rasterize, not accumulate alone.

Refs #11339
2026-11-05 04:05:21 +01:00
Khawar Zamman Wani 0a9717fa64 PDF export: keep the top text and shape layers as vectors, with selectable text (#10355)
* Skip compositing for geometric measurements

* Preserve simple PDF shapes and glyph source ranges

* Preserve text and shapes in PDF export (#10269)

- Export topmost simple text layers as vector glyph outlines with selectable text
- Embed ActualText unicode span and invisible text operators (mode 3 Tr) for 100% extraction
- Support stroked shapes in vector PDF export (centered solid strokes, dashes, line caps/joins)
- Retain glyph style indices in text rendering outlines

* Drop a product name from the glyph outline doc comment

* PDF export: keep path text and synthetic bold raster

* Fix PDF Unicode selection and respect font embedding permissions

* Render the text cache in PDF raster fallback fixtures

* Fix PDF vector selection eligibility and finite text matrices

* Preserve upper PDF vectors and reject overflowing curve controls

* Fix text shape import in PDF regression test

* Cover PDF Unicode map blocks and leading selection whitespace
2026-11-05 03:33:35 +01:00
Dylan M. Taylor 8e9277e20e Pass the Linux signing key to the release workflow (#11344) 2026-11-04 17:44:58 -05:00
David Mann 8e82c0e728 Merge pull request #11278 from davidmann4/integration-batch-16
Integration batch 16: 9 epic PRs on main
2026-11-04 19:20:54 +01:00
David MannandClaude Opus 5.5 14f6ace170 Fix-up #11243 (review): Free Transform a Copy of a targeted mask still copies
The linked-layer path (no copy, the link group's box) applies only when the
layer's own pixels are the target; an unlinked layer mask targeted on a linked
layer is copied as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:33:00 +01:00
David MannandClaude Opus 5.5 c144e36d9c Fix-up #11077 (review): say that pixels past the canvas count for Empty
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:32:59 +01:00
David MannandClaude Opus 5.5 533cf55ef5 Fix-up #11235 (review): a filter already on at the row's first draw deselects
With no criteria kept yet (the row's first draw, e.g. after ui.set while the
panel was closed), a filter that is on counts as applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:32:59 +01:00
David MannandClaude Opus 5.5 1a9be65e0f Fix-up #10965 (review): a continuation resumes once; a layer target checks its receiver
A kept clone of a paused action's continuation could reopen a scope that had
already resumed and finished; every clone now shares one resumed mark. A dialog
paused on a document target (Quick Mask or an alpha channel) and switched to a
layer target now checks the layer that was active at the pause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:32:59 +01:00
David MannandClaude Opus 5.5 a8b69338da Fix-up #11243: Free Transform a Copy of a linked layer makes no copy
Measured in the reference application: Ctrl+Alt+T on a linked layer without
a selection makes no copy; the box frames and moves the whole link group, and
History shows one Free Transform. An unlinked layer still gets its copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:33:00 +01:00
David Mann ff7d36fe4b Merge the batch branch (closing merge of #6647) into the fix-ups 2026-11-04 15:46:08 +01:00
David MannandClaude Opus 5.5 524826e9fc Fix-up #10396: Esc in crop_beyond_canvas hides the frame
Since #10396 Esc hides the crop frame until the next press (#10370), so the
test expects no frame after Esc instead of a new default one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:45:35 +01:00
David MannandClaude Opus 5.5 642bcf6229 Fix-up #10986 with #10965: a play that changes nothing adds no history entry
With #10965 an action play is one history entry, and a play that leaves the
document unchanged adds none (#10944). The Current Layer fill from the uniform
layer alone rewrites the same pixels, so the test expects an entry, and checks
undo and redo, only when the document changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:45:35 +01:00
David MannandClaude Opus 5.5 8bc0179235 Fix-up #11243: a lone copy of a linked layer joins its link group
Measured in the reference application: Duplicate and Layer via Copy of one
linked layer put the copy in the original's link group. Copies of several
layers of one group still get a group of their own, and a copy that is the
only one from its group among several copied layers is still unlinked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:45:35 +01:00
David MannandClaude Opus 5.5 31a9b91913 Merge PR #6647 (closing): its zoom field work landed with #10145; changes reverted, tree unchanged
Merged with the ours strategy so GitHub closes #6647 as merged. Everything
it carries is either on main already (via integration batch #10145) or,
for the later contributors entry, left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:44:59 +01:00
David Mann cb323e9aea Merge PR #10965: Group action playback into one history state 2026-11-04 14:51:18 +01:00
David Mann d7e2bbe4a4 Merge PR #11077: Layers panel: filter by layer attribute (#10881)
# Conflicts:
#	crates/ui-egui/src/panels/layer_filter_ui.rs
#	docs/control-protocol.md
2026-11-04 14:51:17 +01:00
David Mann aac944a315 Merge PR #11223: Actions: map supported classic Gradient recordings
# Conflicts:
#	crates/engine/tests/it/main.rs
2026-11-04 14:51:00 +01:00
David Mann fd736f7ddc Merge PR #10986: Actions: map recorded Content-Aware Fill settings 2026-11-04 14:50:49 +01:00
David Mann e8bc140aa2 Merge PR #11243: Free Transform: linked layers transform together (#11217) 2026-11-04 14:50:48 +01:00
David Mann a7f34dfa9f Merge PR #11222: Actions: support recorded Type Create Work Path 2026-11-04 14:50:47 +01:00
David Mann 3ea385cef3 Merge PR #11017: Layers filter: fill layers match no Kind button, and the Mode list has no Pass Through (#10878) 2026-11-04 14:50:46 +01:00
David Mann 6a989bacdc Merge PR #11235: Layers panel: applying a filter deselects the layers it hides (#10879) 2026-11-04 14:50:45 +01:00
David Mann 8f9cc7bbf5 Merge PR #10396: Crop: Esc hides the crop frame until the next press (#10370) 2026-11-04 14:50:44 +01:00
FredApps 5617e00ca0 Merge pull request #11168 from hnoerenberg/fix/image-assets-save-errors-11117
fix(ui): a save that failed some Image Assets reports which outputs (#11117)
2026-11-04 14:35:55 +01:00
David Mann 2c68cc6b95 Merge pull request #11203 from davidmann4/integration-batch-15
Integration batch 15: Crop, Free Transform and Actions epic PRs
2026-11-04 14:18:54 +01:00
Josias Eli Rodriguez f1419f6d87 test(review): assert originals keep their group in the paired copy-drag test (#11217) 2026-11-04 07:16:30 -06:00
FredApps be3f632402 test: format save hook regression 2026-11-04 13:58:19 +01:00
FredApps 01ff0e2974 Merge pull request #8705 from the-unknown/feat/lens-flare-look
feat(filters): Lens Flare looks like Photoshop's (SSIM 0.97–0.995 per lens)
2026-11-04 13:56:53 +01:00
Codex 9fdcbf93c2 test: cover asset failure after save script shifts tabs 2026-11-04 13:48:59 +01:00
FredApps ec549530b8 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/ui-egui/src/app/files.rs
2026-11-04 13:44:14 +01:00
FredApps 66a0853024 Merge pull request #11244 from hnoerenberg/fix/assets-after-script-close-11121
fix(assets): bind post-save generation to the saved document's identity
2026-11-04 13:42:57 +01:00
FredApps 97ba6d16a9 Merge pull request #11189 from hnoerenberg/fix/windows-replace-recovery-11178
fix(atomic): report where the original survives when the Windows restore fails
2026-11-04 13:42:21 +01:00
Hartmut NörenbergandHartmut Nörenberg bf5c479d29 fix(layers): refuse to ungroup the sole empty layer group (#11118) (#11163)
* fix(layers): refuse to ungroup the sole empty layer group (#11118)

* fix(layers): document the ungroup guard and cover the valid empty-group case

Review follow-up: docstring on ungroup() and a control test showing an empty group that has sibling root layers still ungroups (the group simply disappears).

---------

Co-authored-by: Hartmut Nörenberg <hnoerenberg@users.noreply.github.com>
2026-11-04 13:40:06 +01:00
Josias Eli Rodriguez 91189d9244 fix(review): relink copies made by a panel copy-drag (#11217) 2026-11-04 06:30:07 -06:00