* 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>
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>
* Mirror the shell for right-to-left UI languages (DIST-220-4)
Arabic, Hebrew, Persian and Urdu read right to left, but the window's
chrome was hard-wired left to right, so an RTL catalog (#2505) would
put Arabic labels into a left-to-right layout.
The reading direction now follows the language code
(i18n::dir_for_code: ar, ckb, dv, fa, he, ps, sd, ug, ur, yi), so a
catalog needs no extra setting and the LANGUAGES rows are unchanged.
In a right-to-left language the shell mirrors:
- the menu bar starts at the right with File; menus open under their
title's right edge, and the arrow keys step through them in screen
order
- the toolbox docks on the right, the panel dock and its rail on the
left; tool flyouts open towards the canvas and their corner marks
and the header chevrons follow
- the title, options and status bars read from the right, with the
workspace controls and the Fit/progress group at the left end; the
title and window-drag gap use the free span between them
New module direction.rs holds the helpers (start_panel/end_panel,
row/trailing layouts, horizontal_centered, the popup alignment). Left
to right, every change lays out as before.
No RTL catalog ships yet, so PHOTOCRAFT_UI_DIRECTION=rtl forces the
mirrored layout for review (read once, native only). Panel interiors,
dialogs, menu rows, submenus and document tabs stay left to right, and
egui still draws UI labels without Arabic shaping or bidi reordering;
docs/localization.md and the parity docs say so.
Adds Lucide's chevron-left (the status bar's info menu arrow points
left in RTL; Timeline's Previous frame button asked for it too and drew
the fallback square).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Localization docs: egui shapes Arabic words; what's missing is a font and bidi word order
egui 0.36 shapes right-to-left runs with HarfRust, so letters join within
a word. The gaps are an Arabic fallback font in the UI and paragraph bidi
(words are placed left to right; emilk/egui#8576, #8577).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: MathMattTech <334629259+MathMattTech@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
Photoshop's Artboard tool in the Move flyout (V, ⇧V cycles Move ↔ Artboard):
dragging on an empty spot draws a new board (`layer.new.artboard`), clicking a
board selects it, dragging it moves it with its contents, and the selected
board's edge and corner handles resize it (`layer.artboard.set`,
moveContents: false). Each gesture is one undo step. Options bar: Size preset,
W/H, Make Portrait, Make Landscape, Add New Artboard. Original toolbar glyph
(assets/icons/artboard.svg, ATTRIBUTION.md row).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
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>
The Pen flyout (P / Shift+P) now holds Pen, Freeform Pen and Curvature Pen.
Freeform Pen: a drag draws freehand; on release the trace is fitted with
cubic Beziers within the options bar's Curve Fit (default 2 px) by
photocraft_vector::trace::fit_trace (Schneider, "An Algorithm for
Automatically Fitting Digitized Curves", Graphics Gems 1990, the fitting
Make Work Path already uses), keeping turns sharper than 60 degrees as
corners. Releasing on the start point closes the path.
Curvature Pen: each click adds a smooth point and the path runs through
every point (Catmull-Rom style handles from
photocraft_vector::edit::curvature_knots); dragging a point moves it,
double-clicking toggles corner/smooth, clicking the first point closes,
Enter/Esc finish and Backspace removes the last point, as with the Pen.
Both commit through the Pen's path: a shape layer in Shape mode, else the
targeted vector mask, else the work path, as one History step.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
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>
The engine already had paint.colorReplacement, but the desktop UI had no
tool for it: no Tool entry, no toolbar slot, no options bar and no stroke
routing, so only agents and the CLI could use it.
Tool::ColorReplacement joins the Brush (B) flyout after the Mixer Brush,
so B / Shift+B cycle to it and ui.set / ui.inspect name it. Its options bar
has Mode, Sampling (Continuous / Once / Background Swatch), Limits,
Tolerance and Anti-alias, all mapped to the command's params, with the
foreground as the replacement colour. Strokes are drawn live through
LiveDab, which can now preview the stroke's finishing dab, so the preview
at the last move matches the commit. The command refuses a mask, channel
or Quick Mask target instead of silently recolouring the layer. New icon
derived from Lucide's brush; label translated in all 16 catalogs.
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>
* feat(ui): favorite themes from the appearance button's context menu
Hovering a theme in Preferences > Interface shows a star; clicking it saves
the theme to interface.favoriteThemes (filled accent star once favorited).
Right-clicking the title-bar appearance button opens a menu with Appearance
Settings, then the favorite light themes and the favorite dark themes.
* Regenerate the scorecard for the favorite themes pref
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Both tools join the Marquee flyout (no key, outside the M / Shift+M cycle,
as in Photoshop). A click selects the one-pixel row (column) under the
pointer across the whole canvas; a press-drag moves the line until the
release. The selection goes through select.rect, so Shift adds, Alt
subtracts, Shift+Alt intersects and the options-bar modes apply; Feather
applies, Anti-alias and Style/Width/Height are greyed. Selection-mode
cursor badges, the selection right-click menu and snapping cover them.
Original row/column glyphs with an ATTRIBUTION.md row; tool names in all
catalogs; README tool list and the G6 / ui-parity rows updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Add the Remove tool with non-local patch completion
Tool::Remove (J flyout) paints a hole that paint.remove fills from its
surroundings as a background job, in one undo step. A stroke around an
object also removes what it encloses; the selection limits the hole.
The fill (photocraft_algo::nonlocal) follows Newson, Almansa, Gousseau and
Pérez (IPOL 2017): texture features in the patch distance, onion-peel
initialisation, match fields upsampled between levels and a best-patch
final copy, plus gradient-domain hiding of edge and interior seams.
Sparse holes are filled in tiles. inpaint::complete (Content-Aware Fill,
Spot Healing, Content-Aware Move) is unchanged.
On 20 holes in public-domain photos (remove_quality) the fill keeps 0.89
of the texture it replaces, against 0.54 with the Wexler/PatchMatch
completion. 24 MP (remove_bench): 100 px ring 120 ms, 1000 px ring 2.5 s.
* Translate the Remove tool's label and tighten comments
Adds "Remove Tool" to every UI catalog, following each one's pattern for
tool names and its existing word for Remove. Corrects comments that no
longer matched the code: the benchmark's description, the tile and margin
minimums, and the reasons given for two parameters.
* Put the Remove tool first in the J group
The J flyout's slot now shows Remove until another J tool is picked, and
J from another group selects it; repeated J goes on to Spot Healing Brush,
Healing Brush, Patch, Content-Aware Move and Red Eye.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Appearance modes: sync with system, separate dark and light themes
* Appearance button cycles Auto, Light, Dark
* Appearance: default stays Dark, follow the portal signal instead of polling, split out the Preferences resize
* Preferences: size both appearance cards for the five dark themes
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Photoshop's Vertical, Horizontal, Dual Axis and Diagonal paint symmetry presets beside the
path-based symmetry: axes start at the document centre, Transform Symmetry moves and rotates
them (Shift snaps to 15°, Enter applies, Esc cancels), and all copies merge into one coverage
pass so overlaps don't build opacity. New paint.setSymmetry command and preset icons. Fixes#1648.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The colour chips sit centred in the toolbar with Default Colors and Switch Colors above them. The Color Picker gains L/a/b radios and the out-of-gamut and web-colour warnings, each with a click to take the nearest colour. Panel footers are a full-width bar with their own mask and adjustment glyphs, keeping the right-click footer menus from #1680.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1066 added the gesture; this makes it behave as Photoshop 25.4 does (measured by sweeping
⌥-hover and ⌥-click a pixel at a time across each line, results read back by script):
- the line takes the pointer from 3 px above it to 5 px below it (was ±3);
- a release frees the layer above the line together with the clipped layers stacked above it,
in one history step, and makes it the active layer (was: that layer alone); a clip keeps the
active layer, as before;
- clipping and releasing have their own cursors: PhotoCraft's original clip-below (a hooked
arrow onto the layer below) and clip-release (the same arrow with a small "no" badge),
replacing Lucide corner-right-down for both.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Holding Alt (Option) over the line between two layers in the Layers panel
shows the clip cursor; a click runs Layer > Create Clipping Mask on the upper
layer, or Release Clipping Mask when it is clipped already, as in Photoshop.
The line only counts between siblings that are next to each other in the
stack (not inside an open group or across filtered-out rows), and stays
clear of the eye column and the effects triangle. The cursor is the Lucide
corner-right-down icon.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Direct Selection tool: reshape paths after they're drawn (#790)
Adds Photoshop's Direct Selection tool (A, in the Path Selection flyout) and
the Pen's Cmd/Ctrl (Direct Selection) and Alt (Convert Point) modifiers. It
selects and drags anchors, direction handles and segments of the work path,
the active shape layer's path and a targeted vector mask.
The geometry lives in photocraft_vector::edit; new engine commands
path.moveAnchors, path.moveHandle, path.bendSegment and path.convertPoint
apply it (one history step per drag, drivable over MCP and the control
channel), and the canvas previews a drag with the same code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tool::ALL holds 49 tools now that Direct Selection and Magnetic Lasso both landed
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>
* Add the Magnetic Lasso tool
A selection border that snaps to edges as the pointer moves, in the
Lasso (L) flyout after the Polygonal Lasso.
- photocraft-algo `magnetic`: live-wire edge tracing (Mortensen &
Barrett 1995): Sobel strength, non-maximum suppression and gradient
direction link costs, a shortest path inside a corridor of the
detection width around the pointer's path, sub-pixel edge centres.
Pixels are read lazily in 128 px blocks.
- Engine: `EdgeSource` (the active pixel layer, else the composite) and
`select.magneticLasso`: rough points in, a border along the edges out;
`trace: false` selects a given outline, which is how the tool commits.
- UI: click to start, move to trace (button up or down), points fasten
by themselves (Frequency), click fastens, Alt-click straight, Alt-drag
freehand, Backspace/Delete removes points, click the first point,
double-click or Enter closes, Esc cancels, [ ] change the width.
Options bar: Width, Contrast, Frequency, tablet pressure. `ui.inspect`
reports the border in progress.
- The Lasso and Polygonal Lasso now apply the options bar's Feather;
`select.lasso` rejects a feather outside 0..1000.
- Icon adapted from Noun Project's "Magnetic Lasso Tool" by Lil' Seal
(CC BY 3.0, licence file and ATTRIBUTION.md row).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* it: translate the Magnetic Lasso strings (Italian landed after this PR was written)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Magic Eraser and Background Eraser engine commands
paint.magicEraser erases the wand region (tolerance, anti-alias,
contiguous, sample all layers, opacity) to transparency, tile by tile in
parallel; paint.backgroundEraser erases colours similar to the one under
the brush hotspot (continuous / once / background swatch sampling,
discontiguous / contiguous / find-edges limits, tolerance, protect
foreground colour). Both compare and erase in the layer's own channels at
any depth, respect the selection, turn the Background into Layer 0 like
Photoshop, and are one undo step. The Magic Eraser paints the background
colour on layers with locked transparency.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Magic Eraser and Background Eraser tools in the Eraser group
Shift+E / E cycles Eraser, Background Eraser and Magic Eraser; the
toolbar flyout lists all three. Options bars: Magic Eraser tolerance,
anti-alias, contiguous, sample all layers and opacity; Background Eraser
sampling toggles, limits, tolerance and protect foreground colour. The
Background Eraser cursor always shows its sampling hotspot. Icons are
derived from Lucide's eraser (ISC), recorded in ATTRIBUTION.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Image Rotation, Canvas Size, Crop, Trim and Image Size move vector geometry
Canvas-wide operations remapped only pixels: type, shape and smart-object
transforms, vector masks, saved/work paths, guides, slices, notes and
Count/Ruler marks stayed put, so they jumped back on the next re-render.
New engine module canvas_geom applies the canvas map to all non-pixel
geometry. 90°/180°/flip turns remap every pixel surface exactly (incl.
type/shape/smart/fill caches and smart filter masks) and move geometry
through the matching affine; guides swap orientation under 90° turns;
rect/ellipse/line live shapes stay live; gradient fill angles follow.
Canvas Size/Crop/Trim translate geometry (shapes re-render for the new
canvas), Image Size scales it and re-renders vectors, Arbitrary rotation
also moves unlinked vector masks and document marks. Layer effect angles
stay (Global Light), as in Photoshop.
Tests compare the CPU composite of a rotated document (type, shapes,
vector mask, smart object) against the rotated flattened original, also
after re-rendering everything from geometry, at 8/16/32-bit RGB, CMYK
and Gray.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Magic Eraser: edit encoded tile bytes in parallel (24 MP: 1260 -> 375 ms)
Tiles are copied out as encoded bytes and only the touched samples are
decoded, so the parallel workers do all the work and the serial write-back
is a memcpy. The ignored bench prints a phase breakdown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Play loops the playhead via timeline.setFrame at the timeline fps, using
egui time (wasm-safe, no Instant) and requesting repaints; pausing/closing
stops it. New pause icon. ui-egui tests + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Lowercase reverse-DNS id ai.storyteller.photocraft across icons, Linux
desktop/metainfo/mime/flatpak, Windows WiX, macOS Info.plist and workflows.
- PhotoCraft display name in the title bar, start screen, web loader,
Windows exe metadata, README and docs.
- .gitignore target/, roadmap refresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>