use crate::coord::Coord;
use crate::geom::Viewport;
use crate::paint::Clip;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Region {
pub x: Coord,
pub y: Coord,
pub w: Coord,
pub h: Coord,
pub span: Coord,
}
impl Region {
#[inline]
pub fn new(x: Coord, y: Coord, w: Coord, h: Coord) -> Self {
let sum = w + h;
let span = if sum == 0.0 { 0.0 } else { 2.0 * w * h / sum };
Self { x, y, w, h, span }
}
#[inline]
pub fn from_viewport(vp: &Viewport) -> Self {
Self::new(0.0, 0.0, vp.width_px as Coord, vp.height_px as Coord)
}
#[inline]
pub fn right(&self) -> Coord {
self.x + self.w
}
#[inline]
pub fn bottom(&self) -> Coord {
self.y + self.h
}
#[inline]
pub fn center(&self) -> (Coord, Coord) {
(self.x + self.w * 0.5, self.y + self.h * 0.5)
}
#[inline]
pub fn center_x(&self) -> Coord {
self.x + self.w * 0.5
}
#[inline]
pub fn center_y(&self) -> Coord {
self.y + self.h * 0.5
}
#[inline]
pub fn contains(&self, px: Coord, py: Coord) -> bool {
px >= self.x && px < self.right() && py >= self.y && py < self.bottom()
}
#[inline]
pub fn size(&self, divisor: Coord) -> Coord {
self.span / divisor
}
pub fn split_v<const N: usize>(&self, weights: [Coord; N]) -> [Region; N] {
let total: Coord = weights.iter().sum();
let mut result = [*self; N];
let mut cursor = self.y;
for i in 0..N - 1 {
let band_h = self.h * weights[i] / total;
result[i] = Region::new(self.x, cursor, self.w, band_h);
cursor += band_h;
}
let last_h = self.bottom() - cursor;
result[N - 1] = Region::new(self.x, cursor, self.w, last_h);
result
}
pub fn split_h<const N: usize>(&self, weights: [Coord; N]) -> [Region; N] {
let total: Coord = weights.iter().sum();
let mut result = [*self; N];
let mut cursor = self.x;
for i in 0..N - 1 {
let band_w = self.w * weights[i] / total;
result[i] = Region::new(cursor, self.y, band_w, self.h);
cursor += band_w;
}
let last_w = self.right() - cursor;
result[N - 1] = Region::new(cursor, self.y, last_w, self.h);
result
}
#[inline]
pub fn intersects(&self, other: &Region) -> bool {
self.x < other.right()
&& other.x < self.right()
&& self.y < other.bottom()
&& other.y < self.bottom()
}
#[inline]
pub fn union(&self, other: &Region) -> Region {
let x = self.x.min(other.x);
let y = self.y.min(other.y);
let r = self.right().max(other.right());
let b = self.bottom().max(other.bottom());
Region::new(x, y, r - x, b - y)
}
#[inline]
pub fn inset(&self, frac: Coord) -> Region {
let dx = self.w * frac;
let dy = self.h * frac;
Region::new(self.x + dx, self.y + dy, self.w - dx - dx, self.h - dy - dy)
}
#[inline]
pub fn inset_xy(&self, frac_x: Coord, frac_y: Coord) -> Region {
let dx = self.w * frac_x;
let dy = self.h * frac_y;
Region::new(self.x + dx, self.y + dy, self.w - dx - dx, self.h - dy - dy)
}
#[inline]
pub fn center_h(&self, frac: Coord) -> Region {
let new_w = self.w * frac;
let dx = (self.w - new_w) * 0.5;
Region::new(self.x + dx, self.y, new_w, self.h)
}
#[inline]
pub fn center_v(&self, frac: Coord) -> Region {
let new_h = self.h * frac;
let dy = (self.h - new_h) * 0.5;
Region::new(self.x, self.y + dy, self.w, new_h)
}
#[inline]
pub fn square(&self) -> Region {
let side = self.w.min(self.h);
let dx = (self.w - side) * 0.5;
let dy = (self.h - side) * 0.5;
Region::new(self.x + dx, self.y + dy, side, side)
}
#[inline]
pub fn to_clip(&self) -> Clip {
Clip::new(
self.x as usize,
self.y as usize,
self.right() as usize,
self.bottom() as usize,
)
}
}
#[inline]
pub fn harmonic(a: Coord, b: Coord) -> Coord {
let sum = a + b;
if sum == 0.0 { 0.0 } else { 2.0 * a * b / sum }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geom::Viewport;
const EPSILON: f32 = 1e-4;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn from_viewport_produces_correct_region() {
let vp = Viewport::new(800, 600);
let r = Region::from_viewport(&vp);
assert_eq!(r.x, 0.0);
assert_eq!(r.y, 0.0);
assert_eq!(r.w, 800.0);
assert_eq!(r.h, 600.0);
assert!(approx(r.span, 2.0 * 800.0 * 600.0 / 1400.0));
}
#[test]
fn span_zero_when_dimension_zero() {
let r = Region::new(0.0, 0.0, 100.0, 0.0);
assert_eq!(r.span, 0.0);
let r2 = Region::new(0.0, 0.0, 0.0, 100.0);
assert_eq!(r2.span, 0.0);
let r3 = Region::new(0.0, 0.0, 0.0, 0.0);
assert_eq!(r3.span, 0.0);
}
#[test]
fn split_v_tiles_parent_exactly() {
let parent = Region::new(10.0, 20.0, 300.0, 400.0);
let bands = parent.split_v([1.0, 3.0, 1.0]);
assert_eq!(bands[0].y, parent.y);
assert!(approx(bands[1].y, bands[0].bottom()));
assert!(approx(bands[2].y, bands[1].bottom()));
assert_eq!(bands[2].bottom(), parent.bottom());
for b in &bands {
assert_eq!(b.x, parent.x);
assert_eq!(b.w, parent.w);
}
assert!(approx(bands[0].h, 80.0));
assert!(approx(bands[1].h, 240.0));
assert!(approx(bands[2].h, 80.0));
}
#[test]
fn split_h_tiles_parent_exactly() {
let parent = Region::new(10.0, 20.0, 300.0, 400.0);
let bands = parent.split_h([1.0, 6.0, 1.0]);
assert_eq!(bands[0].x, parent.x);
assert!(approx(bands[1].x, bands[0].right()));
assert!(approx(bands[2].x, bands[1].right()));
assert_eq!(bands[2].right(), parent.right());
for b in &bands {
assert_eq!(b.y, parent.y);
assert_eq!(b.h, parent.h);
}
assert!(approx(bands[0].w, 37.5));
assert!(approx(bands[1].w, 225.0));
assert!(approx(bands[2].w, 37.5));
}
#[test]
fn split_single_element_returns_parent() {
let parent = Region::new(5.0, 10.0, 200.0, 100.0);
let [only] = parent.split_v([1.0]);
assert_eq!(only.x, parent.x);
assert_eq!(only.y, parent.y);
assert_eq!(only.w, parent.w);
assert_eq!(only.h, parent.h);
}
#[test]
fn split_equal_weights_produces_equal_regions() {
let parent = Region::new(0.0, 0.0, 400.0, 300.0);
let bands = parent.split_h([1.0, 1.0, 1.0, 1.0]);
for b in &bands {
assert!(approx(b.w, 100.0));
}
}
#[test]
fn nested_splits_stay_within_parent() {
let root = Region::new(0.0, 0.0, 1920.0, 1080.0);
let [_, content, _] = root.split_h([1.0, 6.0, 1.0]);
let [header, body, footer] = content.split_v([2.0, 12.0, 2.0]);
for r in &[header, body, footer] {
assert!(r.x >= content.x - EPSILON);
assert!(r.y >= content.y - EPSILON);
assert!(r.right() <= content.right() + EPSILON);
assert!(r.bottom() <= content.bottom() + EPSILON);
}
let [a, b] = header.split_h([1.0, 4.0]);
assert!(a.x >= header.x - EPSILON);
assert!(b.right() <= header.right() + EPSILON);
}
#[test]
fn each_region_has_local_span() {
let root = Region::new(0.0, 0.0, 1000.0, 500.0);
let [narrow, wide] = root.split_h([1.0, 9.0]);
assert!(narrow.span != wide.span);
assert!(approx(narrow.span, 2.0 * 100.0 * 500.0 / 600.0));
}
#[test]
fn inset_zero_returns_self() {
let r = Region::new(10.0, 20.0, 300.0, 200.0);
let inset = r.inset(0.0);
assert_eq!(inset.x, r.x);
assert_eq!(inset.y, r.y);
assert_eq!(inset.w, r.w);
assert_eq!(inset.h, r.h);
}
#[test]
fn inset_shrinks_symmetrically() {
let r = Region::new(0.0, 0.0, 100.0, 200.0);
let inset = r.inset(0.1);
assert!(approx(inset.x, 10.0));
assert!(approx(inset.y, 20.0));
assert!(approx(inset.w, 80.0));
assert!(approx(inset.h, 160.0));
}
#[test]
fn center_h_75_percent() {
let r = Region::new(0.0, 0.0, 400.0, 300.0);
let centered = r.center_h(0.75);
assert!(approx(centered.w, 300.0));
assert!(approx(centered.x, 50.0)); assert_eq!(centered.y, r.y);
assert_eq!(centered.h, r.h);
}
#[test]
fn center_v_half() {
let r = Region::new(100.0, 100.0, 400.0, 300.0);
let centered = r.center_v(0.5);
assert!(approx(centered.h, 150.0));
assert!(approx(centered.y, 175.0)); assert_eq!(centered.x, r.x);
assert_eq!(centered.w, r.w);
}
#[test]
fn square_landscape() {
let r = Region::new(0.0, 0.0, 400.0, 200.0);
let sq = r.square();
assert!(approx(sq.w, 200.0));
assert!(approx(sq.h, 200.0));
assert!(approx(sq.x, 100.0)); assert!(approx(sq.y, 0.0));
}
#[test]
fn square_portrait() {
let r = Region::new(0.0, 0.0, 200.0, 400.0);
let sq = r.square();
assert!(approx(sq.w, 200.0));
assert!(approx(sq.h, 200.0));
assert!(approx(sq.x, 0.0));
assert!(approx(sq.y, 100.0)); }
#[test]
fn size_is_span_over_divisor() {
let r = Region::new(0.0, 0.0, 800.0, 600.0);
let expected_span = 2.0 * 800.0 * 600.0 / 1400.0;
assert!(approx(r.size(16.0), expected_span / 16.0));
assert!(approx(r.size(32.0), expected_span / 32.0));
}
#[test]
fn contains_edges() {
let r = Region::new(10.0, 20.0, 100.0, 50.0);
assert!(r.contains(10.0, 20.0));
assert!(!r.contains(110.0, 20.0));
assert!(!r.contains(10.0, 70.0));
assert!(r.contains(50.0, 40.0));
assert!(!r.contains(9.9, 20.0));
}
#[test]
fn to_clip_truncates() {
let r = Region::new(10.5, 20.7, 100.3, 50.9);
let clip = r.to_clip();
assert_eq!(clip.x_start, 10);
assert_eq!(clip.y_start, 20);
assert_eq!(clip.x_end, 110); assert_eq!(clip.y_end, 71); }
#[test]
fn center_point() {
let r = Region::new(10.0, 20.0, 100.0, 200.0);
let (cx, cy) = r.center();
assert!(approx(cx, 60.0));
assert!(approx(cy, 120.0));
}
#[test]
fn harmonic_basic() {
assert!(approx(super::harmonic(100.0, 200.0), 133.3333));
}
#[test]
fn harmonic_equal_values() {
assert!(approx(super::harmonic(50.0, 50.0), 50.0));
}
#[test]
fn harmonic_zero_input() {
assert_eq!(super::harmonic(0.0, 100.0), 0.0);
assert_eq!(super::harmonic(100.0, 0.0), 0.0);
assert_eq!(super::harmonic(0.0, 0.0), 0.0);
}
}