Files

17410 lines
1.0 MiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"source": "fresh GitHub GraphQL and REST reads",
"captured_at": "2026-10-29T02:24:09.464811+00:00",
"count": 108,
"prs": [
{
"number": 9703,
"title": "Fix adjustment layer name clicks targeting masks",
"url": "https://github.com/storytold/photocraft/pull/9703",
"body": "Fixes #9694\n\nClicking an adjustment layer's name now selects its contents instead of its mask, including when that same layer's pixel or vector mask was previously selected. Explicit mask-thumbnail clicks and new-layer creation keep their existing behavior. Fill-layer row targeting and rasterization prompts, and ordinary raster-layer row behavior, are unchanged.\n\nRegression coverage uses actual native UI clicks at 1x and 2x, checks the selected content-thumbnail brackets, and preserves the existing #3369 Properties/creation regression. The new regression fails on the base revision and passes with this fix. An actual Fill-black regression verifies that adjustment content is preserved and only its mask changes. Pixel painting on adjustment layers retains its existing mask fallback.\n\nValidation:\n- 29 mask-thumbnail tests pass; independent code review found no blocker.\n- Strict all-target UI Clippy, formatting, layering, and WebAssembly checks pass.\n- Full UI library run: 4,013 passed, 26 failed, 18 ignored. Integration: 132 passed, 1 failed, 1 ignored. All 27 failures are missing-glyph panics in egui 0.37 with the fonts available in this local build; the full suite is not green. Doctests pass (0 tests).\n- Native macOS offscreen screenshots rendered and visually inspected. Windows/Linux UI has not been manually tested.\n\nBefore (mask thumbnail selected after clicking the name):\n\n![Before](https://raw.githubusercontent.com/vanvo/photocraft/d879000b88edd76743a7774a0517b5bcd2e63ebd/before.png)\n\nAfter (content thumbnail selected after the same click):\n\n![After](https://raw.githubusercontent.com/vanvo/photocraft/d879000b88edd76743a7774a0517b5bcd2e63ebd/after.png)\n\n[Image attribution and licenses](https://raw.githubusercontent.com/vanvo/photocraft/d879000b88edd76743a7774a0517b5bcd2e63ebd/ATTRIBUTION.md)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "3605d30c38294c4d970e52f4a02f44f97e8bc1a9",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/mask_thumbs_ui/tests.rs"
},
{
"path": "crates/ui-egui/src/panels.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095607/job/121682358085"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095888/job/121681757819"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095888/job/121681861941"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095607/job/121682358927"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095888/job/121681862838"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095607/job/121682358648"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095607/job/121682359068"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095888/job/121681860253"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095607/job/121682361404"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095607/job/121682729957"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095607/job/121682729898"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40354095607/job/121682729794"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T17:06:00Z",
"url": "https://github.com/storytold/photocraft/pull/9703#pullrequestreview-5655990760",
"body": "Reviewed at SHA 3605d30.\n\nFixes #9694: clicking an adjustment layer's name in the Layers panel now selects its content, not its mask (`ui.maskTarget` false), including when that layer's pixel or vector mask was selected before. In `layer_row` the old `content_less` rule (adjustment and fill rows default to the mask) is split: fills with a mask keep the mask default; adjustments take the `maskTarget false` branch (`on_thumb || adjustment || !row.primary`). Clicking a mask thumbnail still targets that mask, the vector mask thumbnail still targets the vector mask, and creating a layer keeps its existing behaviour.\n\nChecked: the whole `panels.rs` change (the branch order is unchanged, only the conditions); the edited expectation in the existing #3369 test (fill rows still target the mask, adjustments do not, no vector mask target); the new test across two adjustment kinds at 1x and 2x: mask thumbnail clicks, name click after each, bracket on the content thumbnail, another-layer-then-name, `paint_target` still \"mask\", and Fill black editing the mask and not the adjustment content.\nNote: this is a deliberate behaviour change for adjustment rows, whereas fill rows keep the old default; it is covered by the tests and matches the issue, but it is a product choice I did not check against the other app.\nNot verified: I did not run the tests; the author reports 29 mask-thumbnail tests passing and a local failure set of font-glyph panics that predates this change, which I did not reproduce.\n",
"commit": {
"oid": "3605d30c38294c4d970e52f4a02f44f97e8bc1a9"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"adjustment_names_target_contents_instead_of_masks"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9703"
},
{
"number": 9651,
"title": "perf(algo): row-parallel local_sharpen with fast-path protect_detail (#5426)",
"url": "https://github.com/storytold/photocraft/pull/9651",
"body": "Refs #5426. Follow-up to #5499.\n\n## Problem\n\n#5499 parallelized `local_blur` and the per-dab mix in `DabTool::Focus`, but `local_sharpen` (`crates/algo/src/retouch.rs`) stayed single-threaded. `protect_detail` also recomputed boundary clamping and strided lookups for the 8-neighbour window inside the per-channel loop, for every pixel in the dab. With large brushes (500–2500 px) interactive strokes lagged badly and stalled the application.\n\n## Change\n\n- `local_sharpen` processes window rows in parallel (`out.par_chunks_mut(stride)`), with a `wasm32` serial fallback like `local_blur`.\n- `protect_detail` computes the 3×3 neighbourhood offsets once, outside the channel loop.\n- Interior pixels (`x > 0 && x + 1 < w && y > 0 && y + 1 < h`) use direct stride offsets and skip boundary clamping, which covers over 99.9% of pixels in large dabs.\n- New `retouch_sharpen_exact` integration test compares against the frozen scalar oracle bit for bit across layouts, edges and channels.\n- New `bench_sharpen_tool` example, matching `bench_blur_tool`.\n\n## Timings\n\nGitHub Actions `rc-bench`, 4 vCPU AMD EPYC 9V45. Output is bit-identical at every size (fingerprint `96d1f22de93b16ec` at 8-bit, `9282e5cfc9e3ec48` at 16-bit).\n\n| Depth | Brush | Before | After | Less time |\n| --- | ---: | ---: | ---: | ---: |\n| 8-bit | 100 px | 40.75 ms | 27.37 ms | 33% |\n| 8-bit | 500 px | 214.14 ms | 153.37 ms | 28% |\n| 8-bit | 1000 px | 738.67 ms | 571.52 ms | 23% |\n| 16-bit | 100 px | 39.94 ms | 27.42 ms | 31% |\n| 16-bit | 500 px | 196.86 ms | 134.26 ms | 32% |\n| 16-bit | 1000 px | 597.21 ms | 447.68 ms | 25% |\n\n## Tests\n\n- `cargo test -p photocraft-algo --test it -- retouch_sharpen_exact` (2 passed)\n- Bit-identical fingerprints between base and this PR at every depth and size.\n- Wasm size gate and clean `cargo clippy`.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "67666bdf72a58066df868868cef82e5b9cc838de",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/algo/src/retouch.rs"
},
{
"path": "crates/algo/tests/it/main.rs"
},
{
"path": "crates/algo/tests/it/retouch_sharpen_exact.rs"
},
{
"path": "crates/engine/examples/bench_sharpen_tool.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393509820"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685201/job/121510766804"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685377/job/121510767326"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393597582"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685377/job/121510848043"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685201/job/121510766978"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393508322"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685377/job/121510849890"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685201/job/121510766972"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393560877"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685201/job/121510766515"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685377/job/121510847640"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685201/job/121510769284"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393509224"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393564915"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685201/job/121510958391"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393558240"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40304685201/job/121510956808"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393511108"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562083384/job/122393511236"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T11:38:57Z",
"url": "https://github.com/storytold/photocraft/pull/9651#pullrequestreview-5651366614",
"body": "Reviewed at SHA 67666bd.\n\nWhat it does: `local_sharpen` in `algo/src/retouch.rs` is now row-parallel with rayon (a sequential `chunks_mut` on wasm32). For `protect_detail` it computes the 3x3 neighbour offsets once per pixel, using a branch-free fast path for interior pixels and the old clamped lookup at the edges. An empty window or zero channels returns an empty Vec early (#5426).\n\nChecked: each output row is a disjoint `stride = ow*ch` chunk, so the rows are independent. The interior test (`x>0 && x+1<w && y>0 && y+1<h`) gives exactly the unclamped neighbours, and the edge path is the old clamp formula, so the 1-pixel-wide and 1-pixel-high cases fall to the clamped branch. The alpha channel is passed through as before. The `blur` row slice matches the per-pixel `blur[o + c]` index. The new test freezes the old scalar code as an oracle and compares every output bit across sizes (1x1, 3x7, 37x29), 1 to 5 channels, every alpha position, four windows including empty and offset ones, amounts 0 to 2.5 and both `protect_detail` values, with HDR-range data.\n\nNot verified: I did not run the tests or the benchmark added under `engine/examples`, so I have no figure for the speedup. The wasm path was not built by me. The approval rests on the hosted checks being green at this head.\n",
"commit": {
"oid": "67666bdf72a58066df868868cef82e5b9cc838de"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"matches_scalar_sharpen_bit_for_bit_across_layouts_edges_and_hdr",
"empty_channel_layout_remains_empty",
"scalar_local_sharpen",
"run",
"main"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9651"
},
{
"number": 9623,
"title": "Pencil: keep a 1 px freehand line connected where it turns",
"url": "https://github.com/storytold/photocraft/pull/9623",
"body": "Fixes #8040\n\n## Cause\nWith a 1 px Pencil, `PathWalker::grid_steps` rasterizes each segment of the smoothed stroke on its own line. The last column of a segment can sit up to half a pixel past the segment end, so its minor-axis pixel is extrapolated along that segment. Where a freehand stroke turns, the next segment samples a different line, and its first pixel can land two pixels away from the previous segment's last one. The 1 px line then has a gap (the second screenshot in the issue). Random 6-segment strokes broke this way 189 times out of 2000.\n\nThe Shift-click endpoint matrix from the original report already matches the clicked pixels since #8053. I re-checked all TL/TR/BL/BR/C combinations for nine end points at Smoothing 0 and 10 % through the UI path and found no differences.\n\n## Fix\nThe walker remembers the grid cell of its last step. When a new pixel-grid step does not touch it, `bridge` emits the pixels in between first, so consecutive segments stay 8-connected. Gaps wider than 4 px (a segment cut short by the step budget) are left alone. A straight line split into pieces is already connected, so the #6336 behaviour does not change.\n\n## Verified\n- Regression test `pencil_1px_freehand_line_never_breaks_where_it_turns`: fails on `main` without the fix and passes with it (`cargo test -p photocraft-paint --lib pencil_1px_freehand_line_never_breaks_where_it_turns`).\n- [x] `cargo xtask ci`: fmt, clippy, layers, scorecard, doctests and wasm pass locally. 27 localization/text UI tests (26 in the `photocraft-ui-egui` lib, plus `camera_raw_localization::all_registered_languages_render_camera_raw_and_switch_without_resetting_view_or_filter`) fail on this macOS machine because it has no CJK system fonts (`No glyph for \"模\"`). The same tests fail on `main` without this change.\n\n🤖 Generated with AI, see the Co-Authored-By trailers for the models",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "652075f4da7fe905ad651eebc3a6c276fe1d7a5d",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/paint/src/brush_tests.rs"
},
{
"path": "crates/paint/src/dynamics.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393537839"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176832/job/121447014938"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176709/job/121447013937"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393520141"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176709/job/121447060585"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176832/job/121447014413"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393465298"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176709/job/121447061501"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176832/job/121447014807"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393465826"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176832/job/121447014799"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176709/job/121447060063"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176832/job/121447016375"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393540892"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393536509"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176832/job/121447217665"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393464452"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40285176832/job/121447215441"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393466498"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40562070637/job/122393466269"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T08:29:25Z",
"url": "https://github.com/storytold/photocraft/pull/9623#pullrequestreview-5649212328",
"body": "Reviewed at 652075f (`dynamics.rs` and the new test in `brush_tests.rs`, the whole diff).\n\nCause and fix, as the code shows them: on the pixel-grid path each segment of a freehand stroke is rasterized on its own line, so where the stroke turns the next segment's first pixel can sit two cells from the previous one's last and the 1 px line has a gap. `PathWalker` now remembers the grid cell of the last emitted step (`cell`, set in `emit`, so the filler pixels also update it) and `bridge` fills the cells between it and the next step before that step is emitted.\n\nChecked: `bridge` only runs on grid steps, and only when the gap is at most `MAX_BRIDGE` (4 px) on the larger axis, so a step cut off by the walker's budget is not smeared; it stops once the next cell is within 1.5 on both axes, moving at most one cell per axis per fill, so it terminates in at most four iterations and a diagonal or straight gap is filled with one connected path; `cell` starts as `None` in the constructor, so the first step of a stroke has nothing to bridge to. The new test draws 300 random 7-point strokes with a 1 px aliased tip and asserts the painted pixels are one 8-connected component; it reproduces the report (#8040) and would fail on the gap. The hosted gate is green at this head.\n\nNot verified: I did not run the test, and I did not check whether the bridge can add a pixel the old path did not paint at the very first cell after `pixel_grid` is re-armed in a long-running walker (the walker is built per stroke in the paths I read).\n\nAPPROVE.\n",
"commit": {
"oid": "652075f4da7fe905ad651eebc3a6c276fe1d7a5d"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"pencil_1px_freehand_line_never_breaks_where_it_turns",
"bridge"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9623"
},
{
"number": 9606,
"title": "fix(ui): the Color panel remembers its options across restarts",
"url": "https://github.com/storytold/photocraft/pull/9606",
"body": "Fixes #9592.\n\nThe Color panel forgot its options on every restart: what it shows (Hue Cube, RGB/HSB/… Sliders, Color Wheel), its ramp, Make Ramp Web Safe and which chip it edits. Its state (`ColorPanelState`) is serializable, but nothing saved it. The dock layout goes to the preferences (`panelLayout`), and `ui.ron` holds only eframe's window and egui state.\n\n### Photoshop's behaviour\n\nPhotoshop keeps the Color panel's options (its panel menu choices) across restarts. I couldn't recheck it tonight, because Photoshop stopped answering scripts in my session.\n\n### What changes\n\n- **`Preferences::color_panel`:** a new field holding the options as JSON owned by the shell, like `panel_layout`. `serde(default)` reads older preference files fine.\n- **`color_panel_ui::persist`:** writes them when they change, a small JSON compare per frame next to `dock::persist`.\n- **`color_panel_ui::restore`:** reads them at launch next to `dock::restore`. Missing or unreadable options keep the defaults.\n- This is separate from workspaces: switching a workspace doesn't change the panel's options.\n\n### Verification\n\nLive app with a scratch settings folder: `ui.set {\"colorPanel\": {\"mode\": \"hsbSliders\", \"ramp\": \"grayscaleRamp\"}}`, quit, relaunch:\n\n| | main | this PR | Photoshop |\n|---|---|---|---|\n| Color panel after a restart | Hue Cube, RGB Spectrum (defaults); nothing in `preferences.json` or `ui.ron` | HSB Sliders, Grayscale Ramp | options kept |\n\n### Tests\n\n- `color_panel_tests::color_panel_options_survive_a_restart`: a mode and ramp go through the preferences into a new app; empty preferences keep the defaults. Without saving it comes back as (HueCube, RgbSpectrum).\n- `cargo fmt --check`; clippy `-D warnings` for engine and ui-egui; `cargo test -p photocraft-engine -p photocraft-ui-egui` (with `CRAFT_FONTS_DIR`); `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "6cc516a3d20120b678040050d95ccadc8d1dfd3f",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/prefs.rs"
},
{
"path": "crates/ui-egui/src/color_panel_tests.rs"
},
{
"path": "crates/ui-egui/src/color_panel_ui.rs"
},
{
"path": "crates/ui-egui/src/dock_tests.rs"
},
{
"path": "crates/ui-egui/src/panels.rs"
},
{
"path": "crates/ui-egui/src/prefs_ui.rs"
},
{
"path": "crates/ui-egui/src/state.rs"
},
{
"path": "docs/scorecard.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612868/job/121399742130"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612429/job/121399369668"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612593/job/121399370133"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612593/job/121399413036"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612868/job/121399742121"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612593/job/121399412928"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612868/job/121399742076"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612868/job/121399741791"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612593/job/121399412202"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612868/job/121399743025"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612868/job/121399878046"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612868/job/121399878050"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40269612868/job/121399878090"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T05:50:41Z",
"url": "https://github.com/storytold/photocraft/pull/9606#pullrequestreview-5647898187",
"body": "Code read (diff and the whole preference path). Fixes #9592: the Color panel's options are kept in a new `Preferences.color_panel` JSON field, restored at launch in `prefs_ui::load` and synced each frame by `persist`: it writes the panel's options when they differ from the stored value, and takes the stored value into the live panel when it changed outside the panel (`prefs.set`, a reset). `color_panel_synced` is `serde(skip)`, so it is not saved. An unreadable or missing stored value gives the defaults. Tests cover the round trip, defaults without saved options, an outside change reaching the panel, a reset, and the dock round trip. The scorecard count (12 of 179) matches the new field. No defect found. Tests not run, app not run.\n",
"commit": {
"oid": "6cc516a3d20120b678040050d95ccadc8d1dfd3f"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"color_panel_options_survive_a_restart",
"an_outside_preference_change_reaches_the_live_panel"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9606"
},
{
"number": 9598,
"title": "fix(ui): Liquify's Twirl Counterclockwise gets a mirrored twirl icon",
"url": "https://github.com/storytold/photocraft/pull/9598",
"body": "Fixes #9547.\n\nIn Filter › Liquify, the Twirl Counterclockwise tool used Lucide's `undo-2` arrow, which reads as Undo. Twirl Clockwise right above it uses `rotate-cw`.\n\n### What changes\n\n- **`assets/icons/rotate-ccw.svg`:** `rotate-cw.svg` mirrored horizontally. Its path data is mirrored about x = 12, so no SVG transform is needed. This is the shape Lucide's own `rotate-ccw` has.\n- **`icon_data.rs`:** registers it.\n- **`liquify_ui::tool_icon`:** Twirl Counterclockwise uses it.\n- **`ATTRIBUTION.md`:** the Lucide row counts 114 files and notes that `rotate-ccw.svg` is `rotate-cw.svg` mirrored, as it already does for `star-filled.svg`.\n\n### Tests\n\n- `liquify_ui::tests::twirl_counterclockwise_has_its_own_mirrored_icon`:\n - Twirl CCW's icon is `rotate-ccw`, and it is the mirrored `rotate-cw` path;\n - every Liquify tool's icon exists.\n - It fails on main (`undo-2`).\n- `cargo fmt --check`; clippy `-D warnings` for ui-egui; `cargo test -p photocraft-ui-egui` (with `CRAFT_FONTS_DIR`); `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "47aea89e9220b996f0d5a0d2bb8b5938100b4ad1",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "ATTRIBUTION.md"
},
{
"path": "assets/icons/rotate-ccw.svg"
},
{
"path": "crates/ui-egui/src/icon_data.rs"
},
{
"path": "crates/ui-egui/src/liquify_ui.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177511/job/121385906318"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177247/job/121385905323"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177247/job/121385950340"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177511/job/121385906698"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177247/job/121385950530"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177511/job/121385906422"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177511/job/121385906463"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177247/job/121385949395"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177511/job/121385907423"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177511/job/121386027866"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177511/job/121386027889"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265177511/job/121386027809"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T05:45:19Z",
"url": "https://github.com/storytold/photocraft/pull/9598#pullrequestreview-5647858443",
"body": "Code read (diff only). Fixes #9547: Twirl Counterclockwise used the Undo arrow; it now has a `rotate-ccw` icon, the mirror of the existing `rotate-cw`, added to the icon table and the attribution list (Lucide, ISC, like `rotate-cw`). The test checks the icon name, that the two SVG paths are mirrored, and that every Liquify tool icon named in the test exists. One nit, not blocking: the new test was inserted between the old \"Follow-up to #1494\" doc comment and its test, so that comment now documents the new test, and `alt_reversed_twirl_survives_replay_undo_and_redo` lost its description. I did not check the icon's rendering. No defect found. Tests not run, app not run.\n",
"commit": {
"oid": "47aea89e9220b996f0d5a0d2bb8b5938100b4ad1"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"twirl_counterclockwise_has_its_own_mirrored_icon"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9598"
},
{
"number": 9597,
"title": "fix(ui): the layer list doesn't move with no layer selected",
"url": "https://github.com/storytold/photocraft/pull/9597",
"body": "Fixes #9546.\n\nWith no layer selected, the Layers panel shows the Blend/Opacity and Lock/Fill rows greyed out (#8880), and the layer list starts lower than with a layer selected. In the Studio and Classic themes the first layer row moves down 6 points. Cause: the greyed-out rows were drawn inside one `add_enabled_ui(false, …)` wrapper. A child UI adds one more item gap after itself, which the active layer's rows (two `ui.horizontal`s) don't have. The Pro themes' docks have zero item spacing, so they didn't show it.\n\n### Photoshop's behaviour\n\nPhotoshop keeps the layer list in place whether or not a layer is selected; only the rows above grey out. I didn't open Photoshop for this: Photoshop stopped answering scripts in my session tonight, and the expected layout is the panel's own selected state.\n\n### What changes\n\n`panels.rs` `inactive_blend_rows` has no disabled wrapper any more. Each of its two rows is a `ui.horizontal` that disables itself (`ui.disable()`), so its layout is the same as the active layer's rows.\n\n### Verification\n\n| First layer row top (harness, 1440 × 1000) | main | this PR |\n|---|---|---|\n| Studio, selected / none | 578 / 584 | 578 / 578 |\n| Classic, selected / none | 578 / 584 | 578 / 578 |\n| Pro Medium, selected / none | 594 / 594 | 594 / 594 |\n\n### Tests\n\n- `layer_pct_slider_tests::layer_rows_do_not_move_when_no_layer_is_selected`: the first row's top is the same with and without a selected layer, in Studio, Classic and Pro Medium. On main it fails in Studio (584 vs 578).\n- `opacity_and_fill_stay_visible_but_disabled_with_no_layer_selected` still passes: the controls stay visible and disabled.\n- `cargo fmt --check`; clippy `-D warnings` for ui-egui; `cargo test -p photocraft-ui-egui` (with `CRAFT_FONTS_DIR`); `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "9429df26b371c04c2afa65228a60ded739b5fbc2",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/layer_pct_slider_tests.rs"
},
{
"path": "crates/ui-egui/src/panels.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264952855/job/121385210541"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264953008/job/121385210911"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264953008/job/121385245783"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264952855/job/121385211068"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264953008/job/121385246281"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264952855/job/121385210822"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264952855/job/121385210676"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264953008/job/121385245068"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264952855/job/121385211594"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264952855/job/121385373122"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264952855/job/121385373145"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264952855/job/121385373173"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T07:55:42Z",
"url": "https://github.com/storytold/photocraft/pull/9597#pullrequestreview-5648898287",
"body": "Reviewed at 9429df2 (the whole diff: `panels.rs` and the new test).\n\nThe cause in the PR is right on the code: `inactive_blend_rows` drew both greyed-out rows inside one `ui.add_enabled_ui(false, ...)`, a child UI that adds one item gap after itself, which the active layer's two `ui.horizontal` rows do not have, so the layer list sat 6 points lower in the themes whose docks keep item spacing. The fix gives each row its own `ui.horizontal` that calls `ui.disable()` itself, moves the trailing `ui.add_space(2.0)` out of the wrapper, and leaves the row bodies unchanged apart from indentation (I compared the two versions; the label, lock icon and fill-field code is the same). `ui.disable()` inside the horizontal closure affects only that row's child UI, so nothing after it is greyed out.\n\nThe new test `layer_rows_do_not_move_when_no_layer_is_selected` compares the top of the \"Paint\" row with and without a selected layer in Studio, Classic and Pro Medium, which is the stated symptom. The hosted gate is green at this head. Not verified: I did not run the test or look at the panel on screen, and I took the Photoshop behaviour from the PR text.\n\nAPPROVE.\n",
"commit": {
"oid": "9429df26b371c04c2afa65228a60ded739b5fbc2"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"layer_rows_do_not_move_when_no_layer_is_selected"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9597"
},
{
"number": 9596,
"title": "fix(io): drop a kept Txt2 once no type layers are left",
"url": "https://github.com/storytold/photocraft/pull/9596",
"body": "Fixes the first problem in #9563 (the orphaned `Txt2`; the issue lists others).\n\nA PSD saved from Photoshop with type layers carries a document-level `Txt2` block: the text engine's fonts and layout caches. Once every type layer was deleted, PhotoCraft still wrote that block back unchanged on save. It describes text that no longer exists, and the report measured it at 86 % of one saved file. With type layers present, the export already regenerates `Txt2`. Only the \"no type layers left\" case fell through to the preserved copy.\n\n### Photoshop's behaviour\n\nPer the report: open a PSD with type layers, delete all of them, and save. Photoshop's file has no `Txt2` block. I couldn't rerun this in Photoshop myself tonight, because Photoshop stopped answering scripts in my session. The behaviour is simple to state and matches what the exporter already does when type layers are present.\n\n### What changes\n\n`psd_export`: the preserved `Txt2` is skipped when the document has no type layers, as well as when a fresh one is generated. All other preserved global blocks are untouched.\n\n### Verification\n\n| PSD with a kept `Txt2`, no type layers left | main | this PR | Photoshop (reporter) |\n|---|---|---|---|\n| `Txt2` in the saved file | kept, byte for byte | absent | absent |\n| with a type layer (control) | regenerated | regenerated | present |\n\n### Anything a reviewer should look at first\n\n- The other problems in the issue (an empty `Patt` block and the rest of the list) are left for separate changes, so this PR doesn't say \"Fixes\" for the whole issue.\n\n### Tests\n\n- `psd_export::tests::a_kept_txt2_goes_with_the_last_type_layer`: with no type layers, no `Txt2` is written; with one, it is. On main the first assertion fails.\n- `cargo fmt --check`; clippy `-D warnings` for io; `cargo test -p photocraft-io`; `cargo xtask test-corpus`; `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "e62013cfd652fa60470a0f7a185fbc8f3d2a2c92",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/io/src/psd_export.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034753/job/121382363708"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034770/job/121382363762"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034770/job/121382409315"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034753/job/121382363901"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034770/job/121382409370"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034753/job/121382363687"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034753/job/121382363434"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034770/job/121382408586"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034753/job/121382364947"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034753/job/121382472296"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264034753/job/121382470871"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T05:46:37Z",
"url": "https://github.com/storytold/photocraft/pull/9596#pullrequestreview-5647870259",
"body": "Code read (diff only). Fixes #9563: a `Txt2` block kept from the opened file is dropped on export when no type layers are left (`text_layers.is_empty()`), while the existing rule (dropped when a new one is built) is unchanged. The test saves a document with a stale kept `Txt2` and no text layer (dropped), then adds a text layer (a `Txt2` is written). I did not check that Photoshop drops it as the comment says. No defect found. Tests not run, app not run.\n",
"commit": {
"oid": "e62013cfd652fa60470a0f7a185fbc8f3d2a2c92"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"a_kept_txt2_goes_with_the_last_type_layer"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9596"
},
{
"number": 9595,
"title": "fix(compose, gpu): a Pass Through group with Blend If renders isolated",
"url": "https://github.com/storytold/photocraft/pull/9595",
"body": "Fixes #9554.\n\nA Pass Through group composites its children straight into the backdrop, so an adjustment inside it changes the layers beneath. Photoshop renders such a group isolated, like Normal, in a few cases: below 100 % Fill, and as a clip base that holds an adjustment. PhotoCraft already handles those. Per the report, a group that has its own Blend If is another such case: Photoshop keeps the stored mode Pass Through, but its pixels match an isolated group. PhotoCraft kept compositing straight through, so the group's Invert child inverted the layer beneath before Blend If was even applied.\n\n### Photoshop's behaviour\n\nFrom the report's Photoshop 27.11 runs: (64, 128, 192), then above it a Pass Through group holding only an Invert, with the group's Red › Underlying Layer white point at 110.\n- Photoshop leaves the backdrop at (64, 128, 192).\n- Removing the group's Blend If, or moving the same range onto the child, inverts it.\n- A Normal group with the same range leaves it alone.\n\nI couldn't rerun this myself: Photoshop stopped answering scripts in my session tonight. The reporter's controls separate the cases cleanly, and they match the existing isolation rule.\n\n### What changes\n\nThe pass-through shortcut in `compose::composite_layer`, and its mirror in the GPU planner's `layer_plain`, are skipped when the group has active Blend If (`blend_if_active`). The group then takes the isolated path both compositors already use below 100 % Fill.\n\n### Verification\n\n| (64, 128, 192) under a Pass Through group with an Invert | main | this PR | Photoshop 27.11 (reporter) |\n|---|---|---|---|\n| group Red › Underlying white 110 | (191, 127, 63) | (64, 128, 192) | (64, 128, 192) |\n| no group Blend If (control) | (191, 127, 63) | (191, 127, 63) | inverted |\n\n### Anything a reviewer should look at first\n\n- `change_bounds` and the pass-through adjustment checks (around lines 1661 and 1692) still treat such a group as reaching the layers beneath. That over-estimates the redraw area, which is safe.\n\n### Tests\n\n- `compose::tests::pass_through_group_with_blend_if_is_isolated`: both rows of the table. On main it fails with (191, 127, 63), the reporter's PhotoCraft output.\n- `gpu` `parity::pass_through_group_with_blend_if_is_isolated`: the same group, with and without Blend If, over a noise backdrop. Without the planner change it is 255 levels off.\n- `cargo fmt --check`; clippy `-D warnings` for compose and gpu; `cargo test -p photocraft-compose -p photocraft-gpu`; `cargo xtask test-corpus`; `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "75fa71a4f55a098061b12d91508b514565ab8dcf",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/compose/src/tests.rs"
},
{
"path": "crates/gpu/src/plan.rs"
},
{
"path": "crates/gpu/tests/it/parity.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631605/job/121381121749"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631572/job/121381121288"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631572/job/121381157802"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631605/job/121381121823"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631572/job/121381158203"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631605/job/121381121532"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631605/job/121381121707"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631572/job/121381156591"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631605/job/121381122487"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631605/job/121381282431"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631605/job/121381282339"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263631605/job/121381282316"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T05:48:01Z",
"url": "https://github.com/storytold/photocraft/pull/9595#pullrequestreview-5647882992",
"body": "Code read (diff only). Fixes #9554: a Pass Through group that has Blend If is now rendered isolated, as the group already is below 100% fill, with effects, or as the base of a clipping group with an adjustment: `!blend_if_active(layer, mode)` is added to the pass-through condition in the CPU compositor and in the GPU planner, so an adjustment inside such a group no longer changes the layers beneath. The CPU test uses Invert in a group with Red / Underlying Layer white at 110 (stays 64,128,192) and without the Blend If (inverted); the GPU parity test checks both cases against the CPU. I did not verify the Photoshop values, and GPU output on hardware is only covered if CI has a GPU. No defect found. Tests not run, app not run.\n",
"commit": {
"oid": "75fa71a4f55a098061b12d91508b514565ab8dcf"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"pass_through_group_with_blend_if_is_isolated",
"pass_through_group_with_blend_if_is_isolated"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9595"
},
{
"number": 9594,
"title": "fix(engine): copied type keeps its size across resolutions after an edit",
"url": "https://github.com/storytold/photocraft/pull/9594",
"body": "Fixes #9560.\n\nA type layer copied to a document of another resolution (Duplicate Layer to another document, dragging a layer across, or pasting a copied layer) looked right at first. On its next text edit it re-laid out at the destination's resolution: a 24 pt line from a 72 ppi document grew about 4.2× in a 300 ppi one, and shrank the other way. Text is laid out at `pt × dpi / 72`, and the copies kept the source's point size and box at the destination's dpi.\n\n### Photoshop's behaviour\n\nPer the report (Photoshop 27.11), Photoshop keeps the pixels and adapts the point size: 24 pt from 72 ppi becomes 5.76 pt at 300 ppi, and from 300 to 72 ppi becomes 100 pt. Later edits keep that size. I couldn't rerun this in Photoshop myself tonight, because Photoshop stopped answering scripts in my session.\n\n### What changes\n\n`layer_copy_cmds::adapt_copies`, the step both routes use to fit copied layers to the destination, now runs `canvas_geom::set_resolution` on the copies for the destination's resolution. That is the same compensation Image › Image Size already applies when only the resolution changes:\n- box and path text scale with the dpi ratio;\n- the layer transform undoes it;\n- the PSD `TySh` is retagged.\n\nThe pixels are what Photoshop's adapted point size gives. The type model keeps its point size with a compensating transform, as after Image Size. A rendered copy is unchanged; only later edits now keep the size.\n\n### Verification\n\n| 24 pt \"H\", copied, then edited M → H | main | this PR | Photoshop 27.11 (reporter) |\n|---|---|---|---|\n| 72 → 300 ppi, ink after the edit | about 4.2× larger (58 × 73 vs 14 × 18 here) | same as the source | same as the source |\n| 300 → 72 ppi | about 4.2× smaller | same as the source | same as the source |\n\n### Anything a reviewer should look at first\n\n- The Character panel shows the original point size (24 pt) on the copy, with the scale in its transform, as after an Image Size resolution change. Photoshop shows the adapted size (5.76 pt). Rewriting the point size instead would be a separate change touching every type run, so I kept to the existing mechanism.\n\n### Tests\n\n- `layer_copy_cmds::tests::copied_type_keeps_its_pixel_size_across_resolutions_after_an_edit`: 72→300 and 300→72 ppi, copy, then edit \"H\" → \"M\" → \"H\"; the bounds match the source's. Without the change it fails 72→300 with a 58 × 73 box against 14 × 18.\n- `cargo fmt --check`; clippy `-D warnings` for engine; `cargo test -p photocraft-engine`; `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "46512521fec2b4b462c29d9fefd16398cb81212c",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/layer_copy_cmds.rs"
},
{
"path": "crates/engine/src/layer_copy_cmds/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256129/job/121379968915"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256170/job/121379969090"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256170/job/121380012964"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256129/job/121379968961"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256170/job/121380013531"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256129/job/121379968904"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256129/job/121379968759"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256170/job/121380012022"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256129/job/121379970330"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256129/job/121380105584"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40263256129/job/121380104168"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T05:49:21Z",
"url": "https://github.com/storytold/photocraft/pull/9594#pullrequestreview-5647890395",
"body": "Code read (diff only). Fixes #9560: copying layers between documents of different resolutions now calls `canvas_geom::set_resolution(copies, dest.resolution_dpi)` in `adapt_copies`, so type keeps its pixel size and a later text edit re-lays it out at the destination's resolution without it growing or shrinking. The test copies a type layer 72 to 300 and 300 to 72 ppi and checks the pixel size right after the copy and after two edits. I did not check what `set_resolution` does to non-type layers beyond reading the call. No defect found. Tests not run, app not run.\n",
"commit": {
"oid": "46512521fec2b4b462c29d9fefd16398cb81212c"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Type and typography",
"new_test_names": [
"copied_type_keeps_its_pixel_size_across_resolutions_after_an_edit"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9594"
},
{
"number": 9590,
"title": "fix(io): PSD layer-effect contours keep at most 16 points",
"url": "https://github.com/storytold/photocraft/pull/9590",
"body": "Fixes #9556.\n\nPhotoCraft wrote every point of a layer-effect contour into the PSD's effects descriptor (`ShpC` › `Crv `). Photoshop reads at most 16. A longer contour makes it report \"A problem was encountered reading layer … because of a program error\" and load the layer without any effects. The issue lists six built-in contours longer than 16 points: Cone, Cone - Inverted, Ring, Ring - Double, Rolling Slope - Descending and Rounded Steps. Any effect using one of them (Bevel & Emboss gloss and contour, Satin, shadows, glows) lost the whole style in Photoshop.\n\n### Photoshop's behaviour\n\n- The reporter checked the limit in Photoshop 2026: 16 points load, while 17 and 27 fail and drop the effects.\n- I opened this branch's output in Photoshop 2025 (26.2) by script. The file was the reporter's command: a new 400×300 document, a filled rectangle, and Bevel & Emboss with the gloss contour Ring - Double. The layer loads with Bevel & Emboss, and its gloss contour reads back as \"Ring - Double\" with 16 points.\n- I didn't open main's 27-point file in Photoshop here, because the error dialog would block my scripted session. That row comes from the reporter.\n\n### What changes\n\n`crates/io/src/effects_map.rs`:\n- `contour_value` writes at most 16 points (`PSD_CONTOUR_POINTS`), chosen by `psd_contour_points`.\n - Both ends are kept, then, one at a time, the point farthest in output from the line between the kept points around it. Peaks and valleys survive, so Ring - Double keeps its two rings.\n - Corner flags follow their points.\n- Contours of up to 16 points are written as before.\n- Nothing changes in memory, in rendering or in `.pcraft`: only the PSD writer caps.\n\n### Verification\n\n| Bevel & Emboss, gloss contour Ring - Double, saved as PSD | main | this PR | Photoshop |\n|---|---|---|---|\n| contour points written | 27 | 16 | reads ≤ 16 |\n| opened in Photoshop | error, effects dropped (reporter, 2026) | loads with Bevel & Emboss (2025, scripted) | – |\n\n### Anything a reviewer should look at first\n\n- The reporter posted a diff for the same approach in the issue. This is my own implementation of it; the idea (farthest-point insertion) is a standard curve simplification.\n- The kept points are an approximation of the longer preset curve. A follow-up could make the six presets themselves 16 points or fewer, so PhotoCraft and Photoshop draw exactly the same curve.\n\n### Tests\n\n- `effects_map::tests::psd_contours_keep_at_most_16_points_and_their_peaks`:\n - a 29-point contour with two rings is written as 16 points, ends and both peaks kept, in order;\n - 16 points are written unchanged.\n - Without the cap it writes 29.\n- `cargo fmt --check`; clippy `-D warnings` for io; `cargo test -p photocraft-io`; `cargo xtask test-corpus`; `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "a9744ed44dc782613b19a97b8c159a091f73e669",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/io/src/effects_map.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228650/job/121382972411"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228772/job/121382972686"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228772/job/121383007196"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228650/job/121382972443"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228772/job/121383007536"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228650/job/121382972433"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228650/job/121382972229"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228772/job/121383006651"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228650/job/121382973087"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228650/job/121383092236"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228650/job/121383092220"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40264228650/job/121383092191"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T05:54:47Z",
"url": "https://github.com/storytold/photocraft/pull/9590#pullrequestreview-5647923229",
"body": "Code read (diff and tests). Fixes #9556: a PSD layer-effect contour with more than 16 points makes Photoshop drop all of the layer's effects, so the export now writes at most 16. `psd_contour_points` keeps both ends and then adds, one at a time, the point whose output is farthest from the line between its two kept neighbours; indices stay in order via `partition_point`, and up to 16 points are written unchanged. Degenerate spans (equal inputs, non-finite values) are guarded, and a point is never chosen twice. The tests check a 29-point two-ring contour (16 points, both ends, both peaks, increasing inputs), a 16-point contour as is, and the CodeRabbit case of inputs a few ulps apart. I did not check the 16-point limit against Photoshop. No defect found. Tests not run, app not run.\n",
"commit": {
"oid": "a9744ed44dc782613b19a97b8c159a091f73e669"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"psd_contour_points",
"crv",
"psd_contours_keep_at_most_16_points_and_their_peaks",
"a_point_on_a_tiny_straight_span_is_not_a_bend"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9590"
},
{
"number": 9589,
"title": "fix(compose, gpu): an adjustment layer's This Layer range judges its result before Opacity",
"url": "https://github.com/storytold/photocraft/pull/9589",
"body": "Fixes #9542.\n\nAn adjustment layer has no pixels of its own, so its Blend If \"This Layer\" ranges have to judge something else. PhotoCraft used the composite after Opacity and the mask had already mixed the result into the backdrop. Photoshop judges the adjustment's result at full strength. So below 100 % Opacity, or under a partial mask, a range could hide an effect Photoshop keeps, or let through one it drops.\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2), by script on a synthetic 2×2 RGB 8-bit document filled with (64, 128, 192), with an Invert adjustment layer at 40 % Opacity on top. Read back from a flattened copy:\n\n| Blend If on the Invert layer | Photoshop |\n|---|---|\n| Red › This Layer, black point 150 | (115, 128, 140): kept, since Invert's red 191 passes 150 |\n| Red › This Layer, white point 150 | (64, 128, 192): dropped |\n| none (control) | (115, 128, 140) |\n\nThese match the reporter's Photoshop 27.11 numbers.\n\n### What changes\n\n- **`compose::apply_blend_if`:** for an adjustment layer, This Layer is the adjustment applied to the backdrop (the `before` buffer), before Opacity and the mask mix it in. It is also taken before the layer's blend mode; I measured Normal only, so that part is an assumption.\n- **`gpu` planner `blend_if`:** passes the same unmixed result as C, from an extra `adjust` pass on the retained backdrop, instead of judging B (the mixed composite).\n - The shader already reads C when `p4.y` is clear; only its comment changes.\n\nUnderlying Layer, pixel layers and groups are unchanged.\n\n### Verification\n\n| Invert at 40 % over (64, 128, 192) | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| Red This Layer black 150 | (64, 128, 192): dropped | (115, 128, 140) | (115, 128, 140) |\n| Red This Layer white 150 | (115, 128, 140): leaked | (64, 128, 192) | (64, 128, 192) |\n\n### Anything a reviewer should look at first\n\n- Clipped layers above an adjustment no longer count towards its This Layer value: the judgement is the adjustment's own result. The reporter's follow-up says partial masks behave like Opacity here, and both are now outside the judgement.\n- The GPU adds one adjustment pass for an adjustment layer that has Blend If. Others are unaffected.\n\n### Tests\n\n- `compose::tests::adjustment_blend_if_judges_the_result_before_opacity`: both rows above. On main it fails with (64, 128, 192) for the black point, the reporter's output.\n- `gpu` `parity::blend_if`: its adjustment case now runs at 40 % Opacity. Without the planner change it fails by 94 levels (U8 gray this hard).\n- `cargo fmt --check`; clippy `-D warnings` for compose and gpu; `cargo test -p photocraft-compose -p photocraft-gpu`; `cargo xtask test-corpus`; `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "da157945077190e550b53a5e14b0536f62761292",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/compose/src/tests.rs"
},
{
"path": "crates/gpu/src/compose.wgsl"
},
{
"path": "crates/gpu/src/plan.rs"
},
{
"path": "crates/gpu/tests/it/parity.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901845/job/121372862516"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901941/job/121372862767"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901941/job/121372898456"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901845/job/121372862581"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901941/job/121372898687"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901845/job/121372862465"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901845/job/121372862289"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901941/job/121372897444"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901845/job/121372863455"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901845/job/121372979334"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901845/job/121372979231"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40260901845/job/121372979302"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T03:32:19Z",
"url": "https://github.com/storytold/photocraft/pull/9589#pullrequestreview-5647036365",
"body": "Code read (diff only; tests not run, app not run, no side-by-side with Photoshop).\n\nFixes #9542: an adjustment layer's Blend If \"This Layer\" range now judges the adjustment result at full strength, before Opacity and mask, instead of the already-mixed result.\n\n- CPU path (`apply_blend_if`): clones `before` and applies the adjustment, so `own` is the unmixed result. Group depth handling is unchanged.\n- GPU planner (`blend_if`): retains `before`, runs `self.adjust` on it and passes it as `c`; `p4.y` (\"own is none\") is now always 0, so the shader reads `tex_c` as for other layers. The now-dead `p4.y` branch in the WGSL is harmless; only the comment changed.\n- Tests: a new CPU test with Invert at 40% checks both sides of the 150 threshold against the stated Photoshop values (115,128,140 and 64,128,192), and the GPU parity test now runs the adjustment at 0.4 opacity so CPU and GPU are compared on this case.\n\nNo defect found. Not verified: the quoted Photoshop numbers, and GPU output on real hardware.\n",
"commit": {
"oid": "da157945077190e550b53a5e14b0536f62761292"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"adjustment_blend_if_judges_the_result_before_opacity"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9589"
},
{
"number": 9584,
"title": "fix(compose, gpu): Blend If hard points match Photoshop at 16 bits and for Gray",
"url": "https://github.com/storytold/photocraft/pull/9584",
"body": "Fixes #9521. Fixes #9522.\n\nTwo Blend If precision fixes in the same few lines, from the same report bundle.\n\n## #9521: 16-bit values at a hard point\n\nA 16-bit pixel right at a hard Blend If point fell on the wrong side of it. `blend_if_level` read a channel as `x · 255` and snapped only within 1e-4 of a whole level. So RGB16 32895 read as level 127.996, below a hard black point at 128, and the layer was hidden where Photoshop shows it. The same happens at white points, one step past the level.\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2), by script on synthetic 2×2 16-bit RGB documents: a white layer over a grey background, with Blend If › Blue › Underlying Layer set to a hard black point (then a hard white point) at levels 1, 64, 128 and 200. I swept the background over Photoshop's 16-bit steps n (its 0–32768 scale, written as 2n in a 16-bit raw export) and read a flattened copy:\n\n| Cutoff level c | `c · 32768 / 255` | black point: first step shown | white point: last step shown |\n|---|---|---|---|\n| 1 | 128.50 | 129 | 129 |\n| 64 | 8224.13 | 8224 | 8224 |\n| 128 | 16448.25 | 16448 | 16448 |\n| 200 | 25700.39 | 25700 | 25700 |\n\nSo Photoshop compares at its 16-bit precision. A hard point at level `c` sits on step `round(c · 32768 / 255)`, and a value on that step passes both a black point and a white point there. RGB16 32895 is step 16448, level 128's step, which is why Photoshop keeps the layer the reporter describes.\n\n### What changes\n\n- **`compose::blend_if_level`:** a value whose step `round(x · 32768)` equals its nearest level's step `round(c · 32768 / 255)` is exactly that level; anything else stays `x · 255`.\n - Every 8-bit level is on its own step, so 8-bit behaviour is unchanged. That includes the \"a few millionths off\" drift the old 1e-4 snap was there for (`blend_if_hard_point_includes_its_own_level` still passes).\n- **`compose.wgsl` `blend_if_weight`:** the GPU snaps the same way.\n - 16- and 32-bit layers reach the GPU as half-float, so GPU hard points at those depths stay approximate, as the `blend_if` parity test already notes.\n\n### Verification (#9521)\n\n| RGB16 backdrop under a white layer, Underlying Blue black point 128 | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| 32895 (step 16448) | hidden: backdrop (32895…) | white | white |\n| 32894 (step 16447) | hidden | hidden | hidden |\n\nThe new test also checks every black and white cutoff in the table above, at the step and one past it.\n\n\n## #9522: Gray on mixed 8-bit colours\n\nBlend If's Gray used Rec. 601 luma, 0.299 R + 0.587 G + 0.114 B, unrounded. Photoshop's Gray differs in both weights and rounding. With the same scripted setup on an 8-bit document (a white layer over one backdrop colour, a hard Gray point on Underlying Layer), I binary-searched the first shown value of pure red, green and blue against black points at 20, 25 and 28:\n\n| Channel | black 20 | black 25 | black 28 |\n|---|---|---|---|\n| R | 65 hidden / 66 shown | 81 / 82 | 91 / 92 |\n| G | 33 / 34 | 41 / 42 | 46 / 47 |\n| B | 177 / 178 | 222 / 223 | 250 / 251 |\n\nEvery boundary fits Gray = (30 R + 59 G + 11 B) / 100, rounded half down. For example, R 65 → 19.5 → 19 is hidden and B 250 → 27.5 → 27 is hidden, while R 66 → 19.8 → 20 and B 251 → 27.61 → 28 are shown. So do the reporter's colours, which Photoshop keeps and PhotoCraft hid:\n\n| Backdrop colour, Gray point | Rec. 601 (main) | Photoshop's Gray | main | this PR | Photoshop 2025 |\n|---|---|---|---|---|---|\n| (123, 126, 151), black 128 | 127.95 → hidden | 127.85 → 128 | hidden | shown | shown |\n| (120, 129, 149), white 128 | 128.59 → hidden | 128.5 → 128 | hidden | shown | shown |\n| (121, 129, 149), white 128 (control) | 128.89 → hidden | 128.8 → 129 | hidden | hidden | hidden |\n\n**Change:** `compose::blend_if_gray`, with the same arithmetic in `compose.wgsl` `blend_if_weight`. On whole levels Gray is `⌊(30 R + 59 G + 11 B + 49) / 100⌋`; between levels (deeper documents) it is the unrounded `(30 R + 59 G + 11 B) / 100`.\n\n## Both\n\n### Anything a reviewer should look at first\n\n- 32-bit documents use the same rule, so a float within half a 16-bit step of a level now counts as that level. Before, it was within 1e-4. I haven't measured Photoshop's 32-bit Blend If.\n- Gray on 16- and 32-bit documents: only the weights are measured (on 8-bit). Values between levels keep the unrounded weighting, which is an assumption.\n\n### Tests\n\n- `compose::tests::blend_if_gray_follows_photoshops_weights_and_rounding`: the seven measured single-channel boundaries and the reporter's colours. With Rec. 601 it fails at the first case (red 66 under black 20).\n- `compose::tests::blend_if_16_bit_cutoffs_follow_photoshops_steps`: the four measured cutoffs, black and white, at the step and the next, plus the reporter's 32895/32894 boundary. On main it fails (\"white 1: step 129 shows\").\n- `cargo fmt --check`; clippy `-D warnings` for compose and gpu; `cargo test -p photocraft-compose -p photocraft-gpu` (including the `blend_if` parity test); `cargo xtask test-corpus`; `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "fc0f7b1132f799b5bba7be642c9047499d95a887",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/compose/src/tests.rs"
},
{
"path": "crates/gpu/src/compose.wgsl"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655121/job/121365948116"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655128/job/121365947892"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655128/job/121365989380"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655121/job/121365948021"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655128/job/121365989221"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655121/job/121365947863"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655121/job/121365948049"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655128/job/121365989059"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655121/job/121365948956"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655121/job/121366051780"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655121/job/121366051894"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40258655121/job/121366051814"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T03:41:38Z",
"url": "https://github.com/storytold/photocraft/pull/9584#pullrequestreview-5647082363",
"body": "Code read (diff only; tests not run, app not run, Photoshop measurements not reproduced).\n\nTwo Blend If precision fixes in `crates/compose` and the matching `compose.wgsl` code.\n\n- #9521, `blend_if_level`: a value counts as level `c` when it sits on the 16-bit step `round(c * 32768 / 255)`, instead of within 1e-4 of a whole level. I checked the example in the comment: RGB16 32895 gives step 16448, which is level 128's, and 32894 gives 16447, which is not. Every 8-bit level still lands on its own step, so the old drift absorption is kept. The WGSL uses the same expression.\n- #9522, `blend_if_gray`: (30 R + 59 G + 11 B) / 100, rounded half down on whole levels via `floor((sum + 49) / 100)`. Hand-checking the quoted cases (red 66 passes at 20, 65 does not; 127.85 and 128.5 both read 128; 128.8 reads 129) gives the stated results. The sum is an exact integer in f32, so the rounding is stable. The shader mirrors it with `all(v == round(v))` as the whole-level test.\n- CPU and GPU changes are consistent. Two new tests pin the step boundaries at levels 1, 64, 128 and 200 for both black and white points, plus the single-channel and mixed-colour gray cases.\n\nNo defect found. Not verified: the Photoshop numbers themselves, or GPU output on hardware.\n",
"commit": {
"oid": "fc0f7b1132f799b5bba7be642c9047499d95a887"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"blend_if_gray",
"blend_if_16_bit_cutoffs_follow_photoshops_steps",
"blend_if_gray_follows_photoshops_weights_and_rounding"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9584"
},
{
"number": 9523,
"title": "Fix Type on a Path wrapping and caret positioning across closed-path seams",
"url": "https://github.com/storytold/photocraft/pull/9523",
"body": "Closed-path text clips at the path's first knot even when unused circumference remains. Caret projection also needs to use the same wrapped text interval. Starting near the upper-left seam of an ellipse or the left side of a rectangle exposes this behaviour.\n\nWrap closed-path arc-length sampling and inverse caret projection across the seam. Limit glyph centres to one circuit, using aligned layout bounds to support centred and right-aligned text. Preserve open-path endpoint behaviour and current area-type routing.\n\nSynthetic regression coverage includes rectangle seams, reversed/flipped ellipse geometry, one-circuit clipping, alignment, open endpoints and inverse projection. A UI test checks actual caret indices 1, 4, 7 and 10 using an independent document-space rectangle oracle, including seam crossing.\n\nValidation against upstream b07b7fab559ce2f7c3ba7ef80b7fb50c3aad170e:\n\n- Baseline: three intended text assertion failures, one passing open-path control; corrected caret assertion returns 0 instead of 1. Both Cargo test commands exit 101 after compiling and executing tests.\n- Fixed: text library 232 passed/5 ignored; UI type suite 145 passed/2 ignored; zero failures. Focused corrected caret test passes separately and is included in the UI suite total.\n- Formatting, whitespace, Clippy with -D warnings, dependency layering (31 crates) and native application cargo check pass.\n\nWASM, real-file corpora and isolated GUI verification remain unperformed. Current upstream main and comprehensive competing-PR clearance require refresh before submission. The existing block v0.1.6 future-compatibility warning is nonfatal. This corrects existing Type on a Path functionality; preserve its original contributor attribution (PR #5180).\n\nThis is an independent correction to Type on a Path from #5180. It overlaps #5230; this submission will disclose its live disposition and does not ask maintainers to wait for it. No Work Path preview, component selection, component transforms or Shape dragging is included.\n\nThe submission source is based on `14a4ab9b3f38f0b68fde6add97384919710374c8`. Its three relevant baseline files are byte-identical to the validated package pin, and all four resulting changed files match that package. Recorded test results apply to the original package validation; no new builds or tests were run during publication preparation.\n\n\nSubmission check: upstream main `5c9192bbe2031aecc6023175b24d653795e30872`; prepared source `ca9a3dac54b2141e78c5ee2cb0b156cc98c4c275`.\n\nOverlap: https://github.com/storytold/photocraft/pull/5230 was checked at submission: state `open`, merged `false`. This is an independently validated seam/caret correction to existing Type on a Path (#5180), not a replacement claim for all work in #5230. The validated production patch is not already present verbatim on the checked main. Please compare overlapping hunks before merging. There is no dependency on the four Work Path/Shape contributions.\n\nCurrent upstream-main review diff: 4 files.\n\n<!-- photocraft-focused-submission:closed-path-typography:ca9a3dac54b2141e78c5ee2cb0b156cc98c4c275 -->",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "728782c595e768bdb6febc7f30e4d1456d13fd36",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/text/src/closed_path_tests.rs"
},
{
"path": "crates/text/src/render.rs"
},
{
"path": "crates/text/src/warp.rs"
},
{
"path": "crates/ui-egui/src/type_tool.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484369/job/121311020822"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484278/job/121311020677"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484369/job/121311021239"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484369/job/121311021120"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484369/job/121311021063"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484369/job/121311022330"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484369/job/121311185160"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484369/job/121311185249"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40240484369/job/121311185172"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T23:36:04Z",
"url": "https://github.com/storytold/photocraft/pull/9523#pullrequestreview-5645920841",
"body": "Approving at 728782c. The gate, SonarCloud and every other job pass at this head, and none fails.\n\nI read the delta since my approval at 6003753 (one commit, 728782c). The new `Warp::path_text_x_near_text` takes the caret hit from `path_text_x` and, on a closed path, when the hit lies past the end of the text (in the unused gap of the lap), picks the nearer endpoint along the path: the previous lap's position (`offset - length`) if it is closer to the text start than the hit is to the text end, else the hit as before. Open paths and hits inside the text are unchanged. `offset_at` in type_tool.rs now calls it with the layout's right bound as `text_end`.\n\nThe tests use an independent perimeter oracle for the rectangle and cover a hit just after the last caret and just before the first caret for left, centre and right alignment, plus an open-path test that keeps the endpoint clamping. The doc comment nit from my last review is fixed too (the TYPE-218-1 comment is back above its own test).\n\nLimits: text on a path was not rendered or clicked in the app, only the logic and the unit tests were read. I read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "728782c595e768bdb6febc7f30e4d1456d13fd36"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Type and typography",
"new_test_names": [
"ellipse",
"centre",
"closed_rectangle_left_edge_continues_past_the_top_left_seam",
"closed_ellipse_upper_left_and_apex_cross_the_seam_in_both_directions",
"closed_path_shows_one_lap_without_repeating_glyph_centres",
"open_path_clipping_offsets_and_end_tangents_stay_unchanged",
"closed_path_alignment_uses_one_lap_from_the_text_left_edge",
"closed_path_start_offsets_wrap_and_invalid_coordinates_are_hidden",
"caret_projection_roundtrips_closed_seams_and_open_offsets",
"closed_path_endpoint_test_point",
"closed_path_forward_hit_after_last_caret",
"open_path_hits_keep_endpoint_clamping",
"closed_path_backward_hit_before_first_caret",
"closed_path_upstream_caret_rectangle_left_edge_roundtrip",
"closed_path_aligned_selection_highlights_every_visible_cluster"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9523"
},
{
"number": 9517,
"title": "fix(ui): Layer Style scrolls on a short window so OK stays on screen",
"url": "https://github.com/storytold/photocraft/pull/9517",
"body": "Fixes #9515.\n\nThe Layer Style dialog grows to fit its page, and Bevel & Emboss has become tall: Structure, Shading, the highlight and shadow modes, colours and opacities, then Contour and Range. On a window under about 1000 pt high the dialog ran off the bottom of the window, and OK, Cancel and the lower controls couldn't be reached. The report shows it at about 1435 × 858 on Linux Mint; it isn't platform-specific.\n\n### Photoshop's behaviour\n\nPhotoshop's Layer Style dialog keeps OK and Cancel reachable on every page: they sit at the top of its right-hand column, and the dialog fits the screen. The target here is the same: whatever the page and window height, the dialog's buttons stay on screen.\n\n### What changes\n\n`dialogs.rs`: the Layer Style body sits in a vertical `ScrollArea` capped at the window height minus the dialog chrome. This is how long filter dialogs (Flame, Lighting Effects) already keep their buttons visible (`DIALOG_CHROME`). On a tall window nothing changes, since the cap is never reached.\n\n### Verification\n\nLive app at 1435 × 858 (control channel `ui.resize`), Layer › Layer Style › Bevel & Emboss…:\n\n| | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| OK / Cancel | below the window (unreachable) | on screen, the page scrolls | always reachable |\n\n### Anything a reviewer should look at first\n\n- The whole body scrolls, effect list included, because the list and the page share one row. The Styles page keeps its own inner 340 pt preset list.\n- Photoshop puts OK and Cancel in the right-hand column rather than under the page. I kept PhotoCraft's layout and only made it fit.\n\n### Tests\n\n- `dialogs::tests::layer_style_buttons_stay_on_a_short_window`: a 1435 × 858 harness opens Bevel & Emboss, and OK and Cancel end inside the window. On main it fails with OK at y 978–1006.\n- `cargo fmt --check`; clippy `-D warnings` for ui-egui; `cargo test -p photocraft-ui-egui` (3826 + 133); `cargo xtask layers`; `cargo xtask wasm`. All pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "81b434c6149732eadc94e0465fea81bd075facd4",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/dialogs.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223953930/job/121261285257"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223954026/job/121261285365"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223953930/job/121261285372"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223953930/job/121261285270"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223953930/job/121261284936"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223953930/job/121261286084"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223953930/job/121261358664"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223953930/job/121261358695"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40223953930/job/121261358671"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T19:39:44Z",
"url": "https://github.com/storytold/photocraft/pull/9517#pullrequestreview-5644911791",
"body": "Approving at 81b434c. The gate and SonarCloud pass at this head.\n\nThe Layer Style dialog body is wrapped in a vertical `ScrollArea` with `max_height` set to the window height minus `DIALOG_CHROME` (floored at 120), with a stable `id_salt`, so OK and Cancel stay on screen when Bevel & Emboss is taller than the window. The new kittest test opens Bevel & Emboss in a 1435 x 858 window and asserts both buttons end above the window bottom.\n\nLimits: I did not see the dialog rendered, so the look of the scroll bar and whether the page feels right at other sizes is unchecked.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "81b434c6149732eadc94e0465fea81bd075facd4"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"layer_style_buttons_stay_on_a_short_window"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9517"
},
{
"number": 9512,
"title": "fix(compose, gpu): adjustment layers apply Fill inside the special eight blend modes",
"url": "https://github.com/storytold/photocraft/pull/9512",
"body": "Fixes #9501.\n\nAn adjustment layer below 100 % Fill in one of Photoshop's \"special eight\" blend modes faded its blend by Fill, as if Fill were Opacity. The eight are Color Burn, Linear Burn, Color Dodge, Linear Dodge, Vivid Light, Linear Light, Hard Mix and Difference. In those modes Photoshop applies Fill *inside* the blend: the result is pulled toward the mode's neutral colour, then blended at Opacity. PhotoCraft already does this for pixel layers (`psblend::composite_fill`). The adjustment branch multiplied Fill into the coverage on both the CPU (`compose::blend_back`) and the GPU (`AdjMix`).\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2), by script on a synthetic 4×4 RGB 8-bit document filled with (64, 128, 192). On top, a Levels adjustment layer with Output 128–128, i.e. a constant 0.5. Read back from a flattened copy:\n\n| Levels layer | Photoshop |\n|---|---|\n| Color Dodge, Fill 40 % | (80, 160, 240) |\n| Linear Burn, Fill 40 % | (13, 77, 141) |\n| Normal, Fill 40 % (control) | (90, 128, 166) |\n| Color Dodge, Fill 100 % (control) | (129, 255, 255) |\n\nThese match the reporter's Photoshop 27.11 numbers for a constant-0.5 Color Lookup. They also match the pixel-layer rule: 64 / (1 − 0.4·0.5) = 80, and 64 + (1 − 0.6·0.5) − 1 → 13.\n\n### What changes\n\n- **`compose` (CPU):**\n - With Fill < 100 % in a special-eight mode, the adjustment branch blends with `psblend::blend_rgb_fill(mode, backdrop, result, fill)`, at coverage Opacity × mask.\n - Every other mode, or Fill 100 %, is unchanged (Fill × Opacity as coverage).\n- **`gpu`:**\n - The planner passes Opacity and Fill separately to `AdjMix` (`adjustment_fill`).\n - `fs_adjmix` pulls the result toward the neutral colour, as `composite_fill` does. It keeps a single `blend_rgb` call, per the shader's note on WARP compile time.\n\n### Verification\n\n`photocraft-cli run` on a new 4×4 document: fill `#4080c0`, a Levels layer with output 128–128, then `layer.setProps {blend, fill: 0.4}`, then `document.pixel`:\n\n| | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| Color Dodge, Fill 40 % | (90, 179, 217) | (80, 160, 240) | (80, 160, 240) |\n| Linear Burn, Fill 40 % | (38, 77, 141) | (13, 77, 141) | (13, 77, 141) |\n\n### Anything a reviewer should look at first\n\n- This touches `blend_back`, which my #9509 (#9483, the f32 cancellation over huge HDR values) also changes, on the line next to this one. Whichever merges second needs a one-line rebase, and I'll do it.\n- Dissolve, the adjustment layer's own clipped layers and the Fill = 0 early-out keep their current paths.\n\n### Tests\n\n- `compose::tests::adjustment_fill_changes_the_special_blend_modes`: Color Dodge and Linear Burn at 40 % Fill give Photoshop's values within one level. Without the change it gives (90, 179, 217), the reporter's PhotoCraft output.\n- `gpu` `parity::special_eight_fill_on_adjustment_layers_matches_cpu`: all eight modes at Fill 30 % and 60 %, Opacity 70 %, over an opaque noise backdrop. Without the GPU change it fails by 53 levels (Color Burn).\n- `cargo fmt --check`; clippy `-D warnings` for compose and gpu; `cargo test -p photocraft-compose -p photocraft-gpu`; `cargo xtask test-corpus`; `cargo xtask layers`; `cargo xtask wasm`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "6c55561128caf5ac917eb3d174e8e7bb4bd48e23",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/compose/src/tests.rs"
},
{
"path": "crates/gpu/src/compose.wgsl"
},
{
"path": "crates/gpu/src/plan.rs"
},
{
"path": "crates/gpu/tests/it/parity.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971783/job/121258655629"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971736/job/121258343115"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971783/job/121258655669"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971783/job/121258655719"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971783/job/121258655469"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971783/job/121258656464"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971783/job/121258750411"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971783/job/121258750410"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222971783/job/121258750520"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T19:23:44Z",
"url": "https://github.com/storytold/photocraft/pull/9512#pullrequestreview-5644863033",
"body": "Approving at 6c55561. Gate (ubuntu), SonarCloud and the Windows jobs are green at this head. I read the non-test source in `compose/src/lib.rs`, `gpu/src/compose.wgsl` and `gpu/src/plan.rs`; the tests (including the GPU parity test) were skimmed for what they cover.\n\n- Fill on an adjustment layer. Below 100% Fill, one of the eight blend modes whose result Photoshop pulls toward a neutral colour (`psblend::fill_is_special`) now blends a result pulled toward that neutral colour, with Opacity as the coverage, as a pixel layer does (#9501); for every other mode Fill stays part of the coverage. On the CPU this is `blend_back(.., mode, fill)` calling `blend_rgb_fill` for non-Normal modes, with `(k, fill)` chosen once per layer; Normal mode is unchanged.\n- The GPU path repeats the rule: `adjustment_fill` in `plan.rs` feeds the pass `opacity` and `fill`, and `fs_adjmix` in the shader moves the result toward the mode's neutral (1.0 for the two modes coded 5 and 6, 0.5 for 16 and 17) and uses the Hard Mix formula for mode 19, as the comment says. The delta commit makes a clipped adjustment keep Fill as coverage in every mode, matching the CPU `composite_atop` branch.\n- The test checks Color Dodge and Linear Burn at 40% Fill over a known pixel against values the author attributes to Photoshop (80,160,240 and 13,77,141).\n\nNote: PR 9509 (approved) also changes `blend_back` in the same file, so one of the two will need a rebase; I did not check how they combine. Whether the shader's neutral values and mode codes match the CPU for all eight modes I confirmed only for the modes the shader names.\n\nLimits: the Photoshop reference values are the author's and I did not compare against Photoshop. I did not run the GPU path or the app.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "6c55561128caf5ac917eb3d174e8e7bb4bd48e23"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"blend_back",
"adjustment_fill_changes_the_special_blend_modes",
"adjustment_fill",
"special_eight_fill_on_adjustment_layers_matches_cpu"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9512"
},
{
"number": 9509,
"title": "fix: an edit at full coverage keeps its result over huge 32-bit values",
"url": "https://github.com/storytold/photocraft/pull/9509",
"body": "Fixes #9483.\n\nIn a 32-bit document, a Color Lookup (or any edit) at full strength over very large pixel values could lose its result. A constant-0.1 look over 2²⁰ gave 0.125, and a 0.5 look over 1e8 gave 0. Every blend back to the original pixel computed `o + (r − o)·k`. In f32, `r − o` has already rounded away a result that small against `o`, and adding `o` back can't recover it.\n\n### Photoshop's behaviour\n\nI didn't reproduce this in Photoshop: loading a .cube into a 32-bit Color Lookup by script needs descriptor keys I couldn't confirm. The reporter ran Photoshop 27.11 on this case: at 100 % the bounded LUT output is kept for inputs of 1, 1,000,000 and 100,000,000 (≈ (0.1, 0.9, 0.4) for their cube; 0.5000076 for a constant look). The expectation is that full coverage gives the edit's result, whatever the backdrop.\n\n### What changes\n\nThe three blends that mix an edit back over the original now compute `o·(1−k) + r·k`:\n- `photocraft-compose` `blend_back`: adjustment layers.\n- `photocraft-engine` `pixels::mix_through_selection`: destructive edits, with or without a selection.\n- `photocraft-plugins` `apply`: plug-in filters through a selection, which must stay bit-identical to the built-in route (`example_invert_matches_builtin_invert…` checks that).\n\nAt k = 1 this is exactly `r`. At k = 0 it is exactly `o`, and k = 0 pixels are skipped anyway. In between, it differs from the old form only in the last bit.\n\n### Verification\n\n`photocraft-cli run` on a new 4×4 32-bit document: fill white, then Exposure +20 (pixels 1,048,576), then Color Lookup with an invented constant-0.1 cube, then `document.pixel`:\n\n| | main | this PR | Photoshop 27.11 (reporter) |\n|---|---|---|---|\n| Image › Adjustments › Color Lookup | 0.125 | 0.1 | LUT output kept |\n| Color Lookup adjustment layer | 0.125 | 0.1 | LUT output kept |\n\n### Anything a reviewer should look at first\n\n- Two bit-exact oracles change with the formula: `reused_render_matches_previous_allocating_stack`'s reference stack, and `mix_through_selection`'s selection test. Both now spell the new formula; their tolerances are unchanged.\n- Not covered here: a few other commands mix a result back over the original with their own `o + (r − o)·k` (the mixes in `lens_cmds`, `distort_cmds`, `retouch_cmds`, `extra_cmds` and `edit_menu_cmds`). Over huge 32-bit values they could lose a small result the same way. I kept this PR to the adjustment routes the issue measures. Moving those to a shared mix helper would be a follow-up.\n\n### Tests\n\n- `compose::tests::full_coverage_adjustment_keeps_its_result_over_huge_hdr_values`: a 0.5 look over 1e6, 1e8 and −1e6 composites to 0.5. It gives 0 at 1e8 without the fix.\n- `pixels::tests::full_coverage_keeps_the_edit_over_huge_hdr_values`: a destructive mix keeps 0.5 over 1e8, 0.1 over 1e6 and 0.9 over −1e6. It fails without the fix.\n- `cargo fmt --check`; clippy `-D warnings` for compose and engine (all targets) and plugins (lib); `cargo test -p photocraft-compose -p photocraft-engine`; `cargo test -p photocraft-plugins --lib`; `cargo xtask test-corpus`; `cargo xtask layers`; `cargo xtask wasm`.\n - The plug-ins crate's `abi` test target doesn't build on macOS (`rustix::fs::mknodat`), on main too, so CI covers it.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "d78d7493d3f0b6e760c88de969ac810e9bc5d910",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/compose/src/tests.rs"
},
{
"path": "crates/engine/src/pixels.rs"
},
{
"path": "crates/engine/src/pixels_tests.rs"
},
{
"path": "crates/plugins/src/apply.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40219404620/job/121247486491"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40219404647/job/121247486246"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40219404620/job/121247486689"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40219404620/job/121247486703"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40219404620/job/121247486704"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40219404620/job/121247487854"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40219404620/job/121247586490"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40219404620/job/121247585151"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T18:46:42Z",
"url": "https://github.com/storytold/photocraft/pull/9509#pullrequestreview-5644707748",
"body": "Approving at d78d749. Gate (ubuntu) and SonarCloud are green at this head; the Windows jobs are skipped. The change is small and I read all of the non-test source (`compose/src/lib.rs`, `engine/src/pixels.rs`, `plugins/src/apply.rs`) and the tests.\n\n- Blending an edit back over its original used `o + (a - o) * k`; over a huge 32-bit value the difference rounds the small result away, so full coverage over 1e8 gave 0 instead of 0.5 (#9483). The three places that mix by coverage (adjustment layers in `blend_back`, `mix_through_selection`, and plugin application) now compute `o * (1 - k) + a * k`, which is exact at k = 1 and k = 0 and equal to the old form up to rounding in between. The reference stack in the existing reuse test and the expected value in the selection test were changed to the same form.\n- New tests cover a Color Lookup over 1e6, 1e8 and -1e6 backdrops and the selection mix over huge values.\n\nNote: PR 9512 changes `blend_back` as well (Fill for special blend modes), so the two will need a rebase on each other; I did not check how they combine. The GPU path (`compose.wgsl`) still uses the old form for the final mix, which is outside this PR and I did not check whether it has the same problem.\n\nLimits: I did not build or run the app. The results over HDR data are as the tests state; nothing was observed by me.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "d78d7493d3f0b6e760c88de969ac810e9bc5d910"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"full_coverage_adjustment_keeps_its_result_over_huge_hdr_values",
"full_coverage_keeps_the_edit_over_huge_hdr_values"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9509"
},
{
"number": 9504,
"title": "fix(ui): Keyboard Shortcuts records ⌘Z instead of undoing the capture",
"url": "https://github.com/storytold/photocraft/pull/9504",
"body": "Fixes #9487.\n\nIn Edit › Keyboard Shortcuts, pressing ⌘Z (Ctrl+Z on Windows and Linux) in a shortcut field never recorded it. The field went back to its old shortcut and stopped waiting for keys. Every other key worked.\n\nCause: the dialog keeps its own ⌘Z / ⇧⌘Z history (#4697, `edit_history::keys`). Clicking the field to start the capture is one step of that history, so ⌘Z undid that click before the field could read the key.\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2) on macOS, with a new 200×100 document: Edit › Keyboard Shortcuts, click the empty shortcut of File › Open as Smart Object…, press ⌘Z. The field shows **⌘+Z** with a warning: \"⌘+Z is already in use and will be removed from Edit > Undo if accepted.\" I cancelled the dialog afterwards, so nothing was saved.\n\n### What changes\n\n`crates/ui-egui/src/edit_history.rs`:\n- While the front dialog has a field waiting for keys (`capture: true`), ⌘Z / ⇧⌘Z are left to that field, which records them like any other key.\n- Outside a capture, ⌘Z still steps the dialog back. A step no longer restores the \"waiting for keys\" state of the click that started a capture, so the next key isn't silently captured.\n\n### Verification\n\nLive app (control channel, scratch settings folder): Edit › Keyboard Shortcuts, click File › Open…'s shortcut, press ⌘Z.\n\n| | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| ⌘Z in a waiting shortcut field | capture ends, Open keeps its old key | Open gets ⌘Z, with \"⌘Z is already in use by Edit › Undo and will be removed from it when you click OK.\" | ⌘+Z recorded, \"already in use and will be removed from Edit > Undo if accepted\" |\n| ⌘Z again (not capturing) | – | the change is undone, the field isn't waiting for keys | – (Photoshop's dialog has an Undo button) |\n\n### Anything a reviewer should look at first\n\n- The guard reads the dialog field `capture`, which only the shortcut dialogs set. Other parameter dialogs keep their ⌘Z history unchanged.\n- Rebinding Undo itself (the reporter sets ⌘Z to Toggle Last State and ⇧⌘Z to Undo) works through the same capture. The usual conflict message tells the user that Undo loses ⌘Z.\n\n### Tests\n\n- `prefs_ui::shortcut_capture_tests::cmd_z_is_recorded_while_capturing_and_undoes_otherwise`:\n - ⌘Z captured on Edit › Search is stored as `Cmd+Z`;\n - a following ⌘Z undoes it and leaves capture off.\n - It fails without the guard (nothing recorded) and without the capture reset (\"doesn't wait for keys again\").\n- `cargo fmt --check`; clippy `-D warnings` for ui-egui; `cargo test -p photocraft-ui-egui` (3826 + 133); `cargo xtask layers`; `cargo xtask wasm`. All pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "993c9ed167c6acd9bc8f87c6cff6e2c7abba17a8",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/edit_history.rs"
},
{
"path": "crates/ui-egui/src/shortcut_capture_tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874305/job/121245880324"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874413/job/121245880436"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874305/job/121245880326"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874305/job/121245880133"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874305/job/121245880299"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874305/job/121245881072"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874305/job/121245978266"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874305/job/121245978227"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40218874305/job/121245978401"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T18:30:54Z",
"url": "https://github.com/storytold/photocraft/pull/9504#pullrequestreview-5644657832",
"body": "Approving at 993c9ed. Gate (ubuntu) and SonarCloud are green at this head. The source change is in `edit_history.rs` and I read it with the new test.\n\n- While a Keyboard Shortcuts field is waiting for keys (`capture` is true), the dialog edit history no longer handles Cmd+Z, so the key is recorded as a shortcut rather than stepping the dialog back and undoing the click that started the capture (#9487).\n- Outside a capture, Cmd+Z steps back as before. When a step restores a state that had a capture in progress, the code clears `capture` and `captureMods` and sets the history `settled` snapshot to match, so clearing it is not recorded as a new edit and Redo is kept. That is the reviewer point from CodeRabbit the test cites.\n- The test records Cmd+Z as a shortcut, undoes it, checks the field does not wait for keys again, and redoes it with Shift+Cmd+Z.\n\nLimits: this is keyboard handling in a dialog; I did not run the app or press the keys on macOS, and I did not check other dialogs that use the same history for a `capture` field.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "993c9ed167c6acd9bc8f87c6cff6e2c7abba17a8"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"cmd_z_is_recorded_while_capturing_and_undoes_otherwise"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9504"
},
{
"number": 9493,
"title": "fix(vector): a mask feather continues the mask past the canvas edge",
"url": "https://github.com/storytold/photocraft/pull/9493",
"body": "Fixes #9475.\n\nA feathered layer mask came out too bright where the canvas cut the feather off. The result also changed when the canvas was taller with the same mask. The blur (`photocraft_vector::feather::gaussian`, shared by pixel masks, vector masks and soft shape layers) clamped **each** of its three box passes at the plane's edge. The plane stops at the canvas, so passes 2 and 3 repeated already-blurred values past it.\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2) on macOS, driven by script on synthetic 256×128 RGB 8-bit documents: black background, a white layer, a mask from a rectangular selection with no anti-aliasing (Reveal Selection), then the mask's Feather set in Properties.\n- Mask rectangle x 64–191, y 32–95, Feather 32 px: the canvas edges stay dark, as if the black mask continued past the canvas.\n- Mask white over the left half and the full height, Feather 32 px: every row is identical, so the top and bottom edges don't darken. The mask continues past the edge with its edge values (white there).\n\nThe new blur pads the plane with its edge values by the passes' reach (`3 × (r + 1)`), blurs, and keeps the original area. Both cases come out as Photoshop's.\n\n### Verification\n\nSame steps with `photocraft-cli run` (main = upstream 528491f9a), compared pixel by pixel with Photoshop's output:\n\n| Feather 32 px | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| interior rectangle, max difference from Photoshop | 12 levels (bottom edge, 53 vs 41) | 1 level | – |\n| same mask on a 256×384 canvas, centre crop vs the 256×128 result | 12 levels apart | identical | identical (#9475) |\n| mask white up to the edge, max difference from Photoshop | 1 | 1 | rows identical |\n\nWith the plane's edge inside constant mask, padding changes nothing: interior feathers of 1–4 px are byte-identical to main.\n\n### Anything a reviewer should look at first\n\n- **#9476 is not fixed here.** Small feathers (1 px is 8 levels off at the rectangle's corner, 2 px is 4 levels off) are a separate kernel-shape difference. From 4 px up, main's three box passes already match Photoshop within a level. I measured 0.5–8 px in Photoshop, and below about 3 px neither box passes nor a Gaussian of variance f² fits within a level. So I left that one alone rather than tune a constant.\n- Memory: the edge continuation is done line by line (each row, then each column, padded by `3 × (r + 1)` on its own), so it needs only per-line buffers, not a padded copy of the plane. If a line's buffers can't be reserved, those lines are left unblurred rather than aborting. `padding_each_line_matches_padding_the_plane` checks that this is bitwise identical to padding the whole plane.\n- `parallel_gaussian_matches_the_sequential_reference` now checks the parallel and sequential blurs agree both without and with the edge padding.\n\n### Tests\n\n- `masks::tests::feather_at_the_canvas_edge_continues_the_mask_past_it`:\n - a 32 px feathered rectangle gives the same values on a taller canvas;\n - a mask white up to the edge keeps every row equal and the edge white.\n - It fails without the padding (0.093 apart, about 24 levels).\n- `soft_shape_matches_an_equal_vector_mask` gains a 40 px case that reaches the canvas edge.\n- `cargo fmt --check`; clippy `-D warnings` for vector and compose; `cargo test -p photocraft-vector -p photocraft-compose`; `cargo xtask test-corpus`; `cargo xtask layers`; `cargo xtask wasm`. All pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "61a413d8df9e90ee545eb08e57c5615aa2b29cea",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/masks.rs"
},
{
"path": "crates/compose/src/soft_shape_tests.rs"
},
{
"path": "crates/vector/src/feather.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215343640/job/121235244416"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215343638/job/121235155593"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215343640/job/121235244554"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215343640/job/121235244522"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215343640/job/121235244256"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215343640/job/121235245072"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215343640/job/121235323207"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215343640/job/121235321509"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T18:35:43Z",
"url": "https://github.com/storytold/photocraft/pull/9493#pullrequestreview-5644672718",
"body": "Approving at 61a413d. Gate (ubuntu) and SonarCloud are green at this head. I read the non-test source in `crates/vector/src/feather.rs` and the new mask test in `crates/compose/src/masks.rs`; docs were skimmed.\n\n- The mask blur now continues each line past its ends with its edge values, so a feather that reaches the canvas edge sees the mask carrying on, as the author reports Photoshop does (#9475), rather than clamping every box pass at the edge, which brightened the feather there.\n- It does this per line instead of padding the whole plane: `blur_padded_line` copies a line into a padded buffer (halo = `3(r+1)` on each side, enough for three passes), runs the three box passes, and copies the middle back. The row pass then the column pass on padded lines gives what padding the plane would, and the test `padding_each_line_matches_padding_the_plane` checks exactly that against a real padded plane (equality, not a tolerance).\n- Memory: buffers are reserved up front with `try_reserve_exact` (per worker, with overflow-checked sizes) before the input is touched, so an allocation failure falls back to the sequential path, which also reserves first and leaves the plane unchanged on failure. A test checks the largest buffer stays within the plane size at the maximum feather, and the parallel and sequential paths are compared for both halo 0 and the real halo.\n- The test hooks (`LARGEST_BUFFER`, `FAIL_LINE_BUFFERS`) are `cfg(test)` only.\n\nLimits: Photoshop parity is the author's claim; I did not compare against Photoshop output. I did not build or run the app, so timing on large masks and the look of a feathered edge are unobserved by me.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "61a413d8df9e90ee545eb08e57c5615aa2b29cea"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"feather_at_the_canvas_edge_continues_the_mask_past_it",
"note_buffer",
"blur_rows_chunk",
"line_buffers",
"blur_padded_line",
"row_workers",
"worker_buffers",
"blur_rows",
"blur_plane",
"gaussian_sequential",
"padding_each_line_matches_padding_the_plane",
"a_maximum_feather_does_not_pad_the_whole_plane",
"a_failed_line_buffer_never_leaves_a_half_blurred_mask"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9493"
},
{
"number": 9481,
"title": "fix(ui): new layer effects start from the Make Default values",
"url": "https://github.com/storytold/photocraft/pull/9481",
"body": "Fixes #9477.\n\nMake Default in the Layer Style dialog stored the page's values (`layer.layerStyle.makeDefault`), and Reset to Default read them back. But a **new** effect always started from the factory defaults, so the saved default never appeared. For example: set a Stroke's Size, click Make Default, Cancel, and open Layer Style › Stroke… again. The stroke is back to 1 px Inside.\n\n### Photoshop's behaviour\n\nAfter Make Default, new effects of that kind start from the saved values. Reset to Default brings those values back too. I didn't click Make Default in the installed Photoshop 2025, because it changes that copy's stored preferences. The behaviour above comes from public sources: a [Photoshop Training Channel tip on the Make Default button](https://photoshoptrainingchannel.com/tips/make-default-button-layer-style-panel/) and an [Adobe community thread on resetting layer style defaults](https://community.adobe.com/t5/photoshop-ecosystem-discussions/how-to-reset-the-default-values-of-layer-styles/m-p/14477436).\n\n### What changes\n\nAll in `crates/ui-egui/src/layer_style.rs`:\n- `open` seeds the dialog's fields with the session's user defaults (`initial_fields_with`, `userDefaults`). `initial_fields` keeps its signature and passes an empty map.\n- `add_instance` and `fresh_params` lay a kind's user default over its factory defaults. Keys the default doesn't hold keep their factory values. All three ways of adding an effect go through `add_instance`: opening the dialog on an effect the layer doesn't have, the list's checkbox, and the \"+\" button.\n- Make Default also updates the dialog's copy, so a \"+\" in the same dialog starts from it.\n\n### Verification\n\nLive app with a scratch settings folder, on a new 300×200 document with an empty layer: Layer › Layer Style › Stroke…, Size 7, Make Default, Cancel, then Layer › Layer Style › Stroke… again.\n\n| | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| Stroke page after reopening | Size 1 (factory) | Size 7 (the made default) | the made default (public docs, see above) |\n| New Drop Shadow (no default made) | factory | factory | factory |\n\n### Anything a reviewer should look at first\n\n- The fix is in the dialog only. Engine commands that add effects with explicit params (`layer.layerStyle.stroke`, scripts, presets) are unchanged, so scripted results don't depend on a user's saved defaults.\n- Effects already on a layer keep their own values, as in Photoshop. Only new instances use the default.\n\n### Tests\n\n- `layer_style::tests::new_effects_start_from_the_made_default`:\n - saves a Stroke default (Size 7, Outside);\n - opens the dialog on Stroke and adds a second stroke with \"+\"; both start at 7 / Outside, and Opacity keeps the factory value;\n - a new Drop Shadow keeps the factory size.\n - It fails without the overlay (left `(1, \"inside\")`, right `(7, \"outside\")`).\n- `cargo fmt --check`; clippy `-D warnings` for ui-egui; `cargo test -p photocraft-ui-egui` (3826 + 133); `cargo xtask layers`; `cargo xtask wasm`. All pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "7937adff961027154d795b8905e821244ae4f929",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/layer_style.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287737/job/121219861263"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287676/job/121219861006"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287737/job/121219861412"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287737/job/121219861384"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287737/job/121219861463"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287737/job/121219862238"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287737/job/121219996380"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287737/job/121219996439"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40210287737/job/121219996382"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T18:03:55Z",
"url": "https://github.com/storytold/photocraft/pull/9481#pullrequestreview-5644538284",
"body": "Approving at 7937adf. Gate (ubuntu), SonarCloud, the build without HEIF and the other jobs that ran are green at this head. I read the whole source change (it is all in `layer_style.rs`) and the new test.\n\n- Layer Style › Make Default saved a default but a new effect instance still started from the factory values. The dialog now carries the saved user defaults (`userDefaults`, kind to params, read from `session.presets.layer_defaults` when it opens) and `fresh_params` lays the saved keys over the factory defaults for that kind before filling in the shared light angle. Keys the user did not save keep their factory value; other kinds are untouched.\n- Clicking Make Default also writes the displayed params into the dialog's own `userDefaults` (only when the engine command succeeded), so a + in the same dialog starts from them without reopening it.\n- `initial_fields` keeps its signature and delegates to the new `initial_fields_with`, so other callers are unchanged.\n- The test saves a stroke default (size 7, outside), opens the dialog, adds two strokes and a drop shadow, and checks both strokes start from the saved size and position with the factory opacity, and the shadow keeps the factory size.\n\nLimits: I did not build or run the app. The dialog was not seen, and the saved values were not checked across a restart.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "7937adff961027154d795b8905e821244ae4f929"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"fresh_params",
"new_effects_start_from_the_made_default"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9481"
},
{
"number": 9478,
"title": "fix(text): vertical type sets roman letters upright, one per em, as Photoshop does (#9470)",
"url": "https://github.com/storytold/photocraft/pull/9478",
"body": "Fixes #9470.\n\nVertical type laid Latin letters and digits on their side: the word was turned 90° and ran down the column, so \"VERTICAL\" read sideways. `layout.rs` documented that as \"Photoshop's default\". Photoshop's Vertical Type tool stacks roman letters **upright, one character per em** down the column.\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2) on macOS, on synthetic documents:\n- **Vertical Type tool** (full-screen control): click, type \"ABC\", commit. The letters stand upright, stacked. The layer's `textStyle.baselineDirection` reads **withStream**, and the saved PSD's style run has `/BaselineDirection 1`.\n- **Script**: a vertical \"VERTICAL\" layer in Arial at 36 px measures **26 × 278 px**, which is 7 em steps of 36 px plus a capital's height. Setting `baselineDirection` to **rotated** lays the word on its side, **28 × 174 px**, and the PSD then stores `/BaselineDirection 2`.\n- The document's default style sheet says `/BaselineDirection 2`. The tool overrides it with 1 in each run, so a run without its own value falls back to rotated.\n\n### What changes\n\n- **doc**: `CharStyle::rotate_roman` (false = upright, the default; `serde(default)`).\n- **text/psd**: `BaselineDirection` is read into `rotate_roman` (2 = rotated) through the usual style-sheet lookup, so a run without its own value gets the sheet's. It's written back as 1 or 2.\n- **text/layout**: in vertical type, roman characters (UAX #50 \"rotated\" class, whitespace aside) go through the existing upright path unless their run sets `rotate_roman`:\n - Each takes **one em** down the column. A second shaping pass gives each character the letter spacing that turns its shaped advance into the em, the same way tabs are already widened.\n - Pair kerning (`kern`) is off between the stacked letters.\n - CJK text is unchanged: it stands upright either way.\n- `type_styles_cmds`' perturbed style covers the new field.\n\n### Verification\n\n`photocraft-cli run --new 400×500 --cmd type.create {\"text\":\"VERTICAL\",\"orientation\":\"vertical\",\"font\":\"Arial\",\"size\":36}`, flattened, ink bounding box:\n\n| | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| default vertical type | 27 × 173 (sideways) | 25 × 277 (upright stack) | 26 × 278 (Vertical Type tool default) |\n| `BaselineDirection 2` / rotated | – | sideways, as before | 28 × 174 |\n\n### Anything a reviewer should look at first\n\n- **Existing documents:** vertical text in `.pcraft` files saved before this change has no `rotate_roman`, so it now renders upright, like new vertical type in Photoshop. PSDs keep whatever their runs (or style sheet) say.\n- **Not in this PR:** a Character panel menu toggle (Photoshop's \"Standard Vertical Roman Alignment\") to switch a selection between upright and rotated. The style field and PSD round trip are there for it.\n- `cargo test --workspace` (plugins excluded) passes, except `photocraft`'s `single_instance::tests::a_sandboxed_second_launch_hands_its_paths_over_a_unix_socket`. It passed one of three reruns and this PR doesn't touch it.\n\n### Tests\n\n- `vertical_tests::latin_stands_upright_one_per_em_by_default`: \"VERTICAL\" at 36 px is all upright glyphs, 36 px apart in one column; rendered it's a narrow stack about 8 em tall, taller than the rotated word. It fails without the layout change.\n- `psd::…::baseline_direction_round_trips`: run value 1 beats a sheet's 2; a run without one takes the sheet's; none at all is upright; both values write back. It fails without the PSD read.\n- The rotated-Latin tests now ask for `rotate_roman` explicitly. The CJK test expects Latin inside vertical CJK upright by default and rotated when asked.\n- `cargo fmt --check`; clippy `-D warnings` for doc and text; `cargo test --workspace --exclude photocraft-plugins --no-fail-fast` (only the flaky test above failing); `cargo xtask test-corpus` (4764 passed); `cargo xtask layers`; `cargo xtask wasm`. All pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "cc208ee0440bbdf1858565dca49667272a490c56",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/doc/src/text.rs"
},
{
"path": "crates/engine/src/type_styles_cmds.rs"
},
{
"path": "crates/text/src/layout.rs"
},
{
"path": "crates/text/src/psd.rs"
},
{
"path": "crates/text/src/vertical_tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378693/job/121235271596"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378673/job/121235272127"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378693/job/121235271804"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378693/job/121235271814"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378693/job/121235271898"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378693/job/121235272423"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378693/job/121235361355"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378693/job/121235361335"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40215378693/job/121235361276"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T17:58:39Z",
"url": "https://github.com/storytold/photocraft/pull/9478#pullrequestreview-5644503679",
"body": "Approving at cc208ee. Gate (ubuntu), SonarCloud, the build without HEIF and the other jobs that ran are green at this head. I read the non-test source in `doc/text.rs`, `text/layout.rs` and `text/psd.rs`, and the new and changed tests; the catalog files were not part of the logic and I did not look at them.\n\n- A new `CharStyle::rotate_roman` (default off) says whether vertical type turns roman text (Latin, digits) 90° clockwise along the column. By default roman text now stands upright, one character per em down the column, as Photoshop's Vertical Type tool sets it (issue #9470). Existing documents that relied on the old default will lay out differently; that is the stated intent.\n- In `Layouter`, an upright roman character is a character of the rotating class that is not half-width katakana or Hangul (`upright_roman`) in a run whose style does not rotate. Those characters get the upright class, have pair kerning and the common ligatures switched off (each stands alone), and get letter spacing computed from a first shaping pass so each advances exactly one em plus its tracking (`cluster_advances` reads the cluster advances by byte range; a character with no cluster of its own keeps its plain tracking). The paragraph is then rebuilt with those spacings, and the later tab-width iteration reuses them.\n- PSD: `BaselineDirection` 2 reads as rotated, anything else (1, or none anywhere) as upright, with the run's own value winning over the style sheet's; the value is written back as 2 or 1. A round-trip test covers all of these, the existing vertical layout tests that expect rotation now ask for it explicitly, and a new test checks eight upright letters step one em each in a single column and render much taller than the rotated word.\n\nLimits: I did not build or run the app. How the stacked letters look, spacing in real fonts, and how documents saved before this change now appear were not seen.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "cc208ee0440bbdf1858565dca49667272a490c56"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Type and typography",
"new_test_names": [
"upright_roman",
"cluster_advances",
"baseline_direction_round_trips",
"rotated",
"latin_stands_upright_one_per_em_by_default",
"ligature_engine",
"upright_roman_letters_stay_apart_without_ligatures",
"half_width_cjk_stays_rotated_beside_upright_roman"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9478"
},
{
"number": 9472,
"title": "fix(io): import a Displace smart filter that names its map file without embedding it (#9162)",
"url": "https://github.com/storytold/photocraft/pull/9472",
"body": "Fixes #9162.\n\n## Root cause\n\n#3673 (PR #3680) imports a Displace smart filter only when its map is embedded in `DspD`. Photoshop can also save a Displace that only names the map file in `DspF` (a `Pth ` value) with no `DspD` (ag-psd models the filter with `DspF` only). For that shape `import_params` returned `None`, so the filter became `psd.unsupportedFilter` (\"Displace...\") and `layer.smartObjects.replaceContents` reported \"not supported\" and passed the pixels through, the exact warning in the issue. Re-render could already read a `mapPath` through the document path policy (`filters_ext::resolve_map_path`, #3670); import just never produced one.\n\n## Change (`crates/io/src/displace_map.rs`)\n\n- `fltr_path` decodes a `Pth ` `DspF` (`txtu`, u32 LE size, u32 LE UTF-16 unit count, UTF-16LE path, NUL). All lengths are checked; malformed data returns `None`, never panics.\n- `import_params` falls back to that path as `mapPath` when no map is embedded, and still keeps the item verbatim as the `__displacePsd` template.\n- `template()` accepts a template with either a map or a path. `export_item` accepts `mapPath` only when it equals the template's own `DspF`, so an unedited filter writes back byte-identical; a changed path still errors as before.\n- A Mac alias (`alis`), non-`txtu`, truncated or empty path stays opaque, as before.\n\n## Tests\n\n- Unit: `a_map_named_only_by_its_path_imports_as_map_path` (import, byte-identical round trip, changed path refused, bad paths stay opaque).\n- Regression: `displace_psd_filter_with_only_a_map_path_renders_after_replace_contents` in `crates/engine/tests/it/smart_psd.rs`, at 8, 16 and 32 bit: a synthetic PSD with a path-only Displace pointing at a temp PNG map, re-imported, then `replaceContents`; asserts no warnings and a displaced cache. Before the fix it fails with `the Displace... smart filter is not supported, so it was not applied`.\n\nRan (rustc 1.95): `cargo fmt --check`, `cargo clippy -p photocraft-io -p photocraft-engine --all-targets -- -D warnings`, `cargo test -p photocraft-io`, the engine integration tests (incl. `panic_hunt`), `cargo xtask layers`, `cargo xtask wasm`, and the corpus set (`--features corpus,heif` on psd, codecs, io, engine): all pass. Re-ran the displace tests after rebasing on current `main`.\n\n## Notes\n\n- I could not check the reporter's file. If it does carry a `DspD` that still fails, this is a different cause and the file is needed.\n- Not covered: Mac `alis` paths are still not decoded; an unreadable map file on re-render still passes pixels through silently (existing `resolve_map_path` behaviour).\n- Unrelated: on Windows the `photocraft-engine` lib test binary crashed twice with `STATUS_ACCESS_VIOLATION` when run in parallel; with `--test-threads=1` all 2178 tests pass. This change touches no engine library code.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "4bab657cd4fb4a91ced44d3721e63a002ca5068c",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/tests/it/smart_psd.rs"
},
{
"path": "crates/io/src/displace_map.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393061225"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552098/job/121257057274"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552099/job/121257057410"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393118743"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552098/job/121257057642"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393061796"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552098/job/121257057698"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393062518"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552098/job/121257057526"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552098/job/121257058275"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393062570"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393124659"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552098/job/121257155110"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552098/job/121257155100"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393112131"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40222552098/job/121257155129"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393062864"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561959369/job/122393119649"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T19:17:41Z",
"url": "https://github.com/storytold/photocraft/pull/9472#pullrequestreview-5644841088",
"body": "Re-approving at 4bab657. Gate (ubuntu), SonarCloud and the Windows integration job are green at this head. I read the delta from 0f48dfa, which touches only `crates/io/src/displace_map.rs`.\n\n- `fltr_path` now trusts a `DspF` map path only when the `Pth ` record is well formed: the `txtu` tag, a size field equal to 12 plus twice the unit count and to the buffer length, and the UTF-16 text ending in exactly one NUL with no other NUL inside. A record with a wrong size or without the NUL is treated as not a path and the filter stays opaque.\n- The new `path_only` uses the path only when no `DspD` map is embedded at all, so a damaged embedded map keeps the filter opaque instead of falling back to a readable path. Import and the template check use it.\n- On export, a template that reads its map from a file now requires `mapPath` to equal that path (otherwise it errors) instead of silently dropping it, so the PSD cannot name a map the render never read.\n- The tests add a wrong size, a missing NUL, a damaged embedded map with a readable path, and the missing `mapPath` on export.\n\nLimits: I did not open real PSDs with Displace maps, so behaviour against Photoshop's own files is unobserved by me. I did not build or run the app.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "4bab657cd4fb4a91ced44d3721e63a002ca5068c"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"displace_psd_filter_with_only_a_map_path_renders_after_replace_contents",
"fltr_path",
"path_only",
"pth",
"a_map_named_only_by_its_path_imports_as_map_path"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9472"
},
{
"number": 9457,
"title": "raw: faster tone stage (Shadows, Highlights, Clarity, Texture, Dehaze), same bytes",
"url": "https://github.com/storytold/photocraft/pull/9457",
"body": "Part of #9435 (F8, the tone stage).\n\nWith Shadows, Highlights, Clarity, Texture or Dehaze set, the full-size part of the raw develop cost as much as the whole AHD development at 45 MP (one slider: +26 G instructions on a 13.6 G bilinear develop; Texture +58 G). This change speeds that stage up without changing a single output byte. Only `crates/raw/src/tone.rs` is touched.\n\nWhat it does, all with the same arithmetic in the same order:\n- The bilinear lookup that carries the change maps (and Dehaze's transmission) to full size worked out the source position with divisions for every pixel, twice (plane `a` and plane `b`). The taps of every column and row are now computed once per image, and `a`/`b` are stored together.\n- Texture's Gaussian bases ran the column pass down strided columns, one tap at a time with a mirror per tap. Both passes now accumulate neighbouring samples side by side, tap by tap in the old order.\n- The box mean's column pass runs row-wise in blocks, the running sums per column unchanged.\n- The luminance of the work copy and of Texture's copy is reduced in one pass over the pixels (each cell sums in the old order), and the work copy is skipped when only Texture is set and there is no black point.\n- `log2` of the luminance is taken once for both changes.\n\nNot changed (would change rounding): the per-pixel `log2f`/`exp2f` and the divisions of Dehaze.\n\n## Proof of identical output\n- Unit tests keep the replaced code (`bilinear_ref`, `gauss_ref`, `box_mean_ref`, `reduce`) and compare to_bits: odd sizes, kernels wider than the image, all sigmas/radii used.\n- `cmp` of the developed u16 output against upstream/main on 26 configurations at 45 MP (24/61 MP too), 1 thread: the 12 audit configurations (unc16 bilinear/MHC/AHD, unc14, CR2, LJ92 tiles/strip, deflate, orientation 6, cRAW, 24 MP, 61 MP) and 14 with sliders set: Shadows 50 / Dehaze 20, Shadows 50, Highlights -60, Clarity 50 and -70, Texture 50 and -50, Dehaze 40 and -40, all five mixed (+/-), all at 100, orientation 6 and CR2 with sliders. All identical.\n- `cargo test --release -p photocraft-raw`: 109 lib + 88 integration tests pass; clippy and fmt clean.\n\n## Measurement\n45 MP synthetic DNG (8256x5504, unc16), 1 thread pinned to one quiet core, `perf stat -e instructions:u,cycles:u`, upstream/main and this branch alternating, 3 rounds, medians (develop only; the decode is the same in both):\n\n| demosaic | sliders (Sh,Hi,Cl,Tx,Dh) | instructions | cycles | develop wall |\n|---|---|---|---|---|\n| ahd | none | 60.23 -> 60.09 G | 19.25 -> 16.95 G | 4331 -> 3711 ms (noise: the neutral path is untouched) |\n| ahd | 50,0,0,0,20 | 105.60 -> 84.41 G (1.25x) | 33.38 -> 24.22 G (1.38x) | 7090 -> 5216 ms |\n| ahd | 30,-30,30,30,20 | 165.45 -> 95.18 G (1.74x) | 48.12 -> 26.51 G (1.82x) | 10156 -> 5703 ms |\n| bilinear | 50,0,0,0,0 | 39.49 -> 27.19 G (1.45x) | 12.80 -> 7.88 G (1.62x) | 2657 -> 1623 ms |\n| bilinear | 0,-60,0,0,0 | 39.49 -> 27.19 G (1.45x) | 12.82 -> 7.91 G (1.62x) | 2674 -> 1661 ms |\n| bilinear | 0,0,50,0,0 | 42.17 -> 28.28 G (1.49x) | 13.48 -> 8.22 G (1.64x) | 2929 -> 1772 ms |\n| bilinear | 0,0,0,50,0 | 71.59 -> 30.54 G (2.34x) | 20.47 -> 8.91 G (2.30x) | 4320 -> 1844 ms |\n| bilinear | 0,0,0,0,20 | 25.14 -> 20.42 G (1.23x) | 7.80 -> 5.58 G (1.40x) | 1603 -> 1139 ms |\n| bilinear | 0,0,0,0,-20 | 16.95 -> 16.81 G | 4.37 -> 4.35 G | 860 -> 858 ms |\n\nAgainst the tone stage alone (develop minus the neutral bilinear 13.6 G): Shadows/Highlights 25.9 -> 13.6 G, Clarity 28.6 -> 14.7 G, Texture 58.0 -> 16.9 G, Dehaze 11.5 -> 6.8 G. The synthetic inputs are the ones from the audit in #9435 (no real-camera corpus in the repo).\n\n## Overlap\nThe open PRs #4240-#4243 also touch `tone.rs` but are whole-repository branches that have been conflicting since 2026-10-18; they rewrite `retone` and add a `RetoneCache`, and #4240 changes the map to be computed after the black point. This change stays inside `bilinear`/`gauss`/`box_mean`/`reduce`/`ToneMap::new`/`Haze` and does not touch `retone`; it will conflict with the same functions in those PRs only where they replace them.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "2863c3281c2a22dade0ce92d42bf8900074f76c6",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/raw/src/tone.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122393091189"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199489494/job/121187417960"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199489501/job/121187417305"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122392997226"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199489494/job/121187417819"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122392999471"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199489494/job/121187417670"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122393047218"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199489494/job/121187417848"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199489494/job/121187419180"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122393072686"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122392998689"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199489494/job/121187508436"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122393056435"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199489494/job/121187507378"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122392999346"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561940868/job/122392999325"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T15:24:36Z",
"url": "https://github.com/storytold/photocraft/pull/9457#pullrequestreview-5643852329",
"body": "Approving. This speeds up the full-size tone stage in tone.rs without changing arithmetic: bilinear taps are computed once per column and row (Grid) with the a and b planes stored together, the Gaussian and box-mean column passes accumulate neighbouring samples in blocks in the old tap order, and the work and Texture copies are reduced in one pass when both are needed. I checked that the accumulation order matches the old code (taps summed in sequence from a negative-zero start, edges handled by the mirrored path, kernels wider than the image falling back to it), that the Texture-only case skips the work copy and uses p01 only where it is needed, and that Haze now uses the shared Grid. The old bilinear, gauss and box_mean are kept as reference functions in the tests and compared bit for bit. The gate and SonarCloud checks pass at 2863c32. The identical-output comparisons on real raw files and the instruction counts come from the PR description; I did not rerun them. I read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "2863c3281c2a22dade0ce92d42bf8900074f76c6"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"new",
"change",
"new",
"at",
"at2",
"reduce_pair",
"texture",
"blur_row",
"bilinear_ref",
"gauss_ref",
"box_mean_ref",
"noise",
"same",
"gauss_gives_the_bits_of_the_tap_loop",
"box_mean_gives_the_bits_of_the_column_loop",
"the_tap_tables_give_the_bits_of_the_per_pixel_lookup",
"reduce_pair_gives_the_bits_of_two_reductions"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9457"
},
{
"number": 9436,
"title": "Startup: a failed launch says why in a dialog instead of closing silently (#7643)",
"url": "https://github.com/storytold/photocraft/pull/9436",
"body": "Fixes #7643.\n\nA launch that fails before the event loop starts exits through the `Err` from `eframe::run_native`, and a desktop launch has no console — release builds are GUI-subsystem on Windows, and a Dock/`open` launch has no terminal on macOS — so the error went nowhere and the app \"just closed\". The error wasn't logged either, so a reporter had nothing to attach.\n\n`apps/photocraft/src/startup_report.rs` logs it and, when nothing else can see the failure, shows one native dialog (`rfd::MessageDialog`, the crate already used for the file dialogs):\n\n```\nPhotoCraft could not start.\n\nwgpu error: No suitable GPU adapters found on the system!\n\nRenderer: dx12\nStartup: the previous start didn't finish on vulkan; using dx12\nVersion: 0.15.1 (3f2a9c1d0, 2026-10-01)\nLog file: C:\\Users\\me\\AppData\\Roaming\\Photocraft\\logs\\photocraft.log\n```\n\nThe dialog is skipped when a launch can report the failure some other way, so nothing that worked before changes:\n\n- **A terminal launch** (stderr is a terminal) keeps printing to stderr and exits: `cargo run` and the test harnesses never get a modal box.\n- **A `--control` launch** is driven by an automation client that reads the exit status, so it stays silent too.\n- **A headless Linux launch** (no `DISPLAY`, no `WAYLAND_DISPLAY` — the `startup_without_display` test removes both) has nowhere to put a dialog, so it keeps the error on stderr.\n\nThe `Log file:` line is left out when the logger never opened one (`--version`, a settings directory it can't write), and `Startup:` appears only when the renderer wasn't simply the preference — the crash-fallback case, where the app picks a backend the user never chose.\n\nThe body is English diagnostics like Help › System Info, so a bug report reads the same in every language; only the title goes through `i18n`.\n\n`AppLogger` now remembers the file `attach_dir` opened (`path()`): after the fact the logger only held the `File`, and the dialog has to name the file a reporter should attach.\n\nThe CPU-retry path is unchanged: the parent hands off to `--safe-gpu` and returns before this, so the child reports its own failure.\n\n## Tests\n\n- `logging::tests::path_names_the_attached_log_file` — `None` before a file is attached, then the path `attach_dir` returned.\n- `startup_report::tests::a_desktop_launch_asks_the_user_and_a_script_does_not` — the gate: desktop → dialog; a terminal, an automation client or no display → none.\n- `startup_report::tests::the_text_names_the_error_renderer_version_and_log` — error, renderer, version and log path are in the text; no log line without a file; no `Startup:` line without a fallback.\n- `startup_report::tests::a_fallback_that_was_not_the_preference_is_explained` — the fallback reason lands under the renderer.\n\nChecks: `rustfmt --check`, `cargo clippy -p photocraft --all-targets --features heif -- -D warnings`, `cargo test -p photocraft --lib --bins`.\n\n`docs/development.md` › Logs says the dialog exists, since that paragraph is what tells users which file to attach.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "819f2692db4302019638c57b6ea34cd1e8f4c24b",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "apps/photocraft/src/logging.rs"
},
{
"path": "apps/photocraft/src/main.rs"
},
{
"path": "apps/photocraft/src/startup_report.rs"
},
{
"path": "docs/development.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390195039"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770441/job/121299909306"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770187/job/121299908514"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770452/job/121299909557"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390288699"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770441/job/121299909577"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390263729"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770441/job/121299909525"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390265746"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770441/job/121299909554"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770441/job/121299910817"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390196744"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390246416"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770441/job/121300030522"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770441/job/121300030533"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390279255"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40236770441/job/121300031612"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390197549"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561148367/job/122390197888"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T22:16:33Z",
"url": "https://github.com/storytold/photocraft/pull/9436#pullrequestreview-5645623601",
"body": "Re-approving at 819f269. The gate, SonarCloud and the Windows unit and integration jobs pass at this head, and no job fails.\n\nI read the delta since my approval at 4b7670c: one commit touching four files. It names the `WGPU_BACKEND` value in main.rs and limits the docs sentence so that control, headless and preflight launches are excluded. These are the review nits, with no change to behaviour beyond that.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "819f2692db4302019638c57b6ea34cd1e8f4c24b"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Platform and reliability",
"new_test_names": [
"path_names_the_attached_log_file",
"has_display",
"a_desktop_launch_asks_the_user_and_a_script_does_not",
"the_text_names_the_error_renderer_version_and_log",
"a_fallback_that_was_not_the_preference_is_explained"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9436"
},
{
"number": 9418,
"title": "Color Lookup: honour the .3dl Mesh header's cube edge and output depth (#9078, #9073)",
"url": "https://github.com/storytold/photocraft/pull/9418",
"body": "## What\n\n`parse_3dl` skipped every text line, so it ignored the Lustre `3DMESH` / `Mesh <inBits> <outBits>` header and inferred both the cube edge and the output code range from the data. The header declares the cube edge (2^inBits + 1) and the output range (up to 2^outBits − 1), and Photoshop honours both:\n\n- **#9078**: a 3³ cube whose shaper line has exactly three values (`Mesh 1 12` + `0 512 1023` + 27 nodes) had its shaper counted as a 28th node → `28 rows is not a cube`, whole file rejected. With the header, the declared cube settles the ambiguity: one row too many whose first row has `n` values means that row is the shaper. Node-only tables (the reporter's positive control) are untouched.\n- **#9073**: identical node bytes under `Mesh 2 10` vs `Mesh 2 12` must render differently (white → 255 vs grey 64, measured in Photoshop 27.11). The declaration now wins while it covers the data; a node beyond the declared range is treated as a wrong declaration and keeps the old max-value inference, so no file that loaded before stops loading.\n\nMalformed or out-of-range `Mesh` values are ignored as before; a `Mesh` header that disagrees with the row count is refused with a clear error. Headerless `.3dl` behaviour is unchanged.\n\n## Tests\n\n- `three_dl_mesh_three_point_shaper_is_not_a_node` — the #9078 repro shape (3³, three-value shaper, 12-bit inverse nodes) parses; the shaper-less 27-node control parses identically.\n- `three_dl_mesh_declares_the_output_depth` — the #9073 pair: same bytes, `Mesh 2 10` vs `Mesh 2 12`, corner divides by 1023 vs 4095.\n- `three_dl_mesh_header_mismatch_is_rejected` — mismatch refused; over-range declaration falls back to inference.\n- `color_lookup_mesh_3dl_honours_the_header` (engine) — end-to-end `image.adjustments.colorLookup` on the #9078 file: (64,128,192) renders (191,127,63) ±1, Photoshop's measured result.\n\nVerified locally: `cargo test -p photocraft-cms`, `cargo test -p photocraft-engine --lib` (2187 passed), `cargo test -p photocraft-io --lib`, `cargo clippy -p photocraft-cms -p photocraft-engine --all-targets -- -D warnings`, `cargo xtask layers`, `panic_hunt --ignored`. The change is pure std Rust (no platform code), so wasm is unaffected.\n\nFixes #9078\nFixes #9073",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "da4b838e54f6073cda4798fa7b63b3b7214aaf93",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/cms/src/lutfile.rs"
},
{
"path": "crates/engine/src/adjust_cmds/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40203655947/job/121199914319"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40203677708/job/121199980801"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40203655947/job/121199914276"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40203655947/job/121199914391"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40203655947/job/121199914128"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40203655947/job/121199915360"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40203655947/job/121199986296"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40203655947/job/121199985217"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T18:18:42Z",
"url": "https://github.com/storytold/photocraft/pull/9418#pullrequestreview-5644614398",
"body": "Approving at da4b838. Gate (ubuntu), SonarCloud and the build without HEIF are green at this head; the Windows test jobs were skipped on this run. The source change is all in `cms/src/lutfile.rs` and I read it; the catalog files were not part of the logic.\n\n- `parse_3dl` now honours the `Mesh <inBits> <outBits>` line. A declaration is exactly two integers after the keyword, with `inBits` 1 to 7 and `outBits` 1 to 16; anything else (a non-number, too few or too many fields, out of range) is ignored as every text line was before, so such a file still loads by inference.\n- With a valid declaration the cube edge is 2^inBits + 1 and the node count must be exactly its cube (checked with `checked_pow`); if there is one row more and the first numeric line has exactly `n` values, that row is the shaper and is dropped. Any other count is an error naming the expected and found rows. The output scale is `2^outBits - 1`, also for a small maximum, and only when a node exceeds that range does the old inference apply, so a wrong declaration keeps loading as before.\n- Without a declaration the old rule remains (edge from the cube root of the row count, scale inferred) in a helper, `inferred_3dl_scale`, which keeps the header rule that a headerless table peaking at 255 or 1023 is on the 12-bit scale. A `3DMESH` line with no numbers still counts as a header. The test that used `Mesh 1 10` to mean a header is changed to the bare magic, since that line now declares a 3³ cube.\n- New tests cover the declaration cases, which I skimmed.\n\nLimits: I did not build or run it, and did not load a real `.3dl` file or compare against Photoshop; the Photoshop behaviour cited in the comments (#9073, #9078, #9121) is the author's.\n\nI read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "da4b838e54f6073cda4798fa7b63b3b7214aaf93"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"inferred_3dl_scale",
"mesh_3dl",
"three_dl_mesh_three_point_shaper_is_not_a_node",
"three_dl_mesh_declares_the_output_depth",
"three_dl_mesh_header_mismatch_is_rejected",
"three_dl_malformed_mesh_header_falls_back_to_inference",
"three_dl_mesh_uses_declared_range_for_small_integer_codes",
"color_lookup_mesh_3dl_honours_the_header"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9418"
},
{
"number": 9416,
"title": "Affinity reader: contiguous curve records and a cheaper object tag index",
"url": "https://github.com/storytold/photocraft/pull/9416",
"body": "Part of #9413\n\n## What\n\n- `stream.rs`: a fixed-size record array (`0x2c`, curve nodes) became one `Vec<Value::Bytes>` entry per record. It is now one `Value::Records { size, data }` buffer. Each record still counts as one value against `MAX_VALUES`, so the limit is unchanged. `geometry::curves` reads the nodes straight from the buffer; `Value::array_len` gives the slot count for both array kinds, and the other readers of array fields (slot charging in `access.rs`, gradient positions in `paint.rs`, column widths in `text.rs`) treat the new variant like the array of byte values it replaces. Records of a width other than 18 are still no curve nodes.\n- `stream.rs`: the per-object SipHash `HashMap<Tag, usize>` is gone. Objects with up to 12 fields are scanned; larger ones use a sorted `(tag, first index)` table with a binary search (still logarithmic, so many-field hostile objects cannot go quadratic; no unseeded fast hasher because tags come from the file). First-match semantics and tag 0 handling are unchanged.\n- `model.rs`: `read()` drops the inflated `doc.dat` once the stream is parsed.\n\n`stream::Value` is public and gets a new variant; nothing else in the workspace matches on it.\n\n## Proof of identical output\n\n- A hash of the whole parsed `Document` (all metadata plus raw pixel bytes) equals main on 7 large synthetic documents and on all 39 files of the public corpus.\n- A test keeps the old per-object map and compares `Object::get` for every object size 0..59 with repeated and positional tags. Another checks the record array layout and `array_len`.\n\n## Measurements\n\n`perf stat`, one pinned core, main and branch alternated, 5 rounds, medians; peak RSS from a single run.\n\n| Document | Instructions | Cycles | Peak RSS |\n|---|---|---|---|\n| 2000 curves x 2000 nodes (100 MB) | 2.11e10 -> 1.15e10 (-46%) | 6.04e9 -> 3.83e9 | 1136 -> 531 MB |\n| 50 000 shapes | 3.17e9 -> 2.38e9 (-25%) | 1.27e9 -> 0.82e9 | 174 -> 127 MB |\n| 2000 text layers | 9.8e8 -> 8.6e8 (-12%) | 2.8e8 -> 2.2e8 | 51 -> 38 MB |\n\nDropping `doc.dat` early alone is worth about 63 MB of peak on the curve document. The real corpus is tiny (all 39 files read in about 50 ms in total), so the gain shows on the large synthetic documents.\n\nLeft out because no gain was measured here: `objs()` returning an iterator, the `Vec<char>` text path, the tile-cache retention in `plane()`.\n\nChecks: `cargo fmt --all --check`, `cargo clippy -p photocraft-affinity --all-targets -- -D warnings`, `cargo test --release -p photocraft-affinity`, `cargo check -p photocraft-io`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "742b06805284d8d76d5a29ec1821193f4835fedf",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/affinity/src/access.rs"
},
{
"path": "crates/affinity/src/geometry.rs"
},
{
"path": "crates/affinity/src/model.rs"
},
{
"path": "crates/affinity/src/paint.rs"
},
{
"path": "crates/affinity/src/stream.rs"
},
{
"path": "crates/affinity/src/tests.rs"
},
{
"path": "crates/affinity/src/text.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122393007864"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40186754916/job/121149073892"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40186754920/job/121149073659"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122393004185"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40186754916/job/121149073910"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122392978789"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40186754916/job/121149073855"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122392931484"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40186754916/job/121149073701"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40186754916/job/121149074654"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122392931967"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122392930559"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40186754916/job/121149174014"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122392933797"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40186754916/job/121149172977"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122392934576"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561921519/job/122392934520"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T11:36:34Z",
"url": "https://github.com/storytold/photocraft/pull/9416#pullrequestreview-5642992245",
"body": "Approving. Two allocation cuts in the Affinity reader. Curve-node arrays (`0x2c`) are now one `Value::Records { size, data }` buffer instead of a `Vec` of `Bytes` per record, with the same per-record charge against `MAX_VALUES`; the length check rejects a zero width and any `n * size` that overflows or exceeds the remaining input before anything is allocated, and the allocation uses `try_reserve_exact`. Geometry reads 18-byte records straight from the buffer (`record18`) and treats other widths as no curve nodes, `array_len()` replaces the `Value::Array` length matches, and gradient stops accept the record form. Objects with 12 fields or fewer are scanned instead of hash-indexed, larger ones use a tag-sorted vector with first-occurrence semantics kept by the sort and `dedup_by_key`. The inflated document is dropped before the model is built.\n\nI read the whole non-test diff and checked the other `Value::Array` consumers (raster states and tile indexes, dash patterns, text, shapes) at 742b068: none of them reads `0x2c` arrays, so the new variant doesn't change them. The gate, corpus tests, web build and SonarCloud pass. I read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "742b06805284d8d76d5a29ec1821193f4835fedf"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"record18",
"anchors",
"f32_samples",
"build_index",
"reference",
"lookup_keeps_first_match_for_every_object_size",
"a_fixed_size_array_is_one_buffer_and_one_value_per_record"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9416"
},
{
"number": 9401,
"title": "fix(format): a save on macOS keeps the file's creation time (#9388)",
"url": "https://github.com/storytold/photocraft/pull/9401",
"body": "Fixes #9388.\n\nOn macOS, saving over an existing file reset its creation date (\"Created\" in Finder) to the time of the save. Save writes a temporary file and renames it over the original (`crates/format/src/atomic.rs`), so the renamed file is a new inode born at the save. The issue asked for someone with a Mac to check; this was checked on macOS 27.2 (Apple silicon).\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2) on macOS, by script: save a synthetic 32×32 document as a PSD, wait four seconds, change it and save again. `stat` before and after:\n\n| | created | modified |\n|---|---|---|\n| after the first save | 04:10:59 | 04:10:59 |\n| after the re-save | **04:10:59** (kept) | 04:11:04 |\n\n### What changes\n\n- `atomic::meta::copy_metadata` gains a macOS step, `copy_created`. It reads the replaced file's creation time and sets it on the temporary file, using `std::os::macos::fs::FileTimesExt::set_created` and `File::set_times` (stable since Rust 1.75), before the rename. The rename keeps it.\n- Like the other metadata steps, it's best effort and never touches the bytes.\n- The module's table now lists macOS creation time as carried over. The comment saying it needed an unsafe `setattrlist` is gone.\n\n### Verification\n\n`photocraft-cli run --new … --out f.psd`, wait three seconds, then `photocraft-cli run f.psd --cmd edit.fill … --out f.psd --in-place`:\n\n| | created after the re-save | modified after the re-save | Photoshop 2025 |\n|---|---|---|---|\n| main | moved to the save time (04:10:38 → 04:10:51) | save time | – |\n| this PR | kept (04:16:26 → 04:16:26) | save time (04:16:29) | created kept, modified updated |\n\n### Tests\n\n- `atomic::tests::keeps_the_original_creation_time` (macOS only): a file whose creation time is set well in the past keeps it through `atomic_write`, the new bytes are in place, and no temporary file is left. It fails with the step disabled.\n- `cargo fmt --check`; clippy `-D warnings` for format; `cargo test -p photocraft-format` (68 + 100); `cargo xtask layers`; `cargo xtask wasm`. All pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "f241bba1adfb9c7ec5dd5ed9d86f95b63c7bc2c3",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/automation/src/workspace.rs"
},
{
"path": "crates/format/src/atomic/meta.rs"
},
{
"path": "crates/format/src/atomic/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976762/job/121146724934"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976753/job/121146724743"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976762/job/121146725032"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976762/job/121146724864"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976762/job/121146724993"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976762/job/121146725636"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976762/job/121146799031"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976762/job/121146799005"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40185976762/job/121146798994"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T11:13:17Z",
"url": "https://github.com/storytold/photocraft/pull/9401#pullrequestreview-5642877921",
"body": "Approving. On macOS a save now copies the replaced file's creation time onto the new file before the rename (`copy_created` in `atomic/meta.rs`, via `FileTimesExt::set_created` on a write-opened handle). It is set before the ACL is copied, because a copied ACL that denies its owner write access would refuse the open; the error joins the existing attributes/ACL/permissions result. The automation write path (`workspace.rs`) does the same through its already-open handles with the new `copy_created_file`, ignoring a failure there. The code and the three new tests (plain save, deny-write ACL, automation write) are `cfg(target_os = \"macos\")`, so the Linux gate that passes at f241bba does not compile or run them; I read the code but could not confirm that path builds or behaves. SonarCloud passes. I read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "f241bba1adfb9c7ec5dd5ed9d86f95b63c7bc2c3"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"write_keeps_the_replaced_files_creation_time",
"copy_created",
"keeps_the_original_creation_time",
"keeps_the_creation_time_under_a_deny_write_acl"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9401"
},
{
"number": 9354,
"title": "fix(ui): Full Screen Mode needs a document (#9349)",
"url": "https://github.com/storytold/photocraft/pull/9354",
"body": "Fixes #9349.\n\nWith no document open, View › Screen Mode › Full Screen Mode (and the F cycle) put the start screen on the full screen pasteboard, which is black. With a light theme, the logo and text are dark on black and almost invisible, so it looks like the app has gone blank.\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2) on macOS, by script (`select` on the `screenModeFullScreen` menu item):\n\n| | Photoshop |\n|---|---|\n| no document open | refused (\"not currently available\") |\n| a synthetic 400×300 document open | Full Screen Mode runs |\n\nPhotoshop never shows its start screen on the full screen pasteboard.\n\n### What changes\n\n- `view_cmds::is_enabled`:\n - **Full Screen Mode** and **Full Screen Mode With Menu Bar** need a document.\n - **Cycle Screen Mode** (F) is available with no document only to bring a full screen mode back to Standard.\n- `view_cmds::leave_full_screen_without_document`, called each frame, puts a full screen mode back to Standard when no document is open. That covers closing the last document while in full screen, and anything that reached full screen anyway, such as automation.\n\n### Verification\n\nLive app, Pro Light theme, scratch settings folder, View › Screen Mode › Full Screen Mode:\n\n| | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| no document | the start screen on black; dark text barely visible | unavailable; automation's call drops back to Standard | refused |\n| with a document | Full Screen Mode, black pasteboard | same | same |\n| last document closed while in Full Screen Mode | the start screen on black | Standard Screen Mode with the start screen | not checked (see below) |\n\n### Anything a reviewer should look at first\n\n- **Closing the last document in full screen is an assumption.** I couldn't observe Photoshop there: the display was asleep while I tried. Going back to Standard follows the observed rule that Full Screen Mode needs a document.\n- The full screen pasteboard itself stays black, which is Photoshop's default Full Screen color. Making that colour a preference, as Photoshop's Interface preferences do, would be separate work.\n\n### Tests\n\n- `view_cmds::tests::full_screen_modes_need_a_document` covers:\n - enabled states with and without a document;\n - closing the last document in Full Screen Mode returns to Standard;\n - a full screen mode set with no document drops back.\n - It fails without the change.\n- `cargo fmt --check`; clippy `-D warnings` for ui-egui; `cargo test -p photocraft-ui-egui` (3799 + 132); `cargo xtask layers`; `cargo xtask wasm`. All pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "41eb1de5cff17f5f5b9c22913e48b71565b56a2a",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/panels.rs"
},
{
"path": "crates/ui-egui/src/view_cmds.rs"
},
{
"path": "crates/ui-egui/src/view_cmds/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356857/job/121093990862"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356842/job/121093926996"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356857/job/121093991043"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356857/job/121093990972"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356857/job/121093990951"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356857/job/121093991658"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356857/job/121094049506"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356857/job/121094049438"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40168356857/job/121094049405"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T06:55:52Z",
"url": "https://github.com/storytold/photocraft/pull/9354#pullrequestreview-5641946837",
"body": "Approving (#9349). Full Screen Mode now needs a document: view.screenMode.fullScreen and fullScreenWithMenuBar are unavailable without one and refuse with a clear error before any viewport command goes out (from the toolbar list or automation); Cycle with no document can only go back to Standard; a full screen mode left with no document after the last one closes returns to Standard on the next frame; and the Change Screen Mode button and its list items are greyed out accordingly. The test clicks the real toolbar button with no document and checks no fullscreen command is sent. I read the whole source diff; the gate, the web build and wasm size gate and SonarCloud pass at 41eb1de. I read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "41eb1de5cff17f5f5b9c22913e48b71565b56a2a"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"screen_mode_button_is_inert_without_a_document",
"full_screen_modes_need_a_document"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9354"
},
{
"number": 9342,
"title": "fix(io, engine): Embed Color Profile writes sRGB for an untagged document (#9325)",
"url": "https://github.com/storytold/photocraft/pull/9342",
"body": "Fixes #9325.\n\nA JPEG exported from a new document had no colour profile, even with Export As › Embed Color Profile on. New documents in the default working space are untagged: `icc_profile: None`, which PhotoCraft treats as sRGB (its Dot Gain 20 %-compatible working gray for grayscale). The exporters only embedded a profile the document carried, so viewers and online platforms had to guess.\n\n### Photoshop's behaviour\n\nPhotoshop 2025 (26.2) on macOS, by script: a 64×64 synthetic document filled with one colour.\n- **Save As JPEG** with Embed Color Profile.\n- **Save for Web** with JPEG and Include Profile.\n\nThe embedded profile was read back from each file:\n\n| Document | Save As JPEG | Save for Web JPEG |\n|---|---|---|\n| new (Working RGB: sRGB) | sRGB IEC61966-2.1 | sRGB IEC61966-2.1 |\n| set to Don't Color Manage | sRGB IEC61966-2.1 | sRGB IEC61966-2.1 |\n\n### What changes\n\n- **io** (`flat::export_flat_with`): an untagged image gets the profile assumed for its mode when the target format can carry one. That's built-in sRGB for RGB, and for gray the Dot Gain 20 %-compatible working gray the canvas shows it through (not sGray, which would look darker). This covers Export As, Save As / Save a Copy and the CLI.\n - The codec still leaves the profile out when `embed_icc` is off.\n - Formats that can't hold a profile take the existing path unchanged, so they get no new \"profile dropped\" warning.\n - The CMYK branch already writes the built-in Coated CMYK for an untagged CMYK target, so this follows the same rule.\n- **engine** (Save for Web): with Embed Color Profile on, an untagged document's file carries sRGB. `web_document` already treats such documents \"as sRGB already\". With it off, nothing changes.\n\n### Verification\n\nSame steps on main and this branch: `photocraft-cli run --new … --cmd edit.fill --out new.jpg|png|webp`.\n\n| | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| untagged RGB → JPEG | no profile | sRGB (built-in) | sRGB IEC61966-2.1 |\n| untagged RGB → PNG / WebP | no profile | `iCCP` / `ICCP` sRGB | – |\n| Embed Color Profile off | no profile | no profile | – |\n| tagged (Display P3) | Display P3 | Display P3 | – |\n\n### Anything a reviewer should look at first\n\n- **Every flat export of an untagged document now carries sRGB/sGray when embedding is on.** That includes the CLI's `run --out`, so files grow by PhotoCraft's built-in profile (about 650 bytes). Photoshop does the same for new documents.\n- **Four tests relied on untagged exports having no profile.** Each now asks for that explicitly:\n - `color_cmds::exif_srgb_jpeg_round_trip`: the EXIF ColorSpace path needs a file without ICC.\n - `export_dialog::the_estimate_is_the_exported_files_size`: its premise measures the proxy's pixel error. A fixed-size profile scaled with the pixels blurred that, so the test now runs with `embedProfile: false`.\n - The Affinity preview fixture in `engine/tests/it/affinity.rs`: Affinity previews carry no profile, and the hardened preview reader refuses compressed PNG chunks.\n - `automation::rpc::doc_save_rejects_a_bad_quality…`: a WebP with a profile is the extended `VP8X` layout, so the test looks for the lossy `VP8 ` chunk anywhere rather than at byte 12.\n- The Export As size estimate scales the proxy's whole file, profile included. That was already true for tagged documents. The error is about 650 bytes × the scale ratio, and the exact estimate covers sizes up to `EXACT_ESTIMATE_PX`.\n\n### Tests\n\n- `flat::assumed_profile_tests::untagged_documents_embed_the_profile_they_are_assumed_to_be_in` covers:\n - JPEG, PNG, TIFF and WebP of an untagged RGB document carry sRGB;\n - a gray PNG carries the Dot Gain 20 %-compatible working gray;\n - with embedding off, there's none;\n - a BMP gets no profile warning;\n - a Display P3 document keeps P3.\n- `web_cmds::tests::embed_color_profile_tags_an_untagged_document_srgb` covers Save for Web JPEG and PNG-24, with the option on and off.\n- Both fail without their change.\n- `cargo fmt --check`; clippy `-D warnings` for io, engine, ui-egui and automation; `cargo test` for io, engine (2187 + 277), ui-egui (3798 + 132), automation and cli; the rest of the workspace; `cargo xtask test-corpus` (4733 passed); `cargo xtask layers`; `cargo xtask wasm`. All pass.\n - `photocraft-plugins`' `abi` test target doesn't build on this Mac (`rustix::fs::mknodat` missing). This PR touches neither that crate nor any dependency. Its lib tests pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "c46a74621d36adae9707ab8f3eb39cf6b4c302b9",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/automation/src/rpc.rs"
},
{
"path": "crates/engine/src/color_cmds.rs"
},
{
"path": "crates/engine/src/web_cmds.rs"
},
{
"path": "crates/engine/src/web_cmds/tests.rs"
},
{
"path": "crates/engine/tests/it/affinity.rs"
},
{
"path": "crates/io/src/flat.rs"
},
{
"path": "crates/ui-egui/src/export_dialog.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081305/job/121081536427"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081366/job/121081433896"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081305/job/121081536444"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081305/job/121081536395"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081305/job/121081536192"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081305/job/121081536837"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081305/job/121081613163"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081305/job/121081613150"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40164081305/job/121081613142"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T07:21:10Z",
"url": "https://github.com/storytold/photocraft/pull/9342#pullrequestreview-5642054720",
"body": "Approving (#9325). With Embed Color Profile on, an untagged document is now tagged with the profile it is assumed to be in: sRGB for RGB, and the Dot Gain 20 percent compatible working gray for gray (not sGray, which would show the same samples darker) in the flat export path, and sRGB in the web export path; a tagged document keeps its own profile and nothing is embedded when the option is off. The new test covers jpg, png, tif and webp, gray, option off, bmp (no warning) and a Display P3 document; older tests that assumed no profile were adjusted. This touches the same issue as another open pull request that changes the export dialog's preview and Convert to sRGB path (#9339); I did not check how the two combine. I read the whole source diff; the gate, the web build and wasm size gate and SonarCloud pass at c46a746. I read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "c46a74621d36adae9707ab8f3eb39cf6b4c302b9"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"embed_color_profile_tags_an_untagged_document_srgb",
"doc",
"embedded",
"untagged_documents_embed_the_profile_they_are_assumed_to_be_in"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9342"
},
{
"number": 9339,
"title": "fix(export): embed requested sRGB profile for untagged sources (#9325)",
"url": "https://github.com/storytold/photocraft/pull/9339",
"body": "Fixes #9325.\n\nExport As already offers Convert to sRGB and Embed Color Profile, but skipped conversion for inputs without ICC data. Honor the requested sRGB destination using the **effective source profile from the original session**: a valid embedded profile takes precedence, otherwise use that session’s working profile.\n\nCapture the profile for save callbacks, selected-layer batches, preview workers and exact-size workers. Pin it onto the export copy before conversion; RGB sources whose profile is byte-identical to sRGB retain their pixel tiles. Other sources use the existing colour converter. Preview cache signatures include the source-profile hash so working-space changes invalidate an untagged preview without requiring a document edit.\n\n## Validation\n\n- Full UI suite: **3,935 passed, 20 ignored**; all-target UI Clippy, formatting, dependency layers and WASM passed. The dense 24 MP tile-reuse check was run separately and passed.\n- The new app replay regression fails on the previous head: Policy Off + untagged Display P3 pixels were merely labelled sRGB. It now checks Display P3 and Adobe RGB at 8/16/32-bit against a tagged conversion control, requires a real pixel change, and verifies source immutability.\n- UI cache tests verify working-profile changes without document revision, and embedded-profile precedence. Removing the profile hash makes the cache regression fail. Native workers retain the captured profile even if the original session changes afterwards.\n- Existing real JPEG encode/import tests cover RGB 8/16/32-bit, Gray and CMYK, embedding opt-out, and Document Profile. Native offscreen inspection of the P3 export preview was also performed.\n\nThe existing control screenshot below is UI context; ICC and converted pixel behavior are verified by tests.\n\n![Export As controls](https://raw.githubusercontent.com/vanvo/photocraft/9082d388bcf3fbecea1b649b771582f77f3f5d98/export-options.png)\n\n[Screenshot attribution and licenses](https://github.com/vanvo/photocraft/blob/9082d388bcf3fbecea1b649b771582f77f3f5d98/ATTRIBUTION.md).",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "393645ab91460af2e37cc3ba8f6921e7551f676e",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/export_dialog.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169480996/job/121097233837"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169481088/job/121097234211"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169480996/job/121097233815"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169480996/job/121097233808"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169480996/job/121097233650"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169480996/job/121097234506"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169480996/job/121097302454"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169480996/job/121097302524"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40169480996/job/121097302436"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T07:36:38Z",
"url": "https://github.com/storytold/photocraft/pull/9339#pullrequestreview-5642090549",
"body": "Approving. Exporting an untagged document with the sRGB profile now embeds that profile: the export dialog works out the source profile once (the document's own profile when it matches the mode's color space, otherwise the working profile), stamps it on the export copy, and converts only when the mode isn't RGB or the source isn't already sRGB, so a plain tag reuses the pixel tiles. The profile hash is part of the preview signature so the preview and size estimate follow it, and the preview, exact estimate and write paths all take the same profile. I read the whole non-test diff (one file); the gate and SonarCloud pass at 393645a. I read the code and the CI results; I did not run the app.\n",
"commit": {
"oid": "393645ab91460af2e37cc3ba8f6921e7551f676e"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"effective_source_profile",
"export_document",
"preview_proxy",
"preview_sync",
"exact_estimate",
"poll_or_start_exact_estimate",
"write_export",
"default_export_document",
"default_preview_proxy",
"default_exact_estimate",
"default_poll_or_start_preview",
"check_profile_only_export",
"untagged_srgb_export_tags_without_rewriting_pixels",
"untagged_srgb_export_24mp_keeps_pixel_tiles",
"untagged_jpeg_export_embeds_the_requested_srgb_profile",
"preview_cache_tracks_working_profiles_but_prefers_embedded_profiles",
"preview_workers_keep_the_captured_source_profile",
"replay_converts_untagged_pixels_from_the_original_working_profile"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9339"
},
{
"number": 9282,
"title": "feat(ui): Preset menus for Levels, Exposure, Black & White and Channel Mixer",
"url": "https://github.com/storytold/photocraft/pull/9282",
"body": "Fixes #9263, fixes #9265, fixes #9268, fixes #9270.\n\nLevels, Exposure, Black & White and Channel Mixer gain a **Preset** menu at the top of their editor (Image › Adjustments dialog and the adjustment layer's Properties), as in Photoshop. It lists the preset names Photoshop uses and reads **Custom** once the settings are edited. Choosing a preset is one edit, which the preview follows.\n\n### The settings are original\n\n**These presets' settings are PhotoCraft's own.** Each was designed for the look its name describes. None was copied, measured or read from Photoshop's (or any other application's) preset files, and the clean-room rule in AGENTS.md was followed. Only the names, which describe the looks, follow Photoshop's menus.\n\n- **Levels:**\n - Darker lowers the white output; Lighter brings the white input in.\n - Increase Contrast 1/2/3 pull both input points in, further each step.\n - Lighten Shadows lifts the black output and opens the gamma a little.\n - Midtones Brighter/Darker move the gamma only.\n- **Exposure:** Minus/Plus 1.0 and 2.0 are whole stops.\n- **Black & White:** the filter presets follow what a coloured lens filter does on black-and-white film: it lightens its own colour and darkens its complement.\n - Red: dark skies. Blue: light skies, dark skin tones.\n - The High Contrast variants push further.\n - Infrared makes foliage glow and skies go dark.\n - Maximum Black and Maximum White take every colour to the ends.\n - Neutral Density weighs every colour the same.\n- **Channel Mixer:** monochrome mixes of the channels each filter would let through: a red filter is the red channel, orange is half red and half green, infrared is mostly green.\n\nThe tables are in `crates/ui-egui/src/adjust_presets.rs`, with this note at the top of the file and one comment per preset.\n\n### What changes\n\n- `adjust_presets.rs` (new): the preset tables and `presets(kind, gray)`.\n - `settings` gives a preset over the kind's defaults, as the engine normalises it.\n - `matching` finds which preset the current settings are, or Custom.\n- `adjust_editors::editor` draws a `Preset:` row above these four editors through the common dispatcher, so the dialog and Properties both get it.\n - Levels shows it in RGB and Grayscale documents.\n - Black & White and Channel Mixer show it only in colour documents.\n- `docs/ui-parity.md`: revision.\n\n### Verification\n\nA synthetic chart (a grey ramp over a hue/brightness sweep) run through the presets with the CLI:\n- Each Black & White and Channel Mixer filter brightens its own band and darkens the complement.\n- Infrared whitens the greens and yellows and blackens the blues; Neutral Density is flat.\n- Increase Contrast 3, Lighten Shadows, Midtones Darker and Darker change the tones as named.\n\n| Adjustment | main | this PR | Photoshop 2025 |\n|---|---|---|---|\n| Levels / Exposure / Black & White / Channel Mixer | no Preset menu | Preset menu (these names, Custom after an edit) | Preset menu with these names (its settings weren't used) |\n\n### Anything a reviewer should look at first\n\n- **One shared module** serves all four kinds, wired through the common editor dispatcher. My separate Curves presets PR (#9234) has its own module, and #9251 (another contributor) covers Hue/Saturation. These could be folded into this one later.\n- **The menu is UI-only.** Scripts and agents set the values directly.\n- **The preset values are a design choice**, kept in one table each, and are easy to tune.\n\n### Tests\n\n- `adjust_presets::tests::presets_apply_and_are_recognised`:\n - Every preset is valid, distinct and recognised after it's applied; untouched settings read Default; an edit reads Custom.\n - Colour presets aren't offered in Grayscale.\n- `adjust_presets::tests::presets_give_their_looks`:\n - Contrast steps widen and gamma presets point the right way.\n - Filters lighten their own colour over the complement, and the High Contrast red filter goes further.\n - Exposure stops are exact, and the Channel Mixer presets are monochrome.\n- `adjust_dialog::tests::adjustment_preset_menus_load_presets_and_read_custom_after_an_edit` (UI harness, real menu clicks in all four dialogs):\n - Each opens on Default, loads the chosen preset, and reads Custom after an edit.\n - It fails without the Preset row.\n- The 106 existing adjustment editor tests pass unchanged. `cargo fmt --check`, `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`, `cargo test -p photocraft-ui-egui` (3799 + 132 passed), `cargo xtask layers` and `cargo xtask wasm` pass.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "20803d1f15900ed6ecc7b5c79b4c68f6c3d6dcce",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/adjust_dialog.rs"
},
{
"path": "crates/ui-egui/src/adjust_editors.rs"
},
{
"path": "crates/ui-egui/src/adjust_editors_tests.rs"
},
{
"path": "crates/ui-egui/src/adjust_presets.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "docs/ui-parity.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407141/job/121386613861"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265406983/job/121386613014"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407161/job/121386613814"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407161/job/121386657772"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407141/job/121386613990"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407161/job/121386657851"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407141/job/121386614023"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407141/job/121386614189"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407161/job/121386657059"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407141/job/121386614917"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407141/job/121386755588"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407141/job/121386755644"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40265407141/job/121386755593"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-26T04:14:47Z",
"url": "https://github.com/storytold/photocraft/pull/9282#pullrequestreview-5647322262",
"body": "Re-review at 20803d1 (c47a8a4 plus one docs commit that reorders the ui-parity history newest first; the code is unchanged). Code read (the PR diff; the rebase onto main was not re-diffed line by line). Tests not run, app not run.\n\nMy earlier request was to say in the product that these presets are PhotoCraft's own, not Photoshop's. That is done: `preset_row`, shared by the Properties panel and the adjustment dialog through `editor()`, now leads with a \"PhotoCraft Preset:\" caption whose hover text says these are PhotoCraft's own settings, not Photoshop's presets of the same names. The test `preset_menus_say_they_are_photocrafts_own` hovers the caption for Levels, Exposure, Black & White and Channel Mixer and checks the explanation appears. The preset list, live preview, one-step history and Custom detection are unchanged, and nothing presents the values as Photoshop-equivalent. No remaining defect found.\n",
"commit": {
"oid": "20803d1f15900ed6ecc7b5c79b4c68f6c3d6dcce"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"adjustment_preset_menus_load_presets_and_read_custom_after_an_edit",
"preset_row",
"kind_editor",
"properties_preset_menus_load_every_setting_in_one_step",
"preset_menus_say_they_are_photocrafts_own",
"mode",
"normal",
"presets_apply_and_are_recognised",
"presets_give_their_looks"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9282"
},
{
"number": 9172,
"title": "Gradient Fill layer: default to Foreground to Transparent (#8708)",
"url": "https://github.com/storytold/photocraft/pull/9172",
"body": "Fixes #8708.\n\nA new Gradient Fill layer (Layer › New Fill Layer › Gradient…, or the Layers panel's fill button) was an opaque gradient from the foreground to the background colour. With the default white background it covered the layers below with white at one end, which is what the report calls \"a solid white background instead of transparency\".\n\n### Anything a reviewer should look at first\n\n- **The target is Photoshop's Foreground to Transparent.** In Photoshop 2025 (26.2) on this Mac, Layer › New Fill Layer › Gradient… opens with the foreground fading to transparent (Linear, 90°). Photoshop remembers that dialog's last gradient, and I couldn't rule out that this is a remembered choice without resetting Photoshop's preferences, which I didn't do. I'm taking it as the default because it's the closest case I observed, and it's what the reporter expects.\n- **How it's built:** the foreground at both colour stops plus transparency stops 100% → 0% (`opacity_stops`), the same shape a PSD stores for Photoshop's Foreground to Transparent. An explicit `to` keeps the old two-colour gradient, so scripts that pass both colours don't change.\n\n### What changes\n\n- `crates/engine/src/commands.rs` (`layer.newFillLayer.gradient`): with no `to`, colour stops `from`/`from` plus transparency stops `(0, 100%)`, `(1, 0%)`. The params doc says so.\n- New test `gradient_fill_cmds::tests::new_gradient_fill_layer_defaults_to_foreground_to_transparent`.\n\n### Tests\n\n- The new test checks the default stops, and that a `to` colour still gives a two-colour gradient with no transparency stops. It fails on main (the second stop is the background colour) and passes with the fix.\n- `cargo fmt --check`, `cargo clippy -p photocraft-engine --all-targets -- -D warnings`, `cargo test -p photocraft-engine --lib --tests` (2184 + 277 and the rest passed), `panic_hunt` (`--ignored`), `cargo xtask layers` and `cargo xtask wasm` pass.\n\n### Verification\n\nA 200×100 transparent document, foreground (51, 102, 153), background white, a new Gradient Fill layer with the defaults. Pixel at x = 100, from the top (y = 0, 25, 50, 75, 99):\n\n| | main | this PR | Photoshop 2025 (Foreground to Transparent, Linear 90°) |\n|---|---|---|---|\n| Colour | white → (53, 104, 154) | (51, 102, 153) throughout | (51, 102, 153) throughout |\n| Alpha | 255 everywhere | 0, 64, 128, 191, 252 | 0, 52, 128, 203, 254 |\n\nThe difference in alpha at the quarter points comes from Photoshop's \"Smooth\" gradient method, which eases the ramp. PhotoCraft's gradient fills are linear for every gradient, two-colour ones too, so that's a separate change and not part of this PR.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "8ebb04999d25790f71f9bd88f7a268a6a2754aec",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/commands.rs"
},
{
"path": "crates/engine/src/gradient_fill_cmds/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40118261859/job/120944045597"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40118261830/job/120944045591"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40118261859/job/120944045626"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40118261859/job/120944045609"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40118261859/job/120944045419"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40118261859/job/120944046402"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40118261859/job/120944108169"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40118261859/job/120944107332"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T18:18:17Z",
"url": "https://github.com/storytold/photocraft/pull/9172#pullrequestreview-5639390920",
"body": "All checks are green at 8ebb049. I read the full non-test diff (one command in commands.rs). With no to (absent or null), the new Gradient Fill layer now uses the from colour at both ends and opacity stops (0,1) to (1,0), which is Photoshop Foreground to Transparent; an explicit to keeps the two-colour behaviour. The from default stays the foreground and the usage string is updated to match. No defects found.\n",
"commit": {
"oid": "8ebb04999d25790f71f9bd88f7a268a6a2754aec"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"new_gradient_fill_layer_defaults_to_foreground_to_transparent"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9172"
},
{
"number": 9110,
"title": "test: GPU tests honour WGPU_BACKEND, so they run on Windows with an Intel GPU (#4)",
"url": "https://github.com/storytold/photocraft/pull/9110",
"body": "I ran into this while working on #8974 (the #8764 Smart Filters fix): the targeted tests passed, but running the full suite locally before opening that PR crashed every time on my laptop.\n\nOn Windows with an Intel GPU, `cargo test` crashes with `STATUS_ACCESS_VIOLATION` in the first GPU test (e.g. ui-egui's `background_redraw` tests, then `adjust_preview_gpu`). It's the Intel Vulkan driver crash from #4: the app leaves Vulkan out on Intel, but the tests use wgpu's defaults, and creating an instance with Vulkan in it loads `igvk64.dll`.\n\n`WGPU_BACKEND=dx12` avoids it for the kittest harnesses (egui reads it), but seven tests and four benches build their own instance with `wgpu::Instance::default()`, which takes every backend and ignores the environment, so they still crashed.\n\n## Change\n\n- Those 11 sites use `wgpu::InstanceDescriptor::new_without_display_handle_from_env()`: the same defaults as `Instance::default()` plus `WGPU_BACKEND` and the other `WGPU_*` overrides. Where nothing is set (CI) nothing changes. This is what `parity.rs`'s own `gpu()` helper already does; these were the sites that didn't.\n- `docs/development.md` (`WGPU_BACKEND` row) and the driver notes in `testkit/src/gpu.rs` say to run tests with `WGPU_BACKEND=dx12` on Intel Windows machines.\n\n## Results\n\nIntel UHD Graphics 620, driver 27.20.100.9664, Windows 11:\n\n| | before | after, `WGPU_BACKEND=dx12` |\n|---|---|---|\n| `cargo test -p photocraft-ui-egui --lib` | access violation | 3712 passed |\n| `cargo test -p photocraft-ui-egui --test it` | access violation (`adjust_preview_gpu`) | 130 passed |\n| `cargo test -p photocraft-gpu --test it` | access violation | 83 passed, 4 failed (below) |\n\n`cargo fmt --check` and `cargo clippy -p photocraft-gpu -p photocraft-ui-egui -p photocraft-testkit --all-targets -- -D warnings` are clean.\n\n## Found along the way (not changed here)\n\nOn this GPU over DX12, four `parity` tests fail in **paged** mode with garbage GPU values, not small differences:\n`documents_larger_than_the_texture_limit`, `growing_layer_keeps_its_resident_tiles`, `pages_are_evicted_under_the_budget`, `shared_tiles_upload_once_and_copy`. For example:\n\n viewport (paged): max diff inf/255 at (442,516): cpu [0.516, 0.700, 0.313, 1.0] gpu [2948133000000.0, -0.214, -0.043, 2.75e31]\n\nThey couldn't run here before (Vulkan crashed first). The driver is from 2021, so this may be a driver bug, but since the app uses DX12 on Intel by default it could affect large documents on Intel laptops. I can open a separate issue, and test again after a driver update, if that helps.\n\nPossible follow-up: tests could pick DX12 + GL on Intel Windows automatically, as `gpu_startup::windows_auto_backends` does for the app, instead of relying on `WGPU_BACKEND`. Happy to do that if you'd like it.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "2e716f93b2e2135944e07d821be4eb38fce13ec9",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/gpu/examples/bench_stack_reuse.rs"
},
{
"path": "crates/gpu/tests/it/device_loss.rs"
},
{
"path": "crates/gpu/tests/it/parity.rs"
},
{
"path": "crates/gpu/tests/it/timing.rs"
},
{
"path": "crates/gpu/tests/it/uniform_limits.rs"
},
{
"path": "crates/gpu/tests/it/vector_mask.rs"
},
{
"path": "crates/testkit/src/gpu.rs"
},
{
"path": "crates/ui-egui/examples/bevel_bench.rs"
},
{
"path": "crates/ui-egui/examples/fx_bench.rs"
},
{
"path": "crates/ui-egui/examples/interactive_bench.rs"
},
{
"path": "crates/ui-egui/tests/it/adjust_preview_gpu.rs"
},
{
"path": "crates/ui-egui/tests/it/layout_perf.rs"
},
{
"path": "docs/development.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392275125"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880747/job/120854409008"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880455/job/120854408292"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880741/job/120854408980"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392273678"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880747/job/120854409247"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392361353"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880747/job/120854409249"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392273790"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880747/job/120854409194"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880747/job/120854409714"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392329296"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392326603"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880747/job/120854474921"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880747/job/120854474924"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392344063"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40087880747/job/120854474913"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392275083"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561739765/job/122392275583"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:41:50Z",
"url": "https://github.com/storytold/photocraft/pull/9110#pullrequestreview-5639002103",
"body": "Read the change: wgpu instances built in examples and the testkit note use the env-aware InstanceDescriptor so WGPU_BACKEND is honoured. No behaviour change in the app. CI is green. Approving.\n",
"commit": {
"oid": "2e716f93b2e2135944e07d821be4eb38fce13ec9"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Platform and reliability",
"new_test_names": [],
"regression_source": "https://github.com/storytold/photocraft/pull/9110"
},
{
"number": 9019,
"title": "fix(xtask): reject unknown performance benchmark selections",
"url": "https://github.com/storytold/photocraft/pull/9019",
"body": "## Summary\n\n`cargo xtask perf --bench <typo>` silently filtered out every benchmark, returned success and could update the baseline without taking any measurements. Requested names were never checked against the declared benchmarks.\n\n```diff\n perf --bench does-not-exist --update-baseline\n- succeeds without measuring and writes an empty baseline\n+ reports the unknown name before any build or output write\n```\n\nValidate every requested `--bench` name against `perf/budgets.toml` before planning the run. This also catches mixed valid/invalid selections. Known benchmarks skipped by quick mode or missing source requirements keep their existing behavior.\n\n## Evidence\n\n- Before: the actual command exited 0 with no benchmarks, both with `--reuse` and with `--skip-build`. The new regression expected an error but got `Ok(())`; its temporary baseline was overwritten.\n- After: all 68 xtask tests pass. The regression exercises `perf::run` in full and quick modes and checks that the baseline stays unchanged.\n- `cargo fmt --all -- --check`\n- `cargo clippy -p xtask --all-targets -- -D warnings`\n- `cargo xtask layers`: 31 crates, no violations.\n- CLI checks reject unknown-only and mixed selections without changing reports or baseline. A known benchmark without quick arguments remains accepted in quick mode.\n\n## Merge Danger\n\n**Door:** two-way\n\n**Blast Radius:** benchmark-tooling\n\nOnly xtask argument validation and its documentation change. No dependencies or performance measurements change.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "364c4be1c19728aefa601ee924bc7a0cd6a09789",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "docs/development.md"
},
{
"path": "xtask/src/perf.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561683241/job/122392143868"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041718945/job/120714101024"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041718717/job/120714100184"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041719035/job/120714101175"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561683241/job/122392079655"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041718945/job/120714101065"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561683241/job/122392161028"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041718945/job/120714100819"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561683241/job/122392078838"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041718945/job/120714101005"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041718945/job/120714101689"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561683241/job/122392155517"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041718945/job/120714218180"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561683241/job/122392081109"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40041718945/job/120714219299"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561683241/job/122392080988"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:33:36Z",
"url": "https://github.com/storytold/photocraft/pull/9019#pullrequestreview-5638973719",
"body": "gate and SonarCloud pass at this head. Read the full diff: xtask perf now rejects a --bench name that is not in the budgets file before anything runs, so an unknown name can no longer update the baseline; the test checks both a lone unknown and a known-plus-unknown request leave the baseline file untouched. No defect found.\n",
"commit": {
"oid": "364c4be1c19728aefa601ee924bc7a0cd6a09789"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Platform and reliability",
"new_test_names": [
"unknown_bench_is_rejected_before_updating_the_baseline"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9019"
},
{
"number": 9015,
"title": "fix(xtask): reject leading zeroes in numeric prerelease identifiers",
"url": "https://github.com/storytold/photocraft/pull/9015",
"body": "## Summary\n\n`cargo xtask version set 1.0.0-01` accepted an invalid numeric prerelease identifier, wrote it into the workspace manifest, then failed when Cargo parsed that manifest. The prerelease validator checked allowed characters but omitted the numeric leading-zero rule.\n\n```diff\n version set 1.0.0-01\n- writes an invalid Cargo.toml before Cargo rejects it\n+ returns a validation error before writing\n```\n\nReject numeric identifiers such as `01`, including after a dot. Keep valid identifiers such as `0`, `rc.0`, `01a` and `01-2`.\n\n## Evidence\n\n- Before: the new regression test failed with `validator accepted invalid version 1.0.0-01`. Cargo independently rejected temporary manifests with `invalid leading zero in pre-release identifier`.\n- After: all 68 xtask tests pass, including the regression.\n- `cargo fmt --all -- --check`\n- `cargo clippy -p xtask --all-targets -- -D warnings`\n- `cargo xtask layers`: 31 crates, no violations.\n- CLI checks for `1.0.0-01`, `1.0.0-rc.01` and `1.0.0-00.1` return errors and leave both Cargo.toml and Cargo.lock byte-for-byte unchanged.\n\n- Final review: Grok 4.7 with high reasoning effort reported no blocking findings.\n\n## Merge Danger\n\n**Door:** two-way\n\n**Blast Radius:** release-tooling\n\nOnly xtask version validation changes. No dependencies or product crates change.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "afb4364a8795c42e6353762891b4890d878f9a68",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "xtask/src/version.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681542/job/122392149476"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40039324095/job/120706411706"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40039323951/job/120706410902"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681542/job/122392074353"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40039324095/job/120706412026"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681542/job/122392073768"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40039324095/job/120706412044"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681542/job/122392154752"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40039324095/job/120706411884"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40039324095/job/120706413019"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681542/job/122392159351"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40039324095/job/120706503707"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681542/job/122392157100"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40039324095/job/120706504661"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681542/job/122392126189"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:33:42Z",
"url": "https://github.com/storytold/photocraft/pull/9015#pullrequestreview-5638974295",
"body": "gate and SonarCloud pass at this head. Read the full diff: version validation now rejects numeric pre-release identifiers with leading zeroes (1.0.0-01, rc.01) as semver requires, while 0, 01a and 01-2 stay valid, and an empty pre-release still fails because it yields an empty identifier; tests cover both lists. No defect found.\n",
"commit": {
"oid": "afb4364a8795c42e6353762891b4890d878f9a68"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Platform and reliability",
"new_test_names": [
"rejects_numeric_prerelease_identifiers_with_leading_zeroes"
],
"regression_source": "https://github.com/storytold/photocraft/pull/9015"
},
{
"number": 9004,
"title": "fix(ui): enlarge Move tool alignment icons for legibility",
"url": "https://github.com/storytold/photocraft/pull/9004",
"body": "## Summary\n\nImproves the legibility of the Move tool's alignment controls by increasing icon and button sizes.\n\nFixes #8849.\n\n## Changes\n\n- Increase alignment icon size from 12 pt to 20 pt.\n- Increase button size from 24 pt to 28 pt.\n- Keep existing alignment commands and enabled/disabled behavior unchanged.\n- Limit the change to `crates/ui-egui/src/align_bar.rs`.\n\n## Validation\n\n- Targeted alignment-command test: 1 passed.\n- Rust formatting and Git diff checks passed.\n- No changes to alignment logic or command dispatch.\n\n## Scope\n\nThis is a small, UI-only readability improvement. Visual verification across different display scales and operating systems remains welcome.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "39cf823cdfe486bb967cf81df725d229f7eed903",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/align_bar.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681135/job/122392072055"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652143/job/120677796001"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652233/job/120677796075"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681135/job/122392146376"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652143/job/120677796151"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681135/job/122392156737"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652143/job/120677796018"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681135/job/122392142455"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652143/job/120677795828"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652143/job/120677797529"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681135/job/122392071471"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681135/job/122392073255"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652143/job/120677930878"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652143/job/120677930744"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561681135/job/122392073105"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40030652143/job/120677930692"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:33:48Z",
"url": "https://github.com/storytold/photocraft/pull/9004#pullrequestreview-5638974843",
"body": "gate, SonarCloud and the Windows jobs pass at this head. Read the full diff: the align bar and its ellipsis button draw 20 pt icons in 28 pt hit targets through button_with_icon_size, with enabled state and command dispatch unchanged. No defect found.\n",
"commit": {
"oid": "39cf823cdfe486bb967cf81df725d229f7eed903"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [],
"regression_source": "https://github.com/storytold/photocraft/pull/9004"
},
{
"number": 8986,
"title": "Path tools: edit the saved path selected in the Paths panel (#8478)",
"url": "https://github.com/storytold/photocraft/pull/8986",
"body": "Path Selection, Direct Selection, Free Transform Path and the anchor tools now edit the saved path selected in the Paths panel.\n\nThe selected saved path was drawn on the canvas as the target path (#6550), but the tools only looked at the vector mask, the active shape layer's path and the work path. So \"Path 1\" in the attached `path_layers_no_select.psd` showed up and could not be selected or changed. Direct Selection gets a `PathRef::Saved` target, between the layer path and the work path, and Path Selection gets `PathTarget::Saved`. The `path.*` commands already accepted a saved path's name, so nothing changed in the engine.\n\nTested:\n- New `crates/ui-egui/src/saved_path_edit_tests.rs` (8 tests): dragging an anchor (one history step, undo restores it), marquee select, Path Selection moving the whole path, Free Transform Path params, Add/Delete Anchor Point, an unselected saved path left alone, the saved path winning over a work path on the same spot, and a synthetic PSD written and read back through `photocraft_io` whose saved path is then edited. The drag and Path Selection tests fail on main.\n- The reporter's file locally (not committed): on main a drag on Path 1's first anchor (1475, 2060) does nothing and adds no history step; with this change it moves to (1482, 2063) in one step.\n- `cargo test -p photocraft-ui-egui --lib --tests` (3720 + 130 passed), `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`, `cargo fmt --check`, `cargo xtask layers`.\n\nFixes #8478",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "390dfbbcb026b33d46c52c62360b65deca00f258",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/anchor_tools.rs"
},
{
"path": "crates/ui-egui/src/direct_select.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/saved_path_edit_tests.rs"
},
{
"path": "crates/ui-egui/src/vector_ui.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561673083/job/122392042918"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517040/job/120663964490"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517207/job/120663964583"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561673083/job/122392042065"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517040/job/120663964747"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561673083/job/122392105250"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517040/job/120663964656"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561673083/job/122392104860"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517040/job/120663964235"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517040/job/120663966488"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561673083/job/122392040941"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561673083/job/122392127594"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517040/job/120664138047"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517040/job/120664138011"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561673083/job/122392115111"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40026517040/job/120664137958"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:36:01Z",
"url": "https://github.com/storytold/photocraft/pull/8986#pullrequestreview-5638981860",
"body": "gate, SonarCloud and the Windows jobs pass at this head. Read the full diff: PathRef gains a Saved(index) variant for the saved path selected in the Paths panel, ranked before the work path in active_paths and addressed to the engine by name, so a stale index resolves to an empty name that the engine refuses rather than editing another path. Arrow-key nudging now drops the selection when its path is no longer among the candidates, and every params() call site passes the app. The seven new tests cover direct selection, marquee, ordering against the work path, path selection, free transform, add/delete anchor and a PSD round trip. No defect found.\n",
"commit": {
"oid": "390dfbbcb026b33d46c52c62360b65deca00f258"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"app",
"square",
"with_saved_path",
"saved",
"anchors",
"steps",
"drag",
"direct_selection_drags_an_anchor_of_the_selected_saved_path",
"direct_selection_marquee_selects_and_arrow_params_name_the_saved_path",
"the_saved_path_is_only_edited_while_the_paths_panel_selects_it",
"the_selected_saved_path_comes_before_the_work_path",
"path_selection_moves_the_selected_saved_path",
"free_transform_path_names_the_selected_saved_path",
"add_and_delete_anchor_point_edit_the_selected_saved_path",
"a_saved_path_read_from_a_psd_is_editable",
"arrows_leave_a_saved_path_the_paths_panel_no_longer_selects"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8986"
},
{
"number": 8985,
"title": "Color panel and Color Picker: clip the round cursor to the colour field",
"url": "https://github.com/storytold/photocraft/pull/8985",
"body": "Fixes #8968.\n\nThe round cursor of the Color panel's saturation/brightness field was painted with an unclipped painter. At the extremes its centre sits on the field's edge, so the 6.5 px ring spilled over the hue strip and the chips. The cursor is now clipped to the field. Only the drawing changes: the s/v computed from the pointer still uses the whole field, so the corners still give s/v = 0/1. The Color Picker window's field drew its marker the same way, so it gets the same clip.\n\nValidation:\n- `color_field_cursor_stays_inside_the_field`: fails on main (cursor ring [[49 123] - [59 133]] leaves the field [[54 8] - [270 128]]), passes here\n- `field_marker_stays_inside_the_field` (Color Picker window): fails on main, passes here\n- `cargo test --workspace --lib -- color_field_cursor_stays`, `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`\n- checked the rendered panel at the bottom-left corner\n- (macOS, Apple silicon)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "8e695caf885b529f01f482b3f0c90cdb34df1915",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/color_picker_ui.rs"
},
{
"path": "crates/ui-egui/src/panels.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498695/job/120891489820"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498708/job/120891489648"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498695/job/120891489878"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498695/job/120891489723"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498695/job/120891489690"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498695/job/120891490614"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498695/job/120891562336"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498695/job/120891562343"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40100498695/job/120891562465"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:33:54Z",
"url": "https://github.com/storytold/photocraft/pull/8985#pullrequestreview-5638975214",
"body": "gate, SonarCloud and the Windows jobs pass at this head. Read the full diff: the round cursor of the Color Picker field and of the Color panel field is painted through a painter clipped to the field so it cannot spill onto the hue strip or chips at the extremes; both new tests find the cursor circle and check its visible rect stays inside the field. No defect found.\n",
"commit": {
"oid": "8e695caf885b529f01f482b3f0c90cdb34df1915"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"field_marker_stays_inside_the_field",
"color_field_cursor_stays_inside_the_field"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8985"
},
{
"number": 8931,
"title": "feat(algo): describe the Gaussian blur as data for other devices",
"url": "https://github.com/storytold/photocraft/pull/8931",
"body": "Part of #5772 (Phase 1, step 1.1 of [the plan](https://github.com/storytold/photocraft/issues/5772#issuecomment-6363558247)): the CPU side of the GPU blur operator. Independent of the Phase 0 PRs (#8848, #8852, #8859).\n\n## Why\nPhase 1 runs Gaussian Blur previews on the GPU as a compositor *layer operator*. The GPU must blur exactly as the CPU does: the same kernel weights or box widths, the same reach and the same edge behaviour. The CPU stays the oracle for parity tests and the fallback, and it applies OK. This PR makes that a single source of truth rather than two copies kept in step by hand.\n\n## Change (`crates/algo`, new `separable.rs`)\n- **`Separable`**: the blur as data. It holds either `Kernel(weights)` (radius ≤ 4) or `Boxes(widths)` (above 4), taken from `blur.rs`'s own `gaussian_kernel` and `box_widths`. `reach()` says how far an output pixel reads.\n- **The CPU Gaussian is now built from it.** `blur::gaussian` picks its passes from `Separable::of`, so a change to the kernel (for example #8770's small-radius fix) reaches the GPU automatically. Behaviour is unchanged.\n- **`OperatorRegion { reach, extent }`** is the region rule: `input(out)` gives the source pixels an output area reads. That is the area grown by the reach, inside the extent; beyond the extent the edge repeats.\n- **`edge_extent(canvas, content)`** is the edge-repeat extent the engine already used for layer filters (canvas ∪ layer content). `engine/src/filters.rs` now calls it, so the CPU and the GPU operator share it.\n\n## Tests\n- `the_description_is_the_cpus_blur`, for radii 0.01, 0.3, 0.8, 2, 4, 4.5, 12, 40 and 250, checks that:\n - each radius uses the CPU's kernel or box widths;\n - kernels are normalised and odd-length, and box widths are odd;\n - the reach never exceeds the CPU's own halo.\n- `an_impulse_spreads_no_further_than_the_reach` blurs one opaque pixel on the CPU at each radius and checks that nothing spreads past the reach. The exact kernel spreads exactly that far.\n\n One finding along the way: past the reach, the box passes' running sums leave float residue of around 1e-8. That is far below a 16-bit level, so it's ignored, and the comment says so.\n- `output_depends_only_on_the_input_region` replaces every pixel outside `input(out)` with other noise and gets the same blurred output. It covers areas in the middle and at both canvas corners, where the edge repeats.\n- `cargo test -p photocraft-algo -p photocraft-engine --lib --tests`: all pass (640 + 2,090 + 267 + …)\n- `cargo clippy -p photocraft-algo -p photocraft-engine --all-targets -- -D warnings`, `cargo fmt`, `cargo xtask layers`, `cargo xtask wasm`: clean\n\n## Next in Phase 1\nThe GPU operator itself, in `crates/gpu`, with GPU-vs-CPU parity tests against this description.\n\nThe investigation for that step changed three details of the plan:\n- **Fragment ping-pong passes, as the layer-effect maps already use, rather than compute.** The compositor has no compute pipelines, and fragment passes keep GL in reach.\n- **The planner must know the operator's grown bounds,** because its clipping and occlusion use the layer's unblurred bounds.\n- **Operator fingerprints are folded into the stack-prefix comparison,** which today compares only the `Document`.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "d42dd4c6302c77c1f9dc46b7b53ffa5eb0047a13",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/algo/src/blur.rs"
},
{
"path": "crates/algo/src/lib.rs"
},
{
"path": "crates/algo/src/separable.rs"
},
{
"path": "crates/engine/src/filters.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518743091"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008486/job/122518744087"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008486/job/122518803661"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518743450"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008486/job/122518804049"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518743384"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518743351"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008486/job/122518802225"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518744372"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518745332"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518745057"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518935475"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40598008339/job/122518933173"
},
{
"name": "CodeRabbit",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://app.coderabbit.ai/change-stack/storytold/photocraft/pull/8931?scope=ghh_OusAJGA4HG8ljoOV0wJvVx8NmnZIjc2DdjmxOu0xZWA"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:59:36Z",
"url": "https://github.com/storytold/photocraft/pull/8931#pullrequestreview-5639059166",
"body": "Read the change: Separable::of returns the same choice the CPU blur made before (the box widths when box_widths gives them, else the exact kernel), gaussian() now builds its passes from that description, so behaviour is unchanged for Gaussian Blur, and the None arm only covers a non-Gaussian that never reaches it. reach() saturates, OperatorRegion::input grows by the reach inside the extent and is empty out of range, and edge_extent replaces the doc_bounds.union(content) at both call sites with the same value. The tests check the kernel and box choice per radius, that an impulse spreads no further than the reach, and that output depends only on the input region. All checks are green. Approving.\n",
"commit": {
"oid": "d42dd4c6302c77c1f9dc46b7b53ffa5eb0047a13"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"the_description_is_the_cpus_blur",
"an_impulse_spreads_no_further_than_the_reach",
"output_depends_only_on_the_input_region"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8931"
},
{
"number": 8905,
"title": "fix(plugins): bound a run's total plug-in memory, not just each instance",
"url": "https://github.com/storytold/photocraft/pull/8905",
"body": "A filter run gives each band a fresh wasmi instance capped at 512 MiB of linear memory, and ran one band per rayon worker. The cap was therefore per instance times the thread count: one run on a 16-thread machine could hold about 8 GiB, and a plug-in smart filter repeated that on every re-render.\n\nLimits gains max_total_memory_bytes (default 2 GiB), the budget for all of one run's instances together, and Limits::parallel_bands(threads), which allows only as many concurrent bands as max_memory_bytes fits into that budget (at least one). Plugin::apply uses it to size its groups of parallel bands, so with the defaults a run holds at most four instances at the cap however many cores there are. Plug-ins configured with a smaller per-instance cap keep every worker busy. The limit is listed in docs/plugins.md.\n\nTests (crates/plugins/tests/abi.rs):\n- parallel_bands_fit_the_total_memory_budget: 16 workers with the default limits run 4 bands at once, not 16 (failed before: 16 != 4).\n- one_band_at_a_time_matches_parallel_bands: a budget that holds one instance still covers every band on an 8-thread pool, with the same pixels as parallel bands.\n\nFixes #8603",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "0fdc6259193aab32b8feab7347ddc36196f40a19",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/plugins/src/apply.rs"
},
{
"path": "crates/plugins/src/runtime.rs"
},
{
"path": "crates/plugins/tests/abi.rs"
},
{
"path": "docs/plugins.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120507777494"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953984/job/120507776063"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980954842/job/120507778143"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120507777480"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120507777393"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120507777199"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120507777475"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120507779237"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120507873979"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120507874217"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980953969/job/120516559764"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:38:23Z",
"url": "https://github.com/storytold/photocraft/pull/8905#pullrequestreview-5638989196",
"body": "Read the non-test source: the total-memory limit and parallel_bands cap bound how many plug-in bands run at once, and the arithmetic is saturating. CI is green (tests on Ubuntu and Windows, Sonar). Approving.\n",
"commit": {
"oid": "0fdc6259193aab32b8feab7347ddc36196f40a19"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"parallel_bands_fit_the_total_memory_budget",
"one_band_at_a_time_matches_parallel_bands"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8905"
},
{
"number": 8903,
"title": "Box-filter minified patterns in the CPU and GPU samplers (#8831)",
"url": "https://github.com/storytold/photocraft/pull/8903",
"body": "Fixes #8831.\r\n\r\nBelow 100 % a pattern tile was sampled with one bilinear tap per document pixel, so a shrunk tile aliased into full-contrast stripes and moiré. Now each pixel averages an `n × n` grid of bilinear taps spread over its footprint, with `n = ceil(1 / scale)` capped at 8. That box-filters the tile at the minification ratio. At 100 % and above `n` is 1 and the old single-tap code runs unchanged, so integer placements stay byte-exact (`identity_placement_tiles_exactly` and the overlay tests still pass as they are).\r\n\r\n- **CPU** (`crates/compose/src/pattern.rs`): the bilinear tap moves into `Tile::sample_premul`, and `Tile::sample` becomes that tap plus unpremultiply. The new `Tile::sample_area(place, x, y)` averages the taps in premultiplied space, then unpremultiplies. `pattern::render` uses it, and so do the two direct sampling loops in `effects.rs` (pattern paint for Pattern Overlay and pattern strokes, and the bevel texture). Every entry point the issue lists goes through one of these: pattern fill layers, Pattern Overlay, pattern strokes, `edit.fill` and the Paint Bucket with a pattern, and the Pattern Stamp.\r\n- **GPU** (`crates/gpu/src/compose.wgsl`): `pattern_sample` mirrors this. The bilinear tap is now `pattern_tap`, and `pattern_sample` averages the same tap grid with the same cap and offsets, so it stays pixel-identical to the CPU. Pattern paints and the bevel texture both call it.\r\n\r\nTests:\r\n- `pattern::tests::minified_pattern_is_filtered_not_aliased`: 1 px black/white stripes at 25 %, with a phase that puts every pixel centre on a white tile pixel. One tap returns pure white, which the test checks first. The filtered result is about 0.5, the local mean. The 100 % case of the same tile stays exactly 0 / 1.\r\n- `gpu` parity `pattern_fill_layers` gets a 30 % case (`n = 4`), on top of the existing 70 % case, so the WGSL tap grid is checked against the CPU.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "d387f52f1f2f5681b2c7d1c517aa5b5992c7bc88",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/effects.rs"
},
{
"path": "crates/compose/src/pattern.rs"
},
{
"path": "crates/gpu/src/compose.wgsl"
},
{
"path": "crates/gpu/tests/it/parity.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561642583/job/122391937207"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507164515"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520478900"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775457/job/120507165613"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561642583/job/122391997674"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507164440"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520478773"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507164389"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520478809"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561642583/job/122391935957"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507164448"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520478609"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561642583/job/122391936601"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507164110"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520478870"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507166117"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520480268"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561642583/job/122391983971"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561642583/job/122391937341"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507249313"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520562904"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507248925"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520562886"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507248930"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520562845"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561642583/job/122391987399"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120507249086"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120520562913"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39980775023/job/120519912722"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39984625464/job/120532088446"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:38:30Z",
"url": "https://github.com/storytold/photocraft/pull/8903#pullrequestreview-5638989541",
"body": "Read the CPU change: sample_area averages n x n bilinear taps with n = ceil(1/scale) capped at 8, offsets are centred, unpremultiply handles zero alpha, and 100% stays a single exact tap. The WGSL mirror reads consistently with it, though I did not run the GPU path. The new test covers the aliasing case. CI is green. Approving.\n",
"commit": {
"oid": "d387f52f1f2f5681b2c7d1c517aa5b5992c7bc88"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"sample_premul",
"pixel_taps",
"tap_offset",
"unpremultiply",
"minified_pattern_is_filtered_not_aliased",
"pattern_tap",
"pattern_sample"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8903"
},
{
"number": 8897,
"title": "fix(io): read linked smart objects for PSD export as regular files only",
"url": "https://github.com/storytold/photocraft/pull/8897",
"body": "Saving a PSD embeds the file a linked smart object names, and it read that file with a bare std::fs::read. A link that names a FIFO blocked the save forever (opening a FIFO for reading waits for a writer), and one that names a device such as /dev/zero read until memory ran out. Every other linked-file read already goes through photocraft_format::read_file, which opens without blocking, refuses anything that is not a regular file and reads in bounded chunks; the PSD exporter now uses it too. Such a layer is written as pixels with a warning that says why, as for any other unreadable link.\n\nThe new test psd_export::tests::a_linked_fifo_is_not_read exports a document whose linked smart object points at a FIFO. Before the fix the export never returned (the test times out after 30 s); now it finishes and warns that the path is not a regular file.\n\nFixes #8601",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "eb06798f15516eb7e4fac24c98346fd9052e7f8e",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/format/src/read.rs"
},
{
"path": "crates/io/src/psd_export.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529186573"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987152244/job/120529188126"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529186124"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529186655"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529186598"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529186636"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529188679"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529269331"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529269256"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529269719"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120529269201"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39987151788/job/120542890279"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:38:48Z",
"url": "https://github.com/storytold/photocraft/pull/8897#pullrequestreview-5638990476",
"body": "Read the bounded read_all_with: the total limit is checked up front against the hint, a probe read finds the end without over-allocating, try_reserve failures map to OutOfMemory, and a reader that never ends hits FileTooLarge. The PSD linked-file read now goes through the bounded reader. Tests cover the limit edge and the exact hint. CI is green. Approving.\n",
"commit": {
"oid": "eb06798f15516eb7e4fac24c98346fd9052e7f8e"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"read_path",
"read_all_with",
"too_large",
"out_of_memory",
"reads_stop_at_the_total_limit",
"an_exact_hint_is_not_over_allocated",
"a_linked_fifo_is_not_read"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8897"
},
{
"number": 8896,
"title": "Select and Mask: Photoshop's full-window workspace (#7069, #8148)",
"url": "https://github.com/storytold/photocraft/pull/8896",
"body": "Select › Select and Mask… now opens a full-window workspace laid out like Photoshop's, replacing the parameter dialog.\n\n- **Options bar:** the current tool, Add/Subtract, Size, Hardness (Brush), Sample All Layers, Select Subject, and Refine Hair (greyed out, with a tooltip saying why: #2671).\n- **Tools:** Quick Selection (W), Refine Edge Brush (R), Brush (B), Object Selection (W), Lasso and Polygonal Lasso sharing one slot (L), Hand (H), Zoom (Z). Shift+W and Shift+L cycle the shared slots.\n- **View:** the document under its tab, shown in the chosen view mode. The view keys work as in the old dialog: O M V A T K Y, F cycles, hold X for the original image.\n- **Properties:** Remember Settings; View Mode (views with their letters, Show Edge J, Show Original P, Real-Time Refinement, High Quality Preview, Transparency/Opacity, Overlay colour and Indicates); Refine Mode (Object Aware greyed out: #2788); Edge Detection; Global Refinements with Clear Selection and Invert; Output Settings; Reset, OK, Cancel.\n\nThe tools record `select.refineEdge` `edits`, and the Refine Edge Brush records `refineStrokes`. OK applies them as one history step, Cancel drops everything, and Ctrl+Z undoes one edit at a time. The preview runs in the background on a reduced copy (full size with High Quality Preview). It reuses the selection after the edits it has already applied, so a new stroke only costs that stroke. With Real-Time Refinement, the stroke being drawn joins the preview while the pointer moves. A targeted layer mask is refined and output to the mask. The control channel drives the workspace through `select.selectAndMask {\"ui\": {…}}` (documented in `docs/control-protocol.md`).\n\nScreenshots (rendered offscreen with the `snapshot` example, 1600 × 1000, after Select › Subject):\n\nOnion Skin, the default view, with Quick Selection:\n![Select and Mask workspace in the Onion Skin view](https://raw.githubusercontent.com/wanikhawar/photocraft/e01a58ce2a539c9e5448876e8441970979475a04/pr-8896/select-mask-onion.png)\n\nOverlay, with the Brush's Hardness in the options bar:\n![Select and Mask workspace in the Overlay view](https://raw.githubusercontent.com/wanikhawar/photocraft/e01a58ce2a539c9e5448876e8441970979475a04/pr-8896/select-mask-overlay.png)\n\nBlack & White, previewing Radius 4 px, Smart Radius and Smooth 12, with the Refine Edge Brush picked:\n![Select and Mask workspace in the Black & White view](https://raw.githubusercontent.com/wanikhawar/photocraft/e01a58ce2a539c9e5448876e8441970979475a04/pr-8896/select-mask-blackwhite.png)\n\nPhoto: [Meadow Anemone, Lake Andes National Wildlife Refuge](https://commons.wikimedia.org/wiki/File:Meadow_Anemone_Lake_Andes_National_Wildlife_Refuge_South_Dakota_(53923253226).jpg), USFWS (public domain).\n\nValidation:\n\n- `cargo test -p photocraft-ui-egui --lib --tests`: 3733 unit and 130 integration tests passed.\n- `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`\n- `cargo xtask layers`, `cargo xtask wasm`, `cargo fmt --all -- --check`, `git diff --check`.\n- `cargo xtask scorecard` and `cargo xtask parity` regenerate with no changes.\n- Ran the release build on Linux (Hyprland, Vulkan) and painted with the workspace's tools.\n\nThe brush outline can stutter while painting on some Linux setups. Stroke traces show the same frame pacing with the ordinary canvas brush, so it is not specific to this workspace; it will be tracked in its own issue.\n\nCloses #7069. Closes #8148.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "19fa473039ef11deda7c3200c8fac1de41309086",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/canvas/select_mask_view.rs"
},
{
"path": "crates/ui-egui/src/canvas/tabs.rs"
},
{
"path": "crates/ui-egui/src/control/tests.rs"
},
{
"path": "crates/ui-egui/src/dialogs.rs"
},
{
"path": "crates/ui-egui/src/dialogs/distort_ui.rs"
},
{
"path": "crates/ui-egui/src/dialogs/select_mask_ui.rs"
},
{
"path": "crates/ui-egui/src/dialogs/select_mask_ui/tests.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/input/shortcuts.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/menus/invoke.rs"
},
{
"path": "crates/ui-egui/src/panels/layer_row_ui/edit_tests.rs"
},
{
"path": "crates/ui-egui/src/panels/mask_properties_tests.rs"
},
{
"path": "docs/control-protocol.md"
},
{
"path": "docs/gaps.md"
},
{
"path": "docs/scorecard.md"
},
{
"path": "docs/target-app-parity.md"
},
{
"path": "scorecard/tools.toml"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823642612"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690467803/job/122823640230"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468439/job/122823642559"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468439/job/122823719676"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823642952"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468439/job/122823720395"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823642752"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823642232"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468439/job/122823719036"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823644588"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823644605"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823644295"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823888431"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823888484"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40690468514/job/122823888454"
},
{
"name": "CodeRabbit",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://coderabbit.ai?utm_source=cr_app&utm_medium=github"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-28T13:25:04Z",
"url": "https://github.com/storytold/photocraft/pull/8896#pullrequestreview-5683297464",
"body": "Re-review at SHA 19fa473 (my last review was CHANGES_REQUESTED at 26d0b9c).@@GATE@@\n\nBoth findings are fixed.\n\n1. Modal gate: `distort_ui::modal_workspace` now returns `select.selectAndMask` while `app.distort.select_mask` is open, ahead of the Liquify arm, so unrelated menu and control invocations are refused through `menus::modal_allows`, and the existing `ui` continuation for the same workspace stays routed (`distort_ui.rs:188-189`). The regression coverage is in `control/tests.rs` and `select_mask_ui/tests.rs`.\n2. Preview memory: the preview no longer calls `mask_from_surface` over the whole canvas. `selected_pixels` counts per stored tile (an inverted selection starts from the area size and subtracts each stored tile before adding its coverage). `preview_ants` builds the dense mask only when the area is within `MAX_ANT_PIXELS` (4,000,000); above that, `sparse_preview_ants` walks the stored tiles only, omits ants when the default is selected, and gives up once the tiled work passes the cap or the edges pass `MAX_ANT_EDGES`. No allocation is sized by the canvas any more, and the sparse tests cover a large sparse document.\n\nWhat else changed since 26d0b9c: the 4af5b3a helper split and merged main content. I read the 119 differing PR-only lines in full.\n\nChecked: the tile arithmetic and bounds (out-of-area neighbours count as unselected, as in the dense path), the fallback paths, and that the new tests assert the refusal and the large-sparse case.\n\nNot verified: I did not run anything locally. The new tests were read, not executed.\n",
"commit": {
"oid": "19fa473039ef11deda7c3200c8fac1de41309086"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"overlay_colour_and_what_it_indicates",
"name",
"from_name",
"icon",
"tip",
"brushes",
"edits",
"default",
"mode_index",
"follow_decontaminate",
"refine_params",
"view_params",
"proxy_k",
"size",
"set_size",
"pick",
"stale",
"push",
"points_in",
"reject",
"add_edit",
"add_stroke",
"finish",
"end_gesture",
"close_polygon",
"begin",
"extend",
"undo",
"redo",
"reset",
"drop_stale",
"commit_params",
"remember",
"close",
"cancel_preview",
"cancel_preview",
"scale_edit",
"job",
"ant_edges",
"selected_pixels",
"preview_ants",
"sparse_preview_ants",
"compute",
"take_result",
"wait_preview",
"drive_preview",
"drive_preview",
"to_screen",
"to_doc",
"rect",
"upload",
"section",
"options_bar",
"properties",
"follow_revision",
"paint_canvas",
"paint_cursor",
"canvas_input",
"app_with_selection",
"app_sized",
"ws",
"ui",
"selected",
"cov",
"opens_like_photoshop",
"opening_needs_a_selection_or_a_layer_mask",
"tools_make_edits_and_undo_and_redo_them",
"object_selection_clicks_and_drags",
"ws_mut",
"buttons_and_reset",
"ok_is_one_step_and_matches_the_command",
"cancel_changes_nothing",
"remember_settings",
"decontaminate_outputs_to_a_new_layer",
"a_layer_mask_opens_on_the_mask",
"hostile_control_params_are_errors",
"high_quality_preview_is_full_size",
"edits_scale_to_the_reduced_copy",
"real_time_refinement_previews_the_stroke_being_drawn",
"a_refused_stroke_leaves_no_trail_or_preview",
"opening_again_keeps_the_open_workspace",
"the_preview_reuses_the_edits_it_applied",
"the_preview_recomputes_the_edits_after_the_document_changes",
"a_refused_request_changes_nothing",
"show_edge_shows_no_stroke_without_a_selection",
"a_preview_of_an_old_revision_is_dropped",
"press",
"keys_like_photoshop",
"polygonal_lasso_enter_and_escape",
"renders_photoshops_layout",
"panning_shows_the_hand",
"greyed_controls_explain_themselves",
"call",
"two_documents",
"fingerprint",
"ok_after_a_document_switch_changes_neither_document",
"control_preview_and_pointer_on_another_document_are_refused",
"ok_after_a_layer_switch_leaves_the_new_layer_alone",
"the_reason_a_stale_workspace_closed_is_translated",
"sparse_large_preview_counts_coverage_without_a_dense_canvas_mask",
"refine_row_opens_the_select_and_mask_workspace_and_color_range_dialog"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8896"
},
{
"number": 8892,
"title": "ui: larger Align icons and overflow button; Save for Web keeps its width on an invalid Matte",
"url": "https://github.com/storytold/photocraft/pull/8892",
"body": "Fixes three small UI problems. Each is a one- or two-line change.\n\n**Align icons (#8849).** The Move tool's align buttons drew their glyph at 52% of the 24 px box (12 px), so they were hard to read next to the other options-bar controls. They now use a 16 px glyph in the same 24 px box (`icons::button_with_icon_size`).\n\n**Document overflow button (#8834).** The `»` button at the end of the tab strip was a 12 px glyph in the dim text colour. It is now 16 px in the normal text colour, so it is easier to find. `overflow_button` is shared by the document and panel tab strips, so both get it.\n\n**Save for Web, PNG-8 Matte (#8830).** Emptying or mistyping the Matte field made the whole dialog jump wider. The preview then fails with `matte is \"none\" or \"#rrggbb\"`, and `web_pane` drew that error as a label *beside* the pane in the row of previews, which pushed the pane over and made the resizable dialog grow to its maximum width. The same message is already shown on the row above the panes, so the pane no longer draws it.\n\nBefore/after (PNG-8, Matte emptied, 1300x800 window): the dialog was 1260 px wide and now stays at 970 px. The error line above the panes is unchanged.\n\n**Not changed:** #8853 (eye icon of hidden layers). Photoshop leaves the eye column empty for a hidden layer, so the current behaviour matches it.\n\n**Testing:** `cargo test -p photocraft-ui-egui --lib -- align_bar tab_strip tabs_tests file_ui web_` (69 passed), `cargo fmt --check`, `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`, and offscreen `snapshot` renders before and after each change. UI only; no commands, layering or wasm changes.\n\nRefs #8849, #8834, #8830",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "9267c4fcf83a260d4bae0e441dc1af9bc48aa23b",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/align_bar.rs"
},
{
"path": "crates/ui-egui/src/file_ui.rs"
},
{
"path": "crates/ui-egui/src/tab_strip.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698048/job/120681755541"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698073/job/120681245686"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698048/job/120681755672"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698048/job/120681755572"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698048/job/120681755449"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698048/job/120681757115"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698048/job/120681893509"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698048/job/120681893445"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40031698048/job/120681893531"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-24T16:40:01Z",
"url": "https://github.com/storytold/photocraft/pull/8892#pullrequestreview-5638996509",
"body": "Read the change: the Align icons use a 16 px glyph in the 24 px button, the overflow chevron is 16 px at full text colour, and a Save for Web error no longer draws a label that widened the dialog. CI is green. Approving.\n",
"commit": {
"oid": "9267c4fcf83a260d4bae0e441dc1af9bc48aa23b"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [],
"regression_source": "https://github.com/storytold/photocraft/pull/8892"
},
{
"number": 8347,
"title": "Type: Roman Hanging Punctuation (trailing marks hang, don't steer the alignment)",
"url": "https://github.com/storytold/photocraft/pull/8347",
"body": "## Summary\n\nPhotoshop's Paragraph panel checkbox **Roman Hanging Punctuation**, per paragraph style:\n\n- **Layout** (`crates/text/src/layout.rs`): a trailing run of closing marks (`. , : ; ! ?` and their fullwidth twins `,.:;!?` plus `…`) trims out of the aligned width on center/right lines. A centered line ending in an ellipsis keeps its *text* centered and lets the marks hang past the anchor/box edge instead of pulling everything left; right-aligned content stays flush. A wrapped row of nothing but marks steps back onto the line above (merge-back): contiguous text ranges, the mark's glyphs on the merged row's baseline with manual kerning, the row range extended so carets and selection follow; known gaps: no optical kerning/decorations inside merged runs, no RTL, no vertical type (that's Burasagari's field).\n- **Doc**: `ParagraphStyle.hanging: bool`, serde default off — `.pcraft` round-trips automatically; `format` needs no edits.\n- **PSD**: `/Hanging` bool in the paragraph sheet, written right after `/LeadingType` as Photoshop's own writer places it (verified against the corpus files, which all carry `/Hanging false`); reading defaults off. Unknown PSD blocks stay verbatim.\n- **Engine**: `type.setStyle` takes `\"hanging\":bool` (params doc updated); the report exposes the style.\n- **UI**: checkbox at the bottom of the Paragraph section (Photoshop's place), a row in the Paragraph Styles editor, and the Character dock group grows one row (270→306) so the checkbox fits without scrolling; label translated in all 20 catalogs.\n- `perturbed_para` style-mutation fuzzing covers the new field.\n\n## Tests\n\n- `crates/text/src/tests.rs`: `hanging_marks_do_not_steer_the_alignment` (centered point text keeps its content centered, right stays flush), `a_hanging_run_steps_back_onto_the_line_above` (merge-back: 2 rows → 1, mark past the box edge, caret on the merged row), `hanging_leaves_plain_paragraphs_untouched`, plus the engine-data write test `/Hanging true|false`.\n- `crates/io/tests/psd_style_sheets.rs`: `hanging_punctuation_comes_from_the_paragraph_sheet` (sheet key read, missing key → off).\n- `crates/engine/src/type_cmds.rs`: `set_style_paragraph_hanging_round_trips` (on, off, junk value ignored like a junk `align`).\n- `crates/ui-egui/src/type_tool_tests.rs`: `character_panel_hanging_checkbox_toggles_the_paragraph_style` (toggles both selected layers, untouched elsewhere, checkbox shows the state).\n\n## Gates\n\n`cargo test` on doc/text/engine/io/ui-egui green (175/1898/281+/3242 tests), clippy `-D warnings`, `cargo xtask wasm`, `cargo xtask layers`, `panic_hunt` (adversarial `type.setStyle` params) green, i18n gate green (20/20 catalogs), `cargo xtask scorecard` regenerated (`TYPE-218-10` done), snapshot-verified visually (panel checkbox checked, centered demo).",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "2b1364393cfcd09380cf18a221e7cdd325fc4deb",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/doc/src/text.rs"
},
{
"path": "crates/engine/src/type_cmds.rs"
},
{
"path": "crates/engine/src/type_styles_cmds.rs"
},
{
"path": "crates/io/tests/it/psd_style_sheets.rs"
},
{
"path": "crates/text/src/layout.rs"
},
{
"path": "crates/text/src/psd.rs"
},
{
"path": "crates/text/src/tests.rs"
},
{
"path": "crates/ui-egui/src/dock.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/type_panels_ui.rs"
},
{
"path": "crates/ui-egui/src/type_tool.rs"
},
{
"path": "crates/ui-egui/src/type_tool_tests.rs"
},
{
"path": "docs/scorecard.md"
},
{
"path": "docs/ui-parity.md"
},
{
"path": "scorecard/type.toml"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714817/job/121230220698"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714616/job/121230219872"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714818/job/121230220545"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714817/job/121230220728"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714817/job/121230220542"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714817/job/121230220689"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714817/job/121230221571"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714817/job/121230306673"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714817/job/121230306607"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40213714817/job/121230306629"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T17:48:01Z",
"url": "https://github.com/storytold/photocraft/pull/8347#pullrequestreview-5644437741",
"body": "Re-review at 2b1364393. Both points from my last review are fixed, so I'm approving.\n\nI merged the head with current main 938542c78 locally and ran fmt, clippy and the tests on the merge:\n\n- `cargo fmt --check` and `clippy -D warnings` are clean.\n- Tests pass: text 242 (one more than last time, the new test), engine 16, io 1, ui 70.\n- `docs/scorecard.md` is up to date.\n- Hosted CI on 2b1364393 is green (10 pass, the fuzz matrix is skipped), and there are no unresolved review threads.\n\n**1. Tip-cap test.** `a_full_mark_row_does_not_step_back_onto_a_short_line` now pins the width limit. I reverted the cap at `layout.rs:843` back to `tip = true` and the new test fails at `tests.rs:614`; it passes at the head. The other reverts still behave as before:\n- Without the chain guard, `a_stepped_mark_row_keeps_its_clusters_and_glyphs_in_step` fails (`tests.rs:556`).\n- Without both guards, that test fails along with the new test and `hanging_marks_of_a_split_run_keep_their_breaks` (`tests.rs:588`).\n- Without the justified guard, `hanging_marks_stay_off_a_justified_paragraphs_last_line` fails (`tests.rs:642`).\n\nI re-ran my probe in a 200px box at 16pt, hanging off and on:\n\n| Text | Head | `tip = true` reverted |\n|---|---|---|\n| `word ` + 30 hyphens | 3 lines `[38, 199, 22]` in both modes | 2 lines `[241, 22]` with hanging on, 41px over the box |\n| `word ` + 30 dots | 1 line `[181]` in both modes | unchanged |\n| `Loading` + 34 dots | 2 lines `[199, 18]` in both modes | unchanged |\n\nSo the new test catches the overflow the probe showed last time.\n\n**2. Nit.** The comment at `tests.rs:581` now says 34 dots, which matches the string.\n\n**What I didn't test.** I ran on Linux only. Windows and the other platforms are covered by hosted CI only. I didn't check rendering by eye in the app.\n",
"commit": {
"oid": "2b1364393cfcd09380cf18a221e7cdd325fc4deb"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Type and typography",
"new_test_names": [
"set_style_paragraph_hanging_round_trips",
"hanging_punctuation_comes_from_the_paragraph_sheet",
"hang_cluster",
"hang_weight",
"hang_mark",
"hang_runs",
"hanging_marks_do_not_steer_the_alignment",
"hanging_centers_a_kerned_line_exactly",
"justify_all_spreads_to_the_edge_exactly",
"hanging_only_the_mark_list_trims",
"hanging_keeps_marks_after_a_forced_break_on_their_row",
"a_hanging_run_steps_back_onto_the_line_above",
"a_stepped_mark_row_keeps_its_clusters_and_glyphs_in_step",
"hanging_marks_of_a_split_run_keep_their_breaks",
"a_full_mark_row_does_not_step_back_onto_a_short_line",
"hanging_marks_stay_off_a_justified_paragraphs_last_line",
"hanging_centers_the_box_content_behind_its_marks",
"hanging_keeps_empty_rows_between_forced_breaks",
"hanging_leaves_plain_paragraphs_untouched",
"hanging_punctuation_writes_into_the_paragraph_sheet",
"character_panel_hanging_checkbox_toggles_the_paragraph_style"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8347"
},
{
"number": 8193,
"title": "Filter Gallery › Cutout 3/4: edges and curves stage",
"url": "https://github.com/storytold/photocraft/pull/8193",
"body": "Part 3 of 4 of the Cutout trace (#7903). **Stacked on #8191 and #8192**: this branch contains their commits, so please review stage commit df782119 and the island-preservation follow-up, 86402460.\n\n**Review fix, 25 October 2026 (86402460):** closed outlines retain their original shared boundary if fitting reverses winding or removes more than half their area. New regressions use real island chains at low fidelity.\n\n### What's here\n- `cutout/edges.rs`, step 4: the cracks between regions linked into chains from junction to junction (or closed loops round islands), each crack point moved along its normal to where the anti-aliasing puts the edge, with the regions on either side of each chain.\n- `cutout/curves.rs`, step 5: one fit per chain: rounding off below Edge Fidelity 2, corners kept (squared back where the pixels rounded them), runs simplified with Douglas–Peucker and drawn as Catmull-Rom curves.\n\n### Tests\nTwo regions share one open chain between junctions, and every crack is in exactly one chain; an island is one closed chain; an anti-aliased edge lands mid-pixel; Douglas–Peucker keeps only an L's corner; a square keeps its corners and straight sides; a rounded corner is squared back; open chains keep their ends at every fidelity; curves pass through their vertices.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "864024609b872de3d869879f1632b951c896c6cd",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/algo/src/cutout/colour.rs"
},
{
"path": "crates/algo/src/cutout/curves.rs"
},
{
"path": "crates/algo/src/cutout/edges.rs"
},
{
"path": "crates/algo/src/cutout/mod.rs"
},
{
"path": "crates/algo/src/cutout/regions.rs"
},
{
"path": "crates/algo/src/cutout/util.rs"
},
{
"path": "crates/algo/src/lib.rs"
},
{
"path": "crates/algo/src/quantize.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391227290"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170576/job/121162310137"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170571/job/121162310008"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391153927"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170576/job/121162310112"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391205140"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170576/job/121162309938"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391235076"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170576/job/121162310090"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170576/job/121162310707"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391202838"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391210482"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170576/job/121162388356"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170576/job/121162388388"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391152894"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191170576/job/121162388363"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391215977"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561420011/job/122391238714"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T12:43:15Z",
"url": "https://github.com/storytold/photocraft/pull/8193#pullrequestreview-5643244034",
"body": "Re-review at 86402460. The island-collapse blocker from my df78211 review is fixed.\n\n- `fit` now compares the signed area of a closed chain before and after fitting (`crates/algo/src/cutout/curves.rs:96-108`). If the fit flips the winding, or loses half the area or more, it falls back to the original outline and closes the loop explicitly. Shared chains stay consistent between the two regions, because the decision is made once per chain.\n- `fitting_keeps_small_islands_and_their_winding` (`:346`) runs real `follow_edges` island chains (2×2, 3×3, 4×4 and 6×6) at simplicity 0 and 2 and fidelity 1, 1.5, 2 and 3, skipping islands below `min_area()`. It checks that the winding is kept, that at least half the area survives and that the loop is closed. That covers the cases from my probe table, including 4×4 at simplicity 2 / fidelity 1.\n\nChecked locally on 86402460 merged with main:\n- `cargo +1.99.0 fmt --all -- --check`: clean\n- `cargo clippy -p photocraft-algo --all-targets -- -D warnings`: clean\n- `cargo test -p photocraft-algo --lib cutout`: 25 passed\n- Revert check: with the fallback disabled (`if false && closed` at `:97`), the new test fails at `:366` (24 passed, 1 failed). So the test pins the fix.\n\nGitHub checks on this head are green, and fuzz is skipped.\n\nNot blocking, and still open from last time: the Catmull-Rom overshoot at fidelity 2 (about 0.24 px outside the pixel square on a 4×4 island).\n",
"commit": {
"oid": "864024609b872de3d869879f1632b951c896c6cd"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"bilateral_rows",
"noisy",
"oklab_round_trips_and_marks_the_gamut",
"out_of_gamut_colours_lose_strength_not_hue",
"smoothing_flattens_noise_and_keeps_edges",
"the_palette_finds_the_pictures_own_colours",
"a_tone_of_two_different_hues_splits_in_two",
"an_empty_image_still_has_a_palette",
"sub",
"len",
"signed_area",
"round_off",
"intersect",
"simplify",
"curves",
"settings",
"square",
"close",
"douglas_peucker_keeps_only_points_off_the_line",
"lines_cross_where_they_should",
"rounding_off_keeps_an_open_chains_ends",
"a_square_keeps_its_four_corners_and_straight_sides",
"a_rounded_corner_is_squared_back",
"an_open_chain_ends_where_it_started_at_any_fidelity",
"curves_pass_through_their_vertices",
"fitting_keeps_small_islands_and_their_winding",
"bounds_cover_every_point",
"regions",
"between",
"the_edge_between_two_regions_is_one_chain_between_junctions",
"an_island_is_one_closed_chain",
"an_anti_aliased_edge_is_placed_inside_its_pixel",
"min_area",
"sliver",
"tolerance",
"rounding",
"corner_turn",
"smooth_rounds",
"same_region_rows",
"settings",
"labels",
"components_are_four_connected_and_numbered_in_raster_order",
"merged",
"small_regions_merge_into_the_neighbour_they_share_most_border_with",
"slivers_merge_though_they_are_large",
"paper_is_stretched_towards_black_and_white_and_keeps_its_hue",
"local_colour_drifts_within_a_shape_but_never_crosses_a_cut",
"write",
"write_u64",
"finish",
"transposing_twice_gives_the_image_back",
"find_returns_the_root_and_shortens_the_path"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8193"
},
{
"number": 8192,
"title": "Filter Gallery › Cutout 2/4: regions stage",
"url": "https://github.com/storytold/photocraft/pull/8192",
"body": "Part 2 of 4 of the Cutout trace (#7903). **Stacked on #8191**: this branch contains its commit, so please review only the last commit, 412e3b7c.\n\n### What's here\n`cutout/regions.rs`, step 3 of the trace:\n- 4-connected components of one palette colour, numbered in raster order.\n- Small regions (under the Edge Simplicity area) and slivers merge, smallest first, into the neighbour they share most border with.\n- Each region's paper colour (lightness stretched over the image, average hue at average strength) and the per-pixel local colour that drifts within a shape but never crosses a cut.\n\n`local_paper` now takes the regions' paper colours instead of the fill stage's `Region` type; otherwise the code is as in #7903.\n\n### Tests\n4-connectivity and raster numbering; a speck merges into the neighbour with the longest shared border; a large one-pixel sliver merges; paper is stretched towards black and white with hue kept; local colour stays inside its region.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "412e3b7cf1f4836a5772d93391d21728dbe1f74f",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/algo/src/cutout/colour.rs"
},
{
"path": "crates/algo/src/cutout/mod.rs"
},
{
"path": "crates/algo/src/cutout/regions.rs"
},
{
"path": "crates/algo/src/cutout/util.rs"
},
{
"path": "crates/algo/src/lib.rs"
},
{
"path": "crates/algo/src/quantize.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391199053"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120413179654"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391146698"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120413179511"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120413179499"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391234869"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120413179527"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391233068"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120413179116"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120413181613"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391147859"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391237108"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120413252646"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391204314"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120413252683"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391237602"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561769146/job/120429412548"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561418155/job/122391148587"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T09:50:41Z",
"url": "https://github.com/storytold/photocraft/pull/8192#pullrequestreview-5642599218",
"body": "Reviewed 412e3b7. The new commit on top of #8191 is `regions.rs` plus the `mod.rs` wiring. Approving.\n\n**Local CI** (fresh worktree merged with main, own target dir):\n- `cargo +1.99.0 fmt --all -- --check`: clean\n- `cargo clippy -p photocraft-algo --all-targets -- -D warnings`: clean\n- `cargo test -p photocraft-algo --lib cutout`: 13 passed\n\n**Revert checks** (all restored afterwards; all tests pass again):\n- Choosing the neighbour with the *shortest* shared border (`regions.rs:107`) makes `small_regions_merge_into_the_neighbour_they_share_most_border_with` fail at `regions.rs:308`.\n- Removing the sliver clause from `mergeable` (`regions.rs:88`) makes `slivers_merge_though_they_are_large` fail at `regions.rs:322`.\n\n`merge_regions` reads correctly. Stale heap entries are skipped by the root and area check. Shared borders are resolved and summed only when a region merges. The perimeter update `perim[t] + perim[i] - 2·len` is right because `len` is the total border after deduplication. The merged region is pushed back onto the heap only if it is still mergeable. `components`, `paper_colours` and `local_paper` look good too.\n\nNon-blocking:\n- `regions.rs:235`: `same_region_rows` calls `v.chunks_mut(w)`, which panics when `w == 0`. The same goes for `transpose_u32` with `h == 0` (noted on #8191). An early return for empty images in `local_paper` (or in the part 4 entry point) would cover both.\n- `adj[t]` collects unresolved entries (`regions.rs:117`) until `t` itself merges. That's fine for correctness, but a large background region that never merges keeps every absorbed region's list around until the function returns. Probably not worth changing unless memory on large tiles shows up.\n",
"commit": {
"oid": "412e3b7cf1f4836a5772d93391d21728dbe1f74f"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"bilateral_rows",
"noisy",
"oklab_round_trips_and_marks_the_gamut",
"out_of_gamut_colours_lose_strength_not_hue",
"smoothing_flattens_noise_and_keeps_edges",
"the_palette_finds_the_pictures_own_colours",
"a_tone_of_two_different_hues_splits_in_two",
"an_empty_image_still_has_a_palette",
"min_area",
"sliver",
"tolerance",
"rounding",
"corner_turn",
"smooth_rounds",
"same_region_rows",
"settings",
"labels",
"components_are_four_connected_and_numbered_in_raster_order",
"merged",
"small_regions_merge_into_the_neighbour_they_share_most_border_with",
"slivers_merge_though_they_are_large",
"paper_is_stretched_towards_black_and_white_and_keeps_its_hue",
"local_colour_drifts_within_a_shape_but_never_crosses_a_cut",
"write",
"write_u64",
"finish",
"transposing_twice_gives_the_image_back",
"find_returns_the_root_and_shortens_the_path"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8192"
},
{
"number": 8191,
"title": "Filter Gallery › Cutout 1/4: colour and palette stage",
"url": "https://github.com/storytold/photocraft/pull/8191",
"body": "Part 1 of 4 of the Cutout trace, split out of #7903 on review. Nothing calls it yet; #7903 (part 4) wires it into the Filter Gallery.\n\n### What's here\n- New `cutout` module (`crates/algo/src/cutout/`): the filter's settings and steps 1–2 of the trace in `colour.rs`: Oklab conversion, bilateral smoothing (Tomasi & Manduchi 1998), the palette (median cut + k-means; a tone of two clearly different hues splits in two, colours only noise apart merge) and nearest-colour labelling.\n- `cutout/util.rs`: the `transpose`, union-find `find` and `FxHasher` helpers the later stages share.\n- `quantize.rs`: `median_cut` and `kmeans` become `pub(crate)` (the only change outside the new module).\n- The module carries `#![allow(dead_code)]` until part 4 uses it.\n\n### Tests\nOklab round trip and gamut; out-of-gamut colours keep their hue; smoothing flattens noise but keeps a step; the palette finds the image's own colours, merges noise-apart ones and splits a two-hue tone; transposes and `find`. `cargo test -p photocraft-algo`, clippy `-D warnings`, fmt.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "91e4cec43525ca739c05cff614701fbd3576e389",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/algo/src/cutout/colour.rs"
},
{
"path": "crates/algo/src/cutout/mod.rs"
},
{
"path": "crates/algo/src/cutout/util.rs"
},
{
"path": "crates/algo/src/lib.rs"
},
{
"path": "crates/algo/src/quantize.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391224444"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120412994158"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391202597"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120412994048"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120412994197"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391218784"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120412993732"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391139966"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120412994074"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120412995104"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391222411"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391139056"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120413089224"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391140464"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120413089349"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391141010"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39561764386/job/120429067097"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561415766/job/122391220227"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T09:46:29Z",
"url": "https://github.com/storytold/photocraft/pull/8191#pullrequestreview-5642584382",
"body": "Reviewed 91e4cec. Approving.\n\n**Local CI** (fresh worktree merged with main, own target dir):\n- `cargo +1.99.0 fmt --all -- --check`: clean\n- `cargo clippy -p photocraft-algo --all-targets -- -D warnings`: clean\n- `cargo test -p photocraft-algo --lib cutout`: 8 passed\n- `cargo test -p photocraft-algo --lib quantize`: 16 passed, 2 ignored (`median_cut`/`kmeans` are now `pub(crate)`, and their tests are unchanged)\n\n**Revert check:** with the hue split in `palette()` disabled (`colour.rs:117`), `a_tone_of_two_different_hues_splits_in_two` fails (`colour.rs:253`, giving 2 centres instead of 3). With it restored, all 8 pass.\n\nThe Oklab conversion, gamut handling, smoothing, palette, union-find and transpose all read correctly. Thanks for splitting #7903 up like this; it's much easier to review.\n\nNon-blocking:\n- `util.rs:23`: `transpose_u32` calls `out.chunks_mut(h)` directly, so it panics when `h == 0`. The `transpose` above it goes through `par::chunks_mut`, which returns early when the length is 0. Either guard it the same way or make sure part 4 never calls it on an empty tile.\n- `palette()` can return up to 2× the requested levels because of the hue split. That's documented, but whether it matches Photoshop's \"Number of Levels\" is better settled on #7903, where the slider is wired up.\n- The module is `#![allow(dead_code)]` until part 4 lands. That's fine for a stack, but please remove the allow in #7903.\n",
"commit": {
"oid": "91e4cec43525ca739c05cff614701fbd3576e389"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"bilateral_rows",
"noisy",
"oklab_round_trips_and_marks_the_gamut",
"out_of_gamut_colours_lose_strength_not_hue",
"smoothing_flattens_noise_and_keeps_edges",
"the_palette_finds_the_pictures_own_colours",
"a_tone_of_two_different_hues_splits_in_two",
"an_empty_image_still_has_a_palette",
"min_area",
"sliver",
"tolerance",
"rounding",
"corner_turn",
"smooth_rounds",
"write",
"write_u64",
"finish",
"transposing_twice_gives_the_image_back",
"find_returns_the_root_and_shortens_the_path"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8191"
},
{
"number": 8074,
"title": "Transform: remember selected interpolation (#8067)",
"url": "https://github.com/storytold/photocraft/pull/8074",
"body": "Fixes #8067\n\n## Problem\nTransform sessions did not consistently reuse the selected interpolation. The earlier fix routed the Transform selection through Preferences > General > Image Interpolation, which Image Size and Crop also read, so changing Transform unexpectedly changed their default.\n\n## Change\nTransform now remembers its interpolation in saved tool options. Until the user chooses a Transform-specific mode, sessions inherit General's interpolation. Once selected, Transform reuses that choice across transform entry points and app restarts without changing the General preference. Regression tests cover reuse, persistence, and leaving the global preference unchanged.\n\n## Validation\n- `cargo fmt --all -- --check`\n- `cargo test -p photocraft-ui-egui --lib --tests -j 12 -- --test-threads=8` — 3,342 unit tests passed, 13 ignored; all integration tests passed.\n- `cargo clippy -p photocraft-ui-egui --all-targets -j 12 -- -D warnings`\n- `cargo xtask layers`\n- `cargo xtask wasm` — passed; reports three existing engine dead-code warnings for wasm.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "290f924d0fecc3bf6bea159c0d9e86255d326a96",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/cam_transform.rs"
},
{
"path": "crates/ui-egui/src/prefs_ui.rs"
},
{
"path": "crates/ui-egui/src/state.rs"
},
{
"path": "crates/ui-egui/src/tool_reset.rs"
},
{
"path": "crates/ui-egui/src/transform_tool.rs"
},
{
"path": "crates/ui-egui/src/type_transform.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119571995754"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639679/job/119571994977"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119571995695"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119571995507"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119571995691"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119571995752"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119571996620"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119572065802"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119572065961"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119572065737"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119572065625"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39701639519/job/119580816912"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-22T10:39:23Z",
"url": "https://github.com/storytold/photocraft/pull/8074#pullrequestreview-5612886202",
"body": "Re-reviewed current head `290f924d0fecc3bf6bea159c0d9e86255d326a96`. Transform now persists its interpolation choice as a tool-specific option, reuses it across transform entry points, and keeps the General preference as the fallback until a Transform-specific choice is made. Verified the current-head GitHub Actions run passed `interpolation_selection_is_reused_for_the_next_transform` on Ubuntu: https://github.com/storytold/photocraft/actions/runs/39701639519/job/119572065802. No blocking finding in this scope.",
"commit": {
"oid": "290f924d0fecc3bf6bea159c0d9e86255d326a96"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"interpolation_preference",
"interpolation_selection_is_reused_for_the_next_transform"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8074"
},
{
"number": 8039,
"title": "Compose: Extend Softer outer glows past Size (#7899)",
"url": "https://github.com/storytold/photocraft/pull/8039",
"body": "## Problem\n\n#7899 reports that a Softer Outer Glow falls to zero at `Size`, while Photoshop keeps a short fading tail. The current two-box tent has finite support, which causes the hard cutoff.\n\n## Change\n\n- Preserve the existing tent profile and add a short, range-scaled tail only to Softer outer glows.\n- Derive the tail from the shape's Euclidean outside-distance field, max-combine it with the existing blur, and include its support in CPU effect margins.\n- Use the same tail stage in the GPU compositor. Inner glows and Precise glows are unchanged.\n\nReference comparison from the issue: [Photoshop vs PhotoCraft](https://github.com/user-attachments/assets/96cdaebe-4752-47af-b51e-de41d8adea8f).\n\n## Validation\n\n- `cargo fmt --all -- --check` — passed.\n- `cargo test -j12 -p photocraft-compose --lib softer_outer_glow` — 2 passed.\n- `cargo test -j12 -p photocraft-gpu --lib softer_outer_glow` — 1 passed.\n- `cargo test -j12 -p photocraft-gpu --test parity layer_effects_every_kind -- --nocapture` — passed.\n- `cargo test -p photocraft-gpu shader_validates` — passed.\n- `cargo clippy -j12 -p photocraft-compose -p photocraft-gpu --all-targets -- -D warnings` — passed.\n- `cargo xtask layers` — passed.\n- `cargo xtask perf --quick` — passed; comparison against the recorded baseline was skipped because it was captured on a different machine class.\n- `cargo test --release -j12 -p photocraft-io --features corpus,heif --test corpus psd_tools_import_flatten_oracle -- --nocapture` — passed; `layer_effects.psd` matched with no bad pixels.\n\nThe full `cargo xtask test-corpus` run could not be completed locally: available memory fell below 8 GiB during release compilation, so I stopped it to avoid further memory pressure. CI will run the full corpus suite.\n\nFixes #7899",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "5579cf5439856fed93f41a669a3fc9b56d5813cf",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/effects.rs"
},
{
"path": "crates/gpu/src/compose.wgsl"
},
{
"path": "crates/gpu/src/fx.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561377000/job/122391001611"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859211489"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561377000/job/122391067629"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859211782"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859211223"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561377000/job/122391051093"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859211267"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561377000/job/122391076892"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859211184"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859213943"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561377000/job/122391002656"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561377000/job/122391003169"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859291925"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859292125"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859291787"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561377000/job/122391003326"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118859292014"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39489680272/job/118871822367"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T22:54:54Z",
"url": "https://github.com/storytold/photocraft/pull/8039#pullrequestreview-5592809677",
"body": "Read the whole diff (CPU effects, GPU program and shader). The added Softer outer-glow tail is the same smoothstep falloff in both paths: `softer_glow_tail_width` and `softer_glow_tail_amplitude` are shared, the CPU adds it with `max` over the blurred coverage before the contour/finish step, and the GPU does the same through a new MGlow tail mode (selected by `p0.w`, which the existing glow call leaves at 0) followed by an MFinish max (`p0.x > 1.5`; the old 0/1 values behave as before). `margin()` and the GPU field reach are grown by the tail width, so the tail isn't clipped, and the width is capped at 6 px so large sizes stay local. Inner glows are untouched.\n\nThe tests pin the visible tail, the margin (68 for size 60) and the GPU stage parameters. CI is green at 5579cf5. No changes requested.\n",
"commit": {
"oid": "5579cf5439856fed93f41a669a3fc9b56d5813cf"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"add_softer_glow_tail",
"softer_outer_glow_keeps_a_fading_tail_past_its_size",
"softer_outer_glow_margin_includes_its_tail_support",
"max",
"softer_glow_tail",
"softer_outer_glow_program_keeps_the_tent_and_adds_a_distance_tail"
],
"regression_source": "https://github.com/storytold/photocraft/pull/8039"
},
{
"number": 7903,
"title": "Filter Gallery › Cutout 4/4: trace the image into flat paper shapes",
"url": "https://github.com/storytold/photocraft/pull/7903",
"body": "> **Part 4 of 4.** On review this was split into a stack, one stage per PR:\n> 1. #8191 colour and palette\n> 2. #8192 regions\n> 3. #8193 edges and curves\n> 4. this PR: the fill stage (`cutout/fill.rs`), `trace` / `render`, and the Filter Gallery hookup in `artistic_fx.rs`\n>\n> This branch contains the three earlier commits, so please review the fill/hookup commit 1d09845c and the island-preservation follow-up 5c306990. The original file split moved the code without changing it: `render` hashes over 54 input and setting combinations match the single-file version (2e95bf5). The description below is of the filter as a whole.\n\n**Review fix, 25 October 2026 (5c306990):** closed outlines retain their original shared boundary if fitting reverses winding or removes more than half their area. New regressions use real island chains at low fidelity. Rendered-tone and empty-image regressions are included. The original 54-case hash comparison below applies to the file split before this intentional output correction.\n\n### Problem\nCutout blurred the image and posterized each channel per pixel. Photo noise survived as speckle, edges were ragged, colours banded (inventing greens and teals), every shape got a dark halo, and Edge Fidelity did almost nothing.\n\n### Solution\nCutout now traces the image into shapes and fills them back in as flat paper (new `crates/algo/src/cutout/` module, one file per stage):\n\n1. **Smooth** within tones, edges kept: a bilateral filter (Tomasi & Manduchi 1998) in Oklab.\n2. **Palette** of the picture's own colours: median cut + k-means (the existing `quantize` helpers, now crate-visible). A tone made of two clearly different hues splits in two; colours only noise apart merge.\n3. **Regions**: 4-connected areas of one colour. Regions under the Edge Simplicity area, and slivers, merge smallest first into the neighbour they share most border with.\n4. **Edges**: cracks are linked into chains between junctions, and each crack is moved to where the anti-aliasing on either side puts the edge (sub-pixel). Each chain is fitted once, so neighbouring shapes share one edge.\n5. **Curves**: sharp bends stay corners (squared back where the pixels rounded them); runs are simplified (Douglas–Peucker) and drawn as Catmull-Rom curves. **Edge Fidelity** sets how closely outlines follow the image: rounded and loose at 1, tight at 3.\n6. **Fill**: exact-area scan conversion, so edges are anti-aliased and the coverages of shapes that share an edge sum to one (no seams).\n7. **Colour**: each shape keeps its region's lightness (stretched towards black and white) with the hue and colour strength of its own pixels nearby, so colour can drift gently inside a shape without crossing a cut.\n\n**Tiling.** A trace is whole-image work, so a Filter Gallery stack containing a Cutout now declares `Halo::Bounds` and is prepared once before tiling (`artistic_fx::Prepared`, in the same spirit as `shape_blur::Prepared` and the radial blur plan). Effects up to the last Cutout run over the whole image and it is traced; each tile then fills in the shapes and runs the effects after it. Results don't depend on the tiling.\n\nClean-room: built from the published techniques named above and observed behaviour; no proprietary code.\n\n### Before / after\nDefaults (4 levels, Edge Simplicity 4, Edge Fidelity 2). Public-domain artwork (Vermeer; NASA *Earthrise*).\n\n![Cutout on Girl with a Pearl Earring](https://raw.githubusercontent.com/andeecollard/photocraft/pr-assets/filter-gallery/cutout-girl.png)\n![Cutout on Earthrise](https://raw.githubusercontent.com/andeecollard/photocraft/pr-assets/filter-gallery/cutout-earth.png)\n\n### Tests\n- `cutout::tests`: exact square coverage; shapes cover every pixel exactly once (no seams) at two fidelities; filling is tile-independent; noise merges and a speck survives or goes with Edge Simplicity; Edge Fidelity really changes outlines (the old code changed 0.5% / 2.8% of pixels between fidelities 1→2 / 2→3 on smooth shapes, now about 9% / 5%).\n- `artistic_fx::tests`: flat shapes with 1–2 px anti-aliased edges, a ramp becomes N steps, specks drop with simplicity, a stack around a Cutout is tile-independent; Cutout added to the every-slider-responds and tile-independence test. The existing all-filters test covers 8, 16 and 32-bit.\n- Each stage also has its own unit tests in its PR.\n- `cargo test -p photocraft-algo` (622 passed at the top of the stack), clippy `-D warnings`, `cargo fmt`, `xtask layers`, wasm check, `xtask scorecard --check`.\n\n### Performance\n24 MP, release, Apple Silicon (`bench_gallery`): Cutout ≈ 1.05 s (the old per-pixel posterize was 0.19 s). The trace runs once per apply; filling the tiles is a small part of it (≈ 35 ms over a 24 MP photo).\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "5c30699087b02edcaa23397a0f7076bf503be048",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/algo/src/artistic_fx.rs"
},
{
"path": "crates/algo/src/cutout/colour.rs"
},
{
"path": "crates/algo/src/cutout/curves.rs"
},
{
"path": "crates/algo/src/cutout/edges.rs"
},
{
"path": "crates/algo/src/cutout/fill.rs"
},
{
"path": "crates/algo/src/cutout/mod.rs"
},
{
"path": "crates/algo/src/cutout/regions.rs"
},
{
"path": "crates/algo/src/cutout/util.rs"
},
{
"path": "crates/algo/src/lib.rs"
},
{
"path": "crates/algo/src/quantize.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122390936616"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132908/job/121162193443"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132965/job/121162193465"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122390937151"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132908/job/121162193640"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122391013898"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132908/job/121162193585"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122391008355"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132908/job/121162193609"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132908/job/121162194326"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122390935544"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122390994862"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132908/job/121162255468"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132908/job/121162255414"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122391005966"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40191132908/job/121162255398"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122390984516"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561357732/job/122390937071"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T12:55:08Z",
"url": "https://github.com/storytold/photocraft/pull/7903#pullrequestreview-5643279271",
"body": "Re-review at 5c306990. The island-collapse blocker from my 347991f review is fixed.\n\n- The `curves.rs` change is identical to #8193's 86402460. When a closed fit flips its winding, or loses half its area or more, it falls back to the original outline (`crates/algo/src/cutout/curves.rs:96-108`). So a hole and its island, which share one chain, either both keep their shape or both fall back together.\n- `cutout_keeps_the_tone_of_surviving_islands_at_low_fidelity` (`artistic_fx.rs:1416`) is the end-to-end test I asked for. It covers 2×2 / 3×3 / 4×4 / 6×6 islands at fidelity 1, 1.5 and 2 and compares each against its fidelity-2 tone. 2×2 is held only to contrast, which seems reasonable given the stretch. It includes the 4×4 / simplicity 2 and 2×2 / simplicity 0 cases from my probe.\n- `empty_images_stop_before_the_transpose_and_row_passes` (`cutout/mod.rs:311`) also covers the zero-width and zero-height `trace` cases.\n\nChecked locally on 5c306990 merged with main:\n- `cargo +1.99.0 fmt --all -- --check`: clean\n- `cargo clippy -p photocraft-algo --all-targets -- -D warnings`: clean\n- `cargo test -p photocraft-algo --lib cutout`: 34 passed\n- `cargo test -p photocraft-algo --lib artistic_fx`: 7 passed, 2 ignored\n- `cargo test -p photocraft-algo --lib gallery`: 14 passed, 3 ignored\n- Revert check: with the fallback disabled (`if false && closed` at `curves.rs:97`), both `fitting_keeps_small_islands_and_their_winding` (`curves.rs:366`) and the new e2e test (`artistic_fx.rs:1430`) fail.\n\nGitHub checks on this head are green, and fuzz is skipped.\n\nNot blocking, and still open from last time:\n- The whole-image trace can't be cancelled (`lib.rs:1114-1117`).\n- The `paper_colours` stretch can move the background tone when a small island is present.\n",
"commit": {
"oid": "5c30699087b02edcaa23397a0f7076bf503be048"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"cutout_settings",
"finish",
"cutout_on",
"cutout_makes_flat_shapes_with_clean_edges",
"cutout_keeps_the_tone_of_surviving_islands_at_low_fidelity",
"a_stack_around_a_cutout_is_tile_independent",
"bilateral_rows",
"noisy",
"oklab_round_trips_and_marks_the_gamut",
"out_of_gamut_colours_lose_strength_not_hue",
"smoothing_flattens_noise_and_keeps_edges",
"the_palette_finds_the_pictures_own_colours",
"a_tone_of_two_different_hues_splits_in_two",
"an_empty_image_still_has_a_palette",
"sub",
"len",
"signed_area",
"round_off",
"intersect",
"simplify",
"curves",
"settings",
"square",
"close",
"douglas_peucker_keeps_only_points_off_the_line",
"lines_cross_where_they_should",
"rounding_off_keeps_an_open_chains_ends",
"a_square_keeps_its_four_corners_and_straight_sides",
"a_rounded_corner_is_squared_back",
"an_open_chain_ends_where_it_started_at_any_fidelity",
"curves_pass_through_their_vertices",
"fitting_keeps_small_islands_and_their_winding",
"bounds_cover_every_point",
"regions",
"between",
"the_edge_between_two_regions_is_one_chain_between_junctions",
"an_island_is_one_closed_chain",
"an_anti_aliased_edge_is_placed_inside_its_pixel",
"line",
"a_square_covers_its_area_exactly",
"min_area",
"sliver",
"tolerance",
"rounding",
"corner_turn",
"smooth_rounds",
"settings",
"coverage",
"shapes_cover_every_pixel_once_with_no_seams",
"edge_fidelity_changes_the_outlines",
"empty_images_stop_before_the_transpose_and_row_passes",
"filling_does_not_depend_on_the_tiling",
"noise_merges_into_neighbours_and_noise_apart_colours_are_one",
"same_region_rows",
"settings",
"labels",
"components_are_four_connected_and_numbered_in_raster_order",
"merged",
"small_regions_merge_into_the_neighbour_they_share_most_border_with",
"slivers_merge_though_they_are_large",
"paper_is_stretched_towards_black_and_white_and_keeps_its_hue",
"local_colour_drifts_within_a_shape_but_never_crosses_a_cut",
"write",
"write_u64",
"finish",
"transposing_twice_gives_the_image_back",
"find_returns_the_root_and_shortens_the_path"
],
"regression_source": "https://github.com/storytold/photocraft/pull/7903"
},
{
"number": 7848,
"title": "fix(retouch): preserve Smudge pickup with live-pixel stroke commits",
"url": "https://github.com/storytold/photocraft/pull/7848",
"body": "Pressure-sensitive Smudge strokes in #7281 can commit different pixels from replaying the recorded command: the reproduced 8-bit stroke differed in 7,635 channel values, by up to 47/255. Smudge carries a fixed-size pickup square, but the live path only supplied the current pressure-sized dab; the command working region also lacked the pickup padding.\n\nThis companion includes @wanikhawar’s #7281 unchanged plus one corrective commit. It gives live Smudge the complete pickup neighborhood, pads command replay consistently, and keeps writes inside the actual dab. The live-pixel release optimization remains intact.\n\n### Review and landing order\n\nPlease review the combined result here before either PR lands. Once review and CI pass, merge #7281 first, then move only the corrective commit onto the resulting main, rerun validation and CI on that exact head, and merge this companion. Until then, this PR intentionally includes #7281’s changes as well as the fix.\n\nRelated work: #5804 addresses large-brush allocation/clipping separately; this PR fixes pickup correctness and does not claim to solve that issue.\n\n### Validation\n\n- Native build; 5,212 tests passed, 0 failed, 32 ignored across paint, engine and UI.\n- Exact live/replay comparisons across all six sequential dab tools, pressure, elliptical tips, smoothing, scattering, transparency locks, selections, RGB/Gray/CMYK/Lab and 8/16/32-bit; masks, alpha channels and Quick Mask; stale-document rejection.\n- Independent padded paint oracle and actual canvas pen-input → release → journal replay → undo/redo regression.\n- Strict Clippy, formatting, dependency layers, WASM (including HEIF check), registry panic hunt and quick performance gate passed.\n- Same-machine release benchmark, 24 MP / 600 px Smudge: live release 0.2 → 0.2 ms; total measured live frames 134.5 → 134.0 ms. Correct fallback rendering costs 60.0 → 65.5 ms because it samples the full neighborhood. Small three-run benchmark; quick-gate baseline comparison skipped because its machine differs, and that gate has no retouch scenario.\n\nValidated locally on macOS Apple Silicon. Physical tablet behavior and independent Photoshop fidelity remain unqualified; CI supplies the other platform checks.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "389a1d5bd67072d5581c27989080222a7065b585",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/examples/bench_live_dab_release.rs"
},
{
"path": "crates/engine/src/lib.rs"
},
{
"path": "crates/engine/src/retouch_cmds.rs"
},
{
"path": "crates/engine/src/retouch_cmds/tests.rs"
},
{
"path": "crates/engine/tests/live_dab_matches_replay.rs"
},
{
"path": "crates/paint/src/retouch.rs"
},
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/retouch_ui.rs"
},
{
"path": "crates/ui-egui/src/stroke_timing_tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561165298/job/122390309481"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278402264"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561165298/job/122390255756"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278402528"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278401857"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561165298/job/122390255159"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278402214"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561165298/job/122390314513"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278401589"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278403360"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561165298/job/122390333943"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561165298/job/122390257092"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278508775"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278508441"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278508376"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561165298/job/122390332576"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118278508402"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39316079855/job/118290387159"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T14:05:42Z",
"url": "https://github.com/storytold/photocraft/pull/7848#pullrequestreview-5586186420",
"body": "This replaces my earlier comment on eb2ef35. This time I read the whole combined diff, including the three commits that come from #7281 (the live-pixel release, its review fix and the main merge), plus the test rename. I skimmed the benchmark example (`bench_live_dab_release.rs`) and did not read the new test files line by line.\n\n- **Live commit.** `into_commit` finishes the renderer, runs the remaining dabs, and hands over the surface only if the tool settings, brush and layer are still the ones the stroke started with (`DabTool` is now `PartialEq` and the brush is compared); a refused offer also drops the previous one (`offer_live_dab`), and `dab_cmd` only takes a live result whose command, params and points match.\n- **Full precision.** `WorkTiles` keeps the working copy as `f32` tiles filled from the pre-stroke surface on first write, so each dab sees what `apply_dab_stroke` holds. The tile indexing is clamped and bounds-checked, and the `mix_back` shortcut for untouched pixels is exact.\n- **Smudge pickup.** `Smudge::sampling_radius` is now the single source for the pickup radius, and `DabNeighborhood::Smudge` reads the union of the dab and that square, so live preview, tail dabs and the commit's padding agree.\n- The UI side offers the stroke only for the matching tool kinds and falls back to rendering on a refusal.\n\nCI is green at 389a1d5. The slowdown the description reports for the replay path (60.0 to 65.5 ms) and the lack of physical tablet testing still stand; both are for the maintainers to accept. The landing order in the description (#7281 first) is unchanged.\n",
"commit": {
"oid": "389a1d5bd67072d5581c27989080222a7065b585"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"halo",
"read_rect",
"neighborhood",
"with_neighborhood",
"setup",
"pixels",
"pressure_shape_and_smoothing_match_replay",
"native_modes_selection_and_lock_match_replay",
"changed_document_refuses_live_pixels",
"smudge_pickup_matches_full_neighborhood_when_pressure_expands",
"pressure_sized_live_dabs_commit_only_the_requested_mask_or_channel",
"pressure_sized_smudge_release_matches_replay_and_undo_redo"
],
"regression_source": "https://github.com/storytold/photocraft/pull/7848"
},
{
"number": 7743,
"title": "packaging: the portable zip holds the files, not a folder with the files in it",
"url": "https://github.com/storytold/photocraft/pull/7743",
"body": "Fixes #7699.\n\n`Compress-Archive -Path <folder>` archives the folder itself, so the portable zip contained\n\n```\nphotocraft-<version>-windows-<arch>-portable\\photocraft.exe\nphotocraft-<version>-windows-<arch>-portable\\photocraft-cli.exe\nphotocraft-<version>-windows-<arch>-portable\\portable.txt\n...\n```\n\nrather than those files at its root. `<folder>\\*` archives the contents.\n\nFour things say the root is what was meant:\n\n- this script's own synopsis — `photocraft-<version>-windows-<arch>-portable.zip photocraft.exe + photocraft-cli.exe + portable.txt`;\n- `portable.txt` (\"While this file sits next to photocraft.exe…\") and the advice to \"copy the whole folder\", which with a wrapper means the user's folder contains one more folder before the exe;\n- VectorCraft's `packaging/windows/windows7.ps1`, which writes its portable zip with `Compress-Archive -Path (Join-Path $Stage '*')`;\n- storytold/craft-launcher#627 — the launcher's portable install looked for the program at the archive root and found a wrapper folder instead. (That side now unwraps a single wrapper folder, so this is belt and braces; if you would rather keep the wrapper here, the launcher copes either way and the synopsis is what should change.)\n\nNothing read the layout back after writing it, which is how it drifted, so the three files are now checked inside the archive and the build fails loudly if they are not at its root.\n\n### How this was checked\n\nI have no Windows or PowerShell here, so I have not run `package.ps1`: the change copies the line that already works in `windows7.ps1`, and the check uses `Expand-Archive` + `Test-Path` + `throw` exactly as `sign.ps1` and this script use them. `packaging-lint` runs on this PR and is the static check; a maintainer running the packager on a Windows runner is the real one. No Rust is touched, so `cargo xtask ci` has nothing to say about it.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "fd6fc92015fd08560362b4b32844edf97722f292",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "packaging/windows/package.ps1"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561066525/job/122389972893"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118045484285"
},
{
"name": "lint / lint",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283928/job/118045485529"
},
{
"name": "windows-arm64 / package (cross-compiled on x64)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283294/job/118045482649"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561066525/job/122389902194"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118045484381"
},
{
"name": "Verified AppImage inputs",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283928/job/118045484889"
},
{
"name": "windows-arm64 / install and run on ARM64",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283294/job/118051362227"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118045484366"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561066525/job/122389902851"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118045484056"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561066525/job/122389969105"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118045484485"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118045486297"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561066525/job/122389974469"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561066525/job/122389904669"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118045553602"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118045555176"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40561066525/job/122389903641"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39248283220/job/118055729273"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-19T17:02:06Z",
"url": "https://github.com/storytold/photocraft/pull/7743#pullrequestreview-5574964336",
"body": "I read the whole diff (one hunk in `packaging/windows/package.ps1`) and, at the head, the rest of that script (how `$Portable`, `$TargetDir`, `$Dist` and `$Zip` are built, and `$ErrorActionPreference`), #7699 and the places that name the portable zip (`docs/releasing.md`, `docs/user/getting-started/install.md`, `README.md`). I haven't run PowerShell or the packager (no Windows runner here), so this is by my reading plus the hosted CI on this exact head, which is the static `packaging-lint`, not a packaging run.\n\n**What I checked.**\n- *The bug.* `$Portable` is a directory, so `Compress-Archive -Path $Portable` stores the folder as the top entry and the exe ends up one level down; #7699's report (\"does not contain the expected executable at its root\") is that. `-Path (Join-Path $Portable '*')` stores the folder's contents.\n- *What the wildcard picks up.* `$Portable` is filled by the script with `*.exe`, `README.md`, the licence files, any `OFL-*.txt` fonts licences and `portable.txt`; there are no subfolders and no dot-files, so the wildcard doesn't miss anything that was archived before.\n- *The check.* `$ErrorActionPreference = 'Stop'` is set at line 27, so a failing `Expand-Archive` stops the script; the three `Test-Path` checks `throw` with the zip path. `$Check` is under `$TargetDir\\windows-package`, and it is deleted before the expand, so a leftover from a failed run (the `throw` skips the final `Remove-Item`) is cleaned up on the next run.\n- *Docs.* The pages above only say to download and unzip the portable zip; none describes a wrapper folder, so nothing needs updating. The script's own synopsis says the zip holds the exe, the cli and `portable.txt`, which is now true.\n\nI can't check that the packaging step on a Windows runner finishes and the zip opens as intended; a run of `package.ps1` on a Windows build would show it.\n",
"commit": {
"oid": "fd6fc92015fd08560362b4b32844edf97722f292"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Platform and reliability",
"new_test_names": [],
"regression_source": "https://github.com/storytold/photocraft/pull/7743"
},
{
"number": 7742,
"title": "Actions: a fractional setting an integer field cannot hold is refused, not rounded",
"url": "https://github.com/storytold/photocraft/pull/7742",
"body": "Closes #7669 and #7670. **#7671 (Stroke) is not in this change** — see below.\n\n`Kind::Long` wrote its param through `long()`, which rounds:\n\n```rust\nfn long(v: f64) -> D {\n D::Integer(v.round().clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32)\n}\n```\n\nPhotoshop's field really is an integer, so a fractional setting cannot be written — but rounding it put a different edit in the file than the one recorded, with `skipped` empty and nothing to say so. Measured through `actions.save` and back:\n\n| step | recorded | in the file | read back |\n| --- | --- | --- | --- |\n| `filter.pixelate.crystallize` | `cellSize: 5.4` | `ClSz 5` | `cellSize: 5` |\n| `filter.distort.ripple` | `amount: 81.4` | `Amnt 81` | `amount: 81` |\n\n(The first row is the new round-trip test run against `main`: `left: Some(5.0), right: Some(5.4)`.) The reports measure what that does to the pixels — 385 to 583 differing colour components at every bit depth.\n\n### Why Stroke is out\n\n`edit.stroke` normalises the width itself:\n\n```rust\n// crates/engine/src/extra_cmds.rs:137\nlet width = p.get(\"width\").and_then(Value::as_f64).filter(|w| w.is_finite()).unwrap_or(1.0).clamp(1.0, 250.0).ceil() as usize;\n```\n\n`.ceil()`, so `width: 2.4` renders at **3** — an integer field can hold that exactly, and refusing the step would throw away something expressible. On `main` it renders at 3 and exports `Wdth 2` with `skipped` empty, which is wrong in both directions this PR could have gone. @PerchAnton's #7696 normalises it instead, which is the only approach that reproduces what the user saw. My first version refused it; that was my mistake.\n\n### What changed\n\nThe refusal is a field's own kind, `Kind::LongExact`, carried by the two fields whose command uses the setting as written:\n\n- Crystallize's `ClSz` — `cell_size: f(p, \"cellSize\", 10.0).clamp(3.0, 300.0)`, no rounding\n- Ripple's `Amnt` — `amount: f(p, \"amount\", 100.0).clamp(-999.0, 999.0)`, no rounding\n\n`long_exact` returns `None` for a value such a field cannot hold — a fraction, or a magnitude past `i32` — and `fields_to` passes that up. The step then takes the path a step with no Photoshop equivalent already takes: strict export names it and fails, `skipUnsupported: true` leaves it out and lists it in `skipped`.\n\nThe first version sent **all 26** `Kind::Long` fields through `long_exact` on the strength of three reports, one of which turned out to need the opposite. The other 24 keep `long()` until someone has read each against its command.\n\nRefusing, rather than exporting a near miss, is what the rest of this file does: `pixel_target` already refuses a filter step aimed at a mask because \"Photoshop's descriptor for a plain event has no field for a mask … so such a step can't be exported faithfully\".\n\nThe strict error now reads \"can't be written as a Photoshop step: it has no equivalent event, or a setting its event can't hold\", since both cases arrive there.\n\n### Tests\n\n- `a_fractional_setting_an_integer_field_cannot_hold_is_refused_not_rounded` — both reported steps: strict export fails naming the step, `skipUnsupported` reports it in `skipped`. On `main` the first `unwrap_err` panics with `Ok(…)`.\n- `whole_number_settings_for_integer_fields_still_round_trip` — the same with whole numbers, plus `edit.stroke`, which still exports: written, `skipped` empty, and every param read back as itself. Numbers compare by value and anything else as it is, so string settings (`size`, `color`, `location`) are checked rather than passing vacuously through `as_f64` on both sides.\n\n`cargo xtask ci`: all 5 steps pass (fmt, clippy, test, layers, wasm).\n\n### Still to do here\n\n@PerchAnton has offered their `atn_crystallize_cell_size.rs` and `atn_ripple_amount.rs` suites (four tests each) for this PR, with their authorship kept. Waiting on @echelon to confirm the order before moving them, per the discussion below.\n\n### Not in this change\n\n`long()` stays for the hand-written mappings that use it (`Lvls` input/output levels, `Thsh`, `Tlrn`, `Qlty`, `NCol`, `Dpth`, the Color Balance triples). Most take whole numbers from a range the command already clamps, and each is its own command scope; if you'd like them refused on the same footing, I'm happy to follow up.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "ab844f9cbcd1f1839ed7f286776331cb72e399aa",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/atn_cmds.rs"
},
{
"path": "crates/engine/src/atn_cmds/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118618950082"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118618949633"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118618949556"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118618949474"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118618949154"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118618951895"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118619035390"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118619035221"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39420198816/job/118630164118"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T14:12:28Z",
"url": "https://github.com/storytold/photocraft/pull/7742#pullrequestreview-5586281249",
"body": "Re-reviewed at the new head (both files, both tests). `LongExact` is applied only to Ripple `Amnt` and Crystallize `ClSz`, `long_exact` rejects fractional and out-of-range values rather than rounding or clamping them, and `edit.stroke` keeps rounding because its command ceils the width itself, which is the distinction I checked before. The follow-up commits narrow the refusal per field and make the round-trip test compare string settings (`size`, `color`, `location`) as well as numbers, so the whole-number case is actually checked.\n\nNo defect found. CI is green at this head.\n",
"commit": {
"oid": "ab844f9cbcd1f1839ed7f286776331cb72e399aa"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"long_exact",
"a_fractional_setting_an_integer_field_cannot_hold_is_refused_not_rounded",
"whole_number_settings_for_integer_fields_still_round_trip"
],
"regression_source": "https://github.com/storytold/photocraft/pull/7742"
},
{
"number": 7648,
"title": "Pen: Path Operations menu, as in Photoshop (#1397)",
"url": "https://github.com/storytold/photocraft/pull/7648",
"body": "Closes #1397. Closes #7533.\n\nThe Pen, Freeform Pen and Curvature Pen get Photoshop's **Path Operations** button in the options bar, after the stroke controls. Its icon shows the current operation.\n\n## Behavior\n\n- **Menu:** New Layer, Combine Shapes, Subtract Front Shape, Intersect Shape Areas, Exclude Overlapping Shapes, then Merge Shape Components below a separator.\n- **Shape mode:** New Layer (the default) keeps today's behavior, one shape layer per path. Any other operation adds the new path to the selected shape layer with that operation, so a hole can be cut in a shape (the mug handle in #7533). With no shape layer selected, a new one is started. One History step per path.\n- **Path mode:** New Layer is disabled and the Pen combines. The operation is stored on the subpath added to a targeted vector mask, the selected path or the Work Path (before this, Path mode always combined). With no path selected, a new Work Path is started as before.\n- **Merge Shape Components** bakes the operations into plain outlines. In Shape mode it runs the existing `layer.combineShapes.mergeShapeComponents`. In Path mode it runs a new engine command, `path.mergeShapeComponents` (work, saved or `layer` path, via `edit_path`). Both share the new `vector_cmds::bake_path`, which also keeps an inverted path inverted. The item is disabled with fewer than two components.\n- **Agents:** `ui.set` accepts `penMode` (`path`/`shape`) and `penPathOp`. `ui.inspect` reports `toolOptions.pen_path_op`, and the MCP `ui_set` description lists both fields.\n- **Settings:** the setting is `ToolOptions::pen_path_op` (serde default `newLayer`) and is part of the three Pens' Reset Tool / tool-preset keys. An unknown stored value falls back to New Layer.\n\n- **Changing drawn components** (tester feedback): Path Selection now selects components (click; ⇧-click adds or removes one; a click off them deselects; a drag moves only the selected ones, with their anchors shown solid). It has the Path Operations menu too. With components selected, the menu changes their operation through a new engine command, `path.setOperation` (Work Path, saved path, shape layer or vector mask; one History step), and its icon shows that operation.\n- **Keys:** `+` picks Combine Shapes and `-` picks Subtract Front Shape while a Pen tool or Path Selection is active, for the selected components and for the next path. Adobe Community threads report these as Photoshop's keys. Because `+` is ⇧= on a US layout, ⇧+ no longer steps the blend mode with these tools. It still does with the others.\n- **Continuing an open path:** the Pen carries on an open path from its endpoint in Shape mode (the selected shape layer) and on a targeted vector mask. Before, it only did this on the Work Path and saved paths. Closing the open path that a stroke-only shape layer was made of gives the layer the options bar's fill and stroke, as if the path had been drawn closed.\n\n## Assets and docs\n\n- Six original icons, `assets/icons/path-op-*.svg`, drawn on Lucide's 24 px grid and stroke, with a row in `ATTRIBUTION.md`.\n- The \"Path operations\" tooltip is translated in all 20 catalogs; the menu labels already were.\n- `docs/ui-parity.md`: the row stays **partial**, because the Shape tools don't have the menu yet. `docs/control-protocol.md` documents the new `ui.set` fields.\n\n## Not in this PR\n\n- The same menu on the Shape tools.\n- Keys for Intersect and Exclude: I couldn't find Photoshop's bindings for them.\n- Merge works on the whole path, not only the selected components. Like the existing shape-layer command, its outlines are traced from coverage.\n\n## Validation\n\n- `cargo test -p photocraft-engine --lib --tests`, `cargo test -p photocraft-engine --test panic_hunt -- --ignored`, `cargo test -p photocraft-ui-egui --lib --tests` (3091 lib tests), `cargo test -p photocraft-automation --tests`: pass.\n- `cargo clippy -p photocraft-engine -p photocraft-ui-egui -p photocraft-automation --all-targets -- -D warnings`, `cargo fmt --all -- --check`, `cargo xtask layers`, `cargo xtask wasm`: pass. `cargo xtask parity`: checklist unchanged (no new menu item).\n- New tests:\n - `pen_path_ops_tests.rs`: hole cut in the selected shape layer and its undo; an operation with no shape layer selected; Path-mode operations on the work path, a saved path and a vector mask; the menu driven with kittest (New Layer disabled in Path mode, Merge); Merge disabled with one component; unknown-value fallback.\n - Engine `merge_shape_components_*` plus bad-param cases, and `ui_set_drives_the_pen_path_operation`.\n- Checked visually in an offscreen snapshot and in the running desktop app.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "d24669d412f64ab079a668b0b8520ddbc607410c",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "ATTRIBUTION.md"
},
{
"path": "assets/icons/path-op-combine.svg"
},
{
"path": "assets/icons/path-op-exclude.svg"
},
{
"path": "assets/icons/path-op-intersect.svg"
},
{
"path": "assets/icons/path-op-merge.svg"
},
{
"path": "assets/icons/path-op-new-layer.svg"
},
{
"path": "assets/icons/path-op-subtract.svg"
},
{
"path": "crates/automation/src/server.rs"
},
{
"path": "crates/automation/src/workspace/tests.rs"
},
{
"path": "crates/engine/src/cmd/path_edit_cmds.rs"
},
{
"path": "crates/engine/src/cmd/vector_cmds.rs"
},
{
"path": "crates/engine/src/cmd/vector_cmds/params.rs"
},
{
"path": "crates/engine/src/cmd/vector_cmds/shape.rs"
},
{
"path": "crates/ui-egui/src/app/frame.rs"
},
{
"path": "crates/ui-egui/src/canvas/canvas_tool_menu.rs"
},
{
"path": "crates/ui-egui/src/canvas/tool_input.rs"
},
{
"path": "crates/ui-egui/src/control/dispatch.rs"
},
{
"path": "crates/ui-egui/src/control/params.rs"
},
{
"path": "crates/ui-egui/src/control/tests.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/icon_data.rs"
},
{
"path": "crates/ui-egui/src/input/shortcuts.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/state/tool_options.rs"
},
{
"path": "crates/ui-egui/src/tools.rs"
},
{
"path": "crates/ui-egui/src/tools/direct_select.rs"
},
{
"path": "crates/ui-egui/src/tools/lasso_ui.rs"
},
{
"path": "crates/ui-egui/src/tools/path_components.rs"
},
{
"path": "crates/ui-egui/src/tools/path_components/tests.rs"
},
{
"path": "crates/ui-egui/src/tools/tool_reset.rs"
},
{
"path": "crates/ui-egui/src/tools/vector_ui.rs"
},
{
"path": "crates/ui-egui/src/tools/vector_ui/curvature.rs"
},
{
"path": "crates/ui-egui/src/tools/vector_ui/options.rs"
},
{
"path": "crates/ui-egui/src/tools/vector_ui/overlay.rs"
},
{
"path": "crates/ui-egui/src/tools/vector_ui/path_ops.rs"
},
{
"path": "crates/ui-egui/src/tools/vector_ui/pen.rs"
},
{
"path": "crates/ui-egui/src/tools/vector_ui/pen_path_ops_tests.rs"
},
{
"path": "crates/ui-egui/src/tools/vector_ui/tests.rs"
},
{
"path": "docs/control-protocol.md"
},
{
"path": "docs/ui-parity.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122949824413"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726143667/job/122949821629"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144328/job/122949825347"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144328/job/122949958605"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122949824955"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144328/job/122949960413"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122949824794"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122949825144"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144328/job/122949958110"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122949880798"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122949828205"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122949827433"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122950330796"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122950330778"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40726144216/job/122950330766"
},
{
"name": "CodeRabbit",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://coderabbit.ai?utm_source=cr_app&utm_medium=github"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-28T17:15:54Z",
"url": "https://github.com/storytold/photocraft/pull/7648#pullrequestreview-5686770506",
"body": "Re-review at SHA d24669d (my last review was CHANGES_REQUESTED at f15c93c).@@GATE@@\n\nThe finding is fixed. `freeform_locked_shape` (`tools/vector_ui/pen.rs`) mirrors exactly when the append route would reach the engine's lock refusal: Shape mode, a Path Operation set, a selected shape, and `effective_locks(id)` with `all` or `pixels` (the same condition the engine checks in `shape.create` with `addTo`). `lasso_ui.rs` calls it on the Freeform Pen's press, when no stroke is active, and refuses the press before the drag starts: the status bar reads \"Locked\" as an error, and no drag is taken, so there is no stroke to lose and no History step. Because the refusal comes before the stroke, the silent loss in `freeform_finish` can no longer be reached through a lock that was in place at the press.\n\n`freeform_refuses_a_locked_shape_before_starting_a_stroke` covers both lock types (`all`, `pixels`): the press starts no drag, the status is an error, the History length and the shape path are unchanged, and after undoing the lock a stroke starts.\n\nWhat else changed since f15c93c: control dispatch and import moves from rebasing onto main, with a Work Path comment relocated in `pen.rs`. I read the 113 differing PR-only lines in full. The earlier safeguards (ordinary-Pen draft restore, malformed target, empty vector mask polarity, open-subpath merge) are untouched by this delta.\n\nNot verified: I did not run anything locally, and the new test was read, not executed. A lock applied after the press and before release is not covered by the preflight; I did not look at that case.\n",
"commit": {
"oid": "d24669d412f64ab079a668b0b8520ddbc607410c"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"merge_components",
"set_operation",
"covered",
"merge_shape_components_bakes_a_paths_operations",
"merge_shape_components_on_a_shape_layer_rerenders_it",
"merging_a_vector_mask_to_nothing_keeps_what_it_shows",
"set_operation_changes_existing_components",
"set_operation_on_a_joined_subpath_changes_its_component",
"set_operation_leaves_the_first_component_combining",
"bad_set_operation_params_change_nothing",
"bad_merge_params_change_nothing",
"merge_refuses_open_subpaths",
"pen_mode_param",
"pen_path_op_param",
"ui_set_drives_the_pen_path_operation",
"default_pen_path_op",
"op_value",
"target",
"component_at",
"crossings",
"has_keys",
"app",
"click",
"square",
"shape",
"ops",
"covered",
"history",
"key",
"a_selected_component_takes_the_picked_operation",
"plus_and_minus_pick_combine_and_subtract",
"path_selection_selects_and_moves_components",
"a_stale_selection_changes_nothing",
"the_first_component_keeps_combining",
"the_pen_does_not_edit_a_hidden_component_selection",
"dragging_a_live_shape_keeps_it_live",
"the_pen_carries_on_an_open_shape_path",
"an_open_component_of_a_filled_shape_keeps_its_paint",
"the_pen_carries_on_a_vector_masks_open_subpath",
"commit_path",
"commit_resumed_on_layer",
"merge_target",
"path_op_button",
"app",
"square",
"shape_path",
"work",
"layers",
"covered",
"subtract_front_shape_cuts_a_hole_in_the_selected_shape_layer",
"a_locked_shape_layer_keeps_the_drawn_path",
"refused_hole",
"assert_kept_as_work_path",
"switching_tools_ends_a_refused_draft_once",
"esc_ends_a_refused_draft_and_return_keeps_it",
"an_operation_without_a_selected_shape_starts_a_new_shape_layer",
"path_mode_adds_subpaths_with_the_operation",
"the_options_bar_menu_sets_the_operation_and_merges",
"merge_is_disabled_with_fewer_than_two_components",
"an_unknown_operation_falls_back_to_new_layer",
"freeform_refuses_a_locked_shape_before_starting_a_stroke"
],
"regression_source": "https://github.com/storytold/photocraft/pull/7648"
},
{
"number": 7255,
"title": "fix(engine): guard trim and duplicate merged against documents exceeding render limits (#7223)",
"url": "https://github.com/storytold/photocraft/pull/7255",
"body": "Fixes #7223\n\n### Problem\n`photocraft_compose::flatten` refuses to render canvas areas larger than `MAX_RENDER_PIXELS` (2^30 px) and returns an empty buffer to prevent allocation aborts. When `image.trim` or `image.duplicate {\"mergedOnly\": true}` was run on a document exceeding this limit (e.g. 40,000 × 30,000 px):\n- `image.trim` with `basedOn: \"topLeft\"` panicked with an out-of-bounds index on `buf.px[0]`, and other modes reported a misleading \"nothing to trim to\".\n- `image.duplicate {\"mergedOnly\": true}` passed the empty buffer to `Surface::write_region`, triggering an assertion failure panic on the data length.\n\n### Solution\n- In `crates/engine/src/image_cmds.rs:trim`, guarded against canvas bounds exceeding `photocraft_compose::MAX_RENDER_PIXELS` (or returning an empty buffer for non-empty canvas), returning `Err(EngineError::Other(\"the document is too large to trim\".into()))`.\n- In `crates/engine/src/image_cmds.rs:duplicate`, guarded `mergedOnly` against document bounds exceeding `photocraft_compose::MAX_RENDER_PIXELS`, returning `Err(EngineError::Other(\"the document is too large to duplicate merged\".into()))`.\n- Added regression unit test `trim_and_duplicate_merged_on_too_large_document_fail_gracefully_without_panic` in `image_cmds.rs` verifying both commands fail gracefully without panicking and preserve the document.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "1b810052bc3a876c1eaf1a8766870ffdba9f8a80",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/image_cmds.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118356733559"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118356733789"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118356733959"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118356733916"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118356733704"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118356735668"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118356803756"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118356803721"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39339348810/job/118365383540"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-19T23:37:36Z",
"url": "https://github.com/storytold/photocraft/pull/7255#pullrequestreview-5579038009",
"body": "Reviewed the current head. The shared render-limit helper checks the pixel budget and rejects empty flatten output before command mutation; the regression tests confirm document state, revision, and document count remain unchanged on failure. Exact-head CI passed with no unresolved review threads.",
"commit": {
"oid": "1b810052bc3a876c1eaf1a8766870ffdba9f8a80"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"flatten_within_render_limit",
"trim_and_duplicate_merged_on_too_large_document_fail_gracefully_without_panic"
],
"regression_source": "https://github.com/storytold/photocraft/pull/7255"
},
{
"number": 7153,
"title": "Smart Filters: double-clicking a Liquify filter reopens it in the Liquify workspace",
"url": "https://github.com/storytold/photocraft/pull/7153",
"body": "## What's wrong\n\nDouble-clicking a Liquify smart filter in the Layers panel opened the generic parameter form, whose only field is Mesh Size, instead of the Liquify workspace. `smart_ui::open_editor` sent Camera Raw and the Filter Gallery to their own dialogs, adjustments to their Image › Adjustments editors, and every other filter to `filter_dialog`. OK on that form replayed the recorded strokes on the form's mesh size. Filter › Liquify always opens with no strokes, so the recorded strokes could not be edited again.\n\nTo see it: make a smart object, run Filter › Liquify, drag one stroke, press OK, then double-click the Liquify row under Smart Filters.\n\n## What changed\n\n`main` has the first three items since #8577, which merged this pull request at its earlier head (cfa5242). The diff against `main` is now the fourth item and its test.\n\n- `open_editor` sends a Liquify smart filter to a new `liquify_ui::open_smart_filter`.\n- `open_smart_filter` opens the workspace on the smart object's pixels below that filter, with the filter's recorded strokes and mesh size, as `gallery_ui::open_smart_filter` does (#3426).\n- OK then runs `layer.smartFilter.setParams` with the strokes and the mesh size in one history step, and no second Liquify filter is added. OK after Restore All leaves the filter with no strokes. Cancel leaves the filter unchanged. Filter › Liquify on a smart object still adds a new filter.\n- `open_smart_filter` captures the filter with `smart_filter_dialog::target`, and OK checks it with `smart_filter_dialog::validate_target` before `setParams`, as the other Smart Filter editors do. When the document or the filter stack changed while the workspace was open, OK updates nothing and the status bar asks to reopen the dialog.\n\nTen-W reported this in #6571. Part of #6571: the Puppet Warp and Perspective Warp smart filters it also lists are left for later.\n\n## Tests\n\nTests in `dialogs/liquify_ui/tests.rs` use a smart object with a Liquify filter (one stroke, mesh size 3) under a Gaussian Blur. The first three are in `main` with #8577:\n\n- `smart_filter_double_click_edits_the_recorded_liquify` checks the workspace's strokes, mesh size and source pixels, then that a new stroke and OK update the filter in one step and Undo restores it. Without the new `open_editor` branch it fails at \"no generic parameter form\".\n- `smart_filter_cancel_keeps_and_restore_all_clears_the_recorded_strokes` fails without the `setParams` route in `commit`.\n- `open_smart_filter_rejects_other_filters_and_layers` (the blur, an index past the end, a pixel layer) fails without the Liquify check.\n- `smart_filter_edit_after_the_stack_changed_updates_nothing` moves the Liquify filter below the blur while the workspace is open and checks that OK leaves both filters and the history unchanged. Without the `validate_target` check, OK reports no error and the test fails.\n\nOn Windows 11 (x86-64) with rustc 1.99.0, on top of `main` at dacecbf (merge 6f5d929):\n- `cargo xtask ci` passes fmt, clippy, layers and scorecard.\n- `cargo test --workspace --locked --features heif --lib --bins --tests --no-fail-fast`: 12,079 passed, 1 failed, 93 ignored. The failure is in a crate this branch does not change and fails on `main` on this machine too: `photocraft-gpu`'s `a_refresh_that_submits_several_times_is_one_measurement` (the timing test from #7275, on this GPU).\n- The panic_hunt, doctest and wasm steps pass.\n\nThe branch merges cleanly into `main` at eb6c1ab.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "6f5d929d6172cf211a070f12cde9507aca9eb30a",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/dialogs/liquify_ui.rs"
},
{
"path": "crates/ui-egui/src/dialogs/liquify_ui/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517109847"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524793/job/122517109506"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524793/job/122517184637"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517109917"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524793/job/122517185087"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517109603"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517109931"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524793/job/122517183471"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517111645"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517111783"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517111266"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517337255"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517337218"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40597524762/job/122517337334"
},
{
"name": "CodeRabbit",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://app.coderabbit.ai/change-stack/storytold/photocraft/pull/7153?scope=ghh_OusAJGA4HG8ljoOV0wJvVx8NmnZIjc2DdjmxOu0xZWA"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-27T23:20:12Z",
"url": "https://github.com/storytold/photocraft/pull/7153#pullrequestreview-5675752399",
"body": "Re-review at SHA 6f5d929 (my last APPROVE was at d3dd5d6). Gate is green.\n\nWhat changed: a merge of current main only. Main moved `crates/ui-egui/src/liquify_ui.rs` to `crates/ui-egui/src/dialogs/liquify_ui.rs` and its tests to `dialogs/liquify_ui/tests.rs`; the PR's changes to that file followed it.\n\nChecked: the PR-only changes were 62 added/removed lines before and are 62 now. I compared the two sets line by line, ignoring the file name and indentation: they are identical. The 54 lines that differ in the file-attributed comparison are exactly the same lines now sitting in `dialogs/liquify_ui.rs` (33) and `dialogs/liquify_ui/tests.rs` (29). Nothing was added, removed or edited apart from the move.\n\nNot verified: I did not run anything locally; the hosted gate is green at this head.\n",
"commit": {
"oid": "6f5d929d6172cf211a070f12cde9507aca9eb30a"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"smart_filter_edit_after_the_stack_changed_updates_nothing"
],
"regression_source": "https://github.com/storytold/photocraft/pull/7153"
},
{
"number": 6621,
"title": "Crop: right-click on the pending crop opens the crop menu (#6561)",
"url": "https://github.com/storytold/photocraft/pull/6621",
"body": "Closes #6561\n\nPhotoshop opens a context menu on a pending crop frame with the ratio presets, the saved crop presets, the overlay options and Reset / Cancel / Commit; PhotoCraft opened nothing (the Rectangular Marquee, Lasso, Magic Wand and Pen all open theirs).\n\n## What lands\n\n- **`canvas_tool_menu`**: a crop menu, opened from the right-click path only when `crop_ui::pending` (no menu on the untouched default frame; outside the image the pasteboard menu still wins). Rows:\n - the options-bar ratios (Original Ratio, 1 : 1, 4 : 5, 5 : 7, 2 : 3, 16 : 9), the current one checked; each routes through `crop_size::chosen` so the frame refits exactly as choosing it in the options bar does;\n - the saved crop presets (snapshotted when the menu opens), routed through `crop_presets::select` like the dropdown's entry;\n - the gear toggles Use Classic Mode / Show Cropped Area / Auto Center Preview, checked and greyed exactly as in the gear popup;\n - Reset (`crop_ui::reset`: frame back to the whole canvas, still pending), Cancel (`crop_ui::cancel`) and Commit (`canvas::commit_crop`).\n - `menu_rows()` renders the dynamic preset rows through the same path as the static menus, in `show()` and on the control channel (`ui.state` / `ui.context.choose`); `engine_call` maps Commit to `image.crop` for the automation policy. The menu closes when the crop stops being pending.\n- **Tests**: 7 new in `canvas_tool_menu::tests` — ratio fit + checkmark, gear toggles + Classic-Mode greying, Reset/Cancel/Commit, preset row, close-on-no-longer-pending, and two egui_kittest harness tests: right-click with a pending frame opens the menu and choosing 16:9 refits the frame; right-click with no pending frame opens nothing.\n- **Docs**: `docs/context-menu-parity.md` Crop row + status line + revision history; scorecard UI-217-8 note; regenerated `docs/scorecard.md`.\n\nRow order and exact contents follow the inferred placement in `docs/context-menu-parity.md` (P1); a dated Photoshop capture can refine labels/order later.\n\n## Verification\n\n- `cargo test -p photocraft-ui-egui --lib` — 2933 passed, 0 failed\n- `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings` — clean\n- `cargo xtask layers` — OK\n- `cargo xtask scorecard` — regenerated, committed\n- `cargo xtask wasm` could not run here (the wasm32-unknown-unknown target is not installed in this environment); the change is plain Rust in `ui-egui` with no platform-specific code.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "ead45134d320cb1002724d5ef44f5c56588bbe1d",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/canvas_tool_menu.rs"
},
{
"path": "crates/ui-egui/src/control.rs"
},
{
"path": "crates/ui-egui/src/crop_size.rs"
},
{
"path": "crates/ui-egui/src/crop_ui.rs"
},
{
"path": "crates/ui-egui/src/transform_type_tests.rs"
},
{
"path": "docs/context-menu-parity.md"
},
{
"path": "docs/scorecard.md"
},
{
"path": "scorecard/ui.toml"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937570/job/121188778068"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937241/job/121188776817"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937582/job/121188778179"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937570/job/121188778404"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937570/job/121188778223"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937570/job/121188778252"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937570/job/121188779329"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937570/job/121188866090"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937570/job/121188866156"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40199937570/job/121188866104"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T15:33:29Z",
"url": "https://github.com/storytold/photocraft/pull/6621#pullrequestreview-5643879866",
"body": "Re-review at ead45134d, covering the two commits added after my approval at 23f7e2039.\n\n**05bc75d29: only a gesture that changes the frame ends Reset's no-op.**\n- In the Up handler (crates/ui-egui/src/crop_ui.rs:858-862), `reset_frame` is now cleared inside `if ok && changed`, except for a `Move` or `Resize` whose rect equals `crop_rect`.\n- Before this commit, a pointer gesture never cleared `reset_frame`. Only the existing paths at :393, :435 and :698 did.\n- No new UI strings: the diff only adds code, comments and tests, so no catalogue entries are needed.\n\n**Revert checks.** Two local variants, so each new test pins one half of the change:\n- **Guard set to `if false`** (gestures never clear, the pre-fix behaviour): `reset_then_frame_edit_back_to_bounds_commits_a_real_crop` fails at :1877. Result: 149 passed, 1 failed.\n- **Guard set to `if true`** (every gesture clears): `reset_then_a_still_click_keeps_the_no_op` fails at :1855.\n\n**ead45134d: docs only** (docs/context-menu-parity.md).\n- The Crop row's branch-point column is back to \"No tool menu\", with a pointer to the menu.\n- A new \"Crop canvas menu (#6561, 2026-10-25)\" section describes the menu.\n- The text matches the menu described in the PR.\n\n**Local checks** (ead45134d merged with main 528491f9a):\n- `cargo fmt --all -- --check`: clean.\n- `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`: clean.\n- `cargo test -p photocraft-ui-egui --lib -- crop`: 150 passed.\n\nGitHub checks on this head are green, including both Windows test jobs, gate, corpus and web; fuzz is skipped.\n\n**Not tested:**\n- The app wasn't run, so there was no manual click or drag on a real canvas.\n- No macOS run.\n- I didn't recheck the earlier commits beyond these local runs.\n\nNot blocking, as before: #6620 (still open) also adds a crop context menu. Please coordinate so only one of them lands.\n",
"commit": {
"oid": "ead45134d320cb1002724d5ef44f5c56588bbe1d"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"crop_checked",
"choose_crop",
"crop_app",
"crop_open",
"frame_ratio",
"crop_menu_ratio_rows_fit_the_frame_like_the_options_bar",
"crop_menu_gear_rows_toggle_the_shield_options",
"crop_menu_reset_cancel_and_commit_act_on_the_frame",
"crop_menu_lists_saved_presets_and_applies_one",
"crop_menu_closes_when_the_crop_is_no_longer_pending",
"right_click_on_a_pending_crop_opens_the_crop_menu",
"right_click_without_a_pending_crop_opens_no_menu",
"reset_then_commit_crops_nothing",
"reset_edit_undo_then_commit_crops_nothing",
"reset_then_a_still_click_keeps_the_no_op",
"reset_then_frame_edit_back_to_bounds_commits_a_real_crop"
],
"regression_source": "https://github.com/storytold/photocraft/pull/6621"
},
{
"number": 6510,
"title": "Automation: resolve configured root aliases for absolute paths (#6426)",
"url": "https://github.com/storytold/photocraft/pull/6510",
"body": "<!-- integration-refresh-20261025:start -->\n**Latest integration checks (2026-10-25):** head `d744f155a07cb1703ef0da09c944e2054ccb1550` includes main `c65b5d25d3cfee71ecdb96b6549721e0deac8840` through a normal merge. On macOS arm64 with Rust 1.99.1, 176 scoped tests pass (0 failed, 0 ignored). Formatting, scoped all-target Clippy with `-D warnings`, layers, and the scoped wasm check pass. Test executables and fixtures were removed after each target. Full-workspace and other-platform checks belong to CI on this head. PhotoCraft tests used `PHOTOCRAFT_LOCALE=en-US`; wasm portability uses the project's portable crate set, which excludes the native MCP server.\n<!-- integration-refresh-20261025:end -->\n\nFixes #6426.\n\nAutomation accepts absolute paths using either the configured root spelling or its canonical spelling. Resolve the root once and keep I/O relative to its held directory capability. Traversal, sibling prefixes, ungranted aliases, symlink escapes and retargeted aliases remain refused. Windows drive, verbatim and UNC roots accept ASCII case variants without changing the suffix; Unix stays case-sensitive.\n\nMerged main `ff4738810134d7f7e68b49f2e4f231b686282ff3` in `ebbfa8956`, retaining automation and format native-Windows CI triggers and every upstream JPEG save option.\n\nFollow-up `8b566e3334b20f726377c6a04be0813c34015fb0` addresses the [configured-parent Windows review](https://github.com/storytold/photocraft/pull/6510#pullrequestreview-5625223092): `absolute()` removes parent components on Windows, so retain an already-absolute configured root verbatim before opening its canonical capability. Extend the parent-component regression to exercise the configured spelling on Windows too, both reads, the canonical spelling, and refusal of a parent traversal into a sibling. Request suffix validation and capability-relative I/O are unchanged.\n\nValidation on `8b566e33` (Windows/MSVC, Rust1.95):\n\n- Enhanced regression on the old implementation: 0 passed / 1 failed with `../inside/launch-spelling.bin` incorrectly refused. Fixed implementation: 1 passed / 0 failed, including the negative traversal/sibling assertions.\n- Full automation suite: 88 unit + 75 integration = 163 passed / 0 failed / 0 ignored; doctests completed. Independently run with normal TEMP and a real Windows 8.3 TEMP alias: the same 163 tests each time.\n- Workspace fmt, strict automation Clippy (`--all-targets --no-deps -- -D warnings`), dependency layers and wasm passed. Local resource profile: engine opt0, other workspace crates opt1, registry dependencies opt0 except wasmi opt2; two build jobs/four test threads, no debug info.\n- [Current-head CI](https://github.com/storytold/photocraft/actions/runs/39920361315) completed successfully: Ubuntu 11,000 passed / 88 skipped; native Windows unit 8,652 passed / 71 skipped and integration 2,277 passed / 18 skipped, all with 0 failures. Logs confirm the enhanced configured-parent regression, case/prefix/junction/retarget tests and the Unix case guard. Documentation, packaging lint and SonarCloud passed. Old human approvals predate this update.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "8bb0a5e67474d36ae08d565251edb1ae780dd044",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": ".github/workflows/ci.yml"
},
{
"path": "crates/automation/src/workspace.rs"
},
{
"path": "crates/automation/src/workspace/tests.rs"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387841257"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086454799"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640168/job/122086452590"
},
{
"name": "lint / lint",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640661/job/122086455054"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640593/job/122086454391"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387842017"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640593/job/122086522128"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086454863"
},
{
"name": "Verified AppImage inputs",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640661/job/122086454848"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387831940"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640593/job/122086523322"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086454730"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387852185"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086454765"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640593/job/122086521606"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086456981"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387775986"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086456382"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387775222"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086456401"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387850784"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387776742"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086647455"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086647463"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560461324/job/122387777736"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40473640567/job/122086647503"
},
{
"name": "CodeRabbit",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://coderabbit.ai?utm_source=cr_app&utm_medium=github"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-27T08:54:40Z",
"url": "https://github.com/storytold/photocraft/pull/6510#pullrequestreview-5664421033",
"body": "Re-review at SHA 8bb0a5e (my last APPROVE was at d744f15).\n\nWhat it does: unchanged from my last review (absolute automation paths beneath the canonical or configured root resolve through the held cap-std capability; traversal and escapes are refused). Since then main moved the automation tests into `workspace/tests.rs` and the PR was merged with it.\n\nChecked: read the PR's own patch to `workspace.rs`, `ci.yml` and `docs/control-protocol.md` at this head: `configured_path`/`root.path` handling, `request_path` and `path_beneath_root` are as I reviewed. The test lines differ from the old head only because of the move to `workspace/tests.rs` and indentation; I confirmed the key tests are still present there (`windows_junction_escape_is_rejected_when_supported`, `windows_root_case_variants_keep_io_inside_the_grant`, `root_alias_tests_work_with_an_aliased_windows_temp`, `retargeting_root_alias_does_not_regrant_the_held_authority`), but I did not compare every test body line by line. Hosted CI: gate, lint, corpus, web and the Windows unit leg passed at this head.\n\nNot verified: I did not run the tests locally; the Windows integration leg was still running when I checked.\n",
"commit": {
"oid": "8bb0a5e67474d36ae08d565251edb1ae780dd044"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"canonical_windows_prefixes_match_without_ambient_resolution",
"windows_root_case_variants_keep_io_inside_the_grant",
"unix_root_prefixes_remain_case_sensitive",
"configured_parent_components_accept_resolved_absolute_paths",
"relative_configured_root_accepts_resolved_absolute_paths",
"trusted_root_names_are_not_validated_as_relative_request_names",
"link_directory",
"remove_directory_link",
"linked_root_accepts_both_spellings_and_new_nested_exports",
"root_alias_tests_work_with_an_aliased_windows_temp",
"ungranted_absolute_aliases_and_link_escapes_are_refused",
"retargeting_root_alias_does_not_regrant_the_held_authority"
],
"regression_source": "https://github.com/storytold/photocraft/pull/6510"
},
{
"number": 6492,
"title": "Fail control startup promptly when its port is occupied",
"url": "https://github.com/storytold/photocraft/pull/6492",
"body": "## Summary\n\nBind the control listener before GPU/display initialization, and pass the bound listener to the server. An occupied port now exits promptly with a useful diagnostic and status 2.\n\n```text\ncontrol startup\n validate token and workspace\n bind 127.0.0.1:port → on failure: report error, exit 2\n initialize display/GPU\n start server with bound listener\n```\n\nFixes #6428.\n\n## Evidence\n\n- **Before:** With its requested port held by another process, the desktop logged a bind error but remained running beyond a 10-second deadline.\n **After:** The rebuilt desktop exits promptly with status 2 and the bind diagnostic, without creating the GPU-startup marker.\n- Added an occupied-port process regression with a bounded deadline, and a unit test that authenticates and dispatches through a prebound listener.\n- Windows GNU: all 130 desktop unit tests and 5 startup integration tests passed. Strict all-target Clippy, formatting, layer checks, and all 24 WebAssembly checks passed for the combined local changes. Loopback tests ran outside the sandbox because it denies local socket access.\n\n## Merge Danger\n\n**Door:** two-way\n\nNo data-format or dependency changes; the startup ordering can be reverted.\n\n**Blast Radius:** startup\n\nControl-enabled launches now fail before display initialization when the requested listener cannot bind.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "657cfd503d4ce276df6b5e9f763cb3a5cf2deaeb",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "apps/photocraft/src/control_server.rs"
},
{
"path": "apps/photocraft/src/main.rs"
},
{
"path": "apps/photocraft/tests/control_exit_code.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920492023"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920491725"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920491909"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920492000"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920492001"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920493104"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920535117"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920534954"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920535003"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117920535136"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39209821050/job/117927895097"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T09:49:00Z",
"url": "https://github.com/storytold/photocraft/pull/6492#pullrequestreview-5583441089",
"body": "Read the whole diff at this head, and `main.rs` around it. The control setup (bind, token, workspace) now runs before the log file, single-instance handling and any display or GPU initialization, and an occupied port prints the bind error and exits 2. The bound listener travels in `control` into the app-creation closure and is handed to `control_server::start`, so the server no longer binds on its own. `--control` launches skip single-instance handling, so moving the setup ahead of it changes nothing there. The unit test authenticates and dispatches through a prebound listener, and the process test checks exit 2, the bind message, no GPU marker, and a 10 second bound. CI is green at this head.\n\nOne thing worth a quick check, not a blocker: the CPU-compatibility relaunch near the end of `main` spawns a new process with the same `--control <port>` after a renderer failure. That works only if the listener has been dropped by then, which holds if `control` is owned by the creator closure that `run_native` consumes on return (it is moved in the `if let Some((listener, token, _)) = control` and not used afterwards). If the child ever exits 2 with \"failed to bind\" after a GPU failure, that is the place to look.\n",
"commit": {
"oid": "657cfd503d4ce276df6b5e9f763cb3a5cf2deaeb"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"a_prebound_listener_authenticates_and_dispatches_requests",
"an_occupied_control_port_exits_with_status_2"
],
"regression_source": "https://github.com/storytold/photocraft/pull/6492"
},
{
"number": 6449,
"title": "Hand tool: Cmd held zooms in and Alt zooms out, as in Photoshop",
"url": "https://github.com/storytold/photocraft/pull/6449",
"body": "Fixes #6434.\n\nThe held-key tools in `hold_keys.rs` (from #275, with ⌥Space from #2877) cover Space, ⌘Space and ⌥Space, and ⌘ alone is the Move tool for the painting tools, but the Hand is rightly left out of that, so with the Hand selected ⌘ and ⌥ did nothing. This adds the Hand's own pair, as in Photoshop: ⌘ (Ctrl off the Mac) held is a temporary Zoom In, ⌥ (Alt) a Zoom Out, ⌘⌥ zooms out too, and the Hand is back when the key comes up. It goes through the same `Temporary` path as ⌘Space, so clicks, scrubby drags and the cursor all follow, and a bound key still wins. Two cases keep ⌘ for something else: an open Free Transform (its distort handles), and a modifier pressed in the middle of a Hand drag, which keeps panning instead of stopping the pan. A latched ⌥ from Window › Modifier Keys doesn't switch the Hand, same as ⌘ for the Move tool; happy to switch if you'd rather it did.\n\nValidation:\n- `hold_keys::tests::cmd_and_alt_with_the_hand_tool_zoom_in_and_out_while_held`: fails on main (`held_tool` is `None` with ⌘ held, expected `ZoomIn`), passes here\n- `hold_keys::tests::cmd_mid_pan_or_during_free_transform_is_not_the_hand_tools_zoom`: guards the mid-drag and Free Transform cases (fails if the mid-drag check is removed)\n- `cmd_is_not_the_move_tool_for_selection_hand_zoom_pen_shape_or_type_tools` now expects Zoom In for the Hand, still never Move\n- `cargo test -p photocraft-ui-egui --lib hold_keys::tests::`, `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`, `cargo fmt --all -- --check`\n- `docs/ui-parity.md` row updated\n- (macOS, Apple silicon)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "25c38a0b69b7bff31badcdbe2ef9f2ff6aba1c63",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/hold_keys.rs"
},
{
"path": "crates/ui-egui/src/hold_keys/tests.rs"
},
{
"path": "docs/ui-parity.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116803819765"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342493/job/116803819355"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116803819719"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116803819843"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116803819739"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116803819486"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116803820724"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116813706769"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116813706755"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116813706778"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116813706672"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38848342487/job/116821996374"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T14:07:53Z",
"url": "https://github.com/storytold/photocraft/pull/6449#pullrequestreview-5548906956",
"body": "Approving `25c38a0b69b7bff31badcdbe2ef9f2ff6aba1c63`.\n\nI read the whole diff. In Photoshop, holding Ctrl (⌘ on the Mac) with the Hand tool temporarily zooms in and holding Alt (⌥) zooms out; here ⌘ alone was not the Hand tool's zoom (#6434). New `hold_keys::hand_zoom_held` returns `Temporary::ZoomOut` while ⌥ is held (with or without ⌘) and `Temporary::ZoomIn` for ⌘ alone, but only for the Hand tool, not while a Free Transform is open (which keeps ⌘ for its handles), and not from the middle of a Hand drag (`primary_down && !primary_pressed`), so a modifier pressed while panning does not stop the pan. `held_tool` chains it last, after a bound temporary key (⌘Space is Zoom In) and the Move case, so existing bindings win. The existing test that ⌘ is never Move for the Hand tool now expects Zoom In for the Hand only. The new tests click with ⌘ and ⌥ and check the zoom goes up and down and that ⌘⌥ gives Zoom Out, that releasing returns the Hand (a drag pans again, the tool is unchanged), that ⌘ mid-pan does not zoom or stop the pan, and that a Free Transform keeps ⌘. The parity doc is updated. Hosted CI is green on this head.\n\nI did not run the tests locally; this approval rests on reading the diff and the green CI.\n",
"commit": {
"oid": "25c38a0b69b7bff31badcdbe2ef9f2ff6aba1c63"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"hand_zoom_held",
"cmd_and_alt_with_the_hand_tool_zoom_in_and_out_while_held",
"cmd_mid_pan_or_during_free_transform_is_not_the_hand_tools_zoom"
],
"regression_source": "https://github.com/storytold/photocraft/pull/6449"
},
{
"number": 6387,
"title": "Video Frames to Layers: every page of a multi-page TIFF (part of #6379)",
"url": "https://github.com/storytold/photocraft/pull/6387",
"body": "Part of #6379 (the multi-page TIFF part).\n\nFile › Import › Video Frames to Layers loads every frame of an animated GIF as its own layer, but a multi-page TIFF gave only its first page. `load_frames` (also behind Layer › Video Layers › New Video Layer from File) now takes every full-resolution page of a TIFF's main IFD chain, in file order, one layer (or frame) each:\n\n- `tiff_pages` lists the pages through `codecs::tiff_info`: main-chain directories (`parent: None`) of kind `TiffPageKind::Page`. Thumbnails and other reduced-resolution copies, transparency masks and SubIFDs are left out.\n- Each page opens through `io::import_tiff_page`, so its orientation is applied and a page with Photoshop layer data contributes its composite, as when that page is opened.\n- A TIFF with one page goes through the usual import as before.\n\nThe change sits next to the GIF branch in `load_frames`. @LloydNicholson's #5635 (animated WebP) changes the same function; the two merge without a conflict in `video_cmds.rs`, and the new test sits apart from #5635's tests.\n\nAPNG frames and an option to open frames or pages straight away as layers (the rest of #6379) aren't part of this.\n\nTest: `video_cmds::tests::video_frames_to_layers_takes_every_tiff_page` writes a TIFF with a thumbnail and two pages of different sizes and grey levels: Video Frames to Layers gives exactly two layers, \"Frame 1\" 6×4 and \"Frame 2\" 3×5, with each page's grey, and a single-page TIFF still gives one layer. It fails on main (one layer). The other `video_cmds` tests pass (30).\n\nDiscord: f4lcon.rs",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "8bfbbf816025942b74130adf16fb07265817b691",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/video_cmds.rs"
},
{
"path": "crates/engine/src/video_cmds/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560395494/job/122387534549"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/116993482056"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560395494/job/122387533154"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/116993482398"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/116993482072"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560395494/job/122387608398"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/116993482054"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560395494/job/122387614274"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/116993482180"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/116993483863"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560395494/job/122387533603"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560395494/job/122387534791"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/116993566990"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560395494/job/122387619184"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/116993567456"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38907152622/job/117001737074"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-17T00:45:37Z",
"url": "https://github.com/storytold/photocraft/pull/6387#pullrequestreview-5555146754",
"body": "Re-reviewed and approving `8bfbbf8160`. My earlier change request on `bfb275ee8` is addressed.\n\n**The point (a truncated TIFF page walk reported as complete).** `tiff_pages` now returns `(pages, complete)` from `TiffInfo.complete`, and `load_frames` returns an error before any layer is made when a multi-page TIFF's walk is incomplete: \"only the first N pages could be read (its page list is damaged or too long), so not every page would be imported\". The regression test I asked for is there: `video_frames_to_layers_refuses_a_tiff_with_an_incomplete_page_list` builds a two-page TIFF, points the last directory's next-IFD offset back at the first (a looping chain), and checks the command errors with the \"only the first 2 pages\" text and that the document's layer count is unchanged. The original feature test (thumbnail left out, two full pages with their own sizes and levels, a single-page TIFF still one layer) is unchanged.\n\nCI on this head is green on Linux and Windows, so I did not repeat the run. Non-blocking residual: the refusal sits inside `pages.len() > 1`, so a chain damaged after exactly one readable page still falls through to the existing single-page import. That is the old behaviour for such a file, so I would not hold the PR for it.\n",
"commit": {
"oid": "8bfbbf816025942b74130adf16fb07265817b691"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"tiff_pages",
"write_tiff",
"video_frames_to_layers_takes_every_tiff_page",
"video_frames_to_layers_refuses_a_tiff_with_an_incomplete_page_list"
],
"regression_source": "https://github.com/storytold/photocraft/pull/6387"
},
{
"number": 6383,
"title": "Windows releases ship a symbols zip: the release build's PDBs for profilers (#688)",
"url": "https://github.com/storytold/photocraft/pull/6383",
"body": "Fixes #688.\n\nWindows releases ship no PDBs, so a profile of the installed PhotoCraft (WPA, Superluminal) shows addresses instead of function names, and a PDB from a local rebuild of the same commit doesn't match the published exes. The release build already writes `photocraft.pdb` and `photocraft_cli.pdb` (the exes record them by bare file name), they just weren't packaged.\n\n## Change\n- `packaging/windows/package.ps1`: after the portable zip, copies both PDBs from the release build into `photocraft-<v>-windows-<arch>-symbols.zip` in `dist/release`, plus `symbols.txt`. It fails the packaging step if a PDB is missing rather than shipping a partial zip. Signing doesn't change the PDB signature an exe records, so the PDBs match the signed exes. The shared release workflow (`craft-common-actions` `release.yml` → `draft-release`) uploads everything in `dist/release` and adds it to `SHA256SUMS.txt`, so no workflow change is needed.\n- `packaging/windows/symbols.txt` (new): which binaries they match, and how to load them (copy next to the exe, or add the folder in WPA / Superluminal / Visual Studio / `_NT_SYMBOL_PATH`).\n- `docs/releasing.md`: the asset table and a short \"Symbols zip\" note.\n\nThe release profile has `debug = 0`, so these PDBs carry function names but no source line tables. That is what function-level flame graphs need. Line tables (`CARGO_PROFILE_RELEASE_DEBUG=line-tables-only` for the Windows build) could follow if maintainers want them; I left the build settings alone. The PDB from my local release build is about 9.8 MB (2.3 MB zipped). macOS dSYMs and Linux debug files would be follow-ups in their own packaging scripts.\n\n## Tests\nThere's no test harness for the packaging scripts. I ran these locally:\n- PowerShell's parser on `package.ps1`: 0 errors.\n- The new block on its own against a local `target/release`: it copied `photocraft.pdb` and `symbols.txt` and zipped them. It threw the intended error for the missing `photocraft_cli.pdb`, since I had only built the app.\n- Checked that a release `photocraft.exe` records its PDB as just `photocraft.pdb`, so the shipped file names resolve.\n\nI haven't run the full `package.ps1` (it needs WiX and signing). The release workflow will exercise it.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "74aadae69e3b3f5cd257bca1926acc1b1297b1a2",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "docs/releasing.md"
},
{
"path": "packaging/windows/package.ps1"
},
{
"path": "packaging/windows/symbols.txt"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560364194/job/122387417872"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119301628192"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181384/job/119301626448"
},
{
"name": "lint / lint",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623182501/job/119301630907"
},
{
"name": "windows-arm64 / package (cross-compiled on x64)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623182627/job/119301630376"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560364194/job/122387418467"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119301628131"
},
{
"name": "Verified AppImage inputs",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623182501/job/119301630395"
},
{
"name": "windows-arm64 / install and run on ARM64",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623182627/job/119311771215"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119301628051"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560364194/job/122387467550"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119301628033"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560364194/job/122387417127"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119301627977"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119301630320"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560364194/job/122387468249"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560364194/job/122387418796"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119301726149"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119301728200"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560364194/job/122387418727"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39623181350/job/119315101451"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T13:09:48Z",
"url": "https://github.com/storytold/photocraft/pull/6383#pullrequestreview-5548207780",
"body": "Approving `5c87d9a91fbec895c9089a030c85eaefd693a487`.\n\nI read the whole diff. `packaging/windows/package.ps1` now also builds `photocraft-<version>-windows-<arch>-symbols.zip` from `photocraft.pdb` and `photocraft_cli.pdb` in the release `$Bin` (it throws if either is missing, so a release cannot silently ship without them) plus a new `symbols.txt` explaining how to load them in a profiler; `docs/releasing.md` lists the new artifacts. `$Bin`, `$TargetDir`, `$Dist` and `$PSScriptRoot` are all defined earlier in the script. The diff also carries a rustfmt-only reflow in `smart_filter_authorization.rs`. Hosted CI is green on this head, but that CI does not run `package.ps1`.\n\nI did not run the script, so the release path is unexercised by me. Not checked, for whoever cuts the next release: that `release.yml` uploads every file in `dist/release` (and so picks up the new zips) rather than a fixed list, that the x86 and arm64 builds write both PDBs to `$Bin` under those names, and that the claim that signing leaves the PDB signature unchanged holds.\n",
"commit": {
"oid": "74aadae69e3b3f5cd257bca1926acc1b1297b1a2"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Platform and reliability",
"new_test_names": [],
"regression_source": "https://github.com/storytold/photocraft/pull/6383"
},
{
"number": 6236,
"title": "Shift+Alt+right-click opens Photoshop's HUD Color Picker (#6098)",
"url": "https://github.com/storytold/photocraft/pull/6236",
"body": "## What\n\nImplements the HUD Color Picker (#6098): with a painting tool, **Shift+Alt+right-click** on the canvas now opens Photoshop's floating hue strip + saturation/value square anchored under the pointer, instead of the Brush Preset picker.\n\n- While the right button stays down, moving the pointer over the HUD sets the foreground colour live; releasing the button keeps the colour and closes the HUD.\n- **Esc** restores the colour the gesture started from.\n- The gesture's `tools.setColors` calls share one `coalesce` key, so the whole pick is one history step (Rule 1); nothing paints and no document edit happens.\n- `brush_resize::is_right_gesture` now requires Shift *not* be held, so Alt+right-drag brush resize and the new Shift+Alt HUD gesture stay distinct. Plain right-click still opens the Brush Preset picker.\n- Agents can drive it: `ui.pointer` with `\"button\": \"right\", \"shift\": true, \"alt\": true` (down opens, up closes).\n- New module `crates/ui-egui/src/color_hud.rs` (drawn as meshes, hit-tested from the raw pointer, since egui widgets can't track a held right button); `UiState.color_hud` exposes it to the control channel; documented in `docs/color-picker.md`.\n\n## Tests\n\n- 7 new unit tests (open/gesture exclusivity, live pick + coalescing, release keeps, Esc restores, control channel, `pick_at` mapping, regressions for plain right-click and Alt+right-drag resize).\n- `cargo test -p photocraft-ui-egui --lib`: 2933 passed, 0 failed.\n- `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`: clean.\n- `cargo xtask layers`: OK.\n- Rendered offscreen via the snapshot example and verified the hue strip/square geometry and clamping on screen.\n\nFixes #6098",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "ce546dd4b5451a17b843ef1e129bdb40fbfe3160",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/brush_resize.rs"
},
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/color_hud.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/paint_mouse.rs"
},
{
"path": "crates/ui-egui/src/state.rs"
},
{
"path": "docs/color-picker.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699453/job/121206030438"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699005/job/121206028934"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699475/job/121206030217"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699453/job/121206030626"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699453/job/121206030497"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699453/job/121206030305"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699453/job/121206031478"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699453/job/121206111231"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699453/job/121206111021"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40205699453/job/121206111009"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-25T16:15:01Z",
"url": "https://github.com/storytold/photocraft/pull/6236#pullrequestreview-5644088784",
"body": "Re-review at ce546dd4b. Approving.\n\n**The point from my last review (5643616409) is fixed.** That point was that a Shift+Alt Down followed by a plain Up left the HUD open.\n- `pointer_secondary` (`crates/ui-egui/src/paint_mouse.rs:182-193`) now treats any Up as closing a non-held HUD (`hud_up = !down && color_hud.is_some_and(|h| !h.hold)`).\n- `color_hud::release` still closes only a non-held HUD, so a real-mouse (held) HUD waits for its own button release.\n- **Revert check:** with `if hud_gesture || hud_up {` changed back to `if hud_gesture {`, the new test `a_control_channel_up_closes_the_hud_without_repeating_its_modifiers` fails (panic at `paint_mouse.rs:612`, \"the Up closes it whatever the modifiers say\"). It passes with the fix.\n\n**The earlier points still hold.** I checked them on a worktree of ce546dd4b merged with current main (528491f9a), with `-p photocraft-ui-egui`:\n- `cargo fmt --all -- --check` is clean.\n- `cargo clippy --all-targets -- -D warnings` is clean, so the `hsb_to_rgb` import in `color_hud.rs` builds after merging main.\n- `cargo test --lib -- color_hud paint_mouse brush_resize canvas_tool_menu brush_picker` gives 80 passed.\n- **Quick Selection:** with the HUD gate at `paint_mouse.rs:142` changed from `right_click_picker(tool)` to `has_brush_picker(tool)`, `quick_selections_shift_alt_right_opens_only_the_selection_menu` fails (`color_hud.rs:326`).\n- **Pointer-move guard:** with `!hud.just_opened &&` removed at `color_hud.rs:212-213`, `a_hud_clamped_under_the_still_pointer_picks_nothing_until_it_moves` fails (`color_hud.rs:345`).\n- No new UI strings in this push.\n\nCI on ce546dd4b is green: gate (ubuntu-24.04), Windows unit and integration, corpus, without-heif, web/wasm size gate, mdBook and SonarCloud.\n\nNot tested by me: the HUD with a real mouse and keyboard (only the unit tests above), macOS and Windows locally, and the full workspace test suite.\n",
"commit": {
"oid": "ce546dd4b5451a17b843ef1e129bdb40fbfe3160"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"rgb_color",
"hue_strip_mesh",
"paint_hue_strip",
"paint_sv_square",
"set_foreground",
"harness",
"harness_size",
"button",
"rects",
"foreground",
"shift_alt_right_press_opens_the_hud_not_the_picker",
"quick_selections_shift_alt_right_opens_only_the_selection_menu",
"a_hud_clamped_under_the_still_pointer_picks_nothing_until_it_moves",
"dragging_over_the_hud_picks_the_foreground_live_and_release_keeps_it",
"escape_restores_the_starting_colour",
"plain_right_click_still_opens_the_picker_and_alt_drag_still_resizes",
"shift_alt_right_drag_does_not_resize_the_brush",
"the_control_channel_opens_and_closes_the_hud",
"hue_strip_mesh_indices_stay_within_its_rows",
"pick_at_maps_the_rects_and_ignores_the_gaps",
"shift_alt_right_through_the_control_channel_respects_the_erase_preference",
"a_control_channel_up_closes_the_hud_without_repeating_its_modifiers"
],
"regression_source": "https://github.com/storytold/photocraft/pull/6236"
},
{
"number": 6139,
"title": "Fix painting workflows, File Info and layer selection",
"url": "https://github.com/storytold/photocraft/pull/6139",
"body": "This change fixes nine painting and dialog workflows reported against PhotoCraft 0.9:\n\n- File Info's Description, Keywords and Copyright fields accept multiple lines. Enter inserts a line break, and the scrollable form keeps its buttons accessible. Fixes #6116.\n- Brush and Pencil offer Behind painting, preserving opaque pixels and compositing beneath translucent pixels in the document's native color channels. Fixes #6115.\n- Eyedropper samples become grayscale foreground/background values when targeting a layer mask, Quick Mask or alpha channel. Fixes #6086.\n- Holding Space during a Pen drag moves the anchor and both handles together; releasing Space resumes handle editing. Fixes #6084.\n- The color picker accommodates longer translated action labels and translates its foreground/background title. Fixes #6109.\n- The Move tool shows the platform copy cursor for Alt-drag duplication, retaining that cue until the active copy drag finishes. Fixes #6102.\n- File Info uses Document Title, Copyright Notice and Copyright Info URL labels. Fixes #6125.\n- File Info orders fields by the requested metadata workflow instead of alphabetically. Fixes #6126.\n- An assignable Select Layer Under Cursor command selects the visible hit layer through rotated, flipped and zoomed views, preserving selection on misses and refusing changes during modal dialogs or strokes. Fixes #2298.\n\nRegression coverage includes real keyboard/pointer interactions, metadata confirm/reopen and cancel, masks at 8/16/32-bit depth, native RGB/Gray/CMYK/Lab Behind compositing, selection/transparency locks, live stroke composition, and undo/redo. All visual fixtures are generated locally from PhotoCraft's own UI and synthetic document contents.\n\nValidation: 3,042 UI unit/integration tests passed (13 ignored), plus the engine and paint suites (1,694 and 106 unit tests, respectively), doctests, and the ignored adversarial-command test. Touched-crate clippy with warnings denied, formatting, layer checks, wasm checks and parity generation pass. File Info, German Color Picker and Behind painting were rendered and inspected.\n\n`cargo xtask perf --quick` passed on Apple M4: the 24 MP brush-release scenario measured 1.23 ms median / 1.73 ms p95. The stored baseline uses M4 Pro, so the harness skipped cross-machine regression comparison; this is not a claim that all existing performance budgets are met.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "e2347f8ead85d929193044f139788b850e31c06f",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/automation/tests/smart_filter_authorization.rs"
},
{
"path": "crates/engine/src/brush_cmds.rs"
},
{
"path": "crates/engine/src/brush_cmds/tests.rs"
},
{
"path": "crates/engine/src/shell_commands.rs"
},
{
"path": "crates/paint/src/brush.rs"
},
{
"path": "crates/paint/src/brush_tests.rs"
},
{
"path": "crates/paint/src/render.rs"
},
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/color_picker_ui.rs"
},
{
"path": "crates/ui-egui/src/dialogs.rs"
},
{
"path": "crates/ui-egui/src/eyedropper_ui.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/input_tests.rs"
},
{
"path": "crates/ui-egui/src/menus.rs"
},
{
"path": "crates/ui-egui/src/move_mods.rs"
},
{
"path": "crates/ui-egui/src/options_bar_mode_wheel_tests.rs"
},
{
"path": "crates/ui-egui/src/panels.rs"
},
{
"path": "crates/ui-egui/src/pen_draw_tests.rs"
},
{
"path": "crates/ui-egui/src/vector_ui.rs"
},
{
"path": "crates/ui-egui/src/view_cmds.rs"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38837429747/job/116770263900"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38837429883/job/116770235444"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38837429747/job/116770264170"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38837429747/job/116770264192"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38837429747/job/116770264790"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38837429747/job/116780046269"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38837429747/job/116780046275"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38837429747/job/116780046221"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T02:15:01Z",
"url": "https://github.com/storytold/photocraft/pull/6139#pullrequestreview-5542220545",
"body": "Re-reviewed exact head e2347f8ead85d929193044f139788b850e31c06f against my earlier approval at d1134603144bd960b64a6891ebaffb53e80060f8. The range-diff preserves the nine scoped workflows. The layer-under-cursor command now lives in the upstream shared SHELL_COMMANDS registry, with the UI invocation retained. The canvas visibility guard change is boolean-equivalent and preserves finite-coordinate short-circuiting. The inherited smart-filter authorization test change is formatting only. I found no new blocking issue in these refresh changes.\n\nThis is a source re-review. I have not independently rerun tests or native desktop checks on this head; the author's validation and hosted CI remain separate evidence.",
"commit": {
"oid": "e2347f8ead85d929193044f139788b850e31c06f"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"behind_brush_and_pencil_preserve_foreground_and_round_trip_history",
"behind_preserves_native_channels_and_respects_alpha_selection_and_locks",
"german_color_picker_fits_its_actions_and_translates_the_title",
"file_info_multiline_fields_keep_enter_and_cancel_discards_edits",
"file_info_keeps_multiline_metadata_on_confirm_and_reopen",
"mask_sampling_sets_gray_swatches_without_changing_pixel_sampling",
"move_alt_copy_cursor_follows_the_latched_duplication_drag",
"assignable_pick_layer_shortcut_follows_transformed_canvas_without_editing",
"select_layer_under_cursor",
"space_repositions_a_pen_anchor_and_keeps_its_handles"
],
"regression_source": "https://github.com/storytold/photocraft/pull/6139"
},
{
"number": 6026,
"title": "Marquee: Fixed Size selects exactly the entered width and height (fixes #6015)",
"url": "https://github.com/storytold/photocraft/pull/6026",
"body": "Fixes #6015.\n\n**Problem:** with the Rectangular (or Elliptical) Marquee set to Style › Fixed Size, the selection was one pixel larger on each side than the size entered: 10 × 10 selected 11 × 11, and 1 × 1 selected 2 × 2. The fixed rectangle starts at the press point, which usually sits between pixel edges (e.g. x = 50.4, so 50.4..60.4). `marquee_px` then floors the near edge and ceils the far edge, which gives 50..61. This is the same cause as #5472 for ⇧ squares.\n\n**Fix:** `canvas::fixed_size_px` builds the Fixed Size rectangle on whole pixels: the near edge goes to the closest pixel edge and the far edge is exactly W × H away. Both the live outline and readout and the selection made on release use it, through `marquee_preview_px`. A click without a drag now places the Fixed Size selection, as in Photoshop. Before, a click deselected. Normal and Fixed Ratio are unchanged: a click still deselects.\n\n**Tests:**\n- `canvas::tests::fixed_size_px_is_exactly_the_size`: down-right and up-left, 1 × 1, fractional sizes.\n- `marquee_tests::fixed_size_selects_exactly_the_entered_size`: through the real canvas, for both marquee tools and 10 × 10, 1 × 1 and 64 × 32. The press lands between pixels, then the test checks the readout, the dragged selection and a click.\n\n**Checks run locally:** the marquee tests in `photocraft-ui-egui` (`fixed_size`, `marquee_styles`, `square_px`, `one_pixel`, `the_live_outline`), `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`, `cargo fmt --check`. No new UI strings. CI covers the rest.\n\nDiscord: f4lcon.rs",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "248ba660576f4896f69cbb6639353e2f1d295271",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/marquee_tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38724036741/job/116385373038"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38724036741/job/116385372622"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38724036741/job/116385372882"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38724036741/job/116385374685"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38724036741/job/116386380034"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38724036741/job/116386379984"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38724036741/job/116386379875"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T09:43:20Z",
"url": "https://github.com/storytold/photocraft/pull/6026#pullrequestreview-5545897025",
"body": "Reviewed 248ba660576f4896f69cbb6639353e2f1d295271. The fixed-size branch now constructs an exactly rounded width/height rectangle and consistently uses it for preview and release, including a click. Normal/fixed-ratio click behavior and the separate square constraint are retained.\n\nNo blocking defect found in this change. I reviewed the source and regression coverage and checked the current-head hosted CI results; I did not execute a new local test run for this review. Native interaction/physical-display behavior has not been independently qualified here.",
"commit": {
"oid": "248ba660576f4896f69cbb6639353e2f1d295271"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"fixed_size_px_is_exactly_the_size",
"fixed_size_selects_exactly_the_entered_size"
],
"regression_source": "https://github.com/storytold/photocraft/pull/6026"
},
{
"number": 5939,
"title": "fix(paint): scale blur sigma with brush size (#5892)",
"url": "https://github.com/storytold/photocraft/pull/5939",
"body": "Fixes #5892\n\n### Summary of Changes\n- In `crates/engine/src/retouch_cmds.rs`, decoupled the blur tool sigma calculation from sharpen: blur sigma now scales with brush size `(stroke.brush.size * 0.15).clamp(1.0, 64.0)` instead of `(stroke.brush.size * 0.03).clamp(1.0, 4.0)`, ensuring visible, perceptually expected blur changes at standard 8-bit quantization.\n- Added regression unit test `blur_scales_with_brush_size_and_diffuses_pixels_visibly` in `crates/engine/src/retouch_cmds/tests.rs` verifying pixel diffusion across step edges for 8-bit and 16-bit depths at brush sizes 20 and 60.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "7348e060b4be7204fb69b59079c3b9d808e9aad3",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/retouch_cmds.rs"
},
{
"path": "crates/engine/src/retouch_cmds/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117501274128"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117501274320"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117501274331"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117501274302"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117501274321"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117501275369"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117512091266"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117512091213"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39068585415/job/117524050675"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-17T23:23:14Z",
"url": "https://github.com/storytold/photocraft/pull/5939#pullrequestreview-5560602826",
"body": "Re-reviewed head 7348e060. Both points from my earlier review are handled:\n\n1. The `canvas.rs` change is reverted. The diff now touches only `crates/engine/src/retouch_cmds.rs` and its tests, so the retouch footprint trail from #189 stays as it was.\n2. `blur_scales_with_brush_size_and_diffuses_pixels_visibly` pins the new Blur curve at 8 and 16 bit. Size 20 leaves x=45 and x=75 untouched, size 60 spreads past both, and the edge spreads further at size 60. With the old `(size * 0.03).clamp(1.0, 4.0)`, size 60 gives sigma 1.8, so the `p60[45] > 0` check couldn't pass at 15 px from the edge. The test really guards the change.\n\nLocally on Linux: `cargo +1.99.0 fmt --all -- --check` is clean, and `cargo test -p photocraft-engine --lib retouch_cmds` passes 84 of 84. Hosted CI on this head is fully green.\n\nTwo small notes, not blocking:\n- The PR title (\"…and eliminate false selection trail\") and the description still describe the `canvas.rs` change that was reverted. Please update them so the squash message matches.\n- The `0.15` / `64.0` constants (`retouch_cmds.rs:938`) still have no Photoshop measurement behind them. They're clearly better than a Blur that does nothing (#5892), but a maintainer may want to tune them.\n\nApproving.\n",
"commit": {
"oid": "7348e060b4be7204fb69b59079c3b9d808e9aad3"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"blur_scales_with_brush_size_and_diffuses_pixels_visibly"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5939"
},
{
"number": 5870,
"title": "fix(format): reject duplicate native layer identities",
"url": "https://github.com/storytold/photocraft/pull/5870",
"body": "A malformed native document can contain duplicate layer IDs, leaving layer-comp and other layer references ambiguous even if the loader remaps IDs.\n\n- Validate uniqueness across the complete layer tree before payload reads or ID allocation.\n- Reject duplicates in both preserve-ID and remapping modes, including extreme IDs.\n- Keep smart-object contents identities and embedded-document namespaces independent.\n\nRegression coverage: Regression tests cover duplicates across siblings, ancestors and groups, extreme IDs, rejection before payload fetches, valid remapped comp targets, shared smart contents, embedded documents, and empty trees.\n\nScope: Native-format load validation only; no document schema changes.\n\nValidation:\n- Reviewed the branch diff and checked for existing branch PRs and overlapping contributions.\n- `git diff --check`: passed; merge check against current `main`: clean.\n- Local Rust/corpus gates were not rerun in this publishing environment because Cargo and rustc are unavailable. Included tests are described above; this is not a claim that they have passed here. Upstream PR CI will be checked after submission.\n- No UI layout changes, command additions, or measured scorecard changes.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "f9de0a6abc78aa405b1ef46f5da6cba424106204",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/format/src/convert.rs"
},
{
"path": "crates/format/tests/layer_identities.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560829488/job/122389127439"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38711556557/job/116343905671"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560829488/job/122389124774"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38711556557/job/116343905350"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560829488/job/122389050996"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38711556557/job/116343905849"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560829488/job/122389107408"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38711556557/job/116343908375"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560829488/job/122389116977"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38711556557/job/116344981878"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560829488/job/122389051704"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38711556557/job/116344981764"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40560829488/job/122389052387"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T18:02:42Z",
"url": "https://github.com/storytold/photocraft/pull/5870#pullrequestreview-5551781697",
"body": "Approving. `Loader::document` now validates layer-id uniqueness across the whole tree before any payload fetch or id allocation, in both preserve and remap modes (remapping cannot repair duplicates because layer comps and slices would still be ambiguous). Shared smart-object contents ids are deliberately exempt. The new tests cover nested groups and layer comps. Compatibility risk: a previously loadable file containing duplicate layer ids now fails with `duplicate layer id N`. I cannot tell from the diff whether any released version wrote duplicates (for example through `duplicate()`), so a maintainer may want to check. Not run locally.\n",
"commit": {
"oid": "f9de0a6abc78aa405b1ef46f5da6cba424106204"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"check_ids",
"sample",
"rewrite_manifest",
"assert_duplicate",
"duplicate_layer_ids_are_rejected_across_the_whole_tree",
"duplicate_extreme_layer_ids_are_not_silently_remapped",
"duplicate_layer_ids_are_rejected_before_fetching_payloads",
"get",
"unique_nested_layer_ids_preserve_comp_targets_in_both_load_modes",
"layer_identity_validation_keeps_smart_document_and_contents_scopes_independent",
"an_empty_layer_tree_loads_in_both_modes"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5870"
},
{
"number": 5744,
"title": "fix(ui): keep detached documents in one interactive window",
"url": "https://github.com/storytold/photocraft/pull/5744",
"body": "## Summary\r\n- Keep a torn-off document active and remove it individually from the main tab strip and canvas.\r\n- Mark Float All windows as floating and avoid reactivating a detached document on every focused frame.\r\n- Route canvas input through the focused viewport.\r\n\r\n## Reproduction\r\n1. Open at least two documents.\r\n2. Drag one document tab outside the tab strip to detach it into a floating OS window.\r\n3. Check whether the document still appears in the main tab strip or canvas.\r\n4. Focus the floating window and use a canvas tool.\r\n5. Try Window > Arrange > Float All in Windows.\r\n\r\nExpected: a torn-off document stays active in its floating window, is absent individually from the main tab strip and canvas, and canvas input is handled by the focused viewport. Float All creates floating document windows as well.\r\n\r\n## Checks\r\n- UI integration tests: passed.\r\n- `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`: passed.\r\n- `cargo xtask layers`, `cargo xtask wasm`, and `cargo build -p photocraft`: passed.\r\n- The full UI unit suite has one known GPU precision failure on the AMD RX 5700 at F32 mip 5 (delta 5.96e-8); the floating-window regression tests pass.\r\n\r\nFixes #6277; related original request: #3733",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "50b1f6734799cf7b5d45dd84868976afd31aa711",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/file_open/tests.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/menus.rs"
},
{
"path": "crates/ui-egui/src/state.rs"
},
{
"path": "crates/ui-egui/src/view_cmds.rs"
},
{
"path": "crates/ui-egui/src/view_cmds/tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38792528380/job/117158667211"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38792528380/job/117158667837"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38792528380/job/117158667829"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38792528380/job/117158668761"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38792528380/job/117158732209"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38792528380/job/117158732230"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38792528380/job/117158732144"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T00:22:49Z",
"url": "https://github.com/storytold/photocraft/pull/5744#pullrequestreview-5593443602",
"body": "Read the whole diff. A document torn off into its own window is now marked `floating` and the main canvas, tab strips and Arrange tiles skip it, so the same page is no longer drawn in both viewports. The main canvas falls back to the first docked document when the active one is floating, and stays blank (dots only) when every document is floating. Activation follows focus: a click in the main window only activates a tile or the fallback document while that viewport is focused, and a floating window activates its document once when it gains focus.\n\nTab handling stays consistent with the document list. Tab ids, close, context-menu and overflow actions use document indices, and `move_target_after_filter` maps a drop position in the docked-only strip back to the full list (checked against its two test cases). Floating the same document again reuses its open window rather than opening a second one, and a closed window can be floated again.\n\nThe tests cover the torn-off document in both themes, the Arrange layout with a floating document, tab reorder with a floating document in between, and the float, float-all and reuse behaviour. CI is green at 50b1f67. The branch has merge conflicts, so it needs a rebase before merging.\n",
"commit": {
"oid": "50b1f6734799cf7b5d45dd84868976afd31aa711"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"docked_document_indices",
"main_canvas_document",
"move_target_after_filter",
"torn_off_document_is_not_drawn_again_in_the_main_window",
"tab_reorder_keeps_floating_documents_in_place",
"arrange_layout_only_includes_docked_documents",
"float_in_window_defaults_to_the_active_document",
"float_all_in_windows_gives_each_document_its_own_view"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5744"
},
{
"number": 5650,
"title": "Guides: ⌥ turns a dragged guide; dropping one on either ruler deletes it (#5034)",
"url": "https://github.com/storytold/photocraft/pull/5650",
"body": "Fixes #5034.\n\n## What changed\n\nTwo Photoshop guide behaviours:\n\n- **⌥ (Alt) turns a guide while it's dragged.** This works whether the guide comes out of a ruler or an existing guide is dragged with the Move tool (or with ⌘ and another tool).\n - The guide turns between horizontal and vertical about the pointer, so out of the left ruler it comes out horizontal.\n - Letting go of ⌥ before the release turns it back.\n - An ⌥-click on a guide turns it in place.\n- **Dropping a guide on either ruler deletes it, whichever way it runs.** Before, a horizontal guide dropped on the left ruler was still level with the image, so it only moved.\n\n### Implementation\n\n- **engine:** `view.moveGuide` takes an optional `toOrientation`. A turned guide is one undo step and keeps its colour, and a `%` position is of the new axis. A bad value, or `toOrientation` on another guide command, is refused without touching the guides.\n- **ui (`rulers.rs`):**\n - `GuideDrag` keeps the guide's own orientation (so `index` still points into the right list), plus two new fields: `flipped` (⌥ held) and `over_ruler`.\n - `GuideDrag::track` follows the pointer for both drag paths, and `shows_vertical` is what's drawn and committed.\n - The rulers set `over_ruler` from the pointer each frame.\n - `finish_drag` creates, moves or turns the guide, or deletes it when it's dropped outside the canvas or on a ruler.\n- **canvas.rs:** the Move-tool and ⌘ guide drags use `track` with ⌥.\n- **docs/ui-parity.md:** new row and revision entry.\n\n## How it was checked\n\n- `cargo xtask ci` on macOS passes all 5 steps (fmt, clippy, test, layers, wasm).\n- New tests:\n - `edit_cmds::tests::move_guide_turns_a_guide`: turn with `%` of the new axis, colour carried, one undo step each, bad params refused.\n - `rulers::tests::alt_turns_a_guide_dragged_out_of_a_ruler`: a real egui drag out of the left ruler with ⌥ held gives a horizontal guide at the pointer's y.\n - `rulers::tests::alt_turns_a_guide_moved_with_the_move_tool`: ⌥-drag turns the guide in one undo step, releasing ⌥ first keeps it vertical, and an ⌥-click turns it in place.\n - `rulers::tests::a_guide_dropped_on_either_ruler_is_deleted`: the pointer over the left or top ruler marks the drop and deletes a horizontal guide; over the canvas it moves.\n- In the running app over `--control`:\n - Set up a 400×300 document with a vertical guide at x=200, then used `ui.pointer` with the Move tool: down on the guide, then move and up with `alt: true` at (120, 223).\n - History gained one \"Move Guide\" step.\n - The document saved through `app.save` has a horizontal guide at y=223 and no vertical one (read from the PSD's resource 1032).\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "4b1bd6fb6d264f885cb9caa3b8a6578ee527bd7b",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/edit_cmds.rs"
},
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/move_hover.rs"
},
{
"path": "crates/ui-egui/src/rulers.rs"
},
{
"path": "docs/ui-parity.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38606468604/job/115998533446"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38606468529/job/115998532467"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38606468604/job/115998533921"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38606468604/job/115998533868"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38606468604/job/115998536109"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38606468604/job/116007786595"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38606468604/job/116007786797"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38606468604/job/116007786539"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T00:12:41Z",
"url": "https://github.com/storytold/photocraft/pull/5650#pullrequestreview-5593380251",
"body": "Read the whole diff. ⌥ while dragging a guide now turns it, as in Photoshop, and a guide dropped on either ruler is deleted.\n\n- **Engine:** `view.moveGuide` takes `toOrientation`. A same-orientation value is a plain move, anything but \"horizontal\"/\"vertical\" is rejected, `view.newGuide` rejects the parameter, and a % position is resolved against the axis the guide ends up on. A turned guide is removed from one list and pushed to the other, and its colour is moved to the new axis. It is one history step.\n- **UI:** `GuideDrag::track` recomputes `flipped` and `pos` from the pointer and the current ⌥ state on every press, move and release, so letting go of ⌥ before release turns it back. `finish_drag` checks the drop against the extent of the axis the guide is shown on, which avoids the cross-axis bounds problem. `over_ruler` comes from the previous frame's pointer position, and the comment says so.\n- **Callers:** all `GuideDrag` literals now use `..Default::default()`.\n\nThe tests cover the engine command (colour, % axis, undo, bad params), a ruler drag with ⌥ held, the Move tool with ⌥ held, ⌥ released and ⌥-click, and drops on the left ruler, top ruler and canvas. CI is green at 4b1bd6f. The branch conflicts with main, so it needs a rebase.\n",
"commit": {
"oid": "4b1bd6fb6d264f885cb9caa3b8a6578ee527bd7b"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"move_guide_turns_a_guide",
"alt_turns_a_guide_dragged_out_of_a_ruler",
"alt_turns_a_guide_moved_with_the_move_tool",
"a_guide_dropped_on_either_ruler_is_deleted"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5650"
},
{
"number": 5515,
"title": "Selecting subject now automatically applies to selected layer, and us…",
"url": "https://github.com/storytold/photocraft/pull/5515",
"body": "…es sampleAllLayers only when no layer is selected",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "375d4894a119b6cd5933d53e8fe7f506d2783392",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/smartselect_cmds.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559940478/job/122385938685"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38553141352/job/115815682071"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559940478/job/122385986778"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38553141352/job/115815681693"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559940478/job/122386008947"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38553141352/job/115815682094"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559940478/job/122385939423"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38553141352/job/115815683350"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559940478/job/122385986100"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38553141352/job/115815789407"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559940478/job/122386014947"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38553141352/job/115815789669"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559940478/job/122385940141"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T00:12:20Z",
"url": "https://github.com/storytold/photocraft/pull/5515#pullrequestreview-5593377843",
"body": "Read the whole diff. Select Subject now defaults `sampleAllLayers` to false, so it analyses the selected layer's pixels and falls back to the composite only when no pixel layer is selected. That matches how Photoshop behaves with the Sample All Layers option off, and the existing target logic (`filter(|id| !all_layers && layer has pixels)`) already handled both cases, so nothing else changes. Passing `sampleAllLayers: true` still samples the composite, and the command's parameter description now documents the new default.\n\nThe new test builds a layer holding only a small stroke above a disc, shows that the stroke layer is analysed (the disc isn't picked), then deselects layers and shows the whole image finds the disc. The subject-cache test is flipped to match, and its cache keys still distinguish the two sample targets. CI is green at 375d489.\n",
"commit": {
"oid": "375d4894a119b6cd5933d53e8fe7f506d2783392"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"select_subject_samples_the_selected_layer_else_the_composite"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5515"
},
{
"number": 5493,
"title": "Import Photoshop custom shape sets into the Shapes panel",
"url": "https://github.com/storytold/photocraft/pull/5493",
"body": "Closes #5359.\n\nAdd `.csh` version 2 import through **Shapes → Import Shapes…**, File → Open and `shape.presets.importCsh`. Imported shapes become persistent groups and can be placed with the existing Shapes tools. Repeated names are disambiguated, and an invalid set leaves the existing library unchanged.\n\nAutomation imports accept inline base64 `data`; ambient filesystem `path` imports are denied by the existing preset-import policy. Desktop file selection continues to use the normal file import route.\n\nThe reader uses public format documentation and existing PSD path conversion, with limits on file size, names, shape counts and aggregate path records. Test fixtures are original generated geometry, independently read with ag-psd; no third-party shape assets are included. This adds import only. A Photoshop-written CSH fixture has not yet been verified.\n\nVerification:\n- [Baseline regression](https://github.com/jonathanwxh-cell/photocraft/actions/runs/38520368267) compiled and failed with UnknownCommand for the new import command.\n- [Core implementation checks](https://github.com/jonathanwxh-cell/photocraft/actions/runs/38527261396) passed: 6 engine, 4 parser, 6 shared-reader and 3 UI-routing tests; explicit GPU visual test; ignored panic_hunt; strict affected-crate Clippy; layering and parity generation. Coverage includes geometry, holes, persistence and atomic import. The submission subsequently adds only the separately verified automation policy and localization repairs below.\n- [69 automation library tests, formatting and strict automation Clippy](https://github.com/jonathanwxh-cell/photocraft/actions/runs/38530699766) passed, including path rejection and inline-data authorization coverage.\n- [66 localization tests, formatting and strict UI Clippy](https://github.com/jonathanwxh-cell/photocraft/actions/runs/38535456759) passed, covering the new labels and import status messages in all bundled catalogs.\n- [Full CI on submission commit 77d8898](https://github.com/jonathanwxh-cell/photocraft/actions/runs/38536395258): passed on Linux and Windows, including workspace tests, strict Clippy, dependency layering and scorecard checks; corpus, WASM and web build/size checks passed. Linux also passed without HEIF and with craft-fonts. Optional fuzz was skipped.\n\nVisual evidence was inspected: the Shapes panel gains the imported group and thumbnail, and the placed triangle keeps its 2:1 aspect ratio. The test clicks the real import button with a mocked OS file chooser and places the shape through the engine command. Before/after images show the same candidate before and after importing, not two source revisions.\n\nImplemented and verified with Codex assistance for @jonathanwxh-cell (jonathnwxh).\r\n\n\n| Before import | After import and placement |\n|---|---|\n| ![Empty Shapes panel](https://raw.githubusercontent.com/jonathanwxh-cell/photocraft/1f089577e37bca4d5c73a5e339a3ca64514ec05d/before-import.png) | ![Imported group and placed triangle](https://raw.githubusercontent.com/jonathanwxh-cell/photocraft/1f089577e37bca4d5c73a5e339a3ca64514ec05d/imported-triangle.png) |",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "f97eb2849a68e1ed1c08c5b378770cf35cbcb72d",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/automation/src/workspace.rs"
},
{
"path": "crates/automation/src/workspace/tests.rs"
},
{
"path": "crates/engine/src/cmd/preset_import_cmds.rs"
},
{
"path": "crates/engine/src/presets/shapes.rs"
},
{
"path": "crates/engine/src/presets/shapes/csh.rs"
},
{
"path": "crates/engine/tests/csh_import.rs"
},
{
"path": "crates/psd/src/csh.rs"
},
{
"path": "crates/psd/src/lib.rs"
},
{
"path": "crates/psd/tests/common/csh_fixture.rs"
},
{
"path": "crates/psd/tests/csh.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/jobs_ui.rs"
},
{
"path": "crates/ui-egui/src/panels/preset_panels/panels.rs"
},
{
"path": "crates/ui-egui/src/preset_files_ui.rs"
},
{
"path": "docs/file-format-parity.md"
},
{
"path": "docs/gaps.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122385982608"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171174833"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865312/job/122171172502"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865797/job/122171174131"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122385926167"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865797/job/122171238770"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171175032"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122386006625"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865797/job/122171238159"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171174849"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122385927479"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171174468"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865797/job/122171236861"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171176106"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122385972854"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171177045"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122385995531"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171176059"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122385926878"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122385972693"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171424487"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171424481"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559936811/job/122385927955"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40498865821/job/122171424538"
},
{
"name": "CodeRabbit",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://coderabbit.ai?utm_source=cr_app&utm_medium=github"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-27T12:23:56Z",
"url": "https://github.com/storytold/photocraft/pull/5493#pullrequestreview-5666969724",
"body": "Re-review at SHA f97eb28 (my last review was at 8797856). Gate is green.\n\nWhat changed: one fix commit (9e81ec3, \"store CSH display names and report the group used\") plus merges of main.\n\nChecked: I read the fix commit's Rust patch in full and the residual of the PR-only diff against 8797856.\n1. `engine/src/presets/shapes/csh.rs`: the imported shape name now goes through `photocraft_doc::pattern::display_name`, so `$$$/Presets/Shapes/Tiger=Tiger` is stored as `Tiger`, and a name that is blank after that falls back to \"Shape\". This answers the localization-key issue raised at 8797856.\n2. `ui-egui/src/preset_files_ui.rs`: the synchronous File > Open path now reports the group the importer returned (falling back to the file stem only when none is returned), using a `{group}` template for shapes and the unchanged `{file}` templates for the other kinds. This answers the wrong-group-on-collision issue.\n3. New tests cover both: `csh_import_stores_the_display_form_of_a_localization_key` (display form stored, repeat import gives \"Tiger 2\", blank key gives \"Shape\"), and the status text \"Imported 1 shape from Original\" then \"... from Original 1\" after a collision. The fixture gained `named_polygon_csh`.\n4. The remaining residual is the merge of main: `PRESET_EXTS` now lists asl and act as well as csh, docs \"Last updated\" dates, and a reformatted match arm in the command dispatch.\n\nNot verified: the reformatted dispatch arm and the translation catalogs I read only as far as the residual shows; I did not run the tests locally, and the new `{group}` template string's translations were not checked. I relied on the green gate for the compile and the catalog checks.\n",
"commit": {
"oid": "f97eb2849a68e1ed1c08c5b378770cf35cbcb72d"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"csh_import_requires_inline_data_in_automation",
"decoded_preset_data_cannot_exceed_its_format_budget",
"import",
"csh_import_stores_the_display_form_of_a_localization_key",
"csh_import_makes_an_original_shape_available_without_a_document",
"csh_failed_import_preserves_existing_presets_and_revision",
"csh_invalid_second_shape_does_not_partially_import_a_set",
"csh_multiple_shapes_with_matching_names_remain_individually_available",
"csh_repeat_import_names_are_unique_and_survive_preferences_reload",
"csh_same_winding_inner_subpath_stays_a_hole_after_place_and_reload",
"zero_padding",
"validate_path",
"fixture",
"reads_multiple_names_padding_signed_bounds_and_cubic_controls",
"rejects_every_truncated_prefix_without_panicking",
"rejects_unsupported_headers_excessive_counts_and_invalid_unicode",
"rejects_missing_knots_and_mismatched_open_closed_records",
"csh_open_imports_presets_and_shows_shapes_without_opening_a_document",
"malformed_csh_open_preserves_the_library",
"csh_panel_import_visual_evidence"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5493"
},
{
"number": 5453,
"title": "Shapes: independent fill and stroke paints (solid, gradient, pattern), part 1",
"url": "https://github.com/storytold/photocraft/pull/5453",
"body": "Pattern fills on shape layers rendered as transparent, and the shape commands only kept part of a paint. Gradient opacity stops, midpoints, offset, dither and align, solid alpha or non-RGB colours, and pattern angle, phase and link were dropped or reset on the way through `shape.create`, `shape.edit` and `shape.info`.\r\n\r\nThis is the first part of splitting #3446. It changes only the rendering and engine side, with no UI changes:\r\n\r\n- Pattern tiles and placement move from `photocraft-compose` into `photocraft-vector` (`photocraft_vector::pattern`, re-exported by compose). Sampling now handles non-finite and huge coordinates. A tile over 64 MiB samples its copy-on-write surface instead of allocating a second full buffer.\r\n- The vector renderer resolves pattern paints from the document's pattern table and samples them in the document's colour mode, for the fill and the stroke independently (`Paint::Pattern`, `CompiledShape::with_patterns`, `render_shape_with_patterns`). The CPU and GPU fill/stroke split for clipped layers and interior effects uses the same patterns.\r\n- `shape.create` and `shape.edit` accept and report the complete paint model. Malformed values return errors before the document changes. A pattern paint resolves a document or library pattern and embeds its tile in the same undo step.\r\n- Copy/Paste Fill and Copy/Paste Stroke keep a snapshot of the referenced pattern. Pasting still works after the source document is closed, and pasting refuses to overwrite a different pattern that has the same id.\r\n\r\n## Review fixes (2026-10-15)\r\n\r\n- Pattern sampling accepts every finite positive `f32` scale, including `0.0001`, using a finite `f64` inverse.\r\n- The shape split cache includes retained pattern tile memory in its conservative byte accounting. An entry larger than the budget stays uncached without evicting existing entries.\r\n- Regression tests cover the small-scale sampler and cache accounting/oversize behavior.\r\n\r\n## Validation of the updated code\r\n\r\nValidated at `e22a9baedceefd97daffe84403b23c99c56d6919`:\r\n\r\n- Windows: format, bundled translation catalogs, full workspace tests with HEIF, Clippy with warnings denied, dependency layering and scorecard checks passed.\r\n- Ubuntu: the same checks, the no-HEIF tests, full workspace tests with the CI-pinned Craft fonts, all 24 WASM checks, real-file corpus tests and mdBook passed.\r\n- The Web build passed in a targeted retry after restoring three local checkout files to their exact committed LF bytes. The initial packaging invocation had stopped on CRLF before building; no Git content change was needed. Source snapshots stayed unchanged during all validation runs.\r\n- The production Web WASM is 25,041,756 bytes against this branch's 25,165,824-byte limit, leaving 124,068 bytes. The limit and features were not changed.\r\n- Focused regressions reproduced the pattern-scale and cache-budget failures before the fix and passed afterward. The corrected code also received an independent local review.\r\n\r\nA paired release comparison against the previous PR head used a 6000×4000 rectangle with pattern fill and a 12 px stroke. Across three paired processes per revision, cold median render times changed from 295.5 to 307.9 ms for a 128×128 tile and from 343.8 to 353.8 ms for a 2048×2048 tile; the measured ranges overlapped. Warm cache-hit medians were 35–43 µs, and output pixel hashes matched. The cache byte count is conservative retained-memory accounting, not a measured RAM total. These two CPU cases do not establish a general performance guarantee.\r\n\r\nRefs #2496, #3446",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "e22a9baedceefd97daffe84403b23c99c56d6919",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/compose/src/pattern.rs"
},
{
"path": "crates/compose/src/shape_split.rs"
},
{
"path": "crates/engine/src/lib.rs"
},
{
"path": "crates/engine/src/vector_cmds.rs"
},
{
"path": "crates/engine/tests/shape_paint_fidelity.rs"
},
{
"path": "crates/engine/tests/shape_pattern_clipboard.rs"
},
{
"path": "crates/engine/tests/shape_patterns.rs"
},
{
"path": "crates/gpu/src/plan.rs"
},
{
"path": "crates/gpu/tests/parity.rs"
},
{
"path": "crates/vector/src/lib.rs"
},
{
"path": "crates/vector/src/pattern.rs"
},
{
"path": "crates/vector/tests/shape_patterns.rs"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38785445186/job/116596129155"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38785445036/job/116596127959"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38785445186/job/116596129032"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38785445186/job/116596128560"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38785445186/job/116596131391"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38785445186/job/116610882305"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38785445186/job/116610881955"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38785445186/job/116610881869"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-17T20:51:16Z",
"url": "https://github.com/storytold/photocraft/pull/5453#pullrequestreview-5560030568",
"body": "Reviewed head e22a9baedceefd97daffe84403b23c99c56d6919. No blocking finding in this rendering/engine part of the shape-paint work.\n\nI read all 14 changed-file patches and checked the transaction path. Paint JSON preserves the full gradient/pattern metadata and model colours; invalid input is rejected before committing the cloned document. Pattern resolution and embedding stay in the same edit/undo transaction. Clipboard copies retain COW pattern snapshots after the source document closes and reject a conflicting destination tile with the same ID. CPU/GPU shape splitting receives the document's pattern table and colour mode, and its cache key pins the referenced tile data. The revised cache accounting includes retained pattern tiles; an oversized entry is returned without evicting existing cached entries.\n\nThe small-scale correction is present: every finite positive f32 scale uses a finite f64 inverse, rather than silently reverting below the old threshold. Oversized pattern preparation retains its source surface instead of allocating another complete RGBA buffer. This is not a claim of constant memory use or universally improved performance.\n\nExact-head CI https://github.com/storytold/photocraft/actions/runs/38785445186 and documentation run 38785445036 succeeded. Ubuntu and Windows logs identify merge eaf62ca of this head into 5f9190f8cc1a6b6123125ac7ded9ad744c2c1b3f. I checked passing entries for paint round-trips/undo, clipboard retention and conflict atomicity, small-scale rendering, and the split-cache budget regression. The GPU parity test is listed as passing, but its helper can return early without an adapter; I am not treating that line alone as verified hardware-GPU execution.\n\nI did not run fresh local tests or independently reproduce the release benchmark/working-set measurements. The cache count is conservative retained-byte accounting, not a measured process-RAM total. The Ref relationships to #2496/#3446 are appropriate: this is the engine/rendering part, not completion of the separate UI work.",
"commit": {
"oid": "e22a9baedceefd97daffe84403b23c99c56d6919"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"entry_bytes",
"split_cache_does_not_pin_pattern_tiles_over_its_budget",
"set_clipboard_pattern_id",
"clipboard_pattern",
"check_clipboard_pattern_collision",
"paint_color_json",
"fill_number",
"fill_bool",
"fill_pair",
"fill_scale",
"paint_color",
"pattern_for_fill",
"session",
"active_shape",
"exact_paints_roundtrip_through_create_edit_and_undo",
"solid_alpha_and_library_patterns_render_and_embed_in_the_same_history_step",
"imported_document_pattern_resolves_without_a_global_library_entry",
"legacy_defaults_and_malformed_paint_inputs_are_explicit_and_atomic",
"imported_pattern",
"new_doc",
"shape",
"patterned_fill_and_stroke_clipboards_keep_cow_tiles_after_source_closes",
"replacing_public_clipboards_with_plain_paints_ignores_old_pattern_snapshots",
"legacy_pattern_clipboard_without_a_snapshot_resolves_the_library",
"copied_name_only_and_obsolete_ids_bind_to_the_snapshot_despite_destination_conflicts",
"unresolved_and_conflicting_pattern_clipboards_fail_atomically",
"pattern_parser_falls_back_to_imported_display_names_and_rejects_conflicting_ids",
"library_pattern",
"patterned_shapes_embed_render_and_undo_in_one_command_at_every_depth",
"editing_a_solid_shape_to_pattern_embeds_and_undoes_the_tile",
"patterned_shape_fill_and_stroke_clip_and_effects_match_cpu",
"paint_in_mode",
"new_in_with_budget",
"at",
"pattern",
"fill",
"close",
"pattern_shape_fill_and_stroke_render_at_every_depth_and_model",
"pattern_lookup_placement_and_premultiplied_sampling",
"tiny_positive_pattern_scale_is_preserved_in_sampling_and_shape_rendering",
"oversized_sparse_pattern_samples_without_a_second_full_surface"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5453"
},
{
"number": 5270,
"title": "Layer Style: Gradient Overlay takes any number of colour stops (#5177)",
"url": "https://github.com/storytold/photocraft/pull/5270",
"body": "Closes #5177.\n\nThe Gradient Overlay page modelled its gradient as a From and To colour, so a rainbow gradient on text, as in Photoshop, was impossible. Richer gradients (from PSDs or styles) only survived the dialog through its snapshot.\n\n## Engine\n\n- `layer.layerStyle.gradientOverlay` (and a gradient `stroke`) take `\"stops\": [[position, \"#rrggbb\" | [r, g, b]], …]` (2 to 256, sorted) and `\"opacityStops\": [[position, opacity %], …]` (`[]` is opaque), replacing From/To. The parameter docs say so.\n- Malformed lists are refused at the command entry (`set` and `replace`) as bad params, so a bad edit changes nothing. Given lists replace the gradient's; without them, intermediate stops still survive an edit as before.\n\n## Dialog\n\n- The page shows a ramp with colour-stop markers, edited like the Gradient Editor: click under the ramp to add a stop with the gradient's colour there, drag a marker to move it, drag it away to delete it (two always stay), double-click it or the Color swatch to open the shared Color Picker on that stop. Location and Delete act on the selected stop, and a dropdown offers the Gradients panel's presets to start from.\n- The page always writes `stops` and `opacityStops`, in the command's own grammar; the factory From/To defaults become two stops. Transparency stops are carried through, not edited on the page.\n- The Color Picker request grows an optional `stop` index, also on the control channel (`ui.dialog.open {kind: \"colorPicker\", parent, effect, field: \"stops\", stop: i}`), documented in docs/control-protocol.md.\n\n## Tests\n\n- Engine: explicit stops and transparency stops, sorting and clamping, the replace path keeping stops through a geometry edit, every malformed shape refused with the effect unchanged, a gradient stroke with stops.\n- Dialog: the field grammar round trip, the factory conversion, the page's fill never failing on bad fields; kittest for add, move and delete on the ramp with the two-stop floor, presets and the Delete button; the picker previewing and confirming one stop and refusing a missing one; the control channel's stop target.\n- Rendered offscreen with a four-stop rainbow.\n\n## Gate\n\n`cargo test -p photocraft-engine -p photocraft-ui-egui`, clippy `-D warnings`, `cargo fmt --check`, `cargo xtask wasm`, `panic_hunt --ignored`.\n\n## Screenshot\n\nThe Gradient Overlay page with a four-stop rainbow: the preset dropdown, the ramp with its stop markers, and the selected stop's Color, Location and Delete.\n\n![Gradient Overlay page](https://raw.githubusercontent.com/FabioLissi/photocraft/585aee74162a6b4fb5cd31da99b247e2dd48f9e3/gradient-overlay-stops.png)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "9db1c7ae1d48ce4f31622b5f9029c70c590842f8",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/layer_style.rs"
},
{
"path": "crates/ui-egui/src/control.rs"
},
{
"path": "crates/ui-egui/src/gradient_ui.rs"
},
{
"path": "crates/ui-egui/src/layer_style.rs"
},
{
"path": "crates/ui-egui/src/layer_style/color_picker.rs"
},
{
"path": "crates/ui-egui/src/layer_style/color_picker/tests.rs"
},
{
"path": "crates/ui-egui/src/layer_style/gradient_field.rs"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38465464403/job/115530979094"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38465463133/job/115530976733"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38465464403/job/115530979419"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38465464403/job/115530979261"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38465464403/job/115531299557"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38465464403/job/115584920128"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38465464403/job/115584920057"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38465464403/job/115584919996"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T13:29:14Z",
"url": "https://github.com/storytold/photocraft/pull/5270#pullrequestreview-5585703891",
"body": "I read the engine change, the new gradient field and the colour-picker changes, and CI is green on this head.\n\n- `stops` and `opacityStops` are validated at the command entry for `set` and `replace`: 2 to 256 colour stops, finite numbers, and a single opacity stop refused. A refused edit leaves the gradient as it was, and the test checks that.\n- Without the new keys, existing gradients keep their intermediate stops, opacity stops, alignment and offset on a dialog round trip. The dialog used to drop the whole panel for gradients with more than two stops; it now models every stop.\n- The ramp editor stays in range: delete is disabled at two stops, the selected index is clamped, and drag-away only deletes when more than two stops remain. The picker re-checks the stop index and the target's snapshot before writing a colour back.\n\nThe branch has merge conflicts with `main`, so it needs a rebase before it can merge.\n",
"commit": {
"oid": "9db1c7ae1d48ce4f31622b5f9029c70c590842f8"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"stops_param",
"opacity_stops_param",
"gradient_overlay_takes_explicit_stops",
"color_value",
"picked_value",
"gradient_stop_colors_go_through_the_picker",
"commit",
"edit",
"dialog_app",
"harness",
"stops_in",
"press",
"release",
"ramp_adds_moves_and_deletes_stops",
"presets_delete_and_location",
"stops_round_trip_through_the_field_grammar",
"factory_from_to_become_two_stops",
"the_fill_of_a_page_never_fails"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5270"
},
{
"number": 5176,
"title": "Play CopyCat, AJ and Line and Color action sets; keep patterns across restarts; Shift Edge scale",
"url": "https://github.com/storytold/photocraft/pull/5176",
"body": "## Summary\n\nThis PR makes three more real Photoshop action sets work in PhotoCraft, keeps imported patterns across restarts, and fixes a Select and Mask scale that drew a thick black band around Debaser's distressed ink. It follows #4698, which made the TrueGrit Texture Supply sets play. Every change comes from playing a real, published action set and fixing what stopped it, not from guessing at what Photoshop users might need.\n\n## Why it started\n\nThis comes from testing PhotoCraft against the third-party and custom actions I use in my own workflow. The testing is ongoing: each set that doesn't play yet becomes the next fix. Running that collection in a build of #4698 showed three problems:\n\n- Debaser's Ink Distressing actions drew a heavy black outline around all the line art, and small details merged into solid black.\n- The tutorial folder held three action sets the importer had never seen: COPYCAT, AJ\\_Actions2023 and LineAndColorTutorial.\n- Every action that uses a pattern needed the `.pat` file reloaded in each new session.\n\n| Set | Steps | Unmapped before | Unmapped after |\n| --- | --- | --- | --- |\n| COPYCAT | 244 | 44 | 0 |\n| AJ\\_Actions2023 | 35 | 8 | 4 |\n| LineAndColorTutorial | 34 | 3 | 0 |\n\n## Shift Edge: the black outline\n\nShift Edge at ±100% now moves an edge by its own softness, with a 1 px minimum, instead of a fixed 4 px. That turns Debaser's 8 px black band into a thin rim.\n\n**Cause.** Debaser copies the ink shape onto a distress-texture layer as its mask, then shrinks that mask twice with Select and Mask Shift Edge −100%. Wherever the texture's mask doesn't reach, the ink stays solid black. PhotoCraft treated −100% as \"move the edge `max(radius, 4)` px\", so with Radius 0 the two passes left 8 px untextured around every edge. The 4 px figure came in with a work-in-progress commit and was never measured against Photoshop.\n\n**Options considered.**\n\n| Option | Why not, or why |\n| --- | --- |\n| Scale to edge softness (chosen) | Adobe describes Shift Edge as moving soft-edged borders. A hard mask has little border to move, so about 1 px per pass matches the intent of an action that runs it twice |\n| Fix only `.atn` playback | Leaves the guessed 4 px in the Select and Mask dialog, so the same action recorded in PhotoCraft would still be wrong |\n| Wait for a Photoshop reference | No Photoshop on the test machine. The fix can be refined to an exact scale once a reference render exists |\n\n**The rule.** 100% equals `max(radius, feather, 2 × smooth σ)`, at least 1 px. A side-by-side render of line art with gaps from 2 px to 17 px confirmed it: the old build closed every gap in black, and the new one keeps even the 2 px gap open.\n\n**Caveat.** This affects Select and Mask everywhere, not just actions, and its exact scale is still unverified against Photoshop.\n\n## Action mappings\n\nAlmost every unmapped step already had a PhotoCraft command, so the importer just learned to recognise how Photoshop records it. New engine behaviour was added only where playing a real set proved it was needed.\n\n**Mapped onto existing commands:** Create Clipping Mask, Apply Locking and lock settings, Blending Options' Advanced checkboxes, Diffuse, Delete Layer Mask, smart filter Blending Options, Shift-click layer ranges, and Select and Mask's newer \"output to selection\". Each one exports back to `.atn` too, and a round-trip test covers it.\n\n**Engine changes, each forced by a step that failed:**\n\n| Change | Why |\n| --- | --- |\n| New `history.clear` | Photoshop's Clear History clears only the current document. The nearest existing command, Purge › Histories, clears every open document, which is wrong for an action |\n| `history.newSnapshot` takes `replaceExisting` | CopyCat retakes its \"before/after sharpness\" snapshots each run |\n| `deleteFilterMask` takes `ifPresent` | Photoshop adds an empty filter mask with the first smart filter, and CopyCat deletes it. PhotoCraft never adds one, so the delete must quietly do nothing. This copies the `ifPresent` approach #4698 used for Layer From Background |\n| Bring to Front / Send to Back on a layer already there returns `{\"moved\": false}` | CopyCat brings a new layer to the front of its group when it's already there. Photoshop doesn't stop, so PhotoCraft shouldn't. No undo step is added |\n| Save in an Edit Contents document saves into the smart object | CopyCat edits a smart object's contents and then saves. The desktop app already did this from the menu, but playback looked for a file path and failed |\n\n**Fixed while testing:** a Shift-click range of layers was replayed as a plain click, so CopyCat grouped one layer instead of two. It now replays as a range.\n\n**Left unmapped on purpose:**\n\n- AJ's Lens Correction carries a recorded camera lens profile. Mapping it to something that doesn't correct the same way would be a silent wrong result, so playback stops there with a clear message.\n- Timeline frame steps and an empty Set have no effect on the document in PhotoCraft. They import switched off rather than counting as failures.\n\n## Persistent patterns\n\nImported and defined patterns are now saved in the app's preset store and come back on the next launch, as Photoshop keeps its Patterns panel. Brushes, gradients and actions already persisted; patterns were the gap.\n\n**Storage.** The store gets a small `patterns.json` index (order, names, full sizes) plus one `.pat` file per pattern, saved next to `actions.json`.\n\n- One file per pattern, rather than one big `.pat`, means a pattern is written once. A rename rewrites only the index, and a delete removes only that pattern's files.\n- The `.pat` format already had a tested reader and writer, and the files open in Photoshop too.\n- Built-in patterns are never written; they're regenerated at startup.\n\n**Why large patterns load on use.** Debaser's colour libraries are six `.pat` files of about 1.2 GB each, every pattern a 4500 × 4500 px texture. Loading saved patterns whole at startup would take about 7 GB of memory on every launch. Instead:\n\n- Patterns over 128 px are saved with a small preview file, and only the preview loads at startup.\n- The full pixels are read from disk the first time something paints with the pattern: a fill, a Pattern Fill layer, Pattern Stamp, a layer style, a brush texture or an export.\n- All of those already go through one lookup function, so loading on demand needed changes in one place, not in each feature. This follows how saved brush tips already load.\n\n**Safety rules.** A corrupt pattern file or index is skipped with a warning and never overwritten or deleted, so a later fix can still recover it. If a pattern can't be saved, you get one warning, not one after every action.\n\n**Panel folders.** The Patterns panel removes folder entries whose pattern is missing. Because the store loads in the background, that would have emptied saved folders at every launch. The panel now waits until the store has loaded.\n\n## Verification and open questions\n\nAll checks pass on top of current `main`. The real action files were played end to end as well.\n\n- **Tests:** engine 1,594, UI 2,667, the Shift Edge tests, clippy with warnings as errors, the `panic_hunt` fuzz test, layering and the engine wasm check. New tests cover every new mapping (round trip), Save in Edit Contents, `ifPresent`, Bring to Front at the front, Clear History, replace-existing snapshots, and patterns surviving a restart: rename, delete, corrupt files and large patterns loading on use.\n- **Real files:** all three CopyCat actions ran end to end with `COPYCAT PATTERNS.pat`, producing the expected photocopy look. AJ and Line and Color played up to the stops described above.\n- **Problems already on `main`, not from this PR:** `cargo fmt --check` flags two UI files, and the UI crate's wasm check fails on `tile_overlay.rs`.\n\n**Still open:**\n\n- [x] Measure Shift Edge −100% against a Photoshop render, to replace \"about 1 px\" with Photoshop's real scale\n- [x] Re-run Debaser Ink Distressing with its real texture `.pat` (so far tested with a stand-in noise pattern)\n- [x] Decide whether to map Lens Correction once PhotoCraft can apply a recorded lens profile\n- [x] Keep running the rest of the collection and send what still fails as the next PR\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "c96891c298151c5cb11a55928b9e1c27911f38bd",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/algo/src/matting.rs"
},
{
"path": "crates/engine/src/actions_cmds.rs"
},
{
"path": "crates/engine/src/atn_cmds.rs"
},
{
"path": "crates/engine/src/atn_cmds/tests.rs"
},
{
"path": "crates/engine/src/commands.rs"
},
{
"path": "crates/engine/src/history_cmds.rs"
},
{
"path": "crates/engine/src/lib.rs"
},
{
"path": "crates/engine/src/pattern_cmds.rs"
},
{
"path": "crates/engine/src/preset_store.rs"
},
{
"path": "crates/engine/src/preset_store/patterns.rs"
},
{
"path": "crates/engine/src/preset_store/tests.rs"
},
{
"path": "crates/engine/src/presets/patterns.rs"
},
{
"path": "crates/engine/src/retouch_cmds/pattern_stamp.rs"
},
{
"path": "crates/engine/src/smart_cmds.rs"
},
{
"path": "crates/engine/src/smart_cmds/tests.rs"
},
{
"path": "crates/engine/src/smartselect_cmds.rs"
},
{
"path": "crates/engine/src/tests.rs"
},
{
"path": "crates/ui-egui/src/actions.rs"
},
{
"path": "crates/ui-egui/src/pattern_fill_ui.rs"
},
{
"path": "crates/ui-egui/src/preset_panels.rs"
},
{
"path": "docs/file-format-parity.md"
},
{
"path": "docs/gaps.md"
},
{
"path": "docs/target-app-parity.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38770892819/job/116546101107"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38770892846/job/116546100211"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38770892819/job/116546100732"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38770892819/job/116546100959"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38770892819/job/116546103054"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38770892819/job/116568727536"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38770892819/job/116568727823"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38770892819/job/116568727588"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T13:39:04Z",
"url": "https://github.com/storytold/photocraft/pull/5176#pullrequestreview-5585830576",
"body": "Read the engine, preset-store and UI changes outside the translations and docs. CI is green at c96891c (Linux and Windows tests, corpus, wasm and the size gate).\n\n- **Pattern persistence:** the store never writes built-ins, only accepts the file names it generates (an edited index can't reach other files), caps index and file sizes, and leaves an unreadable index or pattern file untouched instead of overwriting or deleting it. Large patterns load as a 128 px preview and read their pixels on use. `resolve_id` keeps the checks that don't paint (Pattern Stamp enabled state, panel moves) off the file. `pattern.list`, export and fill layers report or use the full pixels, and the tests cover that, including the cache and the failure cases. Without a store the library stays in memory.\n- **Action import/export:** the new events (snapshot, history clear, locking, clipping mask, smart-filter blending options and filter-mask delete, range select, Diffuse) each have an export/import round-trip case. `Save` inside Edit Contents updates the smart object rather than needing a file, with a test for both that and the plain-document case that still fails.\n- **Shift Edge:** 100% now moves a hard edge by one pixel instead of `max(radius, 4)`, and a softer edge by its soft width. That is a visible change for anyone with saved Select and Mask settings or actions that used large values, and the PR description explains why. The tests are updated to match.\n\nThe branch conflicts with the base, so it needs a rebase before merging.\n",
"commit": {
"oid": "c96891c298151c5cb11a55928b9e1c27911f38bd"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"save_in_edit_contents_updates_the_smart_object",
"lock_params",
"locking_from",
"locking_to",
"clear_history",
"replace_existing_snapshot_and_clear_history",
"info",
"hash",
"file_name",
"preview_name",
"builtin_ids",
"is_large",
"preview",
"read_one",
"write_pattern",
"user_patterns",
"define_pattern",
"patterns_survive_a_restart",
"corrupt_pattern_files_are_skipped_and_kept",
"large_patterns_load_on_use"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5176"
},
{
"number": 5165,
"title": "Fix: brush section selection",
"url": "https://github.com/storytold/photocraft/pull/5165",
"body": "Fixes #5136.\n\nSelecting a section in Brush Settings now displays its controls without automatically enabling that section.\n\n- Keep enable/disable behavior on the section checkbox.\n- Preserve independent lock and checkbox-only row interactions.\n- Avoid unnecessary brush updates and journal entries when navigating settings.\n- Add regression tests and update existing expectations.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "27bc6e4b37b701480efa69559ab13eafc52bd94c",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/brush_panel.rs"
},
{
"path": "crates/ui-egui/src/brush_panel_tests.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581552656/job/115912461577"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581552656/job/115912461982"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581552656/job/115912461966"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581552656/job/115912464445"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581552656/job/115934842670"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581552656/job/115934842565"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581552656/job/115934842539"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T15:13:12Z",
"url": "https://github.com/storytold/photocraft/pull/5165#pullrequestreview-5549755629",
"body": "Approving `27bc6e4b37b701480efa69559ab13eafc52bd94c`.\n\nI read the whole diff. In Window > Brush Settings, clicking a controls section's name (Shape Dynamics, Scattering, Texture and so on) used to select it and also turn it on; now it only selects it, and only the checkbox toggles the setting (#5136). Checkbox-only rows and the lock behave as before. The checkbox click no longer also selects the row (`box_clicked`), and the pressed-state drawing follows the same rule. The tests were rewritten to the new behaviour: selecting each section leaves the brush and the journal unchanged, the checkbox toggles without selecting, and the existing F5 and pressed-state tests are updated. Hosted CI is green.\n\nI did not run the tests locally or use it on screen. This reverses the behaviour added for #4476, so I'm relying on #5136 for the intended behaviour; I did not check Photoshop or the parity docs.\n",
"commit": {
"oid": "27bc6e4b37b701480efa69559ab13eafc52bd94c"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"selecting_a_controls_section_keeps_its_enabled_flag_and_brush_history",
"controls_section_checkbox_toggles_without_selecting_its_settings"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5165"
},
{
"number": 5090,
"title": "Actions: add editable PhotoCraft Essentials starter presets",
"url": "https://github.com/storytold/photocraft/pull/5090",
"body": "The Actions panel currently starts empty, so trying a basic action requires recording or importing one first. This adds an editable **PhotoCraft Essentials** set: **Auto Tone, Auto Contrast, Auto Color, Select Subject, and Blur Background**.\n\nCloses #4779.\n\n- Seed the set once after saved actions load on desktop/web, preserving existing names, order, edits, and unreadable action-store files. A persisted marker respects later deletion or renaming.\n- Add **Load Default Actions** to the Actions flyout and empty panel, backed by `actions.loadDefaults` for CLI/MCP. Restoring adds missing names without overwriting custom actions. Headless sessions stay empty until explicitly loaded; recording/playback cannot mutate the library through this command.\n- Use original editable command recipes, with no Photoshop action assets. Select Subject samples visible layers. The auto adjustments retain their existing behavior.\n- Add atomic `layer.blurBackground` for the fifth preset (default radius 8). It detects the subject on the active pixel layer, excludes subject coverage from Gaussian color sampling, and blends the result inside the current selection. It preserves transparency, selection, masks, other layers and off-canvas pixels, with one Undo. No detected subject or cancellation commits no edit. This avoids a recorded Subject → Inverse → Blur sequence accidentally using an old selection after detection fails.\n\nBlur Background uses PhotoCraft's existing heuristic subject detector and Gaussian Blur. Detection quality remains that algorithm's limitation; partially covered edge pixels can still contribute to sampling. This is not depth-aware lens blur or a claim of semantic segmentation parity. The existing Actions player runs steps synchronously; the underlying command supports background-job cancellation when invoked directly through the UI dispatcher.\n\nValidation at final head `61d9e8cc1aed29ed6bea0d0fd2702fa87c4f0206`:\n\n- [CI run 38754105890](https://github.com/storytold/photocraft/actions/runs/38754105890) passed: Windows and Ubuntu tests, strict Clippy, dependency layering, corpus tests, wasm32 checks, and the browser build/size gate. Ubuntu also passed the test variants without HEIF and with craft-fonts. [Documentation](https://github.com/storytold/photocraft/actions/runs/38754106014) passed. The fuzz job was intentionally skipped by the workflow.\n- The denied-storage regression was reproduced before the fix; all 16 browser persistence tests passed locally afterward. The original assertion remains intact. Defaults and their seed marker can still be staged for persistence, while unsaved-change warnings track user changes separately, including failure/retry and changes made during an in-flight write.\n- Merged current main `4e7a17cd`; all visible checks above cover the resulting final head.\n\nFeature-specific validation before the latest main merge:\n\n- Engine/UI tests: **4,570 passed**, 32 opt-in tests ignored. Blur tests cover actual preset playback and one-step Undo/Redo at 8/16/32-bit, selection/alpha/mask/off-canvas preservation, invalid targets/locks/parameters, cancellation and no-subject failure. Sampling tests cover RGB/Gray/CMYK/Lab and both Gaussian implementations.\n- Offscreen UI: selected **Blur Background** by clicking its row and the actual **Play selection** footer button. Inspected before/after screenshots of an original synthetic fixture using the CPU canvas (`--safe-gpu`). Background texture is smoothed while subject texture remains sharp.\n- Layering, wasm, parity, and the separately run ignored `panic_hunt` test passed. Generated parity output was unchanged.\n- Release timing on a 6000 × 4000 synthetic image: **1.5904 seconds** for the whole Blur Background command at radius 8, including detection, on an Apple M1 Pro. Release build used LTO off and 16 codegen units. This is a new operation with no safe equivalent command to use as a before-change baseline.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "61d9e8cc1aed29ed6bea0d0fd2702fa87c4f0206",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "apps/photocraft-web/src/preset_bridge.rs"
},
{
"path": "crates/engine/src/action_defaults.rs"
},
{
"path": "crates/engine/src/actions_cmds.rs"
},
{
"path": "crates/engine/src/background_blur_cmds.rs"
},
{
"path": "crates/engine/src/commands.rs"
},
{
"path": "crates/engine/src/lib.rs"
},
{
"path": "crates/engine/src/preset_store.rs"
},
{
"path": "crates/engine/src/preset_store/tests.rs"
},
{
"path": "crates/ui-egui/src/actions.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "docs/control-protocol.md"
},
{
"path": "docs/decisions.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754105890/job/116487838013"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754106014/job/116487837640"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754105890/job/116487838329"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754105890/job/116487838341"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754105890/job/116487839973"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754105890/job/116497184091"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754105890/job/116497183420"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754105890/job/116497183226"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T13:46:28Z",
"url": "https://github.com/storytold/photocraft/pull/5090#pullrequestreview-5585928498",
"body": "Read the whole change outside the docs and translations. CI is green at 61d9e8c (Linux and Windows tests, corpus, wasm and the size gate).\n\n- **Seeding:** the starter set is seeded once, tracked by `defaults_version` in `actions.json`, so a later delete or rename stays. Existing names and edits win, in-memory and on-disk actions are never overwritten, and an unreadable or unsupported `actions.json` is neither seeded nor written. The tests cover those cases and the explicit \"Load Default Actions\" restore. The command is unavailable during recording or playback.\n- **Web bridge:** generated defaults count as unsaved work only once the user edits them, so a failed first write doesn't raise a loss-of-work warning, and a newer edit during a transaction still gets its own write. The four new bridge tests pin this.\n- **`layer.blurBackground`:** one atomic job. It checks the target (raster only, pixel and all locks, including a locked parent) and the radius, and a cancellation or \"no subject found\" commits nothing. The background colours are blurred with subject coverage excluded from the alpha, so foreground colour doesn't bleed. The result is blended only within the current selection, leaving alpha, masks, other layers and off-canvas pixels as they were. The tests cover this at 8, 16 and 32 bits and in every colour mode.\n\nThe description is clear that the subject detector is the existing heuristic and not depth-aware blur, which sets expectations correctly.\n",
"commit": {
"oid": "61d9e8cc1aed29ed6bea0d0fd2702fa87c4f0206"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"generated_defaults_are_persisted_without_unsaved_user_work",
"denied_storage_marks_default_edits_unsaved_and_retry_saves_them",
"finishing_default_transaction_keeps_newer_action_edits_unsaved",
"failed_default_transaction_does_not_warn_until_a_user_edit",
"defaults",
"enabled",
"load",
"attached",
"saved",
"load_defaults_preserves_names_and_edits_and_is_idempotent",
"defaults_seed_once_and_restore_only_when_requested",
"old_store_and_early_actions_win_over_defaults",
"unreadable_actions_are_not_seeded_or_overwritten",
"auto_defaults_play_on_pixels_and_undo_at_every_depth",
"subject_default_samples_visible_layers_and_undo_restores_selection",
"check",
"enabled",
"run",
"blur_background",
"disc",
"id",
"surface",
"background_energy",
"blur_background_preserves_subject_and_undoes_at_every_depth",
"blur_background_preserves_selection_alpha_masks_and_other_layers",
"background_sampling_excludes_subject_colors_for_both_blur_paths",
"blur_background_without_subject_or_when_cancelled_changes_nothing",
"blur_background_rejects_invalid_targets_params_and_effective_locks",
"blur_background_24mp_timing",
"load_actions",
"load_defaults",
"default_actions_load_from_empty_panel_and_play",
"default_actions_flyout_preserves_custom_actions_and_reveals_restored_set"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5090"
},
{
"number": 5082,
"title": "fix(mixer-brush): Alt-click samples the foreground colour like the Brush (#4873)",
"url": "https://github.com/storytold/photocraft/pull/5082",
"body": "Fixes #4873.\r\n\r\nHolding Alt with the Mixer Brush did nothing: `alt_samples` in `canvas.rs`, which decides which painting tools turn into the Eyedropper while Alt is held, listed the Brush, Pencil, Gradient and Paint Bucket but not the Mixer Brush. This adds `Tool::MixerBrush` to that list, so Alt-click (and held or latched Alt, which go through the same check) samples the foreground colour and shows the Eyedropper cursor, as with the Brush. Control+Alt stays the brush-resize drag.\r\n\r\nTests:\r\n- `alt_click_with_the_mixer_brush_samples_the_foreground` (alt_click_tests.rs): Alt-click on a red pixel with the Mixer Brush sets the foreground to red and paints nothing.\r\n- An extra assertion in the `alt_samples` unit test.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "23bab3cc9204568aaeb012b9e1da70a2caacb879",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/brush_cmds.rs"
},
{
"path": "crates/engine/src/brush_cmds/tests.rs"
},
{
"path": "crates/paint/src/mixer.rs"
},
{
"path": "crates/ui-egui/src/alt_click_tests.rs"
},
{
"path": "crates/ui-egui/src/alt_grab.rs"
},
{
"path": "crates/ui-egui/src/canvas.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634618132"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634618220"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634618249"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634618294"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634618203"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634618841"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634645206"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634645184"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634645164"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117634645147"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39113566682/job/117638559933"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-18T09:23:19Z",
"url": "https://github.com/storytold/photocraft/pull/5082#pullrequestreview-5562323692",
"body": "Approving `23bab3cc9204568aaeb012b9e1da70a2caacb879`. This replaces my earlier approval of `5244ee7c`, which made Alt-click set the foreground.\n\nI re-read the whole diff against @tanish2k09's notes. Both points are addressed:\n- **Alt-click loads the reservoir and leaves the foreground alone.** `MixerBrush` is out of `alt_samples`. Alt-click and Alt-drag now go through the new `paint.mixerLoad` (`brush_cmds.rs:691`), which samples with `sample_cmds::sample_color` and follows the Mixer Brush's Sample All Layers option (overridable with `sampleAllLayers`) and the Eyedropper's Sample Size (`canvas.rs:5128`). Over transparency or off the canvas, nothing is loaded. This matches Adobe's description: Alt-click loads paint from the canvas into the reservoir, and choosing a foreground colour loads that colour instead.\n- **The Eyedropper cursor stays while Alt is held.** `alt_picks` feeds the cursor, `alt_eyedropper` and `alt_grab::alt_click_tool`. Control+Alt is still the brush-resize drag.\n- The sample now survives the next stroke: `mixer_brush` keeps it while the foreground is unchanged and no `color` is passed (`brush_cmds.rs:653`, `MixerState::holds_sample` in `mixer.rs:66`). The UI's Mixer Brush stroke passes no `color` (`retouch_ui.rs:95`), so the load really is what gets painted. With Load after Stroke on, the reservoir is refilled with the sample, as in Photoshop.\n- CodeRabbit's point is handled too: the invalid-input case now checks that the reservoir, the foreground and the revision don't change.\n\n**What I ran:** I used a fresh worktree on this head, merged with current main (it merges cleanly). `cargo +1.99.0 fmt --all -- --check` is clean, and `clippy -p photocraft-paint -p photocraft-engine -p photocraft-ui-egui --all-targets -D warnings` is clean. The lib tests pass: engine 1738, paint 106 and ui-egui 3035, plus `panic_hunt`'s default test. I also reverted parts of the fix: removing the `holds_sample` branch fails `mixer_load_fills_the_reservoir_from_the_canvas_and_keeps_the_foreground`, and removing the Mixer Brush arm in `alt_eyedropper` fails `alt_click_with_the_mixer_brush_loads_the_reservoir_not_the_foreground`. Hosted CI is green on this head, including both Windows jobs. I tested on Linux only and didn't try it interactively.\n\n**Non-blocking**\n- Please update the PR title and description. They still say \"Alt-click samples the foreground colour like the Brush\", which is the opposite of what the PR does now and would end up in the squash commit.\n- Because `holds_sample` compares the reservoir with the foreground captured at sampling time, changing the foreground and then changing it back (for example pressing X twice) brings the old canvas sample back instead of the foreground. That's a small oddity. Clearing `sampled_over` whenever the foreground is set would avoid it, but it doesn't need to block this PR.\n- Photoshop loads a multi-coloured patch unless Load Solid Colors Only is on, so this PR covers the solid-colour case only, as you said. A follow-up issue for Load Solid Colors Only and patterned loads would keep track of that gap.\n",
"commit": {
"oid": "23bab3cc9204568aaeb012b9e1da70a2caacb879"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"mixer_load",
"mixer_load_fills_the_reservoir_from_the_canvas_and_keeps_the_foreground",
"alt_click_with_the_mixer_brush_loads_the_reservoir_not_the_foreground",
"load_mixer_reservoir"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5082"
},
{
"number": 5010,
"title": "Layer Style: Photoshop's layout (three columns, OK/Cancel/New Style/Preview on the right, one-line rows) [depends on #4938]",
"url": "https://github.com/storytold/photocraft/pull/5010",
"body": "**Depends on #4938** (one dialog size on every page; it reuses that PR's fixed-size `Sizing`). This branch includes its commit; review the second commit, `feat(layer-style): Photoshop's layout`, or merge #4938 first.\n\n## Screenshots\n\nBlending Options, side by side.\n\n**Photoshop 2026**\n\n![Photoshop 2026 Layer Style dialog](https://raw.githubusercontent.com/OctaNebula/photocraft/pr-images/5010-photoshop.png)\n\n**This PR**\n\n![PhotoCraft Layer Style dialog with this PR](https://raw.githubusercontent.com/OctaNebula/photocraft/pr-images/5010-this-pr.png)\n\n**Before** (the old layout; this capture comes from a build with #4938's earlier resize grip, widened. On `main` the dialog is 600 pt wide and its height changes per page.)\n\n![PhotoCraft Layer Style dialog before this PR](https://raw.githubusercontent.com/OctaNebula/photocraft/pr-images/5010-before.png)\n\n## Problem\n\nOur Layer Style is tall and narrow: OK / Cancel sit in a row at the bottom, Preview and New Style… are on the pages, and every slider sits under its label on a line of its own. Photoshop's (2026) is wide and short, about 777 × 525: three columns, with OK, Cancel, New Style…, Preview and a live preview swatch on the right, and each slider on one line with its label and field.\n\n## What changes\n\n- **Three columns** (`layer_style::body`): the effect list, the selected page, and a right-hand column with **OK**, **Cancel**, **New Style…**, **Preview** and a **preview swatch**. The list and the page are framed and scroll on their own. The dialog draws its own OK / Cancel and takes Enter, like Guide Layout (`dialogs.rs` skips its shared button row for Layer Style).\n- **Photoshop's size**: 800 × 550 (Photoshop: ~777 × 525; our rows are a little taller), at most the window. Every page fits without scrolling at that size, Blending Options and Bevel & Emboss included.\n- **One-line rows**: `widgets::slider_line` puts the label (right-aligned in a column, `widgets::row_label`), the slider and the value field on one line; the label still scrubs the field. Dropdowns, colours, the angle dial (`widgets::angle_dial_line`) and contours use the same label column, and check boxes sit under the controls, as in Photoshop. The Blending Options page uses them too.\n- **Preview swatch**: the dialog's pending style on Photoshop's sample square (`presets::styles::thumbnail`), built on a scratch layer the way the canvas preview builds it on the real one, cached until the style changes. While the mouse is down (a slider drag) it renders at point size, ~6 ms a change in a dev build, and re-renders at screen resolution when released.\n- **List bar** under the effect list, as in Photoshop: **fx** (the Layers panel's button and press-menu) adds an effect, or turns on and shows the layer's one for a kind that takes one; **▲ / ▼** move the selected instance among those of its kind; the **bin** deletes it.\n- **+** on every kind that takes several instances, also when the layer has none yet.\n- Three new tooltips (\"Move effect up\", \"Move effect down\", \"Delete effect\") are translated in the 20 complete catalogs.\n- **Current Layer**: Blend If's first bar is labelled \"Current Layer:\" as in Photoshop 2026 (it was \"This Layer:\"; the `thisLayer` field is unchanged). It and \"Underlying Layer:\" are now translated in all 20 complete catalogs (the latter was missing from 15).\n\nThe engine is untouched: the dialog's fields, OK (`layer.layerStyle.replace` …) and automation (`ui.dialog.set` / `ui.dialog.confirm`) work as before.\n\n## Tests\n\n- `layer_style_is_laid_out_like_photoshops`: wider than tall; the list left of the page; OK, Cancel, New Style…, Preview right of it, top to bottom.\n- `layer_style_ok_cancel_and_enter`: OK applies the style, Cancel drops it, Enter is OK; each closes the dialog.\n- `layer_style_bin_deletes_the_selected_effect`.\n- `move_instance_reorders_within_its_kind` (also out-of-range moves and unknown ids), `fx_menu_adds_or_shows_an_effect`, `pending_style_is_the_dialogs_and_leaves_the_document_alone`.\n- The existing Layer Style, dialog and #4938 tests pass unchanged apart from `body`'s new signature in five call sites.\n\nRan: `cargo test -p photocraft-ui-egui --lib --tests`, `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`, `cargo fmt --check`, `cargo xtask layers`, `cargo xtask wasm`. I checked Bevel & Emboss, Blending Options, Drop Shadow, Stroke and Styles at 1440 × 900 and Bevel & Emboss at 1366 × 700 with the offscreen `snapshot` example.\n\n## Not in this PR\n\n- Photoshop's **Name** field at the top, and the **Contour** / **Texture** sub-rows under Bevel & Emboss (Texture needs engine support).\n- The effect list's rows are painted, so screen readers and kittest can't name them (as before this PR).\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "cc865221894e21f846d92b721cce7421ea338363",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/adjust_preview.rs"
},
{
"path": "crates/ui-egui/src/dialogs.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/layer_style.rs"
},
{
"path": "crates/ui-egui/src/layer_style/contour.rs"
},
{
"path": "crates/ui-egui/src/panels.rs"
},
{
"path": "crates/ui-egui/src/widgets.rs"
},
{
"path": "crates/ui-egui/src/widgets/angle_dial.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116919460932"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116919460772"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116919460497"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116919460523"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116919460129"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116919462407"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116925659322"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116925659273"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116925659361"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116925659259"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884685832/job/116936144698"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T13:51:11Z",
"url": "https://github.com/storytold/photocraft/pull/5010#pullrequestreview-5585991159",
"body": "Read the Rust changes (dialogs.rs, layer_style.rs, the widgets, contour.rs); the i18n rows I only skimmed. The Layer Style dialog becomes a fixed-size, three-column layout: effect list over a fx / move / delete bar, the page, and OK / Cancel / New Style… / Preview with a swatch on the right.\n\n- The dialog's own OK/Cancel now come back from `layer_style::body` as the outcome, and `dialogs.rs` stops adding the generic Enter handling for this dialog so it is not applied twice. Enter is OK and Esc/close is Cancel, both covered by the new tests along with the fixed size, the short-screen fit and the delete button.\n- `move_instance` swaps only within instances of the same kind and bounds-checks both indices.\n- The page controls keep their `set_param` and defaults path; only the label column (`row_label`, `slider_line`, `angle_dial_line`) changes, and long translated labels are truncated with the full text on hover.\n- The preview swatch is cached on the style hash and renders at point size while a slider is dragged, so it should not cost much per frame.\n\nCI is green at cc86522. Two things for whoever merges: the branch includes the commit from #4938, so that one has to land first (or this needs a rebase after it), and the branch currently conflicts with the base. The \"This Layer:\" → \"Current Layer:\" label change drops the old key from a few catalogs, so the other languages will show the English label until they are updated.\n",
"commit": {
"oid": "cc865221894e21f846d92b721cce7421ea338363"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"sizing_of",
"layer_style_on_bevel",
"frame",
"layer_style_keeps_one_size_on_every_page",
"layer_style_fits_a_short_screen",
"layer_style_is_laid_out_like_photoshops",
"layer_style_ok_cancel_and_enter",
"layer_style_bin_deletes_the_selected_effect",
"boxed",
"effect_list",
"ids_of",
"move_instance",
"list_bar",
"pick_from_fx_menu",
"checkbox_line",
"page",
"side_column",
"preview_swatch",
"pending_style",
"ids",
"move_instance_reorders_within_its_kind",
"fx_menu_adds_or_shows_an_effect",
"pending_style_is_the_dialogs_and_leaves_the_document_alone"
],
"regression_source": "https://github.com/storytold/photocraft/pull/5010"
},
{
"number": 4993,
"title": "Prevent control-channel engine commands from editing behind modal dialogs",
"url": "https://github.com/storytold/photocraft/pull/4993",
"body": "Fixes #4919.\n\nThe control channel applied the existing modal-dialog guard to `ui.menu.invoke` but bypassed it for `engine.execute`. Consequently, automation could change a document while an adjustment dialog still displayed its original state.\n\nApply the same modal-availability policy to both command entry points.\n\nKeep the existing Camera Raw continuation exception, which operates on its active dialog rather than issuing an unrelated document command.\n\nAdded a regression test that:\n- Opens a Hue/Saturation dialog.\n- Rejects Invert through both command entry points.\n- Confirms the document revision and dialog fields remain unchanged.\n- Verifies the command succeeds after the dialog is cancelled.\n\nTests added but not executed locally.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "81964dc5ef77a71a2ae2a938958cf3ed77d90719",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/control.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38390181207/job/115271764890"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38390181207/job/115271765199"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38390181207/job/115271765157"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38390181207/job/115271766524"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38390181207/job/115287816997"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38390181207/job/115287817056"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38390181207/job/115287817075"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T15:10:55Z",
"url": "https://github.com/storytold/photocraft/pull/4993#pullrequestreview-5549725641",
"body": "Approving `81964dc5ef77a71a2ae2a938958cf3ed77d90719`.\n\nI read the whole diff. The control channel's `engine.execute` skipped the modal gate that `ui.menu.invoke` has, so a client could edit the document behind an open dialog. The `modal_allows` check now applies to both methods, with the existing Camera Raw continuation exception, and the error text changes to \"command is unavailable while a modal dialog is open\". The test opens the Hue/Saturation dialog, shows both methods are refused with the revision and the dialog's fields unchanged, and then shows `engine.execute` works after Cancel. Hosted CI is green.\n\nI did not run the tests locally. Worth knowing: the removed comment said `engine.execute` was \"deliberately not subject\" to this gate, so this reverses an earlier design choice; any automation client that executes engine commands while a dialog is open will now get an error. I did not search for such clients or for anything matching the old error string.\n",
"commit": {
"oid": "81964dc5ef77a71a2ae2a938958cf3ed77d90719"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"engine_execute_cannot_change_document_behind_adjustment_dialog"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4993"
},
{
"number": 4992,
"title": "Preserve Dissolve coverage when compositing clipped layers",
"url": "https://github.com/storytold/photocraft/pull/4992",
"body": "Fixes #4921.\n\nClipped layers using Dissolve were composited through the ordinary blend path, producing fractional transparency instead of Dissolve's binary pixel coverage.\n\nApply the deterministic Dissolve coverage decision before compositing clipped layers in both the CPU renderer and GPU shader.\n\nThe correction:\n- Uses the source alpha, layer opacity and fill opacity.\n- Produces the expected stochastic source-or-backdrop pixels.\n- Preserves the clipping base's alpha.\n- Uses document-coordinate noise to keep the pattern stable across rendering regions.\n\nAdded a CPU regression test for binary coverage, opacity/fill interaction and clipping boundaries, plus GPU parity cases at different opacity and fill settings.\n\nTests added but not executed locally.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "194a8976cd6c14441222b287b9b4f4841a0eb2f9",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/compose/src/tests.rs"
},
{
"path": "crates/gpu/src/compose.wgsl"
},
{
"path": "crates/gpu/tests/parity.rs"
},
{
"path": "crates/ui-egui/src/rotate_view.rs"
},
{
"path": "crates/ui-egui/src/wheel_nav.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38473268842/job/115557514771"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38473268842/job/115557514958"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38473268842/job/115557514406"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38473268842/job/115557516887"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38473268842/job/115599823687"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38473268842/job/115599823785"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38473268842/job/115599823665"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T15:10:53Z",
"url": "https://github.com/storytold/photocraft/pull/4992#pullrequestreview-5549725271",
"body": "Approving `194a8976cd6c14441222b287b9b4f4841a0eb2f9`.\n\nI read the whole diff. A clipped Dissolve layer was composited with the ordinary partial-alpha blend, which lost Dissolve's binary coverage (#4921). Now both the CPU path (`composite_atop_any`) and the GPU path (`fs_atop`) decide coverage per pixel by comparing `dissolve_noise` at document coordinates with source alpha times opacity (times fill on the CPU), then composite as Normal at full strength and keep the base's alpha. The tests are good: the output contains only exact source or base pixels, with the coverage near 0.6 x 0.5, the clip hidden outside the base, whole-render versus partial-region equality at 8, 16 and 32 bit with a negative origin, and a CPU/GPU parity case at two opacity/fill pairs. Hosted CI is green. The two rustfmt-only edits in `rotate_view.rs` and `wheel_nav.rs` are unrelated but harmless.\n\nI did not run the tests locally. Not verified: that the GPU `op.opacity` already includes fill (the parity test would fail if not, but only on a runner with an adapter), and that `doc_px(p)` matches the CPU x, y.\n",
"commit": {
"oid": "194a8976cd6c14441222b287b9b4f4841a0eb2f9"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"clipped_dissolve_is_binary_and_preserves_the_base_shape",
"clipped_dissolve_matches_whole_render_at_offset_regions_and_depths"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4992"
},
{
"number": 4985,
"title": "feat(ui): add Shift+right-click brush mode picker",
"url": "https://github.com/storytold/photocraft/pull/4985",
"body": "Fixes #4929.\n\nShift+right-click with the Brush tool opens its Mode picker at the pointer. It shares options-bar modes, including Clear, journals choices through `tools.setBrush`, and preserves document/history state when opened or dismissed. Shift held on press determines the gesture; releasing Shift first or dismissing a held right-button gesture with Escape does not erase or reopen the menu.\n\n## Current-main integration\n\nSubmitted head: `1dc98f80f60e993f3edf20af815391b6a7e7add7`, including main `d5be9b8a`.\n\nThe existing behavior and tests now follow main's split canvas, paint-mouse, control and brush-control modules and egui's current ID API. Upstream changes and both sets of regression coverage are preserved. No new feature or dependency update is added by this integration.\n\n## Validation\n\nThe final commit only associates the Mode menu note with `ui.pointer` and adds a reference from its ordinary right-click behavior. The runtime crates tree is identical to `ca17e9b62728dec96b02234686610fce00dad97b`, where the checks below ran. This documentation follow-up passed diff checking and a separate read-only review. A virtual merge with newer main `1f7fc0f53696e54da79b95015abb37d7c22cdd1a` is conflict-free; that combined tree has not been tested.\n\n- UI tests: **4,306 passed, 1 failed, 19 skipped** using the prescribed Nextest 0.9.150 and CI-pinned craft-fonts. All brush-mode/control/Escape/Shift-release regressions pass.\n- The remaining failure is `system_locale::native_first_launch_follows_os_ui_preferences`: on this Mac's `ja-JP` preference it cannot find the expected first-launch menu label. This untouched test previously also failed on the recorded main baseline. Its assertion is preserved; I am not claiming a fully green UI run.\n- The first run without the optional CJK font input had missing-glyph failures. With the same fixed font checkout as CI, those failures cleared; no application source was changed to suppress them.\n- Strict all-target UI Clippy, formatting, layers and WASM checks passed (24 crates plus the HEIF codec check). No codec or file-format source changed; a full workspace/corpus run was not repeated.\n- Offscreen control-pointer rendering confirmed the mode menu on a synthetic document. A separate read-only review checked the migrated routes, state/journal handling and regressions and found no new issue.\n\nEarlier-head CI is separate from the checks for this submitted commit. The prior docstring-coverage warning has not been treated as resolved by this integration. Codex prepared and verified this follow-up; no human or native-device execution is claimed.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "1dc98f80f60e993f3edf20af815391b6a7e7add7",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/brush_mode_menu.rs"
},
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/canvas/paint_mouse.rs"
},
{
"path": "crates/ui-egui/src/canvas/paint_mouse/tests.rs"
},
{
"path": "crates/ui-egui/src/canvas/tabs.rs"
},
{
"path": "crates/ui-egui/src/control/dispatch.rs"
},
{
"path": "crates/ui-egui/src/control/inspect.rs"
},
{
"path": "crates/ui-egui/src/control/tests.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/panels/brush_controls.rs"
},
{
"path": "crates/ui-egui/src/state.rs"
},
{
"path": "docs/brush-parity.md"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122687847397"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077238/job/122687845463"
},
{
"name": "sonarcloud / Plan coverage shards",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077587/job/122687847218"
},
{
"name": "sonarcloud / Build instrumented test archive",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077587/job/122687899026"
},
{
"name": "gate / gate (ubuntu-24.04)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122687847743"
},
{
"name": "sonarcloud / Tests ${{ matrix.shard }}/${{ inputs.shards }}",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077587/job/122687898631"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122687847734"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122687847638"
},
{
"name": "sonarcloud / SonarCloud Scan",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077587/job/122687897387"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122687849168"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122687849526"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122687849647"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122688062113"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122688062120"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40650077629/job/122688062223"
},
{
"name": "CodeRabbit",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://app.coderabbit.ai/change-stack/storytold/photocraft/pull/4985?scope=ghh_OusAJGA4HG8ljoOV0wJvVx8NmnZIjc2DdjmxOu0xZWA"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-28T08:02:27Z",
"url": "https://github.com/storytold/photocraft/pull/4985#pullrequestreview-5679506261",
"body": "Re-review at SHA 1dc98f8 (my last review was a COMMENT at 8d72fff, where I found no defect in the code).\n\nWhat changed since 8d72fff: the branch merged current main (that is the `Id::new` to `Id::unique`, `&ctx` to `ctx` and docs-date drift), and the author added two behaviour fixes with tests:\n- The gesture is decided by the modifiers held when the right button went down (`brush_mode_menu::gesture_mods`, held in temp data and cleared on release), so letting go of Shift a frame before the button no longer turns Shift+right-click into an erase dab when right-click painting is set to Erase. The control path (`pointer_secondary`) decides on the press too.\n- Escape closing the menu while the right button is still down now also consumes that gesture's release (`dismiss-pointer`), so it cannot paint or erase afterwards.\nThe docs (`control-protocol.md`, `brush-parity.md`) were updated to match.\n\nChecked: read `brush_mode_menu.rs` at 1dc98f8 in full, the `canvas_buttons` and `pointer_secondary` changes in `paint_mouse.rs`, and the PR-only delta since 8d72fff, including the new tests for the early Shift release, a quick second press in the opening frame, and Escape with the button still down. The press-time modifiers are stored on press and removed on release or when no button is down, and the other right-click gestures (Alt resize, Ctrl/Cmd layer list, plain erase or preset picker) are still checked before and after this branch as before. Head unchanged since I started, PR mergeable and clean.@@GATE@@\n\nNot verified: I did not run the UI. The author mentions a first-launch locale test failure that this PR does not touch; I relied on the gate result for this head.\n",
"commit": {
"oid": "1dc98f80f60e993f3edf20af815391b6a7e7add7"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"choose",
"mode_right_click",
"shift_right_mode_picker_selects_journals_and_clear_erases",
"mode_picker_outside_dismissal_does_not_paint_or_erase",
"mode_control_can_pick_a_row_with_ui_click",
"shift_right_mode_picker_cancels_and_other_gestures_keep_their_routes",
"escape_dismissal_consumes_a_held_secondary_release",
"shift_released_before_the_right_button_still_opens_the_mode_picker",
"shift_right_brush_does_not_open_presets_or_paint",
"brush_mode_control_routes_modifiers_and_reports_choices_without_history"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4985"
},
{
"number": 4938,
"title": "Layer Style: one dialog size on every page, and it fits short screens",
"url": "https://github.com/storytold/photocraft/pull/4938",
"body": "## Problem\n\nThe Layer Style dialog's height jumps with each page: it is sized by its content, so Drop Shadow, Blending Options and Bevel & Emboss each make it a different height (Photoshop's keeps one size). Bevel & Emboss makes it about 732 pt tall, and nothing scrolls, so on a window shorter than that (a 768 pt laptop screen once the taskbar and title bar are taken) OK and Cancel are pushed off the bottom.\n\n## Fix\n\n- **One size** (`dialogs.rs`): Layer Style is drawn in a fixed-size `egui::Resize` area, the same path the web-page dialog already uses (now a small `Sizing` per dialog: size, minimum, resizable or not, outline). It is 600 × 760, the room the tallest page needs on a 900 pt screen, and no larger than the window less a margin. It is **not** resizable, as in Photoshop; the web-page dialog keeps its resize grip.\n- **Fits and scrolls** (`layer_style.rs`): the effects list and the settings page each sit in a vertical scroll area as tall as the dialog leaves them, so a short window scrolls the page and OK / Cancel stay visible. The separator follows that height (it was a fixed 330 pt), and the settings column takes the dialog's width. The Styles gallery lost its own fixed 340 pt scroll area: the page scrolls now. The Blend If bars were capped at 320 pt; they span the column, so they stay under their 0 / 255 labels.\n- **Title band** (`dialogs.rs`): a `Resize` clips its contents to its area, which cut off the strip around the title that #1945 made draggable (#1921). This already broke that strip on the web-page dialog on `main`. The sized area's clip now reaches the 7 pt the title band extends (`TITLE_REACH`); the existing `layer_style_title_gutters_can_start_drags` test covers it.\n\nHide whitespace when reviewing `layer_style.rs` (`?w=1`): most of its diff is re-indentation from the two scroll areas.\n\n## Tests\n\n- `layer_style_keeps_one_size_on_every_page`: Bevel & Emboss, Blending Options and Styles show at the same size, with no resize grip.\n- `layer_style_fits_a_short_screen`: on a 1366 × 640 window, Bevel & Emboss fits on screen with OK and Cancel visible.\n- `dragging_the_title_bar_moves_the_dialog` now runs on a 1100 pt-tall window: at its fixed height the dialog left only 63 pt of room below it for the test's 80 pt drag, and the on-screen clamp stopped it there.\n\nRan: `cargo test -p photocraft-ui-egui --lib --tests`, `cargo clippy -p photocraft-ui-egui --all-targets -- -D warnings`, `cargo xtask layers`, `cargo xtask wasm`. I checked the dialog at 1440 × 900 and 1366 × 700 with the offscreen `snapshot` example.\n\n## Not in this PR\n\nPhotoshop's Layer Style is wide and short (about 777 × 525): OK, Cancel, New Style…, Preview and a preview swatch sit in a column on the right, and each slider shares a line with its label and field. Ours stacks each slider under its label and puts OK / Cancel at the bottom, so it is tall and narrow. That layout is #5010, which builds on this PR and makes this fixed size Photoshop's.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "63b7c921634835259139e4d3e7b9d3c396998b4d",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/dialogs.rs"
},
{
"path": "crates/ui-egui/src/layer_style.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116917614197"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116917614319"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116917613890"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116917614166"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116917614211"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116917616333"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116918401276"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116918401102"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116918401001"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116918400917"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38884118253/job/116931626140"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T13:55:39Z",
"url": "https://github.com/storytold/photocraft/pull/4938#pullrequestreview-5586050980",
"body": "Read the `dialogs.rs` change in full and the `layer_style.rs` change, which wraps the effect list and the page in scroll areas of the dialog's height. The old body is moved rather than rewritten.\n\n- `Sizing` and `sizing_of` replace the old `resizable` flag cleanly: Preferences and the web viewer keep their resizable `Resize` and stroke, and Layer Style gets a fixed 600 x 760 size capped to the window less a margin, so the tallest page (Bevel & Emboss) fits and the others stop changing the dialog's height.\n- The clip rect is widened by `TITLE_REACH` so the title band's drag area survives the `Resize` clipping it.\n- `BUTTON_ROW` leaves room under the body so OK and Cancel stay on screen on a short window; the tests check one size across pages, no resize grip, and that the dialog and both buttons fit a 1366 x 640 screen.\n\nCI is green at 63b7c92. The branch conflicts with the base and needs a rebase. #5010 is stacked on this one and rewrites the same body, so this should land first.\n",
"commit": {
"oid": "63b7c921634835259139e4d3e7b9d3c396998b4d"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"sizing_of",
"layer_style_on_bevel",
"frame",
"layer_style_keeps_one_size_on_every_page",
"layer_style_fits_a_short_screen"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4938"
},
{
"number": 4890,
"title": "Add scrollbar visibility preferences (#3262)",
"url": "https://github.com/storytold/photocraft/pull/4890",
"body": "This adds Scrollbar Visibility under Edit > Preferences > Interface, with Default, Always, Auto Hide and Hidden options. Default keeps each theme's current behavior. Hidden removes the bars without disabling wheel scrolling.\n\nThe setting applies to panels and the canvas, survives restarts, and stays active after switching themes. It uses the existing preference commands and includes labels in all 20 languages.\n\nFixes #3262.\n\nValidation on Linux, based on main at ebffaa6e:\n- Full engine and UI suites: 4,377 passed, 31 existing tests ignored, no failures.\n- Formatting, strict all-target Clippy for both crates, layering and scorecard checks passed.\n- Full Wasm build, including HEIF, passed.\n- Added regression coverage for saved settings, old preference files, all 14 themes, and scrolling with hidden bars. Verified the app with offscreen screenshots.\n\nThe change is 179 added lines and 7 removed lines, including tests, translations and docs. No new dependencies; screenshots are kept out of the Git tree.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "ab84cab7355ac999fb1be226950978bafec39605",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/prefs.rs"
},
{
"path": "crates/engine/src/prefs/tests.rs"
},
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/prefs_ui.rs"
},
{
"path": "crates/ui-egui/src/theme.rs"
},
{
"path": "docs/gaps.md"
},
{
"path": "docs/scorecard.md"
},
{
"path": "docs/ui-design.md"
},
{
"path": "docs/ui-parity.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38374672770/job/115219956585"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38374672799/job/115219956120"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38374672770/job/115219957040"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38374672770/job/115219956213"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38374672770/job/115219958647"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38374672770/job/115264304295"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38374672770/job/115264304179"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38374672770/job/115264304244"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T00:37:37Z",
"url": "https://github.com/storytold/photocraft/pull/4890#pullrequestreview-5593534344",
"body": "Read the preference, canvas, theme and Preferences UI changes; the translation entries were skimmed. Interface preferences gain a Scrollbar Visibility choice (Default, Always, Auto Hide, Hidden).\n\n- The new `interface.scrollbarVisibility` setting has a choice list, round-trips through the saved preferences, and an old preferences file without it loads as Default. An invalid value is rejected without changing anything, and `prefs.reset` returns it to Default.\n- `sync_scrollbars` applies the style only when the mode changes, and `theme::apply` clears the marker, so a theme switch re-applies the saved override. The tests cover every theme going through each mode and back to the original style.\n- Hidden zeroes all the bar sizes and opacities, and a test checks that wheel scrolling still works. On the canvas, Hidden hides the bars and Auto Hide shows them only while the pointer is over the view or a button is down.\n\nNo defect found. CI is green at ab84cab. The branch has conflicts with the base and needs a rebase.\n",
"commit": {
"oid": "ab84cab7355ac999fb1be226950978bafec39605"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"scrollbar_visibility_round_trips_and_old_preferences_keep_the_theme_default",
"scrollbar_visibility_applies_in_every_theme_and_survives_theme_changes",
"hidden_scrollbars_keep_wheel_scrolling_available"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4890"
},
{
"number": 4883,
"title": "feat(ui): show selection and active-layer bounds in the status bar",
"url": "https://github.com/storytold/photocraft/pull/4883",
"body": "Addresses #4647.\n\nAdd a \"Selection / Layer Bounds\" option to the status bar's Show menu.\n\nWhen a selection is active, the status bar displays its actual pixel bounds as X, Y, width and height. Otherwise, it shows the active layer's effective bounds, including groups and fill layers.\n\nHandle empty geometry and missing layers without changing the existing status modes or ruler-unit behavior.\n\nAdded regression coverage for selection bounds, active-layer bounds and missing layers.\n\nThis addresses the coordinate and dimensions display requested in #4647. Live drag-position reporting remains outside this change.\n\nValidation: Regression tests added but not executed locally; CI pending.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "ea500486fb40511d3b4782dfacbb89fbff846d8a",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/chrome_ui.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833735216"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833735407"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833735431"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833735406"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833735451"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833736491"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833782439"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833782459"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833782423"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117833782358"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181070435/job/117839116972"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:27:22Z",
"url": "https://github.com/storytold/photocraft/pull/4883#pullrequestreview-5583810948",
"body": "Read the whole diff. The new \"Selection / Layer Bounds\" entry reports the selection's content bounds when there is one and not empty, otherwise the active layer's effective bounds through `layer_bounds` (groups and fill layers included), and says so when neither exists. An empty result gets its own message, and the existing modes and the ruler-unit path are unchanged because the new key is handled before `status_info_text_with_units`. The menu labels now go through `tl!`, and the existing loop test excludes the new key, which does not use the document-only path. The tests check the layer text, the no-layer text, and `select.all` on a 120 x 80 document. A test with a non-rectangular selection would add coverage, but the logic only uses the bounds. CI is green at this head.\n",
"commit": {
"oid": "ea500486fb40511d3b4782dfacbb89fbff846d8a"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"status_bounds_text",
"status_bounds_follow_the_actual_selection_and_active_layer"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4883"
},
{
"number": 4783,
"title": "Fix/4758 type extreme finite geometry 20261013s1215",
"url": "https://github.com/storytold/photocraft/pull/4783",
"body": "Follow-up to #4769. Addresses the remaining text-geometry safety case in #4758.\n\nThe existing TySh parser can accept extremely large but finite transform coefficients. These may produce unusable coordinates when imported text is selected, edited, or rendered.\n\nChanges:\n- Reject nonfinite and excessively large TySh transform coefficients.\n- Share the validation between the main text parser and the fallback parser.\n- Prevent fallback decoding from reintroducing an invalid transform.\n- Preserve normal transform parsing for valid text layers.\n- Add regression tests covering valid transforms, NaN, infinity, and extreme finite values.\n\nThis is separate from #4769's mixed-endian TIFF decoding fix.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "a9d62726bed607bc540db9d09c8dc1be4f9895aa",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/io/src/blocks.rs"
},
{
"path": "crates/io/tests/psd_type_bounds.rs"
},
{
"path": "crates/text/src/psd.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115789244"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115789228"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115789109"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115788968"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115788425"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115791917"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115874582"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115874226"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115874350"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118115874220"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39268931180/job/118128469560"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:26:56Z",
"url": "https://github.com/storytold/photocraft/pull/4783#pullrequestreview-5583807081",
"body": "Read the whole diff. `usable_tysh_transform` (finite, each coefficient at most 1e9 in magnitude) is applied in all three places the matrix can enter: the text parser, the byte-level fallback in `blocks.rs`, and the final transform after the legacy point-origin shift, which is also checked for non-finite values. The `psd_type_bounds` test keeps exercising the render cap for matrices inside the gate, and the new fallback test covers a normal matrix and 1e62, -1e62, infinity and NaN.\n\nSmall thing, not blocking: the comment in `blocks.rs` says unusable matrices \"use identity\", but the code returns `None`. Please make the comment say which. CI is green at this head.\n",
"commit": {
"oid": "a9d62726bed607bc540db9d09c8dc1be4f9895aa"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Type and typography",
"new_test_names": [
"tysh_fallback_cannot_reintroduce_unusable_finite_or_nonfinite_transform",
"overflowing_legacy_origin_does_not_escape_as_a_nonfinite_transform",
"reject_unusable_matrices_before_type_layout"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4783"
},
{
"number": 4763,
"title": "refactor(liquify): reuse native brush resizing and anchored HUD feedback",
"url": "https://github.com/storytold/photocraft/pull/4763",
"body": "Addresses #4739.\n\nLiquify implements its own brush-resize calculations instead of reusing the established canvas brush-resize behavior.\n\nThis creates inconsistent diameter adjustments and visual feedback between Liquify and regular painting tools.\n\nChanges:\n- Reuse the existing native brush-resize gesture detection.\n- Reuse the shared diameter and vertical-adjustment calculations.\n- Account for the Liquify preview's zoom factor.\n- Map Liquify density to the shared normalized adjustment model.\n- Keep the brush outline anchored at the drag starting position.\n- Reuse the brush HUD preview rendering.\n- Display diameter and density feedback while resizing.\n- Keep resizing separate from painting operations.\n\nUpdate regression tests for the shared gesture calculations, including zoom and adjustment boundaries.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "f2804a7cf013519d9ba8bac1da3d96458c4f656b",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/brush_resize.rs"
},
{
"path": "crates/ui-egui/src/liquify_ui.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582936375/job/115917292766"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582936375/job/115917292063"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582936375/job/115917292651"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582936375/job/115917295858"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582936375/job/115934227575"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582936375/job/115934227644"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582936375/job/115934227401"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:17:19Z",
"url": "https://github.com/storytold/photocraft/pull/4763#pullrequestreview-5583718187",
"body": "Read the whole diff. Liquify now uses the shared gesture detection, `resized` and `tip_preview`, so a resize behaves the same as on the canvas. I checked the units: `resized` takes document-space deltas and multiplies the vertical one back by zoom, so passing `dx / zoom` and `dy / zoom` gives the same screen travel for density at any zoom, and the diameter grows by twice the horizontal delta as on the canvas. The outline stays at the press point while resizing, the readout shows diameter and density, and the gesture start now requires a secondary or primary drag start, so a plain hover cannot start it. The tests cover the gesture predicate, the size and density arithmetic at zoom 1 and 2, and the clamp at 1. CI is green at this head.\n",
"commit": {
"oid": "f2804a7cf013519d9ba8bac1da3d96458c4f656b"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Painting",
"new_test_names": [
"resized_brush"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4763"
},
{
"number": 4742,
"title": "fix(macos): bundle photocraft-cli and resolve it from droplet scripts",
"url": "https://github.com/storytold/photocraft/pull/4742",
"body": "Addresses #4714.\n\nThe macOS package builds photocraft-cli but only includes it in the separate CLI download. Generated droplet scripts therefore fail when PhotoCraft is installed without that download.\n\nThis change bundles and signs photocraft-cli inside PhotoCraft.app. Generated droplet scripts now check PHOTOCRAFT_CLI first, then common application installation paths, and finally PATH.\n\nUpdated the shim assertions and macOS packaging documentation.\n\nmacOS packaging and notarization have not been tested locally. Native Finder drag-and-drop support remains outside this change.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "b7145b5a8afb85d0eef3922931d6749436966943",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/automate_cmds.rs"
},
{
"path": "crates/engine/src/automate_cmds/tests.rs"
},
{
"path": "docs/releasing.md"
},
{
"path": "packaging/macos/package.sh"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581772408/job/115913237133"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581772498/job/115913236181"
},
{
"name": "lint / lint",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581775413/job/115913249963"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581772408/job/115913236649"
},
{
"name": "Verified AppImage inputs",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581775413/job/115913249124"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581772408/job/115913236248"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581772408/job/115913238896"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581772408/job/115944157432"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581772408/job/115944157476"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-16T15:12:19Z",
"url": "https://github.com/storytold/photocraft/pull/4742#pullrequestreview-5549743037",
"body": "Approving `b7145b5a8afb85d0eef3922931d6749436966943`.\n\nI read the whole diff. The macOS packaging script now copies `photocraft-cli` into `PhotoCraft.app/Contents/MacOS` and signs it with the hardened runtime before the app bundle is sealed, and the generated droplet shim looks for the CLI in order: `$PHOTOCRAFT_CLI`, `/Applications/PhotoCraft.app/...`, `~/Applications/PhotoCraft.app/...`, then `photocraft-cli` on `PATH`. The test checks the shim text, including that `PHOTOCRAFT_CLI` takes priority, and `docs/releasing.md` is updated. Hosted CI is green.\n\nI did not run anything on macOS. The PR itself says packaging and notarization were not tested, so please build, sign and notarize once before relying on this; nested code signing order and the shim's fixed install paths are the places to look.\n",
"commit": {
"oid": "b7145b5a8afb85d0eef3922931d6749436966943"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Platform and reliability",
"new_test_names": [],
"regression_source": "https://github.com/storytold/photocraft/pull/4742"
},
{
"number": 4711,
"title": "Treat Dissolve as a per-pixel choice",
"url": "https://github.com/storytold/photocraft/pull/4711",
"body": "## Summary\nDissolve on an adjustment, a clipped fill, and a layer with effects mixed by opacity, so 50% Dissolve exported as flat gray. Those paths now pick the layer or the backdrop per pixel, the same way an ordinary Dissolve layer does.\n\nFixes #3050, #3051, and #3052.\n\n## Tests\n`dissolve_adjustment_is_a_per_pixel_choice`, `clipped_dissolve_fill_is_a_per_pixel_choice`, and `color_overlay_keeps_dissolve_coverage` in `photocraft-compose`. `cargo xtask ci` was not run.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "db546ff4822a011a7915b66af3834f94c05767db",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/effects.rs"
},
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/compose/src/tests.rs"
},
{
"path": "crates/gpu/src/compose.wgsl"
},
{
"path": "crates/gpu/tests/parity.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117548626371"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117548626277"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117548626467"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117548626417"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117548626817"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117548627351"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117555346395"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117555346340"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117555346331"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117555346358"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39084669147/job/117566460124"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T03:44:19Z",
"url": "https://github.com/storytold/photocraft/pull/4711#pullrequestreview-5594717250",
"body": "Read the whole diff. Dissolve is now a per-pixel choice (`dissolve_noise(x, y) < coverage`) in the paths that used to mix it by opacity: the adjustment-layer mix, the clipped atop path, the text atop path and the effects merge.\n\n- The CPU and the shader versions of each path use the same rule. Coverage is `opacity * fill_opacity * mask`, or `s.a * opacity` on the text path, and the clipped paths keep the base alpha.\n- The noise is sampled at document coordinates (`rect.x0 + i % width`, and `doc_px(p)` in the shader), so a rendered sub-region matches the whole render. That is covered for 8-bit, 16-bit and float documents.\n- The new tests check that Invert, a clipped fill and a Color Overlay at 50% give only black or white and never gray. They also check the coverage ratio (about 0.3 for 60% opacity and 50% fill).\n\nOne gap: the new GPU parity tests return early when no adapter is available, so hosted CI does not run them. I read the WGSL changes against the CPU code and they agree.\n",
"commit": {
"oid": "db546ff4822a011a7915b66af3834f94c05767db"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"clipped_dissolve_is_binary_and_preserves_the_base_shape",
"clipped_dissolve_matches_whole_render_at_offset_regions_and_depths",
"dissolve_adjustment_is_a_per_pixel_choice",
"clipped_dissolve_fill_is_a_per_pixel_choice",
"color_overlay_keeps_dissolve_coverage",
"dissolve_adjustments_and_effects_match_cpu_in_offset_regions"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4711"
},
{
"number": 4547,
"title": "fix(icons): PhotoCraft icon corner radius matches other Craft apps (#4478)",
"url": "https://github.com/storytold/photocraft/pull/4547",
"body": "Fixes #4478\n\nPhotoCraft's icon appeared more square than FilmCraft and other apps because `packaging/icons.sh` cropped 22 units off each side, cutting away most of the rounded corners.\n\n**Solution:**\nRender the full 512-unit tile like other Craft apps (no crop), matching the `rx=112` radius in the SVG.\n\n**Changes:**\n- `packaging/icons.sh`: Removed Windows/Linux crop (was 468x468, now 512x512)\n- Regenerated all hicolor PNGs and `.ico`\n- Updated `assets/app-icon/README.md` with new dimensions\n- Added corner radius check to `apps/photocraft/src/app_icon.rs`\n\n**Verification:**\nCorner transparency now matches FilmCraft:\n- 256px: 16 transparent pixels (was 6)\n- 512px: 33 transparent pixels (was 12)\n- .ico 256: 16 transparent pixels (was 0)\n\n**Testing:**\n- New test fails if corners are cropped again\n- All app_icon tests pass\n\n— sent on behalf of @mjohnson7cd by Claude Code",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "a08f7a2295488c2692f68902835a693322017b64",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "apps/photocraft/src/app_icon.rs"
},
{
"path": "assets/app-icon/README.md"
},
{
"path": "assets/app-icon/hicolor/128x128/apps/ai.storyteller.photocraft.png"
},
{
"path": "assets/app-icon/hicolor/16x16/apps/ai.storyteller.photocraft.png"
},
{
"path": "assets/app-icon/hicolor/24x24/apps/ai.storyteller.photocraft.png"
},
{
"path": "assets/app-icon/hicolor/256x256/apps/ai.storyteller.photocraft.png"
},
{
"path": "assets/app-icon/hicolor/32x32/apps/ai.storyteller.photocraft.png"
},
{
"path": "assets/app-icon/hicolor/48x48/apps/ai.storyteller.photocraft.png"
},
{
"path": "assets/app-icon/hicolor/512x512/apps/ai.storyteller.photocraft.png"
},
{
"path": "assets/app-icon/hicolor/64x64/apps/ai.storyteller.photocraft.png"
},
{
"path": "assets/app-icon/photocraft.ico"
},
{
"path": "packaging/icons.sh"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384329470"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789856942/job/116611293797"
},
{
"name": "lint / lint",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789858531/job/116611298164"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384387589"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789856942/job/116611294300"
},
{
"name": "Verified AppImage inputs",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789858531/job/116611297593"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384328259"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789856942/job/116611294066"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384331765"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789856942/job/116611296356"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384414397"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789856942/job/116613692935"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789856942/job/116613692846"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384406243"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38789856942/job/116613694959"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384393124"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384339213"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559469937/job/122384339316"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:18:20Z",
"url": "https://github.com/storytold/photocraft/pull/4547#pullrequestreview-5583727527",
"body": "Read the whole diff. Rendering the full 512-unit tile for the hicolor sizes and the `.ico` is the right fix for the square-looking icon, and dropping the `TIGHT` viewBox leaves the macOS render untouched. The README and script comments now agree with the code. The new test reads the 256 px icon, so it fails if the crop comes back: the (10,10) corner pixel must be transparent and the mid-edge pixel opaque. I read the script and test but did not inspect the regenerated PNGs and `.ico` pixel by pixel. CI is green at this head.\n",
"commit": {
"oid": "a08f7a2295488c2692f68902835a693322017b64"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"windows_and_linux_icons_keep_the_round_corners"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4547"
},
{
"number": 4305,
"title": "Filter > Other > Frequency Separation (#1714)",
"url": "https://github.com/storytold/photocraft/pull/4305",
"body": "Closes #1714.\n\n**Filter › Other › Frequency Separation…** sets up frequency separation in one step, instead of the duplicate / Gaussian Blur / Apply Image / blend mode routine.\n\nFrom the active pixel layer it adds a **Frequency Separation** group right above it, holding:\n\n- **Low Frequency** (Normal): the layer through Gaussian Blur at the chosen radius. These are the same pixels Filter › Blur › Gaussian Blur produces.\n- **High Frequency** (Linear Light): `High = (Source − Low) / 2 + ½` in each colour channel, keeping the source's transparency.\n\nThis is Photoshop's recipe: Apply Image › Subtract, Scale 2, Offset 128 in 8-bit, and Add + Invert, Scale 2, Offset 0 in 16-bit, which give the same values. Linear Light computes `Low + 2·High − 1`, so the result is the source again. In 32-bit our Linear Light uses the same formula, so the recipe is unchanged there. The source layer itself is not modified, it is one history step, and the High layer ends up targeted, ready for healing.\n\nThe command is `layer.frequencySeparation` with `{\"radius\": 0.1..1000 = 4}`. From the menu it opens the existing schema dialog, which has a single Radius field. Photoshop has no menu item for this (people record it as an action), so the id is our own. The menu entry goes after Color to Alpha (`PLACE_AFTER`). I kept it out of `filter.*` because it adds layers rather than filtering the target, so mask/channel routing, Fade and Last Filter would all be wrong for it. The menu label is in all 17 catalogs, which the coverage test requires.\n\n**Limits**\n\n- Only RGB and Grayscale. In CMYK and Lab the compositor does not blend on the stored values (in a test the rebuild was off by 0.7 and 0.14), so the command is refused there rather than giving a wrong split.\n- Linear Light clips to 0..1, so 32-bit values above 1 come back as 1.\n- 8-bit stores High to the nearest level, so the rebuild is within ±1 level, the same as Photoshop's recipe.\n- The new layers carry only the source's pixels (no mask, style, opacity or blending options), and the selection is ignored. The rebuild is exact for an opaque, Normal, 100% layer, which is the normal case for this technique.\n- It is refused, with a message, on non-pixel layers (type, shape, adjustment, group, smart object), on empty layers, and when a channel, Quick Mask or layer mask is targeted.\n\nValidation:\n- Engine tests in the module: the composite of Low + High with the source hidden matches the source at 8/16/32-bit RGB and 8/32-bit Gray (tolerance 1/255, 1/65535, 1e-5); Low equals Gaussian Blur at radii 2 and 6.5; group/layer names, order, blend modes and the targeted layer; High's bounds equal the source's; one undo restores the document exactly and redo brings the result back; works on the Background; refusals listed above; bad params give `BadParams` and leave the document unchanged.\n- Each new test was checked against a mutation that should break it: High without `/2`, mask/mask-view/colour-mode checks removed, Linear Light left out, radius ignored, empty-layer check removed. All of them failed.\n- `cargo test -p photocraft-engine --test panic_hunt -- --ignored` passes.\n- ui-egui: `menus::`, `filter_dialog::` and `i18n::` tests pass, including a new one for the menu position and dialog.\n- `cargo clippy -p photocraft-engine -p photocraft-ui-egui --all-targets -- -D warnings`, `cargo fmt --all -- --check`, `cargo xtask layers`, `cargo xtask wasm` all pass. `cargo xtask parity` leaves the checklist unchanged.\n- Checked rendered with the offscreen `snapshot` example: the Layers panel after the command, and the Radius dialog. (macOS, Apple silicon)\n- Full `cargo test -p photocraft-ui-egui`: `new_doc_ui::tests::new_document_depth_labels_and_tooltips_are_translated` and `prefs_ui::tests::preference_labels_are_translated` fail the same way on `main`",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "1b74873991dbb2e9803d228a969291f09c6f1032",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/commands.rs"
},
{
"path": "crates/engine/src/frequency_separation_cmds.rs"
},
{
"path": "crates/engine/src/lib.rs"
},
{
"path": "crates/ui-egui/src/filter_dialog.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/menus.rs"
},
{
"path": "docs/architecture.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38741005428/job/116442379660"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38741005318/job/116442378873"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38741005428/job/116442379676"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38741005428/job/116442379654"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38741005428/job/116442381808"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38741005428/job/116442567072"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38741005428/job/116442566737"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38741005428/job/116442566663"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T03:52:30Z",
"url": "https://github.com/storytold/photocraft/pull/4305#pullrequestreview-5594762253",
"body": "Read the whole source diff apart from the translation files. Filter > Other > Frequency Separation adds a group above the active pixel layer with a Gaussian-blurred \"Low Frequency\" layer and a \"High Frequency\" layer set to Linear Light, as one history step.\n\n- The maths is right: High is (source - Low) / 2 + 0.5 in the colour channels, computed from the stored Low (quantised at 8 and 16 bits), so Low plus High on Linear Light rebuilds the source. The test checks that through the compositor with the source hidden, at 8, 16 and 32 bits in RGB and grey, within one level. A second test breaks the blend mode to prove the first one is not passing by accident.\n- Transparency is handled: High stays transparent where the source is, so its bounds match the source and mid-grey does not fill whole tiles, and layers without alpha are computed over the canvas. Work is done in bounded batches of tiles, so a large layer does not allocate the whole f32 copy.\n- Eligibility is strict: only an RGB or grey document, only a pixel layer with pixels, and never a targeted mask, channel or Quick Mask. Refusals leave the document untouched, and the tests cover type, adjustment, group, smart object, empty layer and channel cases.\n- Low is checked to equal what Gaussian Blur makes of the same layer at the same radius, and one undo removes the whole group.\n",
"commit": {
"oid": "1b74873991dbb2e9803d228a969291f09c6f1032"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"bad",
"radius",
"pixels_of",
"enabled",
"high_pass",
"run",
"texture",
"session",
"paint",
"doc",
"active",
"composite",
"max_diff",
"without",
"group_of",
"pixels",
"low_and_high_composite_back_to_the_source_at_every_depth",
"high_must_be_on_linear_light",
"builds_a_named_group_above_the_source_and_targets_high",
"low_is_the_gaussian_blur_of_the_source_at_the_radius",
"one_undo_removes_everything_and_redo_brings_it_back",
"works_on_the_background",
"refused",
"needs_a_pixel_layer_with_pixels",
"refuses_a_channel_or_mask_target",
"refuses_cmyk_and_lab",
"a_layer_of_transparent_tiles_is_refused_when_run",
"bad_params_are_rejected_without_changes",
"frequency_separation_sits_in_filter_other_after_color_to_alpha"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4305"
},
{
"number": 4042,
"title": "feat(canvas): rotate view with Shift + middle-mouse drag",
"url": "https://github.com/storytold/photocraft/pull/4042",
"body": "Addresses #3950.\n\nAdds Shift + middle-mouse dragging to rotate the canvas while preserving ordinary middle-mouse panning.\n\nThe rotation keeps the document point beneath the cursor in place, including flipped views and non-square pixel aspect ratios.\n\nAdds regression tests for pointer anchoring and invalid drag input.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "f05098562220a663d052ee3918fd36c8c3e17a51",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/rotate_view.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581537763/job/115912409751"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581537763/job/115912410238"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581537763/job/115912410652"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581537763/job/115912412297"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581537763/job/115933698490"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581537763/job/115933698903"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38581537763/job/115933698863"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:20:17Z",
"url": "https://github.com/storytold/photocraft/pull/4042#pullrequestreview-5583745473",
"body": "Read the whole diff. `middle_drag` rotates by 0.35 degrees per pixel of horizontal drag and recomputes the view centre so the document point under the pointer stays put. That holds with the view flipped and with a non-square pixel aspect, and the test checks both, with the pointer and a 90 px drag. It refuses non-finite input and a zero delta. In `canvas_view`, Shift with middle only changes the middle-drag case: plain middle still pans, the Hand pan and the primary-button handling are skipped while rotating, and the rotate cursor is set. The dispatch in `canvas.rs` has no test of its own, so a kittest drag with Shift held would be a good follow-up. CI is green at this head.\n",
"commit": {
"oid": "f05098562220a663d052ee3918fd36c8c3e17a51"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"middle_drag_rotates_about_the_pointer_without_panning",
"middle_drag_ignores_invalid_or_stationary_input"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4042"
},
{
"number": 4035,
"title": "fix(cmyk): Flatten Image and merges keep the inks of a tagged CMYK document (#3778)",
"url": "https://github.com/storytold/photocraft/pull/4035",
"body": "Fixes #3778\n\n## What was wrong\n\nPhotoCraft composites in RGB. For a CMYK document, the layers are read through the document's embedded CMYK profile (for example U.S. Web Coated (SWOP) v2), but the composite was separated back to CMYK through the built-in coated profile. So the pixels were read with one profile and written with another. Every pixel of a tagged CMYK document moved on Layer › Flatten Image, even with only a Background layer. The same flatten path also serves Stamp Visible, Merge Visible, Merge Layers, Copy Merged, Background from Layer and Rasterize Layer Style. Merge Down also built its temporary document without the document's profile.\n\n## What changed\n\n- `compose::flatten_to_surface`, the engine's `composite_layers` and Rasterize Layer Style now separate the composite through the document's own CMYK profile. Merge Down's temporary document keeps the profile.\n- When a CMYK document's composite is a single plain pixel layer (Normal, 100 % opacity and fill, no masks, effects, Blend If, channel exclusion or advanced blending, same depth), Flatten copies its inks exactly, as Photoshop does. Black isn't regenerated, and inks outside the display gamut aren't clipped. Partly transparent pixels that are flattened onto paper still go through the compositor.\n- `photocraft_io::merged_composite`, the helper that turns a PSD's stored merged image into RGB for corpus comparisons, now reads CMYK through the file's embedded profile. Before, every comparison of a CMYK file tagged with a non-default profile was off.\n\n## Tests\n\n- `crates/engine/tests/cmyk_flatten_profile.rs`: a CMYK document at 8 and 16 bit, tagged and untagged, holding separations plus inks no separation produces (K only, rich black, solid cyan, paper). After Flatten Image, Stamp Visible and Background from Layer the inks are bit-identical. After Merge Down and Merge Visible (which go through RGB) they stay within the profile's own round trip. A transparent pixel still flattens to paper. Five of the six tests failed before the fix. The transparency one is a guard for the new direct copy.\n- `crates/io/tests/color.rs::cmyk_merged_composite_reads_through_the_embedded_profile` (8 and 16 bit) failed before the fix.\n- On the file attached to the issue (16-bit CMYK, SWOP), checked locally:\n - Background only: Flatten leaves the inks bit-identical (max difference 0). Before, it was 12.4/255 off on screen.\n - Curves over Background: the flattened image is 1.08/255 (mean absolute difference) from the render. Before, it was 20.0. The remaining difference is the CMYK→RGB→CMYK round trip with the Curves applied. Removing it needs inks-native CMYK compositing.\n\n## Gate\n\nRun locally on macOS: `cargo test -p photocraft-compose -p photocraft-io -p photocraft-engine --lib --tests`, `cargo clippy` on those crates with `--all-targets -D warnings`, `cargo fmt --check` and `cargo xtask layers`, all green. The corpus, wasm and other-OS checks are left to PR CI.\n\n#3855 touches the same code paths. This PR also fixes the corpus merged-image helper.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "15ebf8fc940031abdf772e5fdd687c8b55938639",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/compose/src/lib.rs"
},
{
"path": "crates/engine/src/commands.rs"
},
{
"path": "crates/engine/src/pixels.rs"
},
{
"path": "crates/engine/src/rasterize_style_cmds.rs"
},
{
"path": "crates/engine/tests/cmyk_flatten_profile.rs"
},
{
"path": "crates/io/src/lib.rs"
},
{
"path": "crates/io/tests/color.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38224755494/job/114729409575"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38224755494/job/114729409634"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38224755494/job/114729409823"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38224755494/job/114729411126"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38224755494/job/114738806809"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38224755494/job/114738806221"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T04:40:10Z",
"url": "https://github.com/storytold/photocraft/pull/4035#pullrequestreview-5595033656",
"body": "Read the whole diff. Flatten Image, Stamp Visible, Background from Layer and the merges now keep the inks of a CMYK document tagged with its own profile.\n\n- `plain_cmyk_layer` is conservative: a single visible raster layer, Normal, 100 % opacity and fill, no masks, effects, Blend If, excluded channels, advanced blending or video, and a matching sample type. Anything else takes the old composite path.\n- `copy_inks` handles alpha correctly. With `need_opaque` it bails at the first non-opaque pixel, and the caller resets the surface (`s = Surface::new(fmt)`) before falling back, so no partial copy survives.\n- The other paths (`flatten_to_surface`, `composite_layers`, `bake_one`, `merged_composite`) now separate through `with_cmyk_space(cmyk_space(doc))`, the profile the layers were read with. `merge_down` takes the document so its profile reaches the temporary one.\n- The tests cover exactness of the single-layer cases at 8 and 16 bit, tagged and untagged, and both rows (separations and inks no separation produces). They also check the partly transparent layer over white, a bounded round-trip drift for Merge Down and Merge Visible, and the merged image read through the embedded profile.\n\nAll checks on this head pass (ubuntu, corpus, wasm32, web build).\n\nThis head conflicts with the base, so it needs a rebase before it can merge.\n",
"commit": {
"oid": "15ebf8fc940031abdf772e5fdd687c8b55938639"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"plain_cmyk_layer",
"copy_inks",
"profile",
"document",
"inks",
"max_diff",
"drift",
"exact",
"flatten_keeps_the_inks_of_a_single_layer_exactly",
"stamp_visible_keeps_the_inks_of_a_single_layer_exactly",
"background_from_layer_keeps_the_inks_exactly",
"flatten_of_a_partly_transparent_layer_still_composites_over_white",
"merge_down_separates_through_the_document_profile",
"merge_visible_separates_through_the_document_profile",
"cmyk_merged_composite_reads_through_the_embedded_profile"
],
"regression_source": "https://github.com/storytold/photocraft/pull/4035"
},
{
"number": 4023,
"title": "i18n(es): add 548 missing rows, fix five filter and video terms",
"url": "https://github.com/storytold/photocraft/pull/4023",
"body": "Spanish catalog (`crates/ui-egui/src/i18n/es.tsv`): 548 rows added, 5 changed. Only that file changes.\n\n**Missing rows (548)**\nStrings the code shows but Spanish didn't have, so they appeared in English. Each one already exists in at least three other catalogs (pt-BR, nl, el, uk, ja, pl…) and its English text appears in the code. Rows that only stale catalogs still carry were left out. Most are not `tl!` literals, so the coverage gate doesn't see them:\n- filter and effect names built from the parameter enums (Accented Edges, Glass, Sumi-e, Lens Flare lens types…);\n- tool-group labels (Wand, Healing, Patch, Dodge/Burn…), panel and dialog labels, measurement columns (Area, Perimeter, Integrated Density…);\n- Camera Raw labels (`cameraRaw` context), the `No Color` layer label and four `@id` command overrides.\n\nThey are appended as one section at the end and follow the catalog's existing vocabulary and formal register. Where a row already existed with \"…\" (`HDR Toning…`, `Shear…`, `Picture Frame…`), the new row matches it.\n\n**Changed (5)**\nThese are terms that read like product branding or lose the meaning. They now match the new rows:\n- *Smart Sharpen…*: «Enfoque suavizado» (smoothed: the opposite of sharpening) → «Enfoque inteligente», like *Smart Blur*.\n- *Mezzotint…*: «Grabado» (any engraving) → «Grabado a media tinta».\n- *Tilt-Shift…*: «Cambio de inclinación» lost the shift → «Desenfoque de inclinación y desplazamiento».\n- *Interpret/Replace Footage…*: «material de archivo» means archive footage → «material de vídeo».\n\nWritten from the plain meaning of the English, as the catalog header requires; no other product's localisation was consulted. `cargo test -p photocraft-ui-egui --lib i18n` passes. AI-assisted, native review welcome.\n\nWith love from Panama 🇵🇦\n\nModels: Opus 5.5, Sol 6.1, DeepSeek V4 Flash",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "303a5346fcb5961cf3a776024db58507b401e4ea",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/i18n/es.tsv"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38223353715/job/114724743383"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38223353715/job/114724743263"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38223353715/job/114724743551"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38223353715/job/114724745018"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38223353715/job/114724786716"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38223353715/job/114724786747"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38223353715/job/114724786709"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T04:44:05Z",
"url": "https://github.com/storytold/photocraft/pull/4023#pullrequestreview-5595056228",
"body": "Read the whole diff: one file, `es.tsv`, 536 added rows and five reworded ones.\n\nI checked the whole resulting file with a script. Every row has the expected 2 or 3 tab-separated fields, placeholders (`{n}`, `{app}`) match between key and value in every row, no value is empty, ellipses are kept or dropped consistently, and there are no duplicate (context, key) pairs. A sample of about 60 of the new rows reads naturally and is consistent with the existing glossary (\"capa\", \"Desenfoque de ...\", \"Máscara de ...\", \"Fusión\").\n\nThe five reworded rows are Interpret/Replace Footage (\"material de vídeo\"), Tilt-Shift, Mezzotint and Smart Sharpen. The new \"Smart Sharpen\" row matches the reworded \"Smart Sharpen…\" row, so the file is internally consistent. I could not check those terms against Adobe's Spanish menus, so the contributor should confirm them there, especially \"Enfoque inteligente\" replacing \"Enfoque suavizado\".\n\nAll checks on this head pass (ubuntu, windows, corpus, wasm32, web build).\n\nThis head conflicts with the base, so it needs a rebase; as `es.tsv` is a table, that should be a mechanical merge.\n",
"commit": {
"oid": "303a5346fcb5961cf3a776024db58507b401e4ea"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [],
"regression_source": "https://github.com/storytold/photocraft/pull/4023"
},
{
"number": 3967,
"title": "Add move and delete actions to Smart Filter rows",
"url": "https://github.com/storytold/photocraft/pull/3967",
"body": "Smart Filter rows now offer **Move Up**, **Move Down**, and **Delete Smart Filter** through the existing engine commands. Edge moves are disabled, and each successful edit creates one Undo step while preserving the editable source and active layer. Refs #1265.\n\nThe menu captures document identity, revision and the source snapshot. It closes when edits, Undo/Redo, a document switch, a locking job or a modal editor make its target stale. Explicit layer/filter targets allow editing a row while another layer is active.\n\nThis refresh also fixes selected-row tracking: moving a filter keeps that filter highlighted, deleting an earlier filter keeps the surviving selection, and deleting the selected filter clears it. A refused engine command leaves the selection and filter order unchanged. These behaviors are covered by actual pointer/menu interactions at both UI scales.\n\nHead `3a77519c` integrates pinned main `ebffaa6e`. All 20 catalogs retain main's entries and the translated row-action labels beside the existing Smart Filters key. Main's current shape-badge controls and catalog repairs are preserved. A separate source merge check against main `6f850542` is clean.\n\nValidation on the frozen Linux source:\n\n- Unfiltered UI, engine and GPU suites: **4,466 passed, 0 failed, 32 ignored, 0 filtered**, across 89 result groups and 86 executed binaries.\n- All 14 ordinary Smart UI controls pass, including the eight original menu controls, four new selection/refusal regressions and two current-main shape-badge controls.\n- Strict UI/engine/CLI/GPU Clippy with all targets, formatting and dependency layers pass.\n- Full stock workspace Wasm: **all 24 commands pass**.\n- Selected native and Wasm UI producers: **20 catalog payloads each restored byte-exact to source, 21 language entries, 23 current-root watches and four generated fonts**.\n- The optional popup capture test also passes separately. Current English 1× and French 2× captures were visually inspected; the normal suite retains that test's existing ignored status. These captures show the open menu; the post-edit selection checks are runtime UI regressions.\n\nBuild workers, Rayon workers and test threads are limited to two. Cached lower-package artifacts are checked against the tested source. Current-head hosted CI is **pending**; local results above do not claim a Windows runtime or a hardware benchmark.\n\nHistorical screenshots below belong to the earlier `d43db3e1` integration on Linux llvmpipe. They show the Hungarian/Vietnamese popup presentation and retain their original source attribution.\n\n![Earlier Hungarian Smart Filter menu at 1×](https://raw.githubusercontent.com/zenjabba/photocraft/9180b8b059b24d79db08c870d9c9f7253bb55c09/docs/review-images/20261012-smart-d43-locales/smart-hu-1x.png)\n\n![Earlier Vietnamese Smart Filter menu at 2×](https://raw.githubusercontent.com/zenjabba/photocraft/9180b8b059b24d79db08c870d9c9f7253bb55c09/docs/review-images/20261012-smart-d43-locales/smart-vi-2x.png)\n\n[All six earlier captures and scope](https://github.com/zenjabba/photocraft/tree/9180b8b059b24d79db08c870d9c9f7253bb55c09/docs/review-images/20261012-smart-d43-locales).\n\nTo build this PR, follow the [platform prerequisites](https://github.com/zenjabba/photocraft/blob/3a77519ce2cef0fb60cebd4ea796d599f68868cb/docs/development.md#prerequisites), then:\n\n```sh\ngh repo clone storytold/photocraft\ncd photocraft\ngh pr checkout 3967\ncargo run --locked --release -p photocraft\n```",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "3a77519ce2cef0fb60cebd4ea796d599f68868cb",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/panels.rs"
},
{
"path": "crates/ui-egui/src/smart_ui.rs"
},
{
"path": "crates/ui-egui/src/smart_ui/context_menu.rs"
},
{
"path": "crates/ui-egui/src/smart_ui/context_tests.rs"
},
{
"path": "crates/ui-egui/src/smart_ui/menu_guard_tests.rs"
},
{
"path": "docs/context-menu-parity.md"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38403761738/job/115317844493"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38403761319/job/115317842668"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38403761738/job/115317845519"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38403761738/job/115317845070"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38403761738/job/115317847319"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38403761738/job/115333909216"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38403761738/job/115333908957"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38403761738/job/115333909037"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T04:46:51Z",
"url": "https://github.com/storytold/photocraft/pull/3967#pullrequestreview-5595072511",
"body": "Read the whole diff. Right-clicking a Smart Filter row now offers Move Up, Move Down and Delete Smart Filter, dispatched through the existing `layer.smartFilter.move` / `delete` commands with an explicit layer, so the active layer, selection and Undo/Redo behave as they do for the CLI.\n\n- The menu target holds a weak document snapshot, document id and revision, and `count()` rejects it on any edit, undo/redo, document switch or close, re-opened document with the same id, running job, or open modal. `close_stale` runs at the top of the Layers panel so a popup cannot survive into a document with no filter rows.\n- The up/down mapping accounts for the reversed display order (`index + 1` is visually up, `index - 1` down), and the edge rows are disabled.\n- The selection remap in `selection_update` is correct for all three cases I traced by hand (moving the selected row, a row crossing it forwards or backwards, and delete before/at the selection) and is applied only after `app.run` succeeds, so a refused edit leaves order and selection alone.\n\nThe tests cover this well: every depth and two pixel scales, one history step per action, source contents unchanged, the stale-popup cases, the job and modal cases, and the refused-edit case. No defect found.\n\nAll checks on this head pass. It currently conflicts with the base, so it needs a rebase before merging.\n",
"commit": {
"oid": "3a77519ce2cef0fb60cebd4ea796d599f68868cb"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"state_id",
"pending_id",
"queue",
"count",
"params",
"enabled",
"text",
"row_menu_moves_and_deletes_the_clicked_filter_with_one_undo_step",
"two_filter_row_context_offers_reorder_and_delete",
"existing_filter_move_delete_commands_preserve_source_and_undo_at_every_depth",
"row_menu_disables_edges_and_deletes_the_only_remaining_filter",
"stale_row_menu_closes_on_edits_undo_redo_document_switch_and_same_id_reopen",
"parameter_and_camera_raw_modals_close_the_row_menu_without_mutation",
"pending_document_job_closes_row_menu_and_refuses_new_choices",
"existing_eye_and_name_double_click_remain_distinct_from_secondary_actions",
"capture_native_popup_at_two_scales_and_in_french",
"row_menu_reorder_keeps_the_selected_filter_identity",
"row_menu_delete_before_selection_keeps_the_remaining_filter_selected",
"row_menu_delete_selected_filter_clears_its_row_selection",
"row_menu_refused_edit_preserves_filter_order_and_selection"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3967"
},
{
"number": 3874,
"title": "fix(layer-style): support Blend If Alt-split handle feathering and throttle live preview",
"url": "https://github.com/storytold/photocraft/pull/3874",
"body": "### Summary\n1. Fixes Blend If slider handle interaction: Alt-dragging a black or white triangle now splits it into two half-handles (`[min, max]`) for smooth feathered luminance blending, matching Photoshop 1:1.\n2. Resolves heavy CPU spikes and stuttering during slider drag by throttling live Layer Style document preview compositing to ~30 FPS on the UI thread.\n\n### Details\n- **Alt-Split Triangle Handles:** Refactored slider drag math in `blend_if_bar` (`layer_style.rs`). Alt-dragging the black triangle splits the right half (`[0, max]`); Alt-dragging the white triangle splits the left half (`[min, 255]`). Dragging halves back together seamlessly reunites them. Handle ordering `0 <= black_lo <= black_hi <= white_lo <= white_hi <= 255` is strictly maintained.\n- **Live Preview CPU Throttling:** In `canvas.rs`, dragging sliders previously triggered full synchronous CPU composite passes on every pointer move event (up to 120+ times/sec), pegging CPU cores at 100%. Live Layer Style preview rendering is now throttled to ~30 FPS (>= 33ms) during rapid drag gestures, while guaranteeing the final state is always rendered immediately upon release or timer trigger.\n- Adds `[people.Gotix-eg]` to `contributors/people.toml`.\n\n### Testing\n- Added unit test `blend_if_alt_split_and_drag_behavior` verifying Alt-split, dragging split halves, seamless reunion, drag without Alt, and boundary clamping.\n- All 30 unit tests in `layer_style` pass (`cargo test -p photocraft-ui-egui --lib layer_style::tests`).\n- Zero clippy warnings (`cargo clippy -p photocraft-ui-egui --lib -- -D warnings`).\n- Layering verified (`cargo xtask layers`).",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "92488307722c3b840507827cac7ee9a88470fc8a",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/layer_style.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117192880931"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117192880948"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117192881027"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117192881021"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117192880610"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117192883717"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117204763947"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117204763747"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117204763768"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117204763630"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38966110346/job/117220354763"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T04:51:17Z",
"url": "https://github.com/storytold/photocraft/pull/3874#pullrequestreview-5595098959",
"body": "Read the whole diff and the surrounding `blend_if_bar` / `blend_quad` code. The drag is now a small state machine (`BlendDrag`: Pair, Split, Single) kept across frames, so Alt-dragging an unsplit Blend If handle splits it toward the pointer and dragging a half back over the other reunites it, as in Photoshop.\n\n- `step_blend_drag` only calls `u8::clamp` with ordered bounds: `blend_quad` already forces `q[0] <= q[1] <= q[2] <= q[3]`, and no branch moves a value that a later clamp uses as a bound, so there is no `min > max` panic. The final pass keeps the quad ordered.\n- Pair without Alt moves both halves together, bounded by the neighbouring pair; Single (an already split half) moves only that half between its neighbours.\n- Hit-testing offsets by 2.5 px for split halves to match the half-triangles that are drawn, and the old `usize` drag key is replaced by the new type, so nothing else reads it.\n- The new test walks a split, a move, the reunite, the white-side split, a solid drag and the clamp.\n\nNo defect found. All checks on this head pass.\n",
"commit": {
"oid": "92488307722c3b840507827cac7ee9a88470fc8a"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Compositing and filters",
"new_test_names": [
"blend_if_alt_split_and_drag_behavior"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3874"
},
{
"number": 3748,
"title": "control: suggest near matches for unknown command ids and methods (#3674)",
"url": "https://github.com/storytold/photocraft/pull/3748",
"body": "`engine.execute {\"command\": \"engine.commands\"}` failed with a bare ``unknown command `engine.commands` ``, so an agent that guessed an id had to list the whole registry to correct itself. Unknown protocol methods had the same problem.\n\n`EngineError::UnknownCommand` now appends up to three registered ids close to the bad one, so every path that reports it (headless serve, the desktop control server, MCP `command_run`, `Session::disabled_reason`) gets the hint:\n\n```\nunknown command `engine.commands`; did you mean `command.list`?\nunknown command `document.undo`; did you mean `edit.undo`, `history.undo` or `edit.purge.undo`?\nunknown command `filter.blur.gaussian`; did you mean `filter.blur.gaussianBlur`?\n```\n\nMatching is deterministic and only runs on the error path: case-insensitive edit distance (the spell checker's existing Damerau-Levenshtein, now exposed as `photocraft_text::spell::edit_distance`), then ids that start with the given name, the same last segment in another namespace, and the same namespace with a longer last segment. `engine.commands` -> `command.list` is a small explicit table since spelling can't find it. No aliases are added (#3675 is left alone).\n\nBoth protocol servers also cover the reverse case. A protocol method sent through `engine.execute` (`doc.inspect`, `ui.inspect`) says it is a protocol method. An engine command id sent as a method (`edit.undo`) points at `engine.execute`. Other unknown methods get near method names. The desktop server gains a `METHODS` list for this, and a test checks that every entry is served.\n\nTests: new unit tests in `engine::command_hints`, `automation::rpc` and `ui-egui::control`, plus a case in `tests/mcp.rs`. The engine tests failed before the change. docs/control-protocol.md has a sentence on the new error text.\n\nGate: fmt, clippy -D warnings and tests for text, engine, automation and ui-egui, plus `xtask layers` and `xtask wasm`. I didn't run the full workspace test locally and left that to CI. Three tests also fail on current main without this change: `workspace::tests::registry_filesystem_path_params_are_classified` (`edit.installedProfiles`), `adjust_params::tests::destructive_levels_dont_clip_in_32_bit` and `bridge_reports_unreachable_app` (a connect timeout on this WSL box). I haven't tried it by hand against a running desktop app.\n\nFixes #3674\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "c45d01eafe84e600bb28033a6853cc19c827b7bf",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/automation/src/rpc.rs"
},
{
"path": "crates/automation/tests/mcp.rs"
},
{
"path": "crates/engine/src/command_hints.rs"
},
{
"path": "crates/engine/src/lib.rs"
},
{
"path": "crates/text/src/spell.rs"
},
{
"path": "crates/ui-egui/src/control.rs"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38462884626/job/115522313497"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38462884586/job/115522313488"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38462884626/job/115522313647"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38462884626/job/115522313795"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38462884626/job/115522315453"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38462884626/job/115566904971"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38462884626/job/115566905160"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38462884626/job/115566904970"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:26:31Z",
"url": "https://github.com/storytold/photocraft/pull/3748#pullrequestreview-5583803054",
"body": "Read the whole diff. The hint is built only on the error path. `near_matches` ranks by spelling first (case or a few typos), then by prefix, the same last segment in another namespace, and the same namespace with a longer last segment, with ties broken by the closest spelling and then the name, so the output is deterministic and capped at three. `engine.commands` is aliased to `command.list`. A protocol method sent through `engine.execute`, and an engine command sent as a protocol method, each get a message that says which route to use. `disabled_reason_with` now returns the same text as the error, so every caller reports it the same way. `edit_distance` is a thin public wrapper over the existing Damerau-Levenshtein code. The tests cover the headless server, MCP `command_run`, the capped and ordered list, and the plain message when nothing is close. They name real registry ids such as `filter.blur.gaussianBlur`, so a rename will need them updated. CI is green at this head.\n",
"commit": {
"oid": "c45d01eafe84e600bb28033a6853cc19c827b7bf"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"unknown_names_suggest_the_right_method_or_command",
"unknown",
"protocol_listing_method_points_at_command_list",
"near_misses_suggest_registered_ids",
"suggestions_are_capped_and_ordered",
"protocol_method_hints",
"unknown_names_suggest_the_right_method_or_command"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3748"
},
{
"number": 3554,
"title": "fix(packaging): launch bundled CLI directly from AppImage",
"url": "https://github.com/storytold/photocraft/pull/3554",
"body": "Fixes #3385\nFixes #3176\n\nLaunch the bundled CLI with `PhotoCraft.AppImage --cli …`, including `--cli mcp`\nfor the stdio MCP server. A symlink named `photocraft-cli` also selects the CLI\nthrough the AppImage runtime's `ARGV0`.\n\nThe launcher executes the CLI before desktop integration, removes only the\nleading selector, and preserves arguments, stdin, stdout, stderr, exit status\nand signals. README, development and release instructions include examples.\n\nRebased on main `8407d9ccb125af52944fca9d228383459f94aaef` on 2026-10-18.\nThe README conflict is resolved while retaining the incoming Wayland, fractional\nscaling and printing guidance. The GUI integration body is byte-identical to\ncurrent main, including its AppImageLauncher and AM/AppMan fixes. The diff stays\nwithin the original six packaging/documentation files.\n\nValidation on `dd21e325b515cbf67fd4a461ae5b3e1b195aa53d`\n(tree `2db16a5823deffe08655ae6a04489aeb3f908299`):\n\n- Linux launcher regressions pass with stand-in binaries: CLI dispatch, argument\n boundaries, stdio/stderr, exit codes, missing-CLI errors and normal GUI launch.\n- The same CLI regression fails on unmodified current-main AppRun because it\n launches the GUI. The existing desktop-integration and verified-download suites pass.\n- SIGTERM and SIGINT propagate through both `--cli` and `ARGV0` dispatch (four checks).\n- Shell syntax, ShellCheck over all packaging scripts, actionlint 1.7.12 and\n `git diff --check` pass. A network-isolated Linux mdBook 0.4.52 build passes.\n\nUpstream checks are queued. Fork workflow dispatch is unavailable because GitHub\nActions is disabled on the fork (HTTP 422); the local checks above used the exact\ncommitted source. Full Rust/Windows/corpus CI has not been rerun for this head.\n\nHistorical validation on `62eb0fa5`: [packaging lint](https://github.com/haranishi/photocraft/actions/runs/38371095539),\n[documentation](https://github.com/haranishi/photocraft/actions/runs/38371094591)\nand [full fork CI](https://github.com/haranishi/photocraft/actions/runs/38371094527)\npassed. That source's real CLI was also checked in a native macOS AppDir for MCP\ninitialization, 25-tool discovery and SIGTERM. These results apply to the old head.\nThe mounted Linux AppImage runtime and real-MCP execution on the refreshed head\nremain unverified.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "dd21e325b515cbf67fd4a461ae5b3e1b195aa53d",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": ".github/workflows/packaging-lint.yml"
},
{
"path": "README.md"
},
{
"path": "docs/development.md"
},
{
"path": "docs/releasing.md"
},
{
"path": "packaging/linux/AppRun"
},
{
"path": "packaging/linux/apprun-cli-test.sh"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383320547"
},
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117559175934"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218334/job/117559175862"
},
{
"name": "lint / lint",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218484/job/117559176379"
},
{
"name": "gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383333724"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117559175954"
},
{
"name": "Verified AppImage inputs",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218484/job/117559176272"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117559175988"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383250063"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117559175981"
},
{
"name": "AppImage bundled CLI and MCP entry point",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218484/job/117559176125"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383301779"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117559175817"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117559176654"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383251236"
},
{
"name": "selection-morphology",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383250786"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117567053387"
},
{
"name": "selection-morphology-quick-perf",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383324568"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117567054085"
},
{
"name": "test (windows-latest, ${{ matrix.part }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383252124"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39088218336/job/117577471848"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/40559177469/job/122383251867"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:23:03Z",
"url": "https://github.com/storytold/photocraft/pull/3554#pullrequestreview-5583771056",
"body": "Read the whole diff. `AppRun` handles a leading `--cli` and the `photocraft-cli` symlink name through `ARGV0` before desktop integration, and it uses `exec`, so stdio, signals and the exit status go straight to the CLI. Only the first `--cli` is removed. The README, development and release docs agree with the script. The new `apprun-cli-test.sh` uses stand-in binaries and a path with a space in it, and checks the argument boundaries, the MCP stdio round trip, exit code 37, and that the GUI path is unchanged. It runs as a new job in the packaging lint workflow. CI is green at this head.\n",
"commit": {
"oid": "dd21e325b515cbf67fd4a461ae5b3e1b195aa53d"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Platform and reliability",
"new_test_names": [],
"regression_source": "https://github.com/storytold/photocraft/pull/3554"
},
{
"number": 3520,
"title": "fix(ui): restore square color chips in Studio Dark theme",
"url": "https://github.com/storytold/photocraft/pull/3520",
"body": "## Problem\n\nIn the Studio Dark theme, the toolbar displays foreground and background colors as two large overlapping circles.\n\nIssue #3494 requests restoring the compact square color chips used by the standard theme.\n\n## Root cause\n\nThe Studio Dark palette enables `round_chips`, selecting the large circular layout in the toolbar's color-chip renderer.\n\nThe existing renderer already supports square chips.\n\n## Changes\n\n- Disable `round_chips` specifically for Studio Dark.\n- Reuse the existing overlapping square-chip layout.\n- Preserve foreground/background color selection and the existing color-picker interactions.\n- Leave Studio Light and other theme palettes unchanged.\n\n## Tests\n\nAdded a regression test verifying:\n- Studio Dark selects square color chips.\n- Studio Light retains its existing circular layout.\n- The Pro theme retains its existing square layout.\n\nThe test has not been executed locally. Visual verification and CI validation are pending.\n\n## Issue\n\nFixes #3494.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "d36ec9a21e9c1ed70de2e0d185efd7e084644157",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/theme.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38147452781/job/114493687916"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38147452781/job/114493687933"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38147452781/job/114493687690"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38147452781/job/114493689247"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38147452781/job/114500129214"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38147452781/job/114500129225"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38147452781/job/114500129199"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:19:14Z",
"url": "https://github.com/storytold/photocraft/pull/3520#pullrequestreview-5583735883",
"body": "Read the whole diff. Turning `round_chips` off for Studio Dark reuses the existing square-chip layout, and Studio Light and Pro are unchanged. The test asserts all three variants, so a later palette edit that re-enables it for Studio Dark fails. CI is green at this head.\n",
"commit": {
"oid": "d36ec9a21e9c1ed70de2e0d185efd7e084644157"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"studio_dark_keeps_square_colour_chips"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3520"
},
{
"number": 3519,
"title": "feat(export): optionally trim transparent margins from layer files",
"url": "https://github.com/storytold/photocraft/pull/3519",
"body": "## Problem\n\nLayers to Files always exports each layer at the full document dimensions, even when its visible content occupies only a small area.\n\nFor workflows involving sprites, isolated assets, and reusable design elements, this leaves unnecessary transparent margins around every exported image.\n\nAddresses #3447.\n\n## Changes\n\n- Add an optional `trim` parameter to `file.export.layersToFiles`.\n- Expose a Trim checkbox in the Layers to Files dialog.\n- Calculate the visible alpha bounds of each isolated layer before export.\n- Crop the temporary export document to those bounds using the existing document-cropping implementation.\n- Account for the rendered result, including masks and layer effects.\n- Preserve full document dimensions when trimming is disabled.\n- Keep entirely transparent layers exportable without creating zero-sized images.\n- Leave the original document unchanged.\n\n## Compatibility\n\nTrimming is disabled by default, preserving existing export behavior and automation scripts.\n\nThe option applies independently to each exported layer.\n\n## Tests\n\nAdded regression tests covering:\n- Default full-canvas exports.\n- Trimmed exports and pixel preservation.\n- 8-, 16-, and 32-bit source documents.\n- Non-destructive export behavior.\n- Entirely transparent layers.\n\nThe tests have not been executed locally. CI validation is pending.\n\n## Issue\n\nAddresses #3447.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "807de6c043a512cb947d86b944c758b3db1277c7",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/file_cmds.rs"
},
{
"path": "crates/engine/src/file_cmds/tests.rs"
},
{
"path": "crates/engine/src/image_cmds.rs"
},
{
"path": "crates/ui-egui/src/view_cmds.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582913687/job/115917210744"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582913687/job/115917210037"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582913687/job/115917210530"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582913687/job/115917212369"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582913687/job/115946266125"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582913687/job/115946266330"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38582913687/job/115946266174"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-20T10:19:34Z",
"url": "https://github.com/storytold/photocraft/pull/3519#pullrequestreview-5583738841",
"body": "Read the whole diff. The default stays off in the command and the dialog. With `trim` on, each isolated layer is flattened, its alpha extent is measured in one pass, and the temporary export document is cropped with `crop_doc`, so masks and effects count and the open document is never touched. An empty layer keeps its full canvas, and a layer already at full size is not cropped. The tests cover 8, 16 and 32 bit (4 x 5 result, red pixel, document unchanged), the streamed export path, and the empty layer. CI is green at this head.\n",
"commit": {
"oid": "807de6c043a512cb947d86b944c758b3db1277c7"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"layers_to_files_can_trim_transparent_margins_without_changing_the_document",
"streamed_layer_export_respects_trim",
"trimming_an_empty_layer_keeps_a_valid_nonzero_canvas"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3519"
},
{
"number": 3464,
"title": "fix(vector): target selected saved paths and own Pen gestures",
"url": "https://github.com/storytold/photocraft/pull/3464",
"body": "Selecting a saved path in the Paths panel did not make the path tools target it: Path Selection, Free Transform Path, Direct Selection, Convert Point and Add/Delete Anchor Point kept editing the Work Path or the active shape. Unfinished Pen gestures could also survive a document switch and land in another tab.\n\nRefs #2496, #1397. Builds on #4730 (#4721, a new Pen path joins the selected path) and #4526 (#4022, ⌘/Ctrl edits the path being drawn).\n\nThis PR was reduced to the path targeting and gesture ownership part. The Pen anchor editing, Convert glyph and cursor states that were in earlier revisions will follow in a separate PR on top of this one.\n\n## Behavior\n\n- **Selected saved paths are edited.** Path Selection, Free Transform Path, Direct Selection, Convert Point, Add/Delete Anchor Point and the Pen's Auto Add/Delete act on the work or saved path selected in the Paths panel. An explicit selection comes before the targeted vector mask or the active shape layer's path; without one, the existing fallbacks apply. The selected saved path is drawn on the canvas with the vector tools. Locked layers offer no edit.\n- **Pen destination is captured at the first anchor.** Pen, Curvature Pen and Freeform Pen keep the target they started with (shape layer with its paint, selected path, vector mask or a new Work Path) until the single commit, even if the selection changes in between. The append behavior of #4730 is kept: after drawing, the path stays selected and the next path joins it.\n- **Gestures belong to their document.** Pen drafts, Direct Selection anchors and drags, Path Selection drags and Freeform traces record document, revision and target. After a tab switch, a closed or reopened document, an external edit, or a tool/target change, the gesture is cancelled and its late pointer events are consumed. A Paths panel selection is never carried to a same-named path in another tab. Drafts and anchor selections restored from settings have no owner and are dropped.\n- **Failed commits keep the draft.** If a Pen, Curvature Pen or Freeform commit is rejected, the exact draft stays, the error is shown, and it is not retried each frame. A ⌘/Ctrl press off the draft or a new Freeform trace cannot then edit another path.\n- **`path.set` validates the result.** Appending a subpath (`op`) validates the complete path (finite, f32-representable coordinates, ≤ 4096 knots/subpaths, bounded tessellation) before an undoable edit is recorded.\n- Direct Selection keeps its anchor selection across its own edits and arrow-key nudges (#4500), bound to the new revision.\n\nNo new commands, document fields or dependencies. Edits use the existing commands, one history step each.\n\n## Validation\n\nRebased as one commit on current main (`99e11388`), merge-tree check against main is clean.\n\n- `cargo fmt --all -- --check`, `cargo clippy -p photocraft-ui-egui -p photocraft-engine -p photocraft-vector --all-targets -- -D warnings`, `cargo xtask layers`, `cargo xtask wasm`: pass.\n- `photocraft-vector` and `photocraft-engine` tests pass, including the new `pen_path_append` test and `panic_hunt -- --ignored`.\n- `photocraft-ui-egui --lib`: 2721 passed, 0 failed, 13 ignored. New coverage: `pen_path_ownership_tests`, `pen_gesture_owner_tests`, saved-path cases in `direct_select_tests` / `anchor_tools_tests` (incl. arrow-key nudges on a saved path), Freeform ownership in `lasso_ui` and `vector_ui`.\n- `photocraft-ui-egui --tests` (Windows): all 27 test binaries pass with `RUST_TEST_THREADS=1`. In a parallel run, `marquee_autoscroll` crashed with `STATUS_ACCESS_VIOLATION` in the software GPU adapter (the same Windows WARP issue seen before on this machine); it passes serially. GitHub CI is the reference for the parallel run.\n\nOne upstream test expectation changed meaning with #4526: a ⌘/Ctrl press *on* the draft now edits it in place, so the failed-commit test presses off the draft instead.\n\n## Screenshots\n\nSynthetic offscreen capture (`pen_path_visual_tests`, run with `--ignored`): a saved path is selected in the Paths panel and dragged with Path Selection by the same offset.\n\n| | Before drag | After drag |\n|---|---|---|\n| main (before this PR): the Work Path moves | ![Baseline before](https://raw.githubusercontent.com/Skyfuller1303/photocraft/edc0246bc0973a4dbb2f44383570796a6e387d33/baseline-before.png) | ![Baseline after](https://raw.githubusercontent.com/Skyfuller1303/photocraft/edc0246bc0973a4dbb2f44383570796a6e387d33/baseline-after.png) |\n| This PR on current main: the selected saved curve moves, the Work Path stays | ![Selected saved path before](https://raw.githubusercontent.com/Skyfuller1303/photocraft/370930a634cd14ef236cb288b99c65def3aa2094/saved-path-before.png) | ![Selected saved path after](https://raw.githubusercontent.com/Skyfuller1303/photocraft/370930a634cd14ef236cb288b99c65def3aa2094/saved-path-after.png) |\n\nThe baseline row was captured earlier on main before #4730; Path Selection's targeting on main is unchanged since.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "1eecd00b2dff4ae4c642269cec211cd06ce1c3aa",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/vector_cmds.rs"
},
{
"path": "crates/engine/tests/pen_path_append.rs"
},
{
"path": "crates/ui-egui/src/anchor_tools.rs"
},
{
"path": "crates/ui-egui/src/anchor_tools_tests.rs"
},
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/direct_select.rs"
},
{
"path": "crates/ui-egui/src/direct_select_tests.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/input_tests.rs"
},
{
"path": "crates/ui-egui/src/lasso_ui.rs"
},
{
"path": "crates/ui-egui/src/lasso_ui/tests.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
},
{
"path": "crates/ui-egui/src/mask_thumbs_ui/tests.rs"
},
{
"path": "crates/ui-egui/src/path_selection_tests.rs"
},
{
"path": "crates/ui-egui/src/pen_gesture_owner_tests.rs"
},
{
"path": "crates/ui-egui/src/pen_path_ownership_tests.rs"
},
{
"path": "crates/ui-egui/src/pen_path_visual_tests.rs"
},
{
"path": "crates/ui-egui/src/shape_stroke_ui/shortcut_tests.rs"
},
{
"path": "crates/ui-egui/src/vector_paint_tests.rs"
},
{
"path": "crates/ui-egui/src/vector_ui.rs"
},
{
"path": "crates/vector/src/flatten.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38461074174/job/115516076792"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38461074174/job/115516076182"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38461074174/job/115516076722"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38461074174/job/115516078335"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38461074174/job/115561673282"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38461074174/job/115561673257"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38461074174/job/115561673190"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T08:42:37Z",
"url": "https://github.com/storytold/photocraft/pull/3464#pullrequestreview-5597090618",
"body": "Read the whole diff. Pen, Freeform Pen, Path Selection, Direct Selection and the anchor tools now work on the work or saved path selected in the Paths panel, and their drags and drafts are tied to the document, revision and target they started on.\n\n- The ownership checks compare the document id, the weak `Arc` pointer, the revision, the layer, the selected path, the vector-mask flag and the tool. So a reopened document with the same id and revision is still caught, and a stale drag is cancelled with its remaining pointer events swallowed.\n- The Pen target is captured at the first anchor and committed once through `path.set` (with `combine` when appending), `layer.vectorMask.edit` or `shape.create`. A failed commit keeps the exact draft, marks it `commit_failed`, and is not retried each frame or on tool switch.\n- `path.set` now validates the merged path before touching the document, so appending small components cannot get past the total knot and tessellation budget. The arithmetic guards in `validate_path` and `validate_workload` reject overflow, and the shape limits are unchanged.\n- The runtime owner and drag are `serde(skip)`, so a draft or selection restored from settings is never applied to another tab.\n\nThe tests cover the cases above: reopen, revision, target and tool changes, failed commits, undo and redo, and the limits. CI is green at this head. The branch has a merge conflict and needs a rebase before it can merge.\n",
"commit": {
"oid": "1eecd00b2dff4ae4c642269cec211cd06ce1c3aa"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"session",
"appended_work_and_saved_components_preserve_existing_geometry_and_undo",
"appending_valid_components_cannot_exceed_total_path_budget",
"invalid_path_coordinates_leave_work_and_saved_paths_unchanged",
"large_straight_work_and_saved_paths_append_and_undo_without_shape_coordinate_limits",
"add_and_delete_anchor_tools_edit_the_selected_saved_path",
"capture",
"valid",
"eq",
"current",
"same_target",
"same_path_target",
"valid",
"valid_path",
"still_valid",
"keep_after_command",
"captured_drag",
"app",
"command_with_freeform_and_curvature_edits_the_selected_saved_path_after_modifier_release",
"command_cannot_edit_an_existing_path_when_the_freeform_trace_failed_to_finish",
"save_path",
"saved",
"arrow_keys_nudge_the_selected_saved_path",
"selected_saved_and_work_paths_win_overlapping_shape_hits",
"start_command_drag",
"consume_stale_drag",
"direct_selection_cannot_apply_to_another_document_or_a_reopened_identity",
"external_revision_cancels_preview_and_preserves_the_new_path",
"layer_path_and_tool_changes_cancel_the_owned_edit",
"closing_a_document_and_changing_marquee_target_discard_old_events",
"serialized_anchor_selection_has_no_runtime_ownership_and_is_not_restored",
"command_drag_after_failed_pen_commit_preserves_draft_and_existing_work_path",
"switching_between_selection_and_convert_tools_cancels_an_in_flight_drag",
"the_convert_point_tool_edits_selected_saved_paths_without_changing_the_work_path",
"changing_the_channel_vector_mask_target_cancels_an_owned_edit",
"freeform_harness",
"freeform_run_and_straight_click",
"real_freeform_freehand_and_alt_straight_segments_append_to_the_owned_saved_path_once",
"real_freeform_stale_target_source_revision_and_document_cancel_without_finishing_elsewhere",
"another_freeform_trace_cannot_replace_a_failed_owned_draft_or_retry_it_automatically",
"freeform_alt_release_in_the_next_press_keeps_the_failed_fit",
"app",
"down",
"freeform_and_path_selection_releases_cannot_edit_a_different_document",
"freeform_release_cancels_after_revision_or_target_change",
"freeform_valid_release_is_one_undoable_path_edit",
"frame_cancellation_consumes_old_events_after_switching_tools",
"app",
"steps",
"seed",
"path",
"click",
"draft",
"selected_work_and_saved_paths_append_pen_and_curvature_components_once",
"deselected_work_path_is_replaced_without_touching_saved_paths",
"captured_path_and_shape_mode_do_not_follow_later_ui_selection",
"captured_pen_paint_preserves_float_colour_and_alpha_after_toolbar_changes",
"stale_drafts_are_cancelled_before_tool_switch_or_pointer_input",
"sync_views_and_input_apis_discard_unowned_or_closed_document_drafts",
"same_named_saved_paths_do_not_share_selection_or_drafts_across_tabs",
"reopened_same_id_and_revision_do_not_reuse_the_saved_path_selection",
"ordinary_path_edits_undo_and_redo_preserve_the_explicit_saved_path_selection",
"failed_pen_and_curvature_commits_keep_the_exact_draft_and_do_not_retry_each_frame",
"ordinary_tool_switch_commits_the_owned_draft_once",
"selected_saved_paths_move_and_free_transform_without_changing_work_or_shape",
"explicit_work_path_precedes_vector_mask_and_deselection_restores_mask_fallback",
"pen_keeps_its_captured_vector_mask_when_another_path_is_selected",
"freeform_finish_uses_its_captured_saved_path_and_rejects_foreign_documents",
"path",
"translated",
"button",
"saved_path_drag_visual_fixture",
"eq",
"valid",
"try_pen_commit",
"commit_path",
"selected_document_geometry",
"targeted_vector_mask_id",
"freeform_straight_segments_keep_the_captured_saved_path_and_reject_a_foreign_document",
"failed_freeform_straight_segment_commit_keeps_its_owned_fitted_draft",
"validate_workload",
"general_paths_keep_large_straight_coordinates_and_shape_limits_stay_put",
"general_paths_reject_coordinates_that_overflow_the_ui_or_segment_arithmetic",
"general_paths_bound_total_knots_subpaths_and_combined_tessellation"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3464"
},
{
"number": 3461,
"title": "Replay recorded layer visibility on the intended named layer",
"url": "https://github.com/storytold/photocraft/pull/3461",
"body": "Recorded layer-eye changes use document-local layer IDs, so replaying the action in another document can miss or affect the wrong layer.\r\n\r\nRecord visibility targets by layer name and duplicate-name ordinal, then resolve them consistently in engine and shell playback. Report missing targets and allow explicitly targeted visibility changes even when no layer is active. Existing recordings retain their behavior; newly recorded eye changes use the portable target.\r\n\r\nRegression coverage: replay into another document, duplicate layer names, deselected layers, document Undo and shell playback.\r\n\r\nValidation on current main (614d0689): engine and UI `actions` tests pass.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "5d8cb2a39ad70e31260f1f3fc2010ca132daff65",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/actions_cmds.rs"
},
{
"path": "crates/engine/src/jobs.rs"
},
{
"path": "crates/engine/src/lib.rs"
},
{
"path": "crates/ui-egui/src/actions.rs"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38141301626/job/114475558427"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38141301643/job/114475558312"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38141301626/job/114475558535"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38141301626/job/114475558335"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38141301626/job/114475559519"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38141301626/job/114480773322"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38141301626/job/114480773340"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38141301626/job/114480773291"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T08:45:31Z",
"url": "https://github.com/storytold/photocraft/pull/3461#pullrequestreview-5597122170",
"body": "Read the whole diff. Recorded `layer.setProps` visibility gestures now store `_actionLayer {name, ordinal}` instead of a document-local layer id, and both the engine (`run_recorded`) and the shell (`actions::play`) resolve it through the shared `playback_params`, so replay hits the intended layer in another document and among duplicate names.\n\n- A missing name or ordinal reports a failed step rather than touching another layer.\n- The active layer is used when no `layer` was given, and `names_existing_layer` is widened only for visibility-only params.\n- Both new tests cover the cross-document case, including duplicate names and undo.\n\nThis branch has a merge conflict and needs a rebase before merging.\n",
"commit": {
"oid": "5d8cb2a39ad70e31260f1f3fc2010ca132daff65"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"recorded_eye_visibility_replays_on_another_document_and_duplicate_names",
"shell_playback_resolves_recorded_eye_target_on_new_document"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3461"
},
{
"number": 3455,
"title": "Undo and redo action definition edits from the Actions panel",
"url": "https://github.com/storytold/photocraft/pull/3455",
"body": "Editing an action definition cannot currently be undone with Ctrl/Cmd+Z in the Actions panel.\r\n\r\nAdd bounded, transient action-definition undo/redo history and route Undo/Redo to it while Actions has focus. Restore reordering, deletion, renaming, recording/append, enabled-step changes, named calls and action shortcut bindings without changing document pixels or document History. Playback returns Undo routing to the document; undoing a completed recording does not restart recording.\r\n\r\nRegression coverage includes definition restoration, pending recorded-step deletion, redo invalidation, enabled-step toggles and shell keyboard routing.\r\n\r\nValidation on current main (614d0689): engine and UI `actions` tests pass.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "d84fa7955d260505378a1a596ec80d602036319d",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/actions_cmds.rs"
},
{
"path": "crates/ui-egui/src/actions.rs"
},
{
"path": "crates/ui-egui/src/menus.rs"
},
{
"path": "docs/control-protocol.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38140752894/job/114473957154"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38140752893/job/114473956990"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38140752894/job/114473956969"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38140752894/job/114473957105"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38140752894/job/114473957716"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38140752894/job/114479604010"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38140752894/job/114479603960"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38140752894/job/114479603980"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T08:46:06Z",
"url": "https://github.com/storytold/photocraft/pull/3455#pullrequestreview-5597131410",
"body": "Read the whole diff. Action-definition edits (record, stop, move, delete, toggle, rename) now push a snapshot of the action list, any pending recorded steps and the `actions.play:` shortcuts onto a separate bounded stack. `actions.undo` / `actions.redo` restore it without touching the document or the journal.\n\n- Recording state is re-based on the current journal length, so undoing a pending-step deletion does not duplicate steps. Renames put the old function-key binding back.\n- A new edit clears the redo stack, and undo is disabled while an action plays.\n- The four engine tests and the shell test cover these cases, including the \"document and journal unchanged\" assertions.\n\nNon-blocking: `actions.focused` is only cleared by a pointer press while the panel is drawn (or by `play`). If the panel is hidden by a keyboard shortcut after being clicked, Ctrl+Z would keep going to the action history.\n\nThis branch has a merge conflict and needs a rebase before merging.\n",
"commit": {
"oid": "d84fa7955d260505378a1a596ec80d602036319d"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"snapshot",
"edit_action",
"undo_edit",
"recording_and_appending_steps_can_be_undone_without_restarting_recording",
"definition_edits_undo_redo_without_touching_document_or_journal",
"enabled_step_edits_undo_and_invalidate_redo",
"undo_pending_step_deletion_does_not_duplicate_recording",
"actions_shortcut_undo_and_redo_edit_script_without_undoing_pixels"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3455"
},
{
"number": 3387,
"title": "feat(codecs): add lossless JPEG XL export for 8/16-bit images",
"url": "https://github.com/storytold/photocraft/pull/3387",
"body": "## Summary\n\nAdd lossless JPEG XL (.jxl) export to PhotoCraft using the\npure-Rust zune-jpegxl encoder.\n\n## Changes\n\n- Add JPEG XL format identification, MIME type and extension.\n- Implement lossless 8-bit and 16-bit JPEG XL encoding.\n- Support grayscale, grayscale+alpha, RGB and RGBA images.\n- Integrate JPEG XL into Save As and Export As.\n- Support .jxl through the existing flat-image export pipeline.\n- Preserve appropriate color conversion and fidelity warnings.\n- Handle encoder failures without crashing the application.\n- Document capabilities and limitations.\n\n## Tests\n\nAdded regression tests covering:\n\n- JPEG XL signature detection\n- Format capabilities and extension recognition\n- Deterministic encoding\n- Float-to-integer conversion\n- 8/16-bit lossless output verification using jxl-oxide\n- Grayscale, grayscale+alpha, RGB and RGBA\n- Graceful handling of unsupported dimensions\n\n## Limitations\n\n- Export is currently lossless only.\n- The encoder writes bare JPEG XL codestreams without embedded\n ICC, EXIF, XMP or DPI metadata.\n- JPEG XL import remains separate and is being reviewed in #2868.\n\n## Validation\n\nImplementation and dependency wiring reviewed.\n\nLocal Rust tests and formatting checks have not been executed.\nCI validation is pending.\n\n## Related\n\n- #2868 — JPEG XL import\n- #2789 — JPEG XL support",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "58b83f42a4b39cf56751be53ec8d227efee70f68",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "Cargo.lock"
},
{
"path": "apps/photocraft/src/services.rs"
},
{
"path": "book/src/formats/raster-formats.md"
},
{
"path": "crates/codecs/Cargo.toml"
},
{
"path": "crates/codecs/README.md"
},
{
"path": "crates/codecs/src/codecs/jxl.rs"
},
{
"path": "crates/codecs/src/codecs/mod.rs"
},
{
"path": "crates/codecs/src/format.rs"
},
{
"path": "crates/codecs/src/lib.rs"
},
{
"path": "crates/codecs/tests/jxl_export.rs"
},
{
"path": "crates/ui-egui/src/export_dialog.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723817597"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933877/job/117723817211"
},
{
"name": "eval",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143934037/job/117723817795"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723817686"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723817683"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723817671"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723817463"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723818432"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723848777"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723848823"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723848734"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117723848714"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39143933954/job/117729091225"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T08:51:49Z",
"url": "https://github.com/storytold/photocraft/pull/3387#pullrequestreview-5597200428",
"body": "Read the whole diff. Lossless JPEG XL export through `zune-jpegxl` (pinned), write-only and declared that way (`caps.read = false`, an `ASYMMETRIC_EXCEPTIONS` entry, and an explicit unsupported error on decode).\n\n- Gray, RGB and RGBA at 8 or 16 bits are native; GrayA is widened to RGBA at the same depth.\n- `correct_16_bit_alpha_header` parses the pinned encoder's exact header layout field by field and rewrites only the extra-channel bundle: 17 bits replace 1, so the byte-aligned frame data keeps its alignment. Any other layout fails closed instead of writing wrong alpha.\n- The encoder call is wrapped in `catch_unwind`, buffer-size arithmetic is checked, and one-pixel images return an error rather than panic.\n- Tests decode the output with an independent decoder (`jxl-oxide`, dev-dependency only) and compare samples for every layout and depth, plus 16-bit alpha across the three size-header widths.\n\nNo defects found; CI is green at this head.\n",
"commit": {
"oid": "58b83f42a4b39cf56751be53ec8d227efee70f68"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"correct_16_bit_alpha_header",
"read",
"read_dimension",
"write_bits",
"alpha_header_correction_rejects_unexpected_or_truncated_headers",
"jpeg_xl_is_export_only_and_detects_both_signatures",
"jpeg_xl_encoding_is_deterministic_and_converts_float_to_integer",
"jpeg_xl_decodes_losslessly_with_independent_decoder",
"jpeg_xl_preserves_sixteen_bit_alpha_across_dimension_header_sizes",
"jpeg_xl_rejects_one_pixel_images_without_panicking"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3387"
},
{
"number": 3345,
"title": "Add grouped gradient presets and selectable style icons",
"url": "https://github.com/storytold/photocraft/pull/3345",
"body": "The Gradient Editor requires a separate Gradients panel to choose or save presets. This change embeds the existing grouped browser in the editor and adds preset dropdowns beside the toolbar and Gradient Fill Properties previews.\n\nFollow-up to merged #2859. Main is integrated through `d45b3718`; the current diff contains only the preset-browser and selectable-icon extensions. The parent Properties/editor contribution is already in main and no longer duplicated in this PR.\n\n- Reuse the sidebar's folders, thumbnails, list/size choices, rename/delete/group actions and New action. New saves the current gradient with an inline name field.\n- In Live mode, choosing a preset updates the selected Gradient Fill's colors and opacity in one undo step while preserving geometry and options. Without a selected fill, it updates the tool gradient.\n- In Classic mode, the editor/browser edits the tool gradient. The Properties dropdown always targets the displayed layer. Selecting a preset closes the popup and clears stale stop selections, drags and numeric drafts.\n- Share Linear, Radial, Angle, Reflected and Diamond controls across toolbar, Properties and layer editor. Thumbnails remain default. **Preferences > Interface > Use PhotoCraft-native gradient icons** selects the existing project glyphs and persists through normal Apply/OK, Cancel and reset behavior. Label translated in all 16 catalogs.\n- Clip ramp painting and omit the checkerboard for opaque previews; genuine transparency keeps it. The exact reported preview gaps were not conclusively reproduced: the pixel assertion also passed before this change, so it is preservation coverage rather than a proven regression reproduction.\n\nExisting preset commands and sidebar activation/canvas-drop behavior are reused. No proprietary assets, new engine command, dependency or document-format field is introduced by this follow-up.\n\nRefs #2199; this does not claim to complete the entire issue.\n\n## Validation\n\n**Latest scorecard conflict refresh: `364ea8fb`.** Merged main `d45b3718` and resolved the sole conflict by running `cargo xtask scorecard`. Upstream metrics are retained; compared with main, only the two preference denominators differ (32 unread of 155 fields rather than 32 of 154), accounting for the native-gradient preference. `cargo xtask scorecard --check`, merge simulation and actual-base whitespace checks passed. App tests, builds and screenshot results below belong to their recorded earlier revisions and were not repeated for this generated-document conflict.\n\n**Previous implementation validation: `c2fa2245`.** Merged main after #2859 and resolved the Gradient UI/add-add conflicts by preserving the existing follow-up code: upstream's three parent Gradient files match the original parent revision exactly. All upstream catalog rows are retained; the native-icon caption appears once in each of 16 catalogs, now beside Interface rather than at frequently appended ends. Removed the duplicate contributor stanza introduced by automatic merging; the contributor file matches main. Regenerated the scorecard with its generator. Merge simulation and PR-diff whitespace checks passed.\n\n- On `c2fa2245`, **39 gradient UI tests, 1 icon Apply/Cancel/restart test and 33 engine preference tests passed**. Formatting, engine/UI Clippy with warnings denied, layering, WASM, localization (1,691/1,691 extracted keys), scorecard check and the standard Windows Debug BuildOnly passed.\n- Broader UI run before the last unrelated main refresh: **2,054 passed, 10 ignored, 2 failed**, exit 101. Both failures in `crop_default_mode` queried English labels while native Windows Auto locale supplied German. The complete affected binary subsequently passed **all 5 tests** with `PHOTOCRAFT_LOCALE=en-US`; no test assertion or crop implementation was changed. No native process crash occurred in this run. Retry counts are not added to the full-run count. Final contributor cleanup was also checked with 13 passing credit tests on `6247cc31`.\n- Fresh English native captures on `6247cc31` cover editor presets and both icon variants; the editor was captured and inspected again on `c2fa2245`. Existing hosted before/after screenshots below retain their explicit capture revisions.\n\nThe full UI run was not repeated after the final unrelated main refresh, and the full engine suite was not repeated for these merge fixes. Linux/macOS execution, browser runtime and full-workspace/corpus gates were not run locally. GitHub checks for this push are separate; no overall CI pass is claimed.\n\n[Historical capture commands and validation records](https://github.com/Skyfuller1303/photocraft/blob/83373addc448c89501df00915310597eb1e8e5c2/VALIDATION.md).\n\n## Screenshots\n\nEnglish native UI, synthetic 640 × 480 document and isolated test settings. After captures use `220a32ee`; before uses unchanged parent #2859. Images live at an immutable commit on a separate fork asset branch.\n\n| Gradient Editor before | Grouped browser in editor |\n|---|---|\n| ![Before](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/before-editor-crop-en.png) | ![After](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/editor-purples-crop-en.png) |\n\nToolbar dropdown and saving a preset:\n\n![Live dropdown](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/live-popup-crop-en.png)\n![Saving current gradient](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/popup-save-crop-en.png)\n\nProperties dropdown and saving the displayed layer:\n\n![Properties dropdown](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/arrow-popup-crop-en.png)\n![Saving layer gradient](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/arrow-save-crop-en.png)\n\n| Default style thumbnails | Optional PhotoCraft-native glyphs |\n|---|---|\n| ![Thumbnails](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/icon-setting-preview-crop-en.png) | ![Native glyphs](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/icon-setting-native-crop-en.png) |\n| ![Properties thumbnails](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/icon-setting-preview-properties-en.png) | ![Properties glyphs](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/icon-setting-native-properties-en.png) |\n\n![Saved Interface preference](https://raw.githubusercontent.com/Skyfuller1303/photocraft/83373addc448c89501df00915310597eb1e8e5c2/icon-setting-prefs-crop-en.png)\n\n[Full captures, Classic mode and 200% DPI evidence](https://github.com/Skyfuller1303/photocraft/blob/83373addc448c89501df00915310597eb1e8e5c2/README.md).",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "bfb691f84896fe68543980426632153dc4b4ea69",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/engine/src/prefs.rs"
},
{
"path": "crates/engine/src/prefs/tests.rs"
},
{
"path": "crates/ui-egui/src/gradient_ui.rs"
},
{
"path": "crates/ui-egui/src/gradient_ui/editor_tests.rs"
},
{
"path": "crates/ui-egui/src/gradient_ui/preset_browser.rs"
},
{
"path": "crates/ui-egui/src/gradient_ui/preset_browser_tests.rs"
},
{
"path": "crates/ui-egui/src/gradient_ui/properties_panel.rs"
},
{
"path": "crates/ui-egui/src/i18n/cs.tsv"
},
{
"path": "crates/ui-egui/src/i18n/de.tsv"
},
{
"path": "crates/ui-egui/src/i18n/el.tsv"
},
{
"path": "crates/ui-egui/src/i18n/es.tsv"
},
{
"path": "crates/ui-egui/src/i18n/fr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/hu.tsv"
},
{
"path": "crates/ui-egui/src/i18n/id.tsv"
},
{
"path": "crates/ui-egui/src/i18n/it.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ja.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ko.tsv"
},
{
"path": "crates/ui-egui/src/i18n/nl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pl.tsv"
},
{
"path": "crates/ui-egui/src/i18n/pt-br.tsv"
},
{
"path": "crates/ui-egui/src/i18n/ru.tsv"
},
{
"path": "crates/ui-egui/src/i18n/sr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/tr.tsv"
},
{
"path": "crates/ui-egui/src/i18n/uk.tsv"
},
{
"path": "crates/ui-egui/src/i18n/vi.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hans.tsv"
},
{
"path": "crates/ui-egui/src/i18n/zh-hant.tsv"
},
{
"path": "crates/ui-egui/src/panels.rs"
},
{
"path": "crates/ui-egui/src/prefs_ui.rs"
},
{
"path": "crates/ui-egui/src/preset_panels.rs"
},
{
"path": "docs/scorecard.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38448670437/job/115473627335"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38448670418/job/115473627130"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38448670437/job/115473627878"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38448670437/job/115473627687"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38448670437/job/115473629303"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38448670437/job/115515297372"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38448670437/job/115515297383"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38448670437/job/115515297609"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T08:57:39Z",
"url": "https://github.com/storytold/photocraft/pull/3345#pullrequestreview-5597265645",
"body": "Read the whole diff. The gradient editor and options-bar swatch get an embedded preset browser (shared with the Gradients panel), the Live ribbon becomes a preview while Classic still opens the editor, and style buttons can use native icons behind the new `interface.nativeGradientIcons` pref.\n\n- The pref defaults to false, so existing saved preferences load unchanged, and the added test covers persistence, reset, atomic rejection of invalid values and legacy defaults.\n- Preset picking recolours the current target through `gradient.presets.select` with `applyToLayer` (already in the engine) without creating a layer, and the draft-state generation counter drops stale stop fields after a replacement.\n- `paint_ramp` now draws the checker only when a sample is translucent and clips to the rounded rect, which fixes the fringe at fractional edges.\n- Tests cover both modes, the no-document case, undo/redo as one step and small-window layout across themes.\n\nThe branch has a merge conflict and needs a rebase before it can land.\n",
"commit": {
"oid": "bfb691f84896fe68543980426632153dc4b4ea69"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "UI and workflow",
"new_test_names": [
"native_gradient_icons_persist_reset_and_preserve_legacy_defaults",
"stops_editor",
"opaque_ramp_has_no_checkerboard_fringe_at_fractional_pixel_edges",
"genuinely_transparent_ramp_still_shows_checkerboard_at_both_scales",
"the_options_bar_ribbon_opens_the_editor_only_in_classic_mode",
"target",
"new_params",
"show_for",
"editor_browser_expands_groups_and_selects_without_opening_the_sidebar",
"layer_browser_replaces_color_and_opacity_preserves_geometry_and_undoes_once",
"classic_browser_and_editor_edit_the_tool_even_with_a_gradient_layer_selected",
"picking_a_preset_discards_an_uncommitted_location_draft",
"toolbar_popup_selects_in_both_modes_without_creating_a_layer",
"editor_browser_can_choose_presets_without_a_document",
"browser_new_saves_the_edited_layer_instead_of_the_tool_gradient",
"double_clicking_an_editor_preset_does_not_create_another_layer",
"presets_can_collapse_to_reach_controls_in_a_small_window_across_themes",
"layer_controls",
"properties_dropdown_edits_the_layer_in_both_tool_modes_and_saves_it",
"style_icons_edit_the_selected_layer_with_one_undo_step",
"native_gradient_icons_apply_cancel_and_restart",
"gradient_browser"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3345"
},
{
"number": 3142,
"title": "Read applied Sony RAW metadata and crop ILCE-6000 images",
"url": "https://github.com/storytold/photocraft/pull/3142",
"body": "Older Sony ARWs can carry applied white balance and black levels only in encrypted SR2SubIFD metadata. PhotoCraft previously estimated WB from the scene and used a 512 black fallback, even when the file specified different levels. This change reads the observed common-key metadata with bounded allocation and checked offsets, keeps usable plain tags authoritative, and retains existing fallbacks for malformed metadata or other keys.\n\nThe measured ILCE-6000 image area now produces 6000 × 4000 images from its 6048 × 4024 sensor without overriding an explicit crop. Three measured files use their identical camera-applied WB instead of different grey-world estimates; full-resolution 16-bit ProPhoto-compatible development is retained. The pinned public RX0 fixture now asserts black 800 and applied WB. Sensor pixels continue through the RAW development pipeline.\n\nOnly the observed `11 22 33 44` key is supported. Metadata allocation is capped at 1 MiB and `Limits.max_alloc`; offsets, directory lengths and level values are checked. Synthetic regressions cover both byte orders, independent keystream observations, metadata precedence, invalid levels/gains, truncation, out-of-block offsets and allocation limits. The cipher data was independently recovered from black-box metadata observations; no sibling decoder implementation, camera colour table or dependency is included. See [the measurement and scope notes](https://github.com/optimalbytee/photocraft/blob/codex/sony-arw-metadata/docs/sony-arw-metadata.md).\n\nValidation repeated after integration onto current upstream main (`79261615`):\n\n- `cargo test -j 12 -p photocraft-raw --features corpus --locked`: 120 tests passed, including the SHA256-verified public RAW corpus.\n- `cargo clippy -j 12 -p photocraft-raw --all-targets --features corpus --locked -- -D warnings`: passed.\n- Changed Rust files pass formatting; layering passes for all 29 crates; `git diff --check` passes.\n\nAdditional evidence supplied with the handoff: 5 io RAW integration tests, wasm checks and all five applicable quick performance checks passed on the source commit. Three private ILCE-6000 files imported/exported as 6000 × 4000 RGB16 with a ProPhoto-compatible profile. Matched 24 MP release runs remained around 0.22 seconds per image across five rounds per version; this is a metadata/framing correction, with no speedup claim. The quick performance run used a different platform from its baseline and did not enforce full-size budgets.\n\nCamera calibration and JPEG tone/DRO parity remain incomplete: PhotoCraft is still flatter/darker and less saturated than the camera JPEGs and LightCraft. This work is separate from the colour fitting in #2135 and camera profiles in #2230. Personal RAW/JPEG files and derived diagnostics are not included.\n\nRelated to #50. Informed by [LightCraft's Sony metadata investigation](https://github.com/storytold/lightcraft/issues/535).",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "e2ed234e654e574109ffcdb9756f8e8d1b8a00b2",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/raw/README.md"
},
{
"path": "crates/raw/src/cameras.rs"
},
{
"path": "crates/raw/src/lib.rs"
},
{
"path": "crates/raw/src/sony_metadata.rs"
},
{
"path": "crates/raw/src/tiffep.rs"
},
{
"path": "crates/raw/tests/raw_corpus.rs"
},
{
"path": "docs/file-format-parity.md"
},
{
"path": "docs/gaps.md"
},
{
"path": "docs/sony-arw-metadata.md"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38107328498/job/114375481372"
},
{
"name": "mdBook build",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38107328460/job/114375481091"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38107328498/job/114375481364"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38107328498/job/114375481248"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38107328498/job/114375482490"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38107328498/job/114375502026"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38107328498/job/114375502238"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T09:18:10Z",
"url": "https://github.com/storytold/photocraft/pull/3142#pullrequestreview-5597503674",
"body": "Read the whole diff. Sony ARWs whose plain tags lack a usable black level or white balance now fall back to the encrypted SR2SubIFD, and the ILCE-6000 gets its measured image area.\n\n- The keystream recurrence in `decipher` is right: the ring slot holds k[n-127] and `(n-63) % 127` holds k[n-63], and the final incomplete word stays in the clear. Only the `11 22 33 44` key is accepted.\n- `read` is bounded for hostile files: the length is capped at 1 MiB and `max_alloc`, the directory must fit inside the block, and value offsets are rebased so they cannot point outside the decrypted bytes. Only four SHORT/SSHORT values within 0..=16383 are taken. The truncation sweep and corrupted key, length and offset tests cover it.\n- Precedence is sound: usable plain tags win, and a malformed plain white balance no longer hides a valid encrypted one. `sony_wb` now rejects non-finite and non-positive levels. Adding `s.crop == s.active` to the image-area lookup means a file's explicit crop wins, and a test covers it.\n\nThe branch conflicts with the base and needs a rebase. CI is green on this head, including the corpus tests.\n",
"commit": {
"oid": "e2ed234e654e574109ffcdb9756f8e8d1b8a00b2"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "File fidelity and colour",
"new_test_names": [
"decipher",
"four",
"parse",
"fixture",
"encrypted_applied_white_balance_and_black_reach_the_developer",
"plain_levels_win_and_invalid_encrypted_levels_are_ignored",
"hostile_private_metadata_is_bounded_and_optional",
"measured_keystream_recurrence_and_unencrypted_tail",
"sony_wb"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3142"
},
{
"number": 3139,
"title": "Draw right-to-left layer names in visual word order",
"url": "https://github.com/storytold/photocraft/pull/3139",
"body": "Fixes the word-order half of #1408 / #1693 / #1465 for layer names. Complements the font fallback PRs (#2758, #2646, #2836), which fix the tofu half.\r\n\r\n## Why\r\n\r\negui 0.36 shapes each font run with HarfRust, so an Arabic, Hebrew or Kurdish **word** draws right to left with its letters joined. What it lacks is paragraph bidi: a line's runs are placed left to right in logical order, so a layer named `سلام عليكم` shows as `عليكم سلام`.\r\n\r\n#2836 addresses this with `BidiInfo::reorder_line`. That reverses the characters too, and egui then reverses each word again. Each word ends up mirrored, joining forms come from the wrong neighbours, and the لا ligature is lost (measurements in my comment on #2836).\r\n\r\n## What changes\r\n\r\n- **New `bidi_label::for_egui`.** It puts runs in visual order with the Unicode bidi algorithm (`visual_runs`) and leaves the letters of every stretch egui reverses by itself in logical order:\r\n - A right-to-left run is reversed, then each right-to-left-script stretch inside it is turned back.\r\n - Arabic-Indic digits inside a left-to-right run are pre-reversed.\r\n- **`layer_row_ui::truncated`**, which draws the names in Layers panel rows, uses it for names with right-to-left characters.\r\n - A long name is cut in **logical** order first, so it keeps its beginning and the \"…\" sits at its reading end. egui's own elision would keep the end of a reordered RTL name.\r\n - Galleys are cached by content, so unchanged rows don't re-layout.\r\n- **Other names lay out exactly as before.**\r\n- **New dependency:** `unicode-bidi = \"0.3\"`, already in the tree through usvg/resvg (MIT OR Apache-2.0).\r\n\r\n## Screenshots\r\n\r\nLayers panel with this branch and #2758 (font fallback) merged locally, macOS. Before is `main` + #2758.\r\n\r\n<img width=\"600\" height=\"1400\" alt=\"image\" src=\"https://github.com/user-attachments/assets/0ac4965f-585d-423b-ac0c-1b3e91e12e94\" /> / <img width=\"600\" height=\"1400\" alt=\"image\" src=\"https://github.com/user-attachments/assets/49e3a0d7-f023-4b32-ad08-db02f825d43c\" />\r\n\r\n## Tests\r\n\r\n- **String rules:** Arabic, Hebrew, Kurdish (Sorani), Persian with ZWNJ, European and Arabic-Indic digits, mixed Latin, and the UAX #9 W2 case (`Layer طبقة 2` → `Layer 2 طبقة`).\r\n- **With an Arabic or Hebrew system font installed** (skipped otherwise, like `font_preview`'s test), checks on what egui actually draws:\r\n - the first word's first letter has the larger x\r\n - `سلام` draws fewer glyphs than the mirrored `مالس`, so the لا ligature formed\r\n - a long name elides to fit, keeps its first word, and starts with \"…\"\r\n- `cargo fmt --all --check`, clippy (`-D warnings`), `cargo test -p photocraft-ui-egui --lib`, `cargo xtask layers`, `cargo xtask wasm` and `cargo xtask scorecard --check` are clean.\r\n\r\n## Limits\r\n\r\n- **Single-line painted labels only.** TextEdit, wrapped text and accessibility labels keep the logical string.\r\n- **Arabic-Indic digits written against Arabic letters with no space** (`طبقة١`) share one egui run with them and land on the wrong side.\r\n- **The Properties panel's layer-name header** uses a different path and is not covered yet.\r\n- **This is a stopgap until egui orders runs by bidi level** (emilk/egui#8577, which also fixes the RTL caret issue emilk/egui#8576). Delete `bidi_label` then.\r\n\r\n🤖 Generated with [Claude Code](https://claude.com/claude-code)",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "4da79e8d16ded8b51f06c1ac64e50ccba288274e",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "Cargo.lock"
},
{
"path": "crates/ui-egui/Cargo.toml"
},
{
"path": "crates/ui-egui/src/bidi_label.rs"
},
{
"path": "crates/ui-egui/src/bidi_label_tests.rs"
},
{
"path": "crates/ui-egui/src/layer_row_ui.rs"
},
{
"path": "crates/ui-egui/src/lib.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38357318207/job/115163103289"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38357318207/job/115163103593"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38357318207/job/115163103602"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38357318207/job/115163104641"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38357318207/job/115189380802"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38357318207/job/115189380869"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38357318207/job/115189380855"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T09:17:33Z",
"url": "https://github.com/storytold/photocraft/pull/3139#pullrequestreview-5597496876",
"body": "Read the whole diff. Arabic, Hebrew and other right-to-left layer names now draw with their words in visual order, and left-to-right text is returned untouched.\n\n- `for_egui` reorders runs with the Unicode bidi algorithm and then un-reverses each stretch egui already reverses itself, so letters keep their joining forms and the lam-alef ligature. The tests pin word order, European and Arabic-Indic digits, Persian ZWNJ, and a glyph-position check that skips cleanly when no system font is present.\n- `truncated_bidi` cuts the name in logical order by binary search and reorders afterwards, so a long name keeps its beginning and the ellipsis lands at the reading end. The left-to-right path is unchanged.\n- The one dependency, `unicode-bidi`, is already in the lockfile.\n\nThe branch conflicts with the base and needs a rebase before it can merge. CI is green on this head.\n",
"commit": {
"oid": "4da79e8d16ded8b51f06c1ac64e50ccba288274e"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Type and typography",
"new_test_names": [
"rtl_script",
"push_unreversing_rtl_stretches",
"text_without_right_to_left_characters_is_untouched",
"right_to_left_words_swap_places_but_keep_their_letters",
"numbers_and_latin_keep_reading_left_to_right",
"persian_joiners_stay_inside_their_word",
"system_font",
"fonts_with",
"drawn",
"x_of",
"arabic_draws_first_word_rightmost_with_letters_joined",
"hebrew_draws_first_word_rightmost",
"a_long_right_to_left_layer_name_keeps_its_beginning",
"truncated_bidi"
],
"regression_source": "https://github.com/storytold/photocraft/pull/3139"
},
{
"number": 2300,
"title": "Scroll the viewport while outlining with Polygonal Lasso",
"url": "https://github.com/storytold/photocraft/pull/2300",
"body": "Polygonal Lasso now scrolls the canvas while the pointer rests near or just past an active viewport edge, including between vertex clicks with the button released. This lets a zoomed-in outline continue into an offscreen area without manually switching to navigation.\n\nFixes #2231.\n\nScrolling keeps placed vertices in document coordinates, uses the existing rotated, mirrored and pixel-aspect-aware view mapping and Overscroll bounds, and requests further frames for a stationary pointer. Returning to the interior, opening a dialog or using temporary navigation stops automatic motion. An unfinished polygon is tied to its starting document so switching documents or tools cannot later commit it to a different document. Shift-constrained segments remain supported while the view scrolls.\n\nValidation on `ebf3777aec961d1815aa141d9987eaa645c2273f`:\n- All 22 Polygonal Lasso tests passed, including the new rotation/mirroring/pixel-aspect/Shift regression. Coverage includes all edges and corners, stationary button-up continuation, unchanged vertices and history, next-click mapping, one-step commit/undo, modal and temporary navigation, inactive panes, document/tool changes, Escape and Overscroll-off boundaries.\n- Native UI library: 2,931 passed, 13 ignored, 0 failed.\n- Native offscreen replay: 60 stationary button-up frames moved the view center from `[180, 150]` to `[313.7499, 150]`, revealed the blue marker, and retained both document-space vertices. Before/after renders were inspected.\n- [Hosted CI run 38754718916](https://github.com/storytold/photocraft/actions/runs/38754718916) passed on this exact head: Ubuntu and Windows workspace tests with HEIF; Ubuntu without-HEIF and craft-fonts suites; strict workspace/all-target Clippy; dependency layering; scorecard verification; corpus tests; L0–L6 Wasm checks; and browser release build/size gate. The workflow intentionally skipped fuzzing.\n\nBefore:\n\n<img width=\"2400\" height=\"1600\" alt=\"Before: stationary edge pointer does not scroll\" src=\"https://github.com/user-attachments/assets/05672e4b-4c7d-4912-a53f-1f3f70c9fa19\" />\n\nAfter:\n\n<img width=\"2400\" height=\"1600\" alt=\"After: scrolling reveals the offscreen blue marker and preserves the outline\" src=\"https://github.com/user-attachments/assets/b11c6e30-b1dd-409b-8e9e-fffbee646cd4\" />",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "ebf3777aec961d1815aa141d9987eaa645c2273f",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/ui-egui/src/canvas.rs"
},
{
"path": "crates/ui-egui/src/polygon_lasso_tests.rs"
},
{
"path": "crates/ui-egui/src/shortcuts.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754718916/job/116489979809"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754718916/job/116489979748"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754718916/job/116489979114"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754718916/job/116489981663"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754718916/job/116515408595"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754718916/job/116515408651"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/38754718916/job/116515408292"
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-18T18:14:16Z",
"url": "https://github.com/storytold/photocraft/pull/2300#pullrequestreview-5564229610",
"body": "Approving. I read the whole diff at ebf3777a.\n\n- `polygon_edge_scroll` only moves the view center. Placed vertices stay in document coordinates, and the scroll goes through `ViewXform::unmap_vec` with the view's flip, rotation and pixel aspect, then through the existing Overscroll clamp, so the displayed axes and Overscroll-off bounds are respected. Inputs are guarded (finite pointer, `dt` and zoom, a positive edge band, delta time capped at 50 ms).\n- It does not run while a dialog, popup, text edit, Camera Raw, a drag or temporary navigation (Space or middle button) is active, when the window is unfocused, or when the pointer is over the other tiled pane, and it only runs for the active document in the primary view.\n- `polygon_cancel_stale` now also drops an unfinished polygon when the tool is no longer Polygonal Lasso, and `polygon_doc` is cleared on retract, commit and Escape, so an outline cannot be committed to another document.\n- The new tests cover all eight edges and corners with flip and rotation, button-up continuation with unchanged vertices and history, the next click using the panned transform with a single undo step, pixel aspect and Shift-constrained segments after panning, Overscroll off, dialog/Space/middle-button pauses, the inactive pane, and document and tool switches. The only harness change is rendering dialogs, which the dialog test needs.\n- No new user-visible strings, so no catalog changes are needed.\n\nValidation was source reading plus the hosted checks on this exact head, which all completed green: test jobs on ubuntu and windows, select platforms, corpus, wasm32 check, and web build and wasm size gate (the fuzz job is skipped by design). The PR is mergeable. No local tests were run (builds are paused).\n",
"commit": {
"oid": "ebf3777aec961d1815aa141d9987eaa645c2273f"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Selection, paths and transforms",
"new_test_names": [
"polygon_edge_scroll",
"zoomed_polygon",
"polygon_edge_scroll_continues_with_button_up_and_keeps_vertices_fixed",
"polygon_edge_scroll_places_the_next_vertex_in_the_panned_view_and_commits_once",
"polygon_edge_scroll_keeps_pixel_aspect_and_shift_constrained_segments",
"polygon_edge_scroll_pauses_for_dialogs_and_temporary_navigation",
"polygon_edge_scroll_never_moves_an_inactive_pane_or_survives_a_document_or_tool_switch"
],
"regression_source": "https://github.com/storytold/photocraft/pull/2300"
},
{
"number": 1991,
"title": "feat(actions): import and export reusable Actions JSON files",
"url": "https://github.com/storytold/photocraft/pull/1991",
"body": "## Summary\n\nPartially addresses #1951.\n\nPhotoCraft persists recorded actions internally, but users\ncannot conveniently export their action libraries or import\nactions from another installation.\n\nThis adds JSON import/export through the engine commands `actions.import` and `actions.export`, using PhotoCraft's existing versioned ActionsFile format. The Actions panel, CLI, and MCP share the engine implementation.\n\n## Changes\n\n### Actions Panel\n\nAdd an Actions menu containing:\n\n- Load Actions… (supports JSON alongside the existing ATN path)\n- Save Actions… (.json)\n\nThe panel keeps file selection and status messages; merge, rename, validation, nested-reference retargeting, and serialization run in the engine. Menu labels and status strings are translated.\n\n### Export\n\n- Export the current action library as JSON.\n- Use the existing ActionsFile version 1 format.\n- Preserve action names, commands, parameters, and order.\n- Enforce the existing 16 MiB Actions file limit.\n- Report successful exports through the status system.\n\n### Import\n\n- Read versioned Actions JSON files.\n- Preserve existing actions.\n- Append new actions to the library.\n- Skip already imported identical actions.\n- Rename conflicting imports using `(Imported N)`.\n- Retarget nested action references after renaming.\n- Support nested references by action name or imported index.\n- Update the action-library revision for persistence.\n- Prevent imports while actions are recording or playing.\n\n### File Validation\n\nReject:\n\n- Invalid JSON.\n- Unsupported file versions.\n- Files exceeding the 16 MiB limit.\n- Empty or duplicate action names.\n- Excessively long action names.\n- More than 1,024 imported actions.\n- More than 10,000 steps per action.\n- Invalid nested action indexes.\n\n### Regression Tests\n\nAdded tests for:\n\n- Export/import round-trip preservation.\n- Repeated imports without creating duplicates.\n- Duplicate-name handling.\n- Nested action reference preservation.\n- Invalid JSON and unsupported versions.\n- Duplicate action names.\n- Import rejection during recording.\n- Existing action preservation after failed imports.\n\n## Compatibility\n\nThis uses PhotoCraft's native JSON Actions format.\n\nIt does not add Photoshop `.atn` import support.\n\n## Files Changed\n\n- crates/ui-egui/src/actions.rs\n\n## Validation\n\nCompleted:\n- Four commits pushed to an isolated branch.\n- Source-level implementation review.\n- Regression tests added.\n- Remote branch verification.\n- Duplicate contribution screening.\n\nPending:\n- cargo fmt --all -- --check\n- cargo clippy --workspace --all-targets -- -D warnings\n- cargo test -p photocraft-ui-egui\n- Native file picker verification\n- Browser import/export verification\n- Persistence regression testing\n- Upstream CI\n\nTests have not been executed.\n\n## Issue\n\nPartially addresses #1951.\n\nThe Play-button behavior is handled in a separate PR.",
"state": "OPEN",
"isDraft": false,
"merged": false,
"headRefOid": "202266b90613280e16ae285b53dd4e10d5f0c7ac",
"baseRefName": "main",
"files": {
"nodes": [
{
"path": "crates/automation/src/workspace.rs"
},
{
"path": "crates/engine/src/actions_cmds.rs"
},
{
"path": "crates/engine/src/actions_cmds/json_file.rs"
},
{
"path": "crates/ui-egui/src/actions.rs"
}
],
"pageInfo": {
"hasNextPage": false
}
},
"commits": {
"nodes": [
{
"commit": {
"statusCheckRollup": {
"state": "SUCCESS",
"contexts": {
"nodes": [
{
"name": "select platforms",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834050608"
},
{
"name": "test (without heif)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834050864"
},
{
"name": "test (with craft-fonts)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834050895"
},
{
"name": "corpus tests",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834050927"
},
{
"name": "wasm32 check",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834050914"
},
{
"name": "fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})",
"status": "COMPLETED",
"conclusion": "SKIPPED",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834052054"
},
{
"name": "test (ubuntu-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834087727"
},
{
"name": "test (windows-latest, unit)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834087814"
},
{
"name": "test (windows-latest, integration)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834087859"
},
{
"name": "web build and wasm size gate",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117834087764"
},
{
"name": "test (windows-latest)",
"status": "COMPLETED",
"conclusion": "SUCCESS",
"detailsUrl": "https://github.com/storytold/photocraft/actions/runs/39181176729/job/117839450908"
},
{
"context": "CodeRabbit",
"state": "SUCCESS",
"targetUrl": null
}
],
"pageInfo": {
"hasNextPage": false
}
}
}
}
}
]
},
"approval": {
"state": "APPROVED",
"submittedAt": "2026-10-21T09:51:24Z",
"url": "https://github.com/storytold/photocraft/pull/1991#pullrequestreview-5597895651",
"body": "Read the whole diff. `actions.import` and `actions.export` add a version 1 Actions JSON file, with Load Actions taking `.json` as well as `.atn` and a \"Save Actions… (.json)\" menu entry.\n\n- Import is validated before anything changes: version, at most 1024 actions and 10,000 steps, unique non-empty names within 256 bytes, in-range disabled and dialog indices, and a 16 MiB cap. Nested `actions.play` calls by name or index are retargeted, and a bad index fails the whole import.\n- Name clashes become \"Name (Imported N)\" and the suffix is cut on a char boundary within 256 bytes. Identical actions are reused, so a re-import is a no-op, and the dependency loop is bounded.\n- Both commands refuse to run while recording or playing, and `actions.export` with a `path` is now treated as an ambient path in the workspace authorizer, with a test.\n- No new `tl!` strings: the new menu entry reuses \"Save Actions…\".\n\nCI is green at this head.\n",
"commit": {
"oid": "202266b90613280e16ae285b53dd4e10d5f0c7ac"
},
"author": {
"login": "FredApps"
}
},
"manual_status": "NOT RUN",
"subsystem": "Actions and automation",
"new_test_names": [
"idle",
"validate",
"same_contents",
"same_name_family",
"retarget",
"imported_name",
"json_actions_round_trip_preserves_metadata",
"reimport_after_changing_button_color_keeps_names_unique",
"parents_before_children_are_retargeted_and_reimport_is_idempotent",
"invalid_json_params_and_nested_indices_fail_atomically",
"long_unicode_collision_names_remain_portable_and_idempotent",
"save_json_dialog",
"loading_json_actions_uses_engine_import_and_selects_the_new_action"
],
"regression_source": "https://github.com/storytold/photocraft/pull/1991"
}
],
"exclusions_after_check_inspection": [
{
"number": 7098,
"reason": "Current-head rollup is CodeRabbit only; official corpus distribution and expanded CI remain pending."
},
{
"number": 3226,
"reason": "Held for parent decision: approval asks maintainers to confirm Affinity black-box provenance wording before landing, explicitly described as non-blocking."
}
]
}