Commit Graph
2627 Commits
Author SHA1 Message Date
Dylan M. Taylor 5d4b68f04c Use craft-heif from craft-libs for HEIC/HEIF (#13076)
* Use craft-heif from craft-libs; remove crates/heif

PhotoCraft's HEIF decoder had drifted from LightCraft's by about 700 lines;
both now use craft-heif (storytold/craft-libs#784), built from LightCraft's
copy. HEIC colours now follow the HEVC VUI's range and matrix and libheif's
chroma upsampling. 8-bit files still open as 8-bit documents; a colour
description the VUI specifies now tags the image as an nclx did.

The codecs -> heif standalone exception ends with the local crate, and the
HEIF nextest step drops -p photocraft-heif, which no longer exists.

* Pin craft-heif to the merge of storytold/craft-libs#794

* heif: count the decoder's 16-bit buffer against max_alloc

* heif: match Apple's chroma upsampling; pin craft-heif to storytold/craft-libs#799

* heif: free the decoder's buffer before the HDR conversion and count that conversion's peak

* heif: hold the canvas the decoder really allocates to max_alloc
2026-11-11 01:59:37 -05:00
Tanish Manku e5cffe2375 Merge pull request #7239 from haranishi/review/packaged-cli-docs-20261018
Linux packages: ship the CLI in source Flatpaks and offline automation guides
2026-11-10 23:58:52 -07:00
Tanish Manku 254000acaa Merge pull request #7020 from fjamesprice/perf/camera-raw-gpu-texture-dehaze
perf(camera-raw): develop Dehaze, Clarity and Texture on the GPU in the preview (#6916)
2026-11-10 23:58:06 -07:00
Tanish Manku 1d6cd334ba Merge pull request #6951 from fjamesprice/perf/camera-raw-gpu-local-tone
perf(camera-raw): develop Shadows and Highlights on the GPU in the preview (#6916)
2026-11-10 23:57:54 -07:00
Tanish Manku dc855ae902 Merge pull request #6864 from hidoba/codex/curves-followup-fixes
Fix remaining grayscale Levels/Curves target mismatches
2026-11-10 23:57:32 -07:00
Tanish Manku c2e8c4db82 Merge pull request #6787 from ingling/spot-dot-gain
Color Settings › Spot: dot gain for spot channel and Multichannel ink previews
2026-11-10 23:57:09 -07:00
Michael Higgins 9d5be5ec8e Bridge: never send the control token to a listener that is not the app
## Summary

`photocraft-cli mcp --bridge` sent the control token in clear as its first frame, to whatever had accepted the connection: a `[::1]` listener reached through a `localhost` lookup, or any process holding `127.0.0.1:<port>` while the app was not listening. This PR fixes both cases:

- **Literal loopback only.** `BridgeClient` parses its address into a socket address and connects to it. Only `127.0.0.1:<port>` and `[::1]:<port>` are accepted. `localhost:<port>` means `127.0.0.1:<port>` (what the desktop app binds) and is never looked up.
- **Challenge authentication.** The bridge authenticates with `auth.challenge {clientNonce}` → `{serverNonce, serverProof}` → `auth {proof}`. Both proofs are HMAC-SHA256 keyed by the token over both nonces, each under its own label. The client checks the server's proof before it sends its own and never falls back to sending the token. The token never crosses the wire, so a listener that is not the app gets only a random nonce.
- **One shared server implementation.** `security::ServerHandshake` replaces `authentication_reply`, and both the desktop control server and `photocraft-cli serve` use it. The servers still accept the one-frame `auth {token}`, so existing scripts and older clients keep working. A repeat `auth.challenge` after authenticating gets "already authenticated", like a repeat `auth`.
- HMAC comes from RustCrypto's `hmac` 0.12 with `sha2` 0.10 (`sha2` was already in the lockfile). The proofs are compared without an early exit.
- `docs/control-protocol.md` documents the handshake, the exact HMAC message, and the bridge's address rules. The book's protocol summary is updated too.

Compatibility: a new bridge needs an app from the same release. Against an older app it fails with "refused challenge authentication … the control token was not sent". It does not downgrade.

Known limit: the channel is plain TCP with no TLS, so nothing binds the handshake to the connection. A party sitting between a bridge and a real app could relay the handshake without learning the token. On loopback that needs it to hold the port the bridge dials while the app listens on another, which the literal-address rule above narrows. Older clients that still send `auth {token}` keep their old exposure until they upgrade.

Closes #9777

## Tests

New in `crates/automation/tests/it/bridge_token.rs`:

- `localhost_reaches_the_ipv4_app_not_an_ipv6_listener`: a listener on `[::1]:P` gets no connection, and the app on `127.0.0.1:P` does.
- `a_listener_that_is_not_the_app_never_receives_the_token`: on both loopbacks, a listener that stays silent, refuses, or fakes the challenge receives only the challenge frame, never the token. The error says why.
- `the_bridge_authenticates_to_the_headless_server_by_challenge`: end to end against `rpc::serve_tcp`. A wrong token is refused before its proof is sent.

Unit tests: the handshake in both directions, and that wrong, missing or replayed proofs, the token after a challenge, a second challenge and malformed nonces are all refused (`security.rs`). `loopback_socket` address parsing (`bridge.rs`). The desktop server answering a challenge, then dispatching (`control_server.rs`). The existing bridge and MCP fakes now run the real `ServerHandshake`.

Run locally (Windows 11, targeted tests only):

- **Fail before.** I put upstream's `crates/automation/src/bridge.rs` (the old client) back and ran `cargo test -p photocraft-automation --test it bridge_token`: **3 failed**. The `[::1]` listener received `{"method":"auth","params":{"token":"<the token>"}}`, a `127.0.0.1` listener received the raw token, and the headless test got "authentication required" instead of the refusal. The original audit tests (`audit_nv6_bridge_sends_token_to_ipv6_impostor`, `audit_nv6_bridge_sends_token_to_any_ipv4_holder`) fail the same way on upstream/main.
- **Pass after.**
  - `cargo test -p photocraft-automation --lib --test it -- bridge security tcp`: 23 + 21 passed. This covers the new tests, the existing bridge, MCP-bridge, security and serve TCP tests, and the legacy-token tests that still pass unchanged.
  - `cargo test -p photocraft control_server`: 6 passed, including the new challenge test. The legacy-token tests still pass.
  - `cargo clippy -p photocraft-automation --all-targets -- -D warnings`: clean.
  - `cargo fmt`: clean.
- `Cargo.lock` adds only `hmac` 0.12.1, plus `subtle` under `digest` (already in the lock). Checked with `cargo metadata --locked`.

Not run here: the full gate (`cargo xtask ...`, the workspace-wide tests, wasm, clippy on the desktop app crate). That is left to CI.

API note: the workspace-internal `security::authentication_reply` is replaced by `ServerHandshake::reply`, which returns an `AuthStep`.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Security**
  * Control connections support challenge-response authentication, verifying possession of the configured token without sending it. Token-in-frame authentication remains available for scripts.
  * The MCP bridge accepts literal loopback addresses and maps `localhost` directly to IPv4 loopback. It rejects servers that refuse or cannot complete challenge authentication.
* **Bug Fixes**
  * Rejected authentication exchanges close the connection; successful authentication allows requests to proceed.
* **Documentation**
  * Control protocol and bridge authentication behavior are documented.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 07:35:17 +01:00
skylar taylor-barrick 1f77e59589 Merge origin/main (2772ac1e7) into craft-i18n cutover
Keep all 34 catalog deletions; sync crop options and fill rasterization text via craft-i18n 4a642c06dbcb8172f307f827c1167cd5dd8ac341. Adapt the fill prompt catalog test to embedded catalogs and preserve both parity-document updates.
2026-11-10 20:16:09 -06:00
Vlad Grankovsky 333566a616 Merge upstream main 609e2aa72 into curves follow-up 2026-11-10 19:51:17 +01:00
FredApps 96581039a8 Merge main into feat/ui-217-4-columns-unit 2026-11-10 18:41:49 +01:00
Khawar Zamman WaniandFredApps e1145b604e Make text-on-path placement editable (#11629)
* Add path text placement ranges and drag gestures

* Preview path placement and track bounded canvas damage

* Preserve legacy path anchors and show overflow markers

* Match path marker hit targets and directional cursors

* Fix path placement lint failures

* Cover path text body movement and position locks

* Keep open path text movable at range endpoints

* Target the type path after attaching text

* Fix path text range rendering and placement lifecycle

* Map vertical path text and preserve drag branch continuity

* Cover path placement lifecycle and preview damage regressions

* Preserve closest branch projection and fix CI regressions

* Normalize path endpoints and bind gestures to their viewport

* Simplify viewport ownership guards for Clippy

* Document path placement invariants and regression cases

* Fix path placement ownership, validation and geometry caching

* Correct path placement regression fixtures

* Ignore transient path geometry cache in UI state equality

* fix: allow path text flips with stale placement ranges

* fix: retain path text handles after shortening curves

* test: account for layer translation in path placement drags

* fix: mark path text range with x and o markers on the path

The beginning marker is an x and the end a small circle, both on the
path, with a plus in the circle while the range hides glyphs. The
cursor's triangle points right over the beginning, left over the end,
and both ways over the text body, which moves the whole range.
Direct Selection leaves an anchor or handle under a marker to path
editing; Path Selection always takes the marker.

* fix: resolve coincident path text markers by lap or drag direction

The x and o markers coincide on a closed path's full lap and on an
empty or end-clamped open range. A full lap now moves the whole text;
otherwise the first motion picks the beginning (backward) or the end
(forward). Marker fixtures press on the path, where the markers now
sit.

* fix: let either end of a full-lap path text range be dragged

Text attached to a closed path fills one lap, so its x and o markers
coincide and every press moved the whole text, leaving no way to
shorten the range. As the text begins just ahead of that point and
ends just behind it, a press ahead along the path now grabs the
beginning and one behind grabs the end; the point itself still moves
the whole text.

* test: check full-lap marker drags do not flip the text

* fix: read and write the path text range in PSD

Type on a path keeps its range in the Txt2 frame as /2 /6 [start end],
in curve-segment units with -1 for the default. Three reference-editor
files in ag-psd's test set fix the mapping: text-path ends a third into
the first segment and draws only "so" of "some text", and
text-path-26/27 move the start on an open path.

Import reads both ends (and the flipped flag) into the layer's start and
end. Export writes an open path whole with its start and end in the
range, instead of cutting the path at the start, and a closed path's
limited end as a range from its start. Path text in a document that
never came from a PSD now gets a Txt2 block, so its path is saved at
all, and the warning that the end limit is lost is gone.

* fix: let both selection tools grab a path text marker on an anchor

Direct Selection gave an anchor under a marker the click, so an open
path's default markers (on its end anchors) and a closed path's
coincident full-lap markers (on its first anchor) could not be moved
with that tool. In the reference editor either selection tool drags a
marker wherever the pointer shows the text bar with a triangle, so the
marker now wins for both tools.

The Direct Selection tests for reshaping a type path (#9114) drag its
last anchor, where the default end marker sits; they now end the
range short of it. To reshape at an anchor under a marker, drag the
marker off it or select the anchor with a marquee and nudge it.

* fix: keep explicit and empty path text ranges through PSD

An explicit end at the path's end stays explicit, so centered and right type keeps its alignment within the span. A closed range whose ends meet reads as empty, not as a full lap.

* ci: re-run checks after a download outage

* ci: re-run checks after a download outage

* fix: preserve legacy path text anchors through PSD

* ci: re-run checks against current main

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 16:28:51 +01:00
terminalgateandClaude Sonnet 5.5 fb3b6a4b70 Eraser: add Erase to History (Brush, Pencil and Block modes) (#10692)
* Eraser: add Erase to History (Brush, Pencil and Block modes)

Photoshop's Eraser has an Erase to History option; PhotoCraft had only the History Brush.
With it ticked, the Eraser paints the History panel's source state back instead of
erasing, through paint.historyBrush with eraseToHistory. Pencil and Block modes paint back
through their hard tips. A layer that did not exist in the source state paints that
state's visible image; the History Brush itself still refuses such a layer, as Photoshop's
does.

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

* Keep tool_event within clippy's complexity limit

* Test Erase to History at every depth and tip

* Update the History Brush params comment for Erase to History

* Erase to History paints a layer made since only from a single-layer state

Compared with Photoshop 27.11.0: on a layer the source state does not hold, Erase to History paints
when that state holds a single layer, Background or not, and gives an error when it holds a second
layer, even an empty one. The composite of a multi-layer state is gone; those states now refuse
the layer with the History Brush's error.

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

* Erase to History paints a single source layer's own pixels

Photoshop 27.11.0 paints a layer made since with the source layer's pixels: a 50% opacity layer
gives a solid stroke, where the composite gave a faded one.

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

* Erase to History: Pencil mode paints at full flow; test a group-only source state

Pencil mode has no Flow control, so a lower Flow set in Brush mode must not carry over. A source
state holding only a group has no pixels of its own and refuses a layer made since.

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

* Erase to History keeps its options if a tool key is pressed mid-stroke

The stroke read the current tool and brush when it was committed, so pressing a tool's letter
during the drag dropped eraseToHistory and the stroke failed on a layer made since. The options
and the Eraser's tip are now captured at the press.

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

* Erase to History: a single solid shape paints its colour; name the History row

Photoshop 27.11.0 paints a shape layer's solid colour along the whole stroke, past its path, and
calls the stroke History Eraser in the History panel.

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

* Erase to History: a single shape without a solid fill paints white

Observed in Photoshop 27.11.0: a gradient, pattern or no fill paints plain white along the whole stroke, past the path and whatever the outline, in RGB, CMYK, Grayscale and Lab.

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

* Eraser: Alt at the press reverses Erase to History for that stroke

As in Photoshop 27.11.0: with the option off Alt restores, with it on Alt erases. Alt pressed or released during the stroke changes nothing, and the option itself is left as it was.

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

* Erase to History: name the Photoshop version compared

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

* Erase to History: review fixes for the error and the History Brush tip

A layer without pixels now gets the error that the target did not exist
in that history state, not "no active document". The History Brush keeps
its size, hardness and spacing from the press, as the Eraser does, if a
tool key is pressed mid-stroke.

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

* Brush parity: add the Eraser's Erase to History

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

* Erase to History: document the functions this change touches

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

* History Brush and Erase to History keep their whole brush from the press

Only mode, opacity, flow, size, hardness and spacing were kept from the press; the rest of the brush (tip shape, sampled tip, angle, dynamics, smoothing) came from the session brush at release. A tool key mid-stroke changed that brush, the commit rejected the live preview, and the fallback repainted the whole stroke with the new tool's brush. The stroke's params now carry the press-time brush as "brush" (stored tips as references, as each tool's remembered brush), and paint.historyBrush deep-merges it onto the session brush, so the live preview, the commit, its fallback and a replayed action all paint with it. Tests: a tool switch mid-stroke paints exactly what the same stroke paints without it, for the History Brush and Erase to History in Brush, Pencil and Block mode, across tip shape, sampled tips, dynamics and smoothing; a "brush" param paints as the session brush would; a malformed one is refused.

* History Brush docs: say "outside its shape", not "past its path"

The automation crate flags any command whose params text mentions a path as a possible filesystem path (registry_filesystem_path_params_are_classified); the eraseToHistory description meant a shape's vector outline. Also a test that a state whose one root item is a group of several layers is refused like the one-layer group: a group has no pixels to paint back.

* Test that a Shift-click Erase to History line commits its live pixels unsmoothed

Main's Shift-click fix (#11887) and this branch's press-time History
Brush params meet in finish_stroke_with; check the commit sends
smoothing 0 beside the brush param in all three Eraser modes.

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-10 14:51:20 +01:00
ec97a146f5 Support ordered automation read roots with safe save provenance (#3023)
* fix(cli): reject repeated automation read roots

* feat(automation): add ordered read roots with isolated saves

Stage source and regression tests for GGEO-AI02 validation before updating PR #3023. Repeated reads accumulate; writes remain single-root and implicit saves retain their selected capability provenance.

* style: format multi-root automation extension

* test: use fixed pixel chunks in read-root regression

* docs: reconcile control protocol with upstream TGA options

* docs: state multi-root capabilities directly in API summaries

* test: construct regression actions through upstream defaults

* Clarify headless and desktop directory automation reads

* Format the shared root parser and relocated service regressions

* test: adapt automation import callback to ImportOptions

* test: expect read-root diagnostic for denied smart-object input

* test: reject repeated write roots while allowing ordered read roots

* test: assert explicit empty-root parser diagnostic

* test: exercise leaf-link save using a supported image filename

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-11-10 14:28:12 +01:00
David Mann 2a950d64dd Merge pull request #7377 from smyrgeorge/fuji-compressed-raf
Fujifilm compressed RAF: decode lossless and lossy files instead of the embedded preview
2026-11-10 13:36:29 +01:00
David Mann c64708b836 Merge pull request #11836 from yoriswind/codex/recorded-displace-10705
Map recorded Windows Displace actions
2026-11-09 19:27:57 +01:00
David Mann b059c1a45a Merge pull request #12438 from ljkx/atn-measurement-scale
Actions: map Image › Analysis › Set Measurement Scale › Custom (#10723)
2026-11-09 17:53:15 +01:00
2d6f9d172b Canvas: the redraw area covers a whole command, not just its last revision (#9329) (#10053)
* 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>
2026-11-09 17:30:11 +01:00
David Mann 80b6f3d9b3 Merge remote-tracking branch 'origin/main' into pr-6215
# Conflicts:
#	docs/gaps.md
#	docs/ui-parity.md
2026-11-09 17:06:19 +01:00
David Mann 68547285b5 Merge main into #7377 (Fujifilm compressed RAF) 2026-11-09 17:06:04 +01:00
David Mann d2330fccea Merge pull request #12402 from TrevorBrown-dev/fix/actions-drag-trash-new-12373
Actions panel: a row dragged onto the trash is deleted, onto Create new action duplicated
2026-11-09 16:28:21 +01:00
Martin BallandClaude Opus 5.5 4a4dad43c2 Merge origin/main (7c64653d5, #11193 toolbar side) into craft-i18n-cutover
Keeps the TSV deletions; pins craft-i18n 87f45ab (storytold/craft-i18n#216
head, now also carrying #11193's Move Toolbar rows) until #216 merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 14:55:19 +00:00
TriwarrandFredApps 7c64653d5c Add toolbar side toggle (#11193)
* Add a toolbar side toggle and dock-edge resizing

- Let the toolbox sit on the canvas' left or right, persisted in the panel
  state and switchable from the toolbar header and preferences.
- In the compact arrangement, stack dock, rail and toolbox on the dock's
  physical edge so the toolbox cannot cover the rail.
- Make the dock panel's inner edge the single resize handle: it drives the
  panel width and the dock total (split size and column widths) in the same
  frame, so the rail and dock content stay bound with no reserved room and
  no second handle. The docking layout's own column-edge handle is covered;
  the bar is the theme separator at rest and the accent on hover/drag.
- Persist the layout, keep the split size in sync, handle RTL, and test the
  toolbar side state.

* Toolbar side: dock-edge only with a lone canvas; test every theme

- Use the between-dock-and-canvas layout only when nothing is docked beside the canvas: that view drops the canvas from craft-ui's split, so with a panel there the standard layout keeps it visible and dockable.
- Add canvas_alone_layout/canvas_alone and a unit test for a lone canvas versus a split canvas side.
- Add a kittest that toggles the toolbar Left/Right across every ThemeKind::ALL entry, asserting the panel lands on the requested physical side.
- No dependency changes: Cargo.toml and Cargo.lock are identical to main, so the repository's exact pins are untouched.

* Toolbar side: keep the canvas a drop target during dock drags

While a panel is being dragged, draw the standard layout even with the toolbar at the dock edge. It keeps the canvas in craft-ui's split, so a panel can still be dropped beside it; after the drop the canvas is no longer alone and the standard layout stays. The between-dock-and-canvas layout resumes for a lone canvas once the drag ends.

* Toolbar side: stable drag layout, unwind-safe dock flag, clippy

- Restore the dock-only thread-local through a drop guard, so an unwinding show does not leave a later standard layout canvas-less.
- Do not switch the dock layout mid-drag: craft-ui records its areas on the press, and changing the shape cancels the drag. The layout stays stable; a panel beside the canvas still selects the standard layout before the drag begins.
- Fix the CI clippy errors: explicit auto-deref in end_dock_node/resize_end_dock and field reassignment in the toolbar_side test.

* Toolbar side: anchor the Edit Toolbar flyout; borrow the canvas layout

- more_slot_flyout takes the resolved ToolbarSide instead of the text direction, so a right-hand toolbar in LTR (and a left-hand toolbar in RTL) anchors the flyout away from the canvas like the tool flyouts.
- canvas_alone borrows the saved layout and treats the native-dock legacy layout as canvas-only, avoiding a per-frame clone and traversal.

* Toolbar side: only a horizontal canvas split uses the dock-edge layout

A saved workspace can put the canvas above another panel in a vertical root split; canvas_alone_layout now requires SplitAxis::Horizontal so that shape keeps the standard layout. The guard test covers the vertical case.

* Toolbar side: document the functions this PR adds

Covers ToolbarSide::resolve/other, canvas_alone_layout, the DockOnlyGuard drop, and the two new tests. Docstring coverage of the PR's own additions is now complete; no behaviour change.

* docs: refresh the scorecard after merging main

The merge left docs/scorecard.md stale against the combined settings table; regenerated with cargo xtask scorecard, which is what the gate's scorecard step checks.

* Toolbar side: honor the workspace lock, empty docks and the end-dock shape

- The dock-edge resize handle is inert while the workspace is locked (hover sense, no highlight) and set_end_dock_total refuses to write then; the drag test covers a locked workspace.
- An empty dock takes only the rail's width instead of a clamped blank column; dock_is_empty reports it and a test empties the dock.
- canvas_alone_layout now requires the second child to be the Dock itself, and end_dock_node/resize_end_dock follow horizontal splits only, so a layout the handle cannot resize keeps the standard layout instead of snapping back.
- Tests: DockOnlyGuard restores the flag on drop and unwinding; the theme test clicks the toolbar's move button instead of setting state; a dock-edge harness drags the edge, checks it persists and that a locked workspace refuses it; axis tests and a two-column ui.set {dockWidth} test.
- The flyout indicator follows the resolved toolbar side, the rail keeps its chrome fill and 4/8 inset with the separator inside it, ATTRIBUTION counts the new icon (116), and docs/control-protocol.md describes dockWidth as the whole end dock.

* Toolbar side: floating windows on empty docks, iconic widths, dockWidth columns

- An empty dock still runs the docking pass on the full remaining area, so Studio's floating Properties window keeps rendering with or without the rail; the dock column itself takes no width. A Studio regression builds an empty dock with a floating Properties group and asserts the pass runs and the window is drawn.
- Dock width now uses each column's displayed width (iconic_width while collapsed to icons), and resizing/scaling keeps iconic widths in step. A collapse-to-icons regression checks a 36-point column not reserving 290.
- ui.set {dockWidth} scales every column to the requested total directly instead of resizing only the first column and then finding the sum already equal. The two-column test also covers a target smaller than the other column's expanded width (250 from [300, 180]) and asserts per-column widths.

* Toolbar side: iconic widths, edge release and modal handling, side persistence

- An iconic-only dock keeps its narrow displayed width instead of being raised to the expanded
  minimum, and the dock-edge panel records its laid-out width for a geometry regression.
- The release frame applies its final pointer position before dropping the drag, so the dock keeps
  the width the user released at.
- The dock-edge handle is inert (and any stored drag is dropped) while a modal dialog is open.
- set_display_width changes iconic_width while iconic and leaves the columns expanded width for
  expanding again; the regression resizes an iconic column and checks the restore.
- Clicking the toolbar side persists the panels snapshot immediately through persist_panels, even
  when the dock is not drawn, while Remember Workspace Changes is on.
- The locked-drag test compares against the pre-drag width instead of only settled equality.

* Toolbar side: defer persistence while moving, translate the side labels

- persist_panels defers while the dock persistence pass sees a native window move/resize (the
  same native_moved signal), so a click during an OS resize cannot save intermediate geometry;
  it requests the usual 100 ms repaint and leaves the dock pass to save when the geometry is
  stable.
- The toolbar-side tooltip/accessible labels now pass both literals directly to tl!, and all 34
  registered catalogs carry translations for Move Toolbar to the Right/Left.

* Toolbar side: keep in-progress dock geometry out of toolbar saves

- native_moved caches its answer for the frame, so the dock pass and persist_panels see the same
  movement result even though only the first call observes the change.
- persist_panels defers while a dock-edge drag is stored, a pointer is down or a native window
  moved; it stores a pending flag and requests the usual 100 ms repaint.
- persist_pending runs in the frame tick (before preferences save) and retries the deferred save
  once geometry is stable, so the toolbar side is not lost even when the dock was not drawn.
- The dock persistence pass also skips while the dock-edge drag start is stored, covering a
  synthetic/accessibility click with no pointer button down.

* Toolbar side: keep a same-width dockWidth request a no-op

The dockWidth rewrite scaled and committed the legacy layout even when the requested width
already matched, so a later `ui.set {dockTabs}` took the customized-workspace path and expanded
the Properties group. Restore the early return for an unchanged displayed width and cover it:
setting the current dock width keeps `app.ui.docking` None.

Fixes the `a_new_adjustment_layer_brings_properties_to_the_front` failure on CI.

* Toolbar side: recheck native bounds, expire stale dock-edge drags

- native_moved keeps movement already seen in the frame but compares the bounds again on every
  call, so a later dock save is not fooled by the early persist_pending check caching false
  before the docking UI updates a floating window.
- dock_panel records the frame it rendered; persist_pending drops a stored dock-edge drag start
  once the panel stopped rendering (Shift+Tab hides the dock mid-drag), so deferred saves resume.
- The same-width dockWidth test also selects a tab in a collapsed group and asserts the tab,
  the collapsed state and that no workspace was customized.

* Toolbar side: draw the release width, clear drags when the dock empties

- The release frame draws the panel at the final pointer width just stored, instead of the width
  read before the drag ended, so the panel and toolbar match the saved dock width.
- The empty-dock path clears a stored dock-edge drag start, so a dock that empties mid-drag cannot
  leave deferred saves blocked.
- Tests: nested DockOnlyGuard (flag found set stays set), and a queued `ui.set {dockWidth}`
  through the rendered dock pass checking a two-column dock totals 420 points.

* Toolbar side: cancel the dock-edge drag when resizing is unavailable

- A disabled parent UI (screen picking) never delivers drag_stopped, so dock_panel now clears a
  stored dock-edge drag whenever resizing is unavailable: locked, modal, empty dock or disabled
  UI. The same condition makes the handle hover-only, so it cannot start a new drag while
  disabled.
- Regression: start a resize, disable the UI, release, re-enable and move the pointer; the drag
  is cleared, the dock does not jump and persistence can resume.

* Toolbar side: bound the dock-edge layout and enforce column minima

- The dock-edge panel leaves the toolbar and the 200-point canvas minimum room, so a narrow
  window shrinks the dock instead of the canvas.
- The empty dock runs the docking pass in a child ui that does not consume the parent layout, so
  the toolbar and canvas keep their space.
- Column scaling keeps the 220-point panel minimum: minima are allocated first, the rest is shared
  proportionally, and requests below the sum of minima stop there.
- The dock metrics (empty, iconic-only, width) come from one layout snapshot per frame.
- Removed dead empty-dock branches, narrowed the content-edge interceptor to one point either
  side, corrected the toolbar target comment, used the drag press origin, and documented the
  Side::End assumption.
- Tests: narrow-window canvas minimum, empty dock composed with toolbar and canvas, minimum-aware
  two-column widths, and the empty compose regression.

* Toolbar side: bound the dock-edge layout, enforce panel minima, match iconic widths

- The dock-edge panel leaves the toolbar and the 200-point canvas minimum room, so a narrow
  window shrinks the dock instead of the canvas.
- The empty dock runs the docking pass in a child ui that does not consume the parent layout, so
  the toolbar and canvas keep their space.
- Column scaling allocates each column's 220-point panel minimum first and shares the rest
  proportionally; requests below the sum of minima stop there.
- Iconic columns use craft-ui's renderer rule: iconic_width.unwrap_or(36).clamp(36, 240), including
  the real collapse case that leaves iconic_width unset.
- Dock metrics come from one layout snapshot per frame; dead empty-dock branches are gone; the
  content-edge interceptor is one point either side; the toolbar target comment is corrected and
  the Side::End assumption documented.
- Tests: narrow-window canvas minimum, empty dock composed with toolbar/canvas, minimum-aware
  two-column widths, collapse via Action::SetIconic leaving iconic_width unset, and the 36..240
  clamp. The press-origin change was reverted: Sense::drag starts immediately.

* Toolbar side: distribute refused width, draw actual widths, invalidate stale drags

- scale_columns moves the total toward the request in passes, freezing columns that hit a bound
  (iconic 36..240, expanded minimum 220), so width refused by the iconic cap reaches columns that
  can still grow.
- set_end_dock_total returns the width actually stored; the dock panel uses that width during the
  drag, on release and after, instead of forcing the requested or minimum width.
- The custom drag stores docking_generation; a workspace replacement clears the stale gesture
  before any width mutation.
- Clamp bounds are normalized against the available maximum (including iconic columns) and
  resizing is disabled when the window cannot fit the column minima, so narrow layouts cannot
  panic.
- Tests: iconic-cap surplus redistribution, actual-vs-requested panel geometry during and after a
  drag, workspace replacement during a drag, and a 300pt window resize without panic.

* Fix dock-edge resize bounds and cancellation

* Preserve explicit dock width across legacy migration

* Document dock code, dedupe widths, harden toolbar_side load

- Add doc comments to the functions and tests this PR touches (Docstring Coverage).
- Share the toolbar width (toolbar_width/toolbar_columns), the rail widths and
  CANVAS_MIN_WIDTH instead of repeating the values; no values change.
- Document dockWidth as the requested model width, which a narrow window may draw
  narrower in either layout, and test model 520 / drawn <= 364 / canvas >= 200.
- Document that a dockWidth change is shared equally among columns within their
  minimums and iconic caps; tie ICONIC_WIDTH/ICONIC_MAX_WIDTH to craft-ui's
  DockStyle defaults (36/240) in a test.
- Replace the DOCK_ONLY thread-local and its guard with show_layout(dock_only).
- Move the test-only probe writes behind panel_docking::record, with no-op stubs
  outside tests.
- Read an unknown or malformed saved toolbar_side as Start instead of dropping
  the whole panel state; ui.set still refuses it.

* Test visible dock chrome in narrow windows; name panel body minimum

- The narrow-window dockWidth test now checks the docking controls craft-ui drew:
  hit_rects are full widget rects (before clipping), so the visible part of each,
  intersected with the dock, must stay inside the dock and off the canvas and
  the toolbox, with at least one control visible.
- Name the 200-point guard for drawing a panel's body PANEL_BODY_MIN_WIDTH,
  separate from CANVAS_MIN_WIDTH; no value change.

* test(ui-egui): assert dock controls are clipped at the panel edge

The narrow-window test intersected each hit rect with the dock and then asserted the
intersection lies inside the dock, which always holds. Check each dock control's clipped
interact_rect against the dock instead, and add a squeezed-column test where a control's
full rect runs past the panel edge, so the clip is actually exercised.

* ci: retrigger pull_request workflows after resolving the merge conflict

No changes. GitHub did not queue Actions for 3652ede, the push that
resolved the conflict with main.

* test(ui-egui): assert saves resume after a disabled UI cancels a dock-edge drag

The test's doc says saves must resume once a disabled UI drops the held drag, but nothing
checked it. Assert that the held drag is not saved, that its width is saved as soon as the
drag is cancelled (while the UI is still disabled), and that a width change made after
re-enabling reaches the saved layout too.

* test(ui-egui): cancel a dock-edge drag on a window shrink and check the dockWidth column split

constrained_width_does_not_panic_while_resizing now starts the drag in a wide window and
shrinks the window below the column minima while it is held: the drag must be cancelled and
the dock must stay within the window. Both ui_set_dock_width tests now check how the change is
split between the columns, not only the total.

* test(ui-egui): tighten dock tests per CodeRabbit review 5824274070

- disabling_the_ui_cancels_a_dock_edge_drag: assert the drag is cancelled on the first disabled
  frame, before the pointer is released.
- all_iconic_totals_stop_at_both_bounds_without_losing_expanded_widths: check the stored column
  widths sum to the expected total after each request.
- explicit_width_request_preserves_queue_and_native_state_until_invalid_workspace_recovery: add
  a changed-width (520) case next to the no-op one.
- the_toolbar_side_toggle_works_in_every_interface_theme: with the toolbar on the right, a grouped
  tool's flyout opens to the left of its button.

* test(ui-egui): assert stored widths and native rect per CodeRabbit review 5824824938

- constrained_width_does_not_panic_while_resizing: the stored column widths captured before the
  window shrinks are unchanged after the cancel and after the release.
- explicit_width_request_preserves_queue_and_native_state_until_invalid_workspace_recovery: the
  native state keeps the exact rectangle the test saved until recovery, not just some state.

* Keep the toolbar side when a workspace saved none

A workspace saved before the toolbar could move has no panels.toolbar_side,
so applying it reset a right-hand toolbar to Start. Keep the current side
when the saved value is missing or unreadable; a valid saved side still wins.

* Cover a saved start side and the other panel flags on workspace apply

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:51:29 +01:00
Brennan StehlingandClaude Opus 5.5 8ba961c11a Engine: bounded redraw areas for adjustment, reorder and clipping changes (#9329) (#10052)
* 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>
2026-11-09 15:50:42 +01:00
6121e884ba Merge origin/main into PR #12402 (docs/ui-parity.md change log: keep both rows)
Co-Authored-By: Trevor Brown <trevorbrown.dev@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 15:49:09 +01:00
Brennan StehlingandClaude Opus 5.5 aac3312bfb Perf: one frame stage record across egui's passes; the web keeps slow frames too (#9328) (#10103)
* Perf: the web keeps slow frames too; its clock is performance.now() (#9328)

now_ms() reads web_time::Instant, which is performance.now() on wasm, so
the web build records slow frames, reports perf.slowFrames and logs them
to the browser console. The stroke trace is off on the web for another
reason: it has no environment, stderr or files to write to.

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

* Perf: one frame stage record across egui's passes (#9328)

egui runs a frame's UI again when a pass asks for a discard (a sizing
pass). The second pass opened a new stage record after the first pass
closed its own, so a frame of two 30 ms passes was never slow and the
stroke trace gained a 0-move row per extra pass.

finish_frame now adds each pass's UI time to the record and leaves it
open while ctx.will_discard() says egui will run the pass again; the
last pass closes it. ui_start keeps the first pass's input time.

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

* Perf: a frame's stroke flag covers every pass

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-09 15:49:05 +01:00
David Mann e06ba09af4 Merge remote-tracking branch 'origin/main' into pr-6215
# Conflicts:
#	docs/gaps.md
#	docs/ui-parity.md
2026-11-09 14:38:19 +01:00
719ce3cfda Merge main into #9798 (Cylinder warp editor)
Conflict only in docs/gaps.md's status line: both entries kept.

Co-Authored-By: TimothyBritt <TimothyBritt@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 14:36:07 +01:00
Martin BallandClaude Opus 5.5 3da70d1abe Merge origin/main (1372bae3e) into craft-i18n-cutover
docs/target-app-parity.md: keeps both header changes (i18n-report line and
#9077's Average notes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:27:50 +00:00
David Mann 3369f427ba Merge main into #7377 (Fujifilm compressed RAF)
Only the docs status lines conflicted: main's text, with the compressed RAF entry kept first.
2026-11-09 14:26:32 +01:00
David Mann 1372bae3ea Merge pull request #9077 from ArjenS-LA/fix/average-smart-filters
Execute Average on Smart Objects and imported PSD smart filters
2026-11-09 14:23:56 +01:00
David Mann fbcb6798e0 Merge pull request #9021 from devstivi/apply-to-all-unsaved-prompt
Offer Apply to All in the unsaved-changes prompt
2026-11-09 14:23:44 +01:00
61aa24991a PR #11836: map a recorded Displace path only when a document may name it
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>
2026-11-09 14:04:12 +01:00
David Mann 24cd4a4098 Merge remote-tracking branch 'origin/main' into pr-11836 2026-11-09 13:52:38 +01:00
FredApps 885a6ddf27 Merge pull request #9864 from Higgs32584/fix/8806-rasterize-click-replay
fix(ui-egui): the rasterize prompt no longer replays the click that opened it (#8806)
2026-11-09 11:55:33 +01:00
fjamespriceandClaude Opus 5.5 b02d952e76 perf(camera-raw): the GPU Dehaze opens the dark channel like the CPU (#9273)
main's #9316 opens Dehaze's dark channel (a maximum filter of the same
radius after the minimum) and scales the 0.95 weight by the ratio of the
two means, so a sky beside a dark object clears instead of keeping a
bright halo. The GPU pass built the transmission from the minimum alone,
so its preview kept the halo that OK no longer applied.

The pass now does the same: fs_min gains a maximum mode for the opening,
fs_air sums the dark channel and its opening per tile and then turns the
sums into the weight, and the packing pass reads the weight from that
texel instead of a constant.

The CPU mean is now summed in f64. Its f32 running sum over the copy's
few hundred thousand cells drifted by about 1e-5, which the low
transmission beside haze multiplied past the GPU tests' 1/4096 (2.7e-4 in
clarity_texture_and_dehaze_with_shadows_highlights_and_per_pixel_stages);
with f64 the GPU matches to 1.6e-5 on the halo scene.

New test dehaze_opens_the_dark_channel_like_the_cpu develops #9316's
scene (a clear sky and a dark tower) both ways at three strengths. Dehaze
+40 on the 858x572 proxy: 5.1-7.7 ms on the GPU vs 17.7 ms on the CPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 02:42:52 -05:00
fjamespriceandClaude Opus 5.5 6025c504cf perf(camera-raw): develop Dehaze, Clarity and Texture on the GPU in the preview (#6916)
Step 3 of #6916. Dehaze, Clarity and Texture run as fragment passes
(box, minimum and guided filters) around step 2's Shadows and
Highlights, so every setting but Detail develops on the GPU with one
readback.

- algo: camera_raw::spatial resolves Dehaze, Clarity and Texture per
  frame; develop builds them the same way (130 output hashes unchanged,
  same speed). PerPixel gains the three stages, per_pixel() refuses only
  Detail, and Whites/Blacks are measured after any spatial stage.
- gpu: camera_raw_filters.wgsl; the tone frame's pyramid is made on
  demand; plans record four segments around the main shader's passes.
  Dehaze's light uses two exact selections (cutoff value, then cutoff
  index among its ties). Its <=768 px copy comes from the CPU
  (Dehaze::cells, develop's own code), cached per source and white
  balance: on WARP the GPU's copy split CPU ties at the cutoff and moved
  the light by 5.7e-3.
- GPU vs CPU at most 7.6e-5 (Vulkan, DX12, WARP), tolerance 1/4096.
  858x572: Dehaze +40 6.0 ms vs 29 ms, Clarity +50 3.5 vs 16 ms,
  Texture +50 3.4 vs 10 ms, everything 19 ms vs 650-690 ms (Vulkan).

Part of #6916.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 02:42:52 -05:00
fjamespriceandClaude Opus 5.5 f41d0ad6d9 docs(hardware-parity): leave the status line to main
The status line changes with every hardware change on main, and this
stack's edit to it conflicted three times in three days. The GPU filters
row and the revision row still describe this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nuVUeq1gmQSpF78xazkX9
2026-11-09 02:41:55 -05:00
fjamespriceandClaude Opus 5.5 a60090fff5 perf(camera-raw): develop Shadows and Highlights on the GPU in the preview (#6916)
Step 2 of #6916: the local Laplacian filter behind Shadows and Highlights
(Paris, Hasinoff, Kautz 2011, sampled as in Aubry et al. 2014, as
local_tone.rs does) as wgpu fragment passes, chained in front of step 1's
per-pixel pass with no readback in between.

- algo: local_tone exposes its constants and resolves the sliders into
  LocalTone / TonePass (the remapping is a method); the CPU filter runs
  those passes. PerPixel gains local_tone and measure_whites_blacks;
  whites_blacks_model() exposes the Whites and Blacks tables;
  quantile_rank() is the one rank rule. CPU output unchanged bit for bit
  (86 hashes, 8-bit and float, 171x113 to 2100x1300) and as fast.
- gpu: camera_raw_tone.rs/.wgsl. Pyramid levels are R32Float textures; the
  remapped pyramids of up to 96 references sit in one Rgba32Float atlas per
  level, four per tile. The filter's quantiles, and Whites and Blacks'
  points after it, are exact order statistics on the GPU (16 thresholds per
  round, 8 rounds); a pass renders the Whites and Blacks rows into the
  table texture. Textures, passes and bind groups are planned once per
  frame size. Pipelines build with the rest on the existing thread.
- ui-egui: the proxy worker sends Shadows and Highlights frames to the GPU;
  Dehaze, Texture, Clarity and Detail still use the CPU for the frame.

GPU vs CPU at 8, 16 and 32 bits: at most 9.9e-6 (RTX 5060 Ti, Vulkan and
DX12), 1.03e-5 (WARP); tests keep 1/4096. 858x572 proxy, release: Shadows
+50 8.7 ms (Vulkan) / 10.6 ms (DX12) vs 147-300 ms on the CPU.

Part of #6916.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 02:41:55 -05:00
Martin BallandClaude Opus 5.5 8d8fb69d6d Merge origin/main (d06ad7327, #12403 RAW Develop) into craft-i18n-cutover
Keeps the TSV deletions. The Hindi and RAW Develop catalog tests read the
embedded craft-i18n catalogs behind needs_catalogs(); display_names scans
CRAFT_I18N_DIR's locales/*/photocraft.tsv instead of the deleted src/i18n.
Pins craft-i18n 31d158f (storytold/craft-i18n#216 head, the #12403 top-up)
until #216 merges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 07:38:51 +00:00
FredApps d06ad73271 Naming: use RAW Develop for PhotoCraft's raw-processing feature (#12403)
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.
2026-11-09 08:30:33 +01:00
ljkx c2039dd0e8 Actions: map Image › Analysis › Set Measurement Scale › Custom (#10723)
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.
2026-11-09 05:43:34 +01:00
Tim BrittandClaude Fable 5.1 4f2be5f14d Address CodeRabbit review: keep a move shape-true, never clamp a stored depth, adopt a stored warp only with its preview (#9798)
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>
2026-11-09 04:36:43 +00:00
Tim BrittandClaude Fable 5.1 5872941b12 Warp mode: the cylinder's grid and handles as the reference's, and a stored warp previews unwarped (#9798)
The cylinder is outlined and cut into a 3 × 2 grid (two horizontal curves and
the centre line) that hugs the warped picture. The handle in the middle of the
upper arc sets the bend of both arcs, the one in the middle of the lower arc the
lower edge's height, and the wrap handle sits at the grid's centre and slides to
the right (starting 15 % along), stretching the middle of the picture and
squeezing its sides round the back.

A smart object's stored warp used to enter Warp mode with the layer's already
warped render as the preview texels, which the mesh then warped again, so the
picture never sat on the grid: the session now previews the source placed
without the warp over the warp box. Edit › Transform › Warp carries a check
while the active layer is warped, as the reference's does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 04:36:43 +00:00
Tim BrittandClaude Fable 5.1 5e7bccc4d8 Address review: match the reference's Cylinder editor (#9798)
As a maintainer compared it with the reference application: Bend, H and V stay
in the bar, greyed (no Top/Bottom/Wrap fields); the starting cylinder has its
four corners on the box and both curves 50 % deep; five handles (the top-right
and bottom-left corners, the front of each curve, a wrap handle near the middle
dragged sideways); a drag on the surface moves the whole cylinder; the overlay
is the two curves and the centre line, not a grid.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 04:36:28 +00:00
Tim BrittandClaude Fable 5.1 4243427390 Warp mode: edit Photoshop's Cylinder warp with handles and fields (#4112)
The Warp menu lists Cylinder after the presets (not on type). Picking it starts
a cylinder fitted to the box; handles set its seven values: the corners move a
silhouette and an edge, the middle of a silhouette moves it alone, the middle of
the top and bottom edge sets that ellipse's depth, and a drag across the surface
turns the contents round the cylinder. The options bar shows Top, Bottom and
Wrap in place of Bend, H and V, with Split, Grid and the orientation switch
greyed. The warp stays a cylinder through the commit, so a smart object keeps it
as `warpCylinder`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-09 04:36:28 +00:00
David Mann f88fa5e431 Merge remote-tracking branch 'origin/main' into pr-9272
# Conflicts:
#	docs/ui-parity.md
2026-11-09 00:49:57 +01:00
David Mann a76bb72b1f Merge pull request #10569 from hectorviov/feat/layer-copy-css
Layer › Copy CSS
2026-11-09 00:17:21 +01:00
David Mann 6a3b1a7dfa Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	docs/ui-parity.md
2026-11-08 23:56:07 +01:00
David MannandClaude Opus 5.5 4ebd2da168 Regenerate scorecard after merging main; preference counts of 199
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:55:07 +01:00
Martin BallandClaude Opus 5.5 60b815aa6e Merge origin/main into craft-i18n-cutover
Keeps the deletion of the in-repo catalogs. #9144 added Scroll All
Windows translations to 26 of them; craft-i18n has not synced them yet,
so the label shows in English until the next re-sync and re-pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:46:04 +00:00