- library.selectBy {flag, rating, ratingOp, label, add}; Edit → Select by submenu.
- preset.rename / preset.update (current settings, same groups) / preset.move; preset row
menu: update, rename, move to group (existing or new), delete.
- version.rename / version.update; version button menu: restore, update, rename, set as
before, delete. history.clear (undoable); history row menu: create version from step, clear.
- Keyword chip menu: remove, show photos with keyword, rename / delete keyword.
- A generic one-field dialog (TextPrompt) runs a command with the typed value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The before side used plain defaults, so raws lost their import defaults (sharpening, noise
reduction, import preset) in Before/After. It now shows the photo's import state, or a chosen
history step, version or the current settings (beforeAfter.setBefore); View → Before/After
Settings: Copy After's Settings to Before, Copy Before's Settings to After (undoable), Swap,
Reset; History rows: Copy History Step Settings to Before. Crop and orientation always follow
the current settings so both sides line up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- mask.move {to | delta} reorders the list (no-op moves record nothing); mask.duplicate
{invert} = Duplicate and Invert, the copy placed right after the original.
- Masks list: show/hide eye per row (on hover, always while hidden; new eye-off icon drawn
in code), double-click to rename in place, right-click menu (duplicate, duplicate and
invert, invert, show/hide, move up/down, rename, delete).
- View → Cycle Mask Overlay Color (view.maskOverlayColor without params).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Engine: export split into prepare_export (session thread, cheap) and PreparedExport::run
(Send: read/decode/render/encode); run_batch with per-photo progress, cancel and collected
errors; export_batch keeps the stop-at-first-error behaviour for scripted calls.
- Desktop: the Export dialog, Export with Preset (menu and context menu) and Export with
Previous run their batch on a worker thread (app.export {background: true}); a progress
panel shows count / file / Cancel; ui.inspect reports export {running, last}.
- Headless: settle waits for a running export; `lightcraft-cli snapshot` finishes it before
exiting. Web keeps foreground exports (downloads are main-thread).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Edit → Paste Selected Settings… (⇧⌘V): group checklist prefilled with the copied groups;
develop.paste {groups} pastes only the chosen copied groups.
- Edit → Find… (⌘F, view.focusSearch) focuses the search field.
- Photo context menu: Remove from Album (in an album), Create Version, Paste Selected
Settings…, Show in Finder, Export…, Export with Preset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Built-in export presets (JPEG Small 2048 px, JPEG Large full size, Original + Settings, DNG)
and user presets saved with the library (export.presets / savePreset / deletePreset);
app.export {preset, …overrides} everywhere (desktop, MCP, lightcraft-cli run).
- File → Export with Preset submenu (built-ins, user presets, Custom…); dialog: preset
picker, Save preset, Choose… folder picker, subfolder, if-file-exists (add number /
overwrite / skip), start number, TIFF compression (none / LZW / ZIP); {date} naming token.
- One export_batch for the desktop app and headless MCP (paths, conflicts, sidecars);
ExportOptions::to_json is the exact inverse of from_json.
- ⌘E = Export with Previous (alias); parity: Show in Finder row was stale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Resize modes: long edge, short edge, width, height, W x H (either orientation), megapixels,
percent; "don't enlarge" (default on); ppi written to JFIF density / PNG pHYs / TIFF tags.
- Full-size export of a cropped photo now renders the crop at native resolution (it was
upscaled to the uncropped long edge).
- Formats: Original (file copied byte for byte + XMP sidecar with the edits, named after the
output) and DNG (raw photos re-encoded losslessly, edits in the embedded XMP).
- Export dialog: Resize-to dropdown, values, Don't enlarge, Resolution; DNG/Original hide
pixel options.
- lightcraft-cli render goes through the shared exporter (colour space, metadata, ppi) and
takes any app.export option via --opt KEY=VALUE; MCP export schema lists the new params.
- xtask resolves the workspace root at run time (a moved checkout broke ci).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
library.importPreview scans/probes (probes reused by the import) and marks duplicates;
library.import gains preset, keywords and albumName and is one undo step; File > Add Photos
opens a review grid (thumbnails, checkboxes, duplicates unchecked), add in place or copy into
the dated Originals folders, batched import with progress; engine + headless tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engine: spot.select / spot.update (move target or source, size, feather, opacity, mode) /
spot.refreshSource (next-best source away from the current one) / spot.delete (selected spot);
spot.add resolves the automatic source (so it has a pin and stays put at every size) and selects
the new spot. Remove tool: pins for targets and the selected spot's source, click to select, drag
to move; Size/Feather/Opacity sliders edit new and selected spots; ⌫ deletes the selected spot
(never the photo), / refreshes its source, [ ] size and Shift+[ ] feather (Masking brush too).
Command tests and headless pointer/keyboard tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Op::SetCaptured (validated, journaled), photo.setCaptureTime (the active photo gets the time,
others keep their offsets; or each / shift / hours), Edit Capture Time dialog with preview
(Photo menu, Info panel); engine + headless tests incl. op-log replay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderRequest gains a sample format (OutputDepth: U8 for previews, U16, F32Linear); deep renders
run the per-pixel stage with the exact sRGB curve and return Rendered::deep alongside the 8-bit
image (GPU stays 8-bit: deep exports render on the CPU). Export encodes deep renders: 16-bit PNG and
TIFF, 32-bit float linear TIFF with a linear ICC profile we generate, 10-bit AVIF (YCbCr planes into
ravif). Output sharpening and watermarks work on deep images. ExportOptions.bitDepth with per-format
defaults (TIFF 16-bit, others 8-bit), export dialog bit depth choice (remembered with the last
export), MCP export schema. Tests: a smooth gradient has 1024 levels in a 16-bit TIFF vs 13 in
8-bit, float TIFF decodes to the same light as 16-bit, per-format depth rules, AVIF 10-bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings (Cmd+, / app menu on macOS, Edit elsewhere) with General, Import,
Performance and Interface tabs. App settings (startup view, delete
confirmation, GPU, preview size, filmstrip/grid badges, last library) live in
UiState.settings (ui.json); library settings (raw / non-raw / per-camera
import presets, XMP, thumbnail cache size) go through library.preferences into
prefs.json. Import applies the matching default preset; the photo remembers it
as its import look (unedited; Reset returns to it). Open Library… switches
libraries at runtime.
View modes: F full-screen preview (photo on black, arrows step, I cycles the
info overlay, Esc exits), Cmd+Shift+F window full screen, Cmd+I cycles the
loupe info overlay (off / file + date / exposure), Navigator mini map while
zoomed (click/drag pans).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog keyword ops (one SetMeta batch: undoable, replayed from the op log), hierarchical
a|b|c keywords (tree with counts, parent filters include children), keyword.* commands,
rename/merge dialogs, Keywords-panel suggestions; catalog, engine and headless tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog date runs (stacks never split), Sort.group (auto/day/month/year/none) in the sort
menus, library.groups query, sticky headers, header click selects the group; layout unit
tests and a headless test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RenderRequest gains an output space (default sRGB; previews stay sRGB). The per-pixel stage converts
scene-linear Rec.2020 to the target primaries, gamut-maps into the target gamut and encodes with the
target curve, on CPU and GPU (WGSL finish kernel reads the matrix, luma weights and curve from its
parameter block). sRGB output is byte-identical to before on the CPU. Export embeds our own
generated ICC profile for the space; AVIF stays sRGB. ExportOptions.colorSpace, export dialog and
MCP export schema. Tests: P3 colour survives a P3 export (decoded through the embedded profile),
every space/format round-trips its profile, GPU equivalence per space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- memory::budget (default min(25% RAM, 1.5 GiB); LIGHTCRAFT_MEMORY_MB, app.memoryBudget):
half for engine caches (sources + rendered previews, LRU across them via a global
use tick; the photo on screen stays), a quarter for decodes in flight (WorkGate:
thumbnails, prefetch and import probes wait; loupe/export loads never wait), an
eighth for the GPU free pool.
- GPU: best-fit buffer reuse (<= 25% slack), buffers released outside a render are
pooled at once (no stranded retired lists), pool trimmed after 3 s idle.
- Decodes drop file bytes and raw samples early, colour/crop/orient in place.
- macOS: apps install malloc_zone_pressure_relief as the release hook (after each
decode, import and idle trim): freed large blocks no longer stay resident.
14 raws + loupe + 12 steps (M4 Pro): peak RSS 1.9-2.3 GB -> 0.83-0.99 GB, footprint
2.5-2.6 GB -> 0.91-0.96 GB, heap peak 930 -> 656 MB; stepping median ~13 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix-ups: diagnostic overlays never populate the view-preview cache; Photo Merge's RenderRequest gets
the new overlay field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Photo Merge's Create Stack option now puts the merged DNG on top of a
collapsed stack of its sources (Catalog::stack_with_ops), so the grid
shows the result; test in tests_merge (and no stack without the option).
Photo Merge dialogs live in a Photo > Photo Merge submenu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also: GPU retired-buffer list leaks its buffers at thread exit instead of
dropping them (wgpu's thread-locals may already be gone; the drop aborted
the test process intermittently), and headless UI tests wait up to 120 s
for renders (they timed out on a loaded machine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decoders take a Mode: Header reads everything but the samples (DNG, CR2, ARW,
RAF, RW2; PEF when it has crop tags). NEF/ORF (and PEF without crop tags)
derive geometry/CFA from the samples and still unpack them. decode() stays
bit-exact (debug-hash of every corpus/raw decode identical before/after); the
corpus test asserts probe_info == decode().info() for all 37 files.
Import of 14 corpus raws: 3.4 s -> 1.4-2.0 s; peak RSS through import
1.41-1.49 GB -> 1.12-1.22 GB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pet eyes are found by brightness (the tapetum glows in any colour) and the disc is replaced by
a dark neutral pupil (Darken scales it); redeye.catchlight {index, on, offset} adds a soft
white catchlight at an offset from the pupil centre in pupil radii. Red Eye panel: Red Eye /
Pet Eye mode for new corrections and an Add Catchlight checkbox. CPU/GPU twins updated
together (equivalence covers both modes and the catchlight).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pipeline::redeye: inside the user's ellipse the pupil is found on the source image (a fixed
grid in normalized coordinates, so previews and exports agree): weighted centroid and area of
red-dominant pixels (or, for pet eyes, of the brightest ones). Pupil Size scales the detected
disc; rendering in scene-linear light after white balance neutralizes the red where it is red
(iris and catchlights stay) and darkens it. CPU (lin stage) and GPU kernel redeye_k with an
equivalence test (both the GPU and the CPU scene-linear paths). Indexed controls
redEye.<i>.pupilSize/darken, redeye.add/delete commands, the Red Eye panel (selected eye's
sliders, delete) and the loupe tool (drag an ellipse, click to select).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- engine: `merge.hdr`, `merge.panorama`, `merge.hdrPanorama` (bracket size from
EXIF); MergeJob (Send) with progress/cancel, preview renders through the develop
pipeline (deghost overlay, auto-crop frame); finish writes <name>-HDR/-Pano.dng
next to the first source, imports, selects, applies Auto Settings / auto crop
- ui: Photo Merge dialog (HDR / Panorama / HDR Panorama) with live preview on a
worker thread, background final merge with progress toast; context menu and
Ctrl+H / Ctrl+M (Ctrl now means the physical Control key on macOS)
- cli: `merge hdr|panorama|hdr-panorama`, `synth-merge` for synthetic inputs
- merge::synth: procedural brackets (DNG) and panorama views; docs/merge.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auto uses Upright's consensus level angle as the crop angle; sign verified on a tilted test image
and pinned by a test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolved: album.create (main's one-liner + smart: None), menus (panels in
the Window menu, main's dialog.newFolder ⇧⌘N, app.showInFinder and
app.exportPrevious placed in the menu layout), shortcuts (native-menu skip
+ main's aliases and UI-over-engine rule; Shift+X/Shift+U aliases go
through the culling path; my Shift+P/X/U flag-and-advance dropped in
favour of the aliases). X and Delete stay egui-handled (contextual keys).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
develop.targeted {target: curve|hue|sat|lum, x, y, delta, channel?}: samples the photo at the
point on a proxy render with the target stage and everything after it neutralized, then
nudges the parametric curve region (by the sampled tone and the splits), moves/adds a point
on the master/RGB point curve, or adjusts the colour-mixer (or B&W mix) bands weighted by
their share of the sampled OkLCh hue. UI: target buttons in the curve editor and the colour
mixer; a vertical drag on the loupe sends one step per unit inside one interaction (one
undo step). New original vector icon (Target) in icons.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Up to 8 OkLCh samples (pointColor.pick samples the colour after the mixer on a proxy render);
per sample hue/saturation/luminance shift, variance (compress/expand the spread around the
sample) and an overall + per-axis range with soft edges, applied after the colour mixer in
ColorOps and its WGSL twin (equivalence case). Indexed control specs
(pointColor.<i>.hueShift …) so develop.set/controls/MCP address each sample. Visualize range
is a render overlay (RenderRequest::overlay) drawn after either renderer: the sample's own
edit is left out and colours outside the range turn grey. Point Color flyout in the Color
section with swatches, eyedropper, sliders, visualize and delete.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Delete" parsed as egui's forward-Delete key, so the Mac ⌫ key (Backspace) didn't delete. In the
Masking panel ⌫ now deletes the active mask; while retouching it does nothing. ⇧⌘N no longer
fires album.create (which needs a name).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
curve.refineSaturation (0..100, default 100 = the curve's own saturation): after the tone
curves, the chroma around luma is rescaled so saturation moves from the curve's towards the
pre-curve value, keeping the curve's tone. CPU (finish::refine_saturation) and GPU twin,
equivalence case, slider under the curve editor, crs:CurveRefineSaturation import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compare view (Shift+C): select | candidate side by side with synced zoom and
pan; arrows move the candidate, rating/flag/label keys act on the active
(candidate) photo, swap / make-select. Survey view (N): the selection tiled,
keys act on the active photo, hover x removes from the survey. Auto Advance
setting (Photo menu, bottom bar toggle) and Shift+P/X/U flag-and-advance.
Escape returns to Detail. Headless keyboard test.
Fix: render-job keys include the photo id, so a view slot never shows one
photo's result as current for another with identical settings/size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>