Commit Graph
8763 Commits
Author SHA1 Message Date
yoriswind 211efca709 Keep paused action policy and spelling receivers bound 2026-11-03 19:39:45 -08:00
yoriswind 6d247886a7 Test ATN layer style playback in both history modes 2026-11-03 19:22:21 -08:00
yoriswind 15cfdc8398 Preserve paused action authorization and history boundaries 2026-11-03 17:54:45 -08:00
yoriswind 1bcf7a3c6a Merge batch 14 main before action-history review fixes 2026-11-03 17:27:25 -08:00
David Mann 205db62a29 Merge pull request #11157 from davidmann4/integration-batch-14
Integration batch 14: 9 PRs planned
2026-11-04 01:42:35 +01:00
Dylan M. Taylor 3fd5304dfe text: find paragraphs and trailing blanks without rescanning (#11045)
Layout looked up each paragraph's style with a reverse scan of every
paragraph start, tab placement re-checked the whole rest of the line for
every cluster, and PSD import found each character run's paragraph by
summing paragraph lengths from the first one. Each was quadratic in
counts a file controls.

Use the existing binary search over run starts for paragraphs, one
reverse scan for the line's last inked slot, and precomputed paragraph
ends with a binary search on import.
2026-11-03 19:35:50 -05:00
David Mann dff83780d0 Merge main into integration-batch-14 2026-11-04 00:59:38 +01:00
yoriswind 384e9dd28c Merge commit '81bd97376f881ee3561371b7da52780844d3dfc7' into codex/10965-batch14-main 2026-11-03 15:58:39 -08:00
yoriswind fdc667e010 Report rejected action continuations and exercise a real artboard edit 2026-11-03 15:58:39 -08:00
David Mann 545d447c10 Merge PR #10998: Actions: map recorded Scale Effects and Create Layers 2026-11-04 00:51:01 +01:00
David MannandClaude Opus 5.5 c430647bb0 Batch: drop the Info panel's crop Ratio line from #10453
The reference application's Info panel shows the crop frame's W and H but no
ratio line, so it stays out for now; W and H keep following the frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:18:12 +01:00
David MannandClaude Opus 5.5 75a6828f53 Batch: picking the Crop tool again keeps a committed crop's frame hidden
Measured in the reference application: after ↵ commits a crop, the Crop
tool shows no frame; pressing C again, or another tool and back, keeps it
hidden, and only a press on the canvas brings the frame back. A crop
committed by leaving the tool is different: back on the Crop tool the idle
frame shows at once. #10642 brought the frame back on re-picking; picking
the tool no longer ends the hidden state, and a commit on leaving the tool
does not start it. Tests follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:13:42 +01:00
David MannandClaude Opus 5.5 37c84c18e5 Batch: Ctrl-held Straighten test expects transparent corners with Delete Cropped Pixels off
Main's #10905 (#10867) turns the Background into Layer 0 when a crop keeps
the cropped pixels, so the corners a turn uncovers stay transparent instead
of taking the background colour. The #10950 test now expects that for its
Delete Cropped Pixels off cases; with it on the corners are still the
background colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:39:48 +01:00
David MannandClaude Opus 5.5 c466cac619 Batch: H/P and Info panel tests pick the Crop tool again after a commit
#10642 hides the frame after a committed crop until the next press or
re-picking the tool. The #9855 and #10453 tests expected the default frame
right after the commit; they now check that it is hidden, pick the tool
again and then test the default frame as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 22:56:21 +01:00
David Mann e06d1cbdc1 Merge PR #11067: Layers panel: filter by layer effect (#10880) 2026-11-03 22:55:47 +01:00
David Mann 3c9ae63951 Merge PR #10955: Actions: map recorded Shear curves 2026-11-03 22:55:46 +01:00
David Mann d8ea8061ee Merge PR #11109: Crop: turning the image keeps the frame inside it (#10869) 2026-11-03 22:55:46 +01:00
Psychotoxical 66b9f2f68b Merge remote-tracking branch 'origin/main' into feat/layer-filter-effect
# Conflicts:
#	crates/automation/src/server.rs
#	crates/ui-egui/src/panels/layer_filter_ui.rs
#	crates/ui-egui/src/state.rs
#	docs/control-protocol.md
2026-11-03 21:55:06 +01:00
ljkx c15a281045 Merge remote-tracking branch 'origin/main' into crop-turn-keeps-frame-inside 2026-11-03 21:49:55 +01:00
yoriswind 3028355333 Merge main and retain action history across Batch and Spelling 2026-11-03 12:42:57 -08:00
David Mann 81bd97376f Merge pull request #10387 from davidmann4/fix/flaky-split-route
CMYK: adjustment layers in the special eight keep the Fill rule on the ink path (#10386)
2026-11-03 21:39:21 +01:00
yoriswind 27f86af55b Merge main into recorded layer style mappings 2026-11-03 12:12:41 -08:00
yoriswind 7763e2578f Merge main into recorded Shear mapping 2026-11-03 12:11:27 -08:00
David Mann 576b705a07 Merge PR #10850: fix(algo): Filter Gallery reach no longer overflows on out-of-range stored effect values 2026-11-03 20:47:27 +01:00
David Mann 0d4e565062 Merge PR #9573: fix(ui): Free Transform keeps plain layers over the moving one under a blend or adjustment layer (#9387) 2026-11-03 20:47:24 +01:00
David Mann f199789b00 Merge PR #10453: Crop: the Info panel shows the crop frame's W and H (#3145) 2026-11-03 20:47:20 +01:00
David Mann b91a75185a Merge PR #9855: Crop: H and P pick the Hand and Pen tools over the default frame (#9731) 2026-11-03 20:47:16 +01:00
David Mann 8e29e9d55d Merge PR #11019: Crop: give each overlay its own glyph in the menus and on the options-bar button (#10889) 2026-11-03 20:47:13 +01:00
David Mann 940f3b4715 Merge PR #10565: Free Transform: lift a pixel's coverage up to all of it, as Move does 2026-11-03 20:47:09 +01:00
David Mann e54f07a4dd Merge PR #10642: Crop: no frame around the cropped image until the next press 2026-11-03 20:47:06 +01:00
David Mann 0ec0255533 Merge PR #9842: Fix Crop Clear retaining custom proportions 2026-11-03 20:47:03 +01:00
David Mann 5c3f2e1392 Merge PR #10950: Crop: a Ctrl-held Straighten line keeps the frame size (#10870) 2026-11-03 20:46:59 +01:00
David Mann d93e65d001 Merge pull request #10952 from davidmann4/integration-batch-13
Integration batch 13: 13 parity epic PRs on main
2026-11-03 20:37:25 +01:00
David MannandClaude Opus 5.5 72c2585177 Merge origin/main (287352bf92) into integration-batch-13
Conflict in crates/engine/src/jobs.rs: main's save commit point (#5333,
OPEN/COMMITTED/CANCELLED phase) met the batch's cancel state
(CANCEL_REQUESTED/WORK_FINISHED). Both now share one atomic with a
COMMITTED bit; commit vs cancel and the batch's late-cancel guard each
stay one compare-and-swap. A Batch that refuses a repeated or late cancel
reports CancelRefused::Finishing ("finishing") in jobs.cancel's
notCancelled list, still {"cancelled": 0}. control-protocol.md documents both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:35:44 +01:00
FredApps 287352bf92 Merge pull request #5333 from vistaero/codex/streamed-psb-save
Integrate streamed PSB saves with the shared save service
2026-11-03 18:41:16 +01:00
yoriswind 0edb29a18c Merge upstream main into Shear action mapping 2026-11-03 09:32:21 -08:00
skylar taylor-barrick b323c1ba27 Merge pull request #10731 from RickiNano/fix/10665-pasteboard-pick-offcanvas
Don't auto-select off-canvas layers from the pasteboard (#10665)
2026-11-03 18:17:45 +01:00
David Mann 3d36330d94 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	docs/scorecard.md
2026-11-03 18:08:29 +01:00
FredApps 996a52cb8f PSD export: a cancelled save stops between the bands of a banded layer
A banded layer checks the save's interrupt before each band and before each
channel's part of a band. Once cancelled it stops there and doesn't encode the
layer again raw; the incomplete model is discarded by the caller (which returns
IoError::Cancelled), so the save never reaches its commit point and the target
is untouched. Ex carries the export's Interrupt for this.

Addresses CodeRabbit review on crates/io/src/psd_export/banded.rs.
2026-11-03 18:00:23 +01:00
ljkx 3c82477fac Crop: turning the image keeps the frame inside it (#10869)
In the default mode a drag outside the box turns the image, but the
frame never changed, so its corners left the turned image and the crop
filled them with the background colour.

Each move of the turn now starts from the frame at the press: it keeps
its size and place if it fits the turned image, else moves the shortest
way in, else shrinks about its centre (keeping its proportions) to the
largest size that fits. The view follows the frame's centre, so the box
stays put on screen. Ctrl (Cmd) held during the turn leaves the frame as
it was. Content-Aware Fill and a frame already past the canvas keep
today's behaviour (not measured).
2026-11-03 17:57:16 +01:00
FredAppsandClaude Opus 5.5 d07684a379 fix(ui): Overscroll off keeps an image smaller than the window centred (#10488) (#11085)
* fix(ui): Overscroll off keeps an image smaller than the window centred (#10488)

With Tools › Overscroll off, an image smaller than the window stayed
wherever zooming around the pointer left it, inside the window, so
zooming out drifted it off centre. #1718 measured that on Photoshop 25.4,
but two reports of Photoshop itself (#10488, #4799) say it stays centred
with Overscroll off. The owner chose the centred behaviour:
scrollbars::clamp_axis now centres an axis on which the image is no
larger than the view; a larger image still can't leave its edges, and
Overscroll on keeps the free view.

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

* test(ui): Overscroll off centres a fitting rotated image (#10488)

CodeRabbit on #11085: the rotated clamp path was only tested from an
already-centred view. A 600 px square at 45 degrees, started off centre
in a 900-point view, now ends with its screen bounds centred.

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

* style(ui): keep the rotated centring test within rustfmt's width

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:55:50 +01:00
FredAppsandClaude Opus 5.5 d9dc644db1 fix(ui): Roundness previews its radius within 1 px, and a held frame survives a factor change (#10474) (#11081)
* fix(ui): Roundness previews its radius within 1 px too, and a held frame survives a factor change (#10474)

Follow-up to #10524 from its last CodeRabbit review.

- Roundness's disc reaches floor(r) pixels along an axis, so at 6 px
  and k = 4 the proxy radius 1.5 reached 1 proxy px: a 4 px preview for
  a 6 px result. The factor now keeps floor(r / k) * k within 1 px of
  floor(r), as Squareness already does with round (k = 3 at 6 px, k = 2
  at 50 px). One whole-pixel function per mode replaces the special case.
- A canvas test drives the worker across a factor change: the frame for
  k = 2 stays on the canvas, at its own size, until the k = 1 frame lands.

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

* test(ui): a fractional Roundness radius previews closer at its chosen factor (#10474)

CodeRabbit on #11081: only the factors were pinned. At 6.5 px the
Roundness preview at the chosen k = 3 is compared with OK's result and
must differ less than the k = 4 preview did.

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

* fix(ui): a mid-drag frame for the previous proxy factor still reaches the canvas (#10474)

CodeRabbit on #11081: the display gate keeps a held frame at any factor,
but the mid-drag publish arm still required the same factor, so a frame
finished for the factor the drag just left was dropped. It now uses
same_target_at_any_scale too; a frame already exact for the dialog is
still never displaced.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:55:42 +01:00
d21c403e5f Type: add OpenType Numerals submenu (figure styles) (#11051)
* Type: add OpenType Numerals submenu (figure styles)

Type › OpenType › Numerals sets lnum/onum/pnum/tnum as a radio style
(Default through Tabular Oldstyle). Checkmarks follow the selection
start; TYPE-218-5 scorecard/gaps updated; parity floor 640.

* refactor(engine): one style-at-character lookup for the OpenType menus

feature_on_at and numerals_style_at each walked the runs the same way;
they now share style_at. NumeralsStyle::from_id and numerals_style_of
read the existing tables instead of repeating them.

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

* fix(text): persist OpenType numeral tags through PSD EngineData

Map lnum/pnum/tnum as EngineData bools and the width×height combos as
FigureStyle (Krita/Illustrator), so Numerals choices survive PSD round-trip.

* docs(gaps): note Numerals PSD figure-tag round-trip

* revert(text): keep PSD EngineData to the keys Photoshop writes

Backs out the PSD half of 18610324 and its gaps note (58813de3); the
engine reformat in that commit stays.

- `LiningFigures`, `ProportionalFigures` and `TabularFigures` are not
  Photoshop EngineData keys (the commit says Photoshop only has
  `OldStyle`), so writing them puts keys of our own into users' PSDs.
- The `/FigureStyle` code table (1-4) has no oracle: no Photoshop-authored
  file in photocraft-corpus, and neither ag-psd nor psd-tools reads it.

AGENTS.md rule 3 asks for public specs or observation. PSD persistence
of the numeral styles can come back with a Photoshop-authored oracle.
The Numerals menu still round-trips through .pcraft.

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

* test(text): lock PSD EngineData to Photoshop OldStyle for numerals

Document AGENTS.md clean-room: only onum survives TySh round-trip; lnum/pnum/tnum stay .pcraft-only until a Photoshop-authored corpus oracle.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:55:35 +01:00
David MannandClaude Opus 5.5 ae0da189a2 Address CodeRabbit review: a cancel after an action's saved snapshot keeps that file
When an action saves and then closes or switches away, Batch writes the
saved snapshot inside the action closure. A cancel landing after that write
made process_one return Cancelled, so the input was reported as skipped
although its output exists. The snapshot write is now final: the cancel
stops only the inputs after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:26:39 +01:00
David Mann 559d3fe0f4 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/engine/src/cmd/file_cmds.rs
#	crates/ui-egui/src/app/files.rs
#	crates/ui-egui/src/dialogs/tiff_options_ui.rs
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/ui-parity.md
2026-11-03 17:11:53 +01:00
FredApps f67696cb2c Jobs tests: give the gated-save tests a temp dir on every platform
The cancel-path tests used the Unix-only tmp_dir fixture, so the engine's lib
tests did not compile on Windows (E0425 in CI run 41660911095). They get their
own save_dir helper instead.
2026-11-03 16:42:18 +01:00
Raghav Krishna 1171d6cce4 Test that the six overlay drawings are all different 2026-11-03 21:02:36 +05:30
Raghav Krishna 286a152fee Test that each canvas Crop menu row draws its own glyph 2026-11-03 20:47:07 +05:30
292ce6ab55 fix(ui): Minimum and Maximum preview at the scale they apply (#10474) (#10524)
* fix(ui): Minimum and Maximum preview at the scale they apply (#10474)

Filter > Other > Minimum... and Maximum... were missing from FULL_RESOLUTION_PREVIEW, so on
any document large enough to be proxied their live preview was not what OK writes.

preview_factor keeps the largest proxy factor at which no pixel-sized parameter falls below
its command's own minimum, and for these two commands that minimum is 0.2
(engine/src/filters/validation.rs:90). A typed radius of 1.5 px therefore allows k = 4, and
scale_preview_params divides it to 0.375 px. Both kernels then measure that in whole pixels
and return the crop untouched when it reads 0: Squareness is
`let r = radius.max(0.0).round() as i32;` (algo/src/other.rs:83) and Roundness is
`let r = radius.max(0.0); let ri = r.floor() as i32;` (other.rs:23-24). So the dialog showed
the untouched document while OK applied a real 1.5 px (Squareness) or 1 px (Roundness)
effect - the same failure mode Dust & Scratches already has in that list for (#3265). The
repo already asserts this rule for the filters that are listed, in
fixed_size_filters_preview_at_the_scale_they_apply; Minimum and Maximum simply were not in it.

Fail-first: four rows (1.5 px in both Preserve modes, both filters) added to that test failed
with the production change stashed - `assert_eq!(k, 1, "filter.other.minimum")` reported
left: 4, right: 1 - and pass with it. filter_dialog group 177 pass / 0 fail; full
cargo test -p photocraft-ui-egui --lib 4440 pass / 22 fail, a strict subset of the 24
failures a clean 422ca7be checkout gives in this environment (the i18n, font-picker,
type-tool and layer-row tests that need a CRAFT_FONTS_DIR checkout this machine has no
access to); nothing new failed. clippy --all-targets -D warnings clean, fmt --check clean,
cargo xtask layers OK (31 crates), cargo xtask wasm ok.

Not touched, on purpose: the kernel math, the 0.2..500 radius range, and the fractional-radius
question in #10474. Min/max over an integer lattice cannot represent a fractional radius at
all, and the two Preserve options already snap at different thresholds (0.5 px for Squareness,
1.0 px for Roundness). Picking graded output or one shared rule needs reference captures, and
nothing in docs/ records the reference behaviour today; guessing would put the guess in the
tests as the oracle. The question stays open on the issue. No doc row either: #2063 and #3265
appear only in code comments, not in any docs table.

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

* fix(ui): Minimum and Maximum keep one proxy pixel of radius, not a full-size preview (#10474)

Previewing every radius at full size fixed the identity preview but made
a large radius (up to 500 px, Roundness is O(radius) per pixel) cost
seconds to minutes per tick on an ordinary photo. The bug only appears
when radius / k drops under one kernel pixel, so cap k at floor(radius):
1.5 px still previews at k = 1, while 50 px keeps the 1/4 proxy.

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

* fix(ui): Squareness previews its radius within 1 px of what OK applies (#10474)

Capping k at floor(radius) kept a proxy pixel of radius, but Squareness
rounds the proxy radius: 6 px at k = 4 is round(1.5) = 2 proxy px, an
8 px preview for a 6 px result. Take the largest k whose rounded proxy
radius, scaled back, is within 1 px of round(radius) (k = 3 for 6 px and
50 px); Roundness reads its disc from the unrounded radius and keeps the
floor(radius) cap. The radius is clamped to the command's 0.2-500 range
first, as the engine does.

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

* fix(ui): the canvas keeps the last preview frame when a drag changes the proxy factor (#10474)

Minimum and Maximum's factor now follows the radius, so a radius drag
crossing a whole pixel changed FilterPreviewKey::k, and the display gate
(same_target, which includes k) dropped the held frame: the canvas showed
the unfiltered document until a frame for the new factor landed. The
frame is drawn at its own k (textures read preview.key.k), so the gate
now forgives the factor alone (same_target_at_any_scale); publishing and
freshness keep the strict checks.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 16:12:16 +01:00
FredApps 26d151d061 Jobs: a save past its commit point can't be reported as cancelled
jobs.cancel, cancel_all_jobs and closing a document (cancel_jobs_on) cancelled
any running job, so they could report a save as cancelled while its worker
went on to replace the file. The point of no return was tracked only by the
desktop shell's own Cancel/Esc path.

The commit point now lives in the engine's JobCtx: commit() and a cancel race
on one compare-and-swap, so exactly one wins.
- Cancelled before commit: commit() fails, the save writes nothing and the
  job ends Cancelled.
- Committed: every cancel path leaves the job running and says so
  (Session::try_cancel_job -> CancelRefused::Committing; cancel_all_jobs and
  cancel_jobs_on return a CancelReport). The job then reports its real
  outcome, applied even if its document was closed meanwhile.
- jobs.cancel (engine and desktop control) replies {cancelled: n} as before,
  plus notCancelled: [{job, reason: "committing"}] for a save left running.
  This is not an error: the job is fine and will report how it ended.
  Single-job print refusals are unchanged.
- The shell's SaveCtl::commit and Cancel/Esc use the engine gate, so the
  status bar, control and scripting agree.

Tests: cancel before commit (target untouched, Cancelled), cancel after
commit (refused, Done, file written), cancel_all and document close with
mixed committed/open jobs, a commit/cancel race, and desktop jobs.cancel
before and during the write with real files.
2026-11-03 16:09:07 +01:00