Commit Graph
36 Commits
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 6015af99b7 M2.11: before/after top/bottom (⌥Y) and split top/bottom (⇧⌥Y)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:16:52 -04:00
Brandon ThomasandClaude Opus 5.5 b718d9248a M10.9: Export with Previous (⌥⇧⌘E), export dialog remembers the last options (prefs.json); Show in Finder (⌘R); New Folder… (⇧⌘N)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:10:46 -04:00
Brandon ThomasandClaude Opus 5.5 2c37c26d1e M16.1: ⌫ deletes photos again (was bound to forward-delete), and never in the Masking/Remove/Red Eye panels
"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>
2026-10-01 10:10:46 -04:00
Brandon Thomas e32c4549a7 Merge branch 'worktree-agent-ace55e56f06440d66'
# Conflicts:
#	xtask/src/main.rs
2026-10-01 10:00:06 -04:00
Brandon ThomasandClaude Opus 5.5 382cca190c M16.1: shortcuts — Lightroom-desktop secondary bindings, no double-firing keys
Adds secondary bindings for existing commands (Cmd+D select none, Shift+E export
dialog, Space toggle zoom, Shift+M create version, Shift+X / Shift+U reject /
unflag and advance), listed in Help > Keyboard Shortcuts. Shift+Y no longer also
toggles the History panel; a UI command now shadows the engine command it wraps
on the same key (W, Cmd+Shift+I). New tests reject conflicting bindings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:50:20 -04:00
Brandon Thomas 9886d49823 Merge branch 'worktree-agent-a69dcec0f0c61bc39'
# Conflicts:
#	crates/engine/src/media.rs
#	crates/ui-egui/src/render.rs
2026-10-01 09:39:30 -04:00
Brandon ThomasandClaude Opus 5.5 570d03e202 M5.4c: prefetch filmstrip neighbours; GPU device created at startup off the UI thread
- 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>
2026-10-01 09:23:36 -04:00
Brandon ThomasandClaude Opus 5.5 5fd5384bfb M5.4a: instant first view — cached view render / embedded raw preview, then the full render
- engine: QuickJob — what to show the moment a photo opens: the loupe's last render for the
  current settings (kept by Session::loupe_job under a size-independent "view" key in the
  preview cache, memory + disk with a library; the disk JPEG is written off the render
  thread via PreviewCache::put_deferred), else the embedded camera JPEG of a raw that still
  has its import look (import::has_import_look), else a thumbnail-level render.
- Grid / filmstrip: an unedited raw without a thumbnail shows its cached thumbnail, else its
  embedded preview (Slot::ThumbQuick) at once; the real rendered thumbnail follows at
  background priority (same cache keys, so it still ends up matching the develop render;
  edited photos render directly).
- Loupe (detail, compare, filmstrip selection): Slot::Preview holds the stand-in until
  Slot::Main has the photo; the full render then replaces it in place (no blank frame).
- files::embedded_preview_srgb + PreviewLoader hook (desktop/CLI/MCP and web).
- ui.inspect → loupe {photo, source: render|cached|embedded|small|thumb|none, pending}.

