View > Pixel Aspect Ratio and its Correction toggle only stored a value:
the canvas transform (ViewXform) had no notion of pixel aspect, so the
image was always drawn with square pixels.
ViewXform now carries the displayed pixel aspect ratio and stretches
document x by it before rotation and flip. Drawing, pointer-to-document
mapping, pan and zoom-about deltas, rulers, grid, Fit/Fill on Screen,
scrollbars, the Navigator's view box and paste-in-place visibility all
go through it. Correction is on by default, as in Photoshop. A corrected
view draws through the CPU canvas path, like a flipped one, since the
GPU shader assumes square pixels. The document's pixels are unchanged.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The web file dialog called rfd's pick_file() and ignored the request's
`multiple` flag, and FileDialogAnswer could only carry one file's
contents. Pick several files when File › Open asks for them, answer
with the new ContentsMany variant, and open each as its own document,
reporting a failed file without stopping the rest (as the desktop's
multi-path open does).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With several layers selected and no pixel selection, Cmd+J now copies every
selected layer in one undo step (each copy above its original, the copies
selected), reusing Duplicate Layer's multi-layer path. With a pixel
selection it still copies the active layer's selected pixels.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The schema dialog draws its fields in the order of the command's params
doc, which put Orientation last, below the three sliders. Photoshop's
Warp Text dialog has Style, then Horizontal/Vertical, then Bend,
Horizontal Distortion and Vertical Distortion. Reorder the params doc.
Live preview and opening with the layer's current warp already landed
in #2539 (after v0.6.0).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Crop / marquee size readout in the Units & Rulers unit (cm, mm, in…) as Photoshop shows it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Properties: Transform W/H/X/Y and Canvas W/H in the Units & Rulers unit (cm, mm, in…), typed values converted back to px
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge origin/main: size readout test follows the trimmed unit format
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>
* Free Transform on type: whole-layer frame, grey Distort/Perspective (#2630)
With a selection active, Free Transform on a type layer framed only the
selected part of the text and the preview lifted half of the rendered
glyphs, but the engine then transformed the whole type layer, so the box
and the result disagreed. Type, shapes and smart objects always move
whole, so transform_bounds no longer intersects their frame with the
selection and the preview no longer lifts pixels out of them.
Distort and Perspective stayed enabled for editable type and then failed
on commit. They are now greyed for type layers (menu bar, marquee and
transform-box context menus), as in Photoshop, unless an unlinked mask or
channel is targeted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* transform_type_tests: set CanvasToolMenu::pasteboard (#2630)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(engine): Resize Image During Place drives the implicit Place fit (#204)
* Merge origin/main; regenerate docs/scorecard.md
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>
The live ants for the Rectangular/Elliptical Marquee (and Object Selection)
were an axis-aligned screen box built from the two projected corners of the
drag. A rotated view projects the drag's document rectangle onto a rotated
one, and the box through two of its corners is a different shape in a
different place: on a 30 degree view a corner landed 21 x 50 px away from
where the selection it previews was committed. The release was always right,
only the hinting was drawn elsewhere (#2512).
marquee_drag_ants builds the outline in document space and maps every point
through the view instead: four corners for the rectangle, the ellipse
inscribed in it for the elliptical marquee, with as many vertices as its size
on screen asks for. Every point is snapped to the half-pixel grid like every
other ants outline (marching_ants_segments), so the lines stay crisp at any
zoom. Unrotated the mapping is only a scale, and the corners come out where
they always did.
Tests drag a marquee in a 30 degree view for both tools and check the drawn
outline against the selection the release commits: each corner is the mapped
corner of the dragged rectangle, and each point of the ellipse maps back onto
it.
Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
PAR_MIN_PIXELS / PAR_CHUNK_PIXELS (photocraft-cms) and OPENABLE
(photocraft-engine) are only used by native-only code (the rayon paths
and list_images). Give them the same cfg so wasm32 check and clippy
-D warnings are clean. No behaviour change.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Create Texture (inpaint::synthesize) averaged each block 50/50 into the
already-synthesised overlap. Averaging two unrelated grain patterns cuts
their standard deviation by about 1/sqrt(2), so the fill came out smoother
than its surroundings (75 % of the grain for one 30 px dab), and every
overlapping stroke, which samples the previous ones, lost more (57 % after
eight). Blocks are now joined along Efros & Freeman's minimum-error
boundary cut (Image Quilting, SIGGRAPH 2001): each pixel is copied from
one block or the other, never averaged.
Content-Aware and Proximity Match already keep the grain on main since
#2202 (patch copies instead of voting blur); the new test guards all three.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Wet Edges multiplied every dab by its own rim profile and max-accumulated the
dabs, so the rims of neighbouring dabs showed through the stroke as a chain of
rings. Dabs now accumulate normally and record how deep they reach each pixel
(1 - rn); the stroke keeps the deepest value per pixel and applies the
wet-edge transfer to its accumulated coverage when composited (live preview,
tail preview, commit and retouch coverage alike). The interior of the stroke is
evenly light and only the stroke's own rim is darker.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Showing, hiding or editing a smart filter re-rendered the whole stack from the
placed source on the UI thread: on a 3000x4498 layer with five filters every
click on a filter's eye froze the window for about 1.5 s with no feedback.
- Each render keeps the placed source and the result after every visible filter
(blake3 of the source contents, the placement and the filters so far), so an
edit reruns only the filters from the edited one up. Edit > Purge > All drops
them.
- Smart filter edits apply the edit at once (so bad params still fail
immediately) and re-render as a background job when started with
Session::start: the window keeps drawing, with progress and Esc to cancel
between filters. Session::execute is unchanged.
* Pencil/Eraser: round Pencil cursor and Eraser Pencil mode (#2662)
The Pencil cursor always outlined the square grid_square footprint, but the
aliased dab is a disc from 4 px on. It now draws a circle on the same
pixel-grid square when the tip is round, and the square for 1-3 px tips
whose dab really fills it.
The Eraser options bar gains Photoshop's Mode: Brush / Pencil (replacing
the blend-mode dropdown the Eraser never used). Pencil mode commits
paint.pencil with erase: true (hard, aliased, full flow), live and on
release, and shows the Pencil's cursor. Block mode is left for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Eraser: translate the Pencil mode label in every catalog
The Eraser's Mode dropdown added tl!("Pencil"), which had no entry in the
cs, el, ko, zh-hans and zh-hant catalogs, so every_tl_literal_is_translated
failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Eraser: Block mode (#2770)
Photoshop's Eraser Mode is Brush / Pencil / Block; #2694 added Pencil.
Block erases a hard, aliased square of 16 screen pixels, so 16 / zoom
document pixels, whatever the brush's size, hardness, opacity or flow.
- paint: BrushSettings.square (with aliased) rasterises the dab's whole
pixel-grid square (serde: omitted when false, so saved brushes are
unchanged).
- engine: paint.pencil "block": true builds that brush from the stroke's
"zoom" (brush_cmds::block_size: round(16 / zoom), 1..5000 px); a zoom
that is not a positive number is an Err. Params doc updated.
- ui: Mode dropdown gains Block; the Block-mode Eraser commits
paint.pencil with "block": true, shows the block's square cursor and
greys out Size/preset, Hardness, Opacity and Flow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Eraser: translate the Block mode label
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>
* feat(ui): the Dynamic Color Sliders preference controls the colour slider tracks (#204)
* Regenerate docs/scorecard.md
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>
* Speed up large layer group merges
* Merges: bound by composite_bounds so layer effects outside the pixels are kept
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>
* Pencil/Eraser: round Pencil cursor and Eraser Pencil mode (#2662)
The Pencil cursor always outlined the square grid_square footprint, but the
aliased dab is a disc from 4 px on. It now draws a circle on the same
pixel-grid square when the tip is round, and the square for 1-3 px tips
whose dab really fills it.
The Eraser options bar gains Photoshop's Mode: Brush / Pencil (replacing
the blend-mode dropdown the Eraser never used). Pencil mode commits
paint.pencil with erase: true (hard, aliased, full flow), live and on
release, and shows the Pencil's cursor. Block mode is left for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Eraser: translate the Pencil mode label in every catalog
The Eraser's Mode dropdown added tl!("Pencil"), which had no entry in the
cs, el, ko, zh-hans and zh-hant catalogs, so every_tl_literal_is_translated
failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A live gradient's dither adds a fraction of a level of noise, but gradient
fill layers kept it in float. Color Dodge divides by 1 - cs, so under a
bright dodge layer that sub-level noise came out as speckle several levels
high, and channels at 0 were lifted off black (coloured fringes).
Dithered gradient pixels are now rounded to the document depth's levels
(8/16-bit; float documents unchanged), as a painted gradient stores them.
Same rounding in the GPU shader. Regression tests in compose and a GPU
parity case for stacked dithered gradients under Color Dodge.
Claude-Session: https://claude.ai/code/session_01Qpa5z8GksXAFiDavFkmh4R
Co-authored-by: Leel Wasanatha <260480244+kavishkashehan726-sudo@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Wrap "PhotoCraft — Screen Color Picker" viewport title with tl!()
- Wrap "OK" button in widgets.rs with tl!()
- Add "PhotoCraft — Screen Color Picker" entry to all 14 language catalogs
Co-authored-by: seldoms <seldoms@Mac.lan>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A tap shorter than a UI frame left the stylus with no current sample by the
time the canvas ran: the platform had already cleared it (macOS proximity
out, a non-tablet button-up, the web's pointerleave) or set it back to
pressure 0 (X11 and macOS hover). The press then read as a mouse and painted
a full-size dab, or read the lift's 0. The same happened to the last moves of
a stroke that lifted in the frame they arrived in, which painted at full
pressure.
Stylus now drops zero-pressure samples from a frame that also had contact and
falls back to the frame's last contact sample when the feed was cleared or
reports 0. A tap (a drag still at its press point on release) paints at the
highest pressure seen since the press, so a tap spanning two frames is not
stuck at its first light sample. A mouse still paints at full pressure.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With Resample off, image.imageSize keeps the pixel dimensions and only
changes the resolution, but the dialog still accepted pixel Width/Height
edits and showed them in the Dimensions line and size estimate.
Now, as in Photoshop, with Resample off:
- width and height stay at the document's pixels (readouts included);
- in Pixels or Percent the Width/Height fields are disabled;
- in a physical unit (in, cm, mm, pt, pica) editing Width or Height
changes the resolution to fit that size, keeping the pixels;
- the constrain chain stays linked.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(ui): fit Layers opacity controls within narrow docks (#2309)
* Layers Opacity row: keep the label at the default dock width
Show the label while the blend dropdown keeps 80 px, so the default
Layers dock looks as before and only long translated labels on narrow
docks give way.
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>
Since #2167 the soft tip falls off as (1 - t^2)^4, whose 50% point is at
t = sqrt(1 - 0.5^(1/4)) ~ 0.399, but the Normal Brush Tip cursor still used
the old smoothstep midpoint r * (0.5 + 0.5 * hardness). Soft brushes got a
ring up to 25% too large (size 21, hardness 0: 5.25 px instead of 4.19 px).
photocraft_paint::half_coverage_radius inverts tip_falloff, and the cursor
uses it, so the two can't drift apart again.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Photoshop renames an action in place when its name is double-clicked in the
Actions panel. PhotoCraft had no way to rename an action at all, so every
action kept its "Action N" default name.
- New engine command `actions.rename {"action": name|index, "name": str}`.
Empty, whitespace-only and duplicate names are rejected (the old name
stays). Nested `actions.play` steps that call the action by name, and
its function-key binding, follow the rename. Refused during playback.
- Actions panel: double-clicking the name opens an in-place field with the
name selected; Enter, Tab or clicking elsewhere commits through the
command, Esc cancels. Double-clicking elsewhere on the row still plays.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Move tool: ⌘-drag temporary Move gets the drag box and snapping (#2681)
snap_ui::begin chose the snap gesture from app.ui.tool, the selected tool,
not the tool the press acts as. A ⌘-drag with a painting tool (temporary
Move) or with a selection tool outside the selection (moves the layer)
got no moving box and no smart-guide/guide/grid snapping.
The ⌘ resolution in canvas::tool_event moves into canvas::event_tool,
which begin now uses together with canvas::command_moves_layer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move tool: ⌘-drag with a painting tool floats the selected pixels from the control channel too (#2768)
`move_ui::moves_selected_pixels` asked `app.active_tool()`, which only sees the
held-key override the canvas sets while ⌘ is down. Control-channel, MCP and test
input send ⌘ on the event, so `tool_event` resolved the Move tool but the
selected-pixel checks still saw the Brush and moved the whole layer. The press
now asks with the event's tool (`canvas::event_tool`): the floating-piece drop,
Auto-Select pick, float, lock check and snap box.
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>
Fixes#2691. The hovered row of the Home screen's Recent list shows a ×
that takes the file off the list without opening it, and right-click
offers Remove from Recent and Clear Recent Files. The change is stored in
the preferences, so it survives a restart, and the file comes back when it
is opened again. `file.removeRecent {"path"}` does the same for agents.
Rows carry their file name as their accessible label.
* Actions: record File › Save and Save As steps and replay them (#2032)
Save and Save As run in the desktop shell (dialogs, TIFF prompt, background
save job), outside Session::execute, so they never reached the journal that
action recording copies from. The shell now journals `file.save` {} for a save
in place and `file.saveAs` {path, tiffLayers?} once a Save As has its path
(after the dialog or the TIFF Options answer). The engine treats both as
replayable shell steps; shell playback writes them synchronously without a
dialog. Batch and droplets drop recorded saves, since their destination folder
overrides them (Photoshop's "Override Action 'Save As' Commands").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Batch: skip recorded view steps in batch and droplets (#2752)
Actions recorded in the desktop app can hold the shell's view steps
(view.zoomIn, view.fitOnScreen, ...). Batch and Create Droplet checked
every step against the engine registry and rejected the whole action
with "unknown command". They now skip view steps, which don't change
the output, alongside the recorded saves #2746 already skips.
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>
Save and Save As run in the desktop shell (dialogs, TIFF prompt, background
save job), outside Session::execute, so they never reached the journal that
action recording copies from. The shell now journals `file.save` {} for a save
in place and `file.saveAs` {path, tiffLayers?} once a Save As has its path
(after the dialog or the TIFF Options answer). The engine treats both as
replayable shell steps; shell playback writes them synchronously without a
dialog. Batch and droplets drop recorded saves, since their destination folder
overrides them (Photoshop's "Override Action 'Save As' Commands").
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Esc deselects when no active operation needs cancelling (#2674)
* Esc: keep the selection while a canvas drag is in progress
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Guides: ⌘-drag over a guide moves it with the selection tools too (#2690)
⌘ is the Move tool with any tool, and the Move tool drags a guide under the
pointer before anything else. Painting tools already got this through the
held-⌘ Move override, but the marquees, lassos and Magic Wand keep their own
tool under ⌘ (`command_moves_layer`), so a ⌘-drag over a guide moved the layer
(or drew a lasso outline) and left the guide in place.
`canvas::command_guide_at` takes the guide on a ⌘ press with a selection tool
(same hit test as the Move tool: hidden or locked guides are not hit), the
guide drag snaps like the Move tool's (never onto guides, so not back onto
itself), the Lasso hands the drag's events to the canvas, and the cursor over
a guide is the guide's double arrow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Lasso: ⌘-drag outside the selection moves the layer (#2725)
`lasso_ui::pointer` took a ⌘ press outside the selection before
`canvas::tool_event` reached `command_moves_layer`, so it drew an outline;
`canvas_input` did not forward the drag's moves and release either. The
Lasso now hands such a press to the canvas (the temporary Move tool) and
feeds it the rest of the drag, also when a locked layer refuses the move.
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 Layers panel blend-mode list previews the hovered mode, but egui's
item spacing leaves a gap between rows that hovers no row, so moving the
pointer down the list flashed the layer's current mode back at every gap.
While the pointer is over the open list and between rows, report the
nearest row as hovered, so the preview moves straight from one mode to
the next.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
⌘ is the Move tool with any tool, and the Move tool drags a guide under the
pointer before anything else. Painting tools already got this through the
held-⌘ Move override, but the marquees, lassos and Magic Wand keep their own
tool under ⌘ (`command_moves_layer`), so a ⌘-drag over a guide moved the layer
(or drew a lasso outline) and left the guide in place.
`canvas::command_guide_at` takes the guide on a ⌘ press with a selection tool
(same hit test as the Move tool: hidden or locked guides are not hit), the
guide drag snaps like the Move tool's (never onto guides, so not back onto
itself), the Lasso hands the drag's events to the canvas, and the cursor over
a guide is the guide's double arrow.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): expose painting symmetry for all themes (#2659)
Render the existing symmetry control for Brush, Pencil and Eraser
independent of palette. Add a per-theme, per-tool UI visibility test.
* rustfmt
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>
Scripts (File › Scripts › Browse, Script Events) ignored the `failed` entry that
`actions.play` returns when a step inside the called action fails, so they reported
ok and kept going. Batch and droplets ran each file in a scratch session without
the actions list, so any nested action call failed with "no action named". The
scratch session now gets the caller's actions and playback stack (an action that
batches itself is caught as recursive), and both runners check nested_failure.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Dragging inside a selection with a marquee or lasso (or ⌘-dragging its
pixels as a floating piece) now starts a SelectionMove snap gesture over
the selection's bounds, like the Move tool's box, instead of snapping the
pointer as a marquee corner (marquees) or not snapping at all (lasso).
The selection's own edges are left out of the targets.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Smart Objects: converting several layers keeps them at the contents top level (#2648)
Convert to Smart Object with several selected layers wrapped them in a
temporary group before embedding, so Edit Contents showed one group named
after the top layer. Photoshop puts the selected layers at the top level of
the embedded document. The embedding now goes through layers_to_smart, which
takes the layers directly; a pass-through root composites the same, so the
smart object looks unchanged. A single selected group still converts as is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Smart Objects: stacked layers sit at the contents top level (#2657)
Load Files into Stack with Create Smart Object, and Statistics with
alignment, put the loaded layers in a temporary group before embedding,
so the smart object's contents held one group named after the first file.
Photoshop puts one layer per file at the top level of the contents. Both
call sites now pass the layers to layers_to_smart. The wrapper was an
opaque Normal group of opaque Normal layers, so the composite is the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(prefs): add 80–95% UI scale presets (#2492)
Offer finer five-percent steps between 75% and 100% for small screens.
Exercise the new values through the live preference and egui scaling test.
* Label the 80-95% UI scale choices as percentages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Translate the 80-95% UI scale labels in every catalog
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>
Convert to Smart Object with several selected layers wrapped them in a
temporary group before embedding, so Edit Contents showed one group named
after the top layer. Photoshop puts the selected layers at the top level of
the embedded document. The embedding now goes through layers_to_smart, which
takes the layers directly; a pass-through root composites the same, so the
smart object looks unchanged. A single selected group still converts as is.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
snap_ui::begin chose the snap gesture from app.ui.tool, the selected tool,
not the tool the press acts as. A ⌘-drag with a painting tool (temporary
Move) or with a selection tool outside the selection (moves the layer)
got no moving box and no smart-guide/guide/grid snapping.
The ⌘ resolution in canvas::tool_event moves into canvas::event_tool,
which begin now uses together with canvas::command_moves_layer.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Copy and paste a single channel from the Channels panel (#2307)
With a colour channel (Red, Green, Blue, ...) targeted, Copy copied the whole
layer and Paste made a new layer, so a channel could not be copied into
another one. Now, as in Photoshop:
- Copy with a colour or alpha channel targeted copies that channel's values
as a grayscale clip, inside the selection or across the whole canvas. With
an alpha channel targeted, Copy needs no pixel layer.
- Paste, Paste in Place and Paste Into with a colour channel targeted write
the clip's luminosity into that channel of the active layer, in one undo
step. The other channels and the layer's transparency are left alone, and
a pixel lock refuses the paste.
The logic lives in a new channel_clip module; Copy, Paste and Paste Into only
call it. Cut keeps its old behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Channels: Cut with one channel targeted clears only that channel (#2698)
With a single colour channel (e.g. Red) targeted in the Channels panel,
Edit > Cut copied and cleared every channel of the layer: `edit.cut` is not
one of the channel-routed commands, so it always ran the whole-layer path.
Now, as in Photoshop, Cut with a colour channel targeted copies that channel
as a grayscale clip (the channel Copy from #2307) and fills the cut area of
that channel only with the background colour's value for it, weighted by
selection coverage. The other channels and the layer's transparency are left
alone, and the whole cut is one undo step. With the composite targeted, Cut
is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cut on a colour channel: reuse clear_color_channel from #2730
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Fabian Ghenof <fabianghenof@techie.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
snap_ui::begin ran on the press before the Move tool's Auto-Select
layer.pickAt, so the drag box, the snapped rect and the excluded targets
were the previously selected layers'. Another layer's box moved instead,
and with an empty layer or a group selected there was no box and no
snapping. Re-run it when the pick changed the selection.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With a colour channel (Red, Green, Blue, ...) targeted, Copy copied the whole
layer and Paste made a new layer, so a channel could not be copied into
another one. Now, as in Photoshop:
- Copy with a colour or alpha channel targeted copies that channel's values
as a grayscale clip, inside the selection or across the whole canvas. With
an alpha channel targeted, Copy needs no pixel layer.
- Paste, Paste in Place and Paste Into with a colour channel targeted write
the clip's luminosity into that channel of the active layer, in one undo
step. The other channels and the layer's transparency are left alone, and
a pixel lock refuses the paste.
The logic lives in a new channel_clip module; Copy, Paste and Paste Into only
call it. Cut keeps its old behaviour.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Lasso reads its own pointer events (lasso_ui::canvas_input) and the canvas
cleared the generic drag buttons for it. With a Free Transform box open, the
press reached the box but the moves and the release were dropped (no lasso
outline was in progress), so the box never changed. The open box now owns the
pointer with the Lasso too, as it already does for the Hand (#2302).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>