Commit Graph
4 Commits
Author SHA1 Message Date
dtourolleandClaude Opus 5 d7a81375ee List the steps, and let a photographer step straight to one
Build and test / Desktop (Linux) (push) Successful in 19m30s
Build and test / Layer separation (push) Successful in 25s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 1s
Traceability / Requirement traces (push) Successful in 23s
Build and test / Android (aarch64) (push) Failing after 33m5s
Undo answers "take back the last thing", which is the question asked about a
mistake just noticed. It is the wrong instrument for one noticed six
adjustments later: eight presses, each changing the picture, with no way to see
how far back the mistake is without passing through it. A step is a whole
state, so arriving from six away costs what arriving from one does — which is
what makes a row worth making clickable rather than decorative.

`Edit::Discrete` had to go for the list to be worth drawing. Seventeen call
sites recorded the same anonymous step, which is fine for deciding whether two
changes are one gesture and useless for a panel: seventeen rows reading
"Discrete" is not a history. Every variant now carries enough to name itself,
and the compiler enumerated the sites that had to start saying so. A step that
moved a parameter is still named out of the descriptor, so an operation added
as a YAML declaration appears in the history correctly named with nothing
written for it (FR-DEV-3c).

Choosing a film stock was not undoable at all. The pick went straight to
`choose_film`, which nothing on the history's path ever sees. `pick_film`
records it, and is separate because the same call is also how a *restored* edit
gets its tables back — recording that would push a step for the undo the
photographer had just asked for.

The list is rebuilt off a revision rather than off every redraw. A drag ends in
a redraw per frame while folding into one step, so the unconditional version
would tear down and recreate every row sixty times a second to arrive back at
the list already on screen. The counter is process-wide: a per-instance one
starts every photograph at the same number, so a frontend holding "the revision
I last drew" would keep the previous image's steps on screen — invisible while
every image opens with one identical row, and a wrong-photograph bug the moment
persisted history means it does not.

The step names that no descriptor can supply are constants with a roll, and a
test walks the roll rather than a second copy of it. `resolve` splits so that
"is this catalogued?" can be asked: `derive` turns `history.mask_toggled` into
"Mask Toggled", which names a field rather than an act and, being perfectly
readable, is a mistake nobody would look at twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 23:04:35 +02:00
dtourolleandClaude Opus 5 b89f1cfece Snapshot the whole edit in the history, so a drawn mask can be taken back
The undo stack snapshotted a `Preset` — the parameter map — and a mask layer
is deliberately not a parameter. So drawing one changed nothing the history
could see: `record` returned `false`, no step opened, and the layer the
photographer had just painted had no way back. The interface went on calling
`record` in good faith, including from the mask controls, and nothing failed.
A film stock went missing the same way.

The snapshot is an `EditState` now, so the history is complete by
construction rather than by anyone keeping a list in their head.

`undo` and `redo` return a `Step` rather than a `bool`. Stepping is not the
only outcome a caller has to act on — a step across a change of film leaves
the graph without its tables, and only the caller can bake them — and a `bool`
would let that be dropped by writing nothing at all, which is the shape of
mistake this module had already made once. `DevelopSession` settles the debt
either way; a step that found nowhere to go is left alone, since clearing the
film because undo hit the floor would take the stock off the picture.

Five tests, all of which fail against the old snapshot: a drawn layer is
undoable and redoable, a layer's own settings are a step of their own, a
change of stock is a step and names what it needs baked back, clearing the
film is undoable, and an exposure move does not deep-copy the mask stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 22:54:55 +02:00
dtourolleandClaude Opus 5 2958444835 Let undo reach the repairs before the tool can make one
A history state was a Preset — a map of scalars — which was right while
every edit in the graph was a parameter. A spot is not one, and undo is the
first thing anyone does with a repair: place it, dislike it, take it back.
Left as it was, that press would have stepped some unrelated slider and
left the spot on the photograph, which reads as undo being broken rather
than as undo being absent.

So a state is now the pair, params and spot set. The same door is the one
the mask stack will come through: mask edits are outside undo today for
exactly this reason, and FR-DEV-5 is not finished until they are not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 20:27:34 +02:00
dtourolleandClaude Opus 5 7f524d2fd0 Give a mis-drag a way back
Develop edits now save themselves to a sidecar the moment you leave the
image, so until this there was no way to undo one — the mistake was
persisted and the only recourse was to remember the old number.

The history is a stack of snapshots, because the edit graph is already
plain data: `Preset::capture` reduces it to what differs from default and
`Preset::apply` puts it back, so undo is those two calls and nothing else.
A command object per action, with an inverse beside it, would have been a
second thing every operation had to register — and operations are declared
in YAML precisely so that a new one needs no code written for it. A
snapshot cannot fall behind them.

The interesting part is coalescing. A slider drag emits an event per frame
and must be one step, not forty. Nothing in the interface reports a gesture
boundary — the same wall the render coalescing hit, and it is answered the
same way rather than by threading a "finger is down" out of every slider,
curve point and crop handle. What stands in for the boundary is the control
plus recency: changes to the same control within 700 ms amend one step.
Which control is "the same" is asked of the graph, not listed: an operation
whose declared presentation claims a parameter is one where a single
gesture moves several — a curve point carries an x and a y — so those
coalesce as one widget. Nothing in the history names the tone curve.

The compromise, and it is a real one: a control let go of and picked up
again within the window is one step rather than two. Buying the other
answer costs a gesture-boundary signal on every control, which is more
surface than the difference is worth.

The stack is bounded at 64 states for NFR-RES-1 — a develop session stays
open for hours. Sixty-four rather than a byte cap: what is being bounded is
steps a photographer would want back, and a byte cap would give the
elaborate edit the shallowest history, which is exactly backwards.

The session owns its history and every mutator records into it, so the
callbacks in `lib.rs` cannot change the edit and forget to — with a dozen
generic callbacks that would have been one press of undo away from wrong
every time a control was added. Opening a photograph makes its stored edit
the floor rather than a step: it is not work done in this sitting, and an
undo reaching behind it would discard a previous session's edit and then
save that on the way out.

Not yet done, from FR-DEV-5: history is per-session and in memory, and
there are no named snapshots. What mattered was that a saved mis-drag had
no way back at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:03:34 +02:00