Commit Graph
23 Commits
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 685cf6f2e8 Never crash: ui-egui and the web app free of panicking calls
- import / merge polling: let-else on the task instead of expect("task").
- the merge worker catches a panic and reports "the merge failed
  unexpectedly" instead of dropping its channel, which left "Merging…"
  up forever.
- web: window() returns Option; its ten callers handle a missing window.
- lightcraft-ui-egui and lightcraft-web now #![deny] unwrap/expect/panic/
  unimplemented/todo/unreachable; clippy.toml as in the steering PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:33:45 -04:00
Brandon ThomasandClaude Opus 5.5 966db71295 LR-EDIT-CURVE: point-curve presets (built-in, save, delete, import/export)
A Point Curve dropdown under the curve graph shows the preset the photo's
point curves match (or Custom) and lists our own built-ins (Linear, Medium
Contrast, Strong Contrast: symmetric S-curves, values chosen here) and the
user's presets; Save Point Curve... names and stores the current four
channels, right-click deletes, Import/Export Presets... read and write
.lccurve JSON files. Presets set only the point curves (parametric curve and
Refine Saturation stay) and are saved with the library preferences.

Engine commands: curve.presets, curve.applyPreset, curve.savePreset,
curve.deletePreset, curve.importPresets, curve.exportPresets; UI commands
file.importCurvePresets / file.exportCurvePresets (file dialogs on desktop).

Fixes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:59:25 -04:00
Brandon ThomasandClaude Opus 5.5 439cbd6ca0 LRC-DEV-SOFTPROOF, MENU-CTX-MASK: soft proofing with gamut warnings; mask component menu
Soft proofing (S in the loupe; S in grids stays Expand/Collapse Stack):
- pipeline Proof {space, destWarning, displayWarning}: gamut-map into the
  proof space, show it in the output space; red/blue gamut warnings
  (neutral clipping excluded). CPU only; the GPU path declines proofs.
- RenderJob::with_proof; paper-white surround and Proof Preview label;
  Soft Proofing strip under the histogram (profile, warnings, Create
  Proof Copy = a named virtual copy). view.softProof for control/MCP.
- photo.virtualCopy takes an optional name.

Mask components: MaskComponent.name; mask.component {component, action:
invert|duplicate|delete|rename|op}; right-click / … menu per component,
double-click rename, Intersect button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 04:01:01 -04:00
Brandon ThomasandClaude Opus 5.5 544914bb66 LR-CLOUD-SMARTPREVIEW + LR-LIB-DUPLICATE: smart previews for offline originals, Duplicate
- Smart previews (smart.rs): a ~1 MB proxy per photo in the library's "Smart Previews" folder
  (the decoded preview-size source, scaled, sRGB-encoded JPEG behind a one-line header). When
  a photo's original is missing (an unplugged drive), its source loads from the proxy: the
  photo renders, edits and exports at proxy size. library.smartPreviews {discard?},
  photo.smartPreview status; File ▸ Previews ▸ Build / Discard Smart Previews; Info shows
  "Original offline · editing the smart preview".
- Photo ▸ Duplicate (photo.duplicate): a real "-copy" file with the same settings, metadata,
  rating, flag, label and albums; one undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:25:35 -04:00
