Test strip: density x grade grid on the preview - #661
Merged
Conversation
Print the frame as a 4x4 grid of real renders: Print Density increasing left to right, ISO-R Grade softening top to bottom, so the diagonals read light-to-dark and soft-to-hard like a split-filter test strip. Click a patch to commit its settings. The ladder is absolute (density 0.6/0.9/1.2/1.5, grade 90/105/120/135) rather than an offset around the current values, so a strip printed off one frame is comparable to the next. Session-only display state, like the zone overlay: no WorkspaceConfig field, no migration, no pipeline stage. Patches come from a new RenderWorker slot instead of the display buffer, since they are pixels the canvas does not have; density and grade are absent from the analysis cache key, so the per-frame metering is reused and only the exposure stage onward re-dispatches. - Picking leaves Auto Density / Auto Grade untouched: the patches were rendered under them, so changing them would render something other than the patch that was clicked. - The assembled mosaic is soft-proofed once rather than per tile; the transform is per-pixel, so it is identical and 16x cheaper. Without it a proofed canvas would blow out. - Cleared by any real render, by compare / flat peek taking the canvas, and by load_file, whose navigate-back memo path repaints without going through request_render.
- 6x6 instead of 4x4, over most of both sliders' travel: density 0.4-1.9 in steps of 0.3, grade R55-R180 in steps of 25. Both ladders straddle the defaults and stay inside the ranges their controls accept — outside them the values clamp and two rungs would render the same patch twice, so a test pins that. - Always render at preview resolution, never HQ. 36 full-res renders would take minutes, and each patch is shown at a sixth of the frame's width; the mosaic scales to the content rect regardless. - No gridlines over the mosaic: it reads as one print, the way a real test strip does. Only the patch under the cursor is outlined, so it stays obvious what a click would take. - Axis labels move to the zone numerals' size (bold, 1.25x), sharing one font helper with them, and the rung matching the current settings is accented in place of the box that used to mark that patch. Grid size is derived from the ladders throughout, so this is a two-constant change plus the label and gridline work.
- The button becomes an icon-only toggle sitting next to Set Targets in the Auto Density / Auto Grade row, fixed to the same 36px width so the row doesn't stair-step. It no longer spans the panel. - Shift+T toggles it (Shift+T was free; only Alt+Shift+T was taken), and the tooltip advertises the binding via tooltip_with_shortcut. - Printing shows a toast and ticks the HUD progress bar once per patch, since 36 renders take a few seconds and an icon-only button has no label left to say so. Cancelling mid-print hides the bar, and late progress from a cancelled job cannot resurrect it. CLAUDE.md gains the shortcut registry: REGISTRY is the single source of truth feeding the editor, the ? overlay and tooltips, so any new toggle, tool or action belongs there — with a note to check for collisions, since a duplicate key makes Qt fire activatedAmbiguously and both go dead.
Reprinting an unchanged strip was 36 renders for pixels we already had. The key is the render memo key with density and grade pinned: the strip supplies those two itself, one per patch, so the mosaic is invariant to whatever they currently are. That makes the print/pick/print-again loop a cache hit, since picking a patch changes nothing else — measured at 1749 ms to print vs 0 ms from cache on a real scan. Any other edit (crop, filtration, paper, toning) lands on a different key and re-prints. Reuses RenderMemo, so entries are per frame with the same LRU budget and by-reference contract as the navigate-back memo, and are dropped when the session empties. Keyed at store time rather than dispatch: measured bounds are persisted after a render with render=False, so the config drifts mid-print without invalidating anything (the drift RenderMemo.rekey exists for), and keying at dispatch made every reprint a miss. Safe because a change that did matter would have gone through request_render, which cancels the strip. _render_memo_key takes an optional config to build the pinned variant.
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A darkroom test strip for Print Density and ISO-R Grade. The Tone panel gets a Test Strip button that prints the frame as a 6×6 grid of real renders — density increasing left to right, grade softening top to bottom — so the diagonals read light-to-dark and soft-to-hard exactly like a split-filter test strip. Click the patch you like and its settings commit.
Choosing density and grade previously meant dragging two sliders and re-judging the whole frame after every move. This shows all thirty-six candidates at once.
The ladders are absolute rather than offsets around the current values, so a strip printed off one frame is comparable to the next, and they cover most of both sliders' travel:
0.4 … 1.9in steps of0.3R55 … R180in steps of25Both straddle the defaults and stay inside the ranges their controls accept — outside them the values clamp, and two rungs would render the same patch twice, so a test pins that.
No gridlines are drawn over the mosaic: it reads as one print, the way a real test strip does. Only the patch under the cursor is outlined, so it stays obvious what a click would take, and the rung matching the current settings is accented on its axis label.
How
Follows the zone overlay's split: pure NumPy in
features/exposure/analysis.py, painting inoverlay.py, session-onlyAppStateflags. NoWorkspaceConfigfield, no migration, no pipeline stage, no WGSL. Grid size is derived from the two ladders throughout, so 4×4 → 6×6 was a two-constant change.The one thing the zone grid didn't need is real pixels at other configs. Those come from a new
RenderWorker.build_stripslot on the render thread's warmImageProcessor:densityandgradeare absent from the analysis cache key, so the per-frame metering is reused and only the exposure stage onward re-dispatches. The strip always renders at preview resolution, never HQ — 36 full-res renders would take minutes, and each patch is shown at a sixth of the frame's width.Three decisions worth a reviewer's eye:
load_fileclears the strip, not onlyrequest_render: the navigate-back memo path repaints without going throughrequest_render, which would leave a strip hanging over a different frame. Compare and flat peek clear it too — both want the canvas.Verification
make lint,make typeclean; 2638 tests pass. 31 new unit tests across ladder math, worker, controller lifecycle and overlay paint/click.Headless E2E on
samples/DSC00448.ARW, both engines (gpu_enabledTrue and False, no fallback):0.00e+00) to a full render at its own rung, so patch assignment and mosaic slicing are exactTwo measurement notes, both about the probe rather than the feature: