Skip to content

Snapshots: in-editor checkpoints of the full edit state - #1335

Open
SandeepSubba wants to merge 5 commits into
CyberTimon:mainfrom
SandeepSubba:upstream-snapshots
Open

Snapshots: in-editor checkpoints of the full edit state#1335
SandeepSubba wants to merge 5 commits into
CyberTimon:mainfrom
SandeepSubba:upstream-snapshots

Conversation

@SandeepSubba

Copy link
Copy Markdown

What

Adds Snapshots — named checkpoints of the complete edit state, stored inside the sidecar — so you can save a look, keep editing, and flip between looks without leaving the editor.

Snapshot switching

How this relates to Virtual Copies

RapidRAW already has Virtual Copies (Duplicate Image → Virtual Copy), which create a parallel library entry with its own sidecar — great for deliverables you want side by side in the grid, rated and exported independently:

Existing Duplicate Image menu with Physical Copy / Virtual Copy

Snapshots cover the other half of that workflow (Lightroom ships both concepts for the same reason): checkpoints while working on one entry. Try a bolder grade, keep the safe state one click away, overwrite a checkpoint as the edit evolves — no extra grid entries, and the looks travel inside the one sidecar. An earlier draft of this PR (#1334) was withdrawn because it didn't make this distinction clear.

UI

A Snapshots section at the top of the Presets panel:

  • + saves the current edit as a snapshot (full state: adjustments, masks, AI edits, crop)
  • click a snapshot to apply it
  • right-click for Apply / Overwrite with Current Edit / Rename / Delete

Snapshot context menu

Design notes

  • No backend schema change. Snapshots live in adjustments.snapshots[] ({id, name, createdAt, state}); the sidecar stores adjustments as raw JSON, so they round-trip untouched.
  • Undoable + auto-saved. Every snapshot operation goes through setAdjustments, participating in history and the normal save path.
  • Isolated. Snapshots are excluded from copy/paste and multi-select sync (not in the copyable-key whitelist), so pasting adjustments to other photos never drags checkpoints along.
  • Reset-safe. reset_adjustments_for_paths preserves the snapshots key — clearing the working edit can't destroy checkpoints.

Testing

  • Full round-trip verified in-app: save two snapshots with different looks, switch repeatedly, persistence via sidecar confirmed; snapshot operations undo cleanly
  • cargo check, cargo fmt --check, and clippy on the touched code clean; tsc introduces no new errors

🤖 Generated with Claude Code

@SandeepSubba
SandeepSubba requested a review from CyberTimon as a code owner July 5, 2026 13:12
@CyberTimon

Copy link
Copy Markdown
Owner

Nice PR! Some questions:

  • Does it transfer all snapshots on every slider / applyAdjustments tick? I don't see any hydration logic like we do for masks or ai patches. I'm asking this because of possible performance slowdowns once a user created many snapshots.
  • Can we style the snapshots exactly like presets so they don't feel patched in?

Thanks a lot!

SandeepSubba pushed a commit to SandeepSubba/RapidRAW that referenced this pull request Jul 6, 2026
Addresses review on PR CyberTimon#1335:

- Perf: executeApplyAdjustments structuredClone'd the full adjustments,
  including adjustments.snapshots, and shipped it to apply_adjustments on
  every interactive tick. Each snapshot is a full edit-state copy (and can
  nest base64 mask/AI-patch data), and the existing mask/patch stripping
  only reached top-level masks, not the copies inside snapshots. Destructure
  snapshots out before cloning; the renderer never reads them and they still
  persist through the separate sidecar save path.
- Styling: snapshot rows now use the same tokens as preset rows (rounded-lg,
  bg-surface, hover:bg-surface-hover, small secondary metadata) so they read
  as part of the panel rather than bolted on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5b0777b)
@SandeepSubba

Copy link
Copy Markdown
Author

Thanks for the review! Good catches — pushed a fix for both (cd006d2).

1. Snapshots on every applyAdjustments tick — you're right, and fixed.

executeApplyAdjustments did structuredClone(currentAdjustments) and shipped the whole thing to apply_adjustments on every interactive tick. Since snapshots live inside adjustments, every snapshot rode along — and each one is a full edit-state copy that can nest base64 mask/AI-patch data. Worse, the existing mask/patch stripping only reached the top-level masks/aiPatches, not the copies inside snapshots, so that data wasn't even nulled. With many snapshots that's a real per-tick clone + IPC cost.

The fix just destructures snapshots out before the clone:

const { snapshots: _snapshots, ...renderAdjustments } = currentAdjustments;
const payload = structuredClone(renderAdjustments);

Unlike masks/AI patches, snapshots don't need the send-once hydration dance — the renderer never reads them at all, so they can be dropped outright. They still persist fine because that goes through the separate sidecar save path (debouncedSave(path, adjustments)), which is untouched. Net result: the apply hot path is now constant w.r.t. snapshot count.

2. Styling to match presets — done.

Snapshot rows now use the same tokens as preset rows (p-2 rounded-lg bg-surface hover:bg-surface-hover, small secondary metadata, matching spacing) so they read as part of the panel instead of bolted on. I kept them lighter than preset cards (no thumbnail) since a snapshot is just an edit-state checkpoint rather than a look you'd preview — but if you'd rather they be full preset-style cards with thumbnails, happy to take it there.

