diff --git a/ROADMAP.md b/ROADMAP.md index 144e224b0..36f1ec980 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -105,7 +105,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/ 1. Camera colour of our own: measured calibration for the popular models (50–90 h) 2. CR3 for every Canon body (25–45 h) 3. Render-fidelity suite against Lightroom, then tuning (30–60 h) -4. Full-resolution zoom (8–15 h) +4. ~~Full-resolution zoom~~ (done 2026-10-12) 5. AI masks: Subject / Sky / People (40–70 h + model decision) 6. Compressed ORF and HE NEF (28–55 h) 7. Lens profile database of our own (40–80 h) @@ -192,6 +192,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/ | 2026-10-12 | minor | Mamiya MEF decoded and imported: ZD verified (241); headline numbers unchanged | | 2026-10-12 | minor | Epson ERF decoded: 3 more verified models (240); headline numbers unchanged | | 2026-10-12 | minor | Georgian (`ka`) interface language added (every catalog message) | +| 2026-10-14 | minor | Full-resolution zoom (gap 4) closed: soft proofing gets zoom windows; headline numbers unchanged | | 2026-10-12 | minor | UI/UX remaining 40–70 h → 34–58 h (accessibility audit landed); headline numbers unchanged | | 2026-10-14 | minor | Camera-coverage sweep (#1327): 30 models verified on 40 CC0 files (coverage.py 515 → 545 verified); sensor verification does not establish colour parity (#1277); headline numbers unchanged | | 2026-10-12 | minor | 22 more CR3 bodies verified, EOS R6 Mark III white balance: RAW decoding 77% → 83%, RAW ~56% → ~60%; mainstream 49% → 51%, essentials 64% → 65%; full unchanged at ~61% | diff --git a/crates/engine/src/media.rs b/crates/engine/src/media.rs index 6d1bfe0de..22d76528d 100644 --- a/crates/engine/src/media.rs +++ b/crates/engine/src/media.rs @@ -2189,6 +2189,40 @@ mod tests { assert!(mean(&a) > mean(&b) + 10.0, "two stops brighter after: {} vs {}", mean(&a), mean(&b)); } + // LR-VIEW-ZOOM: what the loupe shows at 1:1 is what an export at full size writes. A window of + // the zoomed frame (the photo's own pixels) is pixel-for-pixel the export, at the centre and + // at a corner, with tone, colour and detail edits on a camera-sized photo + #[test] + fn one_to_one_window_pixels_are_export_pixels() { + use lightcraft_pipeline::{OutputDepth, OutputSpace, PixelWindow}; + let mut s = crate::Session::with_demo(); + let id = s.active().unwrap(); + let p = s.catalog.photo(id).unwrap().clone(); + let (w, h) = (p.width as usize, p.height as usize); + assert!(w.max(h) > 2560, "larger than the preview cap: {w}×{h}"); + let mut d = (*s.develop_of(id).unwrap()).clone(); + d.light.exposure = 0.7; + d.light.contrast = 25.0; + d.color.saturation = 20.0; + s.set_develop(id, d, "Edits").unwrap(); + let export = s.render_export(id, w, h, OutputSpace::Srgb, OutputDepth::U8).unwrap().image; + assert_eq!((export.width, export.height), (w, h), "a full-size export"); + for win in [PixelWindow { x: w / 2 - 256, y: h / 2 - 256, w: 512, h: 384 }, PixelWindow { x: 0, y: h - 300, w: 400, h: 300 }] { + let tile = s.region_job(id, w, h, win, true).unwrap().run().rendered.unwrap().image; + assert_eq!((tile.width, tile.height), (win.w, win.h)); + let mut worst = 0u8; + for y in 0..win.h { + for x in 0..win.w { + let (a, b) = (tile.data[y * win.w + x], export.data[(win.y + y) * w + win.x + x]); + for k in 0..3 { + worst = worst.max(a[k].abs_diff(b[k])); + } + } + } + assert!(worst <= 1, "window {win:?}: the 1:1 view differs from the export by up to {worst} levels"); + } + } + /// The thumbnail of a photo whose preview-level source is decoded is cut from it: the file (here /// one nothing can decode) is not read again. #[test] diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 68b5346e3..44427565e 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -402,12 +402,16 @@ fn request_window(app: &mut LightcraftApp, c: &WindowCtx, v: &WindowView) -> Opt let job = if c.interacting { job.draft() } else { job }; let overlay = if v.before { lightcraft_pipeline::Overlay::None } else { c.overlay }; let job = if v.before { job } else { job.with_hidden_grading(app.ui.grading_hidden_for(c.id)) }; - let job = job.with_overlay(overlay); + // the After window is proofed like the whole-frame render under it; the Before side is + // never proofed (its whole-frame render isn't either) + let proof = app.ui.soft_proof.then_some(app.ui.proof); + let job = job.with_overlay(overlay).with_proof(if v.before { None } else { proof }); // keyed as the renderer keys the texture (a display profile changes the key) let job = app.renderer.job_for(v.slot, job); - // The overlay is part of the tile's identity too, so changing mask view/colour never leaves a - // stale region tile on screen while its replacement renders. - let look = job.settings.hash64() ^ overlay.key().rotate_left(17); + // The overlay and the proof are part of the tile's identity too, so changing mask view/colour + // or the proof never leaves a stale region tile on screen while its replacement renders. + let proof_key = if v.before { 0 } else { proof.map_or(0, |p| p.key()) }; + let look = job.settings.hash64() ^ overlay.key().rotate_left(17) ^ proof_key.rotate_left(41); let view = crate::region::RegionView { photo: c.id, before: v.before, key: job.key, full: (fw, fh), window: win, settings: look }; // windows of other photos, looks and zooms of this side can't be drawn any more app.region_tiles.retain(|(before, _), t| *before != v.before || t.is_current(c.id, (fw, fh), look, c.interacting)); @@ -533,15 +537,16 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let drawn_long = target_rect.width().max(target_rect.height()) * ppp; // the whole frame at about canvas size; the zoomed part of it is a window render (below) let look = d.hash64(); - // Diagnostic views that need whole-frame context, soft proofing, or reads too far for one - // window still render the whole frame at the size it is drawn. A mask overlay is window-safe. + // Diagnostic views that need whole-frame context, or reads too far for one window, still + // render the whole frame at the size it is drawn. Mask overlays and soft proofing (both act + // pixel by pixel) are window-safe. let sizes = crate::region::ViewSizes { drawn_long, canvas_long: canvas.width().max(canvas.height()) * ppp, native_long, texture_side, draft, - windows: !app.ui.soft_proof && overlay_supports_window(overlay), + windows: overlay_supports_window(overlay), }; let mut plan = crate::region::plan(&app.ui.settings, sizes); if plan.window_edge.is_some_and(|edge| app.window_refused == Some((id, look, edge))) { @@ -623,10 +628,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { app.renderer.request(Slot::Before, job, 90); } // zoomed past what the whole-frame render holds: also render the part on screen, at no more - // than 100 % (the GPU magnifies beyond that), for each side that is shown. Mask overlays use - // the same window; diagnostic overlays that need whole-frame context do not. - let windowable_view = - hover_key.is_none() && !app.ui.soft_proof && overlay_supports_window(overlay) && crate::region::same_aspect(display_aspect, aspect); + // than 100 % (the GPU magnifies beyond that), for each side that is shown. Mask overlays and + // soft proofing use the same window; diagnostic overlays that need whole-frame context do not. + let windowable_view = hover_key.is_none() && overlay_supports_window(overlay) && crate::region::same_aspect(display_aspect, aspect); let window_ctx = WindowCtx { id, overlay, diff --git a/crates/ui-egui/src/region.rs b/crates/ui-egui/src/region.rs index b242d30be..953008e0d 100644 --- a/crates/ui-egui/src/region.rs +++ b/crates/ui-egui/src/region.rs @@ -77,8 +77,8 @@ pub struct ViewSizes { /// `None`: no drag. pub draft: Option, /// Whether a window can be drawn over the whole-frame render (not with a diagnostic overlay - /// that needs whole-frame context, soft proofing, or a read too far for one window). Mask - /// overlays are window-safe. + /// that needs whole-frame context, or a read too far for one window). Mask + /// overlays and soft proofing are window-safe. pub windows: bool, } @@ -225,7 +225,7 @@ pub fn plan(settings: &crate::state::AppSettings, v: ViewSizes) -> LoupePlan { let drawn_long = if v.drawn_long.is_finite() { v.drawn_long.max(8.0) } else { canvas_long }; let scale = v.draft.map_or(1.0, |s| if s.is_finite() { s.clamp(0.1, 1.0) } else { 1.0 }); if !v.windows { - // the whole frame at the size it is drawn (soft proof or a whole-frame diagnostic has no + // the whole frame at the size it is drawn (a whole-frame diagnostic has no // window); while a slider drags, a canvas-sized draft as with windows: a drag at 1:1 must // not render the photo's every pixel each frame let long = if v.draft.is_some() { drawn_long.min(canvas_long) } else { drawn_long }; @@ -422,7 +422,7 @@ mod tests { assert_eq!(drag.window_edge, still.window_edge); } - // Given no window (a whole-frame diagnostic or soft proofing), the view at rest renders the + // Given no window (a whole-frame diagnostic), the view at rest renders the // whole frame at the size it is drawn, but a slider drag drafts at the canvas size like the // windowed path #[test] @@ -626,7 +626,7 @@ mod tests { } } - // Given a mode whose picture can't be a window (soft proofing, a whole-frame diagnostic, or a + // Given a mode whose picture can't be a window (a whole-frame diagnostic, or a // spot read too far for one render), the whole-frame render is as big as the view #[test] fn without_windows_the_whole_frame_is_rendered_at_the_drawn_size() { diff --git a/crates/ui-egui/src/tests_preview_limit.rs b/crates/ui-egui/src/tests_preview_limit.rs index dfddb9c4f..070921256 100644 --- a/crates/ui-egui/src/tests_preview_limit.rs +++ b/crates/ui-egui/src/tests_preview_limit.rs @@ -261,6 +261,29 @@ mod in_the_loupe { assert!(w >= 1300 && hh >= 700, "it covers the canvas: {w}×{hh}"); } + // Given soft proofing on at 1:1 (LR-VIEW-ZOOM), the view is a canvas-sized frame plus a + // native window carrying the proof, as without it, instead of a whole-frame render at the + // photo's full size; switching the proof off swaps the window back to the plain look + #[test] + fn soft_proofing_at_one_to_one_is_a_window() { + let (mut h, native) = detail(); + h.request("engine.execute", json!({"command": "view.zoom100"}), T); + h.settle(SETTLE); + let plain = h.app.region_view.expect("a window render without the proof"); + let r = h.request("engine.execute", json!({"command": "view.softProof", "params": {"on": true, "destWarning": true}}), T); + assert_eq!(r["ok"], true, "{r}"); + h.settle(SETTLE); + assert!(h.app.ui.soft_proof); + let main = rendered_long_edge(&h); + assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}"); + let proofed = h.app.region_view.expect("a window render with the proof"); + assert_eq!(proofed.full.0.max(proofed.full.1), native, "cut from the photo's own pixels"); + assert_ne!(proofed.settings, plain.settings, "the proof is part of the window's look"); + h.request("engine.execute", json!({"command": "view.softProof", "params": {"on": false}}), T); + h.settle(SETTLE); + assert_eq!(h.app.region_view.map(|r| r.settings), Some(plain.settings), "back to the plain window"); + } + // Given the user capped the preview at 1600 px, then 1:1 is rendered no larger than that #[test] fn an_explicit_limit_still_caps_one_to_one() { @@ -646,10 +669,16 @@ mod in_the_loupe { #[test] #[ignore = "a measurement, not a check"] fn profile_slider_drag() { - for (size, percent) in - [([1400.0, 900.0], 0.0), ([1400.0, 900.0], 100.0), ([1400.0, 900.0], 400.0), ([2800.0, 1700.0], 100.0), ([2800.0, 1700.0], 400.0)] - { + for (size, percent, proof) in [ + ([1400.0, 900.0], 0.0, false), + ([1400.0, 900.0], 100.0, false), + ([1400.0, 900.0], 400.0, false), + ([2800.0, 1700.0], 100.0, false), + ([2800.0, 1700.0], 400.0, false), + ([1400.0, 900.0], 100.0, true), + ] { let (mut h, native) = detail_in(size); + h.app.ui.soft_proof = proof; h.app.ui.zoom = if percent == 0.0 { crate::state::Zoom::Fit } else { crate::state::Zoom::Percent(percent) }; h.settle(SETTLE); h.app.session.begin_interaction("Exposure").unwrap(); @@ -673,9 +702,10 @@ mod in_the_loupe { ms.sort_by(|a, b| a.total_cmp(b)); let (w, hh) = region_tile(&h).unwrap_or((0, 0)); eprintln!( - "PROFILE {}x{} canvas, {percent:>4}% (0 = fit) of {native} px: tick→shown median {:.1} ms, p90 {:.1} ms, max {:.1} ms; whole-frame render {} px, window tile {w}×{hh}", + "PROFILE {}x{} canvas, {percent:>4}% (0 = fit){} of {native} px: tick→shown median {:.1} ms, p90 {:.1} ms, max {:.1} ms; whole-frame render {} px, window tile {w}×{hh}", size[0], size[1], + if proof { ", soft proof" } else { "" }, ms[ms.len() / 2], ms[ms.len() * 9 / 10], ms[ms.len() - 1], @@ -819,18 +849,6 @@ mod in_the_loupe { assert_eq!(h.app.region_tiles.len(), 2, "{:?}", h.app.region_tiles.keys().collect::>()); } - // Given soft proofing at 1:1 (a mode that draws one image, so no window), the whole-frame - // render is as big as the view, not a canvas-sized upscale - #[test] - fn soft_proofing_at_one_to_one_renders_the_whole_frame_at_full_size() { - let (mut h, native) = detail(); - h.app.ui.soft_proof = true; - h.request("engine.execute", json!({"command": "view.zoom100"}), T); - h.settle(SETTLE); - assert_eq!(h.app.region_view, None); - assert_eq!(rendered_long_edge(&h), native); - } - // Given a window the photo's spots make too big for one render (refused), the loupe falls back // to the whole frame at the drawn size, and tries windows again when the look changes #[test] diff --git a/docs/gaps.md b/docs/gaps.md index 1fa9d705d..3bd61c294 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -18,7 +18,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app | 1 B | **Camera colour is estimated, not measured** (LR-PROF-CAMERACOLOR). Raws start from a per-file fit to the camera JPEG, 5 bundled profiles or 52 spectral matrices; others use a neutral matrix (a black-and-white camera JPEG gives its tone curve only, #577; dark JPEG tones also guide the toe, #1307) | ΔE vs camera JPEG 2.9–10 after fitting (raw-parity.md); 0 chart-calibrated models vs 1,500 Adobe Standard profiles | every raw shooter sees colour that is close but not Lightroom's; some get neutral-matrix colour | 50–90 | [raw-parity](raw-parity.md#colour) | | 2 B | **CR3 beyond four verified bodies** (LR-IMP-FORMATS): `crx.rs` refuses vertical-tile C-RAW, partial-subband and other plane coding, multi-tile 0x200 and adaptive low-frequency gains, and those files fall back to the preview; CR3 sRAW / mRAW and HEIF CR3 too. Verified: M50 / R100 / R8 / R5 Mark II. EOS R7 C-RAW (odd-height QP map) decodes since a45c2b66 and R6 Mark II C-RAW is reported working on 0.6.0, but neither is verified yet (no corpus test or recorded check) | measured: Canon is the largest bucket (33% of photographers) and its share whose camera fully works is the lowest of the big four, 73%; 18 of 22 popular Canon bodies write CR3, 4 verified (raw-parity.md); R7 rule checked only on paired RAW / C-RAW captures ([cr3.md](cr3.md)) | +6.2 points of photographers whose camera works, the biggest single raw gain | 25–45 | [raw-parity](raw-parity.md#remaining-work-ranked-by-photographers-unlocked-per-hour) | | 3 B | **Render fidelity unmeasured** (LR-BEHAV-RENDER-FIDELITY): tone, highlights, texture / clarity / dehaze, NR and sharpening are tuned by eye | no side-by-side suite; a retune needs a new process version (only V1 exists) | edits imported from Lightroom look different; the look "isn't Lightroom's" | 30–60 | [raw-parity](raw-parity.md), [process-versions](process-versions.md) | -| 4 B | **Original-pixel detail at 100%** (LR-VIEW-ZOOM, P0): Detail, Before/After, Compare and Reference are sharp at 1:1–8:1 (zoom windows, #323). Mask overlays and soft proofing still render the whole frame at the drawn size instead of a window: sharp up to the GPU texture side, but a full-size render per edit at 1:1 | checklist row 🟡 | editing with a mask overlay or soft proof on at 1:1 is slow on large files, and blurry past the texture side | 4–8 | [ui-parity](ui-parity.md) | +| 4 | ~~Original-pixel detail at 100%~~ (LR-VIEW-ZOOM): closed 2026-10-12, see Closed recently | checklist row ✅ | — | 0 | [ui-parity](ui-parity.md) | | 5 B | **AI masks: Subject, Sky, People, Background, Landscape** are heuristics; Object / Describe run SAM 3 but its download has no mirror configured | `crates/segment`; ai-masks.md; MASK rows 67% | masking is how most Lightroom users edit today | 40–70 + model decision | [target-app-parity](target-app-parity.md#ai) | | 6 B | **Compressed ORF** (every Olympus / OM body since ~2008) | measured: 66 of 78 Olympus / OM models preview-only; `orf.rs` refuses; OM bucket 3% of photographers, 3% of them fully work | +2.6 points; best gain per hour of any raw gap (~0.2) | 8–15 | [raw-parity](raw-parity.md#compressed-orf-a-clean-room-starting-point) (the two Olympus patents that describe the method) | | 7 | **Nikon High Efficiency NEF** (HE / HE★ on Z 8, Z 9, Z 6III, Z f, Z50II, Z5II when set to HE) | named and preview only (#193); needs a pure-Rust JPEG XS decoder | +1.1 points (only shots taken in HE; lossless works on the same bodies); low gain per hour (~0.04), so not a beta blocker | 20–40 | [raw-parity](raw-parity.md) | @@ -60,6 +60,8 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w ## Closed recently (kept for calibration) +- 2026-10-12: original-pixel detail at 100% (gap 4, LR-VIEW-ZOOM, #323): soft proofing gets zoom windows like + Detail, Before/After, Compare, Reference and mask overlays; 1:1 tested equal to the full-size export. - 2026-10-11: Sony ILCA-99M2 profile. - 2026-10-11: Nikon edge bars on bodies without CropArea (#976, #914): 36 model / size / depth groups measured. - 2026-10-10: Nikon lossy-after-split NEF (#633), ORF 12.8 bpp (#651), HDR gain map / PQ AVIF / float @@ -80,6 +82,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w | 2026-10-14 | minor | Gap 21: de, es, fr, ja and uk catalogs filled (97–99%); 55–90 h → 40–70 h | | 2026-10-14 | minor | Gap 10: 17 uncompressed Olympus and Samsung models verified on one CC0 file each (254 models) | | 2026-10-14 | minor | Gap 29 added: the HDR merge, tested on real 67 MP brackets, keeps moving clipped highlights and merges in batches now; AI Denoise on merged DNGs, JPEG response curves and per-frame memory remain | +| 2026-10-14 | minor | Gap 4 closed: soft proofing at 1:1 renders a window (mask overlays already did); 1:1 pixels tested equal to export; only diagnostic overlays (Visualize Spots, Point Color range, HDR range) stay whole-frame | | 2026-10-13 | patch | Gaps 10, 22 and 23 each had two rows (two branches edited them; the merges kept both): the older copies are removed, and `cargo xtask parity` (in `ci`) now fails when a gap number is listed twice | | 2026-10-13 | minor | Closed recently: Sony ILCA-99M2 profile | | 2026-10-14 | minor | Gap 10: 21 Sony and Leica models verified on one CC0 file each (258 models) | diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 043aebb50..0db3c6262 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -812,6 +812,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | 2026-10-14 | minor | LR-IMP-CAMERA-COVERAGE: Nikon D600, D610 and D5000 NEFs drop the filler and noise columns on the right (a band), and the D600 and D610 measure their white level inside the image (#1627); status unchanged (🟡) | | 2026-10-15 | patch | LR-VIEW-BEFOREAFTER: side by side and stacked panes share click-zoom animation for whole-frame images, window tiles and input; render requests keep the final target (#1821); status unchanged | | 2026-10-14 | patch | LR-MASK-BRUSH: ⇧-click with the brush draws a straight stroke from where the last stroke ended, tapering when the brush changed in between (`cmd:mask.brushStroke` `connect`); status unchanged | +| 2026-10-14 | minor | LR-VIEW-ZOOM 🟡 → ✅: soft proofing gets zoom windows (mask overlays already had them; row text corrected), 1:1 pixels tested equal to the full-size export, slider-drag latency at 1:1 measured on Linux | | 2026-10-13 | patch | LR-VIEW-COMPARE, LRC-LIB-COMPARE: ⌘/Ctrl- and Shift-clicks on the filmstrip in Compare change the selection as in the grid; the two photos shown stay selected and the active photo is unchanged (#187); statuses unchanged | | 2026-10-13 | minor | KEYC-SELECTONLYACTIVE: Edit → Select Only Active Photo (⌘⇧D) lands as a new row; status 🟡 (headless-tested, native macOS menu path unverified) | | 2026-10-13 | patch | LRC-LIB-CATALOG-IMPORT: a catalog whose tables are all still in its `-wal` file (main file schema format 0) opens instead of failing as an unsupported schema format; status unchanged | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 07ae89fd7..557f37a6a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -157,7 +157,7 @@ essentials replaces its guessed 45% raw look with 84% × 0.75 (+1 point). | Discount | Mainstream | Essentials | Evidence | |---|---:|---:|---| -| Interaction fidelity / UI clarity | ×0.95 | ×0.95 | brush live preview gaps and feathering (#517, #518); crop artefacts and limits (#731, #742, #770, #771); Compare white-balance selector (#743–#745); low-res slider previews (#330); dialogs taller than the window (#781); mouse-wheel scroll (#714); 100% zoom capped (gap 4) | +| Interaction fidelity / UI clarity | ×0.95 | ×0.95 | brush live preview gaps and feathering (#517, #518); crop artefacts and limits (#731, #742, #770, #771); Compare white-balance selector (#743–#745); low-res slider previews (#330); dialogs taller than the window (#781); mouse-wheel scroll (#714) | | Stability on real machines | ×0.92 | ×0.92 | launch and quit crashes or silent startup failures (#250 macOS 12 Intel, #315 Windows Intel GPU, #431 Intel Mac, #260, #462, #783 Linux / Nouveau, #790 Windows installer); tests run in CI on FreeBSD only (gap 9) | | File exchange with Lightroom users (mainstream) / opening files people send (essentials) | ×0.92 | ×0.95 | Adobe XMP sidecars change colours (#290), XMP CameraProfile and Upright ignored (#204, #205), catalog import fails (#599), same Light values render differently (#523, #209), no write-back to `.lrcat`; for casual users: CR3 beyond verified bodies, compressed ORF, HE NEF open as previews (#473, #485, #556), HEIC trouble (#693) | | **Product** | **×0.804** | **×0.830** | | @@ -235,13 +235,13 @@ open as previews, and colour on every non-DNG raw is estimated rather than measu **Alpha gate:** all six core workflows (import, cull, develop, save and reopen, batch looks, export) work end to end on macOS; see the table in [roadmap.md](roadmap.md#alpha-gate). The partial rows (CR3 -variants, compressed ORF, HE NEF as previews; capped 100% zoom) degrade but don't block a workflow. +variants, compressed ORF, HE NEF as previews) degrade but don't block a workflow. -**To beta:** ~14 points of readiness, **~340–600 h**: the nine `B` gaps in [gaps.md](gaps.md) +**To beta:** ~14 points of readiness, **~340–600 h**: the `B` gaps in [gaps.md](gaps.md) (~230–430 h), plus burning down the ~92 open core-path user issues (40–80 h) and stability work around them. Beta needs: CR3, ORF and HE NEF decoding from sensor data; measured colour for the popular models and a fidelity suite with -scores; full-resolution zoom; Subject / Sky / People masks; a lens profile database for the +scores; Subject / Sky / People masks; a lens profile database for the popular lenses; tests in CI on macOS, Windows and Linux. ## How hours are calibrated @@ -303,6 +303,7 @@ P2 items 12–25 h; look tuning, performance, packaging, hardening 25–45 h. | 2026-10-14 | minor | Localization ~52% → ~55% (mean of the 12 key languages): de, es, fr, ja and uk catalogs filled | | 2026-10-12 | minor | UI/UX remaining 34–58 h → 32–54 h (canvas tools by keyboard); headline numbers unchanged | | 2026-10-14 | minor | Merge and HDR: merge 100% → 88% (LR-MERGE-HDR ✅ → 🟡 after testing on real 67 MP brackets; gap 29); area percentages unchanged | +| 2026-10-14 | minor | Gap 4 (full-resolution zoom) closed: removed from the interaction-fidelity notes, the alpha-gate partial rows and the beta needs; headline numbers and the ×0.95 interaction factor unchanged | | 2026-10-12 | minor | UI/UX remaining 40–70 h → 34–58 h (accessibility audit landed, ui-parity.md); percentages unchanged | | 2026-10-14 | minor | Camera-coverage sweep (#1327): 30 models verified on 40 CC0 files (`coverage_sweep_corpus.rs`, `coverage-sweep.tsv`; `docs/raw/coverage.py` 515 → 545 verified rows); the counts above are not re-measured here. Sensor verification does not establish colour parity (#1277); headline numbers unchanged | | 2026-10-12 | minor | 22 more CR3 bodies verified on one CC0 file each (raw-parity.md): use-weighted share whose camera decodes 85% → 92%, RAW decoding 77% → 83%; full 60.7% → 61.2% (~61%), mainstream depth 72.1% → 74.0% (~49% → ~51%), essentials depth 77.5% → 78.0% (~64% → ~65%) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index e0bf171ba..e5e0c6e5f 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (canvas tools by keyboard; focus-order test; ~50 unnamed hand-drawn widgets named) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (zoom row: soft proofing sharp at 1:1; canvas tools by keyboard; focus-order test; ~50 unnamed hand-drawn widgets named) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 How LightCraft's interaction compares with Lightroom's: views, panels, tools and handles, modifier keys, shortcuts, zoom, context menus and feel. Rows for individual menu items and shortcuts live in @@ -27,7 +27,7 @@ assessment and the notes that don't fit a row. Visual reference for agents: `pla | Crop, Upright, transform handles | handles, aspect lock, angle, overlays (O / ⇧O), guided Upright | ✅ all of them; overlay cycle on ⇧O | 85% | crop opacity of cut-out area (15.5) | | Masking tools | brush (A / B, flow, density, auto mask, pressure), linear / radial, range masks, AI masks, pins | brush, linear, radial, colour / luminance range, add / subtract / intersect / invert, masks panel, pins | 60% | brush live preview (#517 / #518), no pen pressure, no depth range, AI masks, Feather / Edge controls (15.5) | | Remove / heal | content-aware remove, heal, clone, visualize spots | heal, clone, auto source, visualize spots, red / pet eye | 65% | content-aware (patch synthesis) remove, generative remove, detect distractions | -| Zoom and navigation | Fit / Fill / 1:1 / 2:1 … 11:1, Navigator, spacebar pan | Fit to 800%, pinch zoom, pan, Navigator-like | 60% | **original-pixel detail at 100% with a mask overlay or soft proofing** (no zoom window there; Detail, Before/After, Compare and Reference have them; P0 gap LR-VIEW-ZOOM) | +| Zoom and navigation | Fit / Fill / 1:1 / 2:1 … 11:1, Navigator, spacebar pan | Fit to 800%, pinch zoom, pan, Navigator-like | 60% | diagnostic overlays (Visualize Spots, Point Color range, HDR range) render the whole frame instead of a zoom window; everything else, mask overlays and soft proofing included, is sharp at 1:1–8:1 | | Library culling and metadata | ratings, flags, labels with keys, painter, keywording, metadata panel, filter bar | ratings / flags / labels and advance, keyword list and sets, keywording box, metadata fields, smart albums, filter bar | 85% | painter tool, quick collection, some Classic keys | | Shortcuts | Lightroom desktop and Classic keymaps | Lightroom-desktop keymap with Classic additions, editable | 85% | deliberate differences below; no Classic keymap layer | | Context menus | everywhere | 26 sites (grid, filmstrip, albums, folders, keywords, masks, presets…) | 70% | unaudited against Lightroom's menus item by item | @@ -136,6 +136,7 @@ a real screen reader on any platform yet. | 2026-10-13 | minor | Library culling: backtick cycles the flag; ⌘↑ / ⌘↓ raise / lower flag status | | 2026-10-13 | minor | KEYC-COMPARE: `=` / `-` grid thumbnail size keys noted in the shortcuts section | | 2026-10-13 | minor | Shortcuts: Home / End and Shift+Left / Right in the two library grids, Cmd+O (Open Library…) and Shift+H (Visualize HDR) get bindings and keyboard tests; percentages and the dated counts above are unchanged | +| 2026-10-14 | minor | Zoom row: soft proofing gets zoom windows; LR-VIEW-ZOOM gap closed | | 2026-10-12 | minor | Accessibility: an AccessKit tree audit test over every view, Develop tool and 13 dialogs; unnamed fields, slider ranges and the Auto Stack slider fixed; 30% → 55%; UI remaining 40–70 h → 34–58 h | | 2026-10-11 | minor | Zoom row: Compare and Reference get zoom windows; what remains of LR-VIEW-ZOOM is overlays and soft proofing | | 2026-10-10 | major | Created: measured command and shortcut counts, assessment by area, estimates; moved the shortcut conflicts section here from the checklist |