- Expose ParametricCurve from lightcraft-pipeline and use it in both
curve_luts and the UI curve editor, eliminating duplicate implementations
and ensuring render and graph never drift.
- Invert the parametric model while holding the other three regions fixed,
converting vertical pointer motion relative to the grabbed region's initial
amount so a horizontal drag with zero vertical travel leaves all sliders
untouched even with heavy overlapping neighbouring bumps (fixes AlexChambers
report: Darks=60, Lights=0 grabbed at x=0.51).
- Replace all raw table indexing with .get() per never-crash rules.
- Add unit and headless integration tests verifying that horizontal drag
with other regions non-zero changes nothing.
Co-Authored-By: deepseek-flash <noreply@deepseek.com>
The graph now shows the Effective curve and nothing else, as the parametric tab
should: the four region handles and three split notches are gone. You grab the
line itself, anywhere, and drag it up or down. Which slider moves is decided by
where on the line you grabbed — the region whose window that input level falls
in — and that slider follows on the same frame, so the line, the handle-free
graph and the sliders are one state.
The mapping is the renderer's own: a point's curve level against the region's
slider is a straight line, so the drag inverts it exactly and the line tracks
the hand instead of drifting away from it. The four regions tile the graph, so
any grab moves exactly one slider. ⌥ is still a tenth of the motion, ⌥ or not
the grab itself moves nothing, and a press away from the line is not a gesture
(no value change, no undo step).
The whole gesture is still one `develop` interaction, so it is one undo step.
Note: `ui.data` stores and loads one type, so the drag state is kept as
`Option<(region, x)>` and loaded with `.flatten()` — storing one shape and
loading another silently returns `None` every frame, which is what made the
first attempt drop the drag after a single frame.
Tests rewritten for the line: a drag down and up, which region each quarter of
the graph belongs to, the level/amount inverse at off-centre points, the region
tiling, hovering naming the region, and one undo step per gesture.
Co-Authored-By: deepseek-flash <noreply@deepseek.com>
The parametric graph is now a control surface. Each of the four regions has a
handle on its own bump: drag it up or down and that region's slider moves with
it. Each of the three splits has a notch on the graph's bottom edge: drag it
left or right and the region boundary moves. Every write goes through
`develop.set` on the frame it happens, so the sliders under the graph, the
curve and the handles are all the same state and are seen to agree mid-drag.
A drag runs inside one `develop` interaction, so a whole gesture is one undo
step and Ctrl+Z takes it back at once rather than unwinding it frame by frame.
Details that make it usable:
- The handle is the one under the *press*, not under the pointer once the drag
threshold has passed — that is most of a handle's width away, and hit-testing
the moved pointer grabbed nothing at all on a short drag.
- The drag follows the pointer's motion from the handle's own centre, so a grab
never makes the control jump by the distance from the pointer to that centre.
- Splits are looked for before region handles: a notch sits on the bottom edge
where the curve itself passes on the dark end of a shallow line, so on equal
terms a region handle would always win and a notch could never be grabbed.
- A split is kept a step clear of its neighbours, and a region handle saturates
at ±100 rather than jumping when the pointer goes past what the region can
reach. ⌥ is a tenth of the motion, as the point curve's drag is.
Tests cover a region drag down and up, a split drag (including its bounds), a
drag that starts off every handle moving nothing, the readout naming the control
under the pointer, the level/amount inverse, and one undo step per gesture.
Co-Authored-By: deepseek-flash <noreply@deepseek.com>
The panel drew the Effective curve with its own copy of the region formula,
which omitted the running maximum `lightcraft_pipeline::finish::curve_luts` uses to
keep the curve monotone. With neighbouring regions pushed opposite ways the
drawn line folded back into a dip the renderer never produced, so the graph and
the four sliders under it disagreed.
`ParametricCurve` is now the one curve both use: the region bumps summed and
then forced monotone, sampled at the renderer's resolution. The graph asks it
every frame and holds no state of its own, so a slider moved by hand, Reset and
Undo all redraw the line from the settings in the frame they change — the two
can no longer be left showing different things.
Reset and the undo step were already atomic (the whole Tone Curve is one
`develop` state, and a drag is one engine interaction); tests now pin that the
painted line, not just the settings, follows Reset and Undo.
Tests: the folded shape proves the bare sum is not monotone while the model is;
the painted path is compared point-by-point against the model; Reset clears the
four sliders and the graph together; one undo takes back a whole drag.
Co-Authored-By: deepseek-flash <noreply@deepseek.com>
A message a catalog lacks, or translates as an empty string, shows in
English and fails nothing:
- build.rs no longer requires every formats catalog to carry the same
messages, and tr_format! gets a last arm that formats the English
source, so a message no catalog has builds.
- Coverage tests (every language translates X) and tests of one
language's exact wording are gone; the language-switching, layout,
date, placeholder and user-value tests read the catalog's own value
instead of a hard-coded translation.
- i18n::tests::translation_gaps_report lists, per language, the plain
and format messages it lacks and its untranslated What's New lines.
It always passes; cargo xtask ci
runs it from the test step's binary after the tests.
- catalogs_load checks that every catalog file is a JSON object of
strings with each key once; it.json named "Dimensions" twice.
doc_path::resolve uses craft_docpath::resolve_absolute; a path that resolves but is not absolute here is reported as not on this computer. Adds tests for Windows-form, GVFS-relative and colon-in-name paths, and a git-sources check in nix.yml and release.yml.
Found while testing crates/catalog/src/rules.rs:673 (num_op), reached via Rule::matches at :956-957
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
dialog_field took the focus back every frame while the field wasn't being edited, so Tab never
reached the check box, Cancel or OK of New Album, New Smart Album, Save Metadata Preset and the
other one-field dialogs. It now takes the focus once, when the dialog opens (the way the keyword
dialogs already did).
* CI: dev profile keeps line tables for workspace crates and no debug info for dependencies
* Control server tests: persistent_requests_and_slow_replies_outlive_the_idle_read_timeout no longer races the idle timeout
Each request (and the closing half-shutdown) reaches the socket before the
worker reads, so on a slow machine only the reply waits outlast the 100 ms
idle timeout, as the test intends. The idle test sends its input the same
way, and the no-frames test allows 5 s instead of 2 s to give up.
* CI: one integration-test binary per crate
Each crate's tests/*.rs become modules of tests/it/main.rs (51 test files
into 13 binaries, plus 6 that stay their own). Inner #![cfg] attributes
move to the mod lines. Files that change state for the whole process keep
a binary of their own: codecs jxl_tail_heap and raw sr2_heap (dhat global
allocator), gpu shutdown, shutdown_first and memory, ui-egui shutdown.
Docs, comments and the GPU probe name the new paths and targets
(--test it <module>::); the CI report names an it binary's skips after
the crate.
* CI: tests under nextest, doctests apart, one wasm check
xtask ci runs the tests with cargo nextest run --lib --bins --tests when
cargo-nextest is installed, else cargo test with the same targets and a
one-line notice; doctests run after them with cargo test --workspace --doc.
The heif and hardware GPU steps use the same runner. Examples stay out of
the test build (clippy --all-targets still checks them). Concurrent tests
under nextest follow the RUST_TEST_THREADS cap (NEXTEST_TEST_THREADS).
The wasm check is one cargo check --keep-going over the same crates, and
clippy runs with --keep-going.
* CI: tests run under cargo test by default, nextest with LIGHTCRAFT_NEXTEST=1 (per-process scene cache)
Dropping a wgpu buffer after the device's thread-locals are gone can
abort; the device owns the memory either way.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
The command sweep ran metadata.downloadPlaceNames with empty params, which
starts a real download from download.geonames.org into the settings folder.
The sweep now skips it (the command has its own loopback-server tests) and
asserts that no place names download was started.
Co-Authored-By: Claude <noreply@anthropic.com>
`median_row` (the running median under process V2's highlights/shadows base) kept a sorted
window of f32 and, per pixel, found the leaving and entering values with two binary searches
on `total_cmp`, then `remove` + `insert`. The searches mispredicted on most steps.
Now the window holds `i32` keys that order exactly like `f32::total_cmp` (the same transform
it applies), both ranks come from one branch-free counting pass the compiler vectorizes, and
one `copy_within` moves the keys between them. Equal keys are equal bits, so the output is
unchanged bit for bit.
24 MP Sony A7 III log luminance, one P-core, interleaved: r = 9 (export) 1977-2027 ms ->
912-958 ms, r = 3 1249-1292 -> 658-724, r = 24 2180-2261 -> 972-1030. All cores, r = 9:
170-188 -> 86-102 ms. The GPU path has its own median and is untouched.
New test: special values (NaN of both signs, signed zeros, infinities), long ties and planes
narrower than the window match a per-pixel sorted median bit for bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0121DZuByhKyGLHw5C9JkUue
A colour-range mask compares every pixel against every sample, so
render and export cost is pixels x samples. The interactive sampler
stops at 5, but mask.add / mask.update (colorRange) passed their
`samples` array straight through, so a few hundred samples on a large
render turned into billions of operations and hung rendering and
export. The project already caps similar lists (MAX_STROKE_DABS,
MAX_DETAIL).
The samples list from automation is now capped at 64.
Test: a request with 1000 samples is cut to 64; a two-sample request is
unchanged.
Reported-by: BS.ehlas.cz
The File/Edit/View headings sat only egui's default 8 px apart because frameless
buttons get no padding of their own. The test lays out the in-window menu bar and
checks every pair of neighbouring headings is at least 24 px apart; without the
item-spacing fix the gap is 4 px and the assertion fails.
parse_rgb checked only the length, and u8::from_str_radix accepts a
leading '+', so "#+f+f+f" set the overlay to #0f0f0f. Require six hex
digits, as 80e439f6 does for the other #rrggbb parsers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on 68 photos straightened by hand in Lightroom Classic and 300 left as shot, Auto
straighten turned 79 of the 300 by more than 1° (65 by more than 2°): the edges of planks, a
branch or a bird's wing were taken for the horizon. It now answers only for at most 10° and on at
least 3 agreeing lines (Upright Level is unchanged): 19 of the 300, with the same 14 of the 68
levelled within 0.5° and 1 instead of 3 more than 1° off. When it finds nothing, the app says so
instead of leaving the photo without a word. LR-CROP-AUTO goes to 🟡: it still leaves most tilted
photos alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two corpus tests fail on main with the CC0 corpus present (they skip without it, so no CI
has run them):
- camera_preview::tests::corpus_z8_uses_the_bundled_profile / corpus_z6ii_…: the test fitted
the camera look with `lens: None`. Since 1ee751ebd (Nikon's in-file distortion for the
Z 6II and Z 8, the same day as the tests) the camera's JPEG is matched through that warp,
which files.rs reads with `lens_geometry` and removes from the opcodes before binning the
proxy. The test now does the same; the app path was right all along.
- corpus_raw_decodes: coverage-1639.fff has no JPEG and hands on its 8-bit RGB image as a
baseline TIFF (`rgb_preview`, b5dc500c5); the test only accepted JPEG / JPEG XL previews.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Find Missing Photos took a missing photo's file name with Path::file_name,
which doesn't split on `\` on macOS or Linux, so a library made on Windows
had photos it could only find by content: those with no hash on record
(Lightroom-catalog imports, files edited since) were never found. Both the
missing photos and the files found on disk are now named by one rule: the
part after the last `/` or `\`, in lower case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LibraryLock::acquire wrote catalog.lock.owner with std::fs::write,
which follows a symlink at that name. A library folder someone else
prepared (a shared drive, an unpacked archive, a synced folder) could
carry catalog.lock.owner -> ~/.bashrc or any other file the user can
write, and opening the library replaced that file's content with the
owner JSON. The catalog's staging files had the same problem and were
fixed in #1502; the owner note was missed.
The note is now written through store::create_staging (made
pub(crate)): whatever is at the name is removed (a link itself, never
its target) and the file is created with create_new. Drop already
removed the note with remove_file, which never follows a link.
Test (Unix): a link at catalog.lock.owner pointing at a file outside
the library leaves that file's bytes unchanged through acquire and
drop, and the note lands in the library as a regular file naming this
process.
Reported-by: BS.ehlas.cz
Found while testing crates/engine/src/sync.rs:851 (SyncStep::Remove; compare mark_deleted in crates/engine/src/cmd/library.rs:168)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>