use crate::config::{RoiHint, RoiKind};
#[derive(Debug, Clone)]
pub struct TileMask {
bits: Box<[bool]>,
}
impl TileMask {
pub fn empty(len: usize) -> Self {
Self {
bits: vec![false; len].into_boxed_slice(),
}
}
#[inline]
pub fn get(&self, idx: usize) -> bool {
self.bits.get(idx).copied().unwrap_or(false)
}
#[inline]
pub fn set(&mut self, idx: usize) {
if let Some(b) = self.bits.get_mut(idx) {
*b = true;
}
}
pub fn any(&self) -> bool {
self.bits.iter().any(|b| *b)
}
pub fn len(&self) -> usize {
self.bits.len()
}
pub fn is_empty(&self) -> bool {
self.bits.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct Region {
pub label: String,
pub kind: RoiKind,
pub tiles: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct RoiSet {
cols: u16,
rows: u16,
regions: Vec<Region>,
ignore: TileMask,
excluded: TileMask,
watch: TileMask,
}
impl RoiSet {
pub fn build(hints: &[RoiHint], cols: u16, rows: u16) -> Self {
let len = cols as usize * rows as usize;
let mut ignore = TileMask::empty(len);
let mut excluded = TileMask::empty(len);
let mut watch = TileMask::empty(len);
let mut regions = Vec::with_capacity(hints.len());
for h in hints {
let tiles = tiles_for_rect(h.rect_norm, cols, rows);
for &t in &tiles {
match h.kind {
RoiKind::Ignore => ignore.set(t),
RoiKind::Spinner | RoiKind::Volatile => excluded.set(t),
RoiKind::Watch => watch.set(t),
}
}
regions.push(Region {
label: h.label.clone(),
kind: h.kind,
tiles,
});
}
Self {
cols,
rows,
regions,
ignore,
excluded,
watch,
}
}
pub fn cols(&self) -> u16 {
self.cols
}
pub fn rows(&self) -> u16 {
self.rows
}
pub fn regions(&self) -> &[Region] {
&self.regions
}
pub fn ignore_mask(&self) -> &TileMask {
&self.ignore
}
#[inline]
pub fn is_excluded(&self, idx: usize) -> bool {
self.excluded.get(idx)
}
#[inline]
pub fn is_watch(&self, idx: usize) -> bool {
self.watch.get(idx)
}
pub fn region_indices_of_kind(&self, kind: RoiKind) -> impl Iterator<Item = usize> + '_ {
self.regions
.iter()
.enumerate()
.filter(move |(_, r)| r.kind == kind)
.map(|(i, _)| i)
}
}
pub fn tiles_for_rect(rect_norm: [f32; 4], cols: u16, rows: u16) -> Vec<usize> {
let [x, y, w, h] = rect_norm;
let cols_f = cols as f32;
let rows_f = rows as f32;
let c0 = (x * cols_f).floor().clamp(0.0, cols_f) as i64;
let c1 = ((x + w) * cols_f).ceil().clamp(0.0, cols_f) as i64;
let r0 = (y * rows_f).floor().clamp(0.0, rows_f) as i64;
let r1 = ((y + h) * rows_f).ceil().clamp(0.0, rows_f) as i64;
let mut out = Vec::new();
for r in r0..r1.max(r0) {
for c in c0..c1.max(c0) {
out.push((r as usize) * cols as usize + c as usize);
}
}
if out.is_empty() && w > 0.0 && h > 0.0 {
let c = ((x * cols_f) as i64).clamp(0, cols as i64 - 1) as usize;
let r = ((y * rows_f) as i64).clamp(0, rows as i64 - 1) as usize;
out.push(r * cols as usize + c);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tile_mask_set_get_any_len() {
let mut m = TileMask::empty(4);
assert!(!m.any());
assert_eq!(m.len(), 4);
assert!(!m.is_empty());
m.set(2);
m.set(99); assert!(m.get(2));
assert!(!m.get(0));
assert!(!m.get(99));
assert!(m.any());
assert!(TileMask::empty(0).is_empty());
}
#[test]
fn tiles_for_rect_covers_expected_cells() {
let tiles = tiles_for_rect([0.0, 0.0, 0.5, 0.5], 10, 10);
assert_eq!(tiles.len(), 25);
assert!(tiles.contains(&0));
assert!(!tiles.contains(&9)); }
#[test]
fn tiles_for_rect_tiny_region_gets_one_tile() {
let tiles = tiles_for_rect([0.5, 0.5, 0.001, 0.001], 4, 4);
assert_eq!(tiles.len(), 1);
assert!(tiles_for_rect([0.5, 0.5, 0.0, 0.0], 4, 4).is_empty());
}
#[test]
fn roiset_builds_masks_and_regions() {
let hints = vec![
RoiHint {
kind: RoiKind::Ignore,
label: "clock".into(),
rect_norm: [0.0, 0.0, 0.25, 0.25],
},
RoiHint {
kind: RoiKind::Spinner,
label: "spin".into(),
rect_norm: [0.5, 0.5, 0.25, 0.25],
},
RoiHint {
kind: RoiKind::Watch,
label: "watch".into(),
rect_norm: [0.75, 0.0, 0.25, 0.25],
},
RoiHint {
kind: RoiKind::Volatile,
label: "vol".into(),
rect_norm: [0.0, 0.75, 0.25, 0.25],
},
];
let set = RoiSet::build(&hints, 4, 4);
assert_eq!((set.cols(), set.rows()), (4, 4));
assert_eq!(set.regions().len(), 4);
assert!(set.ignore_mask().any());
assert!(set.ignore_mask().get(0));
assert!(set.is_excluded(10)); assert!(set.is_watch(3)); let spinners: Vec<usize> = set.region_indices_of_kind(RoiKind::Spinner).collect();
assert_eq!(spinners, vec![1]);
}
}