use crate::field::{Rect, Vec2};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Viewport {
pub center: Vec2,
pub size: Vec2,
pub home: Vec2,
}
impl Viewport {
pub fn new(center: Vec2, size: Vec2) -> Self {
Self {
center,
size,
home: center,
}
}
pub fn rect(&self) -> Rect {
let half = Vec2 {
x: self.size.x * 0.5,
y: self.size.y * 0.5,
};
Rect {
min: Vec2 {
x: self.center.x - half.x,
y: self.center.y - half.y,
},
max: Vec2 {
x: self.center.x + half.x,
y: self.center.y + half.y,
},
}
}
pub fn move_to(&mut self, center: Vec2) {
self.center = center;
}
pub fn pan(&mut self, delta: Vec2) {
self.center.x += delta.x;
self.center.y += delta.y;
}
pub fn set_home(&mut self) {
self.home = self.center;
}
pub fn return_home(&mut self) {
self.center = self.home;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FocusZone {
Inside,
Outside,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FocusRing {
pub radius_x: f32,
pub radius_y: f32,
pub offset_x: f32,
pub offset_y: f32,
}
impl FocusRing {
pub fn new(radius_x: f32, radius_y: f32, offset_x: f32, offset_y: f32) -> Self {
Self {
radius_x,
radius_y,
offset_x,
offset_y,
}
}
pub fn contains(&self, center: Vec2, p: Vec2) -> bool {
self.normalized_distance2(center, p) <= 1.0
}
pub fn zone(&self, vp_center: Vec2, p: Vec2) -> FocusZone {
if self.contains(vp_center, p) {
FocusZone::Inside
} else {
FocusZone::Outside
}
}
pub fn normalized_distance2(&self, center: Vec2, p: Vec2) -> f32 {
let ring_center = Vec2 {
x: center.x + self.offset_x,
y: center.y + self.offset_y,
};
let dx = p.x - ring_center.x;
let dy = p.y - ring_center.y;
let rx = self.radius_x.max(0.0001);
let ry = self.radius_y.max(0.0001);
let nx = dx / rx;
let ny = dy / ry;
nx * nx + ny * ny
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rect_is_correct() {
let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
let r = vp.rect();
assert_eq!(r.min, Vec2 { x: -50.0, y: -25.0 });
assert_eq!(r.max, Vec2 { x: 50.0, y: 25.0 });
}
#[test]
fn return_home_works() {
let mut vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
vp.pan(Vec2 { x: 10.0, y: 5.0 });
assert_eq!(vp.center, Vec2 { x: 10.0, y: 5.0 });
vp.return_home();
assert_eq!(vp.center, Vec2 { x: 0.0, y: 0.0 });
}
#[test]
fn focus_ring_contains_axis_aligned() {
let ring = FocusRing::new(10.0, 5.0, 0.0, 0.0);
let c = Vec2 { x: 0.0, y: 0.0 };
assert!(ring.contains(c, Vec2 { x: 0.0, y: 0.0 }));
assert!(ring.contains(c, Vec2 { x: 10.0, y: 0.0 }));
assert!(ring.contains(c, Vec2 { x: 0.0, y: 5.0 }));
assert!(!ring.contains(c, Vec2 { x: 10.01, y: 0.0 }));
assert!(!ring.contains(c, Vec2 { x: 0.0, y: 5.01 }));
}
#[test]
fn focus_ring_respects_offset() {
let ring = FocusRing::new(10.0, 5.0, 4.0, -2.0);
let c = Vec2 { x: 0.0, y: 0.0 };
assert!(ring.contains(c, Vec2 { x: 4.0, y: -2.0 }));
assert!(ring.contains(c, Vec2 { x: 14.0, y: -2.0 }));
assert!(ring.contains(c, Vec2 { x: 4.0, y: 3.0 }));
assert!(!ring.contains(c, Vec2 { x: 14.01, y: -2.0 }));
assert!(!ring.contains(c, Vec2 { x: 4.0, y: 3.01 }));
}
#[test]
fn focus_zone_classifies() {
let ring = FocusRing::new(10.0, 10.0, 0.0, 0.0);
let c = Vec2 { x: 0.0, y: 0.0 };
assert_eq!(ring.zone(c, Vec2 { x: 0.0, y: 0.0 }), FocusZone::Inside);
assert_eq!(ring.zone(c, Vec2 { x: 20.0, y: 0.0 }), FocusZone::Outside);
}
}