* Brushes: import an image file as a brush tip (#3279)
`brush.presets.importImage` reads an image (path, or base64 data with
an optional fileName) through the normal importer, so any format and
bit depth File > Open reads works. Its composite becomes a sampled tip
the way Define Brush Preset reads a selection: darkness x alpha,
cropped to the painted extent, at most 2500 px a side. The bounds are
found band by band first, so a large mostly-white image never needs a
full-size buffer. The preset is named after the file (never replacing
an existing one) and becomes the current brush unless select is false.
Import Brushes... in the Brushes panel and the brush preset picker now
opens a picker for .abr and image files; images go to the new command,
.abr files to brush.presets.importAbr as before. Automation refuses an
ambient `path` for it like the other import commands.
Define Brush Preset and the new command share `sampled_brush`.
Scorecard TOOL-213-12 -> done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate scorecard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: regenerate scorecard after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Regenerate docs/scorecard.md after merging main
* Regenerate docs/scorecard.md after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(ui): remember the selected tool between launches
* fix(ui): preserve explicit Eraser selection during temporary tool overrides
Clear temporary pen and spring state for explicit toolbar, menu, preset and control selections. Keep spring-loaded key behavior; only permanent key choices clear the pen marker. Add exit/relaunch coverage for explicit Eraser with pen and spring overrides.
* Regenerate docs/scorecard.md after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Batch: Include All Subfolders (AUTO-219-5, #3753)
File > Automate > Batch and File > Scripts > Image Processor take an
optional "includeSubfolders" (default false): a folder input also yields
the images in its subfolders, walked depth first in name order, bounded at
32 levels and 100000 files, with a seen-set of canonical folders against
symlink loops and the output folder left out. Each result keeps its
relative subfolder under the output folder, so same-named files in
different subfolders no longer collide. A non-boolean value is BadParams.
Both dialogs gain an "Include All Subfolders" checkbox (translated in
every complete catalog). Scorecard AUTO-219-5 stays partial (naming
template, progress/cancel, error log file); gaps.md G11 updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Batch: translate Include All Subfolders for Hungarian and Turkish
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: 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>
With a selection, ⌘T and the Move tool's Show Transform Controls framed the
layer's content clipped to the selection, so the box was smaller than the
marching ants wherever the layer did not fill the selection. The ants then
stayed where the selection started until the transform was applied.
- transform_frame / target_frame (engine): the box is the selection's own
bounds when a selection limits the transform. transform_bounds is unchanged,
so snapping and the command defaults are as before.
- The selection outline is drawn through the open box (homography, or the warp
in Warp mode), so it follows move, scale, rotate, skew, distort, perspective
and warp before the transform is applied. The whole outline is traced while
such a box is open, not only the part in view where it started.
- marching_ants_lines: ants along screen-space lines; non-finite lines are
skipped and very long ones are clipped to the view.
Part of #3402.
Claude-Session: https://claude.ai/code/session_015vKPoMXFhSvWmX5D9XM646
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 32-bit: legacy Brightness/Contrast, Channel Mixer, Photo Filter and B&W tint unclipped
In 32-bit documents Photoshop keeps these adjustments' results above 1
and below 0. On the photoshop corpus's rgb32/gray32 files:
- Legacy Brightness/Contrast pivots its contrast at 127 of 255 (8/16-bit:
128) and is exact on every pixel.
- Channel Mixer cuts each source percentage to 1/1024 toward zero, counts
255/512 of the constant and is exact on pure red, green, blue, black
and white.
- Photo Filter works on the linear samples, negative results included.
- The Black & White tint is the Color blend mode without the clip at 1,
as the blend mode is there; white tints to (1.05, 1.00, 0.87).
We clipped all four to 0..1. The GPU kernels follow suit under F_HDR
(the legacy pivot and the mixer's steps come from the planner), and the
HDR parity test covers them.
Photoshop oracles: rgb32 and gray32 brightness-contrast-legacy, rgb32
channel-mixer and black-white-tint pass (145 -> 149 of 258); floor
145 -> 149. rgb32 photo-filter improves (0.19 -> 0.035 max); the rest is
its colour (#3790).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Masks: feathers blur with exactly the feather's variance
A mask feather is a Gaussian with sigma = feather, which we approximate
with three box passes per axis. Whole-pixel boxes rounded the variance:
a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so
feathered edges were too soft by up to 4 levels against Photoshop
(psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px
pixel feather).
Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on
the whole radius and a fractional weight on the two samples beyond it,
so three passes have variance sigma^2 exactly. It stays O(n), reusing
the window's leaving sample for the low end weight (50 MP at a 1000 px
feather: 180 ms parallel, was 172).
psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS),
layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %).
Corpus test 239 -> 240; floor 236 -> 240.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Threshold: split on Photoshop's luminosity of whole samples
Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B
(the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey,
taken over the document's whole sample values:
- 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity
rounded to a level with halves down reaches the level;
- 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits
(round(level x 32768 / 255): mid grey 16448 is level 128);
- 32 bits: the luminosity reaches the level.
Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split
28 the other way), threshold_16bits_rgb (100) and threshold_16bits_
grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are
exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on
the level) split the same way on both.
psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test
239 -> 241, floor 236 -> 241.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does
At 16 bits Photoshop doesn't round to 8-bit levels before binning. It
bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels
through a 16-bit step: floor(b * 32768 / (n - 1)), converted with
floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where
8 bits give 127, while 42.5 and 25.5 stay 42 and 25.
Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7,
13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule
missed up to half the range at bin edges. The same rule applies to
CMYK and Lab channels at 16 bits: on the stored inks (#4945's native
path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of
pixels off. 8-bit Posterize is unchanged.
The GPU kernel runs the same rule; 16-bit layers reach it as half
floats, so a sample within their precision of a bin edge can land in
the neighbouring bin there (the CPU, which exports, is exact).
psd-tools 239 -> 241; floor 236 -> 241.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Groups: a pass-through clipping base with an adjustment renders isolated
A pass-through group composites its children straight into the
backdrop, so an adjustment layer inside reaches the layers beneath.
When the group is the base of a clipping group and holds an adjustment
layer, Photoshop renders it isolated instead: in psd-tools
passthrough_clipping_mask_adjustment an Exposure in such a group leaves
the layers beneath alone (we darkened them: half the image 170 levels
off). The same group with a Linear Dodge child and no adjustment keeps
reaching the backdrop (passthrough_clipping_mask_blendmode), and so do
clipping-mask3/4/5, so the rule is the adjustment, not the clipping
alone (isolating every pass-through clipping base broke those two).
CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240.
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>
* Masks: feathers blur with exactly the feather's variance
A mask feather is a Gaussian with sigma = feather, which we approximate
with three box passes per axis. Whole-pixel boxes rounded the variance:
a 6 px feather blurred like 6.48 px and a 3 px one like 3.46 px, so
feathered edges were too soft by up to 4 levels against Photoshop
(psd-tools mask-parameters-no-real-channel.psd: a 6 px vector and 3 px
pixel feather).
Each pass is now an "extended box" (Gwosdek et al. 2011): weight 1 on
the whole radius and a fractional weight on the two samples beyond it,
so three passes have variance sigma^2 exactly. It stays O(n), reusing
the window's leaving sample for the low end weight (50 MP at a 1000 px
feather: 180 ms parallel, was 172).
psd-tools: mask-parameters-no-real-channel 0.0168 -> 0.0060 (PASS),
layer_mask_data.psd 26.9 % -> 3.9 % of pixels off (psb 25.9 % -> 1.8 %).
Corpus test 239 -> 240; floor 236 -> 240.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Threshold: split on Photoshop's luminosity of whole samples
Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B
(the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey,
taken over the document's whole sample values:
- 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity
rounded to a level with halves down reaches the level;
- 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits
(round(level x 32768 / 255): mid grey 16448 is level 128);
- 32 bits: the luminosity reaches the level.
Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split
28 the other way), threshold_16bits_rgb (100) and threshold_16bits_
grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are
exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on
the level) split the same way on both.
psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test
239 -> 241, floor 236 -> 241.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does
At 16 bits Photoshop doesn't round to 8-bit levels before binning. It
bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels
through a 16-bit step: floor(b * 32768 / (n - 1)), converted with
floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where
8 bits give 127, while 42.5 and 25.5 stay 42 and 25.
Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7,
13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule
missed up to half the range at bin edges. The same rule applies to
CMYK and Lab channels at 16 bits: on the stored inks (#4945's native
path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of
pixels off. 8-bit Posterize is unchanged.
The GPU kernel runs the same rule; 16-bit layers reach it as half
floats, so a sample within their precision of a bin edge can land in
the neighbouring bin there (the CPU, which exports, is exact).
psd-tools 239 -> 241; floor 236 -> 241.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Groups: a pass-through clipping base with an adjustment renders isolated
A pass-through group composites its children straight into the
backdrop, so an adjustment layer inside reaches the layers beneath.
When the group is the base of a clipping group and holds an adjustment
layer, Photoshop renders it isolated instead: in psd-tools
passthrough_clipping_mask_adjustment an Exposure in such a group leaves
the layers beneath alone (we darkened them: half the image 170 levels
off). The same group with a Linear Dodge child and no adjustment keeps
reaching the backdrop (passthrough_clipping_mask_blendmode), and so do
clipping-mask3/4/5, so the rule is the adjustment, not the clipping
alone (isolating every pass-through clipping base broke those two).
CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240.
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>
* Threshold: split on Photoshop's luminosity of whole samples
Photoshop's Threshold splits on the luminosity 0.30 R + 0.59 G + 0.11 B
(the blend modes' lum, as Gradient Map uses), not the Rec. 601 grey,
taken over the document's whole sample values:
- 8 bits: 30 R + 59 G + 11 B > 100 level - 50, i.e. the luminosity
rounded to a level with halves down reaches the level;
- 16 bits: 30 R + 59 G + 11 B >= 100 x the level in 16 bits
(round(level x 32768 / 255): mid grey 16448 is level 128);
- 32 bits: the luminosity reaches the level.
Exact on every pixel of psd-tools threshold_rgb (36432 px; Rec. 601 split
28 the other way), threshold_16bits_rgb (100) and threshold_16bits_
grayscale, and the photoshop corpus's rgb16 threshold-128. The sums are
exact in f32, on the CPU and in the GPU kernel, so ties (grey exactly on
the level) split the same way on both.
psd-tools: threshold_rgb and threshold_16bits_rgb pass; corpus test
239 -> 241, floor 236 -> 241.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does
At 16 bits Photoshop doesn't round to 8-bit levels before binning. It
bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels
through a 16-bit step: floor(b * 32768 / (n - 1)), converted with
floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where
8 bits give 127, while 42.5 and 25.5 stay 42 and 25.
Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7,
13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule
missed up to half the range at bin edges. The same rule applies to
CMYK and Lab channels at 16 bits: on the stored inks (#4945's native
path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of
pixels off. 8-bit Posterize is unchanged.
The GPU kernel runs the same rule; 16-bit layers reach it as half
floats, so a sample within their precision of a bin edge can land in
the neighbouring bin there (the CPU, which exports, is exact).
psd-tools 239 -> 241; floor 236 -> 241.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Groups: a pass-through clipping base with an adjustment renders isolated
A pass-through group composites its children straight into the
backdrop, so an adjustment layer inside reaches the layers beneath.
When the group is the base of a clipping group and holds an adjustment
layer, Photoshop renders it isolated instead: in psd-tools
passthrough_clipping_mask_adjustment an Exposure in such a group leaves
the layers beneath alone (we darkened them: half the image 170 levels
off). The same group with a Linear Dodge child and no adjustment keeps
reaching the backdrop (passthrough_clipping_mask_blendmode), and so do
clipping-mask3/4/5, so the rule is the adjustment, not the clipping
alone (isolating every pass-through clipping base broke those two).
CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240.
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>
* Posterize: 16-bit documents bin the 16-bit sample, as Photoshop does
At 16 bits Photoshop doesn't round to 8-bit levels before binning. It
bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels
through a 16-bit step: floor(b * 32768 / (n - 1)), converted with
floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where
8 bits give 127, while 42.5 and 25.5 stay 42 and 25.
Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7,
13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule
missed up to half the range at bin edges. The same rule applies to
CMYK and Lab channels at 16 bits: on the stored inks (#4945's native
path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of
pixels off. 8-bit Posterize is unchanged.
The GPU kernel runs the same rule; 16-bit layers reach it as half
floats, so a sample within their precision of a bin edge can land in
the neighbouring bin there (the CPU, which exports, is exact).
psd-tools 239 -> 241; floor 236 -> 241.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Groups: a pass-through clipping base with an adjustment renders isolated
A pass-through group composites its children straight into the
backdrop, so an adjustment layer inside reaches the layers beneath.
When the group is the base of a clipping group and holds an adjustment
layer, Photoshop renders it isolated instead: in psd-tools
passthrough_clipping_mask_adjustment an Exposure in such a group leaves
the layers beneath alone (we darkened them: half the image 170 levels
off). The same group with a Linear Dodge child and no adjustment keeps
reaching the backdrop (passthrough_clipping_mask_blendmode), and so do
clipping-mask3/4/5, so the rule is the adjustment, not the clipping
alone (isolating every pass-through clipping base broke those two).
CPU compositor and GPU plan alike. psd-tools 239 -> 240; floor 240.
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>
At 16 bits Photoshop doesn't round to 8-bit levels before binning. It
bins the 16-bit sample, floor(v16 * n / 32768), and lands on 8-bit levels
through a 16-bit step: floor(b * 32768 / (n - 1)), converted with
floor((o * 255 + 16384) / 32768). So the middle of 3 levels is 128 where
8 bits give 127, while 42.5 and 25.5 stay 42 and 25.
Exact on every sample of psd-tools posterize_16bits_rgb (levels 3, 7,
13, 21) and posterize_16bits_grayscale (2, 6, 9, 11); the 8-bit rule
missed up to half the range at bin edges. The same rule applies to
CMYK and Lab channels at 16 bits: on the stored inks (#4945's native
path) psd-tools posterize_16bits_cmyk goes from 4.26 % to 0.01 % of
pixels off. 8-bit Posterize is unchanged.
The GPU kernel runs the same rule; 16-bit layers reach it as half
floats, so a sample within their precision of a bin edge can land in
the neighbouring bin there (the CPU, which exports, is exact).
psd-tools 239 -> 241; floor 236 -> 241.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
adjust_as_smart_filter recorded image.adjustments.* (and Shadows/Highlights)
as a smart filter without checking locks, so a Lock All or Lock Pixels smart
object still took the filter. It now refuses with check_pixels_unlocked, the
same rule filter.* smart filters use. Regression test covers both locks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both commands stored the `fps` param unchecked: 0 or a negative rate was
kept, and 1e39 became an infinite rate the next .pcraft save refused. They
now use timeline_cmds::fps_param (finite, above 0, at most MAX_FPS), as
timeline.create and timeline.setProps do; a missing fps keeps the old
fallback. Regression test covers 0, -1, 1e39 and "x" for both commands.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
count.deleteGroup only clamped the active index to the new length, so
removing a group before the active one silently made the next group
active and later count.add calls landed there. Shift the index down
when the removed group precedes it; deleting the active last group
still selects the new last group.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The embedded and working profile names now sit on the choices they belong
to ("Use the embedded profile (Display P3)", "Convert to the working space
(sRGB IEC61966-2.1)") instead of two lines above them; the sentence naming
the file stays. The missing-profile prompt is unchanged.
"Remember this choice for future images" saves the answer as the Color
Settings policy for the file's mode (preserve, convert or off) and turns
Ask On Mismatch off, so files read from then on open without asking. Files
already waiting in the queue with a mismatch in the same mode get the same
answer (open_prompt::answer_remembering); a different question still asks.
The checkbox's tooltip says where to turn the prompt back on.
The catalogs drop the rows nothing uses any more ("Embedded: {embedded}" and
the two old choice labels) and add the four new strings in every language.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
An action can take one of Photoshop's button colours, and the panel can switch to Button Mode so a click plays that action. The colour is stored on the action and is empty until it is set. The new labels are in every complete catalog.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* feat(ui): Darken and Lighten Interface (Ctrl/Cmd+Shift+F1, F2)
Photoshop's Shift+Ctrl+F1 (⇧⌘F1 on macOS) darkens the interface by one of
its four brightnesses and Shift+Ctrl+F2 lightens it, stopping at the
darkest and lightest. PhotoCraft has the same four as themes (Pro, Pro
Medium Gray, Pro Light Gray, Pro Light), so view.darkenInterface and
view.lightenInterface step through them from the theme on screen and save
the choice like picking it in Preferences › Interface (setting the dark or
light theme and the matching appearance). From another theme they step
from the nearest brightness: Medium Gray for a dark one, Light Gray for a
light one. They work with no document open. Both labels are in every
catalogue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(i18n): Vietnamese strings for Darken and Lighten Interface
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(i18n): Hungarian and Turkish strings for Darken and Lighten Interface
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(sr): translate Darken and Lighten Interface
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>
The engine, renderer and PSD mapping already supported Smooth, Chisel Hard
and Chisel Soft, but the dialog's Bevel & Emboss page had no control for it.
Add the Technique dropdown under Style (Photoshop's order), a smooth default
for new bevels, and translations.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* ACEScg: File Handling option opens colour-less EXR renders in the ACES working space
Our render pipelines write ACEScg (AP1 primaries, ACES white ~D60,
linear) and — like most — state no colour space in the EXR header, so
io's linear-sRGB default misrepresented them (saturated hues shifted).
- cms: AcesCg joins the built-in profiles, generated from the published
AP1 primaries and ACES white point with a linear TRC (id "aces-cg",
aliases acescg).
- engine: File Handling gets "Open color-less EXR as ACEScg". When on,
an EXR that io tagged with the linear-sRGB default opens as ACEScg
instead; embedded or EXIF-declared profiles stay untouched.
Translated in every catalog. ACES view transforms (OCIO-based display)
remain future work; this covers the document's source colours.
* scorecard: 26 of 158 (main's #3472 preference added one too)
* Regenerate docs/scorecard.md after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Clicking a snapshot now selects it (highlighted in place of the current
state) until the history moves on. With a snapshot selected the footer's
trash runs history.deleteSnapshot instead of history.deleteState, as in
Photoshop. Snapshot rows and past state rows can also be dragged onto
the trash to delete them.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With always() the gate also ran when a newer push cancelled the run,
taking a runner slot only to report a red check on the superseded
commit. !cancelled() still runs it when a test leg fails (a skipped
required check would count as passing), but not in a cancelled run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* raster: a shifted surface copies in one pass per row without pre-filled tiles
Surface::translated's misaligned path allocated every target tile through
Tile::filled (a per-pixel default fill when the default is nonzero)
before overwriting nearly all of it, and addressed spans through
per-row closures. The path now writes each stored row directly: content
spans copy as single memcpys crossing source tiles as needed, vacated
edges fill from one prebuilt default row, and filled() builds its
default rows in bulk. Same output (the read-back oracle covers every
depth, sign and tile crossing), cheaper for nonzero defaults and one
less pass over each tile.
* raster: a missing source tile in the shifted content reads the default pixel
The misaligned path skipped absent tiles inside the content rect,
leaving zeros where the surface's default pixel belongs (a white mask
with sparse tiles turned black after a move). The span now fills from
the prebuilt default row. echelon's snippet is a regression test, and a
randomized oracle compares the whole path against a per-pixel reference
gather across formats, default pixels, sparse tiles and tile-boundary
crossing shifts.
* ci: retrigger wasm32 (the full workspace wasm check is clean locally; the job failed before writing a log and the run is stuck queued)
* fix(windows): respect top and side taskbars in custom-frame maximize
* Keep work-area correction stable for narrow taskbar reservations
* style: apply rustfmt to taskbar window correction
* fix(window): repaint after custom-frame work-area correction
* Undo and redo Camera Raw adjustments from the keyboard.
Changing a slider and pressing Ctrl+Z did nothing, because the dialog blocked application shortcuts and kept no history of its own. Each finished edit, including one slider drag, is now one step. The control channel accepts ui.undo and ui.redo.
Refs #4100
* Format camera_raw_ui.rs (rustfmt)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The canvas drew guides before transform_tool::draw_overlay, so the moving
pixels of a layer under Free Transform covered them. Split the overlay:
draw_content paints the preview (plain, styled or warped) and the layers
above it right after the document, under the grid and guides; draw_overlay
keeps the box, handles, reference point, warp grid and split guides over
them, as in Photoshop. The hand-pan drag path does the same.
Test: guides_draw_over_the_transform_preview_and_under_its_box checks the
painted order preview < guide < box; it failed before the fix.
* Layer styles: editing effects no longer re-enables a disabled effect list (#3648)
* Layer styles: follow #3648's exact dialog steps in a test
Preview off shows the document as saved (the in-dialog stroke disable is
a pending edit), so the state the issue reports is Photoshop's preview
semantics, not a resurrected effect: the live preview honours the
disable, and OK applies it.
* style: cargo fmt
On top of #5001. Two UI-thread stalls it leaves on a 16000 × 10000, 30-layer PSB
whose layers exceed the GPU memory budget:
- GpuCanvas::composite checked the memory budget for full refreshes only. Hiding
or showing a layer that covers most but not all of the canvas took the
synchronous GPU path, paging every layer in on the UI thread. Damage of
DEFER_PX or more now gets the same check, so it goes to the background redraw.
Small edits keep the GPU path without the check.
- The first draw after a background open scanned every layer's content bounds
on the UI thread (GPU planning). The open job now warms the shared tile bounds
cache before handing the document over (warm_layer_bounds, cancellable between
layers).
Co-authored-by: syncroot <24667144+syncroot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Interface: a preference to hide the Home screen's recent files (#4382)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: drop the hu/tr 'Enable animations' lines, #4362 adds them
Both PRs added the same two lines; whichever merged second would leave a duplicate key (bundled_catalogs_are_consistent rejects it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: regenerate scorecard after merging main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge main; fix i18n (duplicate ja row, missing sr row) and regenerate scorecard
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drop the duplicate hu row
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>
The Swatches panel used the shared preset browser grid, with a 4 px gap
between cells and a rounded, bordered card for each. Photoshop's panel
packs flat squares edge to edge. Place gets a `flush` flag: no gap, no
per-cell border (only the selected and hovered cells are outlined) and
square corners. Swatches sets it; gradients, patterns, styles and shapes
keep the gap, now the named GRID_GAP and GRID_INSET constants.
Test: swatches_touch_without_a_gap clicks 1 px past the first swatch and
expects the second one selected; with the gap that point was empty.
Quick Selection reused the Ellipse Marquee's dashed circle. It now has a
distinct glyph: Lucide's brush with a dashed selection arc around the
bristles (Photoshop's Quick Selection mark). A rasterized test asserts
the two icons read as distinct and that the arc actually renders.
Co-authored-by: highperfocused <highperfocused@pm.me>
While the Info tab was visible, every frame ran
`view.colorSamplers.list` through the command dispatcher, compositing
each sampler point (up to 10) through the whole layer stack, with layer
effects building a 1-pixel effect-map entry each time.
sampler_readouts keeps the readouts for (document, revision). Adding,
moving or deleting a sampler and every pixel edit bump the revision,
so the readouts refresh exactly when they can change and are shared
between frames otherwise.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Hiding Edit › Menus (or Keyboard Shortcuts) left no way back into the
dialog, and Save, Save As, Exit and Preferences could disappear too.
edit.menus now refuses to hide them, the dialog greys their Visible box,
and the menus keep them even when a preferences file lists them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The hex field's six-character limit dropped any paste into a full field
(every paste after the first) and any paste with a leading #. A pasted hex
colour now replaces the whole code.
Co-authored-by: kwesi <22204443+yankskwesi@users.noreply.github.com>
#1322 lifted the number's slot 1 pt above the box's centre because digits
have no descenders. Measured on today's mono font at 4x, the digits' ink
already sits within 0.2 pt of centre without the lift; with it the ink is
0.9 pt high, the focus outline has 1 pt of gap above and 3 pt below, and the
digits start 1 pt above the unit. The slot is centred again.
Tests: value_field_digits_ink_is_centred_and_level_with_the_unit renders the
field (Pro and Studio) and checks the digits' ink is centred and starts level
with the "%"; value_field_number_slot_is_centred_in_its_box replaces the
1 pt-lift test, and the #2823 slim-box test now expects centring. Both new
assertions fail with the lift restored.
Co-authored-by: prekabreki <prekabreki@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The options bar and the Properties panel's Character section both draw the
"type-font" family picker, and the Character one is drawn later. With its
menu closed it removed the shared hover key every frame, so the options bar's
hover never reached the canvas. Only the picker whose menu is open now clears
the key; follow_hover already drops it each frame once nothing writes it.
Test: options_bar_hover_previews_with_the_character_picker_also_shown runs
the whole app (Pro theme, both pickers on screen), hovers a font in the
options bar and checks the canvas shows it, then that leaving the row
restores it. It fails without the fix.
Co-authored-by: prekabreki <prekabreki@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Two monitors without ICC profiles (a common Linux desk, e.g. DP-4 and DP-5)
showed two identical "Display profile not used" cards at every launch. A
reading now posts one notice: one reason line per display, then the advice
line naming them all. Displays added by a later reading still get a card of
their own, and a reading with nothing new stays quiet, as before.
Claude-Session: https://claude.ai/code/session_01SvxNXnS4tcg6BHT9VG67t7
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The separate mip render targets work around reduced-preview corruption on Intel DX12 drivers (#892, verified on Intel UHD 620), but every DX12 adapter took them. Creating a scratch texture per level on every refresh costs about 1 ms of CPU time per brush dab on an NVIDIA DX12 adapter. Gate them to Intel under DX12; PHOTOCRAFT_SEPARATE_MIPS=1/0 forces either way. Part of #4515.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Canvas: themes without a dot pattern draw no dots texture (#4542)
Switching the dark theme to Pro filled the empty workspace with red and
black blocks on the reporter's machine. The blocks repeat every 22 px,
the tile of the canvas-surround dots texture, so it is that texture
showing wrong contents.
Pro and ten other themes set canvas_dot to their canvas colour: they
have no dot pattern, yet still uploaded a texture for it and tiled it
over the whole pasteboard. dots() now returns None for them, so nothing
is uploaded or drawn. Studio, Studio Light and Solarized Dark, whose
dots differ from the canvas, are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: credit Higgs32584 by GitHub profile name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The eyedroppers (the points/subtractPoints params, and clicks on the
canvas while the dialog is open) refused points outside the active
selection. Photoshop samples anywhere and only limits the result to the
selection, which Color Range already does; the refusal is gone.
On Windows with the automatic backend, GL adapters now rank after every
other adapter, WARP included. VMware SVGA3D over GL and GL-only old GPUs
drew a black window (#1788, #1647); WARP renders.