crates/ui-egui/src/lib.rs: replay_save doc comment names both Export As and JPEG; replay_save uses the PR's FormatChoices::from_step, after main's export_dialog::replay branch.
docs/file-format-parity.md: header keeps main's text plus the JPEG Options item (last updated 2026-10-19); TIFF row is main's, JPEG row is the PR's; change-log rows once each, newest first.
docs/gaps.md: header keeps main's text plus the G10 item (last updated 2026-10-19).
crates/ui-egui/src/lib.rs: union in the PhotocraftApp fields, the initialiser and set_wgpu (main's window_device/window_lost/window_recovery_retry and set_available, the PR's gpu_render_state/gpu_fault).
docs/control-protocol.md: ui.gpu.simulateLoss line takes the PR's text and keeps main's destroy: true sentences.
crates/paint/src/brush_tests.rs: both sides added tests at the same spot; kept main's four smoothing tests and added the PR's three pose_axes tests after them.
docs/brush-parity.md: kept main's header and added the PR's change item.
docs/gaps.md: kept main's header, added the PR's change item; G4 progress notes and the change-log row kept from both sides.
docs/control-protocol.md: kept main's eyedropper line (with the document.sampleColor wording) and added the PR's Brush Pose line before it.
docs/control-protocol.md: kept main's table rows (document.inspect with snapshots, layer.list) and added the PR's four-floats wording to document.pixel.
openingRaw is {name, path, demosaic}; ui.demosaic is accepted only while a raw
opens; rawDefaults.demosaic is the method the app's raw opens develop with; Open
re-develops when the chosen method differs from the one the open used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts only in the parity docs: status lines take main's text with the
JPEG Options entry (FILE-215-19) on top; scorecard takes main's rows with
FILE-215-19 done and File compatibility 10 / 12 / 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main added DDS saving (#5993): DDS Options, and FormatChoices carrying the
Targa and DDS answers through every save path (this branch's SaveChoices
carried Targa and JPEG). Resolved by keeping main's name and making it
the one struct for all three answers.
- lib.rs: FormatChoices { tga_bits, dds, jpeg } with from_params (strict),
from_step (lenient JPEG read of a recorded step), apply and record;
main's separate block is folded in. Both prompts (dds_options and
jpeg_options) are kept everywhere: save_to asks Targa, then DDS, then
JPEG Options; save_options_open covers all four. save_in_place takes
main's (path, choices) signature and records the plain-Save step with
plain_save_step for JPEG plus the Targa/DDS answers (save_in_place_with
folded into it). journal_as_plain_save relabels a parked Targa, DDS or
JPEG prompt and keeps tgaBits/dds*/quality/jpegFormat when dropping the
path. replay_save reads choices with from_step.
- SAVE_OPTIONS_OPEN names all four prompts; DDS Options uses it too.
- targa_options_ui: main's plain-Save step (no path) through this
branch's write_answered_save. JPEG Options gets main's Enter-on-a-
focused-Cancel fix.
- view_cmds Save a Copy asks Targa, DDS, then JPEG Options; write_copy
takes FormatChoices.
- control.rs: both app.save tests (JPEG and DDS) kept.
- engine file_cmds: SaveOpts keeps jpeg_format and dds; param docs list
both jpegFormat and ddsCompression/ddsMipmaps. CLI: both --jpeg-format
and --dds-compression/--dds-mipmaps. codecs re-exports JpegFormat and
DdsCompression/DdsOptions.
- docs/book: both entries kept (control-protocol app.save lists both).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DDS textures can now be saved, not just opened: uncompressed (BGRA, BGRX, L8, A8L8; lossless) or BC1/BC2/BC3 (DXT1/3/5) from a clean-room block encoder, with an optional box-filtered mip chain, in the legacy header. Save As and Save a Copy ask with DDS Options; scripts pass `ddsCompression`/`ddsMipmaps` (file commands, control `app.save`, RPC `doc.save`, MCP `doc_save`/`doc_export`), and the CLI takes `--dds-compression`/`--dds-mipmaps`. Targa and DDS answers travel together as `FormatChoices` through the Save As flow.
Review follow-ups:
- File › Save behind DDS or Targa Options records a plain Save with its answer, and replaying `file.save` applies it (#3531).
- The block encoder finds the principal axis from every channel, so a channel that varies on its own can't hide the others' variation.
- DDS joins the shared round-trip suite; the header is built before the output buffer is allocated.
- Enter on a focused Cancel in DDS, Targa or TIFF Options cancels instead of saving.
Save As and Save a Copy to a JPEG now ask for JPEG Options before writing,
as Photoshop does. Re-creates #4330 on current main and folds in the
unique parts of #5580 (#5563).
- codecs: `JpegFormat` (Baseline, Optimized, Progressive) on
`EncodeOptions`. Progressive writes SOF2. Baseline Optimized fits the
Huffman tables only at 4:4:4 (Gray, CMYK) or with Progressive: fitted
tables make the baseline scans non-interleaved, which zune-jpeg 0.5.15
decodes wrongly with 4:2:0 chroma (zune-image#419), as Save for Web
already works around.
- engine: a `jpegFormat` save param (file.saveACopy, Batch, Image
Processor, Layers to Files and every SaveOpts user; anything but the
three names is BadParams before writing) and a shared 0–12 -> 1–100
`jpeg_quality()` that maps NaN/inf to 100.
- ui-egui: `jpeg_options_ui` (Quality 0–12 with Low/Medium/High/Maximum
presets, Format Options), parked like Targa Options. It starts at the
opened JPEG's own quality (`file_encoding`, #4032), else the Export
preference; left there, OK writes that quality exactly and the step
records no `quality`. `SaveChoices` (Targa bits + JPEG) replaces the
bare `tga_bits` through file.save / file.saveAs / app.save, replay and
Save a Copy; automation never prompts. `write_answered_save` is shared
with Targa Options. `park_at` + `Start` let any save seed the prompt.
A File › Save that confirms the document's own flat JPEG is still
journalled as a plain `file.save` (#3531).
- CLI: `--jpeg-format`.
- i18n: five new strings in all 20 catalogs (#4330's duplicate de/es/zh
`Low`/`High` rows dropped: main has them since #4008).
- Docs: book raster-formats JPEG bullet, control protocol, file-format
parity, gaps G10, target-app parity; scorecard FILE-215-19 done.
Co-authored-by: SckraT <70698629+SckraT@users.noreply.github.com>
Co-authored-by: PierrunoYT <95778421+PierrunoYT@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612)
* test(linux): cover the XDG state recovery folder (#6612)
* Curves: Cmd/Ctrl-click on the image adds a point at the pixel's tone (part of #7325)
With Image > Adjustments > Curves open, a Cmd-click (Ctrl-click off the Mac) on
the image adds a point to the channel shown in the dialog, at the pixel's tone
and at the curve's current output there; Shift adds one point on each of Red,
Green and Blue. Only the press adds, an armed eyedropper still wins, and the
control channel's ui.pointer does the same with command: true.
* Curves: Cmd/Ctrl-click adds the point with the Hand tool too; Space and middle button still pan
The Hand tool took the Cmd-click press for a pan, so no point was added; the
press now counts before the Hand. On the control channel, ui.pointer with
space or the middle button pans as the mouse does instead of adding a point.
* Curves: the Cmd/Ctrl-click point's comment says where the curve can still change
* Type: right-click on committed text opens the type layer menu (#7324)
With the Type tool, a right-click only opened a menu while text was being
edited; over committed type nothing opened. The menu was keyed on the edit
session alone (`editing_text`).
Over committed type, a Type tool (not a Type Mask tool) now opens
TYPE_LAYER_MENU: the type edit menu's layer rows, without the edit rows
(Undo, Cut, Copy, Paste, Select All), which would act on the document, and
without Faux Bold/Italic, which act on the characters being edited. Type
outside the selected layers becomes the target first, as a click with the
Type tool makes it. The control channel's right button takes the same path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Type layer menu: the clicked type becomes the active layer in a multiselection (#7324)
The structural rows (Rasterize Type, Convert to Shape, Create Work Path, the
paragraph/point conversions) act on the active layer. When the right-clicked
type was already selected but another selected type layer was active, they
acted on that other layer. Make the clicked type the active layer while
keeping the selection (layer_multi_cmds::set_selection), so the formatting
rows still act on every selected type layer. Found by CodeRabbit on #7430.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
After an error on a device that still works (out of memory, validation), Retry GPU builds a new GPU canvas on the same render state with a fresh health flag, and the views go back to the GPU. After a loss or a failed GPU start it still schedules the retry for the next launch. The dialog says which. Two new strings, translated in all 20 catalogs.
When the window's own wgpu device was lost (a driver reset, e.g. after sleep), egui-wgpu could
not draw another frame: a frame with shapes panicked inside it ("Failed to create staging buffer
for index data", #1215, exit 101, unsaved work lost), and a frozen window's close button opened
an unsaved-changes prompt nobody could see (#3438). The #243 fallback only covered the canvas.
- DeviceHealth::device_lost (wgpu reported the loss, not a recorded fault) and ::probe (a
non-blocking poll that keeps running after a recorded fault).
- RecoveryStore::flush: finish queued autosaves and wait for them.
- gpu_status::check_window, at the end of every frame: once the window's device is lost the
frame draws nothing; the first time every unsaved document is written to crash recovery
(Services::flush_autosaves) and the app closes without asking. The next launch reopens them.
- The app watches the window device on the CPU canvas too; ui.gpu.simulateLoss {destroy: true}
destroys the real device to drive the path.
Verified on an RX 6900 XT, Vulkan and DX12: destroying the device in the running app exited 101
with the egui-wgpu panic before, and now exits 0 with the document recovered on the next launch.
The save dialog asks before replacing only the file it answers with.
After it closes, pick_save can change that path: it gives a name
without an extension the suggested file's extension (#5039), and
Lowercase Extension changes CAT.PSD to CAT.psd. The save then wrote to
the changed path at once, and a file there was replaced without a
question. On Linux this happens when the Swatches panel exports to a
folder that holds Swatches.aco (the one-type portal dialog shows and
asks about "Swatches"), when CAT.PSD is typed where CAT.psd exists, and
in rfd's zenity fallback dialog.
When the path written differs from the dialog's answer and a file
exists there, the save now asks "“{name}” already exists. Replace it?".
Replace writes the file, and ui.dialog.confirm returns the save's
result. Cancel writes nothing and ends the save like the dialog's
Cancel, and Save in the close prompt of an untitled document returns
to that prompt. PhotoCraft counts the file dialog as open until the
question is answered.
A name that reaches the same file, as CAT.PSD and CAT.psd do on
Windows and macOS by default, is not asked about again. The new
same_file service decides this: the desktop app compares device and
inode on Unix and the canonical paths on Windows. The web has no file
stamps and does not ask. The two new strings have rows in all 20
catalogs.
* fix(windows): keep crash-recovery autosaves out of the roaming profile (#5648)
* test(windows): cover the local recovery folder and moving old autosaves (#5648)
Retain both progress histories and integrate the existing Brush Settings changes with upstream. Preserve the response-curve editor, options-label scrubbing and independent preset regression tests where they overlap.
Co-authored-by: GPT-6 <noreply@openai.com>
The rubylith (`\`, ⇧⌥-click on a mask thumbnail) was always red at 50 %. Its colour and
opacity are now Photoshop's Layer Mask Display Options: one setting for every mask, kept
across restarts and not an undo step, as Photoshop does it. They live in the preferences as
`layerMaskOverlay` (`#rrggbb`, 0..100 %, red at 50 by default) and are set through
`channel.options {"channel":"mask","color","opacity"}`, which with no fields reads them back
(prefs.set and prefs.reset work too).
In the UI, as in Photoshop: double-click the layer mask's row in the Channels panel or pick
Layer Mask Display Options… from its menu, Mask Options… on the Layers panel mask thumbnail,
or Channel Options… with the mask targeted (`\` counts). Each opens the Channel Options dialog
with just Color and Opacity.
The rubylith is still drawn over the picture as it looks, mask applied, as Photoshop draws
it. The channel view's texture key carries the options, so changing them redraws it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shortcuts without the UI: shell commands and tools in the engine, reserved keys, set files
A retro against "everything is drivable headless; the UI is a layer" found the shortcut
work leaning on the UI. Fixed:
- The shell's command table (UI_COMMANDS: View › Zoom In, the panels…) moves into the
engine as data (shell_commands::SHELL_COMMANDS; menus::UI_COMMANDS is now an alias), with
the tools (shell_commands::TOOLS: id, Photoshop name, default key; a test keeps it equal
to the toolbar). edit.keyboardShortcuts binds, lists, imports, exports and checks them for
conflicts with no UI: a headless .kys import takes tool keys, and an export is the whole set.
Tools may share a key (Shift cycling); a command given a tool's key takes it from them.
- Reserved system keys are refused by the engine (the dialog uses the same check) and left
out of imported sets.
- importKys / exportKys / summarize take {"path": …} to read or write files, like
actions.save; the automation gate refuses those paths for MCP.
* Keyboard shortcuts: bindings commit only after their exports are written (a bad target changes nothing); docs: the command knows the tools
* Keyboard shortcuts: both export targets are resolved and checked before either file is written; a failed later write removes the earlier one
* docs: reserved keys per platform
* Keyboard shortcuts export: files are staged next to their targets and moved into place only when all are written; a failure leaves the user's files untouched
* Keyboard shortcut exports all or nothing: unique create_new staging and backups beside each target, restored on any failure; the same target twice refused; docs: Ctrl+Shift+Esc
* Shortcut exports: a failed backup move removes its placeholder; undo restores backups without deleting first and names any it couldn't; read-only targets refused up front
* PHOTOCRAFT_PRESENT_MODE / PHOTOCRAFT_FRAME_LATENCY launch overrides for latency A/B tests (#1343)
Linux traces put the remaining stroke latency in presentation queueing (the app's frame fits
in one refresh; the rest waits on the swap chain), and an NVIDIA user cured it by forcing
vsync off. Testers can now compare without rebuilding: PHOTOCRAFT_PRESENT_MODE (fifo,
mailbox, immediate, autovsync, autonovsync) and PHOTOCRAFT_FRAME_LATENCY (1-3), applied over
the platform default. Bad values are logged and ignored. The active mode is logged at start,
kept in GpuInfo.present and shown in every stroke trace line.
RTX 5090, Vulkan, 120 Hz: default autovsync/latency 1 waits 6 ms per 8.3 ms frame; mailbox
renders every 2.3 ms with 0.2 ms wait, so each vsync shows the newest frame.
* docs: present mode and frame latency overrides
* Present mode overrides never request a mode the driver may lack (Mailbox crashed on X11/NVIDIA)
* Preferences › Performance › Low-Latency Drawing, on by default: no vsync wait while drawing (#1343)
Testers' traces put the ~50 ms stroke lag in the swap chain's vsync wait. With vsync off it
disappears (X11/NVIDIA; Windows/NVIDIA via the driver), and a shorter queue doesn't help. The
surface now presents with AutoNoVsync by default: wgpu picks Immediate, else Mailbox, else
Fifo by what the driver offers, so no unsupported mode is ever requested. Turning the
preference off brings vsync back (smooth, but late). It applies at the next launch, like the
other graphics settings. Where only Immediate exists the canvas may tear while it changes;
frames render as fast as input arrives during a stroke and stop when idle.
* Runtime and launch controls: ui.set strokeTrace, --stroke-trace, --present-mode, --frame-latency
The retro's remaining gap: the stroke trace and the presentation overrides were env-only. The trace now switches at runtime over the control channel (ui.set {"strokeTrace": true|false|"stderr"|path}) and with --stroke-trace; --present-mode / --frame-latency sit beside their env vars and win over them. Bad values are reported and ignored; none fails a launch.
* docs: strokeTrace beside the other ui.set fields
* docs: regenerate the scorecard after rebasing onto main
* Low-Latency Drawing: frames capped at 240 fps without vsync (after #4472); ui.set strokeTrace takes on/off only (a CSV path is launch-only); tearing documented as usual (Immediate); pref hidden on web; doc comment order
* stroke_trace: from_value doc says ui.set passes only on or off
* Low-Latency Drawing ships off (opt-in), per the owner's decision
* docs: present mode default is vsync (Low-Latency Drawing ships off)
* Raw: measure the Nikon D90 image area (4288x2848 at (10, 10))
Aligning the decoded D90 sensor plane with the camera's own JPEG puts the
image at (10, 10) in the 4352x2868 sensor plane, giving the camera's
4288x2848 frame (10-pixel margins; the right border reads the camera's flat
255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the
camera's frame instead of the uncropped plane, and make a measured profile
leave a file that carries its own colour calibration (a DNG) untouched.
* Codecs: open Cineon (.cin) film frames read-only
Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from
the same Kodak header family: the generic header (big-endian, orientation at
192, channel count at 193, the channel-1 bits/dimensions), one or three
channels, three 10-bit samples packed into each 32-bit word on a word
boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the
file's log black/white points are a display choice and are not applied. A
depth other than 10 bits or a channel count other than 1/3 is Unsupported;
truncation errors rather than panicking. Read-only (no Cineon writer), so
.cin joins the open filters and Cineon read closes part of #1049's G10 gap.
* PSD: write tiff blocks without copies, inflate merged ZIP once
- ImageSourceData::to_bytes writes the global mask and global blocks
straight from the model with 4-byte alignment instead of cloning each
block (data included) to set its padding. Same bytes.
- The tiff transcoder's nested regions and blocks write into the parent's
buffer (taken and handed back, error or not) instead of building their
own output and copying it up per nesting level. Bodies that fail or are
not transcoded are truncated before being copied as stored or dropped.
Same bytes.
- PsdFile::from_bytes_and_merged decodes the merged image with the parse
when asked: a ZIP stream is inflated once (the decode's stream check is
the parse's); the parse-only check runs only when the decode fails, to
keep from_bytes' errors. PSD import uses it with merged_needed.
- PsdBuilder::composite documents that Gray/CMYK composites stay straight.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2)
Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the
bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults),
and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and
Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys
file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id
so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes
Photoshop's HTML list of every bound command by menu.
edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and
activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use
and whether it is modified. Bad names, keys or counts are errors that change nothing.
* .kys import: tool keys and sets saved on Windows (K3)
Photoshop's <tool> rows now import: each matches our tool by name (our tool names are
Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own
becomes its override, while its own letter keeps the tool cycling with the others (#5036).
A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps
Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a
Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that
would also mean Control on a Mac. The import reports `windows`.
* cargo fmt the extension lists
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
* Arrow-key nudges: a run of nudges is one History state, with Tools preferences (#2259)
Tapping or holding an arrow key nudged the selected layers one pixel per
History state, so one Undo took back one pixel. Nudges of the same thing
that follow each other now share one `coalesce` key (the engine already
folds edits with the same key into one step), so a run is one state and
one Undo takes it back.
- nudge_bundle.rs decides which run a nudge belongs to: the previous nudge
was of the same thing (layers, selection outline), came within the
pause, and left the document as it is now (no other edit, Undo/Redo,
layer selection or save in between).
- move_mods.rs runs layer.translate and select.transformSelection through
it; Alt-nudges (a copy each) never join a run. Selected pixels float
until dropped, which is one state already.
- Free Transform keeps its own session history; its box nudges fold into
one session step (transform_tool.rs).
- Tools preferences: Bundle Arrow-Key Nudges (default on; off gives a
state per press as before) and Nudge Bundle Pause (ms) (default 1000,
100..10000, hidden while bundling is off). Labels translated in all
catalogues.
The two #1428 tests that asserted a state per press for the selection
outline now cover both settings.
* Regenerate docs/scorecard.md after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
As Photoshop's menus on Windows: a tap on Alt (or F10) puts the keyboard
on the in-window menu bar, with File lit; ← and → move along it, ↓, ↩
and Space open the lit menu with its first row lit, Esc or a click
leave. Alt+letter opens the menu with that mnemonic, open or not, and
in an open menu a letter runs the row with that mnemonic or opens its
submenu.
Mnemonics are assigned per menu level from the labels as shown (so
translated), unique within the level: Photoshop's F E I L Y S T V W H
for the top menus where the label has the letter, else the first unused
letter, word starts first; only ASCII letters and digits, since those
are the keys the keyboard reports. They are underlined while Alt is
held, the bar is focused or the menu was opened from the keyboard; a
menu opened with the mouse shows none until Alt, as on Windows. The
underline is a layout job with the placeholder colour, so disabled rows
stay dim; without underlines the text path is unchanged.
The shortcuts defer to the focused bar as they do to an open menu, so E
opens Edit instead of picking the Eraser and the arrows don't nudge the
layer. Rows carry their mnemonic in the navigation state. macOS keeps
the native menu bar, where this doesn't run.
Tests: two unit tests (assignment, key letters) and seven harness tests
(Alt focus and arrows, second Alt / Esc / click, Alt+letter and Enter,
letters in an open menu, underline conditions, the layout job, and the
whole app's shortcut deferral).
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Crop tool: Content-Aware fill of the area a rotated or extended crop uncovers (#3823)
image.crop gains contentAware (bool, default false). After a turned crop or one that extends
past the canvas, the area the original image doesn't cover is filled from its surroundings on
the Background (else the active pixel layer), in the crop's one undo step. Each connected piece
(corner, edge strip) gets its own Content-Aware sampling window and samples only covered pixels;
the patch search is Edit > Fill > Content-Aware's (fill_cmds::content_aware_native, shared). A
crop that uncovers nothing is unchanged; one with nothing to sample from fails and rolls back.
Crop options bar: Fill: Background (Default) / Content-Aware Fill (ToolOptions
crop_content_aware, ui.set cropFill). Generative Expand is not offered.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Crop: adapt to Document::background after main removed is_background
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Crop: translate Background (Default) for Hungarian and Turkish
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fix the merge with main: UI_SET_FIELDS length, Serbian Background (Default)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Merge main: UI_SET_FIELDS counts both new fields
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The filter row's "Kind" was painted text. It is now a dropdown, as in
Photoshop: Kind keeps the five layer-kind toggles, Name lists layers whose
name contains the typed text (ignoring case), and Mode lists layers with
one blend mode (Pass Through included), filtering by Normal at first.
The criteria live in UiState (layer_filter_by, layer_filter_name,
layer_filter_mode; serde defaults, so saved settings still load) and are
drivable with ui.set {layerFilter: {by, kinds, name, mode}}, validated
before anything changes. ui-parity.md gets a partial row: Effect,
Attribute, Color, Smart Object, Selected and Artboard are still missing.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Record action layer selections by name and show their targets
* Use document pixel format in grouped-layer regression
* Document the named layer action UI with a synthetic capture
The layers were only in document.inspect, together with history,
channels and much more, so agents looked for a layer.* command that
didn't exist.
layer.list returns the layers top-down with id, name, kind, visibility,
bounds, selected and active; groups list their children, or with
"flat": true every layer is one row with its depth and parent group.
It takes the same optional "document" index as document.inspect, is not
journaled and bad params are errors.
* feat(commands): find the Elliptical Marquee by 'ellipse' or 'marquee' (#5060)
The Elliptical and Single Row/Column Marquee are options of select.rect
("ellipse":true, "line"), but the command-list filters only looked at
the id and the label ("Rectangular Selection"), so 'ellipse' and
'marquee' found nothing.
Commands can now carry search keywords. command.list / engine.commands
rows include them as "keywords", and the three filters (CLI commands
--filter, the engine.commands RPC and the MCP command_list) share one
matcher over id, label and keywords. select.rect gets the Photoshop
marquee names as keywords.
* Command list replies: document the optional keywords key
The reply-shape test from #3684 pins the exact keys of each row; select.rect
now also carries keywords, so the test and docs/control-protocol.md allow
that one optional string.
Keep X/Y flags through ABR import, presets, commands and painting; retain legacy combined inputs and expose both controls with regression coverage.
Co-authored-by: GPT-6 <noreply@openai.com>
* Docs: document the engine.commands reply shape (#3684)
engine.commands, the engine command command.list and the MCP tool
command_list reply with a bare JSON array of command objects, which the
docs never said. Document the shape (keys, types, an example element,
how filter/enabled_only narrow it) in control-protocol.md and mcp.md,
state it in the MCP tool description, and pin it with an rpc test so
the docs cannot drift. The shape itself is unchanged, so existing
agents and scripts keep working.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Command list docs/test: include main's disabledReason and needsDocument keys
Main added `disabledReason` and `needsDocument` to every command list row
(#3664), so the documented shape and the pinning test now list all eight keys.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a document with unsaved changes was closed, reverted, or the app was
asked to quit, the Save / Don't Save / Cancel prompt showed in the window
but not in ui.inspect, and a control client had no way to answer it: the
app stayed open until someone clicked.
ui.inspect now lists the prompt among its dialogs with the id "discard":
the waiting command, the document it asks about, how many are left and
the choices. ui.dialog.confirm {"dialog": "discard", "choice":
"save"|"dontSave"|"cancel"} answers it through the same code as the
buttons (one document at a time when quitting), and ui.dialog.cancel
cancels it. The automation policy sees the parked command before a save
or don't-save answer runs it.
Fixes#4920
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Draw the active layer's storage tiles over the canvas: a faint
TILE_SIZE grid over the document, every stored tile filled and
labelled tx,ty, tiles with identical pixels in one colour per group
and unique tiles in blue. It shows how sparse a layer is, which tiles
copy-on-write shares, and which ones .pcraft saves once.
It has no menu item and isn't a View > Show item. The control channel
and MCP turn it on with ui.set {"tileGrid": true}; ui.inspect reports
it as view.tile_grid, and View > Extras doesn't hide it.
Grouping hashes each distinct tile allocation once, in parallel (or
serially while jobs hold the pool, #1554), cached per document
revision and active layer. A 36 MP layer of all-different tiles groups
in 3.9 ms (8-bit) and 7.5 ms (16-bit) on 24 cores in release; the
ignored tile_grouping_36mp test reproduces this.
Also documents tileGrid in docs/control-protocol.md and
docs/development.md, and adds my contributors/people.toml entry.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Control: a timeout says whether the command ran (#3662)
A request still queued when its deadline passes is dropped before
dispatch, but it answered with the same text as the transport giving up
on a command that may still apply. The two now read differently: "timeout:
not run ... safe to re-send" and "timeout: no reply in 60s ... may still
apply", both keeping the `timeout` prefix machines match on.
* Control: a transport that gives up knows whether the command was dispatched (#3662)
The transport and the UI thread share the reply deadline, and the
transport's wait runs out first, so the UI thread's "not run" reply
almost never reached a client. A request now carries a dispatch flag
settled once between them: the UI thread claims it before running, the
transport abandons it when it stops waiting. The transport then replies
"not run" for a request that will never run, and "may still apply"
only for one that was dispatched; a request given up on is never run.
* feat: resize Save for Web previews and added palette editing for Gif/PNG8
* Save for Web: add Turkish and Hungarian rows for the preview navigation hint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- image.imageSize takes percent (both sides; an explicit side wins).
- actions.conditional {if, then, else}: Photoshop's Insert Conditional as a step, 33 conditions
on the document (shape, mode, depth, selection, layers, alpha channels, unsaved) and the
active layer (kind, locks, visibility, masks, effects); a failing branch stops the caller.
- .atn events: placeEvent → file.placeEmbedded/placeLinked (path, scale), open → file.openAs,
imageSize in percent, fill from a point → paint.bucket (pixels), conditional. 105 → 108.
- Panel labels conditional steps as 'If cond → then / else'.