use crate::color::Rgba;
use crate::frame::FrameView;
use crate::geometry::Px;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Direction {
Left,
Right,
Up,
Down,
}
impl Direction {
pub const ALL: [Direction; 4] = [
Direction::Left,
Direction::Right,
Direction::Up,
Direction::Down,
];
fn step(self) -> (i32, i32) {
match self {
Direction::Left => (-1, 0),
Direction::Right => (1, 0),
Direction::Up => (0, -1),
Direction::Down => (0, 1),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Tolerance(pub u32);
impl Tolerance {
pub const DEFAULT: Tolerance = Tolerance(30);
pub const STRICT: Tolerance = Tolerance(8);
pub const LOOSE: Tolerance = Tolerance(90);
}
impl Default for Tolerance {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct EdgeCandidate {
pub direction: Direction,
pub distance: u32,
pub position: Px,
pub anchor_color: Rgba,
pub edge_color: Rgba,
pub strength: u32,
}
pub type EdgeQuad = [Option<EdgeCandidate>; 4];
pub fn detect_edges(frame: &FrameView, cursor: Px, tolerance: Tolerance) -> EdgeQuad {
let Some(anchor) = pixel_for_cursor(frame, cursor) else {
return [None, None, None, None];
};
[
scan(frame, cursor, Direction::Left, anchor, tolerance),
scan(frame, cursor, Direction::Right, anchor, tolerance),
scan(frame, cursor, Direction::Up, anchor, tolerance),
scan(frame, cursor, Direction::Down, anchor, tolerance),
]
}
fn pixel_for_cursor(frame: &FrameView, cursor: Px) -> Option<Rgba> {
if cursor.x < 0 || cursor.y < 0 {
return None;
}
frame.pixel(cursor.x as u32, cursor.y as u32)
}
fn scan(
frame: &FrameView,
cursor: Px,
dir: Direction,
anchor: Rgba,
tol: Tolerance,
) -> Option<EdgeCandidate> {
let (dx, dy) = dir.step();
let mut x = cursor.x;
let mut y = cursor.y;
let mut dist = 0u32;
loop {
x += dx;
y += dy;
dist += 1;
if x < 0 || y < 0 {
return None;
}
let Some(here) = frame.pixel(x as u32, y as u32) else {
return None;
};
let delta = anchor.rgb_delta(here);
if delta > tol.0 {
return Some(EdgeCandidate {
direction: dir,
distance: dist,
position: Px { x, y },
anchor_color: anchor,
edge_color: here,
strength: delta,
});
}
}
}
pub fn shrink_to_content(
frame: &FrameView,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
tolerance: Tolerance,
) -> (i32, i32, i32, i32) {
let bg_x = x0.min(x1).max(0).min(frame.width as i32 - 1);
let bg_y = y0.min(y1).max(0).min(frame.height as i32 - 1);
shrink_to_content_with_bg(frame, x0, y0, x1, y1, bg_x, bg_y, tolerance)
}
pub fn shrink_to_content_with_bg(
frame: &FrameView,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
bg_x: i32,
bg_y: i32,
tolerance: Tolerance,
) -> (i32, i32, i32, i32) {
let (rx0, rx1) = (x0.min(x1), x0.max(x1));
let (ry0, ry1) = (y0.min(y1), y0.max(y1));
let fw = frame.width as i32;
let fh = frame.height as i32;
let cx0 = rx0.max(0).min(fw - 1);
let cy0 = ry0.max(0).min(fh - 1);
let cx1 = rx1.max(0).min(fw - 1);
let cy1 = ry1.max(0).min(fh - 1);
if cx1 <= cx0 || cy1 <= cy0 {
return (x0, y0, x1, y1);
}
let bx = bg_x.max(0).min(fw - 1);
let by = bg_y.max(0).min(fh - 1);
let bg = match frame.pixel(bx as u32, by as u32) {
Some(p) => p,
None => return (x0, y0, x1, y1),
};
let tol = tolerance.0;
let row_has_content = |y: i32, x_start: i32, x_end: i32| -> bool {
for x in x_start..=x_end {
if let Some(p) = frame.pixel(x as u32, y as u32) {
if bg.rgb_delta(p) > tol {
return true;
}
}
}
false
};
let col_has_content = |x: i32, y_start: i32, y_end: i32| -> bool {
for y in y_start..=y_end {
if let Some(p) = frame.pixel(x as u32, y as u32) {
if bg.rgb_delta(p) > tol {
return true;
}
}
}
false
};
let mut new_top = cy0;
for y in cy0..=cy1 {
if row_has_content(y, cx0, cx1) {
new_top = y;
break;
}
}
let mut new_bot = cy1;
for y in (new_top..=cy1).rev() {
if row_has_content(y, cx0, cx1) {
new_bot = y;
break;
}
}
let mut new_left = cx0;
for x in cx0..=cx1 {
if col_has_content(x, new_top, new_bot) {
new_left = x;
break;
}
}
let mut new_right = cx1;
for x in (new_left..=cx1).rev() {
if col_has_content(x, new_top, new_bot) {
new_right = x;
break;
}
}
if new_right <= new_left || new_bot <= new_top {
return (x0, y0, x1, y1);
}
(new_left, new_top, new_right, new_bot)
}
#[cfg(test)]
mod tests {
use super::*;
fn solid(width: u32, height: u32, bg: Rgba) -> Vec<u8> {
let mut v = Vec::with_capacity((width * height * 4) as usize);
for _ in 0..(width * height) {
v.extend_from_slice(&[bg.r, bg.g, bg.b, bg.a]);
}
v
}
fn put(buf: &mut [u8], width: u32, x: u32, y: u32, c: Rgba) {
let i = ((y * width + x) * 4) as usize;
buf[i..i + 4].copy_from_slice(&[c.r, c.g, c.b, c.a]);
}
#[test]
fn solid_frame_has_no_edges() {
let buf = solid(16, 16, Rgba::WHITE);
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(&frame, Px::new(8, 8), Tolerance::DEFAULT);
assert!(edges.iter().all(|e| e.is_none()));
}
#[test]
fn cursor_off_frame_returns_none() {
let buf = solid(16, 16, Rgba::WHITE);
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(&frame, Px::new(99, 99), Tolerance::DEFAULT);
assert!(edges.iter().all(|e| e.is_none()));
}
#[test]
fn detects_edge_in_each_direction() {
let mut buf = solid(16, 16, Rgba::WHITE);
for y in 0..16 {
put(&mut buf, 16, 11, y, Rgba::BLACK);
}
for x in 0..16 {
put(&mut buf, 16, x, 3, Rgba::BLACK);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(&frame, Px::new(8, 8), Tolerance::DEFAULT);
let right = edges[1].expect("right edge");
assert_eq!(right.direction, Direction::Right);
assert_eq!(right.distance, 3);
assert_eq!(right.position, Px::new(11, 8));
assert_eq!(right.edge_color, Rgba::BLACK);
let up = edges[2].expect("up edge");
assert_eq!(up.direction, Direction::Up);
assert_eq!(up.distance, 5);
assert_eq!(up.position, Px::new(8, 3));
assert!(edges[0].is_none(), "left should run off frame");
assert!(edges[3].is_none(), "down should run off frame");
}
#[test]
fn returns_nearest_when_multiple_edges_present() {
let mut buf = solid(16, 16, Rgba::WHITE);
for y in 0..16 {
put(&mut buf, 16, 10, y, Rgba::BLACK);
put(&mut buf, 16, 14, y, Rgba::BLACK);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(&frame, Px::new(8, 8), Tolerance::DEFAULT);
let right = edges[1].expect("right");
assert_eq!(right.distance, 2);
assert_eq!(right.position, Px::new(10, 8));
}
#[test]
fn anti_aliased_edge_catches_first_transition() {
let mut buf = solid(16, 16, Rgba::WHITE);
let gray = Rgba::new(180, 180, 180, 255);
for y in 0..16 {
put(&mut buf, 16, 9, y, gray);
put(&mut buf, 16, 10, y, Rgba::BLACK);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(&frame, Px::new(7, 8), Tolerance::DEFAULT);
let right = edges[1].expect("right");
assert_eq!(right.position, Px::new(9, 8));
assert_eq!(right.edge_color, gray);
}
#[test]
fn strict_tolerance_skips_subtle_changes() {
let mut buf = solid(16, 16, Rgba::new(200, 200, 200, 255));
let near = Rgba::new(196, 196, 196, 255); for y in 0..16 {
put(&mut buf, 16, 12, y, near);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
assert!(detect_edges(&frame, Px::new(8, 8), Tolerance::DEFAULT)[1].is_none());
let edges = detect_edges(&frame, Px::new(8, 8), Tolerance::STRICT);
assert_eq!(edges[1].expect("strict right").position, Px::new(12, 8));
}
#[test]
fn shrink_fits_inner_content() {
let mut buf = solid(32, 32, Rgba::WHITE);
for y in 14..22 {
for x in 12..20 {
put(&mut buf, 32, x, y, Rgba::BLACK);
}
}
let frame = FrameView::packed(&buf, 32, 32).unwrap();
let (x0, y0, x1, y1) = shrink_to_content(&frame, 5, 5, 28, 28, Tolerance::DEFAULT);
assert_eq!((x0, y0, x1, y1), (12, 14, 19, 21));
}
#[test]
fn shrink_returns_original_on_uniform_content() {
let buf = solid(16, 16, Rgba::WHITE);
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let r = shrink_to_content(&frame, 2, 2, 14, 14, Tolerance::DEFAULT);
assert_eq!(r, (2, 2, 14, 14));
}
#[test]
fn shrink_handles_out_of_bounds_rect() {
let buf = solid(16, 16, Rgba::WHITE);
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let r = shrink_to_content(&frame, -10, -10, 100, 100, Tolerance::DEFAULT);
assert_eq!(r, (0, 0, 15, 15));
}
#[test]
fn ignores_alpha_channel() {
let mut buf = solid(16, 16, Rgba::new(120, 120, 120, 255));
let translucent_same = Rgba::new(120, 120, 120, 50);
for y in 0..16 {
put(&mut buf, 16, 11, y, translucent_same);
}
let frame = FrameView::packed(&buf, 16, 16).unwrap();
let edges = detect_edges(&frame, Px::new(8, 8), Tolerance::DEFAULT);
assert!(edges[1].is_none());
}
}