Move Drop Shadow's angle dial out of layer_style.rs into a shared
widgets::angle_dial_row that takes the param's range and uses the
dialogs' value_field. The field keeps Motion Blur's -360..360, and it
takes the dialog's opening focus. The filter dialog uses the dial for
direction angles that count counter-clockwise from three o'clock:
Motion Blur, Emboss, Oil Paint, Lighting Effects, Relight and Global
Light. Rotation amounts (Twirl, Rotate Arbitrary, Crop, Lens Correction,
Blur Gallery) and Flame's up-zero angle keep their sliders.
Arrow keys on the focused dial wrap round only when the range is the
dial's own -180..180 (Drop Shadow); on a wider range they step on, so
Motion Blur's 270° goes to 271°, not -89°.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Type: ask to rasterize before Warp on live type (#3004)
Edit › Transform › Warp (the menu, or the warp toggle of an open transform
box) on editable type opened Warp mode, let the user shape the mesh, then
failed on commit with "Warp needs rasterized type". It now opens Photoshop's
"This type layer must be rasterized before proceeding…" prompt, the one the
pixel tools already use. OK applies an open transform box, rasterizes the
type (its own history state) and opens Warp on the pixels; Cancel does
nothing. The prompt's continuation is a menu command from an allow-list,
run with the egui context right after the confirm (control channel, OK
button) or on the next frame.
Distort and Perspective stay greyed for type (#2701), Puppet Warp and
Perspective Warp stay greyed for non-pixel layers, as in Photoshop.
Scorecard TYPE-218-9 -> done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Type: Warp on live type takes the warp styles, no rasterize prompt (#3004)
Photoshop 2026 keeps Edit › Transform › Warp enabled on live type and asks
nothing: the mesh can't be dragged, only the options bar's warp styles
(Warp Text's) apply, and the type stays editable. The earlier version of
this change asked to rasterize first; that is gone (and with it the prompt
continuation, which nothing else used).
Warp mode on a type layer now starts from the layer's own Warp Text style
over the unwarped type, ignores mesh drags and splits (Split, Grid and the
Custom style are greyed/hidden, the split menu items are greyed), and the
commit runs edit.transform.warp, which on type sets the Warp Text: one
history step, recordable, the layer stays Text. None takes a warp off. A
box scaled or rotated before toggling Warp is applied first.
Scorecard TYPE-218-9 note updated: Photoshop doesn't prompt for Warp;
Distort and Perspective stay greyed (#2701).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Alt during a drag unlinks the handle pair (only the outgoing one keeps following),
Alt-press on the last anchor's outgoing handle turns that handle alone, and pressing
the first anchor now closes on release, so a drag pulls out that anchor's handles
(a stand-in knot folded into the first one on commit). A plain click still closes
without touching the first anchor's handles. Still one History step per path.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`hasMask` in document inspection covers only raster masks, so a layer
with a vector mask looked unmasked to an agent. Layers, groups included,
now also report `hasVectorMask` and `vectorMaskEnabled` (null when there
is no vector mask). `hasMask` keeps its meaning.
* 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>
* 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>
* Window frame: trim a caption-less maximize clear of the taskbar (#3144)
* Credit axlecoffee in the About window
* 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>
* Preview Color Lookup LUT selection while cycling
* Test Color Lookup keyboard and wheel cycling
* rustfmt; drop a useless into_iter clippy flags
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 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>
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>
As in Photoshop, right-clicking the W or H field of the Free Transform
(and Free Transform Path) options bar opens a menu of units: Pixels,
Inches, Centimeters, Millimeters, Points, Picas and Percent, the
current one checked. W and H then show the box's size in that unit at
the document's resolution (1200 px at 300 ppi is 10.16 cm), and a
value typed in it scales the box to that length; Percent, the default,
is the scale as before. The pick is remembered with the dialog
settings (`dialogs["edit.freeTransform"].sizeUnit`).
The menu is the rulers' unit menu (`rulers::unit_menu`), the
conversion is the Properties panel's `LenUnit`, and the size limits and
the link toggle work on the scale as before. Length fields are wider
(84 pt) so "2116.67 mm" fits; Percent keeps its 66 pt.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Since Photoshop CC 2019 a double-click on a type layer with the Move tool starts
editing its text without switching tools by hand. PhotoCraft only offered this
from the Layers panel thumbnail. A double-click with the selected Move tool now
picks the topmost visible type layer under the pointer (the Type tool's own hit
test), makes it active and opens an edit session with the caret on the glyph
there, switching to the Type tool. Elsewhere the double-click does nothing, ⌘
held over another tool doesn't count, and an open transform box keeps its
double-click (commit).
The type tests' harness steps a quarter second per frame, past egui's
double-click delay, so the new test uses a 60 fps harness.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The GPU canvas built a full-resolution, mipmapped texture set covering the
whole declared canvas (DocTextures::new) with only per-texture side limits,
so a document's size alone decided how much GPU memory was committed: a
300000x300000 canvas asks for hundreds of gigabytes.
canvas_texture_bytes prices the set in closed form (every tile and mip
level) and DocTextures::new refuses one over canvas_budget (a quarter of
RAM, at least MIN_GPU_BUDGET) without creating anything. refresh reports
"over-budget" and ensure_gpu then draws the document on the reduced CPU
canvas, so a supported large document still displays.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Files opened together (File > Open with several files, paths on the
command line, the OS's "open these documents") appeared in random tab
order: each one opens in the background and its tab was added when it
finished decoding, so small files came first. The paths were also used
in the order the picker returned them.
`open_paths` now sorts the paths naturally (`File_2` before `File_10`,
case-insensitively) and opens them as a set: every background open
carries its position (`OpenOrder`), and when it finishes its document is
moved after the members before it that have already opened, so the set
reads in order however long each file takes. Members opened at once
(preset files) take their place immediately. The web build sorts the
picked files' contents the same way.
Tests: `natural_cmp`, a set opened synchronously, and a set whose
background opens finish out of order, mixed with a tab dropped at a
slot and an open from a forgotten set.
A 1/4 proxy averages away the specks the filter exists to remove, so the
preview showed an untouched image, at a scale that upscaled back to blocky
— while `radius` stayed in document pixels and covered k times the area it
covers in the result. OK always applied at full resolution, so only the
interactive feedback lied; add the filter to FULL_RESOLUTION_PREVIEW and
place its regression test on that path.
Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
* Rulers: stay level and fully ticked on a rotated view; guides turn with the image (#3233, #3277)
The rulers measured along the rotated document axes, so at most angles the
ticks were squeezed into a few pixels (none at 90°) and the rest of the
ruler was empty. They now measure with the view minus its rotation: same
zoom, flip and pixel aspect, same document point under the canvas centre.
Guides were drawn as screen-level lines through the screen position of
(pos, 0), so on a rotated view they no longer marked their document
coordinate while hit-testing still used it. They are now drawn along the
document line across the clip rect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rulers: anchor a rotated view's rulers at the document origin (#3233)
Photoshop 2026 keeps the rulers level at zoom scale on a rotated view, with 0
at the screen position of the document origin (where the rotated image's
top-left corner lands), not at the unrotated frame through the canvas centre.
ruler_xform now re-centres the level mapping so the origin projects where the
rotated view draws it; tick spacing still follows zoom, flip and pixel aspect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rulers: label distances before the origin without a minus sign (#3233)
Photoshop's rulers print the distance from the origin, so ticks left of /
above it read "1000 500 0 500 1000", not "-1000 -500 0". The label is now
the absolute value (extracted into `ruler_label`), for every view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The SV field re-derives Hsva from the colour every frame, and black reads
back as s = 0 — so a drag that bottoms out into black drew the knob at the
bottom-left one frame later, although the colour it picked was right.
Remember what the field was showing (hue *and* saturation) and restore both
only when the colour cannot carry them: both for black, the hue alone for a
grey, whose saturation is genuinely zero.
Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
* 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>
* Avoid white flash when opening screen color picker
* Normalize screen picker source line endings
* rustfmt; doc comment on viewport_builder
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>
* 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>
Right-clicking the image with the Zoom or Hand tool opens Zoom In, Zoom Out,
Fit on Screen, Fill Screen, 100%, 200% and Print Size. Zoom In/Out step about
the right-clicked point (or centre it with Zoom Clicked Point to Center); the
other rows run their View menu command. Outside the image the pasteboard menu
still wins. The canvas keeps the view a menu row sets instead of writing its
frame copy back over it. Agents get the same menu through ui.pointer and
ui.context.choose.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
In the in-window menu bar (Windows, Linux, --in-window-menus), moving from a
submenu's row toward one of its lower items crosses the rows below. egui keeps
the submenu open only while the pointer moves in a straight line at it: a
bowed path or a pause on the way closed it, and the crossed rows lit up as if
chosen.
As in macOS and Photoshop menus, the submenu now stays open while the pointer
is inside the triangle from where it left the row to the submenu's near edge,
until it rests on another row for 0.3 s or moves away. Its row stays lit and
the crossed rows don't light up; when the submenu lets go, the row under the
pointer takes the highlight. Clicks, keys and leaving the triangle behave as
before.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Native brush outline and crosshair cursors on macOS, X11 and Wayland (#1343)
The brush outline and crosshairs were OS cursor bitmaps only on Windows; on
macOS and Linux the app painted them, so they trailed the pointer by the
render, present and compositor latency and jumped to catch up on slow frames.
tool_cursor now hands the OS the bitmap on every desktop root viewport.
macOS takes NSCursor images in logical points, so the bitmap scales with the
UI zoom only there; Windows, X11 and Wayland take physical pixels. Off Windows
bitmaps stay within 255 px (X servers and hardware cursor planes), and a
larger tip falls back to the painted outline plus a small OS crosshair.
The Pencil's grid-snapped square and the pipette stay painted, and the Hand
bitmap stays Windows-only (the other platforms draw their own hands).
Co-authored-by: jerrykimo <104250063+jerrykimo@users.noreply.github.com>
* Cursor test: the Hand is a bitmap only on Windows; elsewhere the OS draws Grab
---------
Co-authored-by: jerrykimo <104250063+jerrykimo@users.noreply.github.com>
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.
* Report which dock tab leaves its strip
* Carry dragged dock tab identity
* Expose dock panel name normalization
* Move individual tabs across native dock groups
* Document cross-group dock tab drops
* Test individual tab drops between dock groups
* Resolve a dropped tab by its original index; rustfmt
The strip reports visible indices, so a reordered group moved the wrong
panel. Adds a regression test.
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>
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>
* 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>
The History panel's menu (☰) now has Photoshop's items: Step Forward, Step
Backward, New Snapshot… (greyed, snapshots aren't built yet), Delete, Clear
History, New Document, History Options… (greyed, built in parallel) and Close,
above the dock's own group items. Each runs an engine command.
- history.stepForward / history.stepBackward: one state along the list
(Err when there is none).
- history.clear: removes every state but the current one without changing
the image. It is undoable: History keeps the cleared states until the next
step, and Edit > Undo puts them back. Fade is dropped and can no longer
underflow on an empty history.
- history.deleteState is enabled only when there is an earlier state.
- Translations for the new labels in all complete-menu languages.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci: refactor workflows to use reusable artcraft-common-actions
* ci: point reusable workflows to storytold/craft-common-actions
* ci: pin storytold/craft-common-actions to commit 27a978f and pass secrets explicitly
* ci: use @main (unpinned) for craft-common-actions with explanation comment
The colour swatches (Layer Style's Stroke, Color Overlay and the rest,
and every `color_swatch` / `color_swatch_button`) painted a square
checkerboard under a rounded fill, so the checker's light cells showed
at the four corners, white specks on the dark themes.
The checker under a rounded swatch is now a repeating two-by-two tile
textured onto a rect with the swatch's corner radius: one shared texture
per context, nearest-filtered so it never blurs, scaled so a cell stays
4 pt. `checker` itself is unchanged for the square previews.
* 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>
The History panel gets Photoshop's source column at the left of every state:
the History Brush icon marks the source, and a click there makes that state
the source through the new `history.setSource {state}` command (not a step,
and the row is not stepped to). `paint.historyBrush`, the interactive History
Brush and Fill > History use the source when no `state` is passed; by
default it is the oldest state held, as before.
The source is kept by state, not index: photocraft-ops gives every history
state a stable id, so the source stays on its state while older states fall
off. As in Photoshop (checked by script), the source state itself does not
fall off the history-state limit or the memory budget, and once it is
deleted or discarded by a new step after stepping back there is no source:
the History Brush then refuses to paint without an explicit `state` until a
new source is set. Rows count the greyed redo states too, so the common
"filter, step back, paint the filtered state in" workflow works.
`document.inspect` / `ui_inspect` report `historySource` {state, label}
(null when its state was deleted). Snapshots are not implemented yet:
`snapshot` returns an "unsupported" error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Populate Layer › Layer Style › Global Light dialog fields from the
active document's shared global lighting rather than schema defaults or
remembered values from other documents. This ensures opening Global Light
and confirming unchanged preserves the active document's lighting.
Fixes#3054.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- menu_catalog: add Type › Anti-Alias › None before Sharp, as in Photoshop.
It used to come from the engine list, appended after Windows and built
without a checked state, so a layer set to None showed no check.
- view_cmds::type_checked reads type_tool::target_text (the edited layer,
else the active or a selected type layer), as the options bar and the
Type commands do, not only the active layer.
- Anti-alias and orientation items stay check items (Some(false)) with no
type layer, so the native menu doesn't rebuild.
- Parity floor 629 -> 630; docs/parity-checklist.md regenerated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The file-open path rejects PSDs nested past MAX_GROUP_DEPTH before
conversion, but the public psd_to_document built file.layer_tree() to the
file's full depth (~16000 levels) before Ctx::build capped it, and layered
TIFF reached it with no preflight at all.
- photocraft-psd: PsdFile::layer_tree_max_depth(n) builds the tree
iteratively and drops the children of groups nested n deep, so walking
or dropping it recursively is bounded. layer_tree() is the uncapped case.
- psd_to_document uses it with MAX_GROUP_DEPTH, the same cap Ctx::build
and apply_link_groups apply, so imports are unchanged.
- Layered TIFF rejects nesting past the cap with the same error as a PSD
(shared psd_import::check_group_depth).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Convert Point is a tool of its own, built on the existing path.convertPoint
edit that Pen + Alt already drives. A click makes a smooth anchor a corner, a
drag from an anchor pulls out smooth handles, a drag on a handle moves it
alone. The tool selects the anchor it converts so its handles show; a press on
empty canvas or a segment does nothing, and Cmd/Ctrl is Direct Selection, as
with the Pen. It shares the Pen's key (P).
Original glyph with an ATTRIBUTION.md row; tool name in all catalogs; README
tool list and the G6 / ui-parity / roadmap counts updated (tools 56 of 68, the
earlier 53 was stale).
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* GPU startup: skip wgpu indirect-call validation shaders (#3107)
wgpu compiles its indirect-call validation compute shaders inside
request_device. On Intel Iris Pro (Metal GPUFamily 1, Monterey) the Metal
compiler hits an internal error on them, which wgpu-core reports as
Device(Lost), so the window device can't be created in any mode, including
--safe-gpu and PHOTOCRAFT_CPU_CANVAS. PhotoCraft issues no indirect draws or
dispatches, so drop the flag; WGPU_VALIDATION_INDIRECT_CALL=1 still re-enables it.
* gpu_startup: skip indirect-call validation only on Intel Macs
Keep wgpu's default validation on Apple silicon, Windows and Linux; only Intel Macs,
where Metal's compiler crashed on the validation shaders (#3107), skip them.
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>
* fix(ui): respect native desktop scale under Auto UI scaling (#1278)
* UI scale: drop the now-unused monitor size, update docs and scorecard
Merge main; Auto no longer reads the monitor size, so remove the parameter and its
computation, simplify the test, and update ui-design.md and UI-217-9 to match.
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>
* Rasterize Extrude blocks and scatter Wind streaks once
* Prepare Spin Blur rotations and fixed-channel sampling
* Share Lens Correction interpolation across color and alpha
* Make wide prepress traps independent of radius
* Add full-operation timing for five imaging algorithms
* Record 24 full-operation benchmarks and exact-output checks
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
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>
Desktops that move windows with Alt+drag grab every Alt+left press, so a
Zoom tool Alt-click never arrived and nothing happened. Watch the Zoom
tool for that grab (alt_grab) so the status bar explains it, and let a
latched Alt (Window > Modifier Keys) zoom out: the click, cursor and zoom
rectangle now honour it.
Claude-Session: https://claude.ai/code/session_01JgB5X7gyFEFo4oDW6vH4VN
Co-authored-by: Leel Wasanatha <260480244+kavishkashehan726-sudo@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Preserve HDR range in float adjustments and stack statistics
* Cover HDR exposure round trips and Levels record order
* Report the backend used by GPU parity tests
* Compare float PSD oracles at native depth and preserve signed Exposure
* Retain merged alpha metadata when decoding float references
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>