use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape};
use peniko::Brush;
#[derive(Debug)]
pub(crate) enum LayerShape {
Rect(Rect),
Path(BezPath),
}
#[derive(Debug)]
pub(crate) struct ColorLayer {
pub(crate) shape: LayerShape,
pub(crate) clip: Option<Rect>,
pub(crate) brush: Brush,
pub(crate) paint_transform: Affine,
}
#[derive(Debug)]
enum Bracket {
Rect(Option<Rect>),
Outline,
Refused,
}
#[derive(Debug, Default)]
pub(crate) struct ColorGlyph {
brackets: Vec<Bracket>,
rect: Option<Rect>,
outlines: Vec<BezPath>,
layers: Vec<ColorLayer>,
refused: bool,
transform: Affine,
}
impl ColorGlyph {
pub(crate) fn is_open(&self) -> bool {
!self.brackets.is_empty()
}
pub(crate) fn transform(&self) -> Affine {
self.transform
}
pub(crate) fn push_clip(&mut self, path: &BezPath, transform: Affine) {
self.open(transform);
match axis_aligned_rect(path) {
Some(rect) => {
let previous = self.rect;
self.rect = Some(previous.map_or(rect, |open| open.intersect(rect)));
self.brackets.push(Bracket::Rect(previous));
}
None => {
self.outlines.push(path.clone());
self.brackets.push(Bracket::Outline);
}
}
}
pub(crate) fn push_refused(&mut self, transform: Affine) {
self.open(transform);
self.refused = true;
self.brackets.push(Bracket::Refused);
}
pub(crate) fn pop(&mut self) -> bool {
match self.brackets.pop() {
Some(Bracket::Rect(previous)) => self.rect = previous,
Some(Bracket::Outline) => {
self.outlines.pop();
}
Some(Bracket::Refused) => {}
None => return false,
}
self.brackets.is_empty()
}
pub(crate) fn fill(&mut self, area: Rect, brush: Brush, paint_transform: Affine) {
if self.refused {
return;
}
let bound = self.rect.map_or(area, |rect| rect.intersect(area));
if bound.width() <= 0.0 || bound.height() <= 0.0 {
return;
}
let shape = match self.outlines.as_slice() {
[] => LayerShape::Rect(bound),
[outline] => LayerShape::Path(outline.clone()),
_ => {
self.refused = true;
return;
}
};
let clip = match &shape {
LayerShape::Rect(_) => None,
LayerShape::Path(path) => (!contains(bound, path.bounding_box())).then_some(bound),
};
self.layers.push(ColorLayer {
shape,
clip,
brush,
paint_transform,
});
}
pub(crate) fn refuse(&mut self) {
self.refused = true;
}
pub(crate) fn take(&mut self) -> Option<Vec<ColorLayer>> {
let refused = self.refused;
self.refused = false;
self.rect = None;
self.outlines.clear();
self.brackets.clear();
let layers = std::mem::take(&mut self.layers);
(!refused).then_some(layers)
}
pub(crate) fn reset(&mut self) {
self.brackets.clear();
self.outlines.clear();
self.layers.clear();
self.rect = None;
self.refused = false;
}
fn open(&mut self, transform: Affine) {
if self.brackets.is_empty() {
self.transform = transform;
}
}
}
fn contains(outer: Rect, inner: Rect) -> bool {
outer.x0 <= inner.x0 && outer.y0 <= inner.y0 && outer.x1 >= inner.x1 && outer.y1 >= inner.y1
}
fn axis_aligned_rect(path: &BezPath) -> Option<Rect> {
let mut points: Vec<Point> = Vec::with_capacity(4);
let mut closed = false;
for element in path.elements() {
if closed {
return None;
}
match element {
PathEl::MoveTo(point) => {
if !points.is_empty() {
return None;
}
points.push(*point);
}
PathEl::LineTo(point) => {
if points.is_empty() || points.len() > 4 {
return None;
}
points.push(*point);
}
PathEl::ClosePath => closed = true,
PathEl::QuadTo(..) | PathEl::CurveTo(..) => return None,
}
}
if points.len() == 5 && points.get(4) == points.first() {
points.truncate(4);
}
let corners: [Point; 4] = points.try_into().ok()?;
for index in 0..corners.len() {
let from = *corners.get(index)?;
let to = *corners.get((index + 1) % corners.len())?;
if from.x != to.x && from.y != to.y {
return None;
}
}
let (x0, x1) = min_max(corners.map(|corner| corner.x))?;
let (y0, y1) = min_max(corners.map(|corner| corner.y))?;
Some(Rect::new(x0, y0, x1, y1))
}
fn min_max(values: [f64; 4]) -> Option<(f64, f64)> {
if !values.iter().all(|value| value.is_finite()) {
return None;
}
let mut low = *values.first()?;
let mut high = low;
for value in values {
low = low.min(value);
high = high.max(value);
}
Some((low, high))
}
#[cfg(test)]
mod tests {
use super::*;
use peniko::color::palette::css::{BLUE, RED};
fn transform() -> Affine {
Affine::translate((3.0, 5.0))
}
fn rect_path(rect: Rect) -> BezPath {
rect.to_path(0.1)
}
fn triangle() -> BezPath {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((10.0, 0.0));
path.line_to((5.0, 10.0));
path.close_path();
path
}
fn bow_tie() -> BezPath {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((10.0, 10.0));
path.line_to((10.0, 0.0));
path.line_to((0.0, 10.0));
path.close_path();
path
}
#[test]
fn a_rectangles_own_path_is_recognised_as_that_rectangle() {
let rect = Rect::new(1.0, 2.0, 30.0, 40.0);
assert_eq!(axis_aligned_rect(&rect_path(rect)), Some(rect));
}
#[test]
fn anything_that_is_not_an_axis_aligned_rectangle_is_not_recognised() {
let mut curved = BezPath::new();
curved.move_to((0.0, 0.0));
curved.curve_to((1.0, 0.0), (2.0, 1.0), (2.0, 2.0));
curved.close_path();
for (label, path) in [
("a triangle", triangle()),
("a self-crossing quadrilateral", bow_tie()),
("a path with a curve in it", curved),
("an empty path", BezPath::new()),
] {
assert_eq!(
axis_aligned_rect(&path),
None,
"{label} is not a rectangle, and must not be widened into one"
);
}
}
#[test]
fn a_layer_inside_rectangles_alone_is_their_intersection() {
let mut glyph = ColorGlyph::default();
glyph.push_clip(&rect_path(Rect::new(0.0, 0.0, 20.0, 20.0)), transform());
glyph.push_clip(&rect_path(Rect::new(10.0, 0.0, 40.0, 12.0)), transform());
glyph.fill(
Rect::new(0.0, 0.0, 40.0, 40.0),
Brush::Solid(RED),
Affine::IDENTITY,
);
assert!(!glyph.pop(), "the outer bracket is still open");
assert!(glyph.pop());
let layers = glyph.take().expect("nothing refused the glyph");
let [layer] = layers.as_slice() else {
panic!("one fill is one layer, got {}", layers.len());
};
assert!(
layer.clip.is_none(),
"the rectangle became the shape itself"
);
match layer.shape {
LayerShape::Rect(rect) => assert_eq!(rect, Rect::new(10.0, 0.0, 20.0, 12.0)),
LayerShape::Path(_) => panic!("no outline clip was open"),
}
}
#[test]
fn a_layer_clipped_away_entirely_records_nothing_and_refuses_nothing() {
let mut glyph = ColorGlyph::default();
glyph.push_clip(&rect_path(Rect::new(0.0, 0.0, 10.0, 10.0)), transform());
glyph.fill(
Rect::new(20.0, 20.0, 30.0, 30.0),
Brush::Solid(RED),
Affine::IDENTITY,
);
assert!(glyph.pop());
assert_eq!(
glyph.take().map(|layers| layers.len()),
Some(0),
"an empty intersection is a layer that paints nothing, not a \
glyph the engine cannot express"
);
}
#[test]
fn an_outline_clip_becomes_the_layers_shape() {
let mut glyph = ColorGlyph::default();
glyph.push_clip(&rect_path(Rect::new(0.0, 0.0, 40.0, 40.0)), transform());
glyph.push_clip(&triangle(), transform());
glyph.fill(
Rect::new(0.0, 0.0, 40.0, 40.0),
Brush::Solid(BLUE),
Affine::IDENTITY,
);
assert!(!glyph.pop());
assert!(glyph.pop());
let layers = glyph.take().expect("nothing refused the glyph");
let [layer] = layers.as_slice() else {
panic!("one fill is one layer, got {}", layers.len());
};
assert!(
layer.clip.is_none(),
"the rectangle around it contains it, so it costs no clip"
);
assert!(matches!(layer.shape, LayerShape::Path(_)));
assert_eq!(layer.brush, Brush::Solid(BLUE), "the layer's own paint");
}
#[test]
fn an_outline_that_escapes_its_rectangle_carries_it_as_a_clip() {
let mut glyph = ColorGlyph::default();
glyph.push_clip(&rect_path(Rect::new(0.0, 0.0, 4.0, 4.0)), transform());
glyph.push_clip(&triangle(), transform());
glyph.fill(
Rect::new(0.0, 0.0, 40.0, 40.0),
Brush::Solid(RED),
Affine::IDENTITY,
);
glyph.pop();
assert!(glyph.pop());
let layers = glyph.take().expect("nothing refused the glyph");
let [layer] = layers.as_slice() else {
panic!("one fill is one layer, got {}", layers.len());
};
assert_eq!(layer.clip, Some(Rect::new(0.0, 0.0, 4.0, 4.0)));
}
#[test]
fn two_outline_clips_at_once_refuse_the_whole_glyph() {
let mut glyph = ColorGlyph::default();
glyph.push_clip(&triangle(), transform());
glyph.fill(
Rect::new(0.0, 0.0, 40.0, 40.0),
Brush::Solid(RED),
Affine::IDENTITY,
);
glyph.push_clip(&triangle(), transform());
glyph.fill(
Rect::new(0.0, 0.0, 40.0, 40.0),
Brush::Solid(BLUE),
Affine::IDENTITY,
);
glyph.pop();
assert!(glyph.pop());
assert!(
glyph.take().is_none(),
"an inexpressible layer takes the whole glyph with it, including \
the layers already collected"
);
}
#[test]
fn an_isolated_bracket_refuses_the_glyph_and_still_balances() {
let mut glyph = ColorGlyph::default();
glyph.push_clip(&rect_path(Rect::new(0.0, 0.0, 40.0, 40.0)), transform());
glyph.push_refused(transform());
glyph.fill(
Rect::new(0.0, 0.0, 40.0, 40.0),
Brush::Solid(RED),
Affine::IDENTITY,
);
assert!(!glyph.pop(), "the blend bracket closes first");
assert!(glyph.pop(), "and the glyph's own bracket after it");
assert!(glyph.take().is_none());
assert!(
!glyph.is_open(),
"taking a refused glyph leaves nothing behind for the next one"
);
}
#[test]
fn an_unmatched_pop_closes_no_glyph() {
let mut glyph = ColorGlyph::default();
assert!(!glyph.pop(), "there was nothing open to close");
assert!(!glyph.is_open());
}
#[test]
fn the_transform_is_the_one_the_first_bracket_opened_under() {
let mut glyph = ColorGlyph::default();
glyph.push_clip(&rect_path(Rect::new(0.0, 0.0, 40.0, 40.0)), transform());
glyph.push_clip(&triangle(), Affine::scale(9.0));
assert_eq!(glyph.transform(), transform());
}
}