a6ea6ba83fecfd56d0df0bdd41d75bee7f1d0bf1
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4642c77e18 |
Dim the histogram while the canvas shows a draft
The histogram is measured on settled frames only, so during a drag it describes the frame from before the gesture while the canvas shows something newer, and nothing said so. The draft flag stopped at the render closure. It now reaches the interface: `canvas-draft` on the window and `Levels.provisional` for the readouts, both set on every canvas render from the flag that chose the frame's resolution, and cleared when a render fails. The histogram panel dims its display reading to half while a draft is up and brings it back when the frame settles. Dimmed rather than captioned, because a caption appearing on every drag would move the column; the raw reading has no frame to lag and is left alone. |
||
|
|
0a5eab0487 |
Wire the develop panels through globals, so a second copy is one line
Every panel in the develop column declared its inputs and its callbacks and had `app.slint` bind each one to a property or a callback on the window root. That is fine while a panel is drawn once. N9 draws them a second time, in the portrait dock, and the wiring is what would have to be copied: `MaskPanel` alone ran to forty lines of forwarding, and a callback added to one copy and not the other compiles, renders, and simply does nothing on the layout nobody was looking at. So the wiring moved to Slint globals. A panel reads the global and calls the global; Rust hooks the global instead of the window; and the instantiation in the column is now the panel's name and a pair of braces — every one of the ten children of the column, with no property that differs by placement left to supply. There is a global per panel family rather than one for all of them, and the reason is an import cycle. Each panel's model struct — `ParamRow`, `MaskRow`, `HistogramView` — is declared in the panel's own file, so a single global holding `[MaskRow]` and `[ParamRow]` would have to live in a file importing `masks.slint` and `adjust.slint` while both imported the global back, which Slint rejects. Breaking that needs six model declarations relocated, which is a change to the data model and not to the plumbing this is about. A global beside the panel it serves also lets each name drop the prefix it was carrying only because the window root is one flat namespace: `root.spot-radius` is `Repair.radius`, and `root.peaking-on` is `Peaking.showing`. `session.slint` is new and holds the two facts every family needs and none of them owns: whether there is an open photograph to edit, and which mode the view is in, with the three readings of the mode derived once instead of at each of the dozen places that tested one. `ViewMode` moves there from `adjust.slint`, where it was only ever a lodger. Nothing on screen changes. What is not here: the tool rail and the status strip still take their properties at the instantiation, because they are drawn once and N9 does not copy them; the preset sheet's own state stays on the window, because the library grid opens the same sheet and a global cannot bind the window's state — which is why `Transfer.open-presets` is handled in `presets.rs`, beside the summary it already had to compute. |
||
|
|
e22945a62d |
Tag the colour-independent status that was already built and unrecorded
NFR-A11Y-3 — no status conveyed by hue alone — read as untagged, and outstanding.md said "no compliance work found". Both were wrong. Five places already implement it, and four of them name the requirement in a comment explaining the design; what none of them had was a TRACES line. - `histogram.rs::percentage` states clipping as a figure and keeps `<0.1%` distinct from `0%`, so the text cannot say "none" while the marker beside it is lit. - `histogram.slint`'s ClipReadout is the other half: a marker that appears and disappears rather than changing tint, and the figure next to it. Either alone reads. - `library.slint`'s star strip is a solid star against an outline, differing in shape and luminance, over an achromatic palette. - `library.slint`'s FlagMark is a tick against a cross, and a reject also dims its whole cell. - `peaking.slint`'s colour chips say "Red" and "Cyan". A control for choosing between hues, presented only as hues, is unusable by exactly the person most likely to need it. The tag is honest about being wider than the evidence, and outstanding.md now records both gaps. Only the clipping clause has a test that would fail if the behaviour were removed; the three Slint components are argued rather than asserted. And the requirement's first named example — catalog colour labels — has no interface at all: `label` is a nullable column nothing writes or shows. That clause is untestable rather than satisfied, and closes when the label UI is built with a shape from the start. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ef07e6ca3e |
Give the canvas tools a rail of their own, and the column one width
Build and test / Desktop (Linux) (push) Failing after 1h14m38s
Build and test / Layer separation (push) Successful in 48s
🐳 Android image / Build and push (push) Successful in 16m30s
Build and test / android-image (push) Successful in 16m31s
Traceability / Requirement traces (push) Successful in 1m47s
Build and test / Android (aarch64) (push) Successful in 1h0m21s
Crop, Local and Repair were chips at the head of the develop column, sharing a row with the adjustment groups and told apart from them by the shape of their highlight. Three things followed from that, and only the last is cosmetic: the column closes, so the way out of a mode went away with the way in — hence the duplicate "Done Cropping" over the canvas; the chips are generated from the operation set, so the widest thing in the sidebar was a row nobody had chosen the contents of; and a mode and a filter are different kinds of state wearing one control. They are a fixed 60px rail down the left now, generated from a single table in toolrail.slint. A tool is one row of it plus a drawing plus a ViewMode variant; nothing in app.slint is touched to add one. What is left of the strip is the group filters, so it is GroupStrip. The column stops measuring itself. Every panel published a content-width and declared it as min-width, and the column took the largest — which spent the photograph's pixels on whatever happened to be widest, and moved the image sideways when switching tools swapped one set of panels for another. It is panel-width now, one number in style.yaml. That number is 360 and it is measured, not picked: the contents report a minimum of 344 in every mode, and they do not compress below it because a Text that does not elide reports the same minimum as preferred. 320 was tried and sliced Paste down the middle. The Flickable's viewport is floored at the layout's minimum rather than its preferred width for the same reason — content that is never told how much room it has cannot adapt to having less. Removing the eight content-width declarations repairs three comments an earlier edit had spliced sentences into. The raw histogram's note on keeping its hint short is rewritten rather than dropped: an over-long hint no longer widens the column, it pushes the column's minimum past the width it has and clips the panel, which makes that constraint sharper rather than obsolete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4b6c110816 |
Count the sensor's own numbers, so a cull can see headroom the render hides
FR-CULL-3's remaining two bullets. What existed was a *display* histogram tagged FR-DSP-7: it binds AdjustPass's Rgba8Unorm output, recovers an 8-bit code value, and counts clipping as `r == 255`. Its own documentation says a clipped bin means "a highlight that is actually gone rather than one the transform might still recover", which is the opposite of what a culling decision needs. FR-CULL-3 asks for the histogram of the sensor data, on the explicit grounds that a rendered image "systematically lies about what is recoverable in the raw", and a readout that measures the render cannot answer that however it is presented. So this is a second instrument beside the first rather than a setting on it. Both are true; they are true about different things; the panel offers both behind a chip row and the words travel with the numbers, because a raw saturation figure drawn under a heading saying Highlights would be mislabelled exactly where the difference matters. **What is reduced over, and what it cost to decide.** ARCH §5.5 specified the pre-demosaic CFA samples. This reduces over the demosaiced scene-linear texture instead, and §5.5 is amended to record the choice rather than let the specification and the code disagree in silence. The texture is camera-native — unbalanced, unmatrixed, uncurved — and normalised by the sensor's own black and white levels, so 1.0 is saturation by construction and the distribution below it is the headroom question with no calibration to carry. Retaining the CFA samples would mean keeping the packed u32 buffer Demosaicer::run currently drops: 48 MB at 24 MP, 120 MB at 60 MP, resident per open photograph whether or not anyone looks at the histogram, on a platform §6.2 exists because memory is scarce on. Three things it therefore cannot say, written into the module docs and into §5.5 rather than left to be discovered: it counts pixels not photosites, so a saturated site drags its interpolated neighbours up and per-channel clipping is smeared by about a demosaic kernel; it cannot see above white, because demosaic.wgsl clamps each photosite at 1.0 for its own good reasons (a Canon 6D reads to 16383 against a declared 15070) so "at saturation" and "a stop past it" share a bin; and it is measured after the CFA pattern is gone, so it can name which colour clipped in the reconstructed image but not which photosite went first. The axis is stops below saturation, 16 bins per stop over 256 bins — the same bin count the display reduction uses, so the fold into drawable columns is shared and a divergence between the two plots would have to be deliberate. A linear axis spends half its width on the top stop, which is why nobody has ever drawn a useful linear raw histogram. The fourth series is the brightest channel rather than luma: these values are unbalanced, so any weighted sum of them is a number about nothing, and the brightest channel is the one that saturates first and so the one the headroom question is actually about. It is a property of the file and not of the render, which has two consequences. It is computed once per photograph and cached — nothing downstream of the demosaic can move a count in it — so a cull does not pay the display histogram's per-frame cost three thousand times. And it describes the whole frame rather than the visible region, deliberately opposite to DevelopSession::histogram: a crop changes what is on screen and changes nothing about what the sensor recorded. Tags are on the reduction, the type, its constructor and the presentation arithmetic, each of which has a test that fails if the behaviour goes. The Slint panel and the push from lib.rs keep their reasoning as prose: nothing asserts them, and a tag would claim coverage the assertions are not making. |
||
|
|
cafa63ca6f |
Let the develop column ask how wide it needs to be
Build and test / Desktop (Linux) (push) Successful in 21m53s
Build and test / Layer separation (push) Successful in 28s
Traceability / Requirement traces (push) Successful in 32s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 53m45s
The column was 280px, a number chosen for a tablet, with 380px bolted on later for a desktop. Both were guesses at how much room the widest row inside needs, and a guess is what cannot work here: the mode strip is one chip per attribute the *operation set declares*, so the row is generated and no constant in app.slint can track it. When the guess came up short the failure was not a tidy clip. The Flickable inside the column never had its `viewport-width` set, so the viewport took its content's preferred width, and a viewport wider than its Flickable is *centred* in it — the same rule the note on the seam's `x: 0` already records a few lines below. So the column lost half of each edge rather than one of them: "HISTOGRAM" read "ISTOGRAM", "Straighten" read "aighten", Copy sat centred while Paste ran off the far side. It looked like a rendering fault and it was an alignment one. So the column asks instead of guessing. Every panel that can appear in it — image, histogram, geometry, settings transfer, masks, repairs, adjust, history — now publishes a `content-width`: how wide it has to be before it starts clipping itself, read off its own layout rather than asserted. Each declares that as its `min-width` too, and that is what makes the aggregation automatic: `column` is a layout, so it already reports the largest minimum among its children, and it does so for the panels that come and go with the mode as well, which live inside `if`s and cannot be named from outside. Grep `content-width` in ui/dr-ui/ui to see every panel with a say in the answer. The mode strip is named explicitly only because it is pinned outside that layout, so nothing else measures it. There is no floor left. A floor is one more guess and the panels state their own minimums now. The only thing still above the measurement is `panel-max-width`, which is not a size but a policy — a column may not take the window from the photograph it exists to serve — and it comes from Rust beside `layout-class` because a width read from `root.width` inside the layout that `root.width` depends on is a binding loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0233df4bf2 |
See what the highlights are doing: a live histogram (FR-DSP-7)
Exposure, blacks and whites were set by eye. Nothing said a highlight had blown — the canvas shows white where a channel is at 250 and white where it is at 255, and the difference is the whole question. **Counted on the GPU, not on the readback.** There is a full frame sitting in CPU memory on every canvas update right now — `AdjustPass::read_output`, the bridge spike S1 removes — and walking it would have been thirty lines and no shader. FR-DSP-7 states the mechanism and not just the feature: "these derive from a GPU-side reduction into a small buffer. Per-frame CPU readback of image data is prohibited." A histogram founded on the bridge would be correct today and deleted by S1, and would meanwhile be the reason the bridge could not go. What crosses the bus here is 4104 bytes whatever the image size. The reduction tallies into workgroup memory first and merges once per workgroup. A photograph is not noise: a clear sky puts tens of thousands of adjacent pixels in one bin, and contending for that single global atomic serialises the dispatch. **On the settled frame only.** `render_now` already knows whether a gesture is still moving — `draft` is the flag `redraw` derives from `was_coalesced` — so the dispatch and its transfer happen once when the slider stops rather than on each of the forty frames a drag emits. Nothing is lost: a histogram flickering past under a finger is not a reading anyone takes. FR-DSP-7 requires exactly this, that it not extend the FR-DSP-3 frame budget. Luma is weighted in 8.8 fixed point — 54, 183, 19, summing to 256 exactly — rather than in floats. Not thrift: it makes the shader's arithmetic reproducible bit for bit, which is what lets the test below be an `assert_eq` against a CPU count rather than a tolerance. ARCH §6.13's line about integer state, applied where it happens to also be free. **What the numbers were checked against.** A flat frame must put all 4096 pixels in one bin and one only. A 256-wide ramp must occupy every level with exactly the same count, which is what catches an off-by-one in the quantisation — a `floor` where a rounding was needed shifts the whole photograph one bin left and looks like nothing at all. And a 101x37 frame of seeded pseudo-random pixels — deliberately not a multiple of the 16x16 workgroup, so the edge tiles run off the image — is compared slot for slot against a second, obvious CPU implementation. Exact equality, no tolerance. The CPU version is a deliberate reimplementation rather than shared code: the bugs worth catching here are ones shared code would commit identically on both sides. Above that, the presentation arithmetic is unit-tested headless, because it is where a wrong answer is invisible. A histogram of the wrong shape looks exactly as plausible as one of the right shape. So: 64 columns because it divides 256 and an uneven fold draws an even ramp as a comb; the peak excludes the end columns, or a night scene scaled against its own black spike is a flat line with no information in it; heights are clamped into the plot; and "0%" is kept distinct from "<0.1%" and from "—", since an indicator reading "clipped" over a figure reading "none" is a panel contradicting itself. Clipping counts a *pixel* with any channel at an extreme, not a channel. Any, because a blown red has no gradation left in it however much green and blue still hold — and it is the saturated highlight, the sunset and the red jersey, that clips first and recovers worst. Per pixel, because counting channels can report 200% of a frame clipped, and a percentage above 100 is a readout nobody trusts again. Two affordances for it, which NFR-A11Y-3 asks for: a bar standing at the end of the plot the tones are piling against, and a figure saying how much. Either alone reads. The panel sits directly under the capture metadata and above every control, because it is what the controls are judged against. It is hand-built rather than generated, and ARCH §4.3a is untroubled: a histogram is not an operation — no parameters, changes nothing, answers a question rather than asking one — and nothing in it reads a parameter out of a descriptor. Three plot colours and a neutral luma trace join the palette. That is the swatch's exception rather than a second one: a per-channel histogram has to say which channel, and no achromatic treatment distinguishes red from blue, so the hue is data exactly as the image beside it is. Held well back from full strength for the reason the theme preamble gives. The bounded, non-parking map wait moves out of `AdjustPass` into `readback::await_mapping`, shared with the histogram's transfer. Thirty lines of load-bearing reasoning about frozen interfaces and lost devices, and two copies of it would have drifted. The histogram describes the frame on the canvas, so it is in the output colour space FR-DSP-7 asks for, and when zoomed it describes the visible region — a photographer inspecting a highlight at 4x is asking about that highlight. A device that cannot build the reduction loses the histogram and keeps the photograph. Still to do for FR-DSP-7: the pixel colour readout under the cursor. 324 tests pass, clippy and fmt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |