Keep clarity's radius when the develop view zooms in
A frame fraction was taken of the render's short edge, and render_scale folds the zoom into the region the render stands for. Zoomed to 1:1 on a corner, clarity, texture and dehaze drew a quarter of the halo the export gets, though the docs promise they preview honestly at any zoom. RenderScale now carries the whole framed frame (within), and a frame fraction is measured against it; at fit nothing changes.
This commit is contained in:
@@ -197,6 +197,10 @@ pub const DETAIL_BASE_UNIFORM_FIELDS: usize = 4;
|
|||||||
pub struct RenderScale {
|
pub struct RenderScale {
|
||||||
render: (u32, u32),
|
render: (u32, u32),
|
||||||
full: (u32, u32),
|
full: (u32, u32),
|
||||||
|
/// The whole framed photograph at source resolution: `full` before the
|
||||||
|
/// zoom and the tile were folded in. What a frame fraction is a fraction
|
||||||
|
/// of — see [`Self::frame_fraction`].
|
||||||
|
frame: (u32, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderScale {
|
impl RenderScale {
|
||||||
@@ -208,9 +212,27 @@ impl RenderScale {
|
|||||||
/// [`crate::EditGraph::render_scale`] works both out from the framing, and
|
/// [`crate::EditGraph::render_scale`] works both out from the framing, and
|
||||||
/// is what a caller should normally use.
|
/// is what a caller should normally use.
|
||||||
pub fn new(render: (u32, u32), full: (u32, u32)) -> Self {
|
pub fn new(render: (u32, u32), full: (u32, u32)) -> Self {
|
||||||
|
let full = (full.0.max(1), full.1.max(1));
|
||||||
Self {
|
Self {
|
||||||
render: (render.0.max(1), render.1.max(1)),
|
render: (render.0.max(1), render.1.max(1)),
|
||||||
full: (full.0.max(1), full.1.max(1)),
|
full,
|
||||||
|
frame: full,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DSP-1 | FR-DSP-2
|
||||||
|
/// The same scale, for a render that shows only part of a larger frame.
|
||||||
|
///
|
||||||
|
/// `frame` is the whole framed photograph at source resolution — the crop
|
||||||
|
/// folded in, the zoom and any tile not. A zoomed view and an export tile
|
||||||
|
/// both look at part of the frame, and a clarity radius is a fraction of
|
||||||
|
/// the *frame*, not of the part: measured against the part, zooming in
|
||||||
|
/// shrinks the halo to a fraction of what the file will get, and two
|
||||||
|
/// neighbouring tiles of an export would each draw their own.
|
||||||
|
pub fn within(self, frame: (u32, u32)) -> Self {
|
||||||
|
Self {
|
||||||
|
frame: (frame.0.max(1), frame.1.max(1)),
|
||||||
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,8 +281,19 @@ impl RenderScale {
|
|||||||
/// For the compositional family — clarity, texture, dehaze — and the same
|
/// For the compositional family — clarity, texture, dehaze — and the same
|
||||||
/// unit `dr-gpu`'s mask rasteriser already converts feathers in. An edit
|
/// unit `dr-gpu`'s mask rasteriser already converts feathers in. An edit
|
||||||
/// stored this way is resolution-independent by construction.
|
/// stored this way is resolution-independent by construction.
|
||||||
|
///
|
||||||
|
/// Measured against the whole frame ([`Self::within`]), scaled by the
|
||||||
|
/// render's own short edge over the viewed region's. When the render shows
|
||||||
|
/// the whole frame the two sizes cancel and this is `fraction` of the
|
||||||
|
/// render's short edge exactly.
|
||||||
pub fn frame_fraction(&self, fraction: f32) -> f32 {
|
pub fn frame_fraction(&self, fraction: f32) -> f32 {
|
||||||
fraction * self.render.0.min(self.render.1) as f32
|
let render = self.render.0.min(self.render.1) as f32;
|
||||||
|
let viewed = self.full.0.min(self.full.1) as f32;
|
||||||
|
let frame = self.frame.0.min(self.frame.1) as f32;
|
||||||
|
if self.frame == self.full {
|
||||||
|
return fraction * render;
|
||||||
|
}
|
||||||
|
fraction * render * (frame / viewed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a radius stated in source pixels survives this render.
|
/// Whether a radius stated in source pixels survives this render.
|
||||||
@@ -914,6 +947,21 @@ mod tests {
|
|||||||
assert!((export.frame_fraction(0.01) - 40.0).abs() < 0.5);
|
assert!((export.frame_fraction(0.01) - 40.0).abs() < 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_frame_fraction_does_not_shrink_with_the_zoom_or_the_tile() {
|
||||||
|
// TRACES: FR-DSP-1 | FR-DSP-2
|
||||||
|
// A 6000×4000 frame. At fit in a 1500×1000 panel, 1% of it is 10
|
||||||
|
// render pixels; zoomed to 1:1 on a 1500×1000 corner of it, the same
|
||||||
|
// 1% is 40 — the 40 the file gets — and an export tile of that corner
|
||||||
|
// must say 40 too, or each tile draws its own halo and the seams show.
|
||||||
|
let fit = RenderScale::new((1500, 1000), (6000, 4000));
|
||||||
|
assert!((fit.frame_fraction(0.01) - 10.0).abs() < 1e-3);
|
||||||
|
let zoomed = RenderScale::new((1500, 1000), (1500, 1000)).within((6000, 4000));
|
||||||
|
assert!((zoomed.frame_fraction(0.01) - 40.0).abs() < 1e-3);
|
||||||
|
let tile = RenderScale::full((1024, 1024)).within((6000, 4000));
|
||||||
|
assert!((tile.frame_fraction(0.01) - 40.0).abs() < 1e-3);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zooming_to_one_to_one_makes_the_preview_exact() {
|
fn zooming_to_one_to_one_makes_the_preview_exact() {
|
||||||
// The reason there is no separate full-resolution preview path: the
|
// The reason there is no separate full-resolution preview path: the
|
||||||
|
|||||||
@@ -952,7 +952,7 @@ impl EditGraph {
|
|||||||
((fw as f32 * view.width).round() as u32).max(1),
|
((fw as f32 * view.width).round() as u32).max(1),
|
||||||
((fh as f32 * view.height).round() as u32).max(1),
|
((fh as f32 * view.height).round() as u32).max(1),
|
||||||
);
|
);
|
||||||
crate::detail::RenderScale::new(render, full)
|
crate::detail::RenderScale::new(render, full).within((fw, fh))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TRACES: FR-DEV-3 | FR-DSP-1
|
/// TRACES: FR-DEV-3 | FR-DSP-1
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user