use frus_core::{Path, PathVerb, Primitive, Rect, Scene};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum Kind {
Rect,
Image,
Path,
Text,
Layer,
}
#[derive(Clone, Debug)]
pub(crate) struct Batch {
pub(crate) kind: Kind,
pub(crate) members: Vec<usize>,
bounds: Rect,
}
fn overlaps(a: Rect, b: Rect) -> bool {
a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height
}
fn path_bounds(path: &Path) -> Rect {
let (mut x0, mut y0) = (f32::MAX, f32::MAX);
let (mut x1, mut y1) = (f32::MIN, f32::MIN);
let mut seen = false;
let mut point = |p: &frus_core::Point| {
seen = true;
x0 = x0.min(p.x);
y0 = y0.min(p.y);
x1 = x1.max(p.x);
y1 = y1.max(p.y);
};
for verb in path.verbs() {
match verb {
PathVerb::MoveTo(p) | PathVerb::LineTo(p) => point(p),
PathVerb::QuadTo { ctrl, to } => {
point(ctrl);
point(to);
}
PathVerb::CubicTo { c1, c2, to } => {
point(c1);
point(c2);
point(to);
}
PathVerb::Close => {}
}
}
if !seen {
return Rect::new(0.0, 0.0, 0.0, 0.0);
}
Rect::new(x0, y0, x1 - x0, y1 - y0)
}
fn footprint(primitive: &Primitive) -> Option<(Kind, Rect)> {
match primitive {
Primitive::Rect { rect, clip, .. } => Some((Kind::Rect, rect.intersect(*clip))),
Primitive::Image { rect, clip, .. } => Some((Kind::Image, rect.intersect(*clip))),
Primitive::Path {
path, stroke, clip, ..
} => {
let half = stroke.as_ref().map_or(0.0, |s| s.width / 2.0);
let b = path_bounds(path);
let grown = Rect::new(
b.x - half,
b.y - half,
b.width + half * 2.0,
b.height + half * 2.0,
);
Some((Kind::Path, grown.intersect(*clip)))
}
Primitive::Text { bounds, clip, .. } | Primitive::RichText { bounds, clip, .. } => {
Some((Kind::Text, bounds.intersect(*clip)))
}
Primitive::Layer {
primitives,
clip,
transform,
..
} => {
let mut inner: Option<Rect> = None;
for p in primitives {
if let Some((_, bounds)) = footprint(p) {
inner = Some(match inner {
Some(u) => u.union(bounds),
None => bounds,
});
}
}
let bounds = inner.unwrap_or(*clip);
let bounds = match transform {
Some(t) => {
let (x0, y0) = (bounds.x, bounds.y);
let (x1, y1) = (bounds.x + bounds.width, bounds.y + bounds.height);
let corners = [
t.affine.apply(frus_core::Point::new(x0, y0)),
t.affine.apply(frus_core::Point::new(x1, y0)),
t.affine.apply(frus_core::Point::new(x0, y1)),
t.affine.apply(frus_core::Point::new(x1, y1)),
];
let min_x = corners.iter().fold(f32::MAX, |m, p| m.min(p.x));
let min_y = corners.iter().fold(f32::MAX, |m, p| m.min(p.y));
let max_x = corners.iter().fold(f32::MIN, |m, p| m.max(p.x));
let max_y = corners.iter().fold(f32::MIN, |m, p| m.max(p.y));
Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
}
None => bounds,
};
Some((Kind::Layer, bounds.intersect(*clip)))
}
}
}
const BAND: f32 = 64.0;
const INDEX_FROM: usize = 128;
const MAX_BANDS: i64 = 16;
const MAX_TABLE: i64 = 1 << 14;
fn band_range(r: Rect) -> Option<(i32, i32)> {
let (a, b) = (r.y, r.y + r.height);
if !(a.is_finite() && b.is_finite()) {
return None;
}
let (y0, y1) = if a <= b { (a, b) } else { (b, a) };
let band = |v: f32| (v / BAND).floor().clamp(-1.0e9, 1.0e9) as i32;
Some((band(y0), band(y1)))
}
struct Bands {
first: i32,
table: Vec<Vec<u32>>,
wide: Vec<u32>,
window: (i64, i64),
}
impl Bands {
fn build(members: &[(usize, Kind, Rect)]) -> Self {
let mut tops: Vec<i32> = members
.iter()
.filter_map(|&(_, _, bounds)| band_range(bounds))
.filter(|&(b0, b1)| i64::from(b1) - i64::from(b0) < MAX_BANDS)
.map(|(b0, _)| b0)
.collect();
let centre = if tops.is_empty() {
0
} else {
let middle = tops.len() / 2;
*tops.select_nth_unstable(middle).1
};
let half = MAX_TABLE / 2;
let mut bands = Bands {
first: 0,
table: Vec::new(),
wide: Vec::new(),
window: (i64::from(centre) - half, i64::from(centre) + half - 1),
};
for (at, &(_, _, bounds)) in members.iter().enumerate() {
bands.insert(at as u32, bounds);
}
bands
}
fn insert(&mut self, at: u32, bounds: Rect) {
let Some((b0, b1)) = band_range(bounds) else {
self.wide.push(at);
return;
};
let (lo, hi) = (i64::from(b0), i64::from(b1));
if hi - lo >= MAX_BANDS || lo < self.window.0 || hi > self.window.1 {
self.wide.push(at);
return;
}
if self.table.is_empty() {
self.first = b0;
}
if b0 < self.first {
let grow = (self.first - b0) as usize;
self.table
.splice(0..0, std::iter::repeat_with(Vec::new).take(grow));
self.first = b0;
}
let need = (b1 - self.first + 1) as usize;
if need > self.table.len() {
self.table.resize_with(need, Vec::new);
}
for band in b0..=b1 {
self.table[(band - self.first) as usize].push(at);
}
}
fn level_with(&self, b0: i32, b1: i32) -> &[Vec<u32>] {
let lo = (i64::from(b0) - i64::from(self.first)).max(0);
let hi = (i64::from(b1) - i64::from(self.first)).min(self.table.len() as i64 - 1);
if lo > hi {
&[]
} else {
&self.table[lo as usize..=hi as usize]
}
}
}
struct Level {
members: Vec<(usize, Kind, Rect)>,
bounds: Rect,
bands: Option<Box<Bands>>,
}
impl Level {
fn new(bounds: Rect) -> Self {
Self {
members: Vec::new(),
bounds,
bands: None,
}
}
fn push(&mut self, index: usize, kind: Kind, bounds: Rect) {
self.bounds = if self.members.is_empty() {
bounds
} else {
self.bounds.union(bounds)
};
let at = self.members.len() as u32;
self.members.push((index, kind, bounds));
match &mut self.bands {
Some(bands) => bands.insert(at, bounds),
None if self.members.len() >= INDEX_FROM => {
self.bands = Some(Box::new(Bands::build(&self.members)));
}
None => {}
}
}
fn probe(&self, bounds: Rect, kind: Kind) -> (bool, bool) {
if let Some(bands) = &self.bands {
if let Some((b0, b1)) = band_range(bounds) {
if (i64::from(b1) - i64::from(b0) + 1) * 2 < bands.table.len() as i64 {
let mut hit = false;
let mut test = |at: u32| -> bool {
let (_, member_kind, member_bounds) = self.members[at as usize];
if overlaps(member_bounds, bounds) {
hit = true;
member_kind != kind
} else {
false
}
};
for &at in &bands.wide {
if test(at) {
return (true, true);
}
}
for band in bands.level_with(b0, b1) {
for &at in band {
if test(at) {
return (true, true);
}
}
}
return (hit, false);
}
}
}
let mut hit = false;
let mut foreign = false;
for &(_, member_kind, member_bounds) in &self.members {
if overlaps(member_bounds, bounds) {
hit = true;
foreign |= member_kind != kind;
}
}
(hit, foreign)
}
}
pub(crate) fn plan(scene: &Scene) -> Vec<Batch> {
plan_footprints(
scene
.primitives()
.iter()
.enumerate()
.filter_map(|(index, p)| footprint(p).map(|(kind, bounds)| (index, kind, bounds))),
)
}
fn plan_footprints(items: impl IntoIterator<Item = (usize, Kind, Rect)>) -> Vec<Batch> {
let mut levels: Vec<Level> = Vec::new();
for (index, kind, bounds) in items {
let mut level = 0;
for (l, existing) in levels.iter().enumerate().rev() {
if !overlaps(existing.bounds, bounds) {
continue;
}
let (hit, foreign) = existing.probe(bounds, kind);
if hit {
level = if foreign { l + 1 } else { l };
break;
}
}
if level == levels.len() {
levels.push(Level::new(bounds));
}
levels[level].push(index, kind, bounds);
}
let mut batches: Vec<Batch> = Vec::new();
for level in &levels {
let mut lanes: [Option<usize>; 4] = [None; 4];
for &(index, kind, bounds) in &level.members {
let lane = match kind {
Kind::Rect => Some(0),
Kind::Image => Some(1),
Kind::Path => Some(2),
Kind::Text => Some(3),
Kind::Layer => None,
};
match lane.and_then(|l| lanes[l]) {
Some(at) => {
let batch = &mut batches[at];
batch.members.push(index);
batch.bounds = batch.bounds.union(bounds);
}
None => {
if let Some(l) = lane {
lanes[l] = Some(batches.len());
}
batches.push(Batch {
kind,
members: vec![index],
bounds,
});
}
}
}
}
batches
}
#[cfg(test)]
mod tests {
use super::*;
use frus_core::{Color, Point};
const RED: Color = Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
fn scene_of(f: impl FnOnce(&mut Scene)) -> Scene {
let mut scene = Scene::new();
f(&mut scene);
scene
}
fn bar(x: f32, y: f32, w: f32, h: f32) -> Path {
Path::rect(Rect::new(x, y, w, h))
}
#[test]
fn things_that_do_not_touch_share_a_draw_call() {
let scene = scene_of(|s| {
for i in 0..4 {
let y = i as f32 * 100.0;
s.fill_rect(Rect::new(0.0, y, 80.0, 40.0), RED);
s.fill_path(&bar(200.0, y, 20.0, 20.0), RED);
}
});
let batches = plan(&scene);
assert_eq!(batches.len(), 2, "{batches:#?}");
assert_eq!(batches[0].kind, Kind::Rect);
assert_eq!(batches[0].members.len(), 4);
assert_eq!(batches[1].kind, Kind::Path);
assert_eq!(batches[1].members.len(), 4);
}
#[test]
fn a_rectangle_over_a_path_is_drawn_after_it() {
let scene = scene_of(|s| {
s.fill_rect(Rect::new(0.0, 0.0, 400.0, 600.0), RED); s.fill_path(&bar(0.0, 540.0, 400.0, 60.0), RED); s.fill_rect(Rect::new(20.0, 550.0, 90.0, 40.0), RED); });
let batches = plan(&scene);
assert_eq!(batches.len(), 3, "{batches:#?}");
assert_eq!(batches[0].members, vec![0]);
assert_eq!(batches[1].members, vec![1]);
assert_eq!(batches[2].members, vec![2], "the button lost its own call");
assert_eq!(batches[2].kind, Kind::Rect);
}
#[test]
fn the_same_rectangle_clear_of_the_path_rejoins_the_batch() {
let scene = scene_of(|s| {
s.fill_rect(Rect::new(0.0, 0.0, 400.0, 100.0), RED);
s.fill_path(&bar(0.0, 540.0, 400.0, 60.0), RED);
s.fill_rect(Rect::new(20.0, 200.0, 90.0, 40.0), RED);
});
let batches = plan(&scene);
assert_eq!(batches.len(), 2, "{batches:#?}");
assert_eq!(batches[0].members, vec![0, 2]);
assert_eq!(batches[1].members, vec![1]);
}
#[test]
fn the_plan_never_reorders_two_overlapping_primitives() {
let scene = scene_of(|s| {
s.fill_rect(Rect::new(0.0, 0.0, 100.0, 100.0), RED);
s.fill_path(&bar(50.0, 50.0, 100.0, 100.0), RED);
s.fill_rect(Rect::new(90.0, 90.0, 100.0, 100.0), RED);
s.fill_path(&bar(0.0, 0.0, 10.0, 10.0), RED);
});
let batches = plan(&scene);
let order: Vec<usize> = batches
.iter()
.flat_map(|b| b.members.iter().copied())
.collect();
let boxes: Vec<Rect> = scene
.primitives()
.iter()
.map(|p| footprint(p).expect("planned").1)
.collect();
for (i, &a) in order.iter().enumerate() {
for &b in &order[i + 1..] {
if overlaps(boxes[a], boxes[b]) {
assert!(a < b, "{a} was drawn before {b} but comes after it");
}
}
}
}
#[test]
fn a_stroke_claims_the_room_it_spills_into() {
let mut stroked = Scene::new();
stroked.stroke_path(
&Path::new()
.move_to(Point::new(100.0, 100.0))
.line_to(Point::new(200.0, 100.0)),
RED,
20.0,
);
stroked.fill_rect(Rect::new(120.0, 95.0, 10.0, 4.0), RED);
assert_eq!(plan(&stroked).len(), 2, "the stroke's width was ignored");
}
#[test]
fn a_panel_dropped_over_a_label_covers_it() {
let mut scene = Scene::new();
scene.set_bounds(Rect::new(20.0, 20.0, 120.0, 20.0));
scene.text(
Point::new(20.0, 20.0),
"Score",
&frus_core::ResolvedTextStyle::exact(14.0),
RED,
);
scene.set_bounds(Rect::new(10.0, 10.0, 200.0, 100.0));
scene.fill_rect(Rect::new(10.0, 10.0, 200.0, 100.0), RED);
let batches = plan(&scene);
assert_eq!(batches.len(), 2, "{batches:#?}");
assert_eq!(batches[0].kind, Kind::Text, "the label goes down first");
assert_eq!(batches[1].kind, Kind::Rect, "the panel goes over it");
}
#[test]
fn labels_batch_together_above_what_they_sit_on() {
let mut scene = Scene::new();
for i in 0..4 {
let y = i as f32 * 40.0;
scene.set_bounds(Rect::new(0.0, y, 200.0, 30.0));
scene.fill_rect(Rect::new(0.0, y, 200.0, 30.0), RED);
scene.text(
Point::new(8.0, y + 6.0),
"row",
&frus_core::ResolvedTextStyle::exact(14.0),
RED,
);
}
let batches = plan(&scene);
assert_eq!(batches.len(), 2, "{batches:#?}");
assert_eq!(batches[0].kind, Kind::Rect);
assert_eq!(batches[0].members.len(), 4);
assert_eq!(batches[1].kind, Kind::Text);
assert_eq!(batches[1].members.len(), 4, "four labels, one call");
}
#[test]
fn text_with_no_box_is_ordered_by_its_place_in_the_scene() {
let mut scene = Scene::new();
scene.fill_rect(Rect::new(0.0, 0.0, 100.0, 100.0), RED);
scene.text(
Point::new(10.0, 10.0),
"hello",
&frus_core::ResolvedTextStyle::exact(16.0),
RED,
);
scene.fill_rect(Rect::new(0.0, 0.0, 100.0, 100.0), RED);
let batches = plan(&scene);
assert_eq!(batches.len(), 3, "{batches:#?}");
assert_eq!(batches[0].members, vec![0]);
assert_eq!(batches[1].kind, Kind::Text);
assert_eq!(batches[2].members, vec![2]);
}
fn plan_linear(items: &[(usize, Kind, Rect)]) -> Vec<(Kind, Vec<usize>)> {
struct Plain {
members: Vec<(usize, Kind, Rect)>,
bounds: Rect,
}
let mut levels: Vec<Plain> = Vec::new();
for &(index, kind, bounds) in items {
let mut level = 0;
for (l, existing) in levels.iter().enumerate().rev() {
if !overlaps(existing.bounds, bounds) {
continue;
}
let mut hit = false;
let mut foreign = false;
for &(_, member_kind, member_bounds) in &existing.members {
if overlaps(member_bounds, bounds) {
hit = true;
foreign |= member_kind != kind;
}
}
if hit {
level = if foreign { l + 1 } else { l };
break;
}
}
if level == levels.len() {
levels.push(Plain {
members: Vec::new(),
bounds,
});
}
let target = &mut levels[level];
target.bounds = if target.members.is_empty() {
bounds
} else {
target.bounds.union(bounds)
};
target.members.push((index, kind, bounds));
}
let mut batches: Vec<(Kind, Vec<usize>)> = Vec::new();
for level in &levels {
let first = batches.len();
for &(index, kind, _) in &level.members {
let existing = if kind == Kind::Layer {
None
} else {
batches[first..].iter_mut().find(|b| b.0 == kind)
};
match existing {
Some(batch) => batch.1.push(index),
None => batches.push((kind, vec![index])),
}
}
}
batches
}
fn shape(batches: &[Batch]) -> Vec<(Kind, Vec<usize>)> {
batches
.iter()
.map(|b| (b.kind, b.members.clone()))
.collect()
}
fn footprints(scene: &Scene) -> Vec<(usize, Kind, Rect)> {
scene
.primitives()
.iter()
.enumerate()
.filter_map(|(i, p)| footprint(p).map(|(k, b)| (i, k, b)))
.collect()
}
struct Dice(u64);
impl Dice {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
fn unit(&mut self) -> f32 {
(self.next() >> 40) as f32 / (1u64 << 24) as f32
}
}
fn any_rect(d: &mut Dice) -> Rect {
let (x, y) = (d.unit() * 400.0, d.unit() * 4000.0);
match d.below(24) {
0 => Rect::UNBOUNDED,
1 => Rect::new(x, y, 0.0, 0.0),
2 => Rect::new(x, y, -d.unit() * 80.0, d.unit() * 80.0),
3 => Rect::new(f32::NAN, y, 20.0, 20.0),
4 => Rect::new(x, y, f32::INFINITY, 20.0),
5 => Rect::new(1.0e8 + x, -1.0e8 + y, 40.0, 40.0),
6 | 7 => {
let (cx, cy) = (d.below(8) as f32, d.below(60) as f32);
Rect::new(cx * 64.0, cy * 64.0, 64.0, 64.0)
}
8..=10 => Rect::new(0.0, 0.0, 400.0 + x, 800.0 + y),
11..=15 => Rect::new(0.0, d.below(70) as f32 * 56.0, 400.0, 56.0),
_ => Rect::new(x, y, d.unit() * 48.0, d.unit() * 48.0),
}
}
fn sparse_rect(d: &mut Dice) -> Rect {
let (x, y) = (d.unit() * 400.0, d.unit() * 6000.0);
match d.below(10) {
0 => Rect::new(x, y, d.unit() * 300.0, 0.0),
1 => Rect::new(x, y, 30.0, -d.unit() * 60.0),
2 | 3 => {
let (cx, cy) = (d.below(8) as f32, d.below(90) as f32);
Rect::new(cx * 64.0, cy * 64.0, 64.0, 64.0)
}
4 | 5 => Rect::new(d.unit() * 200.0, y, 120.0 + d.unit() * 200.0, 56.0),
_ => Rect::new(x, y, 4.0 + d.unit() * 40.0, 4.0 + d.unit() * 40.0),
}
}
fn any_kind(d: &mut Dice) -> Kind {
[Kind::Rect, Kind::Image, Kind::Path, Kind::Text, Kind::Layer][d.below(5) as usize]
}
#[test]
fn the_index_changes_no_plan() {
for seed in 1..=300u64 {
let mut d = Dice(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1);
let n = d.below(900) as usize;
let sparse = seed % 2 == 0;
let items: Vec<(usize, Kind, Rect)> = (0..n)
.map(|i| {
let kind = any_kind(&mut d);
let rect = if sparse {
sparse_rect(&mut d)
} else {
any_rect(&mut d)
};
(i, kind, rect)
})
.collect();
assert_eq!(
shape(&plan_footprints(items.iter().copied())),
plan_linear(&items),
"seed {seed}, {n} footprints, sparse: {sparse}"
);
}
}
#[test]
fn the_index_changes_no_plan_on_a_real_scene() {
let picture = frus_core::ImageData::from_rgba(1, 1, vec![255; 4]).into_handle();
let mut scene = Scene::new();
scene.fill_rect(Rect::new(0.0, 0.0, 400.0, 800.0), RED);
for i in 0..400 {
let y = i as f32 * 48.0;
scene.set_clip(Rect::new(0.0, 0.0, 400.0, 800.0 + (i % 7) as f32 * 300.0));
scene.set_bounds(Rect::new(0.0, y, 400.0, 48.0));
scene.fill_rect(Rect::new(0.0, y, 400.0, 48.0), RED);
scene.fill_path(&bar(12.0, y + 12.0, 24.0, 24.0), RED);
scene.text(
Point::new(48.0, y + 14.0),
"row",
&frus_core::ResolvedTextStyle::exact(14.0),
RED,
);
scene.draw_image(
&picture,
Rect::new(360.0, y + 8.0, 32.0, 32.0),
Rect::new(0.0, 0.0, 1.0, 1.0),
RED,
);
if i % 50 == 0 {
scene.layer(0.5, |s| s.fill_rect(Rect::new(20.0, y, 200.0, 96.0), RED));
}
}
scene.text(
Point::new(10.0, 10.0),
"no box",
&frus_core::ResolvedTextStyle::exact(16.0),
RED,
);
let planned = plan(&scene);
assert_eq!(shape(&planned), plan_linear(&footprints(&scene)));
assert!(
planned.len() > 2,
"a plan with some structure to it: {}",
planned.len()
);
}
#[test]
fn a_busy_level_is_indexed_and_answers_as_a_scan_would() {
let mut d = Dice(0x5EED);
let mut level = Level::new(Rect::new(0.0, 0.0, 0.0, 0.0));
for i in 0..600 {
level.push(i, any_kind(&mut d), sparse_rect(&mut d));
}
level.push(600, Kind::Rect, Rect::new(10.0, 1.0e8, 20.0, 20.0));
level.push(601, Kind::Path, Rect::new(10.0, f32::NAN, 20.0, 20.0));
level.push(602, Kind::Text, Rect::new(0.0, 0.0, 400.0, 64.0 * 40.0));
let bands = level.bands.as_ref().expect("a level this busy is indexed");
assert_eq!(bands.wide.len(), 3, "exactly the three that belong there");
let used = bands.table.iter().filter(|b| !b.is_empty()).count();
assert!(used > 50, "and the rest into bands: {used}");
let mut decisive = 0;
for _ in 0..4000 {
let (query, kind) = (sparse_rect(&mut d), any_kind(&mut d));
let mut hits = 0;
let mut foreign = false;
for &(_, k, b) in &level.members {
if overlaps(b, query) {
hits += 1;
foreign |= k != kind;
}
}
decisive += usize::from(hits == 1);
assert_eq!(
level.probe(query, kind),
(hits > 0, foreign),
"{query:?} as {kind:?}"
);
}
assert!(
decisive > 400,
"a test whose answers hang on one member this rarely cannot fail: {decisive}"
);
}
#[test]
fn a_stray_member_does_not_become_the_origin() {
let mut level = Level::new(Rect::new(0.0, 0.0, 0.0, 0.0));
level.push(0, Kind::Rect, Rect::new(0.0, 1.0e8, 10.0, 10.0));
for i in 1..500 {
level.push(i, Kind::Rect, Rect::new(0.0, i as f32 * 56.0, 400.0, 56.0));
}
let bands = level.bands.as_ref().expect("indexed");
assert_eq!(
bands.wide.len(),
1,
"only the stray goes on the wide list, not the four hundred and ninety-nine rows"
);
}
}