use super::{FLOOR, QUIET, count, module, paint, pitch, span};
use crate::qr::Symbol;
use crate::ui::Model;
use egui_kittest::HarnessBuilder;
use egui_kittest::wgpu::TestRenderer;
fn same(got: f32, want: f32) {
assert!((got - want).abs() < f32::EPSILON, "{got} is not {want}");
}
fn number(n: u32) -> f32 {
f32::from(u16::try_from(n).expect("every number here is a small one"))
}
#[test]
fn the_pitch_is_a_whole_number_of_device_pixels() {
same(pitch(161.0 * 3.5, 161, 1.0), 3.0);
same(pitch(161.0 * 3.5, 161, 2.0), 3.5);
}
#[test]
fn more_room_is_a_bigger_symbol() {
assert!(pitch(1000.0, 161, 1.0) > pitch(500.0, 161, 1.0));
assert!(pitch(500.0, 161, 1.0) > pitch(250.0, 161, 1.0));
}
#[test]
fn a_pane_with_no_room_still_gets_a_module_a_pixel() {
same(pitch(10.0, 161, 1.0), FLOOR);
same(pitch(10.0, 161, 2.0), FLOOR / 2.0);
same(pitch(-1.0, 161, 1.0), FLOOR);
same(pitch(f32::NAN, 161, 1.0), FLOOR);
}
#[test]
fn a_module_sits_inside_its_own_quiet_zone() {
let rect = egui::Rect::from_min_size(egui::pos2(10.0, 20.0), egui::vec2(500.0, 500.0));
let first = module(rect, 0, 0, 4.0);
same(first.min.x, 10.0 + span(QUIET, 4.0));
same(first.min.y, 20.0 + span(QUIET, 4.0));
same(first.width(), 4.0);
same(first.height(), 4.0);
let along = module(rect, 3, 5, 4.0);
same(along.min.x - first.min.x, span(3, 4.0));
same(along.min.y - first.min.y, span(5, 4.0));
}
#[test]
fn every_symbol_s_span_converts_exactly() {
same(span(0, 4.0), 0.0);
same(span(1, 4.0), 4.0);
let widest = Symbol::encode(&[0; 2331]).expect("the ceiling").side() + 2 * QUIET;
assert_eq!(widest, 185);
same(count(widest), 185.0);
same(span(widest, 4.0), 185.0 * 4.0);
}
const ROOM: u32 = 240;
const NUDGE: f32 = 0.3;
fn glass(symbol: &Symbol, per_point: f32) -> image::RgbaImage {
let drawn = symbol.clone();
let mut harness = HarnessBuilder::<Model>::default()
.with_size(egui::Vec2::new(number(ROOM), number(ROOM)))
.with_pixels_per_point(per_point)
.build_state(
move |ctx: &egui::Context, _: &mut Model| {
egui::CentralPanel::default()
.frame(egui::Frame::none().inner_margin(NUDGE))
.show(ctx, |ui| paint(ui, &drawn));
},
Model::default(),
);
harness.run();
TestRenderer::new().render(&harness)
}
fn reads(pixel: image::Rgba<u8>) -> Option<bool> {
if pixel.0[3] != u8::MAX {
return None;
}
let ink = pixel.0.iter().take(3).all(|channel| *channel <= 8);
let paper = pixel.0.iter().take(3).all(|channel| *channel >= 247);
ink.then_some(true).or(paper.then_some(false))
}
fn whole(pitch: f32, per_point: f32) -> u32 {
let wide = pitch * per_point;
(2..=64)
.find(|n| (number(*n) - wide).abs() < f32::EPSILON)
.unwrap_or_else(|| panic!("{wide} device pixels a module is not a whole number"))
}
fn corner(image: &image::RgbaImage) -> (u32, u32) {
let lit: Vec<(u32, u32)> = image
.enumerate_pixels()
.filter(|(_, _, pixel)| reads(**pixel) == Some(true))
.map(|(x, y, _)| (x, y))
.collect();
let least = |axis: fn(&(u32, u32)) -> u32| {
lit.iter()
.map(axis)
.min()
.expect("the symbol put ink on the glass")
};
(least(|at| at.0), least(|at| at.1))
}
#[test]
fn every_module_reads_back_off_the_glass_at_both_device_scales() {
let symbol = Symbol::encode(b"a short one").expect("it fits a symbol");
let across = symbol.side() + 2 * QUIET;
for scale in [1_u32, 2] {
let per_point = number(scale);
let room = number(ROOM) - 2.0 * NUDGE;
let wide = whole(pitch(room, across, per_point), per_point);
let image = glass(&symbol, per_point);
let (x0, y0) = corner(&image);
for y in 0..symbol.side() {
for x in 0..symbol.side() {
let want = symbol.dark(x, y);
let at = |n: usize, from: u32| {
from + u32::try_from(n).expect("a symbol has few modules") * wide
};
for down in 0..wide {
for along in 0..wide {
let (px, py) = (at(x, x0) + along, at(y, y0) + down);
let got = *image.get_pixel(px, py);
assert_eq!(
reads(got),
Some(want),
"module ({x}, {y}) at {scale}x reads {got:?} at ({px}, {py})"
);
}
}
}
}
}
}