M8: dots on the 1/65536 px rounding step stay put too

Review of #1796: rounding a mark's position to 1/65536 px with `round`
sent the halves of that step away from zero, the asymmetry the PR set
out to remove, one step further down. At a step of 25.5 - 2^-17 px, a
pan by 26 pixels put the first mark a pixel off a full render. Both
roundings now go half up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
samhyunandClaude Opus 5.5 committed 2026-11-01 18:40:05 +09:00
1 parent e39e563408
commit c8f34acc59
2 files changed
+28 -5

No files matched your search

+6 -5
View File
@@ -92,13 +92,14 @@ pub(crate) fn draw(pix: &mut Pixmap, view: &View, d: &Dots) {
}
}
/// The device pixel nearest `p` pixels from the view's top-left. A mark halfway between two pixels
/// goes to the right (or lower) one on either side of the origin, and `p` is first rounded to
/// 1/65536 px: a view a whole number of pixels off (a pan, a band, a patch) reaches it with another
/// rounding error, and would otherwise put the marks that fall halfway on the other pixel.
/// The first device pixel of a mark whose edge is `p` pixels from the view's top-left: the one
/// whose left (or top) edge is nearest, and the right (or lower) one when two are as near. `p` is
/// first rounded to 1/65536 px, halves up as well: a view a whole number of pixels off (a pan, a
/// band, a patch) reaches it with another rounding error, and, rounded either way, would put the
/// marks that fall halfway on the other pixel.
fn nearest_px(p: f64) -> f64 {
const STEPS: f64 = 65_536.0;
((p * STEPS).round() / STEPS + 0.5).floor()
((p * STEPS + 0.5).floor() / STEPS + 0.5).floor()
}
/// Which line covers each device pixel along an axis: 0 none, 1 a faded one, 2 a strong one.
+22
View File
@@ -158,3 +158,25 @@ fn dots_halfway_between_pixels_stay_put() {
}
}
}
/// A mark that rounds to a half step of 1/65536 px goes the same way on both sides of a view's
/// origin too: at a step of 25.5 − 2⁻¹⁷ px, the first mark is where 1/65536 px rounds up.
#[test]
fn dots_on_the_rounding_step_stay_put() {
let (d, page) = page_with_square();
let step = 25.5 - 2f64.powi(-17);
for lines in [false, true] {
let opts = Options { dots: Some(Dots { step, lines, ..dots() }), ..Default::default() };
let full = View { origin: (0.0, 0.0), scale: 1.0, width: 300, height: 200 };
let a = render_view_single(&d, page, &full, &opts).unwrap();
for pan in 1..60u32 {
let v = View { origin: (f64::from(pan), f64::from(pan)), scale: 1.0, width: 200, height: 120 };
let b = render_view_single(&d, page, &v, &opts).unwrap();
for y in 0..120 {
for x in 0..200 {
assert_eq!(rgba(&b, x, y), rgba(&a, x + pan, y + pan), "lines {lines}, a pan by {pan}, at {x}, {y}");
}
}
}
}
}