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).
* 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>
* 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>
* 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>
* Let the Navigator preview fill its dock group, drop the scroll fade (#2726)
The Navigator drew a fixed 150 pt preview inside the dock's outer
ScrollArea, so a taller group left dead space below the zoom slider,
and a shorter one scrolled with egui's scroll fade painting a dark
gradient over the thumbnail. The same fade darkened the Color field
and swatches at the bottom of their groups.
The Navigator tab now lays itself out (like Layers and History): the
preview takes the group's height above the zoom controls, keeps the
document's aspect and stays centred. The dock's outer scroll areas no
longer paint the fade; their scroll bar already shows there is more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Navigator wheel test: point below the preview, which now fills the group
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>
* 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>
The appearance worker stored the first portal/gsettings answer and
signalled readiness, but only SettingChanged updates requested a
repaint. If Read replied after the 250 ms start-up wait, an idle Auto
window kept its fallback palette until some unrelated event.
Route the initial read and later signals through one publish step that
repaints the registered context whenever the value changes, and move
the thread/wait setup into start() so a test can drive it without D-Bus.
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>
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 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>
* 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>
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.
* 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>
* Smart Objects: Copy/Paste with no selection keeps the smart object (#2428)
Photoshop's layer-based copy (CC 2018 on) copies the active layer whole
when there is no selection. Edit > Copy always lifted the layer's pixels,
so Paste made a raster layer from a smart object. Copy now also keeps the
smart object layer on the clipboard (with its pixels for Paste Into,
masks, channels and other apps). Paste recreates it with contents of its
own, converted to the destination's colour and depth and placed as the
pixels would be. With a selection, Copy still takes pixels.
The colour/depth/contents-id handling of layer.copyToDocument moves into
shared helpers (adapt_copies, translate_copies) used by both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit: Copy/Paste with no selection keeps type, shape and group layers (#2670)
Photoshop's layer-based copy, verified in Photoshop: with no pixel
selection, Copy takes a type layer, a shape layer or a group (its whole
subtree) whole, and Paste / Paste in Place recreate it instead of a
pixel layer. #2428 did this for smart objects; this extends the same
path. Pixel layers, several selected layers and any copy with a
selection still take pixels.
A group has no pixels of its own, so Copy (now enabled for a group with
no selection) puts its composite on the clipboard for Paste Into, masks
and other apps.
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>
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>
Small aliased tips (dabs at most a pixel apart) now walk each segment
pixel by pixel on the line between the pixels holding its ends, one dab
per pixel along the major axis at the pixel's dab centre. Distance
spacing put two dabs in a row wherever the line crossed a pixel corner,
which left L-shaped doubled pixels at every step of a Pencil line.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Photoshop's layer-based copy (CC 2018 on) copies the active layer whole
when there is no selection. Edit > Copy always lifted the layer's pixels,
so Paste made a raster layer from a smart object. Copy now also keeps the
smart object layer on the clipboard (with its pixels for Paste Into,
masks, channels and other apps). Paste recreates it with contents of its
own, converted to the destination's colour and depth and placed as the
pixels would be. With a selection, Copy still takes pixels.
The colour/depth/contents-id handling of layer.copyToDocument moves into
shared helpers (adapt_copies, translate_copies) used by both.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
#1942 stopped tabs from shaping as .notdef by laying them out as a
space, so a tab was only one space wide. Layout now widens each tab to
the next default stop (every 36 pt, from the anchor of point text or the
left edge of the box). Paragraph text breaks lines with the real tab
widths: it starts from a safe upper bound (space + one interval) and
re-breaks until the widths agree, so expanded tabs never overflow the
box and lines don't wrap early. The text keeps its \t, so PSD round
trips are unchanged. Tab in the Type tool now types a tab.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#2782 and #2853 both fixed the save-dialog test's path expectations; together they left a
top-level joined() helper shadowed by an identical local closure, so clippy -D warnings fails
with "function joined is never used". The test now uses the documented helper.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(prefs): search visible settings across sections (#2593)
Filter the Preferences sidebar by section and editable field labels,
switch to the first match, and show matching settings in the editor.
Keep the draft intact while searching and hide Reset Section during filtering.
* Preferences search: translate the field hint and empty state; rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge fixups: pass the new search argument in a main test; search test uses a visible Type setting
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>
Applying a textReplacement value cleared the layer's character and
paragraph runs, so the text re-flowed from the summary fields (family,
size, colour) and lost weight, italic, tracking, underline, caps and the
rest of the run style. The new text now takes the first character's style
and the first paragraph's settings, as in Photoshop. The value is kept
verbatim, as before.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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>
View > Pixel Aspect Ratio and its Correction toggle only stored a value:
the canvas transform (ViewXform) had no notion of pixel aspect, so the
image was always drawn with square pixels.
ViewXform now carries the displayed pixel aspect ratio and stretches
document x by it before rotation and flip. Drawing, pointer-to-document
mapping, pan and zoom-about deltas, rulers, grid, Fit/Fill on Screen,
scrollbars, the Navigator's view box and paste-in-place visibility all
go through it. Correction is on by default, as in Photoshop. A corrected
view draws through the CPU canvas path, like a flipped one, since the
GPU shader assumes square pixels. The document's pixels are unchanged.
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 web file dialog called rfd's pick_file() and ignored the request's
`multiple` flag, and FileDialogAnswer could only carry one file's
contents. Pick several files when File › Open asks for them, answer
with the new ContentsMany variant, and open each as its own document,
reporting a failed file without stopping the rest (as the desktop's
multi-path open does).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With several layers selected and no pixel selection, Cmd+J now copies every
selected layer in one undo step (each copy above its original, the copies
selected), reusing Duplicate Layer's multi-layer path. With a pixel
selection it still copies the active layer's selected pixels.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The schema dialog draws its fields in the order of the command's params
doc, which put Orientation last, below the three sliders. Photoshop's
Warp Text dialog has Style, then Horizontal/Vertical, then Bend,
Horizontal Distortion and Vertical Distortion. Reorder the params doc.
Live preview and opening with the layer's current warp already landed
in #2539 (after v0.6.0).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Crop / marquee size readout in the Units & Rulers unit (cm, mm, in…) as Photoshop shows it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Properties: Transform W/H/X/Y and Canvas W/H in the Units & Rulers unit (cm, mm, in…), typed values converted back to px
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge origin/main: size readout test follows the trimmed unit format
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>
* Free Transform on type: whole-layer frame, grey Distort/Perspective (#2630)
With a selection active, Free Transform on a type layer framed only the
selected part of the text and the preview lifted half of the rendered
glyphs, but the engine then transformed the whole type layer, so the box
and the result disagreed. Type, shapes and smart objects always move
whole, so transform_bounds no longer intersects their frame with the
selection and the preview no longer lifts pixels out of them.
Distort and Perspective stayed enabled for editable type and then failed
on commit. They are now greyed for type layers (menu bar, marquee and
transform-box context menus), as in Photoshop, unless an unlinked mask or
channel is targeted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* transform_type_tests: set CanvasToolMenu::pasteboard (#2630)
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>
* 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>
The live ants for the Rectangular/Elliptical Marquee (and Object Selection)
were an axis-aligned screen box built from the two projected corners of the
drag. A rotated view projects the drag's document rectangle onto a rotated
one, and the box through two of its corners is a different shape in a
different place: on a 30 degree view a corner landed 21 x 50 px away from
where the selection it previews was committed. The release was always right,
only the hinting was drawn elsewhere (#2512).
marquee_drag_ants builds the outline in document space and maps every point
through the view instead: four corners for the rectangle, the ellipse
inscribed in it for the elliptical marquee, with as many vertices as its size
on screen asks for. Every point is snapped to the half-pixel grid like every
other ants outline (marching_ants_segments), so the lines stay crisp at any
zoom. Unrotated the mapping is only a scale, and the corners come out where
they always did.
Tests drag a marquee in a 30 degree view for both tools and check the drawn
outline against the selection the release commits: each corner is the mapped
corner of the dragged rectangle, and each point of the ellipse maps back onto
it.
Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com>
PAR_MIN_PIXELS / PAR_CHUNK_PIXELS (photocraft-cms) and OPENABLE
(photocraft-engine) are only used by native-only code (the rayon paths
and list_images). Give them the same cfg so wasm32 check and clippy
-D warnings are clean. No behaviour change.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Create Texture (inpaint::synthesize) averaged each block 50/50 into the
already-synthesised overlap. Averaging two unrelated grain patterns cuts
their standard deviation by about 1/sqrt(2), so the fill came out smoother
than its surroundings (75 % of the grain for one 30 px dab), and every
overlapping stroke, which samples the previous ones, lost more (57 % after
eight). Blocks are now joined along Efros & Freeman's minimum-error
boundary cut (Image Quilting, SIGGRAPH 2001): each pixel is copied from
one block or the other, never averaged.
Content-Aware and Proximity Match already keep the grain on main since
#2202 (patch copies instead of voting blur); the new test guards all three.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Wet Edges multiplied every dab by its own rim profile and max-accumulated the
dabs, so the rims of neighbouring dabs showed through the stroke as a chain of
rings. Dabs now accumulate normally and record how deep they reach each pixel
(1 - rn); the stroke keeps the deepest value per pixel and applies the
wet-edge transfer to its accumulated coverage when composited (live preview,
tail preview, commit and retouch coverage alike). The interior of the stroke is
evenly light and only the stroke's own rim is darker.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Showing, hiding or editing a smart filter re-rendered the whole stack from the
placed source on the UI thread: on a 3000x4498 layer with five filters every
click on a filter's eye froze the window for about 1.5 s with no feedback.
- Each render keeps the placed source and the result after every visible filter
(blake3 of the source contents, the placement and the filters so far), so an
edit reruns only the filters from the edited one up. Edit > Purge > All drops
them.
- Smart filter edits apply the edit at once (so bad params still fail
immediately) and re-render as a background job when started with
Session::start: the window keeps drawing, with progress and Esc to cancel
between filters. Session::execute is unchanged.
* Pencil/Eraser: round Pencil cursor and Eraser Pencil mode (#2662)
The Pencil cursor always outlined the square grid_square footprint, but the
aliased dab is a disc from 4 px on. It now draws a circle on the same
pixel-grid square when the tip is round, and the square for 1-3 px tips
whose dab really fills it.
The Eraser options bar gains Photoshop's Mode: Brush / Pencil (replacing
the blend-mode dropdown the Eraser never used). Pencil mode commits
paint.pencil with erase: true (hard, aliased, full flow), live and on
release, and shows the Pencil's cursor. Block mode is left for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Eraser: translate the Pencil mode label in every catalog
The Eraser's Mode dropdown added tl!("Pencil"), which had no entry in the
cs, el, ko, zh-hans and zh-hant catalogs, so every_tl_literal_is_translated
failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Eraser: Block mode (#2770)
Photoshop's Eraser Mode is Brush / Pencil / Block; #2694 added Pencil.
Block erases a hard, aliased square of 16 screen pixels, so 16 / zoom
document pixels, whatever the brush's size, hardness, opacity or flow.
- paint: BrushSettings.square (with aliased) rasterises the dab's whole
pixel-grid square (serde: omitted when false, so saved brushes are
unchanged).
- engine: paint.pencil "block": true builds that brush from the stroke's
"zoom" (brush_cmds::block_size: round(16 / zoom), 1..5000 px); a zoom
that is not a positive number is an Err. Params doc updated.
- ui: Mode dropdown gains Block; the Block-mode Eraser commits
paint.pencil with "block": true, shows the block's square cursor and
greys out Size/preset, Hardness, Opacity and Flow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Eraser: translate the Block mode label
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>