Brandon ThomasandClaude Opus 5.5 9affd0e8d2 LR-EXP-PSD + KEY-PHOTOSHOP: Edit in External Editor (⇧⌘E)
photo.editExternal renders the active photo with its edits as a 16-bit TIFF "<name>-Edit.tif"
(Adobe RGB by default; ProPhoto / P3 / sRGB) next to the original, never overwriting, adds it
stacked on top of the original and selects it. Photo ▸ Edit in External Editor (⇧⌘E, as
observed; the export dialog stays on ⇧E) then opens it in the application set in Settings ▸
General ▸ External Editor, or the system default (desktop: open -a / start / xdg-open,
spawned, never waited for).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:58:19 -04:00
Brandon ThomasandClaude Opus 5.5 8a83f8c6ce LR-BEHAV-BG-TASKS: background export with progress and cancel
- 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>
2026-10-01 22:27:06 -04:00
Brandon ThomasandClaude Opus 5.5 58b6804f36 M16.2: community links — Discord, website, app page and GitHub
Help menu (Join the ArtCraft Discord…, LightCraft Website, LightCraft on GitHub, ArtCraft Website),
a one-click Discord button in the top bar (original chat-bubble icon), and links + a prominent
Discord button in About. Opened through a new open_url service (desktop: system browser; web: new
tab; https only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:02:54 -04:00
Brandon Thomas db89203fb2 Merge branch 'worktree-agent-a27a1456c70c1074d' 2026-10-01 13:27:58 -04:00
Brandon ThomasandClaude Opus 5.5 4075b42d85 Merge main into the masking branch (output space / deep finish, presets, parity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:23:58 -04:00
Brandon Thomas 799ecc08bc Merge branch 'worktree-agent-ad641003294616c82'
# Conflicts:
#	crates/engine/src/library.rs
#	crates/ui-egui/src/lib.rs
#	docs/parity.md
2026-10-01 13:21:16 -04:00
Brandon ThomasandClaude Opus 5.5 03291d6f83 LR-EXP-BITDEPTH: 16-bit PNG/TIFF, 32-bit float linear TIFF, 10-bit AVIF
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>
2026-10-01 13:07:02 -04:00
Brandon ThomasandClaude Opus 5.5 464d4926b9 LR-PREF-GENERAL + LR-IMP-RAWDEFAULT + LR-VIEW-FULLSCREEN/NAVIGATOR/INFOOVERLAY: Settings dialog, import defaults, view modes
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>
2026-10-01 13:03:33 -04:00
Brandon ThomasandClaude Opus 5.5 ca8db6e028 LR-MASK-OVERLAY + LR-MASK-PINS: rendered mask overlay (5 views, colour/opacity, O / Shift+O) and draggable pins per component
The selected mask's evaluated alpha is drawn by the renderer as Overlay::Mask (CPU and GPU share the
alpha the render used; hidden masks are evaluated on demand), kept out of the view-preview cache.
Pins for radial/linear/brush components select their mask on click and move the component on drag;
view.maskPins hides them. CPU/GPU equivalence and headless keyboard/pointer tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:58:27 -04:00
Brandon ThomasandClaude Opus 5.5 7f40a7a7d9 LR-EXP-COLORSPACE: export in sRGB, Display P3, Adobe RGB (1998) compatible, ProPhoto RGB or Rec. 2020
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>
2026-10-01 12:49:13 -04:00
Brandon ThomasandClaude Opus 5.5 64dfa8adbd Merge edit-panel parity (Point Color, Calibration, curve refine saturation + targeted adjustment, red/pet eye, visualize spots)
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>
2026-10-01 11:02:11 -04:00
Brandon ThomasandClaude Opus 5.5 490fe943cd LR-EDIT-COLOR-POINTCOLOR: Point Color — sampled colours with shift, variance, range and visualize
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>
2026-10-01 10:18:28 -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 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 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 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 ThomasandClaude Opus 5.5 baa30ca590 M0.12: web persistence — library, edits and imported originals survive a reload
The catalog Store on the web is an in-memory mirror (apps/lightcraft-web/src/files.rs) loaded
from browser storage at start-up and flushed in the background in modification order (so a
snapshot always lands before the log reset that follows it). Storage is OPFS when writable from
the main thread (createWritable), else IndexedDB (?store=idb forces it); ?reset clears it.
Imported originals are stored as originals/<content hash> and referenced as
web/<hash>/<name>; the main thread keeps a bounded memory cache and loads misses (and the
active photo) asynchronously. view.json and ui.json are saved every second when changed.

Engine: Session::open_library_in(LibraryStores) opens a library from any Store (presets, view
and prefs go through it too); Session::save_view; Copy imports into a non-disk library add.
JS automation API: lightcraft.command(id, paramsJson) (+ web.stats).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 08:18:01 -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 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