Commit Graph
354 Commits
Author SHA1 Message Date
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
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
2e0c426692 Tools: Artboard tool (#2858) (#2862)
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>
2026-10-11 07:12:53 -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
c436aa7294 Tools: Freeform Pen and Curvature Pen (#1054) (#2856)
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>
2026-10-11 06:56:44 -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
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
d79fc5e8ef Free Transform: right-click menu with the transform modes, rotate and flip (#2832) (#2840)
The Free Transform context menu listed only the modes. It now follows
Photoshop's order: the modes, then Content-Aware Scale and Puppet Warp
(greyed while a box is open), Rotate 180°, Rotate 90° CW and CCW, and Flip
Horizontal and Vertical.

The Rotate and Flip items, whether chosen from this menu, Edit › Transform,
a shortcut or the control channel, now turn or mirror the live box about its
reference point without committing it (transform_tool::intercept). Before,
they ran the engine preset on the layer under the open box. They are greyed
while warping. Distort and Perspective are greyed over a box on editable
type, matching #2701. A control-channel right-click during a transform opens
the same menu as the canvas.

Scorecard TOOL-214-5 is now done.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 06:30:33 -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
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
009bef76e7 CLI: batch plays an action from an Actions set with the whole set loaded (#2786) (#2792)
`batch --actions` ran each file in a fresh headless session with no
actions, so a step that plays another action (`actions.play`) failed for
every file, and the Actions panel's own actions.json could not be passed
at all. `--actions` now also takes an Actions set; `--action <name>`
picks the action to play and every per-file session gets the whole set.
A failed step inside a called action fails the file instead of saving it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:01:07 -04:00
2189a1610e fix(crop): stabilize resizing and apply ratio changes immediately (#3206)
Compute snapped handles from press-time geometry to avoid feedback from
Auto Center Preview. Paint the image and crop frame with the same camera.

Apply ratio and size options immediately as centred rectangles contained
in the current frame. Respect saved ratios, keep Swap and X consistent,
and let Enter apply output sizes without an initial resize.

Use container padding for the options bar's text allowance so right-aligned
crop actions do not introduce horizontal overflow.

Add regression coverage for handle snapping, rendered frames, preset/Enter
and Swap/X interactions, and options-bar scrolling at multiple UI scales.
Enable public GitHub profile name syncing for TheTeslak's contributor credit.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:54:09 -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
303b7849e8 raw: fit non-DNG colour to the camera's embedded JPEG (#2135)
* raw: fit non-DNG colour to the camera's embedded JPEG

* raw look: clippy (as_chunks) and rustfmt after merging main

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 05:45:46 -04:00
468b8f2792 Crop tool: New / Delete Crop Preset saves and removes user presets (#1919) (#2723)
* Crop tool: New / Delete Crop Preset saves and removes user presets (#1919)

The Crop options-bar dropdown ends with New Crop Preset... and Delete Crop
Preset..., as in Photoshop. A crop preset is a "crop" tool preset
(tool.presets.*, persisted with the preferences), so it also shows in the
Tool Presets panel. New saves W x H x Resolution (W, H, resolution and
unit) or a ratio under a name made from the values; it is greyed out with
nothing to save. Delete lists the saved crop presets for a multi-select;
built-in size presets stay. The dropdown shows the preset the fields hold.

Engine: tool preset names are checked (1-255 chars) and the store capped;
saved tool presets load entry by entry so a damaged one is skipped; a
multi-name delete is all or nothing. ui.dialog.open gains newCropPreset /
deleteCropPreset.

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

* Regenerate the scorecard after rebasing onto main

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 05:31:19 -04:00
073e1cc1d2 Add the Color Replacement tool to the Brush toolbar group (#1045) (#2719)
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>
2026-10-11 05:20:00 -04:00
094d0776d1 Improve zoomed Liquify preview detail (#2715)
* Improve zoomed Liquify preview detail

* Record Liquify quick benchmark results

* Keep the roadmap as on main; record the Liquify numbers in target-app-parity.md

The branch carried an old copy of the roadmap's parity tables, which have moved
to docs/target-app-parity.md on main.

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 05:03:10 -04:00
67e3d9be9f feat(ui): Font Preview Size drives the font menu samples (#204) (#2668)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 04:52:17 -04:00
93f2ff835a feat(ui): Beep When Done plays the platform beep for finished jobs (#204) (#2658)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 04:29:59 -04:00
233237e9dc Reject bad params in the control channel's synthetic input methods (#2449) (#2656)
ui.pointer, ui.click/ui.move, ui.key and ui.screenshot read every value
with as_f64/as_bool/as_str and fell back to a default on any other type,
and an unknown button or event kind fell through to a left click or a
move. A typo such as "button":"ritgh" ran a different action and still
replied ok.

Each method now validates its whole request before acting, like ui.set:
unknown fields (top-level, under modifiers, or in an event), wrong JSON
types, unknown button/kind/tool values (the error lists the accepted
ones) and out-of-range pressure, tilt and rotation return an error that
names the parameter, and nothing is queued or applied. Null fields still
mean omitted, and valid calls behave as before.

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 04:18:18 -04:00
fa2fe1c90f feat(ui): the brush HUD honors Vary Round Brush Hardness and Brush Preview Color (#204) (#2640)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 03:59:04 -04:00
cfd12263af Rulers: keep tick marks and labels out of the origin corner (#2617)
* Rulers: clip tick marks and labels outside the origin corner

* Rulers test: set ViewXform's new aspect field after merging main

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 03:46:29 -04:00
de58a0a778 feat(ui): the Type units preference drives the type metric units (#204) (#2587)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 03:37:06 -04:00
334ae5779c Enhanced Controls: wire Zoom with Trackpad Pinch (#204) (#2577)
* feat(ui): Zoom with Trackpad Pinch gates the canvas pinch (#204)

* Navigator: pass the pinch preference to wheel_nav::read (new caller on main)

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 03:26:07 -04:00
e44d96e1d4 Favorite themes from the appearance button's right-click menu (#2445)
* 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>
2026-10-11 03:09:47 -04:00
Ahmed HosnyandGitHub c25630b5fa app.save: encode on the save worker, not the UI thread (#2488) (#2517)
* app.save: encode on the save worker, not the UI thread (#2488)

The automation save ran export+write inline inside the control drain,
freezing the window and every queued control request while a large
document encoded — and overrunning the 60s reply deadline. It now uses
the same #2017 background path as File > Save when background jobs are
on: the encode and the capability-scoped write run on a worker, the
document is locked, and the control reply waits on the job (wait: false
answers {job, pending} at once). Tests and the web keep the inline path.

The save job's result now carries the path alongside the warnings, so
waiting callers get the same {path, warnings} reply both ways.

* app.save: never fire saveDocument hooks from automation saves; review fixes (#2488)

Automation saves complete in the job tick, long after run_waiting lifted
its synchronous event suppression, so the background branch fired the
user's saveDocument script bindings and Image Assets — the exact hooks
docs/control-protocol.md promises never run for automation. SaveJob now
carries an automation flag and records through saved_automation, which
skips document_saved on every path; the inline fallback records through
it too, so both modes behave the same. A regression test installs a
saveDocument binding and asserts it fires for an interactive save and
not for app.save.

The desktop's two save workers collapse into one save_through parameterized
by the write step (drift between interactive and automation saves), the
Saved status/notices are translated (Saved {name}, 16 locales), the pending
reply carries warnings like file.save's, and the success payload is built
at one site.
2026-10-11 02:54:14 -04:00
Tanish MankuandGitHub c6a525cc99 Releasing a brush stroke no longer freezes: the commit takes the pixels the live stroke already drew (P32: 136 ms → 0.8 ms) (#2131)
* Releasing a brush stroke no longer freezes: the commit takes the pixels the live stroke already drew (P32: 136 ms → 0.8 ms)

Brush phase 4. On release the canvas dropped its live stroke and ran `paint.stroke` over every
point again (the whole stroke re-rendered), then re-composited the stroke's bounding box, although
the canvas already showed exactly those pixels. Long strokes froze for 100–250 ms at mouse-up
(#2068).

- `Session::offer_live_stroke` hands the finished live stroke to the next command. A
  `paint.stroke` / `paint.pencil` whose command, document revision, params, seed and points match
  it exactly takes its pixels instead of rendering (result `"fromLive": true`). Anything else
  ignores the offer and renders as before. It still runs through the normal dispatch
  (authorization, journal, one history step), and a replay renders identical pixels.
- The canvas feeds the release frame's last points to the live stroke first, then remembers the
  stroke's per-step damage for one revision (`CommittedStroke`): each canvas cache re-composites
  only the steps it hadn't shown, usually nothing, instead of the bounding box.
- P32 (brush release on a long stroke) becomes measurable: perf_scenarios times a 600-point
  200 px soft stroke on 24 MP. 136 ms → 0.83 ms on the GPU canvas, 257 ms → 0.71 ms on the CPU
  canvas (RTX 5090, release). Budget 16 ms, not enforced until a baseline run.
- Scorecard: TOOL-213-10 (⌃⌥-drag resize with readout) is done; TOOL-213-7 (live strokes for every
  brush-based retouch tool) is partial (Spot Healing, Mixer Brush, Background Eraser and Pattern
  Stamp still aren't live).

* Mask view test: a commit that takes the live stroke uploads nothing it already showed
2026-10-11 02:42:12 -04:00
e59bb2909b feat(ui): allow disabling Studio canvas background dots (#2317)
* feat(prefs): persist an optional canvas dot pattern setting (#2264)

* feat(canvas): respect interface canvas dot preference (#2264)

* test(prefs): cover canvas dot toggle persistence and reset (#2264)

* Translate the Show canvas dots preference and regenerate the scorecard

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 02:15:06 -04:00
Elvis33LEandGitHub 29f1ff9e90 Design: bounded GPU stack prefix reuse for property edits (#1164)
* docs: design bounded GPU stack prefix reuse

* docs: advanced blending stays on the CPU; cache admits whole grid chunks and serves partial rects
2026-10-11 02:08:08 -04:00
a8ad82dd46 GPU composite: reuse unchanged lower-stack chunks for property edits (#1169)
* perf(gpu): reuse unchanged lower-stack composite chunks

* test(gpu): verify half-float composite checkpoint reuse

* docs: record measured GPU composite reuse gains and limits

* perf(gpu): admit whole grid chunks, serve partial damage as sub-rect copies

Cache entries were keyed by the exact damage rectangle, so brush strokes
(a new partial rect every frame) filled the budget with entries that
never hit again. Entries are now whole chunk-aligned, chunk-sized grid
cells: a capture merges into the cell it overlaps at the rectangle's
offset, a read is served as a sub-rect copy of the covered part, and
coverage is tracked per cell so a resume only happens where every pixel
of the requested rectangle is cached. After a full render, any in-bounds
partial rectangle hits.

Regression test: two series of disjoint strokes inside the same chunks
leave composite_cache_bytes unchanged (no entry-per-rectangle growth),
a full refresh still finds its chunks and a top-layer edit afterwards
reuses them. The old partial-damage test now expects the covered-chunk
hit and checks outside-the-document renders admit nothing.

* docs: record the GPU reuse measurement in target-app-parity.md

The honest assessment moved from roadmap.md to target-app-parity.md on
main; keep main's roadmap and add only the dated measurement.

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 02:07:48 -04:00
skylar taylor-barrick d0a18cfd83 UI.9: keep browser bundle under size budget on current main 2026-10-10 20:56:34 -06:00
7926161595 Release prep 0.7.0: Linux riscv64 tarball + version bump (#2965)
* Release prep 0.7.0: add Linux riscv64 tar.gz to the release; bump to 0.7.0

- release.yml: new linux-riscv64 job, ported from GridCraft: cross-compiled on ubuntu-24.04
  against ports.ubuntu.com riscv64 libs, tar.gz only, photocraft-cli --version smoke-tested
  under QEMU. The draft-release job now needs it, so SHA256SUMS.txt covers it.
- packaging/linux/package.sh: CROSS_ARCH / CROSS_TARGET / CROSS_COMPILE / EMULATOR support.
- linux_libs: search /usr/lib/riscv64-linux-gnu on riscv64 (was the x86_64 triplet).
- README and docs/releasing.md list the riscv64 tarball.
- Version 0.6.0 -> 0.7.0 (cargo xtask version set), Cargo.lock refreshed; contributors refreshed.

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

* canvas: crisp elliptical marquee ants again in an unrotated view

#2732 (a27d7175) built the elliptical marquee's live ants in document space and snapped every
vertex to the half-pixel grid on its own. Near the edge midpoints the nearly flat arcs became
1 px stairs, whose antialiased steps are grey, so the ants no longer contrast on white or black:
drag_preview_canvas's marquee test fails on main (ubuntu and windows). In an axis-aligned view
(rotation a multiple of 90 degrees) draw the ellipse inscribed in the snapped screen box again,
as before #2732; rotated views keep the mapped ellipse.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:36:30 -04:00
skylar taylor-barrick 77f50330c6 UI.9: refresh shared panel integration against current main 2026-10-10 19:30:56 -06:00
3c70d3b34f Layer > Rasterize > Layer Style bakes effects; Rasterize Layer converts vector masks (#2245) (#2669)
* Add Layer > Rasterize > Layer Style; Rasterize Layer converts vector masks (#2245)

Nothing could bake layer effects into a layer's pixels, and Rasterize >
Layer refused a raster layer that has a vector mask.

New command layer.rasterize.layerStyle (Layer > Rasterize > Layer Style,
and in the Layers context menu). It renders the layer by itself through
the compositor at 100% opacity in Normal mode, so the effects, fill
opacity and masks end up in the pixels, including those past the canvas.
The layer's own opacity, blend mode, knockout and Blend If stay layer
properties. Type, shape, fill and Smart Object content is rasterized
first, all in one history step. With a Normal layer and Normal-mode
effects the composite is unchanged at 8, 16 and 32 bit. Effects in
another mode are evaluated against the isolated layer's transparent
backdrop, because one layer of pixels cannot hold several blend modes.

Rasterize > Layer and All Layers now also turn a vector mask into a
pixel mask ("all vector data") and leave effects live, as Photoshop
does. The menu enablement follows both changes.

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

* Regenerate docs/scorecard.md after rebase (#2245)

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-10 19:20:33 -04:00
325c5b3194 Tools: Single Row Marquee and Single Column Marquee (#2837) (#2848)
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>
2026-10-10 19:10:34 -04:00
9d3ba499ad Layer > New > Background from Layer; the Background stays at the bottom (#2626) (#2663)
* Add Background from Layer and keep the Background at the bottom (#2626)

Layer > New > Background from Layer was missing, and the Background was a
locked layer that Layer > Arrange (with an explicit layer), Reverse and
dragging in the Layers panel could move, or put other layers below.

The new layer.new.backgroundFromLayer command renders the active layer as
it looks over the background colour, drops it to the bottom and locks it
as the Background, in one undo step. It is unavailable when the document
already has a Background, and for groups and adjustment layers.

Arrange refuses to move the Background and stops other layers above it;
layer.moveTo and Reverse refuse edits that would move it off the bottom.
The Layers panel shows no drop indicator for those drops.

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

* Parity floor 629; the empty-space drop test uses a document without a Background

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-10 19:09:26 -04:00
dhiaaddinandGitHub 1e0f8844eb Font menus: add an Arabic sample for families that cover Arabic (#2535) 2026-10-10 19:06:05 -04:00
5b778b49f2 Move tool: highlight the layer Auto-Select would pick on rollover (#2834) (#2843)
* Move tool: highlight the layer Auto-Select would pick on rollover (#2834)

With the Move tool and Auto-Select on (or ⌘ held while it is off), hovering
the canvas outlines, in the theme accent, the layer (or group, per the
Auto-Select target) that a click there would pick. Preferences › Tools ›
Show Highlight on Rollover turns it off (on by default).

The hover asks the click's own hit test (pick_cmds::auto_select_target, now
shared with layer.pickAt), so a type layer is outlined anywhere inside its
text and fully locked layers are clicked through, as on a click. The answer
is cached per document revision and pointer pixel. Nothing is drawn while a
button is down, a drag or transform runs, or the selected pixels would move.

Part of #214 (scorecard TOOL-214-4, now partial).

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

* Move hover: follow main's moves_selected_pixels_with rename

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-10 19:04:19 -04:00
Sir StarchandGitHub 67e34a1d4c Themes: add Breeze Light and Breeze Dark (#2276) (#2335)
Two Studio-layout themes for KDE Plasma desktops: original colour values
chosen to sit close to Breeze's cool blue-grey surfaces and blue accent
(and its 5 px radius) so the app blends into a Plasma desktop, without
copying Breeze's colour schemes. Text readability comes first.

- theme.rs: ThemeKind::BreezeLight / BreezeDark. Every text colour (text,
  secondary, faint, accent, danger, warning) holds 4.5:1 on every surface
  and on hovered, pressed and selected states; the primary button is a
  deeper blue that takes white text and stays above 4.5:1 while it
  darkens on hover and press. Breeze's own inactive text and link blue
  are only about 2.8:1 on its light surfaces, so ours are darker. The
  close button goes red on hover.
- prefs: Theme/LightTheme::BreezeLight ("breezeLight") and
  Theme/DarkTheme::BreezeDark ("breezeDark"): Preferences > Interface,
  `prefs.set`.
- Window > Theme > Breeze Light/Dark Theme (`window.theme.breezeLight`,
  `window.theme.breezeDark`) and `ui.set {"theme": ...}` (aliases:
  breeze, kde, plasma for the light one; ...dark for the dark one).
- i18n: the four new labels in all 16 catalogs.
- docs/ui-design.md and ATTRIBUTION.md (original work, credits Breeze as
  the inspiration; Breeze's schemes are LGPL-2.0-or-later and none of
  their values, files or code are copied).
- Tests: names/aliases, surface order and blue-grey character, the
  contrast matrix for both themes, and the preference/mode selection.
2026-10-10 19:01:53 -04:00
0c4016a4db feat(ui): hold Alt+Space to zoom out, as in Photoshop (#2877)
* feat(ui): Alt turns a held temporary Zoom In into Zoom Out

Alt reverses the Zoom tool's direction, but a held Zoom In (Cmd+Space)
ignored it: with Zoom Out unbound or rebound, Cmd+Alt+Space zoomed in.
held_tool now reports Zoom Out whenever Zoom In is the held binding and
Alt is down, unless Alt is part of the Zoom In key itself (a rebound
Alt+Z still zooms in). The default keys don't change here.

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

* feat(ui): hold Alt+Space to zoom out, as in Photoshop

Photoshop zooms out with Alt+Space (Option+Space on the Mac), usually
pressed as Space first, then Alt, then click. PhotoCraft's Zoom Out
(hold) default was Cmd+Alt+Space only, so Space then Alt still panned.

Zoom Out (hold) now defaults to Alt+Space. Holding Cmd+Alt+Space
matches both Cmd+Space and Alt+Space equally; the previous commit's
Alt rule resolves that to Zoom Out, so it keeps zooming out. Saved
overrides are kept. Holding Space first also avoids the Mac's Spotlight
shortcut and the Alt+Space window menu on many Linux desktops.

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

* chore(contributors): credit FinnNk by GitHub profile name

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:56:04 -04:00
37cb99eb1f Let dock tabs be dragged to reorder within their group (#2272) (#2774)
Dock tabs only sensed clicks, so a drag on a tab fell through to the
strip behind it and moved the whole group; there was no per-group tab
order to change.

DockLayout gains tabOrder (tab names per group, like hidden_tabs, so the
Pro/Studio Color/Swatches swap is safe). It is read leniently: unknown
groups, names and wrong types fall back to the default order instead of
dropping the saved layout. It is drawn through visible_tabs, while
dockTabs and the panel bodies keep using the original indices.

Tabs on the dock strips now sense drags: dragging one along its strip
shows an insertion line and drops it there, a plain click still selects,
and a tab dragged off the strip moves the group as the strip does.
Lock Workspace stops both. The order is part of ui.set {dock}, the
remembered panelLayout and saved workspaces.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:47:36 -04:00
308c3369c0 Control: timeout errors name the likely causes (#2489) (#2497)
* Control: timeout text derives from the reply deadline (#2489 review)

The server deadline and both timeout replies now share one constant:
REPLY_DEADLINE drives the serve thread's clock and the text is built from
it, so the message can never claim a number the server no longer uses.

* Tests: drop the duplicate joined() helper left by #2782 and #2853 both landing

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-10 18:27:03 -04:00
46af2e6e30 Workspaces: preserve the embedding host menu and rail flags (#2620)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:22:30 -04:00
Arthur WagnerandGitHub 2194c7c58f Status bar: show document dimensions in the ruler unit (#2618) 2026-10-10 18:18:19 -04:00
Elvis33LEandGitHub 03f4cc0de1 Correct Photomerge roadmap status (#2831) 2026-10-10 17:57:51 -04:00
a3d2a6d486 Place: wire Resize Image During Place into the implicit fit (#204) (#2538)
* feat(engine): Resize Image During Place drives the implicit Place fit (#204)

* Merge origin/main; regenerate docs/scorecard.md

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:53:25 -04:00
6bbfa77230 Interface: wire Dynamic Color Sliders for the colour tracks (#204) (#2531)
* feat(ui): the Dynamic Color Sliders preference controls the colour slider tracks (#204)

* Regenerate docs/scorecard.md

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:31:24 -04:00
14dfa38581 Actions: double-click an action name to rename it (#2032) (#2769)
Photoshop renames an action in place when its name is double-clicked in the
Actions panel. PhotoCraft had no way to rename an action at all, so every
action kept its "Action N" default name.

- New engine command `actions.rename {"action": name|index, "name": str}`.
  Empty, whitespace-only and duplicate names are rejected (the old name
  stays). Nested `actions.play` steps that call the action by name, and
  its function-key binding, follow the rename. Refused during playback.
- Actions panel: double-clicking the name opens an in-place field with the
  name selected; Enter, Tab or clicking elsewhere commits through the
  command, Esc cancels. Double-clicking elsewhere on the row still plays.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:54:36 -04:00
939c8f2c70 Progress docs: target-app parity, gaps, alpha gate, readiness by audience (#2696)
Progress docs per craftrules standards/progress-docs.md: target-app parity (merges parity-estimate.md), gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and UI, brush, format, hardware and localization parity docs. docs/parity.md is now docs/parity-checklist.md.

The Windows test failure (file_dialog::tests::save_dialogs_remember_where_each_document_was_last_saved) is pre-existing on main: the test hard-codes POSIX paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:09:04 -04:00
5ab78e8ce1 feat(ui): the Use Smart Quotes preference turns typed quotes typographic (#204) (#2522)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:55:53 -04:00