mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
M8: dots quantize their position apart from the view's origin
Review of #1796: quantizing the mark's distance from the view's corner took in the error of the view's origin converted to pixels, so a mark one 1/65536 px step short of a half could still cross it. At a step of 24 and a zoom of (75.5 - 2^-17) / 24, a pan by 51 pixels moved the first mark. The mark's position and the origin are now each quantized before the one is taken from the other. The origin rounds its halves down: some 4 x 10^10 pixels out, its error is exactly half a step, and rounded up it moved the marks again. Past 2^37 pixels the step is finer than f64 and values are used as they are, so huge ones no longer overflow to infinity and come out as a mark at the view's corner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8f34acc59
commit
8c93d0559f
2 files changed
+68
-8
No files matched your search
@@ -57,7 +57,7 @@ pub(crate) fn draw(pix: &mut Pixmap, view: &View, d: &Dots) {
|
||||
let strong = |k: i64| k.rem_euclid(major) == 0;
|
||||
// The device-pixel span of the mark on line `k` along an axis `len` pixels long from `o`.
|
||||
let span = |k: i64, o: f64, len: i64| {
|
||||
let a = (nearest_px(k as f64 * d.step * s - o) as i64).saturating_sub(lead);
|
||||
let a = (nearest_px(k as f64 * d.step * s, o) as i64).saturating_sub(lead);
|
||||
(a.max(0), a.saturating_add(side).min(len))
|
||||
};
|
||||
let row = w as usize * 4;
|
||||
@@ -92,14 +92,20 @@ pub(crate) fn draw(pix: &mut Pixmap, view: &View, d: &Dots) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// The first device pixel of a mark whose edge is `at` pixels from the page origin, in a view whose
|
||||
/// top-left is `o` pixels from it: the pixel whose left (or top) edge is nearest, and the right (or
|
||||
/// lower) one when two are as near. A view a whole number of pixels off (a pan, a band, a patch)
|
||||
/// gets `o` back from its origin with a rounding error, so `at` and `o` are each rounded to
|
||||
/// 1/65536 px first: the mark's own position decides the pixel, and the view only counts it out
|
||||
/// from its corner. The mark's halves go up and the origin's down: far out, where the origin's
|
||||
/// error is half a step, it still comes back to the whole pixel. Past 2³⁷ px the step is finer
|
||||
/// than `f64` and values are used as they are.
|
||||
fn nearest_px(at: f64, o: f64) -> f64 {
|
||||
const STEPS: f64 = 65_536.0;
|
||||
((p * STEPS + 0.5).floor() / STEPS + 0.5).floor()
|
||||
const EXACT: f64 = 137_438_953_472.0; // 2³⁷
|
||||
let up = |v: f64| if v.abs() < EXACT { (v * STEPS + 0.5).floor() / STEPS } else { v };
|
||||
let down = |v: f64| if v.abs() < EXACT { (v * STEPS - 0.5).ceil() / STEPS } else { v };
|
||||
(up(at) - down(o) + 0.5).floor()
|
||||
}
|
||||
|
||||
/// Which line covers each device pixel along an axis: 0 none, 1 a faded one, 2 a strong one.
|
||||
|
||||
@@ -180,3 +180,57 @@ fn dots_on_the_rounding_step_stay_put() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The view's origin is quantized apart from the marks: converted to pixels, a whole-pixel origin
|
||||
/// comes back a hair off, and taken together with a mark one step short of a half it would push the
|
||||
/// mark across.
|
||||
#[test]
|
||||
fn dots_one_rounding_step_short_of_a_half_stay_put() {
|
||||
let (d, page) = page_with_square();
|
||||
let s = (75.5 - 2f64.powi(-17)) / 24.0;
|
||||
for lines in [false, true] {
|
||||
let opts = Options { dots: Some(Dots { lines, ..dots() }), ..Default::default() };
|
||||
let a = render_view_single(&d, page, &View { origin: (0.0, 0.0), scale: s, width: 300, height: 200 }, &opts).unwrap();
|
||||
for pan in 1..100u32 {
|
||||
let v = View { origin: (f64::from(pan) / s, f64::from(pan) / s), scale: s, width: 200, height: 100 };
|
||||
let b = render_view_single(&d, page, &v, &opts).unwrap();
|
||||
for y in 0..100 {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Far out (some 4 × 10¹⁰ pixels), a whole-pixel origin comes back from page units exactly half a
|
||||
/// rounding step off: rounded up, it would move the marks that fall halfway between pixels.
|
||||
#[test]
|
||||
fn far_out_dots_stay_put() {
|
||||
let (d, page) = page_with_square();
|
||||
let s = 0.3;
|
||||
let at = 42_536_140_695.0;
|
||||
let opts = Options { dots: Some(Dots { step: 5.0, ..dots() }), ..Default::default() };
|
||||
let a = render_view_single(&d, page, &View { origin: (at / s, 0.0), scale: s, width: 420, height: 40 }, &opts).unwrap();
|
||||
for dx in 1..200u32 {
|
||||
let v = View { origin: ((at + f64::from(dx)) / s, 0.0), scale: s, width: 200, height: 40 };
|
||||
let b = render_view_single(&d, page, &v, &opts).unwrap();
|
||||
for y in 0..40 {
|
||||
for x in 0..200 {
|
||||
assert_eq!(rgba(&b, x, y), rgba(&a, x + dx, y), "a pan by {dx}, at {x}, {y}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Positions too far out to quantize draw nothing rather than a mark at the corner.
|
||||
#[test]
|
||||
fn dots_out_past_any_page_draw_nothing() {
|
||||
let (d, page) = page_with_square();
|
||||
let plain = render_view_single(&d, page, &View { origin: (1e304, 0.0), scale: 1.0, width: 40, height: 40 }, &Options::default()).unwrap();
|
||||
for (step, x) in [(1e290, 1e304), (1e299, 1e305), (3e303, 1.5e308)] {
|
||||
let opts = Options { dots: Some(Dots { step, ..dots() }), ..Default::default() };
|
||||
let p = render_view_single(&d, page, &View { origin: (x, 0.0), scale: 1.0, width: 40, height: 40 }, &opts).unwrap();
|
||||
assert_eq!(p.data(), plain.data(), "a step of {step} at {x}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user