paint.remove takes several strokes at once (`strokes`) and fills them as one job and one undo
step, so the fill sees every hole together; and `newLayer` writes the fill to a new layer above
the target, only where the hole is, leaving the target as it was (with Sample All Layers it
fills from the composite). The options bar shows Photoshop's three options; with Remove after
each stroke off, strokes collect as a tint on the canvas until Enter or the ✓ applies them and
Esc or ⊘ drops them. Strokes are dropped when the tool or document changes under them.
* engine: expose workspace.enableNarrowOptionsBar through a Workspace helper
* ui: the options bar honors the narrow options bar preference
* docs: scorecard, settings that do nothing 30 -> 29 of 154
* text: missing glyph protection can turn the script fallback stack off (#1909)
* engine: apply type.missingGlyphProtection to the live text engine
* docs: scorecard, settings that do nothing 30 -> 29 of 154
* feat(engine): Ctrl+, toggles a layer's visibility and / its transparency lock
Photoshop's Ctrl+, (Cmd+, on macOS) hides the selected layers and, once
they are all hidden, shows them again: its Layer menu item then reads
Show Layers. layer.hideLayers now does the same, so the key toggles: it
shows them only when every targeted layer is hidden.
The / key toggles Lock Transparent Pixels, as the Layers panel's first
lock button does (layer.toggleLockTransparency). With several layers
selected, any unlocked one means lock them all; the Background, which
can't take this lock, is left out. Each press is one undoable step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(engine): Ctrl+\ targets the layer mask and Ctrl+Alt+2 loads luminosity
Photoshop's Ctrl+\ (Cmd+\) targets the active layer's mask, as clicking
its thumbnail does, beside Ctrl+2 for the composite
(channel.target.layerMask; greyed when the layer has no mask). Its
Ctrl+Alt+2 (Cmd+Option+2) loads the image's luminosity as a selection,
the Ctrl-click on the composite channel (select.loadLuminosity).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(engine): Ctrl+Alt+Backspace fills from history
Photoshop's Ctrl+Alt+Backspace (Cmd+Option+Delete) fills the selection, or
the layer, from the History Brush's source state. edit.fillHistory is
edit.fill with contents "history": the oldest state still held, unless
"state" names another. It joins the other fill keys, so it honours the
Channels panel target and appears with them in Keyboard Shortcuts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(ui): Tab renames the next layer; Alt-click a thumbnail fits the layer
While a layer name is being edited, Tab commits it and starts renaming the
layer below, and Shift+Tab the layer above, as in Photoshop; the
Background, which isn't renamed in place, ends it. Alt-clicking a layer's
thumbnail selects the layer and fits its content on screen (View › Fit
Layer(s) on Screen).
Key tests cover Ctrl+,, /, Ctrl+\, Ctrl+Alt+2 and Ctrl+Alt+Backspace
through the shortcut dispatcher. docs/ui-parity.md lists the new keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): an explicit channel target drives the Layers panel's mask target
After Ctrl+\ targeted the layer mask, Ctrl+2 moved the Channels target back
to the composite, but the Layers panel kept the mask targeted (its own
flag, which only ever followed the engine towards the mask), so painting
still went to the mask. The same happened after clicking the mask
thumbnail. Any channel.target command (Ctrl+2, Ctrl+\, Ctrl+3…) now sets
the panel's mask and vector-mask targets from the engine's target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use Document::is_background after #4036 removed extra_cmds::is_background
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
engine.commands, command.list and the MCP command_list tool now give each
row a disabledReason (null when enabled, else the failed precondition) and
needsDocument (true when the command is off because no document is open),
so an agent can tell "open a document first" from "not runnable". enabled
keeps its meaning. Both listings share one row builder in the engine.
Fixes#3664
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A second `auth` frame on an authenticated TCP connection fell through to
the method dispatcher and came back as an unknown method, in both the
headless serve server and the desktop control server. A shared
`security::repeat_auth_reply` now answers it with `already authenticated`
(keeping the request id) and the connection keeps serving.
Fixes#3668
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Opening a JPEG now estimates the quality it was saved at from its quantisation
tables (the Annex K tables of ITU-T T.81 scaled by the IJG quality curve; exact
for such tables, the closest fit otherwise). Opening a WebP records whether it
is lossless (VP8L) or lossy (VP8), and a lossy one's quality from its base
quantiser index. The settings live on the document (`Document::file_encoding`,
saved in .pcraft) and a save that asks for no quality writes the file back the
same way, as Photoshop's Save does: File › Save, Save As and Save a Copy start
there instead of the fixed codec defaults (quality 90, WebP always lossless).
An explicit quality still wins; new documents and other formats keep the
defaults. Hostile or truncated DQT/VP8 headers give no estimate, never a panic.
Refs #2399
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
As in Photoshop, holding Shift while dragging a new guide out of a ruler
snaps it to the tick marks the ruler draws (tenths of the labelled step),
in the current ruler unit and zoom. Without Shift the guide snaps to the
document, layers, selection and grid as before.
The tick spacing draw_rulers computed inline moves to ruler_scale, shared
by the renderer and the new snap_to_tick, so the snap always matches the
ticks on screen.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The Character panel (and the Properties Character section) gains
Photoshop's row of eight OpenType buttons: Standard Ligatures, Contextual
Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling
Alternates, Ordinals and Fractions. Each shows pressed while its feature is
on at the selection (else the layer) and toggles it through the existing
type.openType.* command, on the same scope as the other Character controls
(the selected characters while typing, else every selected type layer).
Scorecard TYPE-218-5 stays partial: the Numerals and Stylistic Sets
submenus are still missing.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Adds the 1,287 rows the other catalogs have and Italian lacked (filter and adjustment parameters and choices, Camera Raw, Lighting Effects, History and Layers labels, metadata, dialogs), fixes mistranslations in existing rows (Facet, Gradient stops, Save As Preset, Canvas, Spot Color) and uses one term per concept (Predefinito, Traccia, clic, scorciatoia, filmato).
Factors the Korean generated-label check into missing_generated_filter_labels and runs it for Italian too. Adds docs/localization-it.md and updates localization-parity.
A translated label beside a fixed-width control could run over its neighbours: the Layers
Lock/Fill row guarded only the redundant "Lock:" caption, so "Fill:" ("Заливка:" in Russian,
longer still as pt-br "Preenchimento:" / fr "Remplissage :") overlapped the lock icons.
Rather than a per-row fit check, add the one shared primitive for fixed chrome:
`widgets::elided_label(ui, text, max_w)` lays the text out no wider than its room, cuts it with
'…' and puts the full string on hover (built on the existing `tab_strip::elided`). The
Opacity/Fill scrub labels now elide to the room beside their field, so any translation shrinks
instead of overlapping, in every locale.
Documented the rule in docs/ui-design.md: wrap by default; beside a fixed control, elide; a
redundant caption may drop on a narrow dock (the lock icons carry the meaning).
Test: widgets::tests::elided_label_never_exceeds_its_room. Full photocraft-ui-egui suite: 2077
pass; fmt and clippy clean.
Adds the Vietnamese (vi) UI translation and turns on its coverage gate. A maintainer pass merged main, added 1,029 rows from @tonguyenducmanh's catalog (#3453) and 33 new ones, and set complete_menus.
Co-authored-by: tonguyenducmanh <63176793+tonguyenducmanh@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(codecs): decode every frame of an animated GIF (#572)
decode_gif_frames returns each frame composited onto the logical screen
(disposal methods applied) with its delay in milliseconds. decode still
keeps the first frame only. A damaged later frame keeps the frames before
it and marks the first with DecodeWarning::Truncated; dimensions, the
total decoded bytes and a frame cap are checked against the limits.
* feat(io): import_gif_frames hands each GIF frame over as a document (#572)
A frame at a time becomes a single-layer document, so a caller can
flatten and drop it instead of holding every frame twice.
* feat(engine): animated GIFs load every frame into video layers (#572)
New Video Layer from File, Replace Footage, Reload Frame and Video Frames
to Layers take all frames of a GIF instead of the first. The timeline rate
comes from the GIF's delays (the most common one, delays under 20 ms shown
at 100 ms as browsers do); an explicit fps still wins.
* docs: animated GIF frames in video layers (#572)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Tools: Shift key switches tools by default, as in Photoshop (#3525)
Use Shift Key for Tool Switch is on by default in Photoshop: T keeps the
current tool, Shift+T cycles. Saved settings keep their stored value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: the M-key marquee cycling test sends Shift+M, the new default (#3296)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Part of #2966. The History panel's camera button takes a named snapshot
of the document ("Snapshot N", not a history state). Snapshots are
listed under the opening state with a thumbnail and scroll with the
states; clicking one returns the document to it as one new, undoable
state named after it. A double-click or right-click › Rename… renames
it in place, and right-click › Delete removes it. Snapshots live for the
session only, as in Photoshop.
For agents: history.newSnapshot {name?, from?: fullDocument |
mergedLayers | currentLayer}, history.selectSnapshot, renameSnapshot and
deleteSnapshot, addressed by ids that are never reused, and doc.inspect
reports snapshots.
* Content-Aware tools: faster, cancellable at once, same pixels
#2202 matched Photoshop's Content-Aware Fill but made the perf nightly slower (#2575): P21
Content-Aware Fill 10 MP 5x, P22 Spot Healing 2.4x, P49 cancel on 24 MP 4x and over its 200 ms
budget. This keeps every result bit for bit and takes the time back:
- The solver works over the hole's bounds grown by a patch: match field, costs, votes and the
per-target tables are kept there (nothing outside changes or is matched), the vote computes
each target's weight once and writes into a buffer instead of a Vec per pixel.
- Colour adaptation reuses each patch's own moments (target sums kept per EM step, source sums
per level where they pay off), summed in the same order as before.
- Exact early outs: SSD per pixel instead of per row (callers only ask "better?"), and the onion
peel's exhaustive search starts from where the filled neighbours took their sources and stops a
source once it can only lose (first-of-equals kept).
- PatchMatch sweeps a band in wavefronts of blocks when there are fewer bands than threads: each
cell sees the same neighbours and random numbers (SplitMix64 skip) as in raster order.
- Parallel pyramid, valid-source and target setup; no copy of the window when there are no
variants (fill_owned / complete_masked_owned); the job reads the window in parallel bands and
keeps only the selection's bounds for the apply step.
- Cancellation is checked per row everywhere, also in Auto sampling and in its max flow
(maxflow_with / grid_cut_cancellable): a 24 MP job now stops in tens of milliseconds.
- algo::par: rayon on native, sequential on wasm.
Proof: the new `content_aware_bench` example hashes every tool's output (Fill with each sampling
and adaptation option, Edit > Fill, Delete and Fill, Spot Healing x3, Patch, Content-Aware Move,
8/16/32-bit, Gray/CMYK/Lab, and the benchmarks' 10 and 24 MP fills): 117 hashes identical to
main, also with 1, 2 and 4 threads. New tests compare the onion peel with a plain scan, full and
clipped patch distances and moments, the valid-source runs with the summed-area test, thread
counts 1/3/16, in-place and copied fills, and cancellation of Auto sampling and max flow.
perf_scenarios --cpu, Ryzen 7 5800X shared with other work (load 4-7), 3 interleaved rounds:
P21 p50 295-306 -> 167-171 ms (max 342-362 -> 180-184); P22 p50 67-68 -> 38-40 ms (max 69-71 ->
40-41); P49 max 378-389 -> 54-77 ms (p50 94-110 -> 47). retouch_bench: Content-Aware Move
-47..-64 %, Spot Healing Content-Aware -40..-61 %.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Content-Aware: faster Spot Healing dabs than main with #2805, same pixels
Against main with #2805 the branch was slower for small Spot Healing dabs (size 50:
+25-30 %, single-threaded +60 %): its onion peel walked all 49 offsets of every candidate patch,
checking each against the known pixels, and had no bound at the hole's edge.
- Onion peel: the target's known pixels are gathered once (#2805), the search starts from the
filled neighbours' shifted sources or every 16th source at the hole's edge (#2805), the limit
a candidate must stay under is a precomputed sum (no division per pixel), and candidates are
summed four at a time in step: their sums are independent, so the processor overlaps them.
Each is still given up only against the best from before the four, which can only improve.
- Vote: each target's weight and match centre are taken once; a pixel's patches are visited
over the clipped offset range, with the channel count and the colour adaptation fixed at
compile time. Same sums, same order.
- Valid sources: sliding counts per row and stacked runs per column instead of a 7-row check
per pixel; the k-th source is found through per-row and per-64-column counts, so no level
but the coarsest lists them all.
- `integral` stays out of line: inlined into `synthesize` it made Create Texture slower.
Same results: `content_aware_bench -- hash` (112 cases) and `-- hash-big` (the benchmarks' 10 and
24 MP fills) equal main's, also with 1 and 3 threads; the onion peel tests (#2805's and ours)
compare with the plain scan. `content_aware_bench bench spot` runs retouch_bench's Spot Healing
dabs; with `CAF_SPOT_TYPE` (comma-separated) other types, with page faults per dab.
Ryzen 7 5800X, shared (load 5-20), interleaved with main (3af4c46a), min of rounds,
retouch_bench, all threads / one thread:
Spot Healing Content-Aware 50: 49 -> 31 / 204 -> 117 ms; 100: 62 -> 35 / 164 -> 98 ms;
200: 118 -> 67 / 250 -> 158 ms; 300: 212 -> 119 / 411 -> 285 ms. Content-Aware Move -47..-62 %
(one thread -23..-45 %). Rows that don't run this code stay within noise, except Proximity
Match 200 after the Content-Aware dabs in the same process (+8..23 %): it takes ~5800 page
faults there against ~300 on main, because main's heavier dabs left more of the heap faulted
in; alone, or after Content-Aware dabs in `bench spot`, it is equal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Brush and Pencil: hold ~ to erase
Fixes#3439. Holding ~ (the backtick key) with the Brush or Pencil makes
the strokes started while it is held erase, with that tool's own brush,
as in Photoshop; releasing it paints again and the tool never changes.
The key is a Tools › Temporary binding (tools.temporary.erase), so it
can be rebound or removed in Keyboard Shortcuts.
* Brush and Pencil: Clear painting mode, used by ~
Photoshop's Clear joins the Brush's and Pencil's Mode menus, after
Dissolve: chosen there, every stroke erases with that tool's brush until
another mode is picked. Holding ~ now puts the stroke in Clear mode too,
and the Mode menu shows Clear while the key is held. History names the
tool rather than the Eraser. Agents get it as "mode": "clear" on
paint.stroke and paint.pencil.
* Keep the Smoothing doc comment on smoothing_field
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>
Load Work Path, saved path and layer path thumbnails as selections on
Ctrl/Cmd-click. Support Shift to add, Alt to subtract and Shift+Alt to
intersect, without changing the selected path or treating the click as
Save Path. Clicking the row name keeps its existing selection behavior.
Render uncached shape transparency instead of selecting the entire canvas.
Add regression tests for thumbnail hit testing, active-layer preservation,
selection combinations, undo/redo and 8/16/32-bit uncached shapes.
Co-authored-by: kotulsky <7805437+kotulsky@users.noreply.github.com>
File › New › Recent was a hard-coded pair of presets plus Clipboard. Each
document created with the dialog now records its settings (size,
resolution, mode, depth, background, display units) through the new
document.recent.add command: newest first, equal entries collapsed, at most
20, persisted with the presets (a damaged entry is dropped on load, the rest
kept). The Recent tab lists Clipboard, those documents, then the defaults
they don't cover; a click restores every setting, a double-click creates,
and the grid scrolls past three rows. document.recent.list exposes the list
to agents; creation stays in file.new. Closes scorecard row UI-217-12.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Agents read the tool description, not docs/mcp.md, and the command_batch
description only said it stops at the first error. It now also says the
steps run in order and that earlier successful steps stay applied: there
is no rollback. docs/mcp.md gives the stop_on_error default and a recovery
recipe: inspect with doc_inspect, undo with edit.undo once per history
entry (undo does not reverse files written to disk), or experiment on a
copy made with image.duplicate or history.newDocument. A test checks the
listed description.
Claude-Session: https://claude.ai/code/session_01BSB7fMQUccqFQekksDGWpo
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Filter > Vanishing Point now opens a dialog mode on the canvas instead of
the generic parameter dialog:
- Create Plane (C): four clicks, in any order, make a plane (corners are
normalised clockwise from the top-left, the order paste expects).
- Edit Plane (V): drag corner nodes, or edge nodes to slide an edge along
the plane's own perspective; click a plane to select it, Delete removes it.
- The plane grid is drawn at the Grid Size, in the accent colour when the
plane is valid, warning yellow when its perspective is doubtful and red
(outline only) when the quad isn't convex (VpPlane::check).
- With an image on the clipboard the selected plane previews it pasted in
perspective; drag to move it, Width sets its size, New Layer renders it
on a new layer. OK / Enter runs filter.vanishingPoint with the planes and
the paste as one history step; the control channel drives the same
session through {"ui": {...}}.
The engine now rejects a drawn plane whose corners don't form a convex
quad (bow-tie, dented) instead of warping along it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The Clone Source panel gets Photoshop's W/H chain (Maintain aspect
ratio): with it on, a new W or H scales the other by the same factor.
cloneSource.set takes "linked" and cloneSource.list reports it.
The overlay's Darken, Lighten and Difference modes were stored but
never drawn. draw_clone_preview now blends the source with the
composite under the brush (clone_source::overlay_blend), so Difference
goes black where source and image line up.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The mesh was every square grid cell touching the layer's opaque pixels:
a staircase up to a cell outside the shape, and shapes closer than about
a cell shared cells, so pinning one dragged the other along.
build_mesh now traces the outline of the opaque region grown (or shrunk)
by Expansion from a signed distance field, simplifies it (Douglas-Peucker,
corners kept), splits its sides to the Density spacing, fills the inside
with a triangular lattice and triangulates the points (Bowyer-Watson
Delaunay, outline sides recovered by midpoint splits), keeping the
triangles inside the outline. Separate shapes get separate meshes, so an
unpinned one stays put, as in Photoshop. Vertices are numbered in reverse
Cuthill-McKee order to keep the solver's banded factor narrow, and the
density's spacing counts the grown region so a large Expansion on a small
layer no longer builds tens of thousands of vertices.
Scorecard TOOL-214-6 done; docs/gaps.md G7 updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Curves: pencil mode, Smooth and Edit points (#3088)
The Curves editor (Image > Adjustments dialog and the adjustment layer's
Properties panel) gains Photoshop's "Edit points to modify the curve" and
"Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn
curve is a 256-level map per channel, evaluated piecewise-linearly.
- doc: Adjustment::Curves gains `pencil` (serde default), with
curve_map / curve_map_outputs helpers.
- compose: curve_map_lut / curve_lut_mode for drawn curves.
- engine: curves params take `pencil` and `smooth`; channels accept
256-level maps; switching modes draws the spline into maps or fits up to
16 points to the drawing. Eyedroppers fit points first.
- io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read
as padding, so pencil curves from Photoshop were misparsed; they are now
read and written as 256-byte maps, and smart-filter Curves use `Mpng`.
- scorecard UI-217-15 note updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Properties: Curves adjustment layers get the black/gray/white eyedroppers (#3110)
The Set Black/Neutral Gray/White Point pickers existed only in the
Image > Adjustments > Curves dialog (#873). The Properties editor of a
Curves adjustment layer now has them too (layer_picker.rs): with one
armed, a click on the image runs one layer.setAdjustment with the new
`eyedropper` param, which samples the image below the layer (the layer
hidden, like its Properties histogram) at the Eyedropper's Sample Size,
so a repeated click keeps the curve and the edit is one undoable,
recordable step agents can send directly. Esc, another tool or another
layer disarms it; ui.pointer and ui.inspect (`layerPicker`) cover it.
Scorecard UI-217-15 is done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Levels: black, gray and white point eyedroppers (#3116)
Image > Adjustments > Levels and a Levels adjustment layer's Properties
panel get Photoshop's Set Black/Neutral Gray/White Point eyedroppers.
Black/white move each channel's input black/white to the sample, gray
sets each channel's midtone gamma so the sample turns neutral at its
luma (Grayscale: composite input, no gray). The engine takes the same
`eyedropper` param as Curves on image.adjustments.levels,
layer.newAdjustmentLayer.levels and layer.setAdjustment.
On a smart object, image.adjustments.levels/curves with `eyedropper`
now record the resolved adjustment as the smart filter instead of the
raw sample, which re-rendered as an identity (#3127).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Curves: pencil mode, Smooth and Edit points (#3088)
The Curves editor (Image > Adjustments dialog and the adjustment layer's
Properties panel) gains Photoshop's "Edit points to modify the curve" and
"Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn
curve is a 256-level map per channel, evaluated piecewise-linearly.
- doc: Adjustment::Curves gains `pencil` (serde default), with
curve_map / curve_map_outputs helpers.
- compose: curve_map_lut / curve_lut_mode for drawn curves.
- engine: curves params take `pencil` and `smooth`; channels accept
256-level maps; switching modes draws the spline into maps or fits up to
16 points to the drawing. Eyedroppers fit points first.
- io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read
as padding, so pencil curves from Photoshop were misparsed; they are now
read and written as 256-byte maps, and smart-filter Curves use `Mpng`.
- scorecard UI-217-15 note updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Properties: Curves adjustment layers get the black/gray/white eyedroppers (#3110)
The Set Black/Neutral Gray/White Point pickers existed only in the
Image > Adjustments > Curves dialog (#873). The Properties editor of a
Curves adjustment layer now has them too (layer_picker.rs): with one
armed, a click on the image runs one layer.setAdjustment with the new
`eyedropper` param, which samples the image below the layer (the layer
hidden, like its Properties histogram) at the Eyedropper's Sample Size,
so a repeated click keeps the curve and the edit is one undoable,
recordable step agents can send directly. Esc, another tool or another
layer disarms it; ui.pointer and ui.inspect (`layerPicker`) cover it.
Scorecard UI-217-15 is done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Curves: pencil mode, Smooth and Edit points (#3088)
The Curves editor (Image > Adjustments dialog and the adjustment layer's
Properties panel) gains Photoshop's "Edit points to modify the curve" and
"Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn
curve is a 256-level map per channel, evaluated piecewise-linearly.
- doc: Adjustment::Curves gains `pencil` (serde default), with
curve_map / curve_map_outputs helpers.
- compose: curve_map_lut / curve_lut_mode for drawn curves.
- engine: curves params take `pencil` and `smooth`; channels accept
256-level maps; switching modes draws the spline into maps or fits up to
16 points to the drawing. Eyedroppers fit points first.
- io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read
as padding, so pencil curves from Photoshop were misparsed; they are now
read and written as 256-byte maps, and smart-filter Curves use `Mpng`.
- scorecard UI-217-15 note updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* automation: doc.place reads a file through the read root (#2810)
file.placeEmbedded takes an ambient path, so automation refuses it and an
agent could not place a file into the open document at all. doc.place
resolves the path beneath the read root the way doc.open does and hands the
bytes it already read to the public file_cmds::place_bytes, so the existing
cap-std rules stay the only path rules: no new scope logic, and the ambient
command keeps its refusal. Trusted-local callers still run the command.
* automation: export layers, data sets and CSVs through the roots (#3216)
file.export.layersToFiles and file.export.dataSetsAsFiles take an ambient
dir, file.import.variableDataSets an ambient path, so all three are refused
in automation even for files inside the granted roots, and an agent could
not export a single file or a data set at all.
Both halves already existed: the engine writes bytes it has just encoded
(save_doc is encode + write_file, and the data-set exporter encodes with
photocraft_io::export before writing), and the automation side already
holds a write capability — AuthorizedWorkspace::export_sequence creates the
directory, refuses to overwrite, accepts only single-component names and
removes every file it created when a step fails, which is what Render Video
uses. So each command body moves behind the same sink callback
render_video_with takes: the command keeps writing the bytes it is handed
to its dir, the new methods write the very same bytes through the root, and
the two implementations cannot drift.
---------
Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Preferences › Interface › Enable animations (interface.enableAnimations, on by
default). Off, prefs_ui::sync_animations sets egui's animation_time to 0 and
scroll_animation to none in both the dark and light styles, so menus, popups
and collapsing sections change at once and scroll areas jump. It is re-applied
each frame, so a theme change keeps it. Translated in all 16 catalogs; the
scorecard row UI-217-10 goes to partial.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layers panel: Shift+Plus/Minus cycle blend modes; effects dropped on the Trash are deleted (#3102)
- blend_keys.rs: Shift+Plus / Shift+Minus step to the next / previous blend mode, wrapping:
the brush's mode with the Brush or Pencil (tools.setBrush), the Gradient's with the Gradient
tool, otherwise the active layer's (layer.setProps; groups include Pass Through, the
Background is left alone). One undo step per press.
- panels.rs: an effect row dragged onto the footer's Delete button deletes that effect, the
"Effects" row clears the whole style (layer.layerStyle.clear).
- engine: layer.layerStyle.clear takes an optional "effect" index ("Delete Effect" step);
bad indices and layers return Err.
- scorecard UI-217-14 -> done (its wheel, footer drop and thumbnail checker parts were already
on main via #1961, #965).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layers panel: Shift+Plus/Minus step the blend mode on a German layout too (#3102)
On layouts with a key of its own for `+` (German, Italian, Spanish) Shift+`+`
types `*`, which egui has no key for, so egui-winit reports the physical key:
`]` on German. That matched Shift+] (Increase Brush Hardness) before the blend
keys ever saw it. Shift+`-` types `_` and arrives as the physical `/`.
shortcuts::layout_symbols now turns such a fallback press (Shift, the key equal
to its physical key, not a digit, next to the text `*` or `_`) into `+` / `-`
before any shortcut is dispatched. A US Shift+] still arrives as `}` and keeps
raising the hardness; US Shift+8 (`*`) stays a digit for the opacity keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shortcuts: Ö / # are the brush keys on a German layout (#3109)
Photoshop's German shortcuts step the brush size with Ö / # and the hardness
with Shift+Ö / Shift+#, since [ and ] need AltGr there (and that press carries
Alt on the Mac and Windows, which the shortcut match rejects). egui can't name
ö or #, so they arrived as the physical ; and \ and matched nothing; Shift+#
types ' and arrived as Quote.
shortcuts::layout_symbols (from #3102) now reads a table of typed symbol and
Shift: ö / Ö -> [, # / ' -> ], next to * -> + and _ -> -. It runs at the top of
shortcuts::handle (unless a text field has focus), so Liquify's and the
Magnetic Lasso's [ ] get the German keys too. Only presses whose key egui took
from the physical key or from that very text change, and never digits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Brush keys: layout table plus bracket-position fallback for every layout (#3109)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* layout_keys: drop unused field, fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop shortcuts::layout_symbols left by the merge: layout_keys::apply replaces it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Layers panel: Shift+Plus/Minus cycle blend modes; effects dropped on the Trash are deleted (#3102)
- blend_keys.rs: Shift+Plus / Shift+Minus step to the next / previous blend mode, wrapping:
the brush's mode with the Brush or Pencil (tools.setBrush), the Gradient's with the Gradient
tool, otherwise the active layer's (layer.setProps; groups include Pass Through, the
Background is left alone). One undo step per press.
- panels.rs: an effect row dragged onto the footer's Delete button deletes that effect, the
"Effects" row clears the whole style (layer.layerStyle.clear).
- engine: layer.layerStyle.clear takes an optional "effect" index ("Delete Effect" step);
bad indices and layers return Err.
- scorecard UI-217-14 -> done (its wheel, footer drop and thumbnail checker parts were already
on main via #1961, #965).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layers panel: Shift+Plus/Minus step the blend mode on a German layout too (#3102)
On layouts with a key of its own for `+` (German, Italian, Spanish) Shift+`+`
types `*`, which egui has no key for, so egui-winit reports the physical key:
`]` on German. That matched Shift+] (Increase Brush Hardness) before the blend
keys ever saw it. Shift+`-` types `_` and arrives as the physical `/`.
shortcuts::layout_symbols now turns such a fallback press (Shift, the key equal
to its physical key, not a digit, next to the text `*` or `_`) into `+` / `-`
before any shortcut is dispatched. A US Shift+] still arrives as `}` and keeps
raising the hardness; US Shift+8 (`*`) stays a digit for the opacity keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
file.placeEmbedded takes an ambient path, so automation refuses it and an
agent could not place a file into the open document at all. doc.place
resolves the path beneath the read root the way doc.open does and hands the
bytes it already read to the public file_cmds::place_bytes, so the existing
cap-std rules stay the only path rules: no new scope logic, and the ambient
command keeps its refusal. Trusted-local callers still run the command.
Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Content-Aware Scale: transform box with live preview (#3096)
Edit > Content-Aware Scale opened a parameter dialog. Photoshop has no
dialog: it puts a scale-only transform box on the layer (or the
selection's bounds). The menu now opens that box.
- The box reuses Free Transform. Corner and edge drags scale and an
inside drag moves; dragging outside does not rotate, Cmd does not
skew or distort, and the Warp switch and the Transform modes are off.
- The options bar shows X/Y and W/H, then Amount, Protect (an alpha
channel) and Protect skin tones.
- The live preview runs the command's own carving
(ContentAwareScalePreview) on a 384 px copy on a worker thread. A
run for a box that has changed since is cancelled after 150 ms, and
the latest result is drawn through the box.
- Enter runs edit.contentAwareScale with the box's size and its new
x/y params as one history step; Esc cancels.
- With params the command still runs directly.
Scorecard TOOL-214-7: done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Content-Aware Move's transform box: set the new content_aware field
#3095 (Transform On Drop) builds a TransformSession by hand; this PR adds
content_aware to it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* raw: fit non-DNG colour to the camera's embedded JPEG
* raw: preserve fitted Camera Raw look and crop D5100 borders
---------
Co-authored-by: Gert Pellin <gert@pellin.be>
Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Numeric fields: typed units, scrubby labels, double-click reset (#3099)
- Value fields convert a unit typed after a number into the field's unit
(1in, 2.54cm, 72pt, 1pica/pc, 10px, 50%) at the active document's
resolution; Image Size and New Document use their own resolution. A
unit that can't convert, or bad arithmetic, keeps the old value.
- widgets::scrub_label: dragging a label scrubs the value field right
after it on its row (a step a point, Shift x10, Alt a tenth as fast),
with a horizontal-resize cursor. Used by options-bar labels, slider
rows and the Character panel icons. A slider row reports the scrub as
one drag, so adjustment edits coalesce into one history step.
- Adjustment slider rows with a known default and filter dialog sliders
reset on double-click, like Color Balance already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Scrubby labels: drag-only, not focusable, so Tab skips them
Sense::drag() is focusable in egui 0.36, so every scrubby label became an
extra Tab stop and tab_focus_reveals_the_final_narrow_tool_option could no
longer reach the last tool option.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Content-Aware Move: Transform On Drop (#3071)
The Content-Aware Move tool gains Photoshop's Transform On Drop checkbox. With it on, the drop
puts a transform box on the moved patch (the Free Transform box's handles, limited to move,
scale and rotate); Enter, the check button or a double-click computes the content-aware blend
with the transformed patch, Esc or the cancel button drops the whole move. The document is
untouched until the commit, so the move stays one History step.
Engine: paint.contentAwareMove takes scaleX / scaleY (%, 1..10000, negative flips) and rotate
(degrees, clockwise) about the selection's centre before the offset, validated (Err, not a
panic, for non-finite, out-of-range or off-canvas placements). The content and the selection's
shape are resampled through the inverse map on float samples (any depth); the selection is
transformed with the content. A plain move keeps the old, exact code path.
UI: cam_transform.rs holds the box (UiState.cam_transform, view state); the option is remembered
in ToolOptions and settable with ui.set camTransformOnDrop; ui.inspect reports camTransform.
Scorecard TOOL-213-4 stays partial: no live preview of the blended result yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: keep Transform On Drop cases in the Content-Aware Move test, not Patch (#3071)
paint.patch takes no scaleX/scaleY/rotate, so those cases (which only
paint.contentAwareMove rejects) failed patch_fails_gracefully. They are
already covered by content_aware_move_fails_gracefully.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
While editing type, a run of clicks at one spot now selects like Photoshop:
two clicks the word, three the laid-out line (a column in vertical type),
four the paragraph up to its hard return, five all text; the next quick
click starts over with a caret. egui stops counting at three, so the Type
tool keeps its own count per layer (next_click_count) and the ranges come
from a pure helper over the text and its layout (click_selection).
Scorecard TYPE-218-8 is done; docs/gaps.md G8 updated.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Every existing save path (layered TIFF, print PDF, ACO/ASE, .shc, Smart
Filters in PSD, atomic-save failure) already has a save-and-read-back
test; SVG and Photoshop PDF have no writer yet (FILE-215-10).
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Enable Layers context-menu Quick Export as PNG for a multi-layer selection. Capture each selected source before showing one destination prompt and write separate numbered PNG siblings with sanitized layer names, without silently replacing existing sibling files.
Add actual PNG encoding and cancellation regressions, preserve the captured selection when the dialog is open, and update context-menu enablement tests.
Co-authored-by: kotulsky <7805437+kotulsky@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Remove the 320-point list height cap and nested dock scrolling. Reserve space for the footer while filling the available panel height.
Add a panel-height regression test and check recording autoscroll against the visible footer instead of a fixed coordinate.
Co-authored-by: kotulsky <7805437+kotulsky@users.noreply.github.com>
* Tools: Artboard tool (#2858)
Photoshop's Artboard tool in the Move flyout (V, ⇧V cycles Move ↔ Artboard):
dragging on an empty spot draws a new board (`layer.new.artboard`), clicking a
board selects it, dragging it moves it with its contents, and the selected
board's edge and corner handles resize it (`layer.artboard.set`,
moveContents: false). Each gesture is one undo step. Options bar: Size preset,
W/H, Make Portrait, Make Landscape, Add New Artboard. Original toolbar glyph
(assets/icons/artboard.svg, ATTRIBUTION.md row).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Artboard tool: '+' buttons add a board beside the selected one; ⇧ keeps proportions (#3033)
With the Artboard tool, the selected board shows a "+" button beside each
side. A click adds an empty board of the same size (background and preset
copied) on that side, the 100 px artboard gap away and past any board in
the way; ⌥-click adds a copy with its contents instead. A board left of or
above the canvas origin shifts the whole document right/down and grows the
canvas, so it stays visible. Both go through the new engine command
`layer.artboard.addBeside` {layer?, side, duplicate} (one undo step).
⇧ while dragging a corner handle keeps the board's aspect ratio: the
opposite corner stays and the board scales by the larger axis factor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): translate dynamic labels the literal scanner can't see
The Czech catalog covered every enforced key (menus, tl! literals, blend
modes, preferences) but not the labels built at run time: filter and
Filter Gallery parameter names and choices humanized from their keys,
Camera Raw controls, the Flame / Picture Frame / Tree render filters,
measurement columns, file info fields and the macOS application menu.
Those showed in English under Czech.
Adds 853 entries (2,441 -> 3,294) for keys that the other catalogs
already translate and that still exist in the code. Existing entries are
unchanged. Translated clean-room from the plain meaning of the English
labels, with the terminology already used in cs.tsv; no proprietary
localisation was consulted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): credit the Czech catalog in ATTRIBUTION.md and LICENSE-translations.txt
cs.tsv is a bundled translation asset but had no row in ATTRIBUTION.md
and was not named in LICENSE-translations.txt. Add both, worded like the
other catalogs (PhotoCraft contributors, original translations, MIT OR
Apache-2.0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): terminology review fixes for the new dynamic labels
Changes from an independent review pass over the added block:
- "Create By" (De-Interlace: how the missing lines are made) reads
"Vytvořit pomocí", not "Vytvořil" (author)
- luminance is "jas" in Camera Raw, as in the existing "Luminance" label
("Detail jasu", "HSL – jas", "Jas")
- slider labels are nouns, like the existing "Redukce šumu" (Reduce Color
Noise, Sharpen Details, Lighten Grout)
- grain and flame choices agree in gender with their neighbours; "Shadow"
matches "Highlight"/"Světla" as "Stíny"; "At" is "Pozice"; "Water Paper"
is "Mokrý papír"; "Leaves" is "listů" everywhere
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: add my own people.toml entry
Own entry only, as AGENTS.md describes: credited by the name on my public
GitHub profile (sync_github_name), otherwise by @username.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): drop three rows that main added in the meantime
"Columns", "Margin" and "Automatic" were added to cs.tsv on main while this
branch was open, with the same translations, so the appended block no longer
needs them (the catalog has no duplicate keys).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n (cs): translate two strings that are still missing from cs.tsv
"Drag (lasso) or click points (polygonal) ..." and "The current gradient
(pick one in Window > Gradients)" exist in the other catalogs but not yet
in cs.tsv on main.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* i18n (cs): translate context-menu, channel and layer-style labels
These labels are rendered through tl!() with a variable, so the literal-based coverage test does not see them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Localize Czech status messages and notice wrappers
Translate shell messages before interpolating file names and engine errors.
Use the existing plural API for counted objects and preserve English fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Localize Czech UI labels and hardware information
Translate brush controls, accessibility names and rasterization prompts at
render sites. Preserve adapter, driver and display/profile identifiers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Localize Czech composed labels and distinguish Navigator zoom
Translate profile, preset and shortcut messages before interpolation, and use
a Navigator context for the zoom level. Keep shortcut warning state separate
from translated text and preserve document names and shortcut codes.
Cover remaining visible counts and add rendering/glyph regression tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix Czech UI localization review follow-ups
Keep copied System Info and API diagnostics in English while localizing the
on-screen text. Avoid double translation of accessible names, honor command-id
shortcut labels, and translate stored auto backend values only for display.
Refine Czech warning punctuation and catalog wording, simplify template blocks,
and correct the documented catalog count to 3,522 rows. Add four regression tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* people.toml: keep axlecoffee's sync_github_name with their entry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Paths: Stroke Path with any brush-based tool and Simulate Pressure (#2978)
path.stroke now takes every brush-based tool of Photoshop's Stroke Path
dialog: besides the Brush, Pencil and Eraser it renders itself, the
Background Eraser, Clone and Pattern Stamp, Healing and Spot Healing
Brush, History Brush, Blur, Sharpen, Smudge, Dodge, Burn, Sponge, Color
Replacement and Mixer Brush run their paint.* command along each
subpath, in one history step, with only path.stroke journalled.
simulatePressure resamples each subpath and ramps the pressure from 0 at
its start to full at the middle and back to 0 at its end, so a brush
whose size follows pen pressure tapers at both ends.
The Stroke Path dialog (Pen/Path Selection right-click menu, the Paths
panel row menu, Alt-click on the panel's Stroke button) asks for the
tool and Simulate Pressure, as in Photoshop. Scorecard TOOL-214-11 done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Automation: classify path.stroke's params as a document vector path (#2978)
The new Stroke Path params doc says the tools run "along the path", which
registry_filesystem_path_params_are_classified reads as a possible host
file path. path.stroke strokes a document path and forwards only to
paint.* commands that take no files, so it joins the vector-path list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A file with no embedded ICC profile whose EXIF ColorSpace tag says sRGB
(what cameras write) now opens as sRGB, as in Photoshop: it is tagged
sRGB before the Color Settings policy runs, so it is kept, reported as a
mismatch, or converted like any embedded profile. Before, it was read as
untagged and took the working space, so with a wide working space such
as Display P3 its colours were reinterpreted.
Preferences › File Handling › Ignore EXIF Profile Tag (off by default)
restores the untagged reading, and is now shown in the dialog.
photocraft_codecs::exif_color_space reads the tag from the Exif sub-IFD,
bounds-checked; anything malformed reads as no tag. Settings that do
nothing: 43 → 42 of 150.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Type: ask to rasterize before Warp on live type (#3004)
Edit › Transform › Warp (the menu, or the warp toggle of an open transform
box) on editable type opened Warp mode, let the user shape the mesh, then
failed on commit with "Warp needs rasterized type". It now opens Photoshop's
"This type layer must be rasterized before proceeding…" prompt, the one the
pixel tools already use. OK applies an open transform box, rasterizes the
type (its own history state) and opens Warp on the pixels; Cancel does
nothing. The prompt's continuation is a menu command from an allow-list,
run with the egui context right after the confirm (control channel, OK
button) or on the next frame.
Distort and Perspective stay greyed for type (#2701), Puppet Warp and
Perspective Warp stay greyed for non-pixel layers, as in Photoshop.
Scorecard TYPE-218-9 -> done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Type: Warp on live type takes the warp styles, no rasterize prompt (#3004)
Photoshop 2026 keeps Edit › Transform › Warp enabled on live type and asks
nothing: the mesh can't be dragged, only the options bar's warp styles
(Warp Text's) apply, and the type stays editable. The earlier version of
this change asked to rasterize first; that is gone (and with it the prompt
continuation, which nothing else used).
Warp mode on a type layer now starts from the layer's own Warp Text style
over the unwarped type, ignores mesh drags and splits (Split, Grid and the
Custom style are greyed/hidden, the split menu items are greyed), and the
commit runs edit.transform.warp, which on type sets the Warp Text: one
history step, recordable, the layer stays Text. None takes a warp off. A
box scaled or rotated before toggling Warp is applied first.
Scorecard TYPE-218-9 note updated: Photoshop doesn't prompt for Warp;
Distort and Perspective stay greyed (#2701).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Alt during a drag unlinks the handle pair (only the outgoing one keeps following),
Alt-press on the last anchor's outgoing handle turns that handle alone, and pressing
the first anchor now closes on release, so a drag pulls out that anchor's handles
(a stand-in knot folded into the first one on commit). A plain click still closes
without touching the first anchor's handles. Still one History step per path.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`hasMask` in document inspection covers only raster masks, so a layer
with a vector mask looked unmasked to an agent. Layers, groups included,
now also report `hasVectorMask` and `vectorMaskEnabled` (null when there
is no vector mask). `hasMask` keeps its meaning.