use super::bands::strongest_cut;
use super::cells::{group_and_label, DisjointSet};
use super::crops::native_edge;
use super::*;
#[test]
fn disjoint_set_unions_by_size_and_compresses_paths() {
let mut ds = DisjointSet::new(5);
ds.join(0, 1);
ds.join(1, 2);
ds.join(3, 4);
assert_eq!(ds.find(0), ds.find(2));
assert_ne!(ds.find(0), ds.find(3));
ds.join(2, 3);
assert_eq!(ds.find(0), ds.find(4));
}
#[test]
fn strongest_cut_picks_first_maximum_above_threshold() {
let g = [0.0, 0.0, 0.5, 1.3, 1.3, 0.0, 0.0, 0.0];
assert_eq!(strongest_cut(&g, 10.0), 3);
assert_eq!(strongest_cut(&g, 3.0), 0);
}
#[test]
fn group_and_label_keeps_map_insertion_order_and_grows_groups_during_the_fill_scan() {
let cols = 2;
let n = 6;
let mut ds = DisjointSet::new(n);
ds.join(0, 1);
ds.join(1, 2);
ds.join(2, 3);
let (large, labels) = group_and_label(&mut ds, n as i64, cols);
assert_eq!(
large.len(),
1,
"cells 4 and 5 fall below the minimum and must fold into the sole large group"
);
assert_eq!(labels, [0, 0, 0, 0, 0, 0]);
assert_eq!(large[0].len(), 6);
}
#[test]
fn native_edge_falls_back_to_the_scaled_estimate_without_native_stats() {
assert_eq!(native_edge(None, 10, 2.0, 1, 100, 0.9, 0.0), 20);
assert_eq!(native_edge(None, 10, 2.0, -1, 100, 0.9, 5.0), 20);
}
#[test]
fn label_atlas_first_region_wins_on_overlap_and_counts_match_the_labels() {
use super::atlas::AtlasRegion;
let a = AtlasRegion {
rect: Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
exclusions: vec![],
crop: None,
solid: true,
mask: None,
};
let b = AtlasRegion {
rect: Rect {
x: 5.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
exclusions: vec![],
crop: None,
solid: true,
mask: None,
};
let mut out = vec![0u8; 15 * 10];
let counts = label_atlas(&[a, b], 15, 10, &mut out).unwrap();
assert_eq!(out[5], 1);
assert_eq!(out[12], 2);
assert_eq!(out[0], 1);
assert_eq!(counts.len(), 3);
assert_eq!(counts[1] as usize, out.iter().filter(|&&c| c == 1).count());
assert_eq!(counts[2] as usize, out.iter().filter(|&&c| c == 2).count());
}
#[test]
fn finish_falls_back_to_one_solid_pane_with_no_evidence() {
let width = 4usize;
let height = 4usize;
let cell = 24usize; let zeros = vec![0.0; 4];
let input = FinishInput {
width,
height,
cell,
informative_frames: 0.0,
native_frames: 0.0,
row_change: &zeros,
col_change: &zeros,
col_moving: &zeros,
col_mean: &zeros,
col_gain: &zeros,
horizontal_gain: &zeros,
split: &[0.0, 0.0],
evidence: &[0.0, 0.0],
activity: &[0.0],
observations: &[0.0],
pixel_fixed: &zeros,
pixel_moving: &zeros,
native_row_change: None,
native_col_change: None,
row_fixed: None,
row_moving: None,
reference: None,
};
let regions = finish(&input, 96.0, 96.0, 24);
assert_eq!(regions.len(), 1);
assert_eq!(regions[0].kind, Kind::Moving);
assert!(regions[0].solid);
assert_eq!(
regions[0].rect,
Rect {
x: 0.0,
y: 0.0,
width: 96.0,
height: 96.0
}
);
}
fn learn(width: usize, height: usize, frames: &[Vec<u8>]) -> Vec<RegionOut> {
use crate::kernels::layers::{Field, FieldMotion, Learner};
let mut learner = Learner::new(width, height);
let motions = [FieldMotion {
x: 0.0,
y: 4.0,
support: 100,
confidence: 1.0,
}];
let cells = learner.cols * learner.rows;
let (labels, confidence, dynamic) = (vec![0u8; cells], vec![255u8; cells], vec![0u8; cells]);
let field = Field {
motions: &motions,
labels: &labels,
confidence: &confidence,
dynamic: &dynamic,
cols: learner.cols,
rows: learner.rows,
difference: 1.0,
unknown: false,
};
for pair in frames.windows(2) {
assert!(learner.add(&field, &pair[0], &pair[1], None));
}
let input = FinishInput {
width,
height,
cell: learner.cell,
informative_frames: learner.informative_frames as f64,
native_frames: 0.0,
row_change: &learner.row_change,
col_change: &learner.col_change,
col_moving: &learner.col_moving,
col_mean: &learner.col_mean,
col_gain: &learner.col_gain,
horizontal_gain: &learner.horizontal_gain,
split: &learner.split,
evidence: &learner.evidence,
activity: &learner.activity,
observations: &learner.observations,
pixel_fixed: &learner.pixel_fixed,
pixel_moving: &learner.pixel_moving,
native_row_change: None,
native_col_change: None,
row_fixed: None,
row_moving: None,
reference: None,
};
finish(&input, width as f64, height as f64, 1)
}
fn sidebar_frames(
count: usize,
bounce: &[usize],
sparse_right: Option<&dyn Fn(usize) -> bool>,
) -> Vec<Vec<u8>> {
let (w, h) = (120usize, 72usize);
(0..count)
.map(|f| {
let shift = if bounce.contains(&f) { 4 } else { 0 };
let mut g = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
let v = if x < 24 {
let sy = y + 80 - shift;
if (sy / 6) % 2 == 0 && x % 8 < 5 {
200 - x as i32 * 4
} else {
30
}
} else if x >= 96 && sparse_right.is_some() {
let sy = y + f * 4;
if sparse_right.unwrap()(f) && sy % 36 < 2 {
50
} else {
30
}
} else {
let sy = y + f * 4 + 80 - shift;
(((x * 7) ^ (sy * 13)) % 97 + 60) as i32
};
g[y * w + x] = v as u8;
}
}
g
})
.collect()
}
#[test]
fn a_sidebar_still_but_for_a_few_bounces_is_a_fixed_band() {
for bounce in [&[][..], &[10, 11, 30, 31][..]] {
let regions = learn(120, 72, &sidebar_frames(41, bounce, None));
let fixed: Vec<_> = regions
.iter()
.filter(|r| r.kind == Kind::Fixed)
.map(|r| r.rect)
.collect();
assert!(
fixed.iter().any(|r| r.x == 0.0 && r.width == 24.0),
"bounce at {bounce:?}: expected a fixed 24-px left band, got {fixed:?}"
);
}
}
#[test]
fn a_sparse_pane_that_moves_in_most_frames_is_not_a_band() {
let present = |f: usize| !f.is_multiple_of(4);
let frames = sidebar_frames(41, &[], Some(&present));
let regions = learn(120, 72, &frames);
let fixed_right: Vec<_> = regions
.iter()
.filter(|r| r.kind == Kind::Fixed && r.rect.x + r.rect.width == 120.0 && r.rect.x > 0.0)
.map(|r| r.rect)
.collect();
assert!(
fixed_right.is_empty(),
"the moving pane was frozen as a right band: {fixed_right:?}"
);
}
fn wide_margin_bands(sidebar_text: bool) -> super::bands::Bands {
let (w, h) = (100usize, 60usize);
let mut rgba = vec![255u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let v = if x < 20 {
if sidebar_text && y % 6 == 0 && (3..17).contains(&x) {
200
} else {
60
}
} else if x < 60 {
10
} else if (y + x) % 7 < 3 {
230
} else {
40
};
rgba[(y * w + x) * 4..(y * w + x) * 4 + 3].fill(v);
}
}
let reference = Rgba {
width: w,
height: h,
data: &rgba,
};
let frames = 10.0;
let row_change = vec![frames; h];
let col_moving: Vec<f64> = (0..w).map(|x| if x < 60 { 0.0 } else { frames }).collect();
let col_mean = vec![0.0; w];
super::bands::detect_bands(
w as i64,
h as i64,
frames,
&row_change,
None,
(&[], &[]),
&col_moving,
&col_mean,
Some(&reference),
w as f64,
h as f64,
1.0,
)
}
#[test]
fn a_sidebar_beside_an_empty_margin_wider_than_the_cap_is_a_left_band() {
let bands = wide_margin_bands(true);
assert_eq!(bands.left, 20);
assert_eq!(bands.exact_left, Some(20.0));
}
#[test]
fn a_blank_strip_beside_an_empty_margin_is_not_a_left_band() {
let bands = wide_margin_bands(false);
assert_eq!(bands.left, 0);
assert_eq!(bands.exact_left, None);
}
type FrameField = (Vec<(f64, f64)>, Vec<u8>);
fn learn_fields(
width: usize,
height: usize,
frames: &[Vec<u8>],
field: &dyn Fn(usize, usize, usize) -> FrameField,
) -> Vec<RegionOut> {
learn_fields_seeing(width, height, frames, field, false)
}
fn learn_fields_seeing(
width: usize,
height: usize,
frames: &[Vec<u8>],
field: &dyn Fn(usize, usize, usize) -> FrameField,
with_reference: bool,
) -> Vec<RegionOut> {
use crate::kernels::layers::{Field, FieldMotion, Learner};
let mut learner = Learner::new(width, height);
let (cols, rows) = (learner.cols, learner.rows);
let cells = cols * rows;
let (confidence, dynamic) = (vec![255u8; cells], vec![0u8; cells]);
for (f, pair) in frames.windows(2).enumerate() {
let (models, labels) = field(f + 1, cols, rows);
let motions: Vec<FieldMotion> = models
.iter()
.map(|&(x, y)| FieldMotion {
x,
y,
support: 100,
confidence: 1.0,
})
.collect();
let field = Field {
motions: &motions,
labels: &labels,
confidence: &confidence,
dynamic: &dynamic,
cols,
rows,
difference: 1.0,
unknown: false,
};
assert!(learner.add(&field, &pair[0], &pair[1], None));
}
let rgba: Vec<u8> = frames
.last()
.unwrap()
.iter()
.flat_map(|&v| [v, v, v, 255])
.collect();
let input = FinishInput {
width,
height,
cell: learner.cell,
informative_frames: learner.informative_frames as f64,
native_frames: 0.0,
row_change: &learner.row_change,
col_change: &learner.col_change,
col_moving: &learner.col_moving,
col_mean: &learner.col_mean,
col_gain: &learner.col_gain,
horizontal_gain: &learner.horizontal_gain,
split: &learner.split,
evidence: &learner.evidence,
activity: &learner.activity,
observations: &learner.observations,
pixel_fixed: &learner.pixel_fixed,
pixel_moving: &learner.pixel_moving,
native_row_change: None,
native_col_change: None,
reference: with_reference.then(|| Rgba {
width,
height,
data: &rgba,
}),
row_fixed: None,
row_moving: None,
};
finish(&input, width as f64, height as f64, 1)
}
fn texture(x: usize, y: usize) -> u8 {
(((x * 7) ^ (y * 13)) % 97 + 60) as u8
}
#[test]
fn a_bar_that_stays_put_while_its_pixels_flicker_is_fixed_ui() {
for with_reference in [false, true] {
let (moving, fixed) = flickering_bar(&|x, y| texture(x + 500, y), with_reference);
assert_eq!(
moving.len(),
1,
"expected one scroll layer, got {moving:?} (fixed {fixed:?})"
);
assert!(
fixed
.iter()
.any(|r| r.y == 0.0 && r.height == 48.0 && r.width == 96.0),
"expected the bar as fixed UI, got fixed {fixed:?}, moving {moving:?}"
);
}
}
#[test]
fn a_blank_area_whose_only_structure_is_one_line_is_not_fixed_ui() {
let line = |_: usize, y: usize| if y == 20 { 200 } else { 0 };
let (moving, fixed) = flickering_bar(&line, true);
assert!(
fixed.is_empty() && moving.len() == 2,
"expected the blank area to stay a moving region, got moving {moving:?}, fixed {fixed:?}"
);
let (_, fixed) = flickering_bar(&line, false);
assert_eq!(fixed.len(), 1, "{fixed:?}");
}
fn flickering_bar(
bar_paint: &dyn Fn(usize, usize) -> u8,
with_reference: bool,
) -> (Vec<Rect>, Vec<Rect>) {
let (w, h, bar) = (96usize, 144usize, 48usize);
let frames: Vec<Vec<u8>> = (0..41)
.map(|f| {
let mut g = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
g[y * w + x] = if y < bar {
bar_paint(x, y) + if f % 2 == 0 { 2 } else { 0 }
} else {
texture(x, y + f * 4)
};
}
}
g
})
.collect();
let regions = learn_fields_seeing(
w,
h,
&frames,
&|f, cols, rows| {
let mut labels = vec![0u8; cols * rows];
if f % 2 == 1 {
labels[..cols * (bar / 24)].fill(1);
}
let _ = rows;
(vec![(0.0, 4.0), (0.0, 0.0)], labels)
},
with_reference,
);
let moving: Vec<_> = regions
.iter()
.filter(|r| r.kind == Kind::Moving)
.map(|r| r.rect)
.collect();
let fixed: Vec<_> = regions
.iter()
.filter(|r| r.kind == Kind::Fixed)
.map(|r| r.rect)
.collect();
(moving, fixed)
}
#[test]
fn a_second_pane_that_mostly_scrolls_stays_a_scroll_layer() {
let (w, h, split) = (144usize, 144usize, 96usize);
let right_moves = |f: usize| f > 12;
let frames: Vec<Vec<u8>> = (0..41)
.map(|f| {
let (left_offset, right_offset) = if right_moves(f) {
(12 * 4, (f - 12) * 4)
} else {
(f * 4, 0)
};
let mut g = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
g[y * w + x] = if x < split {
texture(x, y + left_offset)
} else {
texture(x + 300, y + right_offset)
};
}
}
g
})
.collect();
let regions = learn_fields(w, h, &frames, &|f, cols, rows| {
let mut labels = vec![0u8; cols * rows];
for y in 0..rows {
for x in 0..cols {
let right = x * 24 >= split;
labels[y * cols + x] = if right == right_moves(f) { 0 } else { 1 };
}
}
(vec![(0.0, 4.0), (0.0, 0.0)], labels)
});
let moving: Vec<_> = regions
.iter()
.filter(|r| r.kind == Kind::Moving)
.map(|r| r.rect)
.collect();
assert_eq!(
moving.len(),
2,
"expected both panes as scroll layers, got {moving:?}"
);
}
fn glass_bands(chrome: usize, fixed: f64, moving: f64, still_rows: usize) -> super::bands::Bands {
glass_bands_over(chrome, fixed, moving, still_rows, &[])
}
fn glass_bands_over(
chrome: usize,
fixed: f64,
moving: f64,
still_rows: usize,
objects: &[(usize, usize, usize)],
) -> super::bands::Bands {
let (w, h) = (204usize, 300usize);
let frames = 10.0;
let samples = ((w - 4).div_ceil(4)) as f64;
let row_change: Vec<f64> = (0..h)
.map(|y| if y < still_rows { 0.0 } else { 5.0 * frames })
.collect();
let row_fixed: Vec<f64> = (0..h)
.map(|y| frames * if y < chrome { fixed } else { 0.02 * samples })
.collect();
let row_moving: Vec<f64> = (0..h)
.map(|y| frames * if y < chrome { moving } else { 0.3 * samples })
.collect();
let col_moving = vec![frames; w];
let col_mean = vec![0.0; w];
let mut pixel_fixed = vec![0.0; w * h];
let pixel_moving = vec![0.0; w * h];
for &(from, to, samples) in objects {
for y in from..to {
for k in 0..samples {
pixel_fixed[y * w + 2 + 4 * k] = frames;
}
}
}
super::bands::detect_bands(
w as i64,
h as i64,
frames,
&row_change,
Some((&row_fixed, &row_moving)),
(&pixel_fixed, &pixel_moving),
&col_moving,
&col_mean,
None,
w as f64,
h as f64,
1.0,
)
}
#[test]
fn a_glass_bar_whose_icons_stand_still_is_a_top_band() {
let bands = glass_bands(40, 15.0, 2.0, 0);
assert_eq!((bands.top, bands.bottom), (40, 300));
}
#[test]
fn rows_that_mostly_follow_the_page_are_not_a_band() {
let bands = glass_bands(40, 15.0, 12.0, 0);
assert_eq!(bands.top, 0);
}
#[test]
fn a_handful_of_still_pixels_in_a_bare_row_is_not_a_band() {
let bands = glass_bands(40, 1.0, 0.0, 0);
assert_eq!(bands.top, 0);
}
#[test]
fn a_fade_over_the_page_without_icons_is_not_a_band() {
let bands = glass_bands(40, 5.0, 1.0, 0);
assert_eq!(bands.top, 0);
}
#[test]
fn a_side_the_change_test_already_bounds_keeps_its_edge() {
let bands = glass_bands(40, 15.0, 2.0, 12);
assert_eq!(bands.top, 12);
}
#[test]
fn objects_floating_right_under_a_glass_band_belong_to_it() {
let bands = glass_bands_over(40, 15.0, 2.0, 0, &[(40, 110, 8)]);
assert_eq!(bands.top, 110);
}
#[test]
fn a_short_gap_between_the_objects_does_not_end_the_band() {
let bands = glass_bands_over(40, 15.0, 2.0, 0, &[(40, 70, 8), (85, 110, 8)]);
assert_eq!(bands.top, 110);
}
#[test]
fn a_float_button_past_a_stretch_of_page_stays_out_of_the_band() {
let bands = glass_bands_over(40, 15.0, 2.0, 0, &[(40, 105, 8), (130, 134, 8)]);
assert_eq!(bands.top, 105);
}
#[test]
fn a_bar_soft_edge_of_a_few_rows_does_not_move_the_band() {
let bands = glass_bands_over(40, 15.0, 2.0, 0, &[(40, 75, 40)]);
assert_eq!(bands.top, 40);
}
#[test]
fn a_few_still_pixels_in_a_row_do_not_extend_the_band() {
let bands = glass_bands_over(40, 15.0, 2.0, 0, &[(40, 110, 4)]);
assert_eq!(bands.top, 40);
}
#[test]
fn a_band_found_by_the_change_test_is_not_extended() {
let bands = glass_bands_over(40, 15.0, 2.0, 12, &[(12, 110, 8)]);
assert_eq!(bands.top, 12);
}
#[test]
fn still_pixels_down_the_whole_screen_do_not_extend_the_band() {
let bands = glass_bands_over(40, 15.0, 2.0, 0, &[(0, 300, 8)]);
assert_eq!((bands.top, bands.bottom), (40, 300));
}