use crate::format::Extent2D;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Scissor {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl Scissor {
pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub const fn covering(extent: Extent2D) -> Self {
Self::new(0, 0, extent.width, extent.height)
}
pub const EMPTY: Self = Self::new(0, 0, 0, 0);
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
pub const fn right(self) -> u32 {
self.x + self.width
}
pub const fn bottom(self) -> u32 {
self.y + self.height
}
pub fn intersect(self, other: Self) -> Self {
let x = self.x.max(other.x);
let y = self.y.max(other.y);
let right = self.right().min(other.right());
let bottom = self.bottom().min(other.bottom());
if right <= x || bottom <= y {
return Self::EMPTY;
}
Self::new(x, y, right - x, bottom - y)
}
pub fn covers(self, extent: Extent2D) -> bool {
self.x == 0 && self.y == 0 && self.width >= extent.width && self.height >= extent.height
}
pub fn clamped_to(self, extent: Extent2D) -> Self {
self.intersect(Self::covering(extent))
}
pub fn shifted(self, dx: u32, dy: u32, extent: Extent2D) -> Self {
let left = self.x.saturating_sub(dx).min(extent.width);
let top = self.y.saturating_sub(dy).min(extent.height);
let right = (self.x + self.width).saturating_sub(dx).min(extent.width);
let bottom = (self.y + self.height).saturating_sub(dy).min(extent.height);
if right <= left || bottom <= top {
return Self::EMPTY;
}
Self::new(left, top, right - left, bottom - top)
}
pub fn covered_device_bounds(min: [f32; 2], max: [f32; 2], extent: Extent2D) -> Self {
if !min.iter().chain(&max).all(|v| v.is_finite()) {
return Self::EMPTY;
}
let inward = |v: f32, limit: u32, up: bool| {
let rounded = if up { v.ceil() } else { v.floor() };
rounded.clamp(0.0, limit as f32) as u32
};
let left = inward(min[0], extent.width, true);
let top = inward(min[1], extent.height, true);
let right = inward(max[0], extent.width, false);
let bottom = inward(max[1], extent.height, false);
if right <= left || bottom <= top {
return Self::EMPTY;
}
Self::new(left, top, right - left, bottom - top)
}
pub fn from_device_bounds(min: [f32; 2], max: [f32; 2], extent: Extent2D) -> Self {
if !min.iter().chain(&max).all(|v| v.is_finite()) {
return Self::EMPTY;
}
let whole = |v: f32, limit: u32| v.round().clamp(0.0, limit as f32) as u32;
let left = whole(min[0], extent.width);
let top = whole(min[1], extent.height);
let right = whole(max[0], extent.width);
let bottom = whole(max[1], extent.height);
if right <= left || bottom <= top {
return Self::EMPTY;
}
Self::new(left, top, right - left, bottom - top)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TARGET: Extent2D = Extent2D::new(100, 80);
#[test]
fn covering_a_target_is_recognized_as_unclipped() {
assert!(Scissor::covering(TARGET).covers(TARGET));
assert!(!Scissor::new(1, 0, 99, 80).covers(TARGET));
assert!(!Scissor::new(0, 0, 100, 79).covers(TARGET));
}
#[test]
fn covered_bounds_round_inward_where_a_clip_rounds_outward() {
let covered = Scissor::covered_device_bounds([10.5, 10.5], [60.5, 60.5], TARGET);
assert_eq!(covered, Scissor::new(11, 11, 49, 49));
assert_eq!(
Scissor::from_device_bounds([10.5, 10.5], [60.5, 60.5], TARGET),
Scissor::new(11, 11, 50, 50)
);
assert_eq!(
Scissor::covered_device_bounds([4.0, 6.0], [20.0, 30.0], TARGET),
Scissor::new(4, 6, 16, 24)
);
assert_eq!(
Scissor::covered_device_bounds([4.2, 4.2], [4.8, 4.8], TARGET),
Scissor::EMPTY
);
assert_eq!(
Scissor::covered_device_bounds([f32::NAN, 0.0], [10.0, 10.0], TARGET),
Scissor::EMPTY
);
}
#[test]
fn intersection_narrows_to_the_overlap() {
let a = Scissor::new(10, 10, 50, 50);
let b = Scissor::new(30, 20, 50, 50);
assert_eq!(a.intersect(b), Scissor::new(30, 20, 30, 40));
assert_eq!(a.intersect(b), b.intersect(a));
}
#[test]
fn disjoint_rectangles_intersect_to_nothing() {
let a = Scissor::new(0, 0, 10, 10);
let b = Scissor::new(20, 20, 10, 10);
assert!(a.intersect(b).is_empty());
assert!(a.intersect(Scissor::new(10, 0, 10, 10)).is_empty());
}
#[test]
fn intersection_is_associative_so_a_clip_stack_may_fold_in_any_order() {
let a = Scissor::new(5, 5, 60, 60);
let b = Scissor::new(10, 0, 40, 70);
let c = Scissor::new(0, 20, 100, 20);
assert_eq!(a.intersect(b).intersect(c), a.intersect(b.intersect(c)));
}
#[test]
fn device_bounds_take_the_pixels_whose_centers_are_inside() {
let rect = Scissor::from_device_bounds([2.4, 0.0], [5.6, 4.0], TARGET);
assert_eq!(rect, Scissor::new(2, 0, 4, 4));
assert_eq!(
Scissor::from_device_bounds([3.0, 0.0], [7.0, 1.0], TARGET),
Scissor::new(3, 0, 4, 1)
);
}
#[test]
fn device_bounds_narrower_than_a_pixel_admit_nothing() {
assert!(Scissor::from_device_bounds([2.1, 0.0], [2.3, 4.0], TARGET).is_empty());
}
#[test]
fn device_bounds_outside_the_target_do_not_wrap_or_saturate() {
assert!(Scissor::from_device_bounds([-50.0, -50.0], [-10.0, -10.0], TARGET).is_empty());
assert_eq!(
Scissor::from_device_bounds([-20.0, -20.0], [10.0, 10.0], TARGET),
Scissor::new(0, 0, 10, 10)
);
assert!(Scissor::from_device_bounds([f32::NAN, 0.0], [10.0, 10.0], TARGET).is_empty());
assert!(Scissor::from_device_bounds([0.0, 0.0], [f32::INFINITY, 10.0], TARGET).is_empty());
}
#[test]
fn clamping_keeps_a_rectangle_inside_its_target() {
let outside = Scissor::new(90, 70, 100, 100);
let clamped = outside.clamped_to(TARGET);
assert_eq!(clamped, Scissor::new(90, 70, 10, 10));
assert!(clamped.right() <= TARGET.width && clamped.bottom() <= TARGET.height);
}
}