tsc clean (no new errors over baseline).

@CyberTimon

Copy link
Copy Markdown
Owner

Please style them as looks (exactly like presets). Don't duplicate too much unnecessary code. Try to reuse some logic. :) Thanks!

My goal is to have the presets panel like this:

Snapshots (titel)
a list of snapshots styled exactly like presets

Presets (titel)
a list of presets (globally)

What do you think about this? If you prefer snapshots should be text only, we can move it into the dropdown component imo.

SandeepSubba pushed a commit to SandeepSubba/RapidRAW that referenced this pull request Jul 8, 2026
@SandeepSubba

Copy link
Copy Markdown
Author

Done — snapshots are now styled exactly like presets, reusing the same card and preview pipeline rather than duplicating either.

Snapshots rendered as preset-style cards above Presets

What changed

  • Extracted the preset card into a shared PresetItemDisplay. Presets and snapshots now render through the exact same component — a subtitle prop switches it to snapshot mode (date instead of TOOL/STYLE, no preset-only badges), and a nameSlot prop lets the name become an inline input for renaming. No duplicated card markup.
  • Snapshots reuse the preset preview pipeline: each is fed in as a preset-shaped { id, adjustments: state }, so its thumbnail is the current image rendered with that snapshot's edit state. The preview id embeds createdAt, so overwriting a snapshot refreshes its thumbnail and the stale one is revoked automatically — no extra invalidation code.
  • Panel layout is now a Snapshots section (title + cards) above a Presets section (title + cards), matching your mock.

Rename (this came up in testing): snapshots are user-renameable — hover a card for a pencil, or right-click → Rename. The name edits inline in place (Enter saves, Esc cancels), so it stays within the card.

Kept snapshots as cards rather than moving them into the dropdown, since with thumbnails they read as looks now. Happy to adjust the section spacing/titles if you'd like them tighter.

subbajeu and others added 4 commits July 8, 2026 23:47
Adds named snapshots stored inside the sidecar, so you can checkpoint an
edit, keep working, and flip between looks without leaving the editor.
Complements library-level Virtual Copies (Duplicate Image > Virtual Copy):
virtual copies are parallel library entries for deliverables; snapshots
are in-place checkpoints while working on one entry.

- New "Snapshots" section at the top of the Presets panel: save the
  current edit with one click, click a snapshot to apply it, context menu
  for apply / overwrite-with-current / rename / delete
- Schema: adjustments.snapshots[] = {id, name, createdAt, state}, where
  state is a full adjustments snapshot (masks, AI edits, crop included).
  The sidecar stores adjustments as raw JSON, so this round-trips with no
  backend schema change; snapshots are excluded from copy/paste and
  multi-select sync via the existing copyable-key whitelist
- Applying/renaming/deleting go through setAdjustments, so every snapshot
  operation is undoable and auto-saved like any other edit
- Reset Adjustments preserves snapshots, so wiping the working state
  cannot destroy checkpoints

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses review on PR CyberTimon#1335:

- Perf: executeApplyAdjustments structuredClone'd the full adjustments,
  including adjustments.snapshots, and shipped it to apply_adjustments on
  every interactive tick. Each snapshot is a full edit-state copy (and can
  nest base64 mask/AI-patch data), and the existing mask/patch stripping
  only reached top-level masks, not the copies inside snapshots. Destructure
  snapshots out before cloning; the renderer never reads them and they still
  persist through the separate sidecar save path.
- Styling: snapshot rows now use the same tokens as preset rows (rounded-lg,
  bg-surface, hover:bg-surface-hover, small secondary metadata) so they read
  as part of the panel rather than bolted on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 5b0777b)
Addresses review feedback (style snapshots exactly like presets, reuse logic):

- Extract the preset card into a shared PresetItemDisplay component. Presets and
  snapshots now render through the same card; a subtitle prop switches it to
  snapshot mode (date instead of TOOL/STYLE, no preset-only badges) and a nameSlot
  prop lets the name become an inline input for renaming.
- Snapshots reuse the preset preview pipeline: each is fed in as a preset-shaped
  { id, adjustments: state } item, so its thumbnail is the current image rendered
  with that snapshot's edit state. The preview id embeds createdAt, so overwriting
  a snapshot refreshes its thumbnail and the stale one is revoked for free.
- Presets panel is now a 'Snapshots' section (title + cards) above a 'Presets'
  section (title + cards), matching the requested layout.
- Snapshots are renameable: hover a card for a pencil, or right-click > Rename;
  the name edits inline in place (Enter saves, Esc cancels).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-existing formatting drift on main (from the CyberTimon#1339 Android LUT fix) that fails
cargo fmt --check in CI; collapses three multi-line calls that fit on one line.
Not part of the snapshots feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Collapse the four snapshot mutators onto one updateSnapshots(list, adj) helper.
- Drop the redundant pre-filter in generateSnapshotPreviews (enqueuePreviews already
  skips already-generated ids).
- Trim over-explanatory comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants