- 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: run the gate through craft-common-actions' shared gate.yml; FreeBSD nightly and on release
ci.yml is new: it calls the shared workflow (cached apt packages, nextest, main-only cache saves, Cargo.lock check). FreeBSD and Windows ARM64 run nightly and on pushes to release instead of on pull requests. Depends on storytold/craft-common-actions#69.
* CI: the gate installs no nextest (xtask ci runs cargo test)
- ORF_DECODED's Camedia branch drops the redundant \\S+ before .*; with two anchored greedies the scanner can no longer retry the same substring count-for-count at lower and lower split points (same match set, verified on 14 body strings).
- {c: 0 for c in classes} is now dict.fromkeys(classes, 0), which the rule wants and which stones the per-class counters.
* 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)
* Packaging: pin the Windows signing client and check its hash
* Packaging: pin appimagetool and check its hash
* Packaging: no pinned tool or runtime for an architecture exits clearly; pin the AppImage runtime and pass it with --runtime-file
Bash scripts use [[ ]] for tests, functions name their positional
parameters and end with an explicit return, and the app-specific packaging
check writes its ::error:: annotations to stderr. No behaviour change.
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