use emblema_geometry::Rect;
use emblema_hal::BlendMode;
use glam::{Affine2, Vec2};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Coverage {
Empty,
Bounded(Rect),
Unbounded,
}
impl Coverage {
pub fn resolve(self, clip: Option<Rect>) -> Option<Rect> {
match self {
Self::Empty => None,
Self::Bounded(rect) => match clip {
Some(clip) => intersect(rect, clip),
None => Some(rect),
},
Self::Unbounded => clip,
}
}
pub fn clipped(self, clip: Rect) -> Self {
match self {
Self::Empty => Self::Empty,
Self::Bounded(rect) => match intersect(rect, clip) {
Some(rect) => Self::Bounded(rect),
None => Self::Empty,
},
Self::Unbounded => Self::Bounded(clip),
}
}
pub fn grown(self, reach: Vec2) -> Self {
match self {
Self::Bounded(rect) if reach.is_finite() => {
Self::Bounded(Rect::new(rect.min - reach.abs(), rect.max + reach.abs()))
}
other => other,
}
}
pub fn union(self, other: Self) -> Self {
match (self, other) {
(Self::Unbounded, _) | (_, Self::Unbounded) => Self::Unbounded,
(Self::Empty, x) | (x, Self::Empty) => x,
(Self::Bounded(a), Self::Bounded(b)) => {
Self::Bounded(Rect::new(a.min.min(b.min), a.max.max(b.max)))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Geometry {
Empty,
Bounded(Rect),
Unbounded,
}
impl Geometry {
pub fn transformed(self, transform: Affine2) -> Coverage {
match self {
Self::Empty => Coverage::Empty,
Self::Unbounded => Coverage::Unbounded,
Self::Bounded(rect) if rect.is_empty() => Coverage::Empty,
Self::Bounded(rect) => {
let corners = [
Vec2::new(rect.min.x, rect.min.y),
Vec2::new(rect.max.x, rect.min.y),
Vec2::new(rect.max.x, rect.max.y),
Vec2::new(rect.min.x, rect.max.y),
]
.map(|corner| transform.transform_point2(corner));
if !corners.iter().all(|corner| corner.is_finite()) {
return Coverage::Empty;
}
let mut bounds = Rect::empty();
for corner in corners {
bounds.union_point(corner);
}
Coverage::Bounded(bounds)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Contents {
pub reach: Vec2,
}
impl Contents {
pub fn tight() -> Self {
Self { reach: Vec2::ZERO }
}
pub fn reaching(reach: Vec2) -> Self {
Self { reach }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Entity {
pub transform: Affine2,
pub blend: BlendMode,
pub clip_depth: u32,
pub contents: Contents,
pub geometry: Geometry,
}
impl Default for Entity {
fn default() -> Self {
Self {
transform: Affine2::IDENTITY,
blend: BlendMode::SrcOver,
clip_depth: 0,
contents: Contents::tight(),
geometry: Geometry::Empty,
}
}
}
impl Entity {
pub fn coverage(&self, clip: Option<Rect>) -> Coverage {
let covered = self
.geometry
.transformed(self.transform)
.grown(self.contents.reach);
match clip {
Some(clip) => covered.clipped(clip),
None => covered,
}
}
}
fn intersect(a: Rect, b: Rect) -> Option<Rect> {
let min = a.min.max(b.min);
let max = a.max.min(b.max);
if min.x > max.x || min.y > max.y {
None
} else {
Some(Rect::new(min, max))
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::f32::consts::FRAC_PI_4;
fn rect(min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> Rect {
Rect::new(Vec2::new(min_x, min_y), Vec2::new(max_x, max_y))
}
fn bounded(geometry: Rect) -> Entity {
Entity {
geometry: Geometry::Bounded(geometry),
..Entity::default()
}
}
#[test]
fn a_transform_that_turns_a_shape_needs_a_larger_box_to_hold_it() {
let square = bounded(rect(-1.0, -1.0, 1.0, 1.0));
let upright = square.coverage(None);
assert_eq!(upright, Coverage::Bounded(rect(-1.0, -1.0, 1.0, 1.0)));
let turned = Entity {
transform: Affine2::from_angle(FRAC_PI_4),
..square
}
.coverage(None);
let Coverage::Bounded(bounds) = turned else {
panic!("a turned square still covers something, got {turned:?}");
};
let half = core::f32::consts::SQRT_2;
assert!(
(bounds.max.x - half).abs() < 1e-5 && (bounds.max.y - half).abs() < 1e-5,
"an eighth turn should reach root two, got {bounds:?}"
);
assert!(bounds.width() > 2.0, "the box grew, got {}", bounds.width());
}
#[test]
fn a_filter_reaches_in_device_pixels_rather_than_in_the_shapes_own_units() {
let scaled = Entity {
transform: Affine2::from_scale(Vec2::splat(10.0)),
contents: Contents::reaching(Vec2::splat(4.0)),
..bounded(rect(0.0, 0.0, 1.0, 1.0))
};
assert_eq!(
scaled.coverage(None),
Coverage::Bounded(rect(-4.0, -4.0, 14.0, 14.0)),
"the unit square scaled to ten, grown by four device pixels"
);
}
#[test]
fn the_clip_narrows_after_the_filter_has_reached() {
let entity = Entity {
contents: Contents::reaching(Vec2::splat(5.0)),
..bounded(rect(0.0, 0.0, 10.0, 10.0))
};
let clip = rect(0.0, 0.0, 8.0, 8.0);
assert_eq!(
entity.coverage(Some(clip)),
Coverage::Bounded(rect(0.0, 0.0, 8.0, 8.0)),
"grown to -5..15 and then cut to the clip"
);
let wrong = Coverage::Bounded(clip).grown(Vec2::splat(5.0));
assert_ne!(
entity.coverage(Some(clip)),
wrong,
"the two orders must not agree, or this test proves nothing"
);
}
#[test]
fn nothing_drawn_stays_nothing_however_it_is_filtered() {
let nothing = Entity::default();
assert_eq!(nothing.coverage(None), Coverage::Empty);
assert_eq!(
nothing.coverage(Some(rect(0.0, 0.0, 10.0, 10.0))),
Coverage::Empty
);
assert_eq!(
Coverage::Empty.grown(Vec2::splat(9.0)),
Coverage::Empty,
"growing nothing leaves nothing"
);
assert_eq!(
bounded(rect(10.0, 10.0, 0.0, 0.0)).coverage(None),
Coverage::Empty
);
let collapsed = Entity {
transform: Affine2::from_scale(Vec2::ZERO),
..bounded(rect(0.0, 0.0, 10.0, 10.0))
};
assert_eq!(
collapsed.coverage(None),
Coverage::Bounded(rect(0.0, 0.0, 0.0, 0.0)),
"a collapsed shape is a point, not an absence"
);
}
#[test]
fn a_transform_to_nowhere_covers_nothing() {
for bad in [f32::INFINITY, f32::NAN] {
let entity = Entity {
transform: Affine2::from_scale(Vec2::splat(bad)),
..bounded(rect(1.0, 1.0, 2.0, 2.0))
};
assert_eq!(
entity.coverage(None),
Coverage::Empty,
"a transform by {bad} covers nothing"
);
}
assert_eq!(
Coverage::Bounded(rect(0.0, 0.0, 1.0, 1.0)).grown(Vec2::splat(f32::NAN)),
Coverage::Bounded(rect(0.0, 0.0, 1.0, 1.0))
);
}
#[test]
fn unbounded_coverage_is_only_ever_bounded_by_a_clip() {
let paint = Entity {
geometry: Geometry::Unbounded,
..Entity::default()
};
assert_eq!(paint.coverage(None), Coverage::Unbounded);
assert_eq!(
paint.coverage(Some(rect(2.0, 2.0, 6.0, 6.0))),
Coverage::Bounded(rect(2.0, 2.0, 6.0, 6.0))
);
assert_eq!(
Entity {
transform: Affine2::from_scale(Vec2::splat(0.5)),
..paint
}
.coverage(None),
Coverage::Unbounded
);
assert_eq!(Coverage::Unbounded.resolve(None), None);
assert_eq!(
Coverage::Unbounded.resolve(Some(rect(0.0, 0.0, 3.0, 3.0))),
Some(rect(0.0, 0.0, 3.0, 3.0))
);
}
#[test]
fn a_clip_that_misses_leaves_nothing() {
let entity = bounded(rect(0.0, 0.0, 4.0, 4.0));
assert_eq!(
entity.coverage(Some(rect(9.0, 9.0, 12.0, 12.0))),
Coverage::Empty
);
assert_eq!(
entity.coverage(Some(rect(4.0, 4.0, 8.0, 8.0))),
Coverage::Bounded(rect(4.0, 4.0, 4.0, 4.0))
);
}
#[test]
fn union_takes_the_widest_claim() {
let a = Coverage::Bounded(rect(0.0, 0.0, 2.0, 2.0));
let b = Coverage::Bounded(rect(5.0, 1.0, 6.0, 9.0));
assert_eq!(a.union(b), Coverage::Bounded(rect(0.0, 0.0, 6.0, 9.0)));
assert_eq!(a.union(Coverage::Empty), a);
assert_eq!(Coverage::Empty.union(a), a);
assert_eq!(a.union(Coverage::Unbounded), Coverage::Unbounded);
assert_eq!(Coverage::Unbounded.union(a), Coverage::Unbounded);
}
#[test]
fn a_reach_grows_outward_whichever_sign_it_is_given() {
let covered = Coverage::Bounded(rect(0.0, 0.0, 4.0, 4.0));
assert_eq!(
covered.grown(Vec2::new(-2.0, -2.0)),
covered.grown(Vec2::new(2.0, 2.0))
);
assert_eq!(
covered.grown(Vec2::new(2.0, 0.0)),
Coverage::Bounded(rect(-2.0, 0.0, 6.0, 4.0)),
"per axis, so a one-dimensional blur grows one dimension"
);
}
}