* fix(compose): Preserve native CMYK fills through PSD round trips
Share depth-quantized ink samples between rendering and PSD caches.
Use the document profile for RGB fill colours and preserve valid imported
caches. Uncached CMYK fills use CPU rendering.
Cover all sample depths, gradient styles and tiled renders. Fix the real
16-bit gradient round trip and raise the psd-tools floor from 307 to 308.
* perf(raster): Reuse exact 8-bit CMYK conversions within rows
Cache the previous ink bytes and ICC result per row, with alpha decoded
independently. Keep the 16/32-bit conversion loops unchanged.
Matched 1 MP reads improve by 77% for smooth ramps, 33% with dithering
and 10% for random colours. Bitwise regression coverage includes all
depths, alpha and tile boundaries.
Open and save .ora with pixel layers (any offset), nested groups, pass-through
groups, names, visibility, opacity, edit/alpha locks and blend modes (svg:* plus
Krita's krita:* names), at 8 and 16 bit. Save applies pixel masks, renders
text/shape/fill/smart layers to pixels and reports what OpenRaster can't hold.
Wired into File > Open/Save, the CLI, automation/MCP, the web picker and the
desktop file associations. Checked against 34 Krita 5.2.9-authored files
(opt-in krita_corpus test) and Krita re-opening our exports.
Co-authored-by: Scott Campbell <251750033+scottcampbelldata@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- The live set found beside Photoshop is offered, never applied unasked: an
"Import keyboard shortcuts?" prompt names the set and how many keys it
changes, with Import / Don't Import. Either answer is recorded in
dialogs.photoshopShortcuts, so it is asked once; ui.inspect shows the
pending offer as kysOffer.
- Parsing and label matching moved to the engine (photocraft_engine::kys), so
edit.keyboardShortcuts {"importKys": <xml>} imports a set from agents, the
CLI and MCP; the app's dialog uses the same plan with its menus' labels.
- "The keyboard shortcut set was not imported: {error}" is translated.
- .kys input over 4 MiB is refused before it is read as text.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Expose native shape stroke controls with staged previews
* Document shape stroke defaults in MCP control schema
* Regenerate docs/scorecard.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Distance fields: the Euclidean passes run on threads, results unchanged (#2119)
The chiselled bevel fields (#2006) and the precise glow fields are built
from an exact Euclidean distance transform, a few sweeps over the whole
map that all ran on one thread. They now run on scoped OS threads, in
groups of rows or columns handed out from a queue: edt_nearest (the
column pass, then the row pass), sharp_edges (rows), refine_edge_normals
(one edge pixel each) and the final per-pixel maps of the bevel and plain
Euclidean fields. Not rayon, as for the blurs (rows_par): maps are built
from inside rayon tiles, where a waiting worker steals other tiles (#276).
Maps under 65536 pixels stay on the calling thread, wasm always does.
Every unit is computed from its own inputs, in the same arithmetic as
before, so the fields are bit for bit the old ones whatever the thread
count: 216 fingerprints (bevel in and out, plain in and out, bevel_maps on
bars at 0 to 150 degrees, thin bars, discs, rounded rectangles, soft,
patchy and half-opaque masks, 40 stars, tiny and 1 pixel maps) are equal
at 1, 7, 16 and the default threads, and equal to the code before.
edt_nearest returns the nearest pixel as u32 (it was usize), which takes its
working memory from 20 to 16 bytes per pixel; a map past 2^32 pixels or a slice that does
not match its size gets "far" distances instead of a panic.
CPU compositor, 12 MP, bevel size change (ms, bevel_bench), main -> this:
ellipse 474 -> 217 (size 8), 990 -> 318 (250); text 747 -> 214 (8),
1110 -> 267 (250). A distance_field call on a 256x256 map goes from 2.2 to
1.8 ms, on 512x512 from 8.0 to 2.8, on 2048x2048 from 99 to 15.
Tests: par_units hands every unit to the closure once with its own
slices (short last unit, no second slice, units of different sizes, 1 to
64 threads); the nearest-pixel transform equals itself on 1, 2, 3 and 8
threads on awkward sizes and returns a seed at the distance it reports;
degenerate sizes; sharp edges and the four Euclidean fields are bit-equal
on 1, 2, 5 and 16 threads on discs, rounded rectangles, bars at several
angles, patchy opacity and an edge on the border. The nested-rayon
deadlock test (#276) now has a chiselled bevel layer in its document.
* GPU effect maps: the Euclidean fields are one window, not bands (#2119)
A bevel size change rebuilds the distance fields over the whole effect
region (the region grows with the size). The GPU path cut that into bands
of max(128, halo) rows, each computed from a window grown by the halo,
which for the bevel fields is reach + 10: at size 250, 262-row bands from
windows of about 786 rows. Every pixel went through the transform and the
edge refinement about three times, a 12 MP region gave about 12 tasks for
32 threads, and the two bevel fields ran one after the other.
The Euclidean fields (chiselled bevels, precise glows) now come from one
window, and distance_field spreads its own passes over the threads. A
rebuild is the whole region, which is also what the CPU compositor
computes, so the GPU canvas gets exactly the CPU's field: bands resolved
ties between equidistant edge pixels per window, and on a rebuild they
differed from the whole region by up to 0.07 px (ellipse) and 1.3 px (600
discs) at size 250, on a few dozen pixels in 12 MP. A partial update (a
brush dab) still computes the damage grown by the halo, from one window.
The chamfer fields (shadows, strokes, spread) are a sequential sweep and
keep the bands, and so does wasm, where there are no threads to spread
over and bands keep the memory small.
RTX 4080 SUPER, release, 12 MP, ms per size change on the GPU canvas
(bevel_bench from #2118, p50, median of 3 runs, before #2006 / main /
this), ellipse: 90 / 103 / 98 (8), 162 / 188 / 119 (50), 286 / 446 / 219
(250); text: 83 / 139 / 113, 140 / 215 / 130, 222 / 489 / 165. Alone in the
process (--no-cpu) ellipse at 250 is 407 -> 157 and text 468 -> 169.
36 MP ellipse at 250: 779 -> 382. Peak RSS: 12 MP at 250 1604 -> 862 MiB,
36 MP at 250 2987 -> 1726 MiB; at size 8 on 36 MP it is 163 MiB more
(1380 -> 1543). Precise glow fields on 12 MP: 46-84 ms -> 26 ms at reach
52-252 (32 ms at 12).
The window test now checks that the whole region is bit for bit the CPU's
field, and that a too small halo is noticed on a part of the region.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
doc_open, doc_save and the other automation file paths only took
forward-slash relative paths, so an agent passing C:/Users/me/a.png with
a C:/ root was refused. An absolute path is now compared with the
root's absolute path as text: if it lies beneath the root, the root's
components are stripped and the rest goes through the same checks as a
relative path and is opened through the same directory capability.
Nothing is resolved on the filesystem, so traversal, prefixes, device
names and link escapes are refused as before; absolute paths outside
the root, or with no root granted, are still refused.
Fixes#2176.
* Expand arithmetic expressions across numeric inputs
* Test arithmetic and scientific notation in indexed color counts
* Drop the numeric_fields example (it wrote PNGs to /tmp; the widget tests cover the behaviour)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
On a smart-object layer, Image > Adjustments now records an editable
smart filter instead of committing disaster.
- new engine helpers:
- adjust_cmds::has_adjustable / adjust_as_smart_filter route
image.adjustments.<kind> onto the source, with the selection as the
filter mask, through smart_cmds::add_smart_filter
- brush/eraser/pencil/live preview strokes now render inside
with_cmyk_space(doc_cmyk_space(session)), so painted colours follow the
document profile the same way the compositor does
- docs/scorecard.md: FILE-215-4 partial -> done
- tests: adjust_cmds/tests + brush_cmds/tests, plus panic_hunt and
the lib suite all green on the committed tree
Known follow-up, already filed:
- FILE-215-5 rows: double-clicking an adjustment smart-filter row is still
a no-op. smart_ui::open_editor only proceeds when
filter_dialog::has_dialog returns true, and has_dialog accepts
filter.*, a fixed non-adjustment set and PREVIEWED - but not
image.adjustments.* (those open through adjust_dialog::open, which isn't
wired into the smart-filter row).
This is one commit on top of the latest upstream/main (rebased to fix the
stale fork ancestry that was blocking PR creation).
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>
* feat(ui): follow system appearance with a System theme
Follow the OS appearance through the existing egui input while retaining System as the saved theme choice. Apply the resolved palette to both egui styles and custom surfaces, and keep document surround settings independent.
Cover launch and live transitions, persistence, manual themes, menu checks, the Interface picker and Home contrast. Validate engine/UI suites, adversarial commands, wasm, layering, parity and the AutoReview gate. Strict Clippy reports pre-existing findings documented in the PR.
Refs: storytold/photocraft#2074
* Keep the document surround on the displayed theme in Sync with system (drop the dark-surround pinning)
---------
Co-authored-by: coygeek <coygeek@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Pen pressure curve: Preferences › Tools shapes how pen pressure reaches every brush
Brush phase 6a (TOOL-213-11, #610). Every pen paints its pressure straight through, so artists
with a heavy or a light hand, or a tablet whose response feels off, had no way to adjust it.
- `tools.pressureCurve`: [input, output] points, linear by default, settable with `prefs.set`
(validated: 2..=16 pairs within 0..1).
- `prefs::PressureCurve` reads it as a monotone cubic (Fritsch–Carlson), so a firmer press never
gives less pressure. It sanitises the points itself (finite, clamped, sorted, one per input,
capped, outputs made non-decreasing; flat beyond the ends), because a whole-section update or a
hand-edited file can store anything.
- The stylus maps every pen sample's pressure through it before any brush sees it, so the live
stroke, the commit and the journal agree. A mouse stays at full pressure.
- Preferences › Tools draws a curve editor under Use Tablet Pressure: drag a point, click to add
one, right-click to remove one, with Linear, Soft and Hard presets. Translated in all 16 catalogs.
* 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>
* Fix SVG import detection and configurable group depth
* Fix malformed SVG error routing and formatting
* Refresh scorecard for SVG depth preference
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Photoshop switches to the Move tool while Cmd (Ctrl off the Mac) is held
with every tool but the Hand, Rotate View, Zoom, Crop, Slice, Path Selection,
shape, Pen and Type tools. #896 did this for the selection tools per press; this
does it for the rest (Brush, Eraser, Gradient, Clone Stamp, Eyedropper, ...).
hold_keys gains Temporary::Move (a modifier, not a rebindable key); the
canvas sets PhotocraftApp::tool_override for the frame and the tool state
machine, the Move tool's selection check (move_ui::moves_selected_pixels)
and the tool modules that gate on the tool read active_tool(). The held-tool
latch now lasts through the release frame, otherwise the Move's Up went to
the selected tool. So Cmd-drag with the Brush moves the layer, Cmd+Alt-drag
duplicates it first (move_mods), Cmd-drag with a selection floats the
selected pixels, and Cmd-click picks the layer under the pointer.
Off the Mac, Cmd+Alt is Ctrl+Alt, which the brush tools keep for the
on-canvas resize (brush_resize, also Alt+right-drag there): with a brush
tool Ctrl+Alt-drag resizes, with the other tools (Gradient, Eyedropper...) it
duplicates. Tests pin both layouts on any host. docs/control-protocol.md
describes `command` on ui.pointer events.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Masks: target a new layer mask and keep separate colour pairs for mask and pixels (#2166)
Photoshop keeps one foreground/background pair for layer masks and one for
layer pixels and swaps them when the edit target changes; adding a mask
targets it, with white/black. PhotoCraft never touched the colours when a
mask was targeted, and adding a mask left the pixels targeted.
- engine: ToolState::target_mask swaps in the other target's pair
(mask pair starts at white/black, colours picked on either side are kept).
- ui-egui: sync_mask_targets applies the effective target (mask targeted,
active layer has a mask, composite channel, no Quick Mask) after every
command, every frame and every control request; the Layer Mask add
commands target the new mask.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fix merge: close mask colour-pair test before the deletion tests
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* scorecard: Marquee Shift, Alt and size readout are done (TOOL-214-2)
The row still said canvas.rs passes false for Shift (#208), but #208 is
fixed: marquee_corners applies Shift (square or circle) and Alt (from the
centre), the W x H readout follows the drag, and marquee_tests.rs covers
all three.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* scorecard: on-canvas brush resize drag is done (TOOL-213-10)
The row said only [ ] resize the brush, but brush_resize.rs (#297, #370)
implements Photoshop's drag: Alt+right-drag, or Control+Option-drag on
macOS, sets diameter and hardness with a circle and readout, and is
covered by nine tests.
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>
* Use shared Color Picker for Layer Style colors (#2015)
* Use shared Color Picker for text style colors (#2015)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Edit › Keyboard Shortcuts: Import Photoshop Shortcuts… loads a .kys set (#1163)
Photoshop saves a shortcut set as .kys XML: <command name="Gaussian Blur...">
rows with one or more <shortcut>Control+Opt+Cmd+G</shortcut>, then the tool
keys. The dialog's new button (and File › Open or a drop of a .kys) parses it
with roxmltree, matches each row to our commands by menu label, and fills the
dialog's overrides, so OK applies the set the way hand-made changes are
(a key moved to a command is taken from its old owner) and Cancel discards it.
Rules, from Photoshop's own ambiguities: a label in several menus (Levels…
under Image › Adjustments and Layer › New Adjustment Layer; All under
Select, Edit › Purge and View › Show) goes to the command whose default is one
of the row's keys, else the one with a default, else the first in menu order.
A row's first key is kept unless one of its keys is already the command's
default (Zoom In is Cmd++ then Cmd+=). Scripts, Bridge, the frames commands
and the macOS application menu have no command here and are counted in the
message. Tool keys are not imported: the tool letters are fixed.
* First launch beside Photoshop imports its live shortcut set; the Open dialog lists .kys
Photoshop keeps the set in use as Keyboard Shortcuts.psp in its per-version
settings folder (macOS ~/Library/Preferences/Adobe Photoshop <v> Settings,
Windows %APPDATA%\Adobe\Adobe Photoshop <v>\Adobe Photoshop <v> Settings),
the same XML a saved .kys holds. The desktop app hands the newest version's
file to the UI (Services::photoshop_shortcuts); at the first launch that has
no shortcut overrides yet, kys_import::auto_import applies it through the
Keyboard Shortcuts dialog's own OK path and posts a notice naming the set and
the count. The preference dialogs.photoshopShortcuts records the source, so
the import happens once; someone who already changed a shortcut keeps theirs.
Reset All to Defaults undoes it.
The Open dialog's filter now lists .kys (it greyed them out), and dropping
Keyboard Shortcuts.psp imports it like a .kys.
* kys import: Ukrainian and Dutch strings; tests follow Cmd+K and Cmd+F as the defaults (#1900)
* kys import: user-facing strings name no other product (#1985)
The button is Import Shortcuts…, its tip names the .kys format, the notice
says "Keyboard shortcuts imported" and names the set, and a file that is not
a set is "not a keyboard shortcut set (.kys)". A set without a name is called
by its file name. Comments, identifiers and the XML root tag keep the format's
own name.
* rustfmt: format panels.rs and prefs_ui.rs
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Notices: auto-hide after a delay, with Interface preferences (#2022)
Non-blocking notices no longer stay on screen until closed: each card
hides itself after Interface › Notification › Auto Hide Notifications'
delay (new, default 6 s), and the timer pauses while the pointer rests
on the stack until it moves away. Two new Interface preferences,
notificationAutoHide and notificationDurationSeconds, are read by
notices.rs; errors still land in the status bar.
The decision lives in the pure notices::expire helper, unit-tested
without an egui clock; Notice.shown_at round-trips through the control
channel with serde defaults. Labels translated across the complete
catalogues. Regenerated docs/scorecard.md.
* Notices: repaint on expiry, and a fresh delay when a long hover ends (#2022)
Addresses review of the auto-hide change:
- When the last notice expires, `show` had already painted it and, with
nothing left, requested no further frame, so the card lingered until
unrelated input. It now requests a repaint in the frame that drops
anything, clearing the stack at once.
- A stationary hover over a settled UI refreshed `shown_at` only on
frames, so hovering longer than the delay then leaving expired the
notice instantly. `show` now tracks the pointer leaving the stack and
restarts the timers on that frame, so hovering never counts as elapsed
time.
Regression tests: expiring a notice requests a frame to clear it;
leaving a long hover gives a fresh delay.
---------
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>
* Toolbar: Studio themes draw large round colour chips with Switch Colors right below
* Toolbar: round chips keep Default Colors, follow Tokens::round_chips, lie side by side in two columns
* Theme: Solarized Dark and Adwaita use the Studio round colour chips
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Brush preset chip: draw the tip preview in the icon colour on light themes
The options-bar chip always painted the round tip white, which nearly vanished on Studio
Light's chrome (246 gray) and disappears entirely on a white header bar. Dark themes keep
Photoshop's white tip; light themes use their icon token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add Adwaita and Adwaita Dark themes
Two opt-in themes that put the Studio layout in GNOME's libadwaita palette, so PhotoCraft
sits with the rest of a GNOME desktop: header bar, sidebar, card and view colours, the
GNOME blue accent (#3584e4), destructive and warning colours, 6 / 9 / 12 px radii, and a
neutral (not red) close button as in GNOME's title bars. The default theme is unchanged.
Wired through ThemeKind, the interface.theme preference (adwaita, adwaitaDark), Window ›
Theme and the control channel, with menu and preference labels in every complete-menus
language ("Adwaita" kept as the proper name). A test holds both themes to WCAG contrast:
7:1 for text and 4.5:1 for dim text on every surface that carries labels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: translate the Adwaita theme names in Dutch and Ukrainian
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Squashed onto main, with the maintainer's asks applied:
- Photoshop's blend menus (layers, brush, pencil, gradient, layer style) stay as they are;
the Layers panel and Blending Options list a Paint.NET mode only for a layer that uses one.
- The six Paint.NET mode labels are translated in every catalog, and the blend-mode i18n test
iterates layer_modes().
- pdn::pixels rejects truncated uncompressed (version 1) data before allocating its buffer.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fix mask trash targeting and Solid Color Fill editing
* Fix Solid Color picker mode conversion and validate RGB ranges
* Apply rustfmt to Solid Color regression tests
* Fix Solid Color mode regression test using distinct RGB values
---------
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* ci(web): build browser release and enforce wasm gate on PRs
* docs(web): describe PR browser build and size checks
* CI web gate: run only when web-relevant files change (main always)
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>
* Save in the background so large saves don't freeze the window (#2017)
File › Save and Save As encoded and wrote the whole document on the UI thread,
so a multi-GB layered TIFF or PSB save left the window unresponsive for the
whole write, with no progress.
Saves now run as a background job: a new `Services::save_file` encodes and
writes atomically on a worker (the desktop app provides it), the document is
locked meanwhile, and the existing progress dialog shows the stage with Cancel.
Cancelling while encoding leaves the file untouched. The save is recorded when
the job ends, the unsaved-changes prompt waits for it, and a window close
during a save is held until it ends. Without the service (web, tests) saves
stay synchronous.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Background save: a save can't be cancelled once writing starts (#2072 review)
- `Services::save_file` now gets a `SaveCtl` and calls `commit()` right before
replacing the file. Cancel and Esc are refused after that, so a "Cancelled"
save can no longer replace the file while the document shows unsaved.
- A save's worker stays counted (`saving()`) until it returns, even when its job
was cancelled: quitting waits for it, and another save to the same file is
refused until it is gone.
- A failed or cancelled save no longer closes a window whose close was held.
- The pending reply carries the job id, and the job becomes `last_started`, so
control and MCP clients can wait for the save.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#2006 made a bevel size change dearer on purpose (+10-75% on the GPU canvas,
2x for CPU text bevels) and asked for a bench of it under perf/.
`bevel_bench` renders a 12 MP document with one Chisel Hard inner bevel on
an anti-aliased ellipse (a smooth outline) or on a text paragraph (many thin
edges). Each case warms the caches one pixel below the size, then times
refreshes with the size growing by one pixel each, so the GPU canvas
rebuilds its distance fields every time (it keeps them while the size
shrinks) and the CPU compositor misses its map cache. Sizes 8, 50 and 250,
GPU and CPU, with `--scene`, `--size`, `--reps`, `--no-cpu` and `--json`.
Registered in perf/budgets.toml as P52-P59 with no budgets (track only,
like P45 and P46): GPU for ellipse and text at 8, 50 and 250, CPU at 8.
docs/scorecard.md is regenerated; the rows read "not run" until the next
baseline.
RTX 4080 SUPER, release, 9 reps, p50 ms, before #2006 (3bc49f5e) -> after:
ellipse GPU 88 -> 105 (8), 169 -> 199 (50), 305 -> 460 (250); text GPU
119 -> 140, 141 -> 252, 238 -> 557; CPU text at 8 402 -> 797. Runs vary by
about 20% on this machine.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Import Affinity issue 1606 special shapes
* Affinity corpus: give the #1606 sample ceilings a margin for font differences between machines
The samples' labels render with whatever fonts are installed, so the measured
differences move by up to 0.8 between machines; each ceiling is now the higher
measurement plus about 1. Also use as_chunks in on_white (clippy with --features corpus).
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>
In Photoshop a single click (no drag) with a shape tool opens a creation
dialog to type the exact size. PhotoCraft ignored the click: the drag
geometry was below 1 px, so finish_shape returned without doing anything.
- New shape_dialog module: Create Rectangle (width, height, four corner
radii with a link toggle, From Center), Create Ellipse, Create Triangle
and Create Polygon (sides, star ratio). Width and height show in the
ruler unit and are stored in document pixels; values are remembered per
tool. OK runs the existing shape.create command at the clicked point
(or centred on it) with the options bar's fill and stroke; invalid
sizes/positions are rejected with an error, never a panic.
- canvas::finish_gesture opens the dialog when the gesture never left
the press point by more than 3 screen px; drags draw exactly as before.
Line and Custom Shape keep ignoring clicks.
- vector_ui::create_shape shares the fill/stroke params with the drag path.
- Catalog entries for the new labels and titles in every complete language.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Crop options bar gets Photoshop's gear menu (Set additional Crop options): Show Cropped Area
(H toggles it while the Crop tool shows a box; off hides everything outside the box behind the
canvas colour) and Enable Crop Shield with Color (Match Canvas, the pasteboard colour, or Custom…,
which opens the Color Picker), Opacity (default 75 %) and Auto Adjust Opacity (half the opacity
while the box is dragged). The shield used to be a fixed black at about 51 %. The options persist in
ToolOptions.crop_shield (older settings get Photoshop's defaults; a stray opacity is clamped) and
ui.set takes cropShield. Classic Mode and Auto Center Preview are left out: PhotoCraft always crops
in Classic Mode.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Crop options bar gets Photoshop's Straighten button (one-shot), and holding ⌘/Ctrl with the
Crop tool is a temporary Straighten. The drawn line (with an angle readout) turns the frame so the
line ends up level, or plumb when it is nearer vertical (the Ruler's straighten_angle convention),
and fits the frame to the largest of its proportions inside the image (Classic Mode). It is a
pending frame edit: Enter commits, Esc cancels. New logic lives in crop_straighten.rs.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The wheel over the Layers panel's blend-mode dropdown (closed or open) steps to the next or previous mode, one per notch and one undo step each, stopping at the ends, as in Photoshop. Fractional wheel deltas add up to whole notches, and the wheel there no longer scrolls the panel (#1747).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
View zoom now means device pixels per document pixel, so 100% shows one image pixel per physical
display pixel on a scaled (125%, 200%) display, as in Photoshop. Fit, Actual Pixels, pan/zoom anchoring,
scrollbars, the navigator, the pixel grid and every hit tolerance go through canvas_ppp / point_zoom.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Crop options bar's overlay button opens Photoshop's overlay menu: six overlays, Auto/Always/Never
show modes, and O / Shift+O to cycle the overlay and its orientation. The default is Rule of Thirds with
Auto Show Overlay, as in Photoshop. ui.set takes cropOverlay, cropOverlayShow and cropOverlayOrientation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Large Tabs makes the document tab strip taller (34 px) with larger type; turning off Export Clipboard stops copies being mirrored to the OS clipboard; Show Channels in Color tints the Channels panel thumbnails (RGB from black to the primary, CMYK from white to the ink). Settings that do nothing: 48 of 141 (#204).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a pending crop frame, the arrows move it 1 px (Shift 10 px) instead of the selection or layer, X
swaps it between portrait and landscape about its centre (with the options-bar ratio), and the frame's
W x H shows beside the pointer while drawing or resizing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preferences > File Handling > Lowercase Extension is live: with it on (the default) a save path's extension is lowercased (CAT.PSD saves as CAT.psd); off keeps the user's spelling. Settings that do nothing: 51 of 141 (#204).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>