Commit Graph
540 Commits
Author SHA1 Message Date
Sir Starch b66ebb84d2 fix(type): count normalized insert text caret (#3074) (#3138) 2026-10-11 16:31:51 -04:00
c234865b17 fix(type): respect Lock All on text edits (#3092) (#3132)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:26:10 -04:00
Sir Starch a893113502 fix(gradient): respect Lock All on Gradient Fills (#3093) (#3120) 2026-10-11 16:19:40 -04:00
Valentin KleinandClaude Opus 5.5 8458cf7095 Transform: Distort, Perspective and Warp keep shape layers live (#3009) (#3025)
Edit › Transform › Distort / Perspective and Warp on a shape layer let the
user edit the box or mesh and then refused at commit ("needs a rasterized
shape"). Photoshop maps the shape's path and keeps it a shape.

- photocraft-vector: new `map` module. `map_points` sends every anchor and
  control point through a map (Distort / Perspective: exact for straight
  segments, exact end tangents for curves). `map_path` follows a curved map
  (Warp), halving each segment until the mapped control points follow the
  mapped curve within a tolerance (at most 64 pieces a segment, bounded in
  total); straight segments whose image is straight stay straight.
- engine: `transform_layer` and `warp_layer` map a shape's path instead of
  refusing; a live rectangle/ellipse/polygon/line becomes a plain path. A
  linked vector mask now follows Distort / Perspective / Warp too. A map that
  sends a point to infinity is an error, not a corrupt path.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:16:04 -04:00
Sir Starch f435eb0b55 fix(paint): respect Lock All on layer masks (#3094) (#3111) 2026-10-11 16:15:08 -04:00
5f55a29d2b Color: honour the EXIF sRGB tag on open; wire Ignore EXIF Profile Tag (#204) (#3026)
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>
2026-10-11 16:07:03 -04:00
b1900ace28 Installed ICC profiles in the profile menus, and Mirror in Print (#3011)
* Installed ICC profiles in the profile menus, and Mirror in Print

- photocraft_engine::installed_profiles: scans the OS colour profile
  folders (Windows, macOS, Linux), bounded and cached; new
  edit.installedProfiles command.
- Print's Printer Profile and the Proof Setup, Convert to Profile and
  Assign Profile dialogs list the installed profiles after the
  built-ins; a profile given by path that isn't listed shows by name.
- file.print emulsionDown mirrors the page (PDF and preview), with a
  Functions › Emulsion Down (mirror) checkbox.
- Print dialog: cm/mm/in, Height and Width fields, Print Resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Print dialog: translate Emulsion Down, Functions, Print Resolution in every catalog; cargo fmt

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>
2026-10-11 15:55:11 -04:00
67747dfd02 Type: Warp on live type takes the warp styles and stays editable, no prompt (#3004) (#3008)
* 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>
2026-10-11 15:35:47 -04:00
KnottyDyes 2d6ff3f6cc Inspect: report each layer's vector mask (#2980)
`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.
2026-10-11 15:21:03 -04:00
28874bbfad Select and Mask: Output To New Document, with or without a layer mask (#2998) (#3007)
* Select and Mask: Output To New Document, with or without a layer mask (#2998)

select.refineEdge takes output newDocument / newDocumentWithMask: a new
document like the source (size, mode, depth, resolution, profile) with
the active layer cut out by the refined edge, or with the matte as a
layer mask. The source document is left untouched. The dialog's Output
choice lists all six targets, translated in every catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n: drop the duplicate pt-br New Layer row (main already has 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>
2026-10-11 15:16:45 -04:00
8a72978549 File Handling: wire Image Previews to embed a PSD thumbnail (#204) (#2971)
* File Handling: wire Image Previews to embed a PSD thumbnail (#204)

Preferences > File Handling > Image Previews is live for PSD/PSB saves. With
Always Save (the default) and Ask When Saving, the file carries Photoshop's
thumbnail image resource (1036: the 28-byte kJpegRGB header and a JFIF stream,
at most 160 px on the long side); Never Save writes none. The thumbnail is the
reduced composite converted to sRGB through the document's profile with
photocraft-cms (8/16/32-bit; Gray, CMYK and Lab become RGB), over white where
transparent. Ask When Saving has no Save As checkbox yet, so it saves the
preview, as Photoshop's checkbox starts on.

The preference reaches Save, Save As, Save a Copy and automation saves in the
app (ExportSettings.image_previews), and the engine's headless saves
(file.saveACopy, batch, Layers/Comps/Artboards to Files) through SaveOpts.
photocraft_io::ExportOptions gains image_previews (default on).

Settings that do nothing: 32 of 153 (#204).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* File Handling: rustfmt the Image Previews save path

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>
2026-10-11 15:14:49 -04:00
Valentin KleinandClaude Opus 5.5 b54fdfd94f Tests: panic_hunt reruns a timed-out command before calling it hung (#2994) (#2997)
The first type.create starts the shared text engine, which scans every
system font once (3.5-5 s under load). panic_hunt billed that to
type.create against its 4 s deadline and failed intermittently. A real
hang reproduces, one-time setup does not: a timeout now counts only if a
rerun on a fresh session times out too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:54:14 -04:00
Valentin KleinandClaude Opus 5.5 139df7ae85 Type: variable font axis sliders; optical size follows the type size (#2985) (#2996)
The Character section (Properties and Window > Character) shows one slider
per axis of the style's variable font below the font style (Weight, Width,
Slant, Italic, or the font's own tag), setting type.setStyle variations; a
drag is one history step. Optical size has no slider: the layout now sets
opsz to the type size unless the style sets it, as CSS font-optical-sizing
auto. Picking a font style or weight resets the wght axis to it, so an
earlier slider value no longer overrides the style the dropdown shows.

Scorecard TYPE-218-4 -> done.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:46:26 -04:00
a35af4e279 Add FITS read/write (astronomy images) (#2956)
* Add FITS read/write codec

FITS (the astronomy format) opens and saves as a flat image: the primary
HDU or first IMAGE extension; gray, or RGB when NAXIS3 = 3; BITPIX 8/16/
32/64/-32/-64 with BSCALE/BZERO/BLANK; rows follow ROWORDER (bottom-up by
default). Camera U16 data stays exact; float data is normalised to 0..1.
Header cards round-trip as text metadata (HIERARCH for long names).
Tile-compressed (fpack) files are refused as unsupported.

Wired into File > Open, Save As and the batch commands as .fits/.fit/.fts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test FITS open and save through photocraft-io

Gray 16-bit, RGB 8-bit and RGB 32-bit float documents round-trip exactly,
and header cards travel as the document's text metadata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* FITS: hostile-header regression test (absurd axes, bad BITPIX, no END, HDU chains, overflowing sizes)

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>
2026-10-11 13:15:22 -04:00
9441484d16 Quick Mask: an empty start and Select All stay apart (#2743) (#2922)
* Keep Quick Mask's empty start and Select All apart (#2743)

Quick Mask stored "no selection" as an all-white mask, the same as Select
All. With Color Indicates Selected Areas that white was coloured over the
whole canvas, and leaving Quick Mask turned any all-white mask (a real
Select All too) into no selection.

The document now records that Quick Mask was entered with no selection.
The empty mask is white for Masked Areas and black for Selected Areas, so
neither shows colour; switching the option on an untouched empty mask
keeps it empty. On exit only that untouched white start becomes no
selection, so Select All survives the round trip. The flag is saved with
the document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Quick Mask tests: pass the active layer to channel_view::render (new main signature)

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>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 12:51:18 -04:00
Myosh1 3f0970923d Add built-in rainbow gradient presets (#2916)
Add a "Rainbows" group to the built-in Gradients panel with five presets: Rainbow,
Pastel Rainbow, Neon Rainbow, Oil Slick and Rainbow Fade. The three hue ramps sample 13
evenly spaced hues (red 0 deg to magenta 300 deg) so the transitions are smooth rather
than the seven coarse ROYGBIV points; Rainbow Fade reuses the classic ramp with
transparent ends. All colours are original (clean-room).

Covered by a new engine test, presets::tests::rainbow_gradient_presets.
2026-10-11 12:18:19 -04:00
David MannandClaude Opus 5.5 66c0b59a23 Layers: Combine Shapes merges the selected shape layers (#2400) (#2906)
Layer › Combine Shapes › Unite / Subtract Front Shape / Intersect Shape
Areas / Exclude Overlapping Shapes only changed the operations between the
components of the active layer. With two single-shape layers selected (the
report's case) the active layer had one component, so nothing happened.

With two or more shape layers selected and no `layer`/`subpath` param, the
commands now merge them into the top-most layer in one history step: the
bottom layer's components keep their operations and every component of the
layers above takes the chosen one, in stacking order, then the other layers
are removed. Merge Shape Components merges them the same way (each layer
combined) and bakes the result. Selected non-shape layers are left alone,
locked layers refuse the merge, and one selected layer behaves as before.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 12:05:57 -04:00
Valentin KleinandClaude Opus 5.5 c1bd296dcf Select and Mask: Refine Edge Brush (#2972) (#2989)
* Select and Mask: live preview in Photoshop's view modes (#1754)

The Select and Mask dialog had no preview of the refined selection. It now
previews live in Photoshop's view modes: Onion Skin (default), Marching
Ants, Overlay, On Black, On White, Black & White and On Layers, with a
per-view Opacity, Show Edge (J) and Show Original (P). F cycles the views,
O/M/V/A/T/K/Y pick one and holding X shows the image without a view.

The view state lives in the dialog's fields, so ui.dialog.set drives it.
The preview runs select.refineEdge on the proxy with output=selection and
builds the view from it (veil fill layers, a layer mask, or the refined
selection, whose marching ants the canvas draws scaled back up), through
the existing filter-preview worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Select and Mask: Refine Edge Brush (#2972)

Select and Mask had no Refine Edge Brush. select.refineEdge now takes
refineStrokes: [{"points":[[x,y],...],"size":px,"mode":"add|subtract"}],
applied in painting order. Added areas become unknown and take a colour
estimate: each pixel's colour projected on the line between the mean known
foreground and background colours around it (normalised convolution over
a box of the brush size, four times as far where none are near), so hair
or fur beyond the edge-detection band joins the selection. Subtracted
areas keep the original edge. Malformed or oversized strokes are rejected
(stroke / point / swept-area limits), never a panic.

The dialog gets the brush: Refine Edge Brush (R), Mode (Expand Detection
Area / Restore Original Edge, Alt swaps), Size ([ and ]) and Clear Brush
Strokes. With it on, dragging on the canvas paints strokes into the
dialog's refineStrokes field, so the live preview, Show Edge and OK use
them, and ui.dialog.set / agents drive them like any other param.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:00:25 -04:00
5025fd2895 Color Sampler tool (#1046) (#2753)
* Color Sampler tool (#1046)

Add the Color Sampler to the Eyedropper (I) flyout: click to pin numbered
sample points to canvas pixels, drag to move, Alt-click (or drag off-canvas)
to remove, Clear All in the options bar, at most 10 per document. Each point
reads one composite pixel (RGB 0-255 and naive device CMYK, same as the
pointer readout) and the Info panel lists them under their number with the
position in the ruler unit; the readouts refresh when the document changes.

Points are per-document engine view state (DocState.color_samplers): they
belong to their document, are discarded on close, and are not saved to
.pcraft or PSD. They are driven by commands shared by the UI, CLI, control
channel and MCP (view.colorSamplers.add/.move/.delete/.clear/.list); the
mutations change no pixels and add no History step.

Points follow the canvas geometry (crop, Canvas Size, Image Size, Image
rotation/flip, rotated crop) through a pending Session.sampler_map that
Session::edit applies before culling the points that land outside the new
canvas; the count removed is reported in the status bar.

- engine: new sampler_cmds.rs (commands + shared readout helpers) and the
  sampler_map wiring in lib.rs, image_cmds.rs, mode_cmds.rs, commands.rs,
  analysis_cmds.rs (compound rolls points back on error).
- ui-egui: Tool::ColorSampler + original sampler.svg icon, toolbar slot,
  Info panel list, options bar, pointer handling, overlay markers, cursor.
- docs/scorecard.md regenerated (TOOL-1649-1 no longer says the tool is
  missing). No menu item, so parity is unchanged (627/627).

* Color Sampler follow-up: tool count, README, i18n, roadmap

The full ui-egui suite (not just the filtered tests) surfaced these gaps:

- state.rs: Tool::ALL.len() assertion 52 -> 53.
- README.md: "52 tools" -> "53 tools" and add Color Sampler to the list,
  so readme_tool_list_matches_tool_all passes.
- i18n: the options-bar hint, "{n} of {max} sample points" and
  "No document open" need entries in every catalog that claims complete
  menus (every_tl_literal_is_translated); added to all 16 (coverage
  1584/1584). The translations were drafted alongside the tool.
- docs/roadmap.md: drop Color Sampler from the missing-tools list and note
  it landed.

* Color Sampler: fit main's new tools and Image Size resample type

Reset Tool covers ColorSampler, the test ViewXform sets aspect, and
Image Size checks Method::None instead of an Option.

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>
2026-10-11 11:43:09 -04:00
buivanvu94 6a2aaebe99 fix(ui): unchanged Custom Proof Setup replaces active document proof profile (#3213)
Populate View › Proof Setup dialog fields from the active document's
current proofing condition rather than schema defaults or remembered
values from other documents. This ensures opening Custom Proof Setup and
confirming unchanged preserves the active proof profile and condition.

Fixes #3055.
2026-10-11 11:18:41 -04:00
c0110fb4d2 fix(engine): allow targeting layer mask and vector mask via channel.target (#2458) (#2705)
* fix(engine): allow targeting layer mask and vector mask via channel.target (#2458)

* fix(ui): apply channel target actions before syncing mask targets in channels panel

---------

Co-authored-by: huudinh1401 <huudinh1401@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 11:06:57 -04:00
d45b3718ce New Document: presets follow Units & Rulers › Preset Resolutions (#204) (#3126)
Preferences › Units & Rulers › New Document Preset Resolutions did nothing:
Print Resolution was hidden as unread, and the File › New presets always used
300 / 72 ppi. As in Photoshop, print and photo presets now keep their size in
inches at the Print Resolution (Letter at 200 ppi is 1700 x 2200 px), and
screen presets keep their pixels at the Screen Resolution. The Clipboard
preset keeps the copied pixels. Values that are not positive and finite (a
hand-edited preferences file) leave the preset as listed, and sizes clamp to
the file.new limit.

Print Resolution is now shown in the Preferences dialog; 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>
2026-10-11 10:15:40 -04:00
Valentin KleinandClaude Opus 5.5 1a54384d80 Rulers: remember View › Rulers for new documents; guides as f64 (#2918) (#2954)
View › Rulers (⌘R) now writes unitsAndRulers.showRulers, which is applied
at launch and whenever the preference changes, so new and opened documents
show the rulers as last set, as in Photoshop. The Preferences dialog doesn't
list it: the View menu sets it.

Guides are Vec<f64> (doc, New Guide/Move Guide, guide layouts, artboard and
canvas geometry, snapping, PSD resource 1032), so a guide placed in ruler
units or read from a PSD's 1/32 px positions keeps its exact place.

Scorecard UI-217-20 done (part of #217).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:05:13 -04:00
1234ba4c6f Type: wire Show Font Names in English (#204) (#2981)
Preferences > Type > Show Font Names in English (default on) now drives the
font menus (Type options bar, Character panel, style and Glyphs panels). With
it off, a family whose name table carries a non-English family name (e.g.
"MS Gothic" -> "MS ゴシック", "Microsoft YaHei" -> "微软雅黑") is listed by
that native name, sorted by the shown label, and the search matches either
name. Choosing it still applies the English family, so documents and PSD
output never change with the preference.

- text: FontDb::localized_family_names() reads each family's first face
  (typographic family name, else family name) and a pure localized_name()
  picks the first non-English record.
- ui-egui: sync_font_names() publishes the labels to the egui context once a
  frame (cached per font generation); family_picker_in shows them.
- prefs: the row is no longer hidden; scorecard: dead settings 43 -> 42.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 09:21:55 -04:00
Valentin KleinandClaude Opus 5.5 7c96e9e120 Layer Style: contour picker and Contour Editor (#2912) (#2949)
Every effect with a contour (Drop/Inner Shadow, Outer/Inner Glow, Satin,
Bevel & Emboss Gloss Contour and Contour element) now shows a contour
thumbnail with a preset picker and an Anti-aliased checkbox. Clicking the
thumbnail opens the Contour Editor: add/drag/delete points (the shared
point-curve editor), Corner per point, Input/Output, Preset, and Load…/Save…
of .shc contour files, read and written from Adobe's public File Formats
Specification (Contours). Edits go to the dialog's pending params, so the
preview follows and OK applies them in the one layer.layerStyle.replace.

- doc: Contour::Custom gains per-point corner flags (serde default).
- compose: contour_curve_lut splits the spline at corner points; CPU and
  GPU effects use it.
- engine: the 12 standard presets with our own control points (older names
  still accepted); `contour` params also take a curve object
  {name, points, corners}; antiAlias params; Satin contour; the bevel
  Contour element edits in place; contours_from_shc / contour_to_shc.
- io: PSD contour points read and write the Cnty (continuity) flag.
- psd: shc.rs parser/writer.
- scorecard FILE-215-15 missing -> partial.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:21:18 -04:00
6601bbc3e7 Layers: Pattern Fill dialog for new and edited pattern fill layers (#2908) (#2979)
* Layers: Pattern Fill dialog for new and edited pattern fill layers (#2908)

Layer > New Fill Layer > Pattern... created the layer with default
settings and no dialog, and double-clicking a Pattern Fill thumbnail only
opened Properties, which has no pattern controls, so a pattern fill could
not be adjusted after creation.

Both now open Photoshop's Pattern Fill dialog: the pattern swatch picker,
Snap to Origin, Angle, Scale and Link with Layer, previewed live on the
canvas from a document snapshot. OK runs one engine command, so it is one
History step; Cancel changes nothing (a new layer is not created).

- engine: new `layer.fill.pattern.set` (pattern_fill_cmds) edits a Pattern
  Fill's pattern, scale, angle, link and phase in one undo step, validating
  every param; `preview` applies it or `layer.newFillLayer.pattern` to a
  copy of the document for the dialog. `new_fill_layer`'s edit is shared
  as `insert_fill_layer` so the preview is exactly what OK commits.
- ui: pattern_fill_ui (dialog body, OK, canvas preview), routed from the
  menu item (no params) and Layer Content Options; explicit params still
  run the engine directly. Catalog entries for the new strings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Pattern Fill test: pick the swatch picker's Patterns button (main added a Patterns dock tab)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 09:16:18 -04:00
10fb9233d5 Tools: wire Enable Flick Panning for the Hand tool (#204) (#2964)
* Tools: wire Enable Flick Panning for the Hand tool (#204)

Preferences > Tools > Enable Flick Panning (default on) was never read.
Like Photoshop, a Hand drag (the selected Hand, the Space-held Hand or a
middle-button drag) released while the pointer is still moving now keeps
the view gliding that way and slows smoothly to a stop; a click, new drag
or key press stops it. Off, the view stops where the drag ended.

flick_pan.rs holds the pure state: the release velocity from the last
80 ms of drag steps, an exponential friction step (tau 0.18 s, so a fast
flick rests in under a second), a start/stop speed threshold, and clamps
for huge, NaN or infinite numbers. The canvas drives it per frame with
request_repaint only while gliding; with Overscroll off the glide is
clamped like the scrollbars and stops at the pan limit.

Settings that do nothing: 43 -> 42 of 149 (docs/scorecard.md).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Preferences: show Enable Flick Panning now that the Hand tool reads it (#204)

Remove tools.enableFlickPanning from prefs::HIDDEN_UNTIL_IMPLEMENTED so the
prefs_usage test passes and the dialog shows it; regenerate the scorecard
(settings that do nothing: 36 of 153) and mark flick panning done in
docs/ui-parity.md.

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>
2026-10-11 09:03:11 -04:00
cb9513c718 Channels: keep RGB off while an alpha or mask is shown, and select from that channel. (#2555)
* 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>
2026-10-11 09:03:05 -04:00
026432815c Preferences: wire Tools › Show Transformation Values (#204) (#2953)
* Preferences: wire Tools › Show Transformation Values (#204)

The size and angle readouts beside the pointer (marquee W/H, Crop frame
W/H and angle, Crop Straighten angle) now go through
canvas::draw_value_readout, which draws them only while
tools.showTransformationValues is on, as in Photoshop. The brush HUD
isn't a transformation value and keeps drawing.

The preference leaves HIDDEN_UNTIL_IMPLEMENTED, so Preferences › Tools
shows it (its label is already translated). Settings that do nothing:
43 → 42 of 149.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Contributors: add my display name

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>
2026-10-11 08:57:49 -04:00
55a617cd66 Place: wire Always Create Smart Objects When Placing (#204) (#2543)
* feat(engine): Always Create Smart Objects When Placing gates the Place layer kind (#204)

* scorecard: regenerate after wiring Always Create Smart Objects When Placing

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>
2026-10-11 08:51:33 -04:00
a0ff3a01f2 Smart Objects: linked smart objects follow their files while open (#2510)
* Smart Objects: linked smart objects follow their files while open

As Photoshop documents ("Linked Smart Objects update automatically when
the Photoshop document is open, provided the source file has changed"),
a linked smart object whose file another app saves is re-rendered
without Update Modified Content. The desktop app stamps the active
document's linked files (size + modification time) on a worker thread
every two seconds and when its window comes back to the front; each
changed file is one Update Modified Content undo step, independent of
the selected layer. Stamps are kept per document; a file that can't be
read yet leaves no step and is tried again; files changed while the
document was closed still wait for Update Modified Content.
layer.smartObjects.updateChanged runs one look (refused over the
control channel like the other layer.smartObjects commands).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Smart Objects: a linked file that can't be read is tried again once it changes, not on every look

A linked file the automatic update couldn't read (still being written, or a
format smart objects don't read) kept its old stamp, so every two-second look
saw it as changed and read it again on the UI thread. It now keeps its new
stamp and is tried again once it changes again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Katt <238968166+KattKnight@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 08:42:52 -04:00
UnemarsandClaude Opus 5.5 0b4b1e4286 Image Size: Photoshop's resampling, measured (#2451)
* Image Size: Photoshop's resampling, measured

Photoshop 25.4's Image Size was measured as a black box (synthetic 16-bit
images in, exact RAW out); every method but Preserve Details' enlargement is
linear, so each was taken apart from its impulse responses.

- Bicubic is the Keys cubic at a = -0.75, not Catmull-Rom.
- Bicubic Smoother (stretched x1.15, a = -0.625) and Bicubic Sharper
  (x1.05, a = -1), with a growing with the reduction, and Bicubic Automatic.
- Reducing widens the filter by 1/s; below 1/4 the image is first averaged
  down to four times the target. Bilinear is never widened and averages
  whole pixels below 1/2. Positions are rounded to 1/128 pixel and stop at
  the edge pixel's centre.
- Two passes, the smaller scale first, clipped and stored between them; the
  bicubic filter is designed for the height's scale and serves both axes.
  Bicubic Automatic splits a mixed resize into Sharper, then Smoother.
- Image Size and Fit Image take automatic, bicubicSmoother, bicubicSharper
  and bicubicAutomatic; the dialog lists Photoshop's methods and opens on
  the interpolation preference.

On 225 probes the mean error falls from 240-550 to under 0.5 levels of
32768; 8-bit results are identical to Photoshop's for 99 % of values.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Format: one rustfmt change main needs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 08:25:21 -04:00
Valentin KleinandClaude Opus 5.5 7107c80042 perf: pasting a layer recomposites only where it draws (#2901) (#2917)
* perf: Duplicate Layer recomposites only where the copy draws (#2883)

Duplicating a layer reported no damage, so the canvas recomposited the
whole document after every Duplicate Layer (P8/P9). The copy itself was
already cheap (tiles are shared Arcs). Report the copy's change bounds
instead, through the step_damage walk extended to bounded additions:
self-contained added layers (not an adjustment, not a new clipping base,
no video or excluded channels) contribute their change bounds; anything
else still recomposites everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf: pasting a layer recomposites only where it draws (#2901)

Paste (and Paste in Place, Paste Into, Paste Outside) inserted the
pasted layer through Session::edit, which reports no damage, so every
paste recomposited the whole document (P10/P11). Capture the document
before the paste and report the new layer's change bounds with
note_insert, as Duplicate Layer does (#2884). Anything that isn't
self-contained (a new clipping base, video, excluded channels) still
recomposites everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:24:51 -04:00
ec28fdd03d Add Photoshop-style guide layout dialog and live preview (#2930)
* feat(ui): add guide layout dialog with live preview

* test(ui): restore path helper used by save dialog assertions

* chore(i18n): avoid conflicts with appended catalog entries

* chore: avoid append conflict for contributor credit

* Merge main: one people.toml entry for Skyfuller1303, drop the duplicate joined helper

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>
2026-10-11 08:23:03 -04:00
Valentin KleinandClaude Opus 5.5 42e4af6416 Tools: Frame tool (#2914) (#2915)
The Frame tool (K, its own toolbar slot after the Crop group) draws a
rectangular or elliptical frame; the options bar picks the shape, Shift
constrains to a square or circle and Alt draws from the centre. A frame
drawn over the selected pixel layer's content takes that layer in;
otherwise it is an empty frame.

Frames are built as Frame from Layers builds them: a group clipped by a
layer mask (rectangular or anti-aliased elliptical). The new engine
command layer.new.frame does it in one Session::edit, so one undo step.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:20:15 -04:00
Valentin KleinandClaude Opus 5.5 614d06892c perf: marquee selections on large documents are built row by row (#2888) (#2907)
The Elliptical Marquee tested and wrote every pixel one at a time
(Surface::write_pixel), single-threaded, with no shared full tiles. The
add/subtract/intersect modes read both selections over the whole canvas
into f32 buffers. On 6000x4000 an ellipse took ~0.6-1.1 s and a mode
combination ~0.3-0.5 s.

ellipse_surface builds the shape per row: a run of fully selected pixels
(bounded analytically, checked with the exact f32 tests at both ends)
and edge pixels that get the original per-pixel coverage test. Tiles are
built in parallel, and fully selected tiles share one tile.
combine_surfaces maps two GRAY8 selections tile by tile through a
256x256 byte table, sharing tiles that come through unchanged. Both
produce exactly the old bytes (equivalence test against the old
per-pixel code).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:11:17 -04:00
a874b149f2 Themes: Photoshop's light-gray and light Pro brightness levels (#2661) (#2892)
Photoshop has four interface brightness levels; PhotoCraft had the two
darkest (Pro, Pro Medium Gray). Add the other two with the Pro layout:

- Pro (Light Gray), id proLightGray: #B8B8B8 panels.
- Pro (Light), id proLight: #F0F0F0 panels.

They are light themes: Preferences > Interface lists them under Light
Theme, Window > Theme has an entry for each, and the legacy
interface.theme preference selects and migrates them. ThemeKind::is_pro
replaces the hard-coded Pro | ProMedium checks so the dock keeps the Pro
tab layout on every Pro level. Labels are translated in every catalog.

Tests check the panel greys, ids and names, the darkest-to-lightest
order, and WCAG contrast of text, icons and accent text on every surface.


Claude-Session: https://claude.ai/code/session_01CVtZHu998NDhf4Z8QvS5pd

Co-authored-by: ngrafton2006 <ngrafton2006@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 08:09:05 -04:00
Valentin KleinandClaude Opus 5.5 3e90c81e3c perf: Duplicate Layer recomposites only where the copy draws (#2883) (#2884)
Duplicating a layer reported no damage, so the canvas recomposited the
whole document after every Duplicate Layer (P8/P9). The copy itself was
already cheap (tiles are shared Arcs). Report the copy's change bounds
instead, through the step_damage walk extended to bounded additions:
self-contained added layers (not an adjustment, not a new clipping base,
no video or excluded channels) contribute their change bounds; anything
else still recomposites everything.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:56:05 -04:00
Valentin KleinandClaude Opus 5.5 9b3c6359ae perf: Select > Modify > Feather/Smooth on 24 MP within budget (#2873) (#2882)
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>
2026-10-11 07:48:51 -04:00
Valentin KleinandClaude Opus 5.5 a030d19437 perf: repeated Select Subject answers from the last mask (#2865) (#2870)
Select Subject recomputed superpixels, saliency, GrabCut and the
full-resolution re-cut on every call (P29: 260-370 ms on 24 MP here,
693 ms p95 in the baseline). The session now keeps the last result,
keyed by the pixels it came from: `same_pixels` (tile pointers) against
a weakly held document, plus the sampled target (composite or layer).
Selection-only edits such as Deselect keep it valid; the mode
(replace/add/subtract/intersect) is still applied each time. One entry
per session, dropped on pixel edits and when its document closes.

Repeat: 7-10 ms p95 (was 270-370 ms). First run unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:44:00 -04:00
1d425ce8f9 Actions: check boxes switch single steps on and off (#2855) (#2866)
Photoshop's Actions panel has a check column: an unchecked step is skipped
by playback, Batch and droplets. PhotoCraft could reorder and delete single
steps but always ran every step.

- Action gains `disabled` (0-based step indices, omitted from actions.json
  when empty), kept in step with actions.move and actions.delete.
- New command actions.toggle {action, step?, enabled?}: one step, or every
  step like the action's own check; bad indices and types are errors.
- actions.play, the desktop playback path and the steps handed to Batch and
  Create Droplet skip unchecked steps; actions.get reports `disabled`.
- Panel: a check box per step, dimmed label when off, "2/3 steps" count.

Part of #219 (AUTO-219-2).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 07:39:35 -04:00
Valentin KleinandClaude Opus 5.5 017d1aea1c Tools: Perspective Crop (#1043) (#2864)
New Perspective Crop tool in the Crop flyout (C): drag a box or click four
corners, drag corners independently onto a skewed object, perspective grid
(Show Grid), Enter commits, Esc cancels. W/H/Resolution come from the Crop
tool's fields (Front Image, Clear), or the quad's mean side lengths.

Engine: image.perspectiveCrop {corners, width?, height?, resolution?,
interpolation?} maps the quad onto an upright canvas through the
quad-to-rectangle homography (Heckbert's square-to-quad map, inverted):
every raster layer, mask, alpha channel and the Quick Mask, one undo step.
The Background stays opaque over the background colour; type, shape, fill
and smart object layers are rasterized first; vector geometry follows the
nearest affine map. Non-convex, self-crossing, counter-clockwise or
degenerate quads are refused with an error.

algo: warp_surface_clipped bounds the per-tile warp by the new canvas even
when a layer reaches past the map's horizon; convex_quad, quad_size,
quad_to_rect.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:28:11 -04:00
5866cd2cf6 Tools: Horizontal and Vertical Type Mask (#1055) (#2861)
The Type flyout (T, Shift+T cycle) gains Vertical Type Mask and Horizontal
Type Mask, in Photoshop's order. They type like the Type tools, with the
canvas under Quick Mask's colour and the glyphs clear while editing. On
commit the glyph coverage replaces the selection; no layer is left behind
and the whole session is one "Type Mask" history step. Escape always
cancels and leaves the selection unchanged.

The commit runs a new engine command, type.createSelection: a type layer's
glyphs (optionally removing that layer, the tools' hidden working text) or
type.create's keys directly, with new/add/subtract/intersect and an
anti-aliasing override. type.create's layer building is shared through
type_cmds::new_text_layer.

Original dashed-T icons (ATTRIBUTION.md), tool label translations in every
catalog, README tool list and the gaps/parity docs updated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 07:06:10 -04:00
bacc825d33 Move tool: drag a marquee from an empty spot to select the layers it touches (#2844) (#2852)
With Auto-Select on (or Cmd held with it off), a Move-tool drag that starts where
no layer has pixels (an empty spot, or the pasteboard) now draws a marquee instead
of moving the previously selected layer. Releasing selects every layer whose
content touches it (Auto-Select: Group selects their outermost groups); Shift adds.

New engine command layer.selectInRect; the UI side lives in move_box.rs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 06:47:53 -04:00
Valentin KleinandClaude Opus 5.5 7609834df8 Channels: Edit › Transform works on a targeted alpha channel (#2835) (#2845)
edit.transform without a "target" (or "layer") param now follows the
Channels panel: a targeted alpha channel or the Quick Mask transforms
instead of the layer. Edit › Transform › Rotate 180°/90° and Flip
Horizontal/Vertical are routed to the panel target like the paint and
filter commands, so they turn a targeted alpha channel, the Quick Mask or
an unlinked layer mask within its frame (one undo step). The Background
no longer greys the transforms when a channel is targeted.

Flips scorecard row BUG-207-4.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:38:36 -04:00
203b5796b4 Tools: remember tool options between launches (#2814) (#2822)
* Tools: remember tool options between launches (#2814)

The options-bar settings in ToolOptions (Move > Auto-Select, marquee
Feather, tolerance, gradient style, crop ratio, eyedropper sample, ...)
only went through serde for Tool Presets, so every launch started from
the defaults. Photoshop keeps each tool's options between sessions.

Preferences gain `toolOptions` (shell-owned JSON, like brushPicker): the
fields that differ from their defaults, saved once the pointer is up and
restored at launch field by field. An unknown key is ignored; a field
that doesn't read back (wrong type, a number out of f32 range) keeps its
default with a log warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Merge upstream/main into fix/2814-persist-tool-options; regenerate docs/scorecard.md

The branch adds a preference, so the prefs audit count in docs/scorecard.md changes
(CI's xtask scorecard staleness test).

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>
2026-10-11 06:20:17 -04:00
mattyandBrandon Thomas ac19db1046 TGA: targa options asks for 24 or 32 bits/pixel, same as in Photoshop (#2760)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 06:17:53 -04:00
Valentin KleinandClaude Opus 5.5 b7b3117aea Droplets: carry the actions their Play Action steps call (#2794) (#2800)
* Batch: a failed step inside a called action fails the file and the script (#2779)

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>

* Droplets: carry the actions their Play Action steps call (#2794)

createDroplet saved only the chosen action's own steps, so a droplet whose
action plays another action failed on every file when run outside the
creating session (photocraft-cli droplet and the .command shim run it in a
fresh Session with no actions).

createDroplet now embeds the called actions, followed transitively with a
seen-set and the 16-level playback cap, under "calledActions" (not
"actions", which the CLI reads as an action set). Index references become
names. runDroplet passes them to file.automate.batch, whose per-file
sessions then play from them. Droplets without the key behave as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Droplets test: build Actions from JSON so the test survives new Action fields

#2866 adds Action.disabled; a struct literal here would stop compiling once both land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:06:20 -04:00
80519e1a17 Type: ⇧⌘> / ⇧⌘< change the type size (and work on Nordic/German layouts) (#2402)
* feat(type): Cmd+Shift+> / < change the type size, Alt for 10 pt steps

Photoshop's type size shortcuts: the selected characters while typing,
else the whole active type layer, in the points the layer shows. The
period and comma keys match by position so the shortcuts also work on
Nordic and German layouts, where Shift+. types a colon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(type): bad size steps fall back or clamp

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>
2026-10-11 05:56:28 -04:00
d96bb50161 Add Anchor Point and Delete Anchor Point tools (#1044) (#2756)
Paths could be reshaped after drawing since #865, but the number of
anchors could not change. Two new engine commands do that, and two tools
in the Pen flyout drive them on the canvas.

- photocraft_vector::edit::add_anchor splits the segment at t with de
  Casteljau, so a curve keeps its shape and gets a smooth anchor; a
  straight segment gets a corner on the line. delete_anchor joins the
  neighbours keeping their handles, drops an emptied subpath and refuses
  to remove a path's only anchor.
- path.addAnchor / path.deleteAnchor take the same targets as the other
  point edits (work path, saved path by name, shape layer path, vector
  mask) and record one history step each.
- Tool::AddAnchorPoint / DeleteAnchorPoint join the P group after the Pen.
  They act on Direct Selection's paths, highlight the segment or anchor
  under the pointer, show a +/- cursor badge, swap with Alt and turn into
  Direct Selection with Cmd/Ctrl. An edit clears a Direct Selection on the
  same path so it never points at a shifted anchor.
- Icons derived from Lucide pen-tool (ATTRIBUTION.md), hints and tool
  names in all 16 catalogs, README tool list, roadmap.

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>
2026-10-11 05:48:07 -04:00