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.
A mask made from the selection takes the selection with it in Photoshop: after
Layer > Layer Mask > Reveal Selection (the Layers panel's mask button with a
selection) the marching ants are gone. PhotoCraft kept the selection, so the
usual next step, Invert (Cmd/Ctrl+I) on the new mask, inverted only the part
inside the old selection and left the whole mask black, instead of flipping
the mask. Hide Selection (the Alt-click variant) already dropped it.
Reveal Selection now clears the selection in the same history step as the
mask: one undo brings the selection back and removes the mask.
Tests: adjust_cmds::tests::reveal_selection_drops_the_selection_so_invert_flips_the_whole_mask
(8/16/32-bit). Tests that deselected right after Reveal Selection no longer
need to (Deselect is disabled without a selection); layer_mask_depth and the
automation agent tasks now expect the selection to be gone.
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test(engine): add Color Range fuzziness 0 exact-match test (#214)
Adds an explicit test for Sampled Colors at Fuzziness 0 (point sampling
and hex colors) asserting that exact matches are fully selected and
adjacent shades are rejected. Updates scorecard TOOL-214-3 to done.
* docs: regenerate scorecard (TOOL-214-3 done)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WeVmjnnPPKdxVTpr2Vjtw
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
polygon_selection rasterised every lasso over the whole canvas (a
w×h f32 mask) and combined it over the whole canvas, whatever the
polygon's size.
Without feather it now rasterises the full-width band of rows the
polygon spans and combines over that band; Add and Subtract keep the
old selection outside it, Replace and Intersect leave it empty there,
exactly as the whole-canvas combine does. Full-width rows keep
`polygon`'s coverage bit-identical (its x arithmetic is relative to the
area's left edge, its y samples whole rows plus the same fractions);
a feathered lasso keeps the whole-canvas path, whose blur reaches past
the polygon. A test compares 24 random polygons, partly off the canvas,
in every mode, with and without anti-aliasing, against the old path.
Measured, a 2000-point lasso of a 400 px circle on 5472×3648: Replace
15–16 ms → 5 ms; Add to a selection unchanged (about 30 ms).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
select.colorRange takes {"select": "skinTones"}: a soft skin-tone model
in the YCbCr chroma plane (an ellipse along the skin run of Chai & Ngan,
1999) with a luminance gate, so near-black and near-white pixels drop
out. Fuzziness widens or narrows it as it does for Sampled Colors; the
selection modes, Invert and Sample All Layers work as for the other
presets. The dialog lists Skin Tones with Fuzziness enabled.
Detect Faces needs a face detector, which there is none of yet:
detectFaces: true is refused with a message instead of being ignored.
Fixes#4455
* fix(selection): keep Color Range inside active selection and reject out-of-bounds samples
* fix(ui): scope Color Range dialog and canvas previews to the active selection
* test(selection): cover Color Range scoping, sampling, invert, Add and undo
* test(ui): ensure Color Range preview and pipette honor existing selection
* test(selection): avoid Debug bound when asserting prepared-query error
* test(selection): validate malformed Color Range sample orders without selection interference
* Color Range: format, and scope the OK undo test's prior selection (#3313)
The existing ok_applies_one_undo_step test picked a prior selection that
excluded the red it then expected Color Range to select; with Color Range
scoped to the active selection it now selects the red inside it.
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>
* Channels: keep RGB off while an alpha or mask is shown, and select from that channel.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Channels: rustfmt the RGB-off view check.
Co-authored-by: Cursor <cursoragent@cursor.com>
* channel view: pass the active layer to render in main's new tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The blur itself was already parallel (9-15 ms on 24 MP); most of the
command was serial glue around it. Modify copied the mask into a padded
buffer and back out sample by sample even with no padding, and combine
built a full-canvas byte buffer, scanned it, then copied it into tiles
on one thread.
- modify: use the mask as is when the selection is clear of the canvas
edge (pad 0); pad and crop row by row otherwise.
- combine / mask_to_surface: fill selection tiles in parallel straight
from the mask, a row at a time; fully selected tiles share one
copy-on-write tile.
Bit-identical: the per-sample oracles (combine, now with whole tiles,
and the old pad/crop) are compared in tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Select › Modify › Feather (and the marquee, lasso and path feather
options) blurred with sigma = radius / 2, so a feather came out half as
wide as Photoshop's. Measured on Photoshop 27.11 (scripted): a 32×32
rectangle feathered 4 px matches sigma = radius within 2 levels along
the edge; sigma = radius / 2 was up to 71 levels off. The mask feather
(Properties › Feather) already uses sigma = feather, fitted on the
psd-tools corpus, so both feathers now agree.
The two margins sized for the old reach grow with it: Modify's
canvas-bounds padding (3 sigma = 3 r) and Fill Path's render area.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Refine Color Range preview sampling and add selection preview modes
* Avoid formatting ICC profiles on Color Range hover
* Color Range preview honours sampleAllLayers like OK; drop the env-var evidence/benchmark tests; Polish rows
The Sampled Colors preview always judged the composite while select.colorRange
judges the active layer for sampleAllLayers:false, so preview and OK could
disagree. It now reads the active layer's proxy pixels in that case (test:
preview_honours_sample_all_layers_like_ok). The two #[ignore]d tests that
wrote PNGs to COLOR_RANGE_EVIDENCE_DIR and timed a 24 MP hover are removed.
pl.tsv gets rows for the new strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
modify() rejected only a non-finite radius or one above the maximum, then
clamped with r.max(0.0). A negative or zero radius reported success: Border
cleared the selection, Expand and Contract did nothing, and Feather 0 was
accepted although its documented range starts at 0.1.
Each command now checks the range in its params string (Border 1..200,
Smooth/Expand/Contract 1..500, Feather 0.1..1000). A radius outside it,
including NaN and infinity, returns BadParams naming that range, and the
selection is left as it was. The clamp is removed.
modify() rejected only non-finite radii and radii above the dialog's
maximum, then clamped with r.max(0.0). A negative radius therefore reported
success while leaving the selection alone (Expand, Contract) or clearing it
(Border), against the function's own rule that out-of-range values are
"refused, never clamped silently". Negative radii now get the same
BadParams error, "radius must be a number in 0..max", and the selection is
untouched.
Test: modify_commands also tries -5, -0.5 and f64::MIN for all five
Modify commands.
Claude-Session: https://claude.ai/code/session_01K3vVft4VXvvy8SpbbUceTW
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Color Range: Photoshop's Sampled Colors model and the eyedropper on the canvas
Sampled Colors now compares colours in Lab (through the document's profile) the way
Photoshop does, reconstructed from Photoshop 2024's own masks (observation only):
per-axis distance outside the samples' Lab box in 8-bit units, a/b weighted x3, the
largest of the three, and a quadratic B-spline falloff of width fuzziness*256/255.
On 8-bit sRGB images it matches Photoshop to under one level of 255 on average
(the previous per-channel RGB distance was off by about 12). Several samples span
one Lab box, like Photoshop's Minimum/Maximum.
While Color Range is open, the pointer over the free canvas is its eyedropper:
a click picks that pixel, Shift adds to the sample, Alt subtracts (also through
ui.pointer on the control channel).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Color Range: Photoshop's Subtract from Sample, click order, 8-bit Lab grid
Measured through Photoshop's real Color Range dialog (eyedropper clicks):
- Subtract from Sample reshapes the samples' Lab box per axis: inside it the
nearer edge moves onto the subtracted value; on an edge or within the axis
width w it moves in to the first of m + k*w/5 beyond it (k <= 5); further out
nothing changes; an edge never crosses the other. Matches every measured box
to +-0.05. The click order matters, so select.colorRange takes an optional
`order` list and the dialog records it.
- The a/b centring shift of the previous change was an artefact of scripted
samples (Photoshop's descriptor Lab scale); with the eyedropper the sampled
colour is fully selected at any fuzziness. Removed.
- 8-bit documents compare pixels and samples on the 8-bit Lab grid, where an
a/b step above the samples' range counts half a unit more and one below half
a unit less (lab_range_coverage_8bit).
On Photoshop's eyedropper masks of a photo the mean error is ~0.46 levels of
255 at fuzziness 40/80 and ~0.02 at 5/10 (previous change ~0.9).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The margin Modify pads a canvas-edge selection with was twice the radius
for every operation, so Feather 1000 added 2002 px a side. It now pads by
each operation's reach (Feather 1.5 r, the others r) and refuses a padded
mask over 2^30 pixels instead of aborting on the allocation. The new
dialog option is translated in every shipped language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA
Contract, Smooth, Expand and Feather gain Photoshop's "Apply effect at canvas bounds" option
(applyAtCanvasBounds, default off). On, the canvas edge counts as a selection edge, so Select All
then Contract shrinks from the edges and Feather softens them; off, the selection continues past
the canvas, as in Photoshop. Border always follows the canvas edge, so a Select All borders to a
band inside it instead of an empty selection.
The mask is padded (empty or edge-replicated) only when the selection touches the canvas edge,
so selections clear of it cost nothing extra.
* Add the Magnetic Lasso tool
A selection border that snaps to edges as the pointer moves, in the
Lasso (L) flyout after the Polygonal Lasso.
- photocraft-algo `magnetic`: live-wire edge tracing (Mortensen &
Barrett 1995): Sobel strength, non-maximum suppression and gradient
direction link costs, a shortest path inside a corridor of the
detection width around the pointer's path, sub-pixel edge centres.
Pixels are read lazily in 128 px blocks.
- Engine: `EdgeSource` (the active pixel layer, else the composite) and
`select.magneticLasso`: rough points in, a border along the edges out;
`trace: false` selects a given outline, which is how the tool commits.
- UI: click to start, move to trace (button up or down), points fasten
by themselves (Frequency), click fastens, Alt-click straight, Alt-drag
freehand, Backspace/Delete removes points, click the first point,
double-click or Enter closes, Esc cancels, [ ] change the width.
Options bar: Width, Contrast, Frequency, tablet pressure. `ui.inspect`
reports the border in progress.
- The Lasso and Polygonal Lasso now apply the options bar's Feather;
`select.lasso` rejects a feather outside 0..1000.
- Icon adapted from Noun Project's "Magnetic Lasso Tool" by Lil' Seal
(CC BY 3.0, licence file and ATTRIBUTION.md row).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* it: translate the Magnetic Lasso strings (Italian landed after this PR was written)
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>
`commands::int` returns an unbounded i64, and the geometry call sites narrowed
it with `as i32`, which truncates by multiples of 2^32: layer.translate moved
a layer 50 px for `dx = 2^32 + 50` and to a large negative offset for
`dx = 3e9`; select.rect and document.pixel read unrelated coordinates;
image.crop cropped a rectangle unrelated to the numbers passed; artboard
rects landed elsewhere. No panic, no Err - the document was silently
mutated and then autosaved, and the behaviour is identical in debug and
release, so panic_hunt cannot see it either.
- new `commands::int_i32(cmd, p, key)` narrows with a bad-params error
instead of a wrapping cast; select.rect, document.pixel, layer.translate,
image.crop and the artboard rect keys go through it
- image.crop builds its rectangle with saturating_add (x + w overflow)
- edit.contentAwareFill's `area` parser returns a bad-params error for
malformed arrays (wrong length, non-numbers, non-finite) and saturating
arithmetic for huge ones - `[1e30, 0, 1e30, 10]` overflowed the i32
additions before (a debug-build panic, a garbage sampling window in
release)
- filters' integer helper clamps out-of-range values like the neighbouring
filter params instead of wrapping (`3e9` came back as a negative offset)
* Select › Color Range: Select presets, samples, localized clusters, invert
select.colorRange only did one sampled colour. Add the dialog's Select
presets (Reds … Magentas, Highlights / Midtones / Shadows, Out of
Gamut), several samples (colors / eyedropper points), the Subtract from
Sample eyedropper (subtractPoints), Localized Color Clusters with Range,
tonal Range, and Invert. Bad params are rejected without touching the
selection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Select › Color Range… opens a real dialog
The menu item ran select.colorRange immediately with the foreground
colour. It now opens a Color Range dialog: Select (Sampled Colors, the
hue families, Highlights / Midtones / Shadows, Out Of Gamut), Fuzziness,
Localized Color Clusters with Range, the tonal range sliders, the three
eyedroppers (on the preview; Shift adds, Alt subtracts), a grayscale
selection / image preview, Invert, OK and Cancel. Controls a mode
doesn't use are disabled. The preview and OK run the same engine
command, so the GUI, CLI and MCP select identically; OK is one history
step and Cancel leaves the selection untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dialogs shrink back when their body gets shorter
The OK / Cancel row was laid out right-to-left with Align::Center, so it
filled the height left over from the previous frame's size and the
dialog could only ever grow. In the Color Range dialog, switching from
Midtones (two extra sliders) to Out Of Gamut or Sampled Colors left an
empty band around the buttons. Aligning the row to the top keeps it
exactly as tall as the buttons, so the dialog fits its content again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>