Real app (24 MP raws, loaded machine): first image 15–90 ms (thumbnail, then embedded
preview), full render 0.2–0.5 s (was 1.0–2.4 s with nothing shown before it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:18:50 -04:00
Brandon Thomas 8e5d6cb521 Merge main (GPU pipeline) into the web branch 2026-10-01 09:02:23 -04:00
Brandon ThomasandClaude Opus 5.5 8f2058a124 M0.13: web render workers — pipeline renders off the main thread
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>
2026-10-01 08:36:15 -04:00
Brandon Thomas 3efd0576d9 Merge branch 'worktree-agent-a77b3b7bf62802f99' 2026-10-01 08:23:23 -04:00
Brandon ThomasandClaude Opus 5.5 c59445ca15 M4.4: segmented controls for Upright modes and crop overlays (no awkward wrapping)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 08:23:16 -04:00
Brandon ThomasandClaude Opus 5.5 30aac43fab M5.2e: renders and exports use the GPU; render_bench GPU column
`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>
2026-10-01 08:19:44 -04:00
Brandon ThomasandClaude Opus 5.5 179aa90f01 Merge M5 CPU performance (stage cache, full-res source level, parallel planes)
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>
2026-10-01 02:05:14 -04:00
Brandon ThomasandClaude Opus 5.5 8c49450872 M10.2: export dialog uses themed sliders, choice buttons and a label column
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:55:36 -04:00
Brandon ThomasandClaude Opus 5.5 37ab239b7b M0.10: headless UI rendering — CPU rasterizer for egui, ui.screenshot headless + fallback, lightcraft-cli snapshot
- 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>
2026-10-01 01:48:48 -04:00
Brandon ThomasandClaude Opus 5.5 b00dda4cec M4.4: Upright — Auto/Level/Vertical/Full from detected lines + Guided upright (up to 4 guides)
- pipeline::upright: own gradient-based line-segment detector (level-line region growing seeded from the
  strongest gradients, weighted principal-axis fit, elongation test); vanishing points by RANSAC over
  segment pairs + least-squares refinement (smallest eigenvector of Σ w·l·lᵀ)
- corrections as a virtual camera rotation K·R·K⁻¹ (vertical VP → y axis; Full also horizontal VP → x axis
  with focal length from VP orthogonality when plausible); Level = rotation; Auto = Full/Vertical when
  moderate, else Level; all re-centred
- Guided: guides stored in normalized lens-corrected coordinates; 2+ vertical/horizontal guides define the
  vanishing points, a single guide levels
- the analysis runs on the lens-corrected image at 1024 px and the result is stored in
  geometry.upright_transform (identical preview/export); settings naming a mode without a transform
  (XMP, CLI) are resolved at render time
- commands geometry.upright {mode} (Update = run again) and geometry.guides {guides, add}
- UI: Upright mode buttons, Update, Draw/Clear Guides in the crop panel; on-canvas guide drawing that
  follows the warp
- tests: segment detector on known lines; Vertical/Auto/Full fix converging verticals; Level fixes a tilted
  horizon; Full fixes both families; guided from two guides; analysis resolution independence; lazy vs
  stored transform; engine commands

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:16:18 -04:00
Brandon ThomasandClaude Opus 5.5 8cbdece6f2 Merge main into geometry/optics branch (embedded lens + XMP sidecar import, ProbeInfo fields)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:08:21 -04:00
Brandon ThomasandClaude Opus 5.5 733e59f84e M4.3: geometry — manual transforms as one homography in the geometry frame, Constrain Crop
- pipeline::transform::manual: Vertical/Horizontal keystone as a virtual camera rotation K·R·K⁻¹ (re-centred,
  local scale preserved), Rotate, Aspect, Scale, Offset X/Y composed into one homography
- the homography lives in the Warp: output → crop affine → inverse perspective → lens model → source, still a
  single resample; masks/crop/spots stay in transformed normalized coordinates
- Constrain Crop: the effective crop shrinks about its centre until no blank (outside-source) area remains;
  aspect unchanged
- UI: Constrain Crop checkbox in the crop panel's Geometry section
- tests: slider signs, known keystones straightened (3 slider combos), constrain removes blank corners,
  preview vs export match

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:07:19 -04:00
Brandon ThomasandClaude Opus 5.5 3ef4eb0687 M9.2: preset files — .lcpreset import/export (groups preserved), XMP presets via crs: mapping
- engine presets.rs: .lcpreset JSON format {format: "lightcraft.preset", version, presets[]},
  parse_preset_file (.lcpreset file / bare preset / array, or XMP), folder expansion,
  Session::add_presets (identical presets skipped, id clashes get fresh user.* ids, built-ins
  never replaced).
- crs::preset_from_xmp: crs: fields → partial settings, crs:Name/crs:Group (fallback: file name /
  "Imported Presets").
- Commands preset.import {paths} and preset.export {path, ids?, group?}; UI commands
  file.importPresets / file.exportPresets (File menu, with native open/save dialogs on desktop),
  Presets panel "⋯" menu (Import Presets…, Export User Presets…) and group context menu
  (Export Group…).
- Tests: export → import round trip keeps groups/settings, duplicates skipped, clashing ids,
  malformed files reported, hand-written XMP preset imports and applies.
- docs/xmp-interop.md: preset file section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:05:26 -04:00
Brandon ThomasandClaude Opus 5.5 a0da99660d M4.2: optics — manual distortion/vignetting, lateral CA (auto + manual), defringe, DNG-embedded profile corrections
- pipeline::optics::Warp: one inverse map (lens-corrected → source) inside the single geometry resample:
  manual radial distortion, embedded DNG WarpRectilinear (per plane), per-channel lateral CA scale,
  radial vignetting gain (manual + embedded FixVignetteRadial); all in normalized radii
- automatic lateral CA estimate: radial edge profiles matched sub-pixel per plane, robust LSQ fit of
  displacement = α·r (cached per source)
- defringe: purple/green hue ranges (OkLab hue) desaturated next to high-contrast edges, width relative
  to the image long edge
- "Enable Lens Corrections" applies only lens data embedded in DNGs (opcodes are no longer baked at decode;
  stored on the photo record, on by default at import when present). No LCP data anywhere.
- new controls: optics.caRed/caBlue, defringe hue ranges; Optics panel: Manual sub-section, Defringe flyout
- tests: barrel grid straightened, CA removal (auto + manual), embedded corrections toggle/scale, preview vs
  export resolution independence, CA estimator accuracy, orientation of embedded lens data

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:57:47 -04:00
Brandon ThomasandClaude Opus 5.5 42d2298118 M5.3: stage cache — slider drags redo only the stages the setting feeds; draft quality during drags
- pipeline: StageCache + render_cached: resampled source, white-balanced/retouched/denoised image
  and each spatial plane (log L, guided base, clarity, texture, dark channel + airlight) are keyed
  by exactly their inputs; keeps a draft-size and a full-size entry warm.
- Exposure moved after the spatial filters (applied per pixel; planes shifted by EV): identical up
  to rounding (test: <= 2 LSB vs. exposing the source), so exposure drags hit the cache too.
- engine: RenderJob::draft / with_stages; ui: loupe and before views own a StageCache, loupe
  renders at Draft quality while interacting.
- engine example render_bench (min-of-N wall + process CPU time).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:42:50 -04:00
Brandon Thomas fa77faeb6b Merge branch 'worktree-agent-a51e3311b37df5c4e' 2026-10-01 00:13:33 -04:00
Brandon ThomasandClaude Opus 5.5 424e23e6e8 M10.6: text watermark on export (Inter, size relative to image, 9 anchors, opacity, shadow)
ab_glyph (Apache-2.0, pure Rust) rasterizes the already-attributed Inter font.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:13:33 -04:00
Brandon ThomasandClaude Opus 5.5 1da0263d56 Merge main into persistence branch (keep JobPool renderer; wasm inline job timing via now_ms)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:09:07 -04:00
Brandon Thomas 77ff166841 Merge branch 'worktree-agent-ac4c81fc23fc995ec' 2026-10-01 00:03:41 -04:00
Brandon ThomasandClaude Opus 5.5 c6954d5bd9 M10.5: export metadata policy (all / all except camera / copyright / none, remove location) with EXIF + XMP embedding
New lightcraft_meta::write_exif (IFD0 + Exif + GPS IFDs; round-trip tested).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:03:38 -04:00
Brandon ThomasandClaude Opus 5.5 2d2bc2ad8f M0.11: web build (WASM)
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>
2026-10-01 00:01:18 -04:00
Brandon ThomasandClaude Opus 5.5 f0ffa11333 M0.1: enforce the asset rule in CI (cargo xtask assets); replace Adobe-published Source Sans 3 with Inter
Every image/icon/font/media/profile file must match an assets/ATTRIBUTION.md path pattern;
Adobe profile/template formats and Adobe-named assets are rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:52:33 -04:00
Brandon ThomasandClaude Opus 5.5 bcecab7d84 M1.5: thumbnail/preview cache (disk + memory LRU, prioritized job pool)
- new crate lightcraft-preview (L3): 128-bit content hashing (SipHash-1-3),
  cost-bounded Lru, DiskCache (JPEG q90 files keyed by content hash +
  settings hash + size + RENDER_CACHE_VERSION; temp+rename writes; LRU prune
  by mtime to 80% of budget; corrupt files dropped), PreviewCache (memory
  LRU over disk), JobPool (per-slot dedupe, priorities, reprioritize/drop,
  inline mode for wasm).
- engine: Session::thumb_job buckets thumbnails to 128/256/384/512 and serves
  them from the cache without decoding the source; decoded thumbnail
  sources now in a 384 MB LRU (was unbounded); library attaches
  <library>/thumbs; library.info cache stats; library.clearPreviews.
- ui-egui: Renderer runs on JobPool; thumbnails scrolled out of the
  prefetch area are dropped from the queue; on-screen thumbnails get
  re-prioritized; ui.inspect reports in-flight renders + thumb textures.
- Demo library (24 photos): grid fully drawn 4.4 s cold -> 0.21 s warm
  (24/24 disk hits, no source decoded).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:51:47 -04:00
Brandon ThomasandClaude Opus 5.5 e7b3724b5f M1.4: crash-safe catalog persistence (op log + snapshot)
- 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>
2026-09-30 23:40:40 -04:00
Brandon ThomasandClaude Opus 5.5 908713c39b M10.2: MCP/CLI export shares the engine encoder (all formats, batch, size limit, sharpening)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:39:08 -04:00
Brandon ThomasandClaude Opus 5.5 72ba795439 M10.2: export dialog + batch export (JPEG/PNG/TIFF/WebP/AVIF, size, file-size limit, output sharpening, naming, folder)
Export encoding lives in lightcraft-engine::export (pure bytes, shared by app/CLI/MCP/web).
Embeds our own sRGB ICC; writer creates the output folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:37:24 -04:00
Brandon ThomasandClaude Opus 5.5 440cedfa9c README: product-style showcase with screenshots of public-domain photos developed in LightCraft; reproducible showcase scripts; display-referred tone map for JPEGs (exact pass-through); fixes (crop panel wrap, overlay clipping, greyscale histogram, painted chevrons)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:56:38 -04:00
Brandon ThomasandClaude Opus 5.5 a7ea39573b M0.7/M0.8: Lightroom-style egui UI (top bar, My Photos, photo/square grid, loupe + filmstrip, Edit panel with histogram/curves/mixer/grading, crop/masking/remove/presets/info/keywords/versions/history, dialogs, shortcuts) + control channel + desktop app; engine file loading via codecs/raw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:34:03 -04:00
Brandon Thomas b0d435f223 continued work 2026-09-30 22:21:57 -04:00