use kurbo::{Affine, Rect};
use peniko::BlendMode;
use vello_common::record::LayerProps;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Group {
clip: bool,
layer: bool,
bounds: Rect,
}
impl Group {
#[must_use]
pub fn closes_clip(&self) -> bool {
self.clip
}
#[must_use]
pub fn closes_layer(&self) -> bool {
self.layer
}
#[must_use]
pub fn bounds(&self) -> Rect {
self.bounds
}
}
#[derive(Debug, Default)]
pub struct GroupStack {
entries: Vec<Group>,
}
impl GroupStack {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.entries.clear();
}
pub fn push_clip(&mut self, bounds: Rect) {
self.entries.push(Group {
clip: true,
layer: false,
bounds,
});
}
pub fn push_layer(&mut self, bounds: Rect, isolated: bool) {
self.entries.push(Group {
clip: true,
layer: isolated,
bounds,
});
}
pub fn pop(&mut self) -> Option<Group> {
self.entries.pop()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn depth(&self) -> usize {
self.entries.len()
}
pub fn bounds(&self) -> impl Iterator<Item = Rect> + '_ {
self.entries.iter().map(Group::bounds)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayerLowering {
Clip,
Isolated,
}
#[must_use]
pub fn lower_layer(alpha: f32) -> LayerLowering {
if alpha >= 1.0 {
LayerLowering::Clip
} else {
LayerLowering::Isolated
}
}
#[must_use]
pub fn layer_props(opacity: f32) -> LayerProps {
LayerProps {
blend_mode: BlendMode::default(),
opacity,
mask: None,
clip_path: None,
}
}
#[must_use]
pub fn snapshot_correction(rect: Rect, scale: f64, transform: Affine) -> Affine {
if scale == 1.0 {
return Affine::IDENTITY;
}
let correction = transform * Affine::scale_about(scale, rect.center()) * transform.inverse();
if correction.as_coeffs().iter().all(|c| c.is_finite()) {
correction
} else {
Affine::IDENTITY
}
}
#[derive(Debug)]
pub struct SnapshotStack {
depth: usize,
correction: Affine,
layer_bracket: Option<usize>,
}
impl Default for SnapshotStack {
fn default() -> Self {
Self::new()
}
}
impl SnapshotStack {
#[must_use]
pub fn new() -> Self {
Self {
depth: 0,
correction: Affine::IDENTITY,
layer_bracket: None,
}
}
pub fn reset(&mut self) {
*self = Self::new();
}
#[must_use]
pub fn correction(&self) -> Affine {
self.correction
}
#[must_use]
pub fn is_open(&self) -> bool {
self.depth > 0
}
pub fn enter(&mut self, rect: Rect, scale: f64, transform: Affine) -> bool {
let outermost = self.depth == 0;
if outermost {
self.correction = snapshot_correction(rect, scale, transform);
}
self.depth = self.depth.saturating_add(1);
outermost
}
pub fn record_layer(&mut self, bracket: usize) {
self.layer_bracket = Some(bracket);
}
pub fn leave(&mut self, open_brackets: usize) -> bool {
if self.depth == 0 {
return false;
}
self.depth -= 1;
if self.depth > 0 {
return false;
}
self.correction = Affine::IDENTITY;
self.layer_bracket
.take()
.is_some_and(|bracket| bracket <= open_brackets)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_full_opacity_layer_lowers_to_its_clip_alone() {
assert_eq!(lower_layer(1.0), LayerLowering::Clip);
assert_eq!(lower_layer(2.0), LayerLowering::Clip);
assert_eq!(lower_layer(0.5), LayerLowering::Isolated);
assert_eq!(lower_layer(0.0), LayerLowering::Isolated);
}
#[test]
fn a_recorded_layer_carries_only_its_opacity() {
let props = layer_props(0.25);
assert_eq!(props.opacity, 0.25);
assert_eq!(props.blend_mode, BlendMode::default());
assert!(props.mask.is_none());
assert!(props.clip_path.is_none());
}
#[test]
fn the_innermost_bracket_is_what_closes_whichever_pop_closes_it() {
let mut stack = GroupStack::new();
stack.push_clip(Rect::new(0.0, 0.0, 10.0, 10.0));
stack.push_layer(Rect::new(2.0, 2.0, 8.0, 8.0), true);
assert_eq!(stack.depth(), 2);
let inner = stack.pop().expect("two brackets are open");
assert!(inner.closes_clip() && inner.closes_layer());
let outer = stack.pop().expect("one bracket is still open");
assert!(outer.closes_clip() && !outer.closes_layer());
assert!(stack.is_empty());
assert!(stack.pop().is_none());
}
#[test]
fn a_full_opacity_layer_bracket_has_no_recorded_layer_to_close() {
let mut stack = GroupStack::new();
stack.push_layer(Rect::new(0.0, 0.0, 4.0, 4.0), false);
let group = stack.pop().expect("a bracket is open");
assert!(group.closes_clip());
assert!(!group.closes_layer());
}
#[test]
fn open_bracket_bounds_are_listed_outermost_first() {
let mut stack = GroupStack::new();
stack.push_clip(Rect::new(0.0, 0.0, 40.0, 40.0));
stack.push_layer(Rect::new(10.0, 10.0, 20.0, 20.0), true);
let bounds: Vec<Rect> = stack.bounds().collect();
assert_eq!(
bounds,
vec![
Rect::new(0.0, 0.0, 40.0, 40.0),
Rect::new(10.0, 10.0, 20.0, 20.0),
]
);
}
#[test]
fn the_correction_scales_the_body_about_the_rect_centre() {
let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
let correction = snapshot_correction(rect, 0.5, Affine::IDENTITY);
assert_eq!(correction * rect.center(), rect.center());
assert_eq!(correction * rect.origin(), (5.0, 5.0).into());
}
#[test]
fn the_correction_conjugates_by_the_body_transform() {
let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
let transform = Affine::translate((100.0, 0.0));
let correction = snapshot_correction(rect, 0.5, transform);
let composed = correction * transform;
assert_eq!(composed * rect.center(), transform * rect.center());
}
#[test]
fn a_scale_of_one_and_a_singular_transform_both_correct_nothing() {
let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
assert_eq!(
snapshot_correction(rect, 1.0, Affine::translate((3.0, 4.0))),
Affine::IDENTITY
);
assert_eq!(
snapshot_correction(rect, 0.5, Affine::scale(0.0)),
Affine::IDENTITY
);
}
#[test]
fn only_the_outermost_bracket_installs_its_presentation() {
let outer = Rect::new(0.0, 0.0, 20.0, 20.0);
let inner = Rect::new(0.0, 0.0, 4.0, 4.0);
let mut stack = SnapshotStack::new();
assert!(stack.enter(outer, 0.5, Affine::IDENTITY));
let installed = stack.correction();
assert_ne!(installed, Affine::IDENTITY);
assert!(!stack.enter(inner, 4.0, Affine::IDENTITY));
assert_eq!(stack.correction(), installed);
assert!(!stack.leave(0));
assert_eq!(stack.correction(), installed);
assert!(!stack.leave(0));
assert_eq!(stack.correction(), Affine::IDENTITY);
assert!(!stack.is_open());
}
#[test]
fn a_bracket_that_opened_a_layer_reports_it_once_at_its_own_pop() {
let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
let mut stack = SnapshotStack::new();
stack.enter(rect, 1.0, Affine::IDENTITY);
stack.record_layer(1);
stack.enter(rect, 1.0, Affine::IDENTITY);
assert!(!stack.leave(1), "the inner pop closes no group");
assert!(stack.leave(1), "the outer pop closes the group it opened");
assert!(!stack.leave(1), "an unbalanced pop closes nothing");
}
#[test]
fn an_unbalanced_pop_snapshot_is_ignored() {
let mut stack = SnapshotStack::new();
assert!(!stack.leave(0));
assert!(!stack.leave(0));
assert!(!stack.is_open());
assert_eq!(stack.correction(), Affine::IDENTITY);
}
#[test]
fn a_stray_pop_inside_a_snapshot_body_does_not_hand_its_group_to_pop_snapshot() {
let rect = Rect::new(0.0, 0.0, 20.0, 20.0);
let mut stack = SnapshotStack::new();
stack.enter(rect, 1.0, Affine::IDENTITY);
stack.record_layer(2);
assert!(!stack.leave(1), "a lost bracket reports no group to close");
assert!(!stack.leave(1), "an unbalanced pop still closes nothing");
}
}