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>
Filter bar above the grid (View > Filter Bar, ⇧F, or the top-bar filter
icon): rating with ≥ / = / ≤, picked / rejected / unflagged, colour labels,
kind, edited/unedited, camera / lens / keyword pickers, Clear and "Save as
Smart Album…" — every control drives library.filter. Two rows when the
grid is narrow. Empty grid under a filter says so. Headless test clicks
the bar's widgets and saves a smart album.
docs/parity.md: rows for smart albums, stacks, virtual copies, compare,
survey, filter bar, menu bar updated; finished top gaps removed.
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>
menubar.rs builds File/Edit/View/Photo/Window/Help from engine + UI
commands (layout table for order and separators, parameterized submenus
for rating, flag, label, sort, add to album), with live labels ("Undo
Exposure"), enabled and checked state; ui.menu.tree exposes it. The
desktop app installs it natively on macOS with muda (pure Rust over
AppKit; no GTK — muda is a macOS-only dependency): app menu (About,
Services, Hide, Quit), Window menu (Minimize/Zoom/Bring All to Front),
Help menu; per-frame state sync; accelerators without Cmd and the text
editing ones step aside while a text field has focus; natively handled
shortcuts are skipped by the egui handler. Other hosts (web, Windows,
Linux) get the same menus in the top bar. Panels/sections/tools move to
the Window menu. Verified with System Events: menus listed with live
enabled state, View > Survey dispatches.
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>
White-preserving primaries matrix (OkLCh hue rotation / chroma scale of each Rec.2020
primary) plus a luminance-preserving green/magenta shadows tint, applied in scene-linear
light before the tone map. CPU (colorops::calibrate) and GPU (finish.wgsl) with an
equivalence case; seven control specs (calibration.*), a settings group, crs: import
(ShadowTint, Red/Green/BlueHue/Saturation) and a Calibration section at the bottom of the
Edit panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One row per catalog feature, menu item, desktop shortcut and Classic extra with
tier, status, evidence (command/control ids, files) and what is missing; per-section
summary, ordered Top gaps, and a shortcut conflict review. Linked from README
(Feature status) and AGENTS.md (pick work from Top gaps).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Once the current photo's loupe render is done, the next and previous photos in filmstrip
order get a background loupe job (Slot::Prefetch, priority below on-screen thumbnails,
once per settings): the decoded 2560 px source is kept in the engine's bounded preview
LRU and the view render is cached, so stepping to a neighbour shows its developed image
in ~50 ms and then re-renders only on the GPU.
- lightcraft_gpu::warm_up (called when the app is created) builds the device and kernels on
a background thread; gpu::ready() answers without blocking and ui.inspect uses it, so the
UI thread never waits for device creation.
- docs/gpu-pipeline.md: opening-a-photo section.
Stepping through 24 MP raws in the loupe (real app): full render 220–580 ms → 45–62 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renders run in dedicated module Web Workers, each a second instance of the app's wasm module
(index.html compiles it once and posts the WebAssembly.Module to worker.js). A job crosses as
JSON (WireJob: source, settings, request); the worker reads originals and cached thumbnails from
browser storage itself, keeps decoded sources and per-view stage caches, and transfers RGBA back.
Jobs follow the worker that decoded the photo; the loupe waits for it. No wasm threads or
SharedArrayBuffer needed; falls back to inline rendering if no worker starts (?workers=0).
Thumbnail disk cache on the web: thumbs/<key>.jpg in OPFS/IndexedDB with an index on the main
thread, pruned to the desktop budget (2 GB → 80 %, LRU).
ui-egui: RenderOffload hook in the Renderer (queue/priorities/dedup stay in the UI).
engine: RenderJob.origin + MediaCache::origin_ref; preview: encode_jpeg/decode_jpeg public.
xtask web: copies worker.js; --serve sends COOP/COEP/CORP (documented in docs/web.md).
Headless Chrome, cold load + grid scroll, 24 demo photos: main-thread busy 12.4 s → 0.7–1.1 s,
long tasks 23 (max 1.1 s) → 0; grid filled in 6.3–8.4 s vs 13–16 s; slider job 4 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`media::develop` renders on the GPU when available (views, render_now, exports;
thumbnails stay on the CPU) and falls back to the CPU otherwise. `app.gpu
{enabled?}` toggles it at runtime, `LIGHTCRAFT_GPU=0` disables it for the process,
`ui.inspect` reports the adapter under perf.gpu.
Performance: dropped device buffers are recycled once their thread has submitted
(fresh 100-300 MB allocations per pass cost as much as the passes), Gaussian
passes ping-pong between two buffers and read all channels of a pixel at once,
and long box radii slide over longer chunks. LIGHTCRAFT_PROFILE prints real GPU
stage timings. render_bench prints CPU and GPU columns and the CPU/GPU difference.
24 MP ARW, loaded machine, min of 7: cold loupe 415 -> 32 ms, exposure drag
22-42 -> 3.9 ms, NR drag (1920) 301+ -> 18 ms, export render 1037+ -> 291 ms;
max 1 LSB difference. docs/gpu-pipeline.md describes the implementation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integrated with M4 optics/Upright: render_impl resolves Upright and uses frame_for (lens + auto CA)
on the cached source; defringe runs in the cached linear stage and optics settings are part of its key.
Stage-cache equivalence test extended with optics/geometry steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ui-egui/softpaint: pure-Rust rasterizer for tessellated egui output (gamma-space premultiplied
blending like egui-wgpu, bilinear texel-centre sampling, scissor clips, tie-break rule so shared
edges blend once, parallel row bands, deterministic) + CPU texture mirror from TexturesDelta.
- ui-egui/headless: HeadlessView (offscreen context with our fonts/theme) and Headless (windowless
app session: logic+ui per frame, answers viewport Screenshot/InnerSize/Close, settle loop,
control requests through the shared handler).
- ui.screenshot {headless: true} in the running app renders from the logic tick (works while the
window is occluded / display asleep); windowed captures fall back to headless after 2 s.
Screenshots now wait for in-flight renders, not just queued ones.
- lightcraft-cli snapshot [--demo|--library DIR] [--script f.jsonl] -o out.png --size WxH --scale S.
- Fixes found with it: dialog backdrop was painted over the dialog itself (now an area below the
window); dialog content padding; crop overlay options use our buttons (registered widget ids).
- Docs: control-protocol.md (headless section), AGENTS.md (prefer headless when unattended).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps/lightcraft-web runs the egui UI in the browser (eframe web, glow/WebGL2):
procedural demo library on load; in-memory imports via file picker
(Cmd+Shift+I) and drag-and-drop (MemStore + bytes-based engine hooks);
export goes through app.export and downloads in the browser; `?bench` logs
first-frame and slider latency. `cargo xtask web [--serve [port]] [--dev]`
builds with the matching wasm-bindgen CLI into <target>/web/; the web app is
part of the CI wasm check. Wasm fixes: lazy profiling Instant in pipeline,
web-time clocks in ui-egui (render timings were 0 on wasm). Docs: docs/web.md.
No persistence on web yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- catalog: Store trait (FsStore: temp+fsync+rename+dir fsync; MemStore),
Journal: CRC-checked JSON-lines op log with sequence numbers + atomic
snapshots; torn final record dropped and truncated, stale records after a
snapshot skipped, mid-log damage preserved as catalog.log.damaged-N.
- PushHistory op so each edit logs one history step (not the full history).
- engine: Session::open_library/persist/close_library; every top-level
command is fsynced before returning; presets.json + view.json;
library.info / library.compact commands.
- app: default library ~/Pictures/LightCraft Library (LIGHTCRAFT_LIBRARY or
--library DIR override), --memory for throwaway sessions, snapshot on quit.
- tests: proptest replay(log)==state with random snapshots/reopens, every
truncation of the last record, stale log, mid-log damage, FsStore, engine
restart without close.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/mcp (lightcraft-mcp, L5): hand-written MCP over stdio (JSON-RPC 2.0,
protocol 2025-06-18 with 2025-03-26/2024-11-05 negotiation): initialize,
ping, tools/list, tools/call, resources/list/read. Two backends behind one
trait: Headless (in-process engine Session; answers engine.execute,
engine.commands, ui.render, app.export) and Remote (the desktop app's
loopback JSON-lines control channel; lazy connect + reconnect).
Tools: helpers (list_commands, run_command, import, query_photos,
select_photos, list_controls, get/set_develop, apply_preset, crop,
render_photo -> image content, export), UI tools when connected
(screenshot -> image content, inspect_ui, set_ui, list_widgets, click,
press_key, type_text, pointer_gesture), plus one cmd_<id> tool generated
per command in the registry (--compact omits them).
apps/lightcraft-cli: `mcp [--connect [ADDR]] [--demo] [--compact] [FILES]`,
`render <in> -o <out> [--set control=value]... [--settings] [--preset]
[--size] [--quality]`, `commands`, `controls`.
Acceptance e2e: crates/mcp/tests/e2e.rs sets exposure over MCP and renders
(headless and through the TCP transport); apps/lightcraft-cli/tests/cli.rs
does the same through the real binary's stdio. Docs: docs/mcp.md,
docs/control-protocol.md. Also rustfmt of one line in pipeline/finish.rs
(main was not fmt-clean).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>