use crate::widget::mask_tools::MaskHistory;
pub struct ScatterMaskWidget {
pub mask: Vec<u8>,
pub level: u8,
history: MaskHistory,
}
impl ScatterMaskWidget {
pub fn new(npoints: usize) -> Self {
let mask = vec![0u8; npoints];
let history = MaskHistory::new(&mask);
Self {
mask,
level: 1,
history,
}
}
pub fn len(&self) -> usize {
self.mask.len()
}
pub fn is_empty(&self) -> bool {
self.mask.is_empty()
}
pub fn reset_len(&mut self, npoints: usize) {
self.mask = vec![0u8; npoints];
self.history.reset(&self.mask);
}
pub fn clear(&mut self) {
let level = self.level;
for cell in &mut self.mask {
if *cell == level {
*cell = 0;
}
}
}
pub fn clear_all(&mut self) {
self.mask.fill(0);
}
pub fn invert(&mut self) {
let level = self.level;
for cell in &mut self.mask {
if *cell == 0 {
*cell = level;
} else if *cell == level {
*cell = 0;
}
}
}
pub fn commit(&mut self) {
self.history.commit(&self.mask);
}
pub fn undo(&mut self) -> bool {
if let Some(snapshot) = self.history.undo() {
self.mask = snapshot;
true
} else {
false
}
}
pub fn redo(&mut self) -> bool {
if let Some(snapshot) = self.history.redo() {
self.mask = snapshot;
true
} else {
false
}
}
pub fn reset_history(&mut self) {
self.history.reset(&self.mask);
}
pub fn can_undo(&self) -> bool {
self.history.can_undo()
}
pub fn can_redo(&self) -> bool {
self.history.can_redo()
}
pub fn update_points(&mut self, level: u8, indices: &[usize], mask: bool) {
for &idx in indices {
if idx < self.mask.len() {
if mask {
self.mask[idx] = level;
} else if self.mask[idx] == level {
self.mask[idx] = 0;
}
}
}
}
pub fn update_stencil(&mut self, level: u8, stencil: &[bool], mask: bool) {
for (idx, &selected) in stencil.iter().enumerate() {
if selected && idx < self.mask.len() {
if mask {
self.mask[idx] = level;
} else if self.mask[idx] == level {
self.mask[idx] = 0;
}
}
}
}
pub fn update_disk(
&mut self,
level: u8,
center: (f32, f32),
radius: f32,
x: &[f32],
y: &[f32],
) {
self.update_disk_with_mask(level, center, radius, x, y, true);
}
pub fn update_disk_with_mask(
&mut self,
level: u8,
center: (f32, f32),
radius: f32,
x: &[f32],
y: &[f32],
mask: bool,
) {
let (cx, cy) = center;
let r2 = radius * radius;
let stencil: Vec<bool> = x
.iter()
.zip(y.iter())
.map(|(&px, &py)| {
let dy = py - cy;
let dx = px - cx;
dy * dy + dx * dx < r2
})
.collect();
self.update_stencil(level, &stencil, mask);
}
pub fn update_polygon(
&mut self,
level: u8,
vertices: &[(f32, f32)],
x: &[f32],
y: &[f32],
mask: bool,
) {
let n = x.len().min(y.len());
let indices: Vec<usize> = (0..n)
.filter(|&idx| point_in_polygon(vertices, y[idx], x[idx]))
.collect();
self.update_points(level, &indices, mask);
}
pub fn update_rectangle(
&mut self,
level: u8,
anchor: (f32, f32),
size: (f32, f32),
px: &[f32],
py: &[f32],
mask: bool,
) {
let (y, x) = anchor;
let (height, width) = size;
let vertices = [
(y, x),
(y + height, x),
(y + height, x + width),
(y, x + width),
];
self.update_polygon(level, &vertices, px, py, mask);
}
pub fn update_below_threshold(
&mut self,
level: u8,
values: &[f32],
threshold: f32,
mask: bool,
) {
let stencil: Vec<bool> = values.iter().map(|&v| v < threshold).collect();
self.update_stencil(level, &stencil, mask);
}
pub fn update_between_thresholds(
&mut self,
level: u8,
values: &[f32],
min: f32,
max: f32,
mask: bool,
) {
let stencil: Vec<bool> = values.iter().map(|&v| min <= v && v <= max).collect();
self.update_stencil(level, &stencil, mask);
}
pub fn update_above_threshold(
&mut self,
level: u8,
values: &[f32],
threshold: f32,
mask: bool,
) {
let stencil: Vec<bool> = values.iter().map(|&v| v > threshold).collect();
self.update_stencil(level, &stencil, mask);
}
pub fn mask_not_finite(&mut self, values: &[f32]) {
let stencil: Vec<bool> = values.iter().map(|&v| !v.is_finite()).collect();
self.update_stencil(self.level, &stencil, true);
}
}
pub fn point_in_polygon(vertices: &[(f32, f32)], row: f32, col: f32) -> bool {
let nvert = vertices.len();
if nvert == 0 {
return false;
}
let mut is_inside = false;
let (mut pt1y, mut pt1x) = vertices[nvert - 1];
for &(pt2y, pt2x) in vertices {
if ((pt1y <= row && row < pt2y) || (pt2y <= row && row < pt1y))
&& (col <= pt1x || col <= pt2x)
{
let xinters = (row - pt1y) * (pt2x - pt1x) / (pt2y - pt1y) + pt1x;
if col < xinters {
is_inside = !is_inside;
}
}
pt1y = pt2y;
pt1x = pt2x;
}
is_inside
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disk_selects_points_within_radius() {
let x = [0.0_f32, 1.0, 2.0, 3.0];
let y = [0.0_f32, 0.0, 0.0, 0.0];
let mut m = ScatterMaskWidget::new(4);
m.update_disk(1, (0.0, 0.0), 2.0, &x, &y);
assert_eq!(m.mask, vec![1, 1, 0, 0]);
}
#[test]
fn disk_radius_boundary_is_strict() {
let x = [2.0_f32];
let y = [0.0_f32];
let mut m = ScatterMaskWidget::new(1);
m.update_disk(1, (0.0, 0.0), 2.0, &x, &y);
assert_eq!(m.mask, vec![0]);
}
#[test]
fn polygon_selects_interior_points() {
let square = [(0.0_f32, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0)];
let x = [2.0_f32, 5.0, 1.0];
let y = [2.0_f32, 5.0, 3.0];
let mut m = ScatterMaskWidget::new(3);
m.update_polygon(1, &square, &x, &y, true);
assert_eq!(m.mask, vec![1, 0, 1]);
}
#[test]
fn point_in_polygon_triangle_inside_vs_outside() {
let tri = [(0.0_f32, 0.0), (0.0, 4.0), (4.0, 0.0)];
assert!(point_in_polygon(&tri, 1.0, 1.0));
assert!(!point_in_polygon(&tri, 3.0, 3.0));
}
#[test]
fn rectangle_selects_points_inside() {
let x = [2.0_f32, 0.0, 5.0];
let y = [2.0_f32, 0.0, 5.0];
let mut m = ScatterMaskWidget::new(3);
m.update_rectangle(1, (1.0, 1.0), (2.0, 2.0), &x, &y, true);
assert_eq!(m.mask, vec![1, 0, 0]);
}
#[test]
fn update_points_masks_and_unmasks_by_index() {
let mut m = ScatterMaskWidget::new(4);
m.update_points(1, &[0, 2], true);
assert_eq!(m.mask, vec![1, 0, 1, 0]);
m.mask[2] = 2; m.update_points(1, &[0, 2], false);
assert_eq!(m.mask, vec![0, 0, 2, 0]);
}
#[test]
fn update_points_ignores_out_of_range_indices() {
let mut m = ScatterMaskWidget::new(2);
m.update_points(1, &[0, 5, 99], true);
assert_eq!(m.mask, vec![1, 0]);
}
#[test]
fn clear_only_affects_current_level() {
let mut m = ScatterMaskWidget::new(4);
m.mask = vec![1, 2, 1, 0];
m.level = 1;
m.clear();
assert_eq!(m.mask, vec![0, 2, 0, 0]);
}
#[test]
fn clear_all_resets_every_level() {
let mut m = ScatterMaskWidget::new(4);
m.mask = vec![1, 2, 255, 0];
m.clear_all();
assert_eq!(m.mask, vec![0, 0, 0, 0]);
}
#[test]
fn invert_swaps_zero_and_current_level_only() {
let mut m = ScatterMaskWidget::new(4);
m.mask = vec![0, 1, 2, 0];
m.level = 1;
m.invert();
assert_eq!(m.mask, vec![1, 0, 2, 1]);
}
#[test]
fn undo_then_redo_round_trips_one_change() {
let mut m = ScatterMaskWidget::new(4);
m.mask = vec![1, 0, 0, 0];
m.commit();
assert!(m.can_undo());
assert!(m.undo());
assert_eq!(m.mask, vec![0, 0, 0, 0]); assert!(!m.can_undo());
assert!(m.can_redo());
assert!(m.redo());
assert_eq!(m.mask, vec![1, 0, 0, 0]);
assert!(!m.can_redo());
}
#[test]
fn undo_is_noop_with_only_baseline() {
let mut m = ScatterMaskWidget::new(4);
assert!(!m.can_undo());
assert!(!m.undo());
}
#[test]
fn threshold_below_is_strict() {
let mut m = ScatterMaskWidget::new(4);
let values = [0.0_f32, 1.0, 2.0, 3.0];
m.update_below_threshold(1, &values, 2.0, true);
assert_eq!(m.mask, vec![1, 1, 0, 0]);
}
#[test]
fn threshold_between_is_inclusive() {
let mut m = ScatterMaskWidget::new(5);
let values = [0.0_f32, 1.0, 2.0, 3.0, 4.0];
m.update_between_thresholds(1, &values, 1.0, 3.0, true);
assert_eq!(m.mask, vec![0, 1, 1, 1, 0]);
}
#[test]
fn threshold_above_is_strict() {
let mut m = ScatterMaskWidget::new(4);
let values = [0.0_f32, 1.0, 2.0, 3.0];
m.update_above_threshold(1, &values, 2.0, true);
assert_eq!(m.mask, vec![0, 0, 0, 1]);
}
#[test]
fn mask_not_finite_masks_nan_and_infinities_only() {
let mut m = ScatterMaskWidget::new(5);
m.level = 1;
let values = [0.0_f32, f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 7.0];
m.mask_not_finite(&values);
assert_eq!(m.mask, vec![0, 1, 1, 1, 0]);
}
#[test]
fn empty_mask_selection_is_noop() {
let mut m = ScatterMaskWidget::new(0);
assert!(m.is_empty());
m.update_disk(1, (0.0, 0.0), 1.0, &[], &[]);
m.update_polygon(1, &[(0.0, 0.0), (1.0, 1.0), (0.0, 1.0)], &[], &[], true);
m.mask_not_finite(&[]);
assert!(m.mask.is_empty());
}
#[test]
fn point_in_polygon_empty_vertices_is_false() {
assert!(!point_in_polygon(&[], 0.0, 0.0));
}
}