Only a drag timed at full size (median of its full-size drafts, after the
first, at most 20 ms) forgets a slow-drag verdict; a drag that drafts small
from its start, or times fewer than two full-size drafts, leaves it unchanged,
as `DragScale::finish` does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Show N hidden locations" put every location removed from Local back at
once. With several hidden, the row now lists them (name, path on hover):
each comes back on its own, or all with Restore All. With one hidden, the
row restores it directly, as before. The command side already had
local.restoreHidden {path}; only the panel called it without a path.
The headless test hides a second place, opens the list from the row,
restores one, checks the other stays hidden, then restores the rest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
A click on a grid stack badge expands or collapses the stack. A double-click,
as in Lightroom, toggled it twice: the second click undid the first and the
stack stayed as it was. The badge now ignores the second click of a
double-click.
set_aspect always turned the ratio to the image's orientation, so a Custom
ratio taller than wide came out landscape on a landscape photo, and a
ratio picked after X (crop.rotateAspect) turned the crop back. A named
ratio now takes the crop's present orientation (landscape, or portrait
after X; a square crop follows the image), a custom [w, h] is used as
given, and crop.aspect takes orientation: "crop" | "given" to choose.
The window reapplies the remembered ratio to an untouched photo with
orientation "crop", as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
GeoNames cities15000 holds towns of 15 000 people or more, so a photo in a
smaller town got the nearest larger one in City (Portofino → Rapallo,
5.3 km), another municipality. metadata.reverseGeocode takes maxDistance
(metres): past it, City and Sublocation stay empty, State and Country
still fill, and the reply names the town as nearestCity. Hostile values
are refused. The command help, the Fill Location tooltip and the parity
row say what the list holds. Default unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Preset commands change the preset list outside pending_log, and a failed
presets.json write was only logged: unsaved() stayed None, so nothing
warned and the web backup (#1987) would zip a stale presets.json; on
desktop a full disk lost the preset silently. The failure is now kept
(presets_error), counted by unsaved() (plus one change), retried by
persist_if_dirty with the same back-off as failed appends, and cleared
once the write succeeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
An X-T30 RAF shot in Acros embeds a black-and-white camera JPEG, so the
per-file look fits only the tone curve, and the colour came from the
neutral "camera RGB ~ sRGB" fallback: a grey sky and muted colours. The
raw decode (lossless compressed) was correct.
Add a hand-kept SAME_SENSOR alias list next to the generated
rawtoaces-data table: the 26.1 MP X-Trans CMOS 4 bodies (X-T30, X-T30 II,
X-Pro3, X100V, X-S10, X-E4) use the X-T3's measured matrices, and their
white balance reads in Kelvin through them.
Photo ▸ Edit In ▸ Open Original in Other Editor handed a photo's stored path
straight to `open` / `xdg-open` / `explorer.exe`, with `Path::is_file()` as the
only check, and Lightroom catalog import kept original paths exactly as the
catalog had them. A crafted `.lrcat` could therefore put `payload.exe`,
`start.lnk`, `run.bat`, `page.hta`, `Evil.app` or `go.command` into the library
and have it run on one user click.
- `lightcraft_engine::open_path`: one gate for every path that leaves
LightCraft for the OS. The extension must be one LightCraft reads
(`import::EXTENSIONS`, which already covers the Edit In copy formats
`tif`/`tiff`/`psd`), it must be a regular file (so a `.app` bundle and any
other folder are out), what a link resolves to must be a read type too, and
what is handed over is the absolute path. A refusal is the sentence shown to
the user.
- Open Original and Edit In go through it; a refused photo is listed in
`skipped` with the reason, and the menu item is greyed out for a record that
can never be opened (extension only, so no disk or share is touched while a
menu is drawn).
- `lightroom_catalog::refuse_non_media` drops such images where the catalog is
read and again where its photos become records, with a warning each, so a
refused entry never becomes an openable library record. A virtual copy of a
refused original goes with it.
Tests: a catalog naming a program (and a virtual copy of it) imports neither
while the catalog's real photos do; a library record naming a program is never
handed to the stub launcher, which records no invocation, and a program
selected beside a photo leaves only the photo, by absolute path. Both fail
before this change.
Co-Authored-By: Claude <noreply@anthropic.com>
On an Intel Mac with an AMD Radeon Pro 5300M the Metal compiler crashes
on candle's mlx_gemm kernel, candle-metal-kernels panics on the compile
error, and the worker reported "the AI model failed unexpectedly" and
loaded the model on Metal again for every next job, so AI masks never
worked there.
The worker now retries a job that fails on the GPU (a panic or a candle
error) once on the CPU, after freeing the model. When that works, the
model stays on the CPU for the session and the job's outcome carries a
notice that segment_poll shows once; when the CPU fails too, the GPU
was not the cause and the next job uses it again. Missing or damaged
model files, render and remote-worker errors are not retried. Only
builds with a GPU path (macOS) retry; LIGHTCRAFT_GPU=0 and
LIGHTCRAFT_GPU_BACKEND=off now keep SAM 3 on the CPU from the start
(lightcraft_gpu::env_off_reason), like AI Denoise and the face models.
lightcraft-segment: Sam3::load_cpu, GPU_BUILD; text_logits returns the
crate's Error so Describe failures are classified like clicks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict only in docs/parity-checklist.md: the generated summary is
refreshed with `cargo xtask parity --write`, and the Last updated line
keeps main's format with this PR's change note (today's date) instead of
the duplicated note the branch carried. ⌥-drag preview still wins over
AllMasks and the selected mask in view_overlay; overlay kinds 6–9 unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no aspect ever chosen, every crop started free, so the Lock had to
be switched on photo after photo. Nothing chosen yet now means locked to
Original, as Lightroom starts; switching the Lock off once is remembered
as free, and an explicit ratio is remembered as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
The Pick flag on thumbnails, in Compare, in the loupe and on the Pick
button was the same outline as the button's idle state, so a pick was
hard to tell at a glance. A new filled pennant (`Icon::FlagPickFilled`)
marks a pick wherever a photo's own flag is shown; buttons, filters and
the reject flag keep their outlines.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
Constrain Crop only applied at render time, and the Crop tool shows the
uncropped view with the stored crop, so in the Crop panel (where the
checkbox is) ticking it or moving a Geometry slider changed nothing.
Session::set_develop now shrinks the stored crop about its centre until it
fits the warped photo when Constrain Crop is turned on, or when an edit
changes the warp (Geometry, Upright, lens corrections, section eyes) while
it is on. A crop edit itself is kept as drawn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Describe merged the detector's per-instance masks. Those stay near 0 even well inside what they
found: a sky came out about 87 % selected, a volleyball net about 70 %, a person about 94 %.
Two instances that touch both fade out where they meet, so a line showed between people standing
side by side.
- The detector's semantic head is now ported: semantic_projection over the pixel decoder, the
transformers implementation's semantic_seg. It matches the reference to about 1e-3, like the
instance masks. tools/sam3_reference.py writes it and the reference test checks it; older
reference files still pass.
- Each instance found (score above the threshold, as before) takes the semantic logits where it
is the strongest instance and near (above -4). The semantic mask is sure inside and runs on
across the boundary between two instances. Far from every instance nothing changes, so
Describe selects nothing it didn't find before.
On 16 prompts over three photos (a mountain landscape, two gym photos), the median coverage
inside each selection rises, e.g.:
- sky 0.87 -> 1.00
- mountains 0.81 -> 0.99
- net 0.71 -> 0.99
- floor 0.92 -> 1.00
- person 0.94 -> 0.99
Each selection gains 0 to 1.9 % of the photo at its soft rim and loses none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
These files carry no JPEG: their preview is an uncompressed 8-bit RGB
IFD (1217 x 913 in the H4D-40 FFF, 218 x 218 in the CFV 3FR). The
largest such image is handed on by embedded_preview as a small baseline
TIFF, which lightcraft_codecs::decode reads, so 3FR / FFF show a preview
in the grid and loupe before they are developed. No Orientation tag is
written: the raw file's applies, as for JPEG previews without one. The
FFF's 16-bit copy (with a TransferFunction of unknown meaning) is not
used.
decode_raw_preview applied a container-declared colour space by forcing
the JPEG decoder on any owned preview; it now checks for JPEG bytes, so
a wrapped TIFF (or a JPEG XL preview) goes to its own decoder.
Checked on the CC0 H4D-40 FFF and CFV 3FR through
embedded_preview_srgb: right colours, the 3FR upright through its
Rotate90. Tests cover the wrap, the choice among RGB images, a short
strip and truncated files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2NDVXA8jQQWeJb4YVTb6b
An FFF (a 3FR converted on the desktop) has no colour matrix of its own:
the camera's stays in the original 3FR header the FFF carries as a
complete TIFF in private IFD0 tag 0xC51B. The new vendor::hasselblad
reader decodes 3FR / FFF through dng::decode_as and, when the file has
no matrix, takes the matrices from that header, so an FFF no longer
develops with the "camera RGB = sRGB" fallback (H4D-40 as-shot white
moves from xy 0.279, 0.356 to 0.325, 0.336).
RGGB checked against the camera preview on the CC0 H4D-40 FFF and CFV
3FR (raw.pixls.us 931 and 709). Synthetic tests cover the embedded
matrix, an own matrix winning, a missing or damaged header, and
truncated files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2NDVXA8jQQWeJb4YVTb6b
The Print dialog's Page section gets Lightroom's Page Options for Single Image
and Contact Sheet: the page number at the bottom right, the print settings line
(sharpening, profile, resolution) at the bottom left, and crop marks at the
corners of each photo. They are `print.set {page: {pageNumbers, pageInfo,
cropMarks}}`, saved with templates, drawn in the preview and composed into the
PDF; marks and text turn white on a dark page.
The HDR / Panorama / HDR Panorama dialogs now open with the options of the
last merge of that kind instead of the defaults, and those options are kept
in the library's prefs.json (Session::last_merge) so they survive a restart.
"With Last Settings" and HDR for Each Bracket Set read the same store.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>