Commit Graph
11 Commits
Author SHA1 Message Date
b7e0d3810d History: count retained tiles incrementally below the byte budget (#1157)
Every edit's History::trim rebuilt a set of every tile in every retained state, even well
under the budget. Retained tile references are now counted as states are recorded, evicted,
undone, redone, purged and cleared, and below the budget trim only looks at the current
document's tiles. Record plus trim on a 24 MP, 150-layer document with 50 states drops from
62 ms to under 2 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:48:31 -04:00
4dcb2e4088 Undo: a paint stroke's undo keeps the painted layer active (#1356) (#1523)
Selecting a layer is not a history step, so each state kept the target it
had when it was created. Undoing a step therefore jumped back to whatever
was selected when the previous step was taken: rasterize a shape with the
Background active, select the rasterized layer, paint, Ctrl+Z, and the
Background became active again.

The state a step leaves behind now targets what was selected just before
the step (Session::edit and compound steps set the current state's target
before recording), as in Photoshop: undo of a stroke or fill keeps the
painted layer, undo of New Layer or Merge goes back to the layers selected
before it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 11:11:50 +07:00
e30b121969 History: tool and preset names, Frame from Layers as one step, undo/redo restore the targeted layers (#513, #528, #446, #496, #495) (#551)
* Elliptical Marquee selections record "Elliptical Marquee" in History (#513)

Both marquee tools commit through `select.rect`, which named every step
"Rectangular Marquee" whatever its `ellipse` param. The step is now named
after the tool that made it; the rectangular marquee keeps its name.

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

* Preset filters record their own names in History (#528)

Blur, Blur More, Sharpen, Sharpen More, Sharpen Edges and Despeckle run
a generic algorithm with fixed parameters and recorded its name
("Gaussian Blur", "Unsharp Mask", "Surface Blur"). The presets now live
in one table with their names, used for the history step and the job
progress text; Gaussian Blur, Unsharp Mask and Surface Blur keep theirs.

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

* Vertical type command no longer claims it renders horizontally (#446)

Vertical type has rendered in columns since #199, but the params text of
`type.orientation.vertical` still said it was "rendered horizontally for
now". It now documents the same `{"layer":id?}` as Horizontal.

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

* Frame from Layers is one history step (#496)

It grouped the layers through `layer.new.groupFromLayers` (its own
"Group Layers" step) and then recorded "Frame from Layers", so one undo
left the group behind. The grouping step is now folded into the frame
step with `purge_last`, as Transform Again on a copy does; if the group
can't be framed, the grouping is undone with nothing left to redo.

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

* Undo and redo restore the layers each history state targeted (#495)

History states held only the document, so after undo or redo the active
layer stayed whatever it was: redoing New Layer left the old layer
active (a Fill then painted it), and undoing Delete Layer left the
restored layer unselected. Each state now also keeps the active and
selected layers it had when it was created (when the document was
opened, or right after the step that made it), and undo and redo target
them again where they still exist. Selecting a layer is still not a
step and doesn't change any state's target, so selecting another layer
and then undoing or redoing goes back to that state's target, as the
reference app's Step Backward does.

The History panel lists redo labels from a new `redo_labels` accessor
instead of cloning and replaying the history every frame.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:54:10 +07:00
f697e43ef7 Transform Again on a Copy (Cmd+Alt+Shift+T): step and repeat (part of #352) (#474)
The hidden shortcut for step-and-repeat was missing. A new command,
`edit.transform.againCopy` (no menu item, Cmd+Alt+Shift+T), duplicates
the active layer and repeats the last transform on the copy, as one
history step labelled "Transform Again", so repeated presses lay out a
row (or a ring) of copies one transform apart. The transform is worked
out before anything changes (`transform_again_params`, now shared with
Edit › Transform › Again), so nothing to repeat leaves no copy; a copy
that can't be transformed (position locked) is taken back with nothing
left to redo (`History::clear_redo`).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:55:50 +07:00
8877a812f9 GPU compositor: page layer surfaces beyond 16384 px with a memory budget (#187) (#187)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:49:38 -04:00
f857b4a31f Move/Free Transform modifier keys (#90); sharp full-resolution transform preview (#91) (#183)
* Move/Free Transform modifier keys (#90); sharp full-resolution transform preview (#91)

Move tool: Shift locks the drag to the dominant axis (re-evaluated with
hysteresis, so moving far enough the other way switches it); Alt at drag
start duplicates the selected layers once the pointer moves and moves the
copies, folded into one "Duplicate + Move" history step; arrow keys nudge
1/10 px (Alt duplicates first) and nudge the Free Transform box. All in a new
move_mods module with two one-line hooks in canvas::tool_event.

Free Transform: Shift on the body locks to 8 directions, Alt-click moves the
reference point, Cmd on an edge skews (Shift: along the edge, Alt: about the
centre), Cmd+Alt+Shift on a corner = perspective.

Preview (#91): the moving pixels were sampled down to 2048 px (nearest
decimation) and drawn bilinear, so a 24 MP layer previewed at 1/3
resolution. Now uploaded at full resolution (up to the device limit, box
filtered beyond), with a lazily built box-filtered mip chain for zoomed-out
views and nearest filtering at >= 200% (or Nearest Neighbor interpolation).

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

* test: clear texture deltas in transform gpu_ctx helper

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:22:50 -04:00
d3b540234c Large images: adapter texture limits, banded compositing/export, proxy thumbnails, history byte budget (#71)
* Compose: render in place and in bands; area-reduced and proxy thumbnails

render_tiled wrote every tile into a Vec and then copied them into the output:
two full-size float copies (6.3 GB for a 14000² document). Tiles are now
written straight into the output band by band.

New streaming APIs so large documents never need a full-size float
composite: render_bands (sink per band of ~8 MP), render_reduced (banded
area average, bit-identical to the old flatten-then-average thumbnail) and
flatten_to_surface (Flatten Image, Merge Visible, Copy Merged and the other
flattened-surface users).

thumbnail() reduces a downsampled proxy (moved from ui-egui to
compose::proxy, now also scaling spot channels) for documents over 16 MP
when that is faithful (no layer effects or vector masks), so navigator,
channel and histogram thumbnails of a 196 MP document cost milliseconds.

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

* GPU canvas: request the adapter's real limits; banded CPU fallback; navigator thumbnail

egui-wgpu asks for an 8192 px texture limit, so the wgpu compositor rejected
every document wider or taller than 8192 px and each refresh fell back to a
full CPU flatten. The desktop and web apps now request the adapter's own
max_texture_dimension_2d and max_buffer_size (gpu_canvas::use_adapter_limits);
the benches use the same limits.

The CPU fallback (documents beyond the GPU limit, or features it lacks) is
GpuCanvas::refresh: it renders and uploads the composite band by band
instead of a full float composite plus a full RGBA8 copy. The CPU canvas
texture is reduced in bands too, and the Navigator on the GPU canvas uses a
cached thumbnail instead of a full-resolution CPU composite per revision.

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

* IO: banded export and import; no full-size copies in encoders; larger decode limits

- Flat export renders the composite in bands and quantises each band; alpha
  is dropped in place when every pixel is opaque. The single-layer native
  path copies surface rows a band at a time (no f32 read_region and no
  per-pixel Vec). Buffers come from try_reserve: Err, not an abort.
- PSD export encodes the merged image band by band into its planes (no
  flatten, no second plane copy); near-opaque documents are re-encoded
  without the white matte so the output is unchanged. Layer interleaved
  bytes are freed before their planes are compressed.
- Flat import converts in ~32 MB bands (and not at all when already RGBA);
  the PSD merged-image un-matte works per tile row.
- Encoders borrow the image when no conversion is needed (Image::converted)
  instead of cloning it; PNG only copies 16-bit data for the byte swap.
- Decode limits: 2^30 pixels and 8 GiB on 64-bit targets (a 20000² JPEG
  was refused at 2^28).

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

* Filters: write tile results in memory-bounded groups

apply_tiled collected every tile's float result before writing any (3.1 GB
for a 14000² layer). Results are now produced and written in groups of about
256 MB (at least one tile per core).

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

* History: bound pixel memory by Preferences > Performance > Memory Usage

History::max_bytes caps the current document's tiles plus the tiles only
history holds (layers, masks, alpha channels); History::trim drops the
oldest states beyond it after every recorded step and when preferences
change, always keeping the last step undoable. The engine sets it from
memory_usage_mb, which was unused.

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

* Large-image benchmark (14k / 20k): open, refresh, thumbnails, filter, stroke, saves

One operation per run on a cached synthetic photo, with the GPU device created
the way the app creates it, so /usr/bin/time -l reports that operation's peak
memory. Documented in docs/development.md.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:45:17 -04:00
2e80c781ee Never crash: ops (history) denies panicking shortcuts (#59)
ops had no panic sites; the attribute keeps it that way.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:40:43 -04:00
Brandon Thomas 536a7346fb more algorithms, progress 2026-10-01 08:25:06 -04:00
Brandon Thomas ae92ac20bf continued wip 2026-09-30 09:37:18 -04:00
Brandon Thomas 16941f7198 one-shot 2026-09-30 05:02:08 -